amulet-core 1.9.18__py3-none-any.whl → 1.9.20__py3-none-any.whl

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

Potentially problematic release.


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

amulet/_version.py CHANGED
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2023-08-16T10:37:55+0100",
11
+ "date": "2023-09-29T11:29:40+0100",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "dafef97fe4fd1f2f713ef1e3503d6b13b20c1c1f",
15
- "version": "1.9.18"
14
+ "full-revisionid": "b673f749bf1c348e31bf0594e71ffa30f6ea8519",
15
+ "version": "1.9.20"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -770,11 +770,19 @@ class BaseLevel:
770
770
  chunk = self.get_chunk(cx, cz, dimension)
771
771
  offset_x, offset_z = x - 16 * cx, z - 16 * cz
772
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))
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
777
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
778
786
  return output, extra_output
779
787
 
780
788
  def set_version_block(
@@ -812,13 +820,14 @@ class BaseLevel:
812
820
  chunk = self.create_chunk(cx, cz, dimension)
813
821
  offset_x, offset_z = x - 16 * cx, z - 16 * cz
814
822
 
815
- (
816
- universal_block,
817
- universal_block_entity,
818
- _,
819
- ) = self.translation_manager.get_version(*version).block.to_universal(
820
- block, block_entity
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
821
828
  )
829
+ for src_block in src_blocks[1:]:
830
+ universal_block += translator.to_universal(src_block)[0]
822
831
  chunk.set_block(offset_x, y, offset_z, universal_block),
823
832
  if isinstance(universal_block_entity, BlockEntity):
824
833
  chunk.block_entities[(x, y, z)] = universal_block_entity
@@ -141,9 +141,9 @@ class SectorManager:
141
141
 
142
142
  @property
143
143
  def sectors(self) -> List[Sector]:
144
- """A list of reserved sectors"""
144
+ """A list of reserved sectors. Ordered by their start location."""
145
145
  with self._lock:
146
- return list(self._reserved)
146
+ return list(sorted(self._reserved, key=lambda i: i.start))
147
147
 
148
148
  def reserve_space(self, length: int) -> Sector:
149
149
  """
@@ -170,6 +170,7 @@ class SectorManager:
170
170
  self._free_start[start_index] = free_sector
171
171
  else:
172
172
  del self._free_start[start_index]
173
+ self._reserved.add(sector)
173
174
  return sector
174
175
  elif self._resizable:
175
176
  sector = Sector(self._stop, self._stop + length)
@@ -261,6 +262,8 @@ class SectorManager:
261
262
  :param sector: The sector to free
262
263
  """
263
264
  with self._lock:
265
+ if sector not in self._reserved:
266
+ raise ValueError("Sector was not reserved")
264
267
  # remove the sector from the reserved storage
265
268
  self._reserved.remove(sector)
266
269
 
@@ -286,99 +289,3 @@ class SectorManager:
286
289
 
287
290
  self._free_start.insert(index, sector)
288
291
  self._add_size_sector(sector)
289
-
290
-
291
- def validate(m):
292
- assert set(m._free_start) == set(m._free_size)
293
- free = sorted(list(m._free_start) + list(m._reserved), key=lambda k: k.start)
294
- if free:
295
- for i in range(len(free) - 1):
296
- assert free[i].stop == free[i + 1].start
297
- assert free[-1].stop == m._stop
298
-
299
-
300
- def test1():
301
- m = SectorManager(2, 3)
302
- validate(m)
303
- m.reserve(Sector(2, 3))
304
- validate(m)
305
- m.reserve(Sector(3, 4))
306
- validate(m)
307
- print(m.sectors)
308
-
309
-
310
- def test2():
311
- m = SectorManager(2, 102)
312
-
313
- m.reserve(Sector(5, 6))
314
- validate(m)
315
- m.reserve(Sector(6, 7))
316
- validate(m)
317
- # m.reserve(Sector(7, 8))
318
- m.reserve(Sector(8, 9))
319
- validate(m)
320
-
321
- try:
322
- m.reserve(Sector(6, 8))
323
- except NoValidSector:
324
- pass
325
- else:
326
- raise Exception
327
-
328
- validate(m)
329
-
330
- try:
331
- m.reserve(Sector(7, 9))
332
- except NoValidSector:
333
- pass
334
- else:
335
- raise Exception
336
-
337
- validate(m)
338
- m.free(Sector(5, 6))
339
- validate(m)
340
- m.free(Sector(6, 7))
341
- validate(m)
342
- m.free(Sector(8, 9))
343
- validate(m)
344
-
345
- m.reserve(Sector(6, 8))
346
- validate(m)
347
- m.free(Sector(6, 8))
348
- validate(m)
349
-
350
- m.reserve(Sector(7, 9))
351
- validate(m)
352
- m.free(Sector(7, 9))
353
-
354
- validate(m)
355
-
356
- assert len(m._free_start) == 1
357
-
358
-
359
- # def test3():
360
- # reserve a number of single length units
361
- # for _ in range(20):
362
- # i = free_indexes.pop(random.randrange(len(free_indexes)))
363
- # m.reserve(Sector(i, i+1))
364
- # validate(m)
365
- # reserved_indexes.append(i)
366
- #
367
- # for _ in range(20):
368
- # index = random.randrange(len(free_indexes))
369
- # di = 1
370
- # i = free_indexes.pop(index)
371
- #
372
- # for _ in range(10_000):
373
- # m.reserve(Sector(i, i+1))
374
- # validate(m)
375
- # print(m.sectors)
376
-
377
-
378
- def test():
379
- test1()
380
- test2()
381
-
382
-
383
- if __name__ == "__main__":
384
- test()
@@ -231,93 +231,108 @@ class AnvilFormat(WorldFormatWrapper[VersionNumberInt]):
231
231
  layers=("region",) + ("entities",) * (self.version >= 2681),
232
232
  )
