452 lines
17 KiB
Java
452 lines
17 KiB
Java
/*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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.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 PathingBehavior() {}
|
|
|
|
private void dispatchPathEvent(PathEvent event) {
|
|
Baritone.INSTANCE.getExecutor().execute(() -> Baritone.INSTANCE.getGameEventHandler().onPathEvent(event));
|
|
}
|
|
|
|
@Override
|
|
public void onTick(TickEvent event) {
|
|
if (event.getType() == TickEvent.Type.OUT) {
|
|
this.cancel();
|
|
return;
|
|
}
|
|
mc.playerController.setPlayerCapabilities(mc.player);
|
|
if (current == null) {
|
|
return;
|
|
}
|
|
boolean safe = current.onTick(event);
|
|
synchronized (pathPlanLock) {
|
|
if (current.failed() || current.finished()) {
|
|
current = null;
|
|
if (goal == null || goal.isInGoal(playerFeet())) {
|
|
logDebug("All done. At " + goal);
|
|
dispatchPathEvent(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.
|
|
dispatchPathEvent(PathEvent.DISCARD_NEXT);
|
|
next = null;
|
|
}
|
|
if (next != null) {
|
|
logDebug("Continuing on to planned next path");
|
|
dispatchPathEvent(PathEvent.CONTINUING_ONTO_PLANNED_NEXT);
|
|
current = next;
|
|
next = null;
|
|
current.onTick(event);
|
|
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) {
|
|
dispatchPathEvent(PathEvent.PATH_FINISHED_NEXT_STILL_CALCULATING);
|
|
// if we aren't calculating right now
|
|
return;
|
|
}
|
|
dispatchPathEvent(PathEvent.CALC_STARTED);
|
|
findPathInNewThread(pathStart(), true, Optional.empty());
|
|
}
|
|
return;
|
|
}
|
|
// at this point, we know current is in progress
|
|
if (safe) {
|
|
// a movement just ended
|
|
if (next != null) {
|
|
if (next.snipsnapifpossible()) {
|
|
// jump directly onto the next path
|
|
logDebug("Splicing into planned next path early...");
|
|
dispatchPathEvent(PathEvent.SPLICING_ONTO_NEXT_EARLY);
|
|
current = next;
|
|
next = null;
|
|
current.onTick(event);
|
|
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...");
|
|
dispatchPathEvent(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<Double> ticksRemainingInSegment() {
|
|
if (current == null) {
|
|
return Optional.empty();
|
|
}
|
|
return Optional.of(current.getPath().ticksRemainingFrom(current.getPosition()));
|
|
}
|
|
|
|
@Override
|
|
public void setGoal(Goal goal) {
|
|
this.goal = goal;
|
|
}
|
|
|
|
@Override
|
|
public Goal getGoal() {
|
|
return goal;
|
|
}
|
|
|
|
@Override
|
|
public PathExecutor getCurrent() {
|
|
return current;
|
|
}
|
|
|
|
@Override
|
|
public PathExecutor getNext() {
|
|
return next;
|
|
}
|
|
|
|
@Override
|
|
public Optional<IPathFinder> getPathFinder() {
|
|
return Optional.ofNullable(AbstractNodeCostSearch.currentlyRunning());
|
|
}
|
|
|
|
@Override
|
|
public boolean isPathing() {
|
|
return this.current != null;
|
|
}
|
|
|
|
@Override
|
|
public void cancel() {
|
|
dispatchPathEvent(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;
|
|
}
|
|
dispatchPathEvent(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<BetterBlockPos> 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<IPath> 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<IPath> 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<PathExecutor> 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()) {
|
|
dispatchPathEvent(PathEvent.CALC_FINISHED_NOW_EXECUTING);
|
|
current = executor.get();
|
|
} else {
|
|
dispatchPathEvent(PathEvent.CALC_FAILED);
|
|
}
|
|
} else {
|
|
if (next == null) {
|
|
if (executor.isPresent()) {
|
|
dispatchPathEvent(PathEvent.NEXT_SEGMENT_CALC_FINISHED);
|
|
next = executor.get();
|
|
} else {
|
|
dispatchPathEvent(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<IPath> findPath(BlockPos start, Optional<IPath> 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.<Long>get();
|
|
} else {
|
|
timeout = Baritone.settings().planAheadTimeoutMS.<Long>get();
|
|
}
|
|
Optional<HashSet<Long>> 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();
|
|
}
|
|
}
|