start replacing WorldGenQueue's backend with a QuadTree
previously it was a list that grew too quickly for large distances
This commit is contained in:
@@ -7,12 +7,11 @@ import com.seibel.lod.core.dataObjects.fullData.sources.ChunkSizedFullDataSource
|
||||
import com.seibel.lod.core.dataObjects.transformers.LodDataBuilder;
|
||||
import com.seibel.lod.core.dependencyInjection.SingletonInjector;
|
||||
import com.seibel.lod.core.generation.tasks.*;
|
||||
import com.seibel.lod.core.pos.DhBlockPos2D;
|
||||
import com.seibel.lod.core.pos.DhLodPos;
|
||||
import com.seibel.lod.core.pos.Pos2D;
|
||||
import com.seibel.lod.core.pos.*;
|
||||
import com.seibel.lod.core.util.gridList.MovableGridRingList;
|
||||
import com.seibel.lod.core.util.objects.QuadTree;
|
||||
import com.seibel.lod.core.util.objects.UncheckedInterruptedException;
|
||||
import com.seibel.lod.core.logging.DhLoggerBuilder;
|
||||
import com.seibel.lod.core.pos.DhChunkPos;
|
||||
import com.seibel.lod.core.util.LodUtil;
|
||||
import com.seibel.lod.core.wrapperInterfaces.IWrapperFactory;
|
||||
import com.seibel.lod.core.wrapperInterfaces.chunk.IChunkWrapper;
|
||||
@@ -21,55 +20,17 @@ import org.apache.logging.log4j.Logger;
|
||||
import java.io.Closeable;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* @author Leetom
|
||||
* @version 2022-11-25
|
||||
*/
|
||||
public class WorldGenerationQueue implements Closeable
|
||||
{
|
||||
public static final int MAX_TASKS_PROCESSED_PER_TICK = 10000;
|
||||
|
||||
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
|
||||
|
||||
private final IDhApiWorldGenerator generator;
|
||||
|
||||
/**
|
||||
* This list contains all of the {@link WorldGenTask}'s that haven't been processed yet. <br>
|
||||
* These tasks may or may not be necessary or valid. <br.
|
||||
* All valid tasks in this list will eventually be added to
|
||||
* the {@link WorldGenerationQueue#waitingTaskGroupsByLodPos} list (provided they aren't garbage collected first).
|
||||
*/
|
||||
private final ConcurrentLinkedQueue<WorldGenTask> looseWoldGenTasks = new ConcurrentLinkedQueue<>();
|
||||
// FIXME: Concurrency issue on close!
|
||||
// FIXME: This is using up a TONS of time to process!
|
||||
private final ConcurrentSkipListMap<DhLodPos, WorldGenTaskGroup> waitingTaskGroupsByLodPos = new ConcurrentSkipListMap<>(
|
||||
(aLodPos, bLodPos) ->
|
||||
{
|
||||
// sort based on detail level, higher detailLevels first (less detailed sections first)
|
||||
if (aLodPos.detailLevel != bLodPos.detailLevel)
|
||||
{
|
||||
return aLodPos.detailLevel - bLodPos.detailLevel;
|
||||
}
|
||||
|
||||
// sort into layers (or squares) around the world origin, closer positions first // (look at the definition of chebyshev distance for an example of what this looks like)
|
||||
// TODO shouldn't we sort based on the player's position, not the world center? Although doing that could potentially cause issues with having to constantly re-sort this list
|
||||
int aDist = aLodPos.getCenterBlockPos().toPos2D().chebyshevDist(Pos2D.ZERO);
|
||||
int bDist = bLodPos.getCenterBlockPos().toPos2D().chebyshevDist(Pos2D.ZERO);
|
||||
if (aDist != bDist)
|
||||
{
|
||||
return aDist - bDist;
|
||||
}
|
||||
else if (aLodPos.x != bLodPos.x)
|
||||
{
|
||||
return aLodPos.x - bLodPos.x;
|
||||
}
|
||||
else
|
||||
{
|
||||
return aLodPos.z - bLodPos.z;
|
||||
}
|
||||
}); // Accessed by poller only
|
||||
/** contains the positions that need to be generated */
|
||||
private final QuadTree<WorldGenTask> waitingTaskQuadTree;
|
||||
|
||||
private final ConcurrentHashMap<DhLodPos, InProgressWorldGenTaskGroup> inProgressGenTasksByLodPos = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -94,6 +55,8 @@ public class WorldGenerationQueue implements Closeable
|
||||
this.maxDataDetail = generator.getMaxDataDetailLevel();
|
||||
this.minDataDetail = generator.getMinDataDetailLevel();
|
||||
|
||||
this.waitingTaskQuadTree = new QuadTree<>(Config.Client.Graphics.Quality.lodChunkRenderDistance.get() * LodUtil.CHUNK_WIDTH, DhBlockPos2D.ZERO /*the quad tree will be re-centered later*/);
|
||||
|
||||
|
||||
if (this.minGranularity < LodUtil.CHUNK_DETAIL_LEVEL)
|
||||
{
|
||||
@@ -114,12 +77,21 @@ public class WorldGenerationQueue implements Closeable
|
||||
|
||||
public CompletableFuture<Boolean> submitGenTask(DhLodPos pos, byte requiredDataDetail, IWorldGenTaskTracker tracker)
|
||||
{
|
||||
// if the generator is shutting down, don't add new tasks
|
||||
// TODO implement multiple detail level generation
|
||||
if (pos.detailLevel != 6)
|
||||
// if (!(pos.detailLevel >= this.minGranularity && pos.detailLevel <= this.maxGranularity))
|
||||
{
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
|
||||
|
||||
// the generator is shutting down, don't add new tasks
|
||||
if (this.generatorClosingFuture != null)
|
||||
{
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
|
||||
// TODO what does these checks and the assert below mean?
|
||||
if (requiredDataDetail < this.minDataDetail)
|
||||
{
|
||||
throw new UnsupportedOperationException("Current generator does not meet requiredDataDetail level");
|
||||
@@ -129,438 +101,192 @@ public class WorldGenerationQueue implements Closeable
|
||||
requiredDataDetail = this.maxDataDetail;
|
||||
}
|
||||
|
||||
|
||||
LodUtil.assertTrue(pos.detailLevel > requiredDataDetail + 4);
|
||||
byte granularity = (byte) (pos.detailLevel - requiredDataDetail);
|
||||
LodUtil.assertTrue(pos.detailLevel > requiredDataDetail + LodUtil.CHUNK_DETAIL_LEVEL/*TODO is chunkDetailLevel the correct replacement? otherwise the magic number was 4*/);
|
||||
|
||||
|
||||
|
||||
|
||||
if (granularity > this.maxGranularity)
|
||||
{
|
||||
// The generation section is too big, split it up
|
||||
|
||||
byte subDetail = (byte) (this.maxGranularity + requiredDataDetail);
|
||||
int subPosWidthCount = pos.getBlockWidth(subDetail);
|
||||
|
||||
DhLodPos cornerSubPos = pos.getCornerLodPos(subDetail);
|
||||
CompletableFuture<Boolean>[] subFutures = new CompletableFuture[subPosWidthCount * subPosWidthCount];
|
||||
ArrayList<WorldGenTask> subTasks = new ArrayList<>(subPosWidthCount * subPosWidthCount);
|
||||
SplitWorldGenTaskTracker splitTaskTracker = new SplitWorldGenTaskTracker(tracker, new CompletableFuture<>());
|
||||
|
||||
// create the new sub-futures
|
||||
int subFutureIndex = 0;
|
||||
for (int xOffset = 0; xOffset < subPosWidthCount; xOffset++)
|
||||
{
|
||||
for (int zOffset = 0; zOffset < subPosWidthCount; zOffset++)
|
||||
{
|
||||
CompletableFuture<Boolean> subFuture = new CompletableFuture<>();
|
||||
subFutures[subFutureIndex++] = subFuture;
|
||||
subTasks.add(new WorldGenTask(cornerSubPos.addOffset(xOffset, zOffset), requiredDataDetail, splitTaskTracker, subFuture));
|
||||
}
|
||||
}
|
||||
|
||||
CompletableFuture.allOf(subFutures).whenComplete((v, ex) ->
|
||||
{
|
||||
if (ex != null)
|
||||
{
|
||||
splitTaskTracker.parentFuture.completeExceptionally(ex);
|
||||
}
|
||||
|
||||
if (!splitTaskTracker.recalculateIsValid())
|
||||
{
|
||||
return; // Auto join future
|
||||
}
|
||||
|
||||
for (CompletableFuture<Boolean> subFuture : subFutures)
|
||||
{
|
||||
boolean successful = subFuture.join();
|
||||
if (!successful)
|
||||
{
|
||||
splitTaskTracker.parentFuture.complete(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
splitTaskTracker.parentFuture.complete(true);
|
||||
});
|
||||
|
||||
this.looseWoldGenTasks.addAll(subTasks);
|
||||
return splitTaskTracker.parentFuture;
|
||||
}
|
||||
else if (granularity < this.minGranularity)
|
||||
{
|
||||
// Too small of a chunk. We'll just over-size the generation.
|
||||
byte parentDetail = (byte) (this.minGranularity + requiredDataDetail);
|
||||
DhLodPos parentPos = pos.convertToDetailLevel(parentDetail);
|
||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||
this.looseWoldGenTasks.add(new WorldGenTask(parentPos, requiredDataDetail, tracker, future));
|
||||
|
||||
return future;
|
||||
}
|
||||
else
|
||||
{
|
||||
// the requested granularity is within the min and max granularity provided by the world generator,
|
||||
// no additional task changes are necessary
|
||||
|
||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||
this.looseWoldGenTasks.add(new WorldGenTask(pos, requiredDataDetail, tracker, future));
|
||||
return future;
|
||||
}
|
||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||
this.waitingTaskQuadTree.set(new DhSectionPos(pos.detailLevel, pos.x, pos.z), new WorldGenTask(pos, requiredDataDetail, tracker, future));
|
||||
return future;
|
||||
}
|
||||
|
||||
|
||||
//===============//
|
||||
// running tasks //
|
||||
//===============//
|
||||
|
||||
public void runCurrentGenTasksUntilBusy(DhBlockPos2D targetPos)
|
||||
{
|
||||
if (this.generator == null)
|
||||
try
|
||||
{
|
||||
throw new IllegalStateException("generator is null");
|
||||
}
|
||||
|
||||
|
||||
// done to prevent generating chunks where the player isn't
|
||||
this.removeOutOfRangeTasks(targetPos);
|
||||
|
||||
// generate terrain until the generator is asked to stop (if the while loop wasn't done the world generator would run out of tasks and will end up idle)
|
||||
while (!this.generator.isBusy())// && !this.waitingTaskGroupsByLodPos.isEmpty()) // TODO why is the isEmpty() commented out?
|
||||
{
|
||||
this.removeOutdatedTaskGroups();
|
||||
this.processLooseTasks();
|
||||
this.startNextWorldGenTask(targetPos);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all invalid {@link WorldGenTask}'s and {@link WorldGenTaskGroup}'s <br>
|
||||
* This generally happens if a worldGenTask has been garbage collected.
|
||||
*/
|
||||
private void removeOutdatedTaskGroups()
|
||||
{
|
||||
Iterator<WorldGenTaskGroup> groupIter = this.waitingTaskGroupsByLodPos.values().iterator();
|
||||
|
||||
// go through each TaskGroup
|
||||
while (groupIter.hasNext())
|
||||
{
|
||||
// go through each WorldGenTask in the TaskGroup
|
||||
WorldGenTaskGroup taskGroup = groupIter.next();
|
||||
Iterator<WorldGenTask> taskIter = taskGroup.worldGenTasks.iterator();
|
||||
while (taskIter.hasNext())
|
||||
if (this.generator == null)
|
||||
{
|
||||
// remove this task if it has been garbage collected
|
||||
WorldGenTask task = taskIter.next();
|
||||
if (!task.taskTracker.isMemoryAddressValid())
|
||||
{
|
||||
taskIter.remove();
|
||||
task.future.complete(false);
|
||||
}
|
||||
throw new IllegalStateException("generator is null");
|
||||
}
|
||||
|
||||
// remove this group if it is now empty
|
||||
if (taskGroup.worldGenTasks.isEmpty())
|
||||
// the generator is shutting down, don't attempt to generate anything
|
||||
if (this.generatorClosingFuture != null)
|
||||
{
|
||||
groupIter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This processes the currently available loose tasks and prepares them
|
||||
* so, they can actually be used for world generation.
|
||||
*/
|
||||
private void processLooseTasks()
|
||||
{
|
||||
int taskProcessed = 0;
|
||||
|
||||
WorldGenTask task = this.looseWoldGenTasks.poll(); // using poll prevents concurrency issues where the list is cleared after asking if it was empty
|
||||
while (task != null && taskProcessed < MAX_TASKS_PROCESSED_PER_TICK)
|
||||
{
|
||||
taskProcessed++;
|
||||
byte taskDataDetail = task.dataDetailLevel;
|
||||
byte taskGranularity = (byte) (task.pos.detailLevel - taskDataDetail);
|
||||
LodUtil.assertTrue(taskGranularity >= LodUtil.CHUNK_DETAIL_LEVEL && taskGranularity >= this.minGranularity && taskGranularity <= this.maxGranularity);
|
||||
|
||||
// Check if a task already exists for this position
|
||||
WorldGenTaskGroup existingWorldGenGroup = this.waitingTaskGroupsByLodPos.get(task.pos);
|
||||
if (existingWorldGenGroup != null)
|
||||
{
|
||||
// a task already exists for this exact position
|
||||
|
||||
if (existingWorldGenGroup.dataDetail <= taskDataDetail)
|
||||
{
|
||||
// the existing group has an equal or lower detail level,
|
||||
// we can just append the new task to its list.
|
||||
existingWorldGenGroup.worldGenTasks.add(task);
|
||||
}
|
||||
else
|
||||
{
|
||||
// the existing group has a higher detail level than this one,
|
||||
// we need to increase the existing group's detail level.
|
||||
existingWorldGenGroup.dataDetail = taskDataDetail;
|
||||
|
||||
// remove the existing task, so it can be re-added after the necessary modifications
|
||||
boolean taskRemoved = this.waitingTaskGroupsByLodPos.remove(task.pos, existingWorldGenGroup);
|
||||
LodUtil.assertTrue(taskRemoved);
|
||||
|
||||
// re-add the task group
|
||||
existingWorldGenGroup.worldGenTasks.add(task);
|
||||
this.addAndCombineTaskGroup(existingWorldGenGroup);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// no task group exists for this position
|
||||
|
||||
// Check if there is one with a higher detail level
|
||||
byte granularity = taskGranularity;
|
||||
boolean addedToHigherDetailGroup = false;
|
||||
while (++granularity <= this.maxGranularity)
|
||||
{
|
||||
existingWorldGenGroup = this.waitingTaskGroupsByLodPos.get(task.pos.convertToDetailLevel((byte) (taskDataDetail + granularity)));
|
||||
if (existingWorldGenGroup != null && existingWorldGenGroup.dataDetail == taskDataDetail)
|
||||
{
|
||||
// We can just append to the higher detail level group
|
||||
existingWorldGenGroup.worldGenTasks.add(task);
|
||||
addedToHigherDetailGroup = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!addedToHigherDetailGroup)
|
||||
{
|
||||
// no higher detail group exists that we can append to,
|
||||
// create a new task group
|
||||
existingWorldGenGroup = new WorldGenTaskGroup(task.pos, taskDataDetail);
|
||||
existingWorldGenGroup.worldGenTasks.add(task);
|
||||
this.addAndCombineTaskGroup(existingWorldGenGroup);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// get the next task to process (will be null if the list is empty)
|
||||
task = this.looseWoldGenTasks.poll();
|
||||
}
|
||||
|
||||
if (taskProcessed != 0)
|
||||
{
|
||||
LOGGER.info("Processed " + taskProcessed + " loose tasks");
|
||||
}
|
||||
}
|
||||
/** adds the new TaskGroup either as a new group or combines it into an existing task group */
|
||||
private void addAndCombineTaskGroup(WorldGenTaskGroup newTaskGroup)
|
||||
{
|
||||
byte newGranularity = (byte) (newTaskGroup.pos.detailLevel - newTaskGroup.dataDetail);
|
||||
LodUtil.assertTrue(newGranularity <= this.maxGranularity && newGranularity >= this.minGranularity);
|
||||
LodUtil.assertTrue(!this.waitingTaskGroupsByLodPos.containsKey(newTaskGroup.pos));
|
||||
|
||||
// Check and merge all those who have exactly the same dataDetail, and overlap the position; but have lower granularity than us
|
||||
if (newGranularity > this.minGranularity)
|
||||
{
|
||||
// TODO: Optimize this check
|
||||
Iterator<WorldGenTaskGroup> groupIter = this.waitingTaskGroupsByLodPos.values().iterator();
|
||||
while (groupIter.hasNext())
|
||||
{
|
||||
WorldGenTaskGroup group = groupIter.next();
|
||||
if (group.dataDetail != newTaskGroup.dataDetail
|
||||
|| !group.pos.overlaps(newTaskGroup.pos))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// We should have already ALWAYS selected the higher granularity.
|
||||
LodUtil.assertTrue(group.pos.detailLevel < newTaskGroup.pos.detailLevel);
|
||||
groupIter.remove(); // Remove and consume all from that lower granularity request
|
||||
newTaskGroup.worldGenTasks.addAll(group.worldGenTasks);
|
||||
}
|
||||
}
|
||||
|
||||
// Now, Check if we are the missing piece in the 4 quadrants, and if so, combine the four into a new higher granularity group
|
||||
if (newGranularity < this.maxGranularity)
|
||||
{
|
||||
// Obviously, only do so if we aren't at the maxGranularity already
|
||||
// Check for merging and upping the granularity
|
||||
DhLodPos corePos = newTaskGroup.pos;
|
||||
DhLodPos parentPos = corePos.convertToDetailLevel((byte) (corePos.detailLevel + 1));
|
||||
int targetChildId = newTaskGroup.pos.getChildIndexOfParent();
|
||||
|
||||
boolean allPassed = true;
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
if (i == targetChildId)
|
||||
continue;
|
||||
WorldGenTaskGroup group = this.waitingTaskGroupsByLodPos.get(parentPos.getChildPosByIndex(i));
|
||||
if (group == null || group.dataDetail != newTaskGroup.dataDetail)
|
||||
{
|
||||
allPassed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allPassed)
|
||||
{
|
||||
LodUtil.assertTrue(!this.waitingTaskGroupsByLodPos.containsKey(parentPos) || this.waitingTaskGroupsByLodPos.get(parentPos).dataDetail != newTaskGroup.dataDetail);
|
||||
WorldGenTaskGroup[] groups = new WorldGenTaskGroup[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
if (i == targetChildId)
|
||||
{
|
||||
groups[i] = newTaskGroup;
|
||||
}
|
||||
else
|
||||
{
|
||||
groups[i] = this.waitingTaskGroupsByLodPos.remove(parentPos.getChildPosByIndex(i));
|
||||
}
|
||||
LodUtil.assertTrue(groups[i] != null && groups[i].dataDetail == newTaskGroup.dataDetail);
|
||||
}
|
||||
|
||||
WorldGenTaskGroup newGroup = this.waitingTaskGroupsByLodPos.get(parentPos);
|
||||
if (newGroup != null)
|
||||
{
|
||||
LodUtil.assertTrue(newGroup.dataDetail != newTaskGroup.dataDetail); // if it is equal, we should have been merged ages ago
|
||||
if (newGroup.dataDetail < newTaskGroup.dataDetail)
|
||||
{
|
||||
// We can just append us into the existing list.
|
||||
for (WorldGenTaskGroup g : groups)
|
||||
{
|
||||
newGroup.worldGenTasks.addAll(g.worldGenTasks);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// We need to upgrade the requested dataDetail of the group.
|
||||
newGroup.dataDetail = newTaskGroup.dataDetail;
|
||||
boolean worked = this.waitingTaskGroupsByLodPos.remove(parentPos, newGroup); // Pop it off for later proper merge check
|
||||
LodUtil.assertTrue(worked);
|
||||
for (WorldGenTaskGroup g : groups)
|
||||
{
|
||||
newGroup.worldGenTasks.addAll(g.worldGenTasks);
|
||||
}
|
||||
this.addAndCombineTaskGroup(newGroup); // Recursive check the new group
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// There should not be any higher granularity to check, as otherwise we would have merged ages ago
|
||||
newGroup = new WorldGenTaskGroup(parentPos, newTaskGroup.dataDetail);
|
||||
for (WorldGenTaskGroup g : groups)
|
||||
{
|
||||
newGroup.worldGenTasks.addAll(g.worldGenTasks);
|
||||
}
|
||||
this.addAndCombineTaskGroup(newGroup); // Recursive check the new group
|
||||
}
|
||||
|
||||
// We have merged. So no need to add the target group
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// done to prevent generating chunks where the player isn't
|
||||
this.removeOutOfRangeTasks(targetPos);
|
||||
|
||||
// generate terrain until the generator is asked to stop (if the while loop wasn't done the world generator would run out of tasks and will end up idle)
|
||||
boolean taskStarted = true;
|
||||
while (!this.generator.isBusy() && taskStarted)// && !this.waitingTaskGroupsByLodPos.isEmpty()) // TODO add !isEmpty()
|
||||
{
|
||||
this.removeGarbageCollectedTasks();
|
||||
taskStarted = this.startNextWorldGenTask(targetPos);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
|
||||
// Finally, we should be safe to add the target group into the list
|
||||
WorldGenTaskGroup existingTaskGroup = this.waitingTaskGroupsByLodPos.put(newTaskGroup.pos, newTaskGroup);
|
||||
LodUtil.assertTrue(existingTaskGroup == null); // should never be replacing other things
|
||||
}
|
||||
|
||||
private void startNextWorldGenTask(DhBlockPos2D targetPos)
|
||||
private void removeOutOfRangeTasks(DhBlockPos2D targetBlockPos)
|
||||
{
|
||||
WorldGenTaskGroup closestTaskGroup = null;
|
||||
long closestGenGroupDist = Long.MAX_VALUE;
|
||||
int lastChebshevDistToOrigin = Integer.MIN_VALUE;
|
||||
boolean continueNextRound = true;
|
||||
byte currentDetailChecking = -1;
|
||||
AtomicInteger numberOfTasksRemoved = new AtomicInteger();
|
||||
|
||||
this.waitingTaskQuadTree.setCenterPos(targetBlockPos, (worldGenTask) -> { numberOfTasksRemoved.getAndIncrement(); });
|
||||
|
||||
// Select the TaskGroup closest to the target pos with the highest detail level
|
||||
for (WorldGenTaskGroup worldGenGroup : this.waitingTaskGroupsByLodPos.values())
|
||||
// if (numberOfTasksRemoved.get() != 0)
|
||||
// {
|
||||
// LOGGER.info(numberOfTasksRemoved.get()+" world gen tasks removed.");
|
||||
// }
|
||||
}
|
||||
|
||||
/** 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)
|
||||
{
|
||||
for (byte detailLevel = QuadTree.TREE_LOWEST_DETAIL_LEVEL; detailLevel < this.waitingTaskQuadTree.treeMaxDetailLevel; detailLevel++)
|
||||
{
|
||||
// the list should be sorted detailLevel first,
|
||||
// so we should break before getting to a different detail level in the list
|
||||
if (currentDetailChecking == -1)
|
||||
MovableGridRingList<WorldGenTask> gridRingList = this.waitingTaskQuadTree.getRingList(detailLevel);
|
||||
Iterator<WorldGenTask> taskIterator = gridRingList.iterator();
|
||||
while (taskIterator.hasNext())
|
||||
{
|
||||
currentDetailChecking = worldGenGroup.dataDetail;
|
||||
}
|
||||
LodUtil.assertTrue(currentDetailChecking == worldGenGroup.dataDetail);
|
||||
|
||||
|
||||
// look for the closest position in each given layer around the world origin
|
||||
// TODO why are we looking around the world's origin?
|
||||
int chebDistToOrigin = worldGenGroup.pos.getCenterBlockPos().toPos2D().chebyshevDist(Pos2D.ZERO);
|
||||
if (chebDistToOrigin > lastChebshevDistToOrigin)
|
||||
{
|
||||
// this worldGenGroup is 1 layer farther from the world origin
|
||||
|
||||
if (!continueNextRound)
|
||||
// go through each WorldGenTask in the TaskGroup
|
||||
WorldGenTask genTask = taskIterator.next();
|
||||
if (genTask != null && !genTask.taskTracker.isMemoryAddressValid())
|
||||
{
|
||||
// We have found the best worldGenGroup, stop looking
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
continueNextRound = false;
|
||||
lastChebshevDistToOrigin = chebDistToOrigin;
|
||||
taskIterator.remove();
|
||||
genTask.future.complete(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// is this worldGenGroup closer to the targetPos than the previous closest?
|
||||
long dist = worldGenGroup.pos.getCenterBlockPos().distSquared(targetPos);
|
||||
if (closestTaskGroup != null && dist >= closestGenGroupDist)
|
||||
}
|
||||
}
|
||||
|
||||
/** @param targetPos the position to center the generation around */
|
||||
private boolean startNextWorldGenTask(DhBlockPos2D targetPos)
|
||||
{
|
||||
WorldGenTask closestTask = 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++)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
// this worldGenGroup is farther away
|
||||
continue;
|
||||
if (newGenTask != null)
|
||||
{
|
||||
// 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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a task has been found, don't look at the next detail level,
|
||||
// everything there will be farther away
|
||||
if (closestTask != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (closestTask == null)
|
||||
{
|
||||
// no task was found, this probably means there isn't anything left to generate
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 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);
|
||||
// removedWorldGenTask can be null // TODO when?
|
||||
|
||||
|
||||
// do we need to modify this task to generate it?
|
||||
if(canGeneratePos((byte) 0, closestTask.pos)) // TODO should 0 be replaced?
|
||||
{
|
||||
// detail level is correct for generation, start generation
|
||||
|
||||
WorldGenTaskGroup closestTaskGroup = new WorldGenTaskGroup(closestTask.pos, (byte) 0); // TODO should 0 be replaced?
|
||||
closestTaskGroup.worldGenTasks.add(closestTask); // TODO
|
||||
|
||||
InProgressWorldGenTaskGroup newInProgressTask = new InProgressWorldGenTaskGroup(closestTaskGroup);
|
||||
InProgressWorldGenTaskGroup previousInProgressTask = this.inProgressGenTasksByLodPos.putIfAbsent(closestTask.pos, newInProgressTask);
|
||||
if (previousInProgressTask == null)
|
||||
{
|
||||
// no task exists for this position, start one
|
||||
this.startWorldGenTaskGroup(newInProgressTask);
|
||||
}
|
||||
else
|
||||
{
|
||||
// this worldGenGroup is closer than the previous closest
|
||||
closestGenGroupDist = dist;
|
||||
closestTaskGroup = worldGenGroup;
|
||||
|
||||
continueNextRound = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// if a new worldGenGroup was found, try starting it
|
||||
if (closestTaskGroup != null)
|
||||
{
|
||||
InProgressWorldGenTaskGroup newInProgressTask = new InProgressWorldGenTaskGroup(closestTaskGroup);
|
||||
InProgressWorldGenTaskGroup previousInProgressTask = this.inProgressGenTasksByLodPos.putIfAbsent(closestTaskGroup.pos, newInProgressTask);
|
||||
|
||||
// Remove the selected task from the waiting list
|
||||
boolean taskRemoved = this.waitingTaskGroupsByLodPos.remove(closestTaskGroup.pos, closestTaskGroup);
|
||||
LodUtil.assertTrue(taskRemoved);
|
||||
|
||||
//LOGGER.info("waiting world gen task count: "+this.waitingTaskGroupsByLodPos.size());
|
||||
|
||||
if (previousInProgressTask != null)
|
||||
{
|
||||
// There is already a worldGenTask running for this position
|
||||
|
||||
// TODO replace the previous inProgress task if one exists
|
||||
// Note: Due to concurrency reasons, even if the currently running task is compatible with
|
||||
// the newly selected task, we cannot use it,
|
||||
// as some chunks may have already been written into.
|
||||
|
||||
// recursively look for a different worldGenTask to start
|
||||
this.startNextWorldGenTask(targetPos);
|
||||
|
||||
// TODO why are we putting the task back? since a compatible task is already running, why would we re-add it to the list
|
||||
WorldGenTaskGroup exchange = this.waitingTaskGroupsByLodPos.put(closestTaskGroup.pos, closestTaskGroup); // put back the task.
|
||||
LodUtil.assertTrue(exchange == null);
|
||||
}
|
||||
else
|
||||
|
||||
// a task has been started
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// detail level is (probably) too high, split up the task
|
||||
LodUtil.assertTrue(closestTask == removedWorldGenTask); // should be the same memory address, removedWorldGenTask shouldn't be null // TODO why shouldn't it be null?
|
||||
|
||||
|
||||
// split up the task and add each one to the tree
|
||||
DhSectionPos sectionPos = new DhSectionPos(closestTask.pos.detailLevel, closestTask.pos.x, closestTask.pos.z);
|
||||
sectionPos.forEachChild((childDhSectionPos) ->
|
||||
{
|
||||
// No worldGenTask is running for this position, start one
|
||||
this.startWorldGenTaskGroup(newInProgressTask);
|
||||
}
|
||||
WorldGenTask newGenTask = new WorldGenTask(new DhLodPos(childDhSectionPos.sectionDetailLevel, childDhSectionPos.sectionX, childDhSectionPos.sectionZ), childDhSectionPos.sectionDetailLevel, removedWorldGenTask.taskTracker, removedWorldGenTask.future /*TODO probably need to do something about the futures here*/);
|
||||
this.waitingTaskQuadTree.set(childDhSectionPos.sectionDetailLevel, childDhSectionPos.sectionX, childDhSectionPos.sectionZ, newGenTask);
|
||||
});
|
||||
|
||||
// return true so we attempt to generate again
|
||||
return true;
|
||||
}
|
||||
}
|
||||
private void startWorldGenTaskGroup(InProgressWorldGenTaskGroup task)
|
||||
{
|
||||
byte dataDetail = task.group.dataDetail;
|
||||
DhLodPos pos = task.group.pos;
|
||||
byte granularity = (byte) (pos.detailLevel - dataDetail);
|
||||
byte taskDetailLevel = task.group.dataDetail;
|
||||
DhLodPos taskPos = task.group.pos;
|
||||
byte granularity = (byte) (taskPos.detailLevel - taskDetailLevel);
|
||||
LodUtil.assertTrue(granularity >= this.minGranularity && granularity <= this.maxGranularity);
|
||||
LodUtil.assertTrue(dataDetail >= this.minDataDetail && dataDetail <= this.maxDataDetail);
|
||||
LodUtil.assertTrue(taskDetailLevel >= this.minDataDetail && taskDetailLevel <= this.maxDataDetail);
|
||||
|
||||
DhChunkPos chunkPosMin = new DhChunkPos(pos.getCornerBlockPos());
|
||||
//LOGGER.info("Generating section {} with granularity {} at {}", pos, granularity, chunkPosMin);
|
||||
DhChunkPos chunkPosMin = new DhChunkPos(taskPos.getCornerBlockPos());
|
||||
LOGGER.info("Generating section "+taskPos+" with granularity "+granularity+" at "+chunkPosMin);
|
||||
|
||||
task.genFuture = startGenerationEvent(this.generator, chunkPosMin, granularity, dataDetail, task.group::onGenerationComplete);
|
||||
task.genFuture = startGenerationEvent(this.generator, chunkPosMin, granularity, taskDetailLevel, task.group::onGenerationComplete);
|
||||
task.genFuture.whenComplete((voidObj, exception) ->
|
||||
{
|
||||
if (exception != null)
|
||||
@@ -568,7 +294,7 @@ public class WorldGenerationQueue implements Closeable
|
||||
// don't log the shutdown exceptions
|
||||
if (!UncheckedInterruptedException.isThrowableInterruption(exception) && !(exception instanceof CancellationException || exception.getCause() instanceof CancellationException))
|
||||
{
|
||||
LOGGER.error("Error generating data for section "+pos, exception);
|
||||
LOGGER.error("Error generating data for section "+taskPos, exception);
|
||||
}
|
||||
|
||||
task.group.worldGenTasks.forEach(worldGenTask -> worldGenTask.future.complete(false));
|
||||
@@ -578,72 +304,12 @@ public class WorldGenerationQueue implements Closeable
|
||||
//LOGGER.info("Section generation at "+pos+" completed");
|
||||
task.group.worldGenTasks.forEach(worldGenTask -> worldGenTask.future.complete(true));
|
||||
}
|
||||
boolean worked = this.inProgressGenTasksByLodPos.remove(pos, task);
|
||||
boolean worked = this.inProgressGenTasksByLodPos.remove(taskPos, task);
|
||||
LodUtil.assertTrue(worked);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes all {@link WorldGenTask}'s and {@link WorldGenTaskGroup}'s
|
||||
* that are outside the player's render distance. <br>
|
||||
* This is done to prevent generating chunks where the player isn't. <br><br>
|
||||
*
|
||||
* TODO it would be better in the long term to query what chunks should be generated each tick
|
||||
* instead of keeping a running list of every chunk pos that could ever need generating.
|
||||
* Said list can get very long and is often troublesome to use.
|
||||
*/
|
||||
private void removeOutOfRangeTasks(DhBlockPos2D targetBlockPos)
|
||||
{
|
||||
int numberOfTasksRemoved = 0;
|
||||
|
||||
DhChunkPos targetChunkPos = new DhChunkPos(targetBlockPos);
|
||||
|
||||
int chunkRenderDistance = Config.Client.Graphics.Quality.lodChunkRenderDistance.get();
|
||||
chunkRenderDistance += 6; // add a buffer where the user can move without clearing any tasks
|
||||
|
||||
DhChunkPos minChunkPos = new DhChunkPos(targetChunkPos.x - chunkRenderDistance, targetChunkPos.z - chunkRenderDistance);
|
||||
DhChunkPos maxChunkPos = new DhChunkPos(targetChunkPos.x + chunkRenderDistance, targetChunkPos.z + chunkRenderDistance);
|
||||
|
||||
|
||||
Iterator<WorldGenTaskGroup> taskGroupIter = this.waitingTaskGroupsByLodPos.values().iterator();
|
||||
|
||||
// go through each TaskGroup
|
||||
while (taskGroupIter.hasNext())
|
||||
{
|
||||
// go through each WorldGenTask in the TaskGroup
|
||||
WorldGenTaskGroup taskGroup = taskGroupIter.next();
|
||||
Iterator<WorldGenTask> taskIter = taskGroup.worldGenTasks.iterator();
|
||||
while (taskIter.hasNext())
|
||||
{
|
||||
// remove this task if it has been garbage collected
|
||||
WorldGenTask task = taskIter.next();
|
||||
DhChunkPos centerChunkPos = new DhChunkPos(task.pos.getCenterBlockPos()); // TODO this assumes the world gen tasks are exactly 1 chunk wide, which isn't always the case. But it works well enough for now
|
||||
|
||||
if (!DhChunkPos.isChunkPosBetween(minChunkPos, centerChunkPos, maxChunkPos))
|
||||
{
|
||||
taskIter.remove();
|
||||
task.future.complete(false);
|
||||
|
||||
numberOfTasksRemoved++;
|
||||
}
|
||||
}
|
||||
|
||||
// remove this group if it is now empty
|
||||
if (taskGroup.worldGenTasks.isEmpty())
|
||||
{
|
||||
taskGroupIter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (numberOfTasksRemoved != 0)
|
||||
{
|
||||
// LOGGER.info(numberOfTasksRemoved+" world gen tasks removed.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//==========//
|
||||
// shutdown //
|
||||
@@ -651,14 +317,40 @@ public class WorldGenerationQueue implements Closeable
|
||||
|
||||
public CompletableFuture<Void> startClosing(boolean cancelCurrentGeneration, boolean alsoInterruptRunning)
|
||||
{
|
||||
this.waitingTaskGroupsByLodPos.values().forEach(worldGenTaskGroup -> worldGenTaskGroup.worldGenTasks.forEach(
|
||||
(worldGenTask) ->
|
||||
{
|
||||
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++)
|
||||
{
|
||||
// TODO remove
|
||||
// Iterator<WorldGenTask> ringListIterator = this.waitingTaskQuadTree.getRingList(detailLevel).iterator();
|
||||
// while (ringListIterator.hasNext())
|
||||
// {
|
||||
// WorldGenTask worldGenTask = ringListIterator.next();
|
||||
// if (worldGenTask != null)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// worldGenTask.future.cancel(true);
|
||||
// }
|
||||
// catch (CancellationException ignored)
|
||||
// { /* don't log shutdown exceptions */ }
|
||||
// }
|
||||
// }
|
||||
|
||||
// TODO shouldn't I clear the list? not just cancel each item?
|
||||
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 */ }
|
||||
}
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
this.waitingTaskGroupsByLodPos.clear();
|
||||
|
||||
ArrayList<CompletableFuture<Void>> inProgressTasksCancelingFutures = new ArrayList<>(this.inProgressGenTasksByLodPos.size());
|
||||
this.inProgressGenTasksByLodPos.values().forEach(runningTaskGroup ->
|
||||
@@ -687,9 +379,6 @@ public class WorldGenerationQueue implements Closeable
|
||||
});
|
||||
this.generatorClosingFuture = CompletableFuture.allOf(inProgressTasksCancelingFutures.toArray(new CompletableFuture[0])); //FIXME: Closer threading issues with runCurrentGenTasksUntilBusy
|
||||
|
||||
this.looseWoldGenTasks.forEach(worldGenTask -> worldGenTask.future.cancel(true)); //.complete(false));
|
||||
this.looseWoldGenTasks.clear();
|
||||
|
||||
return this.generatorClosingFuture;
|
||||
}
|
||||
|
||||
@@ -722,6 +411,12 @@ public class WorldGenerationQueue implements Closeable
|
||||
// helper methods //
|
||||
//================//
|
||||
|
||||
private boolean canGeneratePos(byte worldGenTaskGroupDetailLevel /*when in doubt use 0*/, DhLodPos taskPos)
|
||||
{
|
||||
byte granularity = (byte) (taskPos.detailLevel - worldGenTaskGroupDetailLevel);
|
||||
return (granularity >= this.minGranularity && granularity <= this.maxGranularity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Source: <a href="https://stackoverflow.com/questions/3706219/algorithm-for-iterating-over-an-outward-spiral-on-a-discrete-2d-grid-from-the-or">...</a>
|
||||
* Description: Left-upper semi-diagonal (0-4-16-36-64) contains squared layer number (4 * layer^2).
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
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.MathUtil;
|
||||
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 getLayerDataDetailOffset() { return ColumnRenderSource.SECTION_SIZE_OFFSET; }
|
||||
public final byte getLayerDataDetail(byte sectionDetailLevel) { return (byte) (sectionDetailLevel - this.getLayerDataDetailOffset()); }
|
||||
|
||||
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.getLayerDataDetail(sectionDetailLevel);
|
||||
int maxDist = this.getFurthestBlockDistanceForDetailLevel(targetDetailLevel);
|
||||
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?
|
||||
|
||||
|
||||
// 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 circleRadius)
|
||||
{
|
||||
return (int)Math.ceil(DetailDistanceUtil.getDrawDistanceFromDetail(circleRadius + 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(); }
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user