From 4c27f5058e53e918f424ab0d186e48cbfe16ccb7 Mon Sep 17 00:00:00 2001 From: Tanner Date: Thu, 5 Nov 2026 16:36:10 +0000 Subject: [PATCH] Port and tune 26.3 wood farming --- mosfet/bot.py | 2 + mosfet/commands.py | 4 +- mosfet/game.py | 31 +++++++++- mosfet/info/items.py | 9 +++ mosfet/job.py | 28 +++++++++ mosfet/jobs/clear_leaves.py | 17 +++++- mosfet/jobs/gather_wood.py | 12 +++- mosfet/jobs/grab_supplies.py | 10 +--- mosfet/jobs/plant_tree.py | 8 ++- mosfet/protocol/managers.py | 11 +++- mosfet/utils.py | 53 ++++++++++------- tests/test_break_time.py | 69 ++++++++++++++++++++++ tests/test_clear_leaves.py | 37 ++++++++++++ tests/test_gather_wood.py | 101 ++++++++++++++++++++++++++++++++- tests/test_sapling_entities.py | 60 ++++++++++++++++++++ 15 files changed, 412 insertions(+), 40 deletions(-) create mode 100644 tests/test_break_time.py create mode 100644 tests/test_clear_leaves.py create mode 100644 tests/test_sapling_entities.py diff --git a/mosfet/bot.py b/mosfet/bot.py index 71d5c61..e917e09 100644 --- a/mosfet/bot.py +++ b/mosfet/bot.py @@ -417,6 +417,7 @@ def start_automation(global_state, initialize=True): g.break_time = 0.0 g.item_lock = False g.minimum_cache_slots = g.get('minimum_cache_slots', 27) + g.maximum_supply_slots = g.get('maximum_supply_slots', 33) g.draining = False g.dumping = None g.dump_waiting = None @@ -461,6 +462,7 @@ def start_automation(global_state, initialize=True): g.setdefault('drain_error', None) g.setdefault('item_lock', False) g.setdefault('minimum_cache_slots', 27) + g.setdefault('maximum_supply_slots', 33) g.setdefault('holding_slot', 0) g.setdefault('holding_hand', 0) g.setdefault('time', 0.0) diff --git a/mosfet/commands.py b/mosfet/commands.py index e279125..116c345 100644 --- a/mosfet/commands.py +++ b/mosfet/commands.py @@ -71,7 +71,9 @@ class Commands: } if ((command == 'gather' and data.lower().strip() != 'wood') or command in unsupported or - (command == 'farm' and not data.lower().startswith('crop'))): + (command == 'farm' and + not data.lower().startswith('crop') and + data.lower().strip() != 'wood')): reply = 'not migrated to Minecraft 26.3 yet' print('Reply:', reply) self._send_reply(reply, private, reply_target) diff --git a/mosfet/game.py b/mosfet/game.py index c35d74d..29a29f4 100644 --- a/mosfet/game.py +++ b/mosfet/game.py @@ -2,6 +2,7 @@ import re import time import random from itertools import count +from math import floor, isfinite from munch import Munch from minecraft.networking.packets import Packet, clientbound, serverbound @@ -721,15 +722,41 @@ class Game(LegacyGame): block_id = self.g.chunks.get_block_at(*location) if block_id is None or block_id == blocks.AIR: return False + + holding = self.g.get('holding', 0) + held_slot = (45 if self.g.get('holding_hand', 0) == 1 else + 36 + int(self.g.get('holding_slot', 0) or 0)) + held_stack = self.g.get('inv', {}).get(held_slot) + enchantments = (getattr(held_stack, 'components', {}) or {}).get( + 'enchantments', {}) + in_water = False + on_ground = True + position = self.g.get('pos') + chunks = self.g.get('chunks') + if position is not None and chunks is not None: + x, y, z = (floor(position.x), floor(position.y), + floor(position.z)) + support = chunks.get_block_at(x, floor(position.y - 0.2), z) + if support is not None: + on_ground = support not in blocks.NON_SOLID_IDS + feet = chunks.get_block_at(x, y, z) + eyes = chunks.get_block_at(x, floor(position.y + 1.62), z) + in_water = feet in blocks.WATER_IDS or eyes in blocks.WATER_IDS + + duration = utils.break_time( + block_id, holding, in_water=in_water, on_ground=on_ground, + enchantments=enchantments, effects=self.g.get('effects', {})) + if not isfinite(duration): + return False + packet = PlayerActionPacket( action=0, location=location, face=1, sequence=self._next_sequence()) self._send_action(packet) self.g.breaking = location self.breaking_state = block_id - hardness_time = utils.break_time(block_id, self.g.get('holding', 0)) now = time.monotonic() - self.break_deadline = now + max(0.05, hardness_time) + self.break_deadline = now + duration self.animate() self.next_swing_at = now + self.ARM_SWING_INTERVAL return True diff --git a/mosfet/info/items.py b/mosfet/info/items.py index db58b6e..749ed2b 100644 --- a/mosfet/info/items.py +++ b/mosfet/info/items.py @@ -55,6 +55,15 @@ def get_id(name): return ITEMS['minecraft:' + name]['protocol_id'] +def get_sapling_id(tree_type): + """Return the plantable sapling/propagule item for a tree species.""" + for suffix in ('_sapling', '_propagule'): + item = ITEMS.get('minecraft:' + tree_type + suffix) + if item is not None: + return item['protocol_id'] + return None + + def get_name(idx): return ITEM_NAMES[idx] diff --git a/mosfet/job.py b/mosfet/job.py index ca24588..6c7b061 100644 --- a/mosfet/job.py +++ b/mosfet/job.py @@ -270,6 +270,11 @@ class JobStates(LegacyJobStates): self.g = global_state self.gather_wood_states = gather_wood.GatherWoodStates(global_state) self.gather_crop_states = gather_crop.GatherCropStates(global_state) + self.grab_supplies_states = grab_supplies.GrabSuppliesStates( + global_state) + self.clear_leaves_states = clear_leaves.ClearLeavesStates(global_state) + self.plant_tree_states = plant_tree.PlantTreeStates(global_state) + self.grab_sapling_states = grab_sapling.GrabSaplingStates(global_state) self.cache_items_states = cache_items.CacheItemsStates(global_state) self.sleep_with_bed_states = sleep_with_bed.SleepWithBedStates( global_state) @@ -278,6 +283,25 @@ class JobStates(LegacyJobStates): global_state) self.state = self.idle + def farm_wood(self): + self.sleep_with_bed_states.silent = True + self.cache_items_states.silent = True + self.grab_supplies_states.supplies = { + tuple(items.AXE_IDS): (1, 9), + } + items.set_needed(items.AXE_IDS) + items.set_wanted(items.SAPLING_IDS) + return [ + self.grab_supplies_states, + self.gather_wood_states, + self.clear_leaves_states, + self.plant_tree_states, + self.grab_sapling_states, + self.sleep_with_bed_states, + self.eat_food_states, + self.cache_items_states, + ] + def farm_crop(self): self.cache_items_states.silent = True self.sleep_with_bed_states.silent = True @@ -314,6 +338,10 @@ class JobStates(LegacyJobStates): self.g.game.leave_bed() self.gather_wood_states.state = self.gather_wood_states.idle self.gather_crop_states.state = self.gather_crop_states.idle + self.grab_supplies_states.state = self.grab_supplies_states.idle + self.clear_leaves_states.state = self.clear_leaves_states.idle + self.plant_tree_states.state = self.plant_tree_states.idle + self.grab_sapling_states.state = self.grab_sapling_states.idle self.cache_items_states.state = self.cache_items_states.idle self.sleep_with_bed_states.state = self.sleep_with_bed_states.idle self.sleep_with_bed_states.force_new_bed = False diff --git a/mosfet/jobs/clear_leaves.py b/mosfet/jobs/clear_leaves.py index 5505e4c..dd0f67e 100644 --- a/mosfet/jobs/clear_leaves.py +++ b/mosfet/jobs/clear_leaves.py @@ -17,6 +17,8 @@ from mosfet.info import mcdata from mosfet.info import mobs class ClearLeavesStates: + LEAF_BREAK_INTERVAL = 0.6 + def idle(self): return None @@ -26,9 +28,13 @@ class ClearLeavesStates: self.state = self.cleanup return - sapling_type = self.g.chopped_tree + '_sapling' - sapling_item = items.get_id(sapling_type) + sapling_item = items.get_sapling_id(self.g.chopped_tree) + if sapling_item is None: + print('No sapling for chopped tree, skipping leaf cleanup') + self.state = self.cleanup + return num_saplings = self.g.game.count_items([sapling_item]) + sapling_type = items.get_name(sapling_item) print('Have', num_saplings, sapling_type, 'in inventory') if num_saplings > 8: @@ -57,9 +63,13 @@ class ClearLeavesStates: def break_leaves(self): if not self.g.breaking: if self.leaves: + now = time.monotonic() + if now < self.next_break_at: + return leaf = self.leaves.pop(0) self.g.look_at = leaf - self.g.game.break_block(leaf) + if self.g.game.break_block(leaf): + self.next_break_at = now + self.LEAF_BREAK_INTERVAL print('Breaking leaf', leaf) else: self.wait_time = 1 @@ -86,6 +96,7 @@ class ClearLeavesStates: self.leaves = [] self.wait_time = 0 + self.next_break_at = 0.0 def run(self): self.state() diff --git a/mosfet/jobs/gather_wood.py b/mosfet/jobs/gather_wood.py index 2f29d50..48142a2 100644 --- a/mosfet/jobs/gather_wood.py +++ b/mosfet/jobs/gather_wood.py @@ -32,15 +32,21 @@ class GatherWoodStates: for tree in w.find_trees(p, 100): print('Found tree:', tree) - if tree not in self.bad_trees: - break + if tree in self.bad_trees: + continue + tree_type = blocks.BLOCKS[w.block_at(*tree)].replace('_log', '') + if items.get_sapling_id(tree_type) is None: + print('No plantable sapling for', tree_type, '- skipping') + self.bad_trees.append(tree) + continue + break else: # for print('No good trees left, aborting.') self.state = self.cleanup return self.tree = tree - self.type = blocks.BLOCKS[w.block_at(*tree)].replace('_log', '') + self.type = tree_type print('Type:', self.type) self.state = self.find_openings diff --git a/mosfet/jobs/grab_supplies.py b/mosfet/jobs/grab_supplies.py index 54a7092..182fdfa 100644 --- a/mosfet/jobs/grab_supplies.py +++ b/mosfet/jobs/grab_supplies.py @@ -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 GrabSuppliesStates: @@ -160,8 +159,8 @@ class GrabSuppliesStates: 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 w.get('container_slots', 0) != 27: + print('Got wrong container size, aborting') self.state = self.cleanup return used_slots = self.g.game.count_inventory_slots() @@ -173,10 +172,7 @@ class GrabSuppliesStates: self.state = self.cleanup return - w_info = mcdata.WINDOWS[w.data.window_type] - w_container_slots = w_info.container - - for slot_num in w_container_slots: + for slot_num in range(w.container_slots): if slot_num not in w.contents: continue diff --git a/mosfet/jobs/plant_tree.py b/mosfet/jobs/plant_tree.py index f55901a..757f882 100644 --- a/mosfet/jobs/plant_tree.py +++ b/mosfet/jobs/plant_tree.py @@ -39,10 +39,14 @@ class PlantTreeStates: def select_sapling(self): p = utils.pint(self.g.pos) - sapling_type = self.g.chopped_tree + '_sapling' - sapling_item = items.get_id(sapling_type) + sapling_item = items.get_sapling_id(self.g.chopped_tree) + if sapling_item is None: + print('Aborting planting, no sapling for', self.g.chopped_tree) + self.state = self.cleanup + return if self.g.game.select_item([sapling_item]): + sapling_type = items.get_name(sapling_item) self.g.look_at = utils.padd(p, path.BLOCK_BELOW) self.state = self.wait_select self.wait_time = 1 diff --git a/mosfet/protocol/managers.py b/mosfet/protocol/managers.py index f963946..ce50205 100644 --- a/mosfet/protocol/managers.py +++ b/mosfet/protocol/managers.py @@ -5,7 +5,7 @@ from munch import Munch from minecraft.networking.packets import clientbound, serverbound from minecraft.networking.modern import ( AddEntityPacket, ContainerOpenPacket, ContainerSetContentPacket, - ContainerSetSlotPacket, + ContainerSetSlotPacket, SetEntityDataPacket, PaletteSection, EntityPositionSyncPacket, MoveEntityPositionPacket, MoveEntityPositionRotationPacket, MoveEntityRotationPacket, @@ -347,6 +347,7 @@ class EntityManager: register = self.g.connection.register_packet_listener for packet_type, handler in ( (AddEntityPacket, self.handle_add_entity), + (SetEntityDataPacket, self.handle_entity_data), (RemoveEntitiesPacket, self.handle_remove_entities), (MoveEntityPositionPacket, self.handle_move), (MoveEntityPositionRotationPacket, self.handle_move), @@ -378,6 +379,14 @@ class EntityManager: type=packet.entity_type_id, yaw=packet.yaw, pitch=packet.pitch) + def handle_entity_data(self, packet): + obj = self.g.objects.get(packet.entity_id) + if obj is None or not packet.item_stacks: + return + stack = packet.item_stacks[-1] + obj.item_id = stack.item_id if stack.present else None + obj.item_count = stack.item_count if stack.present else 0 + def _entity(self, entity_id): for collection in (self.g.players, self.g.mobs, self.g.objects): entity = collection.get(entity_id) diff --git a/mosfet/utils.py b/mosfet/utils.py index ea5710f..8cc2d63 100644 --- a/mosfet/utils.py +++ b/mosfet/utils.py @@ -82,33 +82,48 @@ def diffrange(n): sign = 1 if n >= 0 else -1 return range(0, n+sign, sign) -def break_time(block_id, held_item=0, in_water=False, on_ground=True, enchantments=[], effects={}): - # from PrismarineJS/prismarine-block +def break_time(block_id, held_item=0, in_water=False, on_ground=True, + enchantments=None, effects=None): + """Return vanilla block-breaking duration rounded to whole game ticks.""" held_item = str(held_item) block_data = blocks.get(block_id) + hardness = block_data.get('hardness', 0) + if hardness < 0: + return float('inf') - can_harvest = 'harvestTools' not in block_data or str(held_item) in block_data['harvestTools'] + can_harvest = ( + 'harvestTools' not in block_data or + held_item in block_data['harvestTools']) material = block_data.get('material', 'n/a') - tool_multipliers = mcdata.mcd.materials.get(material, []) - is_best_tool = held_item in tool_multipliers - time = block_data['hardness'] + tool_multipliers = mcdata.mcd.materials.get(material, {}) + speed = tool_multipliers.get(held_item, 1.0) - if can_harvest: - time *= 1.5 - else: - time *= 5.0 + enchantments = enchantments or {} + if speed > 1: + efficiency = int(enchantments.get( + 'efficiency', enchantments.get('minecraft:efficiency', 0)) or 0) + if efficiency > 0: + speed += efficiency * efficiency + 1 - if is_best_tool: - speed_multiplier = tool_multipliers[held_item] - # TODO: calc efficiency, haste, mining fatigue - else: - speed_multiplier = 1.0 + effects = effects or {} + haste = int(effects.get('haste', effects.get('minecraft:haste', 0)) or 0) + if haste >= 0 and ('haste' in effects or 'minecraft:haste' in effects): + speed *= 1 + 0.2 * (haste + 1) + fatigue = int(effects.get( + 'mining_fatigue', effects.get('minecraft:mining_fatigue', -1))) + if fatigue >= 0: + speed *= (0.3, 0.09, 0.0027, 0.00081, 0.000243)[min(fatigue, 4)] - time /= speed_multiplier - if in_water: time *= 5.0 - if not on_ground: time *= 5.0 + duration = hardness * (1.5 if can_harvest else 5.0) / speed + if in_water: + duration *= 5.0 + if not on_ground: + duration *= 5.0 - return time + # Vanilla advances breaking progress in 1/20-second ticks; clients cannot + # finish between ticks. Round up, while avoiding floating-point over-round. + ticks = max(1, ceil(duration / TICK - 1e-9)) + return ticks * TICK def search_2d(distance=0): def get_neighbors(x,y,z): diff --git a/tests/test_break_time.py b/tests/test_break_time.py new file mode 100644 index 0000000..3fd1bf3 --- /dev/null +++ b/tests/test_break_time.py @@ -0,0 +1,69 @@ +import math +import unittest +from unittest.mock import patch +from munch import Munch + +from mosfet import utils +from mosfet.game import Game +from mosfet.info import blocks, items + + +class BlockBreakTimingTests(unittest.TestCase): + def test_leaf_time_uses_actual_tool_speed_and_game_ticks(self): + oak_leaves = blocks.MCD_BLOCKS['oak_leaves']['minStateId'] + oak_log = items.get_id('oak_log') + wooden_hoe = items.get_id('wooden_hoe') + + self.assertAlmostEqual(utils.break_time(oak_leaves, oak_log), 0.3) + self.assertAlmostEqual(utils.break_time(oak_leaves, wooden_hoe), 0.15) + self.assertAlmostEqual( + utils.break_time(oak_leaves, oak_log, in_water=True), 1.5) + self.assertAlmostEqual( + utils.break_time(oak_leaves, oak_log, on_ground=False), 1.5) + + def test_efficiency_haste_and_fatigue_affect_break_ticks(self): + oak_leaves = blocks.MCD_BLOCKS['oak_leaves']['minStateId'] + wooden_hoe = items.get_id('wooden_hoe') + self.assertAlmostEqual(utils.break_time( + oak_leaves, wooden_hoe, enchantments={'efficiency': 1}), 0.1) + self.assertAlmostEqual(utils.break_time( + oak_leaves, wooden_hoe, effects={'haste': 0}), 0.15) + self.assertAlmostEqual(utils.break_time( + oak_leaves, wooden_hoe, + effects={'mining_fatigue': 0}), 0.5) + + def test_unbreakable_blocks_have_no_completion_deadline(self): + bedrock = blocks.MCD_BLOCKS['bedrock']['minStateId'] + self.assertTrue(math.isinf(utils.break_time(bedrock))) + + def test_game_sends_finish_after_calculated_tick_duration(self): + class RecordingConnection: + def __init__(self): + self.packets = [] + + def write_packet(self, packet, force=False): + self.packets.append(packet) + + class FakeChunks: + @staticmethod + def get_block_at(*_position): + return blocks.MCD_BLOCKS['oak_leaves']['minStateId'] + + connection = RecordingConnection() + game = object.__new__(Game) + game.g = Munch( + connection=connection, chunks=FakeChunks(), inv={}, + holding=items.get_id('oak_log'), holding_hand=0, + holding_slot=0) + game.sequence = 0 + game.break_deadline = None + game.breaking_state = None + game.next_swing_at = 0.0 + + with patch('mosfet.game.time.monotonic', return_value=100.0): + self.assertTrue(game.break_block((0, 64, 0))) + self.assertAlmostEqual(game.break_deadline, 100.3) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_clear_leaves.py b/tests/test_clear_leaves.py new file mode 100644 index 0000000..3a31e99 --- /dev/null +++ b/tests/test_clear_leaves.py @@ -0,0 +1,37 @@ +import unittest +from unittest.mock import patch +from munch import Munch + +from mosfet.jobs.clear_leaves import ClearLeavesStates + + +class ClearLeavesTests(unittest.TestCase): + def test_spaces_leaf_breaks_at_configured_interval(self): + class FakeGame: + def __init__(self): + self.broken = [] + + def break_block(self, location): + self.broken.append(location) + return True + + game = FakeGame() + state = ClearLeavesStates(Munch( + breaking=False, game=game, look_at=None)) + state.leaves = [(0, 64, 0), (1, 64, 0)] + + with patch('mosfet.jobs.clear_leaves.time.monotonic', return_value=10): + state.break_leaves() + self.assertEqual(game.broken, [(0, 64, 0)]) + + with patch('mosfet.jobs.clear_leaves.time.monotonic', return_value=10.59): + state.break_leaves() + self.assertEqual(game.broken, [(0, 64, 0)]) + + with patch('mosfet.jobs.clear_leaves.time.monotonic', return_value=10.6): + state.break_leaves() + self.assertEqual(game.broken, [(0, 64, 0), (1, 64, 0)]) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_gather_wood.py b/tests/test_gather_wood.py index 4efaee0..8ea3b00 100644 --- a/tests/test_gather_wood.py +++ b/tests/test_gather_wood.py @@ -1,9 +1,15 @@ import unittest +from unittest.mock import patch + from munch import Munch +from mosfet.bot import start_automation from mosfet.commands import Commands from mosfet.info import blocks, items from mosfet.job import JobStates +from mosfet.jobs.gather_wood import GatherWoodStates +from mosfet.jobs.grab_supplies import GrabSuppliesStates +from mosfet.protocol.types import Slot from mosfet.world import World @@ -21,6 +27,27 @@ class GatherWoodPortTests(unittest.TestCase): self.assertTrue(blocks.LOG_IDS) self.assertTrue(blocks.LOG_IDS.issubset(blocks.INDEXED_IDS)) + def test_mangrove_uses_its_26_3_propagule_and_unplantable_logs_are_skipped(self): + self.assertEqual(items.get_sapling_id('mangrove'), + items.get_id('mangrove_propagule')) + self.assertIsNone(items.get_sapling_id('crimson')) + + class FakeWorld: + def find_trees(self, _position, _distance): + return [(0, 64, 0), (3, 64, 0)] + + def block_at(self, x, y, z): + return (blocks.MCD_BLOCKS['crimson_stem']['minStateId'] + if x == 0 else + blocks.MCD_BLOCKS['mangrove_log']['minStateId']) + + state = GatherWoodStates( + Munch(world=FakeWorld(), pos=(0, 64, 0))) + state.find_new_tree() + self.assertEqual(state.tree, (3, 64, 0)) + self.assertEqual(state.type, 'mangrove') + self.assertEqual(state.bad_trees, [(0, 64, 0)]) + def test_tree_search_finds_indexed_26_3_log_states(self): oak_log = blocks.MCD_BLOCKS['oak_log']['minStateId'] oak_leaves = blocks.MCD_BLOCKS['oak_leaves']['minStateId'] @@ -54,7 +81,76 @@ class GatherWoodPortTests(unittest.TestCase): self.assertIs(job.gather_wood_states.state.__func__, job.gather_wood_states.idle.__func__) - def test_gather_wood_command_is_enabled_but_farm_wood_is_not(self): + def test_grab_supplies_reads_protocol_777_container_shape(self): + axe_id = next(iter(items.AXE_IDS)) + axe = Slot(present=True, item_id=axe_id, item_count=1) + clicks = [] + + class FakeGame: + def count_inventory_slots(self): + return 10 + + def click_window(self, *args): + clicks.append(args) + + g = Munch( + item_lock=False, + window=Munch(container_slots=27, contents={0: axe}), + maximum_supply_slots=33, + game=FakeGame(), + ) + state = GrabSuppliesStates(g) + state.target_items = {axe_id} + state.maximum_stacks = 9 + state.grab_items() + + self.assertTrue(g.item_lock) + self.assertEqual(state.count, 1) + self.assertEqual(clicks, [(0, 0, 1, axe)]) + + def test_modern_automation_sets_supply_inventory_limit(self): + g = Munch() + with patch('mosfet.bot.threading.Thread') as thread: + start_automation(g, initialize=False) + self.assertEqual(g.maximum_supply_slots, 33) + thread.return_value.start.assert_called_once() + + def test_farm_wood_job_keeps_legacy_machine_sequence(self): + job = JobStates(Munch()) + try: + self.assertEqual(job.farm_wood(), [ + job.grab_supplies_states, + job.gather_wood_states, + job.clear_leaves_states, + job.plant_tree_states, + job.grab_sapling_states, + job.sleep_with_bed_states, + job.eat_food_states, + job.cache_items_states, + ]) + self.assertEqual(job.grab_supplies_states.supplies, { + tuple(items.AXE_IDS): (1, 9), + }) + self.assertTrue(job.sleep_with_bed_states.silent) + self.assertTrue(job.cache_items_states.silent) + self.assertTrue(items.AXE_IDS.issubset(items.NEEDED_ITEMS)) + self.assertTrue(items.SAPLING_IDS.issubset(items.WANTED_ITEMS)) + + job.grab_supplies_states.state = job.grab_supplies_states.find_barrels + job.clear_leaves_states.state = job.clear_leaves_states.find_leaves + job.plant_tree_states.state = job.plant_tree_states.check_feet + job.grab_sapling_states.state = job.grab_sapling_states.find_saplings + job.stop() + for machine in (job.grab_supplies_states, + job.clear_leaves_states, + job.plant_tree_states, + job.grab_sapling_states): + self.assertIs(machine.state.__func__, machine.idle.__func__) + finally: + items.set_needed(set()) + items.set_wanted(set()) + + def test_gather_and_farm_wood_commands_are_enabled(self): class FakeChat: def __init__(self): self.sent = [] @@ -75,7 +171,8 @@ class GatherWoodPortTests(unittest.TestCase): chat.sent.clear() Commands(g).handle_chat(('CHAT', 'tester', 'tester: 1farm wood')) - self.assertEqual(chat.sent, ['not migrated to Minecraft 26.3 yet']) + self.assertEqual(chat.sent, ['ok']) + self.assertIs(job.state.__func__, job.farm_wood.__func__) if __name__ == '__main__': diff --git a/tests/test_sapling_entities.py b/tests/test_sapling_entities.py new file mode 100644 index 0000000..8215625 --- /dev/null +++ b/tests/test_sapling_entities.py @@ -0,0 +1,60 @@ +import io +import unittest +from munch import Munch + +from minecraft.networking.modern import ( + ModernPlayingReactor, SetEntityDataPacket, +) +from mosfet.info import items +from mosfet.protocol.managers import EntityManager +from mosfet.world import World + + +def encode_varint(value): + encoded = bytearray() + while value & ~0x7F: + encoded.append((value & 0x7F) | 0x80) + value >>= 7 + encoded.append(value) + return bytes(encoded) + + +class SaplingEntityTests(unittest.TestCase): + def test_protocol_777_item_metadata_decodes_item_stack(self): + sapling_id = items.get_id('oak_sapling') + payload = ( + encode_varint(42) + + bytes((8,)) + encode_varint(7) + + encode_varint(1) + encode_varint(sapling_id) + + encode_varint(0) + encode_varint(0) + + bytes((0xFF,)) + ) + packet = SetEntityDataPacket() + packet.read(io.BytesIO(payload)) + + self.assertEqual(packet.entity_id, 42) + self.assertEqual(len(packet.item_stacks), 1) + self.assertEqual(packet.item_stacks[0].item_id, sapling_id) + self.assertIn(SetEntityDataPacket, + ModernPlayingReactor.get_clientbound_packets(None)) + + def test_item_metadata_makes_dropped_sapling_searchable(self): + sapling_id = items.get_id('oak_sapling') + entity = Munch(entity_id=42, x=1.0, y=65.0, z=2.0) + manager = object.__new__(EntityManager) + manager.g = Munch(objects={42: entity}) + packet = Munch( + entity_id=42, + item_stacks=[Munch(present=True, item_id=sapling_id, + item_count=1)], + ) + manager.handle_entity_data(packet) + + found = World(Munch(objects={42: entity})).find_objects( + items.SAPLING_IDS) + self.assertEqual(found, [entity]) + self.assertEqual(entity.item_count, 1) + + +if __name__ == '__main__': + unittest.main()