627 lines
24 KiB
Python
627 lines
24 KiB
Python
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):
|
|
self.block_entities[tuple(position)] = {
|
|
'type': entity_type,
|
|
'data': nbt_plain(data),
|
|
}
|
|
|
|
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]
|
|
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.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)
|
|
|
|
pending = self.g.get('pending_chest_open')
|
|
pending_age = (time.monotonic() - 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)
|
|
is_bot_opening = (
|
|
pending_is_fresh and
|
|
tuple(pending.get('position', ())) == position
|
|
)
|
|
if pending is not None and (is_bot_opening or not pending_is_fresh):
|
|
self.g.pending_chest_open = None
|
|
|
|
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)
|