Source code for nbtparse.minecraft.terrain.chunk

from collections import abc as cabc
import functools
import logging

from ...syntax import tags
from ...syntax import ids
from ...semantics import fields
from ...semantics import nbtobject
from .. import entity
from .. import entity_ids
from .. import entityfactory
from . import tile
from . import voxel


logger = logging.getLogger(__name__)


SECTION_LENGTH = 16
SECTION_HEIGHT = 16
SECTION_WIDTH = 16

SECTIONS_PER_CHUNK = 16


[docs]class HeightMap(cabc.MutableMapping): """The height map of a chunk. Maps coordinates to heights:: hm[3, 4] = 5 Keys may not be inserted or deleted, and must be pairs of integers. Intlist should be the raw list of integers as saved by Minecraft. """ def __init__(self, intlist: [int]): required_len = SECTION_LENGTH * SECTION_WIDTH if len(intlist) != required_len: raise ValueError('Must have exactly {} entries' .format(required_len)) self._intlist = list(intlist) def __repr__(self): return 'HeightMap({!r})'.format(self._intlist) def _fix_idx(self, idx: (int, int)) -> (int, int): x_idx, z_idx = idx if x_idx not in range(SECTION_LENGTH): raise IndexError('X index out of range') if z_idx not in range(SECTION_WIDTH): raise IndexError('Z index out of range') return idx def __getitem__(self, idx: (int, int)) -> int: x_idx, z_idx = self._fix_idx(idx) return self._intlist[z_idx * SECTION_LENGTH + x_idx] def __setitem__(self, idx: (int, int), value: int): x_idx, z_idx = self._fix_idx(idx) self._intlist[z_idx * SECTION_LENGTH + x_idx] = value def __delitem__(self, idx: (int, int)): raise TypeError('Cannot delete items.') def __iter__(self): yield from self._intlist def __len__(self): return len(self._intlist)
[docs] def to_raw(self) -> [int]: """Returns the raw list used by Minecraft.""" return list(self._intlist)
[docs]class HeightMapField(fields.MutableField, fields.SingleField): """Field for :class:`HeightMap`.""" def __init__(self, nbt_name: str, *, default: HeightMap=None): super().__init__(nbt_name, default=default) def __repr__(self): return 'HeightMapField({!r}, default={!r})'.format(self.nbt_name, self.default) @staticmethod
[docs] def to_python(tag: tags.IntArrayTag) -> HeightMap: return HeightMap(tag)
@staticmethod
[docs] def from_python(value: HeightMap) -> tags.IntArrayTag: return tags.IntArrayTag(value.to_raw())
class _BlockField(fields.MutableField, fields.MultiField): """Exposes the blocks in a section. If default is :obj:`None`, a new empty buffer will be created as the default. """ def __init__(self, id_name: str, addid_name: str, damage_name: str, *, length: int, height: int, width: int): if (length * width * height) % 2 == 1: raise ValueError('Cannot create _BlockField with odd length.') self.length = length self.height = height self.width = width nbt_names = (id_name, addid_name, damage_name) super().__init__(nbt_names) def __repr__(self) -> str: id_name, addid_name, damage_name = self.nbt_names return ('_BlockField({!r}, {!r}, {!r}, length={!r}, height={!r}, ' 'width={!r}, default={!r})'.format(id_name, addid_name, damage_name, self.length, self.height, self.width, self.default)) def set_default(self, obj, field_name): """Set this field to its default. Always creates a new VoxelBuffer; the default argument is not supported. """ default = voxel.VoxelBuffer(self.length, self.height, self.width) self.set_field_value(obj, field_name, default) def to_python(self, ids: tags.ByteArrayTag, addids: tags.ByteArrayTag, damages: tags.ByteArrayTag) -> voxel.VoxelBuffer: length = self.length height = self.height width = self.width if addids is None: addids = bytearray((length * width * height) // 2) return voxel.VoxelBuffer.from_raw(ids, addids, damages, length=length, height=height, width=width) @staticmethod def from_python(vb: voxel.VoxelBuffer): result = vb.to_raw() return tuple(tags.ByteArrayTag(x) for x in result)
[docs]class Section(nbtobject.NBTObject): y_index = fields.ByteField('Y') y_index.__doc__ = """The Y-index of this section. From 0 to 15 inclusive. """ blocks = _BlockField('Blocks', 'Add', 'Data', length=SECTION_LENGTH, width=SECTION_WIDTH, height=SECTION_HEIGHT) blocks.__doc__ = """:class:`~.voxel.VoxelBuffer` of this section.""" def __repr__(self): return '<Section: y_index={!r}>'.format(self.y_index)
class _MagicDict(dict): def __missing__(self, key): if key not in range(SECTIONS_PER_CHUNK): raise KeyError(key) result = Section() result.y_index = key self[key] = result return result def __repr__(self): return '_MagicDict({})'.format(super().__repr__())
[docs]class SectionDictField(fields.MutableField, fields.SingleField): """Field for the sections of a chunk. Keys are y_index, values are sections. """ def __init__(self, nbt_name: str, *, default: dict=None): super().__init__(nbt_name, default=default) def __repr__(self): return 'SectionDictField({!r}, default={!r})'.format(self.nbt_name, self.default) @staticmethod
[docs] def to_python(sec_list: tags.ListTag) -> dict: result = _MagicDict() for raw_sec in sec_list: cooked_sec = Section.from_nbt(raw_sec) result[cooked_sec.y_index] = cooked_sec return result
@staticmethod
[docs] def from_python(sec_dict: dict) -> tags.ListTag: result = tags.ListTag(content_id=ids.TAG_Compound) for cooked_sec_y, cooked_sec in sec_dict.items(): cooked_sec.y_index = cooked_sec_y raw_sec = cooked_sec.to_nbt() result.append(raw_sec) return result
[docs] def set_default(self, obj: nbtobject.NBTObject, field_name: str): if self.default is None: self.set_field_value(obj, field_name, _MagicDict()) else: super().set_default(obj)
class _EntityListField(fields.MutableField, fields.SingleField): def __init__(self, nbt_name, *, default_class=entity.Entity, default_value: list=None): super().__init__(nbt_name, default=default_value) self._default_class = default_class def to_python_ex(self, raw_list: tags.ListTag, cnk: 'Chunk') -> list: result = [] for raw_entity in raw_list: cooked = entityfactory.from_nbt(raw_entity, default_class=self._default_class, namespace=cnk._namespace) result.append(cooked) return result def from_python_ex(self, python_list: list, cnk: 'Chunk'): result = tags.ListTag(content_id=ids.TAG_Compound) for cooked in python_list: raw_entity = cooked.to_nbt() result.append(raw_entity) return result
[docs]class Chunk(nbtobject.NBTObject): sections = SectionDictField('Sections') sections.__doc__ = ( """Mutable mapping from Y-indices to :class:`Section`\ s. Y-indices which are not present in the underlying NBT will be automagically created as empty sections upon attempting to retrieve them. The key in this mapping will override the :attr:`~Section.y_index` attribute if they disagree. .. note:: It is acceptable to replace this mapping with an entirely different mapping. If you do so, the magic creation of missing sections will very likely not work. If you prefer creating sections explicitly, code like the following will disable the magic:: c = Chunk.from_nbt(...) c.sections = dict(c.sections) """) tiles = _EntityListField('TileEntities', default_class=tile.TileEntity) tiles.__doc__ = """List of :class:`~.tile.TileEntity` objects. .. note:: This attribute is generally managed by the :class:`~.region.Region` which created this chunk. Manually changing it is usually unnecessary. """ entities = _EntityListField('Entities') def __init__(self, *args, namespace=entity_ids.VANILLA, **kwargs): super().__init__(*args, **kwargs) self._namespace = namespace @classmethod
[docs] def from_bytes(cls, raw: bytes, namespace=entity_ids.VANILLA): result = super().from_bytes(raw) result._namespace = namespace return result
height_map = HeightMapField('HeightMap') height_map.__doc__ = """The height map for this chunk. .. note:: It is planned that a lighting engine will manage this attribute automatically. This is not yet implemented. """ def __repr__(self): return '<Chunk at 0x{:x}>'.format(id(self)) @staticmethod
[docs] def prepare_save(nbt: tags.CompoundTag) -> tags.CompoundTag: """Wrap nbt in a singleton CompoundTag.""" return tags.CompoundTag({'Level': nbt})
@staticmethod
[docs] def prepare_load(nbt: tags.CompoundTag) -> tags.CompoundTag: """Unwrap nbt from a singleton CompoundTag.""" return nbt['Level']