import json import time from math import floor from munch import Munch from minecraft.networking.packets import clientbound, serverbound from minecraft.networking.modern import ( AddEntityPacket, BlockEntityDataPacket, BlockEventPacket, ContainerOpenPacket, ContainerSetContentPacket, ContainerSetSlotPacket, SetEntityDataPacket, nbt_plain, PaletteSection, EntityPositionSyncPacket, MoveEntityPositionPacket, MoveEntityPositionRotationPacket, MoveEntityRotationPacket, PlayerInfoRemovePacket, PlayerInfoUpdatePacket, RemoveEntitiesPacket, SetCursorItemPacket, ) from mosfet.protocol import packets from mosfet.protocol.types import Slot from mosfet.info import blocks, mcdata, mobs class DataManager: """Compatibility facade over the checked-in Minecraft 26.3 data.""" def __init__(self, directory=None): self.blocks = mcdata.mcd.blocks self.blocks_states = dict(blocks.BLOCKS) self.blocks_properties = dict(blocks.PROPS) self.registries = {} self.biomes = {} self.entity_type = { data['protocol_id']: name for name, data in mobs.MOBS.items() } class ChunksManager: def __init__(self, data_manager): self.data = data_manager self.chunks = {} #self.biomes = {} self.index = {} self.loading = False def handle_block(self, block_packet): self.set_block_at(block_packet.location.x, block_packet.location.y, block_packet.location.z, block_packet.block_state_id) def handle_multiblock(self, multiblock_packet): dx = 16 * multiblock_packet.chunk_section_pos.x dy = 16 * multiblock_packet.chunk_section_pos.y dz = 16 * multiblock_packet.chunk_section_pos.z for b in multiblock_packet.records: self.set_block_at(dx+b.x, dy+b.y, dz+b.z, b.block_state_id) def handle_chunk(self, chunk_packet): for i in chunk_packet.chunks: chunk = chunk_packet.chunks[i] self.chunks[(chunk_packet.x, i, chunk_packet.z)] = chunk if chunk.sub_index: dx = chunk.x * 16 dy = chunk.y * 16 dz = chunk.z * 16 for item_id, locations in chunk.sub_index.items(): if item_id not in self.index: self.index[item_id] = set() for l in locations: coords = (dx + l%16, dy + l//256, dz + l%256//16) self.index[item_id].add(coords) #self.biomes[(chunk_packet.x, None, chunk_packet.z)] = chunk_packet.biomes # FIXME if self.loading: print('.', end='', flush=True) def register(self, connection): connection.register_packet_listener(self.handle_block, clientbound.play.BlockChangePacket) connection.register_packet_listener(self.handle_multiblock, clientbound.play.MultiBlockChangePacket) connection.register_packet_listener(self.handle_chunk, packets.ChunkDataPacket) def get_chunk(self, x, y, z): index = (x, y, z) if not index in self.chunks: return None #raise ChunkNotLoadedException(index) return self.chunks[index] def get_loaded_area(self, ignore_empty=False): first = next(iter(self.chunks.keys())) x0 = x1 = first[0] y0 = y1 = first[1] z0 = z1 = first[2] for k in self.chunks.keys(): if ignore_empty and self.chunks[k].empty: continue x0 = min(x0, k[0]) x1 = max(x1, k[0]) y0 = min(y0, k[1]) y1 = max(y1, k[1]) z0 = min(z0, k[2]) z1 = max(z1, k[2]) return ((x0,y0,z0),(x1,y1,z1)) def get_block_at(self, x, y, z): c = self.get_chunk(floor(x/16), floor(y/16), floor(z/16)) if not c: return None return c.get_block_at(x%16, y%16, z%16) def set_block_at(self, x, y, z, block): c = self.get_chunk(floor(x/16), floor(y/16), floor(z/16)) if not c: return None c.set_block_at(x%16, y%16, z%16, block) if block in blocks.INDEXED_IDS: if block not in self.index: self.index[block] = set() self.index[block].add((x, y, z)) def check_loaded(self, chunk_distance): num = (chunk_distance * 2 + 1) ** 2 num_subchunks = num * 16 return len(self.chunks) >= num_subchunks def unload_chunk(self, x, z): for y in range(16): try: del self.chunks[(x, y, z)] except KeyError: pass def unload_all_chunks(self): self.chunks = {} self.index = {} class ChunkNotLoadedException(Exception): def __str__(self): pos = self.args[0] return "Chunk at %d %d %d not loaded (yet?)"%(pos[0], pos[1], pos[2]) class ChatManager: def __init__(self, global_state): self.g = global_state self.handler = None self.g.connection.register_packet_listener(self.print_chat, clientbound.play.ChatMessagePacket) def translate_chat(self, data): if isinstance(data, str): return data result = data.get('text', '') result += data.get('translate', '') if 'with' in data: result += ' ' + ' '.join([self.translate_chat(x) for x in data['with']]) elif 'extra' in data: result += ''.join([self.translate_chat(x) for x in data['extra']]) return result def print_chat(self, chat_packet): #print(chat_packet) #print(chat_packet.json_data) #import json #print(json.dumps(json.loads(chat_packet.json_data), indent=4)) try: source = chat_packet.field_string('position') sender = chat_packet.sender text = self.translate_chat(json.loads(chat_packet.json_data)) print('[%s] %s'%(source, text)) except Exception as ex: print('Exception %r on message (%s): %s' % (ex, chat_packet.field_string('position'), chat_packet.json_data)) return if self.handler: self.handler((source, sender, text)) def set_handler(self, func): self.handler = func def send(self, text): if not text: # Prevents connection bug when sending empty chat message return packet = serverbound.play.ChatPacket() packet.message = text self.g.connection.write_packet(packet) 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 26.3 chunk/block-state cache used by world and navigation.""" def __init__(self, min_y=-64): self.min_y = min_y self.min_section = min_y // 16 self.chunks = {} self.index = {} self.block_entities = {} 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): # Refresh selectors in case block metadata was reloaded before login. from mosfet.info import blocks self.indexed_ids = blocks.INDEXED_IDS 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 entity in getattr(packet, 'block_entities', []): pos = (entity['x'], entity['y'], entity['z']) self.set_block_entity( pos, entity['type'], entity.get('data')) 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_entity(self, position, entity_type, data): position = tuple(position) if data is None: self.block_entities.pop(position, None) return self.block_entities[position] = { 'type': entity_type, 'data': nbt_plain(data), } @staticmethod def _is_sign_state(block_state_id): from mosfet.info import blocks name = blocks.BLOCKS.get(block_state_id) return name is not None and name.endswith('_sign') 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 (self._is_sign_state(previous) and not self._is_sign_state(block_state_id)): self.block_entities.pop(pos, None) 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] self.block_entities = { pos: entity for pos, entity in self.block_entities.items() if not (x0 <= pos[0] < x0 + 16 and z0 <= pos[2] < z0 + 16) } def unload_all_chunks(self): self.chunks.clear() self.index.clear() self.block_entities.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) class ChestOpenManager: """Track chest viewer-count events and report openings by other players.""" OWN_OPEN_TIMEOUT = 3.0 def __init__(self, global_state): self.g = global_state self.g.chest_viewers = self.g.get('chest_viewers') or {} self.g.chest_open_events = self.g.get('chest_open_events') or [] self.g.last_chest_opened_by_other = None self._own_open_suppressions = {} self.g.connection.register_packet_listener( self.handle_block_event, BlockEventPacket) def _block_name(self, packet): registry = getattr(self.g.connection, 'registry_ids', {}).get( 'minecraft:block', {}) name = next((name for name, block_id in registry.items() if block_id == packet.block_id), None) if name is not None: return name.removeprefix('minecraft:') location = packet.location state_id = self.g.chunks.get_block_at( location.x, location.y, location.z) return blocks.BLOCKS.get(state_id) def handle_block_event(self, packet): block_name = self._block_name(packet) if (block_name is None or block_name == 'ender_chest' or not (block_name == 'chest' or block_name.endswith('_chest')) or packet.action != 1): return location = packet.location position = (location.x, location.y, location.z) viewers = packet.parameter previous_viewers = self.g.chest_viewers.get(position, 0) if viewers: self.g.chest_viewers[position] = viewers else: self.g.chest_viewers.pop(position, None) now = time.monotonic() self._own_open_suppressions = { pos: expires for pos, expires in self._own_open_suppressions.items() if expires > now } pending = self.g.get('pending_chest_open') pending_age = (now - pending.get('requested_at', 0) if pending is not None else None) pending_is_fresh = (pending_age is not None and 0 <= pending_age <= self.OWN_OPEN_TIMEOUT) pending_positions = ( pending.get('positions') or (pending.get('position', ()),) if pending is not None else () ) pending_positions = {tuple(pos) for pos in pending_positions} is_bot_opening = ( pending_is_fresh and position in pending_positions ) if pending is not None and not pending_is_fresh: self.g.pending_chest_open = None if is_bot_opening: for own_position in pending_positions: self._own_open_suppressions[own_position] = now + 0.5 is_bot_opening = (is_bot_opening or position in self._own_open_suppressions) if (viewers <= previous_viewers or (is_bot_opening and viewers <= max(1, previous_viewers))): return event = { 'position': position, 'block': block_name, 'viewers': viewers, 'previous_viewers': previous_viewers, 'timestamp': time.time(), } self.g.last_chest_opened_by_other = event self.g.chest_open_events.append(event) del self.g.chest_open_events[:-100] print('[chest] another player opened {} at {} ({} viewer{})'.format( block_name, position, viewers, '' if viewers == 1 else 's'), flush=True) class EntityManager: def __init__(self, global_state): self.g = global_state self.g.mobs = self.g.get('mobs') or {} self.g.players = self.g.get('players') or {} self.g.objects = self.g.get('objects') or {} self.g.player_names = self.g.get('player_names') or {} register = self.g.connection.register_packet_listener for packet_type, handler in ( (AddEntityPacket, self.handle_add_entity), (SetEntityDataPacket, self.handle_entity_data), (BlockEntityDataPacket, self.handle_block_entity_data), (RemoveEntitiesPacket, self.handle_remove_entities), (MoveEntityPositionPacket, self.handle_move), (MoveEntityPositionRotationPacket, self.handle_move), (MoveEntityRotationPacket, self.handle_rotation), (EntityPositionSyncPacket, self.handle_position_sync), (PlayerInfoUpdatePacket, self.handle_player_info), (PlayerInfoRemovePacket, self.handle_player_info_remove), ): register(handler, packet_type) def handle_add_entity(self, packet): name = mobs.MOB_NAMES.get(packet.entity_type_id, 'unknown') common = dict(entity_id=packet.entity_id, x=packet.x, y=packet.y, z=packet.z) if name == 'player': player_uuid = str(packet.entity_uuid) self.g.players[packet.entity_id] = Munch( **common, player_uuid=player_uuid, yaw=packet.yaw, pitch=packet.pitch) elif name == 'item': self.g.objects[packet.entity_id] = Munch( **common, entity_uuid=str(packet.entity_uuid), velocity_x=packet.velocity_x * 8000, velocity_y=packet.velocity_y * 8000, velocity_z=packet.velocity_z * 8000) else: self.g.mobs[packet.entity_id] = Munch( **common, entity_uuid=str(packet.entity_uuid), type=packet.entity_type_id, yaw=packet.yaw, pitch=packet.pitch) def handle_block_entity_data(self, packet): location = packet.location self.g.chunks.set_block_entity( (location.x, location.y, location.z), packet.block_entity_type, packet.data) def handle_entity_data(self, packet): obj = self.g.objects.get(packet.entity_id) if obj is None or not packet.item_stacks: return stack = packet.item_stacks[-1] obj.item_id = stack.item_id if stack.present else None obj.item_count = stack.item_count if stack.present else 0 def _entity(self, entity_id): for collection in (self.g.players, self.g.mobs, self.g.objects): entity = collection.get(entity_id) if entity is not None: return entity return None def handle_move(self, packet): entity = self._entity(packet.entity_id) if entity is None: return entity.x += packet.delta_x entity.y += packet.delta_y entity.z += packet.delta_z if hasattr(packet, 'yaw'): entity.yaw = packet.yaw entity.pitch = packet.pitch def handle_rotation(self, packet): entity = self._entity(packet.entity_id) if entity is not None: entity.yaw = packet.yaw entity.pitch = packet.pitch def handle_position_sync(self, packet): entity = self._entity(packet.entity_id) if entity is not None: entity.x, entity.y, entity.z = packet.x, packet.y, packet.z entity.yaw = packet.yaw entity.pitch = packet.pitch def handle_remove_entities(self, packet): for entity_id in packet.entity_ids: self.g.players.pop(entity_id, None) self.g.mobs.pop(entity_id, None) self.g.objects.pop(entity_id, None) def handle_player_info(self, packet): for profile_uuid, name in packet.entries: if name: uuid_text = str(profile_uuid) self.g.player_names[uuid_text] = name self.g.player_names[name] = profile_uuid self.g.player_names[name.lower()] = profile_uuid def handle_player_info_remove(self, packet): removed = {str(uuid) for uuid in packet.uuids} for key, value in list(self.g.player_names.items()): if str(value) in removed or str(key) in removed: del self.g.player_names[key] class InventoryManager: def __init__(self, global_state): self.g = global_state self.g.inv = self.g.get('inv') or {} self.g.window = self.g.get('window') self.g.item_lock = self.g.get('item_lock', False) self.g.inventory_state_ids = self.g.get('inventory_state_ids') or {} self.g.inventory_partial = False self.g.cursor = Slot(present=False) register = self.g.connection.register_packet_listener register(self.handle_open, ContainerOpenPacket) register(self.handle_content, ContainerSetContentPacket) register(self.handle_slot, ContainerSetSlotPacket) register(self.handle_cursor, SetCursorItemPacket) @staticmethod def to_slot(item): return Slot(item.present, item.item_id, item.item_count, components=item.components, component_ids=item.component_ids, removed_components=item.removed_components, components_unknown=item.components_unknown) def handle_open(self, packet): self.g.window = Munch( window_id=packet.window_id, inventory_type=packet.inventory_type, title=packet.title, contents={}, container_slots=0, state_id=self.g.inventory_state_ids.get(packet.window_id, 0), ) def handle_content(self, packet): slots = {index: self.to_slot(item) for index, item in enumerate(packet.items)} self.g.inventory_state_ids[packet.container_id] = packet.state_id if packet.container_id == 0: self.g.inv = slots self.g.inventory_partial = packet.partial self.g.cursor = self.to_slot(packet.carried_item) self._sync_held_item() elif (self.g.get('window') and packet.container_id == self.g.window.window_id): self.g.window.contents = slots # Player inventory contributes 36 slots to chest menus. self.g.window.container_slots = max(0, len(slots) - 36) self.g.window.state_id = packet.state_id self.g.cursor = self.to_slot(packet.carried_item) self.g.item_lock = False def handle_slot(self, packet): self.g.inventory_state_ids[packet.container_id] = packet.state_id item = self.to_slot(packet.item) if packet.item.components_unknown: self.g.inventory_partial = True if packet.container_id == 0: self.g.inv[packet.slot] = item self.g.item_lock = False if (self.g.get('holding_hand', 0) == 0 and packet.slot == 36 + self.g.get('holding_slot', 0)): self.g.holding = item.item_id if item.present else 0 elif packet.container_id == -1 and packet.slot == -1: self.g.cursor = item elif (self.g.get('window') and packet.container_id == self.g.window.window_id): self.g.window.contents[packet.slot] = item self.g.window.state_id = packet.state_id offset = packet.slot - self.g.window.container_slots if 0 <= offset < 27: self.g.inv[9 + offset] = item elif 27 <= offset < 36: self.g.inv[36 + offset - 27] = item self.g.item_lock = False def _sync_held_item(self): if self.g.get('holding_hand', 0) != 0: return hotbar_slot = int(self.g.get('holding_slot', 0) or 0) item = self.g.inv.get(36 + hotbar_slot) self.g.holding = (item.item_id if item and getattr(item, 'present', False) else 0) def handle_cursor(self, packet): if packet.item.components_unknown: self.g.inventory_partial = True self.g.cursor = self.to_slot(packet.item)