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..4419b8686 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
@@ -90,11 +90,29 @@ public interface IDhApiWorldGenerator extends Closeable, IDhApiOverrideable
*/
default byte getMaxGenerationGranularity() { return (byte) (EDhApiDetailLevel.CHUNK.detailLevel + 2); }
- /**
- * @return true if the generator is unable to accept new generation requests.
+ /**
+ * Starting in API 3.0.0 DH now handles future queuing/management internally.
+ *
+ * Previous description:
+ * true if the generator is unable to accept new generation requests.
+ *
* @since API 1.0.0
+ * @deprecated API 3.0.0
*/
- boolean isBusy();
+ @Deprecated
+ default boolean isBusy() { return false; }
+
+ /**
+ * 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 fd8a064c9..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,16 +137,13 @@ public class DhApiChunk
// set datapoints //
//================//
- // 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)
+ List column = this.dataPoints.get(internalArrayIndex);
+ if (column == null)
{
- // 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/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java b/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java
index 54ab66f10..78fcbaac3 100644
--- a/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java
+++ b/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java
@@ -41,7 +41,7 @@ public final class ModInfo
public static final String NAME = "DistantHorizons";
/** Human-readable version of NAME */
public static final String READABLE_NAME = "Distant Horizons";
- public static final String VERSION = "2.1.3-a-dev";
+ public static final String VERSION = "2.2.1-a-dev";
/** Returns true if the current build is an unstable developer build, false otherwise. */
public static boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev");
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java
index 9fb3d53c7..d1ae1e7eb 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java
@@ -147,23 +147,13 @@ public class SharedApi
public static boolean isChunkAtBlockPosAlreadyUpdating(int blockPosX, int blockPosZ)
{ return UPDATING_CHUNK_POS_SET.contains(new DhChunkPos(new DhBlockPos2D(blockPosX, blockPosZ))); }
+ public static boolean isChunkAtChunkPosAlreadyUpdating(int chunkPosX, int chunkPosZ)
+ { return UPDATING_CHUNK_POS_SET.contains(new DhChunkPos(chunkPosX, chunkPosZ)); }
+
/** handles both block place and break events */
public void chunkBlockChangedEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true); }
-
public void chunkLoadEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, false); }
- public void chunkUnloadEvent(IChunkWrapper chunk, ILevelWrapper level)
- {
- // temporarily disabled since this was originally incorrectly designated as "chunkSaveEvent"
- // but didn't actually fire on chunk save
- // and generally this is unnecessary and drastically reduces LOD building performance
- // when traveling around the world
- if (false)
- {
- this.applyChunkUpdate(chunk, level, false);
- }
- }
-
public void applyChunkUpdate(IChunkWrapper chunkWrapper, ILevelWrapper level, boolean updateNeighborChunks)
{
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/BatchGenerator.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/BatchGenerator.java
index 598687bfe..655797a2a 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
@@ -47,15 +47,6 @@ public class BatchGenerator implements IDhApiWorldGenerator
private static final IWrapperFactory FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
- /**
- * Defines how many tasks can be queued per thread.
- *
- * TODO the multiplier here should change dynamically based on how fast the generator is vs the queuing thread,
- * if this is too high it may cause issues when moving,
- * but if it is too low the generator threads won't have enough tasks to work on
- */
- private static final int MAX_QUEUED_TASKS_PER_THREAD = 3;
-
public AbstractBatchGenerationEnvironmentWrapper generationEnvironment;
public IDhLevel targetDhLevel;
@@ -147,14 +138,6 @@ public class BatchGenerator implements IDhApiWorldGenerator
@Override
public void preGeneratorTaskStart() { this.generationEnvironment.updateAllFutures(); }
- @Override
- public boolean isBusy()
- {
- int worldGenThreadCount = Math.max(Config.Client.Advanced.MultiThreading.numberOfWorldGenerationThreads.get(), 1);
- int maxWorldGenTaskCount = worldGenThreadCount * MAX_QUEUED_TASKS_PER_THREAD;
- return this.generationEnvironment.getEventCount() > maxWorldGenTaskCount;
- }
-
//=========//
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 64b8e9db3..5d72e8a2d 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
@@ -56,6 +56,16 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
+ /**
+ * Defines how many tasks can be queued per thread.
+ *
+ * TODO the multiplier here should change dynamically based on how fast the generator is vs the queuing thread,
+ * if this is too high it may cause issues when moving,
+ * but if it is too low the generator threads won't have enough tasks to work on
+ */
+ private static final int MAX_QUEUED_TASKS_PER_THREAD = 3;
+
+
private final IDhApiWorldGenerator generator;
/** contains the positions that need to be generated */
@@ -207,7 +217,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
// queue generation tasks until the generator is full, or there are no more tasks to generate
boolean taskStarted = true;
- while (!this.generator.isBusy() && taskStarted)
+ while (!this.isGeneratorBusy() && taskStarted)
{
taskStarted = this.startNextWorldGenTask(this.generationTargetPos);
if (!taskStarted)
@@ -234,6 +244,19 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
}
});
}
+ public boolean isGeneratorBusy()
+ {
+ ThreadPoolExecutor executor = ThreadPoolUtil.getWorldGenExecutor();
+ if (executor == null)
+ {
+ // shouldn't happen, but just in case, don't queue more tasks
+ return true;
+ }
+
+ int worldGenThreadCount = Math.max(Config.Client.Advanced.MultiThreading.numberOfWorldGenerationThreads.get(), 1);
+ int maxWorldGenTaskCount = worldGenThreadCount * MAX_QUEUED_TASKS_PER_THREAD;
+ return executor.getQueue().size() > maxWorldGenTaskCount;
+ }
/**
* @param targetPos the position to center the generation around
@@ -470,10 +493,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);
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogApplyShader.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogApplyShader.java
index 4906b75b2..57a8aa5fb 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogApplyShader.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogApplyShader.java
@@ -94,6 +94,12 @@ public class FogApplyShader extends AbstractShaderRenderer
GL32.glBlendEquation(GL32.GL_FUNC_ADD);
GL32.glBlendFuncSeparate(GL32.GL_SRC_ALPHA, GL32.GL_ONE_MINUS_SRC_ALPHA, GL32.GL_ONE, GL32.GL_ONE_MINUS_SRC_ALPHA);
+ // Depth testing must be disabled otherwise this application shader won't apply anything.
+ // setting this isn't necessary in vanilla, but some mods may change this, requiring it to be set manually,
+ // it should be automatically restored after rendering is complete.
+ GL32.glDisable(GL32.GL_DEPTH_TEST);
+
+
// apply the rendered Fog to DH's framebuffer
GL32.glBindFramebuffer(GL32.GL_READ_FRAMEBUFFER, FogShader.INSTANCE.frameBuffer);
GL32.glBindFramebuffer(GL32.GL_DRAW_FRAMEBUFFER, LodRenderer.getActiveFramebufferId());
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogShader.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogShader.java
index 5987af82e..f35eafc94 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogShader.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogShader.java
@@ -167,6 +167,11 @@ public class FogShader extends AbstractShaderRenderer
GL32.glBindTexture(GL32.GL_TEXTURE_2D, LodRenderer.getActiveDepthTextureId());
GL32.glUniform1i(this.uDepthMap, 0);
+ // this is necessary for MC 1.16 (IE Legacy OpenGL)
+ // otherwise the framebuffer isn't cleared correctly and the fog smears across the screen
+ GL32.glClear(GL32.GL_COLOR_BUFFER_BIT | GL32.GL_DEPTH_BUFFER_BIT);
+
+
ScreenQuad.INSTANCE.render();
state.restore();
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/SSAOApplyShader.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/SSAOApplyShader.java
index 43da8521f..5dab1ac3a 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/SSAOApplyShader.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/SSAOApplyShader.java
@@ -125,6 +125,12 @@ public class SSAOApplyShader extends AbstractShaderRenderer
GL32.glBlendEquation(GL32.GL_FUNC_ADD);
GL32.glBlendFuncSeparate(GL32.GL_ZERO, GL32.GL_SRC_ALPHA, GL32.GL_ZERO, GL32.GL_ONE);
+ // Depth testing must be disabled otherwise this application shader won't apply anything.
+ // setting this isn't necessary in vanilla, but some mods may change this, requiring it to be set manually,
+ // it should be automatically restored after rendering is complete.
+ GL32.glDisable(GL32.GL_DEPTH_TEST);
+
+
// apply the rendered SSAO to the LODs
GL32.glBindFramebuffer(GL32.GL_READ_FRAMEBUFFER, SSAOShader.INSTANCE.frameBuffer);
GL32.glBindFramebuffer(GL32.GL_DRAW_FRAMEBUFFER, LodRenderer.getActiveFramebufferId());