Add inventory controls and enhance dashboard

This commit is contained in:
Tanner
2026-11-05 03:00:13 +00:00
parent efa2140a91
commit 659fa49ea1
13 changed files with 979 additions and 36 deletions
+133 -2
View File
@@ -8,7 +8,7 @@ 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,
SetCarriedItemPacket, UseItemOnBlockPacket,
SetCarriedItemPacket, SetHeldSlotPacket, UseItemOnBlockPacket,
)
from mosfet.protocol.packets import (
@@ -45,7 +45,7 @@ class ContainerClickDropPacket(Packet):
VarInt.send(self.container_id, packet_buffer)
VarInt.send(self.state_id, packet_buffer)
Short.send(self.slot, packet_buffer)
Byte.send(1, packet_buffer) # drop the whole stack
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
@@ -166,6 +166,16 @@ class LegacyGame:
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
@@ -665,6 +675,8 @@ class Game(LegacyGame):
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
@@ -717,6 +729,118 @@ class Game(LegacyGame):
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))
@@ -728,6 +852,10 @@ class Game(LegacyGame):
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
@@ -787,6 +915,9 @@ class Game(LegacyGame):
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: