From 4e0254154f05476ec7e66f44378bb62f7a348f69 Mon Sep 17 00:00:00 2001 From: James Seibel Date: Mon, 20 Mar 2023 07:16:35 -0500 Subject: [PATCH] Overhaul the QuadTree object Previously the quad tree was closer to a 3D array than a traditional quadTree. This change brings it closer to a traditional quad tree. --- .../core/generation/WorldGenerationQueue.java | 182 ++++++----- .../com/seibel/lod/core/pos/DhLodPos.java | 2 + .../com/seibel/lod/core/pos/DhSectionPos.java | 26 +- .../lod/core/render/RenderBufferHandler.java | 2 +- .../util/gridList/MovableGridRingList.java | 77 ++--- .../lod/core/util/objects/QuadTree.java | 266 ---------------- .../core/util/objects/quadTree/QuadNode.java | 258 ++++++++++++++++ .../core/util/objects/quadTree/QuadTree.java | 251 +++++++++++++++ core/src/test/java/tests/QuadTreeTest.java | 290 ++++++++++++++++++ 9 files changed, 965 insertions(+), 389 deletions(-) delete mode 100644 core/src/main/java/com/seibel/lod/core/util/objects/QuadTree.java create mode 100644 core/src/main/java/com/seibel/lod/core/util/objects/quadTree/QuadNode.java create mode 100644 core/src/main/java/com/seibel/lod/core/util/objects/quadTree/QuadTree.java create mode 100644 core/src/test/java/tests/QuadTreeTest.java diff --git a/core/src/main/java/com/seibel/lod/core/generation/WorldGenerationQueue.java b/core/src/main/java/com/seibel/lod/core/generation/WorldGenerationQueue.java index be649c1dc..e978a0fc6 100644 --- a/core/src/main/java/com/seibel/lod/core/generation/WorldGenerationQueue.java +++ b/core/src/main/java/com/seibel/lod/core/generation/WorldGenerationQueue.java @@ -9,8 +9,7 @@ import com.seibel.lod.core.dependencyInjection.SingletonInjector; import com.seibel.lod.core.generation.tasks.*; import com.seibel.lod.core.pos.*; import com.seibel.lod.core.util.ThreadUtil; -import com.seibel.lod.core.util.gridList.MovableGridRingList; -import com.seibel.lod.core.util.objects.QuadTree; +import com.seibel.lod.core.util.objects.quadTree.QuadTree; import com.seibel.lod.core.util.objects.UncheckedInterruptedException; import com.seibel.lod.core.logging.DhLoggerBuilder; import com.seibel.lod.core.util.LodUtil; @@ -22,6 +21,7 @@ import java.io.Closeable; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; public class WorldGenerationQueue implements Closeable @@ -210,7 +210,7 @@ public class WorldGenerationQueue implements Closeable { AtomicInteger numberOfTasksRemoved = new AtomicInteger(); - this.waitingTaskQuadTree.setCenterPos(targetBlockPos, (worldGenTask) -> { numberOfTasksRemoved.getAndIncrement(); }); + this.waitingTaskQuadTree.setCenterBlockPos(targetBlockPos, (worldGenTask) -> { numberOfTasksRemoved.getAndIncrement(); }); // if (numberOfTasksRemoved.get() != 0) // { @@ -218,25 +218,25 @@ public class WorldGenerationQueue implements Closeable // } } - /** Removes all {@link WorldGenTask}'s and {@link WorldGenTaskGroup}'s that have been garbage collected. */ - private void removeGarbageCollectedTasks() // TODO remove, potential mystery errors caused by garbage collection isn't worth it (and may not be necessary any more now that we are using a quad tree to hold the tasks). // also this is very slow with the curent quad tree impelmentation - { - for (byte detailLevel = QuadTree.TREE_LOWEST_DETAIL_LEVEL; detailLevel < this.waitingTaskQuadTree.treeMaxDetailLevel; detailLevel++) - { - MovableGridRingList gridRingList = this.waitingTaskQuadTree.getRingList(detailLevel); - Iterator taskIterator = gridRingList.iterator(); - while (taskIterator.hasNext()) - { - // go through each WorldGenTask in the TaskGroup - WorldGenTask genTask = taskIterator.next(); - if (genTask != null && !genTask.taskTracker.isMemoryAddressValid()) - { - taskIterator.remove(); - genTask.future.complete(WorldGenResult.CreateFail()); - } - } - } - } +// /** Removes all {@link WorldGenTask}'s and {@link WorldGenTaskGroup}'s that have been garbage collected. */ +// private void removeGarbageCollectedTasks() // TODO remove, potential mystery errors caused by garbage collection isn't worth it (and may not be necessary any more now that we are using a quad tree to hold the tasks). // also this is very slow with the curent quad tree impelmentation +// { +// for (byte detailLevel = QuadTree.TREE_LOWEST_DETAIL_LEVEL; detailLevel < this.waitingTaskQuadTree.treeMaxDetailLevel; detailLevel++) +// { +// MovableGridRingList gridRingList = this.waitingTaskQuadTree.getRingList(detailLevel); +// Iterator taskIterator = gridRingList.iterator(); +// while (taskIterator.hasNext()) +// { +// // go through each WorldGenTask in the TaskGroup +// WorldGenTask genTask = taskIterator.next(); +// if (genTask != null && !genTask.taskTracker.isMemoryAddressValid()) +// { +// taskIterator.remove(); +// genTask.future.complete(WorldGenResult.CreateFail()); +// } +// } +// } +// } /** * @param targetPos the position to center the generation around @@ -244,53 +244,71 @@ public class WorldGenerationQueue implements Closeable */ private boolean startNextWorldGenTask(DhBlockPos2D targetPos) { - WorldGenTask closestTask = null; + final AtomicReference closestTaskRef = new AtomicReference<>(null); - // look through the tree from lowest to highest detail level to find the next task to generate - for (byte detailLevel = QuadTree.TREE_LOWEST_DETAIL_LEVEL; detailLevel < this.waitingTaskQuadTree.treeMaxDetailLevel; detailLevel++) + // TODO improve + this.waitingTaskQuadTree.forEachRootNode((rootQuadNode) -> { - // look for the task that is closest to the targetPos - long closestGenDist = Long.MAX_VALUE; - - MovableGridRingList gridRingList = this.waitingTaskQuadTree.getRingList(detailLevel); - for (WorldGenTask newGenTask : gridRingList) + if (closestTaskRef.get() == null) { - if (newGenTask != null) + rootQuadNode.forAllLeafValues((worldGenTask) -> { - if (queueFirstGenerationRequestFound) + if (closestTaskRef.get() == null) { - // queue the first task we can find - closestTask = newGenTask; - break; + closestTaskRef.set(worldGenTask); } - else - { - // use chebyShev distance in order to generate in rings around the target pos (also because it is a fast distance calculation) - int chebDistToTargetPos = newGenTask.pos.getCenterBlockPos().toPos2D().chebyshevDist(targetPos.toPos2D()); - if (chebDistToTargetPos < closestGenDist) - { - // this task is closer than the last one - closestTask = newGenTask; - closestGenDist = chebDistToTargetPos; - } - else if (closestTask != null) - { - // this task is farther than the last one, - // assume we have gotten as close as we can - // and queue the task - break; - } - } - } + }); } - - // a task has been found, don't look at the next detail level, - // everything there will be farther away - if (closestTask != null) - { - break; - } - } + }); + + WorldGenTask closestTask = closestTaskRef.get(); + + +// // look through the tree from lowest to highest detail level to find the next task to generate +// for (byte detailLevel = QuadTree.TREE_LOWEST_DETAIL_LEVEL; detailLevel < this.waitingTaskQuadTree.treeMaxDetailLevel; detailLevel++) +// { +// // look for the task that is closest to the targetPos +// long closestGenDist = Long.MAX_VALUE; +// +// MovableGridRingList gridRingList = this.waitingTaskQuadTree.getRingList(detailLevel); +// for (WorldGenTask newGenTask : gridRingList) +// { +// if (newGenTask != null) +// { +// if (queueFirstGenerationRequestFound) +// { +// // queue the first task we can find +// closestTask = newGenTask; +// break; +// } +// else +// { +// // use chebyShev distance in order to generate in rings around the target pos (also because it is a fast distance calculation) +// int chebDistToTargetPos = newGenTask.pos.getCenterBlockPos().toPos2D().chebyshevDist(targetPos.toPos2D()); +// if (chebDistToTargetPos < closestGenDist) +// { +// // this task is closer than the last one +// closestTask = newGenTask; +// closestGenDist = chebDistToTargetPos; +// } +// else if (closestTask != null) +// { +// // this task is farther than the last one, +// // assume we have gotten as close as we can +// // and queue the task +// break; +// } +// } +// } +// } +// +// // a task has been found, don't look at the next detail level, +// // everything there will be farther away +// if (closestTask != null) +// { +// break; +// } +// } @@ -303,7 +321,7 @@ public class WorldGenerationQueue implements Closeable // remove the task we found, we are going to start it and don't want to run it multiple times - WorldGenTask removedWorldGenTask = this.waitingTaskQuadTree.set(closestTask.pos.detailLevel, closestTask.pos.x, closestTask.pos.z, null); + WorldGenTask removedWorldGenTask = this.waitingTaskQuadTree.set(new DhSectionPos(closestTask.pos.detailLevel, closestTask.pos.x, closestTask.pos.z), null); // removedWorldGenTask can be null // TODO when? @@ -351,9 +369,9 @@ public class WorldGenerationQueue implements Closeable childFutures.add(newFuture); WorldGenTask newGenTask = new WorldGenTask(new DhLodPos(childDhSectionPos.sectionDetailLevel, childDhSectionPos.sectionX, childDhSectionPos.sectionZ), childDhSectionPos.sectionDetailLevel, removedWorldGenTask.taskTracker, newFuture); - this.waitingTaskQuadTree.set(childDhSectionPos.sectionDetailLevel, childDhSectionPos.sectionX, childDhSectionPos.sectionZ, newGenTask); + this.waitingTaskQuadTree.set(new DhSectionPos(childDhSectionPos.sectionDetailLevel, childDhSectionPos.sectionX, childDhSectionPos.sectionZ), newGenTask); - boolean valueAdded = this.waitingTaskQuadTree.get(childDhSectionPos.sectionDetailLevel, childDhSectionPos.sectionX, childDhSectionPos.sectionZ) != null; + boolean valueAdded = this.waitingTaskQuadTree.get(new DhSectionPos(childDhSectionPos.sectionDetailLevel, childDhSectionPos.sectionX, childDhSectionPos.sectionZ)) != null; LodUtil.assertTrue(valueAdded); // failed to add world gen task to quad tree, this means the quad tree was the wrong size // LOGGER.info("split feature "+sectionPos+" into "+childDhSectionPos+" "+(valueAdded ? "added" : "notAdded")); @@ -376,7 +394,7 @@ public class WorldGenerationQueue implements Closeable LodUtil.assertTrue(taskDetailLevel >= this.minDataDetail && taskDetailLevel <= this.maxDataDetail); DhChunkPos chunkPosMin = new DhChunkPos(taskPos.getCornerBlockPos()); -// LOGGER.info("Generating section "+taskPos+" with granularity "+granularity+" at "+chunkPosMin); + LOGGER.info("Generating section "+taskPos+" with granularity "+granularity+" at "+chunkPosMin); this.numberOfTasksQueued++; inProgressTaskGroup.genFuture = startGenerationEvent(this.generator, chunkPosMin, granularity, taskDetailLevel, inProgressTaskGroup.group::onGenerationComplete); @@ -414,22 +432,22 @@ public class WorldGenerationQueue implements Closeable queueingThread.shutdownNow(); // remove any incomplete generation tasks - for (byte detailLevel = QuadTree.TREE_LOWEST_DETAIL_LEVEL; detailLevel < this.waitingTaskQuadTree.treeMaxDetailLevel; detailLevel++) - { - MovableGridRingList ringList = this.waitingTaskQuadTree.getRingList(detailLevel); - ringList.clear((worldGenTask) -> - { - if (worldGenTask != null) - { - try - { - worldGenTask.future.cancel(true); - } - catch (CancellationException ignored) - { /* don't log shutdown exceptions */ } - } - }); - } +// for (byte detailLevel = QuadTree.TREE_LOWEST_DETAIL_LEVEL; detailLevel < this.waitingTaskQuadTree.treeMaxDetailLevel; detailLevel++) +// { +// MovableGridRingList ringList = this.waitingTaskQuadTree.getRingList(detailLevel); +// ringList.clear((worldGenTask) -> +// { +// if (worldGenTask != null) +// { +// try +// { +// worldGenTask.future.cancel(true); +// } +// catch (CancellationException ignored) +// { /* don't log shutdown exceptions */ } +// } +// }); +// } // stop and remove any in progress tasks diff --git a/core/src/main/java/com/seibel/lod/core/pos/DhLodPos.java b/core/src/main/java/com/seibel/lod/core/pos/DhLodPos.java index 8c27be564..91bebd028 100644 --- a/core/src/main/java/com/seibel/lod/core/pos/DhLodPos.java +++ b/core/src/main/java/com/seibel/lod/core/pos/DhLodPos.java @@ -28,6 +28,8 @@ public class DhLodPos implements Comparable this.x = x; this.z = z; } + public DhLodPos(DhSectionPos sectionPos) { this(sectionPos.sectionDetailLevel, sectionPos.sectionX, sectionPos.sectionZ); } + diff --git a/core/src/main/java/com/seibel/lod/core/pos/DhSectionPos.java b/core/src/main/java/com/seibel/lod/core/pos/DhSectionPos.java index d1dccbaf0..693420169 100644 --- a/core/src/main/java/com/seibel/lod/core/pos/DhSectionPos.java +++ b/core/src/main/java/com/seibel/lod/core/pos/DhSectionPos.java @@ -46,7 +46,8 @@ public class DhSectionPos this.sectionZ = sectionZ; } - public DhSectionPos(DhBlockPos blockPos) + public DhSectionPos(DhBlockPos blockPos) { this(new DhBlockPos2D(blockPos)); } + public DhSectionPos(DhBlockPos2D blockPos) { DhLodPos lodPos = new DhLodPos(LodUtil.BLOCK_DETAIL_LEVEL, blockPos.x, blockPos.z); lodPos = lodPos.convertToDetailLevel(SECTION_BLOCK_DETAIL_LEVEL); @@ -76,7 +77,7 @@ public class DhSectionPos /** Returns the center for the highest detail level (0) */ - public DhLodPos getCenter() { return this.getCenter((byte) 0); } + public DhLodPos getCenter() { return this.getCenter((byte) 0); } // TODO why does this use detail level 0 instead of this object's detail level? public DhLodPos getCenter(byte returnDetailLevel) { LodUtil.assertTrue(returnDetailLevel <= this.sectionDetailLevel, "returnDetailLevel must be less than sectionDetail"); @@ -110,6 +111,15 @@ public class DhSectionPos return new DhLodUnit(this.sectionDetailLevel, BitShiftUtil.powerOfTwo(offset)); } + /** uses the absolute detail level aka detail levels like {@link LodUtil#CHUNK_DETAIL_LEVEL} instead of the dhSectionPos detaillevels */ // TODO comment + public DhSectionPos convertToDetailLevel(byte newSectionDetailLevel) + { + DhLodPos lodPos = new DhLodPos(this.sectionDetailLevel, this.sectionX, this.sectionZ); + lodPos = lodPos.convertToDetailLevel(newSectionDetailLevel); + + DhSectionPos newPos = new DhSectionPos(newSectionDetailLevel, lodPos); + return newPos; + } /** * Returns the DhLodPos 1 detail level lower

@@ -159,6 +169,18 @@ public class DhSectionPos /** NOTE: This does not consider yOffset! */ public boolean overlaps(DhSectionPos other) { return this.getSectionBBoxPos().overlapsExactly(other.getSectionBBoxPos()); } + /** NOTE: This does not consider yOffset! */ + public boolean contains(DhSectionPos otherPos) + { + DhBlockPos2D otherCornerBlockPos = otherPos.getCorner(LodUtil.BLOCK_DETAIL_LEVEL).getCornerBlockPos(); + + DhBlockPos2D thisMinBlockPos = this.getCorner(LodUtil.BLOCK_DETAIL_LEVEL).getCornerBlockPos(); + DhBlockPos2D thisMaxBlockPos = new DhBlockPos2D(thisMinBlockPos.x + this.getWidth().toBlockWidth(), thisMinBlockPos.z + this.getWidth().toBlockWidth()); + + return thisMinBlockPos.x <= otherCornerBlockPos.x && otherCornerBlockPos.x <= thisMaxBlockPos.x && + thisMinBlockPos.z <= otherCornerBlockPos.z && otherCornerBlockPos.z <= thisMaxBlockPos.z; + } + /** Serialize() is different from toString() as it must NEVER be changed, and should be in a short format */ public String serialize() { return "[" + this.sectionDetailLevel + ',' + this.sectionX + ',' + this.sectionZ + ']'; } diff --git a/core/src/main/java/com/seibel/lod/core/render/RenderBufferHandler.java b/core/src/main/java/com/seibel/lod/core/render/RenderBufferHandler.java index d29ce237f..655304320 100644 --- a/core/src/main/java/com/seibel/lod/core/render/RenderBufferHandler.java +++ b/core/src/main/java/com/seibel/lod/core/render/RenderBufferHandler.java @@ -36,7 +36,7 @@ public class RenderBufferHandler MovableGridRingList referenceList = quadTree.getRingListForDetailLevel((byte) (quadTree.getNumbersOfSectionDetailLevels() - 1)); Pos2D center = referenceList.getCenter(); - this.renderBufferNodesGridList = new MovableGridRingList<>(referenceList.getHalfSize(), center); + this.renderBufferNodesGridList = new MovableGridRingList<>(referenceList.getHalfWidth(), center); } diff --git a/core/src/main/java/com/seibel/lod/core/util/gridList/MovableGridRingList.java b/core/src/main/java/com/seibel/lod/core/util/gridList/MovableGridRingList.java index a630a32e9..d14faca0a 100644 --- a/core/src/main/java/com/seibel/lod/core/util/gridList/MovableGridRingList.java +++ b/core/src/main/java/com/seibel/lod/core/util/gridList/MovableGridRingList.java @@ -34,8 +34,9 @@ public class MovableGridRingList extends ArrayList implements List private final AtomicReference minPosRef = new AtomicReference<>(); /** width of this grid list */ - private final int size; - private final int halfSize; + private final int width; + /** radius or half-width of this grid list */ + private final int halfWidth; private final ReentrantReadWriteLock moveLock = new ReentrantReadWriteLock(); @@ -48,14 +49,14 @@ public class MovableGridRingList extends ArrayList implements List // constructors // //==============// - public MovableGridRingList(int halfSize, Pos2D center) { this(halfSize, center.x, center.y); } - public MovableGridRingList(int halfSize, int centerX, int centerY) + public MovableGridRingList(int halfWidth, Pos2D center) { this(halfWidth, center.x, center.y); } + public MovableGridRingList(int halfWidth, int centerX, int centerY) { - super((halfSize * 2 + 1) * (halfSize * 2 + 1)); + super((halfWidth * 2 + 1) * (halfWidth * 2 + 1)); - this.size = halfSize * 2 + 1; - this.halfSize = halfSize; - this.minPosRef.set(new Pos2D(centerX-halfSize, centerY-halfSize)); + this.width = halfWidth * 2 + 1; + this.halfWidth = halfWidth; + this.minPosRef.set(new Pos2D(centerX- halfWidth, centerY- halfWidth)); this.clear(); } @@ -204,9 +205,9 @@ public class MovableGridRingList extends ArrayList implements List } super.clear(); - super.ensureCapacity(this.size * this.size); + super.ensureCapacity(this.width * this.width); // TODO why are we filling the array will nulls? everything should already be null after the clear - for (int i = 0; i < this.size * this.size; i++) + for (int i = 0; i < this.width * this.width; i++) { super.add(null); } @@ -227,8 +228,8 @@ public class MovableGridRingList extends ArrayList implements List public boolean moveTo(int newCenterX, int newCenterY, Consumer removedItemConsumer, BiConsumer nullableRemovedItemConsumer) { Pos2D cPos = this.minPosRef.get(); - int newMinX = newCenterX - this.halfSize; - int newMinY = newCenterY - this.halfSize; + int newMinX = newCenterX - this.halfWidth; + int newMinY = newCenterY - this.halfWidth; if (cPos.x == newMinX && cPos.y == newMinY) { return false; @@ -248,22 +249,22 @@ public class MovableGridRingList extends ArrayList implements List // if the x or z offset is equal to or greater than // the total width, just delete the current data // and update the pos - if (Math.abs(deltaX) >= this.size || Math.abs(deltaY) >= this.size) + if (Math.abs(deltaX) >= this.width || Math.abs(deltaY) >= this.width) { this.clear(removedItemConsumer); } else { - for (int x = 0; x < this.size; x++) + for (int x = 0; x < this.width; x++) { - for (int y = 0; y < this.size; y++) + for (int y = 0; y < this.width; y++) { Pos2D itemPos = new Pos2D(x+cPos.x, y+cPos.y); if (x - deltaX < 0 || y - deltaY < 0 - || x - deltaX >= this.size - || y - deltaY >= this.size) + || x - deltaX >= this.width + || y - deltaY >= this.width) { T item = this._swapUnsafe(itemPos.x, itemPos.y, null); if (item != null && removedItemConsumer != null) @@ -299,13 +300,13 @@ public class MovableGridRingList extends ArrayList implements List // position getters // //==================// - public Pos2D getCenter() { return new Pos2D(this.minPosRef.get().x + this.halfSize, this.minPosRef.get().y + this.halfSize); } + public Pos2D getCenter() { return new Pos2D(this.minPosRef.get().x + this.halfWidth, this.minPosRef.get().y + this.halfWidth); } public Pos2D getMinPosInRange() { return this.minPosRef.get(); } - public Pos2D getMaxPosInRange() { return new Pos2D(this.minPosRef.get().x + this.size-1, this.minPosRef.get().y + this.size-1); } + public Pos2D getMaxPosInRange() { return new Pos2D(this.minPosRef.get().x + this.width -1, this.minPosRef.get().y + this.width -1); } - public int getSize() { return this.size; } - public int getHalfSize() { return this.halfSize; } + public int getWidth() { return this.width; } + public int getHalfWidth() { return this.halfWidth; } @@ -321,22 +322,22 @@ public class MovableGridRingList extends ArrayList implements List { Pos2D minPos = this.minPosRef.get(); return (x>=minPos.x - && x=minPos.y - && y=min.x - && x=min.y - && y extends ArrayList implements List try { Pos2D min = this.minPosRef.get(); - for (int x = min.x; x < min.x + this.size; x++) + for (int x = min.x; x < min.x + this.width; x++) { - for (int y = min.y; y < min.y + this.size; y++) + for (int y = min.y; y < min.y + this.width; y++) { T t = this._getUnsafe(x, y); consumer.accept(t, new Pos2D(x, y)); @@ -463,12 +464,12 @@ public class MovableGridRingList extends ArrayList implements List private void createRingIteratorList() { this.ringPositionIteratorArray = null; - Pos2D[] posArray = new Pos2D[this.size*this.size]; + Pos2D[] posArray = new Pos2D[this.width *this.width]; int i = 0; - for (int xPos = -this.halfSize; xPos <= this.halfSize; xPos++) + for (int xPos = -this.halfWidth; xPos <= this.halfWidth; xPos++) { - for (int zPos = -this.halfSize; zPos <= this.halfSize; zPos++) + for (int zPos = -this.halfWidth; zPos <= this.halfWidth; zPos++) { posArray[i] = new Pos2D(xPos, zPos); i++; @@ -485,12 +486,12 @@ public class MovableGridRingList extends ArrayList implements List for (int j = 0; j < posArray.length; j++) { - posArray[j] = posArray[j].add(new Pos2D(this.halfSize, this.halfSize)); + posArray[j] = posArray[j].add(new Pos2D(this.halfWidth, this.halfWidth)); } for (Pos2D pos2D : posArray) { - LodUtil.assertTrue(pos2D.x >= 0 && pos2D.x < this.size); - LodUtil.assertTrue(pos2D.y >= 0 && pos2D.y < this.size); + LodUtil.assertTrue(pos2D.x >= 0 && pos2D.x < this.width); + LodUtil.assertTrue(pos2D.y >= 0 && pos2D.y < this.width); } this.ringPositionIteratorArray = posArray; @@ -507,7 +508,7 @@ public class MovableGridRingList extends ArrayList implements List public String toString() { Pos2D p = this.minPosRef.get(); - return this.getClass().getSimpleName() + "[" + (p.x+this.halfSize) + "," + (p.y+this.halfSize) + "] " + this.size + "*" + this.size + "[" + this.size() + "]"; + return this.getClass().getSimpleName() + "[" + (p.x+this.halfWidth) + "," + (p.y+this.halfWidth) + "] " + this.width + "*" + this.width + "[" + this.size() + "]"; } public String toDetailString() @@ -522,7 +523,7 @@ public class MovableGridRingList extends ArrayList implements List str.append(t != null ? t.toString() : "NULL"); str.append(", "); i++; - if (i % this.size == 0) + if (i % this.width == 0) { str.append("\n"); } diff --git a/core/src/main/java/com/seibel/lod/core/util/objects/QuadTree.java b/core/src/main/java/com/seibel/lod/core/util/objects/QuadTree.java deleted file mode 100644 index f9096e883..000000000 --- a/core/src/main/java/com/seibel/lod/core/util/objects/QuadTree.java +++ /dev/null @@ -1,266 +0,0 @@ -package com.seibel.lod.core.util.objects; - -import com.seibel.lod.core.dataObjects.render.ColumnRenderSource; -import com.seibel.lod.core.logging.DhLoggerBuilder; -import com.seibel.lod.core.pos.DhBlockPos2D; -import com.seibel.lod.core.pos.DhSectionPos; -import com.seibel.lod.core.pos.Pos2D; -import com.seibel.lod.core.render.LodQuadTree; -import com.seibel.lod.core.util.BitShiftUtil; -import com.seibel.lod.core.util.DetailDistanceUtil; -import com.seibel.lod.core.util.LodUtil; -import com.seibel.lod.core.util.gridList.MovableGridRingList; -import org.apache.logging.log4j.Logger; - -import java.util.Iterator; -import java.util.function.Consumer; - -/** - * This class represents a quadTree of T type values. - */ -public class QuadTree -{ - /** - * Note: all config values should be via the class that extends this class, and - * by implementing different abstract methods - */ - public static final byte TREE_LOWEST_DETAIL_LEVEL = ColumnRenderSource.SECTION_SIZE_OFFSET; - - private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - - - public final byte getLayerDetailLevelOffset() { return ColumnRenderSource.SECTION_SIZE_OFFSET; } - public final byte getLayerDetailLevel(byte sectionDetailLevel) { return (byte) (sectionDetailLevel - this.getLayerDetailLevelOffset()); } - - public final byte getLayerSectionDetailOffset() { return ColumnRenderSource.SECTION_SIZE_OFFSET; } - public final byte getLayerSectionDetail(byte dataDetail) { return (byte) (dataDetail + this.getLayerSectionDetailOffset()); } - - - /** AKA how many detail levels are in this quad tree */ - public final byte numbersOfSectionDetailLevels; - /** related to {@link QuadTree#numbersOfSectionDetailLevels}, the largest number detail level in this tree. */ - public final byte treeMaxDetailLevel; - - /** contain the actual data in the quad tree structure */ - private final MovableGridRingList[] ringLists; - - public final int blockRenderDistance; - - DhBlockPos2D centerBlockPos; - - - - /** - * Constructor of the quadTree - * @param viewDistance View distance in blocks - */ - public QuadTree( - int viewDistance, - DhBlockPos2D centerBlockPos) - { - DetailDistanceUtil.updateSettings(); //TODO: Move this to somewhere else - this.blockRenderDistance = viewDistance; - this.centerBlockPos = centerBlockPos; - - - // Calculate the max section detail level // - - byte maxDetailLevel = this.getMaxDetailLevelInRange(viewDistance * Math.sqrt(2)); - this.treeMaxDetailLevel = this.getLayerSectionDetail(maxDetailLevel); - this.numbersOfSectionDetailLevels = (byte) (this.treeMaxDetailLevel + 1); - this.ringLists = new MovableGridRingList[this.numbersOfSectionDetailLevels - TREE_LOWEST_DETAIL_LEVEL]; - - - - // Construct the ringLists // - - LOGGER.info("Creating "+MovableGridRingList.class.getSimpleName()+" with player center at "+this.centerBlockPos); - for (byte sectionDetailLevel = TREE_LOWEST_DETAIL_LEVEL; sectionDetailLevel < this.numbersOfSectionDetailLevels; sectionDetailLevel++) - { - byte targetDetailLevel = this.getLayerDetailLevel(sectionDetailLevel); - int maxDist = this.getFurthestBlockDistanceForDetailLevel(targetDetailLevel); - - // TODO temp fix that may or may not allocate the right amount, but it works well enough for now -// int halfSize = MathUtil.ceilDiv(maxDist, BitShiftUtil.powerOfTwo(sectionDetailLevel)) + 8; // +8 to make sure the section is fully contained in the ringList //TODO what does the "8" represent? - int halfSize = BitShiftUtil.powerOfTwo(this.treeMaxDetailLevel-targetDetailLevel); //MathUtil.ceilDiv(maxDist, sectionDetailLevel) + 8; // +8 to make sure the section is fully contained in the ringList //TODO what does the "8" represent? - - // check that the detail level and position are valid - DhSectionPos checkedPos = new DhSectionPos(sectionDetailLevel, halfSize, halfSize); - byte checkedDetailLevel = this.calculateExpectedDetailLevel(this.centerBlockPos, checkedPos); - // validate the detail level - LodUtil.assertTrue(checkedDetailLevel > targetDetailLevel, - "in "+sectionDetailLevel+", getFurthestDistance would return "+maxDist+" which would be contained in range "+(halfSize-2)+", but calculateExpectedDetailLevel at "+checkedPos+" is "+checkedDetailLevel+" <= "+targetDetailLevel); - - - // create the new ring list - Pos2D ringListCenterPos = new Pos2D(BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, sectionDetailLevel), BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, sectionDetailLevel)); - - LOGGER.info("Creating "+MovableGridRingList.class.getSimpleName()+" centered on "+ringListCenterPos+" with halfSize ["+halfSize+"] (maxDist ["+maxDist+"], dataDetail ["+targetDetailLevel+"])"); - this.ringLists[sectionDetailLevel - TREE_LOWEST_DETAIL_LEVEL] = new MovableGridRingList<>(halfSize, ringListCenterPos.x, ringListCenterPos.y); - - } - - }// constructor - - - - //=====================// - // getters and setters // - //=====================// - - /** @return the value at the given section position */ - public final T get(DhSectionPos pos) { return this.get(pos.sectionDetailLevel, pos.sectionX, pos.sectionZ); } - /** - * @param detailLevel detail level of the section - * @param x x coordinate of the section - * @param z z coordinate of the section - * @return the value for the given section position - */ - public final T get(byte detailLevel, int x, int z) { return this.ringLists[detailLevel - TREE_LOWEST_DETAIL_LEVEL].get(x, z); } - - - /** @return the value that was previously in the given position, null if nothing */ - public final T set(DhSectionPos pos, T value) { return this.set(pos.sectionDetailLevel, pos.sectionX, pos.sectionZ, value); } - /** @return the value that was previously in the given position, null if nothing */ - public final T set(byte detailLevel, int x, int z, T value) - { - T previousValue = this.get(detailLevel, x, z); - this.ringLists[detailLevel - TREE_LOWEST_DETAIL_LEVEL].set(x, z, value); - return previousValue; - } - - - - //===============// - // raw ringLists // - //===============// - - /** - * This method returns the RingList for the given detail level - * @apiNote The returned ringList should not be modified!
TODO why? could it cause concurrent modification exceptions? is this only the case for {@link LodQuadTree}? - * @param detailLevel the detail level - * @return the RingList - */ - public final MovableGridRingList getRingList(byte detailLevel) { return this.ringLists[detailLevel - TREE_LOWEST_DETAIL_LEVEL]; } - - public Iterator getRingListIterator(byte detailLevel) { return this.getRingList(detailLevel).iterator(); } - - - - //================// - // get/set center // - //================// - - public void setCenterPos(DhBlockPos2D newCenterPos) { this.setCenterPos(newCenterPos, null); } - public void setCenterPos(DhBlockPos2D newCenterPos, Consumer removedItemConsumer) - { - this.centerBlockPos = newCenterPos; - - // recenter the grid lists if necessary - for (int sectionDetailLevel = TREE_LOWEST_DETAIL_LEVEL; sectionDetailLevel < this.numbersOfSectionDetailLevels; sectionDetailLevel++) - { - Pos2D expectedCenterPos = new Pos2D(BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, sectionDetailLevel), BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, sectionDetailLevel)); - MovableGridRingList gridList = this.ringLists[sectionDetailLevel - TREE_LOWEST_DETAIL_LEVEL]; - - if (!gridList.getCenter().equals(expectedCenterPos)) - { - gridList.moveTo(expectedCenterPos.x, expectedCenterPos.y, removedItemConsumer); - } - } - } - - public final DhBlockPos2D getCenterPos() { return this.centerBlockPos; } - - - - - - //===========================// - // detail level calculations // - //===========================// - - /** - * This method will compute the detail level based on target position and section pos. - * @param targetPos can be the player's position. A reference for calculating the detail level - * @return detail level of this section pos - */ - public final byte calculateExpectedDetailLevel(DhBlockPos2D targetPos, DhSectionPos sectionPos) - { - return DetailDistanceUtil.getDetailLevelFromDistance( - targetPos.dist(sectionPos.getCenter().getCenterBlockPos())); - } - - /** - * Returns the highest detail level in a circle around the center.
- * Note: the returned distance should always be the ceiling estimation of the circleRadius. - * @return the highest detail level in the circle - */ - public final byte getMaxDetailLevelInRange(double circleRadius) { return DetailDistanceUtil.getDetailLevelFromDistance(circleRadius); } - - /** - * Returns the furthest distance to the center for the given detail level.
- * Note: the returned distance should always be the ceiling estimation of the circleRadius. - * @return the furthest distance to the center, in blocks - */ - public final int getFurthestBlockDistanceForDetailLevel(byte detailLevl) - { - return (int)Math.ceil(DetailDistanceUtil.getDrawDistanceFromDetail(detailLevl + 1)); - // +1 because that's the border to the next detail level, and we want to include up to it. - } - - /** Given a section pos at level n this method returns the parent value at level n+1 */ - public final T getParentValue(DhSectionPos pos) { return this.get(pos.getParentPos()); } - - /** - * Given a section pos at level n and a child index, this returns the child section at level n-1 - * @param child0to3 since there are 4 possible children this index identifies which one we are getting - */ - public final T getChildValue(DhSectionPos pos, int child0to3) { return this.get(pos.getChildByIndex(child0to3)); } - - - - //==============// - // base methods // - //==============// - - public boolean isEmpty() - { - for (byte detailLevel = QuadTree.TREE_LOWEST_DETAIL_LEVEL; detailLevel < this.treeMaxDetailLevel; detailLevel++) - { - if (!isDetailLevelEmpty(detailLevel)) - { - return false; - } - } - - return true; - } - public boolean isDetailLevelEmpty(byte detailLevel) { return this.getRingList(detailLevel).isEmpty(); } - - /** returns the number of items in this QuadTree */ - public int size() - { - int size = 0; - for (byte detailLevel = QuadTree.TREE_LOWEST_DETAIL_LEVEL; detailLevel < this.treeMaxDetailLevel; detailLevel++) - { - size += getRingList(detailLevel).size(); - } - - return size; - } - - - public String getDebugString() - { - StringBuilder sb = new StringBuilder(); - for (byte i = 0; i < this.ringLists.length; i++) - { - sb.append("Layer ").append(i + TREE_LOWEST_DETAIL_LEVEL).append(":\n"); - sb.append(this.ringLists[i].toDetailString()); - sb.append("\n"); - sb.append("\n"); - } - return sb.toString(); - } - -} diff --git a/core/src/main/java/com/seibel/lod/core/util/objects/quadTree/QuadNode.java b/core/src/main/java/com/seibel/lod/core/util/objects/quadTree/QuadNode.java new file mode 100644 index 000000000..f2478876a --- /dev/null +++ b/core/src/main/java/com/seibel/lod/core/util/objects/quadTree/QuadNode.java @@ -0,0 +1,258 @@ +package com.seibel.lod.core.util.objects.quadTree; + +import com.seibel.lod.core.logging.DhLoggerBuilder; +import com.seibel.lod.core.pos.DhLodPos; +import com.seibel.lod.core.pos.DhSectionPos; +import org.apache.logging.log4j.Logger; + +import java.util.function.Consumer; + +public class QuadNode +{ + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + + + public DhSectionPos sectionPos; + public T value; + + + /** + * North West
+ * index 0
+ * relative pos (0,0) + */ + public QuadNode nwChild; + /** + * North East
+ * index 1
+ * relative (1,0) + */ + public QuadNode neChild; + /** + * South West
+ * index 2
+ * relative (0,1) + */ + public QuadNode swChild; + /** + * South East
+ * index 3
+ * relative (1,1) + */ + public QuadNode seChild; + + + + + + public QuadNode(DhSectionPos sectionPos) + { + this.sectionPos = sectionPos; + } + + + /** @return the number of non-null child nodes */ + public int childCount() + { + int count = 0; + for (int i = 0; i < 4; i++) + { + if (this.getChildByIndex(i) != null) + { + count++; + } + } + return count; + } + + + + /** + * Returns the DhLodPos 1 detail level lower

+ * + * Relative child positions returned for each index:
+ * 0 = (0,0)
+ * 1 = (1,0)
+ * 2 = (0,1)
+ * 3 = (1,1)
+ * + * @param child0to3 must be an int between 0 and 3 + */ + public QuadNode getChildByIndex(int child0to3) throws IllegalArgumentException + { + switch (child0to3) + { + case 0: + return nwChild; + case 1: + return neChild; + case 2: + return swChild; + case 3: + return seChild; + + default: + throw new IllegalArgumentException("child0to3 must be between 0 and 3"); + } + } + + + /** + * @param sectionPos must be 1 detail level lower than this node's detail level + * @throws IllegalArgumentException if childSectionPos has the wrong detail level or is outside the bounds of this node + * @return the node at the given position + */ + public T getValue(DhSectionPos sectionPos) throws IllegalArgumentException { return this.getOrSetValue(sectionPos, false, null); } + /** + * @param sectionPos must be 1 detail level lower than this node's detail level + * @throws IllegalArgumentException if childSectionPos has the wrong detail level or is outside the bounds of this node + * @return the node at the given position before the new node was set + */ + public T setValue(DhSectionPos sectionPos, T newValue) throws IllegalArgumentException { return this.getOrSetValue(sectionPos, true, newValue); } + /** + * @param inputSectionPos must be 1 detail level lower than this node's detail level + * @throws IllegalArgumentException if childSectionPos has the wrong detail level or is outside the bounds of this + * @return the node at the given position before the new node was set (if the new node should be set) + */ + private T getOrSetValue(DhSectionPos inputSectionPos, boolean replaceValue, T newValue) throws IllegalArgumentException + { + if (!this.sectionPos.contains(inputSectionPos)) + { + LOGGER.error((replaceValue ? "set " : "get ")+inputSectionPos+" center block: "+inputSectionPos.getCenter().getCornerBlockPos()+", this pos: "+this.sectionPos+" this center block: "+this.sectionPos.getCenter().getCornerBlockPos()); + throw new IllegalArgumentException("Input section pos outside of this quadNode's range: "+this.sectionPos+" width: "+this.sectionPos.getWidth()+" input detail level: "+inputSectionPos+" width: "+inputSectionPos.getWidth()); + } + + if (inputSectionPos.sectionDetailLevel > this.sectionPos.sectionDetailLevel) + { + throw new IllegalArgumentException("detail level higher than this node. Node Detail level: "+this.sectionPos.sectionDetailLevel+" input detail level: "+inputSectionPos.sectionDetailLevel); + } + + if (inputSectionPos.sectionDetailLevel == this.sectionPos.sectionDetailLevel && !inputSectionPos.equals(this.sectionPos)) + { + throw new IllegalArgumentException("Node and input detail level are equal, however positions are not; this tree doesn't contain the requested position. Node pos: "+this.sectionPos+", input pos: "+inputSectionPos); + } + + if (inputSectionPos.sectionDetailLevel == this.sectionPos.sectionDetailLevel) + { + // this node is the requested position + T returnValue = this.value; + if (replaceValue) + { + this.value = newValue; + } + return returnValue; + } + else + { + // this node is a parent to the position requested, + // recurse to the next node + +// LOGGER.info((replaceValue ? "set " : "get ")+inputSectionPos+" center block: "+inputSectionPos.getCenter().getCornerBlockPos()+", this pos: "+this.sectionPos+" this center block: "+this.sectionPos.getCenter().getCornerBlockPos()); + + DhLodPos nodeCenterPos = this.sectionPos.getCenter(); //.convertToDetailLevel((byte)0).getCenter(); + DhLodPos inputCenterPos = inputSectionPos.getCenter(); //.convertToDetailLevel((byte)0).getCenter(); + + // may or may not be at the requested detail level + QuadNode childNode; + if (inputCenterPos.x <= nodeCenterPos.x) + { + if (inputCenterPos.z <= nodeCenterPos.z) + { + // TODO merge duplicate code + if (replaceValue && this.nwChild == null) + { + // if no node exists for this position, but we want to insert a new value at this position, create a new node + this.nwChild = new QuadNode<>(this.sectionPos.getChildByIndex(0)); + } +// LOGGER.info("NW"); + childNode = this.nwChild; + } + else + { + if (replaceValue && this.neChild == null) + { + this.neChild = new QuadNode<>(this.sectionPos.getChildByIndex(2)); + } +// LOGGER.info("NE"); + childNode = this.neChild; + } + } + else + { + if (inputCenterPos.z <= nodeCenterPos.z) + { + if (replaceValue && this.swChild == null) + { + this.swChild = new QuadNode<>(this.sectionPos.getChildByIndex(1)); + } +// LOGGER.info("SW"); + childNode = this.swChild; + } + else + { + if (replaceValue && this.seChild == null) + { + this.seChild = new QuadNode<>(this.sectionPos.getChildByIndex(3)); + } +// LOGGER.info("SE"); + childNode = this.seChild; + } + } + + + if (childNode == null) + { + // should only happen when replaceValue = false and the end of a node chain has been reached + return null; + } + else + { + return childNode.getOrSetValue(inputSectionPos, replaceValue, newValue); + } + } + } + + + + /** + * Applies the given consumer to all 4 of this nodes' children.
+ * Note: this will pass in null children. + */ + public void forEachDirectChild(Consumer> callback) + { + for (int i = 0; i < 4; i++) + { + callback.accept(this.getChildByIndex(i)); + } + } + + /** + * Applies the given consumer to all leaf nodes below this node.
+ * Note: this will pass in null values. + */ + public void forAllLeafValues(Consumer callback) + { + if (this.childCount() == 0) + { + // base case, bottom leaf node found + callback.accept(this.value); + } + else + { + for (int i = 0; i < 4; i++) + { + QuadNode childNode = this.getChildByIndex(i); + if (childNode != null) + { + // TODO should this pass in a null value if the child node is null? + childNode.forAllLeafValues(callback); + } + } + } + } + + + @Override + public String toString() { return "pos: "+this.sectionPos+", value: "+this.value; } + +} diff --git a/core/src/main/java/com/seibel/lod/core/util/objects/quadTree/QuadTree.java b/core/src/main/java/com/seibel/lod/core/util/objects/quadTree/QuadTree.java new file mode 100644 index 000000000..7102d1708 --- /dev/null +++ b/core/src/main/java/com/seibel/lod/core/util/objects/quadTree/QuadTree.java @@ -0,0 +1,251 @@ +package com.seibel.lod.core.util.objects.quadTree; + +import com.seibel.lod.core.logging.DhLoggerBuilder; +import com.seibel.lod.core.pos.DhBlockPos2D; +import com.seibel.lod.core.pos.DhSectionPos; +import com.seibel.lod.core.pos.Pos2D; +import com.seibel.lod.core.util.BitShiftUtil; +import com.seibel.lod.core.util.DetailDistanceUtil; +import com.seibel.lod.core.util.LodUtil; +import com.seibel.lod.core.util.gridList.MovableGridRingList; +import org.apache.logging.log4j.Logger; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +/** + * This class represents a quadTree of T type values. + */ +public class QuadTree +{ + public static final byte TREE_LOWEST_DETAIL_LEVEL = 0; + + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + + + + /** The largest number detail level in this tree. */ + public final byte treeMaxDetailLevel; + + /** contain the actual data in the quad tree structure */ + private final MovableGridRingList> topRingList; + + DhBlockPos2D centerBlockPos; + + + + /** + * Constructor of the quadTree + */ + public QuadTree( + int viewDistanceInBlocks, + DhBlockPos2D centerBlockPos) + { + DetailDistanceUtil.updateSettings(); //TODO: Move this to somewhere else + this.centerBlockPos = centerBlockPos; + + this.treeMaxDetailLevel = 10; // TODO we may need to make this dynamic // detail 10 = (2^10) 1024 blocks wide + +// int halfSize = 12; // TODO use this.treeMaxDetailLevel to determine + int halfSize = Math.floorDiv(viewDistanceInBlocks, 2) / BitShiftUtil.powerOfTwo(this.treeMaxDetailLevel); + halfSize = Math.max(halfSize, 1); // at minimum the ring list should have 3x3 (9) root nodes in it, to account for moving around + + Pos2D ringListCenterPos = new Pos2D( + BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, this.treeMaxDetailLevel), + BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, this.treeMaxDetailLevel)); + + this.topRingList = new MovableGridRingList<>(halfSize, ringListCenterPos.x, ringListCenterPos.y); + + }// constructor + + + + //=====================// + // getters and setters // + //=====================// + + /** @return the value at the given section position */ + public final T get(DhSectionPos pos) throws IndexOutOfBoundsException { return this.getOrSet(pos, false, null); } + /** @return the value that was previously in the given position, null if nothing */ + public final T set(DhSectionPos pos, T value) throws IndexOutOfBoundsException { return this.getOrSet(pos, true, value); } + + protected final T getOrSet(DhSectionPos pos, boolean setNewValue, T newValue) throws IndexOutOfBoundsException + { + if (this.isPositionInBounds(pos)) + { + DhSectionPos rootPos = pos.convertToDetailLevel(this.treeMaxDetailLevel); + int ringListPosX = rootPos.sectionX; + int ringListPosZ = rootPos.sectionZ; + + QuadNode topQuadNode = this.topRingList.get(ringListPosX, ringListPosZ); + if (topQuadNode == null) + { + topQuadNode = new QuadNode(rootPos); + boolean successfullyAdded = this.topRingList.set(ringListPosX, ringListPosZ, topQuadNode); + LodUtil.assertTrue(successfullyAdded, "Failed to add top quadTree node at position: "+rootPos); + } + + if (!topQuadNode.sectionPos.contains(pos)) + { + LodUtil.assertNotReach("failed to get a root node that contains the input position: "+pos+" root node pos: "+topQuadNode.sectionPos); + } + + + T returnValue = topQuadNode.getValue(pos); + if (setNewValue) + { + topQuadNode.setValue(pos, newValue); + } + return returnValue; + } + else + { + // TODO give the min and max allowed positions + throw new IndexOutOfBoundsException("QuadTree GetOrSet failed. Position out of bounds, given Position: "+pos); + } + } + + + private boolean isPositionInBounds(DhSectionPos pos) + { + DhSectionPos blockPos = pos.convertToDetailLevel(LodUtil.BLOCK_DETAIL_LEVEL); + + int halfWidthInBlocks = BitShiftUtil.powerOfTwo(this.treeMaxDetailLevel) * Math.floorDiv(this.topRingList.getWidth(), 2); + + int minX = this.centerBlockPos.x - halfWidthInBlocks; + int maxX = this.centerBlockPos.x + halfWidthInBlocks; + + int minZ = this.centerBlockPos.z - halfWidthInBlocks; + int maxZ = this.centerBlockPos.z + halfWidthInBlocks; + + return minX <= blockPos.sectionX && blockPos.sectionX < maxX && + minZ <= blockPos.sectionZ && blockPos.sectionZ < maxZ; + } + + + /** no nulls TODO */ + public void forEachRootNode(Consumer> consumer) + { + this.topRingList.forEachOrdered((rootNode) -> + { + if (rootNode != null) + { + consumer.accept(rootNode); + } + }); + } + + public void forEachLeafValue(Consumer consumer) + { + this.forEachRootNode((rootNode) -> + { + rootNode.forAllLeafValues(consumer); + }); + } + + + + + //================// + // get/set center // + //================// + + public void setCenterBlockPos(DhBlockPos2D newCenterPos) { this.setCenterBlockPos(newCenterPos, null); } + public void setCenterBlockPos(DhBlockPos2D newCenterPos, Consumer> removedItemConsumer) + { + this.centerBlockPos = newCenterPos; + + Pos2D expectedCenterPos = new Pos2D( + BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, this.treeMaxDetailLevel), + BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, this.treeMaxDetailLevel)); + + if (!this.topRingList.getCenter().equals(expectedCenterPos)) + { + this.topRingList.moveTo(expectedCenterPos.x, expectedCenterPos.y, removedItemConsumer); + } + } + + public final DhBlockPos2D getCenterBlockPos() { return this.centerBlockPos; } + + + + //===========================// + // detail level calculations // + //===========================// + + /** + * This method will compute the detail level based on target position and section pos. + * @param targetPos can be the player's position. A reference for calculating the detail level + * @return detail level of this section pos + */ + public final byte calculateExpectedDetailLevel(DhBlockPos2D targetPos, DhSectionPos sectionPos) + { + return DetailDistanceUtil.getDetailLevelFromDistance( + targetPos.dist(sectionPos.getCenter().getCenterBlockPos())); + } + + /** + * Returns the highest detail level in a circle around the center.
+ * Note: the returned distance should always be the ceiling estimation of the circleRadius. + * @return the highest detail level in the circle + */ + public final byte getMaxDetailLevelInRange(double circleRadius) { return DetailDistanceUtil.getDetailLevelFromDistance(circleRadius); } + + /** + * Returns the furthest distance to the center for the given detail level.
+ * Note: the returned distance should always be the ceiling estimation of the circleRadius. + * @return the furthest distance to the center, in blocks + */ + public final int getFurthestBlockDistanceForDetailLevel(byte detailLevl) + { + return (int)Math.ceil(DetailDistanceUtil.getDrawDistanceFromDetail(detailLevl + 1)); + // +1 because that's the border to the next detail level, and we want to include up to it. + } + + /** Given a section pos at level n this method returns the parent value at level n+1 */ + public final T getParentValue(DhSectionPos pos) { return this.get(pos.getParentPos()); } + + /** + * Given a section pos at level n and a child index, this returns the child section at level n-1 + * @param child0to3 since there are 4 possible children this index identifies which one we are getting + */ + public final T getChildValue(DhSectionPos pos, int child0to3) { return this.get(pos.getChildByIndex(child0to3)); } + + + + //==============// + // base methods // + //==============// + + public boolean isEmpty() { return this.leafNodeCount() == 0; } // TODO this should be rewritten to short-circuit + + public int leafNodeCount() + { + AtomicInteger count = new AtomicInteger(0); + this.topRingList.forEachPos((node, pos) -> + { + if (node != null) + { + node.forAllLeafValues((value) -> { count.addAndGet(1); }); + } + }); + + return count.get(); + } + + public int width() { return this.topRingList.getWidth(); } + +// public String getDebugString() +// { +// StringBuilder sb = new StringBuilder(); +// for (byte i = 0; i < this.ringLists.length; i++) +// { +// sb.append("Layer ").append(i + TREE_LOWEST_DETAIL_LEVEL).append(":\n"); +// sb.append(this.ringLists[i].toDetailString()); +// sb.append("\n"); +// sb.append("\n"); +// } +// return sb.toString(); +// } + +} diff --git a/core/src/test/java/tests/QuadTreeTest.java b/core/src/test/java/tests/QuadTreeTest.java new file mode 100644 index 000000000..92b00cd24 --- /dev/null +++ b/core/src/test/java/tests/QuadTreeTest.java @@ -0,0 +1,290 @@ +/* + * This file is part of the Distant Horizons mod (formerly the LOD Mod), + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2022 James Seibel + * + * This program 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, version 3. + * + * This program 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 this program. If not, see . + */ + +package tests; + +import com.seibel.lod.core.logging.DhLoggerBuilder; +import com.seibel.lod.core.pos.DhBlockPos2D; +import com.seibel.lod.core.pos.DhSectionPos; +import com.seibel.lod.core.util.BitShiftUtil; +import com.seibel.lod.core.util.LodUtil; +import com.seibel.lod.core.util.objects.quadTree.QuadTree; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.config.Configurator; +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This is just a quick demo to confirm the testing system is set up correctly. + * + * @author James Seibel + * @version 2022-9-5 + */ +public class QuadTreeTest +{ + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + + private static final int ROOT_NODE_WIDTH_IN_BLOCKS = BitShiftUtil.powerOfTwo(10); + private static final int MIN_TREE_WIDTH_IN_BLOCKS = ROOT_NODE_WIDTH_IN_BLOCKS * 8; + + static + { + Configurator.setRootLevel(Level.ALL); + } + + @Test + public void SectionPosTest() + { + DhSectionPos root = new DhSectionPos((byte)10, 0, 0); + DhSectionPos child = new DhSectionPos((byte)9, 1, 1); + + Assert.assertTrue("section pos contains fail", root.contains(child)); + Assert.assertFalse("section pos contains fail", child.contains(root)); + + + root = new DhSectionPos((byte)10, 1, 0); + child = new DhSectionPos((byte)9, 1, 1); + Assert.assertFalse("section pos contains fail", root.contains(child)); + child = new DhSectionPos((byte)9, 2, 2); + Assert.assertTrue("section pos contains fail", root.contains(child)); + + } + + @Test + public void BasicPositiveQuadTreeTest() + { + QuadTree tree = new QuadTree<>(MIN_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0)); + + + // root node // + testSet(tree, new DhSectionPos((byte)10, 0, 0), 0); + + // first child (0,0) // + testSet(tree, new DhSectionPos((byte)9, 0, 0), 1); + testSet(tree, new DhSectionPos((byte)9, 1, 0), 2); + testSet(tree, new DhSectionPos((byte)9, 0, 1), 3); + testSet(tree, new DhSectionPos((byte)9, 1, 1), 4); + + // second child (0,0) (0,0) // + testSet(tree, new DhSectionPos((byte)8, 0, 0), 5); + testSet(tree, new DhSectionPos((byte)8, 1, 0), 6); + testSet(tree, new DhSectionPos((byte)8, 0, 1), 7); + testSet(tree, new DhSectionPos((byte)8, 1, 1), 8); + // second child (0,0) (1,1) // + testSet(tree, new DhSectionPos((byte)8, 2, 2), 9); + testSet(tree, new DhSectionPos((byte)8, 3, 2), 10); + testSet(tree, new DhSectionPos((byte)8, 2, 3), 11); + testSet(tree, new DhSectionPos((byte)8, 3, 3), 12); + + // third child (0,0) (1,0) (0,0) // + testSet(tree, new DhSectionPos((byte)7, 5, 0), 9); + testSet(tree, new DhSectionPos((byte)7, 6, 0), 10); + testSet(tree, new DhSectionPos((byte)7, 5, 1), 11); + testSet(tree, new DhSectionPos((byte)7, 6, 1), 12); + + } + + @Test + public void BasicNegativeQuadTreeTest() + { + QuadTree tree = new QuadTree<>(MIN_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0)); + + + // root node // + testSet(tree, new DhSectionPos((byte)10, -1, -1), 0); + + // first child (-1,-1) // + testSet(tree, new DhSectionPos((byte)9, -2, -1), 1); + testSet(tree, new DhSectionPos((byte)9, -1, -1), 2); + testSet(tree, new DhSectionPos((byte)9, -2, -2), 3); + testSet(tree, new DhSectionPos((byte)9, -1, -2), 4); + + // TODO +// // second child (-1,-1) (0,0) // +// runTest(tree, new DhSectionPos((byte)8, 0, 0), 5); +// runTest(tree, new DhSectionPos((byte)8, 1, 0), 6); +// runTest(tree, new DhSectionPos((byte)8, 0, 1), 7); +// runTest(tree, new DhSectionPos((byte)8, 1, 1), 8); +// // second child (-1,-1) (1,1) // +// runTest(tree, new DhSectionPos((byte)8, 2, 2), 9); +// runTest(tree, new DhSectionPos((byte)8, 3, 2), 10); +// runTest(tree, new DhSectionPos((byte)8, 2, 3), 11); +// runTest(tree, new DhSectionPos((byte)8, 3, 3), 12); +// +// // third child (-1,-1) (1,0) (0,0) // +// runTest(tree, new DhSectionPos((byte)7, 5, 0), 9); +// runTest(tree, new DhSectionPos((byte)7, 6, 0), 10); +// runTest(tree, new DhSectionPos((byte)7, 5, 1), 11); +// runTest(tree, new DhSectionPos((byte)7, 6, 1), 12); + + } + + @Test + public void QuadTreeMovingTest() + { + int treeWidthInRootNodes = 8; + int treeWidthInBlocks = ROOT_NODE_WIDTH_IN_BLOCKS * treeWidthInRootNodes; + QuadTree tree = new QuadTree<>(treeWidthInBlocks, new DhBlockPos2D(0, 0)); + + + + // root nodes // + testSet(tree, new DhSectionPos((byte)10, 0, 0), 1); + + // first child (0,0) // + DhSectionPos nw = new DhSectionPos(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, 0, 0); + DhSectionPos ne = new DhSectionPos(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, 1, 0); + DhSectionPos sw = new DhSectionPos(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, 0, 1); + DhSectionPos se = new DhSectionPos(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, 1, 1); + + testSet(tree, nw, 2); + testSet(tree, ne, 3); + testSet(tree, sw, 4); + testSet(tree, se, 5); + Assert.assertEquals("incorrect leaf node count", tree.leafNodeCount(), 4); + + + // fake move // + tree.setCenterBlockPos(DhBlockPos2D.ZERO); + Assert.assertEquals("Tree center incorrect", DhBlockPos2D.ZERO, tree.getCenterBlockPos()); + + testGet(tree, nw, 2); + testGet(tree, ne, 3); + testGet(tree, sw, 4); + testGet(tree, se, 5); + Assert.assertEquals("incorrect leaf node count", tree.leafNodeCount(), 4); + + + // small move // + DhBlockPos2D smallMoveBlockPos = new DhBlockPos2D(ROOT_NODE_WIDTH_IN_BLOCKS *2, 0); // move enough that the original root nodes aren't touching the same grid squares they were before, but not far enough as to be garbage collected (TODO reword) + tree.setCenterBlockPos(smallMoveBlockPos); + Assert.assertEquals("Tree center incorrect", smallMoveBlockPos, tree.getCenterBlockPos()); + + // nodes should be found at the same locations + testGet(tree, nw, 2); + testGet(tree, ne, 3); + testGet(tree, sw, 4); + testGet(tree, se, 5); + Assert.assertEquals("incorrect leaf node count", tree.leafNodeCount(), 4); + + + + // big move // + DhBlockPos2D bigMoveBlockPos = new DhBlockPos2D(treeWidthInBlocks * 2, 0); + tree.setCenterBlockPos(bigMoveBlockPos); + Assert.assertEquals("Tree center incorrect", bigMoveBlockPos, tree.getCenterBlockPos()); + + // nothing should be found in the tree + Assert.assertThrows(IndexOutOfBoundsException.class, () -> testGet(tree, nw, null)); + Assert.assertThrows(IndexOutOfBoundsException.class, () -> testGet(tree, ne, null)); + Assert.assertThrows(IndexOutOfBoundsException.class, () -> testGet(tree, sw, null)); + Assert.assertThrows(IndexOutOfBoundsException.class, () -> testGet(tree, se, null)); + + Assert.assertEquals("incorrect leaf node count", tree.leafNodeCount(), 0); + + + + // edge move // + + // move back to the origin for easy testing + tree.setCenterBlockPos(DhBlockPos2D.ZERO); + Assert.assertEquals("Tree center incorrect", DhBlockPos2D.ZERO, tree.getCenterBlockPos()); + + // TODO move me + DhSectionPos outOfBoundsPos = new DhSectionPos(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, ROOT_NODE_WIDTH_IN_BLOCKS, 0); // wrong detail level on purpose, if the detail level was 0 (block) this should work + Assert.assertThrows("incorrect exception thrown ", IndexOutOfBoundsException.class, () -> testSet(tree, outOfBoundsPos, 2)); + Assert.assertEquals("incorrect leaf node count", 0, tree.leafNodeCount()); + + // 1 root node from the edge + DhSectionPos edgePos = new DhSectionPos(LodUtil.BLOCK_DETAIL_LEVEL, -((treeWidthInBlocks/2)-ROOT_NODE_WIDTH_IN_BLOCKS), 0); + testSet(tree, edgePos, 2); + Assert.assertEquals("incorrect leaf node count", 1, tree.leafNodeCount()); + + // edge move + DhBlockPos2D edgeMoveBlockPos = new DhBlockPos2D(ROOT_NODE_WIDTH_IN_BLOCKS, 0); // TODO I can only move this 1 root node away from the center for some reason + tree.setCenterBlockPos(edgeMoveBlockPos); + Assert.assertEquals("Tree center incorrect", edgeMoveBlockPos, tree.getCenterBlockPos()); + + Assert.assertEquals("incorrect leaf node count", 1, tree.leafNodeCount()); + + + } + + + @Test + public void QuadTreeIterationTest() + { + QuadTree tree = new QuadTree<>(MIN_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0)); + + + // root nodes // + testSet(tree, new DhSectionPos((byte)10, 0, 0), 1); + testSet(tree, new DhSectionPos((byte)10, 1, 0), 2); + + // first child (0,0) // + testSet(tree, new DhSectionPos((byte)9, 0, 0), 3); + testSet(tree, new DhSectionPos((byte)9, 1, 0), 4); + testSet(tree, new DhSectionPos((byte)9, 0, 1), 5); + testSet(tree, new DhSectionPos((byte)9, 1, 1), 6); + + + final AtomicInteger rootNodeCount = new AtomicInteger(0); + final AtomicInteger leafCount = new AtomicInteger(0); + final AtomicInteger leafValueSum = new AtomicInteger(0); + tree.forEachRootNode((rootNode) -> + { + rootNodeCount.addAndGet(1); + + rootNode.forAllLeafValues((leafValue) -> + { + leafCount.addAndGet(1); + leafValueSum.addAndGet(leafValue); + }); + }); + + Assert.assertEquals("incorrect root count", 2, rootNodeCount.get()); + Assert.assertEquals("incorrect leaf count", 5, leafCount.get()); + Assert.assertEquals("incorrect leaf value sum", 20, leafValueSum.get()); + + } + + + + private static void testSet(QuadTree tree, DhSectionPos pos, Integer value) + { + // set + Integer setResult = tree.set(pos, value); + Assert.assertNull("set failed "+pos, setResult); + // get + Integer getResult = tree.get(pos); + Assert.assertEquals("get failed "+pos, value, getResult); + } + + private static void testGet(QuadTree tree, DhSectionPos pos, Integer value) + { + // get + Integer getResult = tree.get(pos); + Assert.assertEquals("get failed "+pos, value, getResult); + } + + +}