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.

Files changed (198) hide show
  1. amulet/__init__.py +27 -27
  2. amulet/__pyinstaller/__init__.py +2 -2
  3. amulet/__pyinstaller/hook-amulet.py +4 -4
  4. amulet/_version.py +21 -21
  5. amulet/api/__init__.py +2 -2
  6. amulet/api/abstract_base_entity.py +128 -128
  7. amulet/api/block.py +630 -630
  8. amulet/api/block_entity.py +71 -71
  9. amulet/api/cache.py +107 -107
  10. amulet/api/chunk/__init__.py +6 -6
  11. amulet/api/chunk/biomes.py +207 -207
  12. amulet/api/chunk/block_entity_dict.py +175 -175
  13. amulet/api/chunk/blocks.py +46 -46
  14. amulet/api/chunk/chunk.py +389 -389
  15. amulet/api/chunk/entity_list.py +75 -75
  16. amulet/api/chunk/status.py +167 -167
  17. amulet/api/data_types/__init__.py +4 -4
  18. amulet/api/data_types/generic_types.py +4 -4
  19. amulet/api/data_types/operation_types.py +16 -16
  20. amulet/api/data_types/world_types.py +49 -49
  21. amulet/api/data_types/wrapper_types.py +71 -71
  22. amulet/api/entity.py +74 -74
  23. amulet/api/errors.py +119 -119
  24. amulet/api/history/__init__.py +36 -36
  25. amulet/api/history/base/__init__.py +3 -3
  26. amulet/api/history/base/base_history.py +26 -26
  27. amulet/api/history/base/history_manager.py +63 -63
  28. amulet/api/history/base/revision_manager.py +73 -73
  29. amulet/api/history/changeable.py +15 -15
  30. amulet/api/history/data_types.py +7 -7
  31. amulet/api/history/history_manager/__init__.py +3 -3
  32. amulet/api/history/history_manager/container.py +102 -102
  33. amulet/api/history/history_manager/database.py +279 -279
  34. amulet/api/history/history_manager/meta.py +93 -93
  35. amulet/api/history/history_manager/object.py +116 -116
  36. amulet/api/history/revision_manager/__init__.py +2 -2
  37. amulet/api/history/revision_manager/disk.py +33 -33
  38. amulet/api/history/revision_manager/ram.py +12 -12
  39. amulet/api/item.py +75 -75
  40. amulet/api/level/__init__.py +4 -4
  41. amulet/api/level/base_level/__init__.py +1 -1
  42. amulet/api/level/base_level/base_level.py +1035 -1026
  43. amulet/api/level/base_level/chunk_manager.py +227 -227
  44. amulet/api/level/base_level/clone.py +389 -389
  45. amulet/api/level/base_level/player_manager.py +101 -101
  46. amulet/api/level/immutable_structure/__init__.py +1 -1
  47. amulet/api/level/immutable_structure/immutable_structure.py +94 -94
  48. amulet/api/level/immutable_structure/void_format_wrapper.py +117 -117
  49. amulet/api/level/structure.py +22 -22
  50. amulet/api/level/world.py +19 -19
  51. amulet/api/partial_3d_array/__init__.py +2 -2
  52. amulet/api/partial_3d_array/base_partial_3d_array.py +263 -263
  53. amulet/api/partial_3d_array/bounded_partial_3d_array.py +528 -528
  54. amulet/api/partial_3d_array/data_types.py +15 -15
  55. amulet/api/partial_3d_array/unbounded_partial_3d_array.py +229 -229
  56. amulet/api/partial_3d_array/util.py +152 -152
  57. amulet/api/player.py +65 -65
  58. amulet/api/registry/__init__.py +2 -2
  59. amulet/api/registry/base_registry.py +34 -34
  60. amulet/api/registry/biome_manager.py +153 -153
  61. amulet/api/registry/block_manager.py +156 -156
  62. amulet/api/selection/__init__.py +2 -2
  63. amulet/api/selection/abstract_selection.py +315 -315
  64. amulet/api/selection/box.py +805 -805
  65. amulet/api/selection/group.py +488 -488
  66. amulet/api/structure.py +37 -37
  67. amulet/api/wrapper/__init__.py +8 -8
  68. amulet/api/wrapper/chunk/interface.py +441 -441
  69. amulet/api/wrapper/chunk/translator.py +567 -567
  70. amulet/api/wrapper/format_wrapper.py +772 -772
  71. amulet/api/wrapper/structure_format_wrapper.py +116 -116
  72. amulet/api/wrapper/world_format_wrapper.py +63 -63
  73. amulet/level/__init__.py +1 -1
  74. amulet/level/formats/anvil_forge_world.py +40 -40
  75. amulet/level/formats/anvil_world/__init__.py +3 -3
  76. amulet/level/formats/anvil_world/_sector_manager.py +291 -384
  77. amulet/level/formats/anvil_world/data_pack/__init__.py +2 -2
  78. amulet/level/formats/anvil_world/data_pack/data_pack.py +224 -224
  79. amulet/level/formats/anvil_world/data_pack/data_pack_manager.py +77 -77
  80. amulet/level/formats/anvil_world/dimension.py +177 -177
  81. amulet/level/formats/anvil_world/format.py +769 -769
  82. amulet/level/formats/anvil_world/region.py +384 -384
  83. amulet/level/formats/construction/__init__.py +3 -3
  84. amulet/level/formats/construction/format_wrapper.py +515 -515
  85. amulet/level/formats/construction/interface.py +134 -134
  86. amulet/level/formats/construction/section.py +60 -60
  87. amulet/level/formats/construction/util.py +165 -165
  88. amulet/level/formats/leveldb_world/__init__.py +3 -3
  89. amulet/level/formats/leveldb_world/chunk.py +33 -33
  90. amulet/level/formats/leveldb_world/dimension.py +385 -419
  91. amulet/level/formats/leveldb_world/format.py +659 -641
  92. amulet/level/formats/leveldb_world/interface/chunk/__init__.py +36 -36
  93. amulet/level/formats/leveldb_world/interface/chunk/base_leveldb_interface.py +836 -836
  94. amulet/level/formats/leveldb_world/interface/chunk/generate_interface.py +31 -31
  95. amulet/level/formats/leveldb_world/interface/chunk/leveldb_0.py +30 -30
  96. amulet/level/formats/leveldb_world/interface/chunk/leveldb_1.py +12 -12
  97. amulet/level/formats/leveldb_world/interface/chunk/leveldb_10.py +12 -12
  98. amulet/level/formats/leveldb_world/interface/chunk/leveldb_11.py +12 -12
  99. amulet/level/formats/leveldb_world/interface/chunk/leveldb_12.py +12 -12
  100. amulet/level/formats/leveldb_world/interface/chunk/leveldb_13.py +12 -12
  101. amulet/level/formats/leveldb_world/interface/chunk/leveldb_14.py +12 -12
  102. amulet/level/formats/leveldb_world/interface/chunk/leveldb_15.py +12 -12
  103. amulet/level/formats/leveldb_world/interface/chunk/leveldb_16.py +12 -12
  104. amulet/level/formats/leveldb_world/interface/chunk/leveldb_17.py +12 -12
  105. amulet/level/formats/leveldb_world/interface/chunk/leveldb_18.py +12 -12
  106. amulet/level/formats/leveldb_world/interface/chunk/leveldb_19.py +12 -12
  107. amulet/level/formats/leveldb_world/interface/chunk/leveldb_2.py +12 -12
  108. amulet/level/formats/leveldb_world/interface/chunk/leveldb_20.py +12 -12
  109. amulet/level/formats/leveldb_world/interface/chunk/leveldb_21.py +12 -12
  110. amulet/level/formats/leveldb_world/interface/chunk/leveldb_22.py +12 -12
  111. amulet/level/formats/leveldb_world/interface/chunk/leveldb_23.py +10 -10
  112. amulet/level/formats/leveldb_world/interface/chunk/leveldb_24.py +10 -10
  113. amulet/level/formats/leveldb_world/interface/chunk/leveldb_25.py +24 -24
  114. amulet/level/formats/leveldb_world/interface/chunk/leveldb_26.py +10 -10
  115. amulet/level/formats/leveldb_world/interface/chunk/leveldb_27.py +10 -10
  116. amulet/level/formats/leveldb_world/interface/chunk/leveldb_28.py +10 -10
  117. amulet/level/formats/leveldb_world/interface/chunk/leveldb_29.py +33 -33
  118. amulet/level/formats/leveldb_world/interface/chunk/leveldb_3.py +57 -57
  119. amulet/level/formats/leveldb_world/interface/chunk/leveldb_30.py +10 -10
  120. amulet/level/formats/leveldb_world/interface/chunk/leveldb_31.py +10 -10
  121. amulet/level/formats/leveldb_world/interface/chunk/leveldb_32.py +10 -10
  122. amulet/level/formats/leveldb_world/interface/chunk/leveldb_33.py +10 -10
  123. amulet/level/formats/leveldb_world/interface/chunk/leveldb_34.py +10 -10
  124. amulet/level/formats/leveldb_world/interface/chunk/leveldb_35.py +10 -10
  125. amulet/level/formats/leveldb_world/interface/chunk/leveldb_36.py +10 -10
  126. amulet/level/formats/leveldb_world/interface/chunk/leveldb_37.py +10 -10
  127. amulet/level/formats/leveldb_world/interface/chunk/leveldb_38.py +10 -10
  128. amulet/level/formats/leveldb_world/interface/chunk/leveldb_39.py +12 -12
  129. amulet/level/formats/leveldb_world/interface/chunk/leveldb_4.py +12 -12
  130. amulet/level/formats/leveldb_world/interface/chunk/leveldb_40.py +16 -16
  131. amulet/level/formats/leveldb_world/interface/chunk/leveldb_5.py +12 -12
  132. amulet/level/formats/leveldb_world/interface/chunk/leveldb_6.py +12 -12
  133. amulet/level/formats/leveldb_world/interface/chunk/leveldb_7.py +12 -12
  134. amulet/level/formats/leveldb_world/interface/chunk/leveldb_8.py +180 -180
  135. amulet/level/formats/leveldb_world/interface/chunk/leveldb_9.py +18 -18
  136. amulet/level/formats/leveldb_world/interface/chunk/leveldb_chunk_versions.py +79 -79
  137. amulet/level/formats/mcstructure/__init__.py +3 -3
  138. amulet/level/formats/mcstructure/chunk.py +50 -50
  139. amulet/level/formats/mcstructure/format_wrapper.py +408 -408
  140. amulet/level/formats/mcstructure/interface.py +175 -175
  141. amulet/level/formats/schematic/__init__.py +3 -3
  142. amulet/level/formats/schematic/chunk.py +55 -55
  143. amulet/level/formats/schematic/data_types.py +4 -4
  144. amulet/level/formats/schematic/format_wrapper.py +373 -373
  145. amulet/level/formats/schematic/interface.py +142 -142
  146. amulet/level/formats/sponge_schem/__init__.py +4 -4
  147. amulet/level/formats/sponge_schem/chunk.py +62 -62
  148. amulet/level/formats/sponge_schem/format_wrapper.py +463 -463
  149. amulet/level/formats/sponge_schem/interface.py +118 -118
  150. amulet/level/formats/sponge_schem/varint/__init__.py +1 -1
  151. amulet/level/formats/sponge_schem/varint/varint.py +87 -87
  152. amulet/level/interfaces/chunk/anvil/anvil_0.py +72 -72
  153. amulet/level/interfaces/chunk/anvil/anvil_1444.py +336 -336
  154. amulet/level/interfaces/chunk/anvil/anvil_1466.py +94 -94
  155. amulet/level/interfaces/chunk/anvil/anvil_1467.py +37 -37
  156. amulet/level/interfaces/chunk/anvil/anvil_1484.py +20 -20
  157. amulet/level/interfaces/chunk/anvil/anvil_1503.py +20 -20
  158. amulet/level/interfaces/chunk/anvil/anvil_1519.py +34 -34
  159. amulet/level/interfaces/chunk/anvil/anvil_1901.py +20 -20
  160. amulet/level/interfaces/chunk/anvil/anvil_1908.py +20 -20
  161. amulet/level/interfaces/chunk/anvil/anvil_1912.py +21 -21
  162. amulet/level/interfaces/chunk/anvil/anvil_1934.py +20 -20
  163. amulet/level/interfaces/chunk/anvil/anvil_2203.py +69 -69
  164. amulet/level/interfaces/chunk/anvil/anvil_2529.py +19 -19
  165. amulet/level/interfaces/chunk/anvil/anvil_2681.py +76 -76
  166. amulet/level/interfaces/chunk/anvil/anvil_2709.py +19 -19
  167. amulet/level/interfaces/chunk/anvil/anvil_2844.py +267 -267
  168. amulet/level/interfaces/chunk/anvil/anvil_3463.py +19 -19
  169. amulet/level/interfaces/chunk/anvil/anvil_na.py +607 -607
  170. amulet/level/interfaces/chunk/anvil/base_anvil_interface.py +326 -326
  171. amulet/level/load.py +59 -59
  172. amulet/level/loader.py +95 -95
  173. amulet/level/translators/chunk/bedrock/__init__.py +267 -267
  174. amulet/level/translators/chunk/bedrock/bedrock_nbt_blockstate_translator.py +46 -46
  175. amulet/level/translators/chunk/bedrock/bedrock_numerical_translator.py +39 -39
  176. amulet/level/translators/chunk/bedrock/bedrock_psudo_numerical_translator.py +37 -37
  177. amulet/level/translators/chunk/java/java_1_18_translator.py +40 -40
  178. amulet/level/translators/chunk/java/java_blockstate_translator.py +94 -94
  179. amulet/level/translators/chunk/java/java_numerical_translator.py +62 -62
  180. amulet/libs/leveldb/__init__.py +7 -7
  181. amulet/operations/__init__.py +5 -5
  182. amulet/operations/clone.py +18 -18
  183. amulet/operations/delete_chunk.py +32 -32
  184. amulet/operations/fill.py +30 -30
  185. amulet/operations/paste.py +65 -65
  186. amulet/operations/replace.py +58 -58
  187. amulet/utils/__init__.py +14 -14
  188. amulet/utils/format_utils.py +41 -41
  189. amulet/utils/generator.py +15 -15
  190. amulet/utils/matrix.py +243 -243
  191. amulet/utils/numpy_helpers.py +46 -46
  192. amulet/utils/world_utils.py +349 -349
  193. {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/METADATA +97 -97
  194. amulet_core-1.9.20.dist-info/RECORD +208 -0
  195. amulet_core-1.9.19.dist-info/RECORD +0 -208
  196. {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/WHEEL +0 -0
  197. {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/entry_points.txt +0 -0
  198. {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/top_level.txt +0 -0
@@ -1,1026 +1,1035 @@
1
- from __future__ import annotations
2
-
3
- import time
4
- from typing import Union, Generator, Optional, Tuple, Callable, Set, Iterable
5
- import traceback
6
- import numpy
7
- import itertools
8
- import warnings
9
- import logging
10
- import copy
11
- import os
12
-
13
- from amulet.api.block import Block, UniversalAirBlock
14
- from amulet.api.block_entity import BlockEntity
15
- from amulet.api.entity import Entity
16
- from amulet.api.registry import BlockManager
17
- from amulet.api.registry.biome_manager import BiomeManager
18
- from amulet.api.errors import ChunkDoesNotExist, ChunkLoadError, DimensionDoesNotExist
19
- from amulet.api.chunk import Chunk, EntityList
20
- from amulet.api.selection import SelectionGroup, SelectionBox
21
- from amulet.api.data_types import (
22
- Dimension,
23
- VersionIdentifierType,
24
- BlockCoordinates,
25
- FloatTriplet,
26
- ChunkCoordinates,
27
- )
28
- from amulet.api.chunk.status import StatusFormats
29
- from amulet.api.cache import TempDir
30
- from leveldb import LevelDB
31
- from amulet.utils.generator import generator_unpacker
32
- from amulet.utils.world_utils import block_coords_to_chunk_coords
33
- from .chunk_manager import ChunkManager
34
- from amulet.api.history.history_manager import MetaHistoryManager
35
- from .clone import clone
36
- from amulet.api import wrapper as api_wrapper, level as api_level
37
- import PyMCTranslate
38
- from amulet.api.player import Player
39
- from .player_manager import PlayerManager
40
-
41
- log = logging.getLogger(__name__)
42
-
43
-
44
- class BaseLevel:
45
- """
46
- BaseLevel is a base class for all world-like data.
47
-
48
- It exposes chunk data and other data using a history system to track and enable undoing changes.
49
- """
50
-
51
- def __init__(self, path: str, format_wrapper: api_wrapper.FormatWrapper):
52
- """
53
- Construct a :class:`BaseLevel` object from the given data.
54
-
55
- This should not be used directly. You should instead use :func:`amulet.load_level`.
56
-
57
- :param path: The path to the data being loaded. May be a file or directory. If blank there is no data on disk associated with this.
58
- :param format_wrapper: The :class:`FormatWrapper` instance that the level will wrap around.
59
- """
60
- self._path = path
61
-
62
- self._level_wrapper = format_wrapper
63
- self.level_wrapper.open()
64
-
65
- self._block_palette = BlockManager()
66
- self._block_palette.get_add_block(
67
- UniversalAirBlock
68
- ) # ensure that index 0 is always air
69
-
70
- self._biome_palette = BiomeManager()
71
- self._biome_palette.get_add_biome("universal_minecraft:plains")
72
-
73
- self._history_manager = MetaHistoryManager()
74
-
75
- self._temp_dir = TempDir()
76
- self._history_db = LevelDB(
77
- os.path.join(self._temp_dir, "history_db"), create_if_missing=True
78
- )
79
- self._chunks: ChunkManager = ChunkManager(self, self._history_db)
80
- self._players = PlayerManager(self)
81
-
82
- self.history_manager.register(self._chunks, True)
83
- self.history_manager.register(self._players, True)
84
-
85
- def __del__(self):
86
- self.close()
87
-
88
- @property
89
- def level_wrapper(self) -> api_wrapper.FormatWrapper:
90
- """A class to access data directly from the level."""
91
- return self._level_wrapper
92
-
93
- @property
94
- def sub_chunk_size(self) -> int:
95
- """The normal dimensions of the chunk."""
96
- return self.level_wrapper.sub_chunk_size
97
-
98
- @property
99
- def level_path(self) -> str:
100
- """
101
- The system path where the level is located.
102
-
103
- This may be a directory, file or an empty string depending on the level that is loaded.
104
- """
105
- return self._path
106
-
107
- @property
108
- def translation_manager(self) -> PyMCTranslate.TranslationManager:
109
- """An instance of the translation class for use with this level."""
110
- return self.level_wrapper.translation_manager
111
-
112
- @property
113
- def block_palette(self) -> BlockManager:
114
- """The manager for the universal blocks in this level. New blocks must be registered here before adding to the level."""
115
- return self._block_palette
116
-
117
- @property
118
- def biome_palette(self) -> BiomeManager:
119
- """The manager for the universal blocks in this level. New biomes must be registered here before adding to the level."""
120
- return self._biome_palette
121
-
122
- @property
123
- def selection_bounds(self) -> SelectionGroup:
124
- """The selection(s) that all chunk data must fit within. Usually +/-30M for worlds. The selection for structures."""
125
- warnings.warn(
126
- "BaseLevel.selection_bounds is depreciated and will be removed in the future. Please use BaseLevel.bounds(dimension) instead",
127
- DeprecationWarning,
128
- )
129
- return self.bounds(self.dimensions[0])
130
-
131
- def bounds(self, dimension: Dimension) -> SelectionGroup:
132
- """
133
- The selection(s) that all chunk data must fit within.
134
- This specifies the volume that can be built in.
135
- Worlds will have a single cuboid volume.
136
- Structures may have one or more cuboid volumes.
137
-
138
- :param dimension: The dimension to get the bounds of.
139
- :return: The build volume for the dimension.
140
- """
141
- return self.level_wrapper.bounds(dimension)
142
-
143
- @property
144
- def dimensions(self) -> Tuple[Dimension, ...]:
145
- """The dimensions strings that are valid for this level."""
146
- return tuple(self.level_wrapper.dimensions)
147
-
148
- def get_block(self, x: int, y: int, z: int, dimension: Dimension) -> Block:
149
- """
150
- Gets the universal Block object at the specified coordinates.
151
-
152
- To get the block in a given format use :meth:`get_version_block`
153
-
154
- :param x: The X coordinate of the desired block
155
- :param y: The Y coordinate of the desired block
156
- :param z: The Z coordinate of the desired block
157
- :param dimension: The dimension of the desired block
158
- :return: The universal Block object representation of the block at that location
159
- :raises:
160
- :class:`~amulet.api.errors.ChunkDoesNotExist`: If the chunk does not exist (was deleted or never created)
161
-
162
- :class:`~amulet.api.errors.ChunkLoadError`: If the chunk was not able to be loaded. Eg. If the chunk is corrupt or some error occurred when loading.
163
- """
164
- cx, cz = block_coords_to_chunk_coords(x, z, sub_chunk_size=self.sub_chunk_size)
165
- offset_x, offset_z = x - 16 * cx, z - 16 * cz
166
-
167
- return self.get_chunk(cx, cz, dimension).get_block(offset_x, y, offset_z)
168
-
169
- def _chunk_box(self, cx: int, cz: int, sub_chunk_size: Optional[int] = None):
170
- """Get a SelectionBox containing the whole of a given chunk"""
171
- if sub_chunk_size is None:
172
- sub_chunk_size = self.sub_chunk_size
173
- return SelectionBox.create_chunk_box(cx, cz, sub_chunk_size)
174
-
175
- def _sanitise_selection(
176
- self, selection: Union[SelectionGroup, SelectionBox, None], dimension: Dimension
177
- ) -> SelectionGroup:
178
- if isinstance(selection, SelectionBox):
179
- return SelectionGroup(selection)
180
- elif isinstance(selection, SelectionGroup):
181
- return selection
182
- elif selection is None:
183
- return self.bounds(dimension)
184
- else:
185
- raise ValueError(
186
- f"Expected SelectionBox, SelectionGroup or None. Got {selection}"
187
- )
188
-
189
- def get_coord_box(
190
- self,
191
- dimension: Dimension,
192
- selection: Union[SelectionGroup, SelectionBox, None] = None,
193
- yield_missing_chunks=False,
194
- ) -> Generator[Tuple[ChunkCoordinates, SelectionBox], None, None]:
195
- """
196
- Given a selection will yield chunk coordinates and :class:`SelectionBox` instances into that chunk
197
-
198
- If not given a selection will use the bounds of the object.
199
-
200
- :param dimension: The dimension to take effect in.
201
- :param selection: SelectionGroup or SelectionBox into the level. If None will use :meth:`bounds` for the dimension.
202
- :param yield_missing_chunks: If a chunk does not exist an empty one will be created (defaults to false). Use this with care.
203
- """
204
- selection = self._sanitise_selection(selection, dimension)
205
- if yield_missing_chunks or selection.footprint_area < 1_000_000:
206
- if yield_missing_chunks:
207
- for coord, box in selection.chunk_boxes(self.sub_chunk_size):
208
- yield coord, box
209
- else:
210
- for (cx, cz), box in selection.chunk_boxes(self.sub_chunk_size):
211
- if self.has_chunk(cx, cz, dimension):
212
- yield (cx, cz), box
213
-
214
- else:
215
- # if the selection gets very large iterating over the whole selection and accessing chunks can get slow
216
- # instead we are going to iterate over the chunks and get the intersection of the selection
217
- for cx, cz in self.all_chunk_coords(dimension):
218
- box = SelectionGroup(
219
- SelectionBox.create_chunk_box(cx, cz, self.sub_chunk_size)
220
- )
221
-
222
- if selection.intersects(box):
223
- chunk_selection = selection.intersection(box)
224
- for sub_box in chunk_selection.selection_boxes:
225
- yield (cx, cz), sub_box
226
-
227
- def get_chunk_boxes(
228
- self,
229
- dimension: Dimension,
230
- selection: Union[SelectionGroup, SelectionBox, None] = None,
231
- create_missing_chunks=False,
232
- ) -> Generator[Tuple[Chunk, SelectionBox], None, None]:
233
- """
234
- Given a selection will yield :class:`Chunk` and :class:`SelectionBox` instances into that chunk
235
-
236
- If not given a selection will use the bounds of the object.
237
-
238
- :param dimension: The dimension to take effect in.
239
- :param selection: SelectionGroup or SelectionBox into the level. If None will use :meth:`bounds` for the dimension.
240
- :param create_missing_chunks: If a chunk does not exist an empty one will be created (defaults to false). Use this with care.
241
- """
242
- for (cx, cz), box in self.get_coord_box(
243
- dimension, selection, create_missing_chunks
244
- ):
245
- try:
246
- chunk = self.get_chunk(cx, cz, dimension)
247
- except ChunkDoesNotExist:
248
- if create_missing_chunks:
249
- yield self.create_chunk(cx, cz, dimension), box
250
- except ChunkLoadError:
251
- log.error(f"Error loading chunk\n{traceback.format_exc()}")
252
- else:
253
- yield chunk, box
254
-
255
- def get_chunk_slice_box(
256
- self,
257
- dimension: Dimension,
258
- selection: Union[SelectionGroup, SelectionBox] = None,
259
- create_missing_chunks=False,
260
- ) -> Generator[Tuple[Chunk, Tuple[slice, slice, slice], SelectionBox], None, None]:
261
- """
262
- Given a selection will yield :class:`Chunk`, slices, :class:`SelectionBox` for the contents of the selection.
263
-
264
- :param dimension: The dimension to take effect in.
265
- :param selection: SelectionGroup or SelectionBox into the level. If None will use :meth:`bounds` for the dimension.
266
- :param create_missing_chunks: If a chunk does not exist an empty one will be created (defaults to false)
267
-
268
- >>> for chunk, slices, box in level.get_chunk_slice_box(selection):
269
- >>> chunk.blocks[slice] = ...
270
- """
271
- for chunk, box in self.get_chunk_boxes(
272
- dimension, selection, create_missing_chunks
273
- ):
274
- slices = box.chunk_slice(chunk.cx, chunk.cz, self.sub_chunk_size)
275
- yield chunk, slices, box
276
-
277
- def get_moved_coord_slice_box(
278
- self,
279
- dimension: Dimension,
280
- destination_origin: BlockCoordinates,
281
- selection: Optional[Union[SelectionGroup, SelectionBox]] = None,
282
- destination_sub_chunk_shape: Optional[int] = None,
283
- yield_missing_chunks: bool = False,
284
- ) -> Generator[
285
- Tuple[
286
- ChunkCoordinates,
287
- Tuple[slice, slice, slice],
288
- SelectionBox,
289
- ChunkCoordinates,
290
- Tuple[slice, slice, slice],
291
- SelectionBox,
292
- ],
293
- None,
294
- None,
295
- ]:
296
- """
297
- Iterate over a selection and return slices into the source object and destination object
298
- given the origin of the destination. When copying a selection to a new area the slices will
299
- only be equal if the offset is a multiple of the chunk size. This will rarely be the case
300
- so the slices need to be split up into parts that intersect a chunk in the source and destination.
301
-
302
- :param dimension: The dimension to iterate over.
303
- :param destination_origin: The location where the minimum point of the selection will end up
304
- :param selection: An optional selection. The overlap of this and the dimensions bounds will be used
305
- :param destination_sub_chunk_shape: the chunk shape of the destination object (defaults to self.sub_chunk_size)
306
- :param yield_missing_chunks: Generate empty chunks if the chunk does not exist.
307
- :return:
308
- """
309
- if destination_sub_chunk_shape is None:
310
- destination_sub_chunk_shape = self.sub_chunk_size
311
-
312
- if selection is None:
313
- selection = self.bounds(dimension)
314
- else:
315
- selection = self.bounds(dimension).intersection(selection)
316
- # the offset from self.selection to the destination location
317
- offset = numpy.subtract(
318
- destination_origin, self.bounds(dimension).min, dtype=int
319
- )
320
- for (src_cx, src_cz), box in self.get_coord_box(
321
- dimension, selection, yield_missing_chunks=yield_missing_chunks
322
- ):
323
- dst_full_box = SelectionBox(offset + box.min, offset + box.max)
324
-
325
- first_chunk = block_coords_to_chunk_coords(
326
- dst_full_box.min_x,
327
- dst_full_box.min_z,
328
- sub_chunk_size=destination_sub_chunk_shape,
329
- )
330
- last_chunk = block_coords_to_chunk_coords(
331
- dst_full_box.max_x - 1,
332
- dst_full_box.max_z - 1,
333
- sub_chunk_size=destination_sub_chunk_shape,
334
- )
335
- for dst_cx, dst_cz in itertools.product(
336
- range(first_chunk[0], last_chunk[0] + 1),
337
- range(first_chunk[1], last_chunk[1] + 1),
338
- ):
339
- chunk_box = self._chunk_box(dst_cx, dst_cz, destination_sub_chunk_shape)
340
- dst_box = chunk_box.intersection(dst_full_box)
341
- src_box = SelectionBox(-offset + dst_box.min, -offset + dst_box.max)
342
- src_slices = src_box.chunk_slice(src_cx, src_cz, self.sub_chunk_size)
343
- dst_slices = dst_box.chunk_slice(dst_cx, dst_cz, self.sub_chunk_size)
344
- yield (src_cx, src_cz), src_slices, src_box, (
345
- dst_cx,
346
- dst_cz,
347
- ), dst_slices, dst_box
348
-
349
- def get_moved_chunk_slice_box(
350
- self,
351
- dimension: Dimension,
352
- destination_origin: BlockCoordinates,
353
- selection: Optional[Union[SelectionGroup, SelectionBox]] = None,
354
- destination_sub_chunk_shape: Optional[int] = None,
355
- create_missing_chunks: bool = False,
356
- ) -> Generator[
357
- Tuple[
358
- Chunk,
359
- Tuple[slice, slice, slice],
360
- SelectionBox,
361
- ChunkCoordinates,
362
- Tuple[slice, slice, slice],
363
- SelectionBox,
364
- ],
365
- None,
366
- None,
367
- ]:
368
- """
369
- Iterate over a selection and return slices into the source object and destination object
370
- given the origin of the destination. When copying a selection to a new area the slices will
371
- only be equal if the offset is a multiple of the chunk size. This will rarely be the case
372
- so the slices need to be split up into parts that intersect a chunk in the source and destination.
373
-
374
- :param dimension: The dimension to iterate over.
375
- :param destination_origin: The location where the minimum point of self.selection will end up
376
- :param selection: An optional selection. The overlap of this and self.selection will be used
377
- :param destination_sub_chunk_shape: the chunk shape of the destination object (defaults to self.sub_chunk_size)
378
- :param create_missing_chunks: Generate empty chunks if the chunk does not exist.
379
- :return:
380
- """
381
- for (
382
- (src_cx, src_cz),
383
- src_slices,
384
- src_box,
385
- (dst_cx, dst_cz),
386
- dst_slices,
387
- dst_box,
388
- ) in self.get_moved_coord_slice_box(
389
- dimension,
390
- destination_origin,
391
- selection,
392
- destination_sub_chunk_shape,
393
- create_missing_chunks,
394
- ):
395
- try:
396
- chunk = self.get_chunk(src_cx, src_cz, dimension)
397
- except ChunkDoesNotExist:
398
- chunk = self.create_chunk(dst_cx, dst_cz, dimension)
399
- except ChunkLoadError:
400
- log.error(f"Error loading chunk\n{traceback.format_exc()}")
401
- continue
402
- yield chunk, src_slices, src_box, (dst_cx, dst_cz), dst_slices, dst_box
403
-
404
- def pre_save_operation(self) -> Generator[float, None, bool]:
405
- """
406
- Logic to run before saving. Eg recalculating height maps or lighting.
407
- Is a generator yielding progress from 0 to 1 and returning a bool saying if changes have been made.
408
-
409
- :return: Have any modifications been made.
410
- """
411
- return self.level_wrapper.pre_save_operation(self)
412
-
413
- def save(
414
- self,
415
- wrapper: api_wrapper.FormatWrapper = None,
416
- progress_callback: Callable[[int, int], None] = None,
417
- ):
418
- """
419
- Save the level to the given :class:`FormatWrapper`.
420
-
421
- :param wrapper: If specified will save the data to this wrapper instead of self.level_wrapper
422
- :param progress_callback: Optional progress callback to let the calling program know the progress. Input format chunk_index, chunk_count
423
- :return:
424
- """
425
- for chunk_index, chunk_count in self.save_iter(wrapper):
426
- if progress_callback is not None:
427
- progress_callback(chunk_index, chunk_count)
428
-
429
- def save_iter(
430
- self, wrapper: api_wrapper.FormatWrapper = None
431
- ) -> Generator[Tuple[int, int], None, None]:
432
- """
433
- Save the level to the given :class:`FormatWrapper`.
434
-
435
- This will yield the progress which can be used to update a UI.
436
-
437
- :param wrapper: If specified will save the data to this wrapper instead of self.level_wrapper
438
- :return: A generator of the number of chunks completed and the total number of chunks
439
- """
440
- # TODO change the yield type to match OperationReturnType
441
-
442
- chunk_index = 0
443
-
444
- changed_chunks = list(self._chunks.changed_chunks())
445
- chunk_count = len(changed_chunks)
446
-
447
- if wrapper is None:
448
- wrapper = self.level_wrapper
449
-
450
- output_dimension_map = wrapper.dimensions
451
-
452
- # perhaps make this check if the directory is the same rather than if the class is the same
453
- save_as = wrapper is not self.level_wrapper
454
- if save_as:
455
- # The input wrapper is not the same as the loading wrapper (save-as)
456
- # iterate through every chunk in the input level and save them to the wrapper
457
- log.info(
458
- f"Converting level {self.level_wrapper.path} to level {wrapper.path}"
459
- )
460
- wrapper.translation_manager = (
461
- self.level_wrapper.translation_manager
462
- ) # TODO: this might cause issues in the future
463
- for dimension in self.level_wrapper.dimensions:
464
- chunk_count += len(list(self.level_wrapper.all_chunk_coords(dimension)))
465
-
466
- for dimension in self.level_wrapper.dimensions:
467
- try:
468
- if dimension not in output_dimension_map:
469
- continue
470
- for cx, cz in self.level_wrapper.all_chunk_coords(dimension):
471
- log.info(f"Converting chunk {dimension} {cx}, {cz}")
472
- try:
473
- chunk = self.level_wrapper.load_chunk(cx, cz, dimension)
474
- if chunk.status.as_type(StatusFormats.Java_14) == "full":
475
- wrapper.commit_chunk(chunk, dimension)
476
- except ChunkLoadError:
477
- log.info(f"Error loading chunk {cx} {cz}", exc_info=True)
478
- chunk_index += 1
479
- yield chunk_index, chunk_count
480
- if not chunk_index % 10000:
481
- wrapper.save()
482
- self.level_wrapper.unload()
483
- wrapper.unload()
484
- except DimensionDoesNotExist:
485
- continue
486
-
487
- for dimension, cx, cz in changed_chunks:
488
- if dimension not in output_dimension_map:
489
- continue
490
- try:
491
- chunk = self.get_chunk(cx, cz, dimension)
492
- except ChunkDoesNotExist:
493
- wrapper.delete_chunk(cx, cz, dimension)
494
- except ChunkLoadError:
495
- pass
496
- else:
497
- wrapper.commit_chunk(chunk, dimension)
498
- chunk.changed = False
499
- chunk_index += 1
500
- yield chunk_index, chunk_count
501
- if not chunk_index % 10000:
502
- wrapper.save()
503
- wrapper.unload()
504
-
505
- self.history_manager.mark_saved()
506
- log.info(f"Saving changes to level {wrapper.path}")
507
- wrapper.save()
508
- log.info(f"Finished saving changes to level {wrapper.path}")
509
-
510
- def purge(self):
511
- """
512
- Unload all loaded and cached data.
513
-
514
- This is functionally the same as closing and reopening the world without creating a new class.
515
- """
516
- self.unload()
517
- self.history_manager.purge()
518
-
519
- def close(self):
520
- """
521
- Close the attached level and remove temporary files.
522
-
523
- Use changed method to check if there are any changes that should be saved before closing.
524
- """
525
- self.level_wrapper.close()
526
- self._history_db.close(compact=False)
527
-
528
- def unload(self, safe_area: Optional[Tuple[Dimension, int, int, int, int]] = None):
529
- """
530
- Unload all chunk data not in the safe area.
531
-
532
- :param safe_area: The area that should not be unloaded [dimension, min_chunk_x, min_chunk_z, max_chunk_x, max_chunk_z]. If None will unload all chunk data.
533
- """
534
- self._chunks.unload(safe_area)
535
- self.level_wrapper.unload()
536
-
537
- def unload_unchanged(self):
538
- """Unload all data that has not been marked as changed."""
539
- self._chunks.unload_unchanged()
540
-
541
- @property
542
- def chunks(self) -> ChunkManager:
543
- """
544
- The chunk container.
545
-
546
- Most methods from :class:`ChunkManager` also exists in the level class.
547
- """
548
- return self._chunks
549
-
550
- def all_chunk_coords(self, dimension: Dimension) -> Set[Tuple[int, int]]:
551
- """
552
- The coordinates of every chunk in this dimension of the level.
553
-
554
- This is the combination of chunks saved to the level and chunks yet to be saved.
555
- """
556
- return self._chunks.all_chunk_coords(dimension)
557
-
558
- def has_chunk(self, cx: int, cz: int, dimension: Dimension) -> bool:
559
- """
560
- Does the chunk exist. This is a quick way to check if the chunk exists without loading it.
561
-
562
- :param cx: The x coordinate of the chunk.
563
- :param cz: The z coordinate of the chunk.
564
- :param dimension: The dimension to load the chunk from.
565
- :return: True if the chunk exists. Calling get_chunk on this chunk may still throw ChunkLoadError
566
- """
567
- return self._chunks.has_chunk(dimension, cx, cz)
568
-
569
- def get_chunk(self, cx: int, cz: int, dimension: Dimension) -> Chunk:
570
- """
571
- Gets a :class:`Chunk` class containing the data for the requested chunk.
572
-
573
- :param cx: The X coordinate of the desired chunk
574
- :param cz: The Z coordinate of the desired chunk
575
- :param dimension: The dimension to get the chunk from
576
- :return: A Chunk object containing the data for the chunk
577
- :raises:
578
- :class:`~amulet.api.errors.ChunkDoesNotExist`: If the chunk does not exist (was deleted or never created)
579
-
580
- :class:`~amulet.api.errors.ChunkLoadError`: If the chunk was not able to be loaded. Eg. If the chunk is corrupt or some error occurred when loading.
581
- """
582
- return self._chunks.get_chunk(dimension, cx, cz)
583
-
584
- def create_chunk(self, cx: int, cz: int, dimension: Dimension) -> Chunk:
585
- """
586
- Create an empty chunk and put it at the given location.
587
-
588
- If a chunk exists at the given location it will be overwritten.
589
-
590
- :param cx: The X coordinate of the chunk
591
- :param cz: The Z coordinate of the chunk
592
- :param dimension: The dimension to put the chunk in.
593
- :return: The newly created :class:`Chunk`.
594
- """
595
- chunk = Chunk(cx, cz)
596
- self.put_chunk(chunk, dimension)
597
- return chunk
598
-
599
- def put_chunk(self, chunk: Chunk, dimension: Dimension):
600
- """
601
- Add a given chunk to the level.
602
-
603
- :param chunk: The :class:`Chunk` to add to the level. It will be added at the location stored in :attr:`Chunk.coordinates`
604
- :param dimension: The dimension to add the chunk to.
605
- """
606
- self._chunks.put_chunk(chunk, dimension)
607
-
608
- def delete_chunk(self, cx: int, cz: int, dimension: Dimension):
609
- """
610
- Delete a chunk from the level.
611
-
612
- :param cx: The X coordinate of the chunk
613
- :param cz: The Z coordinate of the chunk
614
- :param dimension: The dimension to delete the chunk from.
615
- """
616
- self._chunks.delete_chunk(dimension, cx, cz)
617
-
618
- def extract_structure(
619
- self, selection: SelectionGroup, dimension: Dimension
620
- ) -> api_level.ImmutableStructure:
621
- """
622
- Extract the region of the dimension specified by ``selection`` to an :class:`~api_level.ImmutableStructure` class.
623
-
624
- :param selection: The selection to extract.
625
- :param dimension: The dimension to extract the selection from.
626
- :return: The :class:`~api_level.ImmutableStructure` containing the extracted region.
627
- """
628
- return api_level.ImmutableStructure.from_level(self, selection, dimension)
629
-
630
- def extract_structure_iter(
631
- self, selection: SelectionGroup, dimension: Dimension
632
- ) -> Generator[float, None, api_level.ImmutableStructure]:
633
- """
634
- Extract the region of the dimension specified by ``selection`` to an :class:`~api_level.ImmutableStructure` class.
635
-
636
- Also yields the progress as a float from 0-1
637
-
638
- :param selection: The selection to extract.
639
- :param dimension: The dimension to extract the selection from.
640
- :return: The :class:`~api_level.ImmutableStructure` containing the extracted region.
641
- """
642
- immutable_level = yield from api_level.ImmutableStructure.from_level_iter(
643
- self, selection, dimension
644
- )
645
- return immutable_level
646
-
647
- def paste(
648
- self,
649
- src_structure: "BaseLevel",
650
- src_dimension: Dimension,
651
- src_selection: SelectionGroup,
652
- dst_dimension: Dimension,
653
- location: BlockCoordinates,
654
- scale: FloatTriplet = (1.0, 1.0, 1.0),
655
- rotation: FloatTriplet = (0.0, 0.0, 0.0),
656
- include_blocks: bool = True,
657
- include_entities: bool = True,
658
- skip_blocks: Tuple[Block, ...] = (),
659
- copy_chunk_not_exist: bool = False,
660
- ):
661
- """Paste a level into this level at the given location.
662
- Note this command may change in the future.
663
-
664
- :param src_structure: The structure to paste into this structure.
665
- :param src_dimension: The dimension of the source structure to copy from.
666
- :param src_selection: The selection to copy from the source structure.
667
- :param dst_dimension: The dimension to paste the structure into.
668
- :param location: The location where the centre of the structure will be in the level
669
- :param scale: The scale in the x, y and z axis. These can be negative to mirror.
670
- :param rotation: The rotation in degrees around each of the axis.
671
- :param include_blocks: Include blocks when pasting the structure.
672
- :param include_entities: Include entities when pasting the structure.
673
- :param skip_blocks: If a block matches a block in this list it will not be copied.
674
- :param copy_chunk_not_exist: If a chunk does not exist in the source should it be copied over as air. Always False where level is a World.
675
- :return:
676
- """
677
- return generator_unpacker(
678
- self.paste_iter(
679
- src_structure,
680
- src_dimension,
681
- src_selection,
682
- dst_dimension,
683
- location,
684
- scale,
685
- rotation,
686
- include_blocks,
687
- include_entities,
688
- skip_blocks,
689
- copy_chunk_not_exist,
690
- )
691
- )
692
-
693
- def paste_iter(
694
- self,
695
- src_structure: "BaseLevel",
696
- src_dimension: Dimension,
697
- src_selection: SelectionGroup,
698
- dst_dimension: Dimension,
699
- location: BlockCoordinates,
700
- scale: FloatTriplet = (1.0, 1.0, 1.0),
701
- rotation: FloatTriplet = (0.0, 0.0, 0.0),
702
- include_blocks: bool = True,
703
- include_entities: bool = True,
704
- skip_blocks: Tuple[Block, ...] = (),
705
- copy_chunk_not_exist: bool = False,
706
- ) -> Generator[float, None, None]:
707
- """Paste a structure into this structure at the given location.
708
- Note this command may change in the future.
709
-
710
- :param src_structure: The structure to paste into this structure.
711
- :param src_dimension: The dimension of the source structure to copy from.
712
- :param src_selection: The selection to copy from the source structure.
713
- :param dst_dimension: The dimension to paste the structure into.
714
- :param location: The location where the centre of the structure will be in the level
715
- :param scale: The scale in the x, y and z axis. These can be negative to mirror.
716
- :param rotation: The rotation in degrees around each of the axis.
717
- :param include_blocks: Include blocks when pasting the structure.
718
- :param include_entities: Include entities when pasting the structure.
719
- :param skip_blocks: If a block matches a block in this list it will not be copied.
720
- :param copy_chunk_not_exist: If a chunk does not exist in the source should it be copied over as air. Always False where level is a World.
721
- :return: A generator of floats from 0 to 1 with the progress of the paste operation.
722
- """
723
- yield from clone(
724
- src_structure,
725
- src_dimension,
726
- src_selection,
727
- self,
728
- dst_dimension,
729
- self.bounds(dst_dimension),
730
- location,
731
- scale,
732
- rotation,
733
- include_blocks,
734
- include_entities,
735
- skip_blocks,
736
- copy_chunk_not_exist,
737
- )
738
-
739
- def get_version_block(
740
- self,
741
- x: int,
742
- y: int,
743
- z: int,
744
- dimension: Dimension,
745
- version: VersionIdentifierType,
746
- ) -> Union[Tuple[Block, BlockEntity], Tuple[Entity, None]]:
747
- """
748
- Get a block at the specified location and convert it to the format of the version specified
749
-
750
- Note the odd return format. In most cases this will return (Block, None) or (Block, BlockEntity) if a block entity is present.
751
-
752
- In select cases (like item frames) it may return (Entity, None)
753
-
754
- :param x: The X coordinate of the desired block
755
- :param y: The Y coordinate of the desired block
756
- :param z: The Z coordinate of the desired block
757
- :param dimension: The dimension of the desired block
758
- :param version: The version to get the block converted to.
759
-
760
- >>> ("java", (1, 16, 2)) # Java 1.16.2 format
761
- >>> ("java", 2578) # Java 1.16.2 format (using the data version)
762
- >>> ("bedrock", (1, 16, 210)) # Bedrock 1.16.210 format
763
- :return: The block at the given location converted to the `version` format. Note the odd return format.
764
- :raises:
765
- :class:`~amulet.api.errors.ChunkDoesNotExist`: If the chunk does not exist (was deleted or never created)
766
-
767
- :class:`~amulet.api.errors.ChunkLoadError`: If the chunk was not able to be loaded. Eg. If the chunk is corrupt or some error occurred when loading.
768
- """
769
- cx, cz = block_coords_to_chunk_coords(x, z, sub_chunk_size=self.sub_chunk_size)
770
- chunk = self.get_chunk(cx, cz, dimension)
771
- offset_x, offset_z = x - 16 * cx, z - 16 * cz
772
-
773
- output, extra_output, _ = self.translation_manager.get_version(
774
- *version
775
- ).block.from_universal(
776
- chunk.get_block(offset_x, y, offset_z), chunk.block_entities.get((x, y, z))
777
- )
778
- return output, extra_output
779
-
780
- def set_version_block(
781
- self,
782
- x: int,
783
- y: int,
784
- z: int,
785
- dimension: Dimension,
786
- version: VersionIdentifierType,
787
- block: Block,
788
- block_entity: BlockEntity = None,
789
- ):
790
- """
791
- Convert the block and block_entity from the given version format to the universal format and set at the location.
792
-
793
- :param x: The X coordinate of the desired block.
794
- :param y: The Y coordinate of the desired block.
795
- :param z: The Z coordinate of the desired block.
796
- :param dimension: The dimension of the desired block.
797
- :param version: The version the given ``block`` and ``block_entity`` come from.
798
-
799
- >>> ("java", (1, 16, 2)) # Java 1.16.2 format
800
- >>> ("java", 2578) # Java 1.16.2 format (using the data version)
801
- >>> ("bedrock", (1, 16, 210)) # Bedrock 1.16.210 format
802
- :param block: The block to set. Must be valid in the specified version.
803
- :param block_entity: The block entity to set. Must be valid in the specified version.
804
- :return: The block at the given location converted to the `version` format. Note the odd return format.
805
- :raises:
806
- ChunkLoadError: If the chunk was not able to be loaded. Eg. If the chunk is corrupt or some error occurred when loading.
807
- """
808
- cx, cz = block_coords_to_chunk_coords(x, z, sub_chunk_size=self.sub_chunk_size)
809
- try:
810
- chunk = self.get_chunk(cx, cz, dimension)
811
- except ChunkDoesNotExist:
812
- chunk = self.create_chunk(cx, cz, dimension)
813
- offset_x, offset_z = x - 16 * cx, z - 16 * cz
814
-
815
- (
816
- universal_block,
817
- universal_block_entity,
818
- _,
819
- ) = self.translation_manager.get_version(*version).block.to_universal(
820
- block, block_entity
821
- )
822
- chunk.set_block(offset_x, y, offset_z, universal_block),
823
- if isinstance(universal_block_entity, BlockEntity):
824
- chunk.block_entities[(x, y, z)] = universal_block_entity
825
- elif (x, y, z) in chunk.block_entities:
826
- del chunk.block_entities[(x, y, z)]
827
- chunk.changed = True
828
-
829
- def get_native_entities(
830
- self, cx: int, cz: int, dimension: Dimension
831
- ) -> Tuple[EntityList, VersionIdentifierType]:
832
- """
833
- Get a list of entities in the native format from a given chunk.
834
- This currently returns the raw data from the chunk but in the future will convert to the world version format.
835
-
836
- :param cx: The chunk x position
837
- :param cz: The chunk z position
838
- :param dimension: The dimension of the chunk.
839
- :return: A copy of the list of entities and the version format they are in.
840
- """
841
- chunk = self.get_chunk(cx, cz, dimension)
842
- # To make this forwards compatible this needs to be deep copied
843
- return copy.deepcopy(chunk._native_entities), chunk._native_version
844
-
845
- def set_native_entites(
846
- self, cx: int, cz: int, dimension: Dimension, entities: Iterable[Entity]
847
- ):
848
- """
849
- Set the entities in the native format.
850
- Note that the format must be compatible with `level_wrapper.max_world_version`.
851
-
852
- :param cx: The chunk x position
853
- :param cz: The chunk z position
854
- :param dimension: The dimension of the chunk.
855
- :param entities: The entities to set on the chunk.
856
- """
857
- chunk = self.get_chunk(cx, cz, dimension)
858
- chunk._native_entities = EntityList(copy.deepcopy(entities))
859
- chunk._native_version = self.level_wrapper.max_world_version
860
- chunk.changed = True
861
-
862
- # def get_entities_in_box(
863
- # self, box: "SelectionGroup"
864
- # ) -> Generator[Tuple[Coordinates, List[object]], None, None]:
865
- # # TODO: some of this logic can probably be moved the chunk class and have this method call that
866
- # # TODO: update this to use the newer entity API
867
- # out_of_place_entities = []
868
- # entity_map: Dict[Tuple[int, int], List[List[object]]] = {}
869
- # for chunk, subbox in self.get_chunk_boxes(box):
870
- # entities = chunk.entities
871
- # in_box = list(filter(lambda e: e.location in subbox, entities))
872
- # not_in_box = filter(lambda e: e.location not in subbox, entities)
873
- #
874
- # in_box_copy = deepcopy(in_box)
875
- #
876
- # entity_map[chunk.coordinates] = [
877
- # not_in_box,
878
- # in_box,
879
- # ] # First index is the list of entities not in the box, the second is for ones that are
880
- #
881
- # yield chunk.coordinates, in_box_copy
882
- #
883
- # if (
884
- # in_box != in_box_copy
885
- # ): # If an entity has been changed, update the dictionary entry
886
- # entity_map[chunk.coordinates][1] = in_box_copy
887
- # else: # Delete the entry otherwise
888
- # del entity_map[chunk.coordinates]
889
- #
890
- # for chunk_coords, entity_list_list in entity_map.items():
891
- # chunk = self.get_chunk(*chunk_coords)
892
- # in_place_entities = list(
893
- # filter(
894
- # lambda e: chunk_coords
895
- # == entity_position_to_chunk_coordinates(e.location),
896
- # entity_list_list[1],
897
- # )
898
- # )
899
- # out_of_place = filter(
900
- # lambda e: chunk_coords
901
- # != entity_position_to_chunk_coordinates(e.location),
902
- # entity_list_list[1],
903
- # )
904
- #
905
- # chunk.entities = in_place_entities + list(entity_list_list[0])
906
- #
907
- # if out_of_place:
908
- # out_of_place_entities.extend(out_of_place)
909
- #
910
- # if out_of_place_entities:
911
- # self.add_entities(out_of_place_entities)
912
- #
913
- # def add_entities(self, entities):
914
- # proper_entity_chunks = map(
915
- # lambda e: (entity_position_to_chunk_coordinates(e.location), e,), entities,
916
- # )
917
- # accumulated_entities: Dict[Tuple[int, int], List[object]] = {}
918
- #
919
- # for chunk_coord, ent in proper_entity_chunks:
920
- # if chunk_coord in accumulated_entities:
921
- # accumulated_entities[chunk_coord].append(ent)
922
- # else:
923
- # accumulated_entities[chunk_coord] = [ent]
924
- #
925
- # for chunk_coord, ents in accumulated_entities.items():
926
- # chunk = self.get_chunk(*chunk_coord)
927
- #
928
- # chunk.entities += ents
929
- #
930
- # def delete_entities(self, entities):
931
- # chunk_entity_pairs = map(
932
- # lambda e: (entity_position_to_chunk_coordinates(e.location), e,), entities,
933
- # )
934
- #
935
- # for chunk_coord, ent in chunk_entity_pairs:
936
- # chunk = self.get_chunk(*chunk_coord)
937
- # entities = chunk.entities
938
- # entities.remove(ent)
939
- # chunk.entities = entities
940
-
941
- @property
942
- def history_manager(self) -> MetaHistoryManager:
943
- """The class that manages undoing and redoing changes."""
944
- return self._history_manager
945
-
946
- def create_undo_point(self, world=True, non_world=True) -> bool:
947
- """
948
- Create a restore point for all the data that has changed.
949
-
950
- :param world: If True the restore point will include world based data.
951
- :param non_world: If True the restore point will include data not related to the world.
952
- :return: If True a restore point was created. If nothing changed no restore point will be created.
953
- """
954
- return self.history_manager.create_undo_point(world, non_world)
955
-
956
- def create_undo_point_iter(
957
- self, world=True, non_world=True
958
- ) -> Generator[float, None, bool]:
959
- """
960
- Create a restore point for all the data that has changed.
961
-
962
- Also yields progress from 0-1
963
-
964
- :param world: If True the restore point will include world based data.
965
- :param non_world: If True the restore point will include data not related to the world.
966
- :return: If True a restore point was created. If nothing changed no restore point will be created.
967
- """
968
- return self.history_manager.create_undo_point_iter(world, non_world)
969
-
970
- @property
971
- def changed(self) -> bool:
972
- """Has any data been modified but not saved to disk"""
973
- return self.history_manager.changed or self.level_wrapper.changed
974
-
975
- def undo(self):
976
- """Undoes the last set of changes to the level."""
977
- self.history_manager.undo()
978
-
979
- def redo(self):
980
- """Redoes the last set of changes to the level."""
981
- self.history_manager.redo()
982
-
983
- def restore_last_undo_point(self):
984
- """
985
- Restore the level to the state it was when self.create_undo_point was last called.
986
-
987
- If an operation errors there may be modifications made that did not get tracked.
988
-
989
- This will revert those changes.
990
- """
991
- self.history_manager.restore_last_undo_point()
992
-
993
- @property
994
- def players(self) -> PlayerManager:
995
- """
996
- The player container.
997
-
998
- Most methods from :class:`PlayerManager` also exists in the level class.
999
- """
1000
- return self._players
1001
-
1002
- def all_player_ids(self) -> Set[str]:
1003
- """
1004
- Returns a set of all player ids that are present in the level.
1005
- """
1006
- return self.players.all_player_ids()
1007
-
1008
- def has_player(self, player_id: str) -> bool:
1009
- """
1010
- Is the given player id present in the level
1011
-
1012
- :param player_id: The player id to check
1013
- :return: True if the player id is present, False otherwise
1014
- """
1015
- return self.players.has_player(player_id)
1016
-
1017
- def get_player(self, player_id: str) -> Player:
1018
- """
1019
- Gets the :class:`Player` object that belongs to the specified player id
1020
-
1021
- If no parameter is supplied, the data of the local player will be returned
1022
-
1023
- :param player_id: The desired player id
1024
- :return: A Player instance
1025
- """
1026
- return self.players.get_player(player_id)
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Union, Generator, Optional, Tuple, Callable, Set, Iterable
5
+ import traceback
6
+ import numpy
7
+ import itertools
8
+ import warnings
9
+ import logging
10
+ import copy
11
+ import os
12
+
13
+ from amulet.api.block import Block, UniversalAirBlock
14
+ from amulet.api.block_entity import BlockEntity
15
+ from amulet.api.entity import Entity
16
+ from amulet.api.registry import BlockManager
17
+ from amulet.api.registry.biome_manager import BiomeManager
18
+ from amulet.api.errors import ChunkDoesNotExist, ChunkLoadError, DimensionDoesNotExist
19
+ from amulet.api.chunk import Chunk, EntityList
20
+ from amulet.api.selection import SelectionGroup, SelectionBox
21
+ from amulet.api.data_types import (
22
+ Dimension,
23
+ VersionIdentifierType,
24
+ BlockCoordinates,
25
+ FloatTriplet,
26
+ ChunkCoordinates,
27
+ )
28
+ from amulet.api.chunk.status import StatusFormats
29
+ from amulet.api.cache import TempDir
30
+ from leveldb import LevelDB
31
+ from amulet.utils.generator import generator_unpacker
32
+ from amulet.utils.world_utils import block_coords_to_chunk_coords
33
+ from .chunk_manager import ChunkManager
34
+ from amulet.api.history.history_manager import MetaHistoryManager
35
+ from .clone import clone
36
+ from amulet.api import wrapper as api_wrapper, level as api_level
37
+ import PyMCTranslate
38
+ from amulet.api.player import Player
39
+ from .player_manager import PlayerManager
40
+
41
+ log = logging.getLogger(__name__)
42
+
43
+
44
+ class BaseLevel:
45
+ """
46
+ BaseLevel is a base class for all world-like data.
47
+
48
+ It exposes chunk data and other data using a history system to track and enable undoing changes.
49
+ """
50
+
51
+ def __init__(self, path: str, format_wrapper: api_wrapper.FormatWrapper):
52
+ """
53
+ Construct a :class:`BaseLevel` object from the given data.
54
+
55
+ This should not be used directly. You should instead use :func:`amulet.load_level`.
56
+
57
+ :param path: The path to the data being loaded. May be a file or directory. If blank there is no data on disk associated with this.
58
+ :param format_wrapper: The :class:`FormatWrapper` instance that the level will wrap around.
59
+ """
60
+ self._path = path
61
+
62
+ self._level_wrapper = format_wrapper
63
+ self.level_wrapper.open()
64
+
65
+ self._block_palette = BlockManager()
66
+ self._block_palette.get_add_block(
67
+ UniversalAirBlock
68
+ ) # ensure that index 0 is always air
69
+
70
+ self._biome_palette = BiomeManager()
71
+ self._biome_palette.get_add_biome("universal_minecraft:plains")
72
+
73
+ self._history_manager = MetaHistoryManager()
74
+
75
+ self._temp_dir = TempDir()
76
+ self._history_db = LevelDB(
77
+ os.path.join(self._temp_dir, "history_db"), create_if_missing=True
78
+ )
79
+ self._chunks: ChunkManager = ChunkManager(self, self._history_db)
80
+ self._players = PlayerManager(self)
81
+
82
+ self.history_manager.register(self._chunks, True)
83
+ self.history_manager.register(self._players, True)
84
+
85
+ def __del__(self):
86
+ self.close()
87
+
88
+ @property
89
+ def level_wrapper(self) -> api_wrapper.FormatWrapper:
90
+ """A class to access data directly from the level."""
91
+ return self._level_wrapper
92
+
93
+ @property
94
+ def sub_chunk_size(self) -> int:
95
+ """The normal dimensions of the chunk."""
96
+ return self.level_wrapper.sub_chunk_size
97
+
98
+ @property
99
+ def level_path(self) -> str:
100
+ """
101
+ The system path where the level is located.
102
+
103
+ This may be a directory, file or an empty string depending on the level that is loaded.
104
+ """
105
+ return self._path
106
+
107
+ @property
108
+ def translation_manager(self) -> PyMCTranslate.TranslationManager:
109
+ """An instance of the translation class for use with this level."""
110
+ return self.level_wrapper.translation_manager
111
+
112
+ @property
113
+ def block_palette(self) -> BlockManager:
114
+ """The manager for the universal blocks in this level. New blocks must be registered here before adding to the level."""
115
+ return self._block_palette
116
+
117
+ @property
118
+ def biome_palette(self) -> BiomeManager:
119
+ """The manager for the universal blocks in this level. New biomes must be registered here before adding to the level."""
120
+ return self._biome_palette
121
+
122
+ @property
123
+ def selection_bounds(self) -> SelectionGroup:
124
+ """The selection(s) that all chunk data must fit within. Usually +/-30M for worlds. The selection for structures."""
125
+ warnings.warn(
126
+ "BaseLevel.selection_bounds is depreciated and will be removed in the future. Please use BaseLevel.bounds(dimension) instead",
127
+ DeprecationWarning,
128
+ )
129
+ return self.bounds(self.dimensions[0])
130
+
131
+ def bounds(self, dimension: Dimension) -> SelectionGroup:
132
+ """
133
+ The selection(s) that all chunk data must fit within.
134
+ This specifies the volume that can be built in.
135
+ Worlds will have a single cuboid volume.
136
+ Structures may have one or more cuboid volumes.
137
+
138
+ :param dimension: The dimension to get the bounds of.
139
+ :return: The build volume for the dimension.
140
+ """
141
+ return self.level_wrapper.bounds(dimension)
142
+
143
+ @property
144
+ def dimensions(self) -> Tuple[Dimension, ...]:
145
+ """The dimensions strings that are valid for this level."""
146
+ return tuple(self.level_wrapper.dimensions)
147
+
148
+ def get_block(self, x: int, y: int, z: int, dimension: Dimension) -> Block:
149
+ """
150
+ Gets the universal Block object at the specified coordinates.
151
+
152
+ To get the block in a given format use :meth:`get_version_block`
153
+
154
+ :param x: The X coordinate of the desired block
155
+ :param y: The Y coordinate of the desired block
156
+ :param z: The Z coordinate of the desired block
157
+ :param dimension: The dimension of the desired block
158
+ :return: The universal Block object representation of the block at that location
159
+ :raises:
160
+ :class:`~amulet.api.errors.ChunkDoesNotExist`: If the chunk does not exist (was deleted or never created)
161
+
162
+ :class:`~amulet.api.errors.ChunkLoadError`: If the chunk was not able to be loaded. Eg. If the chunk is corrupt or some error occurred when loading.
163
+ """
164
+ cx, cz = block_coords_to_chunk_coords(x, z, sub_chunk_size=self.sub_chunk_size)
165
+ offset_x, offset_z = x - 16 * cx, z - 16 * cz
166
+
167
+ return self.get_chunk(cx, cz, dimension).get_block(offset_x, y, offset_z)
168
+
169
+ def _chunk_box(self, cx: int, cz: int, sub_chunk_size: Optional[int] = None):
170
+ """Get a SelectionBox containing the whole of a given chunk"""
171
+ if sub_chunk_size is None:
172
+ sub_chunk_size = self.sub_chunk_size
173
+ return SelectionBox.create_chunk_box(cx, cz, sub_chunk_size)
174
+
175
+ def _sanitise_selection(
176
+ self, selection: Union[SelectionGroup, SelectionBox, None], dimension: Dimension
177
+ ) -> SelectionGroup:
178
+ if isinstance(selection, SelectionBox):
179
+ return SelectionGroup(selection)
180
+ elif isinstance(selection, SelectionGroup):
181
+ return selection
182
+ elif selection is None:
183
+ return self.bounds(dimension)
184
+ else:
185
+ raise ValueError(
186
+ f"Expected SelectionBox, SelectionGroup or None. Got {selection}"
187
+ )
188
+
189
+ def get_coord_box(
190
+ self,
191
+ dimension: Dimension,
192
+ selection: Union[SelectionGroup, SelectionBox, None] = None,
193
+ yield_missing_chunks=False,
194
+ ) -> Generator[Tuple[ChunkCoordinates, SelectionBox], None, None]:
195
+ """
196
+ Given a selection will yield chunk coordinates and :class:`SelectionBox` instances into that chunk
197
+
198
+ If not given a selection will use the bounds of the object.
199
+
200
+ :param dimension: The dimension to take effect in.
201
+ :param selection: SelectionGroup or SelectionBox into the level. If None will use :meth:`bounds` for the dimension.
202
+ :param yield_missing_chunks: If a chunk does not exist an empty one will be created (defaults to false). Use this with care.
203
+ """
204
+ selection = self._sanitise_selection(selection, dimension)
205
+ if yield_missing_chunks or selection.footprint_area < 1_000_000:
206
+ if yield_missing_chunks:
207
+ for coord, box in selection.chunk_boxes(self.sub_chunk_size):
208
+ yield coord, box
209
+ else:
210
+ for (cx, cz), box in selection.chunk_boxes(self.sub_chunk_size):
211
+ if self.has_chunk(cx, cz, dimension):
212
+ yield (cx, cz), box
213
+
214
+ else:
215
+ # if the selection gets very large iterating over the whole selection and accessing chunks can get slow
216
+ # instead we are going to iterate over the chunks and get the intersection of the selection
217
+ for cx, cz in self.all_chunk_coords(dimension):
218
+ box = SelectionGroup(
219
+ SelectionBox.create_chunk_box(cx, cz, self.sub_chunk_size)
220
+ )
221
+
222
+ if selection.intersects(box):
223
+ chunk_selection = selection.intersection(box)
224
+ for sub_box in chunk_selection.selection_boxes:
225
+ yield (cx, cz), sub_box
226
+
227
+ def get_chunk_boxes(
228
+ self,
229
+ dimension: Dimension,
230
+ selection: Union[SelectionGroup, SelectionBox, None] = None,
231
+ create_missing_chunks=False,
232
+ ) -> Generator[Tuple[Chunk, SelectionBox], None, None]:
233
+ """
234
+ Given a selection will yield :class:`Chunk` and :class:`SelectionBox` instances into that chunk
235
+
236
+ If not given a selection will use the bounds of the object.
237
+
238
+ :param dimension: The dimension to take effect in.
239
+ :param selection: SelectionGroup or SelectionBox into the level. If None will use :meth:`bounds` for the dimension.
240
+ :param create_missing_chunks: If a chunk does not exist an empty one will be created (defaults to false). Use this with care.
241
+ """
242
+ for (cx, cz), box in self.get_coord_box(
243
+ dimension, selection, create_missing_chunks
244
+ ):
245
+ try:
246
+ chunk = self.get_chunk(cx, cz, dimension)
247
+ except ChunkDoesNotExist:
248
+ if create_missing_chunks:
249
+ yield self.create_chunk(cx, cz, dimension), box
250
+ except ChunkLoadError:
251
+ log.error(f"Error loading chunk\n{traceback.format_exc()}")
252
+ else:
253
+ yield chunk, box
254
+
255
+ def get_chunk_slice_box(
256
+ self,
257
+ dimension: Dimension,
258
+ selection: Union[SelectionGroup, SelectionBox] = None,
259
+ create_missing_chunks=False,
260
+ ) -> Generator[Tuple[Chunk, Tuple[slice, slice, slice], SelectionBox], None, None]:
261
+ """
262
+ Given a selection will yield :class:`Chunk`, slices, :class:`SelectionBox` for the contents of the selection.
263
+
264
+ :param dimension: The dimension to take effect in.
265
+ :param selection: SelectionGroup or SelectionBox into the level. If None will use :meth:`bounds` for the dimension.
266
+ :param create_missing_chunks: If a chunk does not exist an empty one will be created (defaults to false)
267
+
268
+ >>> for chunk, slices, box in level.get_chunk_slice_box(selection):
269
+ >>> chunk.blocks[slice] = ...
270
+ """
271
+ for chunk, box in self.get_chunk_boxes(
272
+ dimension, selection, create_missing_chunks
273
+ ):
274
+ slices = box.chunk_slice(chunk.cx, chunk.cz, self.sub_chunk_size)
275
+ yield chunk, slices, box
276
+
277
+ def get_moved_coord_slice_box(
278
+ self,
279
+ dimension: Dimension,
280
+ destination_origin: BlockCoordinates,
281
+ selection: Optional[Union[SelectionGroup, SelectionBox]] = None,
282
+ destination_sub_chunk_shape: Optional[int] = None,
283
+ yield_missing_chunks: bool = False,
284
+ ) -> Generator[
285
+ Tuple[
286
+ ChunkCoordinates,
287
+ Tuple[slice, slice, slice],
288
+ SelectionBox,
289
+ ChunkCoordinates,
290
+ Tuple[slice, slice, slice],
291
+ SelectionBox,
292
+ ],
293
+ None,
294
+ None,
295
+ ]:
296
+ """
297
+ Iterate over a selection and return slices into the source object and destination object
298
+ given the origin of the destination. When copying a selection to a new area the slices will
299
+ only be equal if the offset is a multiple of the chunk size. This will rarely be the case
300
+ so the slices need to be split up into parts that intersect a chunk in the source and destination.
301
+
302
+ :param dimension: The dimension to iterate over.
303
+ :param destination_origin: The location where the minimum point of the selection will end up
304
+ :param selection: An optional selection. The overlap of this and the dimensions bounds will be used
305
+ :param destination_sub_chunk_shape: the chunk shape of the destination object (defaults to self.sub_chunk_size)
306
+ :param yield_missing_chunks: Generate empty chunks if the chunk does not exist.
307
+ :return:
308
+ """
309
+ if destination_sub_chunk_shape is None:
310
+ destination_sub_chunk_shape = self.sub_chunk_size
311
+
312
+ if selection is None:
313
+ selection = self.bounds(dimension)
314
+ else:
315
+ selection = self.bounds(dimension).intersection(selection)
316
+ # the offset from self.selection to the destination location
317
+ offset = numpy.subtract(
318
+ destination_origin, self.bounds(dimension).min, dtype=int
319
+ )
320
+ for (src_cx, src_cz), box in self.get_coord_box(
321
+ dimension, selection, yield_missing_chunks=yield_missing_chunks
322
+ ):
323
+ dst_full_box = SelectionBox(offset + box.min, offset + box.max)
324
+
325
+ first_chunk = block_coords_to_chunk_coords(
326
+ dst_full_box.min_x,
327
+ dst_full_box.min_z,
328
+ sub_chunk_size=destination_sub_chunk_shape,
329
+ )
330
+ last_chunk = block_coords_to_chunk_coords(
331
+ dst_full_box.max_x - 1,
332
+ dst_full_box.max_z - 1,
333
+ sub_chunk_size=destination_sub_chunk_shape,
334
+ )
335
+ for dst_cx, dst_cz in itertools.product(
336
+ range(first_chunk[0], last_chunk[0] + 1),
337
+ range(first_chunk[1], last_chunk[1] + 1),
338
+ ):
339
+ chunk_box = self._chunk_box(dst_cx, dst_cz, destination_sub_chunk_shape)
340
+ dst_box = chunk_box.intersection(dst_full_box)
341
+ src_box = SelectionBox(-offset + dst_box.min, -offset + dst_box.max)
342
+ src_slices = src_box.chunk_slice(src_cx, src_cz, self.sub_chunk_size)
343
+ dst_slices = dst_box.chunk_slice(dst_cx, dst_cz, self.sub_chunk_size)
344
+ yield (src_cx, src_cz), src_slices, src_box, (
345
+ dst_cx,
346
+ dst_cz,
347
+ ), dst_slices, dst_box
348
+
349
+ def get_moved_chunk_slice_box(
350
+ self,
351
+ dimension: Dimension,
352
+ destination_origin: BlockCoordinates,
353
+ selection: Optional[Union[SelectionGroup, SelectionBox]] = None,
354
+ destination_sub_chunk_shape: Optional[int] = None,
355
+ create_missing_chunks: bool = False,
356
+ ) -> Generator[
357
+ Tuple[
358
+ Chunk,
359
+ Tuple[slice, slice, slice],
360
+ SelectionBox,
361
+ ChunkCoordinates,
362
+ Tuple[slice, slice, slice],
363
+ SelectionBox,
364
+ ],
365
+ None,
366
+ None,
367
+ ]:
368
+ """
369
+ Iterate over a selection and return slices into the source object and destination object
370
+ given the origin of the destination. When copying a selection to a new area the slices will
371
+ only be equal if the offset is a multiple of the chunk size. This will rarely be the case
372
+ so the slices need to be split up into parts that intersect a chunk in the source and destination.
373
+
374
+ :param dimension: The dimension to iterate over.
375
+ :param destination_origin: The location where the minimum point of self.selection will end up
376
+ :param selection: An optional selection. The overlap of this and self.selection will be used
377
+ :param destination_sub_chunk_shape: the chunk shape of the destination object (defaults to self.sub_chunk_size)
378
+ :param create_missing_chunks: Generate empty chunks if the chunk does not exist.
379
+ :return:
380
+ """
381
+ for (
382
+ (src_cx, src_cz),
383
+ src_slices,
384
+ src_box,
385
+ (dst_cx, dst_cz),
386
+ dst_slices,
387
+ dst_box,
388
+ ) in self.get_moved_coord_slice_box(
389
+ dimension,
390
+ destination_origin,
391
+ selection,
392
+ destination_sub_chunk_shape,
393
+ create_missing_chunks,
394
+ ):
395
+ try:
396
+ chunk = self.get_chunk(src_cx, src_cz, dimension)
397
+ except ChunkDoesNotExist:
398
+ chunk = self.create_chunk(dst_cx, dst_cz, dimension)
399
+ except ChunkLoadError:
400
+ log.error(f"Error loading chunk\n{traceback.format_exc()}")
401
+ continue
402
+ yield chunk, src_slices, src_box, (dst_cx, dst_cz), dst_slices, dst_box
403
+
404
+ def pre_save_operation(self) -> Generator[float, None, bool]:
405
+ """
406
+ Logic to run before saving. Eg recalculating height maps or lighting.
407
+ Is a generator yielding progress from 0 to 1 and returning a bool saying if changes have been made.
408
+
409
+ :return: Have any modifications been made.
410
+ """
411
+ return self.level_wrapper.pre_save_operation(self)
412
+
413
+ def save(
414
+ self,
415
+ wrapper: api_wrapper.FormatWrapper = None,
416
+ progress_callback: Callable[[int, int], None] = None,
417
+ ):
418
+ """
419
+ Save the level to the given :class:`FormatWrapper`.
420
+
421
+ :param wrapper: If specified will save the data to this wrapper instead of self.level_wrapper
422
+ :param progress_callback: Optional progress callback to let the calling program know the progress. Input format chunk_index, chunk_count
423
+ :return:
424
+ """
425
+ for chunk_index, chunk_count in self.save_iter(wrapper):
426
+ if progress_callback is not None:
427
+ progress_callback(chunk_index, chunk_count)
428
+
429
+ def save_iter(
430
+ self, wrapper: api_wrapper.FormatWrapper = None
431
+ ) -> Generator[Tuple[int, int], None, None]:
432
+ """
433
+ Save the level to the given :class:`FormatWrapper`.
434
+
435
+ This will yield the progress which can be used to update a UI.
436
+
437
+ :param wrapper: If specified will save the data to this wrapper instead of self.level_wrapper
438
+ :return: A generator of the number of chunks completed and the total number of chunks
439
+ """
440
+ # TODO change the yield type to match OperationReturnType
441
+
442
+ chunk_index = 0
443
+
444
+ changed_chunks = list(self._chunks.changed_chunks())
445
+ chunk_count = len(changed_chunks)
446
+
447
+ if wrapper is None:
448
+ wrapper = self.level_wrapper
449
+
450
+ output_dimension_map = wrapper.dimensions
451
+
452
+ # perhaps make this check if the directory is the same rather than if the class is the same
453
+ save_as = wrapper is not self.level_wrapper
454
+ if save_as:
455
+ # The input wrapper is not the same as the loading wrapper (save-as)
456
+ # iterate through every chunk in the input level and save them to the wrapper
457
+ log.info(
458
+ f"Converting level {self.level_wrapper.path} to level {wrapper.path}"
459
+ )
460
+ wrapper.translation_manager = (
461
+ self.level_wrapper.translation_manager
462
+ ) # TODO: this might cause issues in the future
463
+ for dimension in self.level_wrapper.dimensions:
464
+ chunk_count += len(list(self.level_wrapper.all_chunk_coords(dimension)))
465
+
466
+ for dimension in self.level_wrapper.dimensions:
467
+ try:
468
+ if dimension not in output_dimension_map:
469
+ continue
470
+ for cx, cz in self.level_wrapper.all_chunk_coords(dimension):
471
+ log.info(f"Converting chunk {dimension} {cx}, {cz}")
472
+ try:
473
+ chunk = self.level_wrapper.load_chunk(cx, cz, dimension)
474
+ if chunk.status.as_type(StatusFormats.Java_14) == "full":
475
+ wrapper.commit_chunk(chunk, dimension)
476
+ except ChunkLoadError:
477
+ log.info(f"Error loading chunk {cx} {cz}", exc_info=True)
478
+ chunk_index += 1
479
+ yield chunk_index, chunk_count
480
+ if not chunk_index % 10000:
481
+ wrapper.save()
482
+ self.level_wrapper.unload()
483
+ wrapper.unload()
484
+ except DimensionDoesNotExist:
485
+ continue
486
+
487
+ for dimension, cx, cz in changed_chunks:
488
+ if dimension not in output_dimension_map:
489
+ continue
490
+ try:
491
+ chunk = self.get_chunk(cx, cz, dimension)
492
+ except ChunkDoesNotExist:
493
+ wrapper.delete_chunk(cx, cz, dimension)
494
+ except ChunkLoadError:
495
+ pass
496
+ else:
497
+ wrapper.commit_chunk(chunk, dimension)
498
+ chunk.changed = False
499
+ chunk_index += 1
500
+ yield chunk_index, chunk_count
501
+ if not chunk_index % 10000:
502
+ wrapper.save()
503
+ wrapper.unload()
504
+
505
+ self.history_manager.mark_saved()
506
+ log.info(f"Saving changes to level {wrapper.path}")
507
+ wrapper.save()
508
+ log.info(f"Finished saving changes to level {wrapper.path}")
509
+
510
+ def purge(self):
511
+ """
512
+ Unload all loaded and cached data.
513
+
514
+ This is functionally the same as closing and reopening the world without creating a new class.
515
+ """
516
+ self.unload()
517
+ self.history_manager.purge()
518
+
519
+ def close(self):
520
+ """
521
+ Close the attached level and remove temporary files.
522
+
523
+ Use changed method to check if there are any changes that should be saved before closing.
524
+ """
525
+ self.level_wrapper.close()
526
+ self._history_db.close(compact=False)
527
+
528
+ def unload(self, safe_area: Optional[Tuple[Dimension, int, int, int, int]] = None):
529
+ """
530
+ Unload all chunk data not in the safe area.
531
+
532
+ :param safe_area: The area that should not be unloaded [dimension, min_chunk_x, min_chunk_z, max_chunk_x, max_chunk_z]. If None will unload all chunk data.
533
+ """
534
+ self._chunks.unload(safe_area)
535
+ self.level_wrapper.unload()
536
+
537
+ def unload_unchanged(self):
538
+ """Unload all data that has not been marked as changed."""
539
+ self._chunks.unload_unchanged()
540
+
541
+ @property
542
+ def chunks(self) -> ChunkManager:
543
+ """
544
+ The chunk container.
545
+
546
+ Most methods from :class:`ChunkManager` also exists in the level class.
547
+ """
548
+ return self._chunks
549
+
550
+ def all_chunk_coords(self, dimension: Dimension) -> Set[Tuple[int, int]]:
551
+ """
552
+ The coordinates of every chunk in this dimension of the level.
553
+
554
+ This is the combination of chunks saved to the level and chunks yet to be saved.
555
+ """
556
+ return self._chunks.all_chunk_coords(dimension)
557
+
558
+ def has_chunk(self, cx: int, cz: int, dimension: Dimension) -> bool:
559
+ """
560
+ Does the chunk exist. This is a quick way to check if the chunk exists without loading it.
561
+
562
+ :param cx: The x coordinate of the chunk.
563
+ :param cz: The z coordinate of the chunk.
564
+ :param dimension: The dimension to load the chunk from.
565
+ :return: True if the chunk exists. Calling get_chunk on this chunk may still throw ChunkLoadError
566
+ """
567
+ return self._chunks.has_chunk(dimension, cx, cz)
568
+
569
+ def get_chunk(self, cx: int, cz: int, dimension: Dimension) -> Chunk:
570
+ """
571
+ Gets a :class:`Chunk` class containing the data for the requested chunk.
572
+
573
+ :param cx: The X coordinate of the desired chunk
574
+ :param cz: The Z coordinate of the desired chunk
575
+ :param dimension: The dimension to get the chunk from
576
+ :return: A Chunk object containing the data for the chunk
577
+ :raises:
578
+ :class:`~amulet.api.errors.ChunkDoesNotExist`: If the chunk does not exist (was deleted or never created)
579
+
580
+ :class:`~amulet.api.errors.ChunkLoadError`: If the chunk was not able to be loaded. Eg. If the chunk is corrupt or some error occurred when loading.
581
+ """
582
+ return self._chunks.get_chunk(dimension, cx, cz)
583
+
584
+ def create_chunk(self, cx: int, cz: int, dimension: Dimension) -> Chunk:
585
+ """
586
+ Create an empty chunk and put it at the given location.
587
+
588
+ If a chunk exists at the given location it will be overwritten.
589
+
590
+ :param cx: The X coordinate of the chunk
591
+ :param cz: The Z coordinate of the chunk
592
+ :param dimension: The dimension to put the chunk in.
593
+ :return: The newly created :class:`Chunk`.
594
+ """
595
+ chunk = Chunk(cx, cz)
596
+ self.put_chunk(chunk, dimension)
597
+ return chunk
598
+
599
+ def put_chunk(self, chunk: Chunk, dimension: Dimension):
600
+ """
601
+ Add a given chunk to the level.
602
+
603
+ :param chunk: The :class:`Chunk` to add to the level. It will be added at the location stored in :attr:`Chunk.coordinates`
604
+ :param dimension: The dimension to add the chunk to.
605
+ """
606
+ self._chunks.put_chunk(chunk, dimension)
607
+
608
+ def delete_chunk(self, cx: int, cz: int, dimension: Dimension):
609
+ """
610
+ Delete a chunk from the level.
611
+
612
+ :param cx: The X coordinate of the chunk
613
+ :param cz: The Z coordinate of the chunk
614
+ :param dimension: The dimension to delete the chunk from.
615
+ """
616
+ self._chunks.delete_chunk(dimension, cx, cz)
617
+
618
+ def extract_structure(
619
+ self, selection: SelectionGroup, dimension: Dimension
620
+ ) -> api_level.ImmutableStructure:
621
+ """
622
+ Extract the region of the dimension specified by ``selection`` to an :class:`~api_level.ImmutableStructure` class.
623
+
624
+ :param selection: The selection to extract.
625
+ :param dimension: The dimension to extract the selection from.
626
+ :return: The :class:`~api_level.ImmutableStructure` containing the extracted region.
627
+ """
628
+ return api_level.ImmutableStructure.from_level(self, selection, dimension)
629
+
630
+ def extract_structure_iter(
631
+ self, selection: SelectionGroup, dimension: Dimension
632
+ ) -> Generator[float, None, api_level.ImmutableStructure]:
633
+ """
634
+ Extract the region of the dimension specified by ``selection`` to an :class:`~api_level.ImmutableStructure` class.
635
+
636
+ Also yields the progress as a float from 0-1
637
+
638
+ :param selection: The selection to extract.
639
+ :param dimension: The dimension to extract the selection from.
640
+ :return: The :class:`~api_level.ImmutableStructure` containing the extracted region.
641
+ """
642
+ immutable_level = yield from api_level.ImmutableStructure.from_level_iter(
643
+ self, selection, dimension
644
+ )
645
+ return immutable_level
646
+
647
+ def paste(
648
+ self,
649
+ src_structure: "BaseLevel",
650
+ src_dimension: Dimension,
651
+ src_selection: SelectionGroup,
652
+ dst_dimension: Dimension,
653
+ location: BlockCoordinates,
654
+ scale: FloatTriplet = (1.0, 1.0, 1.0),
655
+ rotation: FloatTriplet = (0.0, 0.0, 0.0),
656
+ include_blocks: bool = True,
657
+ include_entities: bool = True,
658
+ skip_blocks: Tuple[Block, ...] = (),
659
+ copy_chunk_not_exist: bool = False,
660
+ ):
661
+ """Paste a level into this level at the given location.
662
+ Note this command may change in the future.
663
+
664
+ :param src_structure: The structure to paste into this structure.
665
+ :param src_dimension: The dimension of the source structure to copy from.
666
+ :param src_selection: The selection to copy from the source structure.
667
+ :param dst_dimension: The dimension to paste the structure into.
668
+ :param location: The location where the centre of the structure will be in the level
669
+ :param scale: The scale in the x, y and z axis. These can be negative to mirror.
670
+ :param rotation: The rotation in degrees around each of the axis.
671
+ :param include_blocks: Include blocks when pasting the structure.
672
+ :param include_entities: Include entities when pasting the structure.
673
+ :param skip_blocks: If a block matches a block in this list it will not be copied.
674
+ :param copy_chunk_not_exist: If a chunk does not exist in the source should it be copied over as air. Always False where level is a World.
675
+ :return:
676
+ """
677
+ return generator_unpacker(
678
+ self.paste_iter(
679
+ src_structure,
680
+ src_dimension,
681
+ src_selection,
682
+ dst_dimension,
683
+ location,
684
+ scale,
685
+ rotation,
686
+ include_blocks,
687
+ include_entities,
688
+ skip_blocks,
689
+ copy_chunk_not_exist,
690
+ )
691
+ )
692
+
693
+ def paste_iter(
694
+ self,
695
+ src_structure: "BaseLevel",
696
+ src_dimension: Dimension,
697
+ src_selection: SelectionGroup,
698
+ dst_dimension: Dimension,
699
+ location: BlockCoordinates,
700
+ scale: FloatTriplet = (1.0, 1.0, 1.0),
701
+ rotation: FloatTriplet = (0.0, 0.0, 0.0),
702
+ include_blocks: bool = True,
703
+ include_entities: bool = True,
704
+ skip_blocks: Tuple[Block, ...] = (),
705
+ copy_chunk_not_exist: bool = False,
706
+ ) -> Generator[float, None, None]:
707
+ """Paste a structure into this structure at the given location.
708
+ Note this command may change in the future.
709
+
710
+ :param src_structure: The structure to paste into this structure.
711
+ :param src_dimension: The dimension of the source structure to copy from.
712
+ :param src_selection: The selection to copy from the source structure.
713
+ :param dst_dimension: The dimension to paste the structure into.
714
+ :param location: The location where the centre of the structure will be in the level
715
+ :param scale: The scale in the x, y and z axis. These can be negative to mirror.
716
+ :param rotation: The rotation in degrees around each of the axis.
717
+ :param include_blocks: Include blocks when pasting the structure.
718
+ :param include_entities: Include entities when pasting the structure.
719
+ :param skip_blocks: If a block matches a block in this list it will not be copied.
720
+ :param copy_chunk_not_exist: If a chunk does not exist in the source should it be copied over as air. Always False where level is a World.
721
+ :return: A generator of floats from 0 to 1 with the progress of the paste operation.
722
+ """
723
+ yield from clone(
724
+ src_structure,
725
+ src_dimension,
726
+ src_selection,
727
+ self,
728
+ dst_dimension,
729
+ self.bounds(dst_dimension),
730
+ location,
731
+ scale,
732
+ rotation,
733
+ include_blocks,
734
+ include_entities,
735
+ skip_blocks,
736
+ copy_chunk_not_exist,
737
+ )
738
+
739
+ def get_version_block(
740
+ self,
741
+ x: int,
742
+ y: int,
743
+ z: int,
744
+ dimension: Dimension,
745
+ version: VersionIdentifierType,
746
+ ) -> Union[Tuple[Block, BlockEntity], Tuple[Entity, None]]:
747
+ """
748
+ Get a block at the specified location and convert it to the format of the version specified
749
+
750
+ Note the odd return format. In most cases this will return (Block, None) or (Block, BlockEntity) if a block entity is present.
751
+
752
+ In select cases (like item frames) it may return (Entity, None)
753
+
754
+ :param x: The X coordinate of the desired block
755
+ :param y: The Y coordinate of the desired block
756
+ :param z: The Z coordinate of the desired block
757
+ :param dimension: The dimension of the desired block
758
+ :param version: The version to get the block converted to.
759
+
760
+ >>> ("java", (1, 16, 2)) # Java 1.16.2 format
761
+ >>> ("java", 2578) # Java 1.16.2 format (using the data version)
762
+ >>> ("bedrock", (1, 16, 210)) # Bedrock 1.16.210 format
763
+ :return: The block at the given location converted to the `version` format. Note the odd return format.
764
+ :raises:
765
+ :class:`~amulet.api.errors.ChunkDoesNotExist`: If the chunk does not exist (was deleted or never created)
766
+
767
+ :class:`~amulet.api.errors.ChunkLoadError`: If the chunk was not able to be loaded. Eg. If the chunk is corrupt or some error occurred when loading.
768
+ """
769
+ cx, cz = block_coords_to_chunk_coords(x, z, sub_chunk_size=self.sub_chunk_size)
770
+ chunk = self.get_chunk(cx, cz, dimension)
771
+ offset_x, offset_z = x - 16 * cx, z - 16 * cz
772
+
773
+ translator = self.translation_manager.get_version(*version).block
774
+
775
+ src_blocks = chunk.get_block(offset_x, y, offset_z).block_tuple
776
+ src_block_entity = chunk.block_entities.get((x, y, z))
777
+
778
+ output, extra_output, _ = translator.from_universal(
779
+ src_blocks[0], src_block_entity
780
+ )
781
+ if isinstance(output, Block):
782
+ for src_block in src_blocks[1:]:
783
+ converted_sub_block = translator.from_universal(src_block)[0]
784
+ if isinstance(converted_sub_block, Block):
785
+ output += converted_sub_block
786
+ return output, extra_output
787
+
788
+ def set_version_block(
789
+ self,
790
+ x: int,
791
+ y: int,
792
+ z: int,
793
+ dimension: Dimension,
794
+ version: VersionIdentifierType,
795
+ block: Block,
796
+ block_entity: BlockEntity = None,
797
+ ):
798
+ """
799
+ Convert the block and block_entity from the given version format to the universal format and set at the location.
800
+
801
+ :param x: The X coordinate of the desired block.
802
+ :param y: The Y coordinate of the desired block.
803
+ :param z: The Z coordinate of the desired block.
804
+ :param dimension: The dimension of the desired block.
805
+ :param version: The version the given ``block`` and ``block_entity`` come from.
806
+
807
+ >>> ("java", (1, 16, 2)) # Java 1.16.2 format
808
+ >>> ("java", 2578) # Java 1.16.2 format (using the data version)
809
+ >>> ("bedrock", (1, 16, 210)) # Bedrock 1.16.210 format
810
+ :param block: The block to set. Must be valid in the specified version.
811
+ :param block_entity: The block entity to set. Must be valid in the specified version.
812
+ :return: The block at the given location converted to the `version` format. Note the odd return format.
813
+ :raises:
814
+ ChunkLoadError: If the chunk was not able to be loaded. Eg. If the chunk is corrupt or some error occurred when loading.
815
+ """
816
+ cx, cz = block_coords_to_chunk_coords(x, z, sub_chunk_size=self.sub_chunk_size)
817
+ try:
818
+ chunk = self.get_chunk(cx, cz, dimension)
819
+ except ChunkDoesNotExist:
820
+ chunk = self.create_chunk(cx, cz, dimension)
821
+ offset_x, offset_z = x - 16 * cx, z - 16 * cz
822
+
823
+ translator = self.translation_manager.get_version(*version).block
824
+ src_blocks = block.block_tuple
825
+
826
+ universal_block, universal_block_entity, _ = translator.to_universal(
827
+ src_blocks[0], block_entity
828
+ )
829
+ for src_block in src_blocks[1:]:
830
+ universal_block += translator.to_universal(src_block)[0]
831
+ chunk.set_block(offset_x, y, offset_z, universal_block),
832
+ if isinstance(universal_block_entity, BlockEntity):
833
+ chunk.block_entities[(x, y, z)] = universal_block_entity
834
+ elif (x, y, z) in chunk.block_entities:
835
+ del chunk.block_entities[(x, y, z)]
836
+ chunk.changed = True
837
+
838
+ def get_native_entities(
839
+ self, cx: int, cz: int, dimension: Dimension
840
+ ) -> Tuple[EntityList, VersionIdentifierType]:
841
+ """
842
+ Get a list of entities in the native format from a given chunk.
843
+ This currently returns the raw data from the chunk but in the future will convert to the world version format.
844
+
845
+ :param cx: The chunk x position
846
+ :param cz: The chunk z position
847
+ :param dimension: The dimension of the chunk.
848
+ :return: A copy of the list of entities and the version format they are in.
849
+ """
850
+ chunk = self.get_chunk(cx, cz, dimension)
851
+ # To make this forwards compatible this needs to be deep copied
852
+ return copy.deepcopy(chunk._native_entities), chunk._native_version
853
+
854
+ def set_native_entites(
855
+ self, cx: int, cz: int, dimension: Dimension, entities: Iterable[Entity]
856
+ ):
857
+ """
858
+ Set the entities in the native format.
859
+ Note that the format must be compatible with `level_wrapper.max_world_version`.
860
+
861
+ :param cx: The chunk x position
862
+ :param cz: The chunk z position
863
+ :param dimension: The dimension of the chunk.
864
+ :param entities: The entities to set on the chunk.
865
+ """
866
+ chunk = self.get_chunk(cx, cz, dimension)
867
+ chunk._native_entities = EntityList(copy.deepcopy(entities))
868
+ chunk._native_version = self.level_wrapper.max_world_version
869
+ chunk.changed = True
870
+
871
+ # def get_entities_in_box(
872
+ # self, box: "SelectionGroup"
873
+ # ) -> Generator[Tuple[Coordinates, List[object]], None, None]:
874
+ # # TODO: some of this logic can probably be moved the chunk class and have this method call that
875
+ # # TODO: update this to use the newer entity API
876
+ # out_of_place_entities = []
877
+ # entity_map: Dict[Tuple[int, int], List[List[object]]] = {}
878
+ # for chunk, subbox in self.get_chunk_boxes(box):
879
+ # entities = chunk.entities
880
+ # in_box = list(filter(lambda e: e.location in subbox, entities))
881
+ # not_in_box = filter(lambda e: e.location not in subbox, entities)
882
+ #
883
+ # in_box_copy = deepcopy(in_box)
884
+ #
885
+ # entity_map[chunk.coordinates] = [
886
+ # not_in_box,
887
+ # in_box,
888
+ # ] # First index is the list of entities not in the box, the second is for ones that are
889
+ #
890
+ # yield chunk.coordinates, in_box_copy
891
+ #
892
+ # if (
893
+ # in_box != in_box_copy
894
+ # ): # If an entity has been changed, update the dictionary entry
895
+ # entity_map[chunk.coordinates][1] = in_box_copy
896
+ # else: # Delete the entry otherwise
897
+ # del entity_map[chunk.coordinates]
898
+ #
899
+ # for chunk_coords, entity_list_list in entity_map.items():
900
+ # chunk = self.get_chunk(*chunk_coords)
901
+ # in_place_entities = list(
902
+ # filter(
903
+ # lambda e: chunk_coords
904
+ # == entity_position_to_chunk_coordinates(e.location),
905
+ # entity_list_list[1],
906
+ # )
907
+ # )
908
+ # out_of_place = filter(
909
+ # lambda e: chunk_coords
910
+ # != entity_position_to_chunk_coordinates(e.location),
911
+ # entity_list_list[1],
912
+ # )
913
+ #
914
+ # chunk.entities = in_place_entities + list(entity_list_list[0])
915
+ #
916
+ # if out_of_place:
917
+ # out_of_place_entities.extend(out_of_place)
918
+ #
919
+ # if out_of_place_entities:
920
+ # self.add_entities(out_of_place_entities)
921
+ #
922
+ # def add_entities(self, entities):
923
+ # proper_entity_chunks = map(
924
+ # lambda e: (entity_position_to_chunk_coordinates(e.location), e,), entities,
925
+ # )
926
+ # accumulated_entities: Dict[Tuple[int, int], List[object]] = {}
927
+ #
928
+ # for chunk_coord, ent in proper_entity_chunks:
929
+ # if chunk_coord in accumulated_entities:
930
+ # accumulated_entities[chunk_coord].append(ent)
931
+ # else:
932
+ # accumulated_entities[chunk_coord] = [ent]
933
+ #
934
+ # for chunk_coord, ents in accumulated_entities.items():
935
+ # chunk = self.get_chunk(*chunk_coord)
936
+ #
937
+ # chunk.entities += ents
938
+ #
939
+ # def delete_entities(self, entities):
940
+ # chunk_entity_pairs = map(
941
+ # lambda e: (entity_position_to_chunk_coordinates(e.location), e,), entities,
942
+ # )
943
+ #
944
+ # for chunk_coord, ent in chunk_entity_pairs:
945
+ # chunk = self.get_chunk(*chunk_coord)
946
+ # entities = chunk.entities
947
+ # entities.remove(ent)
948
+ # chunk.entities = entities
949
+
950
+ @property
951
+ def history_manager(self) -> MetaHistoryManager:
952
+ """The class that manages undoing and redoing changes."""
953
+ return self._history_manager
954
+
955
+ def create_undo_point(self, world=True, non_world=True) -> bool:
956
+ """
957
+ Create a restore point for all the data that has changed.
958
+
959
+ :param world: If True the restore point will include world based data.
960
+ :param non_world: If True the restore point will include data not related to the world.
961
+ :return: If True a restore point was created. If nothing changed no restore point will be created.
962
+ """
963
+ return self.history_manager.create_undo_point(world, non_world)
964
+
965
+ def create_undo_point_iter(
966
+ self, world=True, non_world=True
967
+ ) -> Generator[float, None, bool]:
968
+ """
969
+ Create a restore point for all the data that has changed.
970
+
971
+ Also yields progress from 0-1
972
+
973
+ :param world: If True the restore point will include world based data.
974
+ :param non_world: If True the restore point will include data not related to the world.
975
+ :return: If True a restore point was created. If nothing changed no restore point will be created.
976
+ """
977
+ return self.history_manager.create_undo_point_iter(world, non_world)
978
+
979
+ @property
980
+ def changed(self) -> bool:
981
+ """Has any data been modified but not saved to disk"""
982
+ return self.history_manager.changed or self.level_wrapper.changed
983
+
984
+ def undo(self):
985
+ """Undoes the last set of changes to the level."""
986
+ self.history_manager.undo()
987
+
988
+ def redo(self):
989
+ """Redoes the last set of changes to the level."""
990
+ self.history_manager.redo()
991
+
992
+ def restore_last_undo_point(self):
993
+ """
994
+ Restore the level to the state it was when self.create_undo_point was last called.
995
+
996
+ If an operation errors there may be modifications made that did not get tracked.
997
+
998
+ This will revert those changes.
999
+ """
1000
+ self.history_manager.restore_last_undo_point()
1001
+
1002
+ @property
1003
+ def players(self) -> PlayerManager:
1004
+ """
1005
+ The player container.
1006
+
1007
+ Most methods from :class:`PlayerManager` also exists in the level class.
1008
+ """
1009
+ return self._players
1010
+
1011
+ def all_player_ids(self) -> Set[str]:
1012
+ """
1013
+ Returns a set of all player ids that are present in the level.
1014
+ """
1015
+ return self.players.all_player_ids()
1016
+
1017
+ def has_player(self, player_id: str) -> bool:
1018
+ """
1019
+ Is the given player id present in the level
1020
+
1021
+ :param player_id: The player id to check
1022
+ :return: True if the player id is present, False otherwise
1023
+ """
1024
+ return self.players.has_player(player_id)
1025
+
1026
+ def get_player(self, player_id: str) -> Player:
1027
+ """
1028
+ Gets the :class:`Player` object that belongs to the specified player id
1029
+
1030
+ If no parameter is supplied, the data of the local player will be returned
1031
+
1032
+ :param player_id: The desired player id
1033
+ :return: A Player instance
1034
+ """
1035
+ return self.players.get_player(player_id)