/* * 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.pathing.path; import baritone.Baritone; import baritone.api.pathing.calc.IPath; import baritone.api.pathing.movement.ActionCosts; import baritone.api.pathing.movement.IMovement; import baritone.api.pathing.movement.MovementStatus; import baritone.api.pathing.path.IPathExecutor; import baritone.api.utils.*; import baritone.api.utils.input.Input; import baritone.behavior.PathingBehavior; import baritone.pathing.calc.AbstractNodeCostSearch; import baritone.pathing.movement.CalculationContext; import baritone.pathing.movement.Movement; import baritone.pathing.movement.MovementHelper; import baritone.pathing.movement.movements.*; import baritone.utils.BlockStateInterface; import net.minecraft.core.BlockPos; import net.minecraft.core.Vec3i; import net.minecraft.util.Tuple; import net.minecraft.world.phys.Vec3; import java.util.*; import static baritone.api.pathing.movement.MovementStatus.*; /** * Behavior to execute a precomputed path * * @author leijurv */ public class PathExecutor implements IPathExecutor, Helper { private static final double MAX_MAX_DIST_FROM_PATH = 3; private static final double MAX_DIST_FROM_PATH = 2; /** * Default value is equal to 10 seconds. It's find to decrease it, but it must be at least 5.5s (110 ticks). * For more information, see issue #102. * * @see Issue #102 * @see Anime */ private static final double MAX_TICKS_AWAY = 200; private final IPath path; private int pathPosition; private int ticksAway; private int ticksOnCurrent; private Double currentMovementOriginalCostEstimate; private Integer costEstimateIndex; private boolean failed; private boolean recalcBP = true; private HashSet toBreak = new HashSet<>(); private HashSet toPlace = new HashSet<>(); private HashSet toWalkInto = new HashSet<>(); private final PathingBehavior behavior; private final IPlayerContext ctx; private boolean sprintNextTick; public PathExecutor(PathingBehavior behavior, IPath path) { this.behavior = behavior; this.ctx = behavior.ctx; this.path = path; this.pathPosition = 0; } /** * Tick this executor * * @return True if a movement just finished (and the player is therefore in a "stable" state, like, * not sneaking out over lava), false otherwise */ public boolean onTick() { if (pathPosition == path.length() - 1) { pathPosition++; } if (pathPosition >= path.length()) { return true; // stop bugging me, I'm done } Movement movement = (Movement) path.movements().get(pathPosition); BetterBlockPos whereAmI = ctx.playerFeet(); if (!movement.getValidPositions().contains(whereAmI)) { for (int i = 0; i < pathPosition && i < path.length(); i++) {//this happens for example when you lag out and get teleported back a couple blocks if (((Movement) path.movements().get(i)).getValidPositions().contains(whereAmI)) { int previousPos = pathPosition; pathPosition = i; for (int j = pathPosition; j <= previousPos; j++) { path.movements().get(j).reset(); } onChangeInPathPosition(); onTick(); return false; } } for (int i = pathPosition + 3; i < path.length() - 1; i++) { //dont check pathPosition+1. the movement tells us when it's done (e.g. sneak placing) // also don't check pathPosition+2 because reasons if (((Movement) path.movements().get(i)).getValidPositions().contains(whereAmI)) { if (i - pathPosition > 2) { logDebug("Skipping forward " + (i - pathPosition) + " steps, to " + i); } //System.out.println("Double skip sundae"); pathPosition = i - 1; onChangeInPathPosition(); onTick(); return false; } } } Tuple status = closestPathPos(path); if (possiblyOffPath(status, MAX_DIST_FROM_PATH)) { ticksAway++; System.out.println("FAR AWAY FROM PATH FOR " + ticksAway + " TICKS. Current distance: " + status.getA() + ". Threshold: " + MAX_DIST_FROM_PATH); if (ticksAway > MAX_TICKS_AWAY) { logDebug("Too far away from path for too long, cancelling path"); cancel(); return false; } } else { ticksAway = 0; } if (possiblyOffPath(status, MAX_MAX_DIST_FROM_PATH)) { // ok, stop right away, we're way too far. logDebug("too far from path"); cancel(); return false; } //long start = System.nanoTime() / 1000000L; BlockStateInterface bsi = new BlockStateInterface(ctx); for (int i = pathPosition - 10; i < pathPosition + 10; i++) { if (i < 0 || i >= path.movements().size()) { continue; } Movement m = (Movement) path.movements().get(i); List prevBreak = m.toBreak(bsi); List prevPlace = m.toPlace(bsi); List prevWalkInto = m.toWalkInto(bsi); m.resetBlockCache(); if (!prevBreak.equals(m.toBreak(bsi))) { recalcBP = true; } if (!prevPlace.equals(m.toPlace(bsi))) { recalcBP = true; } if (!prevWalkInto.equals(m.toWalkInto(bsi))) { recalcBP = true; } } if (recalcBP) { HashSet newBreak = new HashSet<>(); HashSet newPlace = new HashSet<>(); HashSet newWalkInto = new HashSet<>(); for (int i = pathPosition; i < path.movements().size(); i++) { Movement m = (Movement) path.movements().get(i); newBreak.addAll(m.toBreak(bsi)); newPlace.addAll(m.toPlace(bsi)); newWalkInto.addAll(m.toWalkInto(bsi)); } toBreak = newBreak; toPlace = newPlace; toWalkInto = newWalkInto; recalcBP = false; } /*long end = System.nanoTime() / 1000000L; if (end - start > 0) { System.out.println("Recalculating break and place took " + (end - start) + "ms"); }*/ if (pathPosition < path.movements().size() - 1) { IMovement next = path.movements().get(pathPosition + 1); if (!behavior.baritone.bsi.worldContainsLoadedChunk(next.getDest().x, next.getDest().z)) { logDebug("Pausing since destination is at edge of loaded chunks"); clearKeys(); return true; } } boolean canCancel = movement.safeToCancel(); if (costEstimateIndex == null || costEstimateIndex != pathPosition) { costEstimateIndex = pathPosition; // do this only once, when the movement starts, and deliberately get the cost as cached when this path was calculated, not the cost as it is right now currentMovementOriginalCostEstimate = movement.getCost(); for (int i = 1; i < Baritone.settings().costVerificationLookahead.value && pathPosition + i < path.length() - 1; i++) { if (((Movement) path.movements().get(pathPosition + i)).calculateCost(behavior.secretInternalGetCalculationContext()) >= ActionCosts.COST_INF && canCancel) { logDebug("Something has changed in the world and a future movement has become impossible. Cancelling."); cancel(); return true; } } } double currentCost = movement.recalculateCost(behavior.secretInternalGetCalculationContext()); if (currentCost >= ActionCosts.COST_INF && canCancel) { logDebug("Something has changed in the world and this movement has become impossible. Cancelling."); cancel(); return true; } if (!movement.calculatedWhileLoaded() && currentCost - currentMovementOriginalCostEstimate > Baritone.settings().maxCostIncrease.value && canCancel) { // don't do this if the movement was calculated while loaded // that means that this isn't a cache error, it's just part of the path interfering with a later part logDebug("Original cost " + currentMovementOriginalCostEstimate + " current cost " + currentCost + ". Cancelling."); cancel(); return true; } if (shouldPause()) { logDebug("Pausing since current best path is a backtrack"); clearKeys(); return true; } MovementStatus movementStatus = movement.update(); if (movementStatus == UNREACHABLE || movementStatus == FAILED) { logDebug("Movement returns status " + movementStatus); cancel(); return true; } if (movementStatus == SUCCESS) { //System.out.println("Movement done, next path"); pathPosition++; onChangeInPathPosition(); onTick(); return true; } else { sprintNextTick = shouldSprintNextTick(); if (!sprintNextTick) { ctx.player().setSprinting(false); // letting go of control doesn't make you stop sprinting actually } ticksOnCurrent++; if (ticksOnCurrent > currentMovementOriginalCostEstimate + Baritone.settings().movementTimeoutTicks.value) { // only cancel if the total time has exceeded the initial estimate // as you break the blocks required, the remaining cost goes down, to the point where // ticksOnCurrent is greater than recalculateCost + 100 // this is why we cache cost at the beginning, and don't recalculate for this comparison every tick logDebug("This movement has taken too long (" + ticksOnCurrent + " ticks, expected " + currentMovementOriginalCostEstimate + "). Cancelling."); cancel(); return true; } } return canCancel; // movement is in progress, but if it reports cancellable, PathingBehavior is good to cut onto the next path } private Tuple closestPathPos(IPath path) { double best = -1; BlockPos bestPos = null; for (IMovement movement : path.movements()) { for (BlockPos pos : ((Movement) movement).getValidPositions()) { double dist = VecUtils.entityDistanceToCenter(ctx.player(), pos); if (dist < best || best == -1) { best = dist; bestPos = pos; } } } return new Tuple<>(best, bestPos); } private boolean shouldPause() { Optional current = behavior.getInProgress(); if (!current.isPresent()) { return false; } if (!ctx.player().isOnGround()) { return false; } if (!MovementHelper.canWalkOn(ctx, ctx.playerFeet().below())) { // we're in some kind of sketchy situation, maybe parkouring return false; } if (!MovementHelper.canWalkThrough(ctx, ctx.playerFeet()) || !MovementHelper.canWalkThrough(ctx, ctx.playerFeet().above())) { // suffocating? return false; } if (!path.movements().get(pathPosition).safeToCancel()) { return false; } Optional currentBest = current.get().bestPathSoFar(); if (!currentBest.isPresent()) { return false; } List positions = currentBest.get().positions(); if (positions.size() < 3) { return false; // not long enough yet to justify pausing, its far from certain we'll actually take this route } // the first block of the next path will always overlap // no need to pause our very last movement when it would have otherwise cleanly exited with MovementStatus SUCCESS positions = positions.subList(1, positions.size()); return positions.contains(ctx.playerFeet()); } private boolean possiblyOffPath(Tuple status, double leniency) { double distanceFromPath = status.getA(); if (distanceFromPath > leniency) { // when we're midair in the middle of a fall, we're very far from both the beginning and the end, but we aren't actually off path if (path.movements().get(pathPosition) instanceof MovementFall) { BlockPos fallDest = path.positions().get(pathPosition + 1); // .get(pathPosition) is the block we fell off of return VecUtils.entityFlatDistanceToCenter(ctx.player(), fallDest) >= leniency; // ignore Y by using flat distance } else { return true; } } else { return false; } } /** * Regardless of current path position, snap to the current player feet if possible * * @return Whether or not it was possible to snap to the current player feet */ public boolean snipsnapifpossible() { if (!ctx.player().isOnGround() && ctx.world().getFluidState(ctx.playerFeet()).isEmpty()) { // if we're falling in the air, and not in water, don't splice return false; } else { // we are either onGround or in liquid if (ctx.player().getDeltaMovement().y < -0.1) { // if we are strictly moving downwards (not stationary) // we could be falling through water, which could be unsafe to splice return false; // so don't } } int index = path.positions().indexOf(ctx.playerFeet()); if (index == -1) { return false; } pathPosition = index; // jump directly to current position clearKeys(); return true; } private boolean shouldSprintNextTick() { boolean requested = behavior.baritone.getInputOverrideHandler().isInputForcedDown(Input.SPRINT); // we'll take it from here, no need for minecraft to see we're holding down control and sprint for us behavior.baritone.getInputOverrideHandler().setInputForceState(Input.SPRINT, false); // first and foremost, if allowSprint is off, or if we don't have enough hunger, don't try and sprint if (!new CalculationContext(behavior.baritone, false).canSprint) { return false; } IMovement current = path.movements().get(pathPosition); // traverse requests sprinting, so we need to do this check first if (current instanceof MovementTraverse && pathPosition < path.length() - 3) { IMovement next = path.movements().get(pathPosition + 1); if (next instanceof MovementAscend && sprintableAscend(ctx, (MovementTraverse) current, (MovementAscend) next, path.movements().get(pathPosition + 2))) { if (skipNow(ctx, current)) { logDebug("Skipping traverse to straight ascend"); pathPosition++; onChangeInPathPosition(); onTick(); behavior.baritone.getInputOverrideHandler().setInputForceState(Input.JUMP, true); return true; } else { logDebug("Too far to the side to safely sprint ascend"); } } } // if the movement requested sprinting, then we're done if (requested) { return true; } // however, descend and ascend don't request sprinting, because they don't know the context of what movement comes after it if (current instanceof MovementDescend) { if (pathPosition < path.length() - 2) { // keep this out of onTick, even if that means a tick of delay before it has an effect IMovement next = path.movements().get(pathPosition + 1); if (MovementHelper.canUseFrostWalker(ctx, next.getDest().below())) { // frostwalker only works if you cross the edge of the block on ground so in some cases we may not overshoot // Since MovementDescend can't know the next movement we have to tell it if (next instanceof MovementTraverse || next instanceof MovementParkour) { boolean couldPlaceInstead = Baritone.settings().allowPlace.value && behavior.baritone.getInventoryBehavior().hasGenericThrowaway() && next instanceof MovementParkour; // traverse doesn't react fast enough // this is true if the next movement does not ascend or descends and goes into the same cardinal direction (N-NE-E-SE-S-SW-W-NW) as the descend // in that case current.getDirection() is e.g. (0, -1, 1) and next.getDirection() is e.g. (0, 0, 3) so the cross product of (0, 0, 1) and (0, 0, 3) is taken, which is (0, 0, 0) because the vectors are colinear (don't form a plane) // since movements in exactly the opposite direction (e.g. descend (0, -1, 1) and traverse (0, 0, -1)) would also pass this check we also have to rule out that case // we can do that by adding the directions because traverse is always 1 long like descend and parkour can't jump through current.getSrc().down() boolean sameFlatDirection = !current.getDirection().above().offset(next.getDirection()).equals(BlockPos.ZERO) && current.getDirection().above().cross(next.getDirection()).equals(BlockPos.ZERO); // here's why you learn maths in school if (sameFlatDirection && !couldPlaceInstead) { ((MovementDescend) current).forceSafeMode(); } } } } if (((MovementDescend) current).safeMode() && !((MovementDescend) current).skipToAscend()) { logDebug("Sprinting would be unsafe"); return false; } if (pathPosition < path.length() - 2) { IMovement next = path.movements().get(pathPosition + 1); if (next instanceof MovementAscend && current.getDirection().above().equals(next.getDirection().below())) { // a descend then an ascend in the same direction pathPosition++; onChangeInPathPosition(); onTick(); // okay to skip clearKeys and / or onChangeInPathPosition here since this isn't possible to repeat, since it's asymmetric logDebug("Skipping descend to straight ascend"); return true; } if (canSprintFromDescendInto(ctx, current, next)) { if (next instanceof MovementDescend && pathPosition < path.length() - 3) { IMovement next_next = path.movements().get(pathPosition + 2); if (next_next instanceof MovementDescend && !canSprintFromDescendInto(ctx, next, next_next)) { return false; } } if (ctx.playerFeet().equals(current.getDest())) { pathPosition++; onChangeInPathPosition(); onTick(); } return true; } //logDebug("Turning off sprinting " + movement + " " + next + " " + movement.getDirection() + " " + next.getDirection().down() + " " + next.getDirection().down().equals(movement.getDirection())); } } if (current instanceof MovementAscend && pathPosition != 0) { IMovement prev = path.movements().get(pathPosition - 1); if (prev instanceof MovementDescend && prev.getDirection().above().equals(current.getDirection().below())) { BlockPos center = current.getSrc().above(); // playerFeet adds 0.1251 to account for soul sand // farmland is 0.9375 // 0.07 is to account for farmland if (ctx.player().position().y >= center.getY() - 0.07) { behavior.baritone.getInputOverrideHandler().setInputForceState(Input.JUMP, false); return true; } } if (pathPosition < path.length() - 2 && prev instanceof MovementTraverse && sprintableAscend(ctx, (MovementTraverse) prev, (MovementAscend) current, path.movements().get(pathPosition + 1))) { return true; } } if (current instanceof MovementFall) { Tuple data = overrideFall((MovementFall) current); if (data != null) { BetterBlockPos fallDest = new BetterBlockPos(data.getB()); if (!path.positions().contains(fallDest)) { throw new IllegalStateException(); } if (ctx.playerFeet().equals(fallDest)) { pathPosition = path.positions().indexOf(fallDest); onChangeInPathPosition(); onTick(); return true; } clearKeys(); behavior.baritone.getLookBehavior().updateTarget(RotationUtils.calcRotationFromVec3d(ctx.playerHead(), data.getA(), ctx.playerRotations()), false); behavior.baritone.getInputOverrideHandler().setInputForceState(Input.MOVE_FORWARD, true); return true; } } return false; } private Tuple overrideFall(MovementFall movement) { Vec3i dir = movement.getDirection(); if (dir.getY() < -3) { return null; } if (!movement.toBreakCached.isEmpty()) { return null; // it's breaking } Vec3i flatDir = new Vec3i(dir.getX(), 0, dir.getZ()); int i; outer: for (i = pathPosition + 1; i < path.length() - 1 && i < pathPosition + 3; i++) { IMovement next = path.movements().get(i); if (!(next instanceof MovementTraverse)) { break; } if (!flatDir.equals(next.getDirection())) { break; } for (int y = next.getDest().y; y <= movement.getSrc().y + 1; y++) { BlockPos chk = new BlockPos(next.getDest().x, y, next.getDest().z); if (!MovementHelper.fullyPassable(ctx, chk)) { break outer; } } if (!MovementHelper.canWalkOn(ctx, next.getDest().below())) { break; } } i--; if (i == pathPosition) { return null; // no valid extension exists } double len = i - pathPosition - 0.4; return new Tuple<>( new Vec3(flatDir.getX() * len + movement.getDest().x + 0.5, movement.getDest().y, flatDir.getZ() * len + movement.getDest().z + 0.5), movement.getDest().offset(flatDir.getX() * (i - pathPosition), 0, flatDir.getZ() * (i - pathPosition))); } private static boolean skipNow(IPlayerContext ctx, IMovement current) { double offTarget = Math.abs(current.getDirection().getX() * (current.getSrc().z + 0.5D - ctx.player().position().z)) + Math.abs(current.getDirection().getZ() * (current.getSrc().x + 0.5D - ctx.player().position().x)); if (offTarget > 0.1) { return false; } // we are centered BlockPos headBonk = current.getSrc().subtract(current.getDirection()).above(2); if (MovementHelper.fullyPassable(ctx, headBonk)) { return true; } // wait 0.3 double flatDist = Math.abs(current.getDirection().getX() * (headBonk.getX() + 0.5D - ctx.player().position().x)) + Math.abs(current.getDirection().getZ() * (headBonk.getZ() + 0.5 - ctx.player().position().z)); return flatDist > 0.8; } private static boolean sprintableAscend(IPlayerContext ctx, MovementTraverse current, MovementAscend next, IMovement nextnext) { if (!Baritone.settings().sprintAscends.value) { return false; } if (!current.getDirection().equals(next.getDirection().below())) { return false; } if (nextnext.getDirection().getX() != next.getDirection().getX() || nextnext.getDirection().getZ() != next.getDirection().getZ()) { return false; } if (!MovementHelper.canWalkOn(ctx, current.getDest().below())) { return false; } if (!MovementHelper.canWalkOn(ctx, next.getDest().below())) { return false; } if (!next.toBreakCached.isEmpty()) { return false; // it's breaking } for (int x = 0; x < 2; x++) { for (int y = 0; y < 3; y++) { BlockPos chk = current.getSrc().above(y); if (x == 1) { chk = chk.offset(current.getDirection()); } if (!MovementHelper.fullyPassable(ctx, chk)) { return false; } } } if (MovementHelper.avoidWalkingInto(ctx.world().getBlockState(current.getSrc().above(3)))) { return false; } return !MovementHelper.avoidWalkingInto(ctx.world().getBlockState(next.getDest().above(2))); // codacy smh my head } private static boolean canSprintFromDescendInto(IPlayerContext ctx, IMovement current, IMovement next) { if (next instanceof MovementDescend && next.getDirection().equals(current.getDirection())) { return true; } if (!MovementHelper.canWalkOn(ctx, current.getDest().offset(current.getDirection()))) { return false; } if (next instanceof MovementTraverse && next.getDirection().equals(current.getDirection())) { return true; } return next instanceof MovementDiagonal && Baritone.settings().allowOvershootDiagonalDescend.value; } private void onChangeInPathPosition() { clearKeys(); ticksOnCurrent = 0; } private void clearKeys() { // i'm just sick and tired of this snippet being everywhere lol behavior.baritone.getInputOverrideHandler().clearAllKeys(); } private void cancel() { clearKeys(); behavior.baritone.getInputOverrideHandler().getBlockBreakHelper().stopBreakingBlock(); pathPosition = path.length() + 3; failed = true; } @Override public int getPosition() { return pathPosition; } public PathExecutor trySplice(PathExecutor next) { if (next == null) { return cutIfTooLong(); } return SplicedPath.trySplice(path, next.path, false).map(path -> { if (!path.getDest().equals(next.getPath().getDest())) { throw new IllegalStateException(); } PathExecutor ret = new PathExecutor(behavior, path); ret.pathPosition = pathPosition; ret.currentMovementOriginalCostEstimate = currentMovementOriginalCostEstimate; ret.costEstimateIndex = costEstimateIndex; ret.ticksOnCurrent = ticksOnCurrent; return ret; }).orElseGet(this::cutIfTooLong); // dont actually call cutIfTooLong every tick if we won't actually use it, use a method reference } private PathExecutor cutIfTooLong() { if (pathPosition > Baritone.settings().maxPathHistoryLength.value) { int cutoffAmt = Baritone.settings().pathHistoryCutoffAmount.value; CutoffPath newPath = new CutoffPath(path, cutoffAmt, path.length() - 1); if (!newPath.getDest().equals(path.getDest())) { throw new IllegalStateException(); } logDebug("Discarding earliest segment movements, length cut from " + path.length() + " to " + newPath.length()); PathExecutor ret = new PathExecutor(behavior, newPath); ret.pathPosition = pathPosition - cutoffAmt; ret.currentMovementOriginalCostEstimate = currentMovementOriginalCostEstimate; if (costEstimateIndex != null) { ret.costEstimateIndex = costEstimateIndex - cutoffAmt; } ret.ticksOnCurrent = ticksOnCurrent; return ret; } return this; } @Override public IPath getPath() { return path; } public boolean failed() { return failed; } public boolean finished() { return pathPosition >= path.length(); } public Set toBreak() { return Collections.unmodifiableSet(toBreak); } public Set toPlace() { return Collections.unmodifiableSet(toPlace); } public Set toWalkInto() { return Collections.unmodifiableSet(toWalkInto); } public boolean isSprinting() { return sprintNextTick; } }