diff --git a/.gitignore b/.gitignore
index 6974c5d4f..189596499 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,8 @@ bin/
/run/
/common/run/
/fabric/run/
+/fabric/config/
+/fabric/logs/
/neoforge/run/
/neoforge/runs/
diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/CheckAndCacheBlockChecker.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/CheckAndCacheBlockChecker.java
new file mode 100644
index 000000000..051e47fda
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/CheckAndCacheBlockChecker.java
@@ -0,0 +1,143 @@
+package net.caffeinemc.mods.lithium.common.ai.non_poi_block_search;
+
+import net.caffeinemc.mods.lithium.common.util.collections.FixedChunkAccessSectionBitBuffer;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.SectionPos;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.LevelReader;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunkSection;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * This is intended to be used to optimize Non-POI block searches by pre-checking the ChunkSections, getting the
+ * ChunkAccesses which then allows for returning early or only calling getBlockState in possible ChunkSections.
+ *
+ * Note: Please correctly specify shouldChunkLoad based on whether the getBlockState in vanilla can chunk load or not.
+ * The default in game is that it can. Setting it to true will mean that the search will assume that unloaded chunks
+ * may have the target and will chunk load then search them if and when it reaches it.
+ *
+ * @author jcw780
+ */
+public class CheckAndCacheBlockChecker {
+ private final FixedChunkAccessSectionBitBuffer chunkSections2MaybeContainsMatchingBlock;
+ private final LevelReader levelReader;
+ public final boolean shouldChunkLoad;
+ public final Predicate blockStatePredicate;
+ private int unloadedPossibleChunkSections = 0;
+ public final int minSectionY;
+
+ public CheckAndCacheBlockChecker(BlockPos origin, int horizontalRangeInclusive, int verticalRangeInclusive, LevelReader levelReader,
+ Predicate blockStatePredicate, boolean shouldChunkLoad) {
+ this.chunkSections2MaybeContainsMatchingBlock = new FixedChunkAccessSectionBitBuffer(origin, horizontalRangeInclusive, verticalRangeInclusive);
+ this.levelReader = levelReader;
+ this.shouldChunkLoad = shouldChunkLoad;
+ this.blockStatePredicate = blockStatePredicate;
+ this.minSectionY = levelReader.getMinSection();
+ }
+
+ public void initializeChunks() {
+ this.initializeChunks(null);
+ }
+
+ public void initializeChunks(Consumer chunkCollector) {
+ final boolean nullChunkCollector = chunkCollector == null;
+ for (long chunkPos : this.chunkSections2MaybeContainsMatchingBlock.getChunkPosInRange()) {
+ int x = ChunkPos.getX(chunkPos);
+ int z = ChunkPos.getZ(chunkPos);
+ boolean chunkMaybeHas = false;
+
+ //Never load chunks in the first pass to avoid observably altering chunk loading behavior
+ //Otherwise full region will be loaded vs partial region if the search finds the block early.
+ ChunkAccess chunkAccess = levelReader.getChunk(x, z, ChunkStatus.FULL, false);
+ if (chunkAccess != null) {
+ this.chunkSections2MaybeContainsMatchingBlock.setChunkAccess(chunkPos, chunkAccess);
+ for (int y : this.chunkSections2MaybeContainsMatchingBlock.getSectionYInRange()) {
+ chunkMaybeHas = this.checkChunkSection(chunkAccess, x, y, z) || chunkMaybeHas;
+ }
+ } else if (this.shouldChunkLoad) {
+ /* If the search may chunk load then it is possible that target blocks may be revealed when the search
+ * reaches it. Since we cannot load the chunks and check now, we cannot definitively exclude subchunks
+ * inside the chunk. This means that we must flag subchunks that are within build limit - otherwise air
+ * anyway - for the search.
+ */
+ for (int y : this.chunkSections2MaybeContainsMatchingBlock.getSectionYInRange()) {
+ this.chunkSections2MaybeContainsMatchingBlock.setChunkSectionStatus(SectionPos.asLong(x, y, z),
+ !levelReader.isOutsideBuildHeight(SectionPos.sectionToBlockCoord(y)));
+ ++this.unloadedPossibleChunkSections;
+ }
+ chunkMaybeHas = true;
+
+ }
+
+ if (!nullChunkCollector && chunkMaybeHas) {
+ chunkCollector.accept(chunkPos);
+ }
+ }
+ }
+
+ public int getChunkSize(){
+ return this.chunkSections2MaybeContainsMatchingBlock.numChunks;
+ }
+
+ public boolean hasUnloadedPossibleChunks(){
+ return this.unloadedPossibleChunkSections > 0;
+ }
+
+ private boolean checkChunkSection(ChunkAccess chunkAccess, int chunkX, int chunkY, int chunkZ) {
+ final int chunkSectionYIndex = chunkY - this.minSectionY;
+ LevelChunkSection[] chunkSections = chunkAccess.getSections();
+ if (chunkSectionYIndex >= 0
+ && chunkSectionYIndex < chunkSections.length
+ && chunkSections[chunkSectionYIndex].maybeHas(blockStatePredicate)) {
+ this.chunkSections2MaybeContainsMatchingBlock.setChunkSectionStatus(
+ SectionPos.asLong(chunkX, chunkY, chunkZ), true);
+ return true;
+ }
+ return false;
+ }
+
+ public boolean checkCachedSection(int chunkX, int chunkY, int chunkZ) {
+ return this.chunkSections2MaybeContainsMatchingBlock.getChunkSectionBit(chunkX, chunkY, chunkZ);
+ }
+
+ public ChunkAccess getCachedChunkAccess(long chunkPos) {
+ return this.chunkSections2MaybeContainsMatchingBlock.getChunkAccess(chunkPos);
+ }
+
+ public ChunkAccess getCachedChunkAccess(BlockPos blockPos) {
+ return this.chunkSections2MaybeContainsMatchingBlock.getChunkAccess(blockPos);
+ }
+
+ public boolean shouldStop(){
+ return this.chunkSections2MaybeContainsMatchingBlock.hasNoTrueChunkSections();
+ }
+
+ public boolean checkPosition(BlockPos blockPos) {
+ if(!this.chunkSections2MaybeContainsMatchingBlock.getChunkSectionBit(blockPos)) return false;
+ ChunkAccess chunkAccess = this.chunkSections2MaybeContainsMatchingBlock.getChunkAccess(blockPos);
+ if(chunkAccess == null) {
+ if (!this.shouldChunkLoad) {
+ return false;
+ }
+
+ final int chunkX = SectionPos.blockToSectionCoord(blockPos.getX());
+ final int chunkY = SectionPos.blockToSectionCoord(blockPos.getY());
+ final int chunkZ = SectionPos.blockToSectionCoord(blockPos.getZ());
+ chunkAccess = levelReader.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true);
+ //this chunkAccess cannot be null and reach here because it should throw earlier
+ assert chunkAccess != null;
+ this.chunkSections2MaybeContainsMatchingBlock.setChunkAccess(blockPos, chunkAccess);
+ if (!checkChunkSection(chunkAccess, chunkX, chunkY, chunkZ)) {
+ --this.unloadedPossibleChunkSections;
+ return false;
+ }
+ }
+
+ return blockStatePredicate.test(chunkAccess.getBlockState(blockPos));
+ }
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/CommonBlockSearchesCheckAndCache.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/CommonBlockSearchesCheckAndCache.java
new file mode 100644
index 000000000..07c9ab1fa
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/CommonBlockSearchesCheckAndCache.java
@@ -0,0 +1,32 @@
+package net.caffeinemc.mods.lithium.common.ai.non_poi_block_search;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.level.LevelReader;
+import net.minecraft.world.level.block.state.BlockState;
+
+import java.util.Optional;
+import java.util.function.Predicate;
+
+/**
+ * Uses CheckAndCacheBlockChecker to improve common block searches
+ */
+public class CommonBlockSearchesCheckAndCache {
+ /**
+ * Optimizes BlockPos::findClosestMatch
+ * [Vanilla Copy] search order and chunk-loading - even though the latter is unlikely to be observable in vanilla.
+ */
+ public static Optional blockPosFindClosestMatch(LevelReader levelReader, LivingEntity livingEntity,
+ int horizontalRange, int verticalRange,
+ Predicate blockStatePredicate,
+ boolean shouldChunkLoad){
+ BlockPos mobPos = livingEntity.blockPosition();
+ CheckAndCacheBlockChecker checker = new CheckAndCacheBlockChecker(
+ mobPos, horizontalRange, verticalRange, levelReader, blockStatePredicate, shouldChunkLoad);
+ checker.initializeChunks();
+ if(checker.shouldStop()) {
+ return Optional.empty();
+ }
+ return BlockPos.findClosestMatch(mobPos, horizontalRange, verticalRange, checker::checkPosition);
+ }
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/LithiumMoveToBlockGoal.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/LithiumMoveToBlockGoal.java
new file mode 100644
index 000000000..4e88ff730
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/LithiumMoveToBlockGoal.java
@@ -0,0 +1,14 @@
+package net.caffeinemc.mods.lithium.common.ai.non_poi_block_search;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.ChunkAccess;
+
+import java.util.function.BiPredicate;
+import java.util.function.Predicate;
+
+public interface LithiumMoveToBlockGoal {
+ boolean lithium$findNearestBlock(Predicate requiredBlock,
+ BiPredicate lithium$isValidTarget,
+ final boolean shouldChunkLoad);
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/NonPOISearchDistances.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/NonPOISearchDistances.java
new file mode 100644
index 000000000..7ed25155b
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/NonPOISearchDistances.java
@@ -0,0 +1,54 @@
+package net.caffeinemc.mods.lithium.common.ai.non_poi_block_search;
+
+import net.caffeinemc.mods.lithium.common.util.Distances;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.ChunkPos;
+
+public class NonPOISearchDistances {
+ public static class MoveToBlockGoalDistances {
+ public static int getMinimumSortOrderOfChunk(BlockPos center, final long chunkPos) {
+ return getMinimumSortOrderOfChunk(center, ChunkPos.getX(chunkPos), ChunkPos.getZ(chunkPos));
+ }
+
+ public static int getMinimumSortOrderOfChunk(BlockPos center, final int chunkX, final int chunkZ) {
+ final long closest = Distances.getClosestPositionWithinChunk(center, chunkX, chunkZ);
+
+ final int dX = BlockPos.getX(closest) - center.getX();
+ final int dZ = BlockPos.getZ(closest) - center.getZ();
+
+ //This will always get the closest one due to the nature of the search
+ return getVanillaSortOrderInt(getRing(dX, dZ), dX, dZ);
+ }
+
+ public static int getRing(final int dX, final int dZ){
+ return Math.max(Math.abs(dX), Math.abs(dZ));
+ }
+
+ /**
+ * Sort order function for 1 layer of MoveToBlockGoal findNearestBlock
+ * This is equivalent to:
+ * int withinRingX = Math.abs(dX) * 2 - (dX > 0 ? 1 : 0);
+ * int withinRingZ = Math.abs(dZ) * 2 - (dZ > 0 ? 1 : 0);
+ * return ring << 16 | withinRingX << 8 | withinRingZ;
+ *
+ * This works because the search prioritizes in order of:
+ * 1. The distance of y from the center - Not used
+ * 2. Whether y is - or + (+ is closer) - Not used
+ * 3. The square ring that the block is in (outer is further)
+ * 4. The distance of x from the center
+ * 5. Whether x is - or + (+ is closer)
+ * 6. The distance of z from the center
+ * 7. Whether z is - or + (+ is closer)
+ *
+ * Note: The bit-packing only works for horizontal search ranges of <=128.
+ * You can convert to longs if you somehow exceed that, but also seriously consider POIs instead.
+ *
+ * @param ring Which square ring the block is at relative to the center
+ * @param dX Relative x position of the block to the center
+ * @param dZ Relative z position of the block to the center
+ */
+ public static int getVanillaSortOrderInt(final int ring, final int dX, final int dZ) {
+ return (ring << 16 | Math.abs(dX) << 9 | Math.abs(dZ) << 1) - ((dX > 0 ? 1 : 0) << 8 | (dZ > 0 ? 1 : 0));
+ }
+ }
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/util/Distances.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/util/Distances.java
index d548a4775..ee5b0a852 100644
--- a/common/src/main/java/net/caffeinemc/mods/lithium/common/util/Distances.java
+++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/util/Distances.java
@@ -29,4 +29,15 @@ public static boolean isWithinSquareRadius(BlockPos origin, int radius, BlockPos
public static boolean isWithinCircleRadius(BlockPos origin, double radiusSq, BlockPos pos) {
return origin.distSqr(pos) <= radiusSq;
}
+
+ public static int getClosestAlongSectionAxis(int originAxis, int chunkAxis){
+ final int chunkMinAxis = SectionPos.sectionToBlockCoord(chunkAxis);
+ return Math.min(Math.max(originAxis, chunkMinAxis), chunkMinAxis+15);
+ }
+
+ public static long getClosestPositionWithinChunk(BlockPos origin, int chunkX, int chunkZ){
+ return BlockPos.asLong(getClosestAlongSectionAxis(origin.getX(), chunkX),
+ origin.getY(), getClosestAlongSectionAxis(origin.getZ(), chunkZ));
+
+ }
}
diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/util/collections/FixedChunkAccessSectionBitBuffer.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/util/collections/FixedChunkAccessSectionBitBuffer.java
new file mode 100644
index 000000000..5379bf9a9
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/util/collections/FixedChunkAccessSectionBitBuffer.java
@@ -0,0 +1,173 @@
+package net.caffeinemc.mods.lithium.common.util.collections;
+
+import it.unimi.dsi.fastutil.ints.IntIterable;
+import it.unimi.dsi.fastutil.ints.IntIterator;
+import it.unimi.dsi.fastutil.longs.LongIterable;
+import it.unimi.dsi.fastutil.longs.LongIterator;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.SectionPos;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Collections;
+
+public class FixedChunkAccessSectionBitBuffer {
+ public final int xMin, yMin, zMin;
+ public final int xLength, yLength, zLength, numChunks, numSections;
+
+ public final BitSet chunkSectionBits;
+ public final ArrayList chunkAccesses;
+
+ public FixedChunkAccessSectionBitBuffer(int x0, int x1, int y0, int y1, int z0, int z1) {
+ this.xMin = Math.min(x0, x1);
+ this.yMin = Math.min(y0, y1);
+ this.zMin = Math.min(z0, z1);
+
+ this.xLength = Math.max(x0, x1) - this.xMin + 1;
+ this.yLength = Math.max(y0, y1) - this.yMin + 1;
+ this.zLength = Math.max(z0, z1) - this.zMin + 1;
+
+ this.numChunks = xLength * zLength;
+ this.numSections = yLength * xLength * zLength;
+
+ this.chunkSectionBits = new BitSet(numSections);
+ this.chunkAccesses = new ArrayList<>(Collections.nCopies(xLength * zLength,null));
+ }
+
+ public FixedChunkAccessSectionBitBuffer(BlockPos center, int horizontalRangeInclusive, int verticalRangeInclusive) {
+ this(SectionPos.blockToSectionCoord(center.getX() - horizontalRangeInclusive),
+ SectionPos.blockToSectionCoord(center.getX() + horizontalRangeInclusive),
+ SectionPos.blockToSectionCoord(center.getY() - verticalRangeInclusive),
+ SectionPos.blockToSectionCoord(center.getY() + verticalRangeInclusive),
+ SectionPos.blockToSectionCoord(center.getZ() - horizontalRangeInclusive),
+ SectionPos.blockToSectionCoord(center.getZ() + horizontalRangeInclusive)
+ );
+ }
+
+ public int getSectionIndex(int x, int y, int z) {
+ int dx = x - this.xMin;
+ int dy = y - this.yMin;
+ int dz = z - this.zMin;
+
+ return (dx * this.zLength + dz) * this.yLength + dy;
+ }
+
+ public int getSectionIndex(long sectionPos) {
+ return this.getSectionIndex(
+ SectionPos.x(sectionPos),
+ SectionPos.y(sectionPos),
+ SectionPos.z(sectionPos)
+ );
+ }
+
+ public boolean getChunkSectionBit(BlockPos blockPos) {
+ return this.getChunkSectionBit(SectionPos.blockToSectionCoord(blockPos.getX()), SectionPos.blockToSectionCoord(blockPos.getY()), SectionPos.blockToSectionCoord(blockPos.getZ()));
+ }
+
+ public boolean getChunkSectionBit(int chunkX, int chunkY, int chunkZ) {
+ return this.chunkSectionBits.get(this.getSectionIndex(chunkX, chunkY, chunkZ));
+ }
+
+ public void setChunkSectionStatus(long sectionPos, boolean value) {
+ this.chunkSectionBits.set(this.getSectionIndex(sectionPos), value);
+ }
+
+ public int getChunkIndex(int x, int z) {
+ int dx = x - this.xMin;
+ int dz = z - this.zMin;
+
+ return dx * this.zLength + dz;
+ }
+
+ public int getChunkIndex(long chunkPos) {
+ return this.getChunkIndex(ChunkPos.getX(chunkPos), ChunkPos.getZ(chunkPos));
+ }
+
+ public ChunkAccess getChunkAccess(long chunkPos){
+ return this.chunkAccesses.get(this.getChunkIndex(chunkPos));
+ }
+
+ public ChunkAccess getChunkAccess(BlockPos blockPos){
+ return this.getChunkAccess(ChunkPos.asLong(blockPos));
+ }
+
+ public void setChunkAccess(long chunkPos, ChunkAccess chunkAccess) {
+ this.chunkAccesses.set(this.getChunkIndex(chunkPos), chunkAccess);
+ }
+
+ public void setChunkAccess(BlockPos blockPos, ChunkAccess chunkAccess) {
+ this.setChunkAccess(ChunkPos.asLong(blockPos), chunkAccess);
+ }
+
+ public boolean hasNoTrueChunkSections(){
+ return this.chunkSectionBits.nextSetBit(0) == -1;
+ }
+
+ public LongIterable getChunkPosInRange() {
+ return new LongIterable() {
+ @Override
+ public @NotNull LongIterator iterator(){
+ return getChunkPosInRangeIterator();
+ }
+ };
+ }
+
+ public LongIterator getChunkPosInRangeIterator() {
+ final int xMin = this.xMin;
+ final int xMax = this.xMin + this.xLength - 1;
+ final int zMin = this.zMin;
+ final int zMax = this.zMin + this.zLength - 1;
+ return new LongIterator() {
+ int x = xMin;
+ int z = zMin;
+
+ @Override
+ public long nextLong () {
+ long result = ChunkPos.asLong(x, z);
+ if (z < zMax) {
+ z++;
+ } else {
+ z = zMin;
+ x++;
+ }
+ return result;
+ }
+
+ @Override
+ public boolean hasNext(){
+ return x <= xMax;
+ }
+ };
+ }
+
+ public IntIterable getSectionYInRange() {
+ return new IntIterable() {
+ @Override
+ public @NotNull IntIterator iterator(){
+ return getSectionYInRangeIterator();
+ }
+ };
+ }
+
+ public IntIterator getSectionYInRangeIterator() {
+ final int yMin = this.yMin;
+ final int yLimit = yMin + this.yLength;
+ return new IntIterator() {
+ int y = yMin;
+
+ @Override
+ public int nextInt(){
+ return y++;
+ }
+
+ @Override
+ public boolean hasNext(){
+ return y < yLimit;
+ }
+ };
+ }
+
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/non_poi_block_search/HoglinSpecificSensorMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/non_poi_block_search/HoglinSpecificSensorMixin.java
new file mode 100644
index 000000000..f5a502d38
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/non_poi_block_search/HoglinSpecificSensorMixin.java
@@ -0,0 +1,39 @@
+package net.caffeinemc.mods.lithium.mixin.ai.non_poi_block_search;
+
+import net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.CommonBlockSearchesCheckAndCache;
+import net.minecraft.core.BlockPos;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.tags.BlockTags;
+import net.minecraft.world.entity.ai.sensing.HoglinSpecificSensor;
+import net.minecraft.world.entity.monster.hoglin.Hoglin;
+import net.minecraft.world.level.block.state.BlockState;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+import java.util.Optional;
+
+/**
+ * [Vanilla Copy]
+ * Optimizes Hoglin repellent search.
+ */
+@Mixin(HoglinSpecificSensor.class)
+public abstract class HoglinSpecificSensorMixin {
+ @Unique
+ private static final java.util.function.Predicate IS_VALID_REPELLENT_PREDICATE =
+ HoglinSpecificSensorMixin::lithium$isValidRepellent;
+
+ @Redirect(method = "doTick(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/monster/hoglin/Hoglin;)V",
+ at= @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/ai/sensing/HoglinSpecificSensor;findNearestRepellent(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/monster/hoglin/Hoglin;)Ljava/util/Optional;"))
+ private Optional redirectFindNearestRepellent(HoglinSpecificSensor instance, ServerLevel serverLevel,
+ Hoglin hoglin) {
+ return CommonBlockSearchesCheckAndCache.blockPosFindClosestMatch(serverLevel, hoglin, 8, 4,
+ IS_VALID_REPELLENT_PREDICATE, true);
+ }
+
+ @Unique
+ private static boolean lithium$isValidRepellent(BlockState blockState) {
+ return blockState.is(BlockTags.HOGLIN_REPELLENTS);
+ }
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/non_poi_block_search/MoveToBlockGoalMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/non_poi_block_search/MoveToBlockGoalMixin.java
new file mode 100644
index 000000000..e992af29c
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/non_poi_block_search/MoveToBlockGoalMixin.java
@@ -0,0 +1,234 @@
+package net.caffeinemc.mods.lithium.mixin.ai.non_poi_block_search;
+
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.CheckAndCacheBlockChecker;
+import net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.LithiumMoveToBlockGoal;
+import net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.NonPOISearchDistances.MoveToBlockGoalDistances;
+import net.caffeinemc.mods.lithium.common.util.Pos;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.SectionPos;
+import net.minecraft.world.entity.PathfinderMob;
+import net.minecraft.world.entity.ai.goal.MoveToBlockGoal;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.LevelReader;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunkSection;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
+
+import java.util.function.BiPredicate;
+import java.util.function.Predicate;
+
+/**
+ * [Vanilla Copy] - Chunk aware search order is different, but *SHOULD* result in the same position.
+ * MoveToBlockGoal search is quite laggy if a lot of mobs are trying to start it - e.g. Portal Gold Farms
+ * This is because the searched blocks are not POIs and the search range can be massive - 47x7x47 for zombies.
+ * During this search both getChunk and getBlockState contribute a large portion of the lag.
+ * The current implementation optimizes it by caching the ChunkAccesses and by checking whether the ChunkSection
+ * has the target block using ChunkSection::maybeHas.
+ *
+ * Basic Logic:
+ *
+ * - Prescan chunks and ChunkSections - cache the ChunkAccess and if a ChunkSection has the target block(s) then flag it.
+ *
+ * - If no ChunkSection in the search range has any target block(s), then return early.
+ * - Otherwise pick chunk aware if no chunks need to be loaded, vanilla otherwise.
+ *
+ * Chunk Aware Search:
+ * - The search will proceed on a layer by layer [same as vanilla] then ChunkSection basis if the
+ * ChunkSection has the target block - "empty" ChunkSections will not be iterated through.
+ *
+ * Vanilla Order Search:
+ * - Follow vanilla order but only run getBlockState in possible sections
+ *
+ * Note: If ChunkSections in the search range have A LOT of different blockStates and all ChunkSections have *had*
+ * turtle eggs but the eggs are not in the search there may not be much of a benefit or even possible regression.
+ *
+ * Additional Note: Please correctly specify whether the search may chunk-load to avoid observably altering behavior
+ * in unusual situations. Default getBlockState will chunk-load.
+ *
+ * @author jcw780
+ */
+@Mixin(MoveToBlockGoal.class)
+public abstract class MoveToBlockGoalMixin implements LithiumMoveToBlockGoal {
+ @Shadow
+ @Final
+ protected PathfinderMob mob;
+ @Shadow
+ @Final
+ private int searchRange;
+ @Shadow
+ @Final
+ private int verticalSearchRange;
+ @Shadow
+ protected int verticalSearchStart;
+ @Shadow
+ protected BlockPos blockPos;
+
+ /**
+ * Finds the nearest block matching the predicates.
+ *
+ * Side effect: The matching block position is stored in the blockPos field.
+ *
+ * @return Whether a matching block was found.
+ */
+ @Override
+ public boolean lithium$findNearestBlock(Predicate requiredBlock, BiPredicate lithium$isValidTarget, final boolean shouldChunkLoad) {
+ //Center of the search starts 1 block below the mob's block position
+ BlockPos center = this.mob.blockPosition().offset(0,-1,0);
+
+ //Range is +-(searchRange - 1), +-verticalSearchRange, +-(searchRange - 1)
+ //Cache ChunkAccesses - getting them is surprisingly expensive - and track whether subchunks have the block
+ final LevelReader levelReader = this.mob.level();
+ CheckAndCacheBlockChecker checker = new CheckAndCacheBlockChecker(center,
+ this.searchRange-1, this.verticalSearchRange,
+ levelReader, requiredBlock, shouldChunkLoad);
+ LongArrayList sortedChunksMaybeWithBlock = new LongArrayList(checker.getChunkSize());
+ checker.initializeChunks(sortedChunksMaybeWithBlock::addLast);
+
+ if (checker.shouldStop()) {
+ return false; //No chunks with the target block - return early
+ }
+
+ final int minY = Pos.BlockCoord.getMinY(levelReader);
+ final int maxY = Pos.BlockCoord.getMaxYInclusive(levelReader);
+
+ // Prefer chunk aware search because it also cuts iterations inside "empty" chunk sections
+ if(!checker.hasUnloadedPossibleChunks()){
+ return this.lithium$chunkAwareSearch(center, lithium$isValidTarget, checker, sortedChunksMaybeWithBlock, minY, maxY);
+ }
+
+ // Use vanilla search because unordered search may observably alter chunk-loading behavior
+ return this.lithium$vanillaOrderSearch(center, lithium$isValidTarget, checker, minY, maxY);
+ }
+
+ @Unique
+ private boolean lithium$vanillaOrderSearch(BlockPos center,
+ BiPredicate lithium$isValidTarget,
+ CheckAndCacheBlockChecker checker, final int minY, final int maxY) {
+ BlockPos.MutableBlockPos currentPos = new BlockPos.MutableBlockPos();
+ final int centerY = center.getY();
+
+ for (int layer = this.verticalSearchStart; layer <= this.verticalSearchRange; layer = layer > 0 ? -layer : 1 - layer) {
+ final int y = centerY + layer;
+
+ // Layer outside of build limit - skip
+ // Note: this is likely to be hit because farms where this lags tend to be built at world floor
+ if (y < minY || y > maxY) {
+ continue;
+ }
+
+ for (int ring = 0; ring < this.searchRange; ring++) {
+ for (int dX = 0; dX <= ring; dX = dX > 0 ? -dX : 1 - dX) {
+ for (int dZ = dX < ring && dX > -ring ? ring : 0; dZ <= ring; dZ = dZ > 0 ? -dZ : 1 - dZ) {
+ currentPos.setWithOffset(center, dX, layer, dZ);
+ if (this.mob.isWithinRestriction(currentPos) && checker.checkPosition(currentPos)) {
+ // ChunkAccess is always loaded at this point
+ ChunkAccess chunkAccess = checker.getCachedChunkAccess(currentPos);
+ if (lithium$isValidTarget.test(chunkAccess, currentPos)){
+ this.blockPos = currentPos;
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Unique
+ private boolean lithium$chunkAwareSearch(BlockPos center,
+ BiPredicate lithium$isValidTarget,
+ CheckAndCacheBlockChecker checker, LongArrayList sortedChunksMaybeWithBlock,
+ final int minY, final int maxY) {
+ // Sort chunks by lowest sort order - has the earliest searched position
+ // Note: In this search order, the closest point normally is also the closest point in the search
+ sortedChunksMaybeWithBlock.sort((chunkLong0, chunkLong1) ->
+ MoveToBlockGoalDistances.getMinimumSortOrderOfChunk(center, chunkLong0)
+ - MoveToBlockGoalDistances.getMinimumSortOrderOfChunk(center, chunkLong1)
+ );
+
+ Predicate requiredBlock = checker.blockStatePredicate;
+ final int minSectionY = checker.minSectionY;
+
+ BlockPos.MutableBlockPos foundPos = new BlockPos.MutableBlockPos();
+ BlockPos.MutableBlockPos currentPos = new BlockPos.MutableBlockPos();
+
+ // Same layer order as vanilla - saves iterations if targets are found in the first layer
+ for (int layer = this.verticalSearchStart; layer <= this.verticalSearchRange; layer = layer > 0 ? -layer : 1 - layer) {
+ final int y = center.getY() + layer;
+
+ // Layer outside of build limit - skip
+ // Note: this is likely to be hit because farms where this lags tend to be built at world floor
+ if (y < minY || y > maxY) {
+ continue;
+ }
+
+ final int chunkY = SectionPos.blockToSectionCoord(y);
+ final int ySectionIndex = chunkY - minSectionY;
+
+ int closestFound = Integer.MAX_VALUE;
+ int ringMax = this.searchRange - 1;
+
+ // Iterate through slices of chunks that may have the target blockState
+ for (long chunkPos: sortedChunksMaybeWithBlock) {
+ final int chunkX = ChunkPos.getX(chunkPos);
+ final int chunkZ = ChunkPos.getZ(chunkPos);
+
+ // Break since no subsequent chunks can be closer
+ if (closestFound < MoveToBlockGoalDistances.getMinimumSortOrderOfChunk(center, chunkX, chunkZ)) {
+ break;
+ }
+
+ // Skip if the current subchunk does not have the block
+ if (!checker.checkCachedSection(chunkX, chunkY, chunkZ)) {
+ continue;
+ }
+
+ ChunkAccess chunkAccess = checker.getCachedChunkAccess(chunkPos);
+ // If ChunkSection may have close enough targets, iterate layer in Paletted Container (x then z) order
+ final int chunkBlockX = SectionPos.sectionToBlockCoord(chunkX);
+ int xMin = Math.max(center.getX() - ringMax, chunkBlockX);
+ int xMax = Math.min(center.getX() + ringMax, chunkBlockX + 15);
+ final int chunkBlockZ = SectionPos.sectionToBlockCoord(chunkZ);
+ int zMin = Math.max(center.getZ() - ringMax, chunkBlockZ);
+ int zMax = Math.min(center.getZ() + ringMax, chunkBlockZ + 15);
+ LevelChunkSection levelChunkSection = chunkAccess.getSections()[ySectionIndex];
+ for (int z = zMin; z <= zMax; z++) {
+ for (int x = xMin; x <= xMax; x++) {
+ int dX = x - center.getX();
+ int dZ = z - center.getZ();
+ int ring = MoveToBlockGoalDistances.getRing(dX, dZ);
+ int currentDistance = MoveToBlockGoalDistances.getVanillaSortOrderInt(ring, dX, dZ);
+ if (currentDistance < closestFound
+ && this.mob.isWithinRestriction(currentPos.set(x, y, z))
+ && requiredBlock.test(levelChunkSection.getBlockState(x & 15, y & 15, z & 15))
+ && lithium$isValidTarget.test(chunkAccess, currentPos)) {
+ // Constrain search size when we find a valid target
+ ringMax = ring;
+ xMin = Math.max(center.getX() - ringMax, chunkBlockX);
+ xMax = Math.min(center.getX() + ringMax, chunkBlockX + 15);
+ zMax = Math.min(center.getZ() + ringMax, chunkBlockZ + 15);
+ foundPos.set(x, y, z);
+ closestFound = currentDistance;
+ }
+ }
+ }
+ }
+
+ if (closestFound < Integer.MAX_VALUE) {
+ // Vanilla uses the mutable pos, no need to create immutable copy
+ this.blockPos = foundPos;
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/non_poi_block_search/PiglinSpecificSensorMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/non_poi_block_search/PiglinSpecificSensorMixin.java
new file mode 100644
index 000000000..72ed0aa63
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/non_poi_block_search/PiglinSpecificSensorMixin.java
@@ -0,0 +1,43 @@
+package net.caffeinemc.mods.lithium.mixin.ai.non_poi_block_search;
+
+import net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.CommonBlockSearchesCheckAndCache;
+import net.minecraft.core.BlockPos;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.tags.BlockTags;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.ai.sensing.PiglinSpecificSensor;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.CampfireBlock;
+import net.minecraft.world.level.block.state.BlockState;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+import java.util.Optional;
+import java.util.function.Predicate;
+
+/**
+ * [Vanilla Copy]
+ * Optimizes Piglin repellent search.
+ */
+@Mixin(PiglinSpecificSensor.class)
+public abstract class PiglinSpecificSensorMixin {
+ @Unique
+ private static final Predicate IS_VALID_REPELLENT_PREDICATE =
+ PiglinSpecificSensorMixin::lithium$isValidRepellent;
+
+ @Redirect(method = "doTick", at = @At(value = "INVOKE",
+ target = "Lnet/minecraft/world/entity/ai/sensing/PiglinSpecificSensor;findNearestRepellent(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/LivingEntity;)Ljava/util/Optional;"))
+ public Optional redirectFindNearestRepellent(ServerLevel serverLevel, LivingEntity livingEntity) {
+ return CommonBlockSearchesCheckAndCache.blockPosFindClosestMatch(serverLevel, livingEntity, 8, 4,
+ IS_VALID_REPELLENT_PREDICATE, true);
+ }
+
+ @Unique
+ private static boolean lithium$isValidRepellent(BlockState blockState){
+ final boolean isPiglinRepellent = blockState.is(BlockTags.PIGLIN_REPELLENTS);
+ return isPiglinRepellent && blockState.is(Blocks.SOUL_CAMPFIRE) ?
+ CampfireBlock.isLitCampfire(blockState) : isPiglinRepellent;
+ }
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/non_poi_block_search/RemoveBlockGoalMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/non_poi_block_search/RemoveBlockGoalMixin.java
new file mode 100644
index 000000000..630a1a987
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/non_poi_block_search/RemoveBlockGoalMixin.java
@@ -0,0 +1,54 @@
+package net.caffeinemc.mods.lithium.mixin.ai.non_poi_block_search;
+
+import net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.LithiumMoveToBlockGoal;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.entity.PathfinderMob;
+import net.minecraft.world.entity.ai.goal.MoveToBlockGoal;
+import net.minecraft.world.entity.ai.goal.RemoveBlockGoal;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+import java.util.function.BiPredicate;
+
+
+@Mixin(RemoveBlockGoal.class)
+public abstract class RemoveBlockGoalMixin extends MoveToBlockGoal implements LithiumMoveToBlockGoal {
+ @Shadow
+ @Final
+ private Block blockToRemove;
+
+ @Unique
+ private static final BiPredicate IS_VALID_TARGET_ABOVE_BIPREDICATE =
+ RemoveBlockGoalMixin::lithium$isValidTargetAbove;
+
+ public RemoveBlockGoalMixin(PathfinderMob pathfinderMob, double d, int i) {
+ super(pathfinderMob, d, i);
+ }
+
+ @Redirect(method = "canUse",
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/ai/goal/RemoveBlockGoal;findNearestBlock()Z"))
+ protected boolean redirectFindNearestBlock(RemoveBlockGoal removeBlockGoal) {
+ return ((LithiumMoveToBlockGoal) removeBlockGoal).lithium$findNearestBlock(
+ this::lithium$isValidTargetBlock, IS_VALID_TARGET_ABOVE_BIPREDICATE, false
+ );
+ }
+
+ //Split check condition in order to use maybeHas
+ @Unique
+ private boolean lithium$isValidTargetBlock(BlockState blockState){
+ return blockState.is(this.blockToRemove);
+ }
+
+ @Unique
+ private static boolean lithium$isValidTargetAbove(ChunkAccess chunkAccess, BlockPos.MutableBlockPos mutable) {
+ return chunkAccess.getBlockState(mutable.move(0, 1, 0)).isAir()
+ && chunkAccess.getBlockState(mutable.move(0, 1, 0)).isAir();
+ }
+}
diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/non_poi_block_search/package-info.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/non_poi_block_search/package-info.java
new file mode 100644
index 000000000..981511a03
--- /dev/null
+++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/non_poi_block_search/package-info.java
@@ -0,0 +1,3 @@
+@MixinConfigOption(description = "Optimizes Non-POI block search using maybeHas to return early or reduce block searches")
+package net.caffeinemc.mods.lithium.mixin.ai.non_poi_block_search;
+import net.caffeinemc.gradle.MixinConfigOption;
\ No newline at end of file
diff --git a/common/src/main/resources/lithium.mixins.json b/common/src/main/resources/lithium.mixins.json
index 8c83c7b8e..1ffeb71d0 100644
--- a/common/src/main/resources/lithium.mixins.json
+++ b/common/src/main/resources/lithium.mixins.json
@@ -7,6 +7,10 @@
"defaultRequire" : 1
},
"mixins" : [
+ "ai.non_poi_block_search.HoglinSpecificSensorMixin",
+ "ai.non_poi_block_search.MoveToBlockGoalMixin",
+ "ai.non_poi_block_search.PiglinSpecificSensorMixin",
+ "ai.non_poi_block_search.RemoveBlockGoalMixin",
"ai.pathing.BlockStateBaseMixin",
"ai.pathing.BootstrapMixin",
"ai.pathing.FlyNodeEvaluatorMixin",
diff --git a/lithium-fabric-mixin-config.md b/lithium-fabric-mixin-config.md
index a163c6bc4..168901408 100644
--- a/lithium-fabric-mixin-config.md
+++ b/lithium-fabric-mixin-config.md
@@ -20,6 +20,10 @@ mixin.gen.biome_noise_cache=false
(default: `true`)
Mob AI optimizations
+### `mixin.ai.non_poi_block_search`
+(default: `true`)
+Optimizes Non-POI block search using maybeHas to return early or reduce block searches
+
### `mixin.ai.pathing`
(default: `false`)
A faster code path is used for determining what kind of path-finding node type is associated with a
diff --git a/lithium-neoforge-mixin-config.md b/lithium-neoforge-mixin-config.md
index 4298d0dcc..89305f835 100644
--- a/lithium-neoforge-mixin-config.md
+++ b/lithium-neoforge-mixin-config.md
@@ -20,6 +20,10 @@ mixin.gen.biome_noise_cache=false
(default: `true`)
Mob AI optimizations
+### `mixin.ai.non_poi_block_search`
+(default: `true`)
+Optimizes Non-POI block search using maybeHas to return early or reduce block searches
+
### `mixin.ai.pathing`
(default: `false`)
A faster code path is used for determining what kind of path-finding node type is associated with a