diff --git a/FEATURES.md b/FEATURES.md
index 11f356316..612f700ab 100644
--- a/FEATURES.md
+++ b/FEATURES.md
@@ -1,4 +1,5 @@
# Pathing features
+
- **Long distance pathing and splicing** Baritone calculates paths in segments, and precalculates the next segment when the current one is about to end, so that it's moving towards the goal at all times.
- **Chunk caching** Baritone simplifies chunks to a compacted internal 2-bit representation (AIR, SOLID, WATER, AVOID) and stores them in RAM for better very-long-distance pathing. There is also an option to save these cached chunks to disk. Example
- **Block breaking** Baritone considers breaking blocks as part of its path. It also takes into account your current tool set and hot bar. For example, if you have a Eff V diamond pick, it may choose to mine through a stone barrier, while if you only had a wood pick it might be faster to climb over it.
@@ -14,7 +15,8 @@
- **Pigs** It can sort of control pigs. I wouldn't rely on it though.
# Pathing method
-Baritone uses A*, with some modifications:
+
+Baritone uses A*, with some modifications:
- **Segmented calculation** Traditional A* calculates until the most promising node is in the goal, however in the environment of Minecraft with a limited render distance, we don't know the environment all the way to our goal. Baritone has three possible ways for path calculation to end: finding a path all the way to the goal, running out of time, or getting to the render distance. In the latter two scenarios, the selection of which segment to actually execute falls to the next item (incremental cost backoff). Whenever the path calculation thread finds that the best / most promising node is at the edge of loaded chunks, it increments a counter. If this happens more than 50 times (configurable), path calculation exits early. This happens with very low render distances. Otherwise, calculation continues until the timeout is hit (also configurable) or we find a path all the way to the goal.
- **Incremental cost backoff** When path calculation exits early without getting all the way to the goal, Baritone it needs to select a segment to execute first (assuming it will calculate the next segment at the end of this one). It uses incremental cost backoff to select the best node by varying metrics, then paths to that node. This is unchanged from MineBot and I made a write-up that still applies. In essence, it keeps track of the best node by various increasing coefficients, then picks the node with the least coefficient that goes at least 5 blocks from the starting position.
@@ -27,7 +29,9 @@ Baritone uses A*, with some modifications:
- [Baritone chat control usage](USAGE.md)
# Goals
+
The pathing goal can be set to any of these options:
+
- **GoalBlock** one specific block that the player should stand inside at foot level
- **GoalXZ** an X and a Z coordinate, used for long distance pathing
- **GoalYLevel** a Y coordinate
@@ -38,14 +42,16 @@ The pathing goal can be set to any of these options:
And finally `GoalComposite`. `GoalComposite` is a list of other goals, any one of which satisfies the goal. For example, `mine diamond_ore` creates a `GoalComposite` of `GoalTwoBlocks`s for every diamond ore location it knows of.
-
# Future features
+
Things it doesn't have yet
+
- Trapdoors
- Sprint jumping in a 1x2 corridor
See issues for more.
Things it may not ever have, from most likely to least likely =(
+
- Boats
- Horses (2x3 path instead of 1x2)
diff --git a/README.md b/README.md
index a8f3b1f49..65cbf9321 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
# Baritone
+
@@ -111,7 +112,7 @@ Here are some links to help to get started:
# API
The API is heavily documented, you can find the Javadocs for the latest release [here](https://baritone.leijurv.com/).
-Please note that usage of anything located outside of the ``baritone.api`` package is not supported by the API release
+Please note that usage of anything located outside of the `baritone.api` package is not supported by the API release
jar.
Below is an example of basic usage for changing some settings, and then pathing to an X/Z goal.
@@ -119,7 +120,6 @@ Below is an example of basic usage for changing some settings, and then pathing
```java
BaritoneAPI.getSettings().allowSprint.value = true;
BaritoneAPI.getSettings().primaryTimeoutMS.value = 2000L;
-
BaritoneAPI.getProvider().getPrimaryBaritone().getCustomGoalProcess().setGoalAndPath(new GoalXZ(10000, 20000));
```
diff --git a/SETUP.md b/SETUP.md
index 96991cea6..88340b0c9 100644
--- a/SETUP.md
+++ b/SETUP.md
@@ -6,20 +6,21 @@ The easiest way to install Baritone is to install it as Forge/Neoforge/Fabric mo
Once Baritone is installed, look [here](USAGE.md) for instructions on how to use it.
## Prebuilt official releases
+
Releases are made rarely and are not always up to date with the latest features and bug fixes.
Link to the releases page: [Releases](https://github.com/cabaletta/baritone/releases)
The mapping between Minecraft versions and major Baritone versions is as follows
+
| Minecraft version | 1.12 | 1.13 | 1.14 | 1.15 | 1.16 | 1.17 | 1.18 | 1.19 | 1.20 | 1.21 | 1.21.4 | 1.21.5 | 1.21.6 - 1.21.8 |
|-------------------|------|------|------|------|------|------|------|------|-------|-------|--------|--------|------------------|
| Baritone version | v1.2 | v1.3 | v1.4 | v1.5 | v1.6 | v1.7 | v1.8 | v1.9 | v1.10 | v1.11 | v1.13 | v1.14 | v1.15 |
-Any official release will be GPG signed by leijurv (44A3EA646EADAC6A). Please verify that the hash of the file you download is in `checksums.txt` and that `checksums_signed.asc` is a valid signature by that public keys of `checksums.txt`.
+Any official release will be GPG signed by leijurv (44A3EA646EADAC6A). Please verify that the hash of the file you download is in `checksums.txt` and that `checksums_signed.asc` is a valid signature by that public keys of `checksums.txt`.
The build is fully deterministic and reproducible, and you can verify that by running `docker build --no-cache -t cabaletta/baritone .` yourself and comparing the shasum. This works identically on Travis, Mac, and Linux (if you have docker on Windows, I'd be grateful if you could let me know if it works there too).
-
## Artifacts
Building Baritone will create the final artifacts in the ``dist`` directory. These are the same as the artifacts created in the [releases](https://github.com/cabaletta/baritone/releases).
@@ -31,6 +32,7 @@ If you want to report a bug and spare us some effort, you want `baritone-unoptim
Otherwise, you want `baritone-standalone-*-VERSION.jar`
Here's what the various qualifiers mean
+
- **API**: Only the non-api packages are obfuscated. This should be used in environments where other mods would like to use Baritone's features.
- **Standalone**: Everything is obfuscated. Other mods cannot use Baritone, but you get a bit of extra performance.
- **Unoptimized**: Nothing is obfuscated. This shouldn't be used in production, but is really helpful for crash reports.
@@ -41,6 +43,7 @@ Here's what the various qualifiers mean
If you build from source you will also find mapping files in the `dist` directory. These contain the renamings done by ProGuard and are useful if you want to read obfuscated stack traces.
## Build it yourself
+
- Clone or download Baritone

@@ -48,9 +51,11 @@ If you build from source you will also find mapping files in the `dist` director
- Follow one of the instruction sets below, based on your preference
## Command Line
+
On Mac OSX and Linux, use `./gradlew` instead of `gradlew`.
The recommended Java versions by Minecraft version are
+
| Minecraft version | Java version |
|-------------------------------|---------------|
| 1.12.2 - 1.16.5 | 8 |
@@ -58,7 +63,7 @@ The recommended Java versions by Minecraft version are
| 1.18.2 - 1.20.4 | 17 |
| 1.20.5 - 1.21.8 | 21 |
-Download java: https://adoptium.net/
+Download java:
To check which java version you are using do `java -version` in a command prompt or terminal.
@@ -75,11 +80,13 @@ and `gradlew build -Pbaritone.forge_build` / `gradlew build -Pbaritone.fabric_bu
for Forge/Fabric instead. And you might have to run `setupDecompWorkspace` first.
## IntelliJ
+
- Open the project in IntelliJ as a Gradle project
- Refresh the Gradle project (or, to be safe, just restart IntelliJ)
- Depending on the minecraft version, you may need to run `setupDecompWorkspace` or `genIntellijRuns` in order to get everything working
## Github Actions
+
Most branches have a CI workflow at `.github/workflows/gradle_build.yml`. If you fork this repository and enable actions for your fork
you can push a dummy commit to trigger it and have GitHub build Baritone for you.
diff --git a/USAGE.md b/USAGE.md
index 46241e3fe..fe95dad9a 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -16,7 +16,7 @@ Try `#help` I promise it won't just send you back here =)
"wtf where is cleararea" -> look at `#help sel`
-"wtf where is goto death, goto waypoint" -> look at `#help wp`
+"wtf where is goto death, goto waypoint" -> look at `#help wp`
just look at `#help` lmao
@@ -33,6 +33,7 @@ Watch this [showcase video](https://youtu.be/CZkLXWo4Fg4)!
To toggle a boolean setting, just say its name in chat (for example saying `allowBreak` toggles whether Baritone will consider breaking blocks). For a numeric setting, say its name then the new value (like `primaryTimeoutMS 250`). It's case insensitive. To reset a setting to its default value, say `acceptableThrowawayItems reset`. To reset all settings, say `reset`. To see all settings that have been modified from their default values, say `modified`.
Commands in Baritone:
+
- `thisway 1000` then `path` to go in the direction you're facing for a thousand blocks
- `goal x y z` or `goal x z` or `goal y`, then `path` to set a goal to a certain coordinate then path to it
- `goto x y z` or `goto x z` or `goto y` to go to a certain coordinate (in a single step, starts going immediately)
@@ -47,7 +48,7 @@ Commands in Baritone:
- `build` to build a schematic. `build blah.schematic` will load `schematics/blah.schematic` and build it with the origin being your player feet. `build blah.schematic x y z` to set the origin. Any of those can be relative to your player (`~ 69 ~-420` would build at x=player x, y=69, z=player z-420).
- `schematica` to build the schematic that is currently open in schematica
- `tunnel` to dig and make a tunnel, 1x2. It will only deviate from the straight line if necessary such as to avoid lava. For a dumber tunnel that is really just cleararea, you can `tunnel 3 2 100`, to clear an area 3 high, 2 wide, and 100 deep.
-- `farm` to automatically harvest, replant, or bone meal crops. Use `farm ` or `farm ` to limit the max distance from the starting point or a waypoint.
+- `farm` to automatically harvest, replant, or bone meal crops. Use `farm ` or `farm ` to limit the max distance from the starting point or a waypoint.
- `axis` to go to an axis or diagonal axis at y=120 (`axisHeight` is a configurable setting, defaults to 120).
- `explore x z` to explore the world from the origin of x,z. Leave out x and z to default to player feet. This will continually path towards the closest chunk to the origin that it's never seen before. `explorefilter filter.json` with optional invert can be used to load in a list of chunks to load.
- `invert` to invert the current goal and path. This gets as far away from it as possible, instead of as close as possible. For example, do `goal` then `invert` to run as far as possible from where you're standing at the start.
@@ -67,6 +68,7 @@ Commands in Baritone:
All the settings and documentation are here. If you find HTML easier to read than Javadoc, you can look here.
There are about a hundred settings, but here are some fun / interesting / important ones that you might want to look at changing in normal usage of Baritone. The documentation for each can be found at the above links.
+
- `allowBreak`
- `allowSprint`
- `allowPlace`
@@ -86,12 +88,10 @@ There are about a hundred settings, but here are some fun / interesting / import
- `mineScanDroppedItems`
- `allowDiagonalAscend`
-
-
-
# Troubleshooting / common issues
## Why doesn't Baritone respond to any of my chat commands?
+
This could be one of many things.
First, make sure it's actually installed. An easy way to check is seeing if it created the folder `baritone` in your Minecraft folder.
@@ -101,7 +101,7 @@ Second, make sure that you're using the prefix properly, and that chat control i
For example, Impact disables direct chat control. (i.e. anything typed in chat without a prefix will be ignored and sent publicly). **This is a saved setting**, so if you run Impact once, `chatControl` will be off from then on, **even in other clients**.
So you'll need to use the `#` prefix or edit `baritone/settings.txt` in your Minecraft folder to undo that (specifically, remove the line `chatControl false` then restart your client).
-
## Why can I do `.goto x z` in Impact but nowhere else? Why can I do `-path to x z` in KAMI but nowhere else?
+
These are custom commands that they added; those aren't from Baritone.
The equivalent you're looking for is `goto x z`.
diff --git a/gradle.properties b/gradle.properties
index bd275b61a..3f156c54e 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 dc7669e75..3f45f7ac5 100644
--- a/src/api/java/baritone/api/Settings.java
+++ b/src/api/java/baritone/api/Settings.java
@@ -1550,6 +1550,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);
+
/**
* Sneak when magma blocks are under feet
*/
diff --git a/src/api/java/baritone/api/utils/RotationUtils.java b/src/api/java/baritone/api/utils/RotationUtils.java
index bfab3fa87..2c2ee9147 100644
--- a/src/api/java/baritone/api/utils/RotationUtils.java
+++ b/src/api/java/baritone/api/utils/RotationUtils.java
@@ -175,6 +175,10 @@ public static Optional reachable(IPlayerContext ctx, BlockPos pos, dou
}
public static Optional reachable(IPlayerContext ctx, BlockPos pos, double blockReachDistance, boolean wouldSneak) {
+ // Prevent BetterBlockPos from leaking into Minecraft's block entity map
+ if (pos instanceof BetterBlockPos) {
+ pos = new BlockPos(pos.getX(), pos.getY(), pos.getZ());
+ }
if (BaritoneAPI.getSettings().remainWithExistingLookDirection.value && ctx.isLookingAt(pos)) {
/*
* why add 0.0001?
diff --git a/src/api/java/baritone/api/utils/VecUtils.java b/src/api/java/baritone/api/utils/VecUtils.java
index 4ea94b95a..7037afcfc 100644
--- a/src/api/java/baritone/api/utils/VecUtils.java
+++ b/src/api/java/baritone/api/utils/VecUtils.java
@@ -43,6 +43,10 @@ private VecUtils() {}
* @see #getBlockPosCenter(BlockPos)
*/
public static Vec3 calculateBlockCenter(Level world, BlockPos pos) {
+ // Prevent BetterBlockPos from leaking into Minecraft's block entity map
+ if (pos instanceof BetterBlockPos) {
+ pos = new BlockPos(pos.getX(), pos.getY(), pos.getZ());
+ }
BlockState b = world.getBlockState(pos);
VoxelShape shape = b.getCollisionShape(world, pos);
if (shape.isEmpty()) {
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 e101df74f..133d8a199 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 276ab431b..c50e9b80c 100644
--- a/src/main/java/baritone/process/ElytraProcess.java
+++ b/src/main/java/baritone/process/ElytraProcess.java
@@ -38,12 +38,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 +53,15 @@
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.chunk.ChunkSource;
+import net.minecraft.world.level.chunk.LevelChunk;
+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 java.util.concurrent.TimeUnit;
import static baritone.api.pathing.movement.ActionCosts.COST_INF;
@@ -67,16 +72,36 @@ public class ElytraProcess extends BaritoneProcessHelper implements IBaritonePro
private boolean reachedGoal; // this basically just prevents potential notification spam
private Goal goal;
private ElytraBehavior behavior;
+ private NetherPathfinderContext npfContext;
private boolean predictingTerrain;
+ private boolean allowTight;
+ private boolean allowAboveBuildLimit;
+ private boolean allowAboveRoof;
+ private final Semaphore npfSema = new Semaphore(1);
+
+ private static final int SHORT_LANDING_COLUMN_HEIGHT = 15;
+ private static final int LONG_LANDING_COLUMN_HEIGHT = 39;
+ private static final long LANDING_SEARCH_BUDGET_NANOS = TimeUnit.MILLISECONDS.toNanos(25); // half a tick
+ private int landingColumnHeight = SHORT_LANDING_COLUMN_HEIGHT;
+ private Set badLandingSpots = new HashSet<>();
+ private LandingSearchState landingSearchState;
@Override
public void onLostControl() {
+ onLostControl(true);
+ }
+
+ public void onLostControl(boolean destroyNpf) {
this.state = State.START_FLYING; // TODO: null state?
this.goingToLandingSpot = false;
this.landingSpot = null;
+ this.landingSearchState = null;
this.reachedGoal = false;
this.goal = null;
destroyBehaviorAsync();
+ if (destroyNpf) {
+ destroyNpfContextAsync();
+ }
}
private ElytraProcess(Baritone baritone) {
@@ -109,15 +134,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.npfContext.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();
@@ -140,14 +185,19 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) {
if (ctx.player().isFallFlying() && this.state != State.LANDING && (this.behavior.pathManager.isComplete() || safetyLanding)) {
final BetterBlockPos last = this.behavior.pathManager.path.getLast();
if (last != null && (ctx.player().position().distanceToSqr(last.getCenter()) < (48 * 48) || safetyLanding) && (!goingToLandingSpot || (safetyLanding && this.landingSpot == null))) {
- logDirect("Path complete, picking a nearby safe landing spot...");
+ if (this.landingSearchState == null) {
+ logDirect("Path complete, searching for safe landing spot...");
+ }
BetterBlockPos landingSpot = findSafeLandingSpot(ctx.playerFeet());
// if this fails we will just keep orbiting the last node until we run out of rockets or the user intervenes
if (landingSpot != null) {
+ logDirect("Found potential landing spot.");
this.pathTo0(landingSpot, true);
this.landingSpot = landingSpot;
+ this.goingToLandingSpot = true;
+ } else {
+ this.goingToLandingSpot = false;
}
- this.goingToLandingSpot = true;
}
if (last != null && ctx.player().position().distanceToSqr(last.getCenter()) < 1) {
@@ -163,7 +213,7 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) {
reachedGoal = true;
// we are goingToLandingSpot and we are in the last node of the path
- if (this.goingToLandingSpot) {
+ if (this.goingToLandingSpot && landingSpot != null) {
this.state = State.LANDING;
logDirect("Above the landing spot, landing...");
}
@@ -178,7 +228,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 +239,13 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) {
behavior.landingMode = this.state == State.LANDING;
this.goal = null;
baritone.getInputOverrideHandler().clearAllKeys();
- behavior.tick();
+ if (this.behavior.npfContext.tryAcquireReadLock()) {
+ try {
+ behavior.tick();
+ } finally {
+ this.behavior.npfContext.releaseReadLock();
+ }
+ }
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) {
@@ -283,6 +339,7 @@ public void landingSpotIsBad(BetterBlockPos endPos) {
badLandingSpots.add(endPos);
goingToLandingSpot = false;
this.landingSpot = null;
+ this.landingSearchState = null;
this.state = State.FLYING;
}
@@ -290,7 +347,9 @@ private void destroyBehaviorAsync() {
ElytraBehavior behavior = this.behavior;
if (behavior != null) {
this.behavior = null;
- Baritone.getExecutor().execute(behavior::destroy);
+ Baritone.getExecutor().execute(() -> {
+ behavior.destroy();
+ });
}
}
@@ -306,8 +365,27 @@ public String displayName0() {
@Override
public void repackChunks() {
- if (this.behavior != null) {
- this.behavior.repackChunks();
+ if (this.npfContext == null) return;
+
+ ChunkSource chunkProvider = ctx.world().getChunkSource();
+ BetterBlockPos playerPos = ctx.playerFeet();
+
+ int playerChunkX = playerPos.getX() >> 4;
+ int playerChunkZ = playerPos.getZ() >> 4;
+
+ int minX = playerChunkX - 40;
+ int minZ = playerChunkZ - 40;
+ int maxX = playerChunkX + 40;
+ int maxZ = playerChunkZ + 40;
+
+ for (int x = minX; x <= maxX; x++) {
+ for (int z = minZ; z <= maxZ; z++) {
+ LevelChunk chunk = chunkProvider.getChunk(x, z, false);
+
+ if (chunk != null && !chunk.isEmpty()) {
+ npfContext.queueForPacking(chunk);
+ }
+ }
}
}
@@ -323,18 +401,30 @@ public List getPath() {
@Override
public void pathTo(BlockPos destination) {
+ if (!isSupportedPos(destination)) {
+ throw new IllegalArgumentException("The goal must be within bounds to use elytra flight.");
+ }
+
+ if (ctx.player() != null && !isSupportedPos(ctx.playerFeet())) {
+ throw new IllegalArgumentException("The player must be within bounds to use elytra flight.");
+ }
+
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.onLostControl(false);
+ 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.behavior = new ElytraBehavior(this.baritone, this, getNpfContext(), destination, appendDestination);
+
if (ctx.world() != null) {
- this.behavior.repackChunks();
+ this.repackChunks();
}
this.behavior.pathTo();
}
@@ -347,6 +437,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) {
@@ -357,10 +448,25 @@ 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");
+
+ this.pathTo((new BlockPos(x, y, z)));
+ }
+
+ private boolean isSupportedPos(BlockPos pos) {
+ final boolean isNether = ctx.world().dimension() == Level.NETHER;
+ final int minY = ctx.world().dimensionType().minY();
+ final int maxY = (isNether && !Baritone.settings().elytraAllowAboveRoof.value) ? 127 : Math.min(minY + 384, ctx.world().dimensionType().height() + minY);
+
+ final boolean aboveRoof = Baritone.settings().elytraAllowAboveRoof.value;
+ final boolean aboveBuild = Baritone.settings().elytraAllowAboveBuildLimit.value;
+
+ final boolean enforceMaxY = isNether ? !(aboveRoof && aboveBuild) : !aboveBuild;
+
+ if (pos.getY() < minY) {
+ return false;
}
- this.pathTo(new BlockPos(x, y, z));
+
+ return !enforceMaxY || pos.getY() < maxY;
}
private boolean shouldLandForSafety() {
@@ -470,12 +576,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) {
@@ -525,7 +639,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;
}
@@ -545,30 +659,131 @@ private BetterBlockPos checkLandingSpot(BlockPos pos, LongOpenHashSet checkedSpo
return null; // void
}
- private static final int LANDING_COLUMN_HEIGHT = 15;
- private Set badLandingSpots = new HashSet<>();
-
private BetterBlockPos findSafeLandingSpot(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 pos = queue.poll();
- if (ctx.world().isLoaded(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.above(LANDING_COLUMN_HEIGHT)) && !badLandingSpots.contains(actualLandingSpot.above(LANDING_COLUMN_HEIGHT))) {
- return actualLandingSpot.above(LANDING_COLUMN_HEIGHT);
+ final boolean useHeightmap = ctx.player().getY() > ctx.world().getHeight(Heightmap.Types.MOTION_BLOCKING, start.getX(), start.getZ());
+ if (this.landingSearchState == null || !this.landingSearchState.isCompatible(start, useHeightmap)) {
+ this.landingSearchState = new LandingSearchState(start, this.behavior.destination, useHeightmap);
+ } else {
+ this.landingSearchState.updateStartPosition(start);
+ }
+
+ BetterBlockPos landingSpot = this.landingSearchState.advance();
+ if (landingSpot != null || this.landingSearchState.exhausted) {
+ this.landingSearchState = null;
+ }
+ return landingSpot;
+ }
+
+ private boolean isChunkLoaded(BetterBlockPos pos) {
+ return ctx.world().getChunkSource().hasChunk(pos.x >> 4, pos.z >> 4);
+ }
+
+ private final class LandingSearchState {
+ private final BetterBlockPos origin;
+ private final boolean useHeightmap;
+ private final Queue queue;
+ private final Set visited = new HashSet<>();
+ private final LongOpenHashSet checkedPositions = new LongOpenHashSet();
+ private boolean exhausted;
+
+ private LandingSearchState(BetterBlockPos origin, BetterBlockPos dest, boolean useHeightmap) {
+ this.origin = origin;
+ this.useHeightmap = useHeightmap;
+
+ final BetterBlockPos target = isChunkLoaded(dest) ? dest : origin;
+ this.queue = new PriorityQueue<>(Comparator.comparingInt(pos -> (pos.x - target.x) * (pos.x - target.x) + (pos.z - target.z) * (pos.z - target.z)).thenComparingInt(pos -> -pos.y));
+ this.queue.add(target);
+ }
+
+ private boolean isCompatible(BetterBlockPos start, boolean useHeightmap) {
+ // Restart if we've moved more than a chunk so the priority adjusts and newly loaded chunks get revisited
+ return this.useHeightmap == useHeightmap && this.origin.distanceSq(start) <= (16 * 16);
+ }
+
+ private void updateStartPosition(BetterBlockPos start) {
+ if (this.visited.add(start)) {
+ this.queue.add(start);
+ }
+ }
+
+ private BetterBlockPos advance() {
+ final long deadline = System.nanoTime() + LANDING_SEARCH_BUDGET_NANOS;
+ while (!this.queue.isEmpty()) {
+ if (System.nanoTime() >= deadline) {
+ return null;
+ }
+ BetterBlockPos qPos = this.queue.poll();
+ if (!isChunkLoaded(qPos)) {
+ continue;
+ }
+ BetterBlockPos landing = this.useHeightmap ? this.advanceHeightmap(qPos) : this.advanceUnderground(qPos);
+ if (landing != null) {
+ return landing;
}
- 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.above())) queue.add(pos.above());
- if (visited.add(pos.below())) queue.add(pos.below());
}
+ this.exhausted = true;
+ return null;
+ }
+
+ private BetterBlockPos advanceUnderground(BetterBlockPos pos) {
+ if (isInBounds(ctx.world(), pos) && ctx.world().getBlockState(pos).getBlock() == Blocks.AIR) {
+ BetterBlockPos actualLandingSpot = checkLandingSpot(pos, this.checkedPositions);
+ if (actualLandingSpot != null) {
+ landingColumnHeight = SHORT_LANDING_COLUMN_HEIGHT;
+ if (isColumnAir(actualLandingSpot, landingColumnHeight) && hasAirBubble(actualLandingSpot.above(landingColumnHeight)) && !badLandingSpots.contains(actualLandingSpot.above(landingColumnHeight))) {
+ return actualLandingSpot.above(landingColumnHeight);
+ }
+ }
+ if (this.visited.add(pos.north())) this.queue.add(pos.north());
+ if (this.visited.add(pos.east())) this.queue.add(pos.east());
+ if (this.visited.add(pos.south())) this.queue.add(pos.south());
+ if (this.visited.add(pos.west())) this.queue.add(pos.west());
+ if (this.visited.add(pos.above())) this.queue.add(pos.above());
+ if (this.visited.add(pos.below())) this.queue.add(pos.below());
+ }
+ return null;
+ }
+
+ private BetterBlockPos advanceHeightmap(BetterBlockPos qPos) {
+ int height = ctx.world().getHeight(Heightmap.Types.MOTION_BLOCKING, qPos.getX(), qPos.getZ());
+ BetterBlockPos pos = new BetterBlockPos(qPos.getX(), height + 1, qPos.getZ());
+ if (isInBounds(ctx.world(), pos) && ctx.world().getBlockState(pos).getBlock() == Blocks.AIR) {
+ BetterBlockPos actualLandingSpot = checkLandingSpot(pos, this.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 (this.visited.add(pos.north())) this.queue.add(pos.north());
+ if (this.visited.add(pos.east())) this.queue.add(pos.east());
+ if (this.visited.add(pos.south())) this.queue.add(pos.south());
+ if (this.visited.add(pos.west())) this.queue.add(pos.west());
+ }
+ return null;
+ }
+ }
+
+ private NetherPathfinderContext getNpfContext() {
+ if(this.npfContext == null) {
+ npfSema.acquireUninterruptibly();
+ this.npfContext = new NetherPathfinderContext(
+ Baritone.settings().elytraNetherSeed.value,
+ Baritone.settings().elytraUseCache.value ? baritone.getWorldProvider().getCurrentWorld().directory.resolve("cache") : null,
+ ctx.world()
+ );
+ }
+ return this.npfContext;
+ }
+
+ private void destroyNpfContextAsync() {
+ NetherPathfinderContext npf = this.npfContext;
+ if (npf != null) {
+ this.npfContext = null;
+ Baritone.getExecutor().execute(() -> {
+ npf.destroy();
+ npfSema.release();
+ });
}
- 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/BuildLimitPathFinder.java b/src/main/java/baritone/process/elytra/BuildLimitPathFinder.java
new file mode 100644
index 000000000..65ee7ca70
--- /dev/null
+++ b/src/main/java/baritone/process/elytra/BuildLimitPathFinder.java
@@ -0,0 +1,299 @@
+/*
+ * 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.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.levelgen.Heightmap;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+public class BuildLimitPathFinder implements IElytraPathFinder {
+ final int flightLevel;
+ final IPlayerContext playerCtx;
+ final NetherPathfinderContext netherCtx;
+
+ public BuildLimitPathFinder(IPlayerContext ctx, NetherPathfinderContext netherCtx) {
+ if (ctx == null) {
+ throw new IllegalArgumentException("IPlayerContext cannot be null");
+ }
+ this.playerCtx = ctx;
+
+ if (netherCtx == null) {
+ throw new IllegalArgumentException("NetherPathfinderContext cannot be null");
+ }
+
+ this.flightLevel = ctx.world().getMaxBuildHeight() + 16;
+ this.netherCtx = netherCtx;
+
+ if(netherCtx.getMaxHeight() + ctx.world().getMinBuildHeight() < ctx.world().getMaxBuildHeight()) {
+ throw new IllegalStateException("Nether pathfinder max height is below world build limit, cannot proceed");
+ }
+ }
+
+ /**
+ * 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;
+
+ final BetterBlockPos startFixed = start.y == flightLevel ? start : new BetterBlockPos(start.getX(), flightLevel, start.getZ());
+ final BetterBlockPos destinationFixed = destination.y == flightLevel ? destination : new BetterBlockPos(destination.getX(), flightLevel, destination.getZ());
+
+ BetterBlockPos cur = startFixed;
+ path.add(cur);
+
+ while (path.size() < maxPathSize) {
+ double deltaX = destinationFixed.getX() - cur.getX();
+ double deltaZ = destinationFixed.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(destinationFixed);
+ 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;
+ }
+
+
+ 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(() -> {
+ var np = blockingPathFind(path.get(path.size() - 1), dst);
+ path.addAll(np.collect());
+ return new UnpackedSegment(path.stream(), np.isFinished());
+ });
+ }
+
+
+ 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 = blockingPathFind(src, dst);
+ return new UnpackedSegment(
+ packed.collect().stream(),
+ false
+ );
+ });
+ }
+
+ private UnpackedSegment blockingPathFind(BlockPos src, BlockPos dst) {
+ try {
+ return netherCtx.pathFindAsync(src, dst).get();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(e);
+ } catch (ExecutionException e) {
+ final Throwable cause = e.getCause();
+ if (cause instanceof PathCalculationException) {
+ throw (PathCalculationException) cause;
+ }
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/src/main/java/baritone/process/elytra/ElytraBehavior.java b/src/main/java/baritone/process/elytra/ElytraBehavior.java
index d4913f466..9f69a6b4d 100644
--- a/src/main/java/baritone/process/elytra/ElytraBehavior.java
+++ b/src/main/java/baritone/process/elytra/ElytraBehavior.java
@@ -45,6 +45,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 +75,9 @@ public final class ElytraBehavior implements Helper {
private List visiblePath;
// :sunglasses:
- public final NetherPathfinderContext context;
+ public NetherPathfinderContext npfContext;
+ public IElytraPathFinder pathFinder;
+
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, NetherPathfinderContext npf, BlockPos destination, boolean appendDestination) {
this.baritone = baritone;
this.ctx = baritone.getPlayerContext();
this.clearLines = new CopyOnWriteArrayList<>();
@@ -128,8 +130,15 @@ public ElytraBehavior(Baritone baritone, ElytraProcess process, BlockPos destina
this.solverExecutor = Executors.newSingleThreadExecutor();
this.nextTickBoostCounter = new int[2];
- this.context = new NetherPathfinderContext(Baritone.settings().elytraNetherSeed.value);
- this.boi = new BlockStateOctreeInterface(context);
+ this.npfContext = npf;
+
+ if(ctx.world().dimension() == Level.NETHER) {
+ this.pathFinder = Baritone.settings().elytraAllowAboveRoof.value && Baritone.settings().elytraAllowAboveBuildLimit.value
+ ? new BuildLimitPathFinder(ctx, npfContext)
+ : npfContext;
+ } else {
+ this.pathFinder = Baritone.settings().elytraAllowAboveBuildLimit.value ? new BuildLimitPathFinder(ctx, npfContext) : npfContext;
+ }
}
public final class PathManager {
@@ -159,9 +168,18 @@ public void tick() {
this.ticksNearUnchanged = 0;
}
- // Obstacles are more important than an incomplete path, handle those first.
- this.pathfindAroundObstacles();
+ int minY = ctx.world().dimensionType().minY();
+ int y = ctx.playerFeet().y;
+
+ npfContext.acquireReadLock();
+ try {
+ // Obstacles are more important than an incomplete path, handle those first.
+ this.pathfindAroundObstacles();
+ } finally {
+ npfContext.releaseReadLock();
+ }
this.attemptNextSegment();
+
}
public CompletableFuture pathToDestination() {
@@ -170,7 +188,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 +219,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 +243,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 +283,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);
@@ -291,12 +309,12 @@ public int getNear() {
// mickey resigned
private CompletableFuture path0(BlockPos src, BlockPos dst, UnaryOperator operator) {
- return ElytraBehavior.this.context.pathFindAsync(src, dst)
- .thenApply(UnpackedSegment::from)
+ return ElytraBehavior.this.pathFinder.pathFindAsync(src, dst)
.thenApply(operator)
.thenAcceptAsync(this::setPath, ctx.minecraft()::execute);
}
+ // required read lock to be held
private void pathfindAroundObstacles() {
if (this.recalculating) {
return;
@@ -304,7 +322,7 @@ private void pathfindAroundObstacles() {
int rangeStartIncl = playerNear;
int rangeEndExcl = playerNear;
- while (rangeEndExcl < path.size() && context.hasChunk(new ChunkPos(path.get(rangeEndExcl)))) {
+ while (rangeEndExcl < path.size() && npfContext.hasChunk(new ChunkPos(path.get(rangeEndExcl)))) {
rangeEndExcl++;
}
// rangeEndExcl now represents an index either not in the path, or just outside render distance
@@ -336,7 +354,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 +389,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);
}
}
@@ -446,14 +467,14 @@ public void onRenderPass(RenderEvent event) {
}
public void onChunkEvent(ChunkEvent event) {
- if (event.isPostPopulate() && this.context != null) {
+ if (event.isPostPopulate() && this.npfContext != null) {
final LevelChunk chunk = ctx.world().getChunk(event.getX(), event.getZ());
- this.context.queueForPacking(chunk);
+ npfContext.queueForPacking(chunk);
}
}
public void onBlockChange(BlockChangeEvent event) {
- this.context.queueBlockUpdate(event);
+ npfContext.queueBlockUpdate(event);
}
public void onReceivePacket(PacketEvent event) {
@@ -480,40 +501,19 @@ public void destroy() {
} catch (InterruptedException e) {
e.printStackTrace();
}
- this.context.destroy();
- }
-
- public void repackChunks() {
- ChunkSource chunkProvider = ctx.world().getChunkSource();
-
- BetterBlockPos playerPos = ctx.playerFeet();
-
- int playerChunkX = playerPos.getX() >> 4;
- int playerChunkZ = playerPos.getZ() >> 4;
-
- int minX = playerChunkX - 40;
- int minZ = playerChunkZ - 40;
- int maxX = playerChunkX + 40;
- int maxZ = playerChunkZ + 40;
-
- for (int x = minX; x <= maxX; x++) {
- for (int z = minZ; z <= maxZ; z++) {
- LevelChunk chunk = chunkProvider.getChunk(x, z, false);
-
- if (chunk != null && !chunk.isEmpty()) {
- this.context.queueForPacking(chunk);
- }
- }
- }
}
public void onTick() {
- synchronized (this.context.cullingLock) {
- this.onTick0();
+ if (npfContext.tryAcquireReadLock()) {
+ try {
+ this.onTick0();
+ } finally {
+ npfContext.releaseReadLock();
+ }
}
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);
+ npfContext.queueCacheCulling(ctx.player().chunkPosition().x, ctx.player().chunkPosition().z, Baritone.settings().elytraCacheCullDistance.value);
this.timeLastCacheCull = now;
}
}
@@ -522,11 +522,17 @@ private void onTick0() {
// Fetch the previous solution, regardless of if it's going to be used
this.pendingSolution = null;
if (this.solver != null) {
- try {
- this.pendingSolution = this.solver.get();
- } catch (Exception ignored) {
- // it doesn't matter if get() fails since the solution can just be recalculated synchronously
- } finally {
+ if (this.solver.isDone()) {
+ try {
+ this.pendingSolution = this.solver.get();
+ } catch (Exception ignored) {
+ // it doesn't matter if get() fails since the solution can just be recalculated synchronously
+ } finally {
+ this.solver = null;
+ }
+ } else {
+ // avoid wasting more cycles on a hard solution, we'll do the work synchronously
+ this.solver.cancel(true);
this.solver = null;
}
}
@@ -554,7 +560,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 +583,6 @@ public void tick() {
if (this.pathManager.getPath().isEmpty()) {
return;
}
-
trySwapElytra();
if (ctx.player().horizontalCollision) {
@@ -592,10 +597,10 @@ public void tick() {
// If there's no previously calculated solution to use, or the context used at the end of last tick doesn't match this tick
final Solution solution;
- if (this.pendingSolution == null || !this.pendingSolution.context.equals(solverContext)) {
- solution = this.solveAngles(solverContext);
- } else {
+ if (this.pendingSolution != null && this.pendingSolution.context.equals(solverContext)) {
solution = this.pendingSolution;
+ } else {
+ solution = this.solveAngles(solverContext);
}
if (this.deployedFireworkLastTick) {
@@ -637,11 +642,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(() -> {
+ npfContext.acquireReadLock();
+ try {
+ return this.solveAngles(context);
+ } finally {
+ npfContext.releaseReadLock();
+ }
+ });
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;
@@ -655,6 +668,7 @@ private Solution solveAngles(final SolverContext context) {
int minStep = playerNear;
for (int i = Math.min(playerNear + 20, path.size() - 1); i >= minStep; i--) {
+ if (Thread.interrupted()) return null; // cancelled by the game thread
final List> candidates = new ArrayList<>();
for (int dy : heights) {
if (relaxation == 0 || i == minStep) {
@@ -999,14 +1013,15 @@ private boolean isHitboxClear(final SolverContext context, final Vec3 dest, fina
return clear;
}
- return this.context.raytrace(8, src, dst, NetherPathfinderContext.Visibility.ALL);
+
+ return raytrace(8, src, dst, NetherPathfinderContext.Visibility.ALL);
}
public boolean clearView(Vec3 start, Vec3 dest, boolean ignoreLava) {
final boolean clear;
if (!ignoreLava) {
// if start == dest then the cpp raytracer dies
- clear = start.equals(dest) || this.context.raytrace(start, dest);
+ clear = start.equals(dest) || raytrace(start, dest);
} else {
clear = ctx.world().clip(new ClipContext(start, dest, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, ctx.player())).getType() == HitResult.Type.MISS;
}
@@ -1262,12 +1277,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 passable(x, y, z);
}
}
@@ -1323,4 +1339,80 @@ 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);
+ }
+
+ public boolean raytrace(double startX, double startY, double startZ, double endX, double endY, double endZ) {
+ final int maxHeight = npfContext.getMaxHeight() + ctx.world().getMinBuildHeight();
+ final int minHeight = ctx.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 ctx.world().clip(new ClipContext(start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, ctx.player())).getType() == HitResult.Type.MISS;
+ }
+
+ return npfContext.raytrace(startX, startY, startZ, endX, endY, endZ);
+ }
+
+ public boolean raytrace(Vec3 start, Vec3 end) {
+ final int maxHeight = npfContext.getMaxHeight() + ctx.world().getMinBuildHeight();
+ final int minHeight = ctx.world().getMinBuildHeight();
+ final boolean isOOB = start.y >= maxHeight || end.y >= maxHeight || start.y < minHeight || end.y < minHeight;
+ if (isOOB) {
+ return ctx.world().clip(new ClipContext(start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, ctx.player())).getType() == HitResult.Type.MISS;
+ }
+ return npfContext.raytrace(start.x, start.y, start.z, end.x, end.y, end.z);
+ }
+
+ 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 = npfContext.getMaxHeight() + ctx.world().getMinBuildHeight();
+
+ boolean isOOB = false;
+ for(int i = 1; i < src.length; i += 3) {
+ if (src[i] >= maxHeight || src[i] < ctx.world().getMinBuildHeight() ||
+ dst[i] >= maxHeight || dst[i] < ctx.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 (ctx.world().clip(new ClipContext(start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, ctx.player())).getType() != HitResult.Type.MISS) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ return npfContext.raytrace(count, src, dst, visibility);
+ }
+
+ public boolean passable(int x, int y, int z) {
+ if(y >= ctx.world().getMaxBuildHeight() || y < ctx.world().getMinBuildHeight()) {
+ return true;
+ }
+ return npfContext.passable(x, y, z);
+ }
}
diff --git a/src/main/java/baritone/process/elytra/IElytraPathFinder.java b/src/main/java/baritone/process/elytra/IElytraPathFinder.java
new file mode 100644
index 000000000..d8148b334
--- /dev/null
+++ b/src/main/java/baritone/process/elytra/IElytraPathFinder.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.process.elytra;
+
+import net.minecraft.core.BlockPos;
+
+import java.util.concurrent.CompletableFuture;
+
+public interface IElytraPathFinder {
+ CompletableFuture pathFindAsync(final BlockPos src, final BlockPos dst);
+}
diff --git a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java
index aa9f4965a..69e0a181b 100644
--- a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java
+++ b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java
@@ -24,98 +24,164 @@
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 IElytraPathFinder {
+ 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 +198,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 +212,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 +240,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 +263,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 +280,51 @@ public long getSeed() {
return this.seed;
}
- private static void writeChunkData(LevelChunk chunk, long ptr) {
+ public void acquireReadLock() {
+ this.readLock.lock();
+ }
+
+ public boolean tryAcquireReadLock() {
+ return this.readLock.tryLock();
+ }
+
+ public void releaseReadLock() {
+ 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,27 +341,28 @@ 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 boolean isSupported() {
+ return NetherPathfinder.isThisSystemSupported();
+ }
+ 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/NullElytraProcess.java b/src/main/java/baritone/process/elytra/NullElytraProcess.java
index ed5be935d..a0f0ff1ef 100644
--- a/src/main/java/baritone/process/elytra/NullElytraProcess.java
+++ b/src/main/java/baritone/process/elytra/NullElytraProcess.java
@@ -96,4 +96,6 @@ public boolean isLoaded() {
public boolean isSafeToCancel() {
return true;
}
+
+
}