Implement item cachine for 26.3

This commit is contained in:
Tanner
2026-11-04 18:54:04 +00:00
parent 459e288407
commit 9fd565c3c9
10 changed files with 228 additions and 37 deletions
+6
View File
@@ -395,6 +395,8 @@ def start_automation(global_state, initialize=True):
g.players = g.get('players') or {}
g.breaking = None
g.break_time = 0.0
g.item_lock = False
g.minimum_cache_slots = g.get('minimum_cache_slots', 27)
g.draining = False
g.drain_slots = []
g.drain_waiting = None
@@ -404,6 +406,7 @@ def start_automation(global_state, initialize=True):
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)
g.holding_slot = g.get('holding_slot', 0)
g.chunks.loading = False
# Readiness below is based on nearby chunks, not unrelated view holes.
g.movement_paused = False
@@ -423,6 +426,9 @@ def start_automation(global_state, initialize=True):
g.setdefault('drain_dropped', 0)
g.setdefault('drain_total', 0)
g.setdefault('drain_error', None)
g.setdefault('item_lock', False)
g.setdefault('minimum_cache_slots', 27)
g.setdefault('holding_slot', 0)
old_stop = g.get('movement_stop_event')
old_worker = g.get('movement_thread')
+1 -1
View File
@@ -66,7 +66,7 @@ class Commands:
command = 'nosquarebrackets'
unsupported = {
'gather', 'loiter', 'trade', 'gapple', 'cache', 'fill',
'gather', 'loiter', 'trade', 'gapple', 'fill',
'drop', 'dump', 'break', 'open', 'click', 'use',
'interact', 'respawn',
}
+50 -2
View File
@@ -48,6 +48,27 @@ class ContainerClickDropPacket(Packet):
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
@@ -795,6 +816,33 @@ class Game(LegacyGame):
sequence=self._next_sequence())
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):
# Container open/close packet migration is not part of the crop path.
return None
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
+12 -1
View File
@@ -269,16 +269,27 @@ class JobStates(LegacyJobStates):
def __init__(self, global_state):
self.g = global_state
self.gather_crop_states = gather_crop.GatherCropStates(global_state)
self.cache_items_states = cache_items.CacheItemsStates(global_state)
self.state = self.idle
def farm_crop(self):
return [self.gather_crop_states]
self.cache_items_states.silent = True
items.set_wanted({
items.CARROT_ID,
items.POTATO_ID,
items.WHEAT_SEEDS_ID,
items.BEETROOT_SEEDS_ID,
})
return [self.gather_crop_states, self.cache_items_states]
def stop(self):
if self.g.get('breaking'):
self.g.game.cancel_break()
if self.g.get('window'):
self.g.game.close_window()
self.g.path = []
self.g.look_at = None
self.gather_crop_states.state = self.gather_crop_states.idle
self.cache_items_states.state = self.cache_items_states.idle
self.gather_crop_states.active_section_anchor = None
self.state = self.idle
+46 -20
View File
@@ -13,7 +13,6 @@ from mosfet import utils
from mosfet import path
from mosfet.info import blocks
from mosfet.info import items
from mosfet.info import mcdata
from mosfet.info import mobs
class CacheItemsStates:
@@ -25,8 +24,12 @@ class CacheItemsStates:
self.skip_items = []
num_stacks = self.g.game.count_inventory_slots()
minimum = getattr(
self, 'minimum', self.g.get('minimum_cache_slots', 27))
print('Inventory amount:', num_stacks)
if num_stacks >= self.g.minimum_cache_slots:
if num_stacks == 0:
self.state = self.cleanup
elif num_stacks >= minimum:
self.state = self.find_trapped_chests
else:
print('Aborting caching, not full')
@@ -53,23 +56,30 @@ class CacheItemsStates:
return
chest = self.trapped_chests[0]
self.area = chest
navpath = w.path_to_place_faked(p, chest)
print('navpath:', navpath)
for opening in w.find_cache_openings(chest):
navpath = w.path_to_place(p, opening)
if navpath:
print('navpath to trapped chest:', navpath)
self.opening = opening
self.g.path = navpath
self.state = self.going_to_trapped_chest
return
if navpath:
self.g.path = navpath[:-1]
self.opening = self.g.path[-1]
self.area = chest
self.state = self.going_to_trapped_chest
return
else:
self.trapped_chests.pop(0)
time.sleep(0.1)
self.trapped_chests.pop(0)
time.sleep(0.1)
def going_to_trapped_chest(self):
if utils.pint(self.g.pos) == self.opening:
self.g.look_at = self.area
self.wait_time = 0.5
self.state = self.turn_to_trapped_chest
def turn_to_trapped_chest(self):
self.g.look_at = self.area
self.wait_time -= utils.TICK
if self.wait_time <= 0:
self.state = self.open_chest
def select_chest(self):
@@ -84,7 +94,7 @@ class CacheItemsStates:
w = self.g.world
p = utils.pint(self.g.pos)
for area in w.find_cache_areas(p, 100):
for area in w.find_cache_areas(p, 16):
print('Found area:', area)
if area not in self.bad_areas:
break
@@ -112,10 +122,18 @@ class CacheItemsStates:
def going_to_area(self):
if utils.pint(self.g.pos) == self.opening:
self.g.look_at = self.area
self.wait_time = 0.5
self.state = self.turn_to_cache_spot
def turn_to_cache_spot(self):
self.g.look_at = self.area
self.wait_time -= utils.TICK
if self.wait_time <= 0:
self.state = self.place_chest
def place_chest(self):
self.g.game.place_block(self.area, BlockFace.TOP)
support = utils.padd(self.area, path.BLOCK_BELOW)
self.g.game.place_block(support, BlockFace.TOP)
self.state = self.open_chest
def open_chest(self):
@@ -139,14 +157,19 @@ class CacheItemsStates:
print('Didnt get a window, aborting')
self.state = self.cleanup
return
if w.data.window_type != mcdata.SINGLE_CHEST:
print('Got wrong window, aborting')
if not w.container_slots:
print('Chest contents have not arrived, aborting')
self.g.game.close_window()
self.state = self.cleanup
return
if w.container_slots not in (27, 54):
print('Got non-chest window, aborting')
self.g.game.close_window()
self.state = self.cleanup
return
w_info = mcdata.WINDOWS[w.data.window_type]
w_inventory_slots = w_info.inventory
w_container_slots = w_info.container
w_container_slots = range(w.container_slots)
w_inventory_slots = range(w.container_slots, len(w.contents))
used_slots = self.g.game.count_window_slots()
print('used:', used_slots, 'total:', len(w_container_slots))
@@ -204,6 +227,9 @@ class CacheItemsStates:
def cleanup(self):
self.g.look_at = None
self.state = self.done
if (self.g.get('job') and
getattr(self.g.job.state, '__name__', None) == 'cache_items'):
self.g.job.state = self.g.job.idle
def done(self):
# never gets ran, placeholder
+31 -3
View File
@@ -4,7 +4,8 @@ from munch import Munch
from minecraft.networking.packets import clientbound, serverbound
from minecraft.networking.modern import (
AddEntityPacket, ContainerSetContentPacket, ContainerSetSlotPacket,
AddEntityPacket, ContainerOpenPacket, ContainerSetContentPacket,
ContainerSetSlotPacket,
PaletteSection,
EntityPositionSyncPacket, MoveEntityPositionPacket,
MoveEntityPositionRotationPacket, MoveEntityRotationPacket,
@@ -433,10 +434,13 @@ class InventoryManager:
def __init__(self, global_state):
self.g = global_state
self.g.inv = self.g.get('inv') or {}
self.g.window = self.g.get('window')
self.g.item_lock = self.g.get('item_lock', False)
self.g.inventory_state_ids = self.g.get('inventory_state_ids') or {}
self.g.inventory_partial = False
self.g.cursor = Slot(present=False)
register = self.g.connection.register_packet_listener
register(self.handle_open, ContainerOpenPacket)
register(self.handle_content, ContainerSetContentPacket)
register(self.handle_slot, ContainerSetSlotPacket)
register(self.handle_cursor, SetCursorItemPacket)
@@ -449,6 +453,16 @@ class InventoryManager:
removed_components=item.removed_components,
components_unknown=item.components_unknown)
def handle_open(self, packet):
self.g.window = Munch(
window_id=packet.window_id,
inventory_type=packet.inventory_type,
title=packet.title,
contents={},
container_slots=0,
state_id=self.g.inventory_state_ids.get(packet.window_id, 0),
)
def handle_content(self, packet):
slots = {index: self.to_slot(item)
for index, item in enumerate(packet.items)}
@@ -457,9 +471,14 @@ class InventoryManager:
self.g.inv = slots
self.g.inventory_partial = packet.partial
self.g.cursor = self.to_slot(packet.carried_item)
elif self.g.get('window'):
elif (self.g.get('window') and
packet.container_id == self.g.window.window_id):
self.g.window.contents = slots
# Player inventory contributes 36 slots to chest menus.
self.g.window.container_slots = max(0, len(slots) - 36)
self.g.window.state_id = packet.state_id
self.g.cursor = self.to_slot(packet.carried_item)
self.g.item_lock = False
def handle_slot(self, packet):
self.g.inventory_state_ids[packet.container_id] = packet.state_id
@@ -468,10 +487,19 @@ class InventoryManager:
self.g.inventory_partial = True
if packet.container_id == 0:
self.g.inv[packet.slot] = item
self.g.item_lock = False
elif packet.container_id == -1 and packet.slot == -1:
self.g.cursor = item
elif self.g.get('window'):
elif (self.g.get('window') and
packet.container_id == self.g.window.window_id):
self.g.window.contents[packet.slot] = item
self.g.window.state_id = packet.state_id
offset = packet.slot - self.g.window.container_slots
if 0 <= offset < 27:
self.g.inv[9 + offset] = item
elif 27 <= offset < 36:
self.g.inv[36 + offset - 27] = item
self.g.item_lock = False
def handle_cursor(self, packet):
if packet.item.components_unknown:
+19 -1
View File
@@ -182,7 +182,25 @@ class World:
yield a
def find_cache_areas(self, center, distance):
return self.find_bed_areas(center, distance)
"""Yield nearby, supported air blocks suitable for placing a chest."""
center = tuple(center)
for offset in utils.search_2d(distance):
if utils.hypot(offset[0], offset[2]) > distance:
continue
for dy in (0, 1, -1, 2, -2):
area = (center[0] + offset[0], center[1] + dy,
center[2] + offset[2])
if area == center:
continue
if self.block_at(*area) != blocks.AIR:
continue
if self.block_at(*utils.padd(area, path.BLOCK_ABOVE)) != blocks.AIR:
continue
support = self.block_at(*utils.padd(area, path.BLOCK_BELOW))
if (support is None or support in blocks.NON_SOLID_IDS or
support in blocks.AVOID_IDS):
continue
yield area
def sand_adjacent_safe(self, sand):
for direction in path.CHECK_DIRECTIONS: