diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/worldGeneration/EDhApiWorldGenerationStep.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/worldGeneration/EDhApiWorldGenerationStep.java index c5d544cbd..2bdb82b3b 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/worldGeneration/EDhApiWorldGenerationStep.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/worldGeneration/EDhApiWorldGenerationStep.java @@ -20,6 +20,8 @@ package com.seibel.distanthorizons.api.enums.worldGeneration; /** + * DOWN_SAMPLED,
+ * * EMPTY,
* STRUCTURE_START,
* STRUCTURE_REFERENCE,
@@ -37,6 +39,14 @@ package com.seibel.distanthorizons.api.enums.worldGeneration; */ public enum EDhApiWorldGenerationStep { + /** + * Only used when using N-sized world generators or server-side retrieval. + * This denotes that the given datasource was created using lower quality LOD data from above it in the quad tree.
+ * + * This isn't a valid option for queuing world generation. + */ + DOWN_SAMPLED(-1, "down_sampled"), + EMPTY(0, "empty"), STRUCTURE_START(1, "structure_start"), STRUCTURE_REFERENCE(2, "structure_reference"), diff --git a/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java b/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java index 4b029dfec..1255cd3bc 100644 --- a/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java +++ b/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java @@ -31,7 +31,7 @@ public final class ModInfo public static final String DEDICATED_SERVER_INITIAL_PATH = "dedicated_server_initial"; /** Incremented every time any packets are added, changed or removed, with a few exceptions. */ - public static final int PROTOCOL_VERSION = 9; + public static final int PROTOCOL_VERSION = 10; public static final String WRAPPER_PACKET_PATH = "message"; /** The internal mod name */ diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/FullDataSourceV2.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/FullDataSourceV2.java index 3be3eccec..6407d150f 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/FullDataSourceV2.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/FullDataSourceV2.java @@ -116,7 +116,12 @@ public class FullDataSourceV2 public final LongArrayList[] dataPoints; public boolean isEmpty; - public boolean applyToParent = false; + /** Will be null if we don't want to update this value in the DB */ + @Nullable + public Boolean applyToParent = null; + /** Will be null if we don't want to update this value in the DB */ + @Nullable + public Boolean applyToChildren = null; /** should only be used by methods exposed via the DH API */ private boolean runApiChunkValidation = false; @@ -301,10 +306,47 @@ public class FullDataSourceV2 if (inputDetailLevel == thisDetailLevel) { dataChanged = this.updateFromSameDetailLevel(inputDataSource, remappedIds); + + // same detail level, propagate parent/children update flags from input + if (this.applyToParent != null || inputDataSource.applyToParent != null) + { + this.applyToParent = + // copy over application flag if either are set to continue propagating + (BoolUtil.falseIfNull(this.applyToParent) || BoolUtil.falseIfNull(inputDataSource.applyToParent)) + // don't propagate past the top of the tree + && (DhSectionPos.getDetailLevel(this.pos) < AbstractDataSourceHandler.TOP_SECTION_DETAIL_LEVEL); + } + + // null check to prevent setting a flag we don't want to save in the DB + if (this.applyToChildren != null || inputDataSource.applyToChildren != null) + { + this.applyToChildren = + (BoolUtil.falseIfNull(this.applyToChildren) || BoolUtil.falseIfNull(inputDataSource.applyToChildren)) + // don't propagate past the bottom of the tree + && (DhSectionPos.getDetailLevel(this.pos) > AbstractDataSourceHandler.MIN_SECTION_DETAIL_LEVEL); + } } else if (inputDetailLevel + 1 == thisDetailLevel) { dataChanged = this.updateFromOneBelowDetailLevel(inputDataSource, remappedIds); + + // propagating up, parent will need changes + this.applyToParent = + dataChanged + && (BoolUtil.falseIfNull(this.applyToParent) || BoolUtil.falseIfNull(inputDataSource.applyToParent)) + && (DhSectionPos.getDetailLevel(this.pos) < AbstractDataSourceHandler.TOP_SECTION_DETAIL_LEVEL); + + } + else if (inputDetailLevel - 1 == thisDetailLevel) + { + dataChanged = this.downsampleFromOneAboveDetailLevel(inputDataSource, remappedIds); + + // propagating down, children will need changes + + this.applyToChildren = + dataChanged + && (BoolUtil.falseIfNull(this.applyToChildren) || BoolUtil.falseIfNull(inputDataSource.applyToChildren)) + && (DhSectionPos.getDetailLevel(this.pos) > AbstractDataSourceHandler.MIN_SECTION_DETAIL_LEVEL); } else { @@ -312,12 +354,9 @@ public class FullDataSourceV2 // and would lead to edge cases that don't necessarily need to be supported // (IE what do you do when the input is smaller than a single datapoint in the receiving data source?) // instead it's better to just percolate the updates up - throw new UnsupportedOperationException("Unsupported data source update. Expected input detail level of ["+thisDetailLevel+"] or ["+(thisDetailLevel+1)+"], received detail level ["+inputDetailLevel+"]."); + throw new UnsupportedOperationException("Unsupported data source update. Expected input detail level of ["+(thisDetailLevel-1)+"], ["+thisDetailLevel+"], or ["+(thisDetailLevel+1)+"], received detail level ["+inputDetailLevel+"]."); } - // determine if this data source should be applied to its parent - this.applyToParent = (dataChanged && DhSectionPos.getDetailLevel(this.pos) < AbstractDataSourceHandler.TOP_SECTION_DETAIL_LEVEL); - if (dataChanged) { // update the hash code @@ -326,6 +365,7 @@ public class FullDataSourceV2 return dataChanged; } + public boolean updateFromSameDetailLevel(FullDataSourceV2 inputDataSource, int[] remappedIds) { // both data sources should have the same detail level @@ -348,9 +388,31 @@ public class FullDataSourceV2 { byte thisGenState = this.columnGenerationSteps.getByte(index); byte inputGenState = inputDataSource.columnGenerationSteps.getByte(index); + - if (inputGenState != EDhApiWorldGenerationStep.EMPTY.value - && thisGenState <= inputGenState) + // determine if this column should be updated + boolean genStateAllowsUpdating = false; + // if the input is downsampled, we only want to replace empty or downsampled values + if (inputGenState == EDhApiWorldGenerationStep.DOWN_SAMPLED.value + && + ( + thisGenState == EDhApiWorldGenerationStep.EMPTY.value + || thisGenState == EDhApiWorldGenerationStep.DOWN_SAMPLED.value + )) + { + genStateAllowsUpdating = true; + } + // if the input is any other non-empty value, + // replace anything that is less-complete + else if (inputGenState != EDhApiWorldGenerationStep.EMPTY.value + && thisGenState <= inputGenState) + { + // don't apply less-complete generation data + genStateAllowsUpdating = true; + } + + + if (genStateAllowsUpdating) { // check if the data changed if (this.dataPoints[index] == null) @@ -830,6 +892,101 @@ public class FullDataSourceV2 return value; } + /** + * Only downsamples into a given column if this data source doesn't + * already contain data in that column. + * This is done to prevent accidentally downsampling onto already present higher-detail data. + */ + public boolean downsampleFromOneAboveDetailLevel(FullDataSourceV2 inputDataSource, int[] remappedIds) + { + if (DhSectionPos.getDetailLevel(inputDataSource.pos) - 1 != DhSectionPos.getDetailLevel(this.pos)) + { + throw new IllegalArgumentException("Input data source must be exactly 1 detail level above this data source. Expected [" + (DhSectionPos.getDetailLevel(this.pos) - 1) + "], received [" + DhSectionPos.getDetailLevel(inputDataSource.pos) + "]."); + } + + // input is one detail level higher (lower detail) + // so 1x1 input data points will be converted into 2x2 recipient data point + + + // determine where in this data source should be read from + // since the input is one detail level above this will be one of input position's 4 children + int minParentXPos = DhSectionPos.getX(DhSectionPos.getChildByIndex(inputDataSource.pos, 0)); + int inputOffsetX = (DhSectionPos.getX(this.pos) == minParentXPos) ? 0 : (WIDTH / 2); + int minParentZPos = DhSectionPos.getZ(DhSectionPos.getChildByIndex(inputDataSource.pos, 0)); + int inputOffsetZ = (DhSectionPos.getZ(this.pos) == minParentZPos) ? 0 : (WIDTH / 2); + + + + // merge the input's data points + // into this data source's + boolean dataChanged = false; + for (int x = 0; x < WIDTH; x++) + { + for (int z = 0; z < WIDTH; z++) + { + // recipient index is 1-to-1 + int recipientIndex = relativePosToIndex(x, z); + + int inputX = (x / 2) + inputOffsetX; + int inputZ = (z / 2) + inputOffsetZ; + int inputIndex = relativePosToIndex(inputX, inputZ); + + + // world gen // + + // a separate generation step needs to be used so can replace + // this data with higher-quality data when it is available + byte inputGenStep = EDhApiWorldGenerationStep.DOWN_SAMPLED.value; + this.columnGenerationSteps.set(recipientIndex, inputGenStep); + + + // world compression // + byte worldCompressionMode = inputDataSource.columnWorldCompressionMode.getByte(recipientIndex); + this.columnWorldCompressionMode.set(recipientIndex, worldCompressionMode); + + + + // data points // + + // check if this column should be downsampled + boolean downSampleColumn; + if (this.dataPoints[recipientIndex] == null) + { + downSampleColumn = true; + } + else + { + downSampleColumn = true; // assume empty until we find non-empty data + for (long dataPoint : this.dataPoints[recipientIndex]) + { + if (dataPoint != FullDataPointUtil.EMPTY_DATA_POINT) + { + downSampleColumn = false; + break; + } + } + } + + if (downSampleColumn) + { + LongArrayList inputDataArray = inputDataSource.dataPoints[inputIndex]; + this.dataPoints[recipientIndex] = inputDataArray; + this.remapDataColumn(recipientIndex, remappedIds); + + if (RUN_DATA_ORDER_VALIDATION) + { + throwIfDataColumnInWrongOrder(inputDataSource.pos, this.dataPoints[recipientIndex]); + } + + dataChanged = true; + } + + this.isEmpty = false; + } + } + + return dataChanged; + } //================// @@ -977,7 +1134,7 @@ public class FullDataSourceV2 LongArrayList packedDataPoints = LodDataBuilder.convertApiDataPointListToPackedLongArray(columnDataPoints, this, 0); // TODO there should be an "unknown" compression and generation step, or be defined via the datapoints - this.setSingleColumn(packedDataPoints, relX, relZ, EDhApiWorldGenerationStep.LIGHT, EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS); + this.setSingleColumn(packedDataPoints, relX, relZ, EDhApiWorldGenerationStep.SURFACE, EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS); return columnDataPoints; } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java index 3fa55a3af..3cae19af7 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java @@ -73,6 +73,8 @@ public class LodDataBuilder FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos); dataSource.isEmpty = false; + // chunk updates always propagate up + dataSource.applyToParent = true; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV2.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV2.java index 9ccf04c33..2902c05c4 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV2.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV2.java @@ -70,7 +70,7 @@ public class FullDataSourceProviderV2 protected static final int NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD = 50; /** how many parent update tasks can be in the queue at once */ - protected static final int MAX_UPDATE_TASK_COUNT = NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD * Config.Common.MultiThreading.numberOfThreads.get(); + protected static int getMaxUpdateTaskCount() { return NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD* Config.Common.MultiThreading.numberOfThreads.get(); } /** indicates how long the update queue thread should wait between queuing ticks */ protected static final int UPDATE_QUEUE_THREAD_DELAY_IN_MS = 250; @@ -103,7 +103,7 @@ public class FullDataSourceProviderV2 * Tracks which positions are currently being updated * to prevent duplicate concurrent updates. */ - public final Set parentUpdatingPosSet = ConcurrentHashMap.newKeySet(); + public final Set updatingPosSet = ConcurrentHashMap.newKeySet(); // TODO only run thread if modifications happened recently /** @@ -225,107 +225,8 @@ public class FullDataSourceProviderV2 targetBlockPos = MC_CLIENT.getPlayerBlockPos(); } - // queue parent updates - if (executor.getQueueSize() < MAX_UPDATE_TASK_COUNT - && this.parentUpdatingPosSet.size() < MAX_UPDATE_TASK_COUNT) - { - // get the positions that need to be applied to their parents - LongArrayList parentUpdatePosList = this.repo.getPositionsToUpdate(targetBlockPos.getX(), targetBlockPos.getZ(), MAX_UPDATE_TASK_COUNT); - - // combine updates together based on their parent - HashMap> updatePosByParentPos = new HashMap<>(); - for (Long pos : parentUpdatePosList) - { - updatePosByParentPos.compute(DhSectionPos.getParentPos(pos), (parentPos, updatePosSet) -> - { - if (updatePosSet == null) - { - updatePosSet = new HashSet<>(); - } - updatePosSet.add(pos); - return updatePosSet; - }); - } - - // queue the updates - for (Long parentUpdatePos : updatePosByParentPos.keySet()) - { - // stop if there are already a bunch of updates queued - if (this.parentUpdatingPosSet.size() > MAX_UPDATE_TASK_COUNT - || !this.parentUpdatingPosSet.add(parentUpdatePos)) - { - break; - } - - try - { - executor.execute(() -> - { - ReentrantLock parentWriteLock = this.updateLockProvider.getLock(parentUpdatePos); - boolean parentLocked = false; - try - { - //LOGGER.info("updating parent: "+parentUpdatePos); - - // Locking the parent before the children should prevent deadlocks. - // TryLock is used instead of lock so this thread can handle a different update. - if (parentWriteLock.tryLock()) - { - parentLocked = true; - this.lockedPosSet.add(parentUpdatePos); - - // apply each child pos to the parent - for (Long childPos : updatePosByParentPos.get(parentUpdatePos)) - { - ReentrantLock childReadLock = this.updateLockProvider.getLock(childPos); - try - { - childReadLock.lock(); - this.lockedPosSet.add(childPos); - - try (FullDataSourceV2 dataSource = this.get(childPos)) - { - // can return null when the file handler is being shut down - if (dataSource != null) - { - this.updateDataSourceAtPos(parentUpdatePos, dataSource, false); - this.repo.setApplyToParent(childPos, false); - } - } - } - catch (Exception e) - { - LOGGER.error("Unexpected in update for parent pos: [" + DhSectionPos.toString(parentUpdatePos) + "] Error: [" + e.getMessage() + "].", e); - } - finally - { - childReadLock.unlock(); - this.lockedPosSet.remove(childPos); - } - } - } - } - finally - { - if (parentLocked) - { - parentWriteLock.unlock(); - this.lockedPosSet.remove(parentUpdatePos); - } - - this.parentUpdatingPosSet.remove(parentUpdatePos); - } - }); - } - catch (RejectedExecutionException ignore) - { /* the executor was shut down, it should be back up shortly and able to accept new jobs */ } - catch (Exception e) - { - this.parentUpdatingPosSet.remove(parentUpdatePos); - throw e; - } - } - } + this.runParentUpdates(executor, targetBlockPos); + this.runChildUpdates(executor, targetBlockPos); } catch (InterruptedException ignored) @@ -340,6 +241,243 @@ public class FullDataSourceProviderV2 LOGGER.info("Update thread ["+Thread.currentThread().getName()+"] terminated."); } + /** will always apply updates */ + private void runParentUpdates(PriorityTaskPicker.Executor executor, DhBlockPos targetBlockPos) + { + int maxUpdateTaskCount = getMaxUpdateTaskCount(); + + // queue parent updates + if (executor.getQueueSize() < maxUpdateTaskCount + && this.updatingPosSet.size() < maxUpdateTaskCount) + { + // get the positions that need to be applied to their parents + LongArrayList parentUpdatePosList = this.repo.getPositionsToUpdate(targetBlockPos.getX(), targetBlockPos.getZ(), maxUpdateTaskCount); + + // combine updates together based on their parent + HashMap> updatePosByParentPos = new HashMap<>(); + for (Long pos : parentUpdatePosList) + { + updatePosByParentPos.compute(DhSectionPos.getParentPos(pos), (parentPos, updatePosSet) -> + { + if (updatePosSet == null) + { + updatePosSet = new HashSet<>(); + } + updatePosSet.add(pos); + return updatePosSet; + }); + } + + // queue the updates + for (Long parentUpdatePos : updatePosByParentPos.keySet()) + { + // stop if there are already a bunch of updates queued + if (this.updatingPosSet.size() > maxUpdateTaskCount + || executor.getQueueSize() > maxUpdateTaskCount + || !this.updatingPosSet.add(parentUpdatePos)) + { + break; + } + + try + { + executor.execute(() -> + { + ReentrantLock parentWriteLock = this.updateLockProvider.getLock(parentUpdatePos); + boolean parentLocked = false; + try + { + //LOGGER.info("updating parent: "+parentUpdatePos); + + // Locking the parent before the children should prevent deadlocks. + // TryLock is used instead of lock so this thread can handle a different update. + if (parentWriteLock.tryLock()) + { + parentLocked = true; + this.lockedPosSet.add(parentUpdatePos); + + try (FullDataSourceV2 parentDataSource = this.get(parentUpdatePos)) + { + // will return null if the file handler is shutting down + if (parentDataSource != null) + { + // apply each child pos to the parent + for (Long childPos : updatePosByParentPos.get(parentUpdatePos)) + { + ReentrantLock childReadLock = this.updateLockProvider.getLock(childPos); + try + { + childReadLock.lock(); + this.lockedPosSet.add(childPos); + + try (FullDataSourceV2 childDataSource = this.get(childPos)) + { + // can return null when the file handler is being shut down + if (childDataSource != null) + { + parentDataSource.update(childDataSource); + } + } + } + catch (Exception e) + { + LOGGER.error("Unexpected in parent update propagation for parent pos: ["+DhSectionPos.toString(parentUpdatePos)+"], child pos: [" + DhSectionPos.toString(parentUpdatePos) + "], Error: [" + e.getMessage() + "].", e); + } + finally + { + childReadLock.unlock(); + this.lockedPosSet.remove(childPos); + } + } + + + if (DhSectionPos.getDetailLevel(parentUpdatePos) < TOP_SECTION_DETAIL_LEVEL) + { + parentDataSource.applyToParent = true; + } + + this.updateDataSourceAtPos(parentUpdatePos, parentDataSource, false); + for (Long childPos : updatePosByParentPos.get(parentUpdatePos)) + { + this.repo.setApplyToParent(childPos, false); + } + } + } + } + } + finally + { + if (parentLocked) + { + parentWriteLock.unlock(); + this.lockedPosSet.remove(parentUpdatePos); + } + + this.updatingPosSet.remove(parentUpdatePos); + } + }); + } + catch (RejectedExecutionException ignore) + { /* the executor was shut down, it should be back up shortly and able to accept new jobs */ } + catch (Exception e) + { + this.updatingPosSet.remove(parentUpdatePos); + throw e; + } + } + } + } + /** stops if it finds any LOD data */ + private void runChildUpdates(PriorityTaskPicker.Executor executor, DhBlockPos targetBlockPos) + { + int maxUpdateTaskCount = getMaxUpdateTaskCount(); + + // queue child updates + if (executor.getQueueSize() < maxUpdateTaskCount + && this.updatingPosSet.size() < maxUpdateTaskCount) + { + // get the positions that need to be applied to their children + LongArrayList childUpdatePosList = this.repo.getChildPositionsToUpdate(targetBlockPos.getX(), targetBlockPos.getZ(), maxUpdateTaskCount); + + // queue the updates + for (long parentUpdatePos : childUpdatePosList) + { + // stop if there are already a bunch of updates queued + if (this.updatingPosSet.size() > maxUpdateTaskCount + || executor.getQueueSize() > maxUpdateTaskCount + || !this.updatingPosSet.add(parentUpdatePos)) + { + break; + } + + try + { + executor.execute(() -> + { + ReentrantLock parentReadLock = this.updateLockProvider.getLock(parentUpdatePos); + boolean parentLocked = false; + try + { + //LOGGER.info("updating parent: "+parentUpdatePos); + + // Locking the parent before the children should prevent deadlocks. + // TryLock is used instead of lock so this thread can handle a different update. + if (parentReadLock.tryLock()) + { + parentLocked = true; + this.lockedPosSet.add(parentUpdatePos); + + try (FullDataSourceV2 parentDataSource = this.get(parentUpdatePos)) + { + // will return null if the file handler is shutting down + if (parentDataSource != null) + { + // apply parent to each child + for (int i = 0; i < 4; i++) + { + long childPos = DhSectionPos.getChildByIndex(parentUpdatePos, i); + + ReentrantLock childWriteLock = this.updateLockProvider.getLock(childPos); + try + { + childWriteLock.lock(); + this.lockedPosSet.add(childPos); + + try (FullDataSourceV2 childDataSource = this.get(childPos)) + { + // will return null if the file handler is shutting down + if (childDataSource != null) + { + childDataSource.update(parentDataSource); + + // don't propagate child updates past the bottom of the tree + if (DhSectionPos.getDetailLevel(childPos) != DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL) + { + childDataSource.applyToChildren = true; + } + + this.updateDataSourceAtPos(childPos, childDataSource, false); + } + } + } + catch (Exception e) + { + LOGGER.error("Unexpected in child update propagation for parent pos: ["+DhSectionPos.toString(parentUpdatePos)+"], child pos: [" + DhSectionPos.toString(parentUpdatePos) + "], Error: [" + e.getMessage() + "].", e); + } + finally + { + childWriteLock.unlock(); + this.lockedPosSet.remove(childPos); + } + } + + this.repo.setApplyToChild(parentUpdatePos, false); + } + } + } + } + finally + { + if (parentLocked) + { + parentReadLock.unlock(); + this.lockedPosSet.remove(parentUpdatePos); + } + + this.updatingPosSet.remove(parentUpdatePos); + } + }); + } + catch (RejectedExecutionException ignore) + { /* the executor was shut down, it should be back up shortly and able to accept new jobs */ } + catch (Exception e) + { + this.updatingPosSet.remove(parentUpdatePos); + throw e; + } + } + } + } @@ -645,7 +783,7 @@ public class FullDataSourceProviderV2 this.queuedUpdateCountsByPos .forEach((pos, updateCountRef) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 80f + (updateCountRef.get() * 16f), 0.20f, Color.WHITE)); }); - this.parentUpdatingPosSet + this.updatingPosSet .forEach((pos) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 80f, 0.20f, Color.MAGENTA)); }); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java index 24559c8da..f5d4d03e4 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java @@ -211,7 +211,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im PriorityTaskPicker.Executor updateExecutor = ThreadPoolUtil.getUpdatePropagatorExecutor(); - if (updateExecutor == null || updateExecutor.getQueueSize() >= MAX_UPDATE_TASK_COUNT / 2) + if (updateExecutor == null || updateExecutor.getQueueSize() >= getMaxUpdateTaskCount() / 2) { // don't queue additional world gen requests if the updater is behind return false; @@ -219,7 +219,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im PriorityTaskPicker.Executor fileExecutor = ThreadPoolUtil.getFileHandlerExecutor(); - if (fileExecutor == null || fileExecutor.getQueueSize() >= MAX_UPDATE_TASK_COUNT / 2) + if (fileExecutor == null || fileExecutor.getQueueSize() >= getMaxUpdateTaskCount() / 2) { // don't queue additional world gen requests if the file handler is overwhelmed, // otherwise LODs may not load in properly @@ -313,7 +313,12 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im public boolean isFullyGenerated(ByteArrayList columnGenerationSteps) { return IntStream.range(0, columnGenerationSteps.size()) - .noneMatch(i -> columnGenerationSteps.getByte(i) == EDhApiWorldGenerationStep.EMPTY.value); + .noneMatch(i -> + { + byte value = columnGenerationSteps.getByte(i); + return value == EDhApiWorldGenerationStep.EMPTY.value + || value == EDhApiWorldGenerationStep.DOWN_SAMPLED.value; + }); } public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Generated Provider"); @@ -343,7 +348,8 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im // check if any positions are ungenerated for (int i = 0; i < columnGenStepArray.size(); i++) { - if (columnGenStepArray.getByte(i) == EDhApiWorldGenerationStep.EMPTY.value) + if (columnGenStepArray.getByte(i) == EDhApiWorldGenerationStep.EMPTY.value + || columnGenStepArray.getByte(i) == EDhApiWorldGenerationStep.DOWN_SAMPLED.value) { positionFullyGenerated = false; break; @@ -408,7 +414,8 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im } } - if (currentMinWorldGenStep == EDhApiWorldGenerationStep.EMPTY) + if (currentMinWorldGenStep == EDhApiWorldGenerationStep.EMPTY + || currentMinWorldGenStep == EDhApiWorldGenerationStep.DOWN_SAMPLED) { // queue the task break checkWorldGenLoop; @@ -417,7 +424,8 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im } } - if (currentMinWorldGenStep != EDhApiWorldGenerationStep.EMPTY) + if (currentMinWorldGenStep != EDhApiWorldGenerationStep.EMPTY + && currentMinWorldGenStep != EDhApiWorldGenerationStep.DOWN_SAMPLED) { // no world gen needed for this position return; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/BatchGenerator.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/BatchGenerator.java index 4f8971f8d..26abd39aa 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/BatchGenerator.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/BatchGenerator.java @@ -110,7 +110,7 @@ public class BatchGenerator implements IDhApiWorldGenerator targetStep = EDhApiWorldGenerationStep.FEATURES; break; case INTERNAL_SERVER: - targetStep = EDhApiWorldGenerationStep.LIGHT; // TODO using something other than LIGHT would be good for clarity + targetStep = EDhApiWorldGenerationStep.LIGHT; break; } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java index 362bb16e3..dac5a2590 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java @@ -26,6 +26,7 @@ import com.seibel.distanthorizons.api.objects.data.DhApiChunk; import com.seibel.distanthorizons.api.objects.data.IDhApiFullDataSource; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; +import com.seibel.distanthorizons.core.file.AbstractDataSourceHandler; import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; import com.seibel.distanthorizons.core.generation.tasks.InProgressWorldGenTaskGroup; import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; @@ -463,6 +464,11 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb // set here so the API user doesn't have to pass in this value anywhere themselves pooledDataSource.setRunApiChunkValidation(this.generator.runApiValidation()); + // only apply to children if we aren't at the bottom of the tree + pooledDataSource.applyToChildren = DhSectionPos.getDetailLevel(pooledDataSource.getPos()) > DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL; + pooledDataSource.applyToParent = DhSectionPos.getDetailLevel(pooledDataSource.getPos()) < DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL + 12; + + return this.generator.generateLod( chunkPosMin.getX(), chunkPosMin.getZ(), DhSectionPos.getX(requestPos), DhSectionPos.getZ(requestPos), diff --git a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/AbstractFullDataNetworkRequestQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/AbstractFullDataNetworkRequestQueue.java index feacba444..ba3ad2f8b 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/AbstractFullDataNetworkRequestQueue.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/AbstractFullDataNetworkRequestQueue.java @@ -240,6 +240,11 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende { FullDataSourceV2DTO dataSourceDto = this.networkState.fullDataPayloadReceiver.decodeDataSourceAndReleaseBuffer(response.payload); + // set application flags based on the received detail level, + // this is needed so the data sources propagate correctly + dataSourceDto.applyToChildren = DhSectionPos.getDetailLevel(dataSourceDto.pos) > DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL; + dataSourceDto.applyToParent = DhSectionPos.getDetailLevel(dataSourceDto.pos) < DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL + 12; + AbstractExecutorService executor = ThreadPoolUtil.getNetworkCompressionExecutor(); if (executor == null) { diff --git a/core/src/main/java/com/seibel/distanthorizons/core/sql/dto/FullDataSourceV2DTO.java b/core/src/main/java/com/seibel/distanthorizons/core/sql/dto/FullDataSourceV2DTO.java index c1f09d091..c3c22e462 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/sql/dto/FullDataSourceV2DTO.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/sql/dto/FullDataSourceV2DTO.java @@ -29,6 +29,7 @@ import com.seibel.distanthorizons.core.pooling.PhantomArrayListParent; import com.seibel.distanthorizons.core.pooling.PhantomArrayListPool; import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.network.INetworkObject; +import com.seibel.distanthorizons.core.util.BoolUtil; import com.seibel.distanthorizons.core.util.FullDataPointUtil; import com.seibel.distanthorizons.core.util.ListUtil; import com.seibel.distanthorizons.core.util.LodUtil; @@ -40,6 +41,7 @@ import io.netty.buffer.ByteBuf; import it.unimi.dsi.fastutil.bytes.ByteArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.*; @@ -70,7 +72,12 @@ public class FullDataSourceV2DTO public byte dataFormatVersion; public byte compressionModeValue; - public boolean applyToParent; + /** Will be null if we don't want to update this value in the DB */ + @Nullable + public Boolean applyToParent; + /** Will be null if we don't want to update this value in the DB */ + @Nullable + public Boolean applyToChildren; public long lastModifiedUnixDateTime; public long createdUnixDateTime; @@ -105,6 +112,7 @@ public class FullDataSourceV2DTO dto.lastModifiedUnixDateTime = dataSource.lastModifiedUnixDateTime; dto.createdUnixDateTime = dataSource.createdUnixDateTime; dto.applyToParent = dataSource.applyToParent; + dto.applyToChildren = dataSource.applyToChildren; dto.levelMinY = dataSource.levelMinY; } @@ -195,6 +203,15 @@ public class FullDataSourceV2DTO dataSource.isEmpty = false; + if (this.applyToParent != null) + { + dataSource.applyToParent = this.applyToParent; + } + if (this.applyToChildren != null) + { + dataSource.applyToChildren = this.applyToChildren; + } + return dataSource; } @@ -379,7 +396,8 @@ public class FullDataSourceV2DTO out.writeByte(this.dataFormatVersion); out.writeByte(this.compressionModeValue); - out.writeBoolean(this.applyToParent); + out.writeBoolean(BoolUtil.falseIfNull(this.applyToParent)); + out.writeBoolean(BoolUtil.falseIfNull(this.applyToChildren)); out.writeLong(this.lastModifiedUnixDateTime); out.writeLong(this.createdUnixDateTime); @@ -406,6 +424,7 @@ public class FullDataSourceV2DTO this.compressionModeValue = in.readByte(); this.applyToParent = in.readBoolean(); + this.applyToChildren = in.readBoolean(); this.lastModifiedUnixDateTime = in.readLong(); this.createdUnixDateTime = in.readLong(); @@ -444,6 +463,7 @@ public class FullDataSourceV2DTO .add("dataFormatVersion", this.dataFormatVersion) .add("compressionModeValue", this.compressionModeValue) .add("applyToParent", this.applyToParent) + .add("applyToChildren", this.applyToChildren) .add("lastModifiedUnixDateTime", this.lastModifiedUnixDateTime) .add("createdUnixDateTime", this.createdUnixDateTime) .toString(); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/sql/repo/FullDataSourceV2Repo.java b/core/src/main/java/com/seibel/distanthorizons/core/sql/repo/FullDataSourceV2Repo.java index 9d2081f56..78831881c 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/sql/repo/FullDataSourceV2Repo.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/sql/repo/FullDataSourceV2Repo.java @@ -25,6 +25,7 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.sql.DbConnectionClosedException; import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO; +import com.seibel.distanthorizons.core.util.BoolUtil; import com.seibel.distanthorizons.core.util.ListUtil; import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream; import it.unimi.dsi.fastutil.bytes.ByteArrayList; @@ -100,7 +101,9 @@ public class FullDataSourceV2Repo extends AbstractDhRepo