Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -61,6 +63,16 @@ public abstract class SubLevelContainer {
*/
private final List<SubLevelObserver> 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
*/
Expand Down Expand Up @@ -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();
}

/**
Expand Down Expand Up @@ -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.
*
* <p>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.</p>
*/
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.
* <p>This is backed by the per-tick cache and completes in O(1) time.</p>
*
* @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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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?
*
* <p>Uses a two-level short-circuit:</p>
* <ol>
* <li><b>Dimension-level</b> — if this world has zero loaded sub-levels, skip entirely.</li>
* <li><b>Chunk-level</b> — 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.</li>
* </ol>
*
* @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);
Expand Down Expand Up @@ -64,6 +102,13 @@ public void get(AABB aABB, final Consumer<T> 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);
Expand Down Expand Up @@ -96,6 +141,13 @@ public <U extends T> void get(final EntityTypeTest<T, U> 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);

Expand All @@ -113,7 +165,7 @@ public <U extends T> void get(final EntityTypeTest<T, U> 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);
}
Expand Down