diff --git a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/override/worldGenerator/IDhApiWorldGenerator.java b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/override/worldGenerator/IDhApiWorldGenerator.java
index 7861040cc..501349520 100644
--- a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/override/worldGenerator/IDhApiWorldGenerator.java
+++ b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/override/worldGenerator/IDhApiWorldGenerator.java
@@ -96,6 +96,18 @@ public interface IDhApiWorldGenerator extends Closeable, IDhApiOverrideable
*/
boolean isBusy();
+ /**
+ * Only used if {@link #getReturnType()} returns {@link EDhApiWorldGeneratorReturnType#API_CHUNKS}.
+ * If true DH will run additional validation on the {@link DhApiChunk}'s returned.
+ * This should be disabled during release but should be enabled during development to help spot issues with your data format.
+ *
+ * @see #getReturnType()
+ * @see DhApiChunk
+ * @see EDhApiWorldGeneratorReturnType#API_CHUNKS
+ * @since API 3.0.0
+ */
+ default boolean runApiChunkValidation() { return true; }
+
diff --git a/api/src/main/java/com/seibel/distanthorizons/api/objects/data/DhApiChunk.java b/api/src/main/java/com/seibel/distanthorizons/api/objects/data/DhApiChunk.java
index 879fdad46..04e8a3bc9 100644
--- a/api/src/main/java/com/seibel/distanthorizons/api/objects/data/DhApiChunk.java
+++ b/api/src/main/java/com/seibel/distanthorizons/api/objects/data/DhApiChunk.java
@@ -116,32 +116,19 @@ public class DhApiChunk
*/
public void setDataPoints(int relX, int relZ, List dataPoints) throws IndexOutOfBoundsException, IllegalArgumentException
{
- //================//
- // validate input //
- //================//
+ //==================//
+ // basic validation //
+ //==================//
+
+ // heavier validation is done in the world generator if requested
int internalArrayIndex = (relZ << 4) | relX;
throwIfRelativePosOutOfBounds(relX, relZ);
- // ignore empty inputs
if (dataPoints == null)
{
- return;
- }
-
- // check that each datapoint is valid
- for (int i = 0; i < dataPoints.size(); i++) // standard for-loop used instead of an enhanced for-loop to slightly reduce GC overhead due to iterator allocation
- {
- DhApiTerrainDataPoint dataPoint = dataPoints.get(i);
- if (dataPoint == null)
- {
- throw new IllegalArgumentException("Null DhApiTerrainDataPoints are not allowed. If you want to represent empty terrain, please use AIR.");
- }
-
- if (dataPoint.detailLevel != 0)
- {
- throw new IllegalArgumentException("DhApiTerrainDataPoints has the wrong detail level ["+dataPoint.detailLevel+"], all data points must be block sized; IE their detail level must be [0].");
- }
+ // we don't allow null columns
+ throw new IllegalArgumentException("Null columns aren't allowed. If you want to remove all data from a column please clear the list or pass in an empty list.");
}
@@ -150,20 +137,13 @@ public class DhApiChunk
// set datapoints //
//================//
- // order doesn't need to be checked if there is 0 or 1 items
- if (dataPoints.size() > 1)
+ List column = this.dataPoints.get(internalArrayIndex);
+ if (column == null)
{
- // DH expects datapoints to be in a top-down order
- DhApiTerrainDataPoint first = dataPoints.get(0);
- DhApiTerrainDataPoint last = dataPoints.get(dataPoints.size() - 1);
- if (first.bottomYBlockPos < last.bottomYBlockPos)
- {
- // flip the array if it's in bottom-up order
- Collections.reverse(dataPoints);
- }
+ column = new ArrayList<>();
+ this.dataPoints.set(internalArrayIndex, column);
}
-
- this.dataPoints.set(internalArrayIndex, dataPoints);
+ column.addAll(dataPoints);
}
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 0cd1b2f12..0cb770a2a 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
@@ -19,6 +19,7 @@
package com.seibel.distanthorizons.core.dataObjects.transformers;
+import java.util.Collections;
import java.util.List;
import com.seibel.distanthorizons.api.enums.config.EDhApiWorldCompressionMode;
@@ -34,6 +35,7 @@ import com.seibel.distanthorizons.core.pos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.util.FullDataPointUtil;
import com.seibel.distanthorizons.core.util.LodUtil;
+import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
@@ -295,7 +297,7 @@ public class LodDataBuilder
/** @throws ClassCastException if an API user returns the wrong object type(s) */
- public static FullDataSourceV2 createFromApiChunkData(DhApiChunk apiChunk) throws ClassCastException, DataCorruptedException
+ public static FullDataSourceV2 createFromApiChunkData(DhApiChunk apiChunk, boolean runAdditionalValidation) throws ClassCastException, DataCorruptedException, IllegalArgumentException
{
// get the section position
int sectionPosX = getXOrZSectionPosFromChunkPos(apiChunk.chunkPosX);
@@ -312,6 +314,10 @@ public class LodDataBuilder
for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++)
{
List columnDataPoints = apiChunk.getDataPoints(relBlockX, relBlockZ);
+ if (runAdditionalValidation)
+ {
+ validateOrThrowDataColumn(columnDataPoints);
+ }
// this null check does 2 nice things at the same time:
@@ -352,6 +358,75 @@ public class LodDataBuilder
}
return dataSource;
}
+ private static void validateOrThrowDataColumn(List dataPoints) throws IllegalArgumentException
+ {
+ // order doesn't need to be checked if there is 0 or 1 items
+ if (dataPoints.size() > 1)
+ {
+ // DH expects datapoints to be in a top-down order
+ DhApiTerrainDataPoint first = dataPoints.get(0);
+ DhApiTerrainDataPoint last = dataPoints.get(dataPoints.size() - 1);
+ if (first.bottomYBlockPos < last.bottomYBlockPos)
+ {
+ // flip the array if it's in bottom-up order
+ Collections.reverse(dataPoints);
+ }
+
+ }
+
+
+
+ // check that each datapoint is valid
+ int lastBottomYPos = Integer.MIN_VALUE;
+ for (int i = 0; i < dataPoints.size(); i++) // standard for-loop used instead of an enhanced for-loop to slightly reduce GC overhead due to iterator allocation
+ {
+ DhApiTerrainDataPoint dataPoint = dataPoints.get(i);
+
+ if (dataPoint == null)
+ {
+ throw new IllegalArgumentException("Datapoint: ["+i+"] is null DhApiTerrainDataPoints are not allowed. If you want to represent empty terrain, please use AIR.");
+ }
+
+ if (dataPoint.detailLevel != 0)
+ {
+ throw new IllegalArgumentException("Datapoint: ["+i+"] has the wrong detail level ["+dataPoint.detailLevel+"], all data points must be block sized; IE their detail level must be [0].");
+ }
+
+
+
+ int bottomYPos = dataPoint.bottomYBlockPos;
+ int topYPos = dataPoint.topYBlockPos;
+ int height = (dataPoint.topYBlockPos - dataPoint.bottomYBlockPos);
+
+ // is the datapoint right side up?
+ if (bottomYPos > topYPos)
+ {
+ throw new IllegalArgumentException("Datapoint: ["+i+"] is upside down. Top Pos: ["+topYPos+"], bottom pos: ["+bottomYPos+"].");
+ }
+ // valid height?
+ if (height <= 0 || height >= RenderDataPointUtil.MAX_WORLD_Y_SIZE)
+ {
+ throw new IllegalArgumentException("Datapoint: ["+i+"] has invalid height. Height must be in the range [1 - "+RenderDataPointUtil.MAX_WORLD_Y_SIZE+"] (inclusive).");
+ }
+
+ // is this datapoint overlapping the last one?
+ if (lastBottomYPos > topYPos)
+ {
+ throw new IllegalArgumentException("DhApiTerrainDataPoint ["+i+"] is overlapping with the last datapoint, this top Y: ["+topYPos+"], lastBottomYPos: ["+lastBottomYPos+"].");
+ }
+ // is there a gap between the last datapoint?
+ if (topYPos != lastBottomYPos
+ && lastBottomYPos != Integer.MIN_VALUE)
+ {
+ throw new IllegalArgumentException("DhApiTerrainDataPoint ["+i+"] has a gap between it and index ["+(i-1)+"]. Empty spaces should be filled by air, otherwise DH's downsampling won't calculate lighting correctly.");
+ }
+
+
+ lastBottomYPos = bottomYPos;
+ }
+
+ }
+
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 f03394a07..f335f5107 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
@@ -469,10 +469,10 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
{
try
{
- FullDataSourceV2 dataSource = LodDataBuilder.createFromApiChunkData(dataPoints);
+ FullDataSourceV2 dataSource = LodDataBuilder.createFromApiChunkData(dataPoints, this.generator.runApiChunkValidation());
chunkDataConsumer.accept(dataSource);
}
- catch (DataCorruptedException e)
+ catch (DataCorruptedException | IllegalArgumentException e)
{
LOGGER.error("World generator returned a corrupt chunk. Error: [" + e.getMessage() + "]. World generator disabled.", e);
Config.Client.Advanced.WorldGenerator.enableDistantGeneration.set(false);