From 0f6859e8dccc3b3220a67fbde518e6f7601cc001 Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Wed, 19 Jan 2022 22:05:50 +0000 Subject: [PATCH 01/38] add Enum for biome definitions --- anvil/biome.py | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 anvil/biome.py diff --git a/anvil/biome.py b/anvil/biome.py new file mode 100644 index 0000000..c0039f5 --- /dev/null +++ b/anvil/biome.py @@ -0,0 +1,82 @@ +from enum import Enum, auto + +class Biome(Enum): + Ocean = 0 + Forest = 4 + River = 7 + FrozenOcean = 10 + FrozenRiver = 11 + Beach = 16 + DeepOcean = 24 + StoneShore = 25 + SnowyBeach = 26 + WarmOcean = 44 + LukewarmOcean = 45 + ColdOcean = 46 + DeepWarmOcean = 47 + DeepLukewarmOcean = 48 + DeepColdOcean = 49 + DeepFrozenOcean = 50 + WoodedHills = 18 + FlowerForest = 132 + BirchForest = 27 + BirchForestHills = 28 + TallBirchForest = 155 + TallBirchHills = 156 + DarkForest = 29 + DarkForestHills = 157 + Jungle = 21 + JungleHills = 22 + ModifiedJungle = 149 + JungleEdge = 23 + ModifiedJungleEdge = 151 + BambooJungle = 168 + BambooJungleHills = 169 + Taiga = 5 + TaigaHills = 19 + TaigaMountains = 133 + SnowyTaiga = 30 + SnowyTaigaHills = 31 + SnowyTaigaMountains = 158 + GiantTreeTaiga = 32 + GiantTreeTaigaHills = 33 + GiantSpruceTaiga = 160 + GiantSpruceTaigaHills = 161 + MushroomFields = 14 + MushroomFieldShore = 15 + Swamp = 6 + SwampHills = 134 + Savanna = 35 + SavannaPlateau = 36 + ShatteredSavanna = 163 + ShatteredSavannaPlateau = 164 + Plains = 1 + SunflowerPlains = 129 + Desert = 2 + DesertHills = 17 + DesertLakes = 130 + SnowyTundra = 12 + SnowyMountains = 13 + IceSpikes = 140 + Mountains = 3 + WoodedMountains = 34 + GravellyMountains = 131 + ModifiedGravellyMountains = 162 + MountainEdge = 20 + Badlands = 37 + BadlandsPlateau = 39 + ModifiedBadlandsPlateau = 167 + WoodedBadlandsPlateau = 38 + ModifiedWoodedBadlandsPlateau = 166 + ErodedBadlands = 165 + Nether = 8 + TheEnd = 9 + SmallEndIslands = 40 + EndMidlands = 41 + EndHighlands = 42 + EndBarrens = 43 + SoulSandValley = 170 + CrimsonForest = 171 + WarpedForest = 172 + TheVoid = 127 + BasaltDeltas = 173 \ No newline at end of file From ad4a88e302480ab2459cd35331a7a0a0a8ba1d55 Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Sat, 22 Jan 2022 21:36:40 +0000 Subject: [PATCH 02/38] add Chunk.get_biome method --- anvil/__init__.py | 1 + anvil/chunk.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/anvil/__init__.py b/anvil/__init__.py index 9611b80..2a7c696 100644 --- a/anvil/__init__.py +++ b/anvil/__init__.py @@ -1,5 +1,6 @@ from .chunk import Chunk from .block import Block, OldBlock +from .biome import Biome from .region import Region from .empty_region import EmptyRegion from .empty_chunk import EmptyChunk diff --git a/anvil/chunk.py b/anvil/chunk.py index daad244..e4a5c1e 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -1,5 +1,6 @@ from typing import Union, Tuple, Generator, Optional from nbt import nbt +from .biome import Biome from .block import Block, OldBlock from .region import Region from .errors import OutOfBoundsCoordinates, ChunkNotFound @@ -10,6 +11,10 @@ # so a block value isn't in multiple elements of the array _VERSION_20w17a = 2529 +# This version changes how biomes are stored to allow for biomes at different heights +# https://minecraft.fandom.com/wiki/Java_Edition_19w36a +_VERSION_19w36a = 2203 + # This is the version where "The Flattening" (https://minecraft.gamepedia.com/Java_Edition_1.13/Flattening) happened # where blocks went from numeric ids to namespaced ids (namespace:block_id) _VERSION_17w47a = 1451 @@ -108,6 +113,41 @@ def get_palette(self, section: Union[int, nbt.TAG_Compound]) -> Tuple[Block]: return return tuple(Block.from_palette(i) for i in section['Palette']) + def get_biome(self, x: int, y : int, z: int) -> Biome: + """ + Returns the biome in the given coordinates + + Parameters + ---------- + int x, y, z + Biome's coordinates in the chunk + + Raises + ------ + anvil.OutOfBoundCoordidnates + If X, Y or Z are not in the proper range + + :rtype: :class:`anvil.Biome` + """ + if x < 0 or x > 15: + raise OutOfBoundsCoordinates(f'X ({x!r}) must be in range of 0 to 15') + if z < 0 or z > 15: + raise OutOfBoundsCoordinates(f'Z ({z!r}) must be in range of 0 to 15') + if y < 0 or y > 255: + raise OutOfBoundsCoordinates(f'Y ({y!r}) must be in range of 0 to 255') + + biomes = self.data['Biomes'] + if self.version < _VERSION_19w36a: + # Each biome index refers to a column stored Z then X. + index = z * 16 + x + else: + # https://minecraft.fandom.com/wiki/Java_Edition_19w36a + # Get index on the biome list with the order YZX + # Each biome index refers to a 4x4 volumes here so we do integer division by 4 + index = (y // 4) * 4 * 4 + (z // 4) * 4 + (x // 4) + biome_id = biomes[index] + return Biome(biome_id) + def get_block(self, x: int, y: int, z: int, section: Union[int, nbt.TAG_Compound]=None, force_new: bool=False) -> Union[Block, OldBlock]: """ Returns the block in the given coordinates From 3da587fbe86e768c75fb134a2b26731a99eb0d00 Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Sat, 22 Jan 2022 22:45:21 +0000 Subject: [PATCH 03/38] add set_biome methods --- anvil/empty_chunk.py | 30 +++++++++++++++++++++++++++++- anvil/empty_region.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/anvil/empty_chunk.py b/anvil/empty_chunk.py index a31e7f3..924cd2d 100644 --- a/anvil/empty_chunk.py +++ b/anvil/empty_chunk.py @@ -1,5 +1,6 @@ from typing import List from .block import Block +from .biome import Biome from .empty_section import EmptySection from .errors import OutOfBoundsCoordinates, EmptySectionAlreadyExists from nbt import nbt @@ -19,11 +20,12 @@ class EmptyChunk: version: :class:`int` Chunk's DataVersion """ - __slots__ = ('x', 'z', 'sections', 'version') + __slots__ = ('x', 'z', 'sections', 'biomes', 'version') def __init__(self, x: int, z: int): self.x = x self.z = z self.sections: List[EmptySection] = [None]*16 + self.biomes: List[Biome] = [Biome(0)]*16*16 self.version = 1976 def add_section(self, section: EmptySection, replace: bool = True): @@ -108,6 +110,29 @@ def set_block(self, block: Block, x: int, y: int, z: int): self.add_section(section) section.set_block(block, x, y % 16, z) + def set_biome(self, biome: Biome, x: int, z: int): + """ + Sets biome at given coordinates + + Parameters + ---------- + int x, z + In range of 0 to 15 + + Raises + ------ + anvil.OutOfBoundCoordidnates + If X or Z are not in the proper range + + """ + if x < 0 or x > 15: + raise OutOfBoundsCoordinates(f'X ({x!r}) must be in range of 0 to 15') + if z < 0 or z > 15: + raise OutOfBoundsCoordinates(f'Z ({z!r}) must be in range of 0 to 15') + + index = z * 16 + x + self.biomes[index] = biome + def save(self) -> nbt.NBTFile: """ Saves the chunk data to a :class:`NBTFile` @@ -135,6 +160,8 @@ def save(self) -> nbt.NBTFile: nbt.TAG_String(name='Status', value='full') ]) sections = nbt.TAG_List(name='Sections', type=nbt.TAG_Compound) + biomes = nbt.TAG_Int_Array(name='Biomes') + biomes.value = [biome.value for biome in self.biomes] for s in self.sections: if s: p = s.palette() @@ -144,5 +171,6 @@ def save(self) -> nbt.NBTFile: continue sections.tags.append(s.save()) level.tags.append(sections) + level.tags.append(biomes) root.tags.append(level) return root diff --git a/anvil/empty_region.py b/anvil/empty_region.py index 6dcf675..74ecd51 100644 --- a/anvil/empty_region.py +++ b/anvil/empty_region.py @@ -3,6 +3,7 @@ from .chunk import Chunk from .empty_section import EmptySection from .block import Block +from .biome import Biome from .errors import OutOfBoundsCoordinates from io import BytesIO from nbt import nbt @@ -140,6 +141,33 @@ def set_block(self, block: Block, x: int, y: int, z: int): self.add_chunk(chunk) chunk.set_block(block, x % 16, y, z % 16) + def set_biome(self, biome: Biome, x: int, z: int): + """ + Sets biome at given coordinates. + New chunk is made if it doesn't exist. + + Parameters + ---------- + biome: :class:`Biome` + Biome to place + int x, z + Coordinates + + Raises + ------ + anvil.OutOfBoundsCoordinates + If the biome (x, z) is not inside this region + """ + if not self.inside(x, 0, z): + raise OutOfBoundsCoordinates(f'Biome ({x}, {z}) is not inside this region') + cx = x // 16 + cz = z // 16 + chunk = self.get_chunk(cx, cz) + if chunk is None: + chunk = EmptyChunk(cx, cz) + self.add_chunk(chunk) + chunk.set_biome(biome, x % 16, z % 16) + def set_if_inside(self, block: Block, x: int, y: int, z: int): """ Helper function that only sets From b53bfb41a358ff10ea9188cc23101bd07fe0ca7d Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Sat, 22 Jan 2022 22:55:30 +0000 Subject: [PATCH 04/38] add fill_biome function --- anvil/empty_region.py | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/anvil/empty_region.py b/anvil/empty_region.py index 74ecd51..463c73c 100644 --- a/anvil/empty_region.py +++ b/anvil/empty_region.py @@ -183,6 +183,21 @@ def set_if_inside(self, block: Block, x: int, y: int, z: int): if self.inside(x, y, z): self.set_block(block, x, y, z) + def set_biome_if_inside(self, biome: Biome, x: int, z: int): + """ + Helper function that only sets + the biome if ``self.inside(x, 0, z)`` is true + + Parameters + ---------- + biome: :class:`Biome` + Biome to place + int x, z + Coordinates + """ + if self.inside(x, 0, z): + self.set_biome(biome, x, z) + def fill(self, block: Block, x1: int, y1: int, z1: int, x2: int, y2: int, z2: int, ignore_outside: bool=False): """ Fills in blocks from @@ -217,6 +232,40 @@ def fill(self, block: Block, x1: int, y1: int, z1: int, x2: int, y2: int, z2: in self.set_if_inside(block, x, y, z) else: self.set_block(block, x, y, z) + + def fill_biome(self, biome: Biome, x1: int, z1: int, x2: int, z2: int, ignore_outside: bool=False): + """ + Fills in biomes from + ``(x1, z1)`` to ``(x2, z2)`` + in a rectangle. + + Parameters + ---------- + biome: :class:`Biome` + int x1, z1 + Coordinates + int x2, z2 + Coordinates + ignore_outside + Whether to ignore if coordinates are outside the region + + Raises + ------ + anvil.OutOfBoundsCoordinates + If any of the coordinates are outside the region + """ + if not ignore_outside: + if not self.inside(x1, 0, z1): + raise OutOfBoundsCoordinates(f'First coords ({x1}, {z1}) is not inside this region') + if not self.inside(x2, 0, z2): + raise OutOfBoundsCoordinates(f'Second coords ({x2}, {z2}) is not inside this region') + + for z in from_inclusive(z1, z2): + for x in from_inclusive(x1, x2): + if ignore_outside: + self.set_biome_if_inside(biome, x, z) + else: + self.set_biome(biome, x, z) def save(self, file: Union[str, BinaryIO]=None) -> bytes: """ From 1cf8f77c151e323aeaf12445dd96ab2a42a7948d Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Sat, 22 Jan 2022 23:12:16 +0000 Subject: [PATCH 05/38] update README & setup.py version --- README.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ecaadbc..b788de5 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ region.save('r.0.0.mca') # Todo *things to do before 1.0.0* - [x] Proper documentation -- [ ] Biomes +- [x] Biomes - [x] CI - [ ] More tests - [ ] Tests for 20w17a+ BlockStates format diff --git a/setup.py b/setup.py index 3bcdb07..a629a6e 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name='anvil-parser', - version='0.9.0', + version='0.10.0', author='mat', description='A Minecraft anvil file format parser', long_description=long_description, From 4d2e7f388ee7ad6bd8c30fccbf8264d3893cdaf9 Mon Sep 17 00:00:00 2001 From: bee-boii <100041984+bee-boii@users.noreply.github.com> Date: Wed, 16 Mar 2022 17:44:24 -0400 Subject: [PATCH 06/38] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2439353..365044d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -NBT==1.5.0 +NBT==1.5.1 frozendict==1.2 From 374006549c17d3de0dc59fa14fcefbe692e8cc28 Mon Sep 17 00:00:00 2001 From: bee-boii <100041984+bee-boii@users.noreply.github.com> Date: Wed, 16 Mar 2022 22:14:23 -0400 Subject: [PATCH 07/38] Added support for 1.18 within get_block only --- anvil/chunk.py | 163 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 108 insertions(+), 55 deletions(-) diff --git a/anvil/chunk.py b/anvil/chunk.py index e4a5c1e..a81160b 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -19,6 +19,7 @@ # where blocks went from numeric ids to namespaced ids (namespace:block_id) _VERSION_17w47a = 1451 + def bin_append(a, b, length=None): """ Appends number a to the left of b @@ -27,6 +28,7 @@ def bin_append(a, b, length=None): length = length or b.bit_length() return (a << length) | b + def nibble(byte_array, index): value = byte_array[index // 2] if index % 2: @@ -34,6 +36,7 @@ def nibble(byte_array, index): else: return value & 0b1111 + class Chunk: """ Represents a chunk from a ``.mca`` file. @@ -53,20 +56,26 @@ class Chunk: tile_entities: :class:`nbt.TAG_Compound` ``self.data['TileEntities']`` as an attribute for easier use """ - __slots__ = ('version', 'data', 'x', 'z', 'tile_entities') + + __slots__ = ("version", "data", "x", "z", "tile_entities") def __init__(self, nbt_data: nbt.NBTFile): try: - self.version = nbt_data['DataVersion'].value + self.version = nbt_data["DataVersion"].value except KeyError: # Version is pre-1.9 snapshot 15w32a, so world does not have a Data Version. # See https://minecraft.fandom.com/wiki/Data_version self.version = None - self.data = nbt_data['Level'] - self.x = self.data['xPos'].value - self.z = self.data['zPos'].value - self.tile_entities = self.data['TileEntities'] + if self.version > 2730: + self.data = nbt_data + self.x = nbt_data["xPos"].value + self.z = nbt_data["zPos"].value + else: + self.data = nbt_data["Level"] + self.x = self.data["xPos"].value + self.z = self.data["zPos"].value + self.tile_entities = self.data["TileEntities"] def get_section(self, y: int) -> nbt.TAG_Compound: """ @@ -83,16 +92,22 @@ def get_section(self, y: int) -> nbt.TAG_Compound: anvil.OutOfBoundsCoordinates If Y is not in range of 0 to 15 """ - if y < 0 or y > 15: - raise OutOfBoundsCoordinates(f'Y ({y!r}) must be in range of 0 to 15') - - try: - sections = self.data["Sections"] - except KeyError: - return None + if y < -4 or y > 19: + raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of 0 to 15") + + if self.version > 2730: + try: + sections = self.data["sections"] + except KeyError: + return None + else: + try: + sections = self.data["Sections"] + except KeyError: + return None for section in sections: - if section['Y'].value == y: + if section["Y"].value == y: return section def get_palette(self, section: Union[int, nbt.TAG_Compound]) -> Tuple[Block]: @@ -111,9 +126,9 @@ def get_palette(self, section: Union[int, nbt.TAG_Compound]) -> Tuple[Block]: section = self.get_section(section) if section is None: return - return tuple(Block.from_palette(i) for i in section['Palette']) + return tuple(Block.from_palette(i) for i in section["Palette"]) - def get_biome(self, x: int, y : int, z: int) -> Biome: + def get_biome(self, x: int, y: int, z: int) -> Biome: """ Returns the biome in the given coordinates @@ -130,14 +145,14 @@ def get_biome(self, x: int, y : int, z: int) -> Biome: :rtype: :class:`anvil.Biome` """ if x < 0 or x > 15: - raise OutOfBoundsCoordinates(f'X ({x!r}) must be in range of 0 to 15') + raise OutOfBoundsCoordinates(f"X ({x!r}) must be in range of 0 to 15") if z < 0 or z > 15: - raise OutOfBoundsCoordinates(f'Z ({z!r}) must be in range of 0 to 15') + raise OutOfBoundsCoordinates(f"Z ({z!r}) must be in range of 0 to 15") if y < 0 or y > 255: - raise OutOfBoundsCoordinates(f'Y ({y!r}) must be in range of 0 to 255') + raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of 0 to 255") - biomes = self.data['Biomes'] - if self.version < _VERSION_19w36a: + biomes = self.data["Biomes"] + if self.version < _VERSION_19w36a: # Each biome index refers to a column stored Z then X. index = z * 16 + x else: @@ -148,7 +163,14 @@ def get_biome(self, x: int, y : int, z: int) -> Biome: biome_id = biomes[index] return Biome(biome_id) - def get_block(self, x: int, y: int, z: int, section: Union[int, nbt.TAG_Compound]=None, force_new: bool=False) -> Union[Block, OldBlock]: + def get_block( + self, + x: int, + y: int, + z: int, + section: Union[int, nbt.TAG_Compound] = None, + force_new: bool = False, + ) -> Union[Block, OldBlock]: """ Returns the block in the given coordinates @@ -171,11 +193,11 @@ def get_block(self, x: int, y: int, z: int, section: Union[int, nbt.TAG_Compound :rtype: :class:`anvil.Block` """ if x < 0 or x > 15: - raise OutOfBoundsCoordinates(f'X ({x!r}) must be in range of 0 to 15') + raise OutOfBoundsCoordinates(f"X ({x!r}) must be in range of 0 to 15") if z < 0 or z > 15: - raise OutOfBoundsCoordinates(f'Z ({z!r}) must be in range of 0 to 15') - if y < 0 or y > 255: - raise OutOfBoundsCoordinates(f'Y ({y!r}) must be in range of 0 to 255') + raise OutOfBoundsCoordinates(f"Z ({z!r}) must be in range of 0 to 15") + if y < -64 or y > 319: + raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of 0 to 255") if section is None: section = self.get_section(y // 16) @@ -185,19 +207,19 @@ def get_block(self, x: int, y: int, z: int, section: Union[int, nbt.TAG_Compound if self.version is None or self.version < _VERSION_17w47a: # Explained in depth here https://minecraft.gamepedia.com/index.php?title=Chunk_format&oldid=1153403#Block_format - if section is None or 'Blocks' not in section: + if section is None or "Blocks" not in section: if force_new: - return Block.from_name('minecraft:air') + return Block.from_name("minecraft:air") else: return OldBlock(0) index = y * 16 * 16 + z * 16 + x - block_id = section['Blocks'][index] - if 'Add' in section: - block_id += nibble(section['Add'], index) << 8 + block_id = section["Blocks"][index] + if "Add" in section: + block_id += nibble(section["Add"], index) << 8 - block_data = nibble(section['Data'], index) + block_data = nibble(section["Data"], index) block = OldBlock(block_id, block_data) if force_new: @@ -206,19 +228,31 @@ def get_block(self, x: int, y: int, z: int, section: Union[int, nbt.TAG_Compound return block # If its an empty section its most likely an air block - if section is None or 'BlockStates' not in section: - return Block.from_name('minecraft:air') + + if self.version > 2730: + blockstates_string = "block_states" + else: + blockstates_string = "BlockStates" + + if section is None or blockstates_string not in section: + return Block.from_name("minecraft:air") # Number of bits each block is on BlockStates # Cannot be lower than 4 - bits = max((len(section['Palette']) - 1).bit_length(), 4) + if self.version > 2730: + bits = max((len(section["block_states"]["palette"]) - 1).bit_length(), 4) + else: + bits = max((len(section["Palette"]) - 1).bit_length(), 4) # Get index on the block list with the order YZX - index = y * 16*16 + z * 16 + x + index = y * 16 * 16 + z * 16 + x # BlockStates is an array of 64 bit numbers # that holds the blocks index on the palette list - states = section['BlockStates'].value + if self.version > 2730: + states = section[blockstates_string]["data"].value + else: + states = section[blockstates_string].value # in 20w17a and newer blocks cannot occupy more than one element on the BlockStates array stretches = self.version is None or self.version < _VERSION_20w17a @@ -258,16 +292,31 @@ def get_block(self, x: int, y: int, z: int, section: Union[int, nbt.TAG_Compound # Next state Current state (already shifted) # 0b101010110101101010010 0b01 # will result in bin_append(0b010, 0b01, 2) = 0b01001 - shifted_data = bin_append(data & 2**leftover - 1, shifted_data, bits-leftover) + shifted_data = bin_append( + data & 2**leftover - 1, shifted_data, bits - leftover + ) # get `bits` least significant bits # which are the palette index palette_id = shifted_data & 2**bits - 1 - block = section['Palette'][palette_id] - return Block.from_palette(block) + print(palette_id) - def stream_blocks(self, index: int=0, section: Union[int, nbt.TAG_Compound]=None, force_new: bool=False) -> Generator[Block, None, None]: + # return section + + if self.version > 2730: + block = section["block_states"]["palette"][palette_id] + return Block.from_palette(block) + else: + block = section["Palette"][palette_id] + return Block.from_palette(block) + + def stream_blocks( + self, + index: int = 0, + section: Union[int, nbt.TAG_Compound] = None, + force_new: bool = False, + ) -> Generator[Block, None, None]: """ Returns a generator for all the blocks in given section @@ -295,7 +344,9 @@ def stream_blocks(self, index: int=0, section: Union[int, nbt.TAG_Compound]=None :class:`anvil.Block` """ if isinstance(section, int) and (section < 0 or section > 16): - raise OutOfBoundsCoordinates(f'section ({section!r}) must be in range of 0 to 15') + raise OutOfBoundsCoordinates( + f"section ({section!r}) must be in range of 0 to 15" + ) # For better understanding of this code, read get_block()'s source @@ -303,18 +354,18 @@ def stream_blocks(self, index: int=0, section: Union[int, nbt.TAG_Compound]=None section = self.get_section(section or 0) if self.version < _VERSION_17w47a: - if section is None or 'Blocks' not in section: - air = Block.from_name('minecraft:air') if force_new else OldBlock(0) + if section is None or "Blocks" not in section: + air = Block.from_name("minecraft:air") if force_new else OldBlock(0) for i in range(4096): yield air return while index < 4096: - block_id = section['Blocks'][index] - if 'Add' in section: - block_id += nibble(section['Add'], index) << 8 + block_id = section["Blocks"][index] + if "Add" in section: + block_id += nibble(section["Add"], index) << 8 - block_data = nibble(section['Data'], index) + block_data = nibble(section["Data"], index) block = OldBlock(block_id, block_data) if force_new: @@ -325,14 +376,14 @@ def stream_blocks(self, index: int=0, section: Union[int, nbt.TAG_Compound]=None index += 1 return - if section is None or 'BlockStates' not in section: - air = Block.from_name('minecraft:air') + if section is None or "BlockStates" not in section: + air = Block.from_name("minecraft:air") for i in range(4096): yield air return - states = section['BlockStates'].value - palette = section['Palette'] + states = section["BlockStates"].value + palette = section["Palette"] bits = max((len(palette) - 1).bit_length(), 4) @@ -380,7 +431,9 @@ def stream_blocks(self, index: int=0, section: Union[int, nbt.TAG_Compound]=None data >>= bits data_len -= bits - def stream_chunk(self, index: int=0, section: Union[int, nbt.TAG_Compound]=None) -> Generator[Block, None, None]: + def stream_chunk( + self, index: int = 0, section: Union[int, nbt.TAG_Compound] = None + ) -> Generator[Block, None, None]: """ Returns a generator for all the blocks in the chunk @@ -401,7 +454,7 @@ def get_tile_entity(self, x: int, y: int, z: int) -> Optional[nbt.TAG_Compound]: To iterate through all tile entities in the chunk, use :class:`Chunk.tile_entities` """ for tile_entity in self.tile_entities: - t_x, t_y, t_z = [tile_entity[k].value for k in 'xyz'] + t_x, t_y, t_z = [tile_entity[k].value for k in "xyz"] if x == t_x and y == t_y and z == t_z: return tile_entity @@ -424,5 +477,5 @@ def from_region(cls, region: Union[str, Region], chunk_x: int, chunk_z: int): region = Region.from_file(region) nbt_data = region.chunk_data(chunk_x, chunk_z) if nbt_data is None: - raise ChunkNotFound(f'Could not find chunk ({chunk_x}, {chunk_z})') + raise ChunkNotFound(f"Could not find chunk ({chunk_x}, {chunk_z})") return cls(nbt_data) From c6fa64f1b7933adda1bbd0d181c67a21da9bedf2 Mon Sep 17 00:00:00 2001 From: bee-boii <100041984+bee-boii@users.noreply.github.com> Date: Thu, 17 Mar 2022 06:26:54 -0400 Subject: [PATCH 08/38] Further updated for 1.18 --- anvil/chunk.py | 49 ++++++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/anvil/chunk.py b/anvil/chunk.py index a81160b..e661152 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -6,6 +6,9 @@ from .errors import OutOfBoundsCoordinates, ChunkNotFound import math +# This is the final version before the Minecraft overhaul that includes the +# 1.18 expansion of the world's vertical height from -64 to 319 +_VERSION_1_17_1 = 2730 # This version removes block state value stretching from the storage # so a block value isn't in multiple elements of the array @@ -67,10 +70,11 @@ def __init__(self, nbt_data: nbt.NBTFile): # See https://minecraft.fandom.com/wiki/Data_version self.version = None - if self.version > 2730: + if self.version > _VERSION_1_17_1: self.data = nbt_data self.x = nbt_data["xPos"].value self.z = nbt_data["zPos"].value + # Have not added 1.18 support for 'tile entities' else: self.data = nbt_data["Level"] self.x = self.data["xPos"].value @@ -92,10 +96,14 @@ def get_section(self, y: int) -> nbt.TAG_Compound: anvil.OutOfBoundsCoordinates If Y is not in range of 0 to 15 """ - if y < -4 or y > 19: - raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of 0 to 15") + if self.version > _VERSION_1_17_1: + if y < -4 or y > 19: + raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of -4 to 19") + else: + if y < 0 or y > 15: + raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of 0 to 15") - if self.version > 2730: + if self.version > _VERSION_1_17_1: try: sections = self.data["sections"] except KeyError: @@ -196,8 +204,12 @@ def get_block( raise OutOfBoundsCoordinates(f"X ({x!r}) must be in range of 0 to 15") if z < 0 or z > 15: raise OutOfBoundsCoordinates(f"Z ({z!r}) must be in range of 0 to 15") - if y < -64 or y > 319: - raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of 0 to 255") + if self.version > _VERSION_1_17_1: + if y < -64 or y > 319: + raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of -64 to 319") + else: + if y < 0 or y > 255: + raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of 0 to 255") if section is None: section = self.get_section(y // 16) @@ -229,17 +241,16 @@ def get_block( # If its an empty section its most likely an air block - if self.version > 2730: - blockstates_string = "block_states" + if self.version > _VERSION_1_17_1: + if section is None or 'block_states' not in section: + return Block.from_name("minecraft:air") else: - blockstates_string = "BlockStates" - - if section is None or blockstates_string not in section: - return Block.from_name("minecraft:air") + if section is None or 'BlockStates' not in section: + return Block.from_name("minecraft:air") # Number of bits each block is on BlockStates # Cannot be lower than 4 - if self.version > 2730: + if self.version > _VERSION_1_17_1: bits = max((len(section["block_states"]["palette"]) - 1).bit_length(), 4) else: bits = max((len(section["Palette"]) - 1).bit_length(), 4) @@ -249,10 +260,10 @@ def get_block( # BlockStates is an array of 64 bit numbers # that holds the blocks index on the palette list - if self.version > 2730: - states = section[blockstates_string]["data"].value + if self.version > _VERSION_1_17_1: + states = section['block_states']['data'].value else: - states = section[blockstates_string].value + states = section['BlockStates'].value # in 20w17a and newer blocks cannot occupy more than one element on the BlockStates array stretches = self.version is None or self.version < _VERSION_20w17a @@ -300,11 +311,7 @@ def get_block( # which are the palette index palette_id = shifted_data & 2**bits - 1 - print(palette_id) - - # return section - - if self.version > 2730: + if self.version > _VERSION_1_17_1: block = section["block_states"]["palette"][palette_id] return Block.from_palette(block) else: From 982a4342454e9fec2e62a73421c2fe2e98b28063 Mon Sep 17 00:00:00 2001 From: bee-boii <100041984+bee-boii@users.noreply.github.com> Date: Thu, 17 Mar 2022 08:21:12 -0400 Subject: [PATCH 09/38] Changed version number from 0.10.0 to 0.10.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a629a6e..22f1645 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name='anvil-parser', - version='0.10.0', + version='0.10.1', author='mat', description='A Minecraft anvil file format parser', long_description=long_description, From 71ecd6f9c2aa744629e5ffede4148d9ec8e26053 Mon Sep 17 00:00:00 2001 From: bee-boii <100041984+bee-boii@users.noreply.github.com> Date: Sat, 2 Apr 2022 13:57:09 -0400 Subject: [PATCH 10/38] Made things more flexible. Catches air blocks now. --- anvil/chunk.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/anvil/chunk.py b/anvil/chunk.py index e661152..6fff0bf 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -103,7 +103,7 @@ def get_section(self, y: int) -> nbt.TAG_Compound: if y < 0 or y > 15: raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of 0 to 15") - if self.version > _VERSION_1_17_1: + if 'sections' in self.data: try: sections = self.data["sections"] except KeyError: @@ -240,17 +240,14 @@ def get_block( return block # If its an empty section its most likely an air block - - if self.version > _VERSION_1_17_1: - if section is None or 'block_states' not in section: - return Block.from_name("minecraft:air") - else: - if section is None or 'BlockStates' not in section: - return Block.from_name("minecraft:air") + if section is None: + return Block.from_name("minecraft:air") + elif ('block_states' not in section) and ('BlockStates' not in section): + return Block.from_name("minecraft:air") # Number of bits each block is on BlockStates # Cannot be lower than 4 - if self.version > _VERSION_1_17_1: + if 'block_states' in section: bits = max((len(section["block_states"]["palette"]) - 1).bit_length(), 4) else: bits = max((len(section["Palette"]) - 1).bit_length(), 4) @@ -260,8 +257,12 @@ def get_block( # BlockStates is an array of 64 bit numbers # that holds the blocks index on the palette list - if self.version > _VERSION_1_17_1: - states = section['block_states']['data'].value + if 'block_states' in section: + if 'data' in section['block_states']: + states = section['block_states']['data'].value + else: + print('data tag is missing: chunk.py line ~264') + return Block.from_name("minecraft:air") else: states = section['BlockStates'].value @@ -311,9 +312,13 @@ def get_block( # which are the palette index palette_id = shifted_data & 2**bits - 1 - if self.version > _VERSION_1_17_1: - block = section["block_states"]["palette"][palette_id] - return Block.from_palette(block) + if 'block_states' in section: + if 'palette' in section['block_states']: + block = section["block_states"]["palette"][palette_id] + return Block.from_palette(block) + else: + print('properties tag is missing: chunk.py line ~320') + return Block.from_name("minecraft:air") else: block = section["Palette"][palette_id] return Block.from_palette(block) From acf498ecd5e2eebab234997243253b02fef31c5a Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Mon, 6 Jun 2022 22:12:10 +0100 Subject: [PATCH 11/38] small refactor --- anvil/chunk.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/anvil/chunk.py b/anvil/chunk.py index 6fff0bf..a3ed41c 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -4,7 +4,6 @@ from .block import Block, OldBlock from .region import Region from .errors import OutOfBoundsCoordinates, ChunkNotFound -import math # This is the final version before the Minecraft overhaul that includes the # 1.18 expansion of the world's vertical height from -64 to 319 @@ -72,14 +71,12 @@ def __init__(self, nbt_data: nbt.NBTFile): if self.version > _VERSION_1_17_1: self.data = nbt_data - self.x = nbt_data["xPos"].value - self.z = nbt_data["zPos"].value # Have not added 1.18 support for 'tile entities' else: self.data = nbt_data["Level"] - self.x = self.data["xPos"].value - self.z = self.data["zPos"].value self.tile_entities = self.data["TileEntities"] + self.x = self.data["xPos"].value + self.z = self.data["zPos"].value def get_section(self, y: int) -> nbt.TAG_Compound: """ @@ -104,10 +101,7 @@ def get_section(self, y: int) -> nbt.TAG_Compound: raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of 0 to 15") if 'sections' in self.data: - try: - sections = self.data["sections"] - except KeyError: - return None + sections = self.data["sections"] else: try: sections = self.data["Sections"] @@ -240,17 +234,18 @@ def get_block( return block # If its an empty section its most likely an air block - if section is None: - return Block.from_name("minecraft:air") - elif ('block_states' not in section) and ('BlockStates' not in section): + if (section is None + or 'block_states' not in section + and 'BlockStates' not in section): return Block.from_name("minecraft:air") # Number of bits each block is on BlockStates # Cannot be lower than 4 if 'block_states' in section: - bits = max((len(section["block_states"]["palette"]) - 1).bit_length(), 4) + palette = section["block_states"]["palette"] else: - bits = max((len(section["Palette"]) - 1).bit_length(), 4) + palette = section["Palette"] + bits = max((len(palette) - 1).bit_length(), 4) # Get index on the block list with the order YZX index = y * 16 * 16 + z * 16 + x @@ -268,7 +263,6 @@ def get_block( # in 20w17a and newer blocks cannot occupy more than one element on the BlockStates array stretches = self.version is None or self.version < _VERSION_20w17a - # stretches = True # get location in the BlockStates array via the index if stretches: From bd674411bf29a5ceb454d7da9d34332388b57cda Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Tue, 7 Jun 2022 19:52:10 +0100 Subject: [PATCH 12/38] fix: make stream_blocks and stream_chunk 1.19 compatible --- anvil/chunk.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/anvil/chunk.py b/anvil/chunk.py index a3ed41c..9e40729 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -382,15 +382,26 @@ def stream_blocks( index += 1 return - if section is None or "BlockStates" not in section: + if (section is None + or "BlockStates" not in section + and 'block_states' not in section): air = Block.from_name("minecraft:air") for i in range(4096): yield air return - states = section["BlockStates"].value - palette = section["Palette"] + if 'block_states' in section: + if 'data' in section['block_states']: + states = section['block_states']['data'].value + else: + air = Block.from_name("minecraft:air") + for i in range(4096): + yield air + return + else: + states = section['BlockStates'].value + palette = section['block_states']['palette'] bits = max((len(palette) - 1).bit_length(), 4) stretches = self.version < _VERSION_20w17a @@ -449,7 +460,16 @@ def stream_chunk( ------ :class:`anvil.Block` """ - for section in range(16): + + if 'sections' in self.data: + sections = self.data["sections"] + else: + try: + sections = self.data["Sections"] + except KeyError: + raise StopIteration + + for section in sections: for block in self.stream_blocks(section=section): yield block From 220c28381fd229413497c491c6f67ea716ef6e24 Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Tue, 7 Jun 2022 20:53:51 +0100 Subject: [PATCH 13/38] refactor: add _.._from_section fns --- anvil/chunk.py | 91 ++++++++++++++++++++++---------------------------- 1 file changed, 39 insertions(+), 52 deletions(-) diff --git a/anvil/chunk.py b/anvil/chunk.py index 9e40729..5aaaab1 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -39,6 +39,22 @@ def nibble(byte_array, index): return value & 0b1111 +def _palette_from_section(section: nbt.TAG_Compound) -> nbt.TAG_List: + if 'block_states' in section: + return section["block_states"]["palette"] + else: + return section["Palette"] + + +def _states_from_section(section: nbt.TAG_Compound) -> list: + # BlockStates is an array of 64 bit numbers + # that holds the blocks index on the palette list + if 'block_states' in section: + return section['block_states']['data'].value + else: + return section['BlockStates'].value + + class Chunk: """ Represents a chunk from a ``.mca`` file. @@ -128,7 +144,8 @@ def get_palette(self, section: Union[int, nbt.TAG_Compound]) -> Tuple[Block]: section = self.get_section(section) if section is None: return - return tuple(Block.from_palette(i) for i in section["Palette"]) + palette = _palette_from_section(section) + return tuple(Block.from_palette(i) for i in palette) def get_biome(self, x: int, y: int, z: int) -> Biome: """ @@ -234,33 +251,21 @@ def get_block( return block # If its an empty section its most likely an air block - if (section is None - or 'block_states' not in section - and 'BlockStates' not in section): + if section is None: return Block.from_name("minecraft:air") - + try: + states = _states_from_section(section) + except KeyError: + return Block.from_name("minecraft:air") + # Number of bits each block is on BlockStates # Cannot be lower than 4 - if 'block_states' in section: - palette = section["block_states"]["palette"] - else: - palette = section["Palette"] + palette = _palette_from_section(section) + bits = max((len(palette) - 1).bit_length(), 4) # Get index on the block list with the order YZX index = y * 16 * 16 + z * 16 + x - - # BlockStates is an array of 64 bit numbers - # that holds the blocks index on the palette list - if 'block_states' in section: - if 'data' in section['block_states']: - states = section['block_states']['data'].value - else: - print('data tag is missing: chunk.py line ~264') - return Block.from_name("minecraft:air") - else: - states = section['BlockStates'].value - # in 20w17a and newer blocks cannot occupy more than one element on the BlockStates array stretches = self.version is None or self.version < _VERSION_20w17a @@ -305,17 +310,7 @@ def get_block( # get `bits` least significant bits # which are the palette index palette_id = shifted_data & 2**bits - 1 - - if 'block_states' in section: - if 'palette' in section['block_states']: - block = section["block_states"]["palette"][palette_id] - return Block.from_palette(block) - else: - print('properties tag is missing: chunk.py line ~320') - return Block.from_name("minecraft:air") - else: - block = section["Palette"][palette_id] - return Block.from_palette(block) + return Block.from_palette(palette[palette_id]) def stream_blocks( self, @@ -362,7 +357,7 @@ def stream_blocks( if self.version < _VERSION_17w47a: if section is None or "Blocks" not in section: air = Block.from_name("minecraft:air") if force_new else OldBlock(0) - for i in range(4096): + for _ in range(4096): yield air return @@ -382,26 +377,19 @@ def stream_blocks( index += 1 return - if (section is None - or "BlockStates" not in section - and 'block_states' not in section): - air = Block.from_name("minecraft:air") - for i in range(4096): + air = Block.from_name("minecraft:air") + if section is None: + for _ in range(4096): + yield air + return + try: + states = _states_from_section(section) + except KeyError: + for _ in range(4096): yield air return - if 'block_states' in section: - if 'data' in section['block_states']: - states = section['block_states']['data'].value - else: - air = Block.from_name("minecraft:air") - for i in range(4096): - yield air - return - else: - states = section['BlockStates'].value - - palette = section['block_states']['palette'] + palette = _palette_from_section(section) bits = max((len(palette) - 1).bit_length(), 4) stretches = self.version < _VERSION_20w17a @@ -412,8 +400,7 @@ def stream_blocks( state = index // (64 // bits) data = states[state] - if data < 0: - data += 2**64 + if data < 0: data += 2**64 bits_mask = 2**bits - 1 From cda7c8482a7a2678922852d94dac54f90a76ee4f Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Tue, 7 Jun 2022 21:07:10 +0100 Subject: [PATCH 14/38] feat: add support for >1.17.1 tile entities --- anvil/chunk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anvil/chunk.py b/anvil/chunk.py index 5aaaab1..ce4b00a 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -87,7 +87,7 @@ def __init__(self, nbt_data: nbt.NBTFile): if self.version > _VERSION_1_17_1: self.data = nbt_data - # Have not added 1.18 support for 'tile entities' + self.tile_entities = self.data["block_entities"] else: self.data = nbt_data["Level"] self.tile_entities = self.data["TileEntities"] From 87fa699134ff0fda625c077a3c6b1f556dae8552 Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Tue, 7 Jun 2022 22:10:17 +0100 Subject: [PATCH 15/38] refactor: use range and stream_chunk in correct order --- anvil/chunk.py | 62 ++++++++++++++++++++---------------------- anvil/empty_chunk.py | 17 ++++++------ anvil/empty_region.py | 2 +- anvil/empty_section.py | 2 +- 4 files changed, 40 insertions(+), 43 deletions(-) diff --git a/anvil/chunk.py b/anvil/chunk.py index ce4b00a..8b43cf8 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -55,6 +55,13 @@ def _states_from_section(section: nbt.TAG_Compound) -> list: return section['BlockStates'].value +def _section_height_range(version: Optional[int]) -> range: + if version is not None and version > _VERSION_17w47a: + return range(-4, 20) + else: + return range(16) + + class Chunk: """ Represents a chunk from a ``.mca`` file. @@ -109,12 +116,10 @@ def get_section(self, y: int) -> nbt.TAG_Compound: anvil.OutOfBoundsCoordinates If Y is not in range of 0 to 15 """ - if self.version > _VERSION_1_17_1: - if y < -4 or y > 19: - raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of -4 to 19") - else: - if y < 0 or y > 15: - raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of 0 to 15") + section_range = _section_height_range(self.version) + if y not in section_range: + raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of " + f"{section_range.start} to {section_range.stop}") if 'sections' in self.data: sections = self.data["sections"] @@ -163,12 +168,14 @@ def get_biome(self, x: int, y: int, z: int) -> Biome: :rtype: :class:`anvil.Biome` """ - if x < 0 or x > 15: + section_range = _section_height_range(self.version) + if x not in range(16): raise OutOfBoundsCoordinates(f"X ({x!r}) must be in range of 0 to 15") - if z < 0 or z > 15: + if z not in range(16): raise OutOfBoundsCoordinates(f"Z ({z!r}) must be in range of 0 to 15") - if y < 0 or y > 255: - raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of 0 to 255") + if y // 16 not in section_range: + raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of " + f"{section_range.start * 16} to {section_range.stop * 16 - 1}") biomes = self.data["Biomes"] if self.version < _VERSION_19w36a: @@ -211,16 +218,14 @@ def get_block( :rtype: :class:`anvil.Block` """ - if x < 0 or x > 15: + if x not in range(16): raise OutOfBoundsCoordinates(f"X ({x!r}) must be in range of 0 to 15") - if z < 0 or z > 15: + if z not in range(16): raise OutOfBoundsCoordinates(f"Z ({z!r}) must be in range of 0 to 15") - if self.version > _VERSION_1_17_1: - if y < -64 or y > 319: - raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of -64 to 319") - else: - if y < 0 or y > 255: - raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of 0 to 255") + section_range = _section_height_range(self.version) + if y // 16 not in section_range: + raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of " + f"{section_range.start * 16} to {section_range.stop * 16 - 1}") if section is None: section = self.get_section(y // 16) @@ -344,10 +349,12 @@ def stream_blocks( ------ :class:`anvil.Block` """ - if isinstance(section, int) and (section < 0 or section > 16): - raise OutOfBoundsCoordinates( - f"section ({section!r}) must be in range of 0 to 15" - ) + + if isinstance(section, int): + section_range = _section_height_range(self.version) + if section not in section_range: + raise OutOfBoundsCoordinates(f"section ({section!r}) must be in range of " + f"{section_range.start} to {section_range.stop}") # For better understanding of this code, read get_block()'s source @@ -447,16 +454,7 @@ def stream_chunk( ------ :class:`anvil.Block` """ - - if 'sections' in self.data: - sections = self.data["sections"] - else: - try: - sections = self.data["Sections"] - except KeyError: - raise StopIteration - - for section in sections: + for section in _section_height_range(self.version): for block in self.stream_blocks(section=section): yield block diff --git a/anvil/empty_chunk.py b/anvil/empty_chunk.py index 924cd2d..6fe901b 100644 --- a/anvil/empty_chunk.py +++ b/anvil/empty_chunk.py @@ -70,11 +70,11 @@ def get_block(self, x: int, y: int, z: int) -> Block: Returns ``None`` if the section is empty, meaning the block is most likely an air block. """ - if x < 0 or x > 15: + if x not in range(16): raise OutOfBoundsCoordinates(f'X ({x!r}) must be in range of 0 to 15') - if z < 0 or z > 15: + if z not in range(16): raise OutOfBoundsCoordinates(f'Z ({z!r}) must be in range of 0 to 15') - if y < 0 or y > 255: + if y not in range(256): raise OutOfBoundsCoordinates(f'Y ({y!r}) must be in range of 0 to 255') section = self.sections[y // 16] if section is None: @@ -96,13 +96,12 @@ def set_block(self, block: Block, x: int, y: int, z: int): ------ anvil.OutOfBoundCoordidnates If X, Y or Z are not in the proper range - """ - if x < 0 or x > 15: + if x not in range(16): raise OutOfBoundsCoordinates(f'X ({x!r}) must be in range of 0 to 15') - if z < 0 or z > 15: + if z not in range(16): raise OutOfBoundsCoordinates(f'Z ({z!r}) must be in range of 0 to 15') - if y < 0 or y > 255: + if y not in range(256): raise OutOfBoundsCoordinates(f'Y ({y!r}) must be in range of 0 to 255') section = self.sections[y // 16] if section is None: @@ -125,9 +124,9 @@ def set_biome(self, biome: Biome, x: int, z: int): If X or Z are not in the proper range """ - if x < 0 or x > 15: + if x not in range(16): raise OutOfBoundsCoordinates(f'X ({x!r}) must be in range of 0 to 15') - if z < 0 or z > 15: + if z not in range(16): raise OutOfBoundsCoordinates(f'Z ({z!r}) must be in range of 0 to 15') index = z * 16 + x diff --git a/anvil/empty_region.py b/anvil/empty_region.py index 463c73c..2dc9132 100644 --- a/anvil/empty_region.py +++ b/anvil/empty_region.py @@ -47,7 +47,7 @@ def inside(self, x: int, y: int, z: int, chunk: bool=False) -> bool: factor = 32 if chunk else 512 rx = x // factor rz = z // factor - return not (rx != self.x or rz != self.z or y < 0 or y > 255) + return not (rx != self.x or rz != self.z or y not in range(256)) def get_chunk(self, x: int, z: int) -> EmptyChunk: """ diff --git a/anvil/empty_section.py b/anvil/empty_section.py index 315a28d..4e4e448 100644 --- a/anvil/empty_section.py +++ b/anvil/empty_section.py @@ -49,7 +49,7 @@ def inside(x: int, y: int, z: int) -> bool: int x, y, z Coordinates """ - return x >= 0 and x <= 15 and y >= 0 and y <= 15 and z >= 0 and z <= 15 + return x in range(16) and y in range(16) and z in range(16) def set_block(self, block: Block, x: int, y: int, z: int): """ From 810366261e9fdc5ca9a0ebb8b326e906bef1edb6 Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Tue, 7 Jun 2022 22:16:49 +0100 Subject: [PATCH 16/38] refactor: simplify Block.from_palette --- anvil/block.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/anvil/block.py b/anvil/block.py index a9e0d7e..06d120e 100644 --- a/anvil/block.py +++ b/anvil/block.py @@ -79,9 +79,7 @@ def from_palette(cls, tag: nbt.TAG_Compound): Raw tag from a section's palette """ name = tag['Name'].value - properties = tag.get('Properties') - if properties: - properties = dict(properties) + properties = dict(tag.get('Properties', dict())) return cls.from_name(name, properties=properties) @classmethod @@ -104,6 +102,7 @@ def from_numeric_id(cls, block_id: int, data: int=0): name, properties = LEGACY_ID_MAP[key] return cls('minecraft', name, properties=properties) + class OldBlock: """ Represents a pre 1.13 minecraft block, with a numeric id. From cd329a229ea7388ed7fededb1407cfaabbfaf993 Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Tue, 7 Jun 2022 22:26:33 +0100 Subject: [PATCH 17/38] refactor: greedily add 2**64 to states --- anvil/chunk.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/anvil/chunk.py b/anvil/chunk.py index 8b43cf8..77629ee 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -50,9 +50,12 @@ def _states_from_section(section: nbt.TAG_Compound) -> list: # BlockStates is an array of 64 bit numbers # that holds the blocks index on the palette list if 'block_states' in section: - return section['block_states']['data'].value + states = section['block_states']['data'] else: - return section['BlockStates'].value + states = section['BlockStates'] + + return [state if state >= 0 else states + 2 ** 64 + for state in states.value] def _section_height_range(version: Optional[int]) -> range: @@ -284,8 +287,6 @@ def get_block( # by adding 2^64 # could also use ctypes.c_ulonglong(n).value but that'd require an extra import data = states[state] - if data < 0: - data += 2**64 if stretches: # shift the number to the right to remove the left over bits @@ -297,8 +298,6 @@ def get_block( # if there aren't enough bits it means the rest are in the next number if stretches and 64 - ((bits * index) % 64) < bits: data = states[state + 1] - if data < 0: - data += 2**64 # get how many bits are from a palette index of the next block leftover = (bits - ((state + 1) * 64 % bits)) % bits @@ -407,7 +406,6 @@ def stream_blocks( state = index // (64 // bits) data = states[state] - if data < 0: data += 2**64 bits_mask = 2**bits - 1 @@ -423,8 +421,6 @@ def stream_blocks( if data_len < bits: state += 1 new_data = states[state] - if new_data < 0: - new_data += 2**64 if stretches: leftover = data_len From ddbfff60c10aca1f2c5860e2f8582c4bccd786cb Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Thu, 16 Jun 2022 21:16:03 +0100 Subject: [PATCH 18/38] feat: add 1.18 biome support --- anvil/biome.py | 155 +++++++++++++++++++-------------------- anvil/chunk.py | 69 +++++++++++++---- anvil/legacy.py | 3 + anvil/legacy_biomes.json | 81 ++++++++++++++++++++ 4 files changed, 214 insertions(+), 94 deletions(-) create mode 100644 anvil/legacy_biomes.json diff --git a/anvil/biome.py b/anvil/biome.py index c0039f5..b6d43f0 100644 --- a/anvil/biome.py +++ b/anvil/biome.py @@ -1,82 +1,75 @@ -from enum import Enum, auto +from .legacy import LEGACY_BIOMES_ID_MAP -class Biome(Enum): - Ocean = 0 - Forest = 4 - River = 7 - FrozenOcean = 10 - FrozenRiver = 11 - Beach = 16 - DeepOcean = 24 - StoneShore = 25 - SnowyBeach = 26 - WarmOcean = 44 - LukewarmOcean = 45 - ColdOcean = 46 - DeepWarmOcean = 47 - DeepLukewarmOcean = 48 - DeepColdOcean = 49 - DeepFrozenOcean = 50 - WoodedHills = 18 - FlowerForest = 132 - BirchForest = 27 - BirchForestHills = 28 - TallBirchForest = 155 - TallBirchHills = 156 - DarkForest = 29 - DarkForestHills = 157 - Jungle = 21 - JungleHills = 22 - ModifiedJungle = 149 - JungleEdge = 23 - ModifiedJungleEdge = 151 - BambooJungle = 168 - BambooJungleHills = 169 - Taiga = 5 - TaigaHills = 19 - TaigaMountains = 133 - SnowyTaiga = 30 - SnowyTaigaHills = 31 - SnowyTaigaMountains = 158 - GiantTreeTaiga = 32 - GiantTreeTaigaHills = 33 - GiantSpruceTaiga = 160 - GiantSpruceTaigaHills = 161 - MushroomFields = 14 - MushroomFieldShore = 15 - Swamp = 6 - SwampHills = 134 - Savanna = 35 - SavannaPlateau = 36 - ShatteredSavanna = 163 - ShatteredSavannaPlateau = 164 - Plains = 1 - SunflowerPlains = 129 - Desert = 2 - DesertHills = 17 - DesertLakes = 130 - SnowyTundra = 12 - SnowyMountains = 13 - IceSpikes = 140 - Mountains = 3 - WoodedMountains = 34 - GravellyMountains = 131 - ModifiedGravellyMountains = 162 - MountainEdge = 20 - Badlands = 37 - BadlandsPlateau = 39 - ModifiedBadlandsPlateau = 167 - WoodedBadlandsPlateau = 38 - ModifiedWoodedBadlandsPlateau = 166 - ErodedBadlands = 165 - Nether = 8 - TheEnd = 9 - SmallEndIslands = 40 - EndMidlands = 41 - EndHighlands = 42 - EndBarrens = 43 - SoulSandValley = 170 - CrimsonForest = 171 - WarpedForest = 172 - TheVoid = 127 - BasaltDeltas = 173 \ No newline at end of file +class Biome: + """ + Represents a minecraft biome. + + Attributes + ---------- + namespace: :class:`str` + Namespace of the biome, most of the time this is ``minecraft`` + id: :class:`str` + ID of the biome, for example: forest, warm_ocean, etc... + """ + __slots__ = ('namespace', 'id') + + def __init__(self, namespace: str, biome_id: str=None): + """ + Parameters + ---------- + namespace + Namespace of the biome. If no biome_id is given, assume this is ``biome_id`` and set namespace to ``"minecraft"`` + biome_id + ID of the biome + """ + if biome_id is None: + self.namespace = 'minecraft' + self.id = namespace + else: + self.namespace = namespace + self.id = biome_id + + def name(self) -> str: + """ + Returns the biome in the ``minecraft:biome_id`` format + """ + return self.namespace + ':' + self.id + + def __repr__(self): + return f'Biome({self.name()})' + + def __eq__(self, other): + if not isinstance(other, Biome): + return False + return self.namespace == other.namespace and self.id == other.id + + def __hash__(self): + return hash(self.name()) + + @classmethod + def from_name(cls, name: str): + """ + Creates a new Biome from the format: ``namespace:biome_id`` + + Parameters + ---------- + name + Biome in said format + """ + namespace, biome_id = name.split(':') + return cls(namespace, biome_id) + + @classmethod + def from_numeric_id(cls, biome_id: int): + """ + Creates a new Biome from the numeric biome_id format + + Parameters + ---------- + biome_id + Numeric ID of the biome + """ + if biome_id not in LEGACY_BIOMES_ID_MAP: + raise KeyError(f'Biome {biome_id} not found') + name = LEGACY_BIOMES_ID_MAP[biome_id] + return cls('minecraft', name) \ No newline at end of file diff --git a/anvil/chunk.py b/anvil/chunk.py index 77629ee..9843fb1 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -54,6 +54,10 @@ def _states_from_section(section: nbt.TAG_Compound) -> list: else: states = section['BlockStates'] + # makes sure the number is unsigned + # by adding 2^64 + # could also use ctypes.c_ulonglong(n).value but that'd require an extra import + return [state if state >= 0 else states + 2 ** 64 for state in states.value] @@ -179,18 +183,60 @@ def get_biome(self, x: int, y: int, z: int) -> Biome: if y // 16 not in section_range: raise OutOfBoundsCoordinates(f"Y ({y!r}) must be in range of " f"{section_range.start * 16} to {section_range.stop * 16 - 1}") + + if 'Biomes' not in self.data: + # Each biome index refers to a 4x4x4 volumes here so we do integer division by 4 + section = self.get_section(y // 16) + biomes = section['biomes'] + biomes_palette = biomes['palette'] + if 'data' in biomes: + biomes = biomes['data'] + else: + # When there is only one biome in the section of the palette 'data' + # is not present + return Biome.from_name(biomes_palette[0].value) + + + index = ((y % 16 // 4) * 4 * 4) + (z // 4) * 4 + (x // 4) + bits = (len(biomes_palette) - 1).bit_length() + state = index * bits // 64 + data = biomes[state] + + # shift the number to the right to remove the left over bits + # and shift so the i'th biome is the first one + shifted_data = data >> ((bits * index) % 64) + + # if there aren't enough bits it means the rest are in the next number + if 64 - ((bits * index) % 64) < bits: + data = biomes[state + 1] + + # get how many bits are from a palette index of the next biome + leftover = (bits - ((state + 1) * 64 % bits)) % bits + + # Make sure to keep the length of the bits in the first state + # Example: bits is 5, and leftover is 3 + # Next state Current state (already shifted) + # 0b101010110101101010010 0b01 + # will result in bin_append(0b010, 0b01, 2) = 0b01001 + shifted_data = bin_append( + data & 2**leftover - 1, shifted_data, bits - leftover + ) + + palette_id = shifted_data & 2**bits - 1 + return Biome.from_name(biomes_palette[palette_id].value) - biomes = self.data["Biomes"] - if self.version < _VERSION_19w36a: - # Each biome index refers to a column stored Z then X. - index = z * 16 + x else: - # https://minecraft.fandom.com/wiki/Java_Edition_19w36a - # Get index on the biome list with the order YZX - # Each biome index refers to a 4x4 volumes here so we do integer division by 4 - index = (y // 4) * 4 * 4 + (z // 4) * 4 + (x // 4) - biome_id = biomes[index] - return Biome(biome_id) + biomes = self.data["Biomes"] + if self.version < _VERSION_19w36a: + # Each biome index refers to a column stored Z then X. + index = z * 16 + x + else: + # https://minecraft.fandom.com/wiki/Java_Edition_19w36a + # Get index on the biome list with the order YZX + # Each biome index refers to a 4x4 areas here so we do integer division by 4 + index = (y // 4) * 4 * 4 + (z // 4) * 4 + (x // 4) + biome_id = biomes[index] + return Biome.from_numeric_id(biome_id) def get_block( self, @@ -283,9 +329,6 @@ def get_block( else: state = index // (64 // bits) - # makes sure the number is unsigned - # by adding 2^64 - # could also use ctypes.c_ulonglong(n).value but that'd require an extra import data = states[state] if stretches: diff --git a/anvil/legacy.py b/anvil/legacy.py index 1dbb28b..df78628 100644 --- a/anvil/legacy.py +++ b/anvil/legacy.py @@ -3,3 +3,6 @@ with open(os.path.join(os.path.dirname(__file__), 'legacy_blocks.json'), 'r') as file: LEGACY_ID_MAP = json.load(file) + +with open(os.path.join(os.path.dirname(__file__), 'legacy_biomes.json'), 'r') as file: + LEGACY_BIOMES_ID_MAP = {int(k):v for k, v in json.load(file).items()} \ No newline at end of file diff --git a/anvil/legacy_biomes.json b/anvil/legacy_biomes.json new file mode 100644 index 0000000..5284d9a --- /dev/null +++ b/anvil/legacy_biomes.json @@ -0,0 +1,81 @@ +{ +"0": "ocean", +"4": "forest", +"7": "river", +"10": "frozen_ocean", +"11": "frozen_river", +"16": "beach", +"24": "deep_ocean", +"25": "stone_shore", +"26": "snowy_beach", +"44": "warm_ocean", +"45": "lukewarm_ocean", +"46": "cold_ocean", +"47": "deep_warm_ocean", +"48": "deep_lukewarm_ocean", +"49": "deep_cold_ocean", +"50": "deep_frozen_ocean", +"18": "wooded_hills", +"132": "flower_forest", +"27": "birch_forest", +"28": "birch_forest_hills", +"155": "tall_birch_forest", +"156": "tall_birch_hills", +"29": "dark_forest", +"157": "dark_forest_hills", +"21": "jungle", +"22": "jungle_hills", +"149": "modified_jungle", +"23": "jungle_edge", +"151": "modified_jungle_edge", +"168": "bamboo_jungle", +"169": "bamboo_jungle_hills", +"5": "taiga", +"19": "taiga_hills", +"133": "taiga_mountains", +"30": "snowy_taiga", +"31": "snowy_taiga_hills", +"158": "snowy_taiga_mountains", +"32": "giant_tree_taiga", +"33": "giant_tree_taiga_hills", +"160": "giant_spruce_taiga", +"161": "giant_spruce_taiga_hills", +"14": "mushroom_fields", +"15": "mushroom_field_shore", +"6": "swamp", +"134": "swamp_hills", +"35": "savanna", +"36": "savanna_plateau", +"163": "shattered_savanna", +"164": "shattered_savanna_plateau", +"1": "plains", +"129": "sunflower_plains", +"2": "desert", +"17": "desert_hills", +"130": "desert_lakes", +"12": "snowy_tundra", +"13": "snowy_mountains", +"140": "ice_spikes", +"3": "mountains", +"34": "wooded_mountains", +"131": "gravelly_mountains", +"162": "modified_gravelly_mountains", +"20": "mountain_edge", +"37": "badlands", +"39": "badlands_plateau", +"167": "modified_badlands_plateau", +"38": "wooded_badlands_plateau", +"166": "modified_wooded_badlands_plateau", +"165": "eroded_badlands", +"8": "nether", +"9": "the_end", +"40": "small_end_islands", +"41": "end_midlands", +"42": "end_highlands", +"43": "end_barrens", +"170": "soul_sand_valley", +"171": "crimson_forest", +"172": "warped_forest", +"127": "the_void", +"173": "basalt_deltas" +} \ No newline at end of file From e77b7172130f123b60ae00189f79d17173253fa0 Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Thu, 16 Jun 2022 21:23:51 +0100 Subject: [PATCH 19/38] chore: update README & version --- README.md | 3 ++- setup.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b788de5..aaf6001 100644 --- a/README.md +++ b/README.md @@ -62,4 +62,5 @@ region.save('r.0.0.mca') - [ ] More tests - [ ] Tests for 20w17a+ BlockStates format # Note -Testing done in 1.14.4 and 1.15.2, should work fine for other versions. +Testing done in 1.14.4 - 1.19, should work fine for other versions. +Writing chunks and regions is broken from 1.16 onwards diff --git a/setup.py b/setup.py index 22f1645..d484bed 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name='anvil-parser', - version='0.10.1', + version='0.10.2', author='mat', description='A Minecraft anvil file format parser', long_description=long_description, From a10bc40f5cbfa678a4f27e5542f28287d93485ba Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Thu, 16 Jun 2022 21:36:33 +0100 Subject: [PATCH 20/38] fix: set_biome works with new Biome class --- anvil/empty_chunk.py | 7 ++++++- setup.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/anvil/empty_chunk.py b/anvil/empty_chunk.py index 6fe901b..638c5c6 100644 --- a/anvil/empty_chunk.py +++ b/anvil/empty_chunk.py @@ -4,6 +4,7 @@ from .empty_section import EmptySection from .errors import OutOfBoundsCoordinates, EmptySectionAlreadyExists from nbt import nbt +from .legacy import LEGACY_BIOMES_ID_MAP class EmptyChunk: """ @@ -130,7 +131,11 @@ def set_biome(self, biome: Biome, x: int, z: int): raise OutOfBoundsCoordinates(f'Z ({z!r}) must be in range of 0 to 15') index = z * 16 + x - self.biomes[index] = biome + for k, v in LEGACY_BIOMES_ID_MAP.items(): + if v == biome.id: + self.biomes[index] = k + break + raise ValueError(f'Biome id "{biome.id}" not valid') def save(self) -> nbt.NBTFile: """ diff --git a/setup.py b/setup.py index d484bed..68d0bcc 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name='anvil-parser', - version='0.10.2', + version='0.10.3', author='mat', description='A Minecraft anvil file format parser', long_description=long_description, From 1c6b750bc96ff06ea0c99d657882c66efddb19f1 Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Thu, 16 Jun 2022 21:36:33 +0100 Subject: [PATCH 21/38] fix: set_biome works with new Biome class --- anvil/empty_chunk.py | 9 +++++++-- setup.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/anvil/empty_chunk.py b/anvil/empty_chunk.py index 6fe901b..d7b4443 100644 --- a/anvil/empty_chunk.py +++ b/anvil/empty_chunk.py @@ -4,6 +4,7 @@ from .empty_section import EmptySection from .errors import OutOfBoundsCoordinates, EmptySectionAlreadyExists from nbt import nbt +from .legacy import LEGACY_BIOMES_ID_MAP class EmptyChunk: """ @@ -130,7 +131,11 @@ def set_biome(self, biome: Biome, x: int, z: int): raise OutOfBoundsCoordinates(f'Z ({z!r}) must be in range of 0 to 15') index = z * 16 + x - self.biomes[index] = biome + for k, v in LEGACY_BIOMES_ID_MAP.items(): + if v == biome.id: + self.biomes[index] = k + break + raise ValueError(f'Biome id "{biome.id}" not valid') def save(self) -> nbt.NBTFile: """ @@ -160,7 +165,7 @@ def save(self) -> nbt.NBTFile: ]) sections = nbt.TAG_List(name='Sections', type=nbt.TAG_Compound) biomes = nbt.TAG_Int_Array(name='Biomes') - biomes.value = [biome.value for biome in self.biomes] + biomes.value = [biome for biome in self.biomes] for s in self.sections: if s: p = s.palette() diff --git a/setup.py b/setup.py index d484bed..68d0bcc 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name='anvil-parser', - version='0.10.2', + version='0.10.3', author='mat', description='A Minecraft anvil file format parser', long_description=long_description, From f94f0e6edf2c3f36dc620b6424695e0905e8ed5e Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Fri, 17 Jun 2022 19:53:24 +0100 Subject: [PATCH 22/38] fix: solve failing tests with _get_legacy_biome_id --- anvil/empty_chunk.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/anvil/empty_chunk.py b/anvil/empty_chunk.py index d7b4443..d1badd7 100644 --- a/anvil/empty_chunk.py +++ b/anvil/empty_chunk.py @@ -6,6 +6,14 @@ from nbt import nbt from .legacy import LEGACY_BIOMES_ID_MAP + +def _get_legacy_biome_id(biome: Biome) -> int: + for k, v in LEGACY_BIOMES_ID_MAP.items(): + if v == biome.id: + return k + raise ValueError(f'Biome id "{biome.id}" has no legacy equivalent') + + class EmptyChunk: """ Used for making own chunks @@ -26,7 +34,7 @@ def __init__(self, x: int, z: int): self.x = x self.z = z self.sections: List[EmptySection] = [None]*16 - self.biomes: List[Biome] = [Biome(0)]*16*16 + self.biomes: List[Biome] = [Biome('ocean')]*16*16 self.version = 1976 def add_section(self, section: EmptySection, replace: bool = True): @@ -131,11 +139,7 @@ def set_biome(self, biome: Biome, x: int, z: int): raise OutOfBoundsCoordinates(f'Z ({z!r}) must be in range of 0 to 15') index = z * 16 + x - for k, v in LEGACY_BIOMES_ID_MAP.items(): - if v == biome.id: - self.biomes[index] = k - break - raise ValueError(f'Biome id "{biome.id}" not valid') + self.biomes[index] = biome def save(self) -> nbt.NBTFile: """ @@ -165,7 +169,8 @@ def save(self) -> nbt.NBTFile: ]) sections = nbt.TAG_List(name='Sections', type=nbt.TAG_Compound) biomes = nbt.TAG_Int_Array(name='Biomes') - biomes.value = [biome for biome in self.biomes] + + biomes.value = [_get_legacy_biome_id(biome) for biome in self.biomes] for s in self.sections: if s: p = s.palette() From 27544bb41f035617be130a209a84371cbaf837cc Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Sat, 18 Jun 2022 21:28:14 +0100 Subject: [PATCH 23/38] fix: include missing legacy_biomes.json --- MANIFEST.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index aef7716..b09f078 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ -include anvil/legacy_blocks.json \ No newline at end of file +include anvil/legacy_blocks.json +include anvil/legacy_biomes.json From 63740dc994a15125e3237c105945bae2d984d8c3 Mon Sep 17 00:00:00 2001 From: Ryan Moore Date: Fri, 11 Aug 2023 18:23:21 +1200 Subject: [PATCH 24/38] change version constant for top level tile_entity nbt tag from 1.17.1 to v21w43a --- anvil/chunk.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/anvil/chunk.py b/anvil/chunk.py index 9843fb1..2938f9e 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -5,9 +5,9 @@ from .region import Region from .errors import OutOfBoundsCoordinates, ChunkNotFound -# This is the final version before the Minecraft overhaul that includes the -# 1.18 expansion of the world's vertical height from -64 to 319 -_VERSION_1_17_1 = 2730 +# This version removes the chunk's "Level" NBT tag and moves all contained tags to the top level +# https://minecraft.fandom.com/wiki/Java_Edition_21w43a +_VERSION_21w43a = 2844 # This version removes block state value stretching from the storage # so a block value isn't in multiple elements of the array @@ -99,7 +99,7 @@ def __init__(self, nbt_data: nbt.NBTFile): # See https://minecraft.fandom.com/wiki/Data_version self.version = None - if self.version > _VERSION_1_17_1: + if self.version >= _VERSION_21w43a: self.data = nbt_data self.tile_entities = self.data["block_entities"] else: From c7cb5ddee0b704fcef92ddefa8989f1ac9f7355d Mon Sep 17 00:00:00 2001 From: Misode Date: Tue, 26 Sep 2023 15:00:52 +0200 Subject: [PATCH 25/38] Update Minecraft Wiki links to new domain after fork --- anvil/chunk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/anvil/chunk.py b/anvil/chunk.py index 2938f9e..33b78cf 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -6,7 +6,7 @@ from .errors import OutOfBoundsCoordinates, ChunkNotFound # This version removes the chunk's "Level" NBT tag and moves all contained tags to the top level -# https://minecraft.fandom.com/wiki/Java_Edition_21w43a +# https://minecraft.wiki/w/Java_Edition_21w43a _VERSION_21w43a = 2844 # This version removes block state value stretching from the storage @@ -14,7 +14,7 @@ _VERSION_20w17a = 2529 # This version changes how biomes are stored to allow for biomes at different heights -# https://minecraft.fandom.com/wiki/Java_Edition_19w36a +# https://minecraft.wiki/w/Java_Edition_19w36a _VERSION_19w36a = 2203 # This is the version where "The Flattening" (https://minecraft.gamepedia.com/Java_Edition_1.13/Flattening) happened @@ -96,7 +96,7 @@ def __init__(self, nbt_data: nbt.NBTFile): self.version = nbt_data["DataVersion"].value except KeyError: # Version is pre-1.9 snapshot 15w32a, so world does not have a Data Version. - # See https://minecraft.fandom.com/wiki/Data_version + # See https://minecraft.wiki/w/Data_version self.version = None if self.version >= _VERSION_21w43a: From 123a788bd969be74c08ed3035fce6939790ed403 Mon Sep 17 00:00:00 2001 From: Misode Date: Wed, 27 Sep 2023 00:58:22 +0200 Subject: [PATCH 26/38] Update gamepedia links --- README.md | 2 +- anvil/block.py | 4 ++-- anvil/chunk.py | 4 ++-- docs/index.rst | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index aaf6001..6d4e6c1 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Tests](https://github.com/matcool/anvil-parser/actions/workflows/run-pytest.yml/badge.svg)](https://github.com/matcool/anvil-parser/actions/workflows/run-pytest.yml) [![PyPI - Downloads](https://img.shields.io/pypi/dm/anvil-parser)](https://pypi.org/project/anvil-parser/) -Simple parser for the [Minecraft anvil file format](https://minecraft.gamepedia.com/Anvil_file_format) +Simple parser for the [Minecraft anvil file format](https://minecraft.wiki/w/Anvil_file_format) # Installation This project is available on [PyPI](https://pypi.org/project/anvil-parser/) and can be installed with pip ``` diff --git a/anvil/block.py b/anvil/block.py index 06d120e..67a4ca4 100644 --- a/anvil/block.py +++ b/anvil/block.py @@ -94,8 +94,8 @@ def from_numeric_id(cls, block_id: int, data: int=0): data Numeric data, used to represent variants of the block """ - # See https://minecraft.gamepedia.com/Java_Edition_data_value/Pre-flattening - # and https://minecraft.gamepedia.com/Java_Edition_data_value for current values + # See https://minecraft.wiki/w/Java_Edition_data_value/Pre-flattening + # and https://minecraft.wiki/w/Java_Edition_data_value for current values key = f'{block_id}:{data}' if key not in LEGACY_ID_MAP: raise KeyError(f'Block {key} not found') diff --git a/anvil/chunk.py b/anvil/chunk.py index 33b78cf..337a5c3 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -17,7 +17,7 @@ # https://minecraft.wiki/w/Java_Edition_19w36a _VERSION_19w36a = 2203 -# This is the version where "The Flattening" (https://minecraft.gamepedia.com/Java_Edition_1.13/Flattening) happened +# This is the version where "The Flattening" (https://minecraft.wiki/w/Java_Edition_1.13/Flattening) happened # where blocks went from numeric ids to namespaced ids (namespace:block_id) _VERSION_17w47a = 1451 @@ -282,7 +282,7 @@ def get_block( y %= 16 if self.version is None or self.version < _VERSION_17w47a: - # Explained in depth here https://minecraft.gamepedia.com/index.php?title=Chunk_format&oldid=1153403#Block_format + # Explained in depth here https://minecraft.wiki/w/index.php?title=Chunk_format&oldid=1153403#Block_format if section is None or "Blocks" not in section: if force_new: diff --git a/docs/index.rst b/docs/index.rst index 2253f1e..3308ef0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,7 @@ anvil-parser ============ -Simple parser for the `Minecraft anvil file format `_ +Simple parser for the `Minecraft anvil file format `_ .. toctree:: :maxdepth: 3 From 72ecab8ea2da38c75464a52b5976adeb21a533cb Mon Sep 17 00:00:00 2001 From: Ableytner <56540036+Ableytner@users.noreply.github.com> Date: Sat, 11 Nov 2023 00:27:47 +0100 Subject: [PATCH 27/38] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 365044d..6954c3b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ NBT==1.5.1 -frozendict==1.2 +frozendict==2.0.2 From d52e22a8cd84e2c41d9e6f8774ac095e2f6f6baa Mon Sep 17 00:00:00 2001 From: Ableytner Date: Wed, 21 Feb 2024 18:54:53 +0100 Subject: [PATCH 28/38] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 365044d..6954c3b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ NBT==1.5.1 -frozendict==1.2 +frozendict==2.0.2 From 4b5730ebdc61c75853e621f0cfb378c3ae7ee4c4 Mon Sep 17 00:00:00 2001 From: Ableytner Date: Wed, 21 Feb 2024 23:29:29 +0100 Subject: [PATCH 29/38] fix older mc versions --- anvil/chunk.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/anvil/chunk.py b/anvil/chunk.py index 2938f9e..bd2ed49 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -21,6 +21,10 @@ # where blocks went from numeric ids to namespaced ids (namespace:block_id) _VERSION_17w47a = 1451 +# This represents Versions before 1.9 snapshot 15w32a, so world does not have a Data Version. +# https://minecraft.fandom.com/wiki/Data_version +_VERSION_PRE_15w32a = 100 + def bin_append(a, b, length=None): """ @@ -97,7 +101,7 @@ def __init__(self, nbt_data: nbt.NBTFile): except KeyError: # Version is pre-1.9 snapshot 15w32a, so world does not have a Data Version. # See https://minecraft.fandom.com/wiki/Data_version - self.version = None + self.version = _VERSION_PRE_15w32a if self.version >= _VERSION_21w43a: self.data = nbt_data From 3729c7c9bd6213c639f32b72a0da60bb3551266c Mon Sep 17 00:00:00 2001 From: Ableytner Date: Fri, 23 Feb 2024 16:45:53 +0100 Subject: [PATCH 30/38] add venv to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bb9f8b6..7c1f481 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build/ .vscode/ docs/_build/ examples/test_112.py +venv From a68dbfd15d3f50b3515eeb13e2929a62ad032a9b Mon Sep 17 00:00:00 2001 From: Ableytner Date: Mon, 4 Mar 2024 17:56:49 +0100 Subject: [PATCH 31/38] update fandom links --- anvil/chunk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/anvil/chunk.py b/anvil/chunk.py index 90790e6..6da5a94 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -22,7 +22,7 @@ _VERSION_17w47a = 1451 # This represents Versions before 1.9 snapshot 15w32a, so world does not have a Data Version. -# https://minecraft.fandom.com/wiki/Data_version +# https://minecraft.wiki/w/Data_version _VERSION_PRE_15w32a = 100 @@ -235,7 +235,7 @@ def get_biome(self, x: int, y: int, z: int) -> Biome: # Each biome index refers to a column stored Z then X. index = z * 16 + x else: - # https://minecraft.fandom.com/wiki/Java_Edition_19w36a + # https://minecraft.wiki/w/Java_Edition_19w36a # Get index on the biome list with the order YZX # Each biome index refers to a 4x4 areas here so we do integer division by 4 index = (y // 4) * 4 * 4 + (z // 4) * 4 + (x // 4) From 040508995da07edc2d2563a556e47e022ffd4062 Mon Sep 17 00:00:00 2001 From: Eric K <54124297+Nyveon@users.noreply.github.com> Date: Tue, 5 Mar 2024 12:52:37 -0300 Subject: [PATCH 32/38] refactor: move version constants to separate file --- anvil/chunk.py | 31 ++++++++----------------------- anvil/versions.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 23 deletions(-) create mode 100644 anvil/versions.py diff --git a/anvil/chunk.py b/anvil/chunk.py index 337a5c3..976468a 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -4,22 +4,7 @@ from .block import Block, OldBlock from .region import Region from .errors import OutOfBoundsCoordinates, ChunkNotFound - -# This version removes the chunk's "Level" NBT tag and moves all contained tags to the top level -# https://minecraft.wiki/w/Java_Edition_21w43a -_VERSION_21w43a = 2844 - -# This version removes block state value stretching from the storage -# so a block value isn't in multiple elements of the array -_VERSION_20w17a = 2529 - -# This version changes how biomes are stored to allow for biomes at different heights -# https://minecraft.wiki/w/Java_Edition_19w36a -_VERSION_19w36a = 2203 - -# This is the version where "The Flattening" (https://minecraft.wiki/w/Java_Edition_1.13/Flattening) happened -# where blocks went from numeric ids to namespaced ids (namespace:block_id) -_VERSION_17w47a = 1451 +from .versions import VERSION_21w43a, VERSION_20w17a, VERSION_19w36a, VERSION_17w47a def bin_append(a, b, length=None): @@ -63,7 +48,7 @@ def _states_from_section(section: nbt.TAG_Compound) -> list: def _section_height_range(version: Optional[int]) -> range: - if version is not None and version > _VERSION_17w47a: + if version is not None and version > VERSION_17w47a: return range(-4, 20) else: return range(16) @@ -99,7 +84,7 @@ def __init__(self, nbt_data: nbt.NBTFile): # See https://minecraft.wiki/w/Data_version self.version = None - if self.version >= _VERSION_21w43a: + if self.version >= VERSION_21w43a: self.data = nbt_data self.tile_entities = self.data["block_entities"] else: @@ -227,7 +212,7 @@ def get_biome(self, x: int, y: int, z: int) -> Biome: else: biomes = self.data["Biomes"] - if self.version < _VERSION_19w36a: + if self.version < VERSION_19w36a: # Each biome index refers to a column stored Z then X. index = z * 16 + x else: @@ -281,7 +266,7 @@ def get_block( # global Y to section Y y %= 16 - if self.version is None or self.version < _VERSION_17w47a: + if self.version is None or self.version < VERSION_17w47a: # Explained in depth here https://minecraft.wiki/w/index.php?title=Chunk_format&oldid=1153403#Block_format if section is None or "Blocks" not in section: @@ -321,7 +306,7 @@ def get_block( # Get index on the block list with the order YZX index = y * 16 * 16 + z * 16 + x # in 20w17a and newer blocks cannot occupy more than one element on the BlockStates array - stretches = self.version is None or self.version < _VERSION_20w17a + stretches = self.version is None or self.version < VERSION_20w17a # get location in the BlockStates array via the index if stretches: @@ -403,7 +388,7 @@ def stream_blocks( if section is None or isinstance(section, int): section = self.get_section(section or 0) - if self.version < _VERSION_17w47a: + if self.version < VERSION_17w47a: if section is None or "Blocks" not in section: air = Block.from_name("minecraft:air") if force_new else OldBlock(0) for _ in range(4096): @@ -441,7 +426,7 @@ def stream_blocks( palette = _palette_from_section(section) bits = max((len(palette) - 1).bit_length(), 4) - stretches = self.version < _VERSION_20w17a + stretches = self.version < VERSION_20w17a if stretches: state = index * bits // 64 diff --git a/anvil/versions.py b/anvil/versions.py new file mode 100644 index 0000000..de771b9 --- /dev/null +++ b/anvil/versions.py @@ -0,0 +1,15 @@ +# This version removes the chunk's "Level" NBT tag and moves all contained tags to the top level +# https://minecraft.wiki/w/Java_Edition_21w43a +VERSION_21w43a = 2844 + +# This version removes block state value stretching from the storage +# so a block value isn't in multiple elements of the array +VERSION_20w17a = 2529 + +# This version changes how biomes are stored to allow for biomes at different heights +# https://minecraft.wiki/w/Java_Edition_19w36a +VERSION_19w36a = 2203 + +# This is the version where "The Flattening" (https://minecraft.wiki/w/Java_Edition_1.13/Flattening) happened +# where blocks went from numeric ids to namespaced ids (namespace:block_id) +VERSION_17w47a = 1451 From d03e661f42fb85c612a4ab3bdd9143ec7556a986 Mon Sep 17 00:00:00 2001 From: Eric K <54124297+Nyveon@users.noreply.github.com> Date: Tue, 5 Mar 2024 12:53:54 -0300 Subject: [PATCH 33/38] feat: 1.18+ region saving support --- anvil/empty_region.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/anvil/empty_region.py b/anvil/empty_region.py index 2dc9132..91b09e4 100644 --- a/anvil/empty_region.py +++ b/anvil/empty_region.py @@ -5,6 +5,7 @@ from .block import Block from .biome import Biome from .errors import OutOfBoundsCoordinates +from .versions import VERSION_21w43a from io import BytesIO from nbt import nbt import zlib @@ -289,7 +290,12 @@ def save(self, file: Union[str, BinaryIO]=None) -> bytes: if isinstance(chunk, Chunk): nbt_data = nbt.NBTFile() nbt_data.tags.append(nbt.TAG_Int(name='DataVersion', value=chunk.version)) - nbt_data.tags.append(chunk.data) + + if chunk.version >= VERSION_21w43a: + for tag in chunk.data.tags: + nbt_data.tags.append(tag) + else: + nbt_data.tags.append(chunk.data) else: nbt_data = chunk.save() nbt_data.write_file(buffer=chunk_data) From c8e5526a4d4ea86179a07daeed83d6b738aff9d1 Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Sun, 10 Mar 2024 10:56:27 +0000 Subject: [PATCH 34/38] chore: bump patch version due to writing region fixed by Nyveon --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 68d0bcc..b44ff28 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name='anvil-parser', - version='0.10.3', + version='0.10.4', author='mat', description='A Minecraft anvil file format parser', long_description=long_description, From 2533ff1459238132c51dbebb2642b4acfd6e3e15 Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Sun, 10 Mar 2024 11:56:34 +0000 Subject: [PATCH 35/38] refactor: use -1 as _VERSION_PRE_15w32a and remove None checks from version --- anvil/chunk.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/anvil/chunk.py b/anvil/chunk.py index 6da5a94..0eb73ca 100644 --- a/anvil/chunk.py +++ b/anvil/chunk.py @@ -21,9 +21,10 @@ # where blocks went from numeric ids to namespaced ids (namespace:block_id) _VERSION_17w47a = 1451 -# This represents Versions before 1.9 snapshot 15w32a, so world does not have a Data Version. +# This represents Versions before 1.9 snapshot 15w32a, +# these snapshots do not have a Data Version so we use -1 since -1 is less than any valid data version. # https://minecraft.wiki/w/Data_version -_VERSION_PRE_15w32a = 100 +_VERSION_PRE_15w32a = -1 def bin_append(a, b, length=None): @@ -67,7 +68,7 @@ def _states_from_section(section: nbt.TAG_Compound) -> list: def _section_height_range(version: Optional[int]) -> range: - if version is not None and version > _VERSION_17w47a: + if version > _VERSION_17w47a: return range(-4, 20) else: return range(16) @@ -285,7 +286,7 @@ def get_block( # global Y to section Y y %= 16 - if self.version is None or self.version < _VERSION_17w47a: + if self.version < _VERSION_17w47a: # Explained in depth here https://minecraft.wiki/w/index.php?title=Chunk_format&oldid=1153403#Block_format if section is None or "Blocks" not in section: @@ -325,7 +326,7 @@ def get_block( # Get index on the block list with the order YZX index = y * 16 * 16 + z * 16 + x # in 20w17a and newer blocks cannot occupy more than one element on the BlockStates array - stretches = self.version is None or self.version < _VERSION_20w17a + stretches = self.version < _VERSION_20w17a # get location in the BlockStates array via the index if stretches: From 42df1758cbc5a12d78037d0e39ed7988ce29e39a Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Sun, 21 Jul 2024 19:46:37 +0100 Subject: [PATCH 36/38] feat: publish to PyPI --- .travis.yml | 5 ----- README.md | 16 +++++----------- setup.py | 10 +++++----- 3 files changed, 10 insertions(+), 21 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 443c23c..0000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: python -python: - - "3.7" -script: - - pytest \ No newline at end of file diff --git a/README.md b/README.md index 6d4e6c1..5735470 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,14 @@ # anvil-parser - -[![CodeFactor](https://www.codefactor.io/repository/github/matcool/anvil-parser/badge/master)](https://www.codefactor.io/repository/github/matcool/anvil-parser/overview/master) [![Documentation Status](https://readthedocs.org/projects/anvil-parser/badge/?version=latest)](https://anvil-parser.readthedocs.io/en/latest/?badge=latest) -[![Tests](https://github.com/matcool/anvil-parser/actions/workflows/run-pytest.yml/badge.svg)](https://github.com/matcool/anvil-parser/actions/workflows/run-pytest.yml) -[![PyPI - Downloads](https://img.shields.io/pypi/dm/anvil-parser)](https://pypi.org/project/anvil-parser/) +[![Tests](https://github.com/0xTiger/anvil-parser/actions/workflows/run-pytest.yml/badge.svg)](https://github.com/0xTiger/anvil-parser/actions/workflows/run-pytest.yml) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/anvil-parser)](https://pypi.org/project/anvil-parser2/) -Simple parser for the [Minecraft anvil file format](https://minecraft.wiki/w/Anvil_file_format) +A parser for the [Minecraft anvil file format](https://minecraft.wiki/w/Anvil_file_format). This package was forked from [matcool's anvil-parser](https://github.com/matcool/anvil-parser) in order to additionally support minecraft versions 1.18 and above. # Installation -This project is available on [PyPI](https://pypi.org/project/anvil-parser/) and can be installed with pip -``` -pip install anvil-parser ``` -or directly from github -``` -pip install git+https://github.com/matcool/anvil-parser.git +pip install anvil-parser2 ``` + # Usage ## Reading ```python diff --git a/setup.py b/setup.py index b44ff28..1ca494b 100644 --- a/setup.py +++ b/setup.py @@ -4,13 +4,13 @@ long_description = file.read() setuptools.setup( - name='anvil-parser', - version='0.10.4', - author='mat', - description='A Minecraft anvil file format parser', + name='anvil-parser2', + version='0.10.5', + author='0xTiger', + description='A parser for the Minecraft anvil file format, supports all Minecraft verions', long_description=long_description, long_description_content_type='text/markdown', - url='https://github.com/matcool/anvil-parser', + url='https://github.com/0xTiger/anvil-parser', packages=setuptools.find_packages(), classifiers=[ 'Programming Language :: Python :: 3', From 94575d7f232c50f662749af42654e6f0d1ad1556 Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Sun, 21 Jul 2024 20:17:34 +0100 Subject: [PATCH 37/38] chore: rename everywhere to anvil-parser2 & update github urls --- .github/workflows/run-pytest.yml | 2 +- .readthedocs.yml | 2 +- README.md | 2 +- setup.py | 4 ++-- requirements.txt => tests/requirements.txt | 0 5 files changed, 5 insertions(+), 5 deletions(-) rename requirements.txt => tests/requirements.txt (100%) diff --git a/.github/workflows/run-pytest.yml b/.github/workflows/run-pytest.yml index fad9665..19b47ce 100644 --- a/.github/workflows/run-pytest.yml +++ b/.github/workflows/run-pytest.yml @@ -24,7 +24,7 @@ jobs: run: | python -m pip install --upgrade pip pip install pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + if [ -f tests/requirements.txt ]; then pip install -r tests/requirements.txt; fi - name: Test with pytest run: | pytest diff --git a/.readthedocs.yml b/.readthedocs.yml index d5ab4db..cc6080f 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -7,4 +7,4 @@ python: version: 3.7 install: - requirements: docs/requirements.txt - - requirements: requirements.txt + - requirements: tests/requirements.txt diff --git a/README.md b/README.md index 5735470..ea3f67b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# anvil-parser +# anvil-parser2 [![Documentation Status](https://readthedocs.org/projects/anvil-parser/badge/?version=latest)](https://anvil-parser.readthedocs.io/en/latest/?badge=latest) [![Tests](https://github.com/0xTiger/anvil-parser/actions/workflows/run-pytest.yml/badge.svg)](https://github.com/0xTiger/anvil-parser/actions/workflows/run-pytest.yml) [![PyPI - Downloads](https://img.shields.io/pypi/dm/anvil-parser)](https://pypi.org/project/anvil-parser2/) diff --git a/setup.py b/setup.py index 1ca494b..4e0ae7e 100644 --- a/setup.py +++ b/setup.py @@ -5,12 +5,12 @@ setuptools.setup( name='anvil-parser2', - version='0.10.5', + version='0.10.6', author='0xTiger', description='A parser for the Minecraft anvil file format, supports all Minecraft verions', long_description=long_description, long_description_content_type='text/markdown', - url='https://github.com/0xTiger/anvil-parser', + url='https://github.com/0xTiger/anvil-parser2', packages=setuptools.find_packages(), classifiers=[ 'Programming Language :: Python :: 3', diff --git a/requirements.txt b/tests/requirements.txt similarity index 100% rename from requirements.txt rename to tests/requirements.txt From 91a7eda55f0fb55a948d1072896430c29d906809 Mon Sep 17 00:00:00 2001 From: 0xTiger Date: Tue, 7 Oct 2025 21:42:06 -0400 Subject: [PATCH 38/38] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index ea3f67b..dc29292 100644 --- a/README.md +++ b/README.md @@ -56,5 +56,4 @@ region.save('r.0.0.mca') - [ ] More tests - [ ] Tests for 20w17a+ BlockStates format # Note -Testing done in 1.14.4 - 1.19, should work fine for other versions. -Writing chunks and regions is broken from 1.16 onwards +Testing done in 1.14.4 - 1.21, should work fine for other versions.