From f5dd5a1c53800fe6d2a9070359e8e0cf27471251 Mon Sep 17 00:00:00 2001 From: Niroxbtw <85623921+Niroxbtw@users.noreply.github.com> Date: Sat, 16 Dec 2023 15:07:56 +0100 Subject: [PATCH] new pathingbehavior --- .../baritone/behavior/PathingBehavior.java | 573 +++++++++++++----- 1 file changed, 423 insertions(+), 150 deletions(-) diff --git a/src/main/java/baritone/behavior/PathingBehavior.java b/src/main/java/baritone/behavior/PathingBehavior.java index f7cf555..04474aa 100644 --- a/src/main/java/baritone/behavior/PathingBehavior.java +++ b/src/main/java/baritone/behavior/PathingBehavior.java @@ -2,55 +2,270 @@ import baritone.Baritone; import baritone.api.behavior.IPathingBehavior; -import baritone.api.event.events.RenderEvent; -import baritone.api.event.events.SprintStateEvent; -import baritone.api.event.events.TickEvent; +import baritone.api.event.events.*; import baritone.api.pathing.calc.IPath; import baritone.api.pathing.goals.Goal; import baritone.api.pathing.goals.GoalXZ; +import baritone.api.process.PathingCommand; import baritone.api.utils.BetterBlockPos; import baritone.api.utils.Helper; import baritone.api.utils.PathCalculationResult; import baritone.api.utils.interfaces.IGoalRenderPos; import baritone.pathing.calc.AStarPathFinder; import baritone.pathing.calc.AbstractNodeCostSearch; -import baritone.pathing.calc.FlyAStar; import baritone.pathing.movement.CalculationContext; +import baritone.pathing.movement.MovementHelper; import baritone.pathing.path.PathExecutor; import baritone.utils.PathRenderer; +import baritone.utils.PathingCommandContext; import net.minecraft.util.BlockPos; +import java.util.ArrayList; +import java.util.Comparator; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.LinkedBlockingQueue; -public class PathingBehavior extends Behavior implements IPathingBehavior, Helper { - - private AStarPathFinder inProgress; +public final class PathingBehavior extends Behavior implements IPathingBehavior, Helper { private PathExecutor current; - private PathExecutor next; private Goal goal; private CalculationContext context; + /*eta*/ + private int ticksElapsedSoFar; + private BetterBlockPos startPosition; + + private boolean safeToCancel; + private boolean pauseRequestedLastTick; + private boolean unpausedLastTick; + private boolean pausedThisTick; + private boolean cancelRequested; + private boolean calcFailedLastTick; + + private volatile AbstractNodeCostSearch inProgress; private final Object pathCalcLock = new Object(); private final Object pathPlanLock = new Object(); - private boolean safeToCancel; + private boolean lastAutoJump; private BetterBlockPos expectedSegmentStart; + private final LinkedBlockingQueue toDispatch = new LinkedBlockingQueue<>(); + public PathingBehavior(Baritone baritone) { super(baritone); } + private void queuePathEvent(PathEvent event) { + toDispatch.add(event); + } + + private void dispatchEvents() { + ArrayList curr = new ArrayList<>(); + toDispatch.drainTo(curr); + calcFailedLastTick = curr.contains(PathEvent.CALC_FAILED); + for (PathEvent event : curr) { + baritone.getGameEventHandler().onPathEvent(event); + } + } + @Override - public void setGoal(Goal goal) { + public void onTick(TickEvent event) { + dispatchEvents(); + if (event.getType() == TickEvent.Type.OUT) { + secretInternalSegmentCancel(); + return; + } + + expectedSegmentStart = pathStart(); + tickPath(); + ticksElapsedSoFar++; + dispatchEvents(); + } + + @Override + public void onPlayerSprintState(SprintStateEvent event) { + if (isPathing()) { + event.setState(current.isSprinting()); + } + } + + private void tickPath() { + pausedThisTick = false; + if (pauseRequestedLastTick && safeToCancel) { + pauseRequestedLastTick = false; + if (unpausedLastTick) { + baritone.getInputOverrideHandler().clearAllKeys(); + baritone.getInputOverrideHandler().getBlockBreakHelper().stopBreakingBlock(); + } + unpausedLastTick = false; + pausedThisTick = true; + return; + } + unpausedLastTick = true; + if (cancelRequested) { + cancelRequested = false; + baritone.getInputOverrideHandler().clearAllKeys(); + } + synchronized (pathPlanLock) { + synchronized (pathCalcLock) { + if (inProgress != null) { + // we are calculating + // are we calculating the right thing though? 🤔 + BetterBlockPos calcFrom = inProgress.getStart(); + Optional currentBest = inProgress.bestPathSoFar(); + if ((current == null || !current.getPath().getDest().equals(calcFrom)) // if current ends in inProgress's start, then we're ok + && !calcFrom.equals(ctx.playerFeet()) && !calcFrom.equals(expectedSegmentStart) // if current starts in our playerFeet or pathStart, then we're ok + && (!currentBest.isPresent() || (!currentBest.get().positions().contains(ctx.playerFeet()) && !currentBest.get().positions().contains(expectedSegmentStart))) // if + ) { + // when it was *just* started, currentBest will be empty so we need to also check calcFrom since that's always present + inProgress.cancel(); // cancellation doesn't dispatch any events + } + } + } + if (current == null) { + return; + } + safeToCancel = current.onTick(); + if (current.failed() || current.finished()) { + current = null; + if (goal == null || goal.isInGoal(ctx.playerFeet())) { + logDebug("All done. At " + goal); + queuePathEvent(PathEvent.AT_GOAL); + next = null; + if (Baritone.settings().disconnectOnArrival.value) { + ctx.world().sendQuittingDisconnectingPacket(); + } + return; + } + if (next != null && !next.getPath().positions().contains(ctx.playerFeet()) && !next.getPath().positions().contains(expectedSegmentStart)) { // can contain either one + // 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(); // don't waste a tick doing nothing, get started right away + return; + } + // at this point, current just ended, but we aren't in the goal and have no plan for the future + synchronized (pathCalcLock) { + if (inProgress != null) { + queuePathEvent(PathEvent.PATH_FINISHED_NEXT_STILL_CALCULATING); + return; + } + // we aren't calculating + queuePathEvent(PathEvent.CALC_STARTED); + findPathInNewThread(expectedSegmentStart, true, context); + } + return; + } + // at this point, we know current is in progress + if (safeToCancel && 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; + } + if (Baritone.settings().splicePath.value) { + current = current.trySplice(next); + } + if (next != null && current.getPath().getDest().equals(next.getPath().getDest())) { + next = null; + } + synchronized (pathCalcLock) { + if (inProgress != null) { + // 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 doesn't get us all the way there + return; + } + if (ticksRemainingInSegment(false).get() < Baritone.settings().planningTickLookahead.value) { + // and this path has 7.5 seconds or less left + // don't include the current movement so a very long last movement (e.g. descend) doesn't trip it up + // if we actually included current, it wouldn't start planning ahead until the last movement was done, if the last movement took more than 7.5 seconds on its own + logDebug("Path almost over. Planning ahead..."); + queuePathEvent(PathEvent.NEXT_SEGMENT_CALC_STARTED); + findPathInNewThread(current.getPath().getDest(), false, context); + } + } + } + } + + @Override + public void onPlayerUpdate(PlayerUpdateEvent event) { + /* + if (current != null) { + switch (event.getState()) { + case PRE: + lastAutoJump = ctx.minecraft().gameSettings.autoJump; + ctx.minecraft().gameSettings.autoJump = false; + break; + case POST: + ctx.minecraft().gameSettings.autoJump = lastAutoJump; + break; + default: + break; + } + } + + */ //TODO: CHECK HERE + } + + public void secretInternalSetGoal(Goal goal) { this.goal = goal; } + public boolean secretInternalSetGoalAndPath(PathingCommand command) { + secretInternalSetGoal(command.goal); + if (command instanceof PathingCommandContext) { + context = ((PathingCommandContext) command).desiredCalcContext; + } else { + context = new CalculationContext(baritone, true); + } + if (goal == null) { + return false; + } + if (goal.isInGoal(ctx.playerFeet()) || goal.isInGoal(expectedSegmentStart)) { + return false; + } + synchronized (pathPlanLock) { + if (current != null) { + return false; + } + synchronized (pathCalcLock) { + if (inProgress != null) { + return false; + } + queuePathEvent(PathEvent.CALC_STARTED); + findPathInNewThread(expectedSegmentStart, true, context); + return true; + } + } + } + @Override public Goal getGoal() { return goal; @@ -58,19 +273,17 @@ public Goal getGoal() { @Override public boolean isPathing() { - return this.current != null; + return hasPath() && !pausedThisTick; } @Override - public void cancelEverything() { - current = null; - next = null; - baritone.getInputOverrideHandler().clearAllKeys(); + public PathExecutor getCurrent() { + return current; } @Override - public PathExecutor getCurrent() { - return current; + public PathExecutor getNext() { + return next; } @Override @@ -78,56 +291,179 @@ public Optional getInProgress() { return Optional.ofNullable(inProgress); } + public boolean isSafeToCancel() { + return safeToCancel; + } + + @Override + public void requestPause() { + pauseRequestedLastTick = true; + } + + public boolean cancelSegmentIfSafe() { + if (isSafeToCancel()) { + secretInternalSegmentCancel(); + return true; + } + return false; + } + + @Override + public boolean cancelEverything() { + boolean doIt = isSafeToCancel(); + if (doIt) { + secretInternalSegmentCancel(); + } + return doIt; + } + + public boolean calcFailedLastTick() { // NOT exposed on public api + return calcFailedLastTick; + } + + public void softCancelIfSafe() { + synchronized (pathPlanLock) { + getInProgress().ifPresent(AbstractNodeCostSearch::cancel); // only cancel ours + if (!isSafeToCancel()) { + return; + } + current = null; + next = null; + } + cancelRequested = true; + // do everything BUT clear keys + } + + // just cancel the current path + public void secretInternalSegmentCancel() { + queuePathEvent(PathEvent.CANCELED); + synchronized (pathPlanLock) { + getInProgress().ifPresent(AbstractNodeCostSearch::cancel); + if (current != null) { + current = null; + next = null; + baritone.getInputOverrideHandler().clearAllKeys(); + baritone.getInputOverrideHandler().getBlockBreakHelper().stopBreakingBlock(); + } + } + } + + @Override + public void forceCancel() { // exposed on public api because :sob: + cancelEverything(); + secretInternalSegmentCancel(); + synchronized (pathCalcLock) { + inProgress = null; + } + } + public CalculationContext secretInternalGetCalculationContext() { return context; } - @Override - public void findPath(BetterBlockPos start) { - context = new CalculationContext(baritone, true); - long primaryTimeout; - long failureTimeout; - if (current == null) { - primaryTimeout = Baritone.settings().primaryTimeoutMS.value; - failureTimeout = Baritone.settings().failureTimeoutMS.value; - } else { - primaryTimeout = Baritone.settings().planAheadPrimaryTimeoutMS.value; - failureTimeout = Baritone.settings().planAheadFailureTimeoutMS.value; + public Optional estimatedTicksToGoal() { + BetterBlockPos currentPos = ctx.playerFeet(); + if (goal == null || currentPos == null || startPosition == null) { + return Optional.empty(); } - AbstractNodeCostSearch pathfinder = createPathfinder(start, goal, current == null ? null : current.getPath(), context); - if (!Objects.equals(pathfinder.getGoal(), goal)) { - logDebug("Simplifying " + goal.getClass() + " to GoalXZ due to distance"); + if (goal.isInGoal(ctx.playerFeet())) { + resetEstimatedTicksToGoal(); + return Optional.of(0.0); + } + if (ticksElapsedSoFar == 0) { + return Optional.empty(); } + double current = goal.heuristic(currentPos.x, currentPos.y, currentPos.z); + double start = goal.heuristic(startPosition.x, startPosition.y, startPosition.z); + if (current == start) {// can't check above because current and start can be equal even if currentPos and startPosition are not + return Optional.empty(); + } + double eta = Math.abs(current - goal.heuristic()) * ticksElapsedSoFar / Math.abs(start - current); + return Optional.of(eta); + } - Baritone.getExecutor().execute(() -> { - logDebug("Starting to search for path from " + start + " to " + goal); + private void resetEstimatedTicksToGoal() { + resetEstimatedTicksToGoal(expectedSegmentStart); + } - PathCalculationResult calcResult = pathfinder.calculate(primaryTimeout, failureTimeout); - synchronized (pathPlanLock) { - Optional executor = calcResult.getPath().map(p -> new PathExecutor(PathingBehavior.this, p)); - if (current == null) { - if (executor.isPresent()) { - current = executor.get(); + private void resetEstimatedTicksToGoal(BlockPos start) { + resetEstimatedTicksToGoal(new BetterBlockPos(start)); + } + + private void resetEstimatedTicksToGoal(BetterBlockPos start) { + ticksElapsedSoFar = 0; + startPosition = start; + } + + /** + * See issue #209 + * + * @return The starting {@link BlockPos} for a new path + */ + public BetterBlockPos pathStart() { // TODO move to a helper or util class + BetterBlockPos feet = ctx.playerFeet(); + if (!MovementHelper.canWalkOn(ctx, feet.down())) { + if (ctx.player().onGround) { + double playerX = ctx.player().posX; + double playerZ = ctx.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)); } - } else { - if (next == null) { - if (executor.isPresent()) { - next = executor.get(); - } - } else { - logDirect("Warning: PathingBehaivor illegal state! Discarding invalid path!"); + } + 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(ctx, possibleSupport.down()) && MovementHelper.canWalkThrough(ctx, possibleSupport) && MovementHelper.canWalkThrough(ctx, possibleSupport.up())) { + // this is plausible + //logDebug("Faking path start assuming player is standing off the edge of a block"); + return possibleSupport; } } - synchronized (pathCalcLock) { - inProgress = null; + + } else { + // !onGround + // we're in the middle of a jump + if (MovementHelper.canWalkOn(ctx, feet.down().down())) { + //logDebug("Faking path start assuming player is midair and falling"); + return feet.down(); } } - }); + } + return feet; } - @Override - public void findFlyPath(BetterBlockPos start) { - context = new CalculationContext(baritone, true); + /** + * In a new thread, pathfind to target blockpos + * + * @param start + * @param talkAboutIt + */ + private void findPathInNewThread(final BlockPos start, final boolean talkAboutIt, CalculationContext context) { + // this must be called with synchronization on pathCalcLock! + // actually, we can check this, muahaha + if (!Thread.holdsLock(pathCalcLock)) { + throw new IllegalStateException("Must be called with synchronization on pathCalcLock"); + // why do it this way? it's already indented so much that putting the whole thing in a synchronized(pathCalcLock) was just too much lol + } + if (inProgress != null) { + throw new IllegalStateException("Already doing it"); // should have been checked by caller + } + if (!context.safeForThreadedUse) { + throw new IllegalStateException("Improper context thread safety level"); + } + Goal goal = this.goal; + if (goal == null) { + logDebug("no goal"); // TODO should this be an exception too? definitely should be checked by caller + return; + } long primaryTimeout; long failureTimeout; if (current == null) { @@ -137,30 +473,59 @@ public void findFlyPath(BetterBlockPos start) { primaryTimeout = Baritone.settings().planAheadPrimaryTimeoutMS.value; failureTimeout = Baritone.settings().planAheadFailureTimeoutMS.value; } - AbstractNodeCostSearch pathfinder = createFlyPathfinder(start, goal, current == null ? null : current.getPath(), context); - if (!Objects.equals(pathfinder.getGoal(), goal)) { + AbstractNodeCostSearch pathfinder = createPathfinder(start, goal, current == null ? null : current.getPath(), context); + if (!Objects.equals(pathfinder.getGoal(), goal)) { // will return the exact same object if simplification didn't happen logDebug("Simplifying " + goal.getClass() + " to GoalXZ due to distance"); } - + inProgress = pathfinder; Baritone.getExecutor().execute(() -> { - logDebug("Starting to search for path from " + start + " to " + goal); + if (talkAboutIt) { + logDebug("Starting to search for path from " + start + " to " + goal); + } PathCalculationResult calcResult = pathfinder.calculate(primaryTimeout, failureTimeout); synchronized (pathPlanLock) { Optional executor = calcResult.getPath().map(p -> new PathExecutor(PathingBehavior.this, p)); if (current == null) { if (executor.isPresent()) { - current = executor.get(); + if (executor.get().getPath().positions().contains(expectedSegmentStart)) { + queuePathEvent(PathEvent.CALC_FINISHED_NOW_EXECUTING); + current = executor.get(); + resetEstimatedTicksToGoal(start); + } else { + logDebug("Warning: discarding orphan path segment with incorrect start"); + } + } else { + if (calcResult.getType() != PathCalculationResult.Type.CANCELLATION && calcResult.getType() != PathCalculationResult.Type.EXCEPTION) { + // don't dispatch CALC_FAILED on cancellation + queuePathEvent(PathEvent.CALC_FAILED); + } } } else { if (next == null) { if (executor.isPresent()) { - next = executor.get(); + if (executor.get().getPath().getSrc().equals(current.getPath().getDest())) { + queuePathEvent(PathEvent.NEXT_SEGMENT_CALC_FINISHED); + next = executor.get(); + } else { + logDebug("Warning: discarding orphan next segment with incorrect start"); + } + } else { + queuePathEvent(PathEvent.NEXT_CALC_FAILED); } } else { + //throw new IllegalStateException("I have no idea what to do with this path"); + // no point in throwing an exception here, and it gets it stuck with inProgress being not null logDirect("Warning: PathingBehaivor illegal state! Discarding invalid path!"); } } + if (talkAboutIt && current != null && current.getPath() != null) { + if (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) { inProgress = null; } @@ -179,100 +544,8 @@ private static AbstractNodeCostSearch createPathfinder(BlockPos start, Goal goal return new AStarPathFinder(start.getX(), start.getY(), start.getZ(), transformed, context); } - private static AbstractNodeCostSearch createFlyPathfinder(BlockPos start, Goal goal, IPath previous, CalculationContext context) { - Goal transformed = goal; - if (Baritone.settings().simplifyUnloadedYCoord.value && goal instanceof IGoalRenderPos) { - BlockPos pos = ((IGoalRenderPos) goal).getGoalPos(); - if (!context.bsi.worldContainsLoadedChunk(pos.getX(), pos.getZ())) { - transformed = new GoalXZ(pos.getX(), pos.getZ()); - } - } - return new FlyAStar(start.getX(), start.getY(), start.getZ(), transformed, context); - } - - @Override - public void onTick(TickEvent event) { - if (event.getType() == TickEvent.Type.OUT) { - return; - } - - if (current == null) { - return; - } - - safeToCancel = current.onTick(); - - if (current.failed() || current.finished()) { - logDebug("Finished"); - current = null; - if (goal == null || goal.isInGoal(ctx.playerFeet())) { - logDebug("Finished. At " + goal); - - next = null; - if (Baritone.settings().disconnectOnArrival.value) { - ctx.world().sendQuittingDisconnectingPacket(); - } - return; - } - if (next != null && !next.getPath().positions().contains(ctx.playerFeet()) && - !next.getPath().positions().contains(expectedSegmentStart)) { - logDebug("Next path does not contain current position" + next.getPath().getSrc() + ", " + ctx.playerFeet()); - next = null; - } - - if (next != null) { - logDebug("Continuing on to planned next path"); - current = next; - next = null; - current.onTick(); - return; - } - return; - } - - if (next != null && current.getPath().getDest().equals(next.getPath().getDest())) { - next = null; - } - - synchronized (pathCalcLock) { - if (inProgress != null) { - return; - } - - if (next != null) { - return; - } - - if (goal == null || goal.isInGoal(current.getPath().getDest())) { - return; - } - - Optional ticksRemainingInSegment = ticksRemainingInSegment(false); - - if (ticksRemainingInSegment.isPresent() && ticksRemainingInSegment.get() < Baritone.settings().planningTickLookahead.value) { - logDebug("Path almost over. Planning ahead"); - - if (Baritone.settings().flyInstead.value) - findFlyPath(current.getPath().getDest()); - else - findPath(current.getPath().getDest()); - } - } - } - - @Override - public void onPlayerSprintState(SprintStateEvent event) { - if (isPathing()) { - event.setState(current.isSprinting()); - } - } - @Override public void onRenderPass(RenderEvent event) { PathRenderer.render(event, this); } - - public CalculationContext getCalculationContext() { - return this.context; - } }