Reimplement !drain command

This commit is contained in:
Tanner
2026-11-04 17:29:52 +00:00
parent 6dbc61284a
commit 23ecfa4446
5 changed files with 165 additions and 13 deletions
+19 -2
View File
@@ -116,7 +116,8 @@ def tick(global_state):
print('Chunks loaded in', round(time.time() - g.chunks.loading, 2), 's')
g.chunks.loading = False
g.chunk_wait_report_at = 0
status = 'following_path' if g.path else 'running_job'
status = ('draining_inventory' if g.get('draining') else
'following_path' if g.path else 'running_job')
if g.get('movement_status') != status:
print('[movement] status={}'.format(status), flush=True)
g.movement_status = status
@@ -394,6 +395,12 @@ def start_automation(global_state, initialize=True):
g.players = g.get('players') or {}
g.breaking = None
g.break_time = 0.0
g.draining = False
g.drain_slots = []
g.drain_waiting = None
g.drain_dropped = 0
g.drain_total = 0
g.drain_error = None
g.correction_count = g.get('correction_count', 0)
g.yaw = float(g.get('yaw', 0.0) or 0.0)
g.pitch = float(g.get('pitch', 0.0) or 0.0)
@@ -408,6 +415,15 @@ def start_automation(global_state, initialize=True):
g.game = Game(g)
g.job = JobStates(g)
# 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('drain_slots', [])
g.setdefault('drain_waiting', None)
g.setdefault('drain_dropped', 0)
g.setdefault('drain_total', 0)
g.setdefault('drain_error', None)
old_stop = g.get('movement_stop_event')
old_worker = g.get('movement_thread')
if old_stop is not None:
@@ -444,7 +460,8 @@ def start_automation(global_state, initialize=True):
# of gating itself forever on a momentary missing column.
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)
has_work = (bool(g.path) or job_active or bool(g.breaking) or
bool(g.draining))
if g.get('movement_paused'):
set_status('paused')
elif has_work and not movement_ready:
+3 -3
View File
@@ -57,7 +57,7 @@ class Commands:
unsupported = {
'gather', 'loiter', 'trade', 'gapple', 'cache', 'fill',
'drop', 'dump', 'drain', 'break', 'open', 'click', 'use',
'drop', 'dump', 'break', 'open', 'click', 'use',
'interact', 'respawn',
}
if (command in unsupported or
@@ -473,8 +473,8 @@ class Commands:
## 1drain - drops all items in inventory
if command == 'drain':
self.g.draining = True
reply = 'ok'
count = self.g.game.start_drain()
reply = 'ok' if count else 'empty'
if command == 'gapple':
self.g.job.state = self.g.job.find_gapple
+77 -1
View File
@@ -5,7 +5,7 @@ from itertools import count
from munch import Munch
from minecraft.networking.packets import Packet, clientbound, serverbound
from minecraft.networking.types import BlockFace
from minecraft.networking.types import BlockFace, Byte, Short, VarInt
from minecraft.networking.modern import (
ClientTickEnd, ContainerClickSwapPacket, PlayerActionPacket,
SetCarriedItemPacket, UseItemOnBlockPacket,
@@ -33,6 +33,21 @@ 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(1, packet_buffer) # drop the whole stack
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 LegacyGame:
def __init__(self, global_state):
self.g = global_state
@@ -644,7 +659,68 @@ class Game(LegacyGame):
self.break_deadline = time.monotonic() + max(0.05, hardness_time)
return True
def start_drain(self):
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):
if self.g.get('draining'):
self._tick_drain()
if not self.g.get('breaking') or self.break_deadline is None:
return
if time.monotonic() < self.break_deadline: