/* * 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.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 Goal goal; private ElytraBehavior behavior; private ElytraProcess(Baritone baritone) { super(baritone); baritone.getGameEventHandler().registerEventListener(this); } public static 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(); this.pathTo(destination); this.repackChunks(); } @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("Failed to get to jump off spot, canceling"); return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); } if (ctx.player().isElytraFlying() && this.state != State.LANDING) { final BetterBlockPos last = this.behavior.pathManager.path.getLast(); if (last != null && ctx.player().getDistanceSqToCenter(last) < 1) { if (Baritone.settings().notificationOnPathComplete.value) { logNotification("Pathing complete", false); } if (Baritone.settings().disconnectOnArrival.value) { // don't be active when the user logs back in this.onLostControl(); ctx.world().sendQuittingDisconnectingPacket(); return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); } if (!goingToLandingSpot) { BetterBlockPos landingSpot = findSafeLandingSpot(); if (landingSpot != null) { this.pathTo(landingSpot); this.landingSpot = landingSpot; this.goingToLandingSpot = true; return this.onTick(calcFailed, isSafeToCancel); } // don't spam call findLandingSpot if it somehow fails (it's slow) this.goingToLandingSpot = true; } this.state = State.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(rotation, false); } else { this.onLostControl(); return new PathingCommand(null, PathingCommandType.REQUEST_PAUSE); } } else if (ctx.player().isElytraFlying()) { this.state = State.FLYING; this.goal = null; baritone.getInputOverrideHandler().clearAllKeys(); behavior.tick(); return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); } 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 (!behavior.clearView(new Vec3d(from), behavior.pathManager.getPath().getVec(0), false)) { onLostControl(); // TODO: Get to higher ground and then look again logDirect("Can't see start of path from jump spot, canceling"); return; } if (ex == null) { this.state = State.GET_TO_JUMP; return; } onLostControl(); }); this.state = State.PAUSE; } else { onLostControl(); logDirect("Jump off path didn't include a fall movement, canceling"); 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); } @Override public void onLostControl() { this.goal = null; this.goingToLandingSpot = false; this.state = State.START_FLYING; // TODO: null state? if (this.behavior != null) { this.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.onLostControl(); this.behavior = new ElytraBehavior(this.baritone, this, destination); 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 && this.behavior != null) { // Exiting the world, just destroy this.behavior.destroy(); this.behavior = null; } } @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 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 BetterBlockPos findSafeLandingSpot() { final BetterBlockPos start = ctx.playerFeet(); Queue queue = new PriorityQueue<>(Comparator.comparingInt(pos -> (pos.x - start.x) * (pos.x - start.x) + (pos.z - start.z) * (pos.z - start.z)).thenComparingInt(pos -> -pos.y)); Set 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, 15)) { return actualLandingSpot.up(15); } 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; } }