package net.minecraft.world.entity.ai.village.poi; import com.mojang.datafixers.DataFixer; import com.mojang.datafixers.util.Pair; import it.unimi.dsi.fastutil.longs.Long2ByteMap; import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import it.unimi.dsi.fastutil.longs.LongSet; import java.nio.file.Path; import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.BiPredicate; import java.util.function.BooleanSupplier; import java.util.function.Predicate; import java.util.stream.IntStream; import java.util.stream.Stream; import net.minecraft.Util; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.RegistryAccess; import net.minecraft.core.SectionPos; import net.minecraft.resources.ResourceKey; import net.minecraft.server.level.SectionTracker; import net.minecraft.tags.PoiTypeTags; import net.minecraft.util.RandomSource; import net.minecraft.util.VisibleForDebug; import net.minecraft.util.datafix.DataFixTypes; import net.minecraft.world.entity.ai.village.poi.PoiSection.Packed; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.LevelHeightAccessor; import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.chunk.storage.ChunkIOErrorReporter; import net.minecraft.world.level.chunk.storage.RegionStorageInfo; import net.minecraft.world.level.chunk.storage.SectionStorage; import net.minecraft.world.level.chunk.storage.SimpleRegionStorage; public class PoiManager extends SectionStorage { public static final int MAX_VILLAGE_DISTANCE = 6; public static final int VILLAGE_SECTION_SIZE = 1; private final PoiManager.DistanceTracker distanceTracker; private final LongSet loadedChunks = new LongOpenHashSet(); public PoiManager( RegionStorageInfo info, Path folder, DataFixer fixerUpper, boolean sync, RegistryAccess registryAccess, ChunkIOErrorReporter errorReporter, LevelHeightAccessor levelHeightAccessor ) { super( new SimpleRegionStorage(info, folder, fixerUpper, sync, DataFixTypes.POI_CHUNK), Packed.CODEC, PoiSection::pack, Packed::unpack, PoiSection::new, registryAccess, errorReporter, levelHeightAccessor ); this.distanceTracker = new PoiManager.DistanceTracker(); } public void add(BlockPos pos, Holder type) { this.getOrCreate(SectionPos.asLong(pos)).add(pos, type); } public void remove(BlockPos pos) { this.getOrLoad(SectionPos.asLong(pos)).ifPresent(poiSection -> poiSection.remove(pos)); } public long getCountInRange(Predicate> typePredicate, BlockPos pos, int distance, PoiManager.Occupancy status) { return this.getInRange(typePredicate, pos, distance, status).count(); } public boolean existsAtPosition(ResourceKey type, BlockPos pos) { return this.exists(pos, holder -> holder.is(type)); } public Stream getInSquare(Predicate> typePredicate, BlockPos pos, int distance, PoiManager.Occupancy status) { int i = Math.floorDiv(distance, 16) + 1; return ChunkPos.rangeClosed(new ChunkPos(pos), i).flatMap(chunkPos -> this.getInChunk(typePredicate, chunkPos, status)).filter(poiRecord -> { BlockPos blockPos2 = poiRecord.getPos(); return Math.abs(blockPos2.getX() - pos.getX()) <= distance && Math.abs(blockPos2.getZ() - pos.getZ()) <= distance; }); } public Stream getInRange(Predicate> typePredicate, BlockPos pos, int distance, PoiManager.Occupancy status) { int i = distance * distance; return this.getInSquare(typePredicate, pos, distance, status).filter(poiRecord -> poiRecord.getPos().distSqr(pos) <= i); } @VisibleForDebug public Stream getInChunk(Predicate> typePredicate, ChunkPos posChunk, PoiManager.Occupancy status) { return IntStream.rangeClosed(this.levelHeightAccessor.getMinSectionY(), this.levelHeightAccessor.getMaxSectionY()) .boxed() .map(integer -> this.getOrLoad(SectionPos.of(posChunk, integer).asLong())) .filter(Optional::isPresent) .flatMap(optional -> ((PoiSection)optional.get()).getRecords(typePredicate, status)); } public Stream findAll( Predicate> typePredicate, Predicate posPredicate, BlockPos pos, int distance, PoiManager.Occupancy status ) { return this.getInRange(typePredicate, pos, distance, status).map(PoiRecord::getPos).filter(posPredicate); } public Stream, BlockPos>> findAllWithType( Predicate> typePredicate, Predicate posPredicate, BlockPos pos, int distance, PoiManager.Occupancy status ) { return this.getInRange(typePredicate, pos, distance, status) .filter(poiRecord -> posPredicate.test(poiRecord.getPos())) .map(poiRecord -> Pair.of(poiRecord.getPoiType(), poiRecord.getPos())); } public Stream, BlockPos>> findAllClosestFirstWithType( Predicate> typePredicate, Predicate posPredicate, BlockPos pos, int distance, PoiManager.Occupancy status ) { return this.findAllWithType(typePredicate, posPredicate, pos, distance, status) .sorted(Comparator.comparingDouble(pair -> ((BlockPos)pair.getSecond()).distSqr(pos))); } public Optional find( Predicate> typePredicate, Predicate posPredicate, BlockPos pos, int distance, PoiManager.Occupancy status ) { return this.findAll(typePredicate, posPredicate, pos, distance, status).findFirst(); } public Optional findClosest(Predicate> typePredicate, BlockPos pos, int distance, PoiManager.Occupancy status) { return this.getInRange(typePredicate, pos, distance, status).map(PoiRecord::getPos).min(Comparator.comparingDouble(blockPos2 -> blockPos2.distSqr(pos))); } public Optional, BlockPos>> findClosestWithType( Predicate> typePredicate, BlockPos pos, int distance, PoiManager.Occupancy status ) { return this.getInRange(typePredicate, pos, distance, status) .min(Comparator.comparingDouble(poiRecord -> poiRecord.getPos().distSqr(pos))) .map(poiRecord -> Pair.of(poiRecord.getPoiType(), poiRecord.getPos())); } public Optional findClosest( Predicate> typePredicate, Predicate posPredicate, BlockPos pos, int distance, PoiManager.Occupancy status ) { return this.getInRange(typePredicate, pos, distance, status) .map(PoiRecord::getPos) .filter(posPredicate) .min(Comparator.comparingDouble(blockPos2 -> blockPos2.distSqr(pos))); } public Optional take( Predicate> typePredicate, BiPredicate, BlockPos> combinedTypePosPredicate, BlockPos pos, int distance ) { return this.getInRange(typePredicate, pos, distance, PoiManager.Occupancy.HAS_SPACE) .filter(poiRecord -> combinedTypePosPredicate.test(poiRecord.getPoiType(), poiRecord.getPos())) .findFirst() .map(poiRecord -> { poiRecord.acquireTicket(); return poiRecord.getPos(); }); } public Optional getRandom( Predicate> typePredicate, Predicate posPredicate, PoiManager.Occupancy status, BlockPos pos, int distance, RandomSource random ) { List list = Util.toShuffledList(this.getInRange(typePredicate, pos, distance, status), random); return list.stream().filter(poiRecord -> posPredicate.test(poiRecord.getPos())).findFirst().map(PoiRecord::getPos); } public boolean release(BlockPos pos) { return (Boolean)this.getOrLoad(SectionPos.asLong(pos)) .map(poiSection -> poiSection.release(pos)) .orElseThrow(() -> Util.pauseInIde(new IllegalStateException("POI never registered at " + pos))); } public boolean exists(BlockPos pos, Predicate> typePredicate) { return (Boolean)this.getOrLoad(SectionPos.asLong(pos)).map(poiSection -> poiSection.exists(pos, typePredicate)).orElse(false); } public Optional> getType(BlockPos pos) { return this.getOrLoad(SectionPos.asLong(pos)).flatMap(poiSection -> poiSection.getType(pos)); } @Deprecated @VisibleForDebug public int getFreeTickets(BlockPos pos) { return (Integer)this.getOrLoad(SectionPos.asLong(pos)).map(poiSection -> poiSection.getFreeTickets(pos)).orElse(0); } public int sectionsToVillage(SectionPos sectionPos) { this.distanceTracker.runAllUpdates(); return this.distanceTracker.getLevel(sectionPos.asLong()); } boolean isVillageCenter(long chunkPos) { Optional optional = this.get(chunkPos); return optional == null ? false : (Boolean)optional.map( poiSection -> poiSection.getRecords(holder -> holder.is(PoiTypeTags.VILLAGE), PoiManager.Occupancy.IS_OCCUPIED).findAny().isPresent() ) .orElse(false); } @Override public void tick(BooleanSupplier aheadOfTime) { super.tick(aheadOfTime); this.distanceTracker.runAllUpdates(); } @Override protected void setDirty(long sectionPos) { super.setDirty(sectionPos); this.distanceTracker.update(sectionPos, this.distanceTracker.getLevelFromSource(sectionPos), false); } @Override protected void onSectionLoad(long sectionKey) { this.distanceTracker.update(sectionKey, this.distanceTracker.getLevelFromSource(sectionKey), false); } public void checkConsistencyWithBlocks(SectionPos sectionPos, LevelChunkSection levelChunkSection) { Util.ifElse(this.getOrLoad(sectionPos.asLong()), poiSection -> poiSection.refresh(biConsumer -> { if (mayHavePoi(levelChunkSection)) { this.updateFromSection(levelChunkSection, sectionPos, biConsumer); } }), () -> { if (mayHavePoi(levelChunkSection)) { PoiSection poiSection = this.getOrCreate(sectionPos.asLong()); this.updateFromSection(levelChunkSection, sectionPos, poiSection::add); } }); } private static boolean mayHavePoi(LevelChunkSection section) { return section.maybeHas(PoiTypes::hasPoi); } private void updateFromSection(LevelChunkSection section, SectionPos sectionPos, BiConsumer> posToTypeConsumer) { sectionPos.blocksInside() .forEach( blockPos -> { BlockState blockState = section.getBlockState( SectionPos.sectionRelative(blockPos.getX()), SectionPos.sectionRelative(blockPos.getY()), SectionPos.sectionRelative(blockPos.getZ()) ); PoiTypes.forState(blockState).ifPresent(holder -> posToTypeConsumer.accept(blockPos, holder)); } ); } public void ensureLoadedAndValid(LevelReader levelReader, BlockPos pos, int coordinateOffset) { SectionPos.aroundChunk( new ChunkPos(pos), Math.floorDiv(coordinateOffset, 16), this.levelHeightAccessor.getMinSectionY(), this.levelHeightAccessor.getMaxSectionY() ) .map(sectionPos -> Pair.of(sectionPos, this.getOrLoad(sectionPos.asLong()))) .filter(pair -> !(Boolean)((Optional)pair.getSecond()).map(PoiSection::isValid).orElse(false)) .map(pair -> ((SectionPos)pair.getFirst()).chunk()) .filter(chunkPos -> this.loadedChunks.add(chunkPos.toLong())) .forEach(chunkPos -> levelReader.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.EMPTY)); } final class DistanceTracker extends SectionTracker { private final Long2ByteMap levels = new Long2ByteOpenHashMap(); protected DistanceTracker() { super(7, 16, 256); this.levels.defaultReturnValue((byte)7); } @Override protected int getLevelFromSource(long pos) { return PoiManager.this.isVillageCenter(pos) ? 0 : 7; } @Override protected int getLevel(long chunkPos) { return this.levels.get(chunkPos); } @Override protected void setLevel(long chunkPos, int level) { if (level > 6) { this.levels.remove(chunkPos); } else { this.levels.put(chunkPos, (byte)level); } } public void runAllUpdates() { super.runUpdates(Integer.MAX_VALUE); } } public static enum Occupancy { HAS_SPACE(PoiRecord::hasSpace), IS_OCCUPIED(PoiRecord::isOccupied), ANY(poiRecord -> true); private final Predicate test; private Occupancy(final Predicate test) { this.test = test; } public Predicate getTest() { return this.test; } } }