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