Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ bin/
/run/
/common/run/
/fabric/run/
/fabric/config/
/fabric/logs/
/neoforge/run/
/neoforge/runs/

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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<BlockState> blockStatePredicate;
private int unloadedPossibleChunkSections = 0;
public final int minSectionY;

public CheckAndCacheBlockChecker(BlockPos origin, int horizontalRangeInclusive, int verticalRangeInclusive, LevelReader levelReader,
Predicate<BlockState> 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<Long> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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<BlockPos> blockPosFindClosestMatch(LevelReader levelReader, LivingEntity livingEntity,
int horizontalRange, int verticalRange,
Predicate<BlockState> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<BlockState> requiredBlock,
BiPredicate<ChunkAccess, BlockPos.MutableBlockPos> lithium$isValidTarget,
final boolean shouldChunkLoad);
}
Original file line number Diff line number Diff line change
@@ -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;
* <p>
* 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)
* <p>
* 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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));

}
}
Loading