diff --git a/README.md b/README.md index c43527f..58cce07 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,10 @@ AuthMe password. Override `SERVER`, `PORT`, `PLAYER_NAME`, or `GAME_PASSWORD` through environment variables as needed. Microsoft authentication is not used. The 26.3 client now handles AuthMe, chat commands, chunk/world-state caching, -modern block/item/entity data, basic entity tracking, and path-following movement -(`1goto x y z`). -Inventory, entity tracking, block breaking/placing, and the farming/trading jobs -remain disabled until their 26.3 packets are migrated. The default server is +modern block/item/entity data, entity tracking, read-only inventory/component +sync, and path-following movement (`1goto x y z`). Inventory actions, block +breaking/placing, and farming/trading jobs remain disabled until their 26.3 +packets are migrated. The default server is `127.0.0.1`; set `SERVER` explicitly only when you intentionally want another server. @@ -61,9 +61,12 @@ and all bots will run the command. The current 26.3 command path handles public chat. Private-message routing is not migrated yet. Verified commands are `!ping`, `!pos`, `!loaded`, `!info x y -z`, and addressed path movement such as `1goto x y z`. The command catalog below -is retained from the legacy bot; inventory, interaction, and farming/trading -commands are not yet available on 26.3. +z`, `!inv` (basic slot/count data), and addressed path movement such as +`1goto x y z`. The command catalog below is retained from the legacy bot; +inventory actions, block interaction, and farming/trading commands are not yet +available on 26.3. Common item components (names, damage, enchantments, book +contents, and shulker contents) are decoded; unrecognized component types are +marked partial rather than silently discarded. ### Public Commands diff --git a/mosfet.py b/mosfet.py index 9bbb818..4173cd0 100644 --- a/mosfet.py +++ b/mosfet.py @@ -64,9 +64,9 @@ def root(): def main(): """Connect with offline/AuthMe and enable only the migrated 26.3 features. - Current gameplay support is read-only world/entity state plus path - following; inventory, interaction, and legacy job packet handlers remain - disabled until migrated. + Current gameplay support is read-only world/entity/inventory state plus + path following; inventory actions, block interactions, and legacy job + packet handlers remain disabled until migrated. """ reload_timeout = time.time() reload_requested = False diff --git a/mosfet/commands.py b/mosfet/commands.py index 7e4013f..4ec4e2d 100644 --- a/mosfet/commands.py +++ b/mosfet/commands.py @@ -113,30 +113,75 @@ class Commands: if command == 'inv' or command == 'inventory': inv_list = [] uniq_item_counts = {} - for i in self.g.inv.values(): - if i.present: - inv_list.append((items.ITEM_NAMES[i.item_id], str(i.item_id), i.item_count)) + for slot, i in self.g.inv.items(): + if not i.present: + continue + base_name = items.ITEM_NAMES.get(i.item_id, str(i.item_id)) + components = getattr(i, 'components', {}) or {} + custom_name = components.get('custom_name') or components.get('item_name') + if isinstance(custom_name, dict): + custom_name = custom_name.get('text') or custom_name.get('raw') + display_name = str(custom_name or base_name) + details = [] + for key in ('enchantments', 'stored_enchantments'): + for name, level in components.get(key, {}).items(): + details.append('{} {}'.format(name, level)) + contents = components.get('container') + if contents: + nested_count = sum(bool(getattr(item, 'present', False)) + for item in contents) + details.append('{} items inside'.format(nested_count)) + written = components.get('written_book_content') + if written: + title = written.get('title', {}).get('raw', '') + details.append('book: {} ({} pages)'.format( + title, len(written.get('pages', [])))) + writable = components.get('writable_book_content') + if writable: + details.append('writable book ({} pages)'.format( + len(writable))) + if getattr(i, 'components_unknown', False): + details.append('components partial') + detail_text = ' [{}]'.format(', '.join(details)) if details else '' + inv_list.append((slot, display_name, str(i.item_id), + i.item_count, detail_text)) + uniq_item_counts[i.item_id] = ( + uniq_item_counts.get(i.item_id, 0) + i.item_count) - if i.item_id not in uniq_item_counts: - uniq_item_counts[i.item_id] = 0 - uniq_item_counts[i.item_id] += i.item_count - - inv_list.sort() - console_result = '\n'.join(['{}:{} x {}'.format(*x) for x in inv_list]) + inv_list.sort(key=lambda entry: (entry[1], entry[2])) + console_result = '\n'.join( + 'slot {}: {}:{} x {}{}'.format(*entry) + for entry in inv_list) if not console_result: print('Empty') reply = 'empty' else: print(console_result) + if self.g.get('inventory_partial', False): + print('Warning: inventory contains stacks with undecoded components.') - reply_result_1 = ', '.join(['{}:{} x {}'.format(*x) for x in inv_list]) - reply_result_2 = ', '.join(['{}:{} x {}'.format(items.ITEM_NAMES[k], str(k), v) for k,v in uniq_item_counts.items()]) - reply_result_3 = ', '.join(['{}:{}x{}'.format(items.ITEM_NAMES[k], str(k), v) for k,v in uniq_item_counts.items()]) - reply_result_4 = ', '.join(['{}:{}x{}'.format(re.sub(r'[aeiou]', '', items.ITEM_NAMES[k]), str(k), v) for k,v in uniq_item_counts.items()]) - reply_result_5 = ' '.join(['{}{}x{}'.format(re.sub(r'[aeiou]', '', items.ITEM_NAMES[k]), str(k), v) for k,v in uniq_item_counts.items()]) + reply_result_1 = ', '.join( + '{}:{} x {}{}'.format(*entry[1:]) for entry in inv_list) + reply_result_components = ', '.join( + '{}{}'.format(entry[1], entry[4]) + for entry in inv_list if entry[4]) + reply_result_2 = ', '.join( + '{}:{} x {}'.format(items.ITEM_NAMES.get(k, str(k)), str(k), v) + for k, v in uniq_item_counts.items()) + reply_result_3 = ', '.join( + '{}:{}x{}'.format(items.ITEM_NAMES.get(k, str(k)), str(k), v) + for k, v in uniq_item_counts.items()) + reply_result_4 = ', '.join( + '{}:{}x{}'.format(re.sub(r'[aeiou]', '', items.ITEM_NAMES.get(k, str(k))), str(k), v) + for k, v in uniq_item_counts.items()) + reply_result_5 = ' '.join( + '{}{}x{}'.format(re.sub(r'[aeiou]', '', items.ITEM_NAMES.get(k, str(k))), str(k), v) + for k, v in uniq_item_counts.items()) - for r in [reply_result_1, reply_result_2, reply_result_3, reply_result_4, reply_result_5]: + for r in [reply_result_1, reply_result_components, + reply_result_2, reply_result_3, reply_result_4, + reply_result_5]: reply = r if len(reply) < 256: break diff --git a/mosfet/connect.py b/mosfet/connect.py index 26986a5..47eb13d 100644 --- a/mosfet/connect.py +++ b/mosfet/connect.py @@ -80,10 +80,12 @@ def main(global_state=None): from mosfet.commands import Commands from mosfet.protocol.modern import ModernChatManager from mosfet.protocol.entities_26_3 import ModernEntityManager + from mosfet.protocol.inventory_26_3 import ModernInventoryManager from mosfet.automation_26_3 import start as start_modern_movement global_state.world = World(global_state) global_state.chat = ModernChatManager(global_state) global_state.entities = ModernEntityManager(global_state) + global_state.inventory = ModernInventoryManager(global_state) global_state.commands = Commands(global_state) start_modern_movement(global_state) diff --git a/mosfet/protocol/inventory_26_3.py b/mosfet/protocol/inventory_26_3.py new file mode 100644 index 0000000..5301fc3 --- /dev/null +++ b/mosfet/protocol/inventory_26_3.py @@ -0,0 +1,55 @@ +"""Minimal 26.3 inventory synchronization for legacy read-only commands.""" +from mosfet.protocol.types import Slot +from minecraft.networking.modern import ( + ContainerSetContentPacket, ContainerSetSlotPacket, SetCursorItemPacket, +) + + +class ModernInventoryManager: + def __init__(self, global_state): + self.g = global_state + self.g.inv = self.g.get('inv') or {} + 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_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_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'): + self.g.window.contents = slots + self.g.cursor = self.to_slot(packet.carried_item) + + 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 + elif packet.container_id == -1 and packet.slot == -1: + self.g.cursor = item + elif self.g.get('window'): + self.g.window.contents[packet.slot] = item + + def handle_cursor(self, packet): + if packet.item.components_unknown: + self.g.inventory_partial = True + self.g.cursor = self.to_slot(packet.item) diff --git a/mosfet/protocol/types.py b/mosfet/protocol/types.py index c86e183..920dca3 100644 --- a/mosfet/protocol/types.py +++ b/mosfet/protocol/types.py @@ -103,11 +103,17 @@ class Nbt(Type): class Slot(Type): - def __init__(self, present, item_id=None, item_count=None, nbt=None): + def __init__(self, present, item_id=None, item_count=None, nbt=None, + components=None, component_ids=None, + removed_components=None, components_unknown=False): self.present = present self.item_id = item_id self.item_count = item_count self.nbt = nbt + self.components = components or {} + self.component_ids = component_ids or [] + self.removed_components = removed_components or [] + self.components_unknown = components_unknown def __str__(self): return str(self.__dict__)