From 688297e968e1a542ccd7d1ac6f6064084706b0b3 Mon Sep 17 00:00:00 2001 From: Tanner Date: Sat, 7 Nov 2026 18:23:55 +0000 Subject: [PATCH] Improve labeled chest caching interactions --- mosfet/game.py | 40 +++++++++++++----- mosfet/jobs/cache_items.py | 39 ++++++++++++++++- mosfet/protocol/managers.py | 25 ++++++++--- mosfet/protocol/modern.py | 11 ++++- tests/test_cache_items.py | 79 +++++++++++++++++++++++++++++++++++ tests/test_chest_detection.py | 31 +++++++++++--- tests/test_sneak_placement.py | 40 ++++++++++++++++++ 7 files changed, 240 insertions(+), 25 deletions(-) create mode 100644 tests/test_cache_items.py create mode 100644 tests/test_sneak_placement.py diff --git a/mosfet/game.py b/mosfet/game.py index 2b5444b..fca3d1a 100644 --- a/mosfet/game.py +++ b/mosfet/game.py @@ -26,7 +26,8 @@ from mosfet.protocol.packets import ( from mosfet.protocol.types import Slot from mosfet.protocol.modern import ( - ClientCommandPacket, PlayerCommandPacket, UpdateTimePacket, UseItemPacket, + ClientCommandPacket, PlayerCommandPacket, PlayerInputPacket, + UpdateTimePacket, UseItemPacket, ) from mosfet import utils @@ -386,14 +387,18 @@ class LegacyGame: def open_container(self, location): location = tuple(int(value) for value in location) - bid = self.g.chunks.get_block_at(*location) - block_name = blocks.BLOCKS.get(bid) - if (block_name and block_name != 'ender_chest' and - (block_name == 'chest' or block_name.endswith('_chest'))): - self.g.pending_chest_open = { - 'position': location, - 'requested_at': time.monotonic(), - } + 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): @@ -1042,7 +1047,8 @@ class Game(LegacyGame): self.g.connection.write_packet(ClientTickEnd(), force=True) return True - def place_block(self, location, face, hand=0, cursor_y=None): + 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 @@ -1051,7 +1057,18 @@ class Game(LegacyGame): cursor_x=0.5, cursor_y=cursor_y, cursor_z=0.5, inside_block=False, world_border_hit=False, sequence=self._next_sequence()) - self._send_action(packet) + 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') @@ -1083,3 +1100,4 @@ class Game(LegacyGame): self.g.connection.write_packet(ContainerClosePacket( container_id=window.window_id), force=True) self.g.window = None + self.g.pending_chest_open = None diff --git a/mosfet/jobs/cache_items.py b/mosfet/jobs/cache_items.py index d7bb3b7..f4faafb 100644 --- a/mosfet/jobs/cache_items.py +++ b/mosfet/jobs/cache_items.py @@ -22,6 +22,9 @@ class CacheItemsStates: def init(self): self.skip_slots = [] self.skip_items = [] + self.failed_transfer_slots = set() + self.pending_transfer = None + self.transferred_to_labeled_chest = False self.is_labeled_container = False self.container_item_ids = None @@ -80,6 +83,10 @@ class CacheItemsStates: if not len(self.trapped_chests): self.is_labeled_container = False self.container_item_ids = None + if self.transferred_to_labeled_chest: + print('Already cached items in labeled chests; skipping new chest') + self.state = self.cleanup + return print('No labeled or trapped chests') self.state = self.select_chest return @@ -166,11 +173,13 @@ class CacheItemsStates: def place_chest(self): support = utils.padd(self.area, path.BLOCK_BELOW) - self.g.game.place_block(support, BlockFace.TOP) + self.g.game.place_block(support, BlockFace.TOP, sneaking=True) self.state = self.open_chest def open_chest(self): print('Opening chest') + self.failed_transfer_slots.clear() + self.pending_transfer = None self.g.game.open_container(self.area) self.wait_time = 1 self.state = self.wait @@ -236,6 +245,21 @@ class CacheItemsStates: self.state = self.cleanup return + if self.pending_transfer: + pending = self.pending_transfer + if time.monotonic() < pending['confirm_after']: + return + source = w.contents.get(pending['window_slot']) + moved = (source is None or not source.present or + source.item_id != pending['item_id'] or + source.item_count < pending['item_count']) + if moved: + if pending['is_labeled']: + self.transferred_to_labeled_chest = True + else: + self.failed_transfer_slots.add(pending['window_slot']) + self.pending_transfer = None + w_container_slots = range(w.container_slots) w_inventory_slots = range(w.container_slots, len(w.contents)) @@ -272,7 +296,8 @@ class CacheItemsStates: elif slot.item_id in items.NEEDED_ITEMS: continue - if slot_num in self.skip_slots: + if (slot_num in self.skip_slots or + slot_num in self.failed_transfer_slots): continue slot_list.append((slot_num, slot)) @@ -288,6 +313,13 @@ class CacheItemsStates: print('moving', slot) + self.pending_transfer = { + 'window_slot': slot_num, + 'item_id': slot.item_id, + 'item_count': slot.item_count, + 'is_labeled': self.is_labeled_container, + 'confirm_after': time.monotonic() + 0.15, + } self.g.item_lock = True self.g.game.click_window(slot_num, 0, 1, slot) return @@ -325,6 +357,9 @@ class CacheItemsStates: self.silent = False self.skip_slots = [] + self.failed_transfer_slots = set() + self.pending_transfer = None + self.transferred_to_labeled_chest = False self.skip_items = [] self.labeled_chests = [] diff --git a/mosfet/protocol/managers.py b/mosfet/protocol/managers.py index 32c74e5..d01c40e 100644 --- a/mosfet/protocol/managers.py +++ b/mosfet/protocol/managers.py @@ -365,6 +365,7 @@ class ChestOpenManager: self.g.chest_viewers = self.g.get('chest_viewers') or {} self.g.chest_open_events = self.g.get('chest_open_events') or [] self.g.last_chest_opened_by_other = None + self._own_open_suppressions = {} self.g.connection.register_packet_listener( self.handle_block_event, BlockEventPacket) @@ -396,17 +397,31 @@ class ChestOpenManager: else: self.g.chest_viewers.pop(position, None) + now = time.monotonic() + self._own_open_suppressions = { + pos: expires for pos, expires in self._own_open_suppressions.items() + if expires > now + } pending = self.g.get('pending_chest_open') - pending_age = (time.monotonic() - pending.get('requested_at', 0) + pending_age = (now - pending.get('requested_at', 0) if pending is not None else None) pending_is_fresh = (pending_age is not None and 0 <= pending_age <= self.OWN_OPEN_TIMEOUT) - is_bot_opening = ( - pending_is_fresh and - tuple(pending.get('position', ())) == position + pending_positions = ( + pending.get('positions') or (pending.get('position', ()),) + if pending is not None else () ) - if pending is not None and (is_bot_opening or not pending_is_fresh): + pending_positions = {tuple(pos) for pos in pending_positions} + is_bot_opening = ( + pending_is_fresh and position in pending_positions + ) + if pending is not None and not pending_is_fresh: self.g.pending_chest_open = None + if is_bot_opening: + for own_position in pending_positions: + self._own_open_suppressions[own_position] = now + 0.5 + is_bot_opening = (is_bot_opening or + position in self._own_open_suppressions) if (viewers <= previous_viewers or (is_bot_opening and diff --git a/mosfet/protocol/modern.py b/mosfet/protocol/modern.py index 6aaaef0..602f2e4 100644 --- a/mosfet/protocol/modern.py +++ b/mosfet/protocol/modern.py @@ -4,7 +4,9 @@ import time from contextlib import nullcontext from minecraft.networking.packets import Packet -from minecraft.networking.types import Float, Long, VarInt, VarLong +from minecraft.networking.types import ( + Float, Long, UnsignedByte, VarInt, VarLong, +) from minecraft.networking.modern import ( ChatCommand, ClientboundPlayerChatPacket, ServerboundChatPacket, SystemChat, ) @@ -29,6 +31,13 @@ class UpdateTimePacket(Packet): }) +class PlayerInputPacket(Packet): + """Serverbound key state, including sneak for container-safe placement.""" + id = 0x2B + packet_name = 'player input' + definition = [{'flags': UnsignedByte}] + + class PlayerCommandPacket(Packet): """Client player-command packet; action 0 stops sleeping (protocol 777).""" id = 0x2A diff --git a/tests/test_cache_items.py b/tests/test_cache_items.py new file mode 100644 index 0000000..873d598 --- /dev/null +++ b/tests/test_cache_items.py @@ -0,0 +1,79 @@ +import unittest +from munch import Munch + +from minecraft.networking.types import BlockFace + +from mosfet.info import items +from mosfet.jobs.cache_items import CacheItemsStates + + +class CacheItemsTests(unittest.TestCase): + def test_successful_labeled_transfer_skips_placing_another_cache_chest(self): + item_id = items.get_id('stone') + stack = Munch(present=True, item_id=item_id, item_count=16) + window = Munch(container_slots=27, contents={ + **dict.fromkeys(range(27)), 27: stack, + }) + clicks = [] + + class FakeGame: + @staticmethod + def count_window_slots(): + return 0 + + @staticmethod + def click_window(*args): + clicks.append(args) + + class FakeWorld: + @staticmethod + def find_blocks_indexed(*_args): + return [] + + @staticmethod + def find_labeled_chests(*_args): + return [] + + g = Munch( + game=FakeGame(), world=FakeWorld(), pos=(0, 64, 0), + inv={}, window=window, item_lock=False) + state = CacheItemsStates(g) + state.is_labeled_container = True + state.container_item_ids = {item_id} + + state.move_items() + self.assertEqual(len(clicks), 1) + self.assertFalse(state.transferred_to_labeled_chest) + + window.contents[27] = Munch(present=False, item_id=0, item_count=0) + g.item_lock = False + state.pending_transfer['confirm_after'] = 0 + state.move_items() + self.assertTrue(state.transferred_to_labeled_chest) + + state.labeled_chests = [] + state.trapped_chests = [] + state.choose_trapped_chest() + self.assertIs(state.state.__func__, state.cleanup.__func__) + + def test_cache_chest_placement_sneaks_to_avoid_opening_support_container(self): + calls = [] + + class FakeGame: + @staticmethod + def place_block(*args, **kwargs): + calls.append((args, kwargs)) + + state = CacheItemsStates(Munch(game=FakeGame())) + state.area = (3, 65, 7) + + state.place_chest() + + self.assertEqual(calls, [( + ((3, 64, 7), BlockFace.TOP), {'sneaking': True}, + )]) + self.assertIs(state.state.__func__, state.open_chest.__func__) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_chest_detection.py b/tests/test_chest_detection.py index 7c9d569..863165f 100644 --- a/tests/test_chest_detection.py +++ b/tests/test_chest_detection.py @@ -27,8 +27,8 @@ class ChestDetectionTests(unittest.TestCase): self.manager = ChestOpenManager(self.g) self.position = (12, 64, -7) - def event(self, block_id=2, action=1, viewers=1): - x, y, z = self.position + def event(self, block_id=2, action=1, viewers=1, position=None): + x, y, z = position or self.position return Munch( location=Munch(x=x, y=y, z=z), block_id=block_id, action=action, parameter=viewers, @@ -54,12 +54,27 @@ class ChestDetectionTests(unittest.TestCase): self.manager.handle_block_event(self.event(viewers=1)) self.assertIsNone(self.g.last_chest_opened_by_other) - self.assertIsNone(self.g.pending_chest_open) + self.assertIsNotNone(self.g.pending_chest_open) self.manager.handle_block_event(self.event(viewers=2)) self.assertEqual(self.g.last_chest_opened_by_other['viewers'], 2) self.assertEqual(self.g.last_chest_opened_by_other['previous_viewers'], 1) + def test_bot_open_of_either_double_chest_half_is_not_misreported(self): + other_half = (13, 64, -7) + self.g.pending_chest_open = { + 'position': self.position, + 'positions': (self.position, other_half), + 'requested_at': time.monotonic(), + } + + self.manager.handle_block_event( + self.event(position=other_half, viewers=1)) + + self.assertIsNone(self.g.last_chest_opened_by_other) + self.assertIsNotNone(self.g.pending_chest_open) + self.assertEqual(self.g.chest_viewers[other_half], 1) + def test_ignores_non_chests_and_tracks_close(self): self.manager.handle_block_event(self.event(block_id=1, viewers=1)) self.assertIsNone(self.g.last_chest_opened_by_other) @@ -71,11 +86,13 @@ class ChestDetectionTests(unittest.TestCase): self.assertEqual(len(self.g.chest_open_events), 1) def test_open_container_marks_bot_initiated_chest_open(self): - chest_state = 5552 sent = [] game = Game.__new__(Game) - game.g = Munch(chunks=Munch( - get_block_at=lambda *_position: chest_state)) + other_half = (12, 64, -8) + game.g = Munch( + chunks=Munch(get_block_at=lambda *_position: None), + world=Munch(_double_chest_positions=lambda _position: + (self.position,))) game.place_block = lambda location, face: sent.append( (location, face)) @@ -83,6 +100,8 @@ class ChestDetectionTests(unittest.TestCase): self.assertEqual(sent[0][0], self.position) self.assertEqual(game.g.pending_chest_open['position'], self.position) + self.assertIn(self.position, game.g.pending_chest_open['positions']) + self.assertIn(other_half, game.g.pending_chest_open['positions']) self.assertIsInstance(game.g.pending_chest_open['requested_at'], float) diff --git a/tests/test_sneak_placement.py b/tests/test_sneak_placement.py new file mode 100644 index 0000000..e3a5175 --- /dev/null +++ b/tests/test_sneak_placement.py @@ -0,0 +1,40 @@ +import unittest +from munch import Munch + +from minecraft.networking.modern import ClientTickEnd, UseItemOnBlockPacket +from mosfet.game import Game +from mosfet.protocol.modern import PlayerInputPacket + + +class FakeConnection: + def __init__(self): + self.packets = [] + + def write_packet(self, packet, force=False): + self.packets.append((packet, force)) + + +class SneakPlacementTests(unittest.TestCase): + def test_sneaking_wraps_block_placement_and_is_released(self): + connection = FakeConnection() + game = Game.__new__(Game) + game.g = Munch(connection=connection) + game.sequence = 0 + + game.place_block((3, 64, 7), 1, sneaking=True) + + packets = [packet for packet, _force in connection.packets] + self.assertEqual(len(packets), 6) + self.assertIsInstance(packets[0], PlayerInputPacket) + self.assertEqual(packets[0].flags, 0x20) + self.assertIsInstance(packets[1], ClientTickEnd) + self.assertIsInstance(packets[2], UseItemOnBlockPacket) + self.assertEqual(packets[2].location, (3, 64, 7)) + self.assertIsInstance(packets[3], ClientTickEnd) + self.assertIsInstance(packets[4], PlayerInputPacket) + self.assertEqual(packets[4].flags, 0) + self.assertIsInstance(packets[5], ClientTickEnd) + + +if __name__ == '__main__': + unittest.main()