Files
minecraft-bot/mosfet/game.py
T

1104 lines
41 KiB
Python

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):
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, BlockFace.TOP)
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
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
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 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):
location = tuple(int(value) for value in location)
if cursor_y is None:
cursor_y = 0.99 if int(face) == 1 else 0.5
packet = UseItemOnBlockPacket(
hand=hand, location=location, face=int(face),
cursor_x=0.5, cursor_y=cursor_y,
cursor_z=0.5, 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