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 index 2b5b1dbc5..a37287650 100644 --- 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 @@ -5,6 +5,7 @@ import com.seibel.lod.core.pos.DhSectionPos; import com.seibel.lod.core.util.LodUtil; import org.apache.logging.log4j.Logger; +import java.util.function.BiConsumer; import java.util.function.Consumer; public class QuadNode @@ -12,7 +13,8 @@ public class QuadNode private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - public DhSectionPos sectionPos; + public final DhSectionPos sectionPos; + public final byte minimumDetailLevel; public T value; @@ -45,9 +47,10 @@ public class QuadNode - public QuadNode(DhSectionPos sectionPos) + public QuadNode(DhSectionPos sectionPos, byte minimumDetailLevel) { this.sectionPos = sectionPos; + this.minimumDetailLevel = minimumDetailLevel; } @@ -116,6 +119,8 @@ public class QuadNode */ private T getOrSetValue(DhSectionPos inputSectionPos, boolean replaceValue, T newValue) throws IllegalArgumentException { + // debug validation + 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()); @@ -132,6 +137,14 @@ public class QuadNode 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.minimumDetailLevel) + { + throw new IllegalArgumentException("Input position is requesting a detail level lower than what this node can provide. Node minimum detail level: "+this.minimumDetailLevel+", input pos: "+inputSectionPos); + } + + + + // get/set logic if (inputSectionPos.sectionDetailLevel == this.sectionPos.sectionDetailLevel) { // this node is the requested position @@ -162,7 +175,7 @@ public class QuadNode 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<>(nwPos); + this.nwChild = new QuadNode<>(nwPos, this.minimumDetailLevel); } childNode = this.nwChild; @@ -175,7 +188,7 @@ public class QuadNode if (replaceValue && this.swChild == null) { // if no node exists for this position, but we want to insert a new value at this position, create a new node - this.swChild = new QuadNode<>(swPos); + this.swChild = new QuadNode<>(swPos, this.minimumDetailLevel); } childNode = this.swChild; @@ -188,7 +201,7 @@ public class QuadNode if (replaceValue && this.neChild == null) { // if no node exists for this position, but we want to insert a new value at this position, create a new node - this.neChild = new QuadNode<>(nePos); + this.neChild = new QuadNode<>(nePos, this.minimumDetailLevel); } childNode = this.neChild; @@ -201,7 +214,7 @@ public class QuadNode if (replaceValue && this.seChild == null) { // if no node exists for this position, but we want to insert a new value at this position, create a new node - this.seChild = new QuadNode<>(sePos); + this.seChild = new QuadNode<>(sePos, this.minimumDetailLevel); } childNode = this.seChild; @@ -221,11 +234,15 @@ public class QuadNode * Applies the given consumer to all 4 of this nodes' children.
* Note: this will pass in null children. */ - public void forEachDirectChild(Consumer> callback) + public void forEachDirectChild(BiConsumer, DhSectionPos> callback) { - for (int i = 0; i < 4; i++) + if (this.sectionPos.sectionDetailLevel != this.minimumDetailLevel) { - callback.accept(this.getChildByIndex(i)); + for (int i = 0; i < 4; i++) + { + DhSectionPos childPos = this.sectionPos.getChildByIndex(i); + callback.accept(this.getChildByIndex(i), childPos); + } } } @@ -235,7 +252,7 @@ public class QuadNode */ public void forAllLeafValues(Consumer callback) { - if (this.childCount() == 0) + if (this.childCount() == 0 || this.sectionPos.sectionDetailLevel == this.minimumDetailLevel) { // base case, bottom leaf node found callback.accept(this.value); @@ -254,6 +271,45 @@ public class QuadNode } } + public void deleteAllChildren() { this.deleteAllChildren(null); } + /** @param removedItemConsumer is only fired for non-null nodes, however the value passed in may be null */ + public void deleteAllChildren(Consumer removedItemConsumer) + { + for (int i = 0; i < 4; i++) + { + QuadNode childNode = this.getChildByIndex(i); + if (childNode != null) + { + childNode.deleteAllChildren(removedItemConsumer); + } + } + + + if (nwChild != null) + { + removedItemConsumer.accept(nwChild.value); + } + nwChild = null; + + if (neChild != null) + { + removedItemConsumer.accept(neChild.value); + } + neChild = null; + + if (seChild != null) + { + removedItemConsumer.accept(seChild.value); + } + seChild = null; + + if (swChild != null) + { + removedItemConsumer.accept(swChild.value); + } + swChild = null; + } + @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 index 0998e86e2..b6fe256ed 100644 --- 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 @@ -20,14 +20,14 @@ import java.util.function.Consumer; */ 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; + /** The smallest number detail level in this tree. */ + public final byte treeMinDetailLevel; /** contain the actual data in the quad tree structure */ private final MovableGridRingList> topRingList; @@ -42,13 +42,14 @@ public class QuadTree * Constructor of the quadTree * @param widthInBlocks equivalent to the distance between two opposing sides */ - public QuadTree(int widthInBlocks, DhBlockPos2D centerBlockPos) + public QuadTree(int widthInBlocks, DhBlockPos2D centerBlockPos, byte treeMinDetailLevel) { DetailDistanceUtil.updateSettings(); //TODO: Move this to somewhere else this.centerBlockPos = centerBlockPos; this.widthInBlocks = widthInBlocks; this.treeMaxDetailLevel = 10; // TODO in the future we may need to make this dynamic // detail 10 = (2^10) 1024 blocks wide + this.treeMinDetailLevel = treeMinDetailLevel; int halfSizeInRootNodes = Math.floorDiv(this.widthInBlocks, 2) / BitShiftUtil.powerOfTwo(this.treeMaxDetailLevel); halfSizeInRootNodes = halfSizeInRootNodes + 1; // always add 1 so nodes will always have a parent, even if the tree's center is offset from the root node grid @@ -87,7 +88,7 @@ public class QuadTree return null; } - topQuadNode = new QuadNode(rootPos); + topQuadNode = new QuadNode(rootPos, this.treeMinDetailLevel); boolean successfullyAdded = this.topRingList.set(ringListPosX, ringListPosZ, topQuadNode); LodUtil.assertTrue(successfullyAdded, "Failed to add top quadTree node at position: "+rootPos); } @@ -110,12 +111,21 @@ public class QuadTree int radius = this.diameterInBlocks()/2; DhBlockPos2D minPos = this.getCenterBlockPos().add(new DhBlockPos2D(-radius, -radius)); DhBlockPos2D maxPos =this.getCenterBlockPos().add(new DhBlockPos2D(radius, radius)); - throw new IndexOutOfBoundsException("QuadTree GetOrSet failed. Position out of bounds, min pos: "+minPos+", max pos: "+maxPos+", given Position: "+pos+" = block pos: "+pos.convertToDetailLevel(LodUtil.BLOCK_DETAIL_LEVEL)); + throw new IndexOutOfBoundsException("QuadTree GetOrSet failed. Position out of bounds, min pos: "+minPos+", max pos: "+maxPos+", min detail level: "+this.treeMinDetailLevel+", max detail level: "+this.treeMaxDetailLevel+". Given Position: "+pos+" = block pos: "+pos.convertToDetailLevel(LodUtil.BLOCK_DETAIL_LEVEL)); } } - private boolean isSectionPosInBounds(DhSectionPos testPos) + public boolean isSectionPosInBounds(DhSectionPos testPos) { + // check if the testPos is within the detail level limits of the tree + boolean detailLevelWithinBounds = this.treeMinDetailLevel <= testPos.sectionDetailLevel && testPos.sectionDetailLevel <= this.treeMaxDetailLevel; + if (!detailLevelWithinBounds) + { + return false; + } + + + // check if the testPos is within the X,Z boundry of the tree DhBlockPos2D blockCornerOfTree = this.centerBlockPos.add(new DhBlockPos2D(-this.widthInBlocks/2,-this.widthInBlocks/2)); DhLodPos cornerOfTreePos = new DhLodPos((byte)0, blockCornerOfTree.x, blockCornerOfTree.z); @@ -125,7 +135,7 @@ public class QuadTree return DoSquaresOverlap(cornerOfTreePos, this.widthInBlocks, cornerOfInputPos, inputWidth); } - public static boolean DoSquaresOverlap(DhLodPos rect1Min, int rect1Width, DhLodPos rect2Min, int rect2Width) + private static boolean DoSquaresOverlap(DhLodPos rect1Min, int rect1Width, DhLodPos rect2Min, int rect2Width) { // Determine the coordinates of the rectangles float rect1MinX = rect1Min.x; @@ -157,13 +167,14 @@ public class QuadTree } /** root nodes can be null */ - public void forEachRootNodePos(BiConsumer, Pos2D> consumer) + public void forEachRootNodePos(BiConsumer, DhSectionPos> consumer) { this.topRingList.forEachPosOrdered((rootNode, pos2D) -> { - if (isSectionPosInBounds(new DhSectionPos(this.treeMaxDetailLevel, pos2D.x, pos2D.y))) + DhSectionPos rootPos = new DhSectionPos(this.treeMaxDetailLevel, pos2D.x, pos2D.y); + if (isSectionPosInBounds(rootPos)) { - consumer.accept(rootNode, pos2D); + consumer.accept(rootNode, rootPos); } }); } diff --git a/core/src/test/java/tests/QuadTreeTest.java b/core/src/test/java/tests/QuadTreeTest.java index 735eb5d4e..101866228 100644 --- a/core/src/test/java/tests/QuadTreeTest.java +++ b/core/src/test/java/tests/QuadTreeTest.java @@ -24,6 +24,7 @@ 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.QuadNode; import com.seibel.lod.core.util.objects.quadTree.QuadTree; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Logger; @@ -31,6 +32,7 @@ import org.apache.logging.log4j.core.config.Configurator; import org.junit.Assert; import org.junit.Test; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; public class QuadTreeTest @@ -59,7 +61,7 @@ public class QuadTreeTest @Test public void BasicPositiveQuadTreeTest() { - QuadTree tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS); + QuadTree tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS, LodUtil.BLOCK_DETAIL_LEVEL); Assert.assertEquals("Incorrect basic tree width", BASIC_TREE_ACTUAL_WIDTH_IN_ROOT_NODES, tree.ringListWidth()); @@ -94,7 +96,7 @@ public class QuadTreeTest @Test public void BasicNegativeQuadTreeTest() { - QuadTree tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS); + QuadTree tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS, LodUtil.BLOCK_DETAIL_LEVEL); // root node // @@ -129,7 +131,7 @@ public class QuadTreeTest @Test public void OutOfBoundsQuadTreeTest() { - QuadTree tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0,0)); + QuadTree tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0,0), LodUtil.BLOCK_DETAIL_LEVEL); Assert.assertEquals("tree diameter incorrect", BASIC_TREE_WIDTH_IN_BLOCKS, tree.diameterInBlocks()); @@ -174,7 +176,7 @@ public class QuadTreeTest { int treeWidthInRootNodes = 8; int treeWidthInBlocks = ROOT_NODE_WIDTH_IN_BLOCKS * treeWidthInRootNodes; - QuadTree tree = new QuadTree<>(treeWidthInBlocks, TREE_CENTER_POS); + QuadTree tree = new QuadTree<>(treeWidthInBlocks, TREE_CENTER_POS, LodUtil.BLOCK_DETAIL_LEVEL); // root nodes // @@ -261,7 +263,7 @@ public class QuadTreeTest @Test public void QuadTreeIterationTest() { - QuadTree tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS); + QuadTree tree = new QuadTree<>(BASIC_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS, LodUtil.BLOCK_DETAIL_LEVEL); // root nodes // @@ -298,7 +300,7 @@ public class QuadTreeTest @Test public void CenteredGridListIterationTest() { - final QuadTree tree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS); + final QuadTree tree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS, LodUtil.BLOCK_DETAIL_LEVEL); testSet(tree, new DhSectionPos(tree.treeMaxDetailLevel, 0, 0), 0); // confirm the root node were added @@ -308,10 +310,8 @@ public class QuadTreeTest // attempt to get and remove, each node in the tree final AtomicInteger rootNodePosCount = new AtomicInteger(0); - tree.forEachRootNodePos((renderBufferNode, pos2d) -> + tree.forEachRootNodePos((renderBufferNode, sectionPos) -> { - DhSectionPos sectionPos = new DhSectionPos(tree.treeMaxDetailLevel, pos2d.x, pos2d.y); - testGet(tree, sectionPos, 0); testSet(tree, sectionPos, null); @@ -326,14 +326,14 @@ public class QuadTreeTest { // offset fully inside (10*0,0) - final QuadTree fullyInsideTree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS); + final QuadTree fullyInsideTree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS, LodUtil.BLOCK_DETAIL_LEVEL); DhBlockPos2D fullyInsideOffsetBlockPos = new DhBlockPos2D(MINIMUM_TREE_WIDTH_IN_BLOCKS, MINIMUM_TREE_WIDTH_IN_BLOCKS); fullyInsideTree.setCenterBlockPos(fullyInsideOffsetBlockPos); - fullyInsideTree.forEachRootNodePos((rootNode, pos2D) -> + fullyInsideTree.forEachRootNodePos((rootNode, sectionPos) -> { - testSet(fullyInsideTree, new DhSectionPos(fullyInsideTree.treeMaxDetailLevel, pos2D.x, pos2D.y), 0); + testSet(fullyInsideTree, sectionPos, 0); }); // only 1 root node should be added @@ -345,11 +345,11 @@ public class QuadTreeTest // offset fully inside (10*0,0) - final QuadTree borderInsideTree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(MINIMUM_TREE_WIDTH_IN_BLOCKS * 2, MINIMUM_TREE_WIDTH_IN_BLOCKS * 2)); + final QuadTree borderInsideTree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, new DhBlockPos2D(MINIMUM_TREE_WIDTH_IN_BLOCKS * 2, MINIMUM_TREE_WIDTH_IN_BLOCKS * 2), LodUtil.BLOCK_DETAIL_LEVEL); - borderInsideTree.forEachRootNodePos((rootNode, pos2D) -> + borderInsideTree.forEachRootNodePos((rootNode, sectionPos) -> { - testSet(borderInsideTree, new DhSectionPos(borderInsideTree.treeMaxDetailLevel, pos2D.x, pos2D.y), 0); + testSet(borderInsideTree, sectionPos, 0); }); // only 1 root node should be added @@ -361,14 +361,14 @@ public class QuadTreeTest // offset across (10*-1,0) and (10*0,0) - final QuadTree acrossTree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS); + final QuadTree acrossTree = new QuadTree<>(MINIMUM_TREE_WIDTH_IN_BLOCKS, TREE_CENTER_POS, LodUtil.BLOCK_DETAIL_LEVEL); DhBlockPos2D acrossOffsetBlockPos = new DhBlockPos2D(-MINIMUM_TREE_WIDTH_IN_BLOCKS/4, MINIMUM_TREE_WIDTH_IN_BLOCKS); acrossTree.setCenterBlockPos(acrossOffsetBlockPos); - acrossTree.forEachRootNodePos((rootNode, pos2D) -> + acrossTree.forEachRootNodePos((rootNode, sectionPos) -> { - testSet(acrossTree, new DhSectionPos(acrossTree.treeMaxDetailLevel, pos2D.x, pos2D.y), 0); + testSet(acrossTree, sectionPos, 0); }); // 2 root nodes should be added @@ -381,7 +381,7 @@ public class QuadTreeTest @Test public void TinyGridAlignedTreeTest() { - QuadTree tree = new QuadTree<>(ROOT_NODE_WIDTH_IN_BLOCKS, TREE_CENTER_POS); + QuadTree tree = new QuadTree<>(ROOT_NODE_WIDTH_IN_BLOCKS, TREE_CENTER_POS, LodUtil.BLOCK_DETAIL_LEVEL); // minimum size tree should be 3 root nodes wide Assert.assertEquals("incorrect tree node width", 3, tree.ringListWidth()); Assert.assertEquals("incorrect tree width", ROOT_NODE_WIDTH_IN_BLOCKS, tree.diameterInBlocks()); @@ -404,7 +404,7 @@ public class QuadTreeTest @Test public void TinyGridOffsetTreeTest() { - QuadTree tree = new QuadTree<>(ROOT_NODE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0)); + QuadTree tree = new QuadTree<>(ROOT_NODE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0), LodUtil.BLOCK_DETAIL_LEVEL); // minimum size tree should be 3 root nodes wide Assert.assertEquals("incorrect tree node width", 3, tree.ringListWidth()); Assert.assertEquals("incorrect tree width", ROOT_NODE_WIDTH_IN_BLOCKS, tree.diameterInBlocks()); @@ -434,6 +434,96 @@ public class QuadTreeTest } + @Test + public void TreeDetailLevelLimitTest() + { + QuadTree tree = new QuadTree<>(ROOT_NODE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0), (byte)8); + Assert.assertEquals("Test detail level's need to be adjusted. This isn't necessarily a failed test.", 10, tree.treeMaxDetailLevel); + + // valid detail levels + testSet(tree, new DhSectionPos((byte)10, 0, 0), 1); + testSet(tree, new DhSectionPos((byte)9, 0, 0), 2); + testSet(tree, new DhSectionPos((byte)8, 0, 0), 3); + + // detail level too low + testSet(tree, new DhSectionPos((byte)7, 0, 0), -1, IndexOutOfBoundsException.class); + testSet(tree, new DhSectionPos((byte)6, 0, 0), -1, IndexOutOfBoundsException.class); + + // detail level too high + testSet(tree, new DhSectionPos((byte)11, 0, 0), -1, IndexOutOfBoundsException.class); + testSet(tree, new DhSectionPos((byte)12, 0, 0), -1, IndexOutOfBoundsException.class); + + } + + @Test + public void QuadNodeDetailLimitTest() + { + QuadTree tree = new QuadTree<>(ROOT_NODE_WIDTH_IN_BLOCKS, new DhBlockPos2D(0, 0), (byte)6); + Assert.assertEquals("Test detail level's need to be adjusted. This isn't necessarily a failed test.", 10, tree.treeMaxDetailLevel); + + // create the root node + testSet(tree, new DhSectionPos((byte)10, 0, 0), 1); + + // recurse down the tree + AtomicInteger minimumDetailLevelReachedRef = new AtomicInteger(tree.treeMaxDetailLevel); + tree.forEachRootNode((rootNode) -> + { + rootNode.forEachDirectChild((quadNode, sectionPos) -> + { + // all sections will be null + rootNode.setValue(sectionPos, 0); + }); + + rootNode.forEachDirectChild((quadNode, sectionPos) -> + { + recursivelyCreateNodeChildren(quadNode, tree.treeMinDetailLevel, minimumDetailLevelReachedRef); + }); + }); + + // confirm that the tree can and did iterate all the way down to the minimum detail level + Assert.assertEquals("Minimum detail level never reached", minimumDetailLevelReachedRef.get(), tree.treeMinDetailLevel); + } + private void recursivelyCreateNodeChildren(QuadNode node, byte minDetailLevel, AtomicInteger minimumDetailLevelReachedRef) + { + AtomicBoolean childNodesCreatedRef = new AtomicBoolean(false); + AtomicBoolean childNodesIteratedRef = new AtomicBoolean(false); + + // fill in the null children + node.forEachDirectChild((childNode, childSectionPos) -> + { + node.setValue(childSectionPos, 0); + childNodesCreatedRef.set(true); + }); + + // attempt to recurse down these new children + node.forEachDirectChild((childNode, childSectionPos) -> + { + Assert.assertTrue("Child node recurred too low. Min detail level: "+minDetailLevel+", node detail level: "+childSectionPos.sectionDetailLevel, childSectionPos.sectionDetailLevel >= minDetailLevel); + recursivelyCreateNodeChildren(childNode, minDetailLevel, minimumDetailLevelReachedRef); + + childNodesIteratedRef.set(true); + }); + + + // keep track of how far down the tree we have gone + if (node.sectionPos.sectionDetailLevel < minimumDetailLevelReachedRef.get()) + { + minimumDetailLevelReachedRef.set(node.sectionPos.sectionDetailLevel); + } + + + // assertions + if (childNodesCreatedRef.get()) + { + Assert.assertTrue("node children created below minimum detail level", node.sectionPos.sectionDetailLevel >= minDetailLevel); + } + if (childNodesIteratedRef.get()) + { + Assert.assertTrue("node children iterated below minimum detail level", node.sectionPos.sectionDetailLevel-1 >= minDetailLevel); + } + } + +