233
233
  self._dimension_name_map[dimension_name] = relative_dimension_path
234
- bounds = DefaultSelection
235
- if self.version >= 2709: # This number might be smaller
236
- dimension_settings = (
237
- self.root_tag.compound.get_compound("Data", CompoundTag())
238
- .get_compound("WorldGenSettings", CompoundTag())
239
- .get_compound("dimensions", CompoundTag())
240
- .get_compound(dimension_name, CompoundTag())
241
- )
242
- dimension_tag = dimension_settings.get("type")
243
- if isinstance(dimension_tag, StringTag):
244
- # the settings are in the data pack
245
- dimension_name = dimension_tag.py_str
246
- if ":" in dimension_name:
247
- namespace, base_name = dimension_name.split(":", 1)
248
- if (
249
- self.version >= 2834
250
- and namespace == "minecraft"
251
- and base_name == "overworld"
252
- ):
253
- bounds = SelectionGroup(
234
+ self._bounds[dimension_name] = self._get_dimenion_bounds(dimension_name)
235
+
236
+ def _get_dimenion_bounds(self, dimension_type_str: Dimension) -> SelectionGroup:
237
+ if self.version >= 2709: # This number might be smaller
238
+ # If in a version that supports custom height data packs
239
+ dimension_settings = (
240
+ self.root_tag.compound.get_compound("Data", CompoundTag())
241
+ .get_compound("WorldGenSettings", CompoundTag())
242
+ .get_compound("dimensions", CompoundTag())
243
+ .get_compound(dimension_type_str, CompoundTag())
244
+ )
245
+
246
+ # "type" can be a reference (string) or inline (compound) dimension-type data.
247
+ dimension_type = dimension_settings.get("type")
248
+
249
+ if isinstance(dimension_type, StringTag):
250
+ # Reference type. Load the dimension data
251
+ dimension_type_str = dimension_type.py_str
252
+ if ":" in dimension_type_str:
253
+ namespace, base_name = dimension_type_str.split(":", 1)
254
+ else:
255
+ namespace = "minecraft"
256
+ base_name = dimension_type_str
257
+ name_tuple = namespace, base_name
258
+
259
+ # First try and load the reference from the data pack and then from defaults
260
+ dimension_path = f"data/{namespace}/dimension_type/{base_name}.json"
261
+ if self.data_pack.has_file(dimension_path):
262
+ with self.data_pack.open(dimension_path) as d:
263
+ try:
264
+ dimension_settings_json = json.load(d)
265
+ except json.JSONDecodeError:
266
+ pass
267
+ else:
268
+ if "min_y" in dimension_settings_json and isinstance(
269
+ dimension_settings_json["min_y"], int
270
+ ):
271
+ min_y = dimension_settings_json["min_y"]
272
+ if min_y % 16:
273
+ min_y = 16 * (min_y // 16)
274
+ else:
275
+ min_y = 0
276
+ if "height" in dimension_settings_json and isinstance(
277
+ dimension_settings_json["height"], int
278
+ ):
279
+ height = dimension_settings_json["height"]
280
+ if height % 16:
281
+ height = -16 * (-height // 16)
282
+ else:
283
+ height = 256
284
+
285
+ return SelectionGroup(
254
286
  SelectionBox(
255
- (-30_000_000, -64, -30_000_000),
256
- (30_000_000, 320, 30_000_000),
287
+ (-30_000_000, min_y, -30_000_000),
288
+ (30_000_000, min_y + height, 30_000_000),
257
289
  )
258
290
  )
259
- dimension_path = (
260
- f"data/{namespace}/dimension_type/{base_name}.json"
261
- )
262
- if self.data_pack.has_file(dimension_path):
263
- with self.data_pack.open(dimension_path) as d:
264
- try:
265
- dimension_settings_json = json.load(d)
266
- except json.JSONDecodeError:
267
- pass
268
- else:
269
- if (
270
- "min_y" in dimension_settings_json
271
- and isinstance(
272
- dimension_settings_json["min_y"], int
273
- )
274
- ):
275
- min_y = dimension_settings_json["min_y"]
276
- if min_y % 16:
277
- min_y = 16 * (min_y // 16)
278
- else:
279
- min_y = 0
280
- if (
281
- "height" in dimension_settings_json
282
- and isinstance(
283
- dimension_settings_json["height"], int
284
- )
285
- ):
286
- height = dimension_settings_json["height"]
287
- if height % 16:
288
- height = -16 * (-height // 16)
289
- else:
290
- height = 256
291
-
292
- bounds = SelectionGroup(
293
- SelectionBox(
294
- (-30_000_000, min_y, -30_000_000),
295
- (30_000_000, min_y + height, 30_000_000),
296
- )
297
- )
298
-
299
- elif isinstance(dimension_tag, CompoundTag):
300
- # the settings are here
301
- dimension_compound_tag = dimension_tag
302
- min_y = (
303
- dimension_compound_tag.get_int("min_y", IntTag()).py_int // 16
304
- ) * 16
305
- height = (
306
- -dimension_compound_tag.get_int("height", IntTag(256)).py_int
307
- // 16
308
- ) * -16
309
- bounds = SelectionGroup(
310
- SelectionBox(
311
- (-30_000_000, min_y, -30_000_000),
312
- (30_000_000, min_y + height, 30_000_000),
291
+
292
+ elif name_tuple in {
293
+ ("minecraft", "overworld"),
294
+ ("minecraft", "overworld_caves"),
295
+ }:
296
+ if self.version >= 2825:
297
+ # If newer than the height change version
298
+ return SelectionGroup(
299
+ SelectionBox(
300
+ (-30_000_000, -64, -30_000_000),
301
+ (30_000_000, 320, 30_000_000),
302
+ )
313
303
  )
314
- )
304
+ else:
305
+ return DefaultSelection
306
+ elif name_tuple in {
307
+ ("minecraft", "the_nether"),
308
+ ("minecraft", "the_end"),
309
+ }:
310
+ return DefaultSelection
315
311
  else:
316
- log.error(
317
- f"Expected dimension_tag to be a StringTag or CompoundTag. Got {repr(dimension_tag)} for dimension {dimension_name}"
312
+ log.error(f"Could not find dimension_type {':'.join(name_tuple)}")
313
+
314
+ elif isinstance(dimension_type, CompoundTag):
315
+ # Inline type
316
+ dimension_type_compound = dimension_type
317
+ min_y = (
318
+ dimension_type_compound.get_int("min_y", IntTag()).py_int // 16
319
+ ) * 16
320
+ height = (
321
+ -dimension_type_compound.get_int("height", IntTag(256)).py_int // 16
322
+ ) * -16
323
+ return SelectionGroup(
324
+ SelectionBox(
325
+ (-30_000_000, min_y, -30_000_000),
326
+ (30_000_000, min_y + height, 30_000_000),
318
327
  )
328
+ )
329
+ else:
330
+ log.error(
331
+ f'level_dat["Data"]["WorldGenSettings"]["dimensions"]["{dimension_type_str}"]["type"] was not a StringTag or CompoundTag.'
332
+ )
319
333
 
320
- self._bounds[dimension_name] = bounds
334
+ # Return the default if nothing else returned
335
+ return DefaultSelection
321
336
 
322
337
  def _get_interface(self, raw_chunk_data: Optional[Any] = None) -> "Interface":
323
338
  from amulet.level.loader import Interfaces
@@ -405,9 +420,9 @@ class AnvilFormat(WorldFormatWrapper[VersionNumberInt]):
405
420
  self._register_dimension("DIM-1", THE_NETHER)
406
421
  self._register_dimension("DIM1", THE_END)
407
422
 
408
- for dir_name in os.listdir(self.path):
409
- level_path = os.path.join(self.path, dir_name)
410
- if os.path.isdir(level_path) and dir_name.startswith("DIM"):
423
+ for level_path in glob.glob(os.path.join(glob.escape(self.path), "DIM*")):
424
+ if os.path.isdir(level_path):
425
+ dir_name = os.path.basename(level_path)
411
426
  if AnvilDimensionManager.level_regex.fullmatch(dir_name) is None:
412
427
  continue
413
428
  self._register_dimension(dir_name)
@@ -8,7 +8,6 @@ from typing import (
8
8
  List,
9
9
  TYPE_CHECKING,
10
10
  Tuple,
11
- Union,
12
11
  )
13
12
  from threading import RLock
14
13
  import logging
@@ -25,21 +24,17 @@ from amulet_nbt import (
25
24
  utf8_escape_encoder,
26
25
  )
27
26
 
28
- from amulet.api.errors import ChunkDoesNotExist, DimensionDoesNotExist
27
+ from amulet.api.errors import ChunkDoesNotExist
29
28
  from amulet.api.data_types import ChunkCoordinates
30
29
  from leveldb import LevelDB
31
30
  from .chunk import ChunkData
32
31
 
33
32
  if TYPE_CHECKING:
34
- from amulet.api.data_types import Dimension
35
33
  from .format import LevelDBFormat
36
34
 
37
35
  log = logging.getLogger(__name__)
38
36
 
39
37
  InternalDimension = Optional[int]
40
- OVERWORLD = "minecraft:overworld"
41
- THE_NETHER = "minecraft:the_nether"
42
- THE_END = "minecraft:the_end"
43
38
 
44
39
 
45
40
  class ActorCounter:
@@ -102,12 +97,11 @@ class LevelDBDimensionManager:
102
97
  self._actor_counter = ActorCounter.from_level(level)
103
98
  # self._levels format Dict[level, Dict[Tuple[cx, cz], List[Tuple[full_key, key_extension]]]]
104
99
  self._levels: Dict[InternalDimension, Set[ChunkCoordinates]] = {}
105
- self._dimension_name_map: Dict["Dimension", InternalDimension] = {}
106
100
  self._lock = RLock()
107
101
 
108
- self.register_dimension(None, OVERWORLD)
109
- self.register_dimension(1, THE_NETHER)
110
- self.register_dimension(2, THE_END)
102
+ self.register_dimension(None) # overworld
103
+ self.register_dimension(1) # the nether
104
+ self.register_dimension(2) # the end
111
105
 
112
106
  for key in self._db.keys():
113
107
  if 9 <= len(key) <= 10 and key[8] in [44, 118]: # "," "v"
@@ -117,63 +111,36 @@ class LevelDBDimensionManager:
117
111
  self._add_chunk(key, has_level=True)
118
112
 
119
113
  @property
120
- def dimensions(self) -> List["Dimension"]:
114
+ def dimensions(self) -> List[InternalDimension]:
121
115
  """A list of all the levels contained in the world"""
122
- return list(self._dimension_name_map.keys())
116
+ return list(self._levels)
123
117
 
124
- def register_dimension(
125
- self,
126
- dimension_internal: InternalDimension,
127
- dimension_name: Optional["Dimension"] = None,
128
- ):
118
+ def register_dimension(self, dimension: InternalDimension):
129
119
  """
130
120
  Register a new dimension.
131
121
 
132
- :param dimension_internal: The internal representation of the dimension
133
- :param dimension_name: The name of the dimension shown to the user
122
+ :param dimension: The internal representation of the dimension
134
123
  :return:
135
124
  """
136
- if dimension_name is None:
137
- dimension_name: "Dimension" = f"DIM{dimension_internal}"
138
-
139
125
  with self._lock:
140
- if (
141
- dimension_internal not in self._levels
142
- and dimension_name not in self._dimension_name_map
143
- ):
144
- self._levels[dimension_internal] = set()
145
- self._dimension_name_map[dimension_name] = dimension_internal
146
-
147
- def _get_internal_dimension(self, dimension: "Dimension") -> InternalDimension:
148
- if dimension in self._dimension_name_map:
149
- return self._dimension_name_map[dimension]
150
- else:
151
- raise DimensionDoesNotExist(dimension)
126
+ if dimension not in self._levels:
127
+ self._levels[dimension] = set()
152
128
 
153
- def all_chunk_coords(self, dimension: "Dimension") -> Set[ChunkCoordinates]:
154
- internal_dimension = self._get_internal_dimension(dimension)
155
- if internal_dimension in self._levels:
156
- return self._levels[internal_dimension]
129
+ def all_chunk_coords(self, dimension: InternalDimension) -> Set[ChunkCoordinates]:
130
+ if dimension in self._levels:
131
+ return self._levels[dimension]
157
132
  else:
158
133
  return set()
159
134
 
160
135
  @staticmethod
161
- def _get_key(cx: int, cz: int, internal_dimension: InternalDimension) -> bytes:
162
- if internal_dimension is None:
136
+ def _get_key(cx: int, cz: int, dimension: InternalDimension) -> bytes:
137
+ if dimension is None:
163
138
  return struct.pack("<ii", cx, cz)
164
139
  else:
165
- return struct.pack("<iii", cx, cz, internal_dimension)
166
-
167
- def _has_chunk(
168
- self, cx: int, cz: int, internal_dimension: InternalDimension
169
- ) -> bool:
170
- return (
171
- internal_dimension in self._levels
172
- and (cx, cz) in self._levels[internal_dimension]
173
- )
140
+ return struct.pack("<iii", cx, cz, dimension)
174
141
 
175
- def has_chunk(self, cx: int, cz: int, dimension: Dimension) -> bool:
176
- return self._has_chunk(cx, cz, self._get_internal_dimension(dimension))
142
+ def has_chunk(self, cx: int, cz: int, dimension: InternalDimension) -> bool:
143
+ return dimension in self._levels and (cx, cz) in self._levels[dimension]
177
144
 
178
145
  def _add_chunk(self, key_: bytes, has_level: bool = False):
179
146
  if has_level:
@@ -185,14 +152,15 @@ class LevelDBDimensionManager:
185
152
  self.register_dimension(level)
186
153
  self._levels[level].add((cx, cz))
187
154
 
188
- def get_chunk_data(self, cx: int, cz: int, dimension: "Dimension") -> ChunkData:
155
+ def get_chunk_data(
156
+ self, cx: int, cz: int, dimension: InternalDimension
157
+ ) -> ChunkData:
189
158
  """Get a dictionary of chunk key extension in bytes to the raw data in the key.
190
159
  chunk key extension are the character(s) after <cx><cz>[level] in the key
191
160
  Will raise ChunkDoesNotExist if the chunk does not exist
192
161
  """
193
- internal_dimension = self._get_internal_dimension(dimension)
194
- if self._has_chunk(cx, cz, internal_dimension):
195
- prefix = self._get_key(cx, cz, internal_dimension)
162
+ if self.has_chunk(cx, cz, dimension):
163
+ prefix = self._get_key(cx, cz, dimension)
196
164
  prefix_len = len(prefix)
197
165
  iter_end = prefix + b"\xff\xff\xff\xff"
198
166
 
@@ -288,13 +256,12 @@ class LevelDBDimensionManager:
288
256
  cx: int,
289
257
  cz: int,
290
258
  chunk_data: ChunkData,
291
- dimension: "Dimension",
259
+ dimension: InternalDimension,
292
260
  ):
293
261
  """pass data to the region file class"""
294
262
  # get the region key
295
- internal_dimension = self._get_internal_dimension(dimension)
296
- self._levels[internal_dimension].add((cx, cz))
297
- key_prefix = self._get_key(cx, cz, internal_dimension)
263
+ self._levels[dimension].add((cx, cz))
264
+ key_prefix = self._get_key(cx, cz, dimension)
298
265
 
299
266
  batch = {}
300
267
 
@@ -388,15 +355,14 @@ class LevelDBDimensionManager:
388
355
  if batch:
389
356
  self._db.putBatch(batch)
390
357
 
391
- def delete_chunk(self, cx: int, cz: int, dimension: "Dimension"):
392
- if dimension not in self._dimension_name_map:
358
+ def delete_chunk(self, cx: int, cz: int, dimension: InternalDimension):
359
+ if dimension not in self._levels:
393
360
  return # dimension does not exists so chunk cannot
394
361
 
395
- internal_dimension = self._dimension_name_map[dimension]
396
- if not self._has_chunk(cx, cz, internal_dimension):
362
+ if not self.has_chunk(cx, cz, dimension):
397
363
  return # chunk does not exists
398
364
 
399
- prefix = self._get_key(cx, cz, internal_dimension)
365
+ prefix = self._get_key(cx, cz, dimension)
400
366
  prefix_len = len(prefix)
401
367
  iter_end = prefix + b"\xff\xff\xff\xff"
402
368
  keys = []
@@ -414,6 +380,6 @@ class LevelDBDimensionManager:
414
380
  actor_key = b"actorprefix" + digp[i : i + 8]
415
381
  self._db.delete(actor_key)
416
382
 
417
- self._levels[internal_dimension].remove((cx, cz))
383
+ self._levels[dimension].remove((cx, cz))
418
384
  for key in keys:
419
385
  self._db.delete(key)
@@ -38,22 +38,23 @@ from amulet.api.data_types import (
38
38
  Dimension,
39
39
  AnyNDArray,
40
40
  )
41
- from amulet.api.wrapper import WorldFormatWrapper
42
- from amulet.api.errors import ObjectWriteError, ObjectReadError, PlayerDoesNotExist
41
+ from amulet.api.wrapper import WorldFormatWrapper, DefaultSelection
42
+ from amulet.api.errors import (
43
+ ObjectWriteError,
44
+ ObjectReadError,
45
+ PlayerDoesNotExist,
46
+ ChunkDoesNotExist,
47
+ )
43
48
 
44
49
  from .interface.chunk.leveldb_chunk_versions import (
45
50
  game_to_chunk_version,
46
51
  )
47
- from .dimension import (
48
- LevelDBDimensionManager,
49
- ChunkData,
50
- OVERWORLD,
51
- THE_NETHER,
52
- THE_END,
53
- )
52
+ from .dimension import LevelDBDimensionManager, ChunkData, InternalDimension
54
53
  from .interface.chunk import BaseLevelDBInterface, get_interface
55
54
 
56
- InternalDimension = Optional[int]
55
+ OVERWORLD = "minecraft:overworld"
56
+ THE_NETHER = "minecraft:the_nether"
57
+ THE_END = "minecraft:the_end"
57
58
 
58
59
 
59
60
  class BedrockLevelDAT(NamedTag):
@@ -177,6 +178,7 @@ class LevelDBFormat(WorldFormatWrapper[VersionNumberTuple]):
177
178
  self._root_tag = BedrockLevelDAT(path=dat_path, level_dat_version=9)
178
179
  self._db = None
179
180
  self._dimension_manager = None
181
+ self._dimension_to_internal: dict[Dimension, InternalDimension] = {}
180
182
  self._shallow_load()
181
183
 
182
184
  def _shallow_load(self):
@@ -260,12 +262,12 @@ class LevelDBFormat(WorldFormatWrapper[VersionNumberTuple]):
260
262
  return f"Bedrock Unknown Version"
261
263
 
262
264
  @property
263
- def dimensions(self) -> List["Dimension"]:
265
+ def dimensions(self) -> List[Dimension]:
264
266
  self._verify_has_lock()
265
- return self._dimension_manager.dimensions
267
+ return list(self._dimension_to_internal)
266
268
 
267
269
  # def register_dimension(
268
- # self, dimension_internal: int, dimension_name: Optional["Dimension"] = None
270
+ # self, dimension_internal: int, dimension_name: Optional[Dimension] = None
269
271
  # ):
270
272
  # """
271
273
  # Register a new dimension.
@@ -348,6 +350,12 @@ class LevelDBFormat(WorldFormatWrapper[VersionNumberTuple]):
348
350
  self._dimension_manager = LevelDBDimensionManager(self)
349
351
  self._is_open = True
350
352
  self._has_lock = True
353
+
354
+ self._dimension_to_internal.clear()
355
+ self._dimension_to_internal[OVERWORLD] = None
356
+ self._dimension_to_internal[THE_NETHER] = 1
357
+ self._dimension_to_internal[THE_END] = 2
358
+
351
359
  experiments = self.root_tag.compound.get_compound(
352
360
  "experiments", CompoundTag()
353
361
  )
@@ -362,21 +370,14 @@ class LevelDBFormat(WorldFormatWrapper[VersionNumberTuple]):
362
370
  )
363
371
  )
364
372
  else:
365
- self._bounds[OVERWORLD] = SelectionGroup(
366
- SelectionBox(
367
- (-30_000_000, 0, -30_000_000), (30_000_000, 256, 30_000_000)
368
- )
369
- )
373
+ self._bounds[OVERWORLD] = DefaultSelection
370
374
  self._bounds[THE_NETHER] = SelectionGroup(
371
375
  SelectionBox(
372
376
  (-30_000_000, 0, -30_000_000), (30_000_000, 128, 30_000_000)
373
377
  )
374
378
  )
375
- self._bounds[THE_END] = SelectionGroup(
376
- SelectionBox(
377
- (-30_000_000, 0, -30_000_000), (30_000_000, 256, 30_000_000)
378
- )
379
- )
379
+ self._bounds[THE_END] = DefaultSelection
380
+
380
381
  if b"LevelChunkMetaDataDictionary" in self.level_db:
381
382
  data = self.level_db[b"LevelChunkMetaDataDictionary"]
382
383
  count, data = struct.unpack("<I", data[:4])[0], data[4:]
@@ -396,9 +397,9 @@ class LevelDBFormat(WorldFormatWrapper[VersionNumberTuple]):
396
397
  dimension_name = value.get_string("DimensionName").py_str
397
398
  # The dimension names are stored differently TODO: split local and global names
398
399
  dimension_name = {
399
- "Overworld": "minecraft:overworld",
400
- "Nether": "minecraft:the_nether",
401
- "TheEnd": "minecraft:the_end",
400
+ "Overworld": OVERWORLD,
401
+ "Nether": THE_NETHER,
402
+ "TheEnd": THE_END,
402
403
  }.get(dimension_name, dimension_name)
403
404
 
404
405
  except KeyError:
@@ -407,13 +408,7 @@ class LevelDBFormat(WorldFormatWrapper[VersionNumberTuple]):
407
408
  pass
408
409
  else:
409
410
  previous_bounds = self._bounds.get(
410
- dimension_name,
411
- SelectionGroup(
412
- SelectionBox(
413
- (-30_000_000, 0, -30_000_000),
414
- (30_000_000, 256, 30_000_000),
415
- )
416
- ),
411
+ dimension_name, DefaultSelection
417
412
  )
418
413
  min_y = min(
419
414
  value.get_compound(
@@ -447,6 +442,15 @@ class LevelDBFormat(WorldFormatWrapper[VersionNumberTuple]):
447
442
  (previous_bounds.max_x, max_y, previous_bounds.max_z),
448
443
  )
449
444
  )
445
+
446
+ # Give all other dimensions found an entry
447
+ known_dimensions = set(self._dimension_to_internal.values())
448
+ for internal_dimension in self._dimension_manager.dimensions:
449
+ if internal_dimension not in known_dimensions:
450
+ dimension_name = f"DIM{internal_dimension}"
451
+ self._dimension_to_internal[dimension_name] = internal_dimension
452
+ self._bounds[dimension_name] = DefaultSelection
453
+
450
454
  except LevelDBEncrypted as e:
451
455
  self._is_open = self._has_lock = False
452
456
  raise LevelDBException(
@@ -525,24 +529,34 @@ class LevelDBFormat(WorldFormatWrapper[VersionNumberTuple]):
525
529
  def unload(self):
526
530
  pass
527
531
 
528
- def all_chunk_coords(self, dimension: "Dimension") -> Iterable[ChunkCoordinates]:
532
+ def all_chunk_coords(self, dimension: Dimension) -> Iterable[ChunkCoordinates]:
529
533
  self._verify_has_lock()
530
- yield from self._dimension_manager.all_chunk_coords(dimension)
534
+ if dimension in self._dimension_to_internal:
535
+ yield from self._dimension_manager.all_chunk_coords(
536
+ self._dimension_to_internal[dimension]
537
+ )
531
538
 
532
539
  def has_chunk(self, cx: int, cz: int, dimension: Dimension) -> bool:
533
- return self._dimension_manager.has_chunk(cx, cz, dimension)
540
+ if dimension in self._dimension_to_internal:
541
+ return self._dimension_manager.has_chunk(
542
+ cx, cz, self._dimension_to_internal[dimension]
543
+ )
544
+ return False
534
545
 
535
- def _delete_chunk(self, cx: int, cz: int, dimension: "Dimension"):
536
- self._dimension_manager.delete_chunk(cx, cz, dimension)
546
+ def _delete_chunk(self, cx: int, cz: int, dimension: Dimension):
547
+ if dimension in self._dimension_to_internal:
548
+ self._dimension_manager.delete_chunk(
549
+ cx, cz, self._dimension_to_internal[dimension]
550
+ )
537
551
 
538
552
  def _put_raw_chunk_data(
539
- self, cx: int, cz: int, data: ChunkData, dimension: "Dimension"
553
+ self, cx: int, cz: int, data: ChunkData, dimension: Dimension
540
554
  ):
541
- return self._dimension_manager.put_chunk_data(cx, cz, data, dimension)
555
+ self._dimension_manager.put_chunk_data(
556
+ cx, cz, data, self._dimension_to_internal[dimension]
557
+ )
542
558
 
543
- def _get_raw_chunk_data(
544
- self, cx: int, cz: int, dimension: "Dimension"
545
- ) -> ChunkData:
559
+ def _get_raw_chunk_data(self, cx: int, cz: int, dimension: Dimension) -> ChunkData:
546
560
  """
547
561
  Return the raw data as loaded from disk.
548
562
 
@@ -551,7 +565,11 @@ class LevelDBFormat(WorldFormatWrapper[VersionNumberTuple]):
551
565
  :param dimension: The dimension to load the data from.
552
566
  :return: The raw chunk data.
553
567
  """
554
- return self._dimension_manager.get_chunk_data(cx, cz, dimension)
568
+ if dimension not in self._dimension_to_internal:
569
+ raise ChunkDoesNotExist
570
+ return self._dimension_manager.get_chunk_data(
571
+ cx, cz, self._dimension_to_internal[dimension]
572
+ )
555
573
 
556
574
  def all_player_ids(self) -> Iterable[str]:
557
575
  """
@@ -13,7 +13,7 @@ class Anvil3463Interface(ParentInterface):
13
13
 
14
14
  @staticmethod
15
15
  def minor_is_valid(key: int):
16
- return 3454 <= key < 3480
16
+ return 3454 <= key < 3580
17
17
 
18
18
 
19
19
  export = Anvil3463Interface
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: amulet-core
3
- Version: 1.9.18
3
+ Version: 1.9.20
4
4
  Summary: A Python library for reading/writing Minecraft's various save formats.
5
5
  Home-page: https://www.amuletmc.com
6
6
  Author: James Clare, Ben Gothard et al.
@@ -8,7 +8,7 @@ Author-email: amuleteditor@gmail.com
8
8
  Platform: any
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.6
11
+ Requires-Python: >=3.9
12
12
  Description-Content-Type: text/markdown
13
13
  Requires-Dist: numpy ~=1.17
14
14
  Requires-Dist: amulet-nbt ~=2.0
@@ -1,5 +1,5 @@
1
1
  amulet/__init__.py,sha256=sWKAqhofjzTINMJB22nXeBTITznt4H9rM3ZaaAn2SB4,866
2
- amulet/_version.py,sha256=7ltudBUYV8pYhsEK96sAh2VaaPGef3MmuO8byq_bRfU,498
2
+ amulet/_version.py,sha256=gUdgVfmYy5twLboNe4_FGwrJzp6s23bhMPNOnXdLQJQ,498
3
3
  amulet/__pyinstaller/__init__.py,sha256=JJOm9J0BoU_vwoYyoHgd8vwSdiO5SGlHHWH6EQK6sc4,41
4
4
  amulet/__pyinstaller/hook-amulet.py,sha256=5s6LZxxd47zNsrsJi-PORR7Q6QC079D7pcTkSn1uzCs,158
5
5
  amulet/api/__init__.py,sha256=HJ88KU13JNYES9sptkz0la0OsoRvBGUavuOanaZ_RyM,46
@@ -43,7 +43,7 @@ amulet/api/level/__init__.py,sha256=w-tIez-d-wurUHaxYijjeIPXF3MaV6d5IHEZQo0z5Gg,
43
43
  amulet/api/level/structure.py,sha256=jbcCaaZOoY8cwUxbSKu8R9Zq2HYdPLgiFSYBzoD2OSM,619
44
44
  amulet/api/level/world.py,sha256=HpKe4uvV5Ar2RaNf59wuP5tsxJmComMMNHpnjYAT7_Y,613
45
45
  amulet/api/level/base_level/__init__.py,sha256=Gix697NJxsR4zuYf-Q5Rj2IgrWWMpKvqQTfSc4TVuEY,34
46
- amulet/api/level/base_level/base_level.py,sha256=xX3izpmgREj-FevtFPaHA3ZvlmoFFDlYAQrYVc3hdc8,43105
46
+ amulet/api/level/base_level/base_level.py,sha256=Sfd0Qgu-xGasJO9aLpMFmf68DjMSt6J3w6nmTIsOcSU,43619
47
47
  amulet/api/level/base_level/chunk_manager.py,sha256=Dru933TSAmH3bklVulVvwxyp4VGFa2CHZvjmkDzmLZM,8921
48
48
  amulet/api/level/base_level/clone.py,sha256=1CxmXha7i6Vlz7QOUY4K3q3zbd6l0SqF-ppvB12VHvc,19543
49
49
  amulet/api/level/base_level/player_manager.py,sha256=ecx4AXAWGxuVG5zaQ_lS2Mr-O0VQu685n3KneTkV4nA,3226
@@ -78,9 +78,9 @@ amulet/level/loader.py,sha256=WF8LrPY8S9qAUIEse4FMkb7EDc0DzqNGs1graykj0-c,2869
78
78
  amulet/level/formats/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
79
  amulet/level/formats/anvil_forge_world.py,sha256=oktfFzj7YYzCdpHJeRKPp-XAR6EzGIp9nHeaQSvUHGo,1187
80
80
  amulet/level/formats/anvil_world/__init__.py,sha256=QlLF_96Ub9ceibhR8J1CVfkSaAqXl8dUQWWxFjDJkW0,54
81
- amulet/level/formats/anvil_world/_sector_manager.py,sha256=YKvTlbGlJuX4oBYFGuwVklgS8fvoEjQh9WtFugnD5f4,12660
81
+ amulet/level/formats/anvil_world/_sector_manager.py,sha256=QK-M9C4j_QfcpBxd_GwmUeQbwpnTgc6TC6egqtB4ZP8,11080
82
82
  amulet/level/formats/anvil_world/dimension.py,sha256=Ft38XWSjJ7mRjHqF-XjEIR7uJFTB_oBaWQIPNaMrsgQ,6069
83
- amulet/level/formats/anvil_world/format.py,sha256=LgUUM_D5e2sADlawj7rbf2LCVh_I7pixAHVnHhvsiaQ,27187
83
+ amulet/level/formats/anvil_world/format.py,sha256=FYcBhiKw_kRb5yKLtFkTD-kqlYDsZTXN72snBgjmstI,27502
84
84
  amulet/level/formats/anvil_world/region.py,sha256=-_Ru3na9SJsUWQhzRGOb1uTLoIbg2mdpVUNIa2-TM14,14095
85
85
  amulet/level/formats/anvil_world/data_pack/__init__.py,sha256=L-He67mqDNWp0boI1VDyIz92BfBZqSKLnBcR0bYxQUA,79
86
86
  amulet/level/formats/anvil_world/data_pack/data_pack.py,sha256=N7WwzdQuqmIsjInIqhk-I0UGV67wCFc_ysQMYh0UXYU,6161
@@ -92,8 +92,8 @@ amulet/level/formats/construction/section.py,sha256=_LdEQTLpaWnFhFCsmrzEFKRvXl7k
92
92
  amulet/level/formats/construction/util.py,sha256=dBBITNfxUocxxzVnUpRUetr0KuLtMVGjk0-o6iAUKBs,4930
93
93
  amulet/level/formats/leveldb_world/__init__.py,sha256=dXo6jIok-_suB3KSrhb5NZS7CicGQj-HCLiGpD76QDY,58
94
94
  amulet/level/formats/leveldb_world/chunk.py,sha256=iNMwNs09Kyj9fUo3dFZxdtLyW_uh11aXgDUqe_MKoQo,1202
95
- amulet/level/formats/leveldb_world/dimension.py,sha256=mPBChZNH4ujn-n4__vfC1YuR_ZXUajFqek_B40w6ZA8,16304
96
- amulet/level/formats/leveldb_world/format.py,sha256=gbrBR7iidRaTUNnT5bp35VLFaTZSe2cxOIKeWlQW9Bk,22614
95
+ amulet/level/formats/leveldb_world/dimension.py,sha256=MDnPlQd5LYEJyd9Z9B_-bsbfCkOqQsoTZfEfrxVwupA,14726
96
+ amulet/level/formats/leveldb_world/format.py,sha256=PsnpNE778q83wKw_6bbH06V9Q1avIpyh3U8pj0iTMlw,23401
97
97
  amulet/level/formats/leveldb_world/interface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
98
  amulet/level/formats/leveldb_world/interface/chunk/__init__.py,sha256=S606IgGmRqdQfv223yciHil3qqna8r5DiW8gsrNHyf0,1180
99
99
  amulet/level/formats/leveldb_world/interface/chunk/base_leveldb_interface.py,sha256=3SvaZX_2Umi3ibfD9zVo6pn6vnnJ34NQOJ51oDB8JRs,32470
@@ -174,7 +174,7 @@ amulet/level/interfaces/chunk/anvil/anvil_2529.py,sha256=UTtQkxcM3jUdSWpy02gehEh
174
174
  amulet/level/interfaces/chunk/anvil/anvil_2681.py,sha256=w9lz9014E3yNFdsLUFvF4UP1IE_OzdwFp8lZ0Zl5tCg,2319
175
175
  amulet/level/interfaces/chunk/anvil/anvil_2709.py,sha256=qRvOb67bjyh-HcSEUiqlf1YKMqPfZjeW98Aid92YQy8,417
176
176
  amulet/level/interfaces/chunk/anvil/anvil_2844.py,sha256=0jqoRhw5kRgDUfmm6dF18lxpIsDvm0eSL2rckVFhCCY,10216
177
- amulet/level/interfaces/chunk/anvil/anvil_3463.py,sha256=h8X-p5rPIHuGDBD3YuZGfMR2-kmpAIVdo193H6Wh2mY,422
177
+ amulet/level/interfaces/chunk/anvil/anvil_3463.py,sha256=vin45VuAiRsLhM03n6Sl527-O28fiiwIiTBwE3gklx4,422
178
178
  amulet/level/interfaces/chunk/anvil/anvil_na.py,sha256=9E9Jd9sQQqG_osfYmIg50hbMpZ0c9Dhi1N1ZDurC2VI,22007
179
179
  amulet/level/interfaces/chunk/anvil/base_anvil_interface.py,sha256=fG3nEkRxY9O-yvlBPFJW1abTb8Vw6Lhh7rjMJf7c1SA,11979
180
180
  amulet/level/translators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -201,8 +201,8 @@ amulet/utils/generator.py,sha256=2pKToghl8irYozX5hBJYtjTQVzhljznPCi42hQKRGDQ,364
201
201
  amulet/utils/matrix.py,sha256=KSqluO0H6oHUfe0pU4Esv67pOmHFurK2BDOrZAeNxg8,7665
202
202
  amulet/utils/numpy_helpers.py,sha256=fM0rjZxbUqoTMTrFooZEVVhHfsqv0j_7KPGsjVq4ReM,1232
203
203
  amulet/utils/world_utils.py,sha256=xb6JPrrbwDF0_y4ZYjTJ1ieydL3COzzLosTgcxDDyRc,12559
204
- amulet_core-1.9.18.dist-info/METADATA,sha256=RMOLRl_xYFXWn08XRO8PwnSydbRQtIXANohhjQbbybQ,4262
205
- amulet_core-1.9.18.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
206
- amulet_core-1.9.18.dist-info/entry_points.txt,sha256=53zFNThTPzI8f9ertsyU2DoUpxdWU8rjOA_Cu4YH5Vk,63
207
- amulet_core-1.9.18.dist-info/top_level.txt,sha256=3ZqHzNDiIb9kV8TwSeeXxK_9haSrsu631Qe4ndDo0rc,7
208
- amulet_core-1.9.18.dist-info/RECORD,,
204
+ amulet_core-1.9.20.dist-info/METADATA,sha256=SYeHecDnCp-awuJ5JkzMoHmIWRQMVm5RhR81LTzCfo0,4262
205
+ amulet_core-1.9.20.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
206
+ amulet_core-1.9.20.dist-info/entry_points.txt,sha256=53zFNThTPzI8f9ertsyU2DoUpxdWU8rjOA_Cu4YH5Vk,63
207
+ amulet_core-1.9.20.dist-info/top_level.txt,sha256=3ZqHzNDiIb9kV8TwSeeXxK_9haSrsu631Qe4ndDo0rc,7
208
+ amulet_core-1.9.20.dist-info/RECORD,,