diff --git a/gradle.properties b/gradle.properties index 845c524f6..dfcdd8ece 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ forge_version=45.0.43 fabric_version=0.14.11 -nether_pathfinder_version=1.4.1 +nether_pathfinder_version=1.6 // These dependencies are used for common and tweaker // while mod loaders usually ship their own version diff --git a/src/api/java/baritone/api/Settings.java b/src/api/java/baritone/api/Settings.java index 44dabae2a..aae12889b 100644 --- a/src/api/java/baritone/api/Settings.java +++ b/src/api/java/baritone/api/Settings.java @@ -1543,6 +1543,36 @@ public final class Settings { */ public final Setting elytraChatSpam = new Setting<>(false); + /** + * May reduce memory usage by using a custom allocator for pathfinding + */ + public final Setting elytraCustomAllocator = new Setting<>(true); + + /** + * Allow the pathfinder to attempt flight in tighter spaces, useful in caves but can be dangerous. + */ + public final Setting elytraAllowTightSpaces = new Setting<>(false); + + /** + * Allow the pathfinder to fly above y 128 in the nether. + */ + public final Setting elytraAllowAboveRoof = new Setting<>(false); + + /** + * Allow the pathfinder to access the baritone cache to improve pathing + */ + public final Setting elytraUseCache = new Setting<>(true); + + /** + * Allow the pathfinder to fly above the build limit in the overworld and end. + */ + public final Setting elytraAllowAboveBuildLimit = new Setting<>(true); + + /** + * Minimum distance in blocks of an elytra trip before the pathfinder will try to fly above build limit. (Minimum: 32). Requires {@link #elytraAllowAboveBuildLimit} to be enabled. + */ + public final Setting elytraLongDistanceThreshold = new Setting<>(500); + /** * A map of lowercase setting field names to their respective setting */ diff --git a/src/api/java/baritone/api/pathing/elytra/IElytraContextFactory.java b/src/api/java/baritone/api/pathing/elytra/IElytraContextFactory.java new file mode 100644 index 000000000..37cdba9b5 --- /dev/null +++ b/src/api/java/baritone/api/pathing/elytra/IElytraContextFactory.java @@ -0,0 +1,26 @@ +/* + * 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.api.pathing.elytra; + +import baritone.api.utils.IPlayerContext; + +import java.nio.file.Path; + +public interface IElytraContextFactory { + IElytraPathfinderContext create(IPlayerContext ctx, Path cacheDir); +} diff --git a/src/api/java/baritone/api/pathing/elytra/IElytraPathfinderContext.java b/src/api/java/baritone/api/pathing/elytra/IElytraPathfinderContext.java new file mode 100644 index 000000000..99085f4f1 --- /dev/null +++ b/src/api/java/baritone/api/pathing/elytra/IElytraPathfinderContext.java @@ -0,0 +1,56 @@ +/* + * 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.api.pathing.elytra; + +import baritone.api.event.events.BlockChangeEvent; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.phys.Vec3; + +import java.util.concurrent.CompletableFuture; + +public interface IElytraPathfinderContext { + public boolean hasChunk(ChunkPos pos); + public void queueCacheCulling(int chunkX, int chunkZ, int maxDistanceBlocks); + public void queueForPacking(final LevelChunk chunkIn); + public void queueBlockUpdate(BlockChangeEvent event); + public CompletableFuture pathFindAsync(final BlockPos src, final BlockPos dst); + public boolean raytrace(final double startX, final double startY, final double startZ, + final double endX, final double endY, final double endZ); + public boolean raytrace(final Vec3 start, final Vec3 end); + public boolean raytrace(final int count, final double[] src, final double[] dst, final int visibility); + public void cancel(); + public void destroy(); + public long getSeed(); + + public void RLock(); + public void RUnlock(); + + public int getMaxHeight(); + public boolean passable(int x, int y, int z); + + public static final class Visibility { + public static final int ALL = 0; + public static final int NONE = 1; + public static final int ANY = 2; + private Visibility() {} + } +} + + diff --git a/src/main/java/baritone/process/elytra/UnpackedSegment.java b/src/api/java/baritone/api/pathing/elytra/UnpackedSegment.java similarity index 98% rename from src/main/java/baritone/process/elytra/UnpackedSegment.java rename to src/api/java/baritone/api/pathing/elytra/UnpackedSegment.java index e50ab3235..63a936741 100644 --- a/src/main/java/baritone/process/elytra/UnpackedSegment.java +++ b/src/api/java/baritone/api/pathing/elytra/UnpackedSegment.java @@ -15,7 +15,7 @@ * along with Baritone. If not, see . */ -package baritone.process.elytra; +package baritone.api.pathing.elytra; import baritone.api.utils.BetterBlockPos; import dev.babbaj.pathfinder.PathSegment; diff --git a/src/api/java/baritone/api/process/IElytraProcess.java b/src/api/java/baritone/api/process/IElytraProcess.java index 28328f901..9e0532999 100644 --- a/src/api/java/baritone/api/process/IElytraProcess.java +++ b/src/api/java/baritone/api/process/IElytraProcess.java @@ -17,6 +17,7 @@ package baritone.api.process; +import baritone.api.pathing.elytra.IElytraContextFactory; import baritone.api.pathing.goals.Goal; import net.minecraft.core.BlockPos; @@ -43,6 +44,11 @@ public interface IElytraProcess extends IBaritoneProcess { */ boolean isLoaded(); + + IElytraContextFactory getContextFactory(); + void setContextFactory(IElytraContextFactory factory); + + /* * FOR INTERNAL USE ONLY. MAY BE REMOVED AT ANY TIME. */ diff --git a/src/main/java/baritone/command/defaults/ElytraCommand.java b/src/main/java/baritone/command/defaults/ElytraCommand.java index 2f5eff352..5b362be5f 100644 --- a/src/main/java/baritone/command/defaults/ElytraCommand.java +++ b/src/main/java/baritone/command/defaults/ElytraCommand.java @@ -71,9 +71,6 @@ public void execute(String label, IArgConsumer args) throws CommandException { if (iGoal == null) { throw new CommandInvalidStateException("No goal has been set"); } - if (ctx.world().dimension() != Level.NETHER) { - throw new CommandInvalidStateException("Only works in the nether"); - } try { elytra.pathTo(iGoal); } catch (IllegalArgumentException ex) { @@ -85,7 +82,11 @@ public void execute(String label, IArgConsumer args) throws CommandException { final String action = args.getString(); switch (action) { case "reset": { - elytra.resetState(); + try { + elytra.resetState(); + } catch (IllegalArgumentException ex) { + throw new CommandInvalidStateException(ex.getMessage()); + } logDirect("Reset state but still flying to same goal"); break; } @@ -128,22 +129,26 @@ private Component suggest2b2tSeeds() { private void gatekeep() { MutableComponent gatekeep = Component.literal(""); gatekeep.append("To disable this message, enable the setting elytraTermsAccepted\n"); - gatekeep.append("Baritone Elytra is an experimental feature. It is only intended for long distance travel in the Nether using fireworks for vanilla boost. It will not work with any other mods (\"hacks\") for non-vanilla boost. "); + gatekeep.append("Baritone Elytra is an experimental feature. It is intended for long distance travel in the Nether but will also work in the Overworld, using fireworks for vanilla boost. It will not work with any other mods (\"hacks\") for non-vanilla boost. "); MutableComponent gatekeep2 = Component.literal("If you want Baritone to attempt to take off from the ground for you, you can enable the elytraAutoJump setting (not advisable on laggy servers!). "); gatekeep2.setStyle(gatekeep2.getStyle().withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal(Baritone.settings().prefix.value + "set elytraAutoJump true")))); gatekeep.append(gatekeep2); MutableComponent gatekeep3 = Component.literal("If you want Baritone to go slower, enable the elytraConserveFireworks setting and/or decrease the elytraFireworkSpeed setting. "); gatekeep3.setStyle(gatekeep3.getStyle().withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal(Baritone.settings().prefix.value + "set elytraConserveFireworks true\n" + Baritone.settings().prefix.value + "set elytraFireworkSpeed 0.6\n(the 0.6 number is just an example, tweak to your liking)")))); gatekeep.append(gatekeep3); - MutableComponent gatekeep4 = Component.literal("Baritone Elytra "); - MutableComponent red = Component.literal("wants to know the seed"); - red.setStyle(red.getStyle().withColor(ChatFormatting.RED).withUnderlined(true).withBold(true)); - gatekeep4.append(red); + MutableComponent gatekeep4 = Component.literal("Baritone Elytra for use in the "); + MutableComponent red1 = Component.literal("Nether"); + red1.setStyle(red1.getStyle().withColor(ChatFormatting.RED).withUnderlined(true).withBold(true)); + gatekeep4.append(red1); + gatekeep4.append(", "); + MutableComponent red2 = Component.literal("wants to know the seed"); + red2.setStyle(red2.getStyle().withColor(ChatFormatting.RED).withUnderlined(true).withBold(true)); + gatekeep4.append(red2); gatekeep4.append(" of the world you are in. If it doesn't have the correct seed, it will frequently backtrack. It uses the seed to generate terrain far beyond what you can see, since terrain obstacles in the Nether can be much larger than your render distance. "); gatekeep.append(gatekeep4); gatekeep.append("\n"); if (detectOn2b2t()) { - MutableComponent gatekeep5 = Component.literal("It looks like you're on 2b2t. "); + MutableComponent gatekeep5 = Component.literal("It looks like you're on 2b2t. Terrain prediction can be used but new nether terrain can not be predicted on 2b2t. "); gatekeep5.append(suggest2b2tSeeds()); if (!Baritone.settings().elytraPredictTerrain.value) { gatekeep5.append(Baritone.settings().prefix.value + "elytraPredictTerrain is currently disabled. "); diff --git a/src/main/java/baritone/process/CustomGoalProcess.java b/src/main/java/baritone/process/CustomGoalProcess.java index d0dca9cbf..3e34e0d8a 100644 --- a/src/main/java/baritone/process/CustomGoalProcess.java +++ b/src/main/java/baritone/process/CustomGoalProcess.java @@ -23,6 +23,7 @@ import baritone.api.process.PathingCommand; import baritone.api.process.PathingCommandType; import baritone.utils.BaritoneProcessHelper; +import net.minecraft.ChatFormatting; /** * As set by ExampleBaritoneControl or something idk @@ -57,7 +58,11 @@ public void setGoal(Goal goal) { this.goal = goal; this.mostRecentGoal = goal; if (baritone.getElytraProcess().isActive()) { - baritone.getElytraProcess().pathTo(goal); + try { + baritone.getElytraProcess().pathTo(goal); + } catch (IllegalArgumentException e) { + logDirect("Failed to update elytra goal because: " + e.getMessage(), ChatFormatting.RED); + } } if (this.state == State.NONE) { this.state = State.GOAL_SET; diff --git a/src/main/java/baritone/process/ElytraProcess.java b/src/main/java/baritone/process/ElytraProcess.java index e6d7ee34a..9677b5771 100644 --- a/src/main/java/baritone/process/ElytraProcess.java +++ b/src/main/java/baritone/process/ElytraProcess.java @@ -22,6 +22,7 @@ import baritone.api.event.events.*; import baritone.api.event.events.type.EventState; import baritone.api.event.listener.AbstractGameEventListener; +import baritone.api.pathing.elytra.IElytraContextFactory; import baritone.api.pathing.goals.Goal; import baritone.api.pathing.goals.GoalBlock; import baritone.api.pathing.goals.GoalXZ; @@ -38,12 +39,11 @@ 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.process.elytra.*; import baritone.utils.BaritoneProcessHelper; import baritone.utils.PathingCommandContext; import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import net.minecraft.ChatFormatting; import net.minecraft.core.BlockPos; import net.minecraft.core.NonNullList; import net.minecraft.world.entity.EquipmentSlot; @@ -54,9 +54,12 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.phys.Vec3; import java.util.*; +import java.util.concurrent.Semaphore; import static baritone.api.pathing.movement.ActionCosts.COST_INF; @@ -68,6 +71,11 @@ public class ElytraProcess extends BaritoneProcessHelper implements IBaritonePro private Goal goal; private ElytraBehavior behavior; private boolean predictingTerrain; + private boolean allowTight; + private boolean allowAboveBuildLimit; + private boolean allowAboveRoof; + private IElytraContextFactory contextFactory; + private final Semaphore behaviorSema = new Semaphore(1); @Override public void onLostControl() { @@ -109,15 +117,35 @@ public void resetState() { @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(); - } - if (predictingTerrain != Baritone.settings().elytraPredictTerrain.value) { - logDirect("elytraPredictTerrain setting changed, recalculating path"); - predictingTerrain = Baritone.settings().elytraPredictTerrain.value; - this.resetState(); + try { + final long seedSetting = Baritone.settings().elytraNetherSeed.value; + if (seedSetting != this.behavior.context.getSeed()) { + logDirect("Nether seed changed, recalculating path"); + this.resetState(); + } + if (predictingTerrain != Baritone.settings().elytraPredictTerrain.value && ctx.player().level.dimension() == Level.NETHER) { + logDirect("elytraPredictTerrain setting changed, recalculating path from scratch"); + predictingTerrain = Baritone.settings().elytraPredictTerrain.value; + this.resetState(); + } + if (allowTight != Baritone.settings().elytraAllowTightSpaces.value) { + logDirect("elytraAllowTightSpaces setting changed, recalculating path from scratch"); + allowTight = Baritone.settings().elytraAllowTightSpaces.value; + this.resetState(); + } + if (allowAboveBuildLimit != Baritone.settings().elytraAllowAboveBuildLimit.value) { + logDirect("elytraAllowAboveBuildLimit setting changed, recalculating path from scratch"); + allowAboveBuildLimit = Baritone.settings().elytraAllowAboveBuildLimit.value; + this.resetState(); + } + if (allowAboveRoof != Baritone.settings().elytraAllowAboveRoof.value && ctx.player().level.dimension() == Level.NETHER) { + logDirect("elytraAllowAboveRoof setting changed, recalculating path from scratch"); + allowAboveRoof = Baritone.settings().elytraAllowAboveRoof.value; + this.resetState(); + } + } catch (IllegalArgumentException e) { + logDirect(e.getMessage(), ChatFormatting.RED); + return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); } this.behavior.onTick(); @@ -178,7 +206,7 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { 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().position().y < endPos.y - LANDING_COLUMN_HEIGHT) { + if (ctx.player().position().y < endPos.y - this.landingColumnHeight) { logDirect("bad landing spot, trying again..."); landingSpotIsBad(endPos); } @@ -189,7 +217,12 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { behavior.landingMode = this.state == State.LANDING; this.goal = null; baritone.getInputOverrideHandler().clearAllKeys(); - behavior.tick(); + behavior.context.RLock(); + try { + behavior.tick(); + } finally { + behavior.context.RUnlock(); + } return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); } else if (this.state == State.LANDING) { if (ctx.playerMotion().multiply(1, 0, 1).length() > 0.001) { @@ -290,7 +323,10 @@ private void destroyBehaviorAsync() { ElytraBehavior behavior = this.behavior; if (behavior != null) { this.behavior = null; - Baritone.getExecutor().execute(behavior::destroy); + Baritone.getExecutor().execute(() -> { + behavior.destroy(); + behaviorSema.release(); + }); } } @@ -318,16 +354,33 @@ public BlockPos currentDestination() { @Override public void pathTo(BlockPos destination) { + int minY = ctx.world().dimensionType().minY(); + int maxY = (ctx.world().dimension() == Level.NETHER && !Baritone.settings().elytraAllowAboveRoof.value) ? 127 : Math.min(minY + 384, ctx.world().dimensionType().height() + minY); + if (destination.getY() < minY || destination.getY() >= maxY) { + throw new IllegalArgumentException("The goal must have a y value between " + minY + " and " + maxY); + } + + int playerY = (int)ctx.player().getY(); + if (playerY < minY || playerY >= maxY) { + throw new IllegalArgumentException("The player must have a y value between " + minY + " and " + maxY); + } + this.pathTo0(destination, false); } private void pathTo0(BlockPos destination, boolean appendDestination) { - if (ctx.player() == null || ctx.player().level.dimension() != Level.NETHER) { + if (ctx.player() == null) { return; } this.onLostControl(); - this.predictingTerrain = Baritone.settings().elytraPredictTerrain.value; - this.behavior = new ElytraBehavior(this.baritone, this, destination, appendDestination); + this.predictingTerrain = ctx.player().level.dimension() == Level.NETHER && Baritone.settings().elytraPredictTerrain.value; + this.allowTight = Baritone.settings().elytraAllowTightSpaces.value; + this.allowAboveBuildLimit = Baritone.settings().elytraAllowAboveBuildLimit.value; + this.allowAboveRoof = Baritone.settings().elytraAllowAboveRoof.value; + + this.behaviorSema.acquireUninterruptibly(); + this.behavior = new ElytraBehavior(this.baritone, this, getContextFactory().create(ctx, baritone.getWorldProvider().getCurrentWorld().directory.resolve("cache")), destination, appendDestination); + if (ctx.world() != null) { this.behavior.repackChunks(); } @@ -342,6 +395,7 @@ public void pathTo(Goal iGoal) { if (iGoal instanceof GoalXZ) { GoalXZ goal = (GoalXZ) iGoal; x = goal.getX(); + // ElytraBehavior will automatically change the destination height depending on if we're above or below the roof y = 64; z = goal.getZ(); } else if (iGoal instanceof GoalBlock) { @@ -352,9 +406,28 @@ public void pathTo(Goal iGoal) { } 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"); + + int minY = ctx.world().getMinBuildHeight(); + int maxPathfinderY = minY + 384; // maximum supported world size by the nether-pathfinder library + + final boolean inNether = ctx.player().level.dimension() == Level.NETHER; + final boolean allowRoof = Baritone.settings().elytraAllowAboveRoof.value; + final boolean allowBuildLimit = Baritone.settings().elytraAllowAboveBuildLimit.value; + + if(y < minY) { + throw new IllegalArgumentException("The goal must have a y value greater than " + (minY - 1)); + } + + if(inNether) { + if(!allowRoof && y > 127) { + throw new IllegalArgumentException("The goal must have a y value less than 128 in the nether when #elytraAllowAboveRoof is false"); + } + } + + if(!allowBuildLimit && y > maxPathfinderY) { + throw new IllegalArgumentException("The goal must have a y value less than " + maxPathfinderY + " when #elytraAllowAboveBuildLimit is false"); } + this.pathTo(new BlockPos(x, y, z)); } @@ -383,6 +456,24 @@ public boolean isLoaded() { return true; } + @Override + public IElytraContextFactory getContextFactory() { + if(this.contextFactory == null) { + if(ctx.world().dimension() == Level.NETHER) { + return Baritone.settings().elytraAllowAboveRoof.value && Baritone.settings().elytraAllowAboveBuildLimit.value + ? new SkyPathfinderContextFactory() + : new NetherPathfinderContextFactory(); + } + return Baritone.settings().elytraAllowAboveBuildLimit.value ? new SkyPathfinderContextFactory() : new NetherPathfinderContextFactory(); + } + return this.contextFactory; + } + + @Override + public void setContextFactory(IElytraContextFactory factory) { + this.contextFactory = factory == null ? new NetherPathfinderContextFactory() : factory; + } + @Override public boolean isSafeToCancel() { return !this.isActive() || !(this.state == State.FLYING || this.state == State.START_FLYING); @@ -465,12 +556,20 @@ public double placeBucketCost() { } } - private static boolean isInBounds(BlockPos pos) { - return pos.getY() >= 0 && pos.getY() < 128; + private static boolean isInBounds(Level dim, BlockPos pos) { + DimensionType dimType = dim.dimensionType(); + int minY = dimType.minY(); + int maxY = (dim.dimension() == Level.NETHER && !Baritone.settings().elytraAllowAboveRoof.value) ? 127 : Math.min(minY + 384, dimType.height() + minY); + return pos.getY() >= minY && pos.getY() < maxY; } private boolean isSafeBlock(Block block) { - return block == Blocks.NETHERRACK || block == Blocks.GRAVEL || (block == Blocks.NETHER_BRICKS && Baritone.settings().elytraAllowLandOnNetherFortress.value); + return block == Blocks.NETHERRACK || block == Blocks.GRAVEL || block == Blocks.SOUL_SAND || block == Blocks.SOUL_SOIL || (block == Blocks.NETHER_BRICKS && Baritone.settings().elytraAllowLandOnNetherFortress.value) + || block == Blocks.STONE || block == Blocks.DEEPSLATE || block == Blocks.GRASS_BLOCK || block == Blocks.SAND || block == Blocks.RED_SAND || block == Blocks.TERRACOTTA + || block == Blocks.SNOW || block == Blocks.ICE || block == Blocks.MYCELIUM || block == Blocks.PODZOL + || block == Blocks.DARK_OAK_LEAVES || block == Blocks.JUNGLE_LEAVES + || block == Blocks.END_STONE || block == Blocks.BEDROCK + || block == Blocks.OBSIDIAN || block == Blocks.COBBLESTONE; } private boolean isSafeBlock(BlockPos pos) { @@ -520,7 +619,7 @@ private boolean hasAirBubble(BlockPos pos) { private BetterBlockPos checkLandingSpot(BlockPos pos, LongOpenHashSet checkedSpots) { BlockPos.MutableBlockPos mut = new BlockPos.MutableBlockPos(pos.getX(), pos.getY(), pos.getZ()); - while (mut.getY() >= 0) { + while (mut.getY() >= ctx.world().dimensionType().minY()) { if (checkedSpots.contains(mut.asLong())) { return null; } @@ -540,10 +639,20 @@ private BetterBlockPos checkLandingSpot(BlockPos pos, LongOpenHashSet checkedSpo return null; // void } - private static final int LANDING_COLUMN_HEIGHT = 15; + private static final int SHORT_LANDING_COLUMN_HEIGHT = 15; + private static final int LONG_LANDING_COLUMN_HEIGHT = 39; + private int landingColumnHeight = SHORT_LANDING_COLUMN_HEIGHT; private Set badLandingSpots = new HashSet<>(); private BetterBlockPos findSafeLandingSpot(BetterBlockPos start) { + if(ctx.player().getY() > ctx.world().getHeight(Heightmap.Types.MOTION_BLOCKING, start.getX(), start.getZ())) { + return findSafeLandingSpot_heightmap(start); + } else { + return findSafeLandingSpot_underground(start); + } + } + + private BetterBlockPos findSafeLandingSpot_underground(BetterBlockPos start) { 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(); @@ -551,10 +660,13 @@ private BetterBlockPos findSafeLandingSpot(BetterBlockPos start) { while (!queue.isEmpty()) { BetterBlockPos pos = queue.poll(); - if (ctx.world().isLoaded(pos) && isInBounds(pos) && ctx.world().getBlockState(pos).getBlock() == Blocks.AIR) { + if (ctx.world().isLoaded(pos) && isInBounds(ctx.world(), pos) && ctx.world().getBlockState(pos).getBlock() == Blocks.AIR) { BetterBlockPos actualLandingSpot = checkLandingSpot(pos, checkedPositions); - if (actualLandingSpot != null && isColumnAir(actualLandingSpot, LANDING_COLUMN_HEIGHT) && hasAirBubble(actualLandingSpot.above(LANDING_COLUMN_HEIGHT)) && !badLandingSpots.contains(actualLandingSpot.above(LANDING_COLUMN_HEIGHT))) { - return actualLandingSpot.above(LANDING_COLUMN_HEIGHT); + if(actualLandingSpot != null) { + landingColumnHeight = SHORT_LANDING_COLUMN_HEIGHT; + if (isColumnAir(actualLandingSpot, this.landingColumnHeight) && hasAirBubble(actualLandingSpot.above(this.landingColumnHeight)) && !badLandingSpots.contains(actualLandingSpot.above(this.landingColumnHeight))) { + return actualLandingSpot.above(this.landingColumnHeight); + } } if (visited.add(pos.north())) queue.add(pos.north()); if (visited.add(pos.east())) queue.add(pos.east()); @@ -566,4 +678,34 @@ private BetterBlockPos findSafeLandingSpot(BetterBlockPos start) { } return null; } + + private BetterBlockPos findSafeLandingSpot_heightmap(BetterBlockPos start) { + 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 qPos = queue.poll(); + if (!ctx.world().isLoaded(qPos)) continue; + + var height = ctx.world().getHeight(Heightmap.Types.MOTION_BLOCKING, qPos.getX(), qPos.getZ()); + var pos = new BetterBlockPos(qPos.getX(), height+1, qPos.getZ()); + if (ctx.world().isLoaded(pos) && isInBounds(ctx.world(), pos) && ctx.world().getBlockState(pos).getBlock() == Blocks.AIR) { + BetterBlockPos actualLandingSpot = checkLandingSpot(pos, checkedPositions); + if(actualLandingSpot != null) { + landingColumnHeight = ctx.playerFeet().y - actualLandingSpot.y < LONG_LANDING_COLUMN_HEIGHT ? SHORT_LANDING_COLUMN_HEIGHT : LONG_LANDING_COLUMN_HEIGHT; + if (hasAirBubble(actualLandingSpot.above(landingColumnHeight)) && !badLandingSpots.contains(actualLandingSpot.above(landingColumnHeight))) { + return actualLandingSpot.above(landingColumnHeight); + } + } + + 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()); + } + } + return null; + } } diff --git a/src/main/java/baritone/process/elytra/BlockStateOctreeInterface.java b/src/main/java/baritone/process/elytra/BlockStateOctreeInterface.java index 7db0e2d64..52fdfdd66 100644 --- a/src/main/java/baritone/process/elytra/BlockStateOctreeInterface.java +++ b/src/main/java/baritone/process/elytra/BlockStateOctreeInterface.java @@ -19,6 +19,7 @@ import dev.babbaj.pathfinder.NetherPathfinder; import dev.babbaj.pathfinder.Octree; +import net.minecraft.world.level.dimension.DimensionType; /** * @author Brady @@ -27,6 +28,7 @@ public final class BlockStateOctreeInterface { private final NetherPathfinderContext context; private final long contextPtr; + private final int minY; transient long chunkPtr; // Guarantee that the first lookup will fetch the context by setting MAX_VALUE @@ -36,10 +38,12 @@ public final class BlockStateOctreeInterface { public BlockStateOctreeInterface(final NetherPathfinderContext context) { this.context = context; this.contextPtr = context.context; + this.minY = context.minY; } public boolean get0(final int x, final int y, final int z) { - if ((y | (127 - y)) < 0) { + final int adjustedY = y - this.minY; + if (adjustedY < 0 || adjustedY > 383) { return false; } final int chunkX = x >> 4; @@ -47,8 +51,8 @@ public boolean get0(final int x, final int y, final int z) { if (this.chunkPtr == 0 | ((chunkX ^ this.prevChunkX) | (chunkZ ^ this.prevChunkZ)) != 0) { this.prevChunkX = chunkX; this.prevChunkZ = chunkZ; - this.chunkPtr = NetherPathfinder.getOrCreateChunk(this.contextPtr, chunkX, chunkZ); + this.chunkPtr = NetherPathfinder.getChunkOrDefault(this.contextPtr, chunkX, chunkZ, true); } - return Octree.getBlock(this.chunkPtr, x & 0xF, y & 0x7F, z & 0xF); + return Octree.getBlock(this.chunkPtr, x & 0xF, adjustedY, z & 0xF); } } diff --git a/src/main/java/baritone/process/elytra/ElytraBehavior.java b/src/main/java/baritone/process/elytra/ElytraBehavior.java index d4913f466..d833b869a 100644 --- a/src/main/java/baritone/process/elytra/ElytraBehavior.java +++ b/src/main/java/baritone/process/elytra/ElytraBehavior.java @@ -22,6 +22,8 @@ import baritone.api.behavior.look.IAimProcessor; import baritone.api.behavior.look.ITickableAimProcessor; import baritone.api.event.events.*; +import baritone.api.pathing.elytra.IElytraPathfinderContext; +import baritone.api.pathing.elytra.UnpackedSegment; import baritone.api.pathing.goals.GoalBlock; import baritone.api.utils.*; import baritone.api.utils.input.Input; @@ -45,6 +47,7 @@ import net.minecraft.world.item.Items; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; import net.minecraft.world.level.chunk.ChunkSource; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.material.Material; @@ -74,7 +77,7 @@ public final class ElytraBehavior implements Helper { private List visiblePath; // :sunglasses: - public final NetherPathfinderContext context; + public IElytraPathfinderContext context; public final PathManager pathManager; private final ElytraProcess process; @@ -101,7 +104,6 @@ public final class ElytraBehavior implements Helper { private final int[] nextTickBoostCounter; private BlockStateInterface bsi; - private final BlockStateOctreeInterface boi; public final BetterBlockPos destination; private final boolean appendDestination; @@ -116,7 +118,7 @@ public final class ElytraBehavior implements Helper { private int invTickCountdown = 0; private final Queue invTransactionQueue = new LinkedList<>(); - public ElytraBehavior(Baritone baritone, ElytraProcess process, BlockPos destination, boolean appendDestination) { + public ElytraBehavior(Baritone baritone, ElytraProcess process, IElytraPathfinderContext context, BlockPos destination, boolean appendDestination) { this.baritone = baritone; this.ctx = baritone.getPlayerContext(); this.clearLines = new CopyOnWriteArrayList<>(); @@ -127,9 +129,7 @@ public ElytraBehavior(Baritone baritone, ElytraProcess process, BlockPos destina this.appendDestination = appendDestination; this.solverExecutor = Executors.newSingleThreadExecutor(); this.nextTickBoostCounter = new int[2]; - - this.context = new NetherPathfinderContext(Baritone.settings().elytraNetherSeed.value); - this.boi = new BlockStateOctreeInterface(context); + this.context = context; } public final class PathManager { @@ -159,9 +159,18 @@ public void tick() { this.ticksNearUnchanged = 0; } - // Obstacles are more important than an incomplete path, handle those first. - this.pathfindAroundObstacles(); - this.attemptNextSegment(); + int minY = ctx.world().dimensionType().minY(); + int y = ctx.playerFeet().y; + if (y >= minY && y < minY + context.getMaxHeight()) { + context.RLock(); + try { + // Obstacles are more important than an incomplete path, handle those first. + this.pathfindAroundObstacles(); + } finally { + context.RUnlock(); + } + this.attemptNextSegment(); + } } public CompletableFuture pathToDestination() { @@ -170,7 +179,7 @@ public CompletableFuture pathToDestination() { public CompletableFuture pathToDestination(final BlockPos from) { final long start = System.nanoTime(); - return this.path0(from, ElytraBehavior.this.destination, UnaryOperator.identity()) + return this.path0(from, destinationFixed(), UnaryOperator.identity()) .thenRun(() -> { final double distance = this.path.get(0).distanceTo(this.path.get(this.path.size() - 1)); if (this.completePath) { @@ -201,7 +210,7 @@ public CompletableFuture pathRecalcSegment(final OptionalInt upToIncl) { final List after = upToIncl.isPresent() ? this.path.subList(upToIncl.getAsInt() + 1, this.path.size()) : Collections.emptyList(); final boolean complete = this.completePath; - return this.path0(ctx.playerFeet(), upToIncl.isPresent() ? this.path.get(upToIncl.getAsInt()) : ElytraBehavior.this.destination, segment -> segment.append(after.stream(), complete || (segment.isFinished() && !upToIncl.isPresent()))) + return this.path0(ctx.playerFeet(), upToIncl.isPresent() ? fixDestination(this.path.get(upToIncl.getAsInt())) : destinationFixed(), segment -> segment.append(after.stream(), complete || (segment.isFinished() && !upToIncl.isPresent()))) .whenComplete((result, ex) -> { this.recalculating = false; if (ex != null) { @@ -225,10 +234,10 @@ public void pathNextSegment(final int afterIncl) { final long start = System.nanoTime(); final BetterBlockPos pathStart = this.path.get(afterIncl); - this.path0(pathStart, ElytraBehavior.this.destination, segment -> segment.prepend(before.stream())) + this.path0(pathStart, destinationFixed(), segment -> segment.prepend(before.stream())) .thenRun(() -> { final int recompute = this.path.size() - before.size() - 1; - final double distance = this.path.get(0).distanceTo(this.path.get(recompute)); + final double distance = recompute > 0 ? this.path.get(0).distanceTo(this.path.get(recompute)) : 0; if (this.completePath) { logVerbose(String.format("Computed path (%.1f blocks in %.4f seconds)", distance, (System.nanoTime() - start) / 1e9d)); @@ -265,13 +274,13 @@ public void clear() { private void setPath(final UnpackedSegment segment) { List path = segment.collect(); if (ElytraBehavior.this.appendDestination) { - BlockPos dest = ElytraBehavior.this.destination; + BlockPos dest = destinationFixed(); BlockPos last = !path.isEmpty() ? path.get(path.size() - 1) : null; if (last != null && ElytraBehavior.this.clearView(Vec3.atLowerCornerOf(dest), Vec3.atLowerCornerOf(last), false)) { path.add(new BetterBlockPos(dest)); } else { - logDirect("unable to land at " + ElytraBehavior.this.destination); - process.landingSpotIsBad(new BetterBlockPos(ElytraBehavior.this.destination)); + logDirect("unable to land at " + dest); + process.landingSpotIsBad(new BetterBlockPos(dest)); } } this.path = new NetherPath(path); @@ -292,11 +301,11 @@ public int getNear() { // mickey resigned private CompletableFuture path0(BlockPos src, BlockPos dst, UnaryOperator operator) { return ElytraBehavior.this.context.pathFindAsync(src, dst) - .thenApply(UnpackedSegment::from) .thenApply(operator) .thenAcceptAsync(this::setPath, ctx.minecraft()::execute); } + // required read lock to be held private void pathfindAroundObstacles() { if (this.recalculating) { return; @@ -336,7 +345,8 @@ private void pathfindAroundObstacles() { // obstacle. where do we return to pathing? // if the end of render distance is closer to goal, then that's fine, otherwise we'd be "digging our hole deeper" and making an already bad backtrack worse OptionalInt rejoinMainPathAt; - if (this.path.get(rangeEndExcl - 1).distanceSq(ElytraBehavior.this.destination) < ctx.playerFeet().distanceSq(ElytraBehavior.this.destination)) { + var dest = destinationFixed(); + if (this.path.get(rangeEndExcl - 1).distanceSq(dest) < ctx.playerFeet().distanceSq(dest)) { rejoinMainPathAt = OptionalInt.of(rangeEndExcl - 1); // rejoin after current render distance } else { rejoinMainPathAt = OptionalInt.empty(); // large backtrack detected. ignore render distance, rejoin later on @@ -370,7 +380,9 @@ private void attemptNextSegment() { } final int last = this.path.size() - 1; - if (!this.completePath && ctx.world().isLoaded(this.path.get(last))) { + final BetterBlockPos lastPos = this.path.get(this.path.size() - 1); + // `ctx.world().isLoaded` cannot be used here as it returns false is the y-value is beyond the build limits. + if (!this.completePath && ctx.world().getChunkSource().hasChunk(lastPos.x >> 4,lastPos.z >> 4)) { this.pathNextSegment(last); } } @@ -508,12 +520,15 @@ public void repackChunks() { } public void onTick() { - synchronized (this.context.cullingLock) { + this.context.RLock(); + try { this.onTick0(); + } finally { + this.context.RUnlock(); } final long now = System.currentTimeMillis(); if ((now - this.timeLastCacheCull) / 1000 > Baritone.settings().elytraTimeBetweenCacheCullSecs.value) { - this.context.queueCacheCulling(ctx.player().chunkPosition().x, ctx.player().chunkPosition().z, Baritone.settings().elytraCacheCullDistance.value, this.boi); + this.context.queueCacheCulling(ctx.player().chunkPosition().x, ctx.player().chunkPosition().z, Baritone.settings().elytraCacheCullDistance.value); this.timeLastCacheCull = now; } } @@ -523,7 +538,7 @@ private void onTick0() { this.pendingSolution = null; if (this.solver != null) { try { - this.pendingSolution = this.solver.get(); + this.pendingSolution = this.solver.get(50 * 5, TimeUnit.MILLISECONDS); } catch (Exception ignored) { // it doesn't matter if get() fails since the solution can just be recalculated synchronously } finally { @@ -554,7 +569,7 @@ private void onTick0() { final List path = this.pathManager.getPath(); if (path.isEmpty()) { return; - } else if (this.destination == null) { + } else if (this.destination == null) { // null check why???? this.pathManager.clear(); return; } @@ -577,7 +592,6 @@ public void tick() { if (this.pathManager.getPath().isEmpty()) { return; } - trySwapElytra(); if (ctx.player().horizontalCollision) { @@ -637,11 +651,19 @@ public void onPostTick(TickEvent event) { this.pathManager.updatePlayerNear(); final SolverContext context = this.new SolverContext(true); - this.solver = this.solverExecutor.submit(() -> this.solveAngles(context)); + this.solver = this.solverExecutor.submit(() -> { + this.context.RLock(); + try { + return this.solveAngles(context); + } finally { + this.context.RUnlock(); + } + }); this.solveNextTick = false; } } + // calls passable which requires a read lock private Solution solveAngles(final SolverContext context) { final NetherPath path = context.path; final int playerNear = landingMode ? path.size() - 1 : context.playerNear; @@ -999,7 +1021,7 @@ private boolean isHitboxClear(final SolverContext context, final Vec3 dest, fina return clear; } - return this.context.raytrace(8, src, dst, NetherPathfinderContext.Visibility.ALL); + return this.context.raytrace(8, src, dst, IElytraPathfinderContext.Visibility.ALL); } public boolean clearView(Vec3 start, Vec3 dest, boolean ignoreLava) { @@ -1262,12 +1284,13 @@ private static Vec3 step(final Vec3 motion, final Vec3 lookDirection, final floa return new Vec3(motionX, motionY, motionZ); } + // any call to this must be done with the lock held private boolean passable(int x, int y, int z, boolean ignoreLava) { if (ignoreLava) { final Material mat = this.bsi.get0(x, y, z).getMaterial(); return mat == Material.AIR || mat == Material.LAVA; } else { - return !this.boi.get0(x, y, z); + return this.context.passable(x, y, z); } } @@ -1323,4 +1346,21 @@ void logVerbose(String message) { logDebug(message); } } + + // so we don't get stuck trying to pathfind through the roof + private BetterBlockPos fixDestination(BetterBlockPos dst) { + if (ctx.world().dimension() == Level.NETHER) { + if (ctx.player().getY() >= 128 && dst.y < 128) { + return new BetterBlockPos(dst.x, 128, dst.z); + } + else if (ctx.player().getY() < 128 && dst.y >= 128) { + return new BetterBlockPos(dst.x, 64, dst.z); + } + } + return dst; + } + + private BetterBlockPos destinationFixed() { + return fixDestination(this.destination); + } } diff --git a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java index aa9f4965a..db52ad3c4 100644 --- a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java +++ b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java @@ -19,103 +19,171 @@ import baritone.Baritone; import baritone.api.event.events.BlockChangeEvent; +import baritone.api.pathing.elytra.IElytraPathfinderContext; +import baritone.api.pathing.elytra.UnpackedSegment; import baritone.utils.accessor.IPalettedContainer; import dev.babbaj.pathfinder.NetherPathfinder; import dev.babbaj.pathfinder.Octree; import dev.babbaj.pathfinder.PathSegment; import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceKey; import net.minecraft.util.BitStorage; import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.chunk.PalettedContainer; import net.minecraft.world.phys.Vec3; +import sun.misc.Unsafe; import java.lang.ref.SoftReference; +import java.lang.reflect.Field; +import java.nio.file.Path; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import static net.minecraft.world.level.chunk.LevelChunkSection.SECTION_SIZE; /** * @author Brady */ -public final class NetherPathfinderContext { +public final class NetherPathfinderContext implements IElytraPathfinderContext { + private static final Unsafe UNSAFE; + static { + try { + Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + UNSAFE = (Unsafe) f.get(null); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } private static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState(); // This lock must be held while there are active pointers to chunks in java, // but we just hold it for the entire tick so we don't have to think much about it. - public final Object cullingLock = new Object(); + public final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); + public final ReentrantReadWriteLock.ReadLock readLock = rwl.readLock(); + public final ReentrantReadWriteLock.WriteLock writeLock = rwl.writeLock(); + private final int maxHeight; // Visible for access in BlockStateOctreeInterface final long context; private final long seed; - private final ExecutorService executor; + // write locked operations + private final ExecutorService writeExecutor = Executors.newSingleThreadExecutor(); + // operations that don't make changes to the chunk cache. could use multiple threads but i'm not sure if it would cause problems. + private final ExecutorService readExecutor = Executors.newSingleThreadExecutor(); + private final ResourceKey dimension; + final int minY; + private final BlockStateOctreeInterface boi; - public NetherPathfinderContext(long seed) { - this.context = NetherPathfinder.newContext(seed); + public NetherPathfinderContext(long seed, Path cache, Level world) { + this.dimension = world.dimension(); + this.minY = world.dimensionType().minY(); + final int dim; + if (this.dimension == Level.NETHER) dim = NetherPathfinder.DIMENSION_NETHER; + else if (this.dimension == Level.END) dim = NetherPathfinder.DIMENSION_END; + else dim = NetherPathfinder.DIMENSION_OVERWORLD; + int height = Math.min(world.dimensionType().height(), 384); + if (!Baritone.settings().elytraAllowAboveRoof.value && dim == NetherPathfinder.DIMENSION_NETHER) height = Math.min(height, 128); + this.maxHeight = height; + this.context = NetherPathfinder.newContext(seed, cache != null ? cache.toString() : null, dim, height, Baritone.settings().elytraCustomAllocator.value); this.seed = seed; - this.executor = Executors.newSingleThreadExecutor(); + this.boi = new BlockStateOctreeInterface(this); } public boolean hasChunk(ChunkPos pos) { return NetherPathfinder.hasChunkFromJava(this.context, pos.x, pos.z); } - public void queueCacheCulling(int chunkX, int chunkZ, int maxDistanceBlocks, BlockStateOctreeInterface boi) { - this.executor.execute(() -> { - synchronized (this.cullingLock) { - boi.chunkPtr = 0L; + public void queueCacheCulling(int chunkX, int chunkZ, int maxDistanceBlocks) { + this.writeExecutor.execute(() -> { + writeLock.lock(); + try { + this.boi.chunkPtr = 0L; NetherPathfinder.cullFarChunks(this.context, chunkX, chunkZ, maxDistanceBlocks); + } finally { + writeLock.unlock(); } }); } public void queueForPacking(final LevelChunk chunkIn) { final SoftReference ref = new SoftReference<>(chunkIn); - this.executor.execute(() -> { + this.writeExecutor.execute(() -> { // TODO: Prioritize packing recent chunks and/or ones that the path goes through, // and prune the oldest chunks per chunkPackerQueueMaxSize final LevelChunk chunk = ref.get(); if (chunk != null) { - long ptr = NetherPathfinder.getOrCreateChunk(this.context, chunk.getPos().x, chunk.getPos().z); - writeChunkData(chunk, ptr); + writeLock.lock(); + try { + // we might free this chunk + this.boi.chunkPtr = 0L; + long ptr = NetherPathfinder.allocateAndInsertChunk(this.context, chunk.getPos().x, chunk.getPos().z); + writeChunkData(chunk, ptr); + } finally { + writeLock.unlock(); + } } }); } public void queueBlockUpdate(BlockChangeEvent event) { - this.executor.execute(() -> { + this.writeExecutor.execute(() -> { ChunkPos chunkPos = event.getChunkPos(); - long ptr = NetherPathfinder.getChunkPointer(this.context, chunkPos.x, chunkPos.z); - if (ptr == 0) return; // this shouldn't ever happen - event.getBlocks().forEach(pair -> { - BlockPos pos = pair.first(); - if (pos.getY() >= 128) return; - boolean isSolid = pair.second() != AIR_BLOCK_STATE; - Octree.setBlock(ptr, pos.getX() & 15, pos.getY(), pos.getZ() & 15, isSolid); - }); + // not inserting or deleting from the cache hashmap but it would still be bad for this function to race with itself + writeLock.lock(); + try { + long ptr = NetherPathfinder.getChunk(this.context, chunkPos.x, chunkPos.z); + if (ptr == 0) return; // this shouldn't ever happen + event.getBlocks().forEach(pair -> { + BlockPos pos = pair.first().below(minY); + if (pos.getY() < 0 || pos.getY() >= 384) return; + boolean isSolid = pair.second() != AIR_BLOCK_STATE; + Octree.setBlock(ptr, pos.getX() & 15, pos.getY(), pos.getZ() & 15, isSolid); + }); + } finally { + writeLock.unlock(); + } }); } - public CompletableFuture pathFindAsync(final BlockPos src, final BlockPos dst) { + public CompletableFuture pathFindAsync(final BlockPos src, final BlockPos dst) { + final BlockPos adjustedSrc = src.below(minY); + final BlockPos adjustedDst = dst.below(minY); + boolean generate = Baritone.settings().elytraPredictTerrain.value && this.dimension == Level.NETHER; + Lock l = generate ? writeLock : readLock; + ExecutorService exec = generate ? writeExecutor : readExecutor; return CompletableFuture.supplyAsync(() -> { - final PathSegment segment = NetherPathfinder.pathFind( - this.context, - src.getX(), src.getY(), src.getZ(), - dst.getX(), dst.getY(), dst.getZ(), - true, - false, - 10000, - !Baritone.settings().elytraPredictTerrain.value - ); - if (segment == null) { - throw new PathCalculationException("Path calculation failed"); + l.lock(); + try { + final PathSegment segment = NetherPathfinder.pathFind( + this.context, + adjustedSrc.getX(), adjustedSrc.getY(), adjustedSrc.getZ(), + adjustedDst.getX(), adjustedDst.getY(), adjustedDst.getZ(), + !Baritone.settings().elytraAllowTightSpaces.value, // atleastX4 + false, // refine + 10000, // timeoutMs + !generate, // useAirIfChunkNotLoaded + // TODO: Determine appropriate cost value + 8.0 // fakeChunkCost + ); + if (segment == null) { + throw new PathCalculationException("Path calculation failed"); + } + + return new UnpackedSegment(UnpackedSegment.from(segment).collect().stream().map(pos -> pos.above(minY)), segment.finished); + } finally { + l.unlock(); } - return segment; - }, this.executor); + }, exec); } /** @@ -132,7 +200,9 @@ public CompletableFuture pathFindAsync(final BlockPos src, final Bl */ public boolean raytrace(final double startX, final double startY, final double startZ, final double endX, final double endY, final double endZ) { - return NetherPathfinder.isVisible(this.context, NetherPathfinder.CACHE_MISS_SOLID, startX, startY, startZ, endX, endY, endZ); + final double adjustedStartY = startY - this.minY; + final double adjustedEndY = endY - this.minY; + return NetherPathfinder.isVisible(this.context, NetherPathfinder.CACHE_MISS_SOLID, startX, adjustedStartY, startZ, endX, adjustedEndY, endZ); } /** @@ -144,10 +214,21 @@ public boolean raytrace(final double startX, final double startY, final double s * @return {@code true} if there is visibility between the points */ public boolean raytrace(final Vec3 start, final Vec3 end) { - return NetherPathfinder.isVisible(this.context, NetherPathfinder.CACHE_MISS_SOLID, start.x, start.y, start.z, end.x, end.y, end.z); + final Vec3 adjustedStart = start.subtract(0, this.minY, 0); + final Vec3 adjustedEnd = end.subtract(0, this.minY, 0); + return NetherPathfinder.isVisible(this.context, NetherPathfinder.CACHE_MISS_SOLID, adjustedStart.x, adjustedStart.y, adjustedStart.z, adjustedEnd.x, adjustedEnd.y, adjustedEnd.z); } public boolean raytrace(final int count, final double[] src, final double[] dst, final int visibility) { + if (src.length != count * 3 || dst.length != count * 3) { + throw new IllegalArgumentException("Bad array lengths"); + } + + for(int i = 1; i < src.length; i+= 3) { + src[i] -= this.minY; + dst[i] -= this.minY; + } + switch (visibility) { case Visibility.ALL: return NetherPathfinder.isVisibleMulti(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, false) == -1; @@ -161,9 +242,22 @@ public boolean raytrace(final int count, final double[] src, final double[] dst, } public void raytrace(final int count, final double[] src, final double[] dst, final boolean[] hitsOut, final double[] hitPosOut) { + if (src.length != count * 3 || dst.length != count * 3) { + throw new IllegalArgumentException("Bad array lengths"); + } + + for(int i = 1; i < src.length; i+= 3) { + src[i] -= this.minY; + dst[i] -= this.minY; + } + NetherPathfinder.raytrace(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, hitsOut, hitPosOut); } + public boolean passable(int x, int y, int z) { + return !this.boi.get0(x, y, z); + } + public void cancel() { NetherPathfinder.cancel(this.context); } @@ -171,10 +265,12 @@ public void cancel() { public void destroy() { this.cancel(); // Ignore anything that was queued up, just shutdown the executor - this.executor.shutdownNow(); + this.readExecutor.shutdownNow(); + this.writeExecutor.shutdownNow(); try { - while (!this.executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS)) {} + while (!this.readExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS)) {} + while (!this.writeExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS)) {} } catch (InterruptedException e) { e.printStackTrace(); } @@ -186,16 +282,47 @@ public long getSeed() { return this.seed; } - private static void writeChunkData(LevelChunk chunk, long ptr) { + public void RLock() { + this.readLock.lock(); + } + + public void RUnlock() { + this.readLock.unlock(); + } + + public int getMaxHeight() { + return this.maxHeight; + } + + private static void writeChunkData(LevelChunk chunk, long chunkPtr) { try { LevelChunkSection[] chunkInternalStorageArray = chunk.getSections(); - for (int y0 = 0; y0 < 8; y0++) { + final int maxSections = Math.min(chunkInternalStorageArray.length, 24); // pathfinder support stops at 384/16 sections + for (int y0 = 0; y0 < maxSections; y0++) { final LevelChunkSection extendedblockstorage = chunkInternalStorageArray[y0]; - if (extendedblockstorage == null) { + if (extendedblockstorage == null || extendedblockstorage.hasOnlyAir()) { continue; } final PalettedContainer bsc = extendedblockstorage.getStates(); - final int airId = ((IPalettedContainer) bsc).getPalette().idFor(AIR_BLOCK_STATE); + var palette = ((IPalettedContainer) bsc).getPalette(); + // Mushrooms spawn on the roof and writing them as solid will cause pages to be unnecessarily allocated. + // idFor can't be used because it may update the palette + int airId = -1; + int caveAirId = -1; + int redMushroomId = -1; + int brownMushroomId = -1; + for (int i = 0; i < palette.getSize(); i++) { + BlockState bs = palette.valueFor(i); + if (bs == Blocks.AIR.defaultBlockState()) airId = i; + else if (bs == Blocks.CAVE_AIR.defaultBlockState()) caveAirId = i; + else if (bs == Blocks.RED_MUSHROOM.defaultBlockState()) redMushroomId = i; + else if (bs == Blocks.BROWN_MUSHROOM.defaultBlockState()) brownMushroomId = i; + } + if (airId == -1 & caveAirId == -1) { + final long bytesInSection = SECTION_SIZE / 8; + UNSAFE.setMemory(chunkPtr + (y0 * bytesInSection), bytesInSection, (byte) 0xFF); + continue; + } // pasted from FasterWorldScanner final BitStorage array = ((IPalettedContainer) bsc).getStorage(); if (array == null) continue; @@ -212,26 +339,20 @@ private static void writeChunkData(LevelChunk chunk, long ptr) { int x = (idx & 15); int y = yReal + (idx >> 8); int z = ((idx >> 4) & 15); - Octree.setBlock(ptr, x, y, z, value != airId); + + // Avoid unnecessary writes that may trigger a page allocation + if (!(value == airId | value == caveAirId) & value != redMushroomId & value != brownMushroomId) { + Octree.setBlock(chunkPtr, x, y, z, true); + } } } } - Octree.setIsFromJava(ptr); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } } - public static final class Visibility { - - public static final int ALL = 0; - public static final int NONE = 1; - public static final int ANY = 2; - - private Visibility() {} - } - public static boolean isSupported() { return NetherPathfinder.isThisSystemSupported(); } diff --git a/src/main/java/baritone/process/elytra/NetherPathfinderContextFactory.java b/src/main/java/baritone/process/elytra/NetherPathfinderContextFactory.java new file mode 100644 index 000000000..f3c1b9e1f --- /dev/null +++ b/src/main/java/baritone/process/elytra/NetherPathfinderContextFactory.java @@ -0,0 +1,42 @@ +/* + * 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.elytra; + +import baritone.Baritone; +import baritone.api.pathing.elytra.IElytraContextFactory; +import baritone.api.utils.IPlayerContext; + +import java.nio.file.Path; + +public class NetherPathfinderContextFactory implements IElytraContextFactory { + + @Override + public NetherPathfinderContext create(IPlayerContext ctx, Path cacheDir) { + return new NetherPathfinderContext( + Baritone.settings().elytraNetherSeed.value, + Baritone.settings().elytraUseCache.value ? cacheDir : null, + ctx.world() + ); + } + + @Override + public String toString() { + return "NetherPathfinderContextFactory{}"; + } + +} diff --git a/src/main/java/baritone/process/elytra/NullElytraProcess.java b/src/main/java/baritone/process/elytra/NullElytraProcess.java index 07d5fde0e..11c48ee31 100644 --- a/src/main/java/baritone/process/elytra/NullElytraProcess.java +++ b/src/main/java/baritone/process/elytra/NullElytraProcess.java @@ -18,6 +18,7 @@ package baritone.process.elytra; import baritone.Baritone; +import baritone.api.pathing.elytra.IElytraContextFactory; import baritone.api.pathing.goals.Goal; import baritone.api.process.IElytraProcess; import baritone.api.process.PathingCommand; @@ -83,8 +84,20 @@ public boolean isLoaded() { return false; } + @Override + public IElytraContextFactory getContextFactory() { + return null; + } + + @Override + public void setContextFactory(IElytraContextFactory factory) { + + } + @Override public boolean isSafeToCancel() { return true; } + + } diff --git a/src/main/java/baritone/process/elytra/SkyPathfinderContext.java b/src/main/java/baritone/process/elytra/SkyPathfinderContext.java new file mode 100644 index 000000000..379c400a1 --- /dev/null +++ b/src/main/java/baritone/process/elytra/SkyPathfinderContext.java @@ -0,0 +1,412 @@ +/* + * 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.elytra; + +import baritone.Baritone; +import baritone.api.event.events.BlockChangeEvent; +import baritone.api.pathing.elytra.IElytraPathfinderContext; +import baritone.api.pathing.elytra.UnpackedSegment; +import baritone.api.utils.BetterBlockPos; +import baritone.api.utils.IPlayerContext; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Vec3i; +import net.minecraft.util.Tuple; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.level.levelgen.Heightmap; + +import java.nio.file.Path; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public class SkyPathfinderContext implements IElytraPathfinderContext { + NetherPathfinderContext netherCtx; + IPlayerContext playerCtx; + final int flightLevel; + + public SkyPathfinderContext(IPlayerContext ctx, Path cacheDir) { + if (ctx == null) { + throw new IllegalArgumentException("IPlayerContext cannot be null"); + } + + this.netherCtx = new NetherPathfinderContextFactory().create(ctx, cacheDir); + this.playerCtx = ctx; + this.flightLevel = playerCtx.world().getMaxBuildHeight() + 16; + + if(netherCtx.getMaxHeight() + playerCtx.world().getMinBuildHeight() < playerCtx.world().getMaxBuildHeight()) { + throw new IllegalStateException("Nether pathfinder max height is below world build limit, cannot proceed"); + } + } + + @Override + public boolean hasChunk(ChunkPos pos) { + return netherCtx.hasChunk(pos); + } + + @Override + public void queueCacheCulling(int chunkX, int chunkZ, int maxDistanceBlocks) { + netherCtx.queueCacheCulling(chunkX, chunkZ, maxDistanceBlocks); + } + + @Override + public void queueForPacking(LevelChunk chunkIn) { + netherCtx.queueForPacking(chunkIn); + } + + @Override + public void queueBlockUpdate(BlockChangeEvent event) { + netherCtx.queueBlockUpdate(event); + } + + + /** + * Generates a direct path from the start to the destination at a fixed y-level above build limit + * @param start + * @param destination + * @param bufferDistance Distance from the destination to halt the direct path + * @param maxPathSize Maximum number of nodes in the returned path + * @return A tuple containing the path as a list of BetterBlockPos and a boolean indicating if the path is complete + */ + public Tuple, Boolean> GenerateDirectPath(BetterBlockPos start, BetterBlockPos destination, int bufferDistance, int maxPathSize) { + final LinkedList path = new LinkedList<>(); + final int stepDistance = 32; + + start = start.y == flightLevel ? start : new BetterBlockPos(start.getX(), flightLevel, start.getZ()); + destination = destination.y == flightLevel ? destination : new BetterBlockPos(destination.getX(), flightLevel, destination.getZ()); + + BetterBlockPos cur = start; + path.add(cur); + + while (path.size() < maxPathSize) { + double deltaX = destination.getX() - cur.getX(); + double deltaZ = destination.getZ() - cur.getZ(); + double remainingDistance = Math.sqrt(deltaX * deltaX + deltaZ * deltaZ); + double remainingDistanceSq = deltaX * deltaX + deltaZ * deltaZ; + + if(remainingDistanceSq <= bufferDistance * bufferDistance) { + // We are within the buffer distance, so we can stop here + return new Tuple<>(path, true); + } else if (remainingDistance <= stepDistance) { + path.add(destination); + return new Tuple<>(path, true); + } + + double stepRatio = stepDistance / remainingDistance; + int nextX = (int) Math.round(cur.getX() + deltaX * stepRatio); + int nextZ = (int) Math.round(cur.getZ() + deltaZ * stepRatio); + + cur = new BetterBlockPos(nextX, flightLevel, nextZ); + path.add(cur); + } + + return new Tuple<>(path, false); + } + + /** + * Attempts to find an open spot in the sky to transition up above the build limit so a simple direct path can be followed + * @param start + * @param destination + * @return A tuple containing the path that transitions above build limit and a boolean indicating if a transition was found + */ + public Tuple,Boolean> GenerateTransitionUp(BetterBlockPos start, BetterBlockPos destination) { + final double deltaX = destination.getX() - start.getX(); + final double deltaZ = destination.getZ() - start.getZ(); + final double distance = Math.sqrt(deltaX * deltaX + deltaZ * deltaZ); + + final double scale = 8 / distance; + final double stepX = deltaX * scale; + final double stepZ = deltaZ * scale; + + final int netherMaxHeight = netherCtx.getMaxHeight() + playerCtx.world().getMinBuildHeight() - 1; + + final ChunkPos startChunk = new ChunkPos(start.x >> 4, start.z >> 4); + + if(!isSkyClear(startChunk, start.y)) { + return new Tuple<>(new LinkedList<>(), false); + } + + LinkedList path = new LinkedList<>(); + + // Start with the middle block so the transition doesn't leave the only chunk we can confirm is clear + final BlockPos middlePos = startChunk.getMiddleBlockPosition(netherMaxHeight+4); + + for(int i = 2; i <= 2; i++) { + BetterBlockPos next = new BetterBlockPos( + (int) (middlePos.getX() + (stepX * i)), + netherMaxHeight + (i * 8), + (int) (middlePos.getZ() + (stepZ * i)) + ); + path.add(next); + } + + return new Tuple<>(path, true); + } + + /** + * Attempts to find an open spot in the sky to transition down to a flight level the nether pathfinder can navigiate at. + * @param start + * @return A tuple containing the path (single point) and a boolean indicating if a transition point was found + */ + public Tuple,Boolean> GenerateTransitionDown(BetterBlockPos start) { + final int netherMaxHeight = netherCtx.getMaxHeight() + playerCtx.world().getMinBuildHeight() - 1; + final ChunkPos startChunk = new ChunkPos(start.x >> 4, start.z >> 4); + + LinkedList path = new LinkedList<>(); + + if(!isSkyClear(new ChunkPos(start.x >> 4, start.z >> 4), netherMaxHeight-16)) { + return new Tuple<>(new LinkedList<>(), false); + } + + path.add(new BetterBlockPos(startChunk.getMiddleBlockPosition(netherMaxHeight-8))); + return new Tuple<>(path, true); + } + + public boolean isSkyClear(ChunkPos pos, int y) { + if(!playerCtx.world().getChunkSource().hasChunk(pos.x, pos.z)) { + return false; + } + + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + BlockPos blockPos = pos.getBlockAt(x, y, z); + int height = playerCtx.world().getHeight(Heightmap.Types.MOTION_BLOCKING, blockPos.getX(), blockPos.getZ()); + if (height > y) { + return false; + } + } + } + return true; + } + + + @Override + public CompletableFuture pathFindAsync(BlockPos src, BlockPos dst) { + final int netherMaxHeight = netherCtx.getMaxHeight() + playerCtx.world().getMinBuildHeight() - 1; + final int maxDirectPathSize = 500; + + // There can be some navigation issues around failed transitions if the threshold distance isn't large enough + final double maxDistance = Baritone.settings().elytraLongDistanceThreshold.value >= 32 ? (double) Baritone.settings().elytraLongDistanceThreshold.value : Double.POSITIVE_INFINITY; + + final double distanceXZ = src.distSqr(new Vec3i(dst.getX(), src.getY(), dst.getZ())); + final boolean isLongDistance = distanceXZ > maxDistance * maxDistance; + final boolean srcAboveSupportedHeight = src.getY() >= netherMaxHeight; + final boolean dstAboveSupportedHeight = dst.getY() >= netherMaxHeight; + + if(srcAboveSupportedHeight && dstAboveSupportedHeight) { + var path = GenerateDirectPath(new BetterBlockPos(src), new BetterBlockPos(dst), 0, maxDirectPathSize); + return CompletableFuture.completedFuture(new UnpackedSegment(path.getA().stream(), path.getB())); + } + + if(isLongDistance) { + if(srcAboveSupportedHeight) { + var directPath = GenerateDirectPath(new BetterBlockPos(src), new BetterBlockPos(dst), (int)maxDistance, maxDirectPathSize); + return CompletableFuture.completedFuture( + new UnpackedSegment( + directPath.getA().stream(), + dstAboveSupportedHeight ? directPath.getB() : false + ) + ); + } else { + var transition = GenerateTransitionUp(new BetterBlockPos(src), new BetterBlockPos(dst)); + var path = transition.getA(); + var success = transition.getB(); + + if(success) { + var directPath = GenerateDirectPath(path.get(path.size()-1), new BetterBlockPos(dst), (int)maxDistance, maxDirectPathSize); + path.addAll(directPath.getA()); + + return CompletableFuture.completedFuture( + new UnpackedSegment( + path.stream(), + dstAboveSupportedHeight? directPath.getB() : false + ) + ); + } + } + + // Failed to find a transition point so navigate a bit in the right direction and try + final double deltaX = dst.getX() - src.getX(); + final double deltaZ = dst.getZ() - src.getZ(); + final double scale = (maxDistance/2) / Math.sqrt(deltaX * deltaX + deltaZ * deltaZ); + final double stepX = deltaX * scale; + final double stepZ = deltaZ * scale; + final BlockPos midDst = new BlockPos((int)(src.getX() + stepX), netherMaxHeight, (int)(src.getZ() + stepZ)); + + return incompletePathfind(src, midDst); + } else { + if(srcAboveSupportedHeight) { + var transition = GenerateTransitionDown(new BetterBlockPos(src)); + List path = transition.getA(); + boolean success = transition.getB(); + + if(!success) { + BetterBlockPos newDest = distanceXZ > 32 ? new BetterBlockPos(dst) : new BetterBlockPos(dst.getX(), playerCtx.world().getMaxBuildHeight(), dst.getZ()); + var directPath = GenerateDirectPath(new BetterBlockPos(src), newDest, 0, 2); + return CompletableFuture.completedFuture(new UnpackedSegment(directPath.getA().stream(), directPath.getB())); + } + + return CompletableFuture.supplyAsync(() -> { + try { + var np = netherCtx.pathFindAsync(path.get(path.size() - 1), dst).get(); + path.addAll(np.collect()); + return new UnpackedSegment(path.stream(), np.isFinished()); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + }); + } + + + if(dstAboveSupportedHeight) { + var transition = GenerateTransitionUp(new BetterBlockPos(src), new BetterBlockPos(dst)); + var path = transition.getA(); + var success = transition.getB(); + + if(success) { + var directPath = GenerateDirectPath(path.get(path.size() - 1), new BetterBlockPos(dst), 0, maxDirectPathSize); + path.addAll(directPath.getA()); + return CompletableFuture.completedFuture(new UnpackedSegment(path.stream(), directPath.getB())); + } + + return netherCtx.pathFindAsync(src, new BetterBlockPos(dst.getX(), netherMaxHeight, dst.getZ())); + } + + return netherCtx.pathFindAsync(src, dst); + } + } + + /** + * A wrapper for a nether pathfinder call but the returned path will always indicate it is incomplete + * @param src + * @param dst + * @return a CompletableFuture containing an UnpackedSegment with isFinished always false + */ + private CompletableFuture incompletePathfind(BlockPos src, BlockPos dst) { + return CompletableFuture.supplyAsync(() -> { + UnpackedSegment packed; + try { + packed = netherCtx.pathFindAsync(src, dst).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + return new UnpackedSegment( + packed.collect().stream(), + false + ); + }); + } + + @Override + public boolean raytrace(double startX, double startY, double startZ, double endX, double endY, double endZ) { + final int maxHeight = netherCtx.getMaxHeight() + playerCtx.world().getMinBuildHeight(); + final int minHeight = playerCtx.world().getMinBuildHeight(); + final boolean isOOB = startY >= maxHeight || endY >= maxHeight || startY < minHeight || endY < minHeight; + if (isOOB) { + Vec3 start = new Vec3(startX, startY, startZ); + Vec3 end = new Vec3(endX, endY, endZ); + return playerCtx.world().clip(new ClipContext(start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, playerCtx.player())).getType() == HitResult.Type.MISS; + } + + return netherCtx.raytrace(startX, startY, startZ, endX, endY, endZ); + } + + @Override + public boolean raytrace(Vec3 start, Vec3 end) { + final int maxHeight = netherCtx.getMaxHeight() + playerCtx.world().getMinBuildHeight(); + final int minHeight = playerCtx.world().getMinBuildHeight(); + final boolean isOOB = start.y >= maxHeight || end.y >= maxHeight || start.y < minHeight || end.y < minHeight; + if (isOOB) { + return playerCtx.world().clip(new ClipContext(start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, playerCtx.player())).getType() == HitResult.Type.MISS; + } + return netherCtx.raytrace(start.x, start.y, start.z, end.x, end.y, end.z); + } + + + @Override + public boolean raytrace(int count, double[] src, double[] dst, int visibility) { + if (src.length != count * 3 || src.length != dst.length) { + throw new IllegalArgumentException("Expected source and dst to have length of " + (count * 3)); + } + final int maxHeight = netherCtx.getMaxHeight() + playerCtx.world().getMinBuildHeight(); + + boolean isOOB = false; + for(int i = 1; i < src.length; i += 3) { + if (src[i] >= maxHeight || src[i] < playerCtx.world().getMinBuildHeight() || + dst[i] >= maxHeight || dst[i] < playerCtx.world().getMinBuildHeight()) { + isOOB = true; + break; + } + } + + if(isOOB) { + for (int i = 0; i < count; i++) { + Vec3 start = new Vec3(src[i * 3], src[i * 3 + 1], src[i * 3 + 2]); + Vec3 end = new Vec3(dst[i * 3], dst[i * 3 + 1], dst[i * 3 + 2]); + if (playerCtx.world().clip(new ClipContext(start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, playerCtx.player())).getType() != HitResult.Type.MISS) { + return false; + } + } + return true; + } + + return netherCtx.raytrace(count, src, dst, visibility); + } + + @Override + public void cancel() { + netherCtx.cancel(); + } + + @Override + public void destroy() { + netherCtx.destroy(); + } + + @Override + public long getSeed() { + return netherCtx.getSeed(); + } + + @Override + public void RLock() { + netherCtx.RLock(); + } + + @Override + public void RUnlock() { + netherCtx.RUnlock(); + } + + @Override + public int getMaxHeight() { + return netherCtx.getMaxHeight(); + } + + @Override + public boolean passable(int x, int y, int z) { + if(y >= playerCtx.world().getMaxBuildHeight() || y < playerCtx.world().getMinBuildHeight()) { + return true; + } + return netherCtx.passable(x, y, z); + } +} diff --git a/src/main/java/baritone/process/elytra/SkyPathfinderContextFactory.java b/src/main/java/baritone/process/elytra/SkyPathfinderContextFactory.java new file mode 100644 index 000000000..4da4bc753 --- /dev/null +++ b/src/main/java/baritone/process/elytra/SkyPathfinderContextFactory.java @@ -0,0 +1,37 @@ +/* + * 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.elytra; + +import baritone.api.pathing.elytra.IElytraContextFactory; +import baritone.api.utils.IPlayerContext; + +import java.nio.file.Path; + +public class SkyPathfinderContextFactory implements IElytraContextFactory { + + @Override + public SkyPathfinderContext create(IPlayerContext ctx, Path cacheDir) { + return new SkyPathfinderContext(ctx, cacheDir); + } + + @Override + public String toString() { + return "CustomPathfinderContextFactory{}"; + } + +}