import re import time import importlib import random from itertools import count from math import floor from minecraft.networking.types import BlockFace from mosfet.protocol.managers import ChunkNotLoadedException 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 GatherCropStates: CROPS_PER_BATCH = 16 def idle(self): return None def init(self): self.state = self.find_new_crop def _select_crop(self, candidates, crop_types, center): """Stay with a connected same-species patch, nearest patch first.""" adjacent_offsets = ((1, 0, 0), (-1, 0, 0), (0, 0, 1), (0, 0, -1)) remaining = set(crop_types) component_for = {} component_sizes = {} component_id = 0 while remaining: seed = remaining.pop() species = crop_types[seed] stack = [seed] component = [seed] component_for[seed] = component_id while stack: x, y, z = stack.pop() for dx, dy, dz in adjacent_offsets: adjacent = (x + dx, y + dy, z + dz) if (adjacent in remaining and crop_types[adjacent] == species): remaining.remove(adjacent) component_for[adjacent] = component_id component.append(adjacent) stack.append(adjacent) component_sizes[component_id] = len(component) component_id += 1 eligible = [position for position in candidates if position not in self.bad_crops and position in component_for] if not eligible: return None, 0 active_component = component_for.get(self.active_section_anchor) active_candidates = [position for position in eligible if component_for[position] == active_component] if active_candidates: section_candidates = active_candidates else: # Begin with the nearest available patch. Once selected, keep its # connected component active until no mature crops remain there. first_crop = min(eligible, key=lambda position: ( utils.phyp(center, position), position)) selected_component = component_for[first_crop] section_candidates = [ position for position in eligible if component_for[position] == selected_component ] crop = min(section_candidates, key=lambda position: ( utils.phyp(center, position), position)) self.active_section_anchor = crop return crop, component_sizes[component_for[crop]] def find_new_crop(self): print('Finding new crop...') w = self.g.world p = utils.pint(self.g.pos) mature_crops = [ blocks.MATURE_WHEAT_ID, blocks.MATURE_POTATO_ID, blocks.MATURE_CARROT_ID, blocks.MATURE_BEETROOT_ID, ] candidates = 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_types = { position: blocks.BLOCKS.get(w.block_at(*position)) for position in crop_positions } crop, section_size = self._select_crop(candidates, crop_types, p) if crop is None: print('No good crops left; retrying after a short wait.') self.retry_after = time.monotonic() + 15.0 self.state = self.wait_for_crops return print('Found crop:', crop, '(same-species section size={})'.format(section_size)) self.crop = crop self.type_id = w.block_at(*crop) self.state = self.nav_to_crop def wait_for_crops(self): if time.monotonic() >= self.retry_after: self.state = self.find_new_crop def nav_to_crop(self): w = self.g.world p = utils.pint(self.g.pos) navpath = w.path_to_place(p, self.crop) if navpath: print('Going to crop', self.crop) self.g.path = navpath self.g.look_at = utils.padd(self.crop, path.BLOCK_BELOW) self.state = self.going_to_crop else: print('Cant get to it, blacklisting') time.sleep(0.1) self.bad_crops.append(self.crop) self.state = self.find_new_crop def going_to_crop(self): if utils.pint(self.g.pos) == self.crop: print('At the crop') self.state = self.break_crop def break_crop(self): print('Requesting crop break at {}: block={}, mode={}, held={}'.format( self.crop, self.g.world.block_at(*self.crop), self.g.get('game_mode'), self.g.get('holding'))) if self.g.game.break_block(self.crop): print('Started breaking crop {} (state {})'.format( self.crop, self.type_id)) self.break_deadline = time.time() + 5.0 self.state = self.wait_break else: print('Break request skipped; current block is', self.g.world.block_at(*self.crop)) self.bad_crops.append(self.crop) self.state = self.find_new_crop def wait_break(self): current_state = self.g.world.block_at(*self.crop) if current_state != self.type_id: print('Crop break confirmed at {}: {} -> {}'.format( self.crop, self.type_id, current_state)) self.seed_deadline = time.monotonic() + 8.0 self.seed_wait_logged = False self.state = self.select_seed elif time.time() > self.break_deadline: print('Crop break timed out at {}: expected state {}, server still has {}'.format( self.crop, self.type_id, current_state)) print('Crop did not break; blacklisting', self.crop) self.bad_crops.append(self.crop) self.state = self.find_new_crop def select_seed(self): p = utils.pint(self.g.pos) crop_seeds = { blocks.MATURE_WHEAT_ID: items.WHEAT_SEEDS_ID, blocks.MATURE_POTATO_ID: items.POTATO_ID, blocks.MATURE_CARROT_ID: items.CARROT_ID, blocks.MATURE_BEETROOT_ID: items.BEETROOT_SEEDS_ID, } self.target_seed = crop_seeds[self.type_id] if self.g.game.select_item([self.target_seed]): self.seed_deadline = None self.state = self.wait_select else: if (self.seed_deadline is not None and time.monotonic() > self.seed_deadline): print('No seed collected for crop; skipping', self.crop) self.bad_crops.append(self.crop) self.seed_deadline = None self.state = self.find_new_crop elif not getattr(self, 'seed_wait_logged', False): print('Waiting for crop seed {} in inventory/hotbar'.format( self.target_seed)) self.seed_wait_logged = True return def wait_select(self): if self.target_seed != self.g.holding: return else: self.state = self.place_crop def place_crop(self): p = utils.padd(self.crop, path.BLOCK_BELOW) self.place_deadline = time.time() + 3.0 self.g.game.place_block(p, BlockFace.TOP) print('Placed crop') self.state = self.wait_place def wait_place(self): w = self.g.world current_state = w.block_at(*self.crop) crop_name = blocks.BLOCKS.get(self.type_id) replanted = (current_state not in (None, blocks.AIR) and blocks.BLOCKS.get(current_state) == crop_name and current_state != self.type_id) if replanted: self.completed_crops += 1 if self.completed_crops >= self.CROPS_PER_BATCH: print('Completed {} crop harvests; running maintenance'.format( self.completed_crops)) self.completed_crops = 0 self.state = self.cleanup else: self.state = self.find_new_crop elif time.time() > getattr(self, 'place_deadline', 0): print('Crop did not replant; blacklisting', self.crop) self.bad_crops.append(self.crop) self.state = self.find_new_crop def cleanup(self): self.g.look_at = None self.state = self.done def done(self): # never gets ran, placeholder return None def __init__(self, global_state): self.g = global_state self.state = self.idle self.crop = None self.type_id = None self.target_seed = None self.seed_deadline = None self.seed_wait_logged = False self.active_section_anchor = None self.retry_after = 0.0 self.bad_crops = [] self.completed_crops = 0 def run(self): self.state()