import re import time import random from itertools import count from math import floor, isfinite from munch import Munch from minecraft.networking.packets import Packet, clientbound, serverbound from minecraft.networking.types import BlockFace, Byte, Short, VarInt from minecraft.networking.modern import ( ClientTickEnd, ContainerClickSwapPacket, PlayerActionPacket, PunchPacket, SetCarriedItemPacket, SetHeldSlotPacket, UseItemOnBlockPacket, ) from mosfet.protocol.packets import ( SetSlotPacket, PlayerDiggingPacket, BlockBreakAnimationPacket, AcknowledgePlayerDiggingPacket, HeldItemChangePacket, PickItemPacket, OpenWindowPacket, ClickWindowPacket, CloseWindowPacket, ServerWindowConfirmationPacket, ClientWindowConfirmationPacket, EntityMetadataPacket, SpawnLivingEntityPacket, EntityPositionRotationPacket, DestroyEntitiesPacket, EntityActionPacket, EntityTeleport, InteractEntityPacket, TradeListPacket, SelectTradePacket, DisconnectPacket, UnloadChunkPacket, ) from mosfet.protocol.types import Slot from mosfet.protocol.modern import ( ClientCommandPacket, PlayerCommandPacket, PlayerInputPacket, UpdateTimePacket, UseItemPacket, ) from mosfet import utils from mosfet import path from mosfet import bot from mosfet import vector from mosfet.info import blocks from mosfet.info import items from mosfet.info import mcdata from mosfet.info import mobs class ContainerClickDropPacket(Packet): """Drop the full stack in one player-inventory slot (protocol 777).""" id = 0x12 packet_name = 'container click drop' def write_fields(self, packet_buffer): VarInt.send(self.container_id, packet_buffer) VarInt.send(self.state_id, packet_buffer) Short.send(self.slot, packet_buffer) Byte.send(getattr(self, 'button', 1), packet_buffer) VarInt.send(4, packet_buffer) # ContainerInput.THROW VarInt.send(0, packet_buffer) # no changed-slot predictions VarInt.send(0, packet_buffer) # empty carried item stack class ContainerClickQuickMovePacket(Packet): """Shift-click an inventory slot into the open container (protocol 777).""" id = 0x12 packet_name = 'container click quick move' def write_fields(self, packet_buffer): VarInt.send(self.container_id, packet_buffer) VarInt.send(self.state_id, packet_buffer) Short.send(self.slot, packet_buffer) Byte.send(0, packet_buffer) # mouse button VarInt.send(1, packet_buffer) # ContainerInput.QUICK_MOVE VarInt.send(0, packet_buffer) # no changed-slot predictions VarInt.send(0, packet_buffer) # empty carried item stack class ContainerClosePacket(Packet): id = 0x13 packet_name = 'container close' definition = [{'container_id': VarInt}] class LegacyGame: def __init__(self, global_state): self.g = global_state register = self.g.connection.register_packet_listener register(self.handle_login_success, clientbound.login.LoginSuccessPacket) register(self.handle_block_change, clientbound.play.BlockChangePacket) register(self.handle_join_game, clientbound.play.JoinGamePacket) register(self.handle_position_and_look, clientbound.play.PlayerPositionAndLookPacket) register(self.handle_time_update, clientbound.play.TimeUpdatePacket) register(self.handle_set_slot, SetSlotPacket) register(self.handle_break_animation, BlockBreakAnimationPacket) register(self.handle_break_ack, AcknowledgePlayerDiggingPacket) register(self.handle_window, OpenWindowPacket) register(self.handle_window_confirmation, ClientWindowConfirmationPacket) register(self.handle_spawn_object, clientbound.play.SpawnObjectPacket) register(self.handle_entity_metadata, EntityMetadataPacket) register(self.handle_spawn_living, SpawnLivingEntityPacket) register(self.handle_entity_position, clientbound.play.EntityPositionDeltaPacket) register(self.handle_entity_position_rotation, EntityPositionRotationPacket) register(self.handle_entity_look, clientbound.play.EntityLookPacket) register(self.handle_destroy_entities, DestroyEntitiesPacket) register(self.handle_spawn_player, clientbound.play.SpawnPlayerPacket) register(self.handle_respawn, clientbound.play.RespawnPacket) register(self.handle_player_list, clientbound.play.PlayerListItemPacket) register(self.handle_entity_teleport, EntityTeleport) register(self.handle_update_health, clientbound.play.UpdateHealthPacket) #register(self.handle_entity_velocity, clientbound.play.EntityVelocityPacket) register(self.handle_trade_list, TradeListPacket) register(self.handle_disconnect, DisconnectPacket) register(self.handle_unload_chunk, UnloadChunkPacket) #register(self.handle_packet, Packet, early=True) def handle_login_success(self, packet): print(packet) self.g.name = packet.Username def handle_join_game(self, packet): print('Received join game packet') print('Connected.') self.g.info = packet self.g.eid = packet.entity_id self.g.dimension = packet.world_name.replace('minecraft:', '') def handle_block_change(self, packet): if packet.block_state_id == blocks.SOUL_TORCH: try: self.g.goal = vector.Point3D((packet.location[0], packet.location[1], packet.location[2])) print('new waypoint:', self.g.goal) start = time.time() solution = path.Pathfinder(self.g).astar(utils.pint(self.g.pos), utils.pint(self.g.goal)) if solution: solution = list(solution) self.g.path = solution if self.g.job: self.g.job.stop() print(len(solution)) print(solution) print(round(time.time() - start, 3), 'seconds') else: print('No path found') #say(connection, 'No path found') #g.y_v = 10.0 #g.y_a = -36.0 except BaseException as e: import traceback print(traceback.format_exc()) #print(packet) def handle_position_and_look(self, packet): print(packet) p = vector.Point3D((packet.x, packet.y, packet.z)) self.g.pos = p confirm_packet = serverbound.play.TeleportConfirmPacket() confirm_packet.teleport_id = packet.teleport_id self.g.connection.write_packet(confirm_packet) self.g.correction_count += 1 if self.g.get('path', None) and self.g.correction_count > 5: self.g.correction_count = 0 dest = self.g.path[-1] w = self.g.world p = utils.pint(self.g.pos) new_path = w.path_to_place(p, dest) if new_path: self.g.path = new_path def handle_set_held_slot(self, packet): slot = int(packet.slot) if not 0 <= slot <= 8: return self.g.holding_slot = slot self.g.holding_hand = 0 item = self.g.inv.get(36 + slot) self.g.holding = (item.item_id if item and getattr(item, 'present', False) else 0) def handle_time_update(self, packet): if hasattr(packet, 'time_of_day'): self.g.time = packet.time_of_day % 24000 return updates = getattr(packet, 'clock_updates', ()) if not updates: return registries = getattr(self.g.connection, 'registry_ids', {}) clock_names = { index: name for name, index in registries.get( 'minecraft:world_clock', {}).items() } dimension = self.g.get('dimension', 'overworld') preferred = [ update for update in updates if clock_names.get(update['clock_id'], '').removeprefix( 'minecraft:') in (dimension, 'overworld') ] update = preferred[0] if preferred else updates[0] self.g.time = update['total_ticks'] % 24000 self.g.clock_id = update['clock_id'] self.g.clock_name = clock_names.get(update['clock_id']) self.g.clock_rate = update['rate'] self.g.world_age = packet.age def handle_set_slot(self, packet): g = self.g print(packet) if packet.window_id == 0: g.inv[packet.slot] = packet.slot_data elif g.window: g.window.contents[packet.slot] = packet.slot_data if g.item_lock and packet.window_id >= 0 and not packet.slot_data.present: print('Unlocking item lock') g.item_lock = False def break_block(self, location): p = utils.pint(self.g.pos) #if utils.phyp(p, location) > blocks.BREAK_DISTANCE + 1: # return False bid = self.g.chunks.get_block_at(*location) if bid == 0: return False packet = PlayerDiggingPacket() packet.status = 0 packet.location = location packet.face = 1 self.g.connection.write_packet(packet) self.g.breaking = location self.g.break_time = time.time() + utils.break_time(bid, self.g.holding) return True def break_finish(self): packet = PlayerDiggingPacket() packet.status = 2 packet.location = self.g.breaking packet.face = 1 self.g.connection.write_packet(packet) #self.g.chunks.set_block_at(*self.g.breaking, 0) if self.g.chunks.get_block_at(*self.g.breaking) == 0: self.g.breaking = None def handle_break_animation(self, packet): return print(packet) def handle_break_ack(self, packet): #print(packet) return def respawn(self): packet = serverbound.play.ClientStatusPacket() packet.action_id = serverbound.play.ClientStatusPacket.RESPAWN self.g.connection.write_packet(packet) def animate(self): packet = serverbound.play.AnimationPacket() packet.hand = packet.HAND_MAIN self.g.connection.write_packet(packet) def place_block(self, location, face): packet = serverbound.play.PlayerBlockPlacementPacket() packet.hand = 0 packet.location = location packet.face = face packet.x = 0.5 packet.y = 0.5 packet.z = 0.5 packet.inside_block = False self.g.connection.write_packet(packet) def pick(self, slot): packet = PickItemPacket() packet.slot_to_use = slot self.g.connection.write_packet(packet) def hold(self, slot): packet = HeldItemChangePacket() packet.slot = slot self.g.connection.write_packet(packet) def choose_slot(self, slot): if slot >= 36: slot -= 36 self.hold(slot) else: self.pick(slot) def count_items(self, items): # count how many items are in inv count = 0 for slot, item in self.g.inv.items(): if item.item_id in items: count += item.item_count return count def count_inventory_slots(self): # count how many inventory slots are filled # excludes armour, crafting slots, off-hand count = 0 for slot, item in self.g.inv.items(): if item.present and slot >= 9 and slot <= 45: count += 1 return count def count_window_slots(self): # count how many window slots are filled # excludes player inventory w = self.g.window w_info = mcdata.WINDOWS[w.data.window_type] w_container_slots = w_info.container count = 0 for slot, item in w.contents.items(): if item.present and slot in w_container_slots: count += 1 return count def get_window_slot(self, item_id): # get the first slot that matches item of a window window_items = list(self.g.window.contents.items()) for slot, item in window_items: if not item.present: continue if item.item_id == item_id: return slot, item else: #for return False, False def select_item(self, items): # select the first match from items of inv # uses smallest stack of that match # and optionally the most damaged item inv_items = list(self.g.inv.items()) inv_items.sort(key=lambda x: (x[1].nbt or {}).get('Damage', 0), reverse=True) inv_items.sort(key=lambda x: x[1].item_count or 0) for slot, item in inv_items: if item.item_id in items: self.g.game.choose_slot(slot) self.g.holding = item.item_id return True else: #for return False def select_random_item(self, items): # select a random match from items of inv # this is random per item type # example: 5 stacks wood, 1 stack glass # -> still 50/50 chance between them matches = set() for slot, item in self.g.inv.items(): if item.item_id in items: matches.add(item.item_id) if matches: return self.select_item([random.choice(list(matches))]) else: return False def select_next_item(self): # select the next item slot that has an item for slot, item in self.g.inv.items(): if slot < 9: continue # skip armour slots if item.present: print('slot:', slot, 'item:', item) self.g.game.choose_slot(slot) self.g.holding = item.item_id return True else: # for return False def drop_stack(self): packet = PlayerDiggingPacket() packet.status = 3 packet.location = utils.pint(self.g.pos) packet.face = 1 self.g.connection.write_packet(packet) def open_container(self, location, face=BlockFace.TOP, cursor=None): location = tuple(int(value) for value in location) positions = {location} world = self.g.get('world') if world and hasattr(world, '_double_chest_positions'): positions.update(world._double_chest_positions(location)) for offset in ((1, 0, 0), (-1, 0, 0), (0, 0, 1), (0, 0, -1), (0, 1, 0), (0, -1, 0)): positions.add(utils.padd(location, offset)) self.g.pending_chest_open = { 'position': location, 'positions': tuple(sorted(positions)), 'requested_at': time.monotonic(), } self.place_block(location, face, cursor=cursor) def handle_window(self, packet): print(packet) self.g.window = Munch(data=packet, contents=dict(), count=0) def click_window(self, slot, button, mode, item): w = self.g.window packet = ClickWindowPacket() packet.window_id = w.data.window_id packet.slot = slot packet.button = button packet.action_number = w.count packet.mode = mode packet.clicked_item = item self.g.connection.write_packet(packet) print('<--', packet) w.count += 1 def close_window(self): if self.g.window: packet = CloseWindowPacket() packet.window_id = self.g.window.data.window_id self.g.connection.write_packet(packet) self.g.window = None def handle_window_confirmation(self, packet): print(packet) packet2 = ServerWindowConfirmationPacket() packet2.window_id = packet.window_id packet2.action_number = packet.action_number packet2.accepted = packet.accepted self.g.connection.write_packet(packet2) def handle_spawn_player(self, packet): print(packet) self.g.players[packet.entity_id] = Munch( entity_id=packet.entity_id, player_uuid=packet.player_UUID, x=packet.x, y=packet.y, z=packet.z, yaw=packet.yaw, pitch=packet.pitch, ) def handle_spawn_object(self, packet): #return if packet.type_id != 37: return #print(packet) self.g.objects[packet.entity_id] = Munch( entity_id=packet.entity_id, x=packet.x, y=packet.y, z=packet.z, velocity_x=packet.velocity_x, velocity_y=packet.velocity_y, velocity_z=packet.velocity_z, ) def check_gapple(self, packet): current_gapple_chest = self.g.job.find_gapple_states.current_chest if current_gapple_chest: for entry in packet.metadata: if entry.type != 6: continue if entry.value.item_id in items.GAPPLE_ID: self.g.chat.send('gapple found: ' + str(current_gapple_chest)[1:-1]) print('gapple found:', str(current_gapple_chest)[1:-1]) def handle_entity_metadata(self, packet): if not packet.metadata: return if self.g.job and self.g.job.state == self.g.job.find_gapple_states: self.check_gapple(packet) obj = self.g.objects.get(packet.entity_id, None) if obj: for entry in packet.metadata: if entry.type != 6: continue obj.item_id = entry.value.item_id obj.item_count = entry.value.item_count mob = self.g.mobs.get(packet.entity_id, None) if mob: for entry in packet.metadata: if mob.type == mobs.VILLAGER: if entry.index == 17: mob.profession = entry.value[1] player = self.g.players.get(packet.entity_id, None) if player: return def handle_spawn_living(self, packet): self.g.mobs[packet.entity_id] = Munch( entity_id=packet.entity_id, entity_uuid=packet.entity_uuid, type=packet.type, x=packet.x, y=packet.y, z=packet.z, ) def handle_entity_position(self, packet): mob = self.g.mobs.get(packet.entity_id, None) if mob: mob.x += packet.delta_x / 4096.0 mob.y += packet.delta_y / 4096.0 mob.z += packet.delta_z / 4096.0 player = self.g.players.get(packet.entity_id, None) if player: player.x += packet.delta_x / 4096.0 player.y += packet.delta_y / 4096.0 player.z += packet.delta_z / 4096.0 #if player.player_uuid == '0c123cfa-1697-4427-9413-4b645dee7ec0': print(packet) def handle_entity_position_rotation(self, packet): mob = self.g.mobs.get(packet.entity_id, None) if mob: mob.x += packet.delta_x / 4096.0 mob.y += packet.delta_y / 4096.0 mob.z += packet.delta_z / 4096.0 player = self.g.players.get(packet.entity_id, None) if player: player.x += packet.delta_x / 4096.0 player.y += packet.delta_y / 4096.0 player.z += packet.delta_z / 4096.0 player.yaw = packet.yaw player.pitch = packet.pitch def handle_entity_look(self, packet): player = self.g.players.get(packet.entity_id, None) if player: player.yaw = packet.yaw player.pitch = packet.pitch def handle_entity_teleport(self, packet): mob = self.g.mobs.get(packet.entity_id, None) if mob: mob.x = packet.x mob.y = packet.y mob.z = packet.z player = self.g.players.get(packet.entity_id, None) if player: player.x = packet.x player.y = packet.y player.z = packet.z #if player.player_uuid == '0c123cfa-1697-4427-9413-4b645dee7ec0': print(packet) def handle_entity_velocity(self, packet): obj = self.g.objects.get(packet.entity_id, None) if obj: print(packet) #obj.velocity_x = packet.velocity_x #obj.velocity_y = packet.velocity_y #obj.velocity_z = packet.velocity_z def handle_destroy_entities(self, packet): for eid in packet.entity_ids: if eid in self.g.objects: del self.g.objects[eid] if eid in self.g.mobs: del self.g.mobs[eid] if eid in self.g.players: del self.g.players[eid] def leave_bed(self): packet = EntityActionPacket() packet.entity_id = self.g.eid packet.action_id = 2 packet.jump_boost = 0 self.g.connection.write_packet(packet) def handle_respawn(self, packet): print(packet) self.g.dimension = packet.world_name.replace('minecraft:', '') self.g.chunks.unload_all_chunks() def handle_player_list(self, packet): for action in packet.actions: if isinstance(action, packet.AddPlayerAction): self.g.player_names[action.uuid] = action.name self.g.player_names[action.name] = action.uuid self.g.player_names[action.name.lower()] = action.uuid # porque no los dos? def handle_update_health(self, packet): print(packet) self.g.health = packet.health self.g.food = packet.food if packet.health == 0: print('Died, stopping') print('Use 1respawn to respawn the bot') self.close_window() bot.init(self.g) def use_item(self, hand): packet = serverbound.play.UseItemPacket() packet.hand = hand self.g.connection.write_packet(packet) def interact(self, eid): packet = InteractEntityPacket() packet.entity_id = eid packet.type = 0 packet.hand = 0 packet.sneaking = False self.g.connection.write_packet(packet) def handle_trade_list(self, packet): print(packet) self.g.trades = packet.trades def select_trade(self, num): packet = SelectTradePacket() packet.selected_slot = num self.g.connection.write_packet(packet) def handle_disconnect(self, packet): print(packet) print('Client disconnected!') import os os._exit(1) def handle_unload_chunk(self, packet): self.g.chunks.unload_chunk(packet.chunk_x, packet.chunk_z) def tick(self): if self.g.breaking: self.animate() if time.time() >= self.g.break_time: #- 2*utils.TICK: self.break_finish() if self.g.dumping and not self.g.item_lock: if self.select_item([self.g.dumping]): self.drop_stack() self.g.item_lock = True else: self.g.dumping = None if self.g.draining and not self.g.item_lock: if self.select_next_item(): self.drop_stack() self.g.item_lock = True else: self.g.draining = False if not self.g.path: self.g.correction_count = 0 # Active game facade. The constructor registers no legacy packet listeners; # migrated actions use the 26.3 packet classes imported above. class Game(LegacyGame): ARM_SWING_INTERVAL = 0.1 DOOR_AUTO_OPEN_GRACE = 0.25 DOOR_TURN_DURATION = 0.3 def __init__(self, global_state): self.g = global_state self.sequence = 0 self.break_deadline = None self.breaking_state = None self.next_swing_at = 0.0 self.door_navigation = None self.g.yaw_target_override = None self.g.unopenable_doors = self.g.get('unopenable_doors') or set() from mosfet.protocol.packets import SetHealthPacket # Health is a protocol-777 packet, not the legacy UpdateHealthPacket. # Register its decoder on an already-running reactor as well, so a # hot reload can enable health tracking without reconnecting. Rebuild # the base map first to remove any stale decoder for an earlier ID. reactor = self.g.connection.reactor if hasattr(reactor, 'clientbound_packets'): context = self.g.connection.context reactor.clientbound_packets = { packet.get_id(context): packet for packet in reactor.__class__.get_clientbound_packets(context) } reactor.clientbound_packets[SetHealthPacket.id] = SetHealthPacket reactor.clientbound_packets[UpdateTimePacket.id] = UpdateTimePacket self.g.connection.register_packet_listener( self.handle_set_health, SetHealthPacket) self.g.connection.register_packet_listener( self.handle_time_update, UpdateTimePacket) self.g.connection.register_packet_listener( self.handle_set_held_slot, SetHeldSlotPacket) if not self.g.get('health_update_received', False): self.g.health = None self.g.food = None self.g.hunger = None self.g.food_saturation = None self.g.saturation = None self.g.health_update_received = False self.g.health_updated_at = None self.g.dead = False def handle_set_health(self, packet): self.g.health = float(packet.health) self.g.food = int(packet.food) self.g.hunger = self.g.food self.g.food_saturation = float(packet.food_saturation) self.g.saturation = self.g.food_saturation self.g.health_update_received = True self.g.health_updated_at = time.time() self.g.dead = self.g.health <= 0 print('[status] health={:.1f} hunger={} saturation={:.2f}'.format( self.g.health, self.g.food, self.g.food_saturation), flush=True) def _send_action(self, packet): self.g.connection.write_packet(packet, force=True) self.g.connection.write_packet(ClientTickEnd(), force=True) def _next_sequence(self): self.sequence += 1 return self.sequence def _door_info(self, position): state_id = self.g.world.block_at(*position) if state_id not in blocks.DOOR_IDS: return None properties = blocks.PROPS.get(state_id, {}) lower = tuple(position) if properties.get('half') == 'upper': lower = utils.padd(lower, path.BLOCK_BELOW) lower_state = self.g.world.block_at(*lower) if lower_state in blocks.DOOR_IDS: state_id = lower_state properties = blocks.PROPS.get(state_id, {}) return lower, state_id, properties def _toggle_door(self, lower, state_id): position = utils.pint(self.g.pos) dx, dz = position[0] - lower[0], position[2] - lower[2] if abs(dx) > abs(dz): if dx >= 0: face, cursor = BlockFace.EAST, (0.95, 0.5, 0.5) else: face, cursor = BlockFace.WEST, (0.05, 0.5, 0.5) elif abs(dz) > 0: if dz >= 0: face, cursor = BlockFace.SOUTH, (0.5, 0.5, 0.95) else: face, cursor = BlockFace.NORTH, (0.5, 0.5, 0.05) else: face, cursor = BlockFace.TOP, (0.5, 0.95, 0.5) boxes = blocks.COLLISION_BOXES.get(state_id, ()) if boxes: box = boxes[0] cursor = ((box[0] + box[3]) / 2, (box[1] + box[4]) / 2, (box[2] + box[5]) / 2) self.place_block(lower, face, cursor=cursor) def _look_at_with_yaw_separation(self, target, fallback=None): """Keep target yaw far enough from the player to avoid the near-zero guard.""" player = self.g.pos player_x = player.x if hasattr(player, 'x') else player[0] player_z = player.z if hasattr(player, 'z') else player[2] center = [target[0] + 0.5, target[1] + 0.5, target[2] + 0.5] dx, dz = player_x - center[0], player_z - center[2] distance = utils.hypot(dx, dz) if distance < 0.8: if distance < 1e-6 and fallback is not None: dx, dz = fallback distance = utils.hypot(dx, dz) if distance > 1e-6: scale = 0.8 / distance center[0] = player_x - dx * scale center[2] = player_z - dz * scale return tuple(value - 0.5 for value in center) def _door_yaw_target(self, look_at): target = vector.Point3D(look_at) target.x += 0.5 target.z += 0.5 position = self.g.pos if not hasattr(position, 'x'): position = vector.Point3D(position) direction = position - target direction.y = 0 if direction.length() <= 0.6: return self.g.yaw return direction.normalized().signedAngleDeg( other=bot.YAW_ANGLE_DIR, ref=bot.YAW_ANGLE_REF) def _begin_door_turn(self, navigation, target, fallback=None): look_at = self._look_at_with_yaw_separation(target, fallback) yaw = self.g.yaw target_yaw = self._door_yaw_target(look_at) delta = (target_yaw - yaw + 180) % 360 - 180 navigation['yaw_origin'] = yaw navigation['yaw_door_target'] = yaw + delta self.g.yaw_target_override = navigation['yaw_door_target'] self.g.look_at = look_at def _begin_door_return(self, navigation, now): navigation['phase'] = 'turning_back' navigation['deadline'] = now + self.DOOR_TURN_DURATION self.g.yaw_target_override = navigation.get('yaw_origin') target = navigation.get('forward_look_at') self.g.look_at = (self._look_at_with_yaw_separation(target) if target is not None else navigation['previous_look_at']) def _restore_door_look(self, navigation): self.g.look_at = navigation['previous_look_at'] self.g.yaw_target_override = None self.door_navigation = None def handle_path_doors(self): """Open closed wooden doors in the active route and close them after crossing.""" now = time.monotonic() navigation = self.door_navigation if navigation is not None: lower = navigation['position'] info = self._door_info(lower) is_open = bool(info and info[2].get('open') == 'true') if navigation['phase'] == 'checking': if (info is None or info[1] not in blocks.OPENABLE_DOOR_IDS): self._restore_door_look(navigation) return False if is_open: navigation['phase'] = 'crossing' navigation['opened_by_bot'] = False navigation['saw_open'] = True self.g.look_at = navigation['previous_look_at'] return False if now >= navigation['deadline']: navigation['phase'] = 'opening' navigation['opened_by_bot'] = True navigation['deadline'] = now + 2.0 print('[door] opening on path at', lower, flush=True) self._toggle_door(lower, info[1]) return True if navigation['phase'] == 'opening': if not self.g.path: self._restore_door_look(navigation) return False if is_open: navigation['phase'] = 'crossing' navigation['saw_open'] = True self.g.look_at = navigation['previous_look_at'] return False if now > navigation['deadline']: upper = utils.padd(lower, path.BLOCK_ABOVE) self.g.unopenable_doors.update((lower, upper)) goal = navigation['goal'] self._restore_door_look(navigation) current = utils.pint(self.g.pos) self.g.path = (self.g.world.path_to_place(current, goal) or []) print('Door did not open; routing around', lower, flush=True) return False return True if navigation['phase'] == 'crossing': if navigation['after'] is not None and \ utils.pint(self.g.pos) == navigation['after']: if navigation.get('saw_open', False): navigation['phase'] = 'turning_to_close' navigation['deadline'] = ( now + self.DOOR_TURN_DURATION) navigation['close_door'] = ( navigation.get('opened_by_bot', False) and is_open and info is not None and info[2].get('powered') != 'true') after = navigation['after'] fallback = ((after[0] - lower[0], after[2] - lower[2]) if after is not None else None) self._begin_door_turn(navigation, lower, fallback) return True self._restore_door_look(navigation) return False if not is_open: # If a pressure plate or redstone opened it, wait briefly # for its state update rather than toggling it closed. if (info is not None and info[1] in blocks.OPENABLE_DOOR_IDS and lower in self.g.path): navigation['phase'] = 'checking' navigation['opened_by_bot'] = False navigation['deadline'] = ( now + self.DOOR_AUTO_OPEN_GRACE) return True if (navigation.get('saw_open') and navigation['after'] is not None): return False self._restore_door_look(navigation) return False if navigation['phase'] == 'turning_to_close': if now >= navigation['deadline']: if (navigation.get('close_door') and is_open and info is not None and info[2].get('powered') != 'true'): navigation['phase'] = 'closing' navigation['deadline'] = now + 2.0 print('[door] closing after crossing at', lower, flush=True) self._toggle_door(lower, info[1]) else: self._begin_door_return(navigation, now) return True if navigation['phase'] == 'closing': if not is_open or now > navigation['deadline']: self._begin_door_return(navigation, now) return True if navigation['phase'] == 'turning_back': if now >= navigation['deadline']: self._restore_door_look(navigation) return False return True if not self.g.path: return False current = utils.pint(self.g.pos) path_index = 0 if (len(self.g.path) > 1 and tuple(self.g.path[0]) == current): path_index = 1 if path_index >= len(self.g.path): return False info = self._door_info(tuple(self.g.path[path_index])) if info is None: return False lower, state_id, properties = info is_open = properties.get('open') == 'true' if (not is_open and state_id not in blocks.OPENABLE_DOOR_IDS) or \ utils.phyp(current, lower) > 3: return False after_index = path_index + 1 after = (tuple(self.g.path[after_index]) if after_index < len(self.g.path) else None) if after_index + 1 < len(self.g.path): forward_look_at = tuple(self.g.path[after_index + 1]) elif after is not None: forward_look_at = tuple( after[i] + 2 * (after[i] - lower[i]) for i in range(3)) else: forward_look_at = None navigation = { 'position': lower, 'after': after, 'goal': tuple(self.g.path[-1]), 'phase': 'crossing' if is_open else 'checking', 'deadline': now + self.DOOR_AUTO_OPEN_GRACE, 'opened_by_bot': False, 'saw_open': is_open, 'close_door': False, 'previous_look_at': self.g.get('look_at'), 'forward_look_at': forward_look_at, } self.door_navigation = navigation if not is_open: self.g.look_at = lower return True return False def animate(self): """Ask the 26.3 server to broadcast a main-hand punch animation.""" self._send_action(PunchPacket()) def break_block(self, location): location = tuple(int(value) for value in location) block_id = self.g.chunks.get_block_at(*location) if block_id is None or block_id == blocks.AIR: return False holding = self.g.get('holding', 0) held_slot = (45 if self.g.get('holding_hand', 0) == 1 else 36 + int(self.g.get('holding_slot', 0) or 0)) held_stack = self.g.get('inv', {}).get(held_slot) enchantments = (getattr(held_stack, 'components', {}) or {}).get( 'enchantments', {}) in_water = False on_ground = True position = self.g.get('pos') chunks = self.g.get('chunks') if position is not None and chunks is not None: x, y, z = (floor(position.x), floor(position.y), floor(position.z)) support = chunks.get_block_at(x, floor(position.y - 0.2), z) if support is not None: on_ground = support not in blocks.NON_SOLID_IDS feet = chunks.get_block_at(x, y, z) eyes = chunks.get_block_at(x, floor(position.y + 1.62), z) in_water = feet in blocks.WATER_IDS or eyes in blocks.WATER_IDS duration = utils.break_time( block_id, holding, in_water=in_water, on_ground=on_ground, enchantments=enchantments, effects=self.g.get('effects', {})) if not isfinite(duration): return False packet = PlayerActionPacket( action=0, location=location, face=1, sequence=self._next_sequence()) self._send_action(packet) self.g.breaking = location self.breaking_state = block_id now = time.monotonic() self.break_deadline = now + duration self.animate() self.next_swing_at = now + self.ARM_SWING_INTERVAL return True def respawn(self): self.g.respawn_requested_at = time.monotonic() self.g.connection.write_packet( ClientCommandPacket(action_id=0), force=True) self.g.connection.write_packet(ClientTickEnd(), force=True) def _send_drop_click(self, slot, button=1): state_id = self.g.inventory_state_ids.get(0, 0) self.g.connection.write_packet(ContainerClickDropPacket( container_id=0, state_id=state_id, slot=slot, button=button), force=True) self.g.connection.write_packet(ClientTickEnd(), force=True) return state_id def drop_inventory_slot(self, slot, whole_stack=True): """Drop one item or the full stack from a player inventory slot.""" try: slot = int(slot) except (TypeError, ValueError): return False if (not 5 <= slot <= 45 or self.g.get('window') is not None or self.g.get('draining') or self.g.get('dumping') is not None or self.g.get('drop_waiting')): return False item = self.g.inv.get(slot) if not item or not getattr(item, 'present', False): return False count = int(item.item_count or 0) if count <= 0: return False button = 1 if whole_stack else 0 state_id = self._send_drop_click(slot, button) self.g.drop_waiting = { 'slot': slot, 'item_id': item.item_id, 'expected_count': 0 if whole_stack else count - 1, 'sent_at': time.monotonic(), 'state_id': state_id, } return True def drop_stack(self): """Drop the full stack in the currently held hand (26.3 click).""" slot = (45 if self.g.get('holding_hand', 0) == 1 else 36 + int(self.g.get('holding_slot', 0) or 0)) return self.drop_inventory_slot(slot, whole_stack=True) def _tick_single_drop(self): waiting = self.g.get('drop_waiting') if not waiting: return item = self.g.inv.get(waiting['slot']) remaining = (item.item_count if item and getattr(item, 'present', False) and item.item_id == waiting['item_id'] else 0) if remaining <= waiting['expected_count']: print('[drop] server confirmed slot {} updated'.format( waiting['slot']), flush=True) self.g.drop_waiting = None elif time.monotonic() - waiting['sent_at'] > 3.0: print('[drop] timed out waiting for inventory update on slot {}'.format( waiting['slot']), flush=True) self.g.drop_waiting = None def start_dump(self, item_id): """Start dropping every stack of an item id from player inventory.""" if (self.g.get('window') is not None or self.g.get('draining') or self.g.get('dumping') is not None or self.g.get('drop_waiting')): return None slots = [slot for slot, item in self.g.inv.items() if 5 <= slot <= 45 and getattr(item, 'present', False) and item.item_id == item_id] if not slots: return 0 self.g.dumping = item_id self.g.dump_waiting = None self.g.dump_dropped = 0 self.g.dump_total = len(slots) self.g.dump_error = None return len(slots) def _tick_dump(self): item_id = self.g.get('dumping') if item_id is None: return waiting = self.g.get('dump_waiting') if waiting: item = self.g.inv.get(waiting['slot']) if (not item or not getattr(item, 'present', False) or item.item_id != item_id): self.g.dump_dropped += 1 print('[dump] dropped stack from slot {}; {}/{} complete'.format( waiting['slot'], self.g.dump_dropped, self.g.dump_total), flush=True) self.g.dump_waiting = None elif time.monotonic() - waiting['sent_at'] > 3.0: self.g.dump_error = 'server did not clear inventory slot {}'.format( waiting['slot']) print('[dump] stopped: ' + self.g.dump_error, flush=True) self.g.dumping = None self.g.dump_waiting = None return else: return for slot in range(5, 46): item = self.g.inv.get(slot) if item and getattr(item, 'present', False) and item.item_id == item_id: state_id = self._send_drop_click(slot) self.g.dump_waiting = { 'slot': slot, 'item_count': item.item_count, 'sent_at': time.monotonic(), 'state_id': state_id, } return print('[dump] complete: dropped {} stacks of item {}'.format( self.g.dump_dropped, item_id), flush=True) self.g.dumping = None self.g.dump_waiting = None def leave_bed(self): self._send_action(PlayerCommandPacket( entity_id=self.g.eid, action_id=0, jump_boost=0)) def use_item(self, hand=0): self._send_action(UseItemPacket( hand=hand, sequence=self._next_sequence(), yaw=float(self.g.get('yaw', 0.0)), pitch=float(self.g.get('pitch', 0.0)))) def start_drain(self): if (self.g.get('window') is not None or self.g.get('dumping') is not None or self.g.get('drop_waiting')): return None slots = [slot for slot, item in sorted(self.g.inv.items()) if 9 <= slot <= 45 and getattr(item, 'present', False)] self.g.drain_slots = slots self.g.drain_waiting = None self.g.drain_dropped = 0 self.g.drain_total = len(slots) self.g.drain_error = None self.g.draining = bool(slots) return len(slots) def _tick_drain(self): waiting = self.g.get('drain_waiting') if waiting: slot = waiting['slot'] state_id = self.g.inventory_state_ids.get(0, 0) if state_id != waiting['state_id']: item = self.g.inv.get(slot) if not item or not getattr(item, 'present', False): self.g.drain_dropped += 1 print('[drain] dropped stack from slot {}; {}/{} complete'.format( slot, self.g.drain_dropped, self.g.drain_total), flush=True) self.g.drain_waiting = None else: self.g.drain_error = 'server did not clear inventory slot {}'.format(slot) self.g.draining = False self.g.drain_waiting = None print('[drain] stopped: ' + self.g.drain_error, flush=True) return elif time.monotonic() - waiting['sent_at'] > 3.0: self.g.drain_error = 'timed out waiting for inventory update on slot {}'.format(slot) self.g.draining = False self.g.drain_waiting = None print('[drain] stopped: ' + self.g.drain_error, flush=True) return else: return while self.g.drain_slots: slot = self.g.drain_slots.pop(0) item = self.g.inv.get(slot) if not item or not getattr(item, 'present', False): continue state_id = self.g.inventory_state_ids.get(0, 0) packet = ContainerClickDropPacket( container_id=0, state_id=state_id, slot=slot) self.g.connection.write_packet(packet, force=True) self.g.connection.write_packet(ClientTickEnd(), force=True) self.g.drain_waiting = { 'slot': slot, 'state_id': state_id, 'item_count': item.item_count, 'sent_at': time.monotonic(), } return self.g.draining = False self.g.drain_waiting = None print('[drain] complete: dropped {} stacks'.format( self.g.drain_dropped), flush=True) def tick(self): self._tick_single_drop() if self.g.get('dumping') is not None: self._tick_dump() if self.g.get('draining'): self._tick_drain() if not self.g.get('breaking') or self.break_deadline is None: return now = time.monotonic() if now < self.break_deadline: if now >= self.next_swing_at: self.animate() self.next_swing_at = now + self.ARM_SWING_INTERVAL return packet = PlayerActionPacket( action=3, location=self.g.breaking, face=1, sequence=self._next_sequence()) self._send_action(packet) location = self.g.breaking if self.g.chunks.get_block_at(*location) == self.breaking_state: # Predict the normal client-side block removal. The server's later # block update is authoritative and will correct the cache if the # action was denied or the block changed differently. self.g.chunks.set_block_at(*location, blocks.AIR) self.g.breaking = None self.break_deadline = None self.breaking_state = None self.next_swing_at = 0.0 def cancel_break(self): if not self.g.get('breaking'): return packet = PlayerActionPacket( action=2, location=self.g.breaking, face=1, sequence=self._next_sequence()) self._send_action(packet) self.g.breaking = None self.break_deadline = None self.breaking_state = None self.next_swing_at = 0.0 def select_item(self, item_ids): item_ids = set(item_ids) matches = [(slot, item) for slot, item in self.g.inv.items() if item.present and item.item_id in item_ids] if not matches: return False # Prefer an already-hotbar stack; otherwise swap the requested stack # into an empty hotbar slot using the modern container SWAP input. slot, item = next( ((slot, item) for slot, item in matches if 36 <= slot <= 44), matches[0]) if slot == 45: self.g.holding = item.item_id self.g.holding_hand = 1 self.g.connection.write_packet(ClientTickEnd(), force=True) return True self.g.holding_hand = 0 if 36 <= slot <= 44: hotbar_slot = slot - 36 elif 9 <= slot <= 35: empty_hotbar = next( (candidate - 36 for candidate in range(36, 45) if candidate not in self.g.inv or not self.g.inv[candidate].present), None) hotbar_slot = (empty_hotbar if empty_hotbar is not None else self.g.get('holding_slot', 0) or 0) self.g.connection.write_packet(ContainerClickSwapPacket( container_id=0, state_id=self.g.inventory_state_ids.get(0, 0), slot=slot, button=hotbar_slot), force=True) else: return False if self.g.get('holding_slot') != hotbar_slot: self.g.connection.write_packet(SetCarriedItemPacket( slot=hotbar_slot), force=True) self.g.holding_slot = hotbar_slot self.g.holding = item.item_id self.g.connection.write_packet(ClientTickEnd(), force=True) return True def place_block(self, location, face, hand=0, cursor_y=None, sneaking=False, cursor=None): location = tuple(int(value) for value in location) if cursor is None: if cursor_y is None: cursor_y = 0.99 if int(face) == 1 else 0.5 cursor = (0.5, cursor_y, 0.5) packet = UseItemOnBlockPacket( hand=hand, location=location, face=int(face), cursor_x=float(cursor[0]), cursor_y=float(cursor[1]), cursor_z=float(cursor[2]), inside_block=False, world_border_hit=False, sequence=self._next_sequence()) if sneaking: self.g.connection.write_packet(PlayerInputPacket(flags=0x20), force=True) self.g.connection.write_packet(ClientTickEnd(), force=True) try: self._send_action(packet) finally: self.g.connection.write_packet(PlayerInputPacket(flags=0), force=True) self.g.connection.write_packet(ClientTickEnd(), force=True) else: self._send_action(packet) def count_window_slots(self): window = self.g.get('window') if not window: return 0 return sum( item.present for slot, item in window.contents.items() if slot < window.container_slots) def click_window(self, slot, button=0, mode=1, item=None): window = self.g.get('window') if not window: return False packet = ContainerClickQuickMovePacket( container_id=window.window_id, state_id=self.g.inventory_state_ids.get( window.window_id, window.state_id), slot=slot, ) self.g.connection.write_packet(packet, force=True) self.g.connection.write_packet(ClientTickEnd(), force=True) return True def close_window(self): window = self.g.get('window') if not window: return None self.g.connection.write_packet(ContainerClosePacket( container_id=window.window_id), force=True) self.g.window = None self.g.pending_chest_open = None