diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000000..e32cd6001e --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,57 @@ +name: Build and Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Read build config + id: cfg + run: | + mc=$(grep '^minecraft_version=' gradle.properties | cut -d= -f2) + version=${GITHUB_REF_NAME#v} + { + echo "java=$(grep '^java_version=' gradle.properties | cut -d= -f2)" + echo "mc=$mc" + echo "version=$version" + echo "modver=${version%-$mc}" + } >> "$GITHUB_OUTPUT" + + - name: Set up JDK ${{ steps.cfg.outputs.java }} + uses: actions/setup-java@v4 + with: + java-version: ${{ steps.cfg.outputs.java }} + distribution: 'zulu' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: > + ./gradlew build + -Pmod_version=${{ steps.cfg.outputs.version }} + -Porg.gradle.java.installations.auto-detect=false + "-Porg.gradle.java.installations.paths=$JAVA_HOME" + + - name: Release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + name: Baritone ${{ steps.cfg.outputs.modver }} for Minecraft ${{ steps.cfg.outputs.mc }} + files: dist/* + fail_on_unmatched_files: true diff --git a/README.md b/README.md index 65cbf9321d..bfb62e7075 100644 --- a/README.md +++ b/README.md @@ -1,121 +1,268 @@ # Baritone

- GitHub All Releases + Build + Tests + Release + License

- Minecraft - Minecraft - Minecraft - Minecraft - Minecraft - Minecraft - Minecraft - Minecraft - Minecraft - Minecraft - Minecraft + 1.21.4 + 1.21.5 + 1.21.8 + 1.21.11 + 26.1 + 26.2 + 26.3

-

- Build Status - Release - License - Codacy Badge - Code of Conduct - Known Vulnerabilities - Contributions welcome - Issues - GitHub issues-closed - Pull Requests - GitHub contributors - GitHub commits - Code size - GitHub repo size - Lines of Code - yes -

- -

- Impact integration - Lambda integration - ForgeHax integration - Aristois add-on integration - rootNET integration - Future integration - RusherHack integration -

- -

- forthebadge - forthebadge -

- -A Minecraft pathfinder bot. - -Baritone is the pathfinding system used in [Impact](https://impactclient.net/) since 4.4. [Here's](https://www.youtube.com/watch?v=StquF69-_wI) a (very old!) video I made showing off what it can do. - -[**Baritone Discord Server**](http://discord.gg/s6fRBAUpmr) - -**Quick download links:** - -| Forge | Fabric | NeoForge | -|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------| -| [1.12.2 Forge](https://github.com/cabaletta/baritone/releases/download/v1.2.19/baritone-api-forge-1.2.19.jar) | | | -| [1.16.5 Forge](https://github.com/cabaletta/baritone/releases/download/v1.6.5/baritone-api-forge-1.6.5.jar) | [1.16.5 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.6.5/baritone-api-fabric-1.6.5.jar) | | -| [1.17.1 Forge](https://github.com/cabaletta/baritone/releases/download/v1.7.3/baritone-api-forge-1.7.3.jar) | [1.17.1 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.7.3/baritone-api-fabric-1.7.3.jar) | | -| [1.18.2 Forge](https://github.com/cabaletta/baritone/releases/download/v1.8.6/baritone-api-forge-1.8.6.jar) | [1.18.2 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.8.6/baritone-api-fabric-1.8.6.jar) | | -| [1.19.2 Forge](https://github.com/cabaletta/baritone/releases/download/v1.9.4/baritone-api-forge-1.9.4.jar) | [1.19.2 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.9.4/baritone-api-fabric-1.9.4.jar) | | -| [1.19.3 Forge](https://github.com/cabaletta/baritone/releases/download/v1.9.1/baritone-api-forge-1.9.1.jar) | [1.19.3 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.9.1/baritone-api-fabric-1.9.1.jar) | | -| [1.19.4 Forge](https://github.com/cabaletta/baritone/releases/download/v1.9.5/baritone-api-forge-1.9.5.jar) | [1.19.4 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.9.5/baritone-api-fabric-1.9.5.jar) | | -| [1.20.1 Forge](https://github.com/cabaletta/baritone/releases/download/v1.10.3/baritone-api-forge-1.10.3.jar) | [1.20.1 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.10.3/baritone-api-fabric-1.10.3.jar) | | -| [1.20.3 Forge](https://github.com/cabaletta/baritone/releases/download/v1.10.4/baritone-api-forge-1.10.4.jar) | [1.20.3 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.10.4/baritone-api-fabric-1.10.4.jar) | [1.20.3 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.10.4/baritone-api-neoforge-1.10.4.jar) | -| [1.20.4 Forge](https://github.com/cabaletta/baritone/releases/download/v1.10.4/baritone-api-forge-1.10.4.jar) | [1.20.4 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.10.4/baritone-api-fabric-1.10.4.jar) | [1.20.4 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.10.4/baritone-api-neoforge-1.10.4.jar) | -| [1.21.1 Forge](https://github.com/cabaletta/baritone/releases/download/v1.11.2/baritone-api-forge-1.11.2.jar) | [1.21.1 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.11.2/baritone-api-fabric-1.11.2.jar) | [1.21.1 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.11.2/baritone-api-neoforge-1.11.2.jar) | -| [1.21.3 Forge](https://github.com/cabaletta/baritone/releases/download/v1.11.1/baritone-api-forge-1.11.1.jar) | [1.21.3 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.11.1/baritone-api-fabric-1.11.1.jar) | [1.21.3 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.11.1/baritone-api-neoforge-1.11.1.jar) | -| [1.21.4 Forge](https://github.com/cabaletta/baritone/releases/download/v1.13.1/baritone-api-forge-1.13.1.jar) | [1.21.4 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.13.1/baritone-api-fabric-1.13.1.jar) | [1.21.4 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.13.1/baritone-api-neoforge-1.13.1.jar) | -| [1.21.5 Forge](https://github.com/cabaletta/baritone/releases/download/v1.14.0/baritone-api-forge-1.14.0.jar) | [1.21.5 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.14.0/baritone-api-fabric-1.14.0.jar) | [1.21.5 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.14.0/baritone-api-neoforge-1.14.0.jar) | -| [1.21.6 Forge](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-forge-1.15.0.jar) | [1.21.6 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-fabric-1.15.0.jar) | [1.21.6 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-neoforge-1.15.0.jar) | -| [1.21.7 Forge](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-forge-1.15.0.jar) | [1.21.7 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-fabric-1.15.0.jar) | [1.21.7 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-neoforge-1.15.0.jar) | -| [1.21.8 Forge](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-forge-1.15.0.jar) | [1.21.8 Fabric](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-fabric-1.15.0.jar) | [1.21.8 NeoForge](https://github.com/cabaletta/baritone/releases/download/v1.15.0/baritone-api-neoforge-1.15.0.jar) | - -**How to immediately get started:** Type `#goto 1000 500` in chat to go to x=1000 z=500. Type `#mine diamond_ore` to mine diamond ore. Type `#stop` to stop. For more, read [the usage page](USAGE.md) and/or watch this [tutorial playlist](https://www.youtube.com/playlist?list=PLnwnJ1qsS7CoQl9Si-RTluuzCo_4Oulpa). Also try `#elytra` for Elytra flying in the Nether using fireworks ([trailer](https://youtu.be/4bGGPo8yiHo), [usage](https://youtu.be/NnSlQi-68eQ)). For help, join the [Baritone Discord Server](http://discord.gg/s6fRBAUpmr). - -For other versions of Minecraft or more complicated situations or for development, see [Installation & setup](SETUP.md). For 1.16.5, [click here](https://www.youtube.com/watch?v=_4eVJ9Qz2J8) and see description. Once Baritone is installed, look [here](USAGE.md) for instructions on how to use it. There's a [showcase video](https://youtu.be/CZkLXWo4Fg4) made by @Adovin#6313 on Baritone which I recommend. For help, join the [Baritone Discord Server](http://discord.gg/s6fRBAUpmr). - -This project is an updated version of [MineBot](https://github.com/leijurv/MineBot/), -the original version of the bot for Minecraft 1.8.9, rebuilt for 1.12.2 onwards. Baritone focuses on reliability and particularly performance (it's over [30x faster](https://github.com/cabaletta/baritone/pull/180#issuecomment-423822928) than MineBot at calculating paths). - -Have committed at least once a day from Aug 1, 2018, to Aug 1, 2019. - -1Leijurv3DWTrGAfmmiTphjhXLvQiHg7K2 +A Minecraft pathfinder bot. This is a fork of [Baritone](https://github.com/cabaletta/baritone) by +[leijurv](https://github.com/leijurv), tracking the Fabric branches of +[MeteorDevelopment/baritone](https://github.com/MeteorDevelopment/baritone), with fixes aimed at the +one thing anarchy servers ask of a pathfinder: travelling a very long way, unattended, without +dying. Everything upstream Baritone does — `#goto`, `#mine`, `#follow`, `#build`, `#farm` — works +here unchanged. + +Every fix lands on every supported Minecraft version, except the handful of bugs that only ever +existed on one of them. + +## Supported versions + +| Minecraft | Branch | Java | Release tag | +|-----------|-----------------------------------------------------|------|-------------------| +| 1.21.4 | [`1.21.4`](https://github.com/dekrom/baritone/tree/1.21.4) | 21 | `v-1.21.4` | +| 1.21.5 | [`1.21.5`](https://github.com/dekrom/baritone/tree/1.21.5) | 21 | `v-1.21.5` | +| 1.21.8 | [`1.21.8`](https://github.com/dekrom/baritone/tree/1.21.8) | 21 | `v-1.21.8` | +| 1.21.11 | [`1.21.11`](https://github.com/dekrom/baritone/tree/1.21.11) | 21 | `v-1.21.11` | +| 26.1 | [`26.1`](https://github.com/dekrom/baritone/tree/26.1) | 25 | `v-26.1` | +| 26.2 | [`26.2`](https://github.com/dekrom/baritone/tree/26.2) | 25 | `v-26.2` | +| 26.3 | [`26.3`](https://github.com/dekrom/baritone/tree/26.3) | 25 | `v-26.3` | + +Each Minecraft version has its own branch and its own release. Fabric only. + +## Install + +1. Grab the jar for your Minecraft version from [Releases](https://github.com/dekrom/baritone/releases). + - `baritone-api-fabric-*.jar` — use this one if another mod (Meteor Client, an addon, your own + mod) drives Baritone through its API. This is what most people want. + - `baritone-standalone-fabric-*.jar` — everything exposed, for using Baritone on its own. + - `baritone-unoptimized-fabric-*.jar` — not obfuscated. Use it when reporting a bug so the stack + trace is readable. +2. Drop it in `mods/` next to Fabric API. +3. Type `#goto 1000 500` in chat. + +Verify your download against `checksums.txt` on the release. See [SETUP.md](SETUP.md) for anything +more involved, and [USAGE.md](USAGE.md) for the chat commands. + +## What this fork changes + +### Elytra + +`#elytra` is the reason this fork exists. Upstream's implementation is good at flying a computed +path and bad at everything around it: getting into the air, and getting out of trouble once the +world stops matching the path. + +- **Takeoff from anywhere.** `elytraAutoJump` only knew how to walk to an overhang and step off it. + With no overhang in reach — most of the overworld, and most of a nether highway — it spent a + couple of seconds pathfinding and then told you to go find a cliff. It now falls back to jumping + on the spot, opening the elytra on the way up the way a vanilla double tap does, and lighting a + firework the instant it opens. Three ways it could previously stand still forever are fixed as + well, including landing at exactly y=31 in the nether, where the old fixed takeoff goal was + already satisfied where you stood. +- **A survival fallback.** When the solver ran out of options it returned a heading with no pitch, + which in practice means holding the last pitch straight into the terrain. That is exactly the + case that matters: the world changed under the path, a wall appeared, something knocked you off + course. It now simulates the pitch range and flies whatever trajectory stays clear the longest, + preferring the one that ends higher, and forces a firework when nothing else buys enough time. +- **Physics that match the server.** The flight simulation had drifted from + `LivingEntity.updateFallFlyingMovement`: hardcoded gravity instead of the `GRAVITY` attribute, no + Slow Falling, a cosine term from `Mth`'s lookup table with a factor vanilla does not have, a + missing divide-by-zero guard that produced `NaN` when looking straight up or down, and drag as + `float` where vanilla uses `double`. Every tick of error compounds across the horizon the solver + picks a pitch from. +- **Recalculating instead of hoping.** A path node buried in terrain used to be noticed and + ignored. The segment is now recomputed, rejoining the existing path at the last node still in + open air. +- **`elytraPathLookahead`** exposes how far ahead the solver looks for a node to aim at (was a + hardcoded 20). Higher cuts corners harder at firework speed. +- **`elytraStandingTakeoff`** turns the standing takeoff on and off. On by default, capped at three + attempts so a bad spot cannot burn a stack of rockets. +- **Taking off from a hole.** Where a nether flight ends when the rockets run out is where the next + one starts, and in the basalt deltas that is a crevice with walls a couple of blocks away on every + side and open sky straight up. The standing takeoff asked for a path from your feet, and the + pathfinder starts every path from the nearest 4×4×4 cube of air, found by a search that walks + straight through walls — from a hole, very often a cube on the far side of one. The rocket flew at + it, hit the wall and dropped back in. The path now starts from the first clear cube straight + overhead. With a rocket burning, the survival solver takes the pitch that ends highest rather than + the first one that survives, and it forces a rocket as soon as an impact is anywhere in its + horizon instead of a dozen ticks out. +- **The takeoff rocket was never accepted.** It was lit in the same tick the elytra opened, which no + vanilla client can do — its use packet goes out before the glide packet — and it carried the + camera's rotation while that tick's movement packet carried the takeoff aim, two looks the + server's movement checks compare. The rocket goes the tick after now, every use packet carries + the rotation the movement packet will, and the solver assumes a rocket it has just lit is burning + instead of planning an unboosted glide into the nearest wall while the entity is in transit. A + takeoff the server refuses is reported as such. +- **Lava.** An elytra opens in lava but does nothing there — vanilla moves you by the fluid rules + — so the rocket is the only thrust, and it was aimed at a path that leads through the pond's wall + while the landing logic asked for a new path from inside the pool twenty times a second. In lava + the process now swims up, opens the elytra, aims straight up and lights rockets until it is out; + the landing search waits. +- **Unfinished segments.** A recomputation around an obstacle that ran out of time short of its + rejoin node had the old path stitched on anyway, leaving a blind jump through whatever lay + between; every recomputation ended the same way, and the solver circled next to a path drawn + through solid ground. An unfinished segment now stands on its own and is continued from where the + search got to. +- **A cache that disagrees with the world.** Everything the solver does runs against the + pathfinder's copy of the world, and when that copy was wrong nothing noticed, because the + raytraces look at the same copy. A chunk packet the client discarded was packed as a real chunk + made of air, and a chunk whose packing was missed stayed missed until a cancel. Discarded chunks + are skipped now, and a collision, a "we are inside a block" verdict or a failed recomputation + re-reads the chunks around the player. +- **A hung teardown.** The pathfinder context's teardown waited without limit for a native search + that the cancel cannot interrupt. On 26.2, where the teardown hands back the semaphore the next + engage needs, one hung search silently killed every later `#elytra` for the session; elsewhere it + pinned a thread and leaked the context. The wait is bounded now, and a context whose search is + still wedged after it is abandoned rather than freed underneath it. +- **Cancelling could crash the client.** The nether pathfinder's native context was freed once its + own executors had drained, but the solver and the game thread raytrace through it outside those + executors, and the octree interface caches a raw pointer into it. Running one of those against + freed memory is a segfault, not an exception. It is freed under the lock they hold now, and every + native call is fenced behind a flag that fails safe. +- **The XaeroPlus path overlay.** XaeroPlus finds the elytra path by field name, and the obfuscated + jars renamed those fields, so the path never showed on its map. They keep their names now. + +### Chunk cache + +- Region files were rewritten in place, from the cache save thread, while everything else was + reading them. A read that landed mid-rewrite got a truncated gzip stream and threw away a + 512×512 region's worth of cached terrain. Saves are written to a temp file and renamed over the + old one, so a reader sees either the previous region or the new one. On 26.2 this is what the + nether pathfinder reads with `elytraUseCache` on, and a region it fails to parse is gone for the + rest of the flight. +- Waypoint files were written the same way, in place, and a save cut off mid-write — a crash, or + the game exiting while the file was still being written — left a truncated file that the next + load silently discarded, taking every waypoint of that tag with it. They are written to a temp + file and renamed over the old one too, so an interrupted save keeps the previous waypoints. +- **RAM is bounded.** Regions were only dropped from RAM after the ten minute save, and only by + distance from the player, so nothing capped how much of the cache stayed resident on a long trip. + A maintenance thread now saves changed regions every 30 seconds (`cachedRegionsSaveIntervalSeconds`), + drops regions nothing has touched for three minutes (`cachedRegionsIdleSeconds`), and then the + least recently used ones while the cache is over 192 MiB (`cachedRegionsRamBudgetMB`). A region + with unsaved changes is kept while they can still be saved; with `chunkCaching` off or no cache + directory nothing ever saves them, so those regions are evicted like any other instead of pinning + the whole session's terrain in RAM. A path calculation that reaches a region on disk but not + in RAM loads it back in the background (`cachedRegionsLoadOnDemand`), so terrain seen in an + earlier session counts again. +- **Less packing.** The check meant to skip empty sections tested for `null`, which modern chunks + never are, so every all-air section cost 4096 block lookups to pack to zeros: most of an overworld + column above the surface and the nether above the roof. Those are skipped now, and a chunk that + unloads unchanged since it was packed on load is not packed again + (`repackUnchangedChunksOnUnload` brings that back). +- **Server folder names.** Only Windows had the colon of a `host:port` server replaced, so on an + NTFS drive mounted under Linux such a server never got a cache directory and every region save + failed. Names are sanitised on every platform now, and an existing directory under the old name + keeps being used. + +### Pathfinding correctness + +- Falls were charged one tick of gravity that never happens, because `velocity(0)` is zero. +- Descending onto a block counted one block of fall too many; the fall ends on top of the block. +- `GoalRunAway` folded its bounding box's max corner against the running minimum, so with more than + one position the box collapsed towards the minimum corner. +- The dimension height check compared a y coordinate against a block count, letting the search + climb 64 blocks above the overworld build limit. +- Waterlogged fences, walls, panes, bars, stairs and leaves were treated as plain water and walked + into. +- `fullyPassable` listed the blocks you cannot jump through one at a time, so powder snow, honey, + berry bushes, dripstone and anything in `blocksToAvoid` were considered jumpable. The hitbox is + the same width in the air as on the ground. +- A diagonal's second corner was tested for lava but its magma check read the first corner again. +- `backtrackCostFavoringCoefficient` at 0 or below produced a non-positive action cost, which A\* + rejects, failing the whole calculation instead of favoring nothing. +- `GoalGetToBlock` reported an ETA that never counted down to zero. +- `GoalNear` and `GoalRunAway` squared `int` coordinate differences, which overflow: a point 65,536 + blocks away could satisfy a two-block `GoalNear`. `GoalRunAway` also truncated its radius and + threw when hashed without a y constraint. +- `#find` searched the cache around the player's y where it meant their z. + +### Building + +- `#litematica` could report Litematica as missing with it installed: the check for it pulled in one + of Litematica's world classes before the game could load it. +- With `buildIgnoreDirection` or `buildIgnoreProperties` set, the comparison between the placed + block and the schematic checked each property value by reference rather than by value. Right for + every vanilla property by luck — small integers happen to be interned — and nothing guaranteed it + stayed that way; it compares by value now. + +### Pathing thread + +- `ToolSet` read the live inventory from the calculation thread — a data race with the game thread — + and cached break speeds in a plain `HashMap`. The hotbar and selected slot are now snapshotted on + construction and the cache is concurrent. +- `assumeWalkOnWater` was read off the settings mid-calculation, so flipping it made the cost + function inconsistent with the nodes already expanded. It is read once per `BlockStateInterface`. +- `cancelRequested` is written by the game thread and read by the calculation thread, and was not + `volatile`. +- `bestPathSoFar` allocated a `BetterBlockPos` per node on every tick a calculation was running, + before the cheap checks that almost always decide the outcome; `PathExecutor` built a whole + `CalculationContext` per tick to read two fields off it. + +### 26.2 and 26.3 only + +- Nether pathfinder chunk packing wrote solid sections as air, so `#elytra` planned paths straight + through terrain. The 26.2 port had replaced the removed `LevelChunkSection.SECTION_SIZE` (4096, + blocks per section) with a local `16`, and the all-solid fast path then memset 2 bytes instead of + 512. +- Baritone's threadpool threads are daemon threads. As of 26.2 the game waits on non-daemon threads + at shutdown and files a crash report if any are still alive. +- **Cancelling `#elytra` froze the game.** 26.2 keeps one pathfinder context alive across engages, + and re-engaging blocked the game thread on the old one until it was freed. A path calculation + cannot be interrupted — nether-pathfinder's `cancel()` sets a flag its search loop does not read — + so the client hung for whatever the in-flight calculation had left, up to ten seconds. The engage + is deferred onto the teardown now, and the calculation timeout is three seconds. +- **The endless `Failed to compute path to destination`.** That same persistent context is fed + Baritone's region cache, and a region it fails to parse is never retried, so one bad read leaves + a permanent hole in its world and every later calculation fails against it. Three consecutive + failures discard the context so the next attempt starts from a clean cache; skipped while flying, + where a reset would drop the elytra. +- The above-build-limit fallback divided by a horizontal distance of zero, making every step of a + straight-up path `NaN`, and compared a squared distance against an unsquared 32. +- **Segfault in `getChunkOrDefault`.** The native path calculation ran under the read lock whenever + terrain prediction was off, on the theory that it only reads the cache. With `elytraUseCache` it + also parses region files into it, and an insert that rehashes the map under the solvers' lookups + is heap corruption. A path calculation is always a writer now. +- **`#elytra` going idle mid-engage.** An engage deferred behind the old context's teardown left the + process without a behavior, so it reported itself inactive and dropped its destination; the + pathing manager stopped ticking it and anything polling for the goal issued it again. It stays + active and keeps its destination until the engage lands. +- **`#mine` stopped picking up items.** Upstream's #5058 resolved loot tables through the client's + registries, which never contain them, so every block had no drops. Reverted, as upstream did. + +## Building + +Java 21 for the `1.21.x` branches, Java 25 for `26.x` (see the table above). + +```sh +./gradlew build # jars land in dist/, with checksums.txt +./gradlew test +``` -# Getting Started +Tagging `v-` on a version branch builds it and publishes the release. -Here are some links to help to get started: +## Docs - [Features](FEATURES.md) - - [Installation & setup](SETUP.md) - -- [API Javadocs](https://baritone.leijurv.com/) - -- [Settings](https://baritone.leijurv.com/baritone/api/Settings.html#field.detail) - - [Usage (chat control)](USAGE.md) +- [Settings](https://baritone.leijurv.com/baritone/api/Settings.html#field.detail) +- [API Javadocs](https://baritone.leijurv.com/) (upstream) -## Stars over time - -[![Stargazers over time](https://starchart.cc/cabaletta/baritone.svg)](https://starchart.cc/cabaletta/baritone) - -# 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 -jar. +## API -Below is an example of basic usage for changing some settings, and then pathing to an X/Z goal. +The API is heavily documented. Anything outside the `baritone.api` package is not supported by the +API release jar. ```java BaritoneAPI.getSettings().allowSprint.value = true; @@ -123,26 +270,18 @@ BaritoneAPI.getSettings().primaryTimeoutMS.value = 2000L; BaritoneAPI.getProvider().getPrimaryBaritone().getCustomGoalProcess().setGoalAndPath(new GoalXZ(10000, 20000)); ``` -# FAQ - -## Can I use Baritone as a library in my custom utility client? - -That's what it's for, sure! (As long as usage complies with the LGPL 3.0 License) - -## How is it so fast? - -Magic. (Hours of [leijurv](https://github.com/leijurv/) enduring excruciating pain) - -### Additional Special Thanks To: - -![YourKit-Logo](https://www.yourkit.com/images/yklogo.png) - -YourKit supports open source projects with innovative and intelligent tools for monitoring and profiling Java and .NET applications. +## Credits -YourKit is the creator of the [YourKit Java Profiler](https://www.yourkit.com/java/profiler/), [YourKit .NET Profiler](https://www.yourkit.com/.net/profiler/), and [YourKit YouMonitor](https://www.yourkit.com/youmonitor/). +Baritone is by [leijurv](https://github.com/leijurv) and +[its contributors](https://github.com/cabaletta/baritone/graphs/contributors); it grew out of +[MineBot](https://github.com/leijurv/MineBot/). The Fabric branches this fork tracks are maintained +by [MeteorDevelopment](https://github.com/MeteorDevelopment/baritone). The elytra pathfinder is +[nether-pathfinder](https://github.com/babbaj/nether-pathfinder) by +[babbaj](https://github.com/babbaj). -We thank them for granting Baritone an OSS license so that we can make our software the best it can be. +For help with Baritone itself, the [Baritone Discord](http://discord.gg/s6fRBAUpmr) is the place to +ask. Issues with *this fork* belong [here](https://github.com/dekrom/baritone/issues). -## Why is it called Baritone? +## License -It's named for FitMC's deep sultry voice. +LGPL-3.0, same as upstream. See [LICENSE](LICENSE). diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json index d3dfbee36e..a9adbfb2c0 100644 --- a/fabric/src/main/resources/fabric.mod.json +++ b/fabric/src/main/resources/fabric.mod.json @@ -24,8 +24,8 @@ "mixins.baritone-meteor.json" ], "depends": { - "fabricloader": ">=0.19.3", - "minecraft": ["26.2"] + "fabricloader": ">=0.19.5", + "minecraft": "~26.3" }, "custom": { "modmenu": { diff --git a/forge/src/main/resources/META-INF/mods.toml b/forge/src/main/resources/META-INF/mods.toml index 5328e7bbf4..c6d1f26722 100644 --- a/forge/src/main/resources/META-INF/mods.toml +++ b/forge/src/main/resources/META-INF/mods.toml @@ -35,6 +35,6 @@ A Minecraft pathfinder bot. modId="minecraft" mandatory=true # This version range declares a minimum of the current minecraft version up to but not including the next major version -versionRange="[26.2]" +versionRange="[26.3, 26.4)" ordering="NONE" side="BOTH" diff --git a/gradle.properties b/gradle.properties index 8a1870fb35..19bb4d9675 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,19 +2,19 @@ org.gradle.jvmargs=-Xmx4G available_loaders=fabric -mod_version=26.2-SNAPSHOT +mod_version=26.3-SNAPSHOT maven_group=baritone archives_base_name=baritone java_version=25 -minecraft_version=26.2 +minecraft_version=26.3 -forge_version=65.0.3 +forge_version=66.0.2 -neoforge_version=11-beta +neoforge_version=6-beta -fabric_version=0.19.3 +fabric_version=0.19.5 nether_pathfinder_version=1.6 diff --git a/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/src/main/resources/META-INF/neoforge.mods.toml index 4eec7768ea..567bd7abe5 100644 --- a/neoforge/src/main/resources/META-INF/neoforge.mods.toml +++ b/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -35,7 +35,7 @@ A Minecraft pathfinder bot. modId="minecraft" type="required" # This version range declares a minimum of the current minecraft version up to but not including the next major version -versionRange="[26.2]" +versionRange="[26.3, 26.4)" ordering="NONE" side="BOTH" diff --git a/scripts/proguard.pro b/scripts/proguard.pro index dd63b216ba..90e4d8acca 100644 --- a/scripts/proguard.pro +++ b/scripts/proguard.pro @@ -44,6 +44,19 @@ @baritone.KeepName *; } +# XaeroPlus (BaritonePathSync) reflects the elytra path through these members by name; +# Meteor's obfuscated builds rename them and lose the map overlay +-keepclassmembers class baritone.process.ElytraProcess { + baritone.process.elytra.ElytraBehavior behavior; +} +-keepclassmembers class baritone.process.elytra.ElytraBehavior { + baritone.process.elytra.ElytraBehavior$PathManager pathManager; +} +-keepclassmembers class baritone.process.elytra.ElytraBehavior$PathManager { + baritone.process.elytra.NetherPath path; + public baritone.process.elytra.NetherPath getPath(); +} + # setting names are reflected from field names, so keep field names -keepclassmembers class baritone.api.Settings { public ; diff --git a/src/api/java/baritone/api/Settings.java b/src/api/java/baritone/api/Settings.java index 968cf986c7..fe14ae197c 100644 --- a/src/api/java/baritone/api/Settings.java +++ b/src/api/java/baritone/api/Settings.java @@ -646,16 +646,48 @@ public final class Settings { public final Setting chunkCaching = new Setting<>(true); /** - * On save, delete from RAM any cached regions that are more than 1024 blocks away from the player + * Evict cached regions from RAM. The cache maintenance thread drops a region once it has been idle (not read + * by pathing or scanning, not written by the packer) for longer than {@link #cachedRegionsIdleSeconds}, and + * drops the least recently used regions whenever the resident cache exceeds {@link #cachedRegionsRamBudgetMB}. + * Regions with unsaved changes are written to disk first and are never evicted before that write succeeded, + * unless {@link #chunkCaching} is off and nothing will ever write them. *

- * Temporarily disabled - *

- * Temporarily reenabled - * - * @see Issue #248 + * false keeps every region touched in this session in RAM. */ public final Setting pruneRegionsFromRAM = new Setting<>(true); + /** + * Upper bound for the cached regions kept in RAM, in MiB. A fully cached region costs about 27 MiB + * (1024 chunks of 24 KiB block data). The bound is soft by one maintenance tick. + */ + public final Setting cachedRegionsRamBudgetMB = new Setting<>(192); + + /** + * A cached region that nothing has read or written for this many seconds is evicted from RAM (after being + * saved if it changed). Regions come back from disk on demand. + */ + public final Setting cachedRegionsIdleSeconds = new Setting<>(180); + + /** + * How often changed regions are written to disk. The elytra nether pathfinder reads the region files, so this + * is also how long newly seen terrain takes to become visible to it. + */ + public final Setting cachedRegionsSaveIntervalSeconds = new Setting<>(30); + + /** + * When pathing asks for a region that is not in RAM, load it from disk in the background so the next path + * calculation sees terrain explored in earlier sessions. Without this only the packer (chunks around the + * player) and explicit scans (#find, explore) ever bring regions into RAM. + */ + public final Setting cachedRegionsLoadOnDemand = new Setting<>(true); + + /** + * Pack a chunk again when it unloads even if no block changed since it was packed on load. Off by default: + * the unload pack doubled the packer's work for an identical result. A chunk that changed while loaded, or + * whose load-time pack was dropped or has not run yet, is still packed on unload. + */ + public final Setting repackUnchangedChunksOnUnload = new Setting<>(false); + /** * The chunk packer queue can never grow to larger than this, if it does, the oldest chunks are discarded *

@@ -1445,6 +1477,13 @@ public final class Settings { */ public final Setting elytraPitchRange = new Setting<>(25); + /** + * The number of path nodes ahead of the player that are considered as flight targets. Higher values let the bot + * commit to a further-away node and cut corners, which is smoother at firework speed, but are computationally + * more expensive. + */ + public final Setting elytraPathLookahead = new Setting<>(20); + /** * The minimum speed that the player can drop to (in blocks/tick) before a firework is automatically deployed. */ @@ -1490,6 +1529,12 @@ public final class Settings { */ public final Setting elytraAutoJump = new Setting<>(false); + /** + * When {@link #elytraAutoJump} can't find a ledge to walk off of, jump on the spot and light a firework the + * moment the elytra opens instead of giving up. Costs one firework per attempt. + */ + public final Setting elytraStandingTakeoff = new Setting<>(true); + /** * The seed used to generate chunks for long distance elytra path-finding in the nether. * Defaults to 2b2t's nether seed. diff --git a/src/api/java/baritone/api/pathing/goals/GoalGetToBlock.java b/src/api/java/baritone/api/pathing/goals/GoalGetToBlock.java index 1c04f7c6d1..a0116be6f3 100644 --- a/src/api/java/baritone/api/pathing/goals/GoalGetToBlock.java +++ b/src/api/java/baritone/api/pathing/goals/GoalGetToBlock.java @@ -61,6 +61,13 @@ public double heuristic(int x, int y, int z) { return GoalBlock.calculate(xDiff, yDiff < 0 ? yDiff + 1 : yDiff, zDiff); } + @Override + public double heuristic() { + // the largest heuristic value that is still guaranteed to be inside the goal: the six positions + // adjacent to the block, of which the one directly above is the most expensive + return Math.max(GoalXZ.calculate(1, 0), Math.max(GoalYLevel.calculate(0, 1), GoalYLevel.calculate(0, -1))); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/src/api/java/baritone/api/pathing/goals/GoalNear.java b/src/api/java/baritone/api/pathing/goals/GoalNear.java index 351c080216..630856c539 100644 --- a/src/api/java/baritone/api/pathing/goals/GoalNear.java +++ b/src/api/java/baritone/api/pathing/goals/GoalNear.java @@ -29,20 +29,20 @@ public class GoalNear implements Goal, IGoalRenderPos { private final int x; private final int y; private final int z; - private final int rangeSq; + private final long rangeSq; public GoalNear(BlockPos pos, int range) { this.x = pos.getX(); this.y = pos.getY(); this.z = pos.getZ(); - this.rangeSq = range * range; + this.rangeSq = (long) range * range; } @Override public boolean isInGoal(int x, int y, int z) { - int xDiff = x - this.x; - int yDiff = y - this.y; - int zDiff = z - this.z; + long xDiff = (long) x - this.x; + long yDiff = (long) y - this.y; + long zDiff = (long) z - this.z; return xDiff * xDiff + yDiff * yDiff + zDiff * zDiff <= rangeSq; } @@ -105,7 +105,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return (int) BetterBlockPos.longHash(x, y, z) + rangeSq; + return (int) BetterBlockPos.longHash(x, y, z) + Long.hashCode(rangeSq); } @Override diff --git a/src/api/java/baritone/api/pathing/goals/GoalRunAway.java b/src/api/java/baritone/api/pathing/goals/GoalRunAway.java index 1a1b6e50c4..462968f1b4 100644 --- a/src/api/java/baritone/api/pathing/goals/GoalRunAway.java +++ b/src/api/java/baritone/api/pathing/goals/GoalRunAway.java @@ -34,7 +34,7 @@ public class GoalRunAway implements Goal { private final BlockPos[] from; - private final int distanceSq; + private final double distanceSq; private final Integer maintainY; @@ -47,7 +47,7 @@ public GoalRunAway(double distance, Integer maintainY, BlockPos... from) { throw new IllegalArgumentException("Positions to run away from must not be empty"); } this.from = from; - this.distanceSq = (int) (distance * distance); + this.distanceSq = distance * distance; this.maintainY = maintainY; } @@ -57,9 +57,9 @@ public boolean isInGoal(int x, int y, int z) { return false; } for (BlockPos p : from) { - int diffX = x - p.getX(); - int diffZ = z - p.getZ(); - int distSq = diffX * diffX + diffZ * diffZ; + double diffX = (double) x - p.getX(); + double diffZ = (double) z - p.getZ(); + double distSq = diffX * diffX + diffZ * diffZ; if (distSq < distanceSq) { return false; } @@ -96,9 +96,9 @@ public double heuristic() {// TODO less hacky solution minX = Math.min(minX, p.getX() - distance); minY = Math.min(minY, p.getY() - distance); minZ = Math.min(minZ, p.getZ() - distance); - maxX = Math.max(minX, p.getX() + distance); - maxY = Math.max(minY, p.getY() + distance); - maxZ = Math.max(minZ, p.getZ() + distance); + maxX = Math.max(maxX, p.getX() + distance); + maxY = Math.max(maxY, p.getY() + distance); + maxZ = Math.max(maxZ, p.getZ() + distance); } DoubleOpenHashSet maybeAlwaysInside = new DoubleOpenHashSet(); // see pull request #1978 double minOutside = Double.POSITIVE_INFINITY; @@ -143,8 +143,8 @@ public boolean equals(Object o) { @Override public int hashCode() { int hash = Arrays.hashCode(from); - hash = hash * 1196803141 + distanceSq; - hash = hash * -2053788840 + maintainY; + hash = hash * 1196803141 + Double.hashCode(distanceSq); + hash = hash * -2053788840 + Objects.hashCode(maintainY); return hash; } diff --git a/src/api/java/baritone/api/pathing/movement/ActionCosts.java b/src/api/java/baritone/api/pathing/movement/ActionCosts.java index 691d73b4d5..447ef7c1c3 100644 --- a/src/api/java/baritone/api/pathing/movement/ActionCosts.java +++ b/src/api/java/baritone/api/pathing/movement/ActionCosts.java @@ -87,7 +87,9 @@ static double distanceToTicks(double distance) { double tmpDistance = distance; int tickCount = 0; while (true) { - double fallDistance = velocity(tickCount); + // you have already been falling for one tick by the time you have moved at all; velocity(0) is 0, + // so indexing from tickCount would charge a phantom tick against every fall + double fallDistance = velocity(tickCount + 1); if (tmpDistance <= fallDistance) { return tickCount + tmpDistance / fallDistance; } diff --git a/src/api/java/baritone/api/utils/BlockOptionalMeta.java b/src/api/java/baritone/api/utils/BlockOptionalMeta.java index a9cc2caa56..1505a448d5 100644 --- a/src/api/java/baritone/api/utils/BlockOptionalMeta.java +++ b/src/api/java/baritone/api/utils/BlockOptionalMeta.java @@ -59,7 +59,6 @@ import sun.misc.Unsafe; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.*; @@ -281,9 +280,10 @@ public static ServerLevelStub fastCreate() { @Override public RegistryAccess registryAccess() { - if (client.level != null) { - return client.level.registryAccess(); - } + // Loot tables are populated only by the reloaded server registries built + // in load(). A client RegistryAccess never receives them, so sourcing the + // holder from client.level made every block resolve to zero drops, which + // silently broke #mine item pickup (regression from PR #5058). return registryAccess.join(); } @@ -305,22 +305,22 @@ public static CompletableFuture load() { // Simplified from {@link net.minecraft.server.WorldLoader#load()} CloseableResourceManager closeableResourceManager = new MultiPackResourceManager( PackType.SERVER_DATA, - List.of(ServerPacksSource.createVanillaPackSource()) + List.of(ServerPacksSource.createVanillaPackSource().fullResources()) ); LayeredRegistryAccess baseLayeredRegistry = RegistryLayer.createRegistryAccess(); List> pendingTags = TagLoader.loadTagsForExistingRegistries( closeableResourceManager, baseLayeredRegistry.getLayer(RegistryLayer.STATIC) ); List> worldGenRegistryLookupList = TagLoader.buildUpdatedLookups( - baseLayeredRegistry.getAccessForLoading(RegistryLayer.WORLDGEN), + baseLayeredRegistry.getAccessForLoading(RegistryLayer.WORLD), pendingTags ); LayeredRegistryAccess layeredRegistryAccess = baseLayeredRegistry.replaceFrom( - RegistryLayer.WORLDGEN, + RegistryLayer.WORLD, RegistryDataLoader.load( closeableResourceManager, worldGenRegistryLookupList, - RegistryDataLoader.WORLDGEN_REGISTRIES, + RegistryDataLoader.WORLD_REGISTRIES, ForkJoinPool.commonPool() ).join() ); diff --git a/src/launch/java/baritone/launch/mixins/MixinClientPlayNetHandler.java b/src/launch/java/baritone/launch/mixins/MixinClientPlayNetHandler.java index 1985b31517..729e397ff0 100644 --- a/src/launch/java/baritone/launch/mixins/MixinClientPlayNetHandler.java +++ b/src/launch/java/baritone/launch/mixins/MixinClientPlayNetHandler.java @@ -108,8 +108,8 @@ private void postHandleChunkData(ClientboundLevelChunkWithLightPacket packetIn, new ChunkEvent( EventState.POST, !packetIn.isSkippable() ? ChunkEvent.Type.POPULATE_FULL : ChunkEvent.Type.POPULATE_PARTIAL, - packetIn.getX(), - packetIn.getZ() + packetIn.x(), + packetIn.z() ) ); } diff --git a/src/launch/java/baritone/launch/mixins/MixinMinecraft.java b/src/launch/java/baritone/launch/mixins/MixinMinecraft.java index 37455dbd28..5762e05de3 100644 --- a/src/launch/java/baritone/launch/mixins/MixinMinecraft.java +++ b/src/launch/java/baritone/launch/mixins/MixinMinecraft.java @@ -114,7 +114,7 @@ private void postRunTick(CallbackInfo ci) { method = "tick", at = @At( value = "INVOKE", - target = "net/minecraft/client/multiplayer/ClientLevel.tickEntities()V", + target = "Lnet/minecraft/client/player/LocalPlayer;sendChanges()V", shift = At.Shift.AFTER ) ) diff --git a/src/launch/java/baritone/launch/mixins/MixinRenderPipelines.java b/src/launch/java/baritone/launch/mixins/MixinRenderPipelines.java index f5a8a6e97e..4f07497a24 100644 --- a/src/launch/java/baritone/launch/mixins/MixinRenderPipelines.java +++ b/src/launch/java/baritone/launch/mixins/MixinRenderPipelines.java @@ -18,7 +18,7 @@ package baritone.launch.mixins; import baritone.utils.accessor.IRenderPipelines; -import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.renderpearl.api.pipeline.RenderPipeline; import net.minecraft.client.renderer.RenderPipelines; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; diff --git a/src/launch/java/baritone/launch/mixins/MixinWorldRenderer.java b/src/launch/java/baritone/launch/mixins/MixinWorldRenderer.java index 51bb7e3e83..ee93e521dc 100644 --- a/src/launch/java/baritone/launch/mixins/MixinWorldRenderer.java +++ b/src/launch/java/baritone/launch/mixins/MixinWorldRenderer.java @@ -20,13 +20,11 @@ import baritone.api.BaritoneAPI; import baritone.api.IBaritone; import baritone.api.event.events.RenderEvent; -import com.mojang.blaze3d.buffers.GpuBufferSlice; import com.mojang.blaze3d.resource.GraphicsResourceAllocator; import com.mojang.blaze3d.vertex.PoseStack; -import net.minecraft.client.DeltaTracker; +import com.mojang.renderpearl.api.buffers.GpuBufferSlice; import net.minecraft.client.renderer.LevelRenderer; import net.minecraft.client.renderer.state.level.CameraRenderState; -import org.joml.Matrix4fc; import org.joml.Vector4f; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -44,11 +42,11 @@ public class MixinWorldRenderer { method = "render", at = @At("RETURN") ) - private void onStartHand(final GraphicsResourceAllocator allocator, final DeltaTracker deltaTracker, final boolean outline, final CameraRenderState camera, final Matrix4fc modelViewMatrix, final GpuBufferSlice fog, final Vector4f fogColor, final boolean sky, final CallbackInfo ci) { + private void onStartHand(final GraphicsResourceAllocator resourceAllocator, final boolean renderOutline, final CameraRenderState cameraState, final GpuBufferSlice terrainFog, final Vector4f fogColor, final boolean shouldRenderSky, final boolean consistentDepthRequired, final CallbackInfo ci) { for (IBaritone ibaritone : BaritoneAPI.getProvider().getAllBaritones()) { PoseStack poseStack = new PoseStack(); - poseStack.mulPose(modelViewMatrix); - ibaritone.getGameEventHandler().onRenderPass(new RenderEvent(deltaTracker.getGameTimeDeltaPartialTick(false), poseStack, camera.projectionMatrix)); + poseStack.mulPose(cameraState.viewRotationMatrix); + ibaritone.getGameEventHandler().onRenderPass(new RenderEvent(cameraState.cameraEntityPartialTicks, poseStack, cameraState.projectionMatrix)); } } } diff --git a/src/main/java/baritone/Baritone.java b/src/main/java/baritone/Baritone.java index 53f2bd3e0a..6e1405606f 100755 --- a/src/main/java/baritone/Baritone.java +++ b/src/main/java/baritone/Baritone.java @@ -36,6 +36,7 @@ import baritone.utils.InputOverrideHandler; import baritone.utils.PathingControlManager; import baritone.utils.player.BaritonePlayerContext; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import net.minecraft.client.Minecraft; import java.io.IOException; @@ -58,7 +59,13 @@ public class Baritone implements IBaritone { private static final ThreadPoolExecutor threadPool; static { - threadPool = new ThreadPoolExecutor(4, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<>()); + threadPool = new ThreadPoolExecutor(4, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), + new ThreadFactoryBuilder() + .setNameFormat("baritone-pool-%d") + // as of mc 26.2, mc will await non-daemon threads exit on game stop - crash reporting if any non-daemon thread is still alive + .setDaemon(true) + .build() + ); } private final Minecraft mc; diff --git a/src/main/java/baritone/behavior/LookBehavior.java b/src/main/java/baritone/behavior/LookBehavior.java index 98e12aeff3..bdb8d7e284 100644 --- a/src/main/java/baritone/behavior/LookBehavior.java +++ b/src/main/java/baritone/behavior/LookBehavior.java @@ -158,6 +158,19 @@ public void pig() { } } + /** + * The rotation this tick's movement packet is going to carry: the one {@link #onPlayerUpdate} applies for the + * current target, or the player's own if there is no target or the target leaves the rotation alone. Anything + * else sent this tick that carries a look of its own (a use packet) has to match it exactly, because the + * server-side movement checks compare the two. + */ + public Rotation getRotationForThisTick() { + if (this.target == null || this.target.mode == Target.Mode.NONE) { + return new Rotation(ctx.player().getYRot(), ctx.player().getXRot()); + } + return this.processor.peekRotation(this.target.rotation); + } + public Optional getEffectiveRotation() { if (Baritone.settings().freeLook.value) { return Optional.ofNullable(this.serverRotation); diff --git a/src/main/java/baritone/behavior/PathingBehavior.java b/src/main/java/baritone/behavior/PathingBehavior.java index 6ed20d50f2..70406117c7 100644 --- a/src/main/java/baritone/behavior/PathingBehavior.java +++ b/src/main/java/baritone/behavior/PathingBehavior.java @@ -139,13 +139,15 @@ private void tickPath() { // we are calculating // are we calculating the right thing though? 🤔 BetterBlockPos calcFrom = inProgress.getStart(); - Optional currentBest = inProgress.bestPathSoFar(); if ((current == null || !current.getPath().getDest().equals(calcFrom)) // if current ends in inProgress's start, then we're ok && !calcFrom.equals(ctx.playerFeet()) && !calcFrom.equals(expectedSegmentStart) // if current starts in our playerFeet or pathStart, then we're ok - && (!currentBest.isPresent() || (!currentBest.get().positions().contains(ctx.playerFeet()) && !currentBest.get().positions().contains(expectedSegmentStart))) // if ) { + // only now is it worth reconstructing the best path so far, which allocates one BetterBlockPos per node + Optional currentBest = inProgress.bestPathSoFar(); // when it was *just* started, currentBest will be empty so we need to also check calcFrom since that's always present - inProgress.cancel(); // cancellation doesn't dispatch any events + if (!currentBest.isPresent() || (!currentBest.get().positions().contains(ctx.playerFeet()) && !currentBest.get().positions().contains(expectedSegmentStart))) { + inProgress.cancel(); // cancellation doesn't dispatch any events + } } } } diff --git a/src/main/java/baritone/cache/CacheFiles.java b/src/main/java/baritone/cache/CacheFiles.java new file mode 100644 index 0000000000..1230a5c3f6 --- /dev/null +++ b/src/main/java/baritone/cache/CacheFiles.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.cache; + +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +final class CacheFiles { + + private CacheFiles() {} + + /** + * Replaces {@code target} with a fully written {@code source} in one step where the file system allows it, so a + * save that gets cut off (a crash, or the JVM exiting while a worker thread is still writing) leaves the previous + * file intact instead of a truncated one. + */ + static void moveIntoPlace(Path source, Path target) throws IOException { + try { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); + } + } +} diff --git a/src/main/java/baritone/cache/CachedChunk.java b/src/main/java/baritone/cache/CachedChunk.java index c49394d76e..c2d0c53d6e 100644 --- a/src/main/java/baritone/cache/CachedChunk.java +++ b/src/main/java/baritone/cache/CachedChunk.java @@ -153,6 +153,13 @@ public static int size(int dimension_height) { return 2 * 16 * 16 * dimension_height; } + /** + * Rough heap footprint: the 2-bit block data plus the overview, height map and special-block index. + */ + public int approximateHeapBytes() { + return sizeInBytes + 2_560 + (special == null ? 0 : special.size() * 48); + } + public static int sizeInBytes(int size) { return size / 8; } diff --git a/src/main/java/baritone/cache/CachedRegion.java b/src/main/java/baritone/cache/CachedRegion.java index 38b5490101..312a056048 100644 --- a/src/main/java/baritone/cache/CachedRegion.java +++ b/src/main/java/baritone/cache/CachedRegion.java @@ -27,9 +27,11 @@ import net.minecraft.world.level.dimension.DimensionType; import java.io.*; +import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.*; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; @@ -72,6 +74,16 @@ public final class CachedRegion implements ICachedRegion { */ private boolean hasUnsavedChanges; + /** + * Wall-clock time of the last read or write through CachedWorld; drives idle and LRU eviction + */ + private volatile long lastAccessMs = System.currentTimeMillis(); + + /** + * Set under the region lock when CachedWorld drops this instance from RAM; a late write must go to the live instance + */ + private boolean evicted; + CachedRegion(int x, int z, DimensionType dimension, ResourceKey dimensionId) { this.x = x; this.z = z; @@ -111,9 +123,54 @@ public final ArrayList getLocationsOf(String block) { return res; } - public final synchronized void updateCachedChunk(int chunkX, int chunkZ, CachedChunk chunk) { + /** + * @return false when this instance was evicted from RAM before the write; the caller must look the region up again + */ + public final synchronized boolean updateCachedChunk(int chunkX, int chunkZ, CachedChunk chunk) { + if (evicted) { + return false; + } this.chunks[chunkX][chunkZ] = chunk; hasUnsavedChanges = true; + lastAccessMs = System.currentTimeMillis(); + return true; + } + + final void touch() { + lastAccessMs = System.currentTimeMillis(); + } + + final long getLastAccessMs() { + return lastAccessMs; + } + + final synchronized boolean hasUnsavedChanges() { + return hasUnsavedChanges; + } + + /** + * Marks the region evicted unless it still has unsaved changes (the packer may have written since the last save). + * {@code discardUnsaved} drops them anyway, for when nothing is able to save them. + */ + final synchronized boolean tryEvict(boolean discardUnsaved) { + if (hasUnsavedChanges && !discardUnsaved) { + return false; + } + evicted = true; + return true; + } + + final long approximateHeapBytes() { + long total = 0; + for (int x = 0; x < 32; x++) { + for (int z = 0; z < 32; z++) { + CachedChunk chunk = this.chunks[x][z]; + if (chunk != null) { + total += chunk.approximateHeapBytes(); + } + } + } + return total; } @@ -130,61 +187,74 @@ public synchronized final void save(String directory) { } System.out.println("Saving region " + x + "," + z + " to disk " + path); Path regionFile = getRegionFile(path, this.x, this.z); - if (!Files.exists(regionFile)) { - Files.createFile(regionFile); - } - try ( - FileOutputStream fileOut = new FileOutputStream(regionFile.toFile()); - GZIPOutputStream gzipOut = new GZIPOutputStream(fileOut, 16384); - DataOutputStream out = new DataOutputStream(gzipOut) - ) { - out.writeInt(CACHED_REGION_MAGIC); - for (int x = 0; x < 32; x++) { - for (int z = 0; z < 32; z++) { - CachedChunk chunk = this.chunks[x][z]; - if (chunk == null) { - out.write(CHUNK_NOT_PRESENT); - } else { - out.write(CHUNK_PRESENT); - byte[] chunkBytes = chunk.toByteArray(); - out.write(chunkBytes); - // Messy, but fills the empty 0s that should be trailing to fill up the space. - out.write(new byte[chunk.sizeInBytes - chunkBytes.length]); + // Write to a temp file and atomically rename it over the real one. The nether pathfinder + // (and a concurrently running load) reads these files while we save; an in-place rewrite + // hands them a truncated gzip stream and they treat the region as permanently unparseable. + // The pid keeps multiple game instances sharing this cache dir off each other's temp file; + // concurrent saves of the same region then resolve to last-complete-writer-wins. + Path tmpFile = regionFile.resolveSibling(regionFile.getFileName() + "." + ProcessHandle.current().pid() + ".tmp"); + try { + try ( + FileOutputStream fileOut = new FileOutputStream(tmpFile.toFile()); + GZIPOutputStream gzipOut = new GZIPOutputStream(fileOut, 16384); + DataOutputStream out = new DataOutputStream(gzipOut) + ) { + out.writeInt(CACHED_REGION_MAGIC); + for (int x = 0; x < 32; x++) { + for (int z = 0; z < 32; z++) { + CachedChunk chunk = this.chunks[x][z]; + if (chunk == null) { + out.write(CHUNK_NOT_PRESENT); + } else { + out.write(CHUNK_PRESENT); + byte[] chunkBytes = chunk.toByteArray(); + out.write(chunkBytes); + // Messy, but fills the empty 0s that should be trailing to fill up the space. + out.write(new byte[chunk.sizeInBytes - chunkBytes.length]); + } } } - } - for (int x = 0; x < 32; x++) { - for (int z = 0; z < 32; z++) { - if (chunks[x][z] != null) { - for (int i = 0; i < 256; i++) { - out.writeUTF(BlockUtils.blockToString(chunks[x][z].getOverview()[i].getBlock())); + for (int x = 0; x < 32; x++) { + for (int z = 0; z < 32; z++) { + if (chunks[x][z] != null) { + for (int i = 0; i < 256; i++) { + out.writeUTF(BlockUtils.blockToString(chunks[x][z].getOverview()[i].getBlock())); + } } } } - } - for (int x = 0; x < 32; x++) { - for (int z = 0; z < 32; z++) { - if (chunks[x][z] != null) { - Map> locs = chunks[x][z].getRelativeBlocks(); - out.writeShort(locs.entrySet().size()); - for (Map.Entry> entry : locs.entrySet()) { - out.writeUTF(entry.getKey()); - out.writeShort(entry.getValue().size()); - for (BlockPos pos : entry.getValue()) { - out.writeByte((byte) (pos.getZ() << 4 | pos.getX())); - out.writeInt(pos.getY()-dimension.minY()); + for (int x = 0; x < 32; x++) { + for (int z = 0; z < 32; z++) { + if (chunks[x][z] != null) { + Map> locs = chunks[x][z].getRelativeBlocks(); + out.writeShort(locs.entrySet().size()); + for (Map.Entry> entry : locs.entrySet()) { + out.writeUTF(entry.getKey()); + out.writeShort(entry.getValue().size()); + for (BlockPos pos : entry.getValue()) { + out.writeByte((byte) (pos.getZ() << 4 | pos.getX())); + out.writeInt(pos.getY()-dimension.minY()); + } } } } } - } - for (int x = 0; x < 32; x++) { - for (int z = 0; z < 32; z++) { - if (chunks[x][z] != null) { - out.writeLong(chunks[x][z].cacheTimestamp); + for (int x = 0; x < 32; x++) { + for (int z = 0; z < 32; z++) { + if (chunks[x][z] != null) { + out.writeLong(chunks[x][z].cacheTimestamp); + } } } } + try { + Files.move(tmpFile, regionFile, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException ex) { + Files.move(tmpFile, regionFile, StandardCopyOption.REPLACE_EXISTING); + } + } finally { + // nothing to delete once the move succeeded; this is the failed-write path + Files.deleteIfExists(tmpFile); } hasUnsavedChanges = false; System.out.println("Saved region successfully"); @@ -360,7 +430,7 @@ public final int getZ() { return this.z; } - private static Path getRegionFile(Path cacheDir, int regionX, int regionZ) { + static Path getRegionFile(Path cacheDir, int regionX, int regionZ) { return Paths.get(cacheDir.toString(), "r." + regionX + "." + regionZ + ".bcr"); } } diff --git a/src/main/java/baritone/cache/CachedWorld.java b/src/main/java/baritone/cache/CachedWorld.java index 037cc35f43..9d829b4d21 100644 --- a/src/main/java/baritone/cache/CachedWorld.java +++ b/src/main/java/baritone/cache/CachedWorld.java @@ -18,14 +18,15 @@ package baritone.cache; import baritone.Baritone; -import baritone.api.BaritoneAPI; -import baritone.api.IBaritone; import baritone.api.cache.ICachedWorld; -import baritone.api.cache.IWorldData; import baritone.api.utils.Helper; import com.google.common.cache.CacheBuilder; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongList; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceKey; import net.minecraft.world.level.ChunkPos; @@ -35,13 +36,27 @@ import java.io.IOException; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; /** + * The RAM tier of the chunk cache. The disk tier (one .bcr per region) keeps everything ever seen; RAM holds + * only regions in use: written by the packer, read by pathing or scanning. One shared daemon thread writes + * changed regions every {@code cachedRegionsSaveIntervalSeconds} and evicts idle or over-budget regions + * ({@link RegionRetentionPolicy}); a region is never evicted while it has unsaved changes that can still be + * saved. Pathing misses pull regions back from disk in the background. + * * @author Brady * @since 8/4/2018 */ @@ -52,16 +67,41 @@ public final class CachedWorld implements ICachedWorld, Helper { */ private static final int REGION_MAX = 30_000_000 / 512 + 1; + private static final ChunkPos PACKER_STOP = new ChunkPos(Integer.MIN_VALUE, Integer.MIN_VALUE); + + private static final long MAINTENANCE_TICK_SECONDS = 5; + + private static final int KNOWN_ABSENT_MAX = 65_536; + + private static final int DROPPED_PACKS_MAX = 65_536; + + private static final ScheduledExecutorService MAINTENANCE = Executors.newSingleThreadScheduledExecutor( + new ThreadFactoryBuilder().setNameFormat("baritone-cache-maintenance").setDaemon(true).build()); + + private static final ExecutorService LOADER = Executors.newSingleThreadExecutor( + new ThreadFactoryBuilder().setNameFormat("baritone-cache-loader").setDaemon(true).build()); + + private static final Set ACTIVE = ConcurrentHashMap.newKeySet(); + + static { + MAINTENANCE.scheduleWithFixedDelay(CachedWorld::maintainAll, MAINTENANCE_TICK_SECONDS, MAINTENANCE_TICK_SECONDS, TimeUnit.SECONDS); + } + /** * A map of all of the cached regions. */ - private Long2ObjectMap cachedRegions = new Long2ObjectOpenHashMap<>(); + private final Long2ObjectMap cachedRegions = new Long2ObjectOpenHashMap<>(); /** * The directory that the cached region files are saved to */ private final String directory; + /** + * False when the directory could not be created; every disk access is skipped instead of throwing per region + */ + private final boolean diskAvailable; + /** * Queue of positions to pack. Refers to the toPackMap, in that every element of this queue will be a * key in that map. @@ -74,36 +114,72 @@ public final class CachedWorld implements ICachedWorld, Helper { */ private final Map toPackMap = CacheBuilder.newBuilder().softValues().build().asMap(); + /** + * Chunks whose latest pack was dropped (packer queue over chunkPackerQueueMaxSize, or the soft reference + * collected first), so whatever the cache holds for them predates that load. + */ + private final Set droppedPacks = ConcurrentHashMap.newKeySet(); + + /** + * Region ids with a disk load queued on the loader thread. Guarded by this. + */ + private final LongSet loadsInFlight = new LongOpenHashSet(); + + /** + * Region ids known to have no file on disk, so pathing misses stop hitting the disk. Guarded by this. + */ + private final LongSet knownAbsent = new LongOpenHashSet(); + private final DimensionType dimension; private final ResourceKey dimensionId; + private volatile boolean active; + + private long lastSaveNanos = System.nanoTime(); + CachedWorld(Path directory, DimensionType dimension, ResourceKey dimensionId) { - if (!Files.exists(directory)) { - try { - Files.createDirectories(directory); - } catch (IOException ignored) { - } + boolean available = true; + try { + Files.createDirectories(directory); + } catch (IOException | InvalidPathException e) { + available = false; + System.out.println("Baritone cache directory " + directory + " cannot be created (" + e + "); disk caching is disabled for this world"); } + this.diskAvailable = available; this.directory = directory.toString(); this.dimension = dimension; this.dimensionId = dimensionId; System.out.println("Cached world directory: " + directory); + activate(); + } + + /** + * Starts the packer for this world and puts it under maintenance. Idempotent. + */ + final void activate() { + synchronized (this) { + if (active) { + return; + } + active = true; + } + ACTIVE.add(this); Baritone.getExecutor().execute(new PackerThread()); - Baritone.getExecutor().execute(() -> { - try { - Thread.sleep(30000); - while (true) { - // since a region only saves if it's been modified since its last save - // saving every 10 minutes means that once it's time to exit - // we'll only have a couple regions to save - save(); - Thread.sleep(600000); - } - } catch (InterruptedException e) { - e.printStackTrace(); + } + + /** + * Called when the game leaves this world: the packer drains what is queued, then saves and evicts every region. + */ + final void close() { + synchronized (this) { + if (!active) { + return; } - }); + active = false; + } + ACTIVE.remove(this); + toPackQueue.add(PACKER_STOP); } @Override @@ -115,15 +191,28 @@ public final void queueForPacking(LevelChunk chunk) { @Override public final boolean isCached(int blockX, int blockZ) { - CachedRegion region = getRegion(blockX >> 9, blockZ >> 9); + CachedRegion region = peekRegion(blockX >> 9, blockZ >> 9); if (region == null) { return false; } return region.isCached(blockX & 511, blockZ & 511); } + /** + * Whether the cache holds what the current load of this chunk packed: it is cached, the pack queued when it + * loaded was not dropped, and no pack of it is still waiting. A copy from an earlier load or session does not + * count. + */ + public final boolean holdsPackOfCurrentLoad(int chunkX, int chunkZ) { + ChunkPos pos = new ChunkPos(chunkX, chunkZ); + if (toPackMap.containsKey(pos) || droppedPacks.contains(pos)) { + return false; + } + return isCached(chunkX << 4, chunkZ << 4); + } + public final boolean regionLoaded(int blockX, int blockZ) { - return getRegion(blockX >> 9, blockZ >> 9) != null; + return peekRegion(blockX >> 9, blockZ >> 9) != null; } @Override @@ -158,82 +247,136 @@ public final ArrayList getLocationsOf(String block, int maximum, int c } private void updateCachedChunk(CachedChunk chunk) { - CachedRegion region = getOrCreateRegion(chunk.x >> 5, chunk.z >> 5); - region.updateCachedChunk(chunk.x & 31, chunk.z & 31, chunk); + while (true) { + CachedRegion region = getOrCreateRegion(chunk.x >> 5, chunk.z >> 5); + if (region.updateCachedChunk(chunk.x & 31, chunk.z & 31, chunk)) { + return; + } + // evicted between the lookup and the write; both sides hold the region lock, so retry on the live instance + } } @Override public final void save() { if (!Baritone.settings().chunkCaching.value) { System.out.println("Not saving to disk; chunk caching is disabled."); - allRegions().forEach(region -> { - if (region != null) { - region.removeExpired(); - } - }); // even if we aren't saving to disk, still delete expired old chunks from RAM - prune(); return; } - long start = System.nanoTime() / 1000000L; - allRegions().parallelStream().forEach(region -> { - if (region != null) { - region.save(this.directory); - } - }); - long now = System.nanoTime() / 1000000L; - System.out.println("World save took " + (now - start) + "ms"); - prune(); + int saved = saveDirtyRegions(); + if (saved == 0) { + System.out.println("World save: nothing changed"); + } } /** - * Delete regions that are too far from the player + * @return how many regions had changes and were written */ - private synchronized void prune() { - if (!Baritone.settings().pruneRegionsFromRAM.value) { - return; + private int saveDirtyRegions() { + if (!diskAvailable) { + return 0; } - BlockPos pruneCenter = guessPosition(); + List dirty = new ArrayList<>(); for (CachedRegion region : allRegions()) { - if (region == null) { - continue; + if (region != null && region.hasUnsavedChanges()) { + dirty.add(region); } - int distX = ((region.getX() << 9) + 256) - pruneCenter.getX(); - int distZ = ((region.getZ() << 9) + 256) - pruneCenter.getZ(); - double dist = Math.sqrt(distX * distX + distZ * distZ); - if (dist > 1024) { - logDebug("Deleting cached region from ram"); - cachedRegions.remove(getRegionID(region.getX(), region.getZ())); + } + if (dirty.isEmpty()) { + return 0; + } + long start = System.nanoTime() / 1000000L; + dirty.parallelStream().forEach(region -> region.save(this.directory)); + long now = System.nanoTime() / 1000000L; + System.out.println("World save of " + dirty.size() + " regions took " + (now - start) + "ms"); + return dirty.size(); + } + + private static void maintainAll() { + for (CachedWorld world : ACTIVE) { + try { + world.maintain(); + } catch (Throwable th) { + th.printStackTrace(); } } } + private void maintain() { + long now = System.nanoTime(); + if (now - lastSaveNanos >= Baritone.settings().cachedRegionsSaveIntervalSeconds.value * 1_000_000_000L) { + lastSaveNanos = now; + if (Baritone.settings().chunkCaching.value) { + saveDirtyRegions(); + } else { + allRegions().forEach(region -> { + if (region != null) { + region.removeExpired(); + } + }); // even if we aren't saving to disk, still delete expired old chunks from RAM + } + } + evict(false); + } + /** - * If we are still in this world and dimension, return player feet, otherwise return most recently modified chunk + * Runs on the packer thread after it drained the queue for a closed world. */ - private BlockPos guessPosition() { - for (IBaritone ibaritone : BaritoneAPI.getProvider().getAllBaritones()) { - IWorldData data = ibaritone.getWorldProvider().getCurrentWorld(); - if (data != null && data.getCachedWorld() == this && ibaritone.getPlayerContext().player() != null) { - return ibaritone.getPlayerContext().playerFeet(); - } + private void saveAndEvictAll() { + if (Baritone.settings().chunkCaching.value) { + saveDirtyRegions(); } - CachedChunk mostRecentlyModified = null; - for (CachedRegion region : allRegions()) { - if (region == null) { - continue; - } - CachedChunk ch = region.mostRecentlyModified(); - if (ch == null) { - continue; + if (active) { + // re-entered this world while the old packer was still draining; the live packer's regions stay + return; + } + evict(true); + } + + /** + * Drops regions per {@link RegionRetentionPolicy}. A region that turned dirty since the policy looked at it + * refuses the eviction under its own lock and stays until the next save. + */ + private void evict(boolean everything) { + if (!everything && !Baritone.settings().pruneRegionsFromRAM.value) { + return; + } + // with chunk caching off or no cache directory nothing ever saves a region, so its unsaved changes would + // pin it in RAM for good; drop them instead, as the old distance prune did + boolean saveable = diskAvailable && Baritone.settings().chunkCaching.value; + List regions = allRegions(); + List candidates = new ArrayList<>(regions.size()); + for (CachedRegion region : regions) { + if (region != null) { + candidates.add(new RegionRetentionPolicy.Candidate( + getRegionID(region.getX(), region.getZ()), + region.getLastAccessMs(), + saveable && region.hasUnsavedChanges(), + region.approximateHeapBytes())); } - if (mostRecentlyModified == null || mostRecentlyModified.cacheTimestamp < ch.cacheTimestamp) { - mostRecentlyModified = ch; + } + LongList victims = RegionRetentionPolicy.select( + candidates, + Baritone.settings().cachedRegionsRamBudgetMB.value * 1024L * 1024L, + Baritone.settings().cachedRegionsIdleSeconds.value * 1000L, + System.currentTimeMillis(), + everything); + if (victims.isEmpty()) { + return; + } + int evicted = 0; + synchronized (this) { + for (int i = 0; i < victims.size(); i++) { + long id = victims.getLong(i); + CachedRegion region = cachedRegions.get(id); + if (region != null && region.tryEvict(!saveable)) { + cachedRegions.remove(id); + evicted++; + } } } - if (mostRecentlyModified == null) { - return new BlockPos(0, 0, 0); + if (evicted > 0) { + logDebug("Evicted " + evicted + " cached regions from RAM"); } - return new BlockPos((mostRecentlyModified.x << 4) + 8, 0, (mostRecentlyModified.z << 4) + 8); } private synchronized List allRegions() { @@ -252,25 +395,89 @@ public final void reloadAllFromDisk() { System.out.println("World load took " + (now - start) + "ms"); } - @Override - public final synchronized CachedRegion getRegion(int regionX, int regionZ) { + private synchronized CachedRegion peekRegion(int regionX, int regionZ) { return cachedRegions.get(getRegionID(regionX, regionZ)); } + /** + * The region if it is in RAM. A miss queues a background load from disk, so a path calculation that reaches + * terrain explored in an earlier session sees it on its next attempt. + */ + @Override + public final CachedRegion getRegion(int regionX, int regionZ) { + long id = getRegionID(regionX, regionZ); + synchronized (this) { + CachedRegion region = cachedRegions.get(id); + if (region != null) { + region.touch(); + return region; + } + if (!diskAvailable + || !Baritone.settings().cachedRegionsLoadOnDemand.value + || knownAbsent.contains(id) + || !loadsInFlight.add(id)) { + return null; + } + } + LOADER.execute(() -> loadFromDisk(regionX, regionZ)); + return null; + } + + private void loadFromDisk(int regionX, int regionZ) { + long id = getRegionID(regionX, regionZ); + try { + if (!Files.exists(CachedRegion.getRegionFile(Paths.get(directory), regionX, regionZ))) { + synchronized (this) { + if (knownAbsent.size() >= KNOWN_ABSENT_MAX) { + knownAbsent.clear(); + } + knownAbsent.add(id); + } + return; + } + CachedRegion region = new CachedRegion(regionX, regionZ, dimension, dimensionId); + region.load(this.directory); + synchronized (this) { + cachedRegions.putIfAbsent(id, region); + } + } catch (Throwable th) { + th.printStackTrace(); + } finally { + synchronized (this) { + loadsInFlight.remove(id); + } + } + } + /** * Returns the region at the specified region coordinates. If a - * region is not found, then a new one is created. + * region is not found, then a new one is created (loaded from disk when a file exists). + * The disk read happens outside the world lock so pathing lookups are not stalled by it. * * @param regionX The region X coordinate * @param regionZ The region Z coordinate * @return The region located at the specified coordinates */ - private synchronized CachedRegion getOrCreateRegion(int regionX, int regionZ) { - return cachedRegions.computeIfAbsent(getRegionID(regionX, regionZ), id -> { - CachedRegion newRegion = new CachedRegion(regionX, regionZ, dimension, dimensionId); - newRegion.load(this.directory); - return newRegion; - }); + private CachedRegion getOrCreateRegion(int regionX, int regionZ) { + long id = getRegionID(regionX, regionZ); + synchronized (this) { + CachedRegion region = cachedRegions.get(id); + if (region != null) { + region.touch(); + return region; + } + } + CachedRegion fresh = new CachedRegion(regionX, regionZ, dimension, dimensionId); + if (diskAvailable) { + fresh.load(this.directory); + } + synchronized (this) { + CachedRegion existing = cachedRegions.putIfAbsent(id, fresh); + knownAbsent.remove(id); + CachedRegion region = existing != null ? existing : fresh; + region.touch(); + return region; + } } public void tryLoadFromDisk(int regionX, int regionZ) { @@ -304,18 +511,35 @@ private boolean isRegionInWorld(int regionX, int regionZ) { return regionX <= REGION_MAX && regionX >= -REGION_MAX && regionZ <= REGION_MAX && regionZ >= -REGION_MAX; } + private void markDropped(ChunkPos pos) { + if (droppedPacks.size() >= DROPPED_PACKS_MAX) { + droppedPacks.clear(); + } + droppedPacks.add(pos); + } + private class PackerThread implements Runnable { public void run() { while (true) { try { ChunkPos pos = toPackQueue.take(); + if (pos == PACKER_STOP) { + saveAndEvictAll(); + return; + } LevelChunk chunk = toPackMap.remove(pos); + if (chunk == null) { + markDropped(pos); // soft reference collected before we got to it + continue; + } if (toPackQueue.size() > Baritone.settings().chunkPackerQueueMaxSize.value) { + markDropped(pos); continue; } CachedChunk cached = ChunkPacker.pack(chunk); CachedWorld.this.updateCachedChunk(cached); + droppedPacks.remove(pos); //System.out.println("Processed chunk at " + chunk.x + "," + chunk.z); } catch (InterruptedException e) { e.printStackTrace(); diff --git a/src/main/java/baritone/cache/ChunkPacker.java b/src/main/java/baritone/cache/ChunkPacker.java index 78bca4b7a8..b5a6de007e 100644 --- a/src/main/java/baritone/cache/ChunkPacker.java +++ b/src/main/java/baritone/cache/ChunkPacker.java @@ -53,7 +53,9 @@ public static CachedChunk pack(LevelChunk chunk) { LevelChunkSection[] chunkInternalStorageArray = chunk.getSections(); for (int y0 = 0; y0 < height / 16; y0++) { LevelChunkSection extendedblockstorage = chunkInternalStorageArray[y0]; - if (extendedblockstorage == null) { + // modern chunks never have null sections; an all-air section still packs to zeros, so skip the + // 4096 block lookups (most of an overworld column above the surface, the nether above the roof) + if (extendedblockstorage == null || extendedblockstorage.hasOnlyAir()) { // any 16x16x16 area that's all air will have null storage // for example, in an ocean biome, with air from y=64 to y=256 // the first 4 extended blocks storages will be full diff --git a/src/main/java/baritone/cache/RegionRetentionPolicy.java b/src/main/java/baritone/cache/RegionRetentionPolicy.java new file mode 100644 index 0000000000..440008173c --- /dev/null +++ b/src/main/java/baritone/cache/RegionRetentionPolicy.java @@ -0,0 +1,74 @@ +/* + * 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.cache; + +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongList; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Which cached regions to drop from RAM. Pure so it can be unit tested; CachedWorld applies the result under + * its locks and re-checks the dirty flag there. + *

+ * A dirty region is never selected. Clean regions idle for longer than {@code idleMs} are always selected. + * If the resident bytes still exceed the budget, the least recently used clean regions go next, except those + * touched within {@link #GRACE_MS}: the packer or a path calculation is about to use them again. + */ +final class RegionRetentionPolicy { + + static final long GRACE_MS = 2_000; + + record Candidate(long id, long lastAccessMs, boolean dirty, long bytes) {} + + private RegionRetentionPolicy() {} + + static LongList select(List regions, long budgetBytes, long idleMs, long nowMs, boolean everything) { + LongList victims = new LongArrayList(); + long resident = 0; + for (Candidate c : regions) { + resident += c.bytes(); + } + List clean = new ArrayList<>(); + for (Candidate c : regions) { + if (c.dirty()) { + continue; + } + if (everything || nowMs - c.lastAccessMs() > idleMs) { + victims.add(c.id()); + resident -= c.bytes(); + } else { + clean.add(c); + } + } + if (resident <= budgetBytes) { + return victims; + } + clean.sort(Comparator.comparingLong(Candidate::lastAccessMs)); + for (Candidate c : clean) { + if (resident <= budgetBytes || nowMs - c.lastAccessMs() < GRACE_MS) { + break; + } + victims.add(c.id()); + resident -= c.bytes(); + } + return victims; + } +} diff --git a/src/main/java/baritone/cache/WaypointCollection.java b/src/main/java/baritone/cache/WaypointCollection.java index 222adfdf66..ae22aa8103 100644 --- a/src/main/java/baritone/cache/WaypointCollection.java +++ b/src/main/java/baritone/cache/WaypointCollection.java @@ -93,8 +93,9 @@ private synchronized void load(Waypoint.Tag tag) { private synchronized void save(Waypoint.Tag tag) { Path fileName = this.directory.resolve(tag.name().toLowerCase() + ".mp4"); + Path tempFile = fileName.resolveSibling(fileName.getFileName() + ".tmp"); try ( - FileOutputStream fileOut = new FileOutputStream(fileName.toFile()); + FileOutputStream fileOut = new FileOutputStream(tempFile.toFile()); BufferedOutputStream bufOut = new BufferedOutputStream(fileOut); DataOutputStream out = new DataOutputStream(bufOut) ) { @@ -107,6 +108,12 @@ private synchronized void save(Waypoint.Tag tag) { out.writeInt(waypoint.getLocation().getY()); out.writeInt(waypoint.getLocation().getZ()); } + } catch (IOException ex) { + ex.printStackTrace(); + return; + } + try { + CacheFiles.moveIntoPlace(tempFile, fileName); } catch (IOException ex) { ex.printStackTrace(); } diff --git a/src/main/java/baritone/cache/WorldData.java b/src/main/java/baritone/cache/WorldData.java index 5ea9739f92..95710431bb 100644 --- a/src/main/java/baritone/cache/WorldData.java +++ b/src/main/java/baritone/cache/WorldData.java @@ -17,7 +17,6 @@ package baritone.cache; -import baritone.Baritone; import baritone.api.cache.ICachedWorld; import baritone.api.cache.IWaypointCollection; import baritone.api.cache.IWorldData; @@ -48,10 +47,7 @@ public class WorldData implements IWorldData { } public void onClose() { - Baritone.getExecutor().execute(() -> { - System.out.println("Started saving the world in a new thread"); - cache.save(); - }); + cache.close(); } @Override diff --git a/src/main/java/baritone/cache/WorldProvider.java b/src/main/java/baritone/cache/WorldProvider.java index 28b2d94f11..b5e67e20eb 100644 --- a/src/main/java/baritone/cache/WorldProvider.java +++ b/src/main/java/baritone/cache/WorldProvider.java @@ -25,11 +25,11 @@ import net.minecraft.resources.Identifier; import net.minecraft.world.level.Level; import net.minecraft.world.level.storage.LevelResource; -import org.apache.commons.lang3.SystemUtils; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; @@ -94,6 +94,8 @@ public final void initWorld(Level world) { synchronized (worldCache) { this.currentWorld = worldCache.computeIfAbsent(worldDataDir, d -> new WorldData(d, world.dimensionType(), world.dimension())); } + // a WorldData is reused across visits; its cache holds no regions and no threads while closed + this.currentWorld.cache.activate(); this.mcWorld = ctx.world(); }); } @@ -147,11 +149,8 @@ private Optional> getSaveDirectories(Level world) { mcWorld = ctx.world(); return Optional.empty(); } - if (SystemUtils.IS_OS_WINDOWS) { - folderName = folderName.replace(":", "_"); - } // TODO: This should probably be in "baritone/servers" - worldDir = baritone.getDirectory().resolve(folderName); + worldDir = baritone.getDirectory().resolve(serverFolderName(baritone.getDirectory(), folderName)); // Just write the readme to the baritone directory instead of each server save in it readmeDir = baritone.getDirectory(); } @@ -159,6 +158,39 @@ private Optional> getSaveDirectories(Level world) { return Optional.of(new Pair<>(worldDir, readmeDir)); } + /** + * Windows, and NTFS mounted with windows_names, reject {@code : * ? " < > |} in a file name, so a direct-IP + * server ("host:port") could never get a cache directory there and every region save and load threw. Names + * are sanitised on every platform now (":" becomes "_", the same name Windows always produced). A directory + * under the raw name that already exists, written by an earlier version on Linux, keeps being used. + */ + static String serverFolderName(Path baritoneDir, String rawName) { + String sanitised = sanitizeFileName(rawName); + if (!sanitised.equals(rawName)) { + try { + if (Files.isDirectory(baritoneDir.resolve(rawName))) { + return rawName; + } + } catch (InvalidPathException ignored) { + // the raw name is not a legal path on this platform, so nothing can exist under it + } + } + return sanitised; + } + + static String sanitizeFileName(String name) { + StringBuilder out = new StringBuilder(name.length()); + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + out.append(c < 0x20 || "\\/:*?\"<>|".indexOf(c) >= 0 ? '_' : c); + } + int end = out.length(); + while (end > 0 && (out.charAt(end - 1) == '.' || out.charAt(end - 1) == ' ')) { + end--; + } + return end == 0 ? "_" : out.substring(0, end); + } + /** * Why does this exist instead of fixing the event? Some mods break the event. Lol. */ diff --git a/src/main/java/baritone/command/ExampleBaritoneControl.java b/src/main/java/baritone/command/ExampleBaritoneControl.java index 82f1cc57c3..12d9149f2d 100644 --- a/src/main/java/baritone/command/ExampleBaritoneControl.java +++ b/src/main/java/baritone/command/ExampleBaritoneControl.java @@ -34,13 +34,14 @@ import baritone.command.argument.ArgConsumer; import baritone.command.argument.CommandArguments; import baritone.command.manager.CommandManager; +import com.mojang.blaze3d.Blaze3D; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.HoverEvent; import net.minecraft.network.chat.MutableComponent; -import net.minecraft.util.Util; +import java.net.URI; import java.util.List; import java.util.Locale; import java.util.stream.Stream; @@ -96,7 +97,7 @@ public boolean runCommand(String msg) { return false; } else if (msg.trim().equalsIgnoreCase("orderpizza")) { try { - Util.getPlatform().openUri("https://www.dominos.com/en/pages/order/"); + Blaze3D.openUri(URI.create("https://www.dominos.com/en/pages/order/")); } catch (Exception ignored) {} return false; } diff --git a/src/main/java/baritone/command/defaults/FindCommand.java b/src/main/java/baritone/command/defaults/FindCommand.java index 75d55e94e0..8d5142e926 100644 --- a/src/main/java/baritone/command/defaults/FindCommand.java +++ b/src/main/java/baritone/command/defaults/FindCommand.java @@ -60,7 +60,7 @@ public void execute(String label, IArgConsumer args) throws CommandException { BuiltInRegistries.BLOCK.getKey(block).getPath(), Integer.MAX_VALUE, origin.x, - origin.y, + origin.z, 4 ).stream() ) diff --git a/src/main/java/baritone/event/GameEventHandler.java b/src/main/java/baritone/event/GameEventHandler.java index 8bbdb52d67..db42a599e1 100644 --- a/src/main/java/baritone/event/GameEventHandler.java +++ b/src/main/java/baritone/event/GameEventHandler.java @@ -25,6 +25,7 @@ import baritone.api.utils.Helper; import baritone.api.utils.Pair; import baritone.cache.CachedChunk; +import baritone.cache.CachedWorld; import baritone.cache.WorldProvider; import baritone.utils.BlockStateInterface; import net.minecraft.world.level.ChunkPos; @@ -101,6 +102,14 @@ public void onChunkEvent(ChunkEvent event) { if (event.isPostPopulate() || isPreUnload) { baritone.getWorldProvider().ifWorldLoaded(worldData -> { LevelChunk chunk = world.getChunk(event.getX(), event.getZ()); + if (isPreUnload + && !Baritone.settings().repackUnchangedChunksOnUnload.value + && !chunk.isUnsaved() + && ((CachedWorld) worldData.getCachedWorld()).holdsPackOfCurrentLoad(event.getX(), event.getZ())) { + // packed on load and no block changed since (the client marks a chunk unsaved on every + // setBlockState): the cache already holds this exact chunk + return; + } worldData.getCachedWorld().queueForPacking(chunk); }); } diff --git a/src/main/java/baritone/pathing/calc/AStarPathFinder.java b/src/main/java/baritone/pathing/calc/AStarPathFinder.java index 1c58cbf20e..f2ae74ad13 100644 --- a/src/main/java/baritone/pathing/calc/AStarPathFinder.java +++ b/src/main/java/baritone/pathing/calc/AStarPathFinder.java @@ -112,7 +112,7 @@ protected Optional calculate0(long primaryTimeout, long failureTimeout) { if (!moves.dynamicXZ && !worldBorder.entirelyContains(newX, newZ)) { continue; } - if (currentNode.y + moves.yOffset > height || currentNode.y + moves.yOffset < minY) { + if (currentNode.y + moves.yOffset > minY + height || currentNode.y + moves.yOffset < minY) { continue; } res.reset(); diff --git a/src/main/java/baritone/pathing/calc/AbstractNodeCostSearch.java b/src/main/java/baritone/pathing/calc/AbstractNodeCostSearch.java index 0bfb6ac3f2..a0b97800ec 100644 --- a/src/main/java/baritone/pathing/calc/AbstractNodeCostSearch.java +++ b/src/main/java/baritone/pathing/calc/AbstractNodeCostSearch.java @@ -58,7 +58,7 @@ public abstract class AbstractNodeCostSearch implements IPathFinder, Helper { private volatile boolean isFinished; - protected boolean cancelRequested; + protected volatile boolean cancelRequested; /** * This is really complicated and hard to explain. I wrote a comment in the old version of MineBot but it was so diff --git a/src/main/java/baritone/pathing/movement/MovementHelper.java b/src/main/java/baritone/pathing/movement/MovementHelper.java index 51a23577ba..1c1a1865ed 100644 --- a/src/main/java/baritone/pathing/movement/MovementHelper.java +++ b/src/main/java/baritone/pathing/movement/MovementHelper.java @@ -216,7 +216,7 @@ static boolean canWalkThroughPosition(BlockStateInterface bsi, int x, int y, int return false; } // Everything after this point has to be a special case as it relies on the water not being flowing, which means a special case is needed. - if (Baritone.settings().assumeWalkOnWater.value) { + if (bsi.assumeWalkOnWater) { return false; } @@ -224,7 +224,9 @@ static boolean canWalkThroughPosition(BlockStateInterface bsi, int x, int y, int if (!up.getFluidState().isEmpty() || up.getBlock() instanceof LilyPadBlock) { return false; } - return fluidState.getType() instanceof WaterFluid; + // a waterlogged block (fence, wall, pane, bars, stairs, leaves...) keeps its own collision, + // so it's only passable if the block itself is + return fluidState.getType() instanceof WaterFluid && state.isPathfindable(PathComputationType.LAND); } return state.isPathfindable(PathComputationType.LAND); @@ -235,6 +237,11 @@ static Ternary fullyPassableBlockState(BlockState state) { if (block instanceof AirBlock) { // early return for most common case return YES; } + // the player hitbox is the same width in the air as on the ground, so anything we can't walk + // through we can't jump through either (powder snow, honey, berry bushes, dripstone, blocksToAvoid...) + if (canWalkThroughBlockState(state) == NO) { + return NO; + } // exceptions - blocks that are isPassable true, but we can't actually jump through if (block instanceof BaseFireBlock || block == Blocks.TRIPWIRE @@ -458,11 +465,11 @@ static boolean canWalkOnPosition(BlockStateInterface bsi, int x, int y, int z, B } if (MovementHelper.isFlowing(x, y, z, state, bsi) || upState.getFluidState().getType() == Fluids.FLOWING_WATER) { // the only scenario in which we can walk on flowing water is if it's under still water with jesus off - return isWater(upState) && !Baritone.settings().assumeWalkOnWater.value; + return isWater(upState) && !bsi.assumeWalkOnWater; } // if assumeWalkOnWater is on, we can only walk on water if there isn't water above it // if assumeWalkOnWater is off, we can only walk on water if there is water above it - return isWater(upState) ^ Baritone.settings().assumeWalkOnWater.value; + return isWater(upState) ^ bsi.assumeWalkOnWater; } if (MovementHelper.isLava(state) && !MovementHelper.isFlowing(x, y, z, state, bsi) && Baritone.settings().assumeWalkOnLava.value) { // if we get here it means that assumeWalkOnLava must be true, so put it last diff --git a/src/main/java/baritone/pathing/movement/movements/MovementDescend.java b/src/main/java/baritone/pathing/movement/movements/MovementDescend.java index 0052efed2c..afc4863c6a 100644 --- a/src/main/java/baritone/pathing/movement/movements/MovementDescend.java +++ b/src/main/java/baritone/pathing/movement/movements/MovementDescend.java @@ -157,6 +157,8 @@ public static boolean dynamicFallCost(CalculationContext context, int x, int y, BlockState ontoBlock = context.get(destX, newY, destZ); int unprotectedFallHeight = fallHeight - (y - effectiveStartHeight); // equal to fallHeight - y + effectiveFallHeight, which is equal to -newY + effectiveFallHeight, which is equal to effectiveFallHeight - newY double tentativeCost = WALK_OFF_BLOCK_COST + FALL_N_BLOCKS_COST[unprotectedFallHeight] + frontBreak + costSoFar; + // landing on top of a block ends the fall at newY + 1, so we only actually fall one block less than we probed + double tentativeCostOntoBlock = WALK_OFF_BLOCK_COST + FALL_N_BLOCKS_COST[unprotectedFallHeight - 1] + frontBreak + costSoFar; if (reachedMinimum && MovementHelper.isWater(ontoBlock)) { if (!MovementHelper.canWalkThrough(context, destX, newY, destZ, ontoBlock)) { return false; @@ -208,14 +210,14 @@ public static boolean dynamicFallCost(CalculationContext context, int x, int y, res.x = destX; res.y = newY + 1; res.z = destZ; - res.cost = tentativeCost; + res.cost = tentativeCostOntoBlock; return false; } if (reachedMinimum && context.hasWaterBucket && unprotectedFallHeight <= context.maxFallHeightBucket + 1) { res.x = destX; res.y = newY + 1;// this is the block we're falling onto, so dest is +1 res.z = destZ; - res.cost = tentativeCost + context.placeBucketCost(); + res.cost = tentativeCostOntoBlock + context.placeBucketCost(); return true; } else { return false; diff --git a/src/main/java/baritone/pathing/movement/movements/MovementDiagonal.java b/src/main/java/baritone/pathing/movement/movements/MovementDiagonal.java index 8be1047582..edc1d579bf 100644 --- a/src/main/java/baritone/pathing/movement/movements/MovementDiagonal.java +++ b/src/main/java/baritone/pathing/movement/movements/MovementDiagonal.java @@ -166,7 +166,7 @@ public static void cost(CalculationContext context, int x, int y, int z, int des return; } BlockState cuttingOver2 = context.get(destX, y - 1, z); - if ((!context.allowWalkOnMagmaBlocks && cuttingOver1.is(Blocks.MAGMA_BLOCK)) || MovementHelper.isLava(cuttingOver2)) { + if ((!context.allowWalkOnMagmaBlocks && cuttingOver2.is(Blocks.MAGMA_BLOCK)) || MovementHelper.isLava(cuttingOver2)) { return; } boolean water = false; diff --git a/src/main/java/baritone/pathing/path/PathExecutor.java b/src/main/java/baritone/pathing/path/PathExecutor.java index 04afddfcd7..d76ec17b0d 100644 --- a/src/main/java/baritone/pathing/path/PathExecutor.java +++ b/src/main/java/baritone/pathing/path/PathExecutor.java @@ -27,7 +27,6 @@ import baritone.api.utils.input.Input; import baritone.behavior.PathingBehavior; import baritone.pathing.calc.AbstractNodeCostSearch; -import baritone.pathing.movement.CalculationContext; import baritone.pathing.movement.Movement; import baritone.pathing.movement.MovementHelper; import baritone.pathing.movement.movements.*; @@ -347,7 +346,7 @@ private boolean shouldSprintNextTick() { behavior.baritone.getInputOverrideHandler().setInputForceState(Input.SPRINT, false); // first and foremost, if allowSprint is off, or if we don't have enough hunger, don't try and sprint - if (!new CalculationContext(behavior.baritone, false).canSprint) { + if (!Baritone.settings().allowSprint.value || ctx.player().getFoodData().getFoodLevel() <= 6) { // same as CalculationContext#canSprint, without building a whole context every tick return false; } IMovement current = path.movements().get(pathPosition); diff --git a/src/main/java/baritone/process/BuilderProcess.java b/src/main/java/baritone/process/BuilderProcess.java index c1f275001e..2053712a23 100644 --- a/src/main/java/baritone/process/BuilderProcess.java +++ b/src/main/java/baritone/process/BuilderProcess.java @@ -519,7 +519,7 @@ public int lengthZ() { } // build repeat time layer = 0; - origin = new BlockPos(origin).offset(repeat); + origin = new BlockPos(origin.getX(), origin.getY(), origin.getZ()).offset(repeat); if (!Baritone.settings().buildRepeatSneaky.value) { schematic.reset(); } @@ -1037,7 +1037,7 @@ private static boolean sameBlockstate(BlockState first, BlockState second) { return first.equals(second); // early return if no properties are being ignored } for (Property prop : first.getProperties()) { - if (first.getValue(prop) != second.getValue(prop) + if (!Objects.equals(first.getValue(prop), second.getValue(prop)) && !(ignoreDirection && ORIENTATION_PROPS.contains(prop)) && !ignoredProps.contains(prop.getName())) { return false; diff --git a/src/main/java/baritone/process/ElytraProcess.java b/src/main/java/baritone/process/ElytraProcess.java index 7ee911cded..d490c2bd3f 100644 --- a/src/main/java/baritone/process/ElytraProcess.java +++ b/src/main/java/baritone/process/ElytraProcess.java @@ -37,6 +37,7 @@ import baritone.api.utils.RotationUtils; import baritone.api.utils.input.Input; import baritone.pathing.movement.CalculationContext; +import baritone.pathing.movement.MovementHelper; import baritone.pathing.movement.movements.MovementFall; import baritone.process.elytra.*; import baritone.utils.BaritoneProcessHelper; @@ -47,6 +48,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.NonNullList; import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ServerboundPlayerCommandPacket; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; @@ -62,8 +64,10 @@ import net.minecraft.world.phys.Vec3; import java.util.*; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import static baritone.api.pathing.movement.ActionCosts.COST_INF; @@ -80,10 +84,70 @@ public class ElytraProcess extends BaritoneProcessHelper implements IBaritonePro private boolean allowAboveBuildLimit; private boolean allowAboveRoof; private final Semaphore npfSema = new Semaphore(1); + private CompletableFuture npfDestroyDone = CompletableFuture.completedFuture(null); + /** + * Bumped by every {@link #onLostControl}; a deferred engage queued while the old context was + * tearing down only fires if nothing else (a cancel, another goal) happened in the meantime. + */ + private int engageEpoch; + /** + * Destination of an engage that is waiting on {@link #npfDestroyDone} for the previous context's + * teardown (see {@link #pathTo0}); null when none is. Keeps the process active and its destination + * visible in the meantime, so the pathing manager keeps ticking us and callers polling for the goal + * don't re-issue it. Cleared wherever {@link #engageEpoch} is bumped, which is what drops the deferral. + */ + private BlockPos deferredDestination; + // incremented from the pathfinder's worker threads, reset from wherever a path completes + private final AtomicInteger consecutivePathCalcFailures = new AtomicInteger(); + private static final int PATH_CALC_FAILURES_BEFORE_RESET = 3; 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 + /** + * The shortest drop {@link WalkOffCalculationContext} offers, and therefore the least we have to ask to descend + * for the walk off path to be forced to contain a {@link MovementFall} instead of a flight of stairs. + */ + private static final int TAKEOFF_MIN_FALL_HEIGHT = 8; + /** + * How long to sit in a takeoff state with neither a path nor a calculation running before giving up. Both of + * those are dead ends: nothing in this process starts another calculation, so we would stand still forever. + */ + private static final int TAKEOFF_STALL_TICKS = 60; + /** + * How long a standing takeoff gets to leave the ground and open the elytra before it is written off as a dud + * (a ceiling we didn't account for, knockback, a cobweb, ...). + */ + private static final int STANDING_TAKEOFF_TIMEOUT_TICKS = 40; + /** + * How many standing takeoffs to attempt before giving up. Each one costs a firework, so if this many in a row + * have put us straight back on the ground then something about this spot is wrong and retrying just burns + * rockets. + */ + private static final int MAX_STANDING_TAKEOFFS = 3; + /** + * How many ledges to walk towards before concluding that walking off one is never going to happen. + */ + private static final int MAX_WALK_OFF_ATTEMPTS = 2; + /** + * How far above our feet to look for the first clear 4x4x4 cube when taking off from where we stand. A + * duration-1 rocket lifts a stationary player around 25 blocks straight up before it dies, so a cube any + * further up is out of reach of a vertical takeoff anyway. + */ + private static final int TAKEOFF_MAX_EXIT_HEIGHT = 32; + private int takeoffStallTicks; + private int standingTakeoffs; + private int walkOffAttempts; + private int takeoffAirborneTicks; + private boolean walkOffImpossible; + private boolean takeoffBoostPending; + /** + * Ticks since a takeoff opened the elytra, or {@code -1} outside a takeoff. Only the server can shut an + * elytra again, so one that is shut while we are still in the air this soon after opening it is a takeoff + * the server refused, which is worth telling the user apart from one that merely didn't get anywhere. + */ + private int takeoffOpenedTicksAgo = -1; + private boolean lavaPathRequested; private int landingColumnHeight = SHORT_LANDING_COLUMN_HEIGHT; private Set badLandingSpots = new HashSet<>(); private LandingSearchState landingSearchState; @@ -94,15 +158,31 @@ public void onLostControl() { } public void onLostControl(boolean destroyNpf) { + // Path calculation whenComplete chains land here from the pathfinder's worker threads on + // failure; everything below (behavior handoff, semaphore, epoch) is render-thread state. + if (!ctx.minecraft().isSameThread()) { + ctx.minecraft().execute(() -> this.onLostControl(destroyNpf)); + return; + } + this.engageEpoch++; + this.deferredDestination = null; this.state = State.START_FLYING; // TODO: null state? this.goingToLandingSpot = false; this.landingSpot = null; this.landingSearchState = null; this.reachedGoal = false; this.goal = null; - destroyBehaviorAsync(); + this.takeoffStallTicks = 0; + this.standingTakeoffs = 0; + this.walkOffAttempts = 0; + this.takeoffAirborneTicks = 0; + this.walkOffImpossible = false; + this.takeoffBoostPending = false; + this.takeoffOpenedTicksAgo = -1; + this.lavaPathRequested = false; + CompletableFuture behaviorGone = destroyBehaviorAsync(); if (destroyNpf) { - destroyNpfContextAsync(); + destroyNpfContextAsync(behaviorGone); } } @@ -111,6 +191,41 @@ private ElytraProcess(Baritone baritone) { baritone.getGameEventHandler().registerEventListener(this); } + public void onPathCalculationSuccess() { + this.consecutivePathCalcFailures.set(0); + } + + /** + * A pathfinder context that hit a transient failure while lazily parsing a cached region file + * never retries that region, so once one path calculation fails they usually all do. Persisting + * the context across new goals then turns any external re-command loop into an endless failure + * spam. After a few consecutive failures, throw the context away so the next engage starts clean. + */ + public void onPathCalculationFailure() { + if (this.consecutivePathCalcFailures.incrementAndGet() < PATH_CALC_FAILURES_BEFORE_RESET) { + return; + } + if (ctx.player() != null && ctx.player().isFallFlying()) { + // resetting mid-air would disengage the elytra outright; if the failures persist, + // the grounded retries after landing will trip this + return; + } + this.consecutivePathCalcFailures.set(0); + logDirect("Path calculation failed " + PATH_CALC_FAILURES_BEFORE_RESET + " times in a row, discarding the pathfinder's chunk cache"); + this.onLostControl(); + } + + /** + * Engaged, but without a behavior: {@link #pathTo0} queued its creation behind the previous pathfinder + * context's teardown, either because a setting change just reset us mid-tick or because we were + * re-engaged before that teardown was done. Hold control until the deferred engage lands instead of + * ticking a behavior we don't have, and let go of whatever keys the old one left forced. + */ + private PathingCommand holdForDeferredEngage() { + baritone.getInputOverrideHandler().clearAllKeys(); + return new PathingCommand(null, PathingCommandType.REQUEST_PAUSE); + } + public static IElytraProcess create(final Baritone baritone) { return NetherPathfinderContext.isSupported() ? new ElytraProcess(baritone) @@ -119,7 +234,7 @@ public static IElytraProcess create(final Baritone baritone) { @Override public boolean isActive() { - return this.behavior != null; + return this.behavior != null || this.deferredDestination != null; } @Override @@ -132,10 +247,14 @@ public void resetState() { } } - private static final String AUTO_JUMP_FAILURE_MSG = "Failed to compute a walking path to a spot to jump off from. Consider starting from a higher location, near an overhang. Or, you can disable elytraAutoJump and just manually begin gliding."; + private static final String TAKEOFF_ADVICE_MSG = "Consider starting from a higher location, near an overhang. Or, you can disable elytraAutoJump and just manually begin gliding."; + private static final String AUTO_JUMP_FAILURE_MSG = "Failed to compute a walking path to a spot to jump off from. " + TAKEOFF_ADVICE_MSG; @Override public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { + if (this.behavior == null) { + return holdForDeferredEngage(); + } try { final long seedSetting = Baritone.settings().elytraNetherSeed.value; if (seedSetting != this.behavior.npfContext.getSeed()) { @@ -166,17 +285,31 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { logDirect(e.getMessage(), ChatFormatting.RED); return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); } + if (this.behavior == null) { + // one of the resets above tore the context down and had to defer the re-engage + return holdForDeferredEngage(); + } this.behavior.onTick(); + if (!ctx.player().isFallFlying() && ctx.player().isInLava()) { + return lavaTakeoff(); + } + if (calcFailed) { + if (this.state == State.LOCATE_JUMP || this.state == State.GET_TO_JUMP) { + return standingTakeoff(); + } onLostControl(); logDirect(AUTO_JUMP_FAILURE_MSG); return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); } + // No landing business while in lava. The solver is busy climbing out, and every path the landing search + // asks for from in here starts inside the pool and fails at once, which used to re-issue one every tick. + final boolean inLava = ctx.player().isInLava(); boolean safetyLanding = false; - if (ctx.player().isFallFlying() && shouldLandForSafety()) { + if (ctx.player().isFallFlying() && !inLava && shouldLandForSafety()) { if (Baritone.settings().elytraAllowEmergencyLand.value) { logDirect("Emergency landing - almost out of elytra durability or fireworks"); safetyLanding = true; @@ -184,7 +317,7 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { logDirect("almost out of elytra durability or fireworks, but I'm going to continue since elytraAllowEmergencyLand is false"); } } - if (ctx.player().isFallFlying() && this.state != State.LANDING && (this.behavior.pathManager.isComplete() || safetyLanding)) { + if (ctx.player().isFallFlying() && !inLava && this.state != State.LANDING && (this.behavior.pathManager.isComplete() || safetyLanding)) { final BetterBlockPos last = this.behavior.pathManager.path.getLast(); if (last != null && (last.distToCenterSqr(ctx.player().position()) < (48 * 48) || safetyLanding) && (!goingToLandingSpot || (safetyLanding && this.landingSpot == null))) { if (this.landingSearchState == null) { @@ -240,6 +373,16 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { } if (ctx.player().isFallFlying()) { + if (this.state == State.TAKEOFF_JUMP) { + // the elytra opened without us having asked for it, pick up from wherever that leaves us + this.state = State.START_FLYING; + this.takeoffStallTicks = 0; + this.takeoffBoostPending = true; + this.takeoffOpenedTicksAgo = 0; + } + if (this.takeoffOpenedTicksAgo >= 0) { + this.takeoffOpenedTicksAgo++; + } behavior.landingMode = this.state == State.LANDING; this.goal = null; baritone.getInputOverrideHandler().clearAllKeys(); @@ -250,6 +393,12 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { this.behavior.npfContext.releaseReadLock(); } } + if (this.takeoffBoostPending) { + // a takeoff boost that couldn't be used the moment the elytra opened. after behavior.tick() so + // that it doesn't fight the boost bookkeeping if the solver already decided to use one + this.takeoffBoostPending = false; + this.behavior.useFireworkForTakeoff(); + } 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) { @@ -263,10 +412,22 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { return new PathingCommand(null, PathingCommandType.REQUEST_PAUSE); } + if (this.takeoffOpenedTicksAgo >= 0) { + // we opened the elytra and it is shut again: on the ground that is a takeoff that didn't get + // anywhere, in the air it is the server refusing the glide, and the takeoff rocket with it + if (!ctx.player().onGround()) { + logDirect("The server closed the elytra " + this.takeoffOpenedTicksAgo + " ticks after takeoff."); + } + this.takeoffOpenedTicksAgo = -1; + } + if (this.state == State.FLYING || this.state == State.START_FLYING) { - this.state = ctx.player().onGround() && Baritone.settings().elytraAutoJump.value - ? State.LOCATE_JUMP - : State.START_FLYING; + if (ctx.player().onGround() && Baritone.settings().elytraAutoJump.value) { + this.state = State.LOCATE_JUMP; + this.takeoffStallTicks = 0; + } else { + this.state = State.START_FLYING; + } } if (this.state == State.LOCATE_JUMP) { @@ -275,11 +436,24 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { onLostControl(); return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); } + if (this.walkOffImpossible) { + // there is nothing to walk off of around here, and we already spent a couple of seconds of + // pathfinding finding that out once + return standingTakeoff(); + } if (this.goal == null) { - this.goal = new GoalYLevel(31); + this.goal = takeoffGoal(); } - final IPathExecutor executor = baritone.getPathingBehavior().getCurrent(); - if (executor != null && executor.getPath().getGoal() == this.goal) { + IPathExecutor executor = baritone.getPathingBehavior().getCurrent(); + if (executor != null && executor.getPath().getGoal() != this.goal) { + // a segment left over from whatever we were doing before takeoff. it can never finish while we're + // pausing pathing, and while it exists secretInternalSetGoalAndPath refuses to start the walk off + // calculation, so neither side of this ever moves unless we drop it here + baritone.getPathingBehavior().secretInternalSegmentCancel(); + executor = null; + } + if (executor != null) { + this.takeoffStallTicks = 0; final IMovement fall = executor.getPath().movements().stream() .filter(movement -> movement instanceof MovementFall) .findFirst().orElse(null); @@ -299,10 +473,14 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { }); this.state = State.PAUSE; } else { - onLostControl(); - logDirect(AUTO_JUMP_FAILURE_MSG); - return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); + return standingTakeoff(); } + } else if (baritone.getPathingBehavior().getInProgress().isPresent()) { + this.takeoffStallTicks = 0; + } else if (++this.takeoffStallTicks > TAKEOFF_STALL_TICKS) { + // no path and nothing being calculated: SET_GOAL_AND_PAUSE has already decided it has nothing to + // do, so the executor we're waiting on is never going to appear + return standingTakeoff(); } return new PathingCommandContext(this.goal, PathingCommandType.SET_GOAL_AND_PAUSE, new WalkOffCalculationContext(baritone)); } @@ -312,6 +490,30 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { return new PathingCommand(null, PathingCommandType.REQUEST_PAUSE); } + if (this.state == State.TAKEOFF_JUMP) { + if (++this.takeoffStallTicks > STANDING_TAKEOFF_TIMEOUT_TICKS) { + return abortTakeoff(ctx.player().onGround() + ? "Couldn't get off the ground. " + : "Got into the air but the elytra wouldn't open. "); + } + baritone.getInputOverrideHandler().clearAllKeys(); + if (ctx.player().onGround()) { + // an ordinary jump. vanilla physics, and the movement packets it produces are what the server + // checks its own idea of onGround against before it will accept the request below + this.takeoffAirborneTicks = 0; + baritone.getInputOverrideHandler().setInputForceState(Input.JUMP, true); + } else if (this.takeoffAirborneTicks++ > 0) { + // the second press of the double tap. openElytra() is what actually opens it, but the client + // reports every change of the key to the server (ServerboundPlayerInputPacket), so the inputs it + // sees are the ones a player who opens an elytra sends: press, release, press + baritone.getInputOverrideHandler().setInputForceState(Input.JUMP, true); + if (openElytra()) { + this.state = State.START_FLYING; + } + } + return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); + } + if (this.state == State.GET_TO_JUMP) { final IPathExecutor executor = baritone.getPathingBehavior().getCurrent(); // TODO 1.21.5: replace `ctx.player().getDeltaMovement().y < -0.377` with `ctx.player().fallDistance > 1.0f` @@ -321,8 +523,23 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { && executor.getPath().movements().get(executor.getPosition()) instanceof MovementFall; if (canStartFlying) { + this.takeoffStallTicks = 0; this.state = State.START_FLYING; } else { + if (executor != null || baritone.getPathingBehavior().getInProgress().isPresent()) { + this.takeoffStallTicks = 0; + } else if (++this.takeoffStallTicks > TAKEOFF_STALL_TICKS) { + // the walk off path ran out without us ever falling off it, either because we reached the goal + // by stepping down or because the segment got cancelled. PathingBehavior won't re-path once + // it considers the goal met, so go pick a new ledge instead of standing here + if (++this.walkOffAttempts >= MAX_WALK_OFF_ATTEMPTS) { + return standingTakeoff(); + } + this.takeoffStallTicks = 0; + this.goal = null; + this.state = State.LOCATE_JUMP; + return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); + } return new PathingCommand(null, PathingCommandType.SET_GOAL_AND_PATH); } } @@ -341,6 +558,190 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); } + /** + * Takes off from where we are standing: jump, open the elytra on the way up, and light a firework the moment + * it opens, since a glide that starts with no speed a block and a half off the floor only ends one way. This + * is what flat terrain leaves us with, where {@link WalkOffCalculationContext} has no ledge to offer and the + * alternative is standing around telling the user to go find a cliff. + */ + private PathingCommand standingTakeoff() { + this.walkOffImpossible = true; + this.goal = null; + this.takeoffStallTicks = 0; + this.takeoffAirborneTicks = 0; + baritone.getPathingBehavior().secretInternalSegmentCancel(); + + if (!Baritone.settings().elytraStandingTakeoff.value) { + return abortTakeoff("There is no spot to jump off from, and elytraStandingTakeoff is off. "); + } + if (this.standingTakeoffs >= MAX_STANDING_TAKEOFFS) { + return abortTakeoff("Took off from here " + this.standingTakeoffs + " times and ended up back on the ground every time. "); + } + final BetterBlockPos feet = ctx.playerFeet(); + if (!MovementHelper.fullyPassable(ctx, feet.above(2)) || !MovementHelper.fullyPassable(ctx, feet.above(3))) { + return abortTakeoff("There is no spot to jump off from, and not enough room above me to jump and open the elytra. "); + } + if (!this.behavior.selectFirework()) { + return abortTakeoff("There is no spot to jump off from, and no fireworks in my hotbar to take off from here with. "); + } + if (this.standingTakeoffs++ == 0) { + logDirect("No spot to jump off from, taking off from here instead."); + } + // The path starts from the first clear cube straight above us, which is where a climb on the takeoff + // rocket comes out. In a basalt delta crevice or the hollow beside a lava pond the walls are a couple of + // blocks away on every side, and up is the one direction that is reliably clear. Left to itself the + // pathfinder would start from the nearest clear cube to our feet, and its search for one ignores walls: + // from a hole that is very often a cube on the far side of one, which nothing here could ever reach. + final BetterBlockPos exit = takeoffExit(feet); + // the elytra path has to exist before we leave the ground: the behavior does nothing without one, and the + // handful of ticks between opening the elytra and hitting the ground is no time to compute one + this.state = State.PAUSE; + this.behavior.pathManager.pathToDestination(exit != null ? exit : feet).whenComplete((result, ex) -> { + if (ex == null) { + this.state = State.TAKEOFF_JUMP; + return; + } + onLostControl(); + }); + return new PathingCommand(null, PathingCommandType.REQUEST_PAUSE); + } + + /** + * We are in lava and the elytra is not open. Whatever the ground states were doing can wait: an elytra opens + * in lava (vanilla only refuses in water), and a rocket lit looking straight up is what gets us out. Swimming + * is far too slow, and since vanilla moves us by the fluid rules while we are in one, glide or not, the + * rocket is the only thrust there is and keeps a fraction of its usual push. Pointing it at the pool's wall + * would spend all of that on basalt, so once the elytra is open {@link ElytraBehavior} aims straight up and + * keeps lighting rockets until we are out. + */ + private PathingCommand lavaTakeoff() { + baritone.getPathingBehavior().secretInternalSegmentCancel(); + baritone.getInputOverrideHandler().clearAllKeys(); + // swimming up: slow, but it also lifts us off the pool floor, without which the elytra can't open + baritone.getInputOverrideHandler().setInputForceState(Input.JUMP, true); + if (this.behavior.pathManager.getPath().isEmpty() && !this.lavaPathRequested) { + // nothing to fly along once we're out; the takeoff states would have computed this + this.lavaPathRequested = true; + final BetterBlockPos feet = ctx.playerFeet(); + final BetterBlockPos exit = takeoffExit(feet); + this.behavior.pathManager.pathToDestination(exit != null ? exit : feet); + } + if (!ctx.player().onGround() && openElytra()) { + this.state = State.START_FLYING; + this.takeoffStallTicks = 0; + } + return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); + } + + /** + * The centre of the first 4x4x4 cube of air straight above {@code feet}, aligned the way the elytra + * pathfinder aligns its nodes, or {@code null} if there is none within {@link #TAKEOFF_MAX_EXIT_HEIGHT}. + */ + private BetterBlockPos takeoffExit(BetterBlockPos feet) { + final int minY = ctx.world().dimensionType().minY(); + final int top = minY + this.behavior.npfContext.getMaxHeight(); + final int limit = Math.min(feet.y + TAKEOFF_MAX_EXIT_HEIGHT, top - 4); + final int ox = feet.x & ~3; + final int oz = feet.z & ~3; + final BlockPos.MutableBlockPos mut = new BlockPos.MutableBlockPos(); + cubes: + for (int oy = feet.y & ~3; oy <= limit; oy += 4) { + for (int x = ox; x < ox + 4; x++) { + for (int y = oy; y < oy + 4; y++) { + for (int z = oz; z < oz + 4; z++) { + if (!ctx.world().getBlockState(mut.set(x, y, z)).isAir()) { + continue cubes; + } + } + } + } + return new BetterBlockPos(ox + 2, oy + 2, oz + 2); + } + return null; + } + + /** + * Opens the elytra mid jump, exactly the way {@link net.minecraft.client.player.LocalPlayer} does it when a + * player double taps jump: set the flag locally, then tell the server. Doing it here instead of by toggling + * the jump key means it doesn't hang on the key going up and back down on precisely the right ticks, and it + * is the same thing on the wire either way - the server has already been sent the movement packets from an + * ordinary jump, so it agrees we are off the ground by the time the request reaches it. + * + * @return {@code true} if the elytra is now open + */ + private boolean openElytra() { + if (!ctx.player().tryToStartFallFlying()) { + return false; + } + ctx.player().connection.send(new ServerboundPlayerCommandPacket(ctx.player(), ServerboundPlayerCommandPacket.Action.START_FALL_FLYING)); + // The rocket goes next tick, once the solver has aimed. A vanilla client cannot use an item in the tick + // it starts gliding (its use packets go out before its glide packet, while the elytra is still shut), + // and the use packet carries a look that has to match that tick's movement packet, which the solver + // has not decided yet. The tick costs nothing: the rocket takes a round trip to show up regardless. + this.takeoffBoostPending = true; + this.takeoffOpenedTicksAgo = 0; + return true; + } + + private PathingCommand abortTakeoff(String reason) { + onLostControl(); + logDirect(reason + TAKEOFF_ADVICE_MSG); + return new PathingCommand(null, PathingCommandType.CANCEL_AND_SET_GOAL); + } + + /** + * Where to walk to in order to take off. + *

+ * {@link WalkOffCalculationContext} only offers single steps and falls of at least + * {@link #TAKEOFF_MIN_FALL_HEIGHT} as ways down, so asking to descend at least that far is what forces the + * path to contain a {@link MovementFall} at all. Aiming deeper than that is what makes the biggest reachable + * drop the cheapest route: a long fall costs a handful of ticks where the same descent by single steps costs + * roughly 8 ticks a block, so the pathfinder will happily walk a long way to an overhang. In the nether the + * lava ocean at y=31 is as deep as it is worth aiming, since anything that reaches it is already more runway + * than we need. + *

+ * The level is always strictly below our feet. A goal we are already standing in is never pathed to at all + * ({@link baritone.behavior.PathingBehavior#secretInternalSetGoalAndPath}), which used to leave the process + * paused forever waiting on an executor that nothing was calculating - most easily hit by landing at exactly + * y=31 in the nether, where the old fixed {@code GoalYLevel(31)} was already satisfied. + */ + private Goal takeoffGoal() { + final int feetY = ctx.playerFeet().y; + final int minY = ctx.world().dimensionType().minY(); + final int deepest = ctx.world().dimension() == Level.NETHER ? minY + 31 : minY; + final int level = Math.min(feetY - 1, Math.max(minY, Math.min(deepest, feetY - TAKEOFF_MIN_FALL_HEIGHT))); + return new GoalDescendTo(level); + } + + /** + * {@link GoalYLevel} is an exact-level goal: it is satisfied only by standing at the level, and asks to climb + * back up when below it. For a takeoff all that matters is getting down, so anything at or below the level + * will do. + */ + private static final class GoalDescendTo implements Goal { + + private final int level; + + private GoalDescendTo(int level) { + this.level = level; + } + + @Override + public boolean isInGoal(int x, int y, int z) { + return y <= this.level; + } + + @Override + public double heuristic(int x, int y, int z) { + return y > this.level ? GoalYLevel.calculate(this.level, y) : 0; + } + + @Override + public String toString() { + return "GoalDescendTo{y<=" + this.level + "}"; + } + } + public void landingSpotIsBad(BetterBlockPos endPos) { badLandingSpots.add(endPos); goingToLandingSpot = false; @@ -349,14 +750,13 @@ public void landingSpotIsBad(BetterBlockPos endPos) { this.state = State.FLYING; } - private void destroyBehaviorAsync() { + private CompletableFuture destroyBehaviorAsync() { ElytraBehavior behavior = this.behavior; - if (behavior != null) { - this.behavior = null; - Baritone.getExecutor().execute(() -> { - behavior.destroy(); - }); + if (behavior == null) { + return CompletableFuture.completedFuture(null); } + this.behavior = null; + return CompletableFuture.runAsync(behavior::destroy, Baritone.getExecutor()); } @Override @@ -397,7 +797,7 @@ public void repackChunks() { @Override public BlockPos currentDestination() { - return this.behavior != null ? this.behavior.destination : null; + return this.behavior != null ? this.behavior.destination : this.deferredDestination; } @Override @@ -423,11 +823,37 @@ private void pathTo0(BlockPos destination, boolean appendDestination) { return; } this.onLostControl(false); + if (this.npfContext == null) { + if (!this.npfSema.tryAcquire()) { + // The previous context is still tearing down on a worker thread, pinned by an + // uncancellable native path calculation. Blocking here would hang the render + // thread for its whole remaining duration, so finish this engage when the + // teardown completes instead - unless something else took over in the meantime. + final int epoch = this.engageEpoch; + this.deferredDestination = destination; + this.npfDestroyDone.whenCompleteAsync((v, ex) -> { + if (this.engageEpoch == epoch) { + this.pathTo0(destination, appendDestination); + } + }, ctx.minecraft()::execute); + return; + } + try { + this.npfContext = new NetherPathfinderContext( + Baritone.settings().elytraNetherSeed.value, + Baritone.settings().elytraUseCache.value ? baritone.getWorldProvider().getCurrentWorld().directory.resolve("cache") : null, + ctx.world() + ); + } catch (Throwable t) { + this.npfSema.release(); + throw t; + } + } 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); + this.behavior = new ElytraBehavior(this.baritone, this, this.npfContext, destination, appendDestination); if (ctx.world() != null) { this.repackChunks(); @@ -509,6 +935,7 @@ public enum State { LOCATE_JUMP("Finding spot to jump off"), PAUSE("Waiting for elytra path"), GET_TO_JUMP("Walking to takeoff"), + TAKEOFF_JUMP("Taking off"), START_FLYING("Begin flying"), FLYING("Flying"), LANDING("Landing"); @@ -528,7 +955,10 @@ public void onRenderPass(RenderEvent event) { @Override public void onWorldEvent(WorldEvent event) { if (event.getWorld() != null && event.getState() == EventState.POST) { - // Exiting the world, just destroy + // Exiting the world, just destroy. A world change also invalidates any engage that was + // deferred while the old context tore down - its destination belongs to the old world. + this.engageEpoch++; + this.deferredDestination = null; destroyBehaviorAsync(); } } @@ -562,7 +992,7 @@ public static final class WalkOffCalculationContext extends CalculationContext { public WalkOffCalculationContext(IBaritone baritone) { super(baritone, true); this.allowFallIntoLava = true; - this.minFallHeight = 8; + this.minFallHeight = TAKEOFF_MIN_FALL_HEIGHT; this.maxFallHeightNoWater = 10000; } @@ -770,26 +1200,22 @@ private BetterBlockPos advanceHeightmap(BetterBlockPos qPos) { } } - 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() { + private void destroyNpfContextAsync(CompletableFuture afterBehaviorGone) { NetherPathfinderContext npf = this.npfContext; if (npf != null) { this.npfContext = null; - Baritone.getExecutor().execute(() -> { - npf.destroy(); - npfSema.release(); - }); + // The solver and any still-queued path results keep making native calls until the + // behavior teardown completes, so the context must only be freed after it. + this.npfDestroyDone = afterBehaviorGone.handleAsync((v, ex) -> { + try { + npf.destroy(); + } catch (Throwable t) { + t.printStackTrace(); + } finally { + npfSema.release(); + } + return null; + }, Baritone.getExecutor()); } } } diff --git a/src/main/java/baritone/process/FarmProcess.java b/src/main/java/baritone/process/FarmProcess.java index fbb7f66a2c..da2ff2fdd7 100644 --- a/src/main/java/baritone/process/FarmProcess.java +++ b/src/main/java/baritone/process/FarmProcess.java @@ -21,8 +21,8 @@ import baritone.api.BaritoneAPI; import baritone.api.pathing.goals.Goal; import baritone.api.pathing.goals.GoalBlock; -import baritone.api.pathing.goals.GoalGetToBlock; import baritone.api.pathing.goals.GoalComposite; +import baritone.api.pathing.goals.GoalGetToBlock; import baritone.api.process.IFarmProcess; import baritone.api.process.PathingCommand; import baritone.api.process.PathingCommandType; @@ -42,16 +42,7 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.AirBlock; -import net.minecraft.world.level.block.BambooStalkBlock; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.BonemealableBlock; -import net.minecraft.world.level.block.CactusBlock; -import net.minecraft.world.level.block.CocoaBlock; -import net.minecraft.world.level.block.CropBlock; -import net.minecraft.world.level.block.NetherWartBlock; -import net.minecraft.world.level.block.SugarCaneBlock; +import net.minecraft.world.level.block.*; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; @@ -267,7 +258,7 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) { } if (state.getBlock() instanceof BonemealableBlock) { BonemealableBlock ig = (BonemealableBlock) state.getBlock(); - if (ig.isValidBonemealTarget(ctx.world(), pos, state) && ig.isBonemealSuccess(ctx.world(), ctx.world().getRandom(), pos, state)) { + if (ig.isValidBonemealTarget(ctx.world(), pos, state, BonemealSource.INTERACTION) && ig.isBonemealSuccess(ctx.world(), ctx.world().getRandom(), pos, state, BonemealSource.INTERACTION)) { bonemealable.add(pos); } } diff --git a/src/main/java/baritone/process/elytra/BuildLimitPathFinder.java b/src/main/java/baritone/process/elytra/BuildLimitPathFinder.java index 01624d730f..1961ed5d54 100644 --- a/src/main/java/baritone/process/elytra/BuildLimitPathFinder.java +++ b/src/main/java/baritone/process/elytra/BuildLimitPathFinder.java @@ -108,7 +108,8 @@ public Pair,Boolean> generateTransitionUp(BetterBlockPos st final double deltaZ = destination.getZ() - start.getZ(); final double distance = Math.sqrt(deltaX * deltaX + deltaZ * deltaZ); - final double scale = 8 / distance; + // a destination straight above/below would otherwise make the steps NaN + final double scale = distance > 1.0e-6 ? 8 / distance : 0; final double stepX = deltaX * scale; final double stepZ = deltaZ * scale; @@ -234,7 +235,8 @@ public CompletableFuture pathFindAsync(BlockPos src, BlockPos d boolean success = transition.second(); if(!success) { - BetterBlockPos newDest = distanceXZ > 32 ? new BetterBlockPos(dst) : new BetterBlockPos(dst.getX(), playerCtx.world().getMaxY(), dst.getZ()); + // distanceXZ is squared + BetterBlockPos newDest = distanceXZ > 32 * 32 ? new BetterBlockPos(dst) : new BetterBlockPos(dst.getX(), playerCtx.world().getMaxY(), dst.getZ()); var directPath = generateDirectPath(new BetterBlockPos(src), newDest, 0, 2); return CompletableFuture.completedFuture(new UnpackedSegment(directPath.first().stream(), directPath.second())); } diff --git a/src/main/java/baritone/process/elytra/ElytraBehavior.java b/src/main/java/baritone/process/elytra/ElytraBehavior.java index 7cc23498e1..10380263aa 100644 --- a/src/main/java/baritone/process/elytra/ElytraBehavior.java +++ b/src/main/java/baritone/process/elytra/ElytraBehavior.java @@ -40,7 +40,9 @@ import net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket; import net.minecraft.util.Mth; import net.minecraft.world.InteractionHand; +import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.projectile.FireworkRocketEntity; import net.minecraft.world.inventory.ContainerInput; import net.minecraft.world.item.ItemStack; @@ -85,6 +87,7 @@ public final class ElytraBehavior implements Helper { public final PathManager pathManager; private final ElytraProcess process; + private static final int FIREWORK_COOLDOWN_TICKS = 10; /** * Remaining cool-down ticks between firework usage */ @@ -103,9 +106,11 @@ public final class ElytraBehavior implements Helper { * Updated every time a firework is automatically used */ private int minimumBoostTicks; - - private boolean deployedFireworkLastTick; - private final int[] nextTickBoostCounter; + /** + * Whether the entity of the last rocket we lit has been seen attached to us. Only for the verbose log: + * a rocket that never shows up is one the server ignored. + */ + private boolean fireworkSeenSinceLit = true; private BlockStateInterface bsi; public final BetterBlockPos destination; @@ -132,7 +137,6 @@ public ElytraBehavior(Baritone baritone, ElytraProcess process, NetherPathfinder this.destination = new BetterBlockPos(destination); this.appendDestination = appendDestination; this.solverExecutor = Executors.newSingleThreadExecutor(); - this.nextTickBoostCounter = new int[2]; this.npfContext = npf; @@ -194,6 +198,7 @@ public CompletableFuture pathToDestination(final BlockPos from) { final long start = System.nanoTime(); return this.path0(from, destinationFixed(), UnaryOperator.identity()) .thenRun(() -> { + process.onPathCalculationSuccess(); final double distance = this.path.get(0).distanceTo(this.path.get(this.path.size() - 1)); if (this.completePath) { logVerbose(String.format("Computed path (%.1f blocks in %.4f seconds)", distance, (System.nanoTime() - start) / 1e9d)); @@ -207,6 +212,7 @@ public CompletableFuture pathToDestination(final BlockPos from) { final Throwable cause = ex.getCause(); if (cause instanceof PathCalculationException) { logDirect("Failed to compute path to destination"); + process.onPathCalculationFailure(); } else { logUnhandledException(cause); } @@ -223,13 +229,25 @@ 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() ? fixDestination(this.path.get(upToIncl.getAsInt())) : destinationFixed(), 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 -> { + if (!upToIncl.isPresent() || !segment.isFinished()) { + // Either this went all the way to the destination and speaks for itself, or the + // search ran out of time short of the rejoin node. Tacking the old tail onto a + // segment that never reached it would leave a blind jump in the path, straight + // through whatever lies between, that no obstacle check can ever fix: every + // recomputation ends the same way, and the solver just circles the last node it + // can reach. Leave the path unfinished instead and continue from where it got to. + return segment; + } + return segment.append(after.stream(), complete); + }) .whenComplete((result, ex) -> { this.recalculating = false; if (ex != null) { final Throwable cause = ex.getCause(); if (cause instanceof PathCalculationException) { logDirect("Failed to recompute segment"); + repackNearPlayer("segment recomputation failed"); } else { logUnhandledException(cause); } @@ -264,6 +282,7 @@ public void pathNextSegment(final int afterIncl) { final Throwable cause = ex.getCause(); if (cause instanceof PathCalculationException) { logDirect("Failed to compute next segment"); + repackNearPlayer("next segment computation failed"); if (pathStart.distToCenterSqr(ctx.player().position()) < 16 * 16) { logVerbose("Player is near the segment start, therefore repeating this calculation is pointless. Marking as complete"); completePath = true; @@ -336,8 +355,25 @@ private void pathfindAroundObstacles() { } final BetterBlockPos rangeStart = path.get(rangeStartIncl); if (!ElytraBehavior.this.passable(rangeStart.x, rangeStart.y, rangeStart.z, false)) { - // we're in a wall - return; // previous iterations of this function SHOULD have fixed this by now :rage_cat: + // the node closest to us is now inside a block, so the terrain changed since this path was computed + final BetterBlockPos feet = ctx.playerFeet(); + if (!ElytraBehavior.this.passable(feet.x, feet.y, feet.z, false)) { + // we're inside a block ourselves, recalculating from here can't succeed. while flying that + // means the cache is wrong about where we are, so refresh it + repackNearPlayer("the cache has us inside a block"); + return; + } + int rejoin = rangeEndExcl - 1; + while (rejoin > rangeStartIncl && !ElytraBehavior.this.passable(path.get(rejoin).x, path.get(rejoin).y, path.get(rejoin).z, false)) { + rejoin--; + } + final var wallDest = destinationFixed(); + final OptionalInt rejoinAt = rejoin > rangeStartIncl && path.get(rejoin).distanceSq(wallDest) < feet.distanceSq(wallDest) + ? OptionalInt.of(rejoin) + : OptionalInt.empty(); + this.pathRecalcSegment(rejoinAt) + .thenRun(() -> logVerbose("Recalculated segment, path node was inside a block")); + return; } if (ElytraBehavior.this.process.state != ElytraProcess.State.LANDING && this.ticksNearUnchanged > 100) { @@ -472,9 +508,53 @@ public void onRenderPass(RenderEvent event) { public void onChunkEvent(ChunkEvent event) { if (event.isPostPopulate() && this.npfContext != null) { - final LevelChunk chunk = ctx.world().getChunk(event.getX(), event.getZ()); - npfContext.queueForPacking(chunk); + // A chunk packet the client discarded (outside its view range) still raises this event, and the + // world hands back its shared empty chunk for it. Packing that would record a chunk of pure air as + // real terrain, and the path would then be routed straight through whatever is actually there. + final LevelChunk chunk = ctx.world().getChunkSource().getChunk(event.getX(), event.getZ(), false); + if (chunk != null && !chunk.isEmpty()) { + npfContext.queueForPacking(chunk); + } + } + } + + // Wall-clock so that it also bounds the repacks queued from the recomputation callbacks between ticks + private static final long REPACK_NEAR_COOLDOWN_MS = 2000; + private long lastRepackNearMs; + + /** + * Re-reads the chunks around the player into the pathfinder's cache. Everything the solver and the + * obstacle checks do runs against that cache, so if it disagrees with the world (a chunk whose packing was + * missed, a stale region from the on-disk cache that the live chunk never replaced) the solver aims at + * terrain it thinks is air and nothing in here notices - the raytraces look at the same cache. This is the + * fix that cancelling and re-engaging used to apply by accident, through {@code repackChunks}. + */ + private void repackNearPlayer(final String why) { + if (!ctx.minecraft().isSameThread()) { + ctx.minecraft().execute(() -> this.repackNearPlayer(why)); + return; + } + if (ctx.player() == null || ctx.world() == null || this.npfContext == null) { + return; + } + final long now = System.currentTimeMillis(); + if (now - this.lastRepackNearMs < REPACK_NEAR_COOLDOWN_MS) { + return; + } + this.lastRepackNearMs = now; + final ChunkPos center = ctx.player().chunkPosition(); + final ChunkSource chunkSource = ctx.world().getChunkSource(); + int packed = 0; + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + final LevelChunk chunk = chunkSource.getChunk(center.x() + dx, center.z() + dz, false); + if (chunk != null && !chunk.isEmpty()) { + npfContext.queueForPacking(chunk); + packed++; + } + } } + logVerbose("Repacking " + packed + " chunks around the player: " + why); } public void onBlockChange(BlockChangeEvent event) { @@ -546,11 +626,17 @@ private void onTick0() { // Certified mojang employee incident if (this.remainingFireworkTicks > 0) { this.remainingFireworkTicks--; + if (this.remainingFireworkTicks == 0 && !this.fireworkSeenSinceLit) { + logVerbose("the rocket lit " + FIREWORK_COOLDOWN_TICKS + " ticks ago never showed up, the server ignored it"); + } } if (this.remainingSetBackTicks > 0) { this.remainingSetBackTicks--; } - if (!this.getAttachedFirework().isPresent()) { + if (this.getAttachedFirework().isPresent()) { + this.fireworkSeenSinceLit = true; + } else if (this.remainingFireworkTicks <= 0) { + // nothing burning, and nothing lit recently enough for its entity to still be on its way from the server this.minimumBoostTicks = 0; } @@ -561,6 +647,11 @@ private void onTick0() { this.simulationLine = null; this.aimPos = null; + // ctx AND context???? :DDD + // Before the path check: everything the solver simulates goes through passable(), and the lava escape + // solves without a path + this.bsi = new BlockStateInterface(ctx); + final List path = this.pathManager.getPath(); if (path.isEmpty()) { return; @@ -569,8 +660,6 @@ private void onTick0() { return; } - // ctx AND context???? :DDD - this.bsi = new BlockStateInterface(ctx); this.pathManager.tick(); final int playerNear = this.pathManager.getNear(); @@ -584,17 +673,24 @@ private void onTick0() { * Called by {@link baritone.process.ElytraProcess#onTick(boolean, boolean)} when the process is in control and the player is flying */ public void tick() { - if (this.pathManager.getPath().isEmpty()) { + final boolean inLava = ctx.player().isInLava(); + if (this.pathManager.getPath().isEmpty() && !inLava) { return; } trySwapElytra(); if (ctx.player().horizontalCollision) { logVerbose("hbonk"); + // the simulation never flies into anything it knows about, so this is something it didn't + repackNearPlayer("hit something the cache didn't know about"); } if (ctx.player().verticalCollision) { logVerbose("vbonk"); } + if (this.bsi == null) { + // onTick0 has not run yet (a path calculation has held the lock every tick so far), nothing to solve against + return; + } final SolverContext solverContext = this.new SolverContext(false); this.solveNextTick = true; @@ -607,12 +703,6 @@ public void tick() { solution = this.solveAngles(solverContext); } - if (this.deployedFireworkLastTick) { - this.nextTickBoostCounter[solverContext.boost.isBoosted() ? 1 : 0]++; - this.deployedFireworkLastTick = false; - } - - final boolean inLava = ctx.player().isInLava(); if (inLava) { baritone.getInputOverrideHandler().setInputForceState(Input.JUMP, true); } @@ -635,7 +725,8 @@ public void tick() { solution.context.start, solution.goingTo, solution.context.boost.isBoosted(), - solution.forceUseFirework || inLava + solution.forceUseFirework || inLava, + inLava ); } @@ -660,9 +751,13 @@ public void onPostTick(TickEvent event) { // calls passable which requires a read lock private Solution solveAngles(final SolverContext context) { + if (context.ignoreLava) { + return this.solveLavaEscape(context); + } final NetherPath path = context.path; final int playerNear = landingMode ? path.size() - 1 : context.playerNear; final Vec3 start = context.start; + final int scanAhead = Math.max(1, Baritone.settings().elytraPathLookahead.value); Solution solution = null; for (int relaxation = 0; relaxation < 3; relaxation++) { // try for a strict solution first, then relax more and more (if we're in a corner or near some blocks, it will have to relax its constraints a bit) @@ -671,7 +766,7 @@ private Solution solveAngles(final SolverContext context) { //int minStep = Math.max(0, playerNear - relaxation); int minStep = playerNear; - for (int i = Math.min(playerNear + 20, path.size() - 1); i >= minStep; i--) { + for (int i = Math.min(playerNear + scanAhead, 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) { @@ -744,15 +839,118 @@ private Solution solveAngles(final SolverContext context) { } } } - return solution; + return this.solveSurvival(context, solution); } - private void tickUseFireworks(final Vec3 start, final Vec3 goingTo, final boolean isBoosted, final boolean forceUseFirework) { - if (this.remainingSetBackTicks > 0) { + /** + * In lava the elytra is inert: vanilla moves us by the fluid rules whenever we are in one, glide or not, so + * the only thrust is the rocket, at a fraction of its usual push, and the pool is walled in a few blocks + * away on every side. Aiming at the path would spend the rocket pushing against basalt. The way out is up: + * the steepest climb the simulation keeps clear, and a rocket, every time. Works without a path, since a + * lava takeoff may still be waiting for its first one. + */ + private Solution solveLavaEscape(final SolverContext context) { + final NetherPath path = context.path; + // yaw is irrelevant to a vertical rocket; face where we are going so that the climb comes out pointed + // along the path once the normal solver takes over + final Vec3 towards = path.isEmpty() + ? Vec3.atCenterOf(this.destination) + : path.getVec(Math.min(context.playerNear + 1, path.size() - 1)); + final float yaw = RotationUtils.calcRotationFromVec3d(context.start, towards, ctx.playerRotations()).getYaw(); + final int ticks = Math.max(5, Baritone.settings().elytraSimulationTicks.value); + // simulate with a rocket burning whether or not one is yet: the escape needs one, and tick() lights it + final int ticksBoosted = context.boost.isBoosted() ? Math.max(1, context.boost.getGuaranteedBoostTicks()) : 10; + final int ticksBoostDelay = context.boost.isBoosted() ? 0 : 2; + final PitchResult best = this.solveSurvivalPitch(context, yaw, ticks, ticksBoosted, ticksBoostDelay); + if (best == null) { + return null; // cancelled by the game thread + } + this.simulationLine = best.steps; + final Vec3 last = best.steps.get(best.steps.size() - 1); + return new Solution(context, new Rotation(yaw, best.pitch), context.start.add(last), true, true); + } + + /** + * Last resort when no pitch reaches the path (e.g. the terrain changed and the goal is now walled off): + * fly the pitch that stays collision-free the longest, biased towards climbing, so that the path + * recalculation has time to catch up. If an impact is imminent and a firework would provably extend + * survival, one is forced. + */ + private Solution solveSurvival(final SolverContext context, final Solution unsolved) { + if (landingMode) { + return unsolved; + } + final float yaw = unsolved != null ? unsolved.rotation.getYaw() : ctx.playerRotations().getYaw(); + final int ticks = Math.max(5, Baritone.settings().elytraSimulationTicks.value); + final int ticksBoosted = context.boost.isBoosted() ? Math.max(1, context.boost.getGuaranteedBoostTicks()) : 0; + + final PitchResult best = this.solveSurvivalPitch(context, yaw, ticks, ticksBoosted, 0); + if (best == null) { // cancelled by the game thread + return unsolved; + } + + final int survivedTicks = best.steps.size() - 1; + // No pitch keeps us clear for the whole horizon, or we are in lava: if a rocket lit now would provably + // do better, light one. This used to wait until the impact was a dozen ticks out, which from the top of + // a climb out of a hole meant falling most of the way back in before doing anything about it. + final boolean impactAhead = survivedTicks < ticks; + if ((impactAhead || context.ignoreLava) && !context.boost.isBoosted() && context.hasFireworks) { + final PitchResult boosted = this.solveSurvivalPitch(context, yaw, ticks, 10, 2); + if (boosted != null && (boosted.steps.size() > best.steps.size() + 2 + || (context.ignoreLava && boosted.steps.size() >= best.steps.size()))) { + final Vec3 last = boosted.steps.get(boosted.steps.size() - 1); + this.simulationLine = boosted.steps; + return new Solution(context, new Rotation(yaw, boosted.pitch), context.start.add(last), true, true); + } + } + + this.simulationLine = best.steps; + return new Solution(context, new Rotation(yaw, best.pitch), null, false, false); + } + + private PitchResult solveSurvivalPitch(final SolverContext context, final float yaw, final int ticks, + final int ticksBoosted, final int ticksBoostDelay) { + final float yawRadians = yaw * RotationUtils.DEG_TO_RAD_F; + // a goal far along the direction we are about to face, so that the simulated yaw stays fixed + final Vec3 direction = new Vec3(-Mth.sin(yawRadians) * 1e4, 0, Mth.cos(yawRadians) * 1e4); + final float currentPitch = ctx.playerRotations().getPitch(); + + PitchResult best = null; + // scan the climbing side (negative pitch) first; new walls are usually escapable from above. With a + // rocket burning, don't settle for the first pitch that survives: take the one that ends highest. The + // boost is the one chance to get above whatever is boxing us in, and straight up is where that is most + // likely to be clear (the hole we just took off from, a crevice, the hollow beside a lava pond). + final boolean preferHeight = ticksBoosted > 0; + for (float pitch = currentPitch; pitch >= -90; pitch -= 3) { + if (Thread.interrupted()) return null; + best = this.betterSurvival(context, direction, pitch, ticks, ticksBoosted, ticksBoostDelay, best); + if (!preferHeight && best != null && best.steps.size() - 1 >= ticks) return best; + } + if (best != null && best.steps.size() - 1 >= ticks) return best; + for (float pitch = currentPitch + 3; pitch <= 90; pitch += 3) { + if (Thread.interrupted()) return null; + best = this.betterSurvival(context, direction, pitch, ticks, ticksBoosted, ticksBoostDelay, best); + if (best != null && best.steps.size() - 1 >= ticks) return best; + } + return best; + } + + private PitchResult betterSurvival(final SolverContext context, final Vec3 direction, final float pitch, + final int ticks, final int ticksBoosted, final int ticksBoostDelay, + final PitchResult best) { + final List steps = this.simulate(context, direction, pitch, ticks, ticksBoosted, ticksBoostDelay, true); + // longer survival wins; on equal survival prefer the trajectory that ends higher up + final double score = (steps.size() - 1) * 1000.0 + steps.get(steps.size() - 1).y; + return best == null || score > best.dot ? new PitchResult(pitch, score, steps) : best; + } + + private void tickUseFireworks(final Vec3 start, final Vec3 goingTo, final boolean isBoosted, final boolean forceUseFirework, final boolean inLava) { + // neither the setback delay nor a landing in progress is a reason to keep burning + if (this.remainingSetBackTicks > 0 && !inLava) { logDebug("waiting for elytraFireworkSetbackUseDelay: " + this.remainingSetBackTicks); return; } - if (this.landingMode) { + if (this.landingMode && !inLava) { return; } final boolean useOnDescend = !Baritone.settings().elytraConserveFireworks.value || ctx.player().position().y < goingTo.y + 5; @@ -769,21 +967,71 @@ private void tickUseFireworks(final Vec3 start, final Vec3 goingTo, final boolea && (ctx.player().position().y < goingTo.y - 5 || start.distanceTo(new Vec3(goingTo.x + 0.5, ctx.player().position().y, goingTo.z + 0.5)) > 5) // UGH!!!!!!! && currentSpeed < elytraFireworkSpeed * elytraFireworkSpeed)) ) { - // Prioritize boosting fireworks over regular ones - // TODO: Take the minimum boost time into account? - if (!baritone.getInventoryBehavior().throwaway(true, ElytraBehavior::isBoostingFireworks) && - !baritone.getInventoryBehavior().throwaway(true, ElytraBehavior::isFireworks)) { - logDirect("no fireworks"); - return; - } logVerbose("attempting to use firework" + (forceUseFirework ? " (forced)" : "")); - ctx.playerController().processRightClick(ctx.player(), ctx.world(), InteractionHand.MAIN_HAND); - this.minimumBoostTicks = 10 * (1 + getFireworkBoost(ctx.player().getItemInHand(InteractionHand.MAIN_HAND)).orElse(0)); - this.remainingFireworkTicks = 10; - this.deployedFireworkLastTick = true; + this.useFirework(); } } + /** + * Lights a firework to get off the ground, for an elytra that was opened from a standing jump instead of a + * fall. There is no speed and only a couple of blocks of air to work with, so this can't wait for the solver + * to decide that a boost is warranted. + * + * @return {@code true} if a firework is now burning, {@code false} if we have none to use + */ + public boolean useFireworkForTakeoff() { + if (this.remainingFireworkTicks > 0 || this.getAttachedFirework().isPresent()) { + return true; + } + return this.useFirework(); + } + + /** + * @return {@code true} if a firework was used + */ + private boolean useFirework() { + // the main hand is what processRightClick uses, and selectFirework settles for putting something harmless + // there when the only firework we have is in the off hand, which would right click nothing at all + if (!this.selectFirework() || !isFireworks(ctx.player().getItemInHand(InteractionHand.MAIN_HAND))) { + logDirect("no fireworks"); + return false; + } + // The use packet carries a look of its own, read off the player as it is built, and the server-side + // movement check compares it with the look in this tick's movement packet: the rotation LookBehavior + // is about to apply for the solver's target, not what the player is facing right now. Face that for + // the duration of the click so the two agree; the look behavior sets or restores the real rotation + // later in the tick regardless. + final Rotation wire = this.baritone.getLookBehavior().getRotationForThisTick(); + final float yaw = ctx.player().getYRot(); + final float pitch = ctx.player().getXRot(); + ctx.player().setYRot(wire.getYaw()); + ctx.player().setXRot(wire.getPitch()); + try { + ctx.playerController().processRightClick(ctx.player(), ctx.world(), InteractionHand.MAIN_HAND); + } finally { + ctx.player().setYRot(yaw); + ctx.player().setXRot(pitch); + } + this.minimumBoostTicks = 10 * (1 + getFireworkBoost(ctx.player().getItemInHand(InteractionHand.MAIN_HAND)).orElse(0)); + this.remainingFireworkTicks = FIREWORK_COOLDOWN_TICKS; + this.fireworkSeenSinceLit = false; + return true; + } + + /** + * Puts a firework in the main hand. Picking one that is already in the hotbar is instant, but bringing one up + * from the rest of the inventory is a window click, and those only happen while stationary, so a takeoff has + * to call this before leaving the ground. + * + * @return {@code true} if the main hand now holds a firework + */ + public boolean selectFirework() { + // Prioritize boosting fireworks over regular ones + // TODO: Take the minimum boost time into account? + return baritone.getInventoryBehavior().throwaway(true, ElytraBehavior::isBoostingFireworks) + || baritone.getInventoryBehavior().throwaway(true, ElytraBehavior::isFireworks); + } + private final class SolverContext { public final NetherPath path; @@ -792,6 +1040,9 @@ private final class SolverContext { public final Vec3 motion; public final AABB boundingBox; public final boolean ignoreLava; + public final double gravity; + public final boolean slowFalling; + public final boolean hasFireworks; public final FireworkBoost boost; public final IAimProcessor aimProcessor; @@ -809,13 +1060,26 @@ public SolverContext(boolean async) { this.motion = ctx.playerMotion(); this.boundingBox = ctx.player().getBoundingBox(); this.ignoreLava = ctx.player().isInLava(); - - final Integer fireworkTicksExisted; - if (async && ElytraBehavior.this.deployedFireworkLastTick) { - final int[] counter = ElytraBehavior.this.nextTickBoostCounter; - fireworkTicksExisted = counter[1] > counter[0] ? 0 : null; - } else { - fireworkTicksExisted = ElytraBehavior.this.getAttachedFirework().map(e -> e.tickCount).orElse(null); + this.gravity = ctx.player().getAttributeValue(Attributes.GRAVITY); + this.slowFalling = ctx.player().hasEffect(MobEffects.SLOW_FALLING); + boolean fireworks = false; + for (ItemStack stack : ctx.player().getInventory().getNonEquipmentItems()) { + if (isFireworks(stack)) { + fireworks = true; + break; + } + } + this.hasFireworks = fireworks; + + Integer fireworkTicksExisted = ElytraBehavior.this.getAttachedFirework().map(e -> e.tickCount).orElse(null); + if (fireworkTicksExisted == null && ElytraBehavior.this.remainingFireworkTicks > 0) { + // A rocket we lit within the cooldown whose entity the server hasn't shown us yet: a round trip, + // which on a busy server is several ticks. Solve as though it had been burning since we lit it. + // The boost follows whatever we aim at once it arrives, and planning an unboosted trajectory in + // the meantime - a shallow glide, since without thrust nothing else gets anywhere - is what + // points a takeoff rocket at the nearest wall. If it never shows up the assumption expires with + // the cooldown. The end-of-tick solve is for the next tick, whose cooldown is one lower. + fireworkTicksExisted = FIREWORK_COOLDOWN_TICKS - ElytraBehavior.this.remainingFireworkTicks + (async ? 1 : 0); } this.boost = new FireworkBoost(fireworkTicksExisted, ElytraBehavior.this.minimumBoostTicks); @@ -843,6 +1107,9 @@ public boolean equals(Object o) { && Objects.equals(this.motion, other.motion) && Objects.equals(this.boundingBox, other.boundingBox) && this.ignoreLava == other.ignoreLava + && this.gravity == other.gravity + && this.slowFalling == other.slowFalling + && this.hasFireworks == other.hasFireworks && Objects.equals(this.boost, other.boost); } } @@ -1141,7 +1408,8 @@ private PitchResult solvePitch(final SolverContext context, final Vec3 goal, fin pitch, ticks, ticksBoosted, - ticksBoostDelay + ticksBoostDelay, + false ); if (displacement == null) { continue; @@ -1181,7 +1449,7 @@ private PitchResult solvePitch(final SolverContext context, final Vec3 goal, fin } private List simulate(final SolverContext context, final Vec3 goalDelta, final float pitch, final int ticks, - final int ticksBoosted, final int ticksBoostDelay) { + final int ticksBoosted, final int ticksBoostDelay, final boolean stopOnImpact) { final ITickableAimProcessor aimProcessor = context.aimProcessor.fork(); Vec3 delta = goalDelta; Vec3 motion = context.motion; @@ -1201,7 +1469,7 @@ private List simulate(final SolverContext context, final Vec3 goalDelta, f ); final Vec3 lookDirection = RotationUtils.calcLookDirectionFromRotation(rotation); - motion = step(motion, lookDirection, rotation.getPitch()); + motion = step(motion, lookDirection, rotation.getPitch(), context.gravity, context.slowFalling); delta = delta.subtract(motion); // Collision box while the player is in motion, with additional padding for safety @@ -1220,7 +1488,8 @@ private List simulate(final SolverContext context, final Vec3 goalDelta, f for (int y = ymin; y < ymax; y++) { for (int z = zmin; z < zmax; z++) { if (!this.passable(x, y, z, context.ignoreLava)) { - return null; + // displacement only covers the ticks before the impact + return stopOnImpact ? displacement : null; } } } @@ -1242,7 +1511,16 @@ private List simulate(final SolverContext context, final Vec3 goalDelta, f return displacement; } - private static Vec3 step(final Vec3 motion, final Vec3 lookDirection, final float pitch) { + /** + * A faithful reimplementation of {@code LivingEntity.updateFallFlyingMovement}. Every constant, guard and + * rounding step is kept identical to vanilla so that a simulated trajectory matches what the server will + * actually compute; any divergence here compounds over the simulated horizon and puts the bot into walls. + * + * @param gravity The player's {@code Attributes.GRAVITY} value, which is not always the 0.08 default + * @param slowFalling Whether the player has Slow Falling, which caps gravity while not ascending + */ + private static Vec3 step(final Vec3 motion, final Vec3 lookDirection, final float pitch, + final double gravity, final boolean slowFalling) { double motionX = motion.x; double motionY = motion.y; double motionZ = motion.z; @@ -1250,20 +1528,19 @@ private static Vec3 step(final Vec3 motion, final Vec3 lookDirection, final floa float pitchRadians = pitch * RotationUtils.DEG_TO_RAD_F; double pitchBase2 = Math.sqrt(lookDirection.x * lookDirection.x + lookDirection.z * lookDirection.z); double flatMotion = Math.sqrt(motionX * motionX + motionZ * motionZ); - double thisIsAlwaysOne = lookDirection.length(); - float pitchBase3 = Mth.cos(pitchRadians); - //System.out.println("always the same lol " + -pitchBase + " " + pitchBase3); - //System.out.println("always the same lol " + Math.abs(pitchBase3) + " " + pitchBase2); - //System.out.println("always 1 lol " + thisIsAlwaysOne); - pitchBase3 = (float) ((double) pitchBase3 * (double) pitchBase3 * Math.min(1, thisIsAlwaysOne / 0.4)); - motionY += -0.08 + (double) pitchBase3 * 0.06; + // vanilla uses the exact Math.cos here and keeps the square in double precision, unlike the Mth table + double pitchBase3 = Mth.square(Math.cos((double) pitchRadians)); + // LivingEntity.getEffectiveGravity + double effectiveGravity = slowFalling && motionY <= 0 ? Math.min(gravity, 0.01) : gravity; + + motionY += effectiveGravity * (-1.0 + pitchBase3 * 0.75); if (motionY < 0 && pitchBase2 > 0) { - double speedModifier = motionY * -0.1 * (double) pitchBase3; + double speedModifier = motionY * -0.1 * pitchBase3; motionY += speedModifier; motionX += lookDirection.x * speedModifier / pitchBase2; motionZ += lookDirection.z * speedModifier / pitchBase2; } - if (pitchRadians < 0) { // if you are looking down (below level) + if (pitchRadians < 0 && pitchBase2 > 0) { // if you are looking down (below level) double anotherSpeedModifier = flatMotion * (double) (-Mth.sin(pitchRadians)) * 0.04; motionY += anotherSpeedModifier * 3.2; motionX -= lookDirection.x * anotherSpeedModifier / pitchBase2; @@ -1273,10 +1550,9 @@ private static Vec3 step(final Vec3 motion, final Vec3 lookDirection, final floa motionX += (lookDirection.x / pitchBase2 * flatMotion - motionX) * 0.1; motionZ += (lookDirection.z / pitchBase2 * flatMotion - motionZ) * 0.1; } - motionX *= 0.99f; - motionY *= 0.98f; - motionZ *= 0.99f; - //System.out.println(motionX + " " + motionY + " " + motionZ); + motionX *= 0.9900000095367432; + motionY *= 0.9800000190734863; + motionZ *= 0.9900000095367432; return new Vec3(motionX, motionY, motionZ); } diff --git a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java index 3ed1e19155..4bb6d83fcd 100644 --- a/src/main/java/baritone/process/elytra/NetherPathfinderContext.java +++ b/src/main/java/baritone/process/elytra/NetherPathfinderContext.java @@ -40,6 +40,7 @@ import java.lang.ref.SoftReference; import java.lang.reflect.Field; import java.nio.file.Path; +import java.util.Arrays; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -63,7 +64,8 @@ public final class NetherPathfinderContext implements IElytraPathFinder { } } private static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState(); - private static final int SECTION_SIZE = 16; + // blocks per 16x16x16 section (replaces LevelChunkSection.SECTION_SIZE, removed from vanilla in 26.x) + private static final int SECTION_SIZE = 16 * 16 * 16; // 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 ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); @@ -73,6 +75,9 @@ public final class NetherPathfinderContext implements IElytraPathFinder { // Visible for access in BlockStateOctreeInterface final long context; + // Set under the write lock before freeContext; every native call checks it under the read lock + // so nothing can touch the context pointer after it has been freed. + private volatile boolean destroyed; private final long seed; // write locked operations private final ExecutorService writeExecutor = Executors.newSingleThreadExecutor(); @@ -98,11 +103,30 @@ public NetherPathfinderContext(long seed, Path cache, Level world) { } public boolean hasChunk(ChunkPos pos) { - return NetherPathfinder.hasChunkFromJava(this.context, pos.x(), pos.z()); + readLock.lock(); + try { + if (this.destroyed) { + return false; + } + return NetherPathfinder.hasChunkFromJava(this.context, pos.x(), pos.z()); + } finally { + readLock.unlock(); + } + } + + /** + * Submits to the write executor, dropping the task if the context is already being torn down. + */ + private void executeWrite(Runnable task) { + try { + this.writeExecutor.execute(task); + } catch (java.util.concurrent.RejectedExecutionException ignored) { + // destroy() has shut the executor down; the cache no longer matters + } } public void queueCacheCulling(int chunkX, int chunkZ, int maxDistanceBlocks) { - this.writeExecutor.execute(() -> { + this.executeWrite(() -> { writeLock.lock(); try { this.boi.chunkPtr = 0L; @@ -115,7 +139,7 @@ public void queueCacheCulling(int chunkX, int chunkZ, int maxDistanceBlocks) { public void queueForPacking(final LevelChunk chunkIn) { final SoftReference ref = new SoftReference<>(chunkIn); - this.writeExecutor.execute(() -> { + this.executeWrite(() -> { // 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(); @@ -134,7 +158,7 @@ public void queueForPacking(final LevelChunk chunkIn) { } public void queueBlockUpdate(BlockChangeEvent event) { - this.writeExecutor.execute(() -> { + this.executeWrite(() -> { ChunkPos chunkPos = event.getChunkPos(); // not inserting or deleting from the cache hashmap but it would still be bad for this function to race with itself writeLock.lock(); @@ -157,31 +181,48 @@ public CompletableFuture pathFindAsync(final BlockPos src, fina 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(() -> { - 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"); - } + // pathFind is always a writer, regardless of terrain prediction: it lazily parses baritone cache + // regions into chunkCache (gated only by checkedRegions, see nether-pathfinder parseBaritoneRegion) + // and generates terrain. It therefore has to exclude the getChunkOrDefault/raytrace lookups the + // solvers run under the read lock. Running it under the read lock instead lets those inserts (and the + // rehash they trigger) race a concurrent lookup's bucket walk, corrupting the map -> SIGSEGV in + // getChunkOrDefault. The prediction flag now only selects useAirIfChunkNotLoaded. + Lock l = writeLock; + ExecutorService exec = writeExecutor; + try { + return CompletableFuture.supplyAsync(() -> { + l.lock(); + try { + if (this.destroyed) { + throw new PathCalculationException("Pathfinder context destroyed"); + } + 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 + // The native cancel is a no-op (nether-pathfinder sets ctx->cancelFlag but the + // search loop polls an unrelated global), so this timeout is the only bound on + // how long destroy() can be stuck waiting for an in-flight search. Successful + // segments return ~500ms after first progress; only hopeless searches run it out. + 3000, // 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(); - } - }, exec); + return new UnpackedSegment(UnpackedSegment.from(segment).collect().stream().map(pos -> pos.above(minY)), segment.finished); + } finally { + l.unlock(); + } + }, exec); + } catch (java.util.concurrent.RejectedExecutionException ex) { + return CompletableFuture.failedFuture(new PathCalculationException("Pathfinder context destroyed")); + } } /** @@ -200,7 +241,15 @@ public boolean raytrace(final double startX, final double startY, final double s final double endX, final double endY, final double 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); + readLock.lock(); + try { + if (this.destroyed) { + return false; + } + return NetherPathfinder.isVisible(this.context, NetherPathfinder.CACHE_MISS_SOLID, startX, adjustedStartY, startZ, endX, adjustedEndY, endZ); + } finally { + readLock.unlock(); + } } /** @@ -214,7 +263,15 @@ public boolean raytrace(final double startX, final double startY, final double s public boolean raytrace(final Vec3 start, final Vec3 end) { 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); + readLock.lock(); + try { + if (this.destroyed) { + return false; + } + return NetherPathfinder.isVisible(this.context, NetherPathfinder.CACHE_MISS_SOLID, adjustedStart.x, adjustedStart.y, adjustedStart.z, adjustedEnd.x, adjustedEnd.y, adjustedEnd.z); + } finally { + readLock.unlock(); + } } public boolean raytrace(final int count, final double[] src, final double[] dst, final int visibility) { @@ -227,15 +284,23 @@ public boolean raytrace(final int count, final double[] src, final double[] dst, dst[i] -= this.minY; } - switch (visibility) { - case Visibility.ALL: - return NetherPathfinder.isVisibleMulti(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, false) == -1; - case Visibility.NONE: - return NetherPathfinder.isVisibleMulti(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, true) == -1; - case Visibility.ANY: - return NetherPathfinder.isVisibleMulti(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, true) != -1; - default: - throw new IllegalArgumentException("lol"); + readLock.lock(); + try { + if (this.destroyed) { + return false; + } + switch (visibility) { + case Visibility.ALL: + return NetherPathfinder.isVisibleMulti(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, false) == -1; + case Visibility.NONE: + return NetherPathfinder.isVisibleMulti(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, true) == -1; + case Visibility.ANY: + return NetherPathfinder.isVisibleMulti(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, true) != -1; + default: + throw new IllegalArgumentException("lol"); + } + } finally { + readLock.unlock(); } } @@ -249,10 +314,27 @@ public void raytrace(final int count, final double[] src, final double[] dst, fi dst[i] -= this.minY; } - NetherPathfinder.raytrace(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, hitsOut, hitPosOut); + readLock.lock(); + try { + if (this.destroyed) { + Arrays.fill(hitsOut, true); + return; + } + NetherPathfinder.raytrace(this.context, NetherPathfinder.CACHE_MISS_SOLID, count, src, dst, hitsOut, hitPosOut); + } finally { + readLock.unlock(); + } } + /** + * Callers already hold the read lock (see {@link ElytraBehavior#solveAngles}) - this is the + * solver's innermost loop and taking it again per block would cost more than the call itself. + * {@link #destroyed} is set under the write lock, so it cannot flip while a caller is inside. + */ public boolean passable(int x, int y, int z) { + if (this.destroyed) { + return false; + } return !this.boi.get0(x, y, z); } @@ -260,20 +342,48 @@ public void cancel() { NetherPathfinder.cancel(this.context); } + // Generous relative to the 3000ms native pathFind timeout, which does not bound findAir's BFS or the + // region-file IO a search can trigger. A normal teardown finishes well inside this; overshooting it + // means a search is genuinely wedged in native code and this context is abandoned rather than freed. + private static final long TEARDOWN_TIMEOUT_MS = 6000; + public void destroy() { this.cancel(); // Ignore anything that was queued up, just shutdown the executor this.readExecutor.shutdownNow(); this.writeExecutor.shutdownNow(); + boolean terminated; try { - while (!this.readExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS)) {} - while (!this.writeExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS)) {} + // The native cancel is a no-op, so shutdownNow only drops queued tasks; an in-flight pathFind + // runs to its own timeout. Bound the wait: a wedged native call must not pin this teardown + // forever, because that never releases npfSema and silently kills every later engage. + terminated = this.readExecutor.awaitTermination(TEARDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS) + & this.writeExecutor.awaitTermination(TEARDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); + terminated = false; + } + + if (!terminated) { + // A search is still running in native code. freeContext now would be a use-after-free, and the + // write lock is held by that same wedged search (pathFind is a writer), so taking it would + // deadlock. Abandon (leak) this context instead: mark it destroyed so nothing new calls in, and + // let the next engage build a fresh one. The leak is bounded to one context per wedged search. + this.destroyed = true; + return; } - NetherPathfinder.freeContext(this.context); + // The executors are drained, but the solver and render threads still raytrace through this + // context; the write lock keeps freeContext from pulling the memory out from under them. + writeLock.lock(); + try { + this.destroyed = true; + this.boi.chunkPtr = 0L; + NetherPathfinder.freeContext(this.context); + } finally { + writeLock.unlock(); + } } public long getSeed() { diff --git a/src/main/java/baritone/utils/BlockBreakHelper.java b/src/main/java/baritone/utils/BlockBreakHelper.java index 0c5cf6f000..2b1e10c8df 100644 --- a/src/main/java/baritone/utils/BlockBreakHelper.java +++ b/src/main/java/baritone/utils/BlockBreakHelper.java @@ -21,6 +21,7 @@ import baritone.api.utils.IPlayerContext; import baritone.utils.accessor.IPlayerControllerMP; import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.component.SwingAnimation; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; @@ -62,10 +63,10 @@ public void tick(boolean isLeftClick) { if (ctx.playerController().hasBrokenBlock()) { ctx.playerController().syncHeldItem(); ctx.playerController().clickBlock(((BlockHitResult) trace).getBlockPos(), ((BlockHitResult) trace).getDirection()); - ctx.player().swing(InteractionHand.MAIN_HAND); + ctx.player().swing(InteractionHand.MAIN_HAND, SwingAnimation.DEFAULT, false); } else { if (ctx.playerController().onPlayerDamageBlock(((BlockHitResult) trace).getBlockPos(), ((BlockHitResult) trace).getDirection())) { - ctx.player().swing(InteractionHand.MAIN_HAND); + ctx.player().swing(InteractionHand.MAIN_HAND, SwingAnimation.DEFAULT, false); } if (ctx.playerController().hasBrokenBlock()) { // block broken this tick // break delay timer only applies for multi-tick block breaks like vanilla diff --git a/src/main/java/baritone/utils/BlockPlaceHelper.java b/src/main/java/baritone/utils/BlockPlaceHelper.java index ca8d505239..950b59f56e 100644 --- a/src/main/java/baritone/utils/BlockPlaceHelper.java +++ b/src/main/java/baritone/utils/BlockPlaceHelper.java @@ -21,6 +21,7 @@ import baritone.api.utils.IPlayerContext; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.component.SwingAnimation; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; @@ -47,7 +48,7 @@ public void tick(boolean rightClickRequested) { rightClickTimer = Baritone.settings().rightClickSpeed.value - BASE_PLACE_DELAY; for (InteractionHand hand : InteractionHand.values()) { if (ctx.playerController().processRightClickBlock(ctx.player(), ctx.world(), hand, (BlockHitResult) mouseOver) == InteractionResult.SUCCESS) { - ctx.player().swing(hand); + ctx.player().swing(hand, SwingAnimation.DEFAULT, false); return; } if (!ctx.player().getItemInHand(hand).isEmpty() && ctx.playerController().processRightClick(ctx.player(), ctx.world(), hand) == InteractionResult.SUCCESS) { diff --git a/src/main/java/baritone/utils/BlockStateInterface.java b/src/main/java/baritone/utils/BlockStateInterface.java index 7a8420d8e2..afd4e5d756 100644 --- a/src/main/java/baritone/utils/BlockStateInterface.java +++ b/src/main/java/baritone/utils/BlockStateInterface.java @@ -53,6 +53,8 @@ public class BlockStateInterface { private final boolean useTheRealWorld; + public final boolean assumeWalkOnWater; + private static final BlockState AIR = Blocks.AIR.defaultBlockState(); public BlockStateInterface(IPlayerContext ctx) { @@ -69,6 +71,7 @@ public BlockStateInterface(IPlayerContext ctx, boolean copyLoadedChunks) { this.provider = (ClientChunkCache) world.getChunkSource(); } this.useTheRealWorld = !Baritone.settings().pathThroughCachedOnly.value; + this.assumeWalkOnWater = Baritone.settings().assumeWalkOnWater.value; if (!ctx.minecraft().isSameThread()) { throw new IllegalStateException("BlockStateInterface must be constructed on the main thread"); } diff --git a/src/main/java/baritone/utils/GuiClick.java b/src/main/java/baritone/utils/GuiClick.java index fb8410134c..369104a136 100644 --- a/src/main/java/baritone/utils/GuiClick.java +++ b/src/main/java/baritone/utils/GuiClick.java @@ -22,6 +22,7 @@ import baritone.api.pathing.goals.GoalBlock; import baritone.api.utils.BetterBlockPos; import baritone.api.utils.Helper; +import com.mojang.blaze3d.platform.InputConstants; import com.mojang.blaze3d.vertex.BufferBuilder; import com.mojang.blaze3d.vertex.PoseStack; import net.minecraft.ChatFormatting; @@ -92,7 +93,7 @@ public void extractBackground(GuiGraphicsExtractor grapics, int mouseX, int mous @Override public boolean mouseReleased(MouseButtonEvent event) { if (currentMouseOver != null) { //Catch this, or else a click into void will result in a crash - if (event.button() == 0) { + if (event.button() == InputConstants.MOUSE_BUTTON_LEFT) { if (clickStart != null && !clickStart.equals(currentMouseOver)) { BaritoneAPI.getProvider().getPrimaryBaritone().getSelectionManager().removeAllSelections(); BaritoneAPI.getProvider().getPrimaryBaritone().getSelectionManager().addSelection(BetterBlockPos.from(clickStart), BetterBlockPos.from(currentMouseOver)); @@ -107,7 +108,7 @@ public boolean mouseReleased(MouseButtonEvent event) { } else { BaritoneAPI.getProvider().getPrimaryBaritone().getCustomGoalProcess().setGoalAndPath(new GoalBlock(currentMouseOver)); } - } else if (event.button() == 1) { + } else if (event.button() == InputConstants.MOUSE_BUTTON_RIGHT) { BaritoneAPI.getProvider().getPrimaryBaritone().getCustomGoalProcess().setGoalAndPath(new GoalBlock(currentMouseOver.above())); } } diff --git a/src/main/java/baritone/utils/IRenderer.java b/src/main/java/baritone/utils/IRenderer.java index 4cf39bdac0..59d8005a17 100644 --- a/src/main/java/baritone/utils/IRenderer.java +++ b/src/main/java/baritone/utils/IRenderer.java @@ -22,17 +22,14 @@ import baritone.utils.accessor.IEntityRenderManager; import baritone.utils.accessor.IRenderPipelines; import baritone.utils.accessor.IRenderType; -import com.mojang.blaze3d.pipeline.BlendFunction; -import com.mojang.blaze3d.pipeline.ColorTargetState; -import com.mojang.blaze3d.pipeline.DepthStencilState; -import com.mojang.blaze3d.pipeline.RenderPipeline; -import com.mojang.blaze3d.platform.CompareOp; -import com.mojang.blaze3d.platform.BlendFactor; -import com.mojang.blaze3d.PrimitiveTopology; -import com.mojang.blaze3d.vertex.*; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.renderpearl.api.pipeline.*; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.client.renderer.BindGroupLayouts; +import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.client.renderer.StagedVertexBuffer; import net.minecraft.client.renderer.blockentity.BeaconRenderer; import net.minecraft.client.renderer.rendertype.RenderSetup; @@ -45,6 +42,8 @@ import net.minecraft.world.phys.Vec3; import java.awt.*; +import java.util.Optional; +import java.util.OptionalDouble; import java.util.function.BiFunction; public interface IRenderer { @@ -73,6 +72,7 @@ public interface IRenderer { RenderPipeline BEACON_BEAM_OPAQUE = ((IRenderPipelines) new RenderPipelines()).baritone$registerPipeline(RenderPipeline.builder(BARITONE_BEACON_BEAM_SNIPPET) .withLocation("pipeline/baritone_beacon_beam_opaque") + .withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT)) .withDepthStencilState(new DepthStencilState(CompareOp.ALWAYS_PASS, false)) .withCull(true) .build()); @@ -145,7 +145,17 @@ static void endBuffer(BufferBuilder bufferBuilder, RenderType renderType) { stagedVertexBuffer.upload(); StagedVertexBuffer.ExecuteInfo info = stagedVertexBuffer.getExecuteInfo(draw[0]); if (info != null) { - renderType.prepare().drawFromBuffer(info); + var preparedRenderType = renderType.prepare(); + var renderTarget = Minecraft.getInstance().gameRenderer.mainRenderTarget(); + try (var renderPass = RenderSystem.getDevice().createCommandEncoder().createRenderPass( + () -> "Baritone Render Pass", + renderTarget.getColorTextureView(), + Optional.empty(), + renderTarget.getDepthTextureView(), + OptionalDouble.empty()) + ) { + preparedRenderType.drawFromBuffer(info, renderPass); + } } stagedVertexBuffer.endFrame(); } diff --git a/src/main/java/baritone/utils/PathRenderer.java b/src/main/java/baritone/utils/PathRenderer.java index 6339303e05..3cc75fb7fc 100644 --- a/src/main/java/baritone/utils/PathRenderer.java +++ b/src/main/java/baritone/utils/PathRenderer.java @@ -359,7 +359,7 @@ private static void renderGoalXZBeaconLayer(PoseStack stack, double height, floa stack.translate(0.5D, 0.0D, 0.5D); if (!translucent) { stack.pushPose(); - stack.mulPose(Axis.YP.rotationDegrees(time * 2.25F - 45.0F)); + stack.rotateDegrees(Axis.YP, time * 2.25F - 45.0F); } float v0 = -1.0F + scroll; diff --git a/src/main/java/baritone/utils/ToolSet.java b/src/main/java/baritone/utils/ToolSet.java index eca79d90bf..5743aed3a9 100644 --- a/src/main/java/baritone/utils/ToolSet.java +++ b/src/main/java/baritone/utils/ToolSet.java @@ -35,9 +35,9 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; /** @@ -60,6 +60,14 @@ public class ToolSet { private final LocalPlayer player; + /** + * Snapshot of the hotbar taken on construction. The pathing thread must see one fixed cost function for + * the whole calculation, and it must not touch the live inventory off the main thread. + */ + private final ItemStack[] hotbar = new ItemStack[9]; + + private final int selectedSlot; + /** * Used for evaluating the material cost of a tool. * see {@link #getMaterialCost(ItemStack)} @@ -75,8 +83,12 @@ public class ToolSet { ); public ToolSet(LocalPlayer player) { - breakStrengthCache = new HashMap<>(); + breakStrengthCache = new ConcurrentHashMap<>(); this.player = player; + for (int i = 0; i < 9; i++) { + hotbar[i] = player.getInventory().getItem(i); + } + this.selectedSlot = player.getInventory().getSelectedSlot(); if (Baritone.settings().considerPotionEffects.value) { double amplifier = potionAmplifier(); @@ -143,7 +155,7 @@ public int getBestSlot(Block b, boolean preferSilkTouch, boolean pathingCalculat possible, this lets us make pathing depend on the actual tool to be used (if auto tool is disabled) */ if (!Baritone.settings().autoTool.value && pathingCalculation) { - return player.getInventory().getSelectedSlot(); + return selectedSlot; } int best = 0; @@ -152,7 +164,7 @@ possible, this lets us make pathing depend on the actual tool to be used (if aut boolean bestSilkTouch = false; BlockState blockState = b.defaultBlockState(); for (int i = 0; i < 9; i++) { - ItemStack itemStack = player.getInventory().getItem(i); + ItemStack itemStack = hotbar[i]; if (!Baritone.settings().useSwordToMine.value && itemStack.is(ItemTags.SWORDS)) { continue; } @@ -188,7 +200,7 @@ possible, this lets us make pathing depend on the actual tool to be used (if aut * @return A double containing the destruction ticks with the best tool */ private double getBestDestructionTime(Block b) { - ItemStack stack = player.getInventory().getItem(getBestSlot(b, false, true)); + ItemStack stack = hotbar[getBestSlot(b, false, true)]; return calculateSpeedVsBlock(stack, b.defaultBlockState()) * avoidanceMultiplier(b); } diff --git a/src/main/java/baritone/utils/accessor/IRenderPipelines.java b/src/main/java/baritone/utils/accessor/IRenderPipelines.java index 35ebca33b9..6ef174f140 100644 --- a/src/main/java/baritone/utils/accessor/IRenderPipelines.java +++ b/src/main/java/baritone/utils/accessor/IRenderPipelines.java @@ -17,7 +17,7 @@ package baritone.utils.accessor; -import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.renderpearl.api.pipeline.RenderPipeline; public interface IRenderPipelines { RenderPipeline.Snippet getLinesSnippet(); diff --git a/src/main/java/baritone/utils/pathing/Avoidance.java b/src/main/java/baritone/utils/pathing/Avoidance.java index dcaf6e8a57..5e5f171cb5 100644 --- a/src/main/java/baritone/utils/pathing/Avoidance.java +++ b/src/main/java/baritone/utils/pathing/Avoidance.java @@ -23,7 +23,7 @@ import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap; import net.minecraft.core.BlockPos; import net.minecraft.world.entity.Mob; -import net.minecraft.world.entity.monster.EnderMan; +import net.minecraft.world.entity.monster.Enderman; import net.minecraft.world.entity.monster.spider.Spider; import net.minecraft.world.entity.monster.zombie.ZombifiedPiglin; @@ -76,7 +76,7 @@ public static List create(IPlayerContext ctx) { .filter(entity -> entity instanceof Mob) .filter(entity -> (!(entity instanceof Spider)) || ctx.player().getLightLevelDependentMagicValue() < 0.5) .filter(entity -> !(entity instanceof ZombifiedPiglin) || ((ZombifiedPiglin) entity).getLastHurtByMob() != null) - .filter(entity -> !(entity instanceof EnderMan) || ((EnderMan) entity).isCreepy()) + .filter(entity -> !(entity instanceof Enderman) || ((Enderman) entity).isCreepy()) .forEach(entity -> res.add(new Avoidance(entity.blockPosition(), mobCoeff, Baritone.settings().mobAvoidanceRadius.value))); } return res; diff --git a/src/main/java/baritone/utils/pathing/Favoring.java b/src/main/java/baritone/utils/pathing/Favoring.java index bbc23c7ff5..f4e62cff1d 100644 --- a/src/main/java/baritone/utils/pathing/Favoring.java +++ b/src/main/java/baritone/utils/pathing/Favoring.java @@ -39,7 +39,7 @@ public Favoring(IPlayerContext ctx, IPath previous, CalculationContext context) public Favoring(IPath previous, CalculationContext context) { // create one just from previous path, no mob avoidances favorings = new Long2DoubleOpenHashMap(); favorings.defaultReturnValue(1.0D); - double coeff = context.backtrackCostFavoringCoefficient; + double coeff = Math.max(context.backtrackCostFavoringCoefficient, 0.01); // a coefficient of 0 or below would make actionCost non-positive, which A* rejects if (coeff != 1D && previous != null) { previous.positions().forEach(pos -> favorings.put(BetterBlockPos.longHash(pos), coeff)); } diff --git a/src/main/java/baritone/utils/schematic/litematica/LitematicaHelper.java b/src/main/java/baritone/utils/schematic/litematica/LitematicaHelper.java index a4b806f15b..b8e1e69813 100644 --- a/src/main/java/baritone/utils/schematic/litematica/LitematicaHelper.java +++ b/src/main/java/baritone/utils/schematic/litematica/LitematicaHelper.java @@ -26,7 +26,6 @@ import fi.dy.masa.litematica.schematic.placement.SchematicPlacement; import fi.dy.masa.litematica.schematic.placement.SubRegionPlacement; import fi.dy.masa.litematica.world.SchematicWorldHandler; -import fi.dy.masa.litematica.world.WorldSchematic; import net.minecraft.core.BlockPos; import net.minecraft.core.Vec3i; import net.minecraft.world.level.Level; @@ -37,6 +36,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.function.Supplier; /** * Helper class that provides access or processes data related to Litmatica schematics. @@ -99,7 +99,11 @@ public static Pair getSchematic(int i) { int minY = Integer.MAX_VALUE; int minZ = Integer.MAX_VALUE; HashMap subRegions = new HashMap<>(); - Level schematicWorld = SchematicWorldHandler.getSchematicWorld(); + + // doing this the normal way class loaded WorldSchematic too early + Supplier supplier = SchematicWorldHandler::getSchematicWorld; + Level schematicWorld = supplier.get(); + for (Map.Entry entry : placement.getEnabledRelativeSubRegionPlacements().entrySet()) { SubRegionPlacement subPlacement = entry.getValue(); Vec3i pos = transform(subPlacement.getPos(), placement.getMirror(), placement.getRotation()); diff --git a/src/test/java/baritone/cache/CacheFilesTest.java b/src/test/java/baritone/cache/CacheFilesTest.java new file mode 100644 index 0000000000..201f5f230f --- /dev/null +++ b/src/test/java/baritone/cache/CacheFilesTest.java @@ -0,0 +1,82 @@ +/* + * 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.cache; + +import baritone.api.cache.IWaypoint; +import baritone.api.cache.Waypoint; +import baritone.api.utils.BetterBlockPos; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; +import java.util.stream.Stream; + +import static org.junit.Assert.*; + +public class CacheFilesTest { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void moveIntoPlaceReplacesExistingFile() throws Exception { + Path target = folder.getRoot().toPath().resolve("r.0.0.bcr"); + Path temp = folder.getRoot().toPath().resolve("r.0.0.bcr.tmp"); + Files.write(target, "old".getBytes(StandardCharsets.UTF_8)); + Files.write(temp, "new".getBytes(StandardCharsets.UTF_8)); + + CacheFiles.moveIntoPlace(temp, target); + + assertEquals("new", new String(Files.readAllBytes(target), StandardCharsets.UTF_8)); + assertFalse(Files.exists(temp)); + } + + @Test + public void moveIntoPlaceCreatesMissingFile() throws Exception { + Path target = folder.getRoot().toPath().resolve("home.mp4"); + Path temp = folder.getRoot().toPath().resolve("home.mp4.tmp"); + Files.write(temp, "new".getBytes(StandardCharsets.UTF_8)); + + CacheFiles.moveIntoPlace(temp, target); + + assertEquals("new", new String(Files.readAllBytes(target), StandardCharsets.UTF_8)); + assertFalse(Files.exists(temp)); + } + + @Test + public void waypointsSurviveSaveAndLoad() throws Exception { + Path dir = folder.getRoot().toPath().resolve("waypoints"); + Waypoint home = new Waypoint("base", IWaypoint.Tag.HOME, new BetterBlockPos(1, 64, -2), 1234L); + + new WaypointCollection(dir).addWaypoint(home); + Set loaded = new WaypointCollection(dir).getByTag(IWaypoint.Tag.HOME); + + assertEquals(1, loaded.size()); + IWaypoint waypoint = loaded.iterator().next(); + assertEquals("base", waypoint.getName()); + assertEquals(new BetterBlockPos(1, 64, -2), waypoint.getLocation()); + assertEquals(1234L, waypoint.getCreationTimestamp()); + try (Stream files = Files.list(dir)) { + assertTrue("temporary file left behind", files.noneMatch(p -> p.toString().endsWith(".tmp"))); + } + } +} diff --git a/src/test/java/baritone/cache/RegionRetentionPolicyTest.java b/src/test/java/baritone/cache/RegionRetentionPolicyTest.java new file mode 100644 index 0000000000..58b6584cc2 --- /dev/null +++ b/src/test/java/baritone/cache/RegionRetentionPolicyTest.java @@ -0,0 +1,92 @@ +/* + * 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.cache; + +import baritone.cache.RegionRetentionPolicy.Candidate; +import it.unimi.dsi.fastutil.longs.LongList; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class RegionRetentionPolicyTest { + + private static final long MB = 1024 * 1024; + private static final long NOW = 1_000_000_000L; + private static final long IDLE = 180_000; + + @Test + public void dirtyRegionsAreNeverSelected() { + List regions = List.of( + new Candidate(1, NOW - 10 * IDLE, true, 27 * MB), + new Candidate(2, NOW - 10 * IDLE, true, 27 * MB)); + assertTrue(RegionRetentionPolicy.select(regions, 0, IDLE, NOW, false).isEmpty()); + assertTrue(RegionRetentionPolicy.select(regions, 0, IDLE, NOW, true).isEmpty()); + } + + @Test + public void idleCleanRegionsGoRegardlessOfBudget() { + List regions = List.of( + new Candidate(1, NOW - IDLE - 1, false, 27 * MB), + new Candidate(2, NOW - 1_000, false, 27 * MB)); + LongList victims = RegionRetentionPolicy.select(regions, 1024 * MB, IDLE, NOW, false); + assertEquals(1, victims.size()); + assertEquals(1, victims.getLong(0)); + } + + @Test + public void overBudgetEvictsLeastRecentlyUsedFirstUntilUnderBudget() { + List regions = List.of( + new Candidate(1, NOW - 60_000, false, 27 * MB), + new Candidate(2, NOW - 30_000, false, 27 * MB), + new Candidate(3, NOW - 90_000, false, 27 * MB), + new Candidate(4, NOW - 5_000, false, 27 * MB)); + LongList victims = RegionRetentionPolicy.select(regions, 60 * MB, IDLE, NOW, false); + assertEquals(List.of(3L, 1L), victims); + } + + @Test + public void regionsTouchedWithinGraceStayEvenWhenOverBudget() { + List regions = List.of( + new Candidate(1, NOW - 500, false, 27 * MB), + new Candidate(2, NOW - 100, false, 27 * MB)); + assertTrue(RegionRetentionPolicy.select(regions, 0, IDLE, NOW, false).isEmpty()); + } + + @Test + public void dirtyBytesStillCountAgainstTheBudget() { + List regions = List.of( + new Candidate(1, NOW - 10_000, true, 100 * MB), + new Candidate(2, NOW - 10_000, false, 27 * MB)); + LongList victims = RegionRetentionPolicy.select(regions, 110 * MB, IDLE, NOW, false); + assertEquals(List.of(2L), victims); + assertFalse(victims.contains(1L)); + } + + @Test + public void everythingSelectsEveryCleanRegion() { + List regions = List.of( + new Candidate(1, NOW, false, 27 * MB), + new Candidate(2, NOW, true, 27 * MB), + new Candidate(3, NOW - 1, false, 27 * MB)); + assertEquals(List.of(1L, 3L), RegionRetentionPolicy.select(regions, 1024 * MB, IDLE, NOW, true)); + } +} diff --git a/src/test/java/baritone/cache/WorldProviderFolderNameTest.java b/src/test/java/baritone/cache/WorldProviderFolderNameTest.java new file mode 100644 index 0000000000..56a7e7511c --- /dev/null +++ b/src/test/java/baritone/cache/WorldProviderFolderNameTest.java @@ -0,0 +1,66 @@ +/* + * 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.cache; + +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertEquals; + +public class WorldProviderFolderNameTest { + + @Test + public void hostnamesAreUntouched() { + assertEquals("2b2t.org", WorldProvider.sanitizeFileName("2b2t.org")); + assertEquals("realms", WorldProvider.sanitizeFileName("realms")); + } + + @Test + public void directIpMatchesWhatWindowsAlwaysProduced() { + assertEquals("217.77.15.230_25571", WorldProvider.sanitizeFileName("217.77.15.230:25571")); + } + + @Test + public void everyWindowsReservedCharacterIsReplaced() { + assertEquals("a_b_c_d_e_f_g_h_i", WorldProvider.sanitizeFileName("a\\b/c:d*e?f\"gi")); + assertEquals("x_y", WorldProvider.sanitizeFileName("x|y")); + assertEquals("tab_", WorldProvider.sanitizeFileName("tab\t")); + } + + @Test + public void trailingDotsAndSpacesAreStripped() { + assertEquals("host_1", WorldProvider.sanitizeFileName("host:1. ")); + assertEquals("_", WorldProvider.sanitizeFileName("...")); + } + + @Test + public void existingRawDirectoryKeepsBeingUsed() throws IOException { + Path dir = Files.createTempDirectory("baritone-folder-test"); + try { + assertEquals("host_25565", WorldProvider.serverFolderName(dir, "host:25565")); + Files.createDirectory(dir.resolve("host:25565")); + assertEquals("host:25565", WorldProvider.serverFolderName(dir, "host:25565")); + } finally { + Files.deleteIfExists(dir.resolve("host:25565")); + Files.delete(dir); + } + } +} diff --git a/src/test/java/baritone/pathing/goals/GoalDistanceTest.java b/src/test/java/baritone/pathing/goals/GoalDistanceTest.java new file mode 100644 index 0000000000..00ba3013a2 --- /dev/null +++ b/src/test/java/baritone/pathing/goals/GoalDistanceTest.java @@ -0,0 +1,68 @@ +package baritone.pathing.goals; + +import baritone.api.pathing.goals.GoalNear; +import baritone.api.pathing.goals.GoalRunAway; +import net.minecraft.core.BlockPos; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class GoalDistanceTest { + + @Test + public void nearDoesNotAcceptDistantPositionsAfterIntegerOverflow() { + GoalNear goal = new GoalNear(new BlockPos(0, 64, 0), 2); + assertTrue(goal.isInGoal(2, 64, 0)); + assertFalse(goal.isInGoal(3, 64, 0)); + for (int distance : new int[]{50_000, 65_536, 30_000_000}) { + assertFalse(goal.isInGoal(distance, 64, 0)); + assertFalse(goal.isInGoal(0, 64, -distance)); + } + } + + @Test + public void nearSupportsRadiiWhoseSquaresExceedAnInteger() { + GoalNear goal = new GoalNear(new BlockPos(-1_000_000, 64, 1_000_000), 50_000); + assertTrue(goal.isInGoal(-1_000_000, 64, 1_000_000)); + assertTrue(goal.isInGoal(-950_000, 64, 1_000_000)); + assertFalse(goal.isInGoal(-949_999, 64, 1_000_000)); + } + + @Test + public void runawayDistanceWorksAcrossTheWorld() { + GoalRunAway goal = new GoalRunAway(100, new BlockPos(0, 64, 0)); + assertFalse(goal.isInGoal(99, 64, 0)); + assertTrue(goal.isInGoal(100, 64, 0)); + assertTrue(goal.isInGoal(50_000, 64, 0)); + assertTrue(goal.isInGoal(0, 64, -65_536)); + assertTrue(new GoalRunAway(100, new BlockPos(-30_000_000, 64, 0)) + .isInGoal(30_000_000, 64, 0)); + } + + @Test + public void runawayPreservesLargeAndFractionalRequestedDistances() { + GoalRunAway goal = new GoalRunAway(100_000, new BlockPos(0, 64, 0)); + assertFalse(goal.isInGoal(99_999, 64, 0)); + assertTrue(goal.isInGoal(100_000, 64, 0)); + assertFalse(new GoalRunAway(1.1, new BlockPos(0, 64, 0)).isInGoal(1, 64, 0)); + } + + @Test + public void runawayStillChecksEveryOriginAndRequestedHeight() { + GoalRunAway goal = new GoalRunAway(10, Integer.valueOf(70), + new BlockPos(0, 64, 0), new BlockPos(100, 64, 0)); + assertFalse(goal.isInGoal(95, 70, 0)); + assertFalse(goal.isInGoal(50, 69, 0)); + assertTrue(goal.isInGoal(50, 70, 0)); + } + + @Test + public void equalRunawayGoalsWithoutHeightHaveEqualHashes() { + GoalRunAway first = new GoalRunAway(1.1, new BlockPos(0, 64, 0)); + GoalRunAway second = new GoalRunAway(1.1, new BlockPos(0, 64, 0)); + assertEquals(first, second); + assertEquals(first.hashCode(), second.hashCode()); + } +} diff --git a/src/test/java/baritone/pathing/movement/ActionCostsTest.java b/src/test/java/baritone/pathing/movement/ActionCostsTest.java index a6d7e3e720..4cae886678 100644 --- a/src/test/java/baritone/pathing/movement/ActionCostsTest.java +++ b/src/test/java/baritone/pathing/movement/ActionCostsTest.java @@ -31,8 +31,8 @@ public void testFallNBlocksCost() { double blocks = ticksToBlocks(FALL_N_BLOCKS_COST[i]); assertEquals(blocks, i, 0.00000000001); // If you add another 0 the test fails at i=989 LOL } - assertEquals(FALL_1_25_BLOCKS_COST, 6.2344, 0.00001); - assertEquals(FALL_0_25_BLOCKS_COST, 3.0710, 0.00001); + assertEquals(FALL_1_25_BLOCKS_COST, 5.2344, 0.00001); + assertEquals(FALL_0_25_BLOCKS_COST, 2.0710, 0.00001); assertEquals(JUMP_ONE_BLOCK_COST, 3.1634, 0.00001); } @@ -40,10 +40,10 @@ public double ticksToBlocks(double ticks) { double fallDistance = 0; int integralComponent = (int) Math.floor(ticks); for (int tick = 0; tick < integralComponent; tick++) { - fallDistance += velocity(tick); + fallDistance += velocity(tick + 1); } double partialTickComponent = ticks - Math.floor(ticks); - double finalPartialTickVelocity = velocity(integralComponent); + double finalPartialTickVelocity = velocity(integralComponent + 1); double finalPartialTickDistance = finalPartialTickVelocity * partialTickComponent; fallDistance += finalPartialTickDistance; return fallDistance;