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.
This commit is contained in:
James Seibel
2023-03-20 07:16:35 -05:00
parent adf5d3eb14
commit 4e0254154f
9 changed files with 965 additions and 389 deletions
@@ -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<WorldGenTask> gridRingList = this.waitingTaskQuadTree.getRingList(detailLevel);
Iterator<WorldGenTask> 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<WorldGenTask> gridRingList = this.waitingTaskQuadTree.getRingList(detailLevel);
// Iterator<WorldGenTask> 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<WorldGenTask> 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<WorldGenTask> 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<WorldGenTask> 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<WorldGenTask> 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<WorldGenTask> 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
@@ -28,6 +28,8 @@ public class DhLodPos implements Comparable<DhLodPos>
this.x = x;
this.z = z;
}
public DhLodPos(DhSectionPos sectionPos) { this(sectionPos.sectionDetailLevel, sectionPos.sectionX, sectionPos.sectionZ); }
@@ -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 <br><br>
@@ -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 + ']'; }
@@ -36,7 +36,7 @@ public class RenderBufferHandler
MovableGridRingList<LodRenderSection> 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);
}
@@ -34,8 +34,9 @@ public class MovableGridRingList<T> extends ArrayList<T> implements List<T>
private final AtomicReference<Pos2D> 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<T> extends ArrayList<T> implements List<T>
// 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<T> extends ArrayList<T> implements List<T>
}
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<T> extends ArrayList<T> implements List<T>
public boolean moveTo(int newCenterX, int newCenterY, Consumer<? super T> removedItemConsumer, BiConsumer<Pos2D, ? super T> 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<T> extends ArrayList<T> implements List<T>
// 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<T> extends ArrayList<T> implements List<T>
// 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<T> extends ArrayList<T> implements List<T>
{
Pos2D minPos = this.minPosRef.get();
return (x>=minPos.x
&& x<minPos.x+this.size
&& x<minPos.x+this.width
&& y>=minPos.y
&& y<minPos.y+this.size);
&& y<minPos.y+this.width);
}
private boolean _inRangeAcquired(int x, int y, Pos2D min)
{
return (x>=min.x
&& x<min.x+this.size
&& x<min.x+this.width
&& y>=min.y
&& y<min.y+this.size);
&& y<min.y+this.width);
}
private T _getUnsafe(int x, int y) { return super.get(Math.floorMod(x, this.size) + Math.floorMod(y, this.size)*this.size); }
private void _setUnsafe(int x, int y, T item) { super.set(Math.floorMod(x, this.size) + Math.floorMod(y, this.size)*this.size, item); }
private T _swapUnsafe(int x, int y, T item) { return super.set(Math.floorMod(x, this.size) + Math.floorMod(y, this.size)*this.size, item); }
private T _getUnsafe(int x, int y) { return super.get(Math.floorMod(x, this.width) + Math.floorMod(y, this.width)*this.width); }
private void _setUnsafe(int x, int y, T item) { super.set(Math.floorMod(x, this.width) + Math.floorMod(y, this.width)*this.width, item); }
private T _swapUnsafe(int x, int y, T item) { return super.set(Math.floorMod(x, this.width) + Math.floorMod(y, this.width)*this.width, item); }
// TODO: implement this
@@ -378,9 +379,9 @@ public class MovableGridRingList<T> extends ArrayList<T> implements List<T>
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<T> extends ArrayList<T> implements List<T>
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<T> extends ArrayList<T> implements List<T>
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<T> extends ArrayList<T> implements List<T>
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<T> extends ArrayList<T> implements List<T>
str.append(t != null ? t.toString() : "NULL");
str.append(", ");
i++;
if (i % this.size == 0)
if (i % this.width == 0)
{
str.append("\n");
}
@@ -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<T>
{
/**
* 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<T>[] 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! <br> 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<T> getRingList(byte detailLevel) { return this.ringLists[detailLevel - TREE_LOWEST_DETAIL_LEVEL]; }
public Iterator<T> 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<? super T> 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<T> 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.<br>
* 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. <br>
* 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();
}
}
@@ -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<T>
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public DhSectionPos sectionPos;
public T value;
/**
* North West <br>
* index 0 <br>
* relative pos (0,0)
*/
public QuadNode<T> nwChild;
/**
* North East <br>
* index 1 <br>
* relative (1,0)
*/
public QuadNode<T> neChild;
/**
* South West <br>
* index 2 <br>
* relative (0,1)
*/
public QuadNode<T> swChild;
/**
* South East <br>
* index 3 <br>
* relative (1,1)
*/
public QuadNode<T> 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 <br><br>
*
* Relative child positions returned for each index: <br>
* 0 = (0,0) <br>
* 1 = (1,0) <br>
* 2 = (0,1) <br>
* 3 = (1,1) <br>
*
* @param child0to3 must be an int between 0 and 3
*/
public QuadNode<T> 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<T> 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. <br>
* Note: this will pass in null children.
*/
public void forEachDirectChild(Consumer<QuadNode<T>> callback)
{
for (int i = 0; i < 4; i++)
{
callback.accept(this.getChildByIndex(i));
}
}
/**
* Applies the given consumer to all leaf nodes below this node. <br>
* Note: this will pass in null values.
*/
public void forAllLeafValues(Consumer<? super T> callback)
{
if (this.childCount() == 0)
{
// base case, bottom leaf node found
callback.accept(this.value);
}
else
{
for (int i = 0; i < 4; i++)
{
QuadNode<T> 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; }
}
@@ -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<T>
{
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<QuadNode<T>> 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<T> topQuadNode = this.topRingList.get(ringListPosX, ringListPosZ);
if (topQuadNode == null)
{
topQuadNode = new QuadNode<T>(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<QuadNode<T>> consumer)
{
this.topRingList.forEachOrdered((rootNode) ->
{
if (rootNode != null)
{
consumer.accept(rootNode);
}
});
}
public void forEachLeafValue(Consumer<? super T> 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<QuadNode<? super T>> 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.<br>
* 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. <br>
* 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();
// }
}
+290
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> tree, DhSectionPos pos, Integer value)
{
// get
Integer getResult = tree.get(pos);
Assert.assertEquals("get failed "+pos, value, getResult);
}
}