Apply sign work zones before candidate routing

This commit is contained in:
Tanner
2026-11-06 01:10:31 +00:00
parent a60d913c60
commit ab8f59a89b
11 changed files with 309 additions and 12 deletions
+8 -1
View File
@@ -82,7 +82,11 @@ class LegacyJobStates:
if m.state == m.idle:
continue
if m.state != m.done:
m.run()
self.active_machine = m
try:
m.run()
finally:
self.active_machine = None
return
# if we went through them all
for m in machines:
@@ -256,6 +260,7 @@ class LegacyJobStates:
def __init__(self, global_state):
self.g = global_state
self.active_machine = None
self.init_machines()
self.state = self.idle
@@ -281,6 +286,7 @@ class JobStates(LegacyJobStates):
self.eat_food_states = eat_food.EatFoodStates(global_state)
self.check_threats_states = check_threats.CheckThreatsStates(
global_state)
self.active_machine = None
self.state = self.idle
def farm_wood(self):
@@ -356,4 +362,5 @@ class JobStates(LegacyJobStates):
self.check_threats_states.external_safety = False
self.gather_crop_states.active_section_anchor = None
self.gather_crop_states.completed_crops = 0
self.active_machine = None
self.state = self.idle
+4 -2
View File
@@ -92,10 +92,12 @@ class GatherCropStates:
blocks.MATURE_BEETROOT_ID,
]
candidates = w.find_blocks_indexed(p, mature_crops, 50)
candidates = w.filter_work_positions(
w.find_blocks_indexed(p, mature_crops, 50))
crop_ids = (blocks.WHEAT_IDS | blocks.POTATO_IDS |
blocks.CARROT_IDS | blocks.BEETROOT_IDS)
crop_positions = w.find_blocks_indexed(p, crop_ids, 50)
crop_positions = w.filter_work_positions(
w.find_blocks_indexed(p, crop_ids, 50))
crop_types = {
position: blocks.BLOCKS.get(w.block_at(*position))
for position in crop_positions
+4 -3
View File
@@ -61,12 +61,13 @@ class GatherSandStates:
head = utils.padd(p, path.BLOCK_ABOVE)
for sand in w.find_sand(self.slice, 2, p):
if sand not in self.bad_sand:
if (sand not in self.bad_sand and
w.work_position_allowed(sand)):
print('Found sand:', sand)
break
else: # for
print('No good sands left, aborting.')
self.state = self.cleanup
print('No in-bounds sand left in this slice; trying another.')
self.state = self.find_new_slice
return
self.sand = sand
+2 -1
View File
@@ -30,7 +30,8 @@ class GatherWartStates:
mature_wart = max(blocks.NETHERWART_IDS)
for wart in w.find_blocks_3d(p, [mature_wart], 50, 20, True):
if wart not in self.bad_warts:
if (wart not in self.bad_warts and
w.work_position_allowed(wart)):
break
else: # for
print('No good warts left, aborting.')
+7 -3
View File
@@ -30,10 +30,11 @@ class GatherWoodStates:
w = self.g.world
p = utils.pint(self.g.pos)
for tree in w.find_trees(p, 100):
print('Found tree:', tree)
trees = w.filter_work_positions(w.find_trees(p, 100))
for tree in trees:
if tree in self.bad_trees:
continue
print('Found tree:', tree)
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')
@@ -53,7 +54,10 @@ class GatherWoodStates:
def find_openings(self):
w = self.g.world
self.openings = w.find_tree_openings(self.tree)
self.openings = [
opening for opening in w.find_tree_openings(self.tree)
if w.work_position_allowed(opening)
]
self.state = self.choose_opening
def choose_opening(self):
+2
View File
@@ -44,6 +44,8 @@ class GrabSaplingStates:
if s.entity_id in self.eid_blacklist:
continue
if not w.work_position_allowed(s_pos):
continue
# skip if the sapling is floating
if self.g.chunks.get_block_at(*check) in blocks.LEAF_IDS | {0}:
+2
View File
@@ -80,6 +80,8 @@ class PlantTreeStates:
p = utils.pint(self.g.pos)
for opening in w.find_tree_openings(p)[::-1]:
if not w.work_position_allowed(opening):
continue
print('trying sapling opening', opening)
navpath = w.path_to_place(p, opening)
if navpath:
+121 -2
View File
@@ -76,6 +76,113 @@ class World:
result.sort(key=lambda sign: utils.phyp(center, sign['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()
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)
center = utils.pint(self.g.pos)
policies = []
active_sign_position = None
for sign in self.find_signs(center, distance):
lines = [line.strip() for line in sign['lines'] if line.strip()]
if not lines:
continue
first = lines[0].split(maxsplit=1)
address = first[0].rstrip(':,').casefold()
if address not in addresses:
continue
directives = ([first[1]] if len(first) > 1 else []) + lines[1:]
text = ' '.join(directives).casefold()
sign_policies = []
for match in re.finditer(
r'\bstay\s+within\s*(\d+)(?:\s+(squares?|circle))?\b',
text):
shape = match.group(2) or 'circle'
if shape == 'squares':
shape = 'square'
sign_policies.append({
'position': sign['position'],
'distance': int(match.group(1)),
'shape': shape,
'side': sign['side'],
})
if not sign_policies:
continue
if active_sign_position is None:
# The nearest addressed policy sign overrides farther signs.
active_sign_position = sign['position']
if sign['position'] == active_sign_position:
policies.extend(sign_policies)
return policies
def _active_work_policies(self):
job = self.g.get('job')
machine = getattr(job, 'active_machine', None) if job else None
if machine is None:
return []
support_machines = (
'grab_supplies_states', 'sleep_with_bed_states',
'eat_food_states', 'cache_items_states',
'check_threats_states',
)
if any(machine is getattr(job, name, None)
for name in support_machines):
return []
return self.sign_policies(self.g.get('name', ''))
@staticmethod
def _sign_policy_distance(position, policy):
# Work zones are horizontal; crop height or terrain elevation does not
# consume the sign's radius.
dx = abs(position[0] - policy['position'][0])
dz = abs(position[2] - policy['position'][2])
if policy['shape'] == 'square':
return max(dx, dz)
return utils.hypot(dx, dz)
@classmethod
def _inside_sign_policy(cls, position, policy):
return cls._sign_policy_distance(position, policy) <= policy['distance']
def work_position_allowed(self, position):
"""Whether a work target satisfies all currently active sign rules."""
return all(self._inside_sign_policy(position, policy)
for policy in self._active_work_policies())
def filter_work_positions(self, positions):
"""Filter a candidate set once against the active sign policies."""
policies = self._active_work_policies()
positions = list(positions)
if not policies:
return positions
return [position for position in positions
if all(self._inside_sign_policy(position, policy)
for policy in policies)]
@classmethod
def _path_allowed_by_sign(cls, points, policy):
distances = [cls._sign_policy_distance(point, policy)
for point in points]
radius = policy['distance']
if distances[0] <= radius:
return all(distance <= radius for distance in distances)
# If the bot starts just outside a newly read boundary, let it return
# without allowing it to travel farther away from that sign.
start_distance = distances[0]
entered = False
for distance in distances[1:]:
if distance > start_distance or (entered and distance > radius):
return False
if distance <= radius:
entered = True
return entered
def check_air_column(self, pos, distance):
for i in range(distance):
check = utils.padd(pos, (0, i, 0))
@@ -194,11 +301,23 @@ class World:
maze_solver = path.Pathfinder(self.g)
try:
s = maze_solver.astar(start, place)
return list(s) if s else None
solution = maze_solver.astar(start, place)
if not solution:
return None
navpath = list(solution)
except path.AStarTimeout:
return None
policies = self._active_work_policies()
points = [tuple(start)] + [tuple(point) for point in navpath]
for policy in policies:
if not self._path_allowed_by_sign(points, policy):
print('[sign policy] rejected work path outside {}-block {} '
'around {}'.format(policy['distance'], policy['shape'],
policy['position']), flush=True)
return None
return navpath
def path_to_place_faked(self, start, place):
# same as above, but adds a fake block below and air before pathfinding
# so that the pathfinder can actually make it to the block