diff --git a/src/api/java/baritone/api/behavior/IPathingBehavior.java b/src/api/java/baritone/api/behavior/IPathingBehavior.java index 5444bb838..83db7ab3a 100644 --- a/src/api/java/baritone/api/behavior/IPathingBehavior.java +++ b/src/api/java/baritone/api/behavior/IPathingBehavior.java @@ -58,6 +58,15 @@ public interface IPathingBehavior extends IBehavior { return Optional.of(current.getPath().ticksRemainingFrom(start)); } + /** + * Returns the estimated remaining ticks to the current goal. + * Given that the return type is an optional, {@link Optional#empty()} + * will be returned in the case that there is no current goal. + * + * @return The estimated remaining ticks to the current goal. + */ + Optional estimatedTicksToGoal(); + /** * @return The current pathing goal */ diff --git a/src/api/java/baritone/api/pathing/goals/Goal.java b/src/api/java/baritone/api/pathing/goals/Goal.java index acee68db8..95462f961 100644 --- a/src/api/java/baritone/api/pathing/goals/Goal.java +++ b/src/api/java/baritone/api/pathing/goals/Goal.java @@ -54,4 +54,18 @@ public interface Goal { default double heuristic(BlockPos pos) { return heuristic(pos.getX(), pos.getY(), pos.getZ()); } + + /** + * Returns the heuristic at the goal. + * i.e. {@code heuristic() == heuristic(x,y,z)} + * when {@code isInGoal(x,y,z) == true} + * This is needed by {@code PathingBehavior#estimatedTicksToGoal} because + * some Goals actually do not have a heuristic of 0 when that condition is met + * + * @return The estimate number of ticks to satisfy the goal when the goal + * is already satisfied + */ + default double heuristic() { + return 0; + } } diff --git a/src/api/java/baritone/api/pathing/goals/GoalComposite.java b/src/api/java/baritone/api/pathing/goals/GoalComposite.java index 415f74e56..47522492b 100644 --- a/src/api/java/baritone/api/pathing/goals/GoalComposite.java +++ b/src/api/java/baritone/api/pathing/goals/GoalComposite.java @@ -57,6 +57,16 @@ public class GoalComposite implements Goal { return min; } + @Override + public double heuristic() { + double min = Double.MAX_VALUE; + for (Goal g : goals) { + // just take the highest value that is guaranteed to be inside the goal + min = Math.min(min, g.heuristic()); + } + return min; + } + @Override public String toString() { return "GoalComposite" + Arrays.toString(goals); diff --git a/src/api/java/baritone/api/pathing/goals/GoalInverted.java b/src/api/java/baritone/api/pathing/goals/GoalInverted.java index 197369241..354e2ce39 100644 --- a/src/api/java/baritone/api/pathing/goals/GoalInverted.java +++ b/src/api/java/baritone/api/pathing/goals/GoalInverted.java @@ -45,6 +45,11 @@ public class GoalInverted implements Goal { return -origin.heuristic(x, y, z); } + @Override + public double heuristic() { + return Double.NEGATIVE_INFINITY; + } + @Override public String toString() { return String.format("GoalInverted{%s}", origin.toString()); diff --git a/src/api/java/baritone/api/pathing/goals/GoalNear.java b/src/api/java/baritone/api/pathing/goals/GoalNear.java index 73d64ed9f..6a8226fb4 100644 --- a/src/api/java/baritone/api/pathing/goals/GoalNear.java +++ b/src/api/java/baritone/api/pathing/goals/GoalNear.java @@ -19,6 +19,8 @@ package baritone.api.pathing.goals; import baritone.api.utils.SettingsUtil; import baritone.api.utils.interfaces.IGoalRenderPos; +import it.unimi.dsi.fastutil.doubles.DoubleOpenHashSet; +import it.unimi.dsi.fastutil.doubles.DoubleIterator; import net.minecraft.util.math.BlockPos; public class GoalNear implements Goal, IGoalRenderPos { @@ -51,6 +53,34 @@ public class GoalNear implements Goal, IGoalRenderPos { return GoalBlock.calculate(xDiff, yDiff, zDiff); } + @Override + public double heuristic() {// TODO less hacky solution + int range = (int) Math.ceil(Math.sqrt(rangeSq)); + DoubleOpenHashSet maybeAlwaysInside = new DoubleOpenHashSet(); // see pull request #1978 + double minOutside = Double.POSITIVE_INFINITY; + for (int dx = -range; dx <= range; dx++) { + for (int dy = -range; dy <= range; dy++) { + for (int dz = -range; dz <= range; dz++) { + double h = heuristic(x + dx, y + dy, z + dz); + if (h < minOutside && isInGoal(x + dx, y + dy, z + dz)) { + maybeAlwaysInside.add(h); + } else { + minOutside = Math.min(minOutside, h); + } + } + } + } + double maxInside = Double.NEGATIVE_INFINITY; + DoubleIterator it = maybeAlwaysInside.iterator(); + while (it.hasNext()) { + double inside = it.nextDouble(); + if (inside < minOutside) { + maxInside = Math.max(maxInside, inside); + } + } + return maxInside; + } + @Override public BlockPos getGoalPos() { return new BlockPos(x, y, z); diff --git a/src/api/java/baritone/api/pathing/goals/GoalRunAway.java b/src/api/java/baritone/api/pathing/goals/GoalRunAway.java index 287e3e9ed..503ae7f43 100644 --- a/src/api/java/baritone/api/pathing/goals/GoalRunAway.java +++ b/src/api/java/baritone/api/pathing/goals/GoalRunAway.java @@ -18,6 +18,8 @@ package baritone.api.pathing.goals; import baritone.api.utils.SettingsUtil; +import it.unimi.dsi.fastutil.doubles.DoubleOpenHashSet; +import it.unimi.dsi.fastutil.doubles.DoubleIterator; import net.minecraft.util.math.BlockPos; import java.util.Arrays; @@ -65,7 +67,7 @@ public class GoalRunAway implements Goal { } @Override - public double heuristic(int x, int y, int z) {//mostly copied from GoalBlock + public double heuristic(int x, int y, int z) {// mostly copied from GoalBlock double min = Double.MAX_VALUE; for (BlockPos p : from) { double h = GoalXZ.calculate(p.getX() - x, p.getZ() - z); @@ -80,6 +82,48 @@ public class GoalRunAway implements Goal { return min; } + @Override + public double heuristic() {// TODO less hacky solution + int distance = (int) Math.ceil(Math.sqrt(distanceSq)); + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + for (BlockPos p : from) { + minX = Math.min(minX, p.getX() - distance); + minY = Math.min(minY, p.getY() - distance); + minZ = Math.min(minZ, p.getZ() - distance); + maxX = Math.max(minX, p.getX() + distance); + maxY = Math.max(minY, p.getY() + distance); + maxZ = Math.max(minZ, p.getZ() + distance); + } + DoubleOpenHashSet maybeAlwaysInside = new DoubleOpenHashSet(); // see pull request #1978 + double minOutside = Double.POSITIVE_INFINITY; + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + for (int z = minZ; z <= maxZ; z++) { + double h = heuristic(x, y, z); + if (h < minOutside && isInGoal(x, y, z)) { + maybeAlwaysInside.add(h); + } else { + minOutside = Math.min(minOutside, h); + } + } + } + } + double maxInside = Double.NEGATIVE_INFINITY; + DoubleIterator it = maybeAlwaysInside.iterator(); + while (it.hasNext()) { + double inside = it.nextDouble(); + if (inside < minOutside) { + maxInside = Math.max(maxInside, inside); + } + } + return maxInside; + } + @Override public String toString() { if (maintainY != null) { diff --git a/src/api/java/baritone/api/pathing/goals/GoalStrictDirection.java b/src/api/java/baritone/api/pathing/goals/GoalStrictDirection.java index b48a029ad..e93f47ac0 100644 --- a/src/api/java/baritone/api/pathing/goals/GoalStrictDirection.java +++ b/src/api/java/baritone/api/pathing/goals/GoalStrictDirection.java @@ -64,6 +64,11 @@ public class GoalStrictDirection implements Goal { return heuristic; } + @Override + public double heuristic() { + return Double.NEGATIVE_INFINITY; + } + @Override public String toString() { return String.format( diff --git a/src/main/java/baritone/behavior/PathingBehavior.java b/src/main/java/baritone/behavior/PathingBehavior.java index 50ca7425f..f9c56c5a4 100644 --- a/src/main/java/baritone/behavior/PathingBehavior.java +++ b/src/main/java/baritone/behavior/PathingBehavior.java @@ -52,6 +52,10 @@ public final class PathingBehavior extends Behavior implements IPathingBehavior, private Goal goal; private CalculationContext context; + /*eta*/ + private int ticksElapsedSoFar; + private BetterBlockPos startPosition; + private boolean safeToCancel; private boolean pauseRequestedLastTick; private boolean unpausedLastTick; @@ -98,6 +102,7 @@ public final class PathingBehavior extends Behavior implements IPathingBehavior, expectedSegmentStart = pathStart(); baritone.getPathingControlManager().preTick(); tickPath(); + ticksElapsedSoFar++; dispatchEvents(); } @@ -372,6 +377,40 @@ public final class PathingBehavior extends Behavior implements IPathingBehavior, return context; } + public Optional estimatedTicksToGoal() { + BetterBlockPos currentPos = ctx.playerFeet(); + if (goal == null || currentPos == null || startPosition == null) { + return Optional.empty(); + } + 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); + } + + private void resetEstimatedTicksToGoal() { + resetEstimatedTicksToGoal(expectedSegmentStart); + } + + private void resetEstimatedTicksToGoal(BlockPos start) { + resetEstimatedTicksToGoal(new BetterBlockPos(start)); + } + + private void resetEstimatedTicksToGoal(BetterBlockPos start) { + ticksElapsedSoFar = 0; + startPosition = start; + } + /** * See issue #209 * @@ -468,6 +507,7 @@ public final class PathingBehavior extends Behavior implements IPathingBehavior, 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"); } diff --git a/src/main/java/baritone/command/defaults/DefaultCommands.java b/src/main/java/baritone/command/defaults/DefaultCommands.java index 41c58b003..17f338046 100644 --- a/src/main/java/baritone/command/defaults/DefaultCommands.java +++ b/src/main/java/baritone/command/defaults/DefaultCommands.java @@ -38,6 +38,7 @@ public final class DefaultCommands { new GotoCommand(baritone), new PathCommand(baritone), new ProcCommand(baritone), + new ETACommand(baritone), new VersionCommand(baritone), new RepackCommand(baritone), new BuildCommand(baritone), diff --git a/src/main/java/baritone/command/defaults/ETACommand.java b/src/main/java/baritone/command/defaults/ETACommand.java new file mode 100644 index 000000000..3c16bd113 --- /dev/null +++ b/src/main/java/baritone/command/defaults/ETACommand.java @@ -0,0 +1,78 @@ +/* + * 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.command.defaults; + +import baritone.api.IBaritone; +import baritone.api.pathing.calc.IPathingControlManager; +import baritone.api.process.IBaritoneProcess; +import baritone.api.behavior.IPathingBehavior; +import baritone.api.command.Command; +import baritone.api.command.exception.CommandException; +import baritone.api.command.exception.CommandInvalidStateException; +import baritone.api.command.argument.IArgConsumer; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +public class ETACommand extends Command { + + public ETACommand(IBaritone baritone) { + super(baritone, "eta"); + } + + @Override + public void execute(String label, IArgConsumer args) throws CommandException { + args.requireMax(0); + IPathingControlManager pathingControlManager = baritone.getPathingControlManager(); + IBaritoneProcess process = pathingControlManager.mostRecentInControl().orElse(null); + if (process == null) { + throw new CommandInvalidStateException("No process in control"); + } + IPathingBehavior pathingBehavior = baritone.getPathingBehavior(); + logDirect(String.format( + "Next segment: %.2f\n" + + "Goal: %.2f", + pathingBehavior.ticksRemainingInSegment().orElse(-1.0), + pathingBehavior.estimatedTicksToGoal().orElse(-1.0) + )); + } + + @Override + public Stream tabComplete(String label, IArgConsumer args) { + return Stream.empty(); + } + + @Override + public String getShortDesc() { + return "View the current ETA"; + } + + @Override + public List getLongDesc() { + return Arrays.asList( + "The ETA command provides information about the estimated time until the next segment.", + "and the goal", + "", + "Be aware that the ETA to your goal is really unprecise", + "", + "Usage:", + "> eta - View ETA, if present" + ); + } +} diff --git a/src/main/java/baritone/process/GetToBlockProcess.java b/src/main/java/baritone/process/GetToBlockProcess.java index 00ec26091..78df95883 100644 --- a/src/main/java/baritone/process/GetToBlockProcess.java +++ b/src/main/java/baritone/process/GetToBlockProcess.java @@ -77,6 +77,10 @@ public final class GetToBlockProcess extends BaritoneProcessHelper implements IG public boolean isInGoal(int x, int y, int z) { return false; } + @Override + public double heuristic() { + return Double.NEGATIVE_INFINITY; + } }, PathingCommandType.FORCE_REVALIDATE_GOAL_AND_PATH); } logDirect("No known locations of " + gettingTo + ", canceling GetToBlock"); diff --git a/src/main/java/baritone/process/MineProcess.java b/src/main/java/baritone/process/MineProcess.java index ea35fd566..8b0b5bac0 100644 --- a/src/main/java/baritone/process/MineProcess.java +++ b/src/main/java/baritone/process/MineProcess.java @@ -211,6 +211,10 @@ public final class MineProcess extends BaritoneProcessHelper implements IMinePro public boolean isInGoal(int x, int y, int z) { return false; } + @Override + public double heuristic() { + return Double.NEGATIVE_INFINITY; + } }; } return new PathingCommand(branchPointRunaway, PathingCommandType.REVALIDATE_GOAL_AND_PATH);