def _decode_blocks( self, chunk_sections: nbt.TAG_List ) -> Tuple[Dict[int, SubChunkNDArray], AnyNDArray]: blocks: Dict[int, SubChunkNDArray] = {} palette = [] palette_len = 0 for section in chunk_sections: cy: int = section["Y"].value section_blocks = numpy.frombuffer( section["Blocks"].value, dtype=numpy.uint8 ) del section["Blocks"] section_data = numpy.frombuffer(section["Data"].value, dtype=numpy.uint8) del section["Data"] section_blocks = section_blocks.reshape((16, 16, 16)) section_blocks = section_blocks.astype(numpy.uint16) section_data = world_utils.from_nibble_array(section_data) section_data = section_data.reshape((16, 16, 16)) if "Add" in section: add_blocks = numpy.frombuffer(section["Add"].value, dtype=numpy.uint8) del section["Add"] add_blocks = world_utils.from_nibble_array(add_blocks) add_blocks = add_blocks.reshape((16, 16, 16)) section_blocks |= add_blocks.astype(numpy.uint16) << 8 # TODO: fix this (section_palette, blocks[cy]) = world_utils.fast_unique( numpy.transpose( (section_blocks << 4) + section_data, (2, 0, 1) ) # YZX -> XYZ ) blocks[cy] += palette_len palette_len += len(section_palette) palette.append(section_palette) if palette: final_palette, lut = numpy.unique( numpy.concatenate(palette), return_inverse=True ) final_palette: numpy.ndarray = numpy.array( [final_palette >> 4, final_palette & 15] ).T for cy in blocks: blocks[cy] = lut[blocks[cy]] else: final_palette = numpy.array([], dtype=numpy.object) return blocks, final_palette
def _decode_blocks( self, chunk: Chunk, chunk_sections: Dict[int, TAG_Compound] ) -> AnyNDArray: blocks: Dict[int, SubChunkNDArray] = {} palette = [] palette_len = 0 for cy, section in chunk_sections.items(): section_blocks = numpy.frombuffer( section.pop("Blocks").value, dtype=numpy.uint8 ) section_data = numpy.frombuffer( section.pop("Data").value, dtype=numpy.uint8 ) section_blocks = section_blocks.reshape((16, 16, 16)) section_blocks = section_blocks.astype(numpy.uint16) section_data = world_utils.from_nibble_array(section_data) section_data = section_data.reshape((16, 16, 16)) if "Add" in section: add_blocks = numpy.frombuffer( section.pop("Add").value, dtype=numpy.uint8 ) add_blocks = world_utils.from_nibble_array(add_blocks) add_blocks = add_blocks.reshape((16, 16, 16)) section_blocks |= add_blocks.astype(numpy.uint16) << 8 # TODO: fix this (section_palette, blocks[cy]) = world_utils.fast_unique( numpy.transpose( (section_blocks << 4) + section_data, (2, 0, 1) ) # YZX -> XYZ ) blocks[cy] += palette_len palette_len += len(section_palette) palette.append(section_palette) if palette: final_palette, lut = numpy.unique( numpy.concatenate(palette), return_inverse=True ) final_palette: numpy.ndarray = numpy.array( [final_palette >> 4, final_palette & 15] ).T for cy in blocks: blocks[cy] = lut[blocks[cy]] else: final_palette = numpy.array([], dtype=object) chunk.blocks = blocks return final_palette
def decode(self, cx: int, cz: int, data: Dict[bytes, bytes]) -> Tuple[Chunk, AnyNDArray]: # chunk_key_base = struct.pack("<ii", cx, cz) chunk = Chunk(cx, cz) chunk_palette = numpy.empty(0, dtype=object) if self.features["terrain"].startswith( "2f"): # ["2farray", "2f1palette", "2fnpalette"] subchunks = [ data.get(b"\x2F" + bytes([i]), None) for i in range(16) ] chunk.blocks, chunk_palette = self._load_subchunks(subchunks) elif self.features["terrain"] == "30array": chunk_data = data.get(b"\x30", None) if chunk_data is not None: block_ids = numpy.frombuffer(chunk_data[:2**15], dtype=numpy.uint8).astype( numpy.uint16) block_data = from_nibble_array( numpy.frombuffer(chunk_data[2**15:2**15 + 2**14], dtype=numpy.uint8)) # there is other data here but we are going to skip over it combined_palette, block_array = fast_unique( numpy.transpose( ((block_ids << 4) + block_data).reshape(16, 16, 128), (0, 2, 1))) chunk.blocks = { i: block_array[:, i * 16:(i + 1) * 16, :] for i in range(8) } palette: AnyNDArray = numpy.array( [combined_palette >> 4, combined_palette & 15]).T chunk_palette = numpy.empty(len(palette), dtype=object) for i, b in enumerate(palette): chunk_palette[i] = ((None, tuple(b)), ) else: raise Exception if self.features["finalised_state"] == "int0-2": if b"\x36" in data: val = struct.unpack("<i", data[b"\x36"])[0] else: val = 2 chunk.status = val if self.features["data_2d"] in [ "height512|biome256", "unused_height512|biome256", ]: d2d = data.get(b"\x2D", b"\x00" * 768) height, biome = d2d[:512], d2d[512:] if self.features["data_2d"] == "height512|biome256": pass # TODO: put this data somewhere chunk.biomes = numpy.frombuffer(biome, dtype="uint8").reshape(16, 16) # TODO: impliment key support # \x2D heightmap and biomes # \x31 block entity # \x32 entity # \x33 ticks # \x34 block extra data # \x35 biome state # \x39 7 ints and an end (03)? Honestly don't know what this is # \x3A fire tick? # \x2E 2d legacy # \x30 legacy terrain # unpack block entities and entities if self.features["block_entities"] == "31list": block_entities = self._unpack_nbt_list(data.get(b"\x31", b"")) chunk.block_entities = self._decode_block_entities(block_entities) if self.features["entities"] == "32list" and amulet.entity_support: entities = self._unpack_nbt_list(data.get(b"\x32", b"")) chunk.entities = self._decode_entities(entities) return chunk, chunk_palette
def _load_subchunks( self, subchunks: List[None, bytes] ) -> Tuple[Dict[int, SubChunkNDArray], AnyNDArray]: """ Load a list of bytes objects which contain chunk data This function should be able to load all sub-chunk formats (technically before it) All sub-chunks will almost certainly all have the same sub-chunk version but it should be able to handle a case where that is not true. As such this function will return a Chunk and a rather complicated block_palette The newer formats allow multiple blocks to occupy the same space and the newer versions also include a version ber block. So this will also need returning for the translator to handle. The block_palette will be a numpy array containing tuple objects The tuple represents the "block" however can contain more than one Block object. Inside the tuple are one or more tuples. These include the block version number and the block itself The block version number will be either None if no block version is given or a tuple containing 4 ints. The block will be either a Block class for the newer formats or a tuple of two ints for the older formats """ blocks: Dict[int, SubChunkNDArray] = {} palette: List[Tuple[Tuple[Optional[int], Union[Tuple[int, int], Block], ], ...]] = [(( 17563649, Block( namespace="minecraft", base_name="air", properties={ "block_data": amulet_nbt.TAG_Int(0) }, ), ), )] for cy, data in enumerate(subchunks): if data is None: continue if data[0] in [0, 2, 3, 4, 5, 6, 7]: block_ids = numpy.frombuffer(data[1:1 + 2**12], dtype=numpy.uint8).astype( numpy.uint16) block_data = from_nibble_array( numpy.frombuffer(data[1 + 2**12:1 + 2**12 + 2**11], dtype=numpy.uint8)) combined_palette, block_array = fast_unique( numpy.transpose( ((block_ids << 4) + block_data).reshape(16, 16, 16), (0, 2, 1))) blocks[cy] = block_array + len(palette) for b in numpy.array( [combined_palette >> 4, combined_palette & 15]).T: palette.append(((None, tuple(b)), )) elif data[0] in [1, 8]: if data[0] == 1: storage_count = 1 data = data[1:] else: storage_count, data = data[1], data[2:] sub_chunk_blocks = numpy.zeros((16, 16, 16, storage_count), dtype=numpy.uint32) sub_chunk_palette: List[List[Tuple[Optional[int], Block]]] = [] for storage_index in range(storage_count): ( sub_chunk_blocks[:, :, :, storage_index], palette_data, data, ) = self._load_palette_blocks(data) palette_data_out: List[Tuple[Optional[int], Block]] = [] for block in palette_data: namespace, base_name = block["name"].value.split( ":", 1) if "version" in block: version: Optional[int] = block["version"].value else: version = None if "states" in block: # 1.13 format properties = block["states"].value if version is None: version = 17694720 # 1, 14, 0, 0 else: properties = { "block_data": amulet_nbt.TAG_Int(block["val"].value) } palette_data_out.append(( version, Block( namespace=namespace, base_name=base_name, properties=properties, ), )) sub_chunk_palette.append(palette_data_out) if storage_count == 1: blocks[cy] = sub_chunk_blocks[:, :, :, 0] + len(palette) palette += [(val, ) for val in sub_chunk_palette[0]] elif storage_count > 1: # we have two or more storages so need to find the unique block combinations and merge them together sub_chunk_palette_, sub_chunk_blocks = numpy.unique( sub_chunk_blocks.reshape(-1, storage_count), return_inverse=True, axis=0, ) blocks[cy] = sub_chunk_blocks.reshape(16, 16, 16).astype( numpy.uint32) + len(palette) palette += [ tuple( sub_chunk_palette[storage_index][index] for storage_index, index in enumerate( palette_indexes) if not (storage_index > 0 and sub_chunk_palette[storage_index][index] [1].namespaced_name == "minecraft:air")) for palette_indexes in sub_chunk_palette_ ] else: continue # block_palette should now look like this # List[ # Tuple[ # Tuple[version, Block], ... # ] # ] numpy_palette, lut = brute_sort_objects(palette) for cy in blocks.keys(): blocks[cy] = lut[blocks[cy]] return blocks, numpy_palette
def decode(self, cx: int, cz: int, data: Dict[bytes, bytes], bounds: Tuple[int, int]) -> Tuple[Chunk, AnyNDArray]: """ Create an amulet.api.chunk.Chunk object from raw data given by the format :param cx: chunk x coordinate :param cz: chunk z coordinate :param data: Raw chunk data provided by the format. :param bounds: The minimum and maximum height of the chunk. :return: Chunk object in version-specific format, along with the block_palette for that chunk. """ chunk = Chunk(cx, cz) chunk_palette = numpy.empty(0, dtype=object) chunk.misc = {"bedrock_chunk_data": data} data.pop(b"v", None) data.pop(b",", None) if self._features["terrain"].startswith( "2f"): # ["2farray", "2f1palette", "2fnpalette"] subchunks = {} for key in data.copy().keys(): if len(key) == 2 and key[0:1] == b"\x2F": cy = struct.unpack("b", key[1:2])[0] subchunks[self._chunk_key_to_sub_chunk( cy, bounds[0] >> 4)] = data.pop(key) chunk.blocks, chunk_palette = self._load_subchunks(subchunks) elif self._features["terrain"] == "30array": chunk_data = data.pop(b"\x30", None) if chunk_data is not None: block_ids = numpy.frombuffer(chunk_data[:2**15], dtype=numpy.uint8).astype( numpy.uint16) block_data = from_nibble_array( numpy.frombuffer(chunk_data[2**15:2**15 + 2**14], dtype=numpy.uint8)) # there is other data here but we are going to skip over it combined_palette, block_array = fast_unique( numpy.transpose( ((block_ids << 4) + block_data).reshape(16, 16, 128), (0, 2, 1))) chunk.blocks = { i: block_array[:, i * 16:(i + 1) * 16, :] for i in range(8) } palette: AnyNDArray = numpy.array( [combined_palette >> 4, combined_palette & 15]).T chunk_palette = numpy.empty(len(palette), dtype=object) for i, b in enumerate(palette): chunk_palette[i] = ((None, tuple(b)), ) else: raise Exception if self._features["finalised_state"] == "int0-2": state = data.pop(b"\x36", None) val = 2 if isinstance(state, bytes): if len(state) == 1: # old versions of the game store this as a byte val = struct.unpack("b", state)[0] elif len(state) == 4: # newer versions store it as an int val = struct.unpack("<i", state)[0] chunk.status = val if b"+" in data: height, biome = self._decode_height_3d_biomes( data[b"+"], bounds[0] >> 4) chunk.misc["height"] = height chunk.biomes = biome elif b"\x2D" in data: d2d = data[b"\x2D"] height, biome = ( numpy.frombuffer(d2d[:512], "<i2").reshape((16, 16)), d2d[512:], ) chunk.misc["height"] = height chunk.biomes = numpy.frombuffer(biome, dtype="uint8").reshape(16, 16).T # TODO: implement key support # \x2D heightmap and biomes # \x31 block entity # \x32 entity # \x33 ticks # \x34 block extra data # \x35 biome state # \x39 7 ints and an end (03)? Honestly don't know what this is # \x3A fire tick? # \x2E 2d legacy # \x30 legacy terrain # unpack block entities and entities if self._features["block_entities"] == "31list": block_entities = self._unpack_nbt_list(data.pop(b"\x31", b"")) chunk.block_entities = self._decode_block_entity_list( block_entities) if self._features["entities"] == "32list" and amulet.entity_support: entities = self._unpack_nbt_list(data.pop(b"\x32", b"")) chunk.entities = self._decode_entity_list(entities) return chunk, chunk_palette