diff --git a/shiroha-server/minecraft-patches/features/0076-Lithium-optimized-collections-for-WeightedList.patch b/shiroha-server/minecraft-patches/features/0076-Lithium-optimized-collections-for-WeightedList.patch new file mode 100644 index 0000000..755695c --- /dev/null +++ b/shiroha-server/minecraft-patches/features/0076-Lithium-optimized-collections-for-WeightedList.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: NanaChiyo0721 +Date: Sat, 25 Jul 2026 21:07:48 +0800 +Subject: [PATCH] Lithium optimized collections for WeightedList + + +diff --git a/net/minecraft/util/random/WeightedList.java b/net/minecraft/util/random/WeightedList.java +index a8481259e42aa116e29bccd1e30fdfd91f51a8cc..e6dd554742ab60c23afed69953e7eb6b762754d6 100644 +--- a/net/minecraft/util/random/WeightedList.java ++++ b/net/minecraft/util/random/WeightedList.java +@@ -23,7 +23,7 @@ public class WeightedList { // Paper - non-final + private final WeightedList.@Nullable Selector selector; + + protected WeightedList(final List> items) { // Paper - protected +- this.items = List.copyOf(items); ++ this.items = items.size() > 4 ? new net.caffeinemc.mods.lithium.common.util.collections.HashedReferenceList(items) : new it.unimi.dsi.fastutil.objects.ReferenceArrayList<>(items); // Shiroha - Lithium optimized collections for WeightedList + this.totalWeight = WeightedRandom.getTotalWeight(items, Weighted::weight); + if (this.totalWeight == 0) { + this.selector = null; diff --git a/shiroha-server/minecraft-patches/features/0077-Lithium-Optimizes-Hoglin-repellent-search.patch b/shiroha-server/minecraft-patches/features/0077-Lithium-Optimizes-Hoglin-repellent-search.patch new file mode 100644 index 0000000..4ed9b0e --- /dev/null +++ b/shiroha-server/minecraft-patches/features/0077-Lithium-Optimizes-Hoglin-repellent-search.patch @@ -0,0 +1,36 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: NanaChiyo0721 +Date: Sat, 25 Jul 2026 21:16:19 +0800 +Subject: [PATCH] Lithium Optimizes Hoglin repellent search. + + +diff --git a/net/minecraft/world/entity/ai/sensing/HoglinSpecificSensor.java b/net/minecraft/world/entity/ai/sensing/HoglinSpecificSensor.java +index 7022679c84e184999b416e06d1af2cc2500cb12b..ac8f4acc2aaee8ca9f7598d842ecf29f2ad73be3 100644 +--- a/net/minecraft/world/entity/ai/sensing/HoglinSpecificSensor.java ++++ b/net/minecraft/world/entity/ai/sensing/HoglinSpecificSensor.java +@@ -16,6 +16,14 @@ import net.minecraft.world.entity.monster.hoglin.Hoglin; + import net.minecraft.world.entity.monster.piglin.Piglin; + + public class HoglinSpecificSensor extends Sensor { ++ // Shiroha start - Lithium Optimizes Hoglin repellent search. ++ private static final java.util.function.Predicate IS_VALID_REPELLENT_PREDICATE = ++ HoglinSpecificSensor::lithium$isValidRepellent; ++ ++ private static boolean lithium$isValidRepellent(net.minecraft.world.level.block.state.BlockState blockState) { ++ return blockState.is(BlockTags.HOGLIN_REPELLENTS); ++ } ++ // Shiroha end - Lithium Optimizes Hoglin repellent search. + @Override + public Set> requires() { + return ImmutableSet.of( +@@ -30,8 +38,8 @@ public class HoglinSpecificSensor extends Sensor { + + @Override + protected void doTick(final ServerLevel level, final Hoglin body) { +- Brain brain = body.getBrain(); +- brain.setMemory(MemoryModuleType.NEAREST_REPELLENT, this.findNearestRepellent(level, body)); ++ Brain brain = body.getBrain();; ++ brain.setMemory(MemoryModuleType.NEAREST_REPELLENT, net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.CommonBlockSearchesCheckAndCache.blockPosFindClosestMatch(level, body, 8, 4, IS_VALID_REPELLENT_PREDICATE, true)); // Shiroha - Lithium Optimizes Hoglin repellent search. + Optional adultPiglin = Optional.empty(); + int adultPiglinCount = 0; + List adultHoglins = Lists.newArrayList(); diff --git a/shiroha-server/minecraft-patches/features/0078-Lithium-Optimizes-move-to-block-goal.patch b/shiroha-server/minecraft-patches/features/0078-Lithium-Optimizes-move-to-block-goal.patch new file mode 100644 index 0000000..b4c28e4 --- /dev/null +++ b/shiroha-server/minecraft-patches/features/0078-Lithium-Optimizes-move-to-block-goal.patch @@ -0,0 +1,187 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: NanaChiyo0721 +Date: Sat, 25 Jul 2026 21:30:17 +0800 +Subject: [PATCH] Lithium Optimizes move to block goal + + +diff --git a/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java b/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java +index c6e9b155c2341c13d569f28f54f0c1d2d7313676..7bd3c8a1c627cfd3e377a269c649c0c169634b49 100644 +--- a/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java ++++ b/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java +@@ -5,7 +5,7 @@ import net.minecraft.core.BlockPos; + import net.minecraft.world.entity.PathfinderMob; + import net.minecraft.world.level.LevelReader; + +-public abstract class MoveToBlockGoal extends Goal { ++public abstract class MoveToBlockGoal extends Goal implements net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.LithiumMoveToBlockGoal { // Shiroha - Lithium Optimizes move to block goal + private static final int GIVE_UP_TICKS = 1200; + private static final int STAY_TICKS = 1200; + private static final int INTERVAL_TICKS = 200; +@@ -133,4 +133,167 @@ public abstract class MoveToBlockGoal extends Goal { + } + + protected abstract boolean isValidTarget(LevelReader level, BlockPos pos); ++ // Shiorha start - Lithium Optimizes move to block goal ++ /** ++ * 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(java.util.function.Predicate requiredBlock, java.util.function.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(); ++ net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.CheckAndCacheBlockChecker checker = new net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.CheckAndCacheBlockChecker(center, ++ this.searchRange-1, this.verticalSearchRange, ++ levelReader, requiredBlock, shouldChunkLoad); ++ it.unimi.dsi.fastutil.longs.LongArrayList sortedChunksMaybeWithBlock = new it.unimi.dsi.fastutil.longs.LongArrayList(checker.getChunkSize()); ++ checker.initializeChunks(sortedChunksMaybeWithBlock::addLast); ++ ++ if (checker.shouldStop()) { ++ return false; //No chunks with the target block - return early ++ } ++ ++ final int minY = net.caffeinemc.mods.lithium.common.util.Pos.BlockCoord.getMinY(levelReader); ++ final int maxY = net.caffeinemc.mods.lithium.common.util.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); ++ } ++ ++ private boolean lithium$vanillaOrderSearch(BlockPos center, ++ java.util.function.BiPredicate lithium$isValidTarget, ++ net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.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.isWithinHome(currentPos) && checker.checkPosition(currentPos)) { ++ // ChunkAccess is always loaded at this point ++ net.minecraft.world.level.chunk.ChunkAccess chunkAccess = checker.getCachedChunkAccess(currentPos); ++ if (lithium$isValidTarget.test(chunkAccess, currentPos)){ ++ this.blockPos = currentPos; ++ return true; ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ return false; ++ } ++ ++ private boolean lithium$chunkAwareSearch(BlockPos center, ++ java.util.function.BiPredicate lithium$isValidTarget, ++ net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.CheckAndCacheBlockChecker checker, it.unimi.dsi.fastutil.longs.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) -> ++ net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.NonPOISearchDistances.MoveToBlockGoalDistances.getMinimumSortOrderOfChunk(center, chunkLong0) ++ - net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.NonPOISearchDistances.MoveToBlockGoalDistances.getMinimumSortOrderOfChunk(center, chunkLong1) ++ ); ++ ++ java.util.function.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 = net.minecraft.core.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 = net.minecraft.world.level.ChunkPos.getX(chunkPos); ++ final int chunkZ = net.minecraft.world.level.ChunkPos.getZ(chunkPos); ++ ++ // Break since no subsequent chunks can be closer ++ if (closestFound < net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.NonPOISearchDistances.MoveToBlockGoalDistances.getMinimumSortOrderOfChunk(center, chunkX, chunkZ)) { ++ break; ++ } ++ ++ // Skip if the current subchunk does not have the block ++ if (!checker.checkCachedSection(chunkX, chunkY, chunkZ)) { ++ continue; ++ } ++ ++ net.minecraft.world.level.chunk.ChunkAccess chunkAccess = checker.getCachedChunkAccess(chunkPos); ++ // If ChunkSection may have close enough targets, iterate layer in Paletted Container (x then z) order ++ final int chunkBlockX = net.minecraft.core.SectionPos.sectionToBlockCoord(chunkX); ++ int xMin = Math.max(center.getX() - ringMax, chunkBlockX); ++ int xMax = Math.min(center.getX() + ringMax, chunkBlockX + 15); ++ final int chunkBlockZ = net.minecraft.core.SectionPos.sectionToBlockCoord(chunkZ); ++ int zMin = Math.max(center.getZ() - ringMax, chunkBlockZ); ++ int zMax = Math.min(center.getZ() + ringMax, chunkBlockZ + 15); ++ net.minecraft.world.level.chunk.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 = net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.NonPOISearchDistances.MoveToBlockGoalDistances.getRing(dX, dZ); ++ int currentDistance = net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.NonPOISearchDistances.MoveToBlockGoalDistances.getVanillaSortOrderInt(ring, dX, dZ); ++ if (currentDistance < closestFound ++ && this.mob.isWithinHome(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; ++ } ++ // Shiorha end - Lithium Optimizes move to block goal + } diff --git a/shiroha-server/minecraft-patches/features/0079-Lithium-Optimizes-Piglin-repellent-search.patch b/shiroha-server/minecraft-patches/features/0079-Lithium-Optimizes-Piglin-repellent-search.patch new file mode 100644 index 0000000..d782202 --- /dev/null +++ b/shiroha-server/minecraft-patches/features/0079-Lithium-Optimizes-Piglin-repellent-search.patch @@ -0,0 +1,35 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: NanaChiyo0721 +Date: Sat, 25 Jul 2026 21:30:26 +0800 +Subject: [PATCH] Lithium Optimizes Piglin repellent search. + + +diff --git a/net/minecraft/world/entity/ai/sensing/PiglinSpecificSensor.java b/net/minecraft/world/entity/ai/sensing/PiglinSpecificSensor.java +index 060a53da799a3036a343cc6f1d73e3e5ca4b7af9..7e5accce5a9183165e7604ca5e144e600b6f503d 100644 +--- a/net/minecraft/world/entity/ai/sensing/PiglinSpecificSensor.java ++++ b/net/minecraft/world/entity/ai/sensing/PiglinSpecificSensor.java +@@ -26,6 +26,15 @@ import net.minecraft.world.level.block.CampfireBlock; + import net.minecraft.world.level.block.state.BlockState; + + public class PiglinSpecificSensor extends Sensor { ++ // Shiroha start - Lithium Optimizes Piglin repellent search. ++ private static final java.util.function.Predicate IS_VALID_REPELLENT_PREDICATE = ++ PiglinSpecificSensor::lithium$isValidRepellent; ++ 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; ++ } ++ // Shiroha end - Lithium Optimizes Piglin repellent search. + @Override + public Set> requires() { + return ImmutableSet.of( +@@ -47,7 +56,7 @@ public class PiglinSpecificSensor extends Sensor { + @Override + protected void doTick(final ServerLevel level, final LivingEntity body) { + Brain brain = body.getBrain(); +- brain.setMemory(MemoryModuleType.NEAREST_REPELLENT, findNearestRepellent(level, body)); ++ brain.setMemory(MemoryModuleType.NEAREST_REPELLENT, net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.CommonBlockSearchesCheckAndCache.blockPosFindClosestMatch(level, body, 8, 4, IS_VALID_REPELLENT_PREDICATE, true)); // Shiroha - Lithium Optimizes Piglin repellent search. + Optional nemesis = Optional.empty(); + Optional huntableHoglin = Optional.empty(); + Optional babyHoglin = Optional.empty(); diff --git a/shiroha-server/minecraft-patches/features/0080-Lithium-Optimize-remove-block-goal.patch b/shiroha-server/minecraft-patches/features/0080-Lithium-Optimize-remove-block-goal.patch new file mode 100644 index 0000000..3dcd27c --- /dev/null +++ b/shiroha-server/minecraft-patches/features/0080-Lithium-Optimize-remove-block-goal.patch @@ -0,0 +1,39 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: NanaChiyo0721 +Date: Sat, 25 Jul 2026 21:33:58 +0800 +Subject: [PATCH] Lithium Optimize remove block goal + + +diff --git a/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java b/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java +index ff0f99d4b560a1c0721885b0eba5bd5ab2087bd7..90beeee7b74324f81690776546713b4f04887f75 100644 +--- a/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java ++++ b/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java +@@ -25,6 +25,19 @@ public class RemoveBlockGoal extends MoveToBlockGoal { + private final Mob removerMob; + private int ticksSinceReachedGoal; + private static final int WAIT_AFTER_BLOCK_FOUND = 20; ++ // Shiroha start - Lithium Optimize remove block goal ++ private static final java.util.function.BiPredicate IS_VALID_TARGET_ABOVE_BIPREDICATE = ++ RemoveBlockGoal::lithium$isValidTargetAbove; ++ //Split check condition in order to use maybeHas ++ private boolean lithium$isValidTargetBlock(net.minecraft.world.level.block.state.BlockState blockState){ ++ return blockState.is(this.blockToRemove); ++ } ++ ++ 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(); ++ } ++ // Shiroha end - Lithium Optimize remove block goal + + public RemoveBlockGoal(final Block blockToRemove, final PathfinderMob mob, final double speedModifier, final int verticalSearchRange) { + super(mob, speedModifier, 24, verticalSearchRange); +@@ -39,7 +52,7 @@ public class RemoveBlockGoal extends MoveToBlockGoal { + } else if (this.nextStartTick > 0) { + this.nextStartTick--; + return false; +- } else if (this.findNearestBlock()) { ++ } else if (((net.caffeinemc.mods.lithium.common.ai.non_poi_block_search.LithiumMoveToBlockGoal) this).lithium$findNearestBlock(this::lithium$isValidTargetBlock, IS_VALID_TARGET_ABOVE_BIPREDICATE, false)) { // Shiroha - Lithium Optimize remove block goal + this.nextStartTick = reducedTickDelay(20); + return true; + } else { diff --git a/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/CheckAndCacheBlockChecker.java b/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/CheckAndCacheBlockChecker.java new file mode 100644 index 0000000..e71122b --- /dev/null +++ b/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/CheckAndCacheBlockChecker.java @@ -0,0 +1,159 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ +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.getMinSectionY(); + } + + 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/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/CommonBlockSearchesCheckAndCache.java b/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/CommonBlockSearchesCheckAndCache.java new file mode 100644 index 0000000..9fcbed0 --- /dev/null +++ b/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/CommonBlockSearchesCheckAndCache.java @@ -0,0 +1,48 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ +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/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/LithiumMoveToBlockGoal.java b/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/LithiumMoveToBlockGoal.java new file mode 100644 index 0000000..20b4285 --- /dev/null +++ b/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/LithiumMoveToBlockGoal.java @@ -0,0 +1,30 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ +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/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/NonPOISearchDistances.java b/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/NonPOISearchDistances.java new file mode 100644 index 0000000..58d94d4 --- /dev/null +++ b/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/ai/non_poi_block_search/NonPOISearchDistances.java @@ -0,0 +1,68 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ +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 int dX = Distances.getClosestBlockCoordInSection(center.getX(), chunkX) - center.getX(); + final int dZ = Distances.getClosestBlockCoordInSection(center.getZ(), chunkZ) - 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/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/util/Distances.java b/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/util/Distances.java new file mode 100644 index 0000000..fa7d48b --- /dev/null +++ b/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/util/Distances.java @@ -0,0 +1,83 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ +package net.caffeinemc.mods.lithium.common.util; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; + +public class Distances { + + public static long getMinChunkToBlockDistanceL2Sq(BlockPos origin, int chunkX, int chunkZ) { + int chunkMinX = SectionPos.sectionToBlockCoord(chunkX); + int chunkMinZ = SectionPos.sectionToBlockCoord(chunkZ); + + int xDistance = origin.getX() - chunkMinX; + if (xDistance > 0) { + xDistance = Math.max(0, xDistance - 15); + } + int zDistance = origin.getZ() - chunkMinZ; + if (zDistance > 0) { + zDistance = Math.max(0, zDistance - 15); + } + + return (long) xDistance * (long) xDistance + (long) zDistance * (long) zDistance; + } + + public static BlockPos getClosestPosInChunk(BlockPos origin, int chunkX, int chunkZ) { + int closestX = getClosestBlockCoordInSection(origin.getX(), chunkX); + int closestZ = getClosestBlockCoordInSection(origin.getZ(), chunkZ); + return new BlockPos(closestX, origin.getY(), closestZ); + } + + public static boolean isWithinCubeRadius(BlockPos origin, int radius, BlockPos pos) { + return Math.abs(pos.getX() - origin.getX()) <= radius && + Math.abs(pos.getZ() - origin.getZ()) <= radius; + } + + public static boolean isWithinSphereRadius(BlockPos origin, long radiusSq, BlockPos pos) { + return distanceSq(origin, pos) <= radiusSq; + } + + public static int getClosestBlockCoordInSection(int blockCoord, int sectionCoord) { + final int minBlockInSection = SectionPos.sectionToBlockCoord(sectionCoord); + return Math.min(Math.max(blockCoord, minBlockInSection), minBlockInSection + 15); + } + + public static long getMinSectionDistanceSq(BlockPos origin, int chunkX, int chunkY, int chunkZ) { + int originX = origin.getX(), originY = origin.getY(), originZ = origin.getZ(); + long distX = getClosestBlockCoordInSection(originX, chunkX) - originX; + long distY = getClosestBlockCoordInSection(originY, chunkY) - originY; + long distZ = getClosestBlockCoordInSection(originZ, chunkZ) - originZ; + + return distX * distX + distY * distY + distZ * distZ; + } + + public static long distanceSq(BlockPos a, BlockPos b) { + long dx = a.getX() - b.getX(); + long dy = a.getY() - b.getY(); + long dz = a.getZ() - b.getZ(); + return dx * dx + dy * dy + dz * dz; + } + + public static int distanceSqInt(BlockPos a, BlockPos b) { + int dx = a.getX() - b.getX(); + int dy = a.getY() - b.getY(); + int dz = a.getZ() - b.getZ(); + // Check overflows to avoid silent incorrect results + return Math.addExact(Math.addExact(Math.multiplyExact(dx, dx), Math.multiplyExact(dy, dy)), Math.multiplyExact(dz, dz)); + } +} diff --git a/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/util/Pos.java b/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/util/Pos.java new file mode 100644 index 0000000..e4ba7b1 --- /dev/null +++ b/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/util/Pos.java @@ -0,0 +1,109 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ +package net.caffeinemc.mods.lithium.common.util; + +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.LevelHeightAccessor; + +public class Pos { + + public static class BlockCoord { + public static int getYSize(LevelHeightAccessor view) { + return view.getHeight(); + } + public static int getMinY(LevelHeightAccessor view) { + return view.getMinY(); + } + public static int getMaxYInclusive(LevelHeightAccessor view) { + return view.getMaxY(); + } + public static int getMaxYExclusive(LevelHeightAccessor view) { + return view.getMaxY() + 1; + } + + public static int getMaxInSectionCoord(int sectionCoord) { + return 15 + getMinInSectionCoord(sectionCoord); + } + + public static int getMaxYInSectionIndex(LevelHeightAccessor view, int sectionIndex){ + return getMaxInSectionCoord(SectionYCoord.fromSectionIndex(view, sectionIndex)); + } + + public static int getMinInSectionCoord(int sectionCoord) { + return SectionPos.sectionToBlockCoord(sectionCoord); + } + + public static int getMinYInSectionIndex(LevelHeightAccessor view, int sectionIndex) { + return getMinInSectionCoord(SectionYCoord.fromSectionIndex(view, sectionIndex)); + } + } + + public static class ChunkCoord { + public static int fromBlockCoord(int blockCoord) { + return SectionPos.blockToSectionCoord(blockCoord); + } + + public static int fromBlockSize(int i) { + return i >> 4; //same method as fromBlockCoord, just be clear about coord/size semantic difference + } + } + + public static class SectionYCoord { + public static int getNumYSections(LevelHeightAccessor view) { + return view.getSectionsCount(); + } + public static int getMinYSection(LevelHeightAccessor view) { + return view.getMinSectionY(); + } + public static int getMaxYSectionInclusive(LevelHeightAccessor view) { + return view.getMaxSectionY(); + } + public static int getMaxYSectionExclusive(LevelHeightAccessor view) { + return view.getMaxSectionY() + 1; + } + + public static int fromSectionIndex(LevelHeightAccessor view, int sectionCoord) { + return sectionCoord + SectionYCoord.getMinYSection(view); + } + public static int fromBlockCoord(int blockCoord) { + return SectionPos.blockToSectionCoord(blockCoord); + } + } + + public static class SectionYIndex { + public static int getNumYSections(LevelHeightAccessor view) { + return view.getSectionsCount(); + } + public static int getMinYSectionIndex(LevelHeightAccessor view) { + return 0; + } + public static int getMaxYSectionIndexInclusive(LevelHeightAccessor view) { + return view.getSectionsCount() - 1; + } + public static int getMaxYSectionIndexExclusive(LevelHeightAccessor view) { + return view.getSectionsCount(); + } + + + public static int fromSectionCoord(LevelHeightAccessor view, int sectionCoord) { + return sectionCoord - SectionYCoord.getMinYSection(view); + } + public static int fromBlockCoord(LevelHeightAccessor view, int blockCoord) { + return fromSectionCoord(view, SectionPos.blockToSectionCoord(blockCoord)); + } + } +} diff --git a/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/util/collections/FixedChunkAccessSectionBitBuffer.java b/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/util/collections/FixedChunkAccessSectionBitBuffer.java new file mode 100644 index 0000000..6aefb6a --- /dev/null +++ b/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/util/collections/FixedChunkAccessSectionBitBuffer.java @@ -0,0 +1,189 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ +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.pack(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.pack(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.pack(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/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/util/collections/HashedReferenceList.java b/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/util/collections/HashedReferenceList.java new file mode 100644 index 0000000..f6235e0 --- /dev/null +++ b/shiroha-server/src/main/java/net/caffeinemc/mods/lithium/common/util/collections/HashedReferenceList.java @@ -0,0 +1,293 @@ +/* + * This file is part of Lithium + * + * Lithium is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Lithium is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Lithium. If not, see . + */ +package net.caffeinemc.mods.lithium.common.util.collections; + +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceArrayList; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +/** + * Wraps a {@link List} with a hash table which provides O(1) lookups for {@link Collection#contains(Object)}. The type + * contained by this list must use reference-equality semantics. + */ +@SuppressWarnings("SuspiciousMethodCalls") +public class HashedReferenceList implements List { + private final ReferenceArrayList list; + private final Reference2IntOpenHashMap counter; + + public HashedReferenceList(Collection list) { + this.list = new ReferenceArrayList<>(); + this.list.addAll(list); + + this.counter = new Reference2IntOpenHashMap<>(); + this.counter.defaultReturnValue(0); + + for (T obj : this.list) { + this.counter.addTo(obj, 1); + } + } + + @Override + public int size() { + return this.list.size(); + } + + @Override + public boolean isEmpty() { + return this.list.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return this.counter.containsKey(o); + } + + @Override + public Iterator iterator() { + return this.listIterator(); + } + + @Override + public Object[] toArray() { + return this.list.toArray(); + } + + @Override + public T1[] toArray(T1 @NotNull [] a) { + return this.list.toArray(a); + } + + @Override + public boolean add(T t) { + this.trackReferenceAdded(t); + + return this.list.add(t); + } + + @Override + public boolean remove(Object o) { + this.trackReferenceRemoved(o); + + return this.list.remove(o); + } + + @Override + public boolean containsAll(Collection c) { + for (Object obj : c) { + if (!this.counter.containsKey(obj)) { + return false; + } + } + + return true; + } + + @Override + public boolean addAll(Collection c) { + for (T obj : c) { + this.trackReferenceAdded(obj); + } + + return this.list.addAll(c); + } + + @Override + public boolean addAll(int index, Collection c) { + for (T obj : c) { + this.trackReferenceAdded(obj); + } + + return this.list.addAll(index, c); + } + + @Override + public boolean removeAll(@NotNull Collection c) { + if (this.size() >= 2 && c.size() > 4 && c instanceof List) { + //HashReferenceList uses reference equality, so using ReferenceOpenHashSet is fine + c = new ReferenceOpenHashSet<>(c); + } + this.counter.keySet().removeAll(c); + return this.list.removeAll(c); + } + + @Override + public boolean retainAll(@NotNull Collection c) { + this.counter.keySet().retainAll(c); + return this.list.retainAll(c); + } + + @Override + public void clear() { + this.counter.clear(); + this.list.clear(); + } + + @Override + public T get(int index) { + return this.list.get(index); + } + + @Override + public T set(int index, T element) { + T prev = this.list.set(index, element); + + if (prev != element) { + if (prev != null) { + this.trackReferenceRemoved(prev); + } + + this.trackReferenceAdded(element); + } + + return prev; + } + + @Override + public void add(int index, T element) { + this.trackReferenceAdded(element); + + this.list.add(index, element); + } + + @Override + public T remove(int index) { + T prev = this.list.remove(index); + + if (prev != null) { + this.trackReferenceRemoved(prev); + } + + return prev; + } + + @Override + public int indexOf(Object o) { + return this.list.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return this.list.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return this.listIterator(0); + } + + @Override + public ListIterator listIterator(int index) { + return new ListIterator<>() { + private final ListIterator inner = HashedReferenceList.this.list.listIterator(index); + + @Override + public boolean hasNext() { + return this.inner.hasNext(); + } + + @Override + public T next() { + return this.inner.next(); + } + + @Override + public boolean hasPrevious() { + return this.inner.hasPrevious(); + } + + @Override + public T previous() { + return this.inner.previous(); + } + + @Override + public int nextIndex() { + return this.inner.nextIndex(); + } + + @Override + public int previousIndex() { + return this.inner.previousIndex(); + } + + @Override + public void remove() { + int last = this.previousIndex(); + + if (last == -1) { + throw new NoSuchElementException(); + } + + T prev = HashedReferenceList.this.get(last); + + if (prev != null) { + HashedReferenceList.this.trackReferenceRemoved(prev); + } + + this.inner.remove(); + } + + @Override + public void set(T t) { + int last = this.previousIndex(); + + if (last == -1) { + throw new NoSuchElementException(); + } + + T prev = HashedReferenceList.this.get(last); + + if (prev != t) { + if (prev != null) { + HashedReferenceList.this.trackReferenceRemoved(prev); + } + + HashedReferenceList.this.trackReferenceAdded(t); + } + + this.inner.remove(); + } + + @Override + public void add(T t) { + HashedReferenceList.this.trackReferenceAdded(t); + + this.inner.add(t); + } + }; + } + + @Override + public List subList(int fromIndex, int toIndex) { + return this.list.subList(fromIndex, toIndex); + } + + private void trackReferenceAdded(T t) { + this.counter.addTo(t, 1); + } + + @SuppressWarnings("unchecked") + private void trackReferenceRemoved(Object o) { + if (this.counter.addTo((T) o, -1) <= 1) { + this.counter.removeInt(o); + } + } + +}