diff --git a/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelContainer.java b/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelContainer.java index 0e17ffdf..19d67ef4 100644 --- a/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelContainer.java +++ b/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelContainer.java @@ -11,6 +11,8 @@ import dev.ryanhcode.sable.sublevel.storage.SubLevelOccupancySavedData; import dev.ryanhcode.sable.sublevel.storage.SubLevelRemovalReason; import dev.ryanhcode.sable.util.iterator.ListBackedFilterIterator; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.ObjectList; import net.minecraft.client.multiplayer.ClientLevel; @@ -61,6 +63,16 @@ public abstract class SubLevelContainer { */ private final List observers = new ObjectArrayList<>(); + /** + * A per-tick cache of every chunk position that is occupied by at least one sub-level. + * Rebuilt from scratch each tick in {@link #tick()} so that moving sub-levels + * are tracked correctly. Used by {@link #isChunkOccupied(int, int)} for O(1) lookups. + * + * @see #rebuildOccupiedChunksCache() + * @see #isChunkOccupied(int, int) + */ + private final LongSet occupiedChunks = new LongOpenHashSet(); + /** * The level of the plotgrid */ @@ -143,6 +155,10 @@ public void tick() { this.processSubLevelRemovals(); this.observers.forEach(observer -> observer.tick(this)); + + // Rebuild the occupied-chunk cache so that entity-getter fast-paths + // always see up-to-date positions (sub-levels can move every tick). + this.rebuildOccupiedChunksCache(); } /** @@ -528,4 +544,42 @@ public void removeSubLevel(final SubLevel subLevel, final SubLevelRemovalReason public BitSet getOccupancy() { return this.occupancy; } + + // ───────────────────────────────────────────────────── + // Occupied-chunk cache (fast entity-getter path) + // ───────────────────────────────────────────────────── + + /** + * Rebuilds the occupied-chunk cache from scratch. + * Called every tick so that moving/rotating sub-levels are tracked correctly. + * + *

The cache is a simple {@link LongSet} of chunk-position-encoded longs + * ({@link ChunkPos#asLong(int, int)}). It is consumed by + * {@link #isChunkOccupied(int, int)} which runs in O(1) time.

+ */ + private void rebuildOccupiedChunksCache() { + this.occupiedChunks.clear(); + for (final SubLevel sub : this.allSubLevels) { + final LevelPlot plot = sub.getPlot(); + final ChunkPos min = plot.getChunkMin(); + final ChunkPos max = plot.getChunkMax(); + for (int cx = min.x; cx <= max.x; cx++) { + for (int cz = min.z; cz <= max.z; cz++) { + this.occupiedChunks.add(ChunkPos.asLong(cx, cz)); + } + } + } + } + + /** + * Checks whether any loaded sub-level occupies the given chunk position. + *

This is backed by the per-tick cache and completes in O(1) time.

+ * + * @param chunkX the global chunk X coordinate + * @param chunkZ the global chunk Z coordinate + * @return {@code true} if at least one sub-level occupies this chunk + */ + public boolean isChunkOccupied(final int chunkX, final int chunkZ) { + return this.occupiedChunks.contains(ChunkPos.asLong(chunkX, chunkZ)); + } } diff --git a/common/src/main/java/dev/ryanhcode/sable/util/SubLevelInclusiveLevelEntityGetter.java b/common/src/main/java/dev/ryanhcode/sable/util/SubLevelInclusiveLevelEntityGetter.java index c02e6a3c..235adc21 100644 --- a/common/src/main/java/dev/ryanhcode/sable/util/SubLevelInclusiveLevelEntityGetter.java +++ b/common/src/main/java/dev/ryanhcode/sable/util/SubLevelInclusiveLevelEntityGetter.java @@ -2,6 +2,7 @@ import dev.ryanhcode.sable.Sable; import dev.ryanhcode.sable.api.SubLevelHelper; +import dev.ryanhcode.sable.api.sublevel.SubLevelContainer; import dev.ryanhcode.sable.companion.math.BoundingBox3d; import dev.ryanhcode.sable.sublevel.SubLevel; import net.minecraft.util.AbortableIterationConsumer; @@ -37,6 +38,43 @@ private static void logError(final AABB aabb) { Sable.LOGGER.error("Aborting entity get for abnormally large AABB: {}", aabb, new Throwable("Stack Trace")); } + /** + * Quick test: does any sub-level exist within (or overlap) this AABB? + * + *

Uses a two-level short-circuit:

+ *
    + *
  1. Dimension-level — if this world has zero loaded sub-levels, skip entirely.
  2. + *
  3. Chunk-level — consult the per-tick occupied-chunk cache + * ({@link SubLevelContainer#isChunkOccupied(int, int)}) for every chunk the AABB + * touches. The cache is rebuilt once per tick and provides O(1) lookups.
  4. + *
+ * + * @return {@code true} if at least one sub-level intersects the given AABB + */ + private boolean hasSubLevelInAABB(final AABB aabb) { + final SubLevelContainer container = SubLevelContainer.getContainer(this.level); + // ① No sub-levels in this dimension at all → fast skip + if (container == null || container.getLoadedCount() == 0) { + return false; + } + + // ② Convert the AABB to chunk-range and probe the cache + final int minCX = (int) Math.floor(aabb.minX) >> 4; + final int maxCX = (int) Math.floor(aabb.maxX) >> 4; + final int minCZ = (int) Math.floor(aabb.minZ) >> 4; + final int maxCZ = (int) Math.floor(aabb.maxZ) >> 4; + + for (int cx = minCX; cx <= maxCX; cx++) { + for (int cz = minCZ; cz <= maxCZ; cz++) { + if (container.isChunkOccupied(cx, cz)) { + return true; + } + } + } + + return false; + } + @Override public @Nullable T get(final int i) { return this.delegate.get(i); @@ -64,6 +102,13 @@ public void get(AABB aABB, final Consumer consumer) { return; } + // ── fast path: no sub-level in this region → vanilla behaviour ── + if (!this.hasSubLevelInAABB(aABB)) { + this.delegate.get(aABB, consumer); + return; + } + + // ── slow path: sub-level-aware traversal ── final SubLevel subLevel = Sable.HELPER.getContaining(this.level, aABB.getCenter()); this.delegate.get(aABB, consumer); @@ -96,6 +141,13 @@ public void get(final EntityTypeTest entityTypeTest, AABB aA return; } + // ── fast path: no sub-level in this region → vanilla behaviour ── + if (!this.hasSubLevelInAABB(aABB)) { + this.delegate.get(entityTypeTest, aABB, abortableIterationConsumer); + return; + } + + // ── slow path: sub-level-aware traversal ── final SubLevel subLevel = Sable.HELPER.getContaining(this.level, aABB.getCenter()); this.delegate.get(entityTypeTest, aABB, abortableIterationConsumer); @@ -113,7 +165,7 @@ public void get(final EntityTypeTest entityTypeTest, AABB aA continue; } - final AABB localBounds = bb.set(aABB).transformInverse(otherSubLevel.logicalPose(), bb).toMojang(); + final AABB localBounds = bb.set(aABB).transformInverse(otherSubLevel.logicalPose(), bakedMatrix, bb).toMojang(); this.delegate.get(entityTypeTest, localBounds, abortableIterationConsumer); }