Add sign-labeled chest storage

This commit is contained in:
Tanner
2026-11-07 16:35:57 +00:00
parent ab8f59a89b
commit 43b37b76f6
7 changed files with 401 additions and 66 deletions
+19
View File
@@ -83,6 +83,25 @@ EMERALD_ID = get_id('emerald')
BERRIES_ID = get_id('sweet_berries')
IRON_INGOT_ID = get_id('iron_ingot')
_LABEL_ITEM_IDS = {
'seeds': get_set({
'beetroot', 'beetroot_seeds', 'carrot', 'melon_seeds',
'nether_wart', 'pitcher_pod', 'potato', 'pumpkin_seeds',
'torchflower_seeds', 'wheat_seeds',
}),
'wheat': {WHEAT_ID},
}
def storage_item_ids_for_label(label):
"""Return protocol item IDs assigned to a sign storage label."""
normalized = ' '.join(str(label).casefold().split()).strip(' :')
normalized = (normalized.rstrip('s')
if normalized not in ('seeds', 'beds') else normalized)
return set(_LABEL_ITEM_IDS.get(normalized, ()))
INIT_NEEDED_ITEMS = BED_IDS | FOOD_IDS
INIT_NEEDED_ITEMS.add(CHEST_ID)
NEEDED_ITEMS = INIT_NEEDED_ITEMS
+95 -8
View File
@@ -22,6 +22,8 @@ class CacheItemsStates:
def init(self):
self.skip_slots = []
self.skip_items = []
self.is_labeled_container = False
self.container_item_ids = None
num_stacks = self.g.game.count_inventory_slots()
minimum = getattr(
@@ -36,25 +38,54 @@ class CacheItemsStates:
self.state = self.cleanup
def find_trapped_chests(self):
print('Finding trapped chests...')
print('Finding labeled and trapped chests...')
w = self.g.world
p = utils.pint(self.g.pos)
self.labeled_chests = w.find_labeled_chests(p, 80)
self.checked_labeled_chests = set()
self.trapped_chests = w.find_blocks_indexed(p, blocks.TRAPPED_CHEST_IDS)
print('Found:', self.trapped_chests)
print('Found labeled chests:', len(self.labeled_chests),
'trapped chests:', self.trapped_chests)
self.state = self.choose_trapped_chest
def choose_trapped_chest(self):
print('Choosing a trapped chest...')
print('Choosing a labeled or trapped chest...')
w = self.g.world
p = utils.pint(self.g.pos)
c = self.g.chunks
for container in self.labeled_chests:
key = container['positions']
if key in self.checked_labeled_chests:
continue
self.checked_labeled_chests.add(key)
if not any(item.present and item.item_id in container['item_ids']
for item in self.g.inv.values()):
continue
self.area = container['position']
self.container_item_ids = container['item_ids']
self.is_labeled_container = True
for opening in w.find_cache_openings(self.area):
navpath = w.path_to_place(p, opening)
if navpath:
print('Going to labeled chest', container['labels'],
self.area)
self.opening = opening
self.g.path = navpath
self.state = self.going_to_trapped_chest
return
print('No path to labeled chest', self.area)
if not len(self.trapped_chests):
print('No trapped chests')
self.is_labeled_container = False
self.container_item_ids = None
print('No labeled or trapped chests')
self.state = self.select_chest
return
self.is_labeled_container = False
self.container_item_ids = None
chest = self.trapped_chests[0]
self.area = chest
@@ -83,6 +114,8 @@ class CacheItemsStates:
self.state = self.open_chest
def select_chest(self):
self.is_labeled_container = False
self.container_item_ids = None
if self.g.game.select_item([items.CHEST_ID]):
self.state = self.find_cache_spot
else:
@@ -149,6 +182,41 @@ class CacheItemsStates:
else:
self.state = self.move_items
def _needed_inventory_slots_to_keep(self):
needed = set(items.NEEDED_ITEMS)
groups = (
items.BED_IDS, {items.CHEST_ID}, items.FOOD_IDS,
items.AXE_IDS, items.SHOVEL_IDS,
)
keep = set()
covered = set()
for group in groups:
group = needed.intersection(group)
if not group:
continue
covered.update(group)
stacks = [(slot, item) for slot, item in self.g.inv.items()
if 5 <= slot <= 45 and item.present and
item.item_id in group]
if stacks:
keep.add(max(stacks, key=lambda pair: pair[1].item_count)[0])
for item_id in needed - covered:
stacks = [(slot, item) for slot, item in self.g.inv.items()
if 5 <= slot <= 45 and item.present and
item.item_id == item_id]
if stacks:
keep.add(max(stacks, key=lambda pair: pair[1].item_count)[0])
return keep
def _window_player_slot(self, window_slot):
offset = window_slot - self.g.window.container_slots
if 0 <= offset < 27:
return 9 + offset
if 27 <= offset < 36:
return 36 + offset - 27
return None
def move_items(self):
if self.g.item_lock: return
w = self.g.window
@@ -177,10 +245,13 @@ class CacheItemsStates:
print('Container is too full, aborting')
self.g.game.close_window()
self.g.look_at = None
self.state = self.cleanup
self.state = (self.close_chest if self.is_labeled_container
else self.cleanup)
return
slot_list = []
needed_slots = (self._needed_inventory_slots_to_keep()
if self.is_labeled_container else set())
for slot_num in w_inventory_slots:
if slot_num not in w.contents:
@@ -191,7 +262,14 @@ class CacheItemsStates:
if not slot.present:
continue
if slot.item_id in items.NEEDED_ITEMS:
if self.is_labeled_container:
if slot.item_id not in self.container_item_ids:
continue
inventory_slot = self._window_player_slot(slot_num)
if (slot.item_id in items.NEEDED_ITEMS and
inventory_slot in needed_slots):
continue
elif slot.item_id in items.NEEDED_ITEMS:
continue
if slot_num in self.skip_slots:
@@ -222,7 +300,12 @@ class CacheItemsStates:
self.g.game.close_window()
if not self.silent:
self.g.chat.send('cache at ' + str(self.area)[1:-1])
self.state = self.cleanup
if self.is_labeled_container:
self.is_labeled_container = False
self.container_item_ids = None
self.state = self.choose_trapped_chest
else:
self.state = self.cleanup
def cleanup(self):
self.g.look_at = None
@@ -244,6 +327,10 @@ class CacheItemsStates:
self.skip_slots = []
self.skip_items = []
self.labeled_chests = []
self.checked_labeled_chests = set()
self.is_labeled_container = False
self.container_item_ids = None
self.area = None
self.opening = None
self.trapped_chests = []
+47 -51
View File
@@ -30,7 +30,7 @@ class GrabSuppliesStates:
def init(self):
print('Started grab supplies states')
self.checked_barrels = []
self.checked_supply_chests = set()
used_slots = self.g.game.count_inventory_slots()
print('used:', used_slots, 'total:', self.g.maximum_supply_slots)
@@ -41,6 +41,7 @@ class GrabSuppliesStates:
self.checked_ground_items = set()
self.checked_ground_groups = set()
self.checked_supply_chests = set()
self.ground_stack_counts = {}
self.ground_entity_id = None
self.ground_position = None
@@ -78,8 +79,8 @@ class GrabSuppliesStates:
self.ground_position = None
def check_supplies(self):
# 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.
# Search every configured item group on the ground before looking in
# labeled chests; a missing bed must not hide a nearby chest or food drop.
missing = []
for item_ids, limits in self.supplies.items():
minimum, maximum = limits
@@ -110,9 +111,9 @@ class GrabSuppliesStates:
if missing:
self.target_items, self.target_minimum, self.target_maximum_stacks = missing[0]
print('No more ground drops; checking barrels for',
print('No more ground drops; checking supply-labeled chests for',
self.target_items)
self.state = self.find_barrels
self.state = self.find_supply_chests
return
print('Aborting, dont need any supplies')
@@ -122,7 +123,11 @@ class GrabSuppliesStates:
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
if (self.g.game.count_items(self.target_items) <
self.target_minimum):
self.state = self.find_supply_chests
else:
self.state = self.check_supplies
return
print('Looking for supplies on the ground...')
@@ -199,60 +204,52 @@ class GrabSuppliesStates:
self.ground_position = None
self.state = self.find_ground_items
def find_barrels(self):
print('Finding barrels...')
w = self.g.world
def find_supply_chests(self):
print('Finding chests labeled for this bot\'s supplies...')
p = utils.pint(self.g.pos)
self.supply_chests = self.g.world.find_supply_chests(
p, self.g.name, 80)
print('Found:', len(self.supply_chests), 'supply-labeled chests')
self.state = self.choose_supply_chest
self.barrels = w.find_blocks_indexed(p, blocks.BARREL_IDS, 80)
print('Found:', self.barrels)
self.state = self.choose_barrel
def choose_barrel(self):
print('Choosing a barrel...')
for barrel in self.barrels:
if barrel in self.checked_barrels:
def choose_supply_chest(self):
print('Choosing a supply-labeled chest...')
for container in self.supply_chests:
key = container['positions']
if key in self.checked_supply_chests:
continue
if barrel in self.bad_barrels:
continue
print('Chose:', barrel)
self.barrel = barrel
self.state = self.path_to_barrel
self.checked_supply_chests.add(key)
self.chest = container['position']
self.area = self.chest
print('Chose supply chest:', self.chest)
self.state = self.path_to_supply_chest
return
print('No barrels')
print('No more supply-labeled chests')
self.state = self.cleanup
def path_to_barrel(self):
print('Finding path to barrel')
w = self.g.world
def path_to_supply_chest(self):
print('Finding path to supply chest')
p = utils.pint(self.g.pos)
c = self.g.chunks
navpath = w.path_to_place_faked(p, self.barrel)
navpath = self.g.world.path_to_place_faked(p, self.chest)
print('navpath:', navpath)
if navpath:
self.g.path = navpath[:-1]
self.opening = self.g.path[-1]
self.checked_barrels.append(self.barrel)
self.area = self.barrel
self.state = self.going_to_barrel
self.state = self.going_to_supply_chest
self.checked_supplies = []
return
else:
print('Barrel is unreachable; ending supply task')
self.bad_barrels.append(self.barrel)
self.state = self.cleanup
print('Supply chest is unreachable; trying another labeled chest')
self.state = self.choose_supply_chest
def going_to_barrel(self):
def going_to_supply_chest(self):
if utils.pint(self.g.pos) == self.opening:
self.g.look_at = self.area
self.state = self.open_barrel
self.state = self.open_supply_chest
def open_barrel(self):
print('Opening barrel')
def open_supply_chest(self):
print('Opening supply chest')
self.g.game.open_container(self.area)
self.wait_time = 1
self.state = self.wait
@@ -288,8 +285,8 @@ class GrabSuppliesStates:
self.state = self.grab_items
return
print('Aborting, dont need any more supplies')
self.state = self.close_barrel
print('No matching items in this supply chest')
self.state = self.close_supply_chest
def grab_items(self):
if self.g.item_lock: return
@@ -299,7 +296,7 @@ class GrabSuppliesStates:
print('Didnt get a window, aborting')
self.state = self.cleanup
return
if w.get('container_slots', 0) != 27:
if w.get('container_slots', 0) not in (27, 54):
print('Got wrong container size, aborting')
self.state = self.cleanup
return
@@ -338,11 +335,11 @@ class GrabSuppliesStates:
self.wait_time = 0.25
self.state = self.wait
def close_barrel(self):
print('Closing barrel')
def close_supply_chest(self):
print('Closing supply chest')
self.g.game.close_window()
self.g.look_at = None
self.state = self.choose_barrel
self.state = self.choose_supply_chest
def cleanup(self):
self.state = self.done
@@ -356,10 +353,9 @@ class GrabSuppliesStates:
self.state = self.idle
self.supplies = self.default_supplies()
self.barrels = []
self.checked_barrels = []
self.bad_barrels = []
self.barrel = None
self.supply_chests = []
self.checked_supply_chests = set()
self.supply_chest = None
self.checked_supplies = []
self.target_items = None
self.maximum_stacks = 0
+117
View File
@@ -9,6 +9,7 @@ from copy import copy
from mosfet import utils
from mosfet import path
from mosfet.info import blocks
from mosfet.info import items
from mosfet.info import mobs
class World:
@@ -76,6 +77,122 @@ class World:
result.sort(key=lambda sign: utils.phyp(center, sign['position']))
return result
def _double_chest_positions(self, chest):
state_id = self.block_at(*chest)
properties = blocks.PROPS.get(state_id, {})
chest_type = properties.get('type')
if chest_type not in ('left', 'right'):
return (tuple(chest),)
other_type = 'right' if chest_type == 'left' else 'left'
for offset in ((1, 0, 0), (-1, 0, 0), (0, 0, 1), (0, 0, -1)):
partner = utils.padd(chest, offset)
other = blocks.PROPS.get(self.block_at(*partner), {})
if (other.get('type') == other_type and
other.get('facing') == properties.get('facing')):
return tuple(sorted((tuple(chest), partner)))
return (tuple(chest),)
def find_labeled_chests(self, center, distance=80):
"""Find single/double chests assigned item categories by nearby signs."""
grouped = {}
sign_offsets = (
(0, 0, 0), (0, -1, 0),
(1, 0, 0), (-1, 0, 0), (0, 0, 1), (0, 0, -1),
)
for sign in self.find_signs(center, distance):
categories = set()
labels = set()
for line in sign['lines']:
item_ids = items.storage_item_ids_for_label(line)
if item_ids:
categories.update(item_ids)
labels.add(line.strip())
if not categories:
continue
sign_pos = sign['position']
adjacent = {
utils.padd(sign_pos, offset)
for offset in sign_offsets
if self.block_at(*utils.padd(sign_pos, offset)) in blocks.CHEST_IDS
}
if not adjacent:
nearby = self.find_blocks_indexed(
sign_pos, blocks.CHEST_IDS, 2)
if nearby:
nearest = min(utils.phyp(sign_pos, chest)
for chest in nearby)
adjacent = {chest for chest in nearby
if utils.phyp(sign_pos, chest) == nearest}
for chest in adjacent:
positions = self._double_chest_positions(chest)
record = grouped.setdefault(positions, {
'position': tuple(chest),
'positions': positions,
'item_ids': set(),
'labels': set(),
'sign_positions': set(),
})
record['item_ids'].update(categories)
record['labels'].update(labels)
record['sign_positions'].add(sign_pos)
result = list(grouped.values())
result.sort(key=lambda chest: utils.phyp(center, chest['position']))
return result
def find_supply_chests(self, center, bot_name, distance=80):
"""Find chests whose sign is addressed to this bot's supplies."""
bot_name = str(bot_name).casefold()
group_name = (bot_name[:-1] if len(bot_name) > 1 and
bot_name[-1] in '123' else None)
addresses = {bot_name}
if group_name:
addresses.add(group_name)
grouped = {}
sign_offsets = (
(0, 0, 0), (0, -1, 0),
(1, 0, 0), (-1, 0, 0), (0, 0, 1), (0, 0, -1),
)
for sign in self.find_signs(center, distance):
text = ' '.join(line for line in sign['lines'] if line.strip())
tokens = re.findall(r'[a-z0-9_]+', text.casefold())
if (len(tokens) < 2 or tokens[0] not in addresses or
'supplies' not in tokens[1:]):
continue
sign_pos = sign['position']
candidates = {
utils.padd(sign_pos, offset)
for offset in sign_offsets
if self.block_at(*utils.padd(sign_pos, offset)) in blocks.CHEST_IDS
}
if not candidates:
nearby = self.find_blocks_indexed(
sign_pos, blocks.CHEST_IDS, 2)
if nearby:
nearest = min(utils.phyp(sign_pos, chest) for chest in nearby)
candidates = {chest for chest in nearby
if utils.phyp(sign_pos, chest) == nearest}
for chest in candidates:
positions = self._double_chest_positions(chest)
record = grouped.setdefault(positions, {
'position': tuple(chest),
'positions': positions,
'labels': set(),
'sign_positions': set(),
})
record['labels'].update(tokens[1:])
record['sign_positions'].add(sign_pos)
result = list(grouped.values())
result.sort(key=lambda chest: utils.phyp(center, chest['position']))
return result
def sign_policies(self, bot_name, distance=64):
"""Parse sign policies addressed to this bot or its numbered group."""
bot_name = str(bot_name).casefold()
+1 -1
View File
@@ -147,7 +147,7 @@ class GatherWoodPortTests(unittest.TestCase):
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.grab_supplies_states.state = job.grab_supplies_states.find_supply_chests
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
+45 -6
View File
@@ -8,6 +8,14 @@ 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(items.FOOD, [
'cooked_porkchop', 'cooked_beef', 'bread', 'cooked_chicken',
'cooked_cod', 'cooked_salmon',
])
self.assertEqual(items.FOOD_IDS, items.get_set({
'cooked_porkchop', 'cooked_beef', 'bread', 'cooked_chicken',
'cooked_cod', 'cooked_salmon',
}))
self.assertEqual(state.supplies, {
tuple(items.BED_IDS): (1, 1),
(items.CHEST_ID,): (1, 1),
@@ -44,19 +52,50 @@ class GroundSupplyTests(unittest.TestCase):
self.assertEqual(state.target_items, (items.CHEST_ID,))
self.assertIs(state.state.__func__, state.find_ground_items.__func__)
def test_unreachable_barrel_ends_supply_task_without_trying_more(self):
def test_supply_selection_uses_only_addressed_supply_chests(self):
position = (4, 64, 0)
chest_record = {
'position': position,
'positions': (position,),
'labels': {'mosfet supplies'},
'sign_positions': {(4, 64, 1)},
}
class FakeWorld:
@staticmethod
def path_to_place_faked(_start, _barrel):
return None
def find_supply_chests(_center, _bot_name, _distance):
return [chest_record]
@staticmethod
def find_blocks_indexed(*_args):
raise AssertionError('grab_supplies must not search barrels')
state = GrabSuppliesStates(Munch(
name='mosfet1', pos=(0.5, 64.0, 0.5), world=FakeWorld()))
state.find_supply_chests()
state.choose_supply_chest()
self.assertEqual(state.chest, position)
self.assertIs(state.state.__func__, state.path_to_supply_chest.__func__)
def test_unreachable_supply_chest_ends_task_without_searching_barrels(self):
class FakeWorld:
@staticmethod
def path_to_place_faked(_start, _chest):
return None
position = (10, 64, 10)
state = GrabSuppliesStates(Munch(
pos=(0.5, 64.0, 0.5), world=FakeWorld(), chunks=Munch()))
state.barrel = (10, 64, 10)
state.path_to_barrel()
state.supply_chests = [{
'position': position, 'positions': (position,),
'labels': {'mosfet supplies'},
}]
state.choose_supply_chest()
state.path_to_supply_chest()
state.choose_supply_chest()
self.assertIs(state.state.__func__, state.cleanup.__func__)
self.assertEqual(state.bad_barrels, [(10, 64, 10)])
def test_job_walks_to_and_picks_up_dropped_supply_before_barrels(self):
axe_id = next(iter(items.AXE_IDS))
+77
View File
@@ -0,0 +1,77 @@
import unittest
from munch import Munch
from mosfet.info import blocks, items
from mosfet.jobs.cache_items import CacheItemsStates
from mosfet.world import World
class LabeledStorageTests(unittest.TestCase):
def test_labels_resolve_to_item_categories(self):
seeds = items.storage_item_ids_for_label('Seeds')
wheat = items.storage_item_ids_for_label('Wheat')
self.assertEqual(seeds, items.get_set({
'beetroot', 'beetroot_seeds', 'carrot', 'melon_seeds',
'nether_wart', 'pitcher_pod', 'potato', 'pumpkin_seeds',
'torchflower_seeds', 'wheat_seeds',
}))
self.assertEqual(items.storage_item_ids_for_label('Dirt'), set())
self.assertEqual(items.storage_item_ids_for_label('Stone'), set())
self.assertIn(items.WHEAT_SEEDS_ID, seeds)
self.assertEqual(wheat, {items.WHEAT_ID})
self.assertNotIn(items.WHEAT_SEEDS_ID, wheat)
def test_sign_label_associates_with_adjacent_double_chest(self):
sign_pos = (0, 64, 0)
left_chest = (0, 64, 1)
right_chest = (1, 64, 1)
blocks_by_pos = {left_chest: 5552, right_chest: 5560}
chunks = Munch(
block_entities={sign_pos: {
'type': 7,
'data': {'front_text': {'messages': [
'Seeds', '', '', '',
]}},
}},
index={},
get_block_at=lambda *pos: blocks_by_pos.get(tuple(pos)),
)
chests = World(Munch(chunks=chunks)).find_labeled_chests(
sign_pos, 10)
self.assertEqual(len(chests), 1)
self.assertEqual(chests[0]['positions'], (left_chest, right_chest))
self.assertIn(items.WHEAT_SEEDS_ID, chests[0]['item_ids'])
self.assertEqual(chests[0]['labels'], {'Seeds'})
def test_wheat_chest_does_not_cache_seeds(self):
wheat = Munch(present=True, item_id=items.WHEAT_ID, item_count=64)
seeds = Munch(present=True, item_id=items.WHEAT_SEEDS_ID, item_count=64)
clicks = []
class FakeGame:
@staticmethod
def count_window_slots():
return 0
@staticmethod
def click_window(*args):
clicks.append(args)
cache = CacheItemsStates(Munch(
inv={9: wheat, 10: seeds}, item_lock=False,
window=Munch(container_slots=27, contents={
**dict.fromkeys(range(27)), 27: wheat, 28: seeds,
}),
game=FakeGame(),
))
cache.is_labeled_container = True
cache.container_item_ids = items.storage_item_ids_for_label('Wheat')
cache.move_items()
self.assertEqual(clicks, [(27, 0, 1, wheat)])
if __name__ == '__main__':
unittest.main()