diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EOverdrawPrevention.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EOverdrawPrevention.java
index c00df2790..46f82868b 100644
--- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EOverdrawPrevention.java
+++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EOverdrawPrevention.java
@@ -24,9 +24,14 @@ package com.seibel.distanthorizons.api.enums.config;
* LIGHT
* MEDIUM
* HEAVY
+ *
+ * CUSTOM
*
* @since API 1.0.0
+ * @deprecated will be removed when DH updates to MC 1.21
+ * After removal a float value will be used to control overdraw instead.
*/
+@Deprecated
public enum EOverdrawPrevention
{
// Reminder:
@@ -36,6 +41,11 @@ public enum EOverdrawPrevention
NONE,
LIGHT,
MEDIUM,
- HEAVY;
+ HEAVY,
+ /**
+ * Should not be passed in.
+ * Is returned if the overdraw value doesn't match any of the enums defined here.
+ */
+ CUSTOM;
}
diff --git a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/IDhApiConfigValue.java b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/IDhApiConfigValue.java
index ecb7079be..90b9d13aa 100644
--- a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/IDhApiConfigValue.java
+++ b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/IDhApiConfigValue.java
@@ -53,8 +53,10 @@ public interface IDhApiConfigValue
* Sets the config's value.
* If the newValue is set to null then the config
* will revert to using the True Value.
- * If the config cannot be set via the API this method will return false.
- *
+ * If the config cannot be set via the API this method will return false.
+ *
+ * To unset the config's value pass in Null.
+ *
* @return true if the value was set, false otherwise.
*/
boolean setValue(T newValue);
diff --git a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiGraphicsConfig.java b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiGraphicsConfig.java
index d09fed095..f31d1fc9b 100644
--- a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiGraphicsConfig.java
+++ b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiGraphicsConfig.java
@@ -113,13 +113,30 @@ public interface IDhApiGraphicsConfig extends IDhApiConfigGroup
//===========================//
/**
- * If enabled the near clip plane is extended to reduce
- * overdraw and improve Z-fighting at extreme render distances.
+ * Sets the distance used by the near clip plane to reduce
+ * overdraw.
* Disabling this reduces holes in the world due to the near clip plane
* being too close to the camera and the terrain not being covered by vanilla terrain.
+ *
+ * @deprecated Use {@link IDhApiGraphicsConfig#overdrawPreventionRadius()} instead.
*/
+ @Deprecated
IDhApiConfigValue overdrawPrevention();
+ /**
+ * Sets the radius used by the near clip shader to reduce
+ * overdraw.
+ * Measured in percentages of the render distance, IE:
+ * 0.5 = 50% vanilla render distance
+ * 0.1 = 10% vanilla render distance
+ *
+ * Setting this to 0 will reduce/prevent holes in the world due to clipping to close to the camera
+ * but may cause overdraw issues with transparent or non-full blocks.
+ *
+ * @since API 1.1.0
+ */
+ IDhApiConfigValue overdrawPreventionRadius();
+
/**
* Modifies how bright fake chunks are.
* This is done when generating the vertex data and is applied before any shaders.
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 2dff42bd3..9283d700a 100644
--- a/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java
+++ b/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java
@@ -34,14 +34,14 @@ 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.0.1-a-dev";
+ public static final String VERSION = "2.0.2-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");
/** This version should only be updated when breaking changes are introduced to the DH API */
public static final int API_MAJOR_VERSION = 1;
/** This version should be updated whenever new methods are added to the DH API */
- public static final int API_MINOR_VERSION = 0;
+ public static final int API_MINOR_VERSION = 1;
/** This version should be updated whenever non-breaking fixes are added to the DH API */
public static final int API_PATH_VERSION = 0;
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiGraphicsConfig.java b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiGraphicsConfig.java
index c43ed456f..985a4b15d 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiGraphicsConfig.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiGraphicsConfig.java
@@ -109,9 +109,14 @@ public class DhApiGraphicsConfig implements IDhApiGraphicsConfig
// public IDhApiConfigValue getDisableDirectionalCulling()
// { return new DhApiConfigValue(AdvancedGraphics.disableDirectionalCulling); }
+ @Deprecated
@Override
public IDhApiConfigValue overdrawPrevention()
- { return new DhApiConfigValue(Config.Client.Advanced.Graphics.AdvancedGraphics.overdrawPrevention); }
+ { return new DhApiConfigValue(Config.Client.Advanced.Graphics.AdvancedGraphics.overdrawPreventionPreset); }
+
+ @Override
+ public IDhApiConfigValue overdrawPreventionRadius()
+ { return new DhApiConfigValue(Config.Client.Advanced.Graphics.AdvancedGraphics.overdrawPrevention); }
@Override
public IDhApiConfigValue brightnessMultiplier()
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java
index 1dd22995f..67207fe77 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java
@@ -49,6 +49,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapp
import io.netty.buffer.ByteBuf;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
import java.nio.ByteBuffer;
@@ -161,46 +162,73 @@ public class ClientApi
// level events //
//==============//
- public void clientLevelUnloadEvent(IClientLevelWrapper level)
+ public void clientLevelUnloadEvent(@Nullable IClientLevelWrapper level)
{
- LOGGER.info("Unloading client level [" + level + "].");
-
- AbstractDhWorld world = SharedApi.getAbstractDhWorld();
- if (world != null)
+ try
{
- world.unloadLevel(level);
- ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(level));
+ if (level == null)
+ {
+ // can happen on certain multiverse servers
+ return;
+ }
+ LOGGER.info("Unloading client level [" + level + "].");
+
+ AbstractDhWorld world = SharedApi.getAbstractDhWorld();
+ if (world != null)
+ {
+ world.unloadLevel(level);
+ ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(level));
+ }
+ else
+ {
+ this.waitingClientLevels.remove(level);
+ }
}
- else
+ catch (Exception e)
{
- this.waitingClientLevels.remove(level);
+ // handle errors here to prevent blowing up a mixin or API up stream
+ LOGGER.error("Unexpected error in ClientApi.clientLevelUnloadEvent(), error: "+e.getMessage(), e);
}
}
- public void clientLevelLoadEvent(IClientLevelWrapper level) { this.clientLevelLoadEvent(level, false); }
- public void multiverseClientLevelLoadEvent(IClientLevelWrapper level) { this.clientLevelLoadEvent(level, true); }
- private void clientLevelLoadEvent(IClientLevelWrapper level, boolean isServerCommunication)
+ public void clientLevelLoadEvent(@Nullable IClientLevelWrapper level) { this.clientLevelLoadEvent(level, false); }
+ public void multiverseClientLevelLoadEvent(@Nullable IClientLevelWrapper level) { this.clientLevelLoadEvent(level, true); }
+ private void clientLevelLoadEvent(@Nullable IClientLevelWrapper level, boolean isServerCommunication)
{
- if (this.isServerCommunicationEnabled && !isServerCommunication)
+ try
{
- LOGGER.info("Server supports communication, deferring loading.");
- return;
- }
-
-
- LOGGER.info("Loading " + (isServerCommunication ? "Multiverse" : "") + " client level [" + level + "].");
-
- AbstractDhWorld world = SharedApi.getAbstractDhWorld();
- if (world != null)
- {
- world.getOrLoadLevel(level);
- ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level));
+ if (this.isServerCommunicationEnabled && !isServerCommunication)
+ {
+ LOGGER.info("Server supports communication, deferring loading.");
+ return;
+ }
+ if (level == null)
+ {
+ // can happen on certain multiverse servers
+ return;
+ }
- this.loadWaitingChunksForLevel(level);
+
+
+ LOGGER.info("Loading " + (isServerCommunication ? "Multiverse" : "") + " client level [" + level + "].");
+
+ AbstractDhWorld world = SharedApi.getAbstractDhWorld();
+ if (world != null)
+ {
+ world.getOrLoadLevel(level);
+ ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level));
+
+ this.loadWaitingChunksForLevel(level);
+ }
+ else
+ {
+ this.waitingClientLevels.add(level);
+ }
}
- else
+ catch (Exception e)
{
- this.waitingClientLevels.add(level);
+ // handle errors here to prevent blowing up a mixin or API up stream
+ LOGGER.error("Unexpected error in ClientApi.clientLevelLoadEvent(), error: "+e.getMessage(), e);
}
}
private void loadWaitingChunksForLevel(IClientLevelWrapper level)
@@ -258,26 +286,35 @@ public class ClientApi
IProfilerWrapper profiler = MC.getProfiler();
profiler.push("DH-ClientTick");
- boolean doFlush = System.nanoTime() - this.lastFlushNanoTime >= SPAM_LOGGER_FLUSH_NS;
- if (doFlush)
+ try
{
- this.lastFlushNanoTime = System.nanoTime();
- SpamReducedLogger.flushAll();
- }
- ConfigBasedLogger.updateAll();
- ConfigBasedSpamLogger.updateAll(doFlush);
-
- IDhClientWorld clientWorld = SharedApi.getIDhClientWorld();
- if (clientWorld != null)
- {
- clientWorld.clientTick();
-
- // Ignore local world gen, as it's managed by server ticking
- if (!(clientWorld instanceof DhClientServerWorld))
+ boolean doFlush = System.nanoTime() - this.lastFlushNanoTime >= SPAM_LOGGER_FLUSH_NS;
+ if (doFlush)
{
- SharedApi.worldGenTick(clientWorld::doWorldGen);
+ this.lastFlushNanoTime = System.nanoTime();
+ SpamReducedLogger.flushAll();
+ }
+ ConfigBasedLogger.updateAll();
+ ConfigBasedSpamLogger.updateAll(doFlush);
+
+ IDhClientWorld clientWorld = SharedApi.getIDhClientWorld();
+ if (clientWorld != null)
+ {
+ clientWorld.clientTick();
+
+ // Ignore local world gen, as it's managed by server ticking
+ if (!(clientWorld instanceof DhClientServerWorld))
+ {
+ SharedApi.worldGenTick(clientWorld::doWorldGen);
+ }
}
}
+ catch (Exception e)
+ {
+ // handle errors here to prevent blowing up a mixin or API up stream
+ LOGGER.error("Unexpected error in ClientApi.clientTickEvent(), error: "+e.getMessage(), e);
+ }
+
profiler.pop();
}
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 420eef015..fcc4f5f47 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
@@ -52,6 +52,8 @@ public class SharedApi
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
private static final Set UPDATING_CHUNK_SET = ConcurrentHashMap.newKeySet();
+ private static final int MAX_UPDATING_CHUNK_COUNT_PER_THREAD = 100;
+
private static AbstractDhWorld currentWorld;
@@ -226,6 +228,13 @@ public class SharedApi
}
private static void bakeChunkLightingAndSendToLevelAsync(IChunkWrapper chunkWrapper, @Nullable ArrayList neighbourChunkList, IDhLevel dhLevel)
{
+ if (UPDATING_CHUNK_SET.size() >= MAX_UPDATING_CHUNK_COUNT_PER_THREAD * Config.Client.Advanced.MultiThreading.numberOfLodBuilderThreads.get())
+ {
+ // The maximum number of chunks are already queued, don't add more.
+ // This is done to prevent overloading the system if the user flys extremely fast and queues too many chunks
+ return;
+ }
+
// prevent duplicate update requests
if (UPDATING_CHUNK_SET.contains(chunkWrapper.getChunkPos()))
{
@@ -237,6 +246,11 @@ public class SharedApi
// lighting the chunk needs to be done on a separate thread to prevent lagging any of the event threads
ThreadPoolExecutor executor = ThreadPools.getLightPopulatorExecutor();
+ if (executor == null)
+ {
+ return;
+ }
+
executor.execute(() ->
{
LOGGER.trace(chunkWrapper.getChunkPos() + " " + executor.getActiveCount() + " / " + executor.getQueue().size() + " - " + executor.getCompletedTaskCount());
@@ -245,7 +259,8 @@ public class SharedApi
{
// Save or populate the chunk wrapper's lighting
// this is done so we don't have to worry about MC unloading the lighting data for this chunk
- if (chunkWrapper.isLightCorrect())
+ boolean onlyUseDhLighting = Config.Client.Advanced.LodBuilding.onlyUseDhLightingEngine.get();
+ if (!onlyUseDhLighting && chunkWrapper.isLightCorrect())
{
try
{
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java b/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java
index b5711adb6..38e6044d0 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java
@@ -547,17 +547,29 @@ public class Config
// + "Disable this if you see LODs disappearing at the corners of your vision.")
// .build();
- public static ConfigEntry overdrawPrevention = new ConfigEntry.Builder()
+ /**
+ * @deprecated Use overdrawPrevention instead, will be removed when DH updates to MC 1.21
+ * After removal a float value will be used to control overdraw instead.
+ */
+ @Deprecated
+ public static ConfigEntry overdrawPreventionPreset = new ConfigEntry.Builder()
.set(EOverdrawPrevention.MEDIUM)
+ .comment("")
+ .setAppearance(EConfigEntryAppearance.ONLY_IN_API)
+ .setPerformance(EConfigEntryPerformance.NONE)
+ .build();
+
+ public static ConfigEntry overdrawPrevention = new ConfigEntry.Builder()
+ .setMinDefaultMax(0.0, 0.4, 1.0)
.comment(""
- + "Determines how far Distant Horizon's near clip plane will render. \n"
+ + "Determines how far from the camera Distant Horizons will start rendering. \n"
+ + "Measured as a percentage of the vanilla render distance.\n"
+ "\n"
+ "Higher values will prevent LODs from rendering behind vanilla blocks at a higher distance,\n"
+ "but may cause holes to appear in the LODs. \n"
- + "Holes are most likely at the left and right edges of the screen \n"
- + "when flying through unloaded terrain. \n"
+ + "Holes are most likely to appear when flying through unloaded terrain. \n"
+ "\n"
- + "Increasing the vanilla render distance increases the effectiveness of these options."
+ + "Increasing the vanilla render distance increases the effectiveness of this setting."
+ "")
.setPerformance(EConfigEntryPerformance.NONE)
.build();
@@ -769,6 +781,19 @@ public class Config
+ "")
.build();
+ public static ConfigEntry onlyUseDhLightingEngine = new ConfigEntry.Builder()
+ .set(false)
+ .comment(""
+ + "If false LODs will be lit by Minecraft's lighting engine when possible \n"
+ + "and fall back to the DH lighting engine only when necessary. \n"
+ + "\n"
+ + "If true LODs will only be lit using Distant Horizons' lighting engine. \n"
+ + "\n"
+ + "Generally it is best to leave this disabled and should only be enabled \n"
+ + "if there are lighting issues or for debugging. \n"
+ + "")
+ .build();
+
}
public static class Multiplayer
@@ -1447,14 +1472,26 @@ public class Config
try
{
+ // listener can only be added after the config has finished initializing
+ Client.Advanced.Graphics.AdvancedGraphics.overdrawPreventionPreset.addListener(OverdrawPreventionPresetConfigEventHandler.INSTANCE);
+ }
+ catch (Exception e)
+ {
+ LOGGER.error("Unexpected exception when running config delayed listener setup. Error: [" + e.getMessage() + "].", e);
+ }
+
+ try
+ {
+ // TODO automatically get all instances of AbstractPresetConfigEventHandler and fire "setUiOnlyConfigValues"
ThreadPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
RenderQualityPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
QuickRenderToggleConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
+ OverdrawPreventionPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
RenderCacheConfigEventHandler.getInstance();
}
catch (Exception e)
{
- LOGGER.error("Unexpected exception when setting up complicated config listeners. Error: [" + e.getMessage() + "].", e);
+ LOGGER.error("Unexpected exception when running config delayed UI setup. Error: [" + e.getMessage() + "].", e);
}
}
}
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/presets/OverdrawPreventionPresetConfigEventHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/presets/OverdrawPreventionPresetConfigEventHandler.java
new file mode 100644
index 000000000..216238d53
--- /dev/null
+++ b/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/presets/OverdrawPreventionPresetConfigEventHandler.java
@@ -0,0 +1,86 @@
+/*
+ * This file is part of the Distant Horizons mod
+ * licensed under the GNU LGPL v3 License.
+ *
+ * Copyright (C) 2020-2023 James Seibel
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package com.seibel.distanthorizons.core.config.eventHandlers.presets;
+
+import com.seibel.distanthorizons.api.enums.config.EMaxHorizontalResolution;
+import com.seibel.distanthorizons.api.enums.config.EOverdrawPrevention;
+import com.seibel.distanthorizons.api.enums.config.quickOptions.EQualityPreset;
+import com.seibel.distanthorizons.core.config.Config;
+import com.seibel.distanthorizons.core.config.ConfigEntryWithPresetOptions;
+import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
+import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class OverdrawPreventionPresetConfigEventHandler extends AbstractPresetConfigEventHandler
+{
+ public static final OverdrawPreventionPresetConfigEventHandler INSTANCE = new OverdrawPreventionPresetConfigEventHandler();
+
+ private static final Logger LOGGER = LogManager.getLogger();
+
+
+ private final ConfigEntryWithPresetOptions overdrawPrevention = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.AdvancedGraphics.overdrawPrevention,
+ new HashMap()
+ {{
+ this.put(EOverdrawPrevention.HEAVY, 0.6);
+ this.put(EOverdrawPrevention.MEDIUM, 0.4);
+ this.put(EOverdrawPrevention.LIGHT, 0.25);
+ this.put(EOverdrawPrevention.NONE, 0.0);
+ }});
+
+
+
+ //==============//
+ // constructors //
+ //==============//
+
+ /** private since we only ever need one handler at a time */
+ private OverdrawPreventionPresetConfigEventHandler()
+ {
+ // add each config used by this preset
+ this.configList.add(this.overdrawPrevention);
+
+
+ for (ConfigEntryWithPresetOptions config : this.configList)
+ {
+ // ignore try-using, the listener should only ever be added once and should never be removed
+ new ConfigChangeListener<>(config.configEntry, (val) -> { this.onConfigValueChanged(); });
+ }
+ }
+
+
+
+ //==============//
+ // enum getters //
+ //==============//
+
+ @Override
+ protected IConfigEntry getPresetConfigEntry() { return Config.Client.Advanced.Graphics.AdvancedGraphics.overdrawPreventionPreset; }
+
+ @Override
+ protected List getPresetEnumList() { return Arrays.asList(EOverdrawPrevention.values()); }
+ @Override
+ protected EOverdrawPrevention getCustomPresetEnum() { return EOverdrawPrevention.CUSTOM; }
+
+}
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/file/ConfigFileHandling.java b/core/src/main/java/com/seibel/distanthorizons/core/config/file/ConfigFileHandling.java
index 8459ce9ef..ff18ad659 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/config/file/ConfigFileHandling.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/config/file/ConfigFileHandling.java
@@ -25,7 +25,6 @@ import com.seibel.distanthorizons.core.config.ConfigBase;
import com.seibel.distanthorizons.core.config.types.AbstractConfigType;
import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
-import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -112,7 +111,7 @@ public class ConfigFileHandling
// Attempt to get the version number
currentCfgVersion = (Integer) tmpNightConfig.get("_version");
tmpNightConfig.close();
- } catch (Exception e) {e.printStackTrace();}
+ } catch (Exception ignored) { }
if (currentCfgVersion == configBase.configVersion)
{}
@@ -227,9 +226,21 @@ public class ConfigFileHandling
return;
}
- entry.pureSet((T) ConfigTypeConverters.attemptToConvertFromString(entry.getType(), nightConfig.get(entry.getNameWCategory())));
+ // try converting the value if necessary
+ Class> expectedValueClass = entry.getType();
+ Object value = nightConfig.get(entry.getNameWCategory());
+ Object convertedValue = ConfigTypeConverters.attemptToConvertFromString(expectedValueClass, value);
+ if (!convertedValue.getClass().equals(expectedValueClass))
+ {
+ LOGGER.error("Unable to convert config value ["+value+"] from ["+(value != null ? value.getClass() : "NULL")+"] to ["+expectedValueClass+"] for config ["+entry.name+"], " +
+ "the default config value will be used instead ["+entry.getDefaultValue()+"]. " +
+ "Make sure a converter is defined in ["+ConfigTypeConverters.class.getSimpleName()+"].");
+ convertedValue = entry.getDefaultValue();
+ }
+ entry.pureSet((T) convertedValue);
- if (entry.getTrueValue() == null) {
+ if (entry.getTrueValue() == null)
+ {
LOGGER.warn("Entry [" + entry.getNameWCategory() + "] returned as null from the config. Using default value.");
entry.pureSet(entry.getDefaultValue());
}
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/file/ConfigTypeConverters.java b/core/src/main/java/com/seibel/distanthorizons/core/config/file/ConfigTypeConverters.java
index 3b8ba9711..57418ad80 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/config/file/ConfigTypeConverters.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/config/file/ConfigTypeConverters.java
@@ -37,12 +37,13 @@ public class ConfigTypeConverters
// Once you've made a converter add it to here where the first value is the type you want to convert and the 2nd value is the converter
public static final Map, ConverterBase> convertObjects = new HashMap, ConverterBase>()
{{
- put(Short.class, new ShortConverter());
- put(Long.class, new LongConverter());
- put(Float.class, new FloatConverter());
- put(Byte.class, new ByteConverter());
+ this.put(Short.class, new ShortConverter());
+ this.put(Long.class, new LongConverter());
+ this.put(Float.class, new FloatConverter());
+ this.put(Double.class, new DoubleConverter());
+ this.put(Byte.class, new ByteConverter());
- put(Map.class, new MapConverter());
+ this.put(Map.class, new MapConverter());
}};
public static Class> isClassConvertable(Class> clazz)
@@ -73,10 +74,12 @@ public class ConfigTypeConverters
{
return attemptToConvertFromString(value.getClass(), value);
}
- public static Object attemptToConvertFromString(Class> clazz, Object value)
+ public static Object attemptToConvertFromString(Class> outputClass, Object value)
{
- Class> convertablClass = isClassConvertable(clazz);
- if (convertablClass != null) {
+ boolean valueNeedsConverting = (value == null || value.getClass().equals(String.class));
+ Class> convertablClass = isClassConvertable(outputClass);
+ if (valueNeedsConverting && convertablClass != null)
+ {
return convertFromString(convertablClass, (String) value);
}
return value;
@@ -140,7 +143,12 @@ public class ConfigTypeConverters
{
@Override public String convertToString(Object item) { return ((Float) item).toString(); }
@Override public Float convertFromString(String s) { return Float.valueOf(s); }
-
+ }
+
+ public static class DoubleConverter extends ConverterBase
+ {
+ @Override public String convertToString(Object item) { return ((Double) item).toString(); }
+ @Override public Double convertFromString(String s) { return Double.valueOf(s); }
}
public static class ByteConverter extends ConverterBase
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/types/enums/EConfigEntryAppearance.java b/core/src/main/java/com/seibel/distanthorizons/core/config/types/enums/EConfigEntryAppearance.java
index 06b367832..24dd3fd51 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/config/types/enums/EConfigEntryAppearance.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/config/types/enums/EConfigEntryAppearance.java
@@ -32,7 +32,9 @@ public enum EConfigEntryAppearance
/** Will only show the option in the UI. The option will be reverted on game restart */
ONLY_IN_GUI(true, false),
/** Only show the option in the file. There would be no way to access it using the UI */
- ONLY_IN_FILE(true, false);
+ ONLY_IN_FILE(true, false),
+ /** The option is only available via code. Generally this is only used for deprecated options. */
+ ONLY_IN_API(false, false);
/** Sets whether the option should show in the UI */
public final boolean showInGui;
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnBox.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnBox.java
index e231289c7..0584aa79b 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnBox.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnBox.java
@@ -52,6 +52,8 @@ public class ColumnBox
// if there isn't any data below this LOD, make this LOD's color opaque to prevent seeing void through transparent blocks
// Note: this LOD should still be considered transparent for this method's checks, otherwise rendering bugs may occur
+ // FIXME this transparency change should be applied before this point since this could affect other areas
+ // This may also be better than handling the LOD as transparent, but that is TBD
if (!RenderDataPointUtil.doesDataPointExist(bottomData))
{
color = ColorUtil.setAlpha(color, 255);
@@ -269,7 +271,17 @@ public class ColumnBox
adjIndex++)
{
long adjPoint = adjColumnView.get(adjIndex);
- boolean adjTransparent = RenderDataPointUtil.getAlpha(adjPoint) < 255 && LodRenderer.transparencyEnabled;
+
+ // if the adjacent data point is over the void
+ // don't consider it as transparent
+ // FIXME this transparency change should be applied before this point since this could affect other areas
+ boolean adjOverVoid = false;
+ if (adjIndex > 0)
+ {
+ long adjBellowPoint = adjColumnView.get(adjIndex-1);
+ adjOverVoid = !RenderDataPointUtil.doesDataPointExist(adjBellowPoint);
+ }
+ boolean adjTransparent = !adjOverVoid && RenderDataPointUtil.getAlpha(adjPoint) < 255 && LodRenderer.transparencyEnabled;
// continue if this data point is transparent or the adjacent point is not
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnRenderBufferBuilder.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnRenderBufferBuilder.java
index dcdd65be7..ebb7052a4 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnRenderBufferBuilder.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnRenderBufferBuilder.java
@@ -39,6 +39,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ThreadPoolExecutor;
/**
* Used to populate the buffers in a {@link ColumnRenderSource} object.
@@ -66,6 +67,17 @@ public class ColumnRenderBufferBuilder
IDhClientLevel clientLevel, Reference renderBufferRef,
ColumnRenderSource renderSource, ColumnRenderSource[] adjData)
{
+ ThreadPoolExecutor bufferBuilderExecutor = ThreadPools.getBufferBuilderExecutor();
+ ThreadPoolExecutor bufferUploaderExecutor = ThreadPools.getBufferUploaderExecutor();
+ if ((bufferBuilderExecutor == null || bufferBuilderExecutor.isTerminated()) ||
+ (bufferUploaderExecutor == null || bufferUploaderExecutor.isTerminated()))
+ {
+ // one or more of the thread pools has been shut down
+ CompletableFuture future = new CompletableFuture<>();
+ future.cancel(true);
+ return future;
+ }
+
//LOGGER.info("RenderRegion startBuild @ "+renderSource.sectionPos);
return CompletableFuture.supplyAsync(() ->
{
@@ -101,7 +113,7 @@ public class ColumnRenderBufferBuilder
LOGGER.error("\"LodNodeBufferBuilder\" was unable to build quads: ", e3);
throw e3;
}
- }, ThreadPools.getBufferBuilderExecutor())
+ }, bufferBuilderExecutor)
.thenApplyAsync((quadBuilder) ->
{
try
@@ -136,7 +148,7 @@ public class ColumnRenderBufferBuilder
LOGGER.error("\"LodNodeBufferBuilder\" was unable to upload buffer: ", e3);
throw e3;
}
- }, ThreadPools.getBufferUploaderExecutor())
+ }, bufferUploaderExecutor)
.handle((columnRenderBuffer, ex) ->
{
//LOGGER.info("RenderRegion endBuild @ {}", renderSource.sectionPos);
@@ -168,10 +180,11 @@ public class ColumnRenderBufferBuilder
// Variable initialization
EDebugRendering debugMode = Config.Client.Advanced.Debugging.debugRendering.get();
+ // TODO make a config for this
// can be uncommented to limit which section positions are build and thus, rendered
// useful when debugging a specific section
-// if (renderSource.sectionPos.sectionDetailLevel == 6
-// && renderSource.sectionPos.sectionZ == 0 && renderSource.sectionPos.sectionX == 0)
+// if (renderSource.sectionPos.getDetailLevel() == 6
+// && renderSource.sectionPos.getZ() == 0 && renderSource.sectionPos.getX() == 0)
// {
// int test = 0;
// }
@@ -185,10 +198,11 @@ public class ColumnRenderBufferBuilder
{
for (int z = 0; z < ColumnRenderSource.SECTION_SIZE; z++)
{
+ // TODO make a config for this
// can be uncommented to limit the buffer building to a specific
// relative position in this section.
// useful for debugging a single column's rendering
-// if (x != 1 || z != 1)
+// if (x != 0 || (z != 0 && z != 1))
// {
// continue;
// }
@@ -304,6 +318,7 @@ public class ColumnRenderBufferBuilder
// We only stop when we find a block that is void or non-existing block
for (int i = 0; i < columnRenderData.size(); i++)
{
+ // TODO make a config for this
// can be uncommented to limit which vertical LOD is generated
// if (i != 0)
// {
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/columnViews/IColumnDataView.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/columnViews/IColumnDataView.java
index f0214991d..825824162 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/columnViews/IColumnDataView.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/columnViews/IColumnDataView.java
@@ -25,6 +25,7 @@ public interface IColumnDataView
{
long get(int index);
+ // FIXME probably horizontal size in blocks?
int size();
default Iterator iterator()
@@ -43,8 +44,10 @@ public interface IColumnDataView
};
}
+ // FIXME measured in blocks?
int verticalSize();
+ // FIXME how many datapoints in this LOD?
int dataCount();
IColumnDataView subView(int dataIndexStart, int dataCount);
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/ChunkToLodBuilder.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/ChunkToLodBuilder.java
index 7a7c496f9..7157d1271 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/ChunkToLodBuilder.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/ChunkToLodBuilder.java
@@ -100,6 +100,12 @@ public class ChunkToLodBuilder implements AutoCloseable
return;
}
+ ThreadPoolExecutor lodBuilderExecutor = ThreadPools.getChunkToLodBuilderExecutor();
+ if (lodBuilderExecutor == null)
+ {
+ return;
+ }
+
for (int i = 0; i < threadCount; i++)
{
@@ -114,7 +120,7 @@ public class ChunkToLodBuilder implements AutoCloseable
{
this.runningCount.decrementAndGet();
}
- }, ThreadPools.getChunkToLodBuilderExecutor());
+ }, lodBuilderExecutor);
}
}
private void tickThreadTask()
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/FullDataToRenderDataTransformer.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/FullDataToRenderDataTransformer.java
index f3d34615a..8f077881b 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/FullDataToRenderDataTransformer.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/FullDataToRenderDataTransformer.java
@@ -280,7 +280,7 @@ public class FullDataToRenderDataTransformer
// solid block check
- if (avoidSolidBlocks && !block.isSolid() && !block.isLiquid())
+ if (avoidSolidBlocks && !block.isSolid() && !block.isLiquid() && block.getOpacity() != IBlockStateWrapper.FULLY_OPAQUE)
{
if (colorBelowWithAvoidedBlocks)
{
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 b6513df41..f6eecfa6e 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
@@ -21,6 +21,7 @@ package com.seibel.distanthorizons.core.dataObjects.transformers;
import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
+import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.util.FullDataPointUtil;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
@@ -28,12 +29,15 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import it.unimi.dsi.fastutil.longs.LongArrayList;
+import org.apache.logging.log4j.Logger;
public class LodDataBuilder
{
-
+ private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IBlockStateWrapper AIR = SingletonInjector.INSTANCE.get(IWrapperFactory.class).getAirBlockStateWrapper();
+ private static boolean getTopErrorLogged = false;
+
public static ChunkSizedFullDataAccessor createChunkData(IChunkWrapper chunkWrapper)
{
@@ -65,10 +69,24 @@ public class LodDataBuilder
IBlockStateWrapper topBlockState = chunkWrapper.getBlockState(x, y, z);
while (!topBlockState.isAir() && y < chunkWrapper.getMaxBuildHeight())
{
- // This is necessary in some edge cases with snow layers and some other blocks that may not appear in the height map but do block light.
- // Interestingly this doesn't appear to be the case in the DhLightingEngine, if this same logic is added there the lighting breaks for the affected blocks.
- y++;
- topBlockState = chunkWrapper.getBlockState(x, y, z);
+ try
+ {
+ // This is necessary in some edge cases with snow layers and some other blocks that may not appear in the height map but do block light.
+ // Interestingly this doesn't appear to be the case in the DhLightingEngine, if this same logic is added there the lighting breaks for the affected blocks.
+ y++;
+ topBlockState = chunkWrapper.getBlockState(x, y, z);
+ }
+ catch (Exception e)
+ {
+ if (!getTopErrorLogged)
+ {
+ LOGGER.warn("Unexpected issue in LodDataBuilder, future errors won't be logged. Chunk [" + chunkWrapper.getChunkPos() + "] with max height: [" + chunkWrapper.getMaxBuildHeight() + "] had issue getting block at pos [" + x + "," + y + "," + z + "] error: " + e.getMessage(), e);
+ getTopErrorLogged = true;
+ }
+
+ y--;
+ break;
+ }
}
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataMetaFile.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataMetaFile.java
index c13f21890..3c09bfa5b 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataMetaFile.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataMetaFile.java
@@ -247,7 +247,7 @@ public class FullDataMetaFile extends AbstractMetaDataContainerFile implements I
ThreadPoolExecutor executor = ThreadPools.getFileHandlerExecutor();
- if (!executor.isTerminated())
+ if (executor != null && !executor.isTerminated())
{
// load the data source
@@ -344,7 +344,7 @@ public class FullDataMetaFile extends AbstractMetaDataContainerFile implements I
else
{
ThreadPoolExecutor executor = ThreadPools.getFileHandlerExecutor();
- if (!executor.isTerminated())
+ if (executor != null && !executor.isTerminated())
{
// wait for the update to finish before returning the data source
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/RenderSourceFileHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/RenderSourceFileHandler.java
index 6396ba783..d417929e6 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/RenderSourceFileHandler.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/RenderSourceFileHandler.java
@@ -104,7 +104,7 @@ public class RenderSourceFileHandler implements IRenderSourceProvider
{
// don't continue if the handler has been shut down
ThreadPoolExecutor executor = ThreadPools.getFileHandlerExecutor();
- if (executor.isTerminated())
+ if (executor != null && executor.isTerminated())
{
return CompletableFuture.completedFuture(null);
}
@@ -266,11 +266,14 @@ public class RenderSourceFileHandler implements IRenderSourceProvider
private String[] f3Log()
{
ThreadPoolExecutor executor = ThreadPools.getFileHandlerExecutor();
+ String queueSize = executor != null ? executor.getQueue().size()+"" : "-";
+ String completedTaskSize = executor != null ? executor.getCompletedTaskCount()+"" : "-";
+
ArrayList lines = new ArrayList<>();
lines.add("Render Source File Handler [" + this.clientLevel.getClientLevelWrapper().getDimensionType().getDimensionName() + "]");
lines.add(" Loaded files: " + this.loadedMetaFileBySectionPos.size());
- lines.add(" Thread pool tasks: " + executor.getQueue().size() + " (completed: " + executor.getCompletedTaskCount() + ")");
+ lines.add(" Thread pool tasks: " + queueSize + " (completed: " + completedTaskSize + ")");
int totalFutures = this.taskTracker.size();
EnumMap tasksOutstanding = new EnumMap<>(ETaskType.class);
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/subDimMatching/SubDimCompare.java b/core/src/main/java/com/seibel/distanthorizons/core/file/subDimMatching/SubDimCompare.java
index 1688efd20..7cb01b7ae 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/file/subDimMatching/SubDimCompare.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/file/subDimMatching/SubDimCompare.java
@@ -57,7 +57,7 @@ public class SubDimCompare implements Comparable
}
/** returns a number between 0 (no equal datapoint) and 1 (totally equal) */
- public double getPercentEqual() { return (double) equalDataPoints / (double) totalDataPoints; }
+ public double getPercentEqual() { return (double) this.equalDataPoints / (double) this.totalDataPoints; }
@Override
@@ -66,7 +66,7 @@ public class SubDimCompare implements Comparable
if (this.equalDataPoints != other.equalDataPoints)
{
// compare based on data points
- return Integer.compare(this.equalDataPoints, other.equalDataPoints);
+ return Double.compare(this.getPercentEqual(), other.getPercentEqual());
}
else
{
@@ -83,4 +83,7 @@ public class SubDimCompare implements Comparable
|| this.playerPosDist <= MAX_SIMILAR_PLAYER_POS_DISTANCE_IN_BLOCKS;
}
+ @Override
+ public String toString() { return this.equalDataPoints + "/" + this.totalDataPoints + ": " + this.getPercentEqual() + " playerPos: " + this.playerPosDist; }
+
}
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/subDimMatching/SubDimensionLevelMatcher.java b/core/src/main/java/com/seibel/distanthorizons/core/file/subDimMatching/SubDimensionLevelMatcher.java
index 48bfd88b4..078b41cb4 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/file/subDimMatching/SubDimensionLevelMatcher.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/file/subDimMatching/SubDimensionLevelMatcher.java
@@ -332,7 +332,7 @@ public class SubDimensionLevelMatcher implements AutoCloseable
String subDimShortName = LodUtil.shortenString(testLevelFolder.getName(), 8); // variables are separated out for easier debugging
- String equalPercent = LodUtil.shortenString(subDimCompare.getPercentEqual()+"", 5);
+ String equalPercent = LodUtil.shortenString(mostSimilarSubDim.getPercentEqual()+"", 5);
LOGGER.info("Sub dimension ["+subDimShortName+"...] is current dimension probability: "+equalPercent+" ("+equalDataPoints+"/"+totalDataPointCount+")");
}
catch (Exception e)
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/DhLightingEngine.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/DhLightingEngine.java
index d5d2fc1fe..86c6a54ed 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/generation/DhLightingEngine.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/DhLightingEngine.java
@@ -72,6 +72,8 @@ public class DhLightingEngine
DhChunkPos centerChunkPos = centerChunk.getChunkPos();
AdjacentChunkHolder adjacentChunkHolder = new AdjacentChunkHolder(centerChunk);
+ long startTimeNs = System.nanoTime();
+
// try-finally to handle the stableArray resources
StableLightPosStack blockLightPosQueue = null;
@@ -143,25 +145,25 @@ public class DhLightingEngine
{
for (int relZ = 0; relZ < LodUtil.CHUNK_WIDTH; relZ++)
{
- // get the light
- int maxY = chunk.getLightBlockingHeightMapValue(relX, relZ);
- DhBlockPos skyLightPos = new DhBlockPos(chunk.getMinBlockX() + relX, maxY, chunk.getMinBlockZ() + relZ);
- if (skyLightPos.y < chunk.getMinBuildHeight() || skyLightPos.y > chunk.getMaxBuildHeight())
+ // set each pos' sky light all the way down until a opaque block is hit
+ for (int y = chunk.getMaxBuildHeight(); y >= chunk.getMinBuildHeight(); y--)
{
- // this shouldn't normally happen
- if (!warningLogged)
+ IBlockStateWrapper block = chunk.getBlockState(relX, y, relZ);
+ if (block != null && block.getOpacity() != IBlockStateWrapper.FULLY_TRANSPARENT)
{
- warningLogged = true;
- LOGGER.debug("Lighting chunk at pos " + chunk.getChunkPos() + " may have a missing or incomplete heightmap. Chunk min/max [" + chunk.getMinBuildHeight() + "/" + chunk.getMaxBuildHeight() + "], skylight pos: " + skyLightPos);
+ // keep moving down until we find a non-transparent block
+ break;
}
- continue;
+
+
+ // add sky light to the queue
+ DhBlockPos skyLightPos = new DhBlockPos(chunk.getMinBlockX() + relX, y, chunk.getMinBlockZ() + relZ);
+ skyLightPosQueue.push(skyLightPos.x, skyLightPos.y, skyLightPos.z, maxSkyLight);
+
+ // set the chunk's sky light
+ skyLightPos.mutateToChunkRelativePos(relBlockPos);
+ chunk.setDhSkyLight(relBlockPos.x, relBlockPos.y, relBlockPos.z, maxSkyLight);
}
- skyLightPosQueue.push(skyLightPos.x, skyLightPos.y, skyLightPos.z, maxSkyLight);
-
-
- // set the light
- skyLightPos.mutateToChunkRelativePos(relBlockPos);
- chunk.setDhSkyLight(relBlockPos.x, relBlockPos.y, relBlockPos.z, maxSkyLight);
}
}
}
@@ -199,7 +201,10 @@ public class DhLightingEngine
centerChunk.setIsDhLightCorrect(true);
centerChunk.setUseDhLighting(true);
- LOGGER.trace("Finished generating lighting for chunk: [" + centerChunkPos + "]");
+
+ long endTimeNs = System.nanoTime();
+ float totalTimeMs = (endTimeNs - startTimeNs) / 1_000_000.0f;
+ LOGGER.trace("Finished generating lighting for chunk: [" + centerChunkPos + "] in ["+totalTimeMs+"] milliseconds");
}
/** Applies each {@link LightPos} from the queue to the given set of {@link IChunkWrapper}'s. */
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 0fd8cafe4..3a4510aed 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
@@ -518,7 +518,8 @@ public class WorldGenerationQueue implements IWorldGenerationQueue, IDebugRender
try
{
int waitTimeInSeconds = 3;
- if (!ThreadPools.getWorldGenExecutor().awaitTermination(waitTimeInSeconds, TimeUnit.SECONDS))
+ ThreadPoolExecutor executor = ThreadPools.getWorldGenExecutor();
+ if (executor != null && !executor.awaitTermination(waitTimeInSeconds, TimeUnit.SECONDS))
{
LOGGER.warn("World generator thread pool shutdown didn't complete after [" + waitTimeInSeconds + "] seconds. Some world generator requests may still be running.");
}
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/jar/installer/WebDownloader.java b/core/src/main/java/com/seibel/distanthorizons/core/jar/installer/WebDownloader.java
index 7a2184648..6be2f9f25 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/jar/installer/WebDownloader.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/jar/installer/WebDownloader.java
@@ -74,13 +74,19 @@ public class WebDownloader
fos, 1024))
{
byte[] data = new byte[1024];
- int i;
+ int i, percent = -1;
while ((i = in.read(data, 0, 1024)) >= 0)
{
totalDataRead = totalDataRead + i;
bout.write(data, 0, i);
-// int percent = (int) ((totalDataRead * 100) / filesize);
-// System.out.println(percent);
+
+ // TODO: Link this to an atomic integer rather than printing it to log
+ int newPercent = (int) ((totalDataRead * 100) / filesize);
+ if (percent != newPercent)
+ {
+ percent = newPercent;
+ System.out.println(String.valueOf(percent) +"% downloaded");
+ }
}
}
}
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/jar/updater/SelfUpdater.java b/core/src/main/java/com/seibel/distanthorizons/core/jar/updater/SelfUpdater.java
index 6f5894d93..380aa58b5 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/jar/updater/SelfUpdater.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/jar/updater/SelfUpdater.java
@@ -117,7 +117,7 @@ public class SelfUpdater
LOGGER.info("New version (" + ModrinthGetter.getLatestNameForVersion(mcVersion) + ") of " + ModInfo.READABLE_NAME + " is available");
- newFileLocation = JarUtils.jarFile.getParentFile().toPath().resolve("update").resolve(ModInfo.NAME + "-" + ModrinthGetter.getLatestNameForVersion(mcVersion) + ".jar").toFile();
+ newFileLocation = JarUtils.jarFile.getParentFile().toPath().resolve("update").resolve(ModInfo.NAME + "-" + ModrinthGetter.getLatestNameForVersion(mcVersion) + "-" + mcVersion + ".jar").toFile();
if (Config.Client.Advanced.AutoUpdater.enableSilentUpdates.get())
{
// Auto-update mod
@@ -141,7 +141,7 @@ public class SelfUpdater
if (!pipeline.get("status").equals("success"))
{
- LOGGER.warn("Pipeline for branch ["+ ModJarInfo.Git_Branch +"], commit ["+ pipeline.get("id") +"], has either failed to build, or still building.");
+ LOGGER.warn("Pipeline for branch ["+ ModJarInfo.Git_Branch +"], pipeline ID ["+ pipeline.get("id") +"], has either failed to build, or is still building.");
return false;
}
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java
index f19c3ba5a..561695128 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java
@@ -138,8 +138,15 @@ public class DhClientLevel extends DhLevel implements IDhClientLevel
@Override
public void clientTick()
{
- this.chunkToLodBuilder.tick();
- this.clientside.clientTick();
+ try
+ {
+ this.chunkToLodBuilder.tick();
+ this.clientside.clientTick();
+ }
+ catch (Exception e)
+ {
+ LOGGER.error("Unexpected clientTick Exception: "+e.getMessage(), e);
+ }
}
public void doWorldGen()
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java b/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java
index 25c09c34b..dbb6358cc 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java
@@ -324,10 +324,10 @@ public class LodRenderSection implements IDebugRenderable
if (showRenderSectionStatus && this.pos.getDetailLevel() == DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL)
{
DebugRenderer.makeParticle(
- new DebugRenderer.BoxParticle(
- new DebugRenderer.Box(this.pos, 32f, 64f, 0.2f, Color.yellow),
- 0.5, 16f
- )
+ new DebugRenderer.BoxParticle(
+ new DebugRenderer.Box(this.pos, 32f, 64f, 0.2f, Color.yellow),
+ 0.5, 16f
+ )
);
}
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/LodRenderProgram.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/LodRenderProgram.java
index bd2d64a73..66b63a02e 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/LodRenderProgram.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/LodRenderProgram.java
@@ -30,6 +30,7 @@ import com.seibel.distanthorizons.core.render.glObject.vertexAttribute.VertexAtt
import com.seibel.distanthorizons.core.render.glObject.vertexAttribute.VertexPointer;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.render.fog.LodFogConfig;
+import com.seibel.distanthorizons.core.util.RenderUtil;
import com.seibel.distanthorizons.coreapi.util.math.Mat4f;
import com.seibel.distanthorizons.coreapi.util.math.Vec3f;
import com.seibel.distanthorizons.core.wrapperInterfaces.IVersionConstants;
@@ -54,7 +55,8 @@ public class LodRenderProgram extends ShaderProgram
public final int lightMapUniform;
- // Fog Uniforms
+ // Fog/Clip Uniforms
+ public final int clipDistanceUniform;
// Noise Uniforms
public final int noiseEnabledUniform;
@@ -84,6 +86,8 @@ public class LodRenderProgram extends ShaderProgram
lightMapUniform = getUniformLocation("lightMap");
+ // Fog/Clip Uniforms
+ clipDistanceUniform = getUniformLocation("clipDistance");
// Noise Uniforms
noiseEnabledUniform = getUniformLocation("noiseEnabled");
@@ -166,10 +170,10 @@ public class LodRenderProgram extends ShaderProgram
vao.unbindBuffersFromAllBindingPoint();
}
- public void fillUniformData(Mat4f combinedMatrix, int lightmapBindPoint, int worldYOffset, int vanillaDrawDistance)
+ public void fillUniformData(Mat4f combinedMatrix, int lightmapBindPoint, int worldYOffset, float partialTicks)
{
super.bind();
- vanillaDrawDistance += 32; // Give it a 2 chunk boundary for near fog.
+
// uniforms
setUniform(combinedMatUniform, combinedMatrix);
setUniform(mircoOffsetUniform, 0.01f); // 0.01 block offset
@@ -182,6 +186,9 @@ public class LodRenderProgram extends ShaderProgram
// Debug
setUniform(whiteWorldUniform, Config.Client.Advanced.Debugging.enableWhiteWorld.get());
+ // Fog/Clip Uniforms
+ float dhNearClipDistance = RenderUtil.getNearClipPlaneDistanceInBlocks(partialTicks);
+ setUniform(clipDistanceUniform, dhNearClipDistance);
}
public void setModelPos(Vec3f modelPos)
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/LodRenderer.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/LodRenderer.java
index 840c35abc..1f2b4633f 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/LodRenderer.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/LodRenderer.java
@@ -254,7 +254,7 @@ public class LodRenderer
// and often do change the projection entirely, as well as the output usage.
//EVENT_LOGGER.debug("Skipping shadow pass render.");
- //return;
+ return;
}
// Note: Since lightmapTexture is changing every frame, it's faster to recreate it than to reuse the old one.
@@ -365,7 +365,7 @@ public class LodRenderer
}
/*---------Get required data--------*/
- int vanillaBlockRenderedDistance = MC_RENDER.getRenderDistance() * LodUtil.CHUNK_WIDTH;
+ //int vanillaBlockRenderedDistance = MC_RENDER.getRenderDistance() * LodUtil.CHUNK_WIDTH;
//Mat4f modelViewProjectionMatrix = RenderUtil.createCombinedModelViewProjectionMatrix(baseProjectionMatrix, baseModelViewMatrix, partialTicks);
Mat4f projectionMatrix = RenderUtil.createLodProjectionMatrix(baseProjectionMatrix, partialTicks);
@@ -375,7 +375,7 @@ public class LodRenderer
/*---------Fill uniform data--------*/
this.shaderProgram.fillUniformData(modelViewProjectionMatrix, /*Light map = GL_TEXTURE0*/ 0,
- MC.getWrappedClientLevel().getMinHeight(), vanillaBlockRenderedDistance);
+ MC.getWrappedClientLevel().getMinHeight(), partialTicks);
lightmap.bind();
if (ENABLE_IBO)
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointReducingList.java b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointReducingList.java
new file mode 100644
index 000000000..800763083
--- /dev/null
+++ b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointReducingList.java
@@ -0,0 +1,1049 @@
+/*
+ * This file is part of the Distant Horizons mod
+ * licensed under the GNU LGPL v3 License.
+ *
+ * Copyright (C) 2020-2023 James Seibel
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package com.seibel.distanthorizons.core.util;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView;
+import com.seibel.distanthorizons.core.dataObjects.render.columnViews.IColumnDataView;
+import com.seibel.distanthorizons.core.util.LodUtil.AssertFailureException;
+import it.unimi.dsi.fastutil.longs.LongArrays;
+import it.unimi.dsi.fastutil.shorts.ShortArrays;
+
+/**
+ * A list of data points whose sole purpose is to {@link #reduce(int)} them.
+ * Each data point, henceforth referred to as a "node", is represented by 2 packed longs.
+ * The "data" long contains the data point itself, as encoded by
+ * {@link RenderDataPointUtil#createDataPoint(int, int, int, int, int, int, int, int, int)}.
+ * The "links" long contains 4 packed 16-bit integers, which "point" to other nodes
+ * In the sense that the index represented by the integer is another node in this list.
+ * The 4 links are: bigger, smaller, higher, and lower.
+ * All nodes are stored in 2 parallel long[]'s, namely {@link #data} and {@link #links}.
+ *
+ * All nodes are internally sorted in 2 different orders at the same time:
+ * lowest-to-highest, and smallest-to-biggest.
+ * Both of these orders are important for reduction logic.
+ * Traversal in both orders is equally possible and important.
+ *
+ * @author Builderb0y
+ */
+public class RenderDataPointReducingList
+{
+
+ /**
+ * Setting this to true will cause the list to sanity-check
+ * its own links automatically every time it modifies itself.
+ * This is mostly just useful for debugging.
+ * This should be set to false in production,
+ * because these sanity checks are slow and happen often.
+ */
+ private static final boolean ASSERTS = false;
+ /**
+ * Number of special cases to use for step 1 of {@link #reduce(int)}.
+ * 2 works well for big globe worlds.
+ * 0 is probably better for vanilla, but vanilla has vastly fewer segments/nodes
+ * than big globe does, so the difference in efficiency matters a lot less.
+ */
+ private static final int SPECIAL_CASES = 2;
+
+ /** the bit offset of {@link #links} where the lower link is stored. */
+ public static final int LOWER_SHIFT = 0;
+ /** the bit offset of {@link #links} where the higher link is stored. */
+ public static final int HIGHER_SHIFT = 16;
+ /** the bit offset of {@link #links} where the smaller link is stored. */
+ public static final int SMALLER_SHIFT = 32;
+ /** the bit offset of {@link #links} where the bigger link is stored. */
+ public static final int BIGGER_SHIFT = 48;
+ /**
+ * a bit mask for extracting links from elements of {@link #links}.
+ * all links are 16 bits in length, so this constant has the lower 16 bits set,
+ * and all remaining bits cleared.
+ */
+ public static final int LINK_MASK = 0xFFFF;
+ /** a constant to indicate that a link is non-existent. */
+ public static final int NULL = LINK_MASK;
+
+ /** the default element of {@link #data} to indicate that there is no data. */
+ public static final long DEFAUlT_DATA = 0L;
+ /** the default element of {@link #links} to indicate that a node is not linked to any other nodes. */
+ public static final long DEFAULT_LINKS = -1L;
+
+ /**
+ * indexes of the nodes at the ends of this list.
+ * access these fields through the getters,
+ * not by the backing fields. the getters will
+ * perform automatic short <-> int conversions.
+ *
+ * @implNote these fields behave as if they were unsigned,
+ * and the getters will behave accordingly.
+ * not that DH supports a wide enough Y range
+ * to overflow these fields, but still.
+ */
+ private short lowest, highest, smallest, biggest;
+ private short sizeWithAir, sizeWithoutAir;
+ private final long[] links, data;
+ /**
+ * a temporary array to be used for sorting nodes.
+ * the array is first populated such that every index
+ * up to our current size represents a valid index.
+ * then this array is sorted.
+ * finally, the nodes are re-linked according
+ * to the order of elements in this array.
+ */
+ private final short[] sortingArray;
+
+
+
+ //=============//
+ // constructor //
+ //=============//
+
+ public RenderDataPointReducingList(IColumnDataView view)
+ {
+ int size = view.size();
+ if (size == 0)
+ {
+ this.setLowest(NULL);
+ this.setHighest(NULL);
+ this.setSmallest(NULL);
+ this.setBiggest(NULL);
+ this.links = LongArrays.EMPTY_ARRAY;
+ this.data = LongArrays.EMPTY_ARRAY;
+ this.sortingArray = ShortArrays.EMPTY_ARRAY;
+ if (ASSERTS) this.checkLinks();
+
+ return;
+ }
+
+ // Allocate an array big enough to hold (2 * size - 1) nodes.
+ // This is the number of nodes we would have if none
+ // of the nodes in the provided view are touching,
+ // and we need to add air nodes between all of them.
+ // We will use this array for sorting the nodes,
+ // first by lowest-to-highest, then by smallest-to-biggest.
+ int arrayCapacity = (size << 1) - 1;
+ this.sortingArray = new short[arrayCapacity];
+ this.links = new long[arrayCapacity];
+ java.util.Arrays.fill(this.links, DEFAULT_LINKS);
+ this.data = new long[arrayCapacity];
+ int sizeWithoutAir = 0;
+ for (int index = 0; index < size; index++)
+ {
+ long packedData = view.get(index);
+ // first "pass" (if you can call it that) skips nodes with 0 height, and nodes that aren't visible.
+ // air nodes will be inserted *after* the nodes have been sorted by Y level.
+ if (isDataVisible(packedData) && RenderDataPointUtil.getYMin(packedData) < RenderDataPointUtil.getYMax(packedData))
+ {
+ this.setData(sizeWithoutAir, packedData);
+ this.setSortingIndex(sizeWithoutAir, sizeWithoutAir);
+ sizeWithoutAir++;
+ }
+ }
+
+ // Check if all segments to merge are air or otherwise invisible (barriers).
+ // If they are, then this list can stay empty.
+ if (sizeWithoutAir == 0)
+ {
+ this.setLowest(NULL);
+ this.setHighest(NULL);
+ this.setSmallest(NULL);
+ this.setBiggest(NULL);
+ if (ASSERTS) this.checkLinks();
+
+ return;
+ }
+
+ //sort the nodes by Y level.
+ this.sortByPosition(sizeWithoutAir);
+ //next pass: link the nodes together, and insert air nodes as necessary.
+ int sizeWithAir = sizeWithoutAir;
+ for (int sortingIndex = 1; sortingIndex < sizeWithoutAir; sortingIndex++)
+ {
+ int lowerIndex = this.getSortingIndex(sortingIndex - 1);
+ int higherIndex = this.getSortingIndex(sortingIndex);
+ long lowerData = this.getData(lowerIndex);
+ long higherData = this.getData(higherIndex);
+ int lowerMaxY = RenderDataPointUtil.getYMax(lowerData);
+ int higherMinY = RenderDataPointUtil.getYMin(higherData);
+
+ if (lowerMaxY == higherMinY)
+ {
+ //the two nodes touch.
+ this.setHigher(lowerIndex, higherIndex);
+ this.setLower(higherIndex, lowerIndex);
+ }
+ else if (lowerMaxY < higherMinY)
+ {
+ //the two nodes do not touch.
+ this.setData(
+ sizeWithAir,
+ RenderDataPointUtil.createDataPoint(
+ 0,
+ 0,
+ 0,
+ 0,
+ higherMinY,
+ lowerMaxY,
+ RenderDataPointUtil.getLightSky(higherData),
+ RenderDataPointUtil.getLightBlock(higherData),
+ RenderDataPointUtil.getGenerationMode(higherData)
+ )
+ );
+
+ this.setSortingIndex(sizeWithAir, sizeWithAir);
+ this.setLower(higherIndex, sizeWithAir);
+ this.setHigher(lowerIndex, sizeWithAir);
+ this.setLower(sizeWithAir, lowerIndex);
+ this.setHigher(sizeWithAir, higherIndex);
+ sizeWithAir++;
+ }
+ else
+ {
+ // the two nodes overlap.
+ throw new IllegalArgumentException(RenderDataPointUtil.toString(lowerData) + " overlaps with " + RenderDataPointUtil.toString(higherData));
+ }
+ }
+ this.lowest = this.sortingArray[0];
+ this.highest = this.sortingArray[sizeWithoutAir - 1];
+
+ // now sort by size.
+ this.sortBySize(sizeWithAir);
+ for (int sortingIndex = 1; sortingIndex < sizeWithAir; sortingIndex++)
+ {
+ int smallerIndex = this.getSortingIndex(sortingIndex - 1);
+ int biggerIndex = this.getSortingIndex(sortingIndex);
+ this.setBigger(smallerIndex, biggerIndex);
+ this.setSmaller(biggerIndex, smallerIndex);
+ }
+
+ this.smallest = this.sortingArray[0];
+ this.biggest = this.sortingArray[sizeWithAir - 1];
+
+ this.setSizeWithAir(sizeWithAir);
+ this.setSizeWithoutAir(sizeWithoutAir);
+
+ if (ASSERTS) this.checkLinks();
+ }
+
+
+
+ //========//
+ // reduce //
+ //========//
+
+ /**
+ * merges and/or eliminates nodes until our {@link #sizeWithoutAir}
+ * is less than or equal to the provided target size.
+ * this method assumes that the list is already sorted by size.
+ * if it is not sorted, you should call {@link #sortBySizeAndReLink()} first.
+ * note also that the list is sorted in its constructor,
+ * so if this is a new, unmodified list, then it is safe to call this method.
+ *
+ * algorithm:
+ * 1: try to merge the smallest segment with the segment above or below it.
+ * this will only succeed if the adjacent node has the same alpha as it.
+ * 1a: if there is only one adjacent node which matches this criteria,
+ * we will merge with that node.
+ *
+ * 1b: if both adjacent nodes match this criteria,
+ * attempt to merge with the smaller one.
+ * 1b1: if both adjacent nodes are the same height,
+ * merge with the higher one.
+ *
+ * 1c: if there are no adjacent nodes which match this criteria,
+ * repeat step 1 with the next smallest segment instead.
+ * continue trying bigger and bigger segments until we either:
+ * * have a success, or
+ * * reach the end of this list.
+ * 2: if we reach the end of the list before having a success, try again,
+ * but this time, we are allowed to erase a segment entirely without merging it
+ * if there are equal-alpha'd segments above and below it.
+ * 3: if we still fail, force the lowest segment to merge with the segment above it,
+ * with no restrictions on alpha.
+ * the highest alpha of the two segments takes priority though.
+ * 4: repeat until our size is less than or equal to the target size.
+ *
+ * notes:
+ * changing the size of a node requires re-sorting that node,
+ * but it does not require re-sorting the whole list.
+ * additionally, because of the fact that nodes are sorted smallest to biggest,
+ * when a node is expanded, its new size will be
+ * strictly less than or equal to twice its old size.
+ * the significance of this is that in practice,
+ * nodes should not need to be moved very far to be re-sorted.
+ *
+ * special case: there are a lot of segments of length 1 in big globe worlds.
+ * these will genuinely have a long way to move on re-sort.
+ * so, they are handled in a separate loop.
+ *
+ * after step 1 is completed, step 2 can't change the
+ * list in a way which would give step 1 more work to do,
+ * so step 2 is repeated as many times as necessary,
+ * without jumping back to the start.
+ * step 3 however can change the list in a way which gives previous
+ * steps more work to do, so after step 3 merges something,
+ * we jump back to step 1 and start over.
+ */
+ public void reduce(int target)
+ {
+ if (this.mergeVerySmallConnectedSegments(target)) return;
+
+ if (this.mergeConnectedSegments(target)) return;
+ if (this.removeLeastImportantSegments(target)) return;
+ this.forceBottomToMerge(target);
+ }
+
+
+
+ //======================//
+ // reduction operations //
+ //======================//
+
+ /**
+ * verifies that this list is in the "correct" state,
+ * and throws an {@link AssertFailureException} if it isn't.
+ */
+ @VisibleForTesting
+ public void checkLinks()
+ {
+ LodUtil.assertTrue(this.getSizeWithAir() >= 0, "size with air < 0");
+ LodUtil.assertTrue(this.getSizeWithoutAir() >= 0, "size without air < 0");
+ LodUtil.assertTrue(this.getSizeWithoutAir() <= this.getSizeWithAir(), "more segments without air than with air");
+
+ if (this.getSizeWithAir() == 0)
+ {
+ LodUtil.assertTrue(this.getSmallest() == NULL, "size is 0, but we have a smallest node");
+ LodUtil.assertTrue(this.getBiggest() == NULL, "size is 0, but we have a biggest node");
+ LodUtil.assertTrue(this.getLowest() == NULL, "size is 0, but we have a lowest node");
+ LodUtil.assertTrue(this.getHighest() == NULL, "size is 0, but we have a highest node");
+ }
+ else
+ {
+ int sizeWithAir = 0, sizeWithoutAir = 0;
+ for (int index = this.getSmallest(); index != NULL; index = this.getBigger(index))
+ {
+ int smaller = this.getSmaller(index);
+ int bigger = this.getBigger(index);
+ LodUtil.assertTrue((smaller != NULL ? this.getBigger(smaller) : this.getSmallest()) == index, "one-way link");
+ LodUtil.assertTrue((bigger != NULL ? this.getSmaller(bigger) : this.getBiggest()) == index, "one-way link");
+ LodUtil.assertTrue(smaller == NULL || this.getSize(index) >= this.getSize(smaller), "node is not sorted by size");
+ sizeWithAir++;
+
+ if (this.isIndexVisible(index)) sizeWithoutAir++;
+ }
+ LodUtil.assertTrue(sizeWithAir == this.getSizeWithAir() && sizeWithoutAir == this.getSizeWithoutAir(), "node count does not match size");
+
+ sizeWithAir = sizeWithoutAir = 0;
+ for (int index = this.getLowest(); index != NULL; index = this.getHigher(index))
+ {
+ int lower = this.getLower(index);
+ int higher = this.getHigher(index);
+ LodUtil.assertTrue((lower != NULL ? this.getHigher(lower) : this.getLowest()) == index, "one-way link");
+ LodUtil.assertTrue((higher != NULL ? this.getLower(higher) : this.getHighest()) == index, "one-way link");
+ LodUtil.assertTrue(this.getMaxY(index) > this.getMinY(index), "node has inverted Y levels");
+ LodUtil.assertTrue(lower == NULL || this.getMinY(index) == this.getMaxY(lower), "node does not touch its lower neighbor");
+ sizeWithAir++;
+
+ if (this.isIndexVisible(index)) sizeWithoutAir++;
+ }
+ LodUtil.assertTrue(sizeWithAir == this.getSizeWithAir() && sizeWithoutAir == this.getSizeWithoutAir(), "node count does not match size");
+ }
+ }
+
+ /** removes the node at the given index from this list. */
+ public void remove(int index)
+ {
+ int lower = this.getLower (index);
+ int higher = this.getHigher (index);
+ int smaller= this.getSmaller(index);
+ int bigger = this.getBigger (index);
+ int alpha = this.getAlpha (index);
+
+
+ if (lower != NULL) this.setHigher(lower, higher);
+ else this.setLowest(higher);
+
+ if (higher != NULL) this.setLower(higher, lower);
+ else this.setHighest(lower);
+
+ if (smaller != NULL) this.setBigger(smaller, bigger);
+ else this.setSmallest(bigger);
+
+ if (bigger != NULL) this.setSmaller(bigger, smaller);
+ else this.setBiggest(smaller);
+
+ this.setData(index, DEFAUlT_DATA);
+ this.links[index] = DEFAULT_LINKS;
+ this.sizeWithAir--;
+
+ if (isAlphaVisible(alpha)) this.sizeWithoutAir--;
+ }
+
+ /**
+ * refreshes the smallest-to-biggest order of this list.
+ * as a reminder, the list is internally sorted from smallest-to-biggest
+ * and lowest-to-highest at the same time. part of reduction logic
+ * can invalidate the smallest-to-biggest order, so this method re-computes it.
+ * this method does not touch the lowest-to-highest order of the list.
+ *
+ * this method requires that all nodes are already sorted from
+ * lowest-to-highest, so it is not applicable to use this method in
+ * the constructor before the lowest-to-highest order is initialized.
+ */
+ @VisibleForTesting
+ public void sortBySizeAndReLink()
+ {
+ if (this.getSizeWithAir() <= 1)
+ {
+ return;
+ }
+
+
+ long[] datas = this.data;
+ int writeIndex = 0;
+ for (int readIndex = this.getLowest(); readIndex != NULL; readIndex = this.getHigher(readIndex))
+ {
+ if (datas[readIndex] != DEFAUlT_DATA)
+ {
+ this.setSortingIndex(writeIndex++, readIndex);
+ }
+ }
+
+ this.sortBySize(writeIndex);
+ for (int index = 1; index < writeIndex; index++)
+ {
+ int smaller = this.getSortingIndex(index - 1);
+ int bigger = this.getSortingIndex(index);
+ this.setSmaller(bigger, smaller);
+ this.setBigger(smaller, bigger);
+ }
+
+ this.smallest = this.sortingArray[0];
+ this.biggest = this.sortingArray[writeIndex - 1];
+ this.setSmaller(this.getSmallest(), NULL);
+ this.setBigger(this.getBiggest(), NULL);
+ }
+
+ /**
+ * sorts our {@link #sortingArray} in order of smallest-to-biggest,
+ * but does NOT update our links accordingly.
+ */
+ @VisibleForTesting
+ public void sortBySize(int size)
+ {
+ short[] array = this.sortingArray;
+ it.unimi.dsi.fastutil.Arrays.quickSort(
+ 0,
+ size,
+ // comparator
+ (int index1, int index2) ->
+ {
+ return Integer.compare(
+ this.getSize(this.getSortingIndex(index1)),
+ this.getSize(this.getSortingIndex(index2))
+ );
+ },
+ // swapper
+ (int index1, int index2) ->
+ {
+ ShortArrays.swap(array, index1, index2);
+ }
+ );
+ }
+
+ /**
+ * sorts our {@link #sortingArray} in order of lowest-to-highest,
+ * but does NOT update our links accordingly.
+ */
+ @VisibleForTesting
+ public void sortByPosition(int size)
+ {
+ short[] array = this.sortingArray;
+ it.unimi.dsi.fastutil.Arrays.quickSort(
+ 0,
+ size,
+ // comparator
+ (int index1, int index2) ->
+ {
+ return Integer.compare(
+ this.getMinY(this.getSortingIndex(index1)),
+ this.getMinY(this.getSortingIndex(index2))
+ );
+ },
+ // swapper
+ (int index1, int index2) ->
+ {
+ ShortArrays.swap(array, index1, index2);
+ }
+ );
+ }
+
+ /**
+ * moves the smaller node to the correct position in the list,
+ * under the assumption that all other nodes are already sorted.
+ * this method should be called when the smaller node is
+ * merged with another node, causing it to become bigger.
+ *
+ * important: this method ONLY handles the case where a node
+ * is made bigger. it does NOT handle the case where a node
+ * is made smaller. if the node is made smaller, it will be
+ * left in its current position, even if that position is wrong.
+ */
+ public void resortSize(int smaller)
+ {
+ int bigger = this.getBigger(smaller);
+
+ // check if the node needs to be moved at all,
+ // and return if it doesn't.
+ if (bigger == NULL || this.getSize(smaller) <= this.getSize(bigger))
+ {
+ return;
+ }
+
+ // first remove smaller from before bigger.
+ int smallest = this.getSmaller(smaller);
+ if (smallest != NULL) this.setBigger(smallest, bigger);
+ else this.setSmallest(bigger);
+ this.setSmaller(bigger, smallest);
+
+ // next, find the position to re-insert the node.
+ do bigger = this.getBigger(bigger);
+ while (bigger != NULL && this.getSize(smaller) > this.getSize(bigger));
+
+ // lastly, re-insert the node where it belongs.
+ this.setSmaller(smaller, bigger != NULL ? this.getSmaller(bigger) : this.getBiggest());
+ this.setBigger(smaller, bigger);
+
+ if (bigger != NULL) this.setSmaller(bigger, smaller);
+ else this.setBiggest(smaller);
+
+ smallest = this.getSmaller(smaller);
+ if (smallest != NULL) this.setBigger(smallest, smaller);
+ else this.setSmallest(smaller);
+ }
+
+ /**
+ * shared logic for merging segments in step 1 documented in {@link #reduce(int)}.
+ *
+ * returns the index of the next node to be used for iteration.
+ *
+ * @param fastPath if true, we are in the "fast path" for removing
+ * segments whose size is less than or equal to {@link #SPECIAL_CASES}.
+ * this fast path functions somewhat differently from the normal path,
+ * the important things to note for this method are:
+ *
+ * the fast path does not re-sort nodes when their size changes.
+ * this leaves the list in an invalid state, and it is up to the caller to re-sort
+ * the list via {@link #sortBySizeAndReLink()} after the fast path is done.
+ *
+ * at the time of writing this, the fast path iterates in reverse order.
+ * as such, when fastPath is set to true, this method will return
+ * current's smaller neighbor, when fastPath is set to false,
+ * this method will return current's bigger neighbor instead.
+ */
+ private int tryMergeStep1(int current, boolean fastPath)
+ {
+ int result = fastPath ? this.getSmaller(current) : this.getBigger(current);
+ int higher = this.getHigher(current);
+ int lower = this.getLower(current);
+ int toExtendDownwards;
+ int toRemove;
+
+
+ if (higher != NULL && this.getAlpha(higher) == this.getAlpha(current))
+ {
+ if (lower != NULL && this.getAlpha(lower) == this.getAlpha(current))
+ {
+ if (this.getSize(higher) <= this.getSize(lower))
+ {
+ toExtendDownwards = higher;
+ toRemove = current;
+ }
+ else
+ {
+ toExtendDownwards = current;
+ toRemove = lower;
+ }
+ }
+ else
+ {
+ toExtendDownwards = higher;
+ toRemove = current;
+ }
+ }
+ else
+ {
+ if (lower != NULL && this.getAlpha(lower) == this.getAlpha(current))
+ {
+ toExtendDownwards = current;
+ toRemove = lower;
+ }
+ else
+ {
+ return result;
+ }
+ }
+
+ // if we're about to remove the next node for iteration,
+ // then we need to continue iterating at the node after that.
+ // result will only be returned if fastPath is true,
+ // so the node after that is always the smaller one. that's why I don't need to do
+ // if (result == toRemove) result = fastPath ? this.getSmaller(result) : this.getBigger(result);
+ if (result == toRemove) result = this.getSmaller(result);
+
+ this.setMinY(toExtendDownwards, this.getMinY(toRemove));
+ if (!fastPath) this.resortSize(toExtendDownwards);
+
+ this.remove(toRemove);
+
+ // if we're NOT on the fast path, and we reach this line,
+ // then we have just modified the list in a way which may
+ // invalidate assumptions made by the step 1 loop.
+ // so, return smallest to signal that the loop should start over.
+ // starting over is not usually a big deal,
+ // because small nodes are usually merged quite quickly.
+ // in my testing, I didn't see the step 1 loop run more
+ // than twice as many times as the starting list size.
+ return fastPath ? result : this.getSmallest();
+ }
+
+ /**
+ * returns the largest node whose height is strictly less than the provided size,
+ * or null if all contained nodes are greater than or equal to the provided size.
+ *
+ * special cases:
+ * if the list is empty, then null is returned,
+ * because the loop will not run and biggest will be null.
+ *
+ * if all nodes are less tall than size, then the largest node is returned,
+ * because the loop will run for all nodes, but will not return any of them,
+ * so the fallback path of returning the biggest node is used.
+ *
+ * if all nodes are at least as tall as size, then null is returned,
+ * because the loop will immediately return the
+ * smallest node's smaller neighbor, which is null.
+ */
+ private int lowerNode(int size)
+ {
+ for (int node = this.getSmallest(); node != NULL; node = this.getBigger(node))
+ {
+ if (this.getSize(node) >= size)
+ {
+ return this.getSmaller(node);
+ }
+ }
+ return this.getBiggest();
+ }
+
+ /**
+ * handles special cases for step 1 of {@link #reduce(int)}.
+ * in other words, handles all the nodes whose size
+ * is less than or equal to {@link #SPECIAL_CASES}.
+ *
+ * returns true if this step single-handedly brought
+ * the list's size down to less than or equal to target,
+ * or false if more steps need to be performed.
+ */
+ private boolean mergeVerySmallConnectedSegments(int target)
+ {
+ for (int specialCase = 1; specialCase <= SPECIAL_CASES; specialCase++)
+ {
+ for (int current = this.lowerNode(specialCase + 1); current != NULL; )
+ {
+ if (this.getSizeWithoutAir() <= target)
+ {
+ this.sortBySizeAndReLink();
+ if (ASSERTS) this.checkLinks();
+
+ return true;
+ }
+ current = this.tryMergeStep1(current, true);
+ }
+ this.sortBySizeAndReLink();
+
+ if (ASSERTS) this.checkLinks();
+ }
+
+ return false;
+ }
+
+ /**
+ * handles the general case for step 1 of {@link #reduce(int)}.
+ * in other words, handles all the nodes whose size
+ * is strictly greater than {@link #SPECIAL_CASES},
+ * and all the nodes which are smaller, but failed
+ * to be merged in {@link #mergeVerySmallConnectedSegments(int)}
+ *
+ * returns true if this step single-handedly brought
+ * the list's size down to less than or equal to target,
+ * or false if more steps need to be performed.
+ */
+ private boolean mergeConnectedSegments(int target)
+ {
+ for (int current = this.getSmallest(); current != NULL;)
+ {
+ if (this.getSizeWithoutAir() <= target)
+ {
+ return true;
+ }
+
+ current = this.tryMergeStep1(current, false);
+ if (ASSERTS) this.checkLinks();
+ }
+ return false;
+ }
+
+ /**
+ * handles step 2 of {@link #reduce(int)}, where nodes are allowed to be erased.
+ *
+ * returns true if this step single-handedly brought
+ * the list's size down to less than or equal to target,
+ * or false if more steps need to be performed.
+ */
+ private boolean removeLeastImportantSegments(int target)
+ {
+ for (int center = this.getSmallest(); center != NULL; )
+ {
+ if (this.getSizeWithoutAir() <= target)
+ {
+ return true;
+ }
+
+ int lower = this.getLower(center);
+ int higher = this.getHigher(center);
+ if (lower != NULL && higher != NULL && this.getAlpha(lower) == this.getAlpha(higher))
+ {
+ this.setMinY(higher, this.getMinY(lower));
+ this.resortSize(higher);
+ this.remove(lower);
+ this.remove(center);
+ if (ASSERTS) this.checkLinks();
+
+ center = this.getSmallest();
+ }
+ else
+ {
+ center = this.getBigger(center);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * handles step 3 of {@link #reduce(int)}, where nodes
+ * are forced to merge in order to fit the desired target,
+ * even if they normally shouldn't merge because it would look bad.
+ *
+ * returns after this step brings the list's
+ * size down to less than or equal to target.
+ */
+ private void forceBottomToMerge(int target)
+ {
+ for (int lowest = this.getLowest(); lowest != NULL; )
+ {
+ if (this.getSizeWithoutAir() <= target)
+ {
+ return;
+ }
+
+ int lowY = this.getMinY(lowest);
+ int higher = this.getHigher(lowest);
+ inner:
+ while (true)
+ {
+ if (higher == NULL)
+ {
+ //if we reach this line, then target is 0 or negative.
+ this.setLowest(NULL);
+ this.setHighest(NULL);
+ this.setSmallest(NULL);
+ this.setBiggest(NULL);
+ this.setSizeWithAir(0);
+ this.setSizeWithoutAir(0);
+
+ if (ASSERTS) this.checkLinks();
+
+ return;
+ }
+
+ // don't merge the lowest segment with an invisible segment.
+ // in other words, we don't want
+ // visible
+ // invisible
+ // visible
+ // to be replaced with
+ // visible
+ // invisible
+ // instead, we want to eliminate the invisible segment too,
+ // and set the minY of the top visible segment
+ // to the minY of the bottom visible segment.
+ if (this.isIndexVisible(higher))
+ {
+ this.setMinY(higher, lowY);
+ this.resortSize(higher);
+ this.remove(lowest);
+
+ if (ASSERTS) this.checkLinks();
+
+ lowest = this.getLowest();
+ break inner;
+ }
+ else
+ {
+ this.remove(lowest);
+ lowest = higher;
+ higher = this.getHigher(higher);
+ //don't update lowY.
+ }
+ }
+ }
+ }
+
+ /**
+ * reduces the view to a single data point,
+ * whose min Y is the lowest of all data points in the provided view,
+ * and every other property of the returned data point
+ * matches those of the data point with the highest
+ * Y level in the provided view.
+ *
+ * @implNote this method does not allocate any objects.
+ */
+ public static long reduceToOne(IColumnDataView view)
+ {
+ int size = view.size();
+ if (size <= 0)
+ {
+ return RenderDataPointUtil.createVoidDataPoint((byte)(1));
+ }
+
+ long highest;
+ int lowest;
+ int index = 0;
+ //first loop: find the first visible segment.
+ foundVisible:
+ {
+ for (; index < size; index++)
+ {
+ long dataPoint = view.get(index);
+ if (isDataVisible(dataPoint))
+ {
+ highest = dataPoint;
+ lowest = RenderDataPointUtil.getYMin(dataPoint);
+ break foundVisible;
+ }
+ }
+ //no visible segments, return void.
+ return RenderDataPointUtil.createVoidDataPoint((byte) (1));
+ }
+
+ //second loop: merge the rest of the segments.
+ for (; index < size; index++)
+ {
+ long dataPoint = view.get(index);
+ if (isDataVisible(dataPoint))
+ {
+ int y = RenderDataPointUtil.getYMin(dataPoint);
+ if (y > highest) highest = dataPoint;
+ else if (y < lowest) lowest = y;
+ }
+ }
+
+ return (highest & ~RenderDataPointUtil.DEPTH_SHIFTED_MASK) | ((lowest & RenderDataPointUtil.DEPTH_MASK) << RenderDataPointUtil.DEPTH_SHIFT);
+ }
+
+
+ /** transfers the contents of this list to the provided view, in order of highest to lowest. */
+ public void copyTo(ColumnArrayView view)
+ {
+ // reminder: DH explodes horribly when I copy the nodes
+ // from lowest to highest instead of highest to lowest.
+ int writeIndex = 0;
+ for (int node = this.getHighest(); node != NULL; node = this.getLower(node))
+ {
+ if (this.isIndexVisible(node))
+ {
+ view.set(writeIndex++, this.getData(node));
+ }
+ }
+
+ // this list could be empty if all the segments for merging are invisible,
+ // but we must ensure that the view is non-empty.
+ // so, if we didn't set any data points, add a void data point.
+ if (writeIndex == 0)
+ {
+ view.set(writeIndex++, RenderDataPointUtil.createVoidDataPoint((byte) (1)));
+ }
+
+ for (int size = view.size(); writeIndex < size; writeIndex++)
+ {
+ view.set(writeIndex, RenderDataPointUtil.EMPTY_DATA);
+ }
+ }
+
+
+
+ //=========//
+ // getters //
+ //=========//
+
+ public int getSmallest() { return Short.toUnsignedInt(this.smallest); }
+ public int getBiggest() { return Short.toUnsignedInt(this.biggest); }
+
+ public int getLowest() { return Short.toUnsignedInt(this.lowest); }
+ public int getHighest() { return Short.toUnsignedInt(this.highest); }
+
+ public int getSizeWithAir() { return Short.toUnsignedInt(this.sizeWithAir); }
+ public int getSizeWithoutAir() { return Short.toUnsignedInt(this.sizeWithoutAir); }
+
+ public int getSortingIndex(int index) { return Short.toUnsignedInt(this.sortingArray[index]); }
+
+ public int getLower(int index) { return ((int) (this.links[index] >>> LOWER_SHIFT)) & LINK_MASK; }
+ public int getHigher(int index) { return ((int) (this.links[index] >>> HIGHER_SHIFT)) & LINK_MASK; }
+
+ public int getSmaller(int index) { return ((int) (this.links[index] >>> SMALLER_SHIFT)) & LINK_MASK; }
+ public int getBigger(int index) { return ((int) (this.links[index] >>> BIGGER_SHIFT)) & LINK_MASK; }
+
+ public long getData(int index) { return this.data[index]; }
+
+ public int getMinY(int index) { return RenderDataPointUtil.getYMin(this.getData(index)); }
+ public int getMaxY(int index) { return RenderDataPointUtil.getYMax(this.getData(index)); }
+
+ public int getSize(int index)
+ {
+ long data = this.getData(index);
+ return RenderDataPointUtil.getYMax(data) - RenderDataPointUtil.getYMin(data);
+ }
+
+ public int getRed(int index) { return RenderDataPointUtil.getRed(this.getData(index)); }
+ public int getGreen(int index) { return RenderDataPointUtil.getGreen(this.getData(index)); }
+ public int getBlue(int index) { return RenderDataPointUtil.getBlue(this.getData(index)); }
+ public int getAlpha(int index) { return RenderDataPointUtil.getAlpha(this.getData(index)); }
+
+ public int getBlockLight(int index) { return RenderDataPointUtil.getLightBlock(this.getData(index)); }
+ public int getSkyLight(int index) { return RenderDataPointUtil.getLightSky(this.getData(index)); }
+
+
+
+ //=========//
+ // setters //
+ //=========//
+
+ public void setSmallest(int smallest) { this.smallest = (short)(smallest); }
+ public void setBiggest(int biggest) { this.biggest = (short)(biggest); }
+
+ public void setLowest(int lowest) { this.lowest = (short)(lowest); }
+ public void setHighest(int highest) { this.highest = (short)(highest); }
+
+ public void setSizeWithAir(int sizeWithAir) { this.sizeWithAir = (short)(sizeWithAir); }
+ public void setSizeWithoutAir(int sizeWithoutAir) { this.sizeWithoutAir = (short)(sizeWithoutAir); }
+
+ public void setSortingIndex(int index, int to) { this.sortingArray[index] = (short)(to); }
+
+ public void setLower(int index, int lowerIndex)
+ {
+ this.links[index] = (this.links[index] & ~(((long)(LINK_MASK)) << LOWER_SHIFT)) | (((long)(lowerIndex & LINK_MASK)) << LOWER_SHIFT);
+ }
+ public void setHigher(int index, int higherIndex)
+ {
+ this.links[index] = (this.links[index] & ~(((long)(LINK_MASK)) << HIGHER_SHIFT)) | (((long)(higherIndex & LINK_MASK)) << HIGHER_SHIFT);
+ }
+
+ public void setSmaller(int index, int smallerIndex)
+ {
+ this.links[index] = (this.links[index] & ~(((long)(LINK_MASK)) << SMALLER_SHIFT)) | (((long)(smallerIndex & LINK_MASK)) << SMALLER_SHIFT);
+ }
+ public void setBigger(int index, int biggerIndex)
+ {
+ this.links[index] = (this.links[index] & ~(((long)(LINK_MASK)) << BIGGER_SHIFT)) | (((long)(biggerIndex & LINK_MASK)) << BIGGER_SHIFT);
+ }
+
+ public void setData(int index, long data) { this.data[index] = data; }
+
+ public void setMinY(int index, int minY)
+ {
+ this.data[index] = (this.data[index] & ~RenderDataPointUtil.DEPTH_SHIFTED_MASK) | ((minY & RenderDataPointUtil.DEPTH_MASK) << RenderDataPointUtil.DEPTH_SHIFT);
+ }
+ public void setMaxY(int index, int maxY)
+ {
+ this.data[index] = (this.data[index] & ~RenderDataPointUtil.HEIGHT_SHIFTED_MASK) | ((maxY & RenderDataPointUtil.HEIGHT_MASK) << RenderDataPointUtil.HEIGHT_SHIFT);
+ }
+
+ public void setRed(int index, int red)
+ {
+ this.data[index] = (this.data[index] & ~(RenderDataPointUtil.RED_MASK << RenderDataPointUtil.RED_SHIFT)) | ((red & RenderDataPointUtil.RED_MASK) << RenderDataPointUtil.RED_SHIFT);
+ }
+ public void setGreen(int index, int green)
+ {
+ this.data[index] = (this.data[index] & ~(RenderDataPointUtil.GREEN_MASK << RenderDataPointUtil.GREEN_SHIFT)) | ((green & RenderDataPointUtil.GREEN_MASK) << RenderDataPointUtil.GREEN_SHIFT);
+ }
+
+ public void setBlue(int index, int blue) {
+ this.data[index] = (this.data[index] & ~(RenderDataPointUtil.BLUE_MASK << RenderDataPointUtil.BLUE_SHIFT)) | ((blue & RenderDataPointUtil.BLUE_MASK) << RenderDataPointUtil.BLUE_SHIFT);
+ }
+ public void setAlpha(int index, int alpha)
+ {
+ alpha >>>= RenderDataPointUtil.ALPHA_DOWNSIZE_SHIFT;
+ this.data[index] = (this.data[index] & ~(RenderDataPointUtil.ALPHA_MASK << RenderDataPointUtil.ALPHA_SHIFT)) | ((alpha & RenderDataPointUtil.ALPHA_MASK) << RenderDataPointUtil.ALPHA_SHIFT);
+ }
+
+ public void setBlockLight(int index, int blockLight)
+ {
+ this.data[index] = (this.data[index] & ~(RenderDataPointUtil.BLOCK_LIGHT_MASK << RenderDataPointUtil.BLOCK_LIGHT_SHIFT)) | ((blockLight & RenderDataPointUtil.BLOCK_LIGHT_MASK) << RenderDataPointUtil.BLOCK_LIGHT_SHIFT);
+ }
+ public void setSkyLight(int index, int skyLight)
+ {
+ this.data[index] = (this.data[index] & ~(RenderDataPointUtil.SKY_LIGHT_MASK << RenderDataPointUtil.SKY_LIGHT_SHIFT)) | ((skyLight & RenderDataPointUtil.SKY_LIGHT_MASK) << RenderDataPointUtil.SKY_LIGHT_SHIFT);
+ }
+
+
+ //================//
+ // helper methods //
+ //================//
+
+ public boolean isIndexVisible(int index) { return isDataVisible(this.getData(index)); }
+
+ public static boolean isDataVisible(long data) { return isAlphaVisible(RenderDataPointUtil.getAlpha(data)); }
+
+ public static boolean isAlphaVisible(int alpha) { return alpha >= 16; }
+
+
+
+ //==============//
+ // base methods //
+ //==============//
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder(this.sizeWithAir << 8).append("lowest to highest:");
+ for (int index = this.lowest; index != NULL; index = this.getHigher(index))
+ {
+ builder.append('\n').append(RenderDataPointUtil.toString(this.getData(index)));
+ }
+
+ builder.append("\nsmallest to biggest:");
+ for (int index = this.smallest; index != NULL; index = this.getBigger(index))
+ {
+ builder.append('\n').append(RenderDataPointUtil.toString(this.getData(index)));
+ }
+
+ return builder.toString();
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointUtil.java b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointUtil.java
index 4edca2d40..77f6459c8 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointUtil.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointUtil.java
@@ -256,12 +256,17 @@ public class RenderDataPointUtil
// TODO this should probably be moved
// TODO what is the purpose of these?
+ //these were needed by the old logic for mergeMultiData(),
+ //which has now been replaced by RenderDataPointReducingList.
+ //so, these are no longer necessary, but left here for the same
+ //reason the old logic is left here: in case it's ever needed again.
+ /*
private static final ThreadLocal tLocalIndices = new ThreadLocal<>();
private static final ThreadLocal tLocalIncreaseIndex = new ThreadLocal<>();
private static final ThreadLocal tLocalIndexHandled = new ThreadLocal<>();
private static final ThreadLocal tLocalHeightAndDepth = new ThreadLocal<>();
private static final ThreadLocal tDataIndexCache = new ThreadLocal<>();
-
+ */
/**
* This method merge column of multiple data together
@@ -271,6 +276,32 @@ public class RenderDataPointUtil
*/
public static void mergeMultiData(IColumnDataView sourceData, ColumnArrayView output)
{
+ int target = output.verticalSize();
+ if (target <= 0)
+ {
+ // I expect this to never be the case,
+ // but RenderDataPointReducingList handles it sanely,
+ // so I might as well handle it sanely here too.
+ output.fill(EMPTY_DATA);
+ }
+ else if (target == 1)
+ {
+ output.set(0, RenderDataPointReducingList.reduceToOne(sourceData));
+ for (int index = 1, size = output.size(); index < size; index++)
+ {
+ output.set(index, EMPTY_DATA);
+ }
+ }
+ else
+ {
+ RenderDataPointReducingList list = new RenderDataPointReducingList(sourceData);
+ list.reduce(output.verticalSize());
+ list.copyTo(output);
+ }
+
+
+ //old logic left here in case it's ever needed again.
+ /*
if (output.dataCount() != 1)
{
throw new IllegalArgumentException("output must be only reserved for one datapoint!");
@@ -612,6 +643,7 @@ public class RenderDataPointUtil
}
}
+ */
}
}
\ No newline at end of file
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderUtil.java b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderUtil.java
index ed351f84c..5411e8057 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderUtil.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderUtil.java
@@ -145,16 +145,21 @@ public class RenderUtil
*/
public static Mat4f createLodProjectionMatrix(Mat4f mcProjMat, float partialTicks)
{
+ // in James' testing a near clip plane distance of 2 blocks is enough to allow the fragment
+ // culling to take effect instead of seeing the near clip plane.
+ float nearClipDist = 2f; //MC_RENDER.getRenderDistance() * LodUtil.CHUNK_WIDTH / 4.0f; //getNearClipPlaneDistanceInBlocks(partialTicks);
+ if (Config.Client.Advanced.Debugging.lodOnlyMode.get())
+ {
+ nearClipDist = 0.1f;
+ }
+
int farPlaneDistanceInBlocks = RenderUtil.getFarClipPlaneDistanceInBlocks();
+ float farClipDist = (float) ((farPlaneDistanceInBlocks + LodUtil.REGION_WIDTH) * Math.sqrt(2));
// Create a copy of the current matrix, so it won't be modified.
Mat4f lodProj = mcProjMat.copy();
-
// Set new far and near clip plane values.
- lodProj.setClipPlanes(
- getNearClipPlaneDistanceInBlocks(partialTicks),
- (float) ((farPlaneDistanceInBlocks + LodUtil.REGION_WIDTH) * Math.sqrt(2)));
-
+ lodProj.setClipPlanes(nearClipDist, farClipDist);
return lodProj;
}
@@ -198,34 +203,21 @@ public class RenderUtil
else
{
// TODO make this option dependent on player speed.
- // if the player is flying quickly, lower the near clip plane to account for slow chunk loading.
+ // If the player is flying quickly, lower the near clip plane to account for slow chunk loading.
// If the player is moving quickly they are less likely to notice overdraw.
- EOverdrawPrevention clipPlaneDistance = Config.Client.Advanced.Graphics.AdvancedGraphics.overdrawPrevention.get();
- switch (clipPlaneDistance)
+ nearClipPlane = Config.Client.Advanced.Graphics.AdvancedGraphics.overdrawPrevention.get().floatValue();
+ nearClipPlane *= vanillaBlockRenderedDistance;
+
+ // the near clip plane should never be closer than 1/10th of a block,
+ // otherwise Z-fighting and other issues may occur
+ if (nearClipPlane < 0.1f)
{
- default: // shouldn't be necessary, just here to make the compiler happy
- case NONE:
- nearClipPlane = 0.1f;
- break;
-
- case LIGHT:
- nearClipPlane = vanillaBlockRenderedDistance * 0.25f;
- break;
-
- case MEDIUM:
- nearClipPlane = vanillaBlockRenderedDistance * 0.4f;
- break;
-
-
- case HEAVY:
- // recommend render distance ot 6 or higher, otherwise holes may appear
- nearClipPlane = vanillaBlockRenderedDistance * 0.6f;
- break;
+ nearClipPlane = 0.1f;
}
}
- // modify the based on the player's FOV
+ // modify based on the player's FOV
double fov = MC_RENDER.getFov(partialTicks);
double aspectRatio = (double) MC_RENDER.getScreenWidth() / MC_RENDER.getScreenHeight();
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/threading/ConfigThreadPool.java b/core/src/main/java/com/seibel/distanthorizons/core/util/threading/ConfigThreadPool.java
index 47a509b91..108d41415 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/util/threading/ConfigThreadPool.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/util/threading/ConfigThreadPool.java
@@ -90,9 +90,8 @@ public class ConfigThreadPool
{
if (this.executor != null)
{
- //LOGGER.info("Stopping File Handler");
+ //LOGGER.info("Stopping thread pool");
this.executor.shutdownNow();
- this.executor = null;
}
this.threadCount = 0;
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/threading/ThreadPools.java b/core/src/main/java/com/seibel/distanthorizons/core/util/threading/ThreadPools.java
index a4f851b7c..1084c4d8d 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/util/threading/ThreadPools.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/util/threading/ThreadPools.java
@@ -22,6 +22,7 @@ package com.seibel.distanthorizons.core.util.threading;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.util.ThreadUtil;
+import org.jetbrains.annotations.Nullable;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadPoolExecutor;
@@ -42,13 +43,16 @@ public class ThreadPools
public static final DhThreadFactory FILE_HANDLER_THREAD_FACTORY = new DhThreadFactory("File Handler", Thread.MIN_PRIORITY);
private static ConfigThreadPool fileHandlerThreadPool;
+ @Nullable
public static ThreadPoolExecutor getFileHandlerExecutor() { return fileHandlerThreadPool.executor; }
public static final DhThreadFactory WORLD_GEN_THREAD_FACTORY = new DhThreadFactory("World Gen", Thread.MIN_PRIORITY);
private static ConfigThreadPool worldGenThreadPool;
+ @Nullable
public static ThreadPoolExecutor getWorldGenExecutor() { return worldGenThreadPool.executor; }
private static ThreadPoolExecutor bufferUploaderThreadPool;
+ @Nullable
public static ThreadPoolExecutor getBufferUploaderExecutor() { return bufferUploaderThreadPool; }
@@ -63,14 +67,17 @@ public class ThreadPools
public static final DhThreadFactory LIGHT_POPULATOR_THREAD_FACTORY = new DhThreadFactory("LOD Builder - Light Populator", Thread.MIN_PRIORITY);
private static ConfigThreadPool lightPopulatorThreadPool;
+ @Nullable
public static ThreadPoolExecutor getLightPopulatorExecutor() { return lightPopulatorThreadPool.executor; }
public static final DhThreadFactory CHUNK_TO_LOD_BUILDER_THREAD_FACTORY = new DhThreadFactory("LOD Builder - Chunk to Lod Builder", Thread.MIN_PRIORITY);
private static ConfigThreadPool chunkToLodBuilderThreadPool;
+ @Nullable
public static ThreadPoolExecutor getChunkToLodBuilderExecutor() { return chunkToLodBuilderThreadPool.executor; }
public static final DhThreadFactory BUFFER_BUILDER_THREAD_FACTORY = new DhThreadFactory("LOD Builder - Buffer Builder", Thread.MIN_PRIORITY);
private static ConfigThreadPool bufferBuilderThreadPool;
+ @Nullable
public static ThreadPoolExecutor getBufferBuilderExecutor() { return bufferBuilderThreadPool.executor; }
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientServerWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientServerWorld.java
index 935299539..c0584ced3 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientServerWorld.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientServerWorld.java
@@ -29,6 +29,7 @@ import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
+import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.HashMap;
@@ -69,7 +70,7 @@ public class DhClientServerWorld extends AbstractDhWorld implements IDhClientWor
//=========//
@Override
- public DhClientServerLevel getOrLoadLevel(ILevelWrapper wrapper)
+ public DhClientServerLevel getOrLoadLevel(@NotNull ILevelWrapper wrapper)
{
if (wrapper instanceof IServerLevelWrapper)
{
@@ -105,13 +106,13 @@ public class DhClientServerWorld extends AbstractDhWorld implements IDhClientWor
}
@Override
- public DhClientServerLevel getLevel(ILevelWrapper wrapper) { return this.levelWrapperByDhLevel.get(wrapper); }
+ public DhClientServerLevel getLevel(@NotNull ILevelWrapper wrapper) { return this.levelWrapperByDhLevel.get(wrapper); }
@Override
public Iterable extends IDhLevel> getAllLoadedLevels() { return this.dhLevels; }
@Override
- public void unloadLevel(ILevelWrapper wrapper)
+ public void unloadLevel(@NotNull ILevelWrapper wrapper)
{
if (this.levelWrapperByDhLevel.containsKey(wrapper))
{
@@ -160,7 +161,12 @@ public class DhClientServerWorld extends AbstractDhWorld implements IDhClientWor
this.saveAndFlush();
this.f3Message.close();
- for (DhClientServerLevel level : this.dhLevels)
+
+ // clear dhLevels to prevent concurrent modification errors
+ HashSet levelsToClose = new HashSet<>(this.dhLevels);
+ this.dhLevels.clear();
+ // close each level
+ for (DhClientServerLevel level : levelsToClose)
{
LOGGER.info("Unloading level " + level.getServerLevelWrapper().getDimensionType().getDimensionName());
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientWorld.java
index de242b2e0..bb0795168 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientWorld.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientWorld.java
@@ -31,6 +31,7 @@ import com.seibel.distanthorizons.core.util.objects.EventLoop;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
+import org.jetbrains.annotations.NotNull;
import javax.annotation.CheckForNull;
import java.io.File;
@@ -84,7 +85,7 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
//=========//
@Override
- public DhClientLevel getOrLoadLevel(ILevelWrapper wrapper)
+ public DhClientLevel getOrLoadLevel(@NotNull ILevelWrapper wrapper)
{
if (!(wrapper instanceof IClientLevelWrapper))
{
@@ -105,7 +106,7 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
}
@Override
- public DhClientLevel getLevel(ILevelWrapper wrapper)
+ public DhClientLevel getLevel(@NotNull ILevelWrapper wrapper)
{
if (!(wrapper instanceof IClientLevelWrapper))
{
@@ -119,7 +120,7 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
public Iterable extends IDhLevel> getAllLoadedLevels() { return this.levels.values(); }
@Override
- public void unloadLevel(ILevelWrapper wrapper)
+ public void unloadLevel(@NotNull ILevelWrapper wrapper)
{
if (!(wrapper instanceof IClientLevelWrapper))
{
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java
index b7134364f..7411459af 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java
@@ -29,6 +29,7 @@ import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
+import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.HashMap;
@@ -48,7 +49,7 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld
public DhServerWorld()
{
super(EWorldEnvironment.Server_Only);
-
+
this.saveStructure = new LocalSaveStructure();
this.levels = new HashMap<>();
@@ -57,7 +58,7 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld
LOGGER.info("Started "+DhServerWorld.class.getSimpleName()+" of type "+this.environment);
}
-
+
//=========//
@@ -79,15 +80,15 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld
this.getLevel(dest).addPlayer(player);
this.getLevel(origin).removePlayer(player);
}
-
+
@Override
- public DhServerLevel getOrLoadLevel(ILevelWrapper wrapper)
+ public DhServerLevel getOrLoadLevel(@NotNull ILevelWrapper wrapper)
{
if (!(wrapper instanceof IServerLevelWrapper))
{
return null;
}
-
+
return this.levels.computeIfAbsent((IServerLevelWrapper) wrapper, (serverLevelWrapper) ->
{
File levelFile = this.saveStructure.getLevelFolder(wrapper);
@@ -95,29 +96,29 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld
return new DhServerLevel(this.saveStructure, serverLevelWrapper, this.remotePlayerConnectionHandler);
});
}
-
+
@Override
- public DhServerLevel getLevel(ILevelWrapper wrapper)
+ public DhServerLevel getLevel(@NotNull ILevelWrapper wrapper)
{
if (!(wrapper instanceof IServerLevelWrapper))
{
return null;
}
-
+
return this.levels.get(wrapper);
}
-
+
@Override
public Iterable extends IDhLevel> getAllLoadedLevels() { return this.levels.values(); }
-
+
@Override
- public void unloadLevel(ILevelWrapper wrapper)
+ public void unloadLevel(@NotNull ILevelWrapper wrapper)
{
if (!(wrapper instanceof IServerLevelWrapper))
{
return;
}
-
+
if (this.levels.containsKey(wrapper))
{
LOGGER.info("Unloading level {} ", this.levels.get(wrapper));
@@ -133,18 +134,18 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld
public void doWorldGen() {
this.levels.values().forEach(DhServerLevel::doWorldGen);
}
-
+
@Override
public CompletableFuture saveAndFlush()
{
return CompletableFuture.allOf(this.levels.values().stream().map(DhServerLevel::saveAsync).toArray(CompletableFuture[]::new));
}
-
+
@Override
public void close()
{
this.remotePlayerConnectionHandler.close();
-
+
for (DhServerLevel level : this.levels.values())
{
LOGGER.info("Unloading level " + level.getLevelWrapper().getDimensionType().getDimensionName());
@@ -158,7 +159,7 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld
level.close();
}
-
+
this.levels.clear();
LOGGER.info("Closed DhWorld of type " + this.environment);
}
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/IDhWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/IDhWorld.java
index 388929e89..dd631015c 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/world/IDhWorld.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/world/IDhWorld.java
@@ -21,17 +21,18 @@ package com.seibel.distanthorizons.core.world;
import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
+import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
public interface IDhWorld
{
- IDhLevel getOrLoadLevel(ILevelWrapper levelWrapper);
- IDhLevel getLevel(ILevelWrapper wrapper);
+ IDhLevel getOrLoadLevel(@NotNull ILevelWrapper levelWrapper);
+ IDhLevel getLevel(@NotNull ILevelWrapper wrapper);
Iterable extends IDhLevel> getAllLoadedLevels();
- void unloadLevel(ILevelWrapper levelWrapper);
+ void unloadLevel(@NotNull ILevelWrapper levelWrapper);
CompletableFuture saveAndFlush();
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/block/IBlockStateWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/block/IBlockStateWrapper.java
index 8858c240b..2045631fd 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/block/IBlockStateWrapper.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/block/IBlockStateWrapper.java
@@ -25,6 +25,11 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
/** A Minecraft version independent way of handling Blocks. */
public interface IBlockStateWrapper extends IDhApiBlockStateWrapper
{
+ int FULLY_TRANSPARENT = 0;
+ int FULLY_OPAQUE = 16;
+
+
+
String getSerialString();
/**
diff --git a/core/src/main/resources/assets/distanthorizons/lang/en_us.json b/core/src/main/resources/assets/distanthorizons/lang/en_us.json
index 4dff0608d..c51bfc844 100644
--- a/core/src/main/resources/assets/distanthorizons/lang/en_us.json
+++ b/core/src/main/resources/assets/distanthorizons/lang/en_us.json
@@ -268,7 +268,7 @@
"distanthorizons.config.client.advanced.graphics.advancedGraphics.overdrawPrevention":
"Overdraw Prevention",
"distanthorizons.config.client.advanced.graphics.advancedGraphics.overdrawPrevention.@tooltip":
- "Enabling this will prevent some overdraw issues,\nbut may cause nearby LODs to render incorrectly, especially when near fancy leaves or non-full blocks.\nLess noticeable with a longer vanilla render distance.",
+ "Determines how far from the camera Distant Horizons will start rendering. \nMeasured as a percentage of the vanilla render distance.\n\nHigher values will prevent LODs from rendering behind vanilla blocks at a higher distance,\nbut may cause holes to appear in the LODs. \nHoles are most likely to appear when flying through unloaded terrain. \n\nIncreasing the vanilla render distance increases the effectiveness of this setting.",
"distanthorizons.config.client.advanced.graphics.advancedGraphics.seamlessOverdraw":
"(Experimental) Seamless Overdraw",
"distanthorizons.config.client.advanced.graphics.advancedGraphics.seamlessOverdraw.@tooltip":
@@ -335,6 +335,10 @@
"Minimum Time Between Chunk Updates In Seconds",
"distanthorizons.config.client.advanced.lodBuilding.minTimeBetweenChunkUpdatesInSeconds.@tooltip":
"Determines how long must pass between LOD chunk updates before another. \nupdate can occur\n\nIncreasing this value will reduce CPU load but may may cause \nLODs to become outdated more frequently or for longer.",
+ "distanthorizons.config.client.advanced.lodBuilding.onlyUseDhLightingEngine":
+ "Only Use DH Lighting Engine",
+ "distanthorizons.config.client.advanced.lodBuilding.onlyUseDhLightingEngine.@tooltip":
+ "If false LODs will be lit by Minecraft's lighting engine when possible \nand fall back to the DH lighting engine only when necessary. \n\nIf true LODs will only be lit using Distant Horizons' lighting engine. \n\nGenerally it is best to leave this disabled and should only be enabled \nif there are lighting issues or for debugging.",
"distanthorizons.config.client.advanced.multiplayer":
diff --git a/core/src/main/resources/shaders/flat_shaded.frag b/core/src/main/resources/shaders/flat_shaded.frag
index edcddbcca..e6d7a9407 100644
--- a/core/src/main/resources/shaders/flat_shaded.frag
+++ b/core/src/main/resources/shaders/flat_shaded.frag
@@ -8,6 +8,9 @@ in vec4 vPos;
out vec4 fragColor;
+// Fog/Clip Uniforms
+uniform float clipDistance = 0.0;
+
// Noise Uniforms
uniform bool noiseEnabled;
uniform int noiseSteps;
@@ -24,47 +27,53 @@ float rand(vec3 co) { return rand(co.xy + rand(co.z)); }
// EG. setting stepSize to 4 then this would be the result of this function
// In: 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, ..., 1.1, 1.2, 1.3
// Out: 0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, ..., 1.0, 1.0, 1.25
-vec3 quantize(vec3 val, int stepSize) {
+vec3 quantize(vec3 val, int stepSize)
+{
return floor(val * stepSize) / stepSize;
}
+void applyNoise(inout vec4 fragColor, const in float viewDist)
+{
+ vec3 vertexNormal = normalize(cross(dFdy(vPos.xyz), dFdx(vPos.xyz)));
+ // This bit of code is required to fix the vertex position problem cus of floats in the verted world position varuable
+ vec3 fixedVPos = vPos.xyz + vertexNormal * 0.001;
+
+ float noiseAmplification = noiseIntensity * 0.01;
+ float lum = (fragColor.r + fragColor.g + fragColor.b) / 3.0;
+ noiseAmplification = (1.0 - pow(lum * 2.0 - 1.0, 2.0)) * noiseAmplification; // Lessen the effect on depending on how dark the object is, equasion for this is -(2x-1)^{2}+1
+ noiseAmplification *= fragColor.a; // The effect would lessen on transparent objects
+
+ // Random value for each position
+ float randomValue = rand(quantize(fixedVPos, noiseSteps))
+ * 2.0 * noiseAmplification - noiseAmplification;
+
+ // Modifies the color
+ // A value of 0 on the randomValue will result in the original color, while a value of 1 will result in a fully bright color
+ vec3 newCol = fragColor.rgb + (1.0 - fragColor.rgb) * randomValue;
+ newCol = clamp(newCol, 0.0, 1.0);
+
+ if (noiseDropoff != 0) {
+ float distF = min(viewDist / noiseDropoff, 1.0);
+ newCol = mix(newCol, fragColor.rgb, distF); // The further away it gets, the less noise gets applied
+ }
+
+ fragColor.rgb = newCol;
+}
+
+
-/**
- * Fragment Shader
- *
- * author: James Seibel
- * author: coolGi
- * version: 7-2-2023
- */
void main()
{
fragColor = vertexColor;
- // TODO: Move into its own function instead of in an if statement
- if (noiseEnabled) {
- vec3 vertexNormal = normalize(cross(dFdy(vPos.xyz), dFdx(vPos.xyz)));
- // This bit of code is required to fix the vertex position problem cus of floats in the verted world position varuable
- vec3 fixedVPos = vPos.xyz + vertexNormal * 0.001;
-
- float noiseAmplification = noiseIntensity * 0.01;
- float lum = (fragColor.r + fragColor.g + fragColor.b) / 3.0;
- noiseAmplification = (1.0 - pow(lum * 2.0 - 1.0, 2.0)) * noiseAmplification; // Lessen the effect on depending on how dark the object is, equasion for this is -(2x-1)^{2}+1
- noiseAmplification *= fragColor.a; // The effect would lessen on transparent objects
-
- // Random value for each position
- float randomValue = rand(quantize(fixedVPos, noiseSteps))
- * 2.0 * noiseAmplification - noiseAmplification;
-
- // Modifies the color
- // A value of 0 on the randomValue will result in the original color, while a value of 1 will result in a fully bright color
- vec3 newCol = fragColor.rgb + (1.0 - fragColor.rgb) * randomValue;
- newCol = clamp(newCol, 0.0, 1.0);
-
- if (noiseDropoff != 0) {
- float distF = min(length(vertexWorldPos) / noiseDropoff, 1.0);
- newCol = mix(newCol, fragColor.rgb, distF); // The further away it gets, the less noise gets applied
- }
-
- fragColor.rgb = newCol;
+ float viewDist = length(vertexWorldPos);
+ if (viewDist < clipDistance && clipDistance > 0.0)
+ {
+ discard;
+ }
+
+ if (noiseEnabled)
+ {
+ applyNoise(fragColor, viewDist);
}
}