from __future__ import annotations
import copy
from sys import getsizeof
import re
from typing import Dict, Iterable, List, Tuple, Union, overload, Generator
import amulet_nbt
from .errors import InvalidBlockException
from ..utils import Int
def blockstate_to_block(blockstate: str) -> "Block":
namespace, base_name, properties = Block.parse_blockstate_string(blockstate)
return Block(namespace=namespace, base_name=base_name, properties=properties)
[docs]class Block:
"""
Class to handle data about various blockstates and allow for extra blocks to be created and interacted with.
.. important::
Creating version specific block objects via the `Block()` constructor instead of using
:meth:`api.world.World.get_block_instance` is supported but not encouraged. To avoid possible caveats of doing this,
make sure to either only instantiate blocks with Amulet blockstate data or use
:meth:`api.world.World.get_block_instance` instead
Here's a few examples on how create a Block object with extra blocks:
Creating a new Block object with the base of ``stone`` and has an extra block of ``water[level=1]``:
>>> stone = blockstate_to_block("minecraft:stone")
>>> water_level_1 = blockstate_to_block("minecraft:water[level=1]")
>>> stone_with_extra_block = stone + water_level_1
>>> repr(stone_with_extra_block)
'Block(minecraft:stone, minecraft:water[level=1])'
Creating a new Block object using the namespace and base_name:
>>> granite = Block(namespace="minecraft", base_name="granite")
Creating a new Block object with another layer of extra blocks:
>>> stone_water_granite = stone_with_extra_block + granite # Doesn't modify any of the other objects
>>> repr(stone_water_granite)
'Block(minecraft:stone, minecraft:water[level=1], minecraft:granite)'
Creating a new Block object by removing an extra block from all layers:
*Note: This removes all instances of the Block object from extra blocks*
>>> stone_granite = stone_water_granite - water_level_1 # Doesn't modify any of the other objects either
>>> repr(stone_granite)
'Block(minecraft:stone, minecraft:granite)'
Creating a new Block object by removing a specific layer:
>>> oak_log_axis_x = blockstate_to_block("minecraft:oak_log[axis=x]")
>>> stone_water_granite_water_oak_log = stone_water_granite + water_level_1 + oak_log_axis_x
>>> repr(stone_water_granite_water_oak_log)
'Block(minecraft:stone, minecraft:water[level=1], minecraft:granite, minecraft:water[level=1], minecraft:oak_log[axis=x])'
>>> stone_granite_water_oak_log = stone_water_granite_water_oak_log.remove_layer(0)
>>> repr(stone_granite_water_oak_log)
'Block(minecraft:stone, minecraft:granite, minecraft:water[level=1], minecraft:oak_log[axis=x])'
"""
__slots__ = (
"_namespaced_name",
"_namespace",
"_base_name",
"_properties",
"_extra_blocks",
"_blockstate",
) # Reduces memory footprint
blockstate_regex = re.compile(
r"(?:(?P<namespace>[a-z0-9_.-]+):)?(?P<base_name>[a-z0-9/._-]+)(?:\[(?P<property_name>[a-z0-9_]+)=(?P<property_value>[a-z0-9_\"]+)(?P<properties>.*)\])?"
)
# blockstate_regex = re.compile(
# r"(?:(?P<namespace>[a-z0-9_.-]+):)?(?P<base_name>[a-z0-9/._-]+)(?:\[(?P<property_name>[a-z0-9_]+)=(?P<property_value>[a-z0-9_]+)(?P<properties>.*)\])?"
# )
parameters_regex = re.compile(r"(?:,(?P<name>[a-z0-9_]+)=(?P<value>[a-z0-9_\"]+))")
# parameters_regex = re.compile(r"(?:,(?P<name>[a-z0-9_]+)=(?P<value>[a-z0-9_]+))")
[docs] def __init__(
self,
namespace: str,
base_name: str,
properties: Dict[str, amulet_nbt.BaseValueType] = None,
extra_blocks: Union[Block, Iterable[Block]] = None,
):
self._blockstate = None
self._namespaced_name = None
assert (isinstance(namespace, str) or namespace is None) and isinstance(
base_name, str
), f"namespace and base_name must be strings {namespace} {base_name}"
self._namespace = namespace
self._base_name = base_name
if properties is None:
properties = {}
assert isinstance(properties, dict) and all(
isinstance(val, amulet_nbt.BaseValueType) for val in properties.values()
), properties
self._properties = properties
self._extra_blocks = ()
if extra_blocks:
if isinstance(extra_blocks, Block):
extra_blocks = [extra_blocks]
self._extra_blocks = tuple(extra_blocks)
self._gen_blockstate()
@property
def namespaced_name(self) -> str:
"""
The namespace:base_name of the blockstate represented by the Block object (IE: `minecraft:stone`)
:return: The namespace:base_name of the blockstate
"""
return self._namespaced_name
@property
def namespace(self) -> str:
"""
The namespace of the blockstate represented by the Block object (IE: `minecraft`)
:return: The namespace of the blockstate
"""
return self._namespace
@property
def base_name(self) -> str:
"""
The base name of the blockstate represented by the Block object (IE: `stone`, `dirt`)
:return: The base name of the blockstate
"""
return self._base_name
@property
def properties(self) -> Dict[str, amulet_nbt.BaseValueType]:
"""
The mapping of properties of the blockstate represented by the Block object (IE: `{"level": "1"}`)
:return: A dictionary of the properties of the blockstate
"""
return copy.deepcopy(self._properties)
@property
def blockstate(self) -> str:
"""
The full blockstate string of the blockstate represented by the Block object (IE: `minecraft:stone`, `minecraft:oak_log[axis=x]`)
:return: The blockstate string
"""
return self._blockstate
@property
def base_block(self) -> Block:
"""
Returns the block without any extra blocks
:return: A Block object
"""
if len(self.extra_blocks) == 0:
return self
else:
return Block(
namespace=self.namespace,
base_name=self.base_name,
properties=self.properties,
)
@property
def extra_blocks(self) -> Union[Tuple, Tuple[Block]]:
"""
Returns a tuple of the extra blocks contained in the Block instance
:return: A tuple of Block objects
"""
return self._extra_blocks
def _gen_blockstate(self):
self._namespaced_name = self._blockstate = f"{self.namespace}:{self.base_name}"
if self.properties:
props = [
f"{key}={value.to_snbt()}"
for key, value in sorted(self.properties.items())
]
self._blockstate += f"[{','.join(props)}]"
if self.extra_blocks:
self._blockstate += (
f"{{{' , '.join(block.blockstate for block in self.extra_blocks)}}}"
)
@staticmethod
def parse_blockstate_string(
blockstate: str,
) -> Tuple[str, str, Dict[str, amulet_nbt.BaseValueType]]:
match = Block.blockstate_regex.match(blockstate)
namespace = match.group("namespace") or "minecraft"
base_name = match.group("base_name")
if match.group("property_name") is not None:
properties = {match.group("property_name"): match.group("property_value")}
else:
properties = {}
properties_string = match.group("properties")
if properties_string is not None:
properties_match = Block.parameters_regex.finditer(properties_string)
for match in properties_match:
properties[match.group("name")] = match.group("value")
return (
namespace,
base_name,
{k: amulet_nbt.from_snbt(v) for k, v in sorted(properties.items())},
)
[docs] def __str__(self) -> str:
"""
:return: The base blockstate string of the Block object
"""
return self.blockstate
[docs] def __repr__(self) -> str:
"""
:return: The base blockstate string of the Block object along with the blockstate strings of included extra blocks
"""
return f"Block({', '.join([str(b) for b in (self, *self.extra_blocks)])})"
def __len__(self):
return len(self._extra_blocks) + 1
def _compare_extra_blocks(self, other: Block) -> bool:
if len(self.extra_blocks) != len(other.extra_blocks):
return False
if len(self.extra_blocks) == 0:
return True
for our_extra_block, their_extra_block in zip(
self.extra_blocks, other.extra_blocks
):
if our_extra_block != their_extra_block:
return False
return True
[docs] def __eq__(self, other: Block) -> bool:
"""
Checks the equality of this Block object to another Block object
:param other: The Block object to check against
:return: True if the Blocks objects are equal, False otherwise
"""
if self.__class__ != other.__class__:
return False
return self.blockstate == other.blockstate and self._compare_extra_blocks(other)
[docs] def __gt__(self, other: Block) -> bool:
"""
Allows blocks to be sorted so numpy.unique can be used on them
"""
if self.__class__ != other.__class__:
return False
return self.blockstate > other.blockstate
[docs] def __hash__(self) -> int:
"""
Hashes the Block object
:return: A hash of the Block object
"""
current_hash = hash(self.blockstate)
if self.extra_blocks:
current_hash = current_hash + hash(self.extra_blocks)
return current_hash
[docs] def __add__(self, other: Block) -> Block:
"""
Allows for other Block objects to be added to this Block object's ``extra_blocks``
:param other: The Block object to add to the end of this Block object's `extra_blocks`
:return: A new Block object with the same data but with an additional Block at the end of ``extra_blocks``
"""
if not isinstance(other, Block):
return NotImplemented
if (
len(other.extra_blocks) == 0
): # Reduces the amount of extra objects/references created
other_cpy = other
else:
other_cpy = Block(
namespace=other.namespace,
base_name=other.base_name,
properties=other.properties,
)
other_extras = []
for eb in other.extra_blocks:
if (
len(eb.extra_blocks) == 0
): # Reduces the amount of extra objects/references created
other_extras.append(eb)
else:
other_extras.append(
Block(
namespace=eb.namespace,
base_name=eb.base_name,
properties=eb.properties,
)
)
return Block(
namespace=self.namespace,
base_name=self.base_name,
properties=self.properties,
extra_blocks=[*self.extra_blocks, other_cpy, *other_extras],
)
[docs] def __sub__(self, other: Block) -> Block:
"""
Allows for other Block objects to be subtracted from this Block object's ``extra_blocks``
:param other: The Block object to subtract from this Block objects' ``extra_blocks``
:return: A new Block object without any instances of the subtracted block in ``extra_blocks``
"""
if not isinstance(other, Block):
return NotImplemented
if (
len(other.extra_blocks) == 0
): # Reduces the amount of extra objects/references created
other_cpy = other
else:
other_cpy = Block(
namespace=other.namespace,
base_name=other.base_name,
properties=other.properties,
)
other_extras = []
for eb in other.extra_blocks:
if len(eb.extra_blocks) == 0:
other_extras.append(eb)
else:
other_extras.append(
Block(
namespace=eb.namespace,
base_name=eb.base_name,
properties=eb.properties,
)
)
# Sets are unordered, so a regular set subtraction doesn't always return the order we want (it sometimes will!)
# So we loop through all of our extra blocks and only append those to the new_extras list if they aren't in
# extra_blocks_to_remove
new_extras = []
extra_blocks_to_remove = (other_cpy, *other_extras)
for eb in self.extra_blocks:
if eb not in extra_blocks_to_remove:
new_extras.append(eb)
return Block(
namespace=self.namespace,
base_name=self.base_name,
properties=self.properties,
extra_blocks=new_extras,
)
[docs] def remove_layer(self, layer: int) -> Block:
"""
Removes the Block object from the specified layer and returns the resulting new Block object
:param layer: The layer of extra block to remove
:return: A new instance of Block with the same data but with the extra block at specified layer removed
:raises `InvalidBlockException`: Raised when you remove the base block from a Block with no other extra blocks
"""
if layer == 0 and len(self.extra_blocks) > 0:
new_base = self._extra_blocks[0]
return Block(
namespace=new_base.namespace,
base_name=new_base.base_name,
properties=new_base.properties,
extra_blocks=[*self._extra_blocks[1:]],
)
elif layer > len(self.extra_blocks):
raise InvalidBlockException("You cannot remove a non-existant layer")
elif layer == 0:
raise InvalidBlockException(
"Removing the base block with no extra blocks is not supported"
)
return Block(
namespace=self.namespace,
base_name=self.base_name,
properties=self.properties,
extra_blocks=[*self.extra_blocks[: layer - 1], *self.extra_blocks[layer:]],
)
[docs] def __sizeof__(self):
size = (
getsizeof(self.namespace)
+ getsizeof(self.base_name)
+ getsizeof(self.properties)
+ getsizeof(self.blockstate)
)
for eb in self.extra_blocks:
size += getsizeof(eb)
return size
[docs]class BlockManager:
"""
Class to handle the mappings between Block objects and their index-based internal IDs
"""
[docs] def __init__(self):
"""
Creates a new BlockManager object
"""
self._index_to_block: List[Block] = []
self._block_to_index_map: Dict[Block, int] = {}
def __len__(self):
return len(self._index_to_block)
def __contains__(self, item: Block) -> bool:
return item in self._block_to_index_map
def blocks(self) -> Tuple[Block]:
return tuple(self._index_to_block)
def items(self) -> Generator[Tuple[int, Block]]:
for index, block in enumerate(self._index_to_block):
yield index, block
@overload
def __getitem__(self, item: Block) -> int:
...
@overload
def __getitem__(self, item: Int) -> Block:
...
[docs] def __getitem__(self, item):
"""
If a Block object is passed to this function, it'll return the internal ID/index of the
blockstate. If an int is given, this method will return the Block object at that specified index.
:param item: The Block object or int to get the mapping data of
:return: An int if a Block object was supplied, a Block object if an int was supplied
"""
try:
if isinstance(item, Block):
return self._block_to_index_map[item]
return self._index_to_block[item]
except (KeyError, IndexError):
raise KeyError(
f"There is no {item} in the BlockManager. "
f"You might want to use the `add_block` function for your blocks before accessing them."
)
[docs] def get_add_block(self, block: Block) -> int:
"""
Adds a Block object to the internal Block object/ID mappings. If the Block already exists in the mappings,
then the existing ID is returned
:param block: The Block to add to the manager
:return: The internal ID of the Block
"""
if block in self._block_to_index_map:
return self._block_to_index_map[block]
self._block_to_index_map[block] = i = len(self._block_to_index_map)
self._index_to_block.append(block)
return i