"""Small 26.3 world cache used while the legacy automation is being ported. It retains numeric block-state IDs from chunk palettes and block updates. It intentionally does not assign 1.16 block meanings to modern state IDs. """ from math import floor class PaletteSection: def __init__(self, bits, palette, words, size): self.bits = bits self.palette = palette self.words = words self.size = size self.overrides = {} self.mask = (1 << bits) - 1 if bits else 0 self.per_word = 64 // bits if bits else 0 def get(self, index): if index in self.overrides: return self.overrides[index] if self.bits == 0: return self.palette[0] word = self.words[index // self.per_word] palette_index = (word >> ((index % self.per_word) * self.bits)) & self.mask return self.palette[palette_index] if self.palette is not None else palette_index def set(self, index, block_state_id): self.overrides[index] = block_state_id class ChunkColumn: def __init__(self, x, z, sections, min_section): self.x = x self.z = z self.sections = { min_section + offset: section for offset, section in enumerate(sections) } def get_block_at(self, x, y, z): section = self.sections.get(y // 16) if section is None: return None index = ((y & 15) * 16 + (z & 15)) * 16 + (x & 15) return section['blocks'].get(index) def set_block_at(self, x, y, z, block_state_id): section = self.sections.get(y // 16) if section is None: return None index = ((y & 15) * 16 + (z & 15)) * 16 + (x & 15) previous = section['blocks'].get(index) section['blocks'].set(index, block_state_id) return previous class WorldState: """Sparse loaded-chunk/block-state cache with the old manager's read API.""" def __init__(self, min_y=-64): self.min_y = min_y self.min_section = min_y // 16 self.chunks = {} self.index = {} from mosfet.info import blocks self.indexed_ids = blocks.INDEXED_IDS self.center = None self.radius = None self.dimension = 'minecraft:overworld' self.player_id = None def set_dimension(self, dimension, dimension_type=None): if dimension: self.dimension = dimension if dimension_type: min_y = dimension_type.get('min_y', self.min_y) if isinstance(min_y, int): self.min_y = min_y self.min_section = min_y // 16 def load_chunk(self, packet): self.unload_chunk(packet.x, packet.z) chunk = ChunkColumn(packet.x, packet.z, packet.sections, self.min_section) self.chunks[(packet.x, packet.z)] = chunk for section_y, section in chunk.sections.items(): palette = section['blocks'] if not self.indexed_ids.intersection( palette.palette or [palette.get(i) for i in range(palette.size)]): continue for index in range(palette.size): state_id = palette.get(index) if state_id not in self.indexed_ids: continue local_x = index & 0xF local_z = (index >> 4) & 0xF local_y = (index >> 8) & 0xF pos = (packet.x * 16 + local_x, section_y * 16 + local_y, packet.z * 16 + local_z) self.index.setdefault(state_id, set()).add(pos) def handle_block_update(self, packet): self.set_block_at(packet.x, packet.y, packet.z, packet.block_state_id) def handle_section_update(self, packet): for x, y, z, state_id in packet.changes: self.set_block_at(x, y, z, state_id) def get_chunk(self, x, z): return self.chunks.get((x, z)) def get_block_at(self, x, y, z): chunk = self.get_chunk(floor(x / 16), floor(z / 16)) if chunk is None: return None return chunk.get_block_at(floor(x), floor(y), floor(z)) def set_block_at(self, x, y, z, block_state_id): x, y, z = floor(x), floor(y), floor(z) chunk = self.get_chunk(floor(x / 16), floor(z / 16)) if chunk is None: return False previous = chunk.set_block_at(x, y, z, block_state_id) pos = (x, y, z) if previous in self.indexed_ids: self.index.get(previous, set()).discard(pos) if block_state_id in self.indexed_ids: self.index.setdefault(block_state_id, set()).add(pos) return True def unload_chunk(self, x, z): chunk = self.chunks.pop((x, z), None) if chunk is None: return x0, z0 = x * 16, z * 16 for state_id, locations in list(self.index.items()): self.index[state_id] = { pos for pos in locations if not (x0 <= pos[0] < x0 + 16 and z0 <= pos[2] < z0 + 16) } if not self.index[state_id]: del self.index[state_id] def unload_all_chunks(self): self.chunks.clear() self.index.clear() def get_loaded_area(self, ignore_empty=False): if not self.chunks: return None xs = [pos[0] for pos in self.chunks] zs = [pos[1] for pos in self.chunks] ys = [section_y for chunk in self.chunks.values() for section_y in chunk.sections] return ((min(xs), min(ys), min(zs)), (max(xs), max(ys), max(zs))) def check_loaded(self, chunk_distance=None): if self.center is None: return bool(self.chunks) radius = self.radius if chunk_distance is None else chunk_distance if radius is None: return bool(self.chunks) cx, cz = self.center return all((x, z) in self.chunks for x in range(cx - radius, cx + radius + 1) for z in range(cz - radius, cz + radius + 1)) @property def loaded_chunk_count(self): return len(self.chunks) def __repr__(self): return ('WorldState(dimension={!r}, chunks={}, min_y={}, center={!r}, ' 'radius={!r})').format( self.dimension, len(self.chunks), self.min_y, self.center, self.radius) def read_palette_container(stream, *, size, local_bits_max): from minecraft.networking.types import Byte, UnsignedLong, VarInt bits = Byte.read(stream) if bits == 0: palette = [VarInt.read(stream)] elif bits <= local_bits_max: palette_size = VarInt.read(stream) palette = [VarInt.read(stream) for _ in range(palette_size)] else: palette = None word_count = ((size + (64 // bits) - 1) // (64 // bits)) if bits else 0 words = [UnsignedLong.read(stream) for _ in range(word_count)] return PaletteSection(bits, palette, words, size) def decode_chunk_sections(data): from io import BytesIO from minecraft.networking.types import Short stream = BytesIO(data) sections = [] while stream.tell() < len(data): start = stream.tell() if len(data) - start < 4: raise ValueError('truncated chunk section header at byte {}'.format(start)) non_empty_blocks = Short.read(stream) fluid_count = Short.read(stream) blocks = read_palette_container(stream, size=4096, local_bits_max=8) biomes = read_palette_container(stream, size=64, local_bits_max=3) sections.append({ 'non_empty_blocks': non_empty_blocks, 'fluid_count': fluid_count, 'blocks': blocks, 'biomes': biomes, }) if stream.tell() <= start: raise ValueError('chunk section decoder made no progress') return sections