/* * This file is part of Baritone. * * Baritone is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Baritone is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Baritone. If not, see . */ package baritone.behavior; import baritone.Baritone; import baritone.api.behavior.IPathingBehavior; import baritone.api.event.events.PathEvent; import baritone.api.event.events.PlayerUpdateEvent; import baritone.api.event.events.RenderEvent; import baritone.api.event.events.TickEvent; import baritone.api.pathing.calc.IPath; import baritone.api.pathing.calc.IPathFinder; import baritone.api.pathing.goals.Goal; import baritone.api.pathing.goals.GoalXZ; import baritone.api.utils.BetterBlockPos; import baritone.api.utils.interfaces.IGoalRenderPos; import baritone.pathing.calc.AStarPathFinder; import baritone.pathing.calc.AbstractNodeCostSearch; import baritone.pathing.calc.CutoffPath; import baritone.pathing.movement.MovementHelper; import baritone.pathing.path.PathExecutor; import baritone.utils.BlockBreakHelper; import baritone.utils.Helper; import baritone.utils.PathRenderer; import net.minecraft.util.math.BlockPos; import net.minecraft.world.chunk.EmptyChunk; import java.util.*; import java.util.concurrent.LinkedBlockingQueue; import java.util.stream.Collectors; public final class PathingBehavior extends Behavior implements IPathingBehavior, Helper { public static final PathingBehavior INSTANCE = new PathingBehavior(); private PathExecutor current; private PathExecutor next; private Goal goal; private volatile boolean isPathCalcInProgress; private final Object pathCalcLock = new Object(); private final Object pathPlanLock = new Object(); private boolean lastAutoJump; private final LinkedBlockingQueue toDispatch = new LinkedBlockingQueue<>(); private PathingBehavior() {} private void queuePathEvent(PathEvent event) { toDispatch.add(event); } private void dispatchEvents() { ArrayList curr = new ArrayList<>(); toDispatch.drainTo(curr); for (PathEvent event : curr) { Baritone.INSTANCE.getGameEventHandler().onPathEvent(event); } } @Override public void onTick(TickEvent event) { dispatchEvents(); if (event.getType() == TickEvent.Type.OUT) { cancel(); return; } mc.playerController.setPlayerCapabilities(mc.player); tickPath(); dispatchEvents(); } private void tickPath() { if (current == null) { return; } boolean safe = current.onTick(); synchronized (pathPlanLock) { if (current.failed() || current.finished()) { current = null; if (goal == null || goal.isInGoal(playerFeet())) { logDebug("All done. At " + goal); queuePathEvent(PathEvent.AT_GOAL); next = null; return; } if (next != null && !next.getPath().positions().contains(playerFeet())) { // if the current path failed, we may not actually be on the next one, so make sure logDebug("Discarding next path as it does not contain current position"); // for example if we had a nicely planned ahead path that starts where current ends // that's all fine and good // but if we fail in the middle of current // we're nowhere close to our planned ahead path // so need to discard it sadly. queuePathEvent(PathEvent.DISCARD_NEXT); next = null; } if (next != null) { logDebug("Continuing on to planned next path"); queuePathEvent(PathEvent.CONTINUING_ONTO_PLANNED_NEXT); current = next; next = null; current.onTick(); return; } // at this point, current just ended, but we aren't in the goal and have no plan for the future synchronized (pathCalcLock) { if (isPathCalcInProgress) { queuePathEvent(PathEvent.PATH_FINISHED_NEXT_STILL_CALCULATING); // if we aren't calculating right now return; } queuePathEvent(PathEvent.CALC_STARTED); findPathInNewThread(pathStart(), true, Optional.empty()); } return; } // at this point, we know current is in progress if (safe && next != null && next.snipsnapifpossible()) { // a movement just ended; jump directly onto the next path logDebug("Splicing into planned next path early..."); queuePathEvent(PathEvent.SPLICING_ONTO_NEXT_EARLY); current = next; next = null; current.onTick(); return; } synchronized (pathCalcLock) { if (isPathCalcInProgress) { // if we aren't calculating right now return; } if (next != null) { // and we have no plan for what to do next return; } if (goal == null || goal.isInGoal(current.getPath().getDest())) { // and this path dosen't get us all the way there return; } if (ticksRemainingInSegment().get() < Baritone.settings().planningTickLookAhead.get()) { // and this path has 5 seconds or less left logDebug("Path almost over. Planning ahead..."); queuePathEvent(PathEvent.NEXT_SEGMENT_CALC_STARTED); findPathInNewThread(current.getPath().getDest(), false, Optional.of(current.getPath())); } } } } @Override public void onPlayerUpdate(PlayerUpdateEvent event) { if (current != null) { switch (event.getState()) { case PRE: lastAutoJump = mc.gameSettings.autoJump; mc.gameSettings.autoJump = false; break; case POST: mc.gameSettings.autoJump = lastAutoJump; break; default: break; } } } @Override public Optional ticksRemainingInSegment() { if (current == null) { return Optional.empty(); } return Optional.of(current.getPath().ticksRemainingFrom(current.getPosition())); } @Override public void setGoal(Goal goal) { this.goal = goal; } public boolean setGoalAndPath(Goal goal) { setGoal(goal); return path(); } @Override public Goal getGoal() { return goal; } @Override public PathExecutor getCurrent() { return current; } @Override public PathExecutor getNext() { return next; } @Override public Optional getPathFinder() { return Optional.ofNullable(AbstractNodeCostSearch.currentlyRunning()); } @Override public boolean isPathing() { return this.current != null; } @Override public void cancel() { queuePathEvent(PathEvent.CANCELED); current = null; next = null; Baritone.INSTANCE.getInputOverrideHandler().clearAllKeys(); AbstractNodeCostSearch.getCurrentlyRunning().ifPresent(AbstractNodeCostSearch::cancel); BlockBreakHelper.stopBreakingBlock(); } public void forceCancel() { // NOT exposed on public api isPathCalcInProgress = false; } /** * Start calculating a path if we aren't already * * @return true if this call started path calculation, false if it was already calculating or executing a path */ @Override public boolean path() { if (goal == null) { return false; } if (goal.isInGoal(playerFeet())) { return false; } synchronized (pathPlanLock) { if (current != null) { return false; } synchronized (pathCalcLock) { if (isPathCalcInProgress) { return false; } queuePathEvent(PathEvent.CALC_STARTED); findPathInNewThread(pathStart(), true, Optional.empty()); return true; } } } /** * See issue #209 * * @return The starting {@link BlockPos} for a new path */ public BlockPos pathStart() { BetterBlockPos feet = playerFeet(); if (!MovementHelper.canWalkOn(feet.down())) { if (player().onGround) { double playerX = player().posX; double playerZ = player().posZ; ArrayList closest = new ArrayList<>(); for (int dx = -1; dx <= 1; dx++) { for (int dz = -1; dz <= 1; dz++) { closest.add(new BetterBlockPos(feet.x + dx, feet.y, feet.z + dz)); } } closest.sort(Comparator.comparingDouble(pos -> ((pos.x + 0.5D) - playerX) * ((pos.x + 0.5D) - playerX) + ((pos.z + 0.5D) - playerZ) * ((pos.z + 0.5D) - playerZ))); for (int i = 0; i < 4; i++) { BetterBlockPos possibleSupport = closest.get(i); double xDist = Math.abs((possibleSupport.x + 0.5D) - playerX); double zDist = Math.abs((possibleSupport.z + 0.5D) - playerZ); if (xDist > 0.8 && zDist > 0.8) { // can't possibly be sneaking off of this one, we're too far away continue; } if (MovementHelper.canWalkOn(possibleSupport.down()) && MovementHelper.canWalkThrough(possibleSupport) && MovementHelper.canWalkThrough(possibleSupport.up())) { // this is plausible logDebug("Faking path start assuming player is standing off the edge of a block"); return possibleSupport; } } } else { // !onGround // we're in the middle of a jump if (MovementHelper.canWalkOn(feet.down().down())) { logDebug("Faking path start assuming player is midair and falling"); return feet.down(); } } } return feet; } /** * In a new thread, pathfind to target blockpos * * @param start * @param talkAboutIt */ private void findPathInNewThread(final BlockPos start, final boolean talkAboutIt, final Optional previous) { synchronized (pathCalcLock) { if (isPathCalcInProgress) { throw new IllegalStateException("Already doing it"); } isPathCalcInProgress = true; } Baritone.INSTANCE.getExecutor().execute(() -> { if (talkAboutIt) { logDebug("Starting to search for path from " + start + " to " + goal); } Optional path = findPath(start, previous); if (Baritone.settings().cutoffAtLoadBoundary.get()) { path = path.map(p -> { IPath result = p.cutoffAtLoadedChunks(); if (result instanceof CutoffPath) { logDebug("Cutting off path at edge of loaded chunks"); logDebug("Length decreased by " + (p.length() - result.length())); } else { logDebug("Path ends within loaded chunks"); } return result; }); } Optional executor = path.map(p -> { IPath result = p.staticCutoff(goal); if (result instanceof CutoffPath) { logDebug("Static cutoff " + p.length() + " to " + result.length()); } return result; }).map(PathExecutor::new); synchronized (pathPlanLock) { if (current == null) { if (executor.isPresent()) { queuePathEvent(PathEvent.CALC_FINISHED_NOW_EXECUTING); current = executor.get(); } else { queuePathEvent(PathEvent.CALC_FAILED); } } else { if (next == null) { if (executor.isPresent()) { queuePathEvent(PathEvent.NEXT_SEGMENT_CALC_FINISHED); next = executor.get(); } else { queuePathEvent(PathEvent.NEXT_CALC_FAILED); } } else { throw new IllegalStateException("I have no idea what to do with this path"); } } } if (talkAboutIt && current != null && current.getPath() != null) { if (goal == null || goal.isInGoal(current.getPath().getDest())) { logDebug("Finished finding a path from " + start + " to " + goal + ". " + current.getPath().getNumNodesConsidered() + " nodes considered"); } else { logDebug("Found path segment from " + start + " towards " + goal + ". " + current.getPath().getNumNodesConsidered() + " nodes considered"); } } synchronized (pathCalcLock) { isPathCalcInProgress = false; } }); } /** * Actually do the pathing * * @param start * @return */ private Optional findPath(BlockPos start, Optional previous) { Goal goal = this.goal; if (goal == null) { logDebug("no goal"); return Optional.empty(); } if (Baritone.settings().simplifyUnloadedYCoord.get()) { BlockPos pos = null; if (goal instanceof IGoalRenderPos) { pos = ((IGoalRenderPos) goal).getGoalPos(); } if (pos != null && world().getChunk(pos) instanceof EmptyChunk) { logDebug("Simplifying " + goal.getClass() + " to GoalXZ due to distance"); goal = new GoalXZ(pos.getX(), pos.getZ()); } } long timeout; if (current == null) { timeout = Baritone.settings().pathTimeoutMS.get(); } else { timeout = Baritone.settings().planAheadTimeoutMS.get(); } Optional> favoredPositions; if (Baritone.settings().backtrackCostFavoringCoefficient.get() == 1D) { favoredPositions = Optional.empty(); } else { favoredPositions = previous.map(IPath::positions).map(Collection::stream).map(x -> x.map(BetterBlockPos::longHash)).map(x -> x.collect(Collectors.toList())).map(HashSet::new); // <-- okay this is EPIC } try { IPathFinder pf = new AStarPathFinder(start.getX(), start.getY(), start.getZ(), goal, favoredPositions); return pf.calculate(timeout); } catch (Exception e) { logDebug("Pathing exception: " + e); e.printStackTrace(); return Optional.empty(); } } public void revalidateGoal() { if (!Baritone.settings().cancelOnGoalInvalidation.get()) { return; } synchronized (pathPlanLock) { if (current == null || goal == null) { return; } Goal intended = current.getPath().getGoal(); BlockPos end = current.getPath().getDest(); if (intended.isInGoal(end) && !goal.isInGoal(end)) { // this path used to end in the goal // but the goal has changed, so there's no reason to continue... cancel(); } } } @Override public void onRenderPass(RenderEvent event) { PathRenderer.render(event, this); } @Override public void onDisable() { Baritone.INSTANCE.getInputOverrideHandler().clearAllKeys(); } }