baritone/src/main/java/baritone/pathing/calc/AbstractNodeCostSearch.java

245 lines
9.3 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.pathing.calc;
import baritone.Baritone;
import baritone.api.pathing.calc.IPath;
import baritone.api.pathing.calc.IPathFinder;
import baritone.api.pathing.goals.Goal;
import baritone.api.utils.BetterBlockPos;
import baritone.api.utils.Helper;
import baritone.api.utils.PathCalculationResult;
import baritone.pathing.movement.CalculationContext;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import baritone.utils.NotificationHelper;
import java.util.Optional;
/**
* Any pathfinding algorithm that keeps track of nodes recursively by their cost (e.g. A*, dijkstra)
*
* @author leijurv
*/
public abstract class AbstractNodeCostSearch implements IPathFinder, Helper {
protected final int startX;
protected final int startY;
protected final int startZ;
protected final Goal goal;
private final CalculationContext context;
/**
* @see <a href="https://github.com/cabaletta/baritone/issues/107">Issue #107</a>
*/
private final Long2ObjectOpenHashMap<PathNode> map;
protected PathNode startNode;
protected PathNode mostRecentConsidered;
protected final PathNode[] bestSoFar = new PathNode[COEFFICIENTS.length];
private volatile boolean isFinished;
protected boolean cancelRequested;
/**
* This is really complicated and hard to explain. I wrote a comment in the old version of MineBot but it was so
* long it was easier as a Google Doc (because I could insert charts).
*
* @see <a href="https://docs.google.com/document/d/1WVHHXKXFdCR1Oz__KtK8sFqyvSwJN_H4lftkHFgmzlc/edit">here</a>
*/
protected static final double[] COEFFICIENTS = {1.5, 2, 2.5, 3, 4, 5, 10};
/**
* If a path goes less than 5 blocks and doesn't make it to its goal, it's not worth considering.
*/
protected static final double MIN_DIST_PATH = 5;
/**
* there are floating point errors caused by random combinations of traverse and diagonal over a flat area
* that means that sometimes there's a cost improvement of like 10 ^ -16
* it's not worth the time to update the costs, decrease-key the heap, potentially repropagate, etc
* <p>
* who cares about a hundredth of a tick? that's half a millisecond for crying out loud!
*/
protected static final double MIN_IMPROVEMENT = 0.01;
AbstractNodeCostSearch(int startX, int startY, int startZ, Goal goal, CalculationContext context) {
this.startX = startX;
this.startY = startY;
this.startZ = startZ;
this.goal = goal;
this.context = context;
this.map = new Long2ObjectOpenHashMap<>(Baritone.settings().pathingMapDefaultSize.value, Baritone.settings().pathingMapLoadFactor.value);
}
public void cancel() {
cancelRequested = true;
}
@Override
public synchronized PathCalculationResult calculate(long primaryTimeout, long failureTimeout) {
if (isFinished) {
throw new IllegalStateException("Path finder cannot be reused!");
}
cancelRequested = false;
try {
IPath path = calculate0(primaryTimeout, failureTimeout).map(IPath::postProcess).orElse(null);
if (cancelRequested) {
return new PathCalculationResult(PathCalculationResult.Type.CANCELLATION);
}
if (path == null) {
return new PathCalculationResult(PathCalculationResult.Type.FAILURE);
}
int previousLength = path.length();
path = path.cutoffAtLoadedChunks(context.bsi);
if (path.length() < previousLength) {
Helper.HELPER.logDebug("Cutting off path at edge of loaded chunks");
Helper.HELPER.logDebug("Length decreased by " + (previousLength - path.length()));
} else {
Helper.HELPER.logDebug("Path ends within loaded chunks");
}
previousLength = path.length();
path = path.staticCutoff(goal);
if (path.length() < previousLength) {
Helper.HELPER.logDebug("Static cutoff " + previousLength + " to " + path.length());
}
if (goal.isInGoal(path.getDest())) {
return new PathCalculationResult(PathCalculationResult.Type.SUCCESS_TO_GOAL, path);
} else {
return new PathCalculationResult(PathCalculationResult.Type.SUCCESS_SEGMENT, path);
}
} catch (Exception e) {
Helper.HELPER.logDirect("Pathing exception: " + e);
e.printStackTrace();
return new PathCalculationResult(PathCalculationResult.Type.EXCEPTION);
} finally {
// this is run regardless of what exception may or may not be raised by calculate0
isFinished = true;
}
}
protected abstract Optional<IPath> calculate0(long primaryTimeout, long failureTimeout);
/**
* Determines the distance squared from the specified node to the start
* node. Intended for use in distance comparison, rather than anything that
* considers the real distance value, hence the "sq".
*
* @param n A node
* @return The distance, squared
*/
protected double getDistFromStartSq(PathNode n) {
int xDiff = n.x - startX;
int yDiff = n.y - startY;
int zDiff = n.z - startZ;
return xDiff * xDiff + yDiff * yDiff + zDiff * zDiff;
}
/**
* Attempts to search the block position hashCode long to {@link PathNode} map
* for the node mapped to the specified pos. If no node is found,
* a new node is created.
*
* @param x The x position of the node
* @param y The y position of the node
* @param z The z position of the node
* @param hashCode The hash code of the node, provided by {@link BetterBlockPos#longHash(int, int, int)}
* @return The associated node
* @see <a href="https://github.com/cabaletta/baritone/issues/107">Issue #107</a>
*/
protected PathNode getNodeAtPosition(int x, int y, int z, long hashCode) {
PathNode node = map.get(hashCode);
if (node == null) {
node = new PathNode(x, y, z, goal);
map.put(hashCode, node);
}
return node;
}
@Override
public Optional<IPath> pathToMostRecentNodeConsidered() {
return Optional.ofNullable(mostRecentConsidered).map(node -> new Path(startNode, node, 0, goal, context));
}
@Override
public Optional<IPath> bestPathSoFar() {
return bestSoFar(false, 0);
}
protected Optional<IPath> bestSoFar(boolean logInfo, int numNodes) {
if (startNode == null) {
return Optional.empty();
}
double bestDist = 0;
for (int i = 0; i < COEFFICIENTS.length; i++) {
if (bestSoFar[i] == null) {
continue;
}
double dist = getDistFromStartSq(bestSoFar[i]);
if (dist > bestDist) {
bestDist = dist;
}
if (dist > MIN_DIST_PATH * MIN_DIST_PATH) { // square the comparison since distFromStartSq is squared
if (logInfo) {
if (COEFFICIENTS[i] >= 3) {
System.out.println("Warning: cost coefficient is greater than three! Probably means that");
System.out.println("the path I found is pretty terrible (like sneak-bridging for dozens of blocks)");
System.out.println("But I'm going to do it anyway, because yolo");
}
System.out.println("Path goes for " + Math.sqrt(dist) + " blocks");
logDebug("A* cost coefficient " + COEFFICIENTS[i]);
}
return Optional.of(new Path(startNode, bestSoFar[i], numNodes, goal, context));
}
}
// instead of returning bestSoFar[0], be less misleading
// if it actually won't find any path, don't make them think it will by rendering a dark blue that will never actually happen
if (logInfo) {
logDebug("Even with a cost coefficient of " + COEFFICIENTS[COEFFICIENTS.length - 1] + ", I couldn't get more than " + Math.sqrt(bestDist) + " blocks");
logDebug("No path found =(");
if (Baritone.settings().desktopNotifications.value) {
NotificationHelper.notify("No path found =(", true);
}
}
return Optional.empty();
}
@Override
public final boolean isFinished() {
return isFinished;
}
@Override
public final Goal getGoal() {
return goal;
}
public BetterBlockPos getStart() {
return new BetterBlockPos(startX, startY, startZ);
}
protected int mapSize() {
return map.size();
}
}