def _encode_blocks( self, blocks: Blocks, palette: AnyNDArray ) -> amulet_nbt.TAG_List: sections = amulet_nbt.TAG_List() for cy in range(16): if cy in blocks: block_sub_array = numpy.transpose( blocks.get_sub_chunk(cy), (1, 2, 0) ).ravel() sub_palette_, block_sub_array = numpy.unique( block_sub_array, return_inverse=True ) sub_palette = self._encode_palette(palette[sub_palette_]) if ( len(sub_palette) == 1 and sub_palette[0]["Name"].value == "minecraft:air" ): continue section = amulet_nbt.TAG_Compound() section["Y"] = amulet_nbt.TAG_Byte(cy) if self.features["long_array_format"] == "compact": section["BlockStates"] = amulet_nbt.TAG_Long_Array( encode_long_array(block_sub_array) ) elif self.features["long_array_format"] == "1.16": section["BlockStates"] = amulet_nbt.TAG_Long_Array( encode_long_array(block_sub_array, dense=False) ) section["Palette"] = sub_palette sections.append(section) return sections
def test_longarray(self): with open(get_data_path("longarraytest.json")) as json_data: test_data = json.load(json_data) test_ran = False for test_entry in test_data["tests"]: test_ran = True block_array = numpy.asarray(test_entry["block_array"]) long_array = numpy.asarray(test_entry["long_array"]) palette_size = test_entry["palette_size"] numpy.testing.assert_array_equal( block_array, decode_long_array(long_array, len(block_array), (palette_size - 1).bit_length()), ) numpy.testing.assert_array_equal( long_array, encode_long_array(block_array, (palette_size - 1).bit_length()), ) # Make sure some test are ran in case the data file failed to load or has a wrong format. self.assertTrue(test_ran)
def test_encode_decode(self): for signed in (False, True): for dense in (False, True): for bits_per_entry in range(4, 65): for size in set([2**p for p in range(1, 13)] + list(range(100, 5000, 100))): if signed: arr = numpy.random.randint(-(2**63), 2**63, size, dtype=numpy.int64) else: arr = numpy.random.randint(0, 2**64, size, dtype=numpy.uint64) arr >>= 64 - bits_per_entry packed = encode_long_array(arr, bits_per_entry, dense) arr2 = decode_long_array(packed, len(arr), bits_per_entry, dense=dense, signed=signed) numpy.testing.assert_array_equal( arr, arr2, f"Long array does not equal. Dense: {dense}, bits per entry: {bits_per_entry}, size: {size}", )
def _encode_block_section( self, chunk: Chunk, sections: Dict[int, TAG_Compound], palette: AnyNDArray, cy: int, ) -> bool: if cy in chunk.blocks: block_sub_array = numpy.transpose(chunk.blocks.get_sub_chunk(cy), (1, 2, 0)).ravel() sub_palette_, block_sub_array = numpy.unique(block_sub_array, return_inverse=True) sub_palette = self._encode_block_palette(palette[sub_palette_]) if (len(sub_palette) == 1 and sub_palette[0]["Name"].value == "minecraft:air"): return False section = sections.setdefault(cy, TAG_Compound()) section["BlockStates"] = TAG_Long_Array( encode_long_array(block_sub_array, dense=self.LongArrayDense, min_bits_per_entry=4)) section["Palette"] = sub_palette return True return False
def _encode_biome_section( self, chunk: Chunk, sections: Dict[int, TAG_Compound], cy: int, ) -> bool: chunk.biomes.convert_to_3d() if cy in chunk.biomes: biome_sub_array = numpy.transpose(chunk.biomes.get_section(cy), (1, 2, 0)).ravel() sub_palette_, biome_sub_array = numpy.unique(biome_sub_array, return_inverse=True) sub_palette = self._encode_biome_palette( chunk.biome_palette[sub_palette_]) section = sections.setdefault(cy, TAG_Compound()) biomes = section["biomes"] = TAG_Compound({"palette": sub_palette}) if len(sub_palette) != 1: biomes["data"] = TAG_Long_Array( encode_long_array(biome_sub_array, dense=self.LongArrayDense)) return True return False
def _encode_height( self, chunk: Chunk, level: TAG_Compound, bounds: Tuple[int, int] ): maps = [ "WORLD_SURFACE_WG", "OCEAN_FLOOR_WG", "MOTION_BLOCKING", "MOTION_BLOCKING_NO_LEAVES", "OCEAN_FLOOR", ] if self._features["height_map"] == "C|V1": # 1466 maps = ("LIQUID", "SOLID", "LIGHT", "RAIN") elif self._features["height_map"] == "C|V2": # 1484 maps.append("LIGHT_BLOCKING") elif self._features["height_map"] == "C|V3": # 1503 maps.append("LIGHT_BLOCKING") maps.append("WORLD_SURFACE") elif self._features["height_map"] == "C|V4": # 1908 maps.append("WORLD_SURFACE") else: raise Exception heightmaps_temp: Dict[str, numpy.ndarray] = chunk.misc.get("height_mapC", {}) heightmaps = TAG_Compound() for heightmap in maps: if ( heightmap in heightmaps_temp and isinstance(heightmaps_temp[heightmap], numpy.ndarray) and heightmaps_temp[heightmap].size == 256 ): heightmaps[heightmap] = TAG_Long_Array( encode_long_array( heightmaps_temp[heightmap].ravel() - bounds[0], (bounds[1] - bounds[0]).bit_length(), self.LongArrayDense, ) ) level["Heightmaps"] = heightmaps
def _encode_block_section( self, chunk: Chunk, sections: Dict[int, TAG_Compound], palette: AnyNDArray, cy: int, ): if cy in chunk.blocks: block_sub_array = numpy.transpose(chunk.blocks.get_sub_chunk(cy), (1, 2, 0)).ravel() sub_palette_, block_sub_array = numpy.unique(block_sub_array, return_inverse=True) sub_palette = self._encode_block_palette(palette[sub_palette_]) section = sections.setdefault(cy, TAG_Compound()) block_states = section["block_states"] = TAG_Compound( {"palette": sub_palette}) if len(sub_palette) != 1: block_states["data"] = TAG_Long_Array( encode_long_array(block_sub_array, dense=self.LongArrayDense, min_bits_per_entry=4)) return True return False
def encode(self, chunk: "Chunk", palette: AnyNDArray, max_world_version: Tuple[str, int]) -> amulet_nbt.NBTFile: """ Encode a version-specific chunk to raw data for the format to store. :param chunk: The version-specific chunk to translate and encode. :param palette: The block_palette the ids in the chunk correspond to. :param max_world_version: The key to use to find the encoder. :return: amulet_nbt.NBTFile """ misc = chunk.misc data = amulet_nbt.NBTFile(amulet_nbt.TAG_Compound(), "") data["Level"] = amulet_nbt.TAG_Compound() data["Level"]["xPos"] = amulet_nbt.TAG_Int(chunk.cx) data["Level"]["zPos"] = amulet_nbt.TAG_Int(chunk.cz) if self.features["data_version"] == "int": data["DataVersion"] = amulet_nbt.TAG_Int(max_world_version[1]) if self.features["last_update"] == "long": data["Level"]["LastUpdate"] = amulet_nbt.TAG_Long( misc.get("last_update", 0)) # Order the float value based on the order they would be run. Newer replacements for the same come just after # to save back find the next lowest valid value. if self.features["status"] in ["j13", "j14"]: status = chunk.status.as_type(self.features["status"]) data["Level"]["Status"] = amulet_nbt.TAG_String(status) else: status = chunk.status.as_type("float") if self.features["terrain_populated"] == "byte": data["Level"]["TerrainPopulated"] = amulet_nbt.TAG_Byte( int(status > -0.3)) if self.features["light_populated"] == "byte": data["Level"]["LightPopulated"] = amulet_nbt.TAG_Byte( int(status > -0.2)) if self.features["V"] == "byte": data["Level"]["V"] = amulet_nbt.TAG_Byte(misc.get("V", 1)) if self.features["inhabited_time"] == "long": data["Level"]["InhabitedTime"] = amulet_nbt.TAG_Long( misc.get("inhabited_time", 0)) if self.features[ "biomes"] == "256BA": # TODO: support the optional variant if chunk.status.value > -0.7: chunk.biomes.convert_to_2d() data["Level"]["Biomes"] = amulet_nbt.TAG_Byte_Array( chunk.biomes.astype(dtype=numpy.uint8)) elif self.features["biomes"] == "256IA": if chunk.status.value > -0.7: chunk.biomes.convert_to_2d() data["Level"]["Biomes"] = amulet_nbt.TAG_Int_Array( chunk.biomes.astype(dtype=numpy.uint32)) elif self.features["biomes"] == "1024IA": if chunk.status.value > -0.7: chunk.biomes.convert_to_3d() data["Level"]["Biomes"] = amulet_nbt.TAG_Int_Array( numpy.transpose( numpy.asarray(chunk.biomes[:, 0:64, :]).astype( numpy.uint32), (1, 2, 0), ).ravel(), # YZX -> XYZ ) if self.features["height_map"] == "256IA": data["Level"]["HeightMap"] = amulet_nbt.TAG_Int_Array( misc.get("height_map256IA", numpy.zeros(256, dtype=numpy.uint32))) elif self.features["height_map"] in { "C|36LA|V1", "C|36LA|V2", "C|36LA|V3", "C|36LA|V4", }: maps = [ "WORLD_SURFACE_WG", "OCEAN_FLOOR_WG", "MOTION_BLOCKING", "MOTION_BLOCKING_NO_LEAVES", "OCEAN_FLOOR", ] if self.features["height_map"] == "C|36LA|V1": # 1466 maps = ("LIQUID", "SOILD", "LIGHT", "RAIN") elif self.features["height_map"] == "C|36LA|V2": # 1484 maps.append("LIGHT_BLOCKING") elif self.features["height_map"] == "C|36LA|V3": # 1503 maps.append("LIGHT_BLOCKING") maps.append("WORLD_SURFACE") elif self.features["height_map"] == "C|36LA|V4": # 1908 maps.append("WORLD_SURFACE") else: raise Exception heightmaps_temp: Dict[str, numpy.ndarray] = misc.get("height_mapC", {}) heightmaps = amulet_nbt.TAG_Compound() heightmap_length = (36 if max_world_version[1] < 2556 else 37 ) # this value is probably actually much lower for heightmap in maps: if heightmap in heightmaps_temp: array = encode_long_array(heightmaps_temp[heightmap], max_world_version[1] < 2556, 9) assert ( array.size == heightmap_length ), f"Expected an array of length {heightmap_length} but got an array of length {array.size}" heightmaps[heightmap] = amulet_nbt.TAG_Long_Array(array) else: heightmaps[heightmap] = amulet_nbt.TAG_Long_Array( numpy.zeros(heightmap_length, dtype=">i8")) data["Level"]["Heightmaps"] = heightmaps if self.features["blocks"] in [ "Sections|(Blocks,Data,Add)", "Sections|(BlockStates,Palette)", ]: data["Level"]["Sections"] = self._encode_blocks( chunk.blocks, palette) else: raise Exception( f'Unsupported block format {self.features["blocks"]}') if self.features["block_light"] in [ "Sections|2048BA" ] or self.features["sky_light"] in ["Sections|2048BA"]: for section in data["Level"]["Sections"]: y = section["Y"].value if self.features["block_light"] == "Sections|2048BA": block_light = misc.get("block_light", {}) if y in block_light: section["BlockLight"] = block_light[y] else: section["BlockLight"] = amulet_nbt.TAG_Byte_Array( numpy.full(2048, 255, dtype=numpy.uint8)) if self.features["sky_light"] == "Sections|2048BA": sky_light = misc.get("sky_light", {}) if y in sky_light: section["SkyLight"] = sky_light[y] else: section["SkyLight"] = amulet_nbt.TAG_Byte_Array( numpy.full(2048, 255, dtype=numpy.uint8)) if self.features["entities"] == "list": if amulet.entity_support: data["Level"]["Entities"] = self._encode_entities( chunk.entities) else: data["Level"]["Entities"] = self._encode_entities( misc.get("java_entities_temp", amulet_nbt.TAG_List())) if self.features["block_entities"] == "list": data["Level"]["TileEntities"] = self._encode_block_entities( chunk.block_entities) if self.features["tile_ticks"] in ["list", "list(optional)"]: ticks = misc.get("tile_ticks", amulet_nbt.TAG_List()) if self.features["tile_ticks"] == "list(optional)": if len(ticks) > 0: data["Level"]["TileTicks"] = ticks elif self.features["tile_ticks"] == "list": data["Level"]["TileTicks"] = ticks if self.features["liquid_ticks"] == "list": data["Level"]["LiquidTicks"] = misc.get("liquid_ticks", amulet_nbt.TAG_List()) if self.features["liquids_to_be_ticked"] == "16list|list": data["Level"]["LiquidsToBeTicked"] = misc.get( "liquids_to_be_ticked", amulet_nbt.TAG_List([amulet_nbt.TAG_List() for _ in range(16)]), ) if self.features["to_be_ticked"] == "16list|list": data["Level"]["ToBeTicked"] = misc.get( "to_be_ticked", amulet_nbt.TAG_List([amulet_nbt.TAG_List() for _ in range(16)]), ) if self.features["post_processing"] == "16list|list": data["Level"]["PostProcessing"] = misc.get( "post_processing", amulet_nbt.TAG_List([amulet_nbt.TAG_List() for _ in range(16)]), ) if self.features["structures"] == "compound": data["Level"]["Structures"] = misc.get( "structures", amulet_nbt.TAG_Compound({ "References": amulet_nbt.TAG_Compound(), "Starts": amulet_nbt.TAG_Compound(), }), ) return data