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
+19 -1
View File
@@ -289,6 +289,11 @@ def init(global_state):
g.break_time = 0
g.dumping = None
g.dump_waiting = None
g.dump_dropped = 0
g.dump_total = 0
g.dump_error = None
g.drop_waiting = None
g.draining = False
g.item_lock = False
g.command_lock = False
@@ -413,6 +418,12 @@ def start_automation(global_state, initialize=True):
g.item_lock = False
g.minimum_cache_slots = g.get('minimum_cache_slots', 27)
g.draining = False
g.dumping = None
g.dump_waiting = None
g.dump_dropped = 0
g.dump_total = 0
g.dump_error = None
g.drop_waiting = None
g.drain_slots = []
g.drain_waiting = None
g.drain_dropped = 0
@@ -437,6 +448,12 @@ def start_automation(global_state, initialize=True):
# Preserve active work during hot reloads, but give the worker all fields
# introduced by the currently migrated inventory controls.
g.setdefault('draining', False)
g.setdefault('dumping', None)
g.setdefault('dump_waiting', None)
g.setdefault('dump_dropped', 0)
g.setdefault('dump_total', 0)
g.setdefault('dump_error', None)
g.setdefault('drop_waiting', None)
g.setdefault('drain_slots', [])
g.setdefault('drain_waiting', None)
g.setdefault('drain_dropped', 0)
@@ -485,7 +502,8 @@ def start_automation(global_state, initialize=True):
movement_ready = initial_world_ready
job_active = (g.job is not None and g.job.state != g.job.idle)
has_work = (bool(g.path) or job_active or bool(g.breaking) or
bool(g.draining))
bool(g.draining) or g.get('dumping') is not None or
bool(g.get('drop_waiting')))
if g.get('movement_paused'):
set_status('paused')
elif has_work and not movement_ready:
+7 -9
View File
@@ -67,8 +67,7 @@ class Commands:
unsupported = {
'gather', 'trade', 'gapple', 'fill',
'drop', 'dump', 'break', 'open', 'click', 'use',
'interact',
'break', 'open', 'click', 'use', 'interact',
}
if (command in unsupported or
(command == 'farm' and not data.lower().startswith('crop'))):
@@ -471,7 +470,7 @@ class Commands:
## 1drop - drops the current stack its holding
if command == 'drop':
self.g.game.drop_stack()
reply = 'ok' if self.g.game.drop_stack() else 'empty or busy'
## 1select [id] - moves item with id into main hand
if command == 'select' and data:
@@ -484,16 +483,15 @@ class Commands:
## 1dump [id] - drops all items matching id
if command == 'dump' and data:
item = int(data)
if self.g.game.count_items([item]):
self.g.dumping = item
reply = 'ok'
else:
reply = 'not found'
count = self.g.game.start_dump(item)
reply = ('busy' if count is None else
'ok' if count else 'not found')
## 1drain - drops all items in inventory
if command == 'drain':
count = self.g.game.start_drain()
reply = 'ok' if count else 'empty'
reply = ('busy' if count is None else
'ok' if count else 'empty')
if command == 'gapple':
self.g.job.state = self.g.job.find_gapple
+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:
+12
View File
@@ -471,6 +471,7 @@ class InventoryManager:
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
@@ -488,6 +489,9 @@ class InventoryManager:
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
@@ -501,6 +505,14 @@ class InventoryManager:
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