From ef7bf1d7daa47e2a4349f19f918f489876ec4ebd Mon Sep 17 00:00:00 2001 From: Tanner Date: Thu, 5 Nov 2026 23:22:29 +0000 Subject: [PATCH] Grab useful supplies off the ground --- mosfet/job.py | 21 +++-- mosfet/jobs/grab_supplies.py | 156 +++++++++++++++++++++++++++++++++-- tests/test_crop_cache.py | 5 +- tests/test_gather_wood.py | 3 + tests/test_grab_supplies.py | 112 +++++++++++++++++++++++++ 5 files changed, 282 insertions(+), 15 deletions(-) create mode 100644 tests/test_grab_supplies.py diff --git a/mosfet/job.py b/mosfet/job.py index 6c7b061..e9085a1 100644 --- a/mosfet/job.py +++ b/mosfet/job.py @@ -152,9 +152,9 @@ class LegacyJobStates: ] self.sleep_with_bed_states.silent = True self.cache_items_states.silent = True - self.grab_supplies_states.supplies = { - tuple(items.AXE_IDS): (1, 9), - } + self.grab_supplies_states.supplies = ( + self.grab_supplies_states.default_supplies()) + self.grab_supplies_states.supplies[tuple(items.AXE_IDS)] = (1, 9) items.set_needed(items.AXE_IDS) items.set_wanted(items.SAPLING_IDS) @@ -286,9 +286,9 @@ class JobStates(LegacyJobStates): 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), - } + self.grab_supplies_states.supplies = ( + self.grab_supplies_states.default_supplies()) + self.grab_supplies_states.supplies[tuple(items.AXE_IDS)] = (1, 9) items.set_needed(items.AXE_IDS) items.set_wanted(items.SAPLING_IDS) return [ @@ -312,8 +312,13 @@ class JobStates(LegacyJobStates): items.WHEAT_SEEDS_ID, items.BEETROOT_SEEDS_ID, }) - return [self.gather_crop_states, self.sleep_with_bed_states, - self.eat_food_states, self.cache_items_states] + return [ + self.grab_supplies_states, + self.gather_crop_states, + self.sleep_with_bed_states, + self.eat_food_states, + self.cache_items_states + ] def eat(self): return [self.eat_food_states] diff --git a/mosfet/jobs/grab_supplies.py b/mosfet/jobs/grab_supplies.py index 182fdfa..b73fc88 100644 --- a/mosfet/jobs/grab_supplies.py +++ b/mosfet/jobs/grab_supplies.py @@ -16,6 +16,15 @@ from mosfet.info import items from mosfet.info import mobs class GrabSuppliesStates: + DEFAULT_SUPPLIES = { + tuple(items.BED_IDS): (1, 1), + (items.CHEST_ID,): (1, 1), + tuple(items.FOOD_IDS): (1, 1), + } + + def default_supplies(self): + return dict(self.DEFAULT_SUPPLIES) + def idle(self): return None @@ -30,31 +39,166 @@ class GrabSuppliesStates: self.state = self.cleanup return + self.checked_ground_items = set() + self.checked_ground_groups = set() + self.ground_stack_counts = {} + self.ground_entity_id = None + self.ground_position = None if self.supplies: self.state = self.check_supplies else: print('Aborting getting supplies, none specified') self.state = self.cleanup + def _has_nearby_ground_items(self, item_ids, maximum_stacks): + if (maximum_stacks and + self.ground_stack_counts.get(item_ids, 0) >= maximum_stacks): + return False + p = utils.pint(self.g.pos) + return any( + obj.entity_id not in self.checked_ground_items and + utils.phyp(p, (obj.x, obj.y, obj.z)) <= 16 + for obj in self.g.world.find_objects(item_ids)) + + def has_nearby_ground_supply(self): + return any( + self._has_nearby_ground_items(item_ids, limits[1]) + for item_ids, limits in self.supplies.items()) + + def _record_ground_pickup(self): + self.checked_ground_items.add(self.ground_entity_id) + self.ground_stack_counts[self.target_items] = ( + self.ground_stack_counts.get(self.target_items, 0) + 1) + if (self.target_maximum_stacks and + self.ground_stack_counts[self.target_items] >= + self.target_maximum_stacks): + self.checked_ground_groups.add(self.target_items) + self.g.path = [] + self.ground_entity_id = None + self.ground_position = None + def check_supplies(self): - # check if we need to grab anything - for items, limits in self.supplies.items(): + # Search every configured item group on the ground before going to a + # barrel; a missing bed must not hide a nearby chest or food drop. + missing = [] + for item_ids, limits in self.supplies.items(): minimum, maximum = limits - print('Checking items:', items) - num_items = self.g.game.count_items(items) + print('Checking items:', item_ids) + num_items = self.g.game.count_items(item_ids) print('Have:', num_items) + if (item_ids not in self.checked_ground_groups and + self._has_nearby_ground_items(item_ids, maximum)): + print('Found matching supplies on the ground') + self.target_items = item_ids + self.target_minimum = minimum + self.target_maximum_stacks = maximum + self.state = self.find_ground_items + return + if num_items >= minimum: print('Have enough, skipping') continue - print('Need at least one item') + missing.append((item_ids, minimum, maximum)) + if item_ids not in self.checked_ground_groups: + self.target_items = item_ids + self.target_minimum = minimum + self.target_maximum_stacks = maximum + self.state = self.find_ground_items + return + + if missing: + self.target_items, self.target_minimum, self.target_maximum_stacks = missing[0] + print('No more ground drops; checking barrels for', + self.target_items) self.state = self.find_barrels return print('Aborting, dont need any supplies') self.state = self.cleanup + def find_ground_items(self): + stack_count = self.ground_stack_counts.get(self.target_items, 0) + if self.target_maximum_stacks and stack_count >= self.target_maximum_stacks: + self.checked_ground_groups.add(self.target_items) + self.state = self.check_supplies + return + + print('Looking for supplies on the ground...') + p = utils.pint(self.g.pos) + ground_items = self.g.world.find_objects(self.target_items) + ground_items.sort(key=lambda obj: utils.phyp( + p, (obj.x, obj.y, obj.z))) + + for obj in ground_items: + if obj.entity_id in self.checked_ground_items: + continue + current_count = self.g.game.count_items(self.target_items) + if utils.phyp(p, (obj.x, obj.y, obj.z)) > 16: + continue + target = utils.pint((obj.x, obj.y, obj.z)) + self.ground_entity_id = obj.entity_id + self.ground_position = target + self.ground_required_count = max( + self.target_minimum, current_count + 1) + if utils.phyp(p, (obj.x, obj.y, obj.z)) <= 1.25: + self.g.path = [] + self.wait_time = 1.0 + self.state = self.wait_for_ground_item + return + + navpath = self.g.world.path_to_place(p, target) + if not navpath: + self.checked_ground_items.add(obj.entity_id) + continue + + print('Going to ground item', obj.item_id, target) + self.g.path = navpath + self.state = self.going_to_ground_item + return + + self.checked_ground_groups.add(self.target_items) + self.state = self.check_supplies + + def going_to_ground_item(self): + obj = self.g.objects.get(self.ground_entity_id) + have_enough = (self.g.game.count_items(self.target_items) >= + self.ground_required_count) + if have_enough: + print('Picked up required ground supplies') + self._record_ground_pickup() + self.state = self.check_supplies + elif (obj is None or + utils.pint(self.g.pos) == self.ground_position or + utils.phyp(self.g.pos, (obj.x, obj.y, obj.z)) <= 1.25): + self.wait_time = 1.0 + self.state = self.wait_for_ground_item + + def wait_for_ground_item(self): + if (self.g.game.count_items(self.target_items) >= + self.ground_required_count): + print('Picked up required ground supplies') + self._record_ground_pickup() + self.state = self.check_supplies + return + + if self.wait_time > 0: + self.wait_time -= utils.TICK + return + + if self.ground_entity_id not in self.g.objects: + # Entity removal and inventory updates can arrive in either order. + self.state = self.check_supplies + return + + print('Could not pick up ground item; trying another') + self.checked_ground_items.add(self.ground_entity_id) + self.g.path = [] + self.ground_entity_id = None + self.ground_position = None + self.state = self.find_ground_items + def find_barrels(self): print('Finding barrels...') w = self.g.world @@ -215,7 +359,7 @@ class GrabSuppliesStates: self.g = global_state self.state = self.idle - self.supplies = {} + self.supplies = self.default_supplies() self.barrels = [] self.checked_barrels = [] self.bad_barrels = [] diff --git a/tests/test_crop_cache.py b/tests/test_crop_cache.py index 1721b67..b4d37d5 100644 --- a/tests/test_crop_cache.py +++ b/tests/test_crop_cache.py @@ -9,11 +9,13 @@ from mosfet.job import JobStates class TestCropCaching(unittest.TestCase): def test_crop_farm_runs_silent_cache_machine_and_keeps_seeds(self): old_wanted = items.WANTED_ITEMS + supply_state = object() crop_state = object() cache_state = Munch(silent=False) sleep_state = Munch(silent=False) eat_state = object() jobs = JobStates.__new__(JobStates) + jobs.grab_supplies_states = supply_state jobs.gather_crop_states = crop_state jobs.cache_items_states = cache_state jobs.sleep_with_bed_states = sleep_state @@ -22,7 +24,8 @@ class TestCropCaching(unittest.TestCase): try: machines = JobStates.farm_crop(jobs) self.assertEqual( - machines, [crop_state, sleep_state, eat_state, cache_state]) + machines, [supply_state, crop_state, sleep_state, + eat_state, cache_state]) self.assertTrue(cache_state.silent) self.assertTrue(sleep_state.silent) self.assertTrue({ diff --git a/tests/test_gather_wood.py b/tests/test_gather_wood.py index 8ea3b00..6facc3f 100644 --- a/tests/test_gather_wood.py +++ b/tests/test_gather_wood.py @@ -130,6 +130,9 @@ class GatherWoodPortTests(unittest.TestCase): ]) self.assertEqual(job.grab_supplies_states.supplies, { tuple(items.AXE_IDS): (1, 9), + tuple(items.BED_IDS): (1, 1), + (items.CHEST_ID,): (1, 1), + tuple(items.FOOD_IDS): (1, 1), }) self.assertTrue(job.sleep_with_bed_states.silent) self.assertTrue(job.cache_items_states.silent) diff --git a/tests/test_grab_supplies.py b/tests/test_grab_supplies.py new file mode 100644 index 0000000..c21710d --- /dev/null +++ b/tests/test_grab_supplies.py @@ -0,0 +1,112 @@ +import unittest +from munch import Munch + +from mosfet.info import items +from mosfet.jobs.grab_supplies import GrabSuppliesStates + + +class GroundSupplyTests(unittest.TestCase): + def test_default_needed_supplies_are_configured_by_constructor(self): + state = GrabSuppliesStates(Munch()) + self.assertEqual(state.supplies, { + tuple(items.BED_IDS): (1, 1), + (items.CHEST_ID,): (1, 1), + tuple(items.FOOD_IDS): (1, 1), + }) + + def test_chest_drop_is_checked_even_when_bed_is_missing(self): + chest = Munch(entity_id=8, item_id=items.CHEST_ID, + x=2.0, y=64.0, z=0.0) + + class FakeWorld: + @staticmethod + def find_objects(item_ids): + return [chest] if items.CHEST_ID in item_ids else [] + + class FakeGame: + @staticmethod + def count_inventory_slots(): + return 0 + + @staticmethod + def count_items(_item_ids): + return 0 + + state = GrabSuppliesStates(Munch( + pos=(0.5, 64.0, 0.5), world=FakeWorld(), game=FakeGame(), + maximum_supply_slots=33)) + state.init() + state.check_supplies() + state.find_ground_items() + self.assertIn(tuple(items.BED_IDS), state.checked_ground_groups) + state.check_supplies() + + self.assertEqual(state.target_items, (items.CHEST_ID,)) + self.assertIs(state.state.__func__, state.find_ground_items.__func__) + + def test_job_walks_to_and_picks_up_dropped_supply_before_barrels(self): + axe_id = next(iter(items.AXE_IDS)) + dropped_axe = Munch( + entity_id=7, item_id=axe_id, item_count=1, + x=3.2, y=64.0, z=0.2) + + class FakeWorld: + def __init__(self): + self.path_requests = [] + + def find_objects(self, item_ids): + return ([dropped_axe] if axe_id in item_ids else []) + + def path_to_place(self, start, target): + self.path_requests.append((start, target)) + return [start, target] + + class FakeGame: + def __init__(self): + self.item_count = 0 + + def count_inventory_slots(self): + return 5 + + def count_items(self, item_ids): + return self.item_count if axe_id in item_ids else 0 + + world = FakeWorld() + game = FakeGame() + g = Munch( + pos=(0.5, 64.0, 0.5), world=world, game=game, + chunks=Munch(), objects={7: dropped_axe}, path=[], + maximum_supply_slots=33, + ) + state = GrabSuppliesStates(g) + state.supplies = {tuple(items.AXE_IDS): (1, 9)} + + state.init() + state.check_supplies() + state.find_ground_items() + self.assertIs(state.state.__func__, state.going_to_ground_item.__func__) + self.assertEqual(g.path[-1], (3, 64, 0)) + self.assertEqual(world.path_requests, [((0, 64, 0), (3, 64, 0))]) + + g.pos = (3.5, 64.0, 0.5) + state.going_to_ground_item() + self.assertIs(state.state.__func__, state.wait_for_ground_item.__func__) + + game.item_count = 1 + state.wait_for_ground_item() + self.assertIs(state.state.__func__, state.check_supplies.__func__) + state.check_supplies() + self.assertIs(state.state.__func__, state.cleanup.__func__) + + # Nearby dropped tools are collected even when inventory already meets + # the minimum, so tossing an axe during setup still gets noticed. + game.item_count = 1 + state.init() + state.check_supplies() + self.assertIs(state.state.__func__, state.find_ground_items.__func__) + state.find_ground_items() + self.assertEqual(state.ground_required_count, 2) + + +if __name__ == '__main__': + unittest.main()