baritone/src/main/java/baritone/process/ElytraProcess.java

523 lines
20 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.process;
import baritone.Baritone;
import baritone.api.IBaritone;
import baritone.api.event.events.*;
import baritone.api.event.events.type.EventState;
import baritone.api.event.listener.AbstractGameEventListener;
import baritone.api.pathing.goals.Goal;
import baritone.api.pathing.goals.GoalBlock;
import baritone.api.pathing.goals.GoalXZ;
import baritone.api.pathing.goals.GoalYLevel;
import baritone.api.pathing.movement.IMovement;
import baritone.api.pathing.path.IPathExecutor;
import baritone.api.process.IBaritoneProcess;
import baritone.api.process.IElytraProcess;
import baritone.api.process.PathingCommand;
import baritone.api.process.PathingCommandType;
import baritone.api.utils.BetterBlockPos;
import baritone.api.utils.Rotation;
import baritone.api.utils.RotationUtils;
import baritone.api.utils.input.Input;
import baritone.pathing.movement.CalculationContext;
import baritone.pathing.movement.movements.MovementFall;
import baritone.process.elytra.ElytraBehavior;
import baritone.process.elytra.NetherPathfinderContext;
import baritone.process.elytra.NullElytraProcess;
import baritone.utils.BaritoneProcessHelper;
import baritone.utils.PathingCommandContext;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import net.minecraft.block.Block;
import net.minecraft.block.state.IBlockState;
import net.minecraft.init.Blocks;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import java.util.*;
import static baritone.api.pathing.movement.ActionCosts.COST_INF;
public class ElytraProcess extends BaritoneProcessHelper implements IBaritoneProcess, IElytraProcess, AbstractGameEventListener {
public State state;
private boolean goingToLandingSpot;
private BetterBlockPos landingSpot;
private boolean reachedGoal; // this basically just prevents potential notification spam
private Goal goal;
private ElytraBehavior behavior;
private ElytraProcess(Baritone baritone) {
super(baritone);
baritone.getGameEventHandler().registerEventListener(this);
}
public static <T extends IElytraProcess> T create(final Baritone baritone) {
return (T) (NetherPathfinderContext.isSupported()
? new ElytraProcess(baritone)
: new NullElytraProcess(baritone));
}
@Override
public boolean isActive() {
return this.behavior != null;
}
@Override
public void resetState() {
BlockPos destination = this.currentDestination();
this.onLostControl();
if (destination != null) {
this.pathTo(destination);
this.repackChunks();
}
}
private static final String AUTO_JUMP_FAILURE_MSG = "Failed to compute a walking path to a spot to jump off from. Consider starting from a higher location, near an overhang. Or, you can disable elytraAutoJump and just manually begin gliding.";
@Override
public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) {
final long seedSetting = Baritone.settings().elytraNetherSeed.value;
if (seedSetting != this.behavior.context.getSeed()) {
logDirect("Nether seed changed, recalculating path");
this.resetState();
}
this.behavior.onTick();
if (calcFailed) {
onLostControl();
logDirect(AUTO_JUMP_FAILURE_MSG);
return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL);
}
if (ctx.player().isElytraFlying() && this.state != State.LANDING && this.behavior.pathManager.isComplete()) {
final BetterBlockPos last = this.behavior.pathManager.path.getLast();
if (last != null && ctx.player().getDistanceSqToCenter(last) < (48 * 48) && !goingToLandingSpot) {
logDirect("Path complete, picking a nearby safe landing spot...");
BetterBlockPos landingSpot = findSafeLandingSpot(last);
// if this fails we will just keep orbiting the last node until we run out of rockets or the user intervenes
if (landingSpot != null) {
this.pathTo0(landingSpot, true);
this.landingSpot = landingSpot;
this.goingToLandingSpot = true;
}
}
if (last != null && ctx.player().getDistanceSqToCenter(last) < 1) {
if (Baritone.settings().notificationOnPathComplete.value && !reachedGoal) {
logNotification("Pathing complete", false);
}
if (Baritone.settings().disconnectOnArrival.value && !reachedGoal) {
// don't be active when the user logs back in
this.onLostControl();
ctx.world().sendQuittingDisconnectingPacket();
return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL);
}
reachedGoal = true;
// we are goingToLandingSpot and we are in the last node of the path
if (this.goingToLandingSpot) {
this.state = State.LANDING;
logDirect("Above the landing spot, landing...");
}
}
}
if (this.state == State.LANDING) {
final BetterBlockPos endPos = this.landingSpot != null ? this.landingSpot : behavior.pathManager.path.getLast();
if (ctx.player().isElytraFlying() && endPos != null) {
Vec3d from = ctx.player().getPositionVector();
Vec3d to = new Vec3d(((double) endPos.x) + 0.5, from.y, ((double) endPos.z) + 0.5);
Rotation rotation = RotationUtils.calcRotationFromVec3d(from, to, ctx.playerRotations());
baritone.getLookBehavior().updateTarget(new Rotation(rotation.getYaw(), 0), false); // this will be overwritten, probably, by behavior tick
if (ctx.player().posY < endPos.y - LANDING_COLUMN_HEIGHT) {
logDirect("bad landing spot, trying again...");
landingSpotIsBad(endPos);
}
}
}
if (ctx.player().isElytraFlying()) {
behavior.landingMode = this.state == State.LANDING;
this.goal = null;
baritone.getInputOverrideHandler().clearAllKeys();
behavior.tick();
return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL);
} else if (this.state == State.LANDING) {
if (Math.sqrt(ctx.player().motionX * ctx.player().motionX + ctx.player().motionZ * ctx.player().motionZ) > 0.001) {
logDirect("Landed, but still moving, waiting for velocity to die down... ");
baritone.getInputOverrideHandler().setInputForceState(Input.SNEAK, true);
return new PathingCommand(null, PathingCommandType.REQUEST_PAUSE);
}
logDirect("Done :)");
baritone.getInputOverrideHandler().clearAllKeys();
this.onLostControl();
return new PathingCommand(null, PathingCommandType.REQUEST_PAUSE);
}
if (this.state == State.FLYING || this.state == State.START_FLYING) {
this.state = ctx.player().onGround && Baritone.settings().elytraAutoJump.value
? State.LOCATE_JUMP
: State.START_FLYING;
}
if (this.state == State.LOCATE_JUMP) {
if (this.goal == null) {
this.goal = new GoalYLevel(31);
}
final IPathExecutor executor = baritone.getPathingBehavior().getCurrent();
if (executor != null && executor.getPath().getGoal() == this.goal) {
final IMovement fall = executor.getPath().movements().stream()
.filter(movement -> movement instanceof MovementFall)
.findFirst().orElse(null);
if (fall != null) {
final BetterBlockPos from = new BetterBlockPos(
(fall.getSrc().x + fall.getDest().x) / 2,
(fall.getSrc().y + fall.getDest().y) / 2,
(fall.getSrc().z + fall.getDest().z) / 2
);
behavior.pathManager.pathToDestination(from).whenComplete((result, ex) -> {
if (ex == null) {
this.state = State.GET_TO_JUMP;
return;
}
onLostControl();
});
this.state = State.PAUSE;
} else {
onLostControl();
logDirect(AUTO_JUMP_FAILURE_MSG);
return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL);
}
}
return new PathingCommandContext(this.goal, PathingCommandType.SET_GOAL_AND_PAUSE, new WalkOffCalculationContext(baritone));
}
// yucky
if (this.state == State.PAUSE) {
return new PathingCommand(null, PathingCommandType.REQUEST_PAUSE);
}
if (this.state == State.GET_TO_JUMP) {
final IPathExecutor executor = baritone.getPathingBehavior().getCurrent();
final boolean canStartFlying = ctx.player().fallDistance > 1.0f
&& !isSafeToCancel
&& executor != null
&& executor.getPath().movements().get(executor.getPosition()) instanceof MovementFall;
if (canStartFlying) {
this.state = State.START_FLYING;
} else {
return new PathingCommand(null, PathingCommandType.SET_GOAL_AND_PATH);
}
}
if (this.state == State.START_FLYING) {
if (!isSafeToCancel) {
// owned
baritone.getPathingBehavior().secretInternalSegmentCancel();
}
baritone.getInputOverrideHandler().clearAllKeys();
if (ctx.player().fallDistance > 1.0f) {
baritone.getInputOverrideHandler().setInputForceState(Input.JUMP, true);
}
}
return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL);
}
public void landingSpotIsBad(BetterBlockPos endPos) {
badLandingSpots.add(endPos);
goingToLandingSpot = false;
this.landingSpot = null;
this.state = State.FLYING;
}
@Override
public void onLostControl() {
this.goal = null;
this.goingToLandingSpot = false;
this.landingSpot = null;
this.reachedGoal = false;
this.state = State.START_FLYING; // TODO: null state?
destroyBehaviorAsync();
}
private void destroyBehaviorAsync() {
ElytraBehavior behavior = this.behavior;
if (behavior != null) {
Baritone.getExecutor().execute(behavior::destroy);
this.behavior = null;
}
}
@Override
public double priority() {
return 0; // higher priority than CustomGoalProcess
}
@Override
public String displayName0() {
return "Elytra - " + this.state.description;
}
@Override
public void repackChunks() {
if (this.behavior != null) {
this.behavior.repackChunks();
}
}
@Override
public BlockPos currentDestination() {
return this.behavior != null ? this.behavior.destination : null;
}
@Override
public void pathTo(BlockPos destination) {
this.pathTo0(destination, false);
}
private void pathTo0(BlockPos destination, boolean appendDestination) {
if (ctx.player() == null || ctx.player().dimension != -1) {
return;
}
this.onLostControl();
this.behavior = new ElytraBehavior(this.baritone, this, destination, appendDestination);
if (ctx.world() != null) {
this.behavior.repackChunks();
}
this.behavior.pathTo();
}
@Override
public void pathTo(Goal iGoal) {
final int x;
final int y;
final int z;
if (iGoal instanceof GoalXZ) {
GoalXZ goal = (GoalXZ) iGoal;
x = goal.getX();
y = 64;
z = goal.getZ();
} else if (iGoal instanceof GoalBlock) {
GoalBlock goal = (GoalBlock) iGoal;
x = goal.x;
y = goal.y;
z = goal.z;
} else {
throw new IllegalArgumentException("The goal must be a GoalXZ or GoalBlock");
}
if (y <= 0 || y >= 128) {
throw new IllegalArgumentException("The y of the goal is not between 0 and 128");
}
this.pathTo(new BlockPos(x, y, z));
}
@Override
public boolean isLoaded() {
return true;
}
@Override
public boolean isSafeToCancel() {
return !this.isActive() || !(this.state == State.FLYING || this.state == State.START_FLYING);
}
public enum State {
LOCATE_JUMP("Finding spot to jump off"),
PAUSE("Waiting for elytra path"),
GET_TO_JUMP("Walking to takeoff"),
START_FLYING("Begin flying"),
FLYING("Flying"),
LANDING("Landing");
public final String description;
State(String desc) {
this.description = desc;
}
}
@Override
public void onRenderPass(RenderEvent event) {
if (this.behavior != null) this.behavior.onRenderPass(event);
}
@Override
public void onWorldEvent(WorldEvent event) {
if (event.getWorld() != null && event.getState() == EventState.POST) {
// Exiting the world, just destroy
destroyBehaviorAsync();
}
}
@Override
public void onChunkEvent(ChunkEvent event) {
if (this.behavior != null) this.behavior.onChunkEvent(event);
}
@Override
public void onBlockChange(BlockChangeEvent event) {
if (this.behavior != null) this.behavior.onBlockChange(event);
}
@Override
public void onReceivePacket(PacketEvent event) {
if (this.behavior != null) this.behavior.onReceivePacket(event);
}
@Override
public void onPostTick(TickEvent event) {
IBaritoneProcess procThisTick = baritone.getPathingControlManager().mostRecentInControl().orElse(null);
if (this.behavior != null && procThisTick == this) this.behavior.onPostTick(event);
}
/**
* Custom calculation context which makes the player fall into lava
*/
public static final class WalkOffCalculationContext extends CalculationContext {
public WalkOffCalculationContext(IBaritone baritone) {
super(baritone, true);
this.allowFallIntoLava = true;
this.minFallHeight = 8;
this.maxFallHeightNoWater = 10000;
}
@Override
public double costOfPlacingAt(int x, int y, int z, IBlockState current) {
return COST_INF;
}
@Override
public double breakCostMultiplierAt(int x, int y, int z, IBlockState current) {
return COST_INF;
}
@Override
public double placeBucketCost() {
return COST_INF;
}
}
private static boolean isInBounds(BlockPos pos) {
return pos.getY() >= 0 && pos.getY() < 128;
}
private boolean isSafeBlock(Block block) {
return block == Blocks.NETHERRACK || block == Blocks.GRAVEL || block == Blocks.NETHER_BRICK;
}
private boolean isSafeBlock(BlockPos pos) {
return isSafeBlock(ctx.world().getBlockState(pos).getBlock());
}
private boolean isAtEdge(BlockPos pos) {
return !isSafeBlock(pos.north())
|| !isSafeBlock(pos.south())
|| !isSafeBlock(pos.east())
|| !isSafeBlock(pos.west())
// corners
|| !isSafeBlock(pos.north().west())
|| !isSafeBlock(pos.north().east())
|| !isSafeBlock(pos.south().west())
|| !isSafeBlock(pos.south().east());
}
private boolean isColumnAir(BlockPos landingSpot, int minHeight) {
BlockPos.MutableBlockPos mut = new BlockPos.MutableBlockPos(landingSpot);
final int maxY = mut.getY() + minHeight;
for (int y = mut.getY() + 1; y <= maxY; y++) {
mut.setPos(mut.getX(), y, mut.getZ());
if (!ctx.world().isAirBlock(mut)) {
return false;
}
}
return true;
}
private boolean hasAirBubble(BlockPos pos) {
final int radius = 4; // Half of the full width, rounded down, as we're counting blocks in each direction from the center
BlockPos.MutableBlockPos mut = new BlockPos.MutableBlockPos();
for (int x = -radius; x <= radius; x++) {
for (int y = -radius; y <= radius; y++) {
for (int z = -radius; z <= radius; z++) {
mut.setPos(pos.getX() + x, pos.getY() + y, pos.getZ() + z);
if (!ctx.world().isAirBlock(mut)) {
return false;
}
}
}
}
return true;
}
private BetterBlockPos checkLandingSpot(BlockPos pos, LongOpenHashSet checkedSpots) {
BlockPos.MutableBlockPos mut = new BlockPos.MutableBlockPos(pos);
while (mut.getY() >= 0) {
if (checkedSpots.contains(mut.toLong())) {
return null;
}
checkedSpots.add(mut.toLong());
Block block = ctx.world().getBlockState(mut).getBlock();
if (isSafeBlock(block)) {
if (!isAtEdge(mut)) {
return new BetterBlockPos(mut);
}
return null;
} else if (block != Blocks.AIR) {
return null;
}
mut.setPos(mut.getX(), mut.getY() - 1, mut.getZ());
}
return null; // void
}
private static final int LANDING_COLUMN_HEIGHT = 15;
private Set<BetterBlockPos> badLandingSpots = new HashSet<>();
private BetterBlockPos findSafeLandingSpot(BetterBlockPos start) {
Queue<BetterBlockPos> queue = new PriorityQueue<>(Comparator.<BetterBlockPos>comparingInt(pos -> (pos.x - start.x) * (pos.x - start.x) + (pos.z - start.z) * (pos.z - start.z)).thenComparingInt(pos -> -pos.y));
Set<BetterBlockPos> visited = new HashSet<>();
LongOpenHashSet checkedPositions = new LongOpenHashSet();
queue.add(start);
while (!queue.isEmpty()) {
BetterBlockPos pos = queue.poll();
if (ctx.world().isBlockLoaded(pos) && isInBounds(pos) && ctx.world().getBlockState(pos).getBlock() == Blocks.AIR) {
BetterBlockPos actualLandingSpot = checkLandingSpot(pos, checkedPositions);
if (actualLandingSpot != null && isColumnAir(actualLandingSpot, LANDING_COLUMN_HEIGHT) && hasAirBubble(actualLandingSpot.up(LANDING_COLUMN_HEIGHT)) && !badLandingSpots.contains(actualLandingSpot.up(LANDING_COLUMN_HEIGHT))) {
return actualLandingSpot.up(LANDING_COLUMN_HEIGHT);
}
if (visited.add(pos.north())) queue.add(pos.north());
if (visited.add(pos.east())) queue.add(pos.east());
if (visited.add(pos.south())) queue.add(pos.south());
if (visited.add(pos.west())) queue.add(pos.west());
if (visited.add(pos.up())) queue.add(pos.up());
if (visited.add(pos.down())) queue.add(pos.down());
}
}
return null;
}
}