Port nether wart farming to 26.3

This commit is contained in:
Tanner
2026-11-07 17:25:51 +00:00
parent 9251edc36a
commit 95f4aa4821
7 changed files with 237 additions and 57 deletions
+2 -1
View File
@@ -62,7 +62,8 @@ FARMING_PITCH = 45.0
def pitch_lock_for_job(job_state): def pitch_lock_for_job(job_state):
state = getattr(job_state, 'state', None) if job_state else None state = getattr(job_state, 'state', None) if job_state else None
return (FARMING_PITCH if getattr(state, '__name__', None) == 'farm_crop' return (FARMING_PITCH
if getattr(state, '__name__', None) in ('farm_crop', 'farm_wart')
else None) else None)
+1 -1
View File
@@ -73,7 +73,7 @@ class Commands:
command in unsupported or command in unsupported or
(command == 'farm' and (command == 'farm' and
not data.lower().startswith('crop') and not data.lower().startswith('crop') and
data.lower().strip() != 'wood')): data.lower().strip() not in ('wood', 'wart'))):
reply = 'not migrated to Minecraft 26.3 yet' reply = 'not migrated to Minecraft 26.3 yet'
print('Reply:', reply) print('Reply:', reply)
self._send_reply(reply, private, reply_target) self._send_reply(reply, private, reply_target)
+1 -1
View File
@@ -78,7 +78,7 @@ SAPLINGS = sorted(name for name in MCD_BLOCKS
if name.endswith('_sapling') or name.endswith('_propagule')) if name.endswith('_sapling') or name.endswith('_propagule'))
BEDS = sorted(name for name in MCD_BLOCKS if name.endswith('_bed')) BEDS = sorted(name for name in MCD_BLOCKS if name.endswith('_bed'))
INDEXED = ['chest', 'trapped_chest', 'emerald_block', 'barrel'] + BEDS + [ INDEXED = ['chest', 'trapped_chest', 'emerald_block', 'barrel'] + BEDS + [
'wheat', 'potatoes', 'carrots', 'beetroots', 'wheat', 'potatoes', 'carrots', 'beetroots', 'nether_wart',
] + LOGS ] + LOGS
LOG_IDS = get_set(LOGS) LOG_IDS = get_set(LOGS)
+13 -1
View File
@@ -270,11 +270,12 @@ class LegacyJobStates:
class JobStates(LegacyJobStates): class JobStates(LegacyJobStates):
"""26.3 facade for migrated gather-wood and crop workflows.""" """26.3 facade for migrated gathering and farming workflows."""
def __init__(self, global_state): def __init__(self, global_state):
self.g = global_state self.g = global_state
self.gather_wood_states = gather_wood.GatherWoodStates(global_state) self.gather_wood_states = gather_wood.GatherWoodStates(global_state)
self.gather_crop_states = gather_crop.GatherCropStates(global_state) self.gather_crop_states = gather_crop.GatherCropStates(global_state)
self.gather_wart_states = gather_wart.GatherWartStates(global_state)
self.grab_supplies_states = grab_supplies.GrabSuppliesStates( self.grab_supplies_states = grab_supplies.GrabSuppliesStates(
global_state) global_state)
self.clear_leaves_states = clear_leaves.ClearLeavesStates(global_state) self.clear_leaves_states = clear_leaves.ClearLeavesStates(global_state)
@@ -308,6 +309,17 @@ class JobStates(LegacyJobStates):
self.cache_items_states, self.cache_items_states,
] ]
def farm_wart(self):
self.sleep_with_bed_states.silent = True
self.cache_items_states.silent = True
items.set_wanted({items.NETHERWART_ID})
return [
self.gather_wart_states,
self.sleep_with_bed_states,
self.eat_food_states,
self.cache_items_states,
]
def farm_crop(self): def farm_crop(self):
self.cache_items_states.silent = True self.cache_items_states.silent = True
self.sleep_with_bed_states.silent = True self.sleep_with_bed_states.silent = True
+72 -52
View File
@@ -1,52 +1,52 @@
import re
import time import time
import importlib
import random
from itertools import count
from math import floor
from minecraft.networking.types import BlockFace from minecraft.networking.types import BlockFace
from mosfet.protocol.managers import ChunkNotLoadedException
from mosfet import utils
from mosfet import path from mosfet import path
from mosfet import utils
from mosfet.info import blocks from mosfet.info import blocks
from mosfet.info import items from mosfet.info import items
from mosfet.info import mcdata
from mosfet.info import mobs
class GatherWartStates: class GatherWartStates:
BREAK_TIMEOUT = 5.0
ITEM_TIMEOUT = 8.0
PLACE_TIMEOUT = 3.0
def idle(self): def idle(self):
return None return None
def init(self): def init(self):
self.state = self.find_new_wart self.state = self.find_new_wart
def _blacklist_wart(self):
if self.wart is not None and self.wart not in self.bad_warts:
self.bad_warts.append(self.wart)
def find_new_wart(self): def find_new_wart(self):
print('Finding new wart...') print('Finding new wart...')
w = self.g.world world = self.g.world
p = utils.pint(self.g.pos) center = utils.pint(self.g.pos)
mature_wart = max(blocks.NETHERWART_IDS) mature_wart = max(blocks.NETHERWART_IDS)
for wart in w.find_blocks_3d(p, [mature_wart], 50, 20, True): candidates = world.find_blocks_indexed(
if (wart not in self.bad_warts and center, {mature_wart}, 50)
w.work_position_allowed(wart)): candidates = world.filter_work_positions(candidates)
break wart = next((position for position in candidates
else: # for if position not in self.bad_warts), None)
if wart is None:
print('No good warts left, aborting.') print('No good warts left, aborting.')
self.state = self.cleanup self.state = self.cleanup
return return
print('Found wart:', wart) print('Found wart:', wart)
self.wart = wart self.wart = wart
self.type_id = mature_wart
self.state = self.nav_to_wart self.state = self.nav_to_wart
def nav_to_wart(self): def nav_to_wart(self):
w = self.g.world world = self.g.world
p = utils.pint(self.g.pos) position = utils.pint(self.g.pos)
navpath = world.path_to_place(position, self.wart)
navpath = w.path_to_place(p, self.wart)
if navpath: if navpath:
print('Going to wart', self.wart) print('Going to wart', self.wart)
@@ -55,8 +55,7 @@ class GatherWartStates:
self.state = self.going_to_wart self.state = self.going_to_wart
else: else:
print('Cant get to it, blacklisting') print('Cant get to it, blacklisting')
time.sleep(0.1) self._blacklist_wart()
self.bad_warts.append(wart)
self.state = self.find_new_wart self.state = self.find_new_wart
def going_to_wart(self): def going_to_wart(self):
@@ -65,63 +64,84 @@ class GatherWartStates:
self.state = self.break_wart self.state = self.break_wart
def break_wart(self): def break_wart(self):
self.g.game.break_block(self.wart) current = self.g.world.block_at(*self.wart)
self.wait_time = 0.5 if current != self.type_id:
self.state = self.wait print('Wart changed before break; searching again')
self._blacklist_wart()
self.state = self.find_new_wart
return
def wait(self): if self.g.game.break_block(self.wart):
# wait for the item self.break_deadline = time.monotonic() + self.BREAK_TIMEOUT
if self.wait_time > 0: self.state = self.wait_break
self.wait_time -= utils.TICK
else: else:
print('Break request skipped; blacklisting', self.wart)
self._blacklist_wart()
self.state = self.find_new_wart
def wait_break(self):
current = self.g.world.block_at(*self.wart)
if current != self.type_id:
print('Wart break confirmed at {}: {} -> {}'.format(
self.wart, self.type_id, current))
self.item_deadline = time.monotonic() + self.ITEM_TIMEOUT
self.item_wait_logged = False
self.state = self.select_wart self.state = self.select_wart
elif time.monotonic() > self.break_deadline:
print('Wart did not break; blacklisting', self.wart)
self._blacklist_wart()
self.state = self.find_new_wart
def select_wart(self): def select_wart(self):
p = utils.pint(self.g.pos)
if self.g.game.select_item([items.NETHERWART_ID]): if self.g.game.select_item([items.NETHERWART_ID]):
self.state = self.wait_select self.state = self.wait_select
self.wait_time = 0.5 return
else:
print('Aborting planting, no wart') if time.monotonic() > self.item_deadline:
self.state = self.cleanup print('No nether wart collected; skipping', self.wart)
self._blacklist_wart()
self.state = self.find_new_wart
elif not self.item_wait_logged:
print('Waiting for nether wart in inventory/hotbar')
self.item_wait_logged = True
def wait_select(self): def wait_select(self):
# wait a bit to select if self.g.get('holding') == items.NETHERWART_ID:
if self.wait_time > 0:
self.wait_time -= utils.TICK
else:
self.state = self.place_wart self.state = self.place_wart
def place_wart(self): def place_wart(self):
p = utils.pint(self.g.pos) soul_sand = utils.padd(self.wart, path.BLOCK_BELOW)
self.g.game.place_block(p, BlockFace.TOP) self.g.game.place_block(soul_sand, BlockFace.TOP)
print('Placed wart') self.place_deadline = time.monotonic() + self.PLACE_TIMEOUT
self.state = self.wait_place self.state = self.wait_place
self.wait_time = 0.5
def wait_place(self): def wait_place(self):
# wait a bit for chunk data to update current = self.g.world.block_at(*self.wart)
if self.wait_time > 0: if current in blocks.NETHERWART_IDS and current != self.type_id:
self.wait_time -= utils.TICK print('Nether wart replanted at', self.wart)
else:
self.state = self.cleanup self.state = self.cleanup
elif time.monotonic() > self.place_deadline:
print('Nether wart did not replant; blacklisting', self.wart)
self._blacklist_wart()
self.state = self.find_new_wart
def cleanup(self): def cleanup(self):
self.g.look_at = None self.g.look_at = None
self.state = self.done self.state = self.done
def done(self): def done(self):
# never gets ran, placeholder
return None return None
def __init__(self, global_state): def __init__(self, global_state):
self.g = global_state self.g = global_state
self.state = self.idle self.state = self.idle
self.wart = None self.wart = None
self.type_id = None
self.bad_warts = [] self.bad_warts = []
self.wait_time = 0 self.break_deadline = None
self.item_deadline = None
self.place_deadline = None
self.item_wait_logged = False
def run(self): def run(self):
self.state() self.state()
+6 -1
View File
@@ -6,15 +6,20 @@ from mosfet.bot import FARMING_PITCH, pitch_lock_for_job, update_pitch
class TestFarmingPitch(unittest.TestCase): class TestFarmingPitch(unittest.TestCase):
def test_only_crop_farming_locks_pitch(self): def test_crop_and_wart_farming_lock_pitch(self):
def farm_crop(): def farm_crop():
pass pass
def farm_wart():
pass
def sleep(): def sleep():
pass pass
self.assertEqual( self.assertEqual(
pitch_lock_for_job(Munch(state=farm_crop)), FARMING_PITCH) pitch_lock_for_job(Munch(state=farm_crop)), FARMING_PITCH)
self.assertEqual(
pitch_lock_for_job(Munch(state=farm_wart)), FARMING_PITCH)
self.assertIsNone(pitch_lock_for_job(Munch(state=sleep))) self.assertIsNone(pitch_lock_for_job(Munch(state=sleep)))
self.assertIsNone(pitch_lock_for_job(None)) self.assertIsNone(pitch_lock_for_job(None))
+142
View File
@@ -0,0 +1,142 @@
import unittest
from munch import Munch
from mosfet.commands import Commands
from mosfet.info import blocks, items
from mosfet.job import JobStates
from mosfet.jobs.gather_wart import GatherWartStates
class GatherWartTests(unittest.TestCase):
def test_nether_wart_states_are_indexed_for_patch_search(self):
self.assertTrue(blocks.NETHERWART_IDS.issubset(blocks.INDEXED_IDS))
def test_farm_wart_command_is_enabled_and_keeps_job_sequence(self):
old_wanted = items.WANTED_ITEMS
job = JobStates(Munch())
class FakeChat:
def __init__(self):
self.sent = []
def set_handler(self, _handler):
pass
def send(self, message):
self.sent.append(message)
chat = FakeChat()
g = Munch(name='mosfet1', chat=chat, job=job,
inv={0: Munch(item_id=next(iter(items.BED_IDS)))})
try:
Commands(g).handle_chat(('CHAT', 'tester',
'tester: 1farm wart'))
self.assertEqual(chat.sent, ['ok'])
self.assertIs(job.state.__func__, job.farm_wart.__func__)
self.assertIs(job.farm_wart()[0], job.gather_wart_states)
self.assertIn(items.NETHERWART_ID, items.WANTED_ITEMS)
finally:
items.WANTED_ITEMS = old_wanted
def test_finds_only_indexed_mature_wart_allowed_by_work_signs(self):
position = (5, 64, 8)
mature = max(blocks.NETHERWART_IDS)
class FakeWorld:
def __init__(self):
self.search = None
self.filter_input = None
def find_blocks_indexed(self, center, block_ids, distance):
self.search = (center, block_ids, distance)
return [position]
def filter_work_positions(self, positions):
self.filter_input = list(positions)
return list(positions)
world = FakeWorld()
state = GatherWartStates(Munch(world=world, pos=(1.5, 64, 1.5)))
state.find_new_wart()
self.assertEqual(world.search, ((1, 64, 1), {mature}, 50))
self.assertEqual(world.filter_input, [position])
self.assertEqual(state.wart, position)
self.assertIs(state.state.__func__, state.nav_to_wart.__func__)
def test_harvests_confirms_replant_on_soul_sand_below_wart(self):
position = (5, 64, 8)
mature = max(blocks.NETHERWART_IDS)
unripe = min(blocks.NETHERWART_IDS)
class FakeWorld:
def __init__(self):
self.state_id = mature
def block_at(self, *_position):
return self.state_id
class FakeGame:
def __init__(self, global_state):
self.g = global_state
self.break_calls = []
self.place_calls = []
def break_block(self, target):
self.break_calls.append(target)
return True
def select_item(self, item_ids):
self.g.holding = items.NETHERWART_ID
return items.NETHERWART_ID in item_ids
def place_block(self, target, face):
self.place_calls.append((target, face))
world = FakeWorld()
g = Munch(world=world, pos=position, holding=0, look_at=None)
game = FakeGame(g)
g.game = game
state = GatherWartStates(g)
state.wart = position
state.type_id = mature
state.break_wart()
self.assertIs(state.state.__func__, state.wait_break.__func__)
self.assertEqual(game.break_calls, [position])
world.state_id = blocks.AIR
state.wait_break()
self.assertIs(state.state.__func__, state.select_wart.__func__)
state.select_wart()
state.wait_select()
self.assertIs(state.state.__func__, state.place_wart.__func__)
state.place_wart()
support = (position[0], position[1] - 1, position[2])
self.assertEqual(game.place_calls, [(support, 1)])
world.state_id = unripe
state.wait_place()
self.assertIs(state.state.__func__, state.cleanup.__func__)
def test_unreachable_wart_is_blacklisted_without_undefined_variable(self):
position = (5, 64, 8)
class FakeWorld:
@staticmethod
def path_to_place(_start, _target):
return None
state = GatherWartStates(Munch(
world=FakeWorld(), pos=(1, 64, 1), path=[]))
state.wart = position
state.nav_to_wart()
self.assertIn(position, state.bad_warts)
self.assertIs(state.state.__func__, state.find_new_wart.__func__)
if __name__ == '__main__':
unittest.main()