Add optional DhApiChunk validation for world gen

This commit is contained in:
James Seibel
2024-08-12 21:47:48 -05:00
parent a98955530f
commit c7c5ab17bc
4 changed files with 102 additions and 35 deletions
@@ -96,6 +96,18 @@ public interface IDhApiWorldGenerator extends Closeable, IDhApiOverrideable
*/
boolean isBusy();
/**
* Only used if {@link #getReturnType()} returns {@link EDhApiWorldGeneratorReturnType#API_CHUNKS}. <Br>
* If true DH will run additional validation on the {@link DhApiChunk}'s returned. <Br>
* 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; }
@@ -116,32 +116,19 @@ public class DhApiChunk
*/
public void setDataPoints(int relX, int relZ, List<DhApiTerrainDataPoint> 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<DhApiTerrainDataPoint> 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);
}
@@ -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<DhApiTerrainDataPoint> 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<DhApiTerrainDataPoint> 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;
}
}
@@ -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);