Files
minecraft-bot/mosfet/protocol/managers.py
T

508 lines
19 KiB
Python

import json
from math import floor
from munch import Munch
from minecraft.networking.packets import clientbound, serverbound
from minecraft.networking.modern import (
AddEntityPacket, ContainerOpenPacket, ContainerSetContentPacket,
ContainerSetSlotPacket,
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 = {}
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 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)
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),
(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 _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)
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
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 handle_cursor(self, packet):
if packet.item.components_unknown:
self.g.inventory_partial = True
self.g.cursor = self.to_slot(packet.item)