Compare commits

...

65 Commits

Author SHA1 Message Date
James Seibel 4a3c24f39e Add proof-of-concept dynamic fade 2026-01-17 10:16:35 -06:00
James Seibel 7b6fd03d78 Change render wrapper get Texture error returns 2026-01-17 09:56:21 -06:00
s809 1a540cf2bc Make sure payload chunk is readable 2026-01-14 22:17:46 +05:00
James Seibel 20fc2efb46 Improve concurrent iterating in QuadTree 2026-01-10 17:03:43 -06:00
James Seibel d8beba2498 minor cleanup in LodBufferContainer cleanup 2026-01-10 17:02:56 -06:00
James Seibel 9f0cb5a394 Add forge specific icon/logo
Done to fix a forge limitation where logos can't contain a file pathhttps://github.com/MinecraftForge/MinecraftForge/issues/7348
2026-01-10 11:56:08 -06:00
James Seibel df63401d11 DB updater use correct classloader 2026-01-10 08:21:09 -06:00
James Seibel db95951ade minor reformat and comment 2026-01-10 08:20:44 -06:00
s809 1e020f93a6 Reapply "Run plugin messages on a DH thread"
This reverts commit ff3145336d.
2026-01-09 20:29:23 +05:00
James Seibel 7aee6dfb44 Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2026-01-07 07:50:25 -06:00
James Seibel 546a51a295 expand distant beacon beams for visiblity 2026-01-07 07:50:22 -06:00
James Seibel ec7e791e9f Change EMinecraftColor -> MinecraftTextFormat
No need for an enum when all the values are strings
2026-01-06 07:10:40 -06:00
James Seibel d60dec3d82 Merge branch 'main' into 'main'
Fix typo in high vanilla render distance warning

See merge request distant-horizons-team/distant-horizons-core!94
2026-01-05 13:00:17 +00:00
s809 89a80103f0 Wrong message target 2026-01-04 20:04:30 +05:00
s809 8e14a7223c Add a chat message for incompatible messages 2026-01-04 19:36:24 +05:00
meanwhile131 7cf1e901f5 Fix typo in high vanilla render distance warning 2026-01-01 15:06:30 +04:00
James Seibel ba923fa829 Fix neoforge thread causing resource loading to fail 2025-12-26 14:13:27 -06:00
s809 505dbe2f62 Replace the failure state with future exceptions 2025-12-27 00:51:30 +05:00
James Seibel 48c5828e8f up version number 2.4.5 -> 2.4.6-dev 2025-12-24 22:41:27 -06:00
James Seibel eb2317934f up version number 2.4.4 -> 2.4.5 2025-12-24 22:06:53 -06:00
James Seibel 60537cda1b Replace MC color code strings with an enum 2025-12-24 22:04:50 -06:00
James Seibel 508ff2b776 Fix null pointer in ChunkUpdateQueueManager 2025-12-24 21:53:39 -06:00
James Seibel 7c4ac2bd7e remove dev from version number 2025-12-23 22:55:40 -06:00
James Seibel 8c13c2cf47 Fix toggling world gen not recreating queue 2025-12-23 22:55:40 -06:00
James Seibel 802019ff72 up DH api version 5.0.0 -> 5.1.0 2025-12-23 20:01:06 -06:00
James Seibel 141890556c Revert "remove deprecated getHeight() from DhApiLevelWrapper"
This reverts commit 50bdb73a52.
2025-12-23 19:56:28 -06:00
James Seibel 353838db41 add experimental option to ignore rendering dimensions by name 2025-12-23 12:22:00 -06:00
James Seibel f1547477c9 add clientLevelWrapper to DhApiRenderParam 2025-12-23 12:20:42 -06:00
James Seibel 535a645a84 minor internal API cleanup 2025-12-23 12:19:14 -06:00
James Seibel 2dc7f02b32 Remove experimental option onlyLoadCenterLods
option is now merged into main
2025-12-23 12:18:28 -06:00
James Seibel 50bdb73a52 remove deprecated getHeight() from DhApiLevelWrapper
use getMaxHeight() instead
2025-12-23 12:06:15 -06:00
James Seibel 53e6c95432 commenting DhTerrainShaderProgram 2025-12-23 08:57:51 -06:00
James Seibel 36f0029e45 Fix earth curvature shader compiling 2025-12-23 08:47:44 -06:00
s809 5067e970a2 Use another method to create a buffer 2025-12-23 12:50:02 +05:00
James Seibel 167ca94e69 Remove deprecated disableVanillaFog config 2025-12-22 20:31:24 -06:00
James Seibel 8d94b86bfd Hide network config changes by default 2025-12-22 14:51:29 -06:00
James Seibel a29567430e Net only log changed config values 2025-12-22 14:37:34 -06:00
James Seibel fb2dae48e2 re-enable remote timestamp getting 2025-12-22 14:21:12 -06:00
James Seibel 948b4bfd9c comment out debug log 2025-12-22 14:17:57 -06:00
James Seibel ca44256ca9 disable full data debug phantom array stacks 2025-12-22 14:17:47 -06:00
James Seibel a29b6a5aab remove unnecessary config appearance check 2025-12-22 14:17:13 -06:00
James Seibel 868254ccc8 try fixing rare leak in delayed data source cache
Didn't fix the problem, but shouldn't hurt
2025-12-22 14:16:34 -06:00
James Seibel 195fde8d73 quad tree spilt request cleanup 2025-12-22 13:58:26 -06:00
James Seibel ce7b9b94b6 fix/improve world gen/retrieval error handling 2025-12-22 13:58:26 -06:00
James Seibel 1f0c2e286a fix network splitting requests 2025-12-22 13:58:26 -06:00
James Seibel f79fd5e06f error handling in AbstactDhLevel chunk update 2025-12-22 13:58:26 -06:00
James Seibel 47c1d3955f failed attmpt to fix leaks
Breaks split world gen requests
2025-12-22 13:57:49 -06:00
James Seibel 2c5f5a3d4c minor refactors 2025-12-22 09:46:21 -06:00
James Seibel 81c533051e close errored data sources in full data provider 2025-12-22 08:35:15 -06:00
James Seibel 5cbe5ecfd8 Fix dis/re-enabling world gen queuing 2025-12-21 19:44:48 -06:00
James Seibel d4b4d28c9f Fix null error log in Data source provider 2025-12-21 08:53:27 -06:00
James Seibel b8e653b5f7 Fix phantom checkout not updating stack trace 2025-12-21 08:52:44 -06:00
James Seibel 80fea09598 Fix concurrency error in LodQuadTree 2025-12-21 08:52:25 -06:00
James Seibel 1d4f914a9f Merge branch 'worldGenRefactor' 2025-12-20 10:53:39 -06:00
James Seibel bf92dea2eb reduce stuttering at the cost of lighting quality 2025-12-20 10:52:51 -06:00
s809 2dd675b8da Handle generated LOD updates outside the render thread 2025-12-20 15:22:26 +05:00
s809 ff3145336d Revert "Run plugin messages on a DH thread"
This reverts commit 280181c91e.
2025-12-20 14:32:39 +05:00
James Seibel 280181c91e Run plugin messages on a DH thread 2025-12-19 16:54:29 -06:00
James Seibel 60232e713b refactor world gen queue 2025-12-19 16:54:07 -06:00
James Seibel 55d9030954 Remove extra particle for world gen 2025-12-18 10:20:01 -06:00
James Seibel 452bd75f5d remove chunkWrapper.isStillValid() 2025-12-18 10:18:07 -06:00
James Seibel 72be1e2602 Remove LodRenderSection.isFullyGenerated() 2025-12-18 10:17:36 -06:00
James Seibel 1c30213aca up version number 2.4.3 -> 2.4.4-dev 2025-12-18 10:04:41 -06:00
James Seibel 4a1513ed65 fix compiling 2025-12-17 22:41:22 -06:00
James Seibel 6d98c9cb84 start world gen refactoring 2025-12-17 22:39:23 -06:00
78 changed files with 2405 additions and 1711 deletions
@@ -169,7 +169,7 @@ public interface IDhApiWorldGenerator extends Closeable, IDhApiOverrideable
* *
* After the {@link IDhApiWorldGenerator} has been generated, it should be passed into the * After the {@link IDhApiWorldGenerator} has been generated, it should be passed into the
* resultConsumer's {@link Consumer#accept(Object)} method. * resultConsumer's {@link Consumer#accept(Object)} method.
* Note: if air blocks aren't included in the with the {@link DhApiChunk} with proper lighting, lower detail levels will appear as black/unlit. * Note: if air blocks aren't included in the {@link IDhApiFullDataSource} with proper lighting, lower detail levels will appear as black/unlit.
* *
* @implNote the default implementation of this method throws an {@link UnsupportedOperationException}, * @implNote the default implementation of this method throws an {@link UnsupportedOperationException},
* and must be overridden when {@link #getReturnType()} returns {@link EDhApiWorldGeneratorReturnType#API_CHUNKS}. * and must be overridden when {@link #getReturnType()} returns {@link EDhApiWorldGeneratorReturnType#API_CHUNKS}.
@@ -20,6 +20,8 @@
package com.seibel.distanthorizons.api.methods.events.sharedParameterObjects; package com.seibel.distanthorizons.api.methods.events.sharedParameterObjects;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass;
import com.seibel.distanthorizons.api.interfaces.world.IDhApiLevelWrapper;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiWorldLoadEvent;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam; import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam;
import com.seibel.distanthorizons.api.objects.math.DhApiMat4f; import com.seibel.distanthorizons.api.objects.math.DhApiMat4f;
@@ -27,7 +29,7 @@ import com.seibel.distanthorizons.api.objects.math.DhApiMat4f;
* Contains information relevant to Distant Horizons and Minecraft rendering. * Contains information relevant to Distant Horizons and Minecraft rendering.
* *
* @author James Seibel * @author James Seibel
* @version 2024-1-31 * @version 2025-12-23
* @since API 1.0.0 * @since API 1.0.0
*/ */
public class DhApiRenderParam implements IDhApiEventParam public class DhApiRenderParam implements IDhApiEventParam
@@ -61,6 +63,13 @@ public class DhApiRenderParam implements IDhApiEventParam
public final int worldYOffset; public final int worldYOffset;
/**
* The level currently being rendered.
*
* @since API 5.1.0
*/
public final IDhApiLevelWrapper clientLevelWrapper;
//==============// //==============//
@@ -70,12 +79,13 @@ public class DhApiRenderParam implements IDhApiEventParam
public DhApiRenderParam(DhApiRenderParam parent) public DhApiRenderParam(DhApiRenderParam parent)
{ {
this( this(
parent.renderPass, parent.renderPass,
parent.partialTicks, parent.partialTicks,
parent.nearClipPlane, parent.farClipPlane, parent.nearClipPlane, parent.farClipPlane,
parent.mcProjectionMatrix.copy(), parent.mcModelViewMatrix.copy(), parent.mcProjectionMatrix.copy(), parent.mcModelViewMatrix.copy(),
parent.dhProjectionMatrix.copy(), parent.dhModelViewMatrix.copy(), parent.dhProjectionMatrix.copy(), parent.dhModelViewMatrix.copy(),
parent.worldYOffset parent.worldYOffset,
parent.clientLevelWrapper
); );
} }
public DhApiRenderParam( public DhApiRenderParam(
@@ -84,7 +94,8 @@ public class DhApiRenderParam implements IDhApiEventParam
float nearClipPlane, float farClipPlane, float nearClipPlane, float farClipPlane,
DhApiMat4f newMcProjectionMatrix, DhApiMat4f newMcModelViewMatrix, DhApiMat4f newMcProjectionMatrix, DhApiMat4f newMcModelViewMatrix,
DhApiMat4f newDhProjectionMatrix, DhApiMat4f newDhModelViewMatrix, DhApiMat4f newDhProjectionMatrix, DhApiMat4f newDhModelViewMatrix,
int worldYOffset int worldYOffset,
IDhApiLevelWrapper clientLevelWrapper
) )
{ {
this.renderPass = renderPass; this.renderPass = renderPass;
@@ -101,6 +112,7 @@ public class DhApiRenderParam implements IDhApiEventParam
this.dhModelViewMatrix = newDhModelViewMatrix; this.dhModelViewMatrix = newDhModelViewMatrix;
this.worldYOffset = worldYOffset; this.worldYOffset = worldYOffset;
this.clientLevelWrapper = clientLevelWrapper;
} }
@@ -38,14 +38,14 @@ public final class ModInfo
public static final String NAME = "DistantHorizons"; public static final String NAME = "DistantHorizons";
/** Human-readable version of NAME */ /** Human-readable version of NAME */
public static final String READABLE_NAME = "Distant Horizons"; public static final String READABLE_NAME = "Distant Horizons";
public static final String VERSION = "2.4.3-b"; public static final String VERSION = "2.4.6-b-dev";
/** Returns true if the current build is an unstable developer build, false otherwise. */ /** Returns true if the current build is an unstable developer build, false otherwise. */
public static final boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev"); public static final boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev");
/** This version should only be updated when breaking changes are introduced to the DH API */ /** This version should only be updated when breaking changes are introduced to the DH API */
public static final int API_MAJOR_VERSION = 5; public static final int API_MAJOR_VERSION = 5;
/** This version should be updated whenever new methods are added to the DH API */ /** 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 */ /** This version should be updated whenever non-breaking fixes are added to the DH API */
public static final int API_PATCH_VERSION = 0; public static final int API_PATCH_VERSION = 0;
@@ -20,7 +20,9 @@
package com.seibel.distanthorizons.core; package com.seibel.distanthorizons.core;
import com.github.luben.zstd.ZstdOutputStream; import com.github.luben.zstd.ZstdOutputStream;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiBeforeRenderEvent;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.eventHandlers.IgnoredDimensionCsvHandler;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericRenderObjectFactory; import com.seibel.distanthorizons.core.render.renderer.generic.GenericRenderObjectFactory;
@@ -171,6 +173,8 @@ public class Initializer
} }
} }
DhApi.events.bind(DhApiBeforeRenderEvent.class, IgnoredDimensionCsvHandler.INSTANCE);
} }
} }
@@ -28,6 +28,7 @@ import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiHeightFogCo
import com.seibel.distanthorizons.core.config.api.DhApiConfigValue; import com.seibel.distanthorizons.core.config.api.DhApiConfigValue;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.api.converters.ApiFogDrawModeConverter; import com.seibel.distanthorizons.core.config.api.converters.ApiFogDrawModeConverter;
import com.seibel.distanthorizons.core.config.api.converters.InvertedBoolConverter;
public class DhApiFogConfig implements IDhApiFogConfig public class DhApiFogConfig implements IDhApiFogConfig
{ {
@@ -67,7 +68,7 @@ public class DhApiFogConfig implements IDhApiFogConfig
@Override @Override
@Deprecated @Deprecated
public IDhApiConfigValue<Boolean> disableVanillaFog() public IDhApiConfigValue<Boolean> disableVanillaFog()
{ return new DhApiConfigValue<>(Config.Client.Advanced.Graphics.Fog.disableVanillaFog); } { return new DhApiConfigValue<>(Config.Client.Advanced.Graphics.Fog.enableVanillaFog, new InvertedBoolConverter()); }
@Override @Override
public IDhApiConfigValue<Boolean> enableVanillaFog() public IDhApiConfigValue<Boolean> enableVanillaFog()
{ return new DhApiConfigValue<>(Config.Client.Advanced.Graphics.Fog.enableVanillaFog); } { return new DhApiConfigValue<>(Config.Client.Advanced.Graphics.Fog.enableVanillaFog); }
@@ -23,7 +23,9 @@ import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.api.enums.config.EDhApiMcRenderingFadeMode; import com.seibel.distanthorizons.api.enums.config.EDhApiMcRenderingFadeMode;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.*; import com.seibel.distanthorizons.api.methods.events.abstractEvents.*;
import com.seibel.distanthorizons.api.objects.DhApiResult;
import com.seibel.distanthorizons.core.api.internal.rendering.DhRenderState; import com.seibel.distanthorizons.core.api.internal.rendering.DhRenderState;
import com.seibel.distanthorizons.core.enums.MinecraftTextFormat;
import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure; import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.network.messages.MessageRegistry; import com.seibel.distanthorizons.core.network.messages.MessageRegistry;
@@ -31,21 +33,21 @@ import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.render.DhApiRenderProxy; import com.seibel.distanthorizons.core.render.DhApiRenderProxy;
import com.seibel.distanthorizons.core.render.renderer.*; import com.seibel.distanthorizons.core.render.renderer.*;
import com.seibel.distanthorizons.core.util.TimerUtil; import com.seibel.distanthorizons.core.util.TimerUtil;
import com.seibel.distanthorizons.core.util.math.Mat4f;
import com.seibel.distanthorizons.core.util.objects.Pair; import com.seibel.distanthorizons.core.util.objects.Pair;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.network.session.NetworkSession; import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel; import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel;
import com.seibel.distanthorizons.core.render.glObject.GLProxy; import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.world.AbstractDhWorld; import com.seibel.distanthorizons.core.world.AbstractDhWorld;
import com.seibel.distanthorizons.core.world.DhClientWorld; import com.seibel.distanthorizons.core.world.DhClientWorld;
import com.seibel.distanthorizons.core.world.IDhClientWorld;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper;
@@ -53,13 +55,23 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapp
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4f;
import org.joml.Vector4f;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.GL46;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.lang.management.GarbageCollectorMXBean; import java.io.IOException;
import java.lang.management.ManagementFactory; import java.text.NumberFormat;
import java.util.*; import java.util.*;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.function.Function;
/** /**
* This holds the methods that should be called * This holds the methods that should be called
@@ -95,7 +107,6 @@ public class ClientApi
private boolean isDevBuildMessagePrinted = false; private boolean isDevBuildMessagePrinted = false;
private boolean lowMemoryWarningPrinted = false; private boolean lowMemoryWarningPrinted = false;
private boolean highVanillaRenderDistanceWarningPrinted = false; private boolean highVanillaRenderDistanceWarningPrinted = false;
private boolean g1GarbageCollectorWarningPrinted = false;
private long lastStaticWarningMessageSentMsTime = 0L; private long lastStaticWarningMessageSentMsTime = 0L;
@@ -156,10 +167,10 @@ public class ClientApi
if (Config.Common.Logging.Warning.showReplayWarningOnStartup.get()) if (Config.Common.Logging.Warning.showReplayWarningOnStartup.get())
{ {
MC_CLIENT.sendChatMessage("\u00A76" + "Distant Horizons: Replay detected." + "\u00A7r"); // gold color MC_CLIENT.sendChatMessage(MinecraftTextFormat.ORANGE + "Distant Horizons: Replay detected." + MinecraftTextFormat.CLEAR_FORMATTING);
MC_CLIENT.sendChatMessage("DH may behave strangely or have missing functionality."); MC_CLIENT.sendChatMessage("DH may behave strangely or have missing functionality.");
MC_CLIENT.sendChatMessage("In order to use pre-generated LODs, put your DH database(s) in:"); MC_CLIENT.sendChatMessage("In order to use pre-generated LODs, put your DH database(s) in:");
MC_CLIENT.sendChatMessage("\u00A77"+".Minecraft" + File.separator + ClientOnlySaveStructure.SERVER_DATA_FOLDER_NAME + File.separator + ClientOnlySaveStructure.REPLAY_SERVER_FOLDER_NAME + File.separator + "DIMENSION_NAME"+"\u00A7r"); // light gray color MC_CLIENT.sendChatMessage(MinecraftTextFormat.GRAY +".Minecraft" + File.separator + ClientOnlySaveStructure.SERVER_DATA_FOLDER_NAME + File.separator + ClientOnlySaveStructure.REPLAY_SERVER_FOLDER_NAME + File.separator + "DIMENSION_NAME"+ MinecraftTextFormat.CLEAR_FORMATTING);
MC_CLIENT.sendChatMessage("This can be disabled in DH's config under Advanced -> Logging."); MC_CLIENT.sendChatMessage("This can be disabled in DH's config under Advanced -> Logging.");
MC_CLIENT.sendChatMessage(""); MC_CLIENT.sendChatMessage("");
} }
@@ -245,7 +256,7 @@ public class ClientApi
} }
} }
public void clientLevelLoadEvent(IClientLevelWrapper levelWrapper) public void clientLevelLoadEvent(@Nullable IClientLevelWrapper levelWrapper)
{ {
// wait a moment before loading the level to give the server a chance to handle the client's login request // wait a moment before loading the level to give the server a chance to handle the client's login request
if (MC_CLIENT.clientConnectedToDedicatedServer()) if (MC_CLIENT.clientConnectedToDedicatedServer())
@@ -332,10 +343,27 @@ public class ClientApi
*/ */
public void pluginMessageReceived(@NotNull AbstractNetworkMessage message) public void pluginMessageReceived(@NotNull AbstractNetworkMessage message)
{ {
NetworkSession networkSession = this.pluginChannelApi.networkSession; @Nullable ThreadPoolExecutor executor = ThreadPoolUtil.networkClientHandlerExecutor();
if (networkSession != null) if (executor == null)
{ {
networkSession.tryHandleMessage(message); LOGGER.warn("warn");
return;
}
try
{
executor.execute(() ->
{
NetworkSession networkSession = this.pluginChannelApi.networkSession;
if (networkSession != null)
{
networkSession.tryHandleMessage(message);
}
});
}
catch (RejectedExecutionException e)
{
LOGGER.warn("Plugin message executor rejected");
} }
} }
@@ -511,10 +539,10 @@ public class ClientApi
this.rendererDisabledBecauseOfExceptions = true; this.rendererDisabledBecauseOfExceptions = true;
LOGGER.error("Unexpected Renderer error in render pass [" + renderPass + "]. Error: " + e.getMessage(), e); LOGGER.error("Unexpected Renderer error in render pass [" + renderPass + "]. Error: " + e.getMessage(), e);
MC_CLIENT.sendChatMessage("\u00A74\u00A7l\u00A7uERROR: Distant Horizons renderer has encountered an exception!"); MC_CLIENT.sendChatMessage(MinecraftTextFormat.DARK_RED + "" + MinecraftTextFormat.BOLD + "ERROR: Distant Horizons renderer has encountered an exception!" + MinecraftTextFormat.CLEAR_FORMATTING);
MC_CLIENT.sendChatMessage("\u00A74Renderer disabled to try preventing GL state corruption."); MC_CLIENT.sendChatMessage(MinecraftTextFormat.DARK_RED + "Renderer disabled to try preventing GL state corruption." + MinecraftTextFormat.CLEAR_FORMATTING);
MC_CLIENT.sendChatMessage("\u00A74Toggle DH rendering via the config UI to re-activate DH rendering."); MC_CLIENT.sendChatMessage(MinecraftTextFormat.DARK_RED + "Toggle DH rendering via the config UI to re-activate DH rendering." + MinecraftTextFormat.CLEAR_FORMATTING);
MC_CLIENT.sendChatMessage("\u00A74Error: " + e); MC_CLIENT.sendChatMessage(MinecraftTextFormat.DARK_RED + "Error: " + MinecraftTextFormat.CLEAR_FORMATTING + e);
} }
@@ -533,8 +561,11 @@ public class ClientApi
* The first fade pass. * The first fade pass.
* Called after MC finishes rendering the opaque passes. * Called after MC finishes rendering the opaque passes.
*/ */
public void renderFadeOpaque() public void renderFadeOpaque() // TODO this is actually the transparent pass
{ {
DepthCalculator.INSTANCE.getMcTransparentDepthTexture();
DepthCalculator.INSTANCE.tryCalculateAsync();
// only fade when DH is rendering // only fade when DH is rendering
if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT
&& &&
@@ -555,8 +586,10 @@ public class ClientApi
* Called after MC finishes rendering both opaque * Called after MC finishes rendering both opaque
* and transparent passes. * and transparent passes.
*/ */
public void renderFadeTransparent() public void renderFadeTransparent() // TODO this is actually the opaque pass
{ {
DepthCalculator.INSTANCE.getMcOpaqueDepthTexture();
// only fade when DH is rendering // only fade when DH is rendering
if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT) if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT)
{ {
@@ -585,27 +618,30 @@ public class ClientApi
/** Trigger once on key press, with CLIENT PLAYER. */ /** Trigger once on key press, with CLIENT PLAYER. */
public void keyPressedEvent(int glfwKey) public void keyPressedEvent(int glfwKey)
{ {
if (!Config.Client.Advanced.Debugging.enableDebugKeybindings.get()) //if (!Config.Client.Advanced.Debugging.enableDebugKeybindings.get())
{ //{
// keybindings are disabled // // keybindings are disabled
return; // return;
} //}
if (glfwKey == GLFW.GLFW_KEY_F8) if (glfwKey == GLFW.GLFW_KEY_F8)
{ {
Config.Client.Advanced.Debugging.debugRendering.set(EDhApiDebugRendering.next(Config.Client.Advanced.Debugging.debugRendering.get())); DepthCalculator.INSTANCE.pause = true;
MC_CLIENT.sendChatMessage("F8: Set debug mode to " + Config.Client.Advanced.Debugging.debugRendering.get()); //Config.Client.Advanced.Debugging.debugRendering.set(EDhApiDebugRendering.next(Config.Client.Advanced.Debugging.debugRendering.get()));
//MC_CLIENT.sendChatMessage("F8: Set debug mode to " + Config.Client.Advanced.Debugging.debugRendering.get());
} }
else if (glfwKey == GLFW.GLFW_KEY_F6) else if (glfwKey == GLFW.GLFW_KEY_F6)
{ {
Config.Client.Advanced.Debugging.rendererMode.set(EDhApiRendererMode.next(Config.Client.Advanced.Debugging.rendererMode.get())); DepthCalculator.INSTANCE.pause = true;
MC_CLIENT.sendChatMessage("F6: Set rendering to " + Config.Client.Advanced.Debugging.rendererMode.get()); //Config.Client.Advanced.Debugging.rendererMode.set(EDhApiRendererMode.next(Config.Client.Advanced.Debugging.rendererMode.get()));
//MC_CLIENT.sendChatMessage("F6: Set rendering to " + Config.Client.Advanced.Debugging.rendererMode.get());
} }
else if (glfwKey == GLFW.GLFW_KEY_P) else if (glfwKey == GLFW.GLFW_KEY_P)
{ {
prefLoggerEnabled = !prefLoggerEnabled; DepthCalculator.INSTANCE.pause = true;
MC_CLIENT.sendChatMessage("P: Debug Pref Logger is " + (prefLoggerEnabled ? "enabled" : "disabled")); //prefLoggerEnabled = !prefLoggerEnabled;
//MC_CLIENT.sendChatMessage("P: Debug Pref Logger is " + (prefLoggerEnabled ? "enabled" : "disabled"));
} }
} }
@@ -659,8 +695,7 @@ public class ClientApi
// remind the user that this is a development build // remind the user that this is a development build
String message = String message =
// green text MinecraftTextFormat.DARK_GREEN + "Distant Horizons: nightly/unstable build, version: [" + ModInfo.VERSION+"]." + MinecraftTextFormat.CLEAR_FORMATTING + "\n" +
"\u00A72" + "Distant Horizons: nightly/unstable build, version: [" + ModInfo.VERSION+"]." + "\u00A7r\n" +
"Issues may occur with this version.\n" + "Issues may occur with this version.\n" +
"Here be dragons!\n"; "Here be dragons!\n";
MC_CLIENT.sendChatMessage(message); MC_CLIENT.sendChatMessage(message);
@@ -684,7 +719,7 @@ public class ClientApi
{ {
String message = String message =
// orange text // orange text
"\u00A76" + "Distant Horizons: Low memory detected." + "\u00A7r \n" + MinecraftTextFormat.ORANGE + "Distant Horizons: Low memory detected." + MinecraftTextFormat.CLEAR_FORMATTING + "\n" +
"Stuttering or low FPS may occur. \n" + "Stuttering or low FPS may occur. \n" +
"Please increase Minecraft's available memory to 4 GB or more. \n" + "Please increase Minecraft's available memory to 4 GB or more. \n" +
"This warning can be disabled in DH's config under Advanced -> Logging. \n"; "This warning can be disabled in DH's config under Advanced -> Logging. \n";
@@ -706,15 +741,13 @@ public class ClientApi
this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis(); this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis();
String message = String message =
// yellow text MinecraftTextFormat.YELLOW + "Distant Horizons: High vanilla render distance detected." + MinecraftTextFormat.CLEAR_FORMATTING + "\n" +
"\u00A7e" + "Distant Horizons: High vanilla render distance detected." + "\u00A7r \n" + "Using a high vanilla render distance uses a lot of CPU power \n" +
"Using a high vanilla render distance uses a lot of CPU power \n" + "and doesn't improve graphics much after about 12.\n" +
"and doesn't improve graphics much after about 12.\n" + "Lowering your vanilla render distance will give you better FPS\n" +
"Lowing your vanilla render distance will give you better FPS\n" + "and reduce stuttering at a similar visual quality.\n" +
"and reduce stuttering at a similar visual quality.\n" + MinecraftTextFormat.GRAY + "A vanilla render distance of 8 is recommended." + MinecraftTextFormat.CLEAR_FORMATTING + "\n" +
// gray text "This message can be disabled in DH's config under Advanced -> Logging.\n";
"\u00A77" + "A vanilla render distance of 8 is recommended." + "\u00A7r \n" +
"This message can be disabled in DH's config under Advanced -> Logging.\n";
MC_CLIENT.sendChatMessage(message); MC_CLIENT.sendChatMessage(message);
} }
} }
@@ -82,15 +82,15 @@ public class ServerApi
// level events // // level events //
//==============// //==============//
public void serverLevelLoadEvent(IServerLevelWrapper level) public void serverLevelLoadEvent(IServerLevelWrapper levelWrapper)
{ {
LOGGER.debug("Server Level " + level + " loading"); LOGGER.debug("Server Level " + levelWrapper + " loading");
AbstractDhWorld serverWorld = SharedApi.getAbstractDhWorld(); AbstractDhWorld serverWorld = SharedApi.getAbstractDhWorld();
if (serverWorld != null) if (serverWorld != null)
{ {
serverWorld.getOrLoadLevel(level); serverWorld.getOrLoadLevel(levelWrapper);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level)); ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(levelWrapper));
} }
} }
public void serverLevelUnloadEvent(IServerLevelWrapper level) public void serverLevelUnloadEvent(IServerLevelWrapper level)
@@ -25,6 +25,7 @@ import com.seibel.distanthorizons.core.Initializer;
import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateData; import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateData;
import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateQueueManager; import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateQueueManager;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.eventHandlers.IgnoredDimensionCsvHandler;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.generation.DhLightingEngine; import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.level.DhClientLevel; import com.seibel.distanthorizons.core.level.DhClientLevel;
@@ -184,12 +185,11 @@ public class SharedApi
public void chunkBlockChangedEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true, false); } public void chunkBlockChangedEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true, false); }
public void chunkLoadEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true, true); } public void chunkLoadEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true, true); }
//public void applyChunkUpdate(IChunkWrapper chunkWrapper, ILevelWrapper level, boolean canGetNeighboringChunks) { this.applyChunkUpdate(chunkWrapper, level, canGetNeighboringChunks, false); }
public void applyChunkUpdate(IChunkWrapper chunkWrapper, ILevelWrapper level, boolean canGetNeighboringChunks, boolean newlyLoaded) public void applyChunkUpdate(IChunkWrapper chunkWrapper, ILevelWrapper level, boolean canGetNeighboringChunks, boolean newlyLoaded)
{ {
//========================// //===================//
// world and level checks // // validation checks //
//========================// //===================//
if (chunkWrapper == null) if (chunkWrapper == null)
{ {
@@ -217,7 +217,6 @@ public class SharedApi
return; return;
} }
// only continue if the level is loaded // only continue if the level is loaded
IDhLevel dhLevel = dhWorld.getLevel(level); IDhLevel dhLevel = dhWorld.getLevel(level);
if (dhLevel == null) if (dhLevel == null)
@@ -232,6 +231,7 @@ public class SharedApi
return; return;
} }
// ignore chunk updates if the network should handle them
if (dhLevel instanceof DhClientLevel) if (dhLevel instanceof DhClientLevel)
{ {
if (!((DhClientLevel) dhLevel).shouldProcessChunkUpdate(chunkWrapper.getChunkPos())) if (!((DhClientLevel) dhLevel).shouldProcessChunkUpdate(chunkWrapper.getChunkPos()))
@@ -240,7 +240,14 @@ public class SharedApi
} }
} }
// shoudln't normally happen, but just in case // ignore chunk updates for non-rendered levels
String dimName = dhLevel.getLevelWrapper().getDimensionName();
if (IgnoredDimensionCsvHandler.INSTANCE.dimensionNameShouldBeIgnored(dimName))
{
return;
}
// shouldn't normally happen, but just in case
if (CHUNK_UPDATE_QUEUE_MANAGER.contains(chunkWrapper.getChunkPos())) if (CHUNK_UPDATE_QUEUE_MANAGER.contains(chunkWrapper.getChunkPos()))
{ {
// TODO this will prevent some LODs from updating across dimensions if multiple levels are loaded // TODO this will prevent some LODs from updating across dimensions if multiple levels are loaded
@@ -248,94 +255,20 @@ public class SharedApi
} }
queueChunkUpdate(chunkWrapper, dhLevel);
//===============================//
// update the necessary chunk(s) //
//===============================//
if (!canGetNeighboringChunks)
{
// only update the center chunk
queueChunkUpdate(chunkWrapper, null, dhLevel, false);
return;
}
ArrayList<IChunkWrapper> neighboringChunkList = getNeighborChunkListForChunk(chunkWrapper, dhLevel);
if (newlyLoaded)
{
// this means this chunkWrapper is a newly loaded chunk
// which may be missing some neighboring chunk data
// because it is bordering the render distance
// thus, only the chunks neighboring this chunkWrapper will get updated
// because those are more likely to have their full neighboring chunk data
//TODO this does not prevent those neighboring chunks from updating
// this newly loaded chunk that were just skipped
// leading to occasional lighting issues
for (IChunkWrapper neighboringChunk : neighboringChunkList)
{
if (neighboringChunk == chunkWrapper)
{
continue;
}
this.applyChunkUpdate(neighboringChunk, level, true, false);
}
}
else
{
// if not all neighboring chunk data is available, do not try to update
if (neighboringChunkList.size() < 9)
{
return;
}
// update the center with any existing neighbour chunks.
// this is done so lighting changes are propagated correctly
queueChunkUpdate(chunkWrapper, neighboringChunkList, dhLevel, true);
}
}
private static ArrayList<IChunkWrapper> getNeighborChunkListForChunk(IChunkWrapper chunkWrapper, IDhLevel dhLevel)
{
// get the neighboring chunk list
ArrayList<IChunkWrapper> neighborChunkList = new ArrayList<>(9);
for (int xOffset = -1; xOffset <= 1; xOffset++)
{
for (int zOffset = -1; zOffset <= 1; zOffset++)
{
if (xOffset == 0 && zOffset == 0)
{
// center chunk
neighborChunkList.add(chunkWrapper);
}
else
{
// neighboring chunk
DhChunkPos neighborPos = new DhChunkPos(chunkWrapper.getChunkPos().getX() + xOffset, chunkWrapper.getChunkPos().getZ() + zOffset);
IChunkWrapper neighborChunk = dhLevel.getLevelWrapper().tryGetChunk(neighborPos);
if (neighborChunk != null)
{
neighborChunkList.add(neighborChunk);
}
}
}
}
return neighborChunkList;
} }
private static void queueChunkUpdate(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighborChunkList, IDhLevel dhLevel, boolean canGetNeighboringChunks) private static void queueChunkUpdate(IChunkWrapper chunkWrapper, IDhLevel dhLevel)
{ {
// return if the chunk is already queued // return if the chunk is already queued
if (CHUNK_UPDATE_QUEUE_MANAGER.contains(chunkWrapper.getChunkPos())) if (CHUNK_UPDATE_QUEUE_MANAGER.contains(chunkWrapper.getChunkPos()))
{ {
return; return;
} }
// add chunk update data to preUpdate queue // add chunk update data to preUpdate queue
ChunkUpdateData updateData = new ChunkUpdateData(chunkWrapper, neighborChunkList, dhLevel, canGetNeighboringChunks); ChunkUpdateData updateData = new ChunkUpdateData(chunkWrapper, dhLevel);
CHUNK_UPDATE_QUEUE_MANAGER.addItemToPreUpdateQueue(chunkWrapper.getChunkPos(), updateData); CHUNK_UPDATE_QUEUE_MANAGER.addItemToPreUpdateQueue(chunkWrapper.getChunkPos(), updateData);
@@ -343,7 +276,8 @@ public class SharedApi
// (this prevents doing extra work queuing tasks that may not be necessary) // (this prevents doing extra work queuing tasks that may not be necessary)
// and makes sure the chunks closest to the player are updated first // and makes sure the chunks closest to the player are updated first
PriorityTaskPicker.Executor executor = ThreadPoolUtil.getChunkToLodBuilderExecutor(); PriorityTaskPicker.Executor executor = ThreadPoolUtil.getChunkToLodBuilderExecutor();
if (executor != null && executor.getQueueSize() < executor.getPoolSize()) if (executor != null
&& executor.getQueueSize() < executor.getPoolSize())
{ {
try try
{ {
@@ -383,10 +317,7 @@ public class SharedApi
// update the necessary chunk(s) // // update the necessary chunk(s) //
//===============================// //===============================//
// process preUpdate queue
processQueuedChunkPreUpdate(); processQueuedChunkPreUpdate();
// process update queue
processQueuedChunkUpdate(); processQueuedChunkUpdate();
// queue the next position if there are still positions to process // queue the next position if there are still positions to process
@@ -415,8 +346,7 @@ public class SharedApi
IDhLevel dhLevel = preUpdateData.dhLevel; IDhLevel dhLevel = preUpdateData.dhLevel;
IChunkWrapper chunkWrapper = preUpdateData.chunkWrapper; IChunkWrapper chunkWrapper = preUpdateData.chunkWrapper;
boolean canGetNeighboringChunks = preUpdateData.canGetNeighboringChunks; chunkWrapper.createDhHeightMaps();
ArrayList<IChunkWrapper> neighborChunkList = preUpdateData.neighborChunkList;
try try
{ {
@@ -433,34 +363,6 @@ public class SharedApi
// do not update the chunk if the hash is the same // do not update the chunk if the hash is the same
return; return;
} }
// if this chunk will update and can get neighbors
// then queue neighboring chunks to update as well
// neighboring chunk will get added directly to the update queue
// so they won't queue further chunk updates
if (neighborChunkList != null
&& !neighborChunkList.isEmpty())
{
for (IChunkWrapper adjacentChunk : neighborChunkList)
{
// pulling a new chunkWrapper is necessary to prevent concurrent modification on the existing chunkWrappers
IChunkWrapper newCenterChunk = dhLevel.getLevelWrapper().tryGetChunk(adjacentChunk.getChunkPos());
if (newCenterChunk != null)
{
ChunkUpdateData newUpdateData;
if (canGetNeighboringChunks)
{
newUpdateData = new ChunkUpdateData(newCenterChunk, getNeighborChunkListForChunk(newCenterChunk, dhLevel), dhLevel, true);
}
else
{
newUpdateData = new ChunkUpdateData(newCenterChunk, null, dhLevel, false);
}
CHUNK_UPDATE_QUEUE_MANAGER.addItemToUpdateQueue(newCenterChunk.getChunkPos(), newUpdateData);
}
}
}
} }
CHUNK_UPDATE_QUEUE_MANAGER.addItemToUpdateQueue(chunkWrapper.getChunkPos(), preUpdateData); CHUNK_UPDATE_QUEUE_MANAGER.addItemToUpdateQueue(chunkWrapper.getChunkPos(), preUpdateData);
@@ -473,8 +375,6 @@ public class SharedApi
private static void processQueuedChunkUpdate() private static void processQueuedChunkUpdate()
{ {
//LOGGER.trace(chunkWrapper.getChunkPos() + " " + executor.getActiveCount() + " / " + executor.getQueue().size() + " - " + executor.getCompletedTaskCount());
ChunkUpdateData updateData = CHUNK_UPDATE_QUEUE_MANAGER.updateQueue.popClosest(); ChunkUpdateData updateData = CHUNK_UPDATE_QUEUE_MANAGER.updateQueue.popClosest();
if (updateData == null) if (updateData == null)
{ {
@@ -484,15 +384,11 @@ public class SharedApi
IChunkWrapper chunkWrapper = updateData.chunkWrapper; IChunkWrapper chunkWrapper = updateData.chunkWrapper;
IDhLevel dhLevel = updateData.dhLevel; IDhLevel dhLevel = updateData.dhLevel;
ILevelWrapper levelWrapper = dhLevel.getLevelWrapper(); ILevelWrapper levelWrapper = dhLevel.getLevelWrapper();
// having a list of the nearby chunks is needed for lighting and beacon generation
@Nullable ArrayList<IChunkWrapper> nearbyChunkList = updateData.neighborChunkList;
// a non-null list is needed for the lighting engine // having a list of the nearby chunks is needed for lighting and beacon generation
if (nearbyChunkList == null) ArrayList<IChunkWrapper> nearbyChunkList = tryGetNeighborChunkListForChunk(chunkWrapper);
{
nearbyChunkList = new ArrayList<IChunkWrapper>();
nearbyChunkList.add(chunkWrapper);
}
try try
{ {
@@ -508,6 +404,35 @@ public class SharedApi
{ {
LOGGER.error("Unexpected error when updating chunk at pos: [" + chunkWrapper.getChunkPos() + "]", e); LOGGER.error("Unexpected error when updating chunk at pos: [" + chunkWrapper.getChunkPos() + "]", e);
} }
CHUNK_UPDATE_QUEUE_MANAGER.queuedChunkWrapperByChunkPos.remove(updateData.chunkWrapper.getChunkPos());
}
private static ArrayList<IChunkWrapper> tryGetNeighborChunkListForChunk(IChunkWrapper chunkWrapper)
{
// get the neighboring chunk list
ArrayList<IChunkWrapper> neighborChunkList = new ArrayList<>(9);
for (int xOffset = -1; xOffset <= 1; xOffset++)
{
for (int zOffset = -1; zOffset <= 1; zOffset++)
{
if (xOffset == 0 && zOffset == 0)
{
// center chunk
neighborChunkList.add(chunkWrapper);
}
else
{
// neighboring chunk
DhChunkPos neighborPos = new DhChunkPos(chunkWrapper.getChunkPos().getX() + xOffset, chunkWrapper.getChunkPos().getZ() + zOffset);
IChunkWrapper neighborChunk = CHUNK_UPDATE_QUEUE_MANAGER.tryGetChunk(neighborPos);
if (neighborChunk != null)
{
neighborChunkList.add(neighborChunk);
}
}
}
}
return neighborChunkList;
} }
@@ -2,6 +2,7 @@ package com.seibel.distanthorizons.core.api.internal.chunkUpdating;
import com.seibel.distanthorizons.core.api.internal.SharedApi; import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import org.jetbrains.annotations.Nullable;
import java.util.Comparator; import java.util.Comparator;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@@ -105,6 +106,7 @@ public class ChunkPosQueue
this.furthestQueue.remove(closest); this.furthestQueue.remove(closest);
return this.updateDataByChunkPos.remove(closest); return this.updateDataByChunkPos.remove(closest);
} }
@Nullable
public ChunkUpdateData popFurthest() public ChunkUpdateData popFurthest()
{ {
if (this.furthestQueue.isEmpty()) if (this.furthestQueue.isEmpty())
@@ -9,18 +9,13 @@ import java.util.ArrayList;
public class ChunkUpdateData public class ChunkUpdateData
{ {
public IChunkWrapper chunkWrapper; public IChunkWrapper chunkWrapper;
@Nullable
public ArrayList<IChunkWrapper> neighborChunkList;
public IDhLevel dhLevel; public IDhLevel dhLevel;
public boolean canGetNeighboringChunks;
public ChunkUpdateData(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighborChunkList, IDhLevel dhLevel, boolean canGetNeighborChunks) public ChunkUpdateData(IChunkWrapper chunkWrapper, IDhLevel dhLevel)
{ {
this.chunkWrapper = chunkWrapper; this.chunkWrapper = chunkWrapper;
this.neighborChunkList = neighborChunkList;
this.dhLevel = dhLevel; this.dhLevel = dhLevel;
this.canGetNeighboringChunks = canGetNeighborChunks;
} }
} }
@@ -1,16 +1,22 @@
package com.seibel.distanthorizons.core.api.internal.chunkUpdating; package com.seibel.distanthorizons.core.api.internal.chunkUpdating;
import com.google.common.cache.CacheBuilder;
import com.seibel.distanthorizons.core.api.internal.ClientApi; import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.api.internal.SharedApi; import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.enums.MinecraftTextFormat;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.world.EWorldEnvironment; import com.seibel.distanthorizons.core.world.EWorldEnvironment;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import org.jetbrains.annotations.Nullable;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
public class ChunkUpdateQueueManager public class ChunkUpdateQueueManager
{ {
@@ -21,6 +27,12 @@ public class ChunkUpdateQueueManager
public final ChunkPosQueue preUpdateQueue; public final ChunkPosQueue preUpdateQueue;
public final Set<DhChunkPos> ignoredChunkPosSet = Collections.newSetFromMap(new ConcurrentHashMap<>()); public final Set<DhChunkPos> ignoredChunkPosSet = Collections.newSetFromMap(new ConcurrentHashMap<>());
public final ConcurrentMap<DhChunkPos, IChunkWrapper> queuedChunkWrapperByChunkPos = CacheBuilder.newBuilder()
.expireAfterWrite(20, TimeUnit.SECONDS)
.<DhChunkPos, IChunkWrapper>build()
.asMap();
/** dynamically changes based on the number of threads currently available */
public int maxSize = 500; public int maxSize = 500;
private static long lastOverloadedLogMessageMsTime = 0; private static long lastOverloadedLogMessageMsTime = 0;
@@ -68,22 +80,27 @@ public class ChunkUpdateQueueManager
* If there are no more slots, replaces the item furthest from the center in the update queue. * If there are no more slots, replaces the item furthest from the center in the update queue.
*/ */
public void addItemToPreUpdateQueue(DhChunkPos pos, ChunkUpdateData updateData) public void addItemToPreUpdateQueue(DhChunkPos pos, ChunkUpdateData updateData)
{ this.addItemToQueue(pos, updateData, this.preUpdateQueue); }
public void addItemToUpdateQueue(DhChunkPos pos, ChunkUpdateData updateData)
{ this.addItemToQueue(pos, updateData, this.updateQueue); }
private void addItemToQueue(DhChunkPos pos, ChunkUpdateData updateData, ChunkPosQueue queue)
{ {
int remainingSlots = this.maxSize - this.getQueuedCount(); int remainingSlots = this.maxSize - this.getQueuedCount();
// If no slots are left, get one by removing the item furthest from the center // If no slots are left, get one by removing the item furthest from the center
if (remainingSlots <= 0) if (remainingSlots <= 0)
{ {
if (!this.updateQueue.isEmpty()) ChunkUpdateData removedData = queue.popFurthest();
if (removedData != null)
{ {
this.updateQueue.popFurthest(); this.queuedChunkWrapperByChunkPos.remove(removedData.chunkWrapper.getChunkPos());
}
else
{
this.preUpdateQueue.popFurthest();
} }
} }
this.preUpdateQueue.addItem(pos, updateData);
queue.addItem(pos,updateData);
this.queuedChunkWrapperByChunkPos.putIfAbsent(pos, updateData.chunkWrapper);
remainingSlots = this.maxSize - this.getQueuedCount(); remainingSlots = this.maxSize - this.getQueuedCount();
if (remainingSlots <= 0) if (remainingSlots <= 0)
@@ -92,24 +109,6 @@ public class ChunkUpdateQueueManager
} }
} }
public void addItemToUpdateQueue(DhChunkPos pos, ChunkUpdateData updateData)
{
int remainingSlots = this.maxSize - this.getQueuedCount();
// If no slots are left, get one by removing the item furthest from the center
if (remainingSlots <= 0)
{
this.updateQueue.popFurthest();
}
this.updateQueue.addItem(pos,updateData);
remainingSlots = this.maxSize - this.getQueuedCount();
if (remainingSlots <= 0)
{
this.sendOverloadMessage();
}
}
private void sendOverloadMessage() private void sendOverloadMessage()
{ {
@@ -119,7 +118,7 @@ public class ChunkUpdateQueueManager
{ {
lastOverloadedLogMessageMsTime = System.currentTimeMillis(); lastOverloadedLogMessageMsTime = System.currentTimeMillis();
String message = "\u00A76" + "Distant Horizons overloaded, too many chunks queued for LOD processing. " + "\u00A7r" + String message = MinecraftTextFormat.ORANGE + "Distant Horizons overloaded, too many chunks queued for LOD processing. " + MinecraftTextFormat.CLEAR_FORMATTING +
"\nThis may result in holes in your LODs. " + "\nThis may result in holes in your LODs. " +
"\nFix: move through the world slower, decrease your vanilla render distance, slow down your world pre-generator (IE Chunky), or increase the Distant Horizons' CPU thread counts. " + "\nFix: move through the world slower, decrease your vanilla render distance, slow down your world pre-generator (IE Chunky), or increase the Distant Horizons' CPU thread counts. " +
"\nMax queue count [" + SharedApi.CHUNK_UPDATE_QUEUE_MANAGER.maxSize + "] ([" + SharedApi.MAX_UPDATING_CHUNK_COUNT_PER_THREAD_AND_PLAYER + "] per thread+players)."; "\nMax queue count [" + SharedApi.CHUNK_UPDATE_QUEUE_MANAGER.maxSize + "] ([" + SharedApi.MAX_UPDATING_CHUNK_COUNT_PER_THREAD_AND_PLAYER + "] per thread+players).";
@@ -140,6 +139,26 @@ public class ChunkUpdateQueueManager
} }
} }
/**
* Tries to return a cloned chunk wrapper from memory.
* Returns null if no chunk is available.
* <br><br>
* This is done instead of accessing the MC level since
* accessing the level often requires running on the render or server
* thread, which causes stuttering.
*/
@Nullable
public IChunkWrapper tryGetChunk(DhChunkPos pos)
{
IChunkWrapper existingWrapper = this.queuedChunkWrapperByChunkPos.get(pos);
if (existingWrapper == null)
{
return null;
}
return existingWrapper.copy();
}
//=========// //=========//
@@ -118,6 +118,20 @@ public class Config
public static ConfigEntry<Boolean> dynamicFadeUseOpaqueMcDepth = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "")
.build();
public static ConfigEntry<String> dynamicFadeExportPath = new ConfigEntry.Builder<String>()
.set("C:/Users/James_Seibel/Desktop/")
.comment(""
+ "")
.build();
public static class Advanced public static class Advanced
{ {
// common config links need to have their destination // common config links need to have their destination
@@ -457,6 +471,15 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Boolean> expandDistantBeacons = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "If true LOD beacon beams will be rendered wider at extreme distances, \n"
+ "making them easier to see. \n"
+ "If false all LOD beacon beams will only ever be 1 block wide. \n"
+ "")
.build();
public static ConfigEntry<Boolean> enableCloudRendering = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableCloudRendering = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.comment("" .comment(""
@@ -505,11 +528,6 @@ public class Config
+ "Note: Other mods may conflict with this setting. \n" + "Note: Other mods may conflict with this setting. \n"
+ "") + "")
.build(); .build();
@Deprecated
public static ConfigEntry<Boolean> disableVanillaFog = new ConfigEntry.Builder<Boolean>()
.set(!enableVanillaFog.get())
.setAppearance(EConfigEntryAppearance.ONLY_IN_API)
.build();
@@ -571,14 +589,6 @@ public class Config
static
{
disableVanillaFog.addListener(
new ConfigChangeListener<Boolean>(disableVanillaFog,
(disableVanillaFog) -> enableVanillaFog.setApiValue(disableVanillaFog))
);
}
public static class HeightFog public static class HeightFog
{ {
public static ConfigUIComment heightFogHeader = new ConfigUIComment.Builder().setParentConfigClass(HeightFog.class).build(); public static ConfigUIComment heightFogHeader = new ConfigUIComment.Builder().setParentConfigClass(HeightFog.class).build();
@@ -846,7 +856,7 @@ public class Config
public static ConfigUIComment experimentalHeader = new ConfigUIComment.Builder().setParentConfigClass(Experimental.class).build(); public static ConfigUIComment experimentalHeader = new ConfigUIComment.Builder().setParentConfigClass(Experimental.class).build();
public static ConfigEntry<Integer> earthCurveRatio = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> earthCurveRatio = new ConfigEntry.Builder<Integer>()
.setMinDefaultMax(0, 0, 5000) .setMinDefaultMax(-5000, 0, 5000)
.comment("" .comment(""
+ "This is the earth size ratio when applying the curvature shader effect. \n" + "This is the earth size ratio when applying the curvature shader effect. \n"
+ "Note: Enabling this feature may cause rendering bugs. \n" + "Note: Enabling this feature may cause rendering bugs. \n"
@@ -856,24 +866,26 @@ public class Config
+ "100 = 1 to 100 (63,710 blocks) \n" + "100 = 1 to 100 (63,710 blocks) \n"
+ "10000 = 1 to 10000 (637.1 blocks) \n" + "10000 = 1 to 10000 (637.1 blocks) \n"
+ "\n" + "\n"
+ "Note: Due to current limitations, the min value is 50 \n" + "Note: Due to current limitations, the min value is ["+WorldCurvatureConfigEventHandler.MIN_VALID_CURVE_VALUE+"] \n"
+ "and the max value is 5000. Any values outside this range \n" + "and the max value is 5000. Any values outside this range \n"
+ "will be set to 0 (disabled).") + "will be set to 0 (disabled).")
.addListener(WorldCurvatureConfigEventHandler.INSTANCE) .addListener(WorldCurvatureConfigEventHandler.INSTANCE)
.build(); .build();
// TODO should be replaced with a better long-term solution public static ConfigEntry<String> ignoredDimensionCsv = new ConfigEntry.Builder<String>()
@Deprecated .set("")
public static ConfigEntry<Boolean> onlyLoadCenterLods = new ConfigEntry.Builder<Boolean>() .comment(""
.set(false) + "A comma separated list of dimension resource locations where DH won't render. \n"
.comment("" + "\n"
+ "For internal testing:\n" + "Example: \"minecraft:the_nether,minecraft:the_end\"\n"
+ "Skips loading adjacent LODs to significantly reduce load times (~5x)\n" + "\n"
+ "but causes lighting on LOD borders to appear as full-bright\n" + "Note:\n"
+ "and other graphical bugs.\n" + "Some DH settings will be disabled and/or changed to improve \n"
+ "") + "visuals when DH rendering is disabled. \n"
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE) + "")
.build(); .addListener(IgnoredDimensionCsvHandler.INSTANCE)
.build();
} }
} }
@@ -1422,7 +1434,7 @@ public class Config
.set(false) .set(false)
// enabling this can be quite detrimental to performance, // enabling this can be quite detrimental to performance,
// so hiding it in the config file should reduce people accidentally enabling it // so hiding it in the config file should reduce people accidentally enabling it
.setAppearance(isRunningInDevEnvironment() ? EConfigEntryAppearance.ALL : EConfigEntryAppearance.ONLY_IN_FILE) .setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.comment("" .comment(""
+ "Enabling this will drastically increase chunk processing time\n" + "Enabling this will drastically increase chunk processing time\n"
+ "and you may need to increase your CPU load to handle it.\n" + "and you may need to increase your CPU load to handle it.\n"
@@ -1603,6 +1615,14 @@ public class Config
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerLevel> logConnectionConfigChangesToFile = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.setChatCommandName("logging.logConnectionConfigChanges")
.set(EDhApiLoggerLevel.WARN)
.comment(""
+ "If enabled, config changes sent by the server will be logged. \n"
+ "")
.build();
public static ConfigCategory warning = new ConfigCategory.Builder().set(Warning.class).build(); public static ConfigCategory warning = new ConfigCategory.Builder().set(Warning.class).build();
@@ -1865,6 +1885,8 @@ public class Config
ThreadPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues(); ThreadPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
RenderQualityPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues(); RenderQualityPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
QuickRenderToggleConfigEventHandler.INSTANCE.setUiOnlyConfigValues(); QuickRenderToggleConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
IgnoredDimensionCsvHandler.INSTANCE.onConfigValueSet();
} }
catch (Exception e) catch (Exception e)
{ {
@@ -17,23 +17,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.seibel.distanthorizons.core.generation.tasks; package com.seibel.distanthorizons.core.config.api.converters;
import java.util.concurrent.CompletableFuture; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConverter;
/** /**
* @author Leetom * Used to support deprecated config options that may be identical
* @version 2022-11-25 * in implementation but with the On/Off values flipped.
*
* @author James Seibel
* @version 2025-12-22
*/ */
public final class InProgressWorldGenTaskGroup public class InvertedBoolConverter implements IConverter<Boolean, Boolean>
{ {
public final WorldGenTaskGroup group;
public CompletableFuture<Void> genFuture = null;
@Override
public Boolean convertToCoreType(Boolean core)
{ return !core; }
public InProgressWorldGenTaskGroup(WorldGenTaskGroup group) @Override
{ public Boolean convertToApiType(Boolean api)
this.group = group; { return !api; }
}
} }
@@ -0,0 +1,125 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 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 <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.config.eventHandlers;
import com.seibel.distanthorizons.api.enums.config.EDhApiMcRenderingFadeMode;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiBeforeRenderEvent;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiCancelableEventParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.coreapi.util.StringUtil;
public class IgnoredDimensionCsvHandler extends DhApiBeforeRenderEvent implements IConfigListener
{
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public static IgnoredDimensionCsvHandler INSTANCE = new IgnoredDimensionCsvHandler();
private String[] dimensionNames = null;
//=============//
// constructor //
//=============//
/** private since we only ever need one handler at a time */
private IgnoredDimensionCsvHandler() { }
//=================//
// config handling //
//=================//
@Override
public void onConfigValueSet()
{
String ignoredDimensionCsvString = Config.Client.Advanced.Graphics.Experimental.ignoredDimensionCsv.get();
if (ignoredDimensionCsvString == null
|| ignoredDimensionCsvString.isEmpty())
{
LOGGER.info("Dimension ignoring disabled, DH will render all dimensions.");
this.dimensionNames = null;
}
else
{
try
{
this.dimensionNames = ignoredDimensionCsvString.split(",");
LOGGER.info("DH set to ignore dimensions: ["+ StringUtil.join(", ", this.dimensionNames)+"].");
}
catch (Exception e)
{
LOGGER.error("Failed to separate ignored dimensions from CSV string, error: ["+e.getMessage()+"].", e);
this.dimensionNames = null;
}
}
}
//===================//
// external handling //
//===================//
@Override
public void beforeRender(DhApiCancelableEventParam<DhApiRenderParam> event)
{
String dimName = event.value.clientLevelWrapper.getDimensionName();
if (IgnoredDimensionCsvHandler.INSTANCE.dimensionNameShouldBeIgnored(dimName))
{
event.cancelEvent();
Config.Client.Advanced.Graphics.Fog.enableVanillaFog.setApiValue(true);
Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.setApiValue(EDhApiMcRenderingFadeMode.NONE);
}
else
{
Config.Client.Advanced.Graphics.Fog.enableVanillaFog.setApiValue(null);
Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.setApiValue(null);
}
}
public boolean dimensionNameShouldBeIgnored(String dimName)
{
if (this.dimensionNames == null
|| this.dimensionNames.length == 0)
{
return false;
}
for (int i = 0; i < this.dimensionNames.length; i++)
{
String dimNameToIgnore = this.dimensionNames[i];
if (dimName.equalsIgnoreCase(dimNameToIgnore))
{
return true;
}
}
return false;
}
}
@@ -35,7 +35,7 @@ public class WorldCurvatureConfigEventHandler implements IConfigListener
{ {
public static WorldCurvatureConfigEventHandler INSTANCE = new WorldCurvatureConfigEventHandler(); public static WorldCurvatureConfigEventHandler INSTANCE = new WorldCurvatureConfigEventHandler();
private static final int MIN_VALID_CURVE_VALUE = 50; public static final int MIN_VALID_CURVE_VALUE = 50;
/** private since we only ever need one handler at a time */ /** private since we only ever need one handler at a time */
@@ -52,6 +52,11 @@ public class WorldCurvatureConfigEventHandler implements IConfigListener
// shouldn't update the UI, otherwise we may end up fighting the user // shouldn't update the UI, otherwise we may end up fighting the user
Config.Client.Advanced.Graphics.Experimental.earthCurveRatio.set(MIN_VALID_CURVE_VALUE); Config.Client.Advanced.Graphics.Experimental.earthCurveRatio.set(MIN_VALID_CURVE_VALUE);
} }
else if (curveRatio < 0 && curveRatio > -MIN_VALID_CURVE_VALUE)
{
// same as above, but in the negative direction
Config.Client.Advanced.Graphics.Experimental.earthCurveRatio.set(-MIN_VALID_CURVE_VALUE);
}
} }
@@ -121,7 +121,7 @@ public class FullDataSourceV2
public Boolean applyToChildren = null; public Boolean applyToChildren = null;
/** should only be used by methods exposed via the DH API */ /** should only be used by methods exposed via the DH API */
private boolean runApiChunkValidation = false; private boolean runApiSetterValidation = false;
@@ -202,8 +202,9 @@ public class FullDataSourceV2
public static FullDataSourceV2 createEmpty(long pos) public static FullDataSourceV2 createEmpty(long pos)
{ {
FullDataPointIdMap map = new FullDataPointIdMap(pos);
return new FullDataSourceV2( return new FullDataSourceV2(
pos, new FullDataPointIdMap(pos), pos, map,
// data points, genSteps, and columnCompression are all null since // data points, genSteps, and columnCompression are all null since
// nothing has been generated yet. // nothing has been generated yet.
// Using the default value of all 0's is adequate // Using the default value of all 0's is adequate
@@ -1296,7 +1297,7 @@ public class FullDataSourceV2
// API methods // // API methods //
//=============// //=============//
public void setRunApiChunkValidation(boolean runValidation) { this.runApiChunkValidation = runValidation; } public void setRunApiSetterValidation(boolean runValidation) { this.runApiSetterValidation = runValidation; }
@Override @Override
public int getWidthInDataColumns() { return WIDTH; } public int getWidthInDataColumns() { return WIDTH; }
@@ -1308,7 +1309,7 @@ public class FullDataSourceV2
try try
{ {
LodDataBuilder.putListInTopDownOrder(columnDataPoints); LodDataBuilder.putListInTopDownOrder(columnDataPoints);
if (this.runApiChunkValidation) if (this.runApiSetterValidation)
{ {
LodDataBuilder.validateOrThrowApiDataColumn(columnDataPoints); LodDataBuilder.validateOrThrowApiDataColumn(columnDataPoints);
} }
@@ -295,24 +295,21 @@ public class LodBufferContainer implements AutoCloseable
{ {
this.buffersUploaded = false; this.buffersUploaded = false;
GLProxy.queueRunningOnRenderThread(() -> for (GLVertexBuffer buffer : this.vbos)
{ {
for (GLVertexBuffer buffer : this.vbos) if (buffer != null)
{ {
if (buffer != null) buffer.destroyAsync();
{
buffer.destroyAsync();
}
} }
}
for (GLVertexBuffer buffer : this.vbosTransparent)
for (GLVertexBuffer buffer : this.vbosTransparent)
{
if (buffer != null)
{ {
if (buffer != null) buffer.destroyAsync();
{
buffer.destroyAsync();
}
} }
}); }
} }
} }
@@ -372,10 +372,10 @@ public class LodQuadBuilder
if (quad.direction.axis.isHorizontal() || quad.direction == EDhDirection.DOWN) if (quad.direction.axis.isHorizontal() || quad.direction == EDhDirection.DOWN)
{ {
if (this.grassSideRenderingMode == EDhApiGrassSideRendering.AS_DIRT if (this.grassSideRenderingMode == EDhApiGrassSideRendering.AS_DIRT
// if we want the color to fade, only apply the dirt color to the bottom vertices // if we want the color to fade, only apply the dirt color to the bottom vertices
|| (this.grassSideRenderingMode == EDhApiGrassSideRendering.FADE_TO_DIRT && quadBase[i][1] == 0) || (this.grassSideRenderingMode == EDhApiGrassSideRendering.FADE_TO_DIRT && quadBase[i][1] == 0)
// always render the bottom as dirt // always render the bottom as dirt
|| quad.direction == EDhDirection.DOWN) || quad.direction == EDhDirection.DOWN)
{ {
// for horizontal and bottom faces of grass blocks, use the dirt color to // for horizontal and bottom faces of grass blocks, use the dirt color to
// prevent green cliff walls // prevent green cliff walls
@@ -1,12 +0,0 @@
package com.seibel.distanthorizons.core.enums;
/**
* TODO
* might be deprecated in the future? in that case we'll probably want a wrapper
* function to handle colors for new MC versions
*
* source: https://minecraft.wiki/w/Formatting_codes
*/
public class EMinecraftColor
{
}
@@ -0,0 +1,35 @@
package com.seibel.distanthorizons.core.enums;
/**
* might be deprecated in the future? in that case we'll probably want a wrapper
* function to handle colors for new MC versions
* <br><br>
* source: https://minecraft.wiki/w/Formatting_codes
*/
public class MinecraftTextFormat
{
public static final String BLACK = "\u00A70";
public static final String DARK_BLUE = "\u00A71";
public static final String DARK_GREEN = "\u00A72";
public static final String DARK_AQUA = "\u00A73";
public static final String DARK_RED = "\u00A74";
public static final String DARK_PURPLE = "\u00A75";
public static final String ORANGE = "\u00A76";
public static final String GRAY = "\u00A77";
public static final String DARK_GRAY = "\u00A78";
public static final String BLUE = "\u00A79";
public static final String GREEN = "\u00A7a";
public static final String AQUA = "\u00A7b";
public static final String RED = "\u00A7c";
public static final String LIGHT_PURPLE = "\u00A7d";
public static final String YELLOW = "\u00A7e";
public static final String WHITE = "\u00A7f";
public static final String OBFUSCATED = "\u00A7k";
public static final String BOLD = "\u00A7l";
public static final String STRIKETHROUGH = "\u00A7m";
public static final String UNDERLINE = "\u00A7n";
public static final String ITALIC = "\u00A7o";
public static final String CLEAR_FORMATTING = "\u00A7r";
}
@@ -96,7 +96,12 @@ public class DelayedFullDataSourceSaveCache implements AutoCloseable
// no data currently in the memory cache for this position // no data currently in the memory cache for this position
memoryDataSource = FullDataSourceV2.createEmpty(inputPos); memoryDataSource = FullDataSourceV2.createEmpty(inputPos);
pair = new DataSourceSavedTimePair(memoryDataSource); pair = new DataSourceSavedTimePair(memoryDataSource);
this.dataSourceByPosition.put(inputPos, pair); DataSourceSavedTimePair oldPair = this.dataSourceByPosition.put(inputPos, pair);
if (oldPair != null)
{
// shouldn't happen, but just in case
this.handleDataSourceRemoval(oldPair.dataSource);
}
} }
else else
{ {
@@ -28,8 +28,8 @@ import com.seibel.distanthorizons.core.file.fullDatafile.V2.FullDataUpdatePropag
import com.seibel.distanthorizons.core.file.structure.ISaveStructure; import com.seibel.distanthorizons.core.file.structure.ISaveStructure;
import com.seibel.distanthorizons.core.generation.DhLightingEngine; import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue; import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue;
import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; import com.seibel.distanthorizons.core.generation.tasks.ERetrievalResultState;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
@@ -38,6 +38,7 @@ import com.seibel.distanthorizons.core.pooling.PhantomArrayListPool;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.util.ExceptionUtil;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker; import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
@@ -68,7 +69,9 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
* TODO this should be dynamically allocated based on CPU load * TODO this should be dynamically allocated based on CPU load
* and abilities. * and abilities.
*/ */
public static final int MAX_WORLD_GEN_REQUESTS_PER_THREAD = 20; public static final int MAX_WORLD_GEN_REQUESTS_PER_THREAD = 20;
public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Generated Provider");
private final AtomicReference<IFullDataSourceRetrievalQueue> worldGenQueueRef = new AtomicReference<>(null); private final AtomicReference<IFullDataSourceRetrievalQueue> worldGenQueueRef = new AtomicReference<>(null);
@@ -85,15 +88,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
public GeneratedFullDataSourceProvider(IDhLevel level, ISaveStructure saveStructure) throws SQLException, IOException public GeneratedFullDataSourceProvider(IDhLevel level, ISaveStructure saveStructure) throws SQLException, IOException
{ this(level, saveStructure, null); } { this(level, saveStructure, null); }
public GeneratedFullDataSourceProvider(IDhLevel level, ISaveStructure saveStructure, @Nullable File saveDirOverride) throws SQLException, IOException public GeneratedFullDataSourceProvider(IDhLevel level, ISaveStructure saveStructure, @Nullable File saveDirOverride) throws SQLException, IOException
{ { super(level, saveStructure, saveDirOverride); }
super(level, saveStructure, saveDirOverride);
this.addDataSourceUpdateListener((@NotNull FullDataSourceV2 updatedData) ->
{
this.onWorldGenTaskComplete(WorldGenResult.CreateSuccess(updatedData.getPos()), null);
});
}
@@ -122,32 +117,43 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// events // // events //
//========// //========//
private void onWorldGenTaskComplete(WorldGenResult genTaskResult, Throwable exception) private void onWorldGenTaskComplete(@NotNull Long genPos, @Nullable DataSourceRetrievalResult genTaskResult, @Nullable Throwable exception)
{ {
if (exception != null) try
{ {
// don't log shutdown exceptions if (exception != null)
if (!(exception instanceof CancellationException || exception.getCause() instanceof CancellationException))
{ {
LOGGER.error("Uncaught Gen Task Exception at ["+genTaskResult.pos+"], error: ["+exception.getMessage()+"].", exception); // don't log shutdown exceptions
if (!ExceptionUtil.isInterruptOrReject(exception))
{
LOGGER.error("Uncaught Gen Task Exception at [" + genPos + "], error: [" + exception.getMessage() + "].", exception);
}
return;
}
Objects.requireNonNull(genTaskResult);
if (genTaskResult.state == ERetrievalResultState.SUCCESS)
{
LodUtil.assertTrue(genTaskResult.dataSource != null, "Successful retrieval object should have a datasource.");
this.dataUpdater.updateDataSource(genTaskResult.dataSource);
this.fireOnGenPosSuccessListeners(genTaskResult.pos);
genTaskResult.dataSource.close();
}
else if (genTaskResult.state == ERetrievalResultState.REQUIRES_SPLITTING)
{
// task was split
LodUtil.assertTrue(genTaskResult.dataSource == null, "Split retrieval object should not have a datasource.");
}
else
{
// shouldn't happen, but just in case
LOGGER.warn("Unexpected gen Task state at: [" + DhSectionPos.toString(genTaskResult.pos) + "], state: [" + genTaskResult.state + "], datasource: NULL, exception: NULL.");
} }
} }
else if (genTaskResult.success) catch (Exception e)
{ {
this.fireOnGenPosSuccessListeners(genTaskResult.pos); LOGGER.error("Unexpected issue during onWorldGenTaskComplete, error: ["+e.getMessage()+"].", e);
return;
}
else
{
// generation didn't complete
LOGGER.debug("Gen Task Failed at " + genTaskResult.pos);
}
// if the generation task was split up into smaller positions, add the on-complete event to them
for (CompletableFuture<WorldGenResult> siblingFuture : genTaskResult.childFutures)
{
siblingFuture.whenComplete((siblingGenTaskResult, siblingEx) -> this.onWorldGenTaskComplete(siblingGenTaskResult, siblingEx));
} }
} }
@@ -207,10 +213,10 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
} }
@Override @Override
public boolean canQueueRetrieval() { return this.canQueueRetrieval(false); } public boolean canQueueRetrievalNow() { return this.canQueueRetrievalNow(false); }
public boolean canQueueRetrieval(boolean pruneWaitingTasksAboveLimit) public boolean canQueueRetrievalNow(boolean pruneWaitingTasksAboveLimit)
{ {
if (!super.canQueueRetrieval()) if (!super.canQueueRetrievalNow())
{ {
return false; return false;
} }
@@ -270,12 +276,16 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
int availableTaskSlots = maxWorldGenQueueCount - worldGenQueue.getWaitingTaskCount(); int availableTaskSlots = maxWorldGenQueueCount - worldGenQueue.getWaitingTaskCount();
if (availableTaskSlots <= 0) if (availableTaskSlots == 0)
{
return false;
}
else if (availableTaskSlots < 0)
{ {
if (pruneWaitingTasksAboveLimit) if (pruneWaitingTasksAboveLimit)
{ {
AtomicInteger tasksToCancel = new AtomicInteger(-availableTaskSlots + 1); AtomicInteger tasksToCancel = new AtomicInteger(availableTaskSlots * -1);
worldGenQueue.removeRetrievalRequestIf(x -> tasksToCancel.getAndDecrement() > 0); worldGenQueue.removeRetrievalRequestIf(taskPos -> tasksToCancel.getAndDecrement() > 0);
} }
else else
{ {
@@ -288,7 +298,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
} }
@Override @Override
public CompletableFuture<WorldGenResult> queuePositionForRetrieval(Long genPos) public CompletableFuture<DataSourceRetrievalResult> queuePositionForRetrieval(Long genPos)
{ {
IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get(); IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get();
if (worldGenQueue == null) if (worldGenQueue == null)
@@ -296,13 +306,8 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
return null; return null;
} }
WorldGenTaskTracker genTaskTracker = new WorldGenTaskTracker(genPos); CompletableFuture<DataSourceRetrievalResult> worldGenFuture = worldGenQueue.submitRetrievalTask(genPos, (byte) (DhSectionPos.getDetailLevel(genPos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL));
CompletableFuture<WorldGenResult> worldGenFuture = worldGenQueue.submitRetrievalTask(genPos, (byte) (DhSectionPos.getDetailLevel(genPos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL), genTaskTracker); worldGenFuture.whenComplete((r, e) -> this.onWorldGenTaskComplete(genPos, r, e));
worldGenFuture.whenComplete((genTaskResult, ex) ->
{
//LOGGER.info("gen task complete ["+DhSectionPos.toString(genPos)+"]");
//this.onWorldGenTaskComplete(genTaskResult, ex);
});
return worldGenFuture; return worldGenFuture;
} }
@@ -321,22 +326,20 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
public void clearRetrievalQueue() { this.worldGenQueueRef.set(null); } public void clearRetrievalQueue() { this.worldGenQueueRef.set(null); }
public boolean isFullyGenerated(ByteArrayList columnGenerationSteps) public boolean generationStepsAreFullyGenerated(ByteArrayList columnGenerationSteps)
{ {
return IntStream.range(0, columnGenerationSteps.size()) return IntStream.range(0, columnGenerationSteps.size())
.noneMatch(i -> .noneMatch((int intValue) ->
{ {
byte value = columnGenerationSteps.getByte(i); byte value = columnGenerationSteps.getByte(intValue);
return value == EDhApiWorldGenerationStep.EMPTY.value return value == EDhApiWorldGenerationStep.EMPTY.value
|| value == EDhApiWorldGenerationStep.DOWN_SAMPLED.value; || value == EDhApiWorldGenerationStep.DOWN_SAMPLED.value;
}); });
} }
public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Generated Provider");
@Override @Override
public LongArrayList getPositionsToRetrieve(Long pos) public LongArrayList getPositionsToRetrieve(long pos)
{ {
IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get(); IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get();
if (worldGenQueue == null) if (worldGenQueue == null)
@@ -352,7 +355,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
{ {
ByteArrayList columnGenStepArray = checkout.getByteArray(0, FullDataSourceV2.WIDTH*FullDataSourceV2.WIDTH); ByteArrayList columnGenStepArray = checkout.getByteArray(0, FullDataSourceV2.WIDTH*FullDataSourceV2.WIDTH);
this.repo.getColumnGenerationStepForPos(pos, columnGenStepArray); this.repo.getColumnGenerationStepForPos(pos, columnGenStepArray);
if (!columnGenStepArray.isEmpty()) if (columnGenStepArray.size() != 0)
{ {
boolean positionFullyGenerated = true; boolean positionFullyGenerated = true;
@@ -378,12 +381,11 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// this section is missing one or more columns, queue the missing ones for generation. // this section is missing one or more columns, queue the missing ones for generation.
// TODO speed up this logic by only checking ungenerated columns
LongArrayList generationList = new LongArrayList(); LongArrayList generationList = new LongArrayList();
byte lowestGeneratorDetailLevel = (byte) Math.min( byte lowestGeneratorDetailLevel = (byte) Math.min(
worldGenQueue.lowestDataDetail() + DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL, worldGenQueue.lowestDataDetail() + DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL,
DhSectionPos.getDetailLevel(pos)); DhSectionPos.getDetailLevel(pos));
DhSectionPos.forEachChildAtDetailLevel(pos, lowestGeneratorDetailLevel, (genPos) -> DhSectionPos.forEachChildAtDetailLevel(pos, lowestGeneratorDetailLevel, (genPos) ->
{ {
@@ -471,48 +473,13 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// helper classes // // helper classes //
//================// //================//
// TODO may not be needed
private class WorldGenTaskTracker implements IWorldGenTaskTracker
{
/** just used when debugging/troubleshooting */
private final long pos;
public WorldGenTaskTracker(long pos) { this.pos = pos; }
@Override
public Consumer<FullDataSourceV2> getDataSourceConsumer()
{
return (dataSource) ->
{
GeneratedFullDataSourceProvider.this.delayedFullDataSourceSaveCache.writeDataSourceToMemoryAndQueueSave(dataSource);
};
}
@Override
public CompletableFuture<Boolean> shouldGenerateSplitChild(long pos)
{
return GeneratedFullDataSourceProvider.this.getAsync(pos).thenApply(fullDataSource ->
{
//noinspection TryFinallyCanBeTryWithResources
try
{
return !GeneratedFullDataSourceProvider.this.isFullyGenerated(fullDataSource.columnGenerationSteps);
}
finally
{
fullDataSource.close();
}
});
}
}
private CompletableFuture<Void> onDataSourceSaveAsync(FullDataSourceV2 fullDataSource) private CompletableFuture<Void> onDataSourceSaveAsync(FullDataSourceV2 fullDataSource)
{ {
// block lights should have been populated at the chunkWrapper stage // block lights should have been populated at the chunkWrapper stage
// waiting to populate the data source's skylight at this stage prevents re-lighting and // waiting to populate the data source's skylight at this stage prevents re-lighting and
// allows us to reduce cross-chunk lighting issues by lighting the whole 4x4 LOD at once // allows us to reduce cross-chunk lighting issues by lighting the whole 4x4 LOD at once
DhLightingEngine.INSTANCE.bakeDataSourceSkyLight(fullDataSource, LodUtil.MAX_MC_LIGHT); int skyLight = this.level.getLevelWrapper().hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT;
DhLightingEngine.INSTANCE.bakeDataSourceSkyLight(fullDataSource, skyLight);
return this.updateDataSourceAsync(fullDataSource); return this.updateDataSourceAsync(fullDataSource);
} }
@@ -524,7 +491,6 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
{ {
boolean shouldDoWorldGen(); boolean shouldDoWorldGen();
@Nullable
DhBlockPos2D getTargetPosForGeneration(); DhBlockPos2D getTargetPosForGeneration();
/** Fired whenever a section has completed generating */ /** Fired whenever a section has completed generating */
@@ -23,9 +23,11 @@ import com.google.common.cache.CacheBuilder;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.structure.ISaveStructure; import com.seibel.distanthorizons.core.file.structure.ISaveStructure;
import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue; import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue;
import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.level.LodRequestModule; import com.seibel.distanthorizons.core.level.LodRequestModule;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.generation.tasks.ERetrievalResultState;
import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoadRequestQueue; import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoadRequestQueue;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -74,7 +76,7 @@ public class RemoteFullDataSourceProvider extends GeneratedFullDataSourceProvide
//==================// //==================//
@Override @Override
public boolean canQueueRetrieval() { return this.canQueueRetrieval(true); } public boolean canQueueRetrievalNow() { return this.canQueueRetrievalNow(true); }
@Override @Override
@Nullable @Nullable
@@ -102,10 +104,20 @@ public class RemoteFullDataSourceProvider extends GeneratedFullDataSourceProvide
Long timestamp = this.getTimestampForPos(pos); Long timestamp = this.getTimestampForPos(pos);
if (timestamp != null) if (timestamp != null)
{ {
this.syncOnLoadRequestQueue.submitRequest(pos, timestamp, fullDataSource -> this.syncOnLoadRequestQueue.submitRequest(pos, timestamp)
{ .thenAccept((DataSourceRetrievalResult result) ->
this.updateDataSourceAsync(fullDataSource).whenComplete((result, throwable) -> fullDataSource.close()); {
}); if (result.state == ERetrievalResultState.SUCCESS
&& result.dataSource != null)
{
this.updateDataSourceAsync(result.dataSource)
.handle((voidObj, throwable) ->
{
result.dataSource.close();
return null;
});
}
});
} }
return super.get(pos); return super.get(pos);
@@ -25,7 +25,7 @@ import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSour
import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.file.fullDatafile.IDataSourceUpdateListenerFunc; import com.seibel.distanthorizons.core.file.fullDatafile.IDataSourceUpdateListenerFunc;
import com.seibel.distanthorizons.core.file.structure.ISaveStructure; import com.seibel.distanthorizons.core.file.structure.ISaveStructure;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
@@ -48,7 +48,6 @@ import java.sql.SQLException;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
/** /**
* Handles reading/writing {@link FullDataSourceV2} * Handles reading/writing {@link FullDataSourceV2}
@@ -86,9 +85,9 @@ public class FullDataSourceProviderV2 implements IDebugRenderable, AutoCloseable
protected final String levelId; protected final String levelId;
private final FullDataUpdaterV2 dataUpdater; protected final FullDataUpdaterV2 dataUpdater;
private final FullDataUpdatePropagatorV2 updatePropagator; protected final FullDataUpdatePropagatorV2 updatePropagator;
private final DataMigratorV1 dataMigratorV1; protected final DataMigratorV1 dataMigratorV1;
@@ -201,9 +200,10 @@ public class FullDataSourceProviderV2 implements IDebugRenderable, AutoCloseable
return FullDataSourceV2.createEmpty(pos); return FullDataSourceV2.createEmpty(pos);
} }
FullDataSourceV2 dataSource = null;
try try
{ {
FullDataSourceV2 dataSource = this.createDataSourceFromDto(dto); dataSource = this.createDataSourceFromDto(dto);
// automatically create and save adjacent data if missing // automatically create and save adjacent data if missing
if (dto.dataFormatVersion == FullDataSourceV2DTO.DATA_FORMAT.V1_NO_ADJACENT_DATA) if (dto.dataFormatVersion == FullDataSourceV2DTO.DATA_FORMAT.V1_NO_ADJACENT_DATA)
@@ -222,6 +222,15 @@ public class FullDataSourceProviderV2 implements IDebugRenderable, AutoCloseable
this.tryLogCorruptedDataError(DhSectionPos.toString(pos), e); this.tryLogCorruptedDataError(DhSectionPos.toString(pos), e);
this.repo.deleteWithKey(pos); this.repo.deleteWithKey(pos);
} }
catch (Exception e)
{
if (dataSource != null)
{
dataSource.close();
}
throw e;
}
} }
catch (InterruptedException ignore) { } catch (InterruptedException ignore) { }
catch (IOException e) catch (IOException e)
@@ -243,6 +252,11 @@ public class FullDataSourceProviderV2 implements IDebugRenderable, AutoCloseable
catch (Exception e) catch (Exception e)
{ {
String message = e.getMessage(); String message = e.getMessage();
if (message == null)
{
message = "NULL";
}
if (CORRUPT_DATA_ERRORS_LOGGED.add(message)) if (CORRUPT_DATA_ERRORS_LOGGED.add(message))
{ {
LOGGER.warn("Unexpected error getting: [" + DhSectionPos.toString(pos) + "], this error message will only be logged once, error: [" + message + "].", e); LOGGER.warn("Unexpected error getting: [" + DhSectionPos.toString(pos) + "], this error message will only be logged once, error: [" + message + "].", e);
@@ -360,7 +374,7 @@ public class FullDataSourceProviderV2 implements IDebugRenderable, AutoCloseable
* to the beginning of your override. * to the beginning of your override.
* Otherwise, parent retrieval limits will be ignored. * Otherwise, parent retrieval limits will be ignored.
*/ */
public boolean canQueueRetrieval() public boolean canQueueRetrievalNow()
{ {
// Retrieval shouldn't happen while an unknown number of // Retrieval shouldn't happen while an unknown number of
// legacy data sources are present. // legacy data sources are present.
@@ -369,15 +383,15 @@ public class FullDataSourceProviderV2 implements IDebugRenderable, AutoCloseable
} }
/** /**
* @return null if this provider can't generate any positions and * @return null if this provider can't generate any positions or
* an empty array if all positions were generated * an empty array if all positions were generated
*/ */
@Nullable @Nullable
public LongArrayList getPositionsToRetrieve(Long pos) { return null; } public LongArrayList getPositionsToRetrieve(long pos) { return null; }
/** @return true if the position was queued, false if not */ /** @return null if the position couldn't be queued */
@Nullable @Nullable
public CompletableFuture<WorldGenResult> queuePositionForRetrieval(Long genPos) { return null; } public CompletableFuture<DataSourceRetrievalResult> queuePositionForRetrieval(Long genPos) { return null; }
/** does nothing if the given position isn't present in the queue */ /** does nothing if the given position isn't present in the queue */
public void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf) { } public void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf) { }
@@ -185,7 +185,7 @@ public class FullDataUpdatePropagatorV2 implements IDebugRenderable, AutoCloseab
parentLocked = true; parentLocked = true;
this.dataUpdater.lockedPosSet.add(parentUpdatePos); this.dataUpdater.lockedPosSet.add(parentUpdatePos);
try (FullDataSourceV2 parentDataSource = this.provider.get(parentUpdatePos)) // TODO can we cache anything in memory to speed up the propagation process? Compression/Disk IO is by far the slowest part of this process try (FullDataSourceV2 parentDataSource = this.provider.get(parentUpdatePos))
{ {
// will return null if the file handler is shutting down // will return null if the file handler is shutting down
if (parentDataSource != null) if (parentDataSource != null)
@@ -19,13 +19,11 @@
package com.seibel.distanthorizons.core.generation; package com.seibel.distanthorizons.core.generation;
import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.LodQuadTree; import com.seibel.distanthorizons.core.render.LodQuadTree;
import com.seibel.distanthorizons.core.util.objects.RollingAverage; import com.seibel.distanthorizons.core.util.objects.RollingAverage;
import org.jetbrains.annotations.Nullable;
import java.io.Closeable; import java.io.Closeable;
import java.util.List; import java.util.List;
@@ -92,7 +90,7 @@ public interface IFullDataSourceRetrievalQueue extends Closeable
*/ */
void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf); void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf);
CompletableFuture<WorldGenResult> submitRetrievalTask(long pos, byte requiredDataDetail, IWorldGenTaskTracker tracker); CompletableFuture<DataSourceRetrievalResult> submitRetrievalTask(long pos, byte requiredDataDetail);
@@ -7,7 +7,9 @@ import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider; import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider;
import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.generation.tasks.ERetrievalResultState;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.util.FormatUtil; import com.seibel.distanthorizons.core.util.FormatUtil;
@@ -148,20 +150,22 @@ public class PregenManager
this.fullDataSourceProvider.getAsync(nextSectionPos) this.fullDataSourceProvider.getAsync(nextSectionPos)
.thenAccept(fullDataSource -> .thenAccept(fullDataSource ->
{ {
if (this.fullDataSourceProvider.isFullyGenerated(fullDataSource.columnGenerationSteps)) if (this.fullDataSourceProvider.generationStepsAreFullyGenerated(fullDataSource.columnGenerationSteps))
{ {
this.pendingGenerations.invalidate(fullDataSource.getPos()); this.pendingGenerations.invalidate(fullDataSource.getPos());
} }
else else
{ {
this.fullDataSourceProvider.queuePositionForRetrieval(fullDataSource.getPos()).thenAccept(result -> { this.fullDataSourceProvider.queuePositionForRetrieval(fullDataSource.getPos())
if (!result.success) .whenComplete((DataSourceRetrievalResult result, Throwable throwable) ->
{ {
LOGGER.warn("Failed to generate section " + DhSectionPos.toString(result.pos)); if (throwable != null)
} {
LOGGER.warn("Failed to generate section " + DhSectionPos.toString(result.pos));
this.pendingGenerations.invalidate(result.pos); }
});
this.pendingGenerations.invalidate(result.pos);
});
} }
fullDataSource.close(); fullDataSource.close();
@@ -1,8 +1,8 @@
package com.seibel.distanthorizons.core.generation; package com.seibel.distanthorizons.core.generation;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; import com.seibel.distanthorizons.core.generation.tasks.ERetrievalResultState;
import com.seibel.distanthorizons.core.level.DhClientLevel; import com.seibel.distanthorizons.core.level.DhClientLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.multiplayer.client.AbstractFullDataNetworkRequestQueue; import com.seibel.distanthorizons.core.multiplayer.client.AbstractFullDataNetworkRequestQueue;
@@ -14,10 +14,8 @@ import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.WorldGenUtil; import com.seibel.distanthorizons.core.util.WorldGenUtil;
import com.seibel.distanthorizons.core.util.objects.RollingAverage; import com.seibel.distanthorizons.core.util.objects.RollingAverage;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.*; import java.util.concurrent.*;
public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQueue implements IFullDataSourceRetrievalQueue, IDebugRenderable public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQueue implements IFullDataSourceRetrievalQueue, IDebugRenderable
@@ -54,46 +52,28 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue
public byte highestDataDetail() { return LodUtil.BLOCK_DETAIL_LEVEL; } public byte highestDataDetail() { return LodUtil.BLOCK_DETAIL_LEVEL; }
@Override @Override
public CompletableFuture<WorldGenResult> submitRetrievalTask(long sectionPos, byte requiredDataDetail, IWorldGenTaskTracker tracker) public CompletableFuture<DataSourceRetrievalResult> submitRetrievalTask(long sectionPos, byte requiredDataDetail)
{ {
long generationStartMsTime = System.currentTimeMillis(); long generationStartMsTime = System.currentTimeMillis();
return super.submitRequest(sectionPos, fullDataSource -> { CompletableFuture<DataSourceRetrievalResult> future = super.submitRequest(sectionPos, /* client timestamp */null);
Objects.requireNonNull(tracker.getDataSourceConsumer()).accept(fullDataSource); future.thenAccept((DataSourceRetrievalResult result) ->
fullDataSource.close(); {
}) if (result.state == ERetrievalResultState.SUCCESS)
.thenApply(requestResult -> {
{ long totalGenTimeInMs = System.currentTimeMillis() - generationStartMsTime;
long totalGenTimeInMs = System.currentTimeMillis() - generationStartMsTime;
int chunkWidth = DhSectionPos.getChunkWidth(sectionPos);
int chunkWidth = DhSectionPos.getChunkWidth(sectionPos); int chunkCount = chunkWidth * chunkWidth;
int chunkCount = chunkWidth * chunkWidth; double timePerChunk = (double) totalGenTimeInMs / (double) chunkCount;
double timePerChunk = (double)totalGenTimeInMs / (double)chunkCount;
this.rollingAverageChunkGenTimeInMs.add(timePerChunk); // only add the time on successes
// it won't be a perfect estimate but fails will often come back faster, skewing the time faster
switch (requestResult) this.rollingAverageChunkGenTimeInMs.add(timePerChunk);
{ }
case SUCCEEDED: });
return WorldGenResult.CreateSuccess(sectionPos); return future;
case FAILED:
return WorldGenResult.CreateFail();
case REQUIRES_SPLITTING:
List<CompletableFuture<WorldGenResult>> childFutures = new ArrayList<>(4);
DhSectionPos.forEachChild(sectionPos, childPos -> {
tracker.shouldGenerateSplitChild(childPos).thenAccept(shouldGenerate -> {
if (shouldGenerate)
{
childFutures.add(this.submitRetrievalTask(childPos, requiredDataDetail, tracker));
}
});
});
return WorldGenResult.CreateSplit(childFutures);
}
LodUtil.assertNotReach("Unexpected and unhandled request response result: ["+requestResult+"]");
return WorldGenResult.CreateFail();
});
} }
@Override @Override
@@ -109,7 +89,7 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue
@Override @Override
protected int getRequestRateLimit() { return this.networkState.sessionConfig.getGenerationRequestRateLimit(); } protected int getRequestRateLimit() { return this.networkState.sessionConfig.getGenerationRequestRateLimit(); }
@Override @Override
protected boolean isSectionAllowedToGenerate(long sectionPos, DhBlockPos2D targetPos) protected boolean sectionInAllowedGenerationRadius(long sectionPos, DhBlockPos2D targetPos)
{ {
if (this.networkState.sessionConfig.getGenerationMaxChunkRadius() > 0) if (this.networkState.sessionConfig.getGenerationMaxChunkRadius() > 0)
{ {
@@ -127,12 +107,13 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue
return DhSectionPos.getChebyshevSignedBlockDistance(sectionPos, targetPos) <= this.networkState.sessionConfig.getMaxGenerationRequestDistance() * 16; return DhSectionPos.getChebyshevSignedBlockDistance(sectionPos, targetPos) <= this.networkState.sessionConfig.getMaxGenerationRequestDistance() * 16;
} }
@Override @Override
protected boolean onBeforeRequest(long sectionPos, CompletableFuture<ERequestResult> future) protected boolean onBeforeRequest(long sectionPos, CompletableFuture<DataSourceRetrievalResult> future)
{ {
if (DhSectionPos.getDetailLevel(sectionPos) > DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL // split up large requests if N-sized gen isn't enabled
&& !Config.Server.Experimental.enableNSizedGeneration.get()) if (!Config.Server.Experimental.enableNSizedGeneration.get()
&& DhSectionPos.getDetailLevel(sectionPos) > DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL)
{ {
future.complete(ERequestResult.REQUIRES_SPLITTING); future.complete(DataSourceRetrievalResult.CreateSplit());
return false; return false;
} }
@@ -26,11 +26,8 @@ import com.seibel.distanthorizons.api.objects.data.DhApiChunk;
import com.seibel.distanthorizons.api.objects.data.IDhApiFullDataSource; import com.seibel.distanthorizons.api.objects.data.IDhApiFullDataSource;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.generation.tasks.InProgressWorldGenTaskGroup; import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalTask;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenTask;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenTaskGroup;
import com.seibel.distanthorizons.core.level.IDhServerLevel; import com.seibel.distanthorizons.core.level.IDhServerLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
@@ -52,14 +49,12 @@ import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.world.DhApiWorldProxy; import com.seibel.distanthorizons.core.world.DhApiWorldProxy;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import java.awt.*; import java.awt.*;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.function.Consumer;
public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDebugRenderable public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDebugRenderable
{ {
@@ -71,9 +66,8 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
private final IDhServerLevel level; private final IDhServerLevel level;
/** contains the positions that need to be generated */ /** contains the positions that need to be generated */
private final ConcurrentHashMap<Long, WorldGenTask> waitingTasks = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Long, DataSourceRetrievalTask> waitingTasks = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, DataSourceRetrievalTask> inProgressGenTasksByLodPos = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, InProgressWorldGenTaskGroup> inProgressGenTasksByLodPos = new ConcurrentHashMap<>();
/** largest numerical detail level allowed */ /** largest numerical detail level allowed */
public final byte lowestDataDetail; public final byte lowestDataDetail;
@@ -98,13 +92,14 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
private int estimatedRemainingChunkCount = 0; private int estimatedRemainingChunkCount = 0;
private final RollingAverage rollingAverageChunkGenTimeInMs = new RollingAverage(Runtime.getRuntime().availableProcessors() * 500); private final RollingAverage rollingAverageChunkGenTimeInMs = new RollingAverage(Runtime.getRuntime().availableProcessors() * 500);
public RollingAverage getRollingAverageChunkGenTimeInMs() { return this.rollingAverageChunkGenTimeInMs; } @Override public RollingAverage getRollingAverageChunkGenTimeInMs() { return this.rollingAverageChunkGenTimeInMs; }
//==============// //=============//
// constructors // // constructor //
//==============// //=============//
///region constructor
public WorldGenerationQueue(IDhApiWorldGenerator generator, IDhServerLevel level) public WorldGenerationQueue(IDhApiWorldGenerator generator, IDhServerLevel level)
{ {
@@ -118,20 +113,31 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
LOGGER.info("Created world gen queue"); LOGGER.info("Created world gen queue");
} }
///endregion constructor
//=================//
// world generator // //===============//
// task handling // // task handling //
//=================// //===============//
///region task handling
@Override @Override
public CompletableFuture<WorldGenResult> submitRetrievalTask(long pos, byte requiredDataDetail, IWorldGenTaskTracker tracker) public CompletableFuture<DataSourceRetrievalResult> submitRetrievalTask(long pos, byte requiredDataDetail)
{ {
// the generator is shutting down, don't add new tasks // the generator is shutting down, don't add new tasks
if (this.generatorClosingFuture != null) if (this.generatorClosingFuture != null)
{ {
return CompletableFuture.completedFuture(WorldGenResult.CreateFail()); CompletableFuture<DataSourceRetrievalResult> f = new CompletableFuture<>();
f.completeExceptionally(new CancellationException());
return f;
}
// use the existing task if present
DataSourceRetrievalTask existingGenTask = this.waitingTasks.get(pos);
if (existingGenTask != null)
{
return existingGenTask.future;
} }
@@ -145,13 +151,12 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
requiredDataDetail = this.lowestDataDetail; requiredDataDetail = this.lowestDataDetail;
} }
// Assert that the data at least can fill in 1 single ChunkSizedFullDataAccessor // the request should be at least chunk-sized
LodUtil.assertTrue(DhSectionPos.getDetailLevel(pos) > requiredDataDetail + LodUtil.CHUNK_DETAIL_LEVEL); LodUtil.assertTrue(DhSectionPos.getDetailLevel(pos) > requiredDataDetail + LodUtil.CHUNK_DETAIL_LEVEL);
DataSourceRetrievalTask genTask = new DataSourceRetrievalTask(pos, requiredDataDetail);
CompletableFuture<WorldGenResult> future = new CompletableFuture<>(); this.waitingTasks.put(pos, genTask);
this.waitingTasks.put(pos, new WorldGenTask(pos, requiredDataDetail, tracker, future)); return genTask.future;
return future;
} }
@Override @Override
@@ -161,11 +166,17 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
{ {
if (removeIf.accept(genPos)) if (removeIf.accept(genPos))
{ {
this.waitingTasks.remove(genPos); DataSourceRetrievalTask removedTask = this.waitingTasks.remove(genPos);
if (removedTask != null)
{
// cancel tasks so any waiting future steps can be triggered
removedTask.future.cancel(true);
}
} }
}); });
} }
///endregion task handling
@@ -248,18 +259,27 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
} }
// find the closest task
TaskDistancePair closestTaskPair = this.waitingTasks.reduceEntries(1024, TaskDistancePair closestTaskPair = this.waitingTasks.reduceEntries(1024,
entry -> new TaskDistancePair(entry.getValue(), DhSectionPos.getSectionBBoxPos(entry.getValue().pos).getCenterBlockPos().toPos2D().chebyshevDist(targetPos.toPos2D())), // get the target distance for each task
(TaskDistancePair aTaskPair, TaskDistancePair bTaskPair) -> (aTaskPair.dist < bTaskPair.dist) ? aTaskPair : bTaskPair); (Map.Entry<Long, DataSourceRetrievalTask> entry) ->
{
DataSourceRetrievalTask task = entry.getValue();
int distance = DhSectionPos.getCenterBlockPos(task.pos).chebyshevDist(targetPos);
return new TaskDistancePair(entry.getValue(), distance);
},
// find the closest task
(TaskDistancePair aTaskPair, TaskDistancePair bTaskPair) ->
{
return (aTaskPair.dist < bTaskPair.dist) ? aTaskPair : bTaskPair;
});
if (closestTaskPair == null) if (closestTaskPair == null)
{ {
// FIXME concurrency issue // the waitingTasks was modified while this check was running
return false; return false;
} }
DataSourceRetrievalTask closestTask = closestTaskPair.task;
WorldGenTask closestTask = closestTaskPair.task;
// remove the task we found, we are going to start it and don't want to run it multiple times // remove the task we found, we are going to start it and don't want to run it multiple times
this.waitingTasks.remove(closestTask.pos, closestTask); this.waitingTasks.remove(closestTask.pos, closestTask);
@@ -269,76 +289,69 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
{ {
// detail level is correct for generation, start generation // detail level is correct for generation, start generation
WorldGenTaskGroup closestTaskGroup = new WorldGenTaskGroup(closestTask.pos, (byte)(closestTask.pos - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL)); DataSourceRetrievalTask existingTask = this.inProgressGenTasksByLodPos.get(closestTask.pos);
closestTaskGroup.worldGenTasks.add(closestTask); if (existingTask == null)
if (!this.inProgressGenTasksByLodPos.containsKey(closestTask.pos))
{ {
// no task exists for this position, start one // no task exists for this position, start one
InProgressWorldGenTaskGroup newTaskGroup = new InProgressWorldGenTaskGroup(closestTaskGroup); this.startWorldGenTaskGroup(closestTask);
this.startWorldGenTaskGroup(newTaskGroup);
} }
else else
{ {
// TODO replace the previous inProgress task if one exists // shouldn't normally happen, but if
// Note: Due to concurrency reasons, even if the currently running task is compatible with // we somehow queued the same task twice:
// the newly selected task, we cannot use it, // merge the two futures so they both complete
// as some chunks may have already been written into.
//LOGGER.warn("A task already exists for this position, todo: "+DhSectionPos.toString(closestTask.pos)); existingTask.future.thenApply((DataSourceRetrievalResult result)->
{
closestTask.future.complete(result);
return closestTask.future; // return value ignored
});
existingTask.future.exceptionally((Throwable throwable)->
{
closestTask.future.completeExceptionally(throwable);
return null; // return value ignored
});
} }
// a task has been started
return true;
} }
else else
{ {
// detail level is too high (if the detail level was too low, the generator would've ignored the request), // detail level is too high (if the detail level was too low, the generator would've ignored the request),
// split up the task // split up the task
closestTask.future.complete(DataSourceRetrievalResult.CreateSplit());
// split up the task and add each one to the tree
LinkedList<CompletableFuture<WorldGenResult>> childFutures = new LinkedList<>();
long sectionPos = closestTask.pos;
WorldGenTask finalClosestTask = closestTask;
DhSectionPos.forEachChild(sectionPos, (childDhSectionPos) ->
{
CompletableFuture<WorldGenResult> newFuture = new CompletableFuture<>();
childFutures.add(newFuture);
WorldGenTask newGenTask = new WorldGenTask(childDhSectionPos, DhSectionPos.getDetailLevel(childDhSectionPos), finalClosestTask.taskTracker, newFuture);
this.waitingTasks.put(newGenTask.pos, newGenTask);
});
// send the child futures to the future recipient, to notify them of the new tasks
closestTask.future.complete(WorldGenResult.CreateSplit(childFutures));
// return true so we attempt to generate again
return true;
} }
}
private void startWorldGenTaskGroup(InProgressWorldGenTaskGroup newTaskGroup)
{
byte taskDetailLevel = newTaskGroup.group.dataDetail;
long taskPos = newTaskGroup.group.pos;
LodUtil.assertTrue(taskDetailLevel >= this.highestDataDetail && taskDetailLevel <= this.lowestDataDetail);
int generationRequestChunkWidthCount = BitShiftUtil.powerOfTwo(DhSectionPos.getDetailLevel(taskPos) - taskDetailLevel - 4); // minus 4 is equal to dividing by 16 to convert to chunk scale
// a task has been started or queued,
// queue another task
return true;
}
private boolean canGenerateDetailLevel(byte taskDetailLevel)
{
byte requestedDetailLevel = (byte) (taskDetailLevel - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
return (this.highestDataDetail <= requestedDetailLevel && requestedDetailLevel <= this.lowestDataDetail);
}
private void startWorldGenTaskGroup(DataSourceRetrievalTask worldGenTask)
{
long taskPos = worldGenTask.pos;
LodUtil.assertTrue(
worldGenTask.requestDetailLevel >= this.highestDataDetail
&& worldGenTask.requestDetailLevel <= this.lowestDataDetail,
"World gen task started that isn't within the range that the generator can create.");
long generationStartMsTime = System.currentTimeMillis(); long generationStartMsTime = System.currentTimeMillis();
CompletableFuture<Void> generationFuture = this.startGenerationEvent(taskPos, taskDetailLevel, generationRequestChunkWidthCount, newTaskGroup.group::consumeDataSource); CompletableFuture<FullDataSourceV2> generationFuture = this.startGenerationEvent(worldGenTask);
// calculate generation speed
generationFuture.thenRun(() -> generationFuture.thenRun(() ->
{ {
long totalGenTimeInMs = System.currentTimeMillis() - generationStartMsTime; long totalGenTimeInMs = System.currentTimeMillis() - generationStartMsTime;
int chunkCount = generationRequestChunkWidthCount * generationRequestChunkWidthCount; int chunkCount = worldGenTask.widthInChunks * worldGenTask.widthInChunks;
double timePerChunk = (double)totalGenTimeInMs / (double)chunkCount; double timePerChunk = (double)totalGenTimeInMs / (double)chunkCount;
this.rollingAverageChunkGenTimeInMs.add(timePerChunk); this.rollingAverageChunkGenTimeInMs.add(timePerChunk);
}); });
newTaskGroup.genFuture = generationFuture; generationFuture.handle((FullDataSourceV2 fullDataSource, Throwable exception) ->
LodUtil.assertTrue(newTaskGroup.genFuture != null);
newTaskGroup.genFuture.whenComplete((voidObj, exception) ->
{ {
try try
{ {
@@ -350,157 +363,51 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
LOGGER.error("Error generating data for pos: " + DhSectionPos.toString(taskPos), exception); LOGGER.error("Error generating data for pos: " + DhSectionPos.toString(taskPos), exception);
} }
newTaskGroup.group.worldGenTasks.forEach(worldGenTask -> worldGenTask.future.complete(WorldGenResult.CreateFail())); LodUtil.assertTrue(fullDataSource == null);
worldGenTask.future.completeExceptionally(exception);
} }
else else
{ {
newTaskGroup.group.worldGenTasks.forEach(worldGenTask -> worldGenTask.future.complete(WorldGenResult.CreateSuccess(taskPos))); boolean taskRemoved = this.inProgressGenTasksByLodPos.remove(taskPos, worldGenTask);
LodUtil.assertTrue(taskRemoved, "Unable to find in progress generator task with position ["+DhSectionPos.toString(taskPos)+"]");
worldGenTask.future.complete(DataSourceRetrievalResult.CreateSuccess(taskPos, fullDataSource));
} }
boolean worked = this.inProgressGenTasksByLodPos.remove(taskPos, newTaskGroup);
LodUtil.assertTrue(worked, "Unable to find in progress generator task with position ["+DhSectionPos.toString(taskPos)+"]");
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.error("Unexpected error completing world gen task at pos: ["+DhSectionPos.toString(taskPos)+"].", e); LOGGER.error("Unexpected error completing world gen task at pos: ["+DhSectionPos.toString(taskPos)+"].", e);
worldGenTask.future.completeExceptionally(e);
} }
finally finally
{ {
this.tryQueueNewWorldGenRequestsAsync(); this.tryQueueNewWorldGenRequestsAsync();
} }
return null;
}); });
this.inProgressGenTasksByLodPos.put(taskPos, newTaskGroup);
} }
private CompletableFuture<Void> startGenerationEvent( private CompletableFuture<FullDataSourceV2> startGenerationEvent(DataSourceRetrievalTask task)
long requestPos,
byte targetDataDetail,
int generationRequestChunkWidthCount,
Consumer<FullDataSourceV2> dataSourceConsumer
)
{ {
DhChunkPos chunkPosMin = new DhChunkPos(DhSectionPos.getSectionBBoxPos(requestPos).getCornerBlockPos()); this.inProgressGenTasksByLodPos.put(task.pos, task);
DhChunkPos chunkPosMin = new DhChunkPos(new DhBlockPos2D(DhSectionPos.getMinCornerBlockX(task.pos), DhSectionPos.getMinCornerBlockZ(task.pos)));
EDhApiDistantGeneratorMode generatorMode = Config.Common.WorldGenerator.distantGeneratorMode.get(); EDhApiDistantGeneratorMode generatorMode = Config.Common.WorldGenerator.distantGeneratorMode.get();
EDhApiWorldGeneratorReturnType returnType = this.generator.getReturnType(); EDhApiWorldGeneratorReturnType returnType = this.generator.getReturnType();
switch (returnType) switch (returnType)
{ {
case VANILLA_CHUNKS: case VANILLA_CHUNKS:
{ {
return this.generator.generateChunks( return this.startVanillaChunkGenerationEvent(task, chunkPosMin, generatorMode);
chunkPosMin.getX(), chunkPosMin.getZ(),
generationRequestChunkWidthCount,
targetDataDetail,
generatorMode,
ThreadPoolUtil.getWorldGenExecutor(),
(Object[] generatedObjectArray) ->
{
try
{
IChunkWrapper chunkWrapper = WRAPPER_FACTORY.createChunkWrapper(generatedObjectArray);
// only light the chunk here if necessary,
// lighting before this point is preferred but for potenial legacy API uses this
// check should be done
if (!chunkWrapper.isDhBlockLightingCorrect())
{
ArrayList<IChunkWrapper> nearbyChunkList = new ArrayList<>();
nearbyChunkList.add(chunkWrapper);
byte maxSkyLight = this.level.getLevelWrapper().hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT;
DhLightingEngine.INSTANCE.bakeChunkBlockLighting(chunkWrapper, nearbyChunkList, maxSkyLight);
}
try (FullDataSourceV2 dataSource = LodDataBuilder.createFromChunk(this.level.getLevelWrapper(), chunkWrapper))
{
LodUtil.assertTrue(dataSource != null);
dataSourceConsumer.accept(dataSource);
}
}
catch (ClassCastException e)
{
LOGGER.error("World generator return type incorrect. Error: [" + e.getMessage() + "]. World generator disabled.", e);
Config.Common.WorldGenerator.enableDistantGeneration.set(false);
}
catch (Exception e)
{
LOGGER.error("Unexpected world generator error. Error: [" + e.getMessage() + "]. World generator disabled.", e);
Config.Common.WorldGenerator.enableDistantGeneration.set(false);
}
}
);
} }
case API_CHUNKS: case API_CHUNKS:
{ {
return this.generator.generateApiChunks( return this.startApiChunkGenerationEvent(task, chunkPosMin, generatorMode);
chunkPosMin.getX(), chunkPosMin.getZ(),
generationRequestChunkWidthCount,
targetDataDetail,
generatorMode,
ThreadPoolUtil.getWorldGenExecutor(),
(DhApiChunk dataPoints) ->
{
try(FullDataSourceV2 dataSource = LodDataBuilder.createFromApiChunkData(dataPoints, this.generator.runApiValidation()))
{
dataSourceConsumer.accept(dataSource);
}
catch (DataCorruptedException | IllegalArgumentException e)
{
LOGGER.error("World generator returned a corrupt chunk. Error: [" + e.getMessage() + "]. World generator disabled.", e);
Config.Common.WorldGenerator.enableDistantGeneration.set(false);
}
catch (ClassCastException e)
{
LOGGER.error("World generator return type incorrect. Error: [" + e.getMessage() + "]. World generator disabled.", e);
Config.Common.WorldGenerator.enableDistantGeneration.set(false);
}
}
);
} }
case API_DATA_SOURCES: case API_DATA_SOURCES:
{ {
// done to reduce GC overhead return this.startApiDataSourceGenerationEvent(task, chunkPosMin, generatorMode);
FullDataSourceV2 pooledDataSource = FullDataSourceV2.createEmpty(requestPos);
// set here so the API user doesn't have to pass in this value anywhere themselves
pooledDataSource.setRunApiChunkValidation(this.generator.runApiValidation());
// only apply to children if we aren't at the bottom of the tree
pooledDataSource.applyToChildren = DhSectionPos.getDetailLevel(pooledDataSource.getPos()) > DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL;
pooledDataSource.applyToParent = DhSectionPos.getDetailLevel(pooledDataSource.getPos()) < DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL + 12;
return this.generator.generateLod(
chunkPosMin.getX(), chunkPosMin.getZ(),
DhSectionPos.getX(requestPos), DhSectionPos.getZ(requestPos),
(byte) (DhSectionPos.getDetailLevel(requestPos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL),
pooledDataSource,
generatorMode,
ThreadPoolUtil.getWorldGenExecutor(),
(IDhApiFullDataSource apiDataSource) ->
{
try
{
FullDataSourceV2 fullDataSource = (FullDataSourceV2) apiDataSource;
try
{
dataSourceConsumer.accept(fullDataSource);
}
finally
{
fullDataSource.close();
}
}
catch (IllegalArgumentException e)
{
LOGGER.error("World generator returned a corrupt data source. Error: [" + e.getMessage() + "]. World generator disabled.", e);
Config.Common.WorldGenerator.enableDistantGeneration.set(false);
}
catch (ClassCastException e)
{
LOGGER.error("World generator return type incorrect. Error: [" + e.getMessage() + "]. World generator disabled.", e);
Config.Common.WorldGenerator.enableDistantGeneration.set(false);
}
}
);
} }
default: default:
{ {
@@ -509,30 +416,181 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
} }
} }
} }
private CompletableFuture<FullDataSourceV2> startVanillaChunkGenerationEvent(
DataSourceRetrievalTask task, DhChunkPos chunkPosMin, EDhApiDistantGeneratorMode generatorMode)
{
final CompletableFuture<FullDataSourceV2> returnFuture = new CompletableFuture<>();
ArrayList<IChunkWrapper> generatedChunks = new ArrayList<>(task.widthInChunks * task.widthInChunks);
CompletableFuture<Void> chunkGenFuture = this.generator.generateChunks(
chunkPosMin.getX(), chunkPosMin.getZ(),
task.widthInChunks,
task.requestDetailLevel,
generatorMode,
ThreadPoolUtil.getWorldGenExecutor(),
(Object[] generatedObjectArray) ->
{
try
{
IChunkWrapper chunkWrapper = WRAPPER_FACTORY.createChunkWrapper(generatedObjectArray);
generatedChunks.add(chunkWrapper);
}
catch (ClassCastException e)
{
LOGGER.error("World generator return type incorrect. Error: [" + e.getMessage() + "]. World generator disabled.", e);
Config.Common.WorldGenerator.enableDistantGeneration.set(false);
}
catch (Exception e)
{
LOGGER.error("Unexpected world generator error. Error: [" + e.getMessage() + "]. World generator disabled.", e);
Config.Common.WorldGenerator.enableDistantGeneration.set(false);
}
}
);
chunkGenFuture.exceptionally((throwable) ->
{
returnFuture.completeExceptionally(throwable);
return null;
});
chunkGenFuture.thenRun(() ->
{
FullDataSourceV2 requestedDataSource = FullDataSourceV2.createEmpty(task.pos);
// process chunks //
for (int i = 0; i < generatedChunks.size(); i++)
{
IChunkWrapper chunkWrapper = generatedChunks.get(i);
// only light the chunk here if necessary,
// lighting before this point is preferred but for legacy API use this
// check should be done
if (!chunkWrapper.isDhBlockLightingCorrect())
{
ArrayList<IChunkWrapper> nearbyChunkList = new ArrayList<>();
nearbyChunkList.add(chunkWrapper);
byte maxSkyLight = this.level.getLevelWrapper().hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT;
DhLightingEngine.INSTANCE.bakeChunkBlockLighting(chunkWrapper, nearbyChunkList, maxSkyLight);
}
try (FullDataSourceV2 generatedDataSource = LodDataBuilder.createFromChunk(this.level.getLevelWrapper(), chunkWrapper))
{
LodUtil.assertTrue(generatedDataSource != null);
requestedDataSource.updateFromDataSource(generatedDataSource);
}
}
DhLightingEngine.INSTANCE.bakeDataSourceSkyLight(requestedDataSource, LodUtil.MAX_MC_LIGHT);
returnFuture.complete(requestedDataSource);
});
return returnFuture;
}
private CompletableFuture<FullDataSourceV2> startApiChunkGenerationEvent(
DataSourceRetrievalTask task, DhChunkPos chunkPosMin, EDhApiDistantGeneratorMode generatorMode)
{
final CompletableFuture<FullDataSourceV2> returnFuture = new CompletableFuture<>();
ArrayList<DhApiChunk> generatedChunks = new ArrayList<>(task.widthInChunks * task.widthInChunks);
CompletableFuture<Void> chunkGenFuture = this.generator.generateApiChunks(
chunkPosMin.getX(), chunkPosMin.getZ(),
task.widthInChunks,
task.requestDetailLevel,
generatorMode,
ThreadPoolUtil.getWorldGenExecutor(),
(DhApiChunk apiChunk) -> { generatedChunks.add(apiChunk); }
);
chunkGenFuture.exceptionally((throwable) ->
{
returnFuture.completeExceptionally(throwable);
return null;
});
chunkGenFuture.thenRun(() ->
{
FullDataSourceV2 requestedDataSource = FullDataSourceV2.createEmpty(task.pos);
for (int i = 0; i < generatedChunks.size(); i++)
{
DhApiChunk apiChunk = generatedChunks.get(i);
try(FullDataSourceV2 generatedDataSource = LodDataBuilder.createFromApiChunkData(apiChunk, this.generator.runApiValidation()))
{
requestedDataSource.updateFromDataSource(generatedDataSource);
}
catch (DataCorruptedException | IllegalArgumentException e)
{
LOGGER.error("World generator returned a corrupt API chunk. Error: [" + e.getMessage() + "]. World generator disabled.", e);
Config.Common.WorldGenerator.enableDistantGeneration.set(false);
}
}
returnFuture.complete(requestedDataSource);
});
return returnFuture;
}
private CompletableFuture<FullDataSourceV2> startApiDataSourceGenerationEvent(
DataSourceRetrievalTask task, DhChunkPos chunkPosMin, EDhApiDistantGeneratorMode generatorMode)
{
final CompletableFuture<FullDataSourceV2> returnFuture = new CompletableFuture<>();
// done to reduce GC overhead
FullDataSourceV2 pooledDataSource = FullDataSourceV2.createEmpty(task.pos);
// set here so the API user doesn't have to pass in this value anywhere themselves
pooledDataSource.setRunApiSetterValidation(this.generator.runApiValidation());
// only apply to children if we aren't at the bottom of the tree
pooledDataSource.applyToChildren = DhSectionPos.getDetailLevel(pooledDataSource.getPos()) > DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL;
pooledDataSource.applyToParent = DhSectionPos.getDetailLevel(pooledDataSource.getPos()) < DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL + 12; // TODO what does this 12 reference?
CompletableFuture<Void> lodGenFuture = this.generator.generateLod(
chunkPosMin.getX(), chunkPosMin.getZ(),
DhSectionPos.getX(task.pos), DhSectionPos.getZ(task.pos),
(byte) (DhSectionPos.getDetailLevel(task.pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL),
pooledDataSource,
generatorMode,
ThreadPoolUtil.getWorldGenExecutor(),
(IDhApiFullDataSource apiDataSource) -> { }
);
lodGenFuture.exceptionally((throwable) ->
{
returnFuture.completeExceptionally(throwable);
pooledDataSource.close();
return null;
});
lodGenFuture.thenRun(() ->
{
returnFuture.complete(pooledDataSource);
});
return returnFuture;
}
//===================// //===================//
// getters / setters // // getters / setters //
//===================// //===================//
///region getters/setters
@Override public int getWaitingTaskCount() { return this.waitingTasks.size(); } @Override public int getWaitingTaskCount() { return this.waitingTasks.size(); }
@Override public int getInProgressTaskCount() { return this.inProgressGenTasksByLodPos.size(); } @Override public int getInProgressTaskCount() { return this.inProgressGenTasksByLodPos.size(); }
@Override @Override public byte lowestDataDetail() { return this.lowestDataDetail; }
public byte lowestDataDetail() { return this.lowestDataDetail; } @Override public byte highestDataDetail() { return this.highestDataDetail; }
@Override
public byte highestDataDetail() { return this.highestDataDetail; }
@Override @Override public int getEstimatedRemainingTaskCount() { return this.estimatedRemainingTaskCount; }
public int getEstimatedRemainingTaskCount() { return this.estimatedRemainingTaskCount; } @Override public void setEstimatedRemainingTaskCount(int newEstimate) { this.estimatedRemainingTaskCount = newEstimate; }
@Override
public void setEstimatedRemainingTaskCount(int newEstimate) { this.estimatedRemainingTaskCount = newEstimate; }
@Override @Override public int getRetrievalEstimatedRemainingChunkCount() { return this.estimatedRemainingChunkCount; }
public int getRetrievalEstimatedRemainingChunkCount() { return this.estimatedRemainingChunkCount; } @Override public void setRetrievalEstimatedRemainingChunkCount(int newEstimate) { this.estimatedRemainingChunkCount = newEstimate; }
@Override
public void setRetrievalEstimatedRemainingChunkCount(int newEstimate) { this.estimatedRemainingChunkCount = newEstimate; }
@Override @Override
public void addDebugMenuStringsToList(List<String> messageList) { } public void addDebugMenuStringsToList(List<String> messageList) { }
@@ -550,13 +608,55 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
return chunkCount; return chunkCount;
} }
///endregion getters/setters
//=======//
// debug //
//=======//
///region debug
@Override
public void debugRender(DebugRenderer renderer)
{
int levelMinY = this.level.getLevelWrapper().getMinHeight();
int levelMaxY = this.level.getLevelWrapper().getMaxHeight();
// show the wireframe a bit lower than world max height,
// since most worlds don't render all the way up to the max height
int levelHeightRange = (levelMaxY - levelMinY);
int maxY = levelMaxY - (levelHeightRange / 2);
// blue - queued
this.waitingTasks.keySet().forEach((Long pos) ->
{
renderer.renderBox(
new DebugRenderer.Box(pos, levelMinY, maxY, 0.05f, Color.blue)
);
});
// red - in progress
this.inProgressGenTasksByLodPos.forEach((Long pos, DataSourceRetrievalTask task) ->
{
renderer.renderBox(
new DebugRenderer.Box(pos, levelMinY, maxY, 0.05f, Color.red)
);
});
}
///endregion debug
//==========// //==========//
// shutdown // // shutdown //
//==========// //==========//
///region shutdown
@Override public CompletableFuture<Void> startClosingAsync(boolean cancelCurrentGeneration, boolean alsoInterruptRunning) @Override
public CompletableFuture<Void> startClosingAsync(boolean cancelCurrentGeneration, boolean alsoInterruptRunning)
{ {
LOGGER.info("Closing world gen queue"); LOGGER.info("Closing world gen queue");
this.queueingThread.shutdownNow(); this.queueingThread.shutdownNow();
@@ -564,33 +664,32 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
// stop and remove any in progress tasks // stop and remove any in progress tasks
ArrayList<CompletableFuture<Void>> inProgressTasksCancelingFutures = new ArrayList<>(this.inProgressGenTasksByLodPos.size()); ArrayList<CompletableFuture<Void>> inProgressTasksCancelingFutures = new ArrayList<>(this.inProgressGenTasksByLodPos.size());
this.inProgressGenTasksByLodPos.values().forEach(runningTaskGroup -> this.inProgressGenTasksByLodPos.values().forEach((DataSourceRetrievalTask genTask) ->
{ {
CompletableFuture<Void> genFuture = runningTaskGroup.genFuture; // Do this to prevent it getting swapped out CompletableFuture<DataSourceRetrievalResult> genFuture = genTask.future;
if (genFuture == null)
{
// genFuture's shouldn't be null, but sometimes they are...
LOGGER.info("Null gen future: "+runningTaskGroup.group.pos);
return;
}
if (cancelCurrentGeneration) if (cancelCurrentGeneration)
{ {
genFuture.cancel(alsoInterruptRunning); genFuture.cancel(alsoInterruptRunning);
} }
inProgressTasksCancelingFutures.add(genFuture.handle((voidObj, exception) -> inProgressTasksCancelingFutures.add(genFuture.handle((DataSourceRetrievalResult result, Throwable throwable) ->
{ {
if (exception instanceof CompletionException) if (throwable instanceof CompletionException)
{ {
exception = exception.getCause(); throwable = throwable.getCause();
} }
if (!UncheckedInterruptedException.isInterrupt(exception) if (!UncheckedInterruptedException.isInterrupt(throwable)
&& !(exception instanceof CancellationException)) && !(throwable instanceof CancellationException))
{ {
LOGGER.error("Error when terminating data generation for pos: ["+DhSectionPos.toString(runningTaskGroup.group.pos)+"], error: ["+exception.getMessage()+"].", exception); LOGGER.error("Error when terminating data generation for pos: ["+DhSectionPos.toString(genTask.pos)+"], error: ["+throwable.getMessage()+"].", throwable);
}
if (result != null
&& result.dataSource != null)
{
result.dataSource.close();
} }
return null; return null;
@@ -623,7 +722,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
LOGGER.info("World generator thread pool shutdown with [" + queueSize + "] incomplete tasks."); LOGGER.info("World generator thread pool shutdown with [" + queueSize + "] incomplete tasks.");
} }
this.inProgressGenTasksByLodPos.values().forEach((inProgressWorldGenTaskGroup) -> inProgressWorldGenTaskGroup.genFuture.cancel(true)); this.inProgressGenTasksByLodPos.values().forEach((inProgressWorldGenTaskGroup) -> inProgressWorldGenTaskGroup.future.cancel(true));
this.waitingTasks.values().forEach((worldGenTask) -> worldGenTask.future.cancel(true)); this.waitingTasks.values().forEach((worldGenTask) -> worldGenTask.future.cancel(true));
@@ -644,63 +743,22 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
LOGGER.info("Finished closing " + WorldGenerationQueue.class.getSimpleName()); LOGGER.info("Finished closing " + WorldGenerationQueue.class.getSimpleName());
} }
///endregion shutdown
//=======//
// debug //
//=======//
@Override
public void debugRender(DebugRenderer renderer)
{
int levelMinY = this.level.getLevelWrapper().getMinHeight();
int levelMaxY = this.level.getLevelWrapper().getMaxHeight();
// show the wireframe a bit lower than world max height,
// since most worlds don't render all the way up to the max height
int levelHeightRange = (levelMaxY - levelMinY);
int maxY = levelMaxY - (levelHeightRange / 2);
// blue - queued
this.waitingTasks.keySet().forEach((pos) ->
{
renderer.renderBox(
new DebugRenderer.Box(pos, levelMinY, maxY, 0.05f, Color.blue));
});
// red - in progress
this.inProgressGenTasksByLodPos.forEach((pos, t) ->
{
renderer.renderBox(
new DebugRenderer.Box(pos, levelMinY, maxY, 0.05f, Color.red));
});
}
//================//
// helper methods //
//================//
private boolean canGenerateDetailLevel(byte taskDetailLevel)
{
byte requestedDetailLevel = (byte) (taskDetailLevel - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
return (this.highestDataDetail <= requestedDetailLevel && requestedDetailLevel <= this.lowestDataDetail);
}
//================// //================//
// helper classes // // helper classes //
//================// //================//
///region helper classes
/** Used during task starting to determine the closest task */
private static class TaskDistancePair private static class TaskDistancePair
{ {
public final WorldGenTask task; public final DataSourceRetrievalTask task;
public final int dist; public final int dist;
public TaskDistancePair(WorldGenTask task, int dist) public TaskDistancePair(DataSourceRetrievalTask task, int dist)
{ {
this.task = task; this.task = task;
this.dist = dist; this.dist = dist;
@@ -708,4 +766,8 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
} }
///endregion helper classes
} }
@@ -20,20 +20,33 @@
package com.seibel.distanthorizons.core.generation.tasks; package com.seibel.distanthorizons.core.generation.tasks;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import org.jetbrains.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
/** /**
* @author Leetom * @see DataSourceRetrievalTask
* @version 2022-11-25
*/ */
public interface IWorldGenTaskTracker public class DataSourceRetrievalResult
{ {
public final ERetrievalResultState state;
/** the position that was generated, will be null if nothing was generated */
public final long pos;
@Nullable @Nullable
Consumer<FullDataSourceV2> getDataSourceConsumer(); public final FullDataSourceV2 dataSource;
//==============//
// constructors //
//==============//
public static DataSourceRetrievalResult CreateSplit() { return new DataSourceRetrievalResult(ERetrievalResultState.REQUIRES_SPLITTING, 0, null); }
public static DataSourceRetrievalResult CreateSuccess(long pos, FullDataSourceV2 generatedDataSource) { return new DataSourceRetrievalResult(ERetrievalResultState.SUCCESS, pos, generatedDataSource); }
private DataSourceRetrievalResult(ERetrievalResultState state, long pos, @Nullable FullDataSourceV2 dataSource)
{
this.state = state;
this.pos = pos;
this.dataSource = dataSource;
}
CompletableFuture<Boolean> shouldGenerateSplitChild(long pos);
} }
@@ -19,29 +19,37 @@
package com.seibel.distanthorizons.core.generation.tasks; package com.seibel.distanthorizons.core.generation.tasks;
import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
/** /**
* @author Leetom * @see DataSourceRetrievalResult
* @version 2022-11-25
*/ */
public final class WorldGenTask public final class DataSourceRetrievalTask
{ {
public final long pos; public final long pos;
public final byte dataDetailLevel; /**
public final IWorldGenTaskTracker taskTracker; * Usually the same as {@link DataSourceRetrievalTask#pos}, but
public final CompletableFuture<WorldGenResult> future; * can differ if the task needs something different.
*/
public final byte requestDetailLevel;
public final int widthInChunks;
public final CompletableFuture<DataSourceRetrievalResult> future = new CompletableFuture<>();
public WorldGenTask(long pos, byte dataDetail, IWorldGenTaskTracker taskTracker, CompletableFuture<WorldGenResult> future) //=============//
// constructor //
//=============//
public DataSourceRetrievalTask(long pos, byte dataDetail)
{ {
this.dataDetailLevel = dataDetail;
this.pos = pos; this.pos = pos;
this.taskTracker = taskTracker; this.requestDetailLevel = dataDetail;
this.future = future; this.widthInChunks = BitShiftUtil.powerOfTwo(DhSectionPos.getDetailLevel(this.pos) - this.requestDetailLevel - 4); // minus 4 is equal to dividing by 16 to convert to chunk scale
} }
} }
@@ -0,0 +1,13 @@
package com.seibel.distanthorizons.core.generation.tasks;
/**
* SUCCESS <br>
* REQUIRES_SPLITTING <br>
*
* @see DataSourceRetrievalResult
*/
public enum ERetrievalResultState
{
SUCCESS,
REQUIRES_SPLITTING,
}
@@ -1,51 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 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 <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.generation.tasks;
import java.util.Collection;
import java.util.LinkedList;
import java.util.concurrent.CompletableFuture;
public class WorldGenResult
{
/** true if terrain was generated */
public final boolean success;
/** the position that was generated, will be null if nothing was generated */
public final long pos;
/** if a position is too high detail for world generator to handle it, these futures are for its 4 children positions after being split up. */
public final LinkedList<CompletableFuture<WorldGenResult>> childFutures = new LinkedList<>();
public static WorldGenResult CreateSplit(Collection<CompletableFuture<WorldGenResult>> siblingFutures) { return new WorldGenResult(false, 0, siblingFutures); }
public static WorldGenResult CreateFail() { return new WorldGenResult(false, 0, null); }
public static WorldGenResult CreateSuccess(long pos) { return new WorldGenResult(true, pos, null); }
private WorldGenResult(boolean success, long pos, Collection<CompletableFuture<WorldGenResult>> childFutures)
{
this.success = success;
this.pos = pos;
if (childFutures != null)
{
this.childFutures.addAll(childFutures);
}
}
}
@@ -1,67 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 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 <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.generation.tasks;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.function.Consumer;
/**
* @author Leetom
* @version 2022-11-25
*/
@Deprecated // TODO look into how these are used and if they should continue to be used
public final class WorldGenTaskGroup
{
public final long pos;
public byte dataDetail;
/** Only accessed by the generator polling thread */
public final LinkedList<WorldGenTask> worldGenTasks = new LinkedList<>();
public WorldGenTaskGroup(long pos, byte dataDetail)
{
this.pos = pos;
this.dataDetail = dataDetail;
}
public void consumeDataSource(FullDataSourceV2 dataSource)
{
Iterator<WorldGenTask> tasks = this.worldGenTasks.iterator();
while (tasks.hasNext())
{
WorldGenTask task = tasks.next();
Consumer<FullDataSourceV2> dataSourceConsumer = task.taskTracker.getDataSourceConsumer();
if (dataSourceConsumer == null)
{
tasks.remove();
task.future.complete(WorldGenResult.CreateFail());
}
else
{
dataSourceConsumer.accept(dataSource);
}
}
}
}
@@ -104,7 +104,7 @@ public class JarUtils
*/ */
public static InputStream accessFile(String resource) public static InputStream accessFile(String resource)
{ {
final ClassLoader loader = Thread.currentThread().getContextClassLoader(); final ClassLoader loader = JarUtils.class.getClassLoader();
// this is the path within the jar file // this is the path within the jar file
InputStream input = loader.getResourceAsStream(resource); InputStream input = loader.getResourceAsStream(resource);
if (input == null) if (input == null)
@@ -192,23 +192,30 @@ public abstract class AbstractDhLevel implements IDhLevel
return this.updateDataSourcesAsync(fullDataSource) return this.updateDataSourcesAsync(fullDataSource)
.thenRun(() -> .thenRun(() ->
{ {
HashSet<DhChunkPos> updatedChunkPosSet = this.updatedChunkPosSetBySectionPos.remove(fullDataSource.getPos()); try
if (updatedChunkPosSet != null)
{ {
for (DhChunkPos chunkPos : updatedChunkPosSet) HashSet<DhChunkPos> updatedChunkPosSet = this.updatedChunkPosSetBySectionPos.remove(fullDataSource.getPos());
if (updatedChunkPosSet != null)
{ {
// save after the data source has been updated to prevent saving the hash without the associated datasource for (DhChunkPos chunkPos : updatedChunkPosSet)
Integer chunkHash = this.updatedChunkHashesByChunkPos.remove(chunkPos);
if (this.chunkHashRepo != null && chunkHash != null)
{ {
this.chunkHashRepo.save(new ChunkHashDTO(chunkPos, chunkHash)); // save after the data source has been updated to prevent saving the hash without the associated datasource
} Integer chunkHash = this.updatedChunkHashesByChunkPos.remove(chunkPos);
if (this.chunkHashRepo != null && chunkHash != null)
ApiEventInjector.INSTANCE.fireAllEvents( {
this.chunkHashRepo.save(new ChunkHashDTO(chunkPos, chunkHash));
}
ApiEventInjector.INSTANCE.fireAllEvents(
DhApiChunkModifiedEvent.class, DhApiChunkModifiedEvent.class,
new DhApiChunkModifiedEvent.EventParam(this.getLevelWrapper(), chunkPos.getX(), chunkPos.getZ())); new DhApiChunkModifiedEvent.EventParam(this.getLevelWrapper(), chunkPos.getX(), chunkPos.getZ()));
}
} }
} }
catch (Exception e)
{
LOGGER.error("Unexpected issue after onDataSourceSaveAsync, error: ["+e.getMessage()+"].", e);
}
}); });
} }
@@ -102,13 +102,12 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I
{ return Config.Common.WorldGenerator.enableDistantGeneration.get() && !this.worldGenPlayerCenteringQueue.isEmpty(); } { return Config.Common.WorldGenerator.enableDistantGeneration.get() && !this.worldGenPlayerCenteringQueue.isEmpty(); }
@Override @Override
@Nullable
public DhBlockPos2D getTargetPosForGeneration() public DhBlockPos2D getTargetPosForGeneration()
{ {
IServerPlayerWrapper firstPlayer = this.worldGenPlayerCenteringQueue.peek(); IServerPlayerWrapper firstPlayer = this.worldGenPlayerCenteringQueue.peek();
if (firstPlayer == null) if (firstPlayer == null)
{ {
return null; return DhBlockPos2D.ZERO;
} }
// Put first player in back before removing from front, so it can be removed by other thread without blocking // Put first player in back before removing from front, so it can be removed by other thread without blocking
@@ -247,7 +247,6 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
} }
@Override @Override
@Nullable
public DhBlockPos2D getTargetPosForGeneration() { return new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos()); } public DhBlockPos2D getTargetPosForGeneration() { return new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos()); }
@@ -259,13 +258,6 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
@Override @Override
public void onWorldGenTaskComplete(long pos) public void onWorldGenTaskComplete(long pos)
{ {
DebugRenderer.makeParticle(
new DebugRenderer.BoxParticle(
new DebugRenderer.Box(pos, 128f, 156f, 0.09f, Color.red.darker()),
0.2, 32f
)
);
this.clientside.reloadPos(pos); this.clientside.reloadPos(pos);
} }
@@ -379,9 +371,9 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
private static class LodRequestState extends LodRequestModule.AbstractLodRequestState private static class LodRequestState extends LodRequestModule.AbstractLodRequestState
{ {
LodRequestState(DhClientLevel level, ClientNetworkState networkState) LodRequestState(DhClientLevel clientLevel, ClientNetworkState networkState)
{ {
this.retrievalQueue = new RemoteWorldRetrievalQueue(networkState, level); this.retrievalQueue = new RemoteWorldRetrievalQueue(networkState, clientLevel);
} }
} }
@@ -132,14 +132,6 @@ public class DhClientServerLevel extends AbstractDhServerLevel implements IDhCli
public void onWorldGenTaskComplete(long pos) public void onWorldGenTaskComplete(long pos)
{ {
super.onWorldGenTaskComplete(pos); super.onWorldGenTaskComplete(pos);
DebugRenderer.makeParticle(
new DebugRenderer.BoxParticle(
new DebugRenderer.Box(pos, 128f, 156f, 0.09f, Color.red.darker()),
0.2, 32f
)
);
this.clientside.reloadPos(pos); this.clientside.reloadPos(pos);
} }
@@ -56,7 +56,7 @@ public class DhServerLevel extends AbstractDhServerLevel
return true; //todo; return true; //todo;
} }
@Override @Override
public @Nullable DhBlockPos2D getTargetPosForGeneration() public DhBlockPos2D getTargetPosForGeneration()
{ {
DhBlockPos2D targetPos = super.getTargetPosForGeneration(); DhBlockPos2D targetPos = super.getTargetPosForGeneration();
if (targetPos == null) if (targetPos == null)
@@ -24,6 +24,7 @@ import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener; import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
import com.seibel.distanthorizons.core.config.types.ConfigEntry; import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.MinecraftTextFormat;
import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
@@ -302,32 +303,32 @@ public class DhLogger implements IConfigListener
String prefix = "[" + ModInfo.READABLE_NAME + "] "; String prefix = "[" + ModInfo.READABLE_NAME + "] ";
if (logLevel == Level.ERROR) if (logLevel == Level.ERROR)
{ {
prefix += "\u00A74"; prefix += MinecraftTextFormat.DARK_RED;
} }
else if (logLevel == Level.WARN) else if (logLevel == Level.WARN)
{ {
prefix += "\u00A76"; prefix += MinecraftTextFormat.ORANGE;
} }
else if (logLevel == Level.INFO) else if (logLevel == Level.INFO)
{ {
prefix += "\u00A7f"; prefix += MinecraftTextFormat.AQUA;
} }
else if (logLevel == Level.DEBUG) else if (logLevel == Level.DEBUG)
{ {
prefix += "\u00A77"; prefix += MinecraftTextFormat.GREEN;
} }
else if (logLevel == Level.TRACE) else if (logLevel == Level.TRACE)
{ {
prefix += "\u00A78"; prefix += MinecraftTextFormat.DARK_GRAY;
} }
else else
{ {
prefix += "\u00A7f"; prefix += MinecraftTextFormat.WHITE;
} }
prefix += "\u00A7l\u00A7u"; prefix += MinecraftTextFormat.BOLD + "" + MinecraftTextFormat.WHITE;
prefix += logLevel.name(); prefix += logLevel.name();
prefix += ":\u00A7r "; prefix += MinecraftTextFormat.CLEAR_FORMATTING + " ";
mc_client.sendChatMessage(prefix + message); mc_client.sendChatMessage(prefix + message);
} }
@@ -6,6 +6,7 @@ import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.types.ConfigEntry; import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.level.DhClientLevel; import com.seibel.distanthorizons.core.level.DhClientLevel;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
@@ -33,9 +34,7 @@ import java.awt.*;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRenderable, AutoCloseable public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRenderable, AutoCloseable
{ {
@@ -58,7 +57,7 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
private volatile CompletableFuture<Void> closingFuture = null; private volatile CompletableFuture<Void> closingFuture = null;
protected final ConcurrentMap<Long, RequestQueueEntry> waitingTasksBySectionPos = new ConcurrentHashMap<>(); protected final ConcurrentMap<Long, NetRequestTask> waitingTasksBySectionPos = new ConcurrentHashMap<>();
/** /**
* This semaphore prevents a given thread from accidentally locking on the same group * This semaphore prevents a given thread from accidentally locking on the same group
* multiple times, as the semaphore is tied to the given thread. <br> * multiple times, as the semaphore is tied to the given thread. <br>
@@ -74,16 +73,6 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
private final SupplierBasedRateLimiter<Void> rateLimiter = new SupplierBasedRateLimiter<>(this::getRequestRateLimit); private final SupplierBasedRateLimiter<Void> rateLimiter = new SupplierBasedRateLimiter<>(this::getRequestRateLimit);
private final Set<Long> succeededPositions = Collections.newSetFromMap(CacheBuilder.newBuilder()
.expireAfterWrite(20, TimeUnit.MINUTES)
.<Long, Boolean>build()
.asMap());
private final Set<Long> requiresSplittingPositions = Collections.newSetFromMap(CacheBuilder.newBuilder()
.expireAfterWrite(20, TimeUnit.MINUTES)
.<Long, Boolean>build()
.asMap());
//=============// //=============//
@@ -108,8 +97,8 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
//==================// //==================//
protected abstract int getRequestRateLimit(); protected abstract int getRequestRateLimit();
protected abstract boolean isSectionAllowedToGenerate(long sectionPos, DhBlockPos2D targetPos); protected abstract boolean sectionInAllowedGenerationRadius(long sectionPos, DhBlockPos2D targetPos);
protected abstract boolean onBeforeRequest(long sectionPos, CompletableFuture<ERequestResult> future); protected abstract boolean onBeforeRequest(long sectionPos, CompletableFuture<DataSourceRetrievalResult> future);
protected abstract String getQueueName(); protected abstract String getQueueName();
@@ -119,74 +108,57 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
// request submitting // // request submitting //
//====================// //====================//
public CompletableFuture<ERequestResult> submitRequest(long sectionPos, Consumer<FullDataSourceV2> dataSourceConsumer) public CompletableFuture<DataSourceRetrievalResult> submitRequest(long sectionPos, @Nullable Long clientTimestamp)
{ return this.submitRequest(sectionPos, null, dataSourceConsumer); }
public CompletableFuture<ERequestResult> submitRequest(long sectionPos, @Nullable Long clientTimestamp, Consumer<FullDataSourceV2> dataSourceConsumer)
{ {
if (this.succeededPositions.contains(sectionPos)) NetRequestTask requestEntry = this.waitingTasksBySectionPos.compute(sectionPos, (Long pos, NetRequestTask existingNetTask) ->
{ {
return CompletableFuture.completedFuture(ERequestResult.FAILED); // ignore already queued tasks
} if (existingNetTask != null)
if (this.requiresSplittingPositions.contains(sectionPos))
{
return CompletableFuture.completedFuture(ERequestResult.REQUIRES_SPLITTING);
}
AtomicBoolean added = new AtomicBoolean(false);
RequestQueueEntry entry = this.waitingTasksBySectionPos.compute(sectionPos, (pos, existingQueueEntry) ->
{
if (existingQueueEntry != null)
{ {
return existingQueueEntry; return existingNetTask;
} }
RequestQueueEntry newEntry = new RequestQueueEntry(dataSourceConsumer, clientTimestamp);
newEntry.future.whenComplete((requestResult, throwable) -> NetRequestTask newRequestEntry = new NetRequestTask(pos, clientTimestamp);
newRequestEntry.future.whenComplete((DataSourceRetrievalResult requestResult, Throwable throwable) ->
{ {
this.waitingTasksBySectionPos.remove(sectionPos); this.waitingTasksBySectionPos.remove(pos);
switch (requestResult) if (throwable != null)
{ {
case SUCCEEDED: if (!(throwable instanceof CancellationException))
this.finishedRequests.incrementAndGet(); {
this.succeededPositions.add(pos);
return;
case REQUIRES_SPLITTING:
this.requiresSplittingPositions.add(sectionPos);
return;
case FAILED:
this.failedRequests.incrementAndGet(); this.failedRequests.incrementAndGet();
return; }
default: return;
if (throwable != null && !(throwable instanceof CancellationException)) }
{
this.failedRequests.incrementAndGet(); switch (requestResult.state)
} {
case SUCCESS:
this.finishedRequests.incrementAndGet();
break;
case REQUIRES_SPLITTING:
break; break;
} }
}); });
added.set(true); return newRequestEntry;
return newEntry;
}); });
if (!added.get()) return requestEntry.future;
{
return CompletableFuture.completedFuture(ERequestResult.FAILED);
}
return entry.future;
} }
public synchronized boolean tick(DhBlockPos2D targetPos) public synchronized boolean tick(DhBlockPos2D targetPos)
{ {
if (DhApiWorldProxy.INSTANCE.worldLoaded() && DhApiWorldProxy.INSTANCE.getReadOnly()) if (DhApiWorldProxy.INSTANCE.worldLoaded()
&& DhApiWorldProxy.INSTANCE.getReadOnly())
{ {
return false; return false;
} }
if (this.closingFuture != null || !this.networkState.isReady()) if (this.closingFuture != null
|| !this.networkState.isReady())
{ {
return false; return false;
} }
@@ -209,145 +181,132 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
} }
private void sendNextRequest(DhBlockPos2D targetPos) private void sendNextRequest(DhBlockPos2D targetPos)
{ {
Map.Entry<Long, RequestQueueEntry> mapEntry = this.waitingTasksBySectionPos.entrySet().stream() Map.Entry<Long, NetRequestTask> nearestMapEntry = this.waitingTasksBySectionPos
.filter(task -> task.getValue().networkDataSourceFuture == null) .entrySet().stream()
.min(Comparator.comparingInt(x -> DhSectionPos.getChebyshevSignedBlockDistance(x.getKey(), targetPos))) .filter(task -> task.getValue().networkDataSourceFuture == null)
.orElse(null); .min(Comparator.comparingInt(mapEntry -> DhSectionPos.getChebyshevSignedBlockDistance(mapEntry.getKey(), targetPos)))
.orElse(null);
if (mapEntry == null) if (nearestMapEntry == null)
{ {
this.pendingTasksSemaphore.release(); this.pendingTasksSemaphore.release();
return; return;
} }
long sectionPos = mapEntry.getKey(); long requestPos = nearestMapEntry.getKey();
RequestQueueEntry entry = mapEntry.getValue(); NetRequestTask requestTask = nearestMapEntry.getValue();
if (!this.isSectionAllowedToGenerate(sectionPos, targetPos)) if (!this.sectionInAllowedGenerationRadius(requestPos, targetPos))
{ {
entry.future.cancel(false); requestTask.future.cancel(false);
this.pendingTasksSemaphore.release(); this.pendingTasksSemaphore.release();
return; return;
} }
if (!this.onBeforeRequest(sectionPos, entry.future)) if (!this.onBeforeRequest(requestPos, requestTask.future))
{ {
this.pendingTasksSemaphore.release(); this.pendingTasksSemaphore.release();
return; return;
} }
Long offsetEntryTimestamp = entry.updateTimestamp != null Long offsetEntryTimestamp = requestTask.updateTimestamp != null
? entry.updateTimestamp + this.networkState.getServerTimeOffset() ? requestTask.updateTimestamp + this.networkState.getServerTimeOffset()
: null; : null;
CompletableFuture<FullDataSourceResponseMessage> dataSourceFuture = this.networkState.getSession().sendRequest( CompletableFuture<FullDataSourceResponseMessage> dataSourceNetworkFuture = this.networkState.getSession().sendRequest(
new FullDataSourceRequestMessage(this.level.getLevelWrapper(), sectionPos, offsetEntryTimestamp), new FullDataSourceRequestMessage(this.level.getLevelWrapper(), requestPos, offsetEntryTimestamp),
FullDataSourceResponseMessage.class FullDataSourceResponseMessage.class
); );
entry.networkDataSourceFuture = dataSourceFuture; requestTask.networkDataSourceFuture = dataSourceNetworkFuture;
dataSourceFuture.handle((response, throwable) ->
Executor networkCompressionExecutor = ThreadPoolUtil.getNetworkCompressionExecutor();
if (networkCompressionExecutor == null)
{ {
this.pendingTasksSemaphore.release(); return;
}
try
{ dataSourceNetworkFuture.handleAsync((FullDataSourceResponseMessage response, Throwable throwable) ->
if (throwable != null) {
{ this.handleNetResponse(requestTask, response, throwable);
throw throwable; return null;
} }, networkCompressionExecutor);
}
if (response.payload != null) private void handleNetResponse(NetRequestTask requestTask, FullDataSourceResponseMessage response, Throwable throwable)
{ {
FullDataSourceV2DTO dataSourceDto = this.networkState.fullDataPayloadReceiver.decodeDataSource(response.payload); this.pendingTasksSemaphore.release();
// set application flags based on the received detail level, try
// this is needed so the data sources propagate correctly {
dataSourceDto.applyToChildren = DhSectionPos.getDetailLevel(dataSourceDto.pos) > DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL; if (throwable != null)
dataSourceDto.applyToParent = DhSectionPos.getDetailLevel(dataSourceDto.pos) < DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL + 12; {
throw throwable;
AbstractExecutorService executor = ThreadPoolUtil.getNetworkCompressionExecutor(); }
if (executor == null)
{ if (response.payload == null)
LOGGER.warn("Unable to handle FullDataPayload - getNetworkCompressionExecutor() is null"); {
dataSourceDto.close(); LodUtil.assertTrue(this.changedOnly, "Received empty data source response for not changes-only request");
return null; return;
} }
CompletableFuture.runAsync(() ->
{ try(FullDataSourceV2DTO dataSourceDto = this.networkState.fullDataPayloadReceiver.decodeDataSource(response.payload))
try {
{ // set application flags based on the received detail level,
this.level.updateBeaconBeamsForSectionPos(dataSourceDto.pos, response.payload.beaconBeams); // this is needed so the data sources propagate correctly
dataSourceDto.applyToChildren = DhSectionPos.getDetailLevel(dataSourceDto.pos) > DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL;
FullDataSourceV2 fullDataSource = dataSourceDto.createDataSource(this.level.getLevelWrapper(), null); dataSourceDto.applyToParent = DhSectionPos.getDetailLevel(dataSourceDto.pos) < DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL + 12;
entry.dataSourceConsumer.accept(fullDataSource);
}
catch (Exception e) this.level.updateBeaconBeamsForSectionPos(dataSourceDto.pos, response.payload.beaconBeams);
{
throw new RuntimeException(e); FullDataSourceV2 fullDataSource = dataSourceDto.createDataSource(this.level.getLevelWrapper(), null);
} requestTask.future.complete(DataSourceRetrievalResult.CreateSuccess(dataSourceDto.pos, fullDataSource));
finally }
{ }
dataSourceDto.close(); catch (SectionRequiresSplittingException ignored)
} {
}, executor); requestTask.future.complete(DataSourceRetrievalResult.CreateSplit());
} }
else catch (SessionClosedException | CancellationException ignored)
{ {
LodUtil.assertTrue(this.changedOnly, "Received empty data source response for not changes-only request"); requestTask.future.cancel(false);
} }
} catch (RequestRejectedException e)
catch (SectionRequiresSplittingException ignored) {
{ LOGGER.info("Request rejected by the server, message: [" + e.getMessage() + "].");
return entry.future.complete(ERequestResult.REQUIRES_SPLITTING); requestTask.future.completeExceptionally(e);
} }
catch (SessionClosedException | CancellationException ignored) catch (RateLimitedException e)
{ {
return entry.future.cancel(false); LOGGER.info("Rate limited by server, re-queueing task [" + DhSectionPos.toString(requestTask.pos) + "], message: [" + e.getMessage() + "].");
}
catch (RequestRejectedException e) // Skip all requests for 1 second
{ this.rateLimiter.acquireAll();
LOGGER.info("Request rejected by the server: " + e.getMessage());
return entry.future.complete(ERequestResult.FAILED); requestTask.networkDataSourceFuture = null;
} }
catch (RateLimitedException e) catch (RequestOutOfRangeException e)
{ {
LOGGER.info("Rate limited by server, re-queueing task [" + DhSectionPos.toString(sectionPos) + "]: " + e.getMessage()); LOGGER.debug("Out of range, re-queueing task [" + DhSectionPos.toString(requestTask.pos) + "], message: [" + e.getMessage() + "].");
// Skip all requests for 1 second requestTask.networkDataSourceFuture = null;
this.rateLimiter.acquireAll(); }
catch (Throwable e)
entry.networkDataSourceFuture = null; {
return null; requestTask.retryAttempts--;
} LOGGER.error("Unexpected error: ["+e.getMessage()+"] while fetching full data source, attempts left: ["+requestTask.retryAttempts+"] / ["+MAX_RETRY_ATTEMPTS+"]", e);
catch (RequestOutOfRangeException e)
{ // Retry logic
LOGGER.debug("Out of range, re-queueing task [" + DhSectionPos.toString(sectionPos) + "]: " + e.getMessage()); if (requestTask.retryAttempts > 0)
{
entry.networkDataSourceFuture = null; requestTask.networkDataSourceFuture = null;
return null; }
} else
catch (Throwable e) {
{ requestTask.future.completeExceptionally(e);
entry.retryAttempts--; }
LOGGER.error("Error while fetching full data source, attempts left: {} / {}", entry.retryAttempts, MAX_RETRY_ATTEMPTS, e); }
// Retry logic
if (entry.retryAttempts > 0)
{
entry.networkDataSourceFuture = null;
return null;
}
else
{
return entry.future.complete(ERequestResult.FAILED);
}
}
return entry.future.complete(ERequestResult.SUCCEEDED);
});
} }
@@ -357,22 +316,30 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
public void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf) public void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf)
{ {
for (Map.Entry<Long, RequestQueueEntry> mapEntry : (Iterable<? extends Map.Entry<Long, RequestQueueEntry>>) this.waitingTasksBySectionPos.entrySet().stream() // remove tasks furthest
.sorted(Comparator.comparingInt((Map.Entry<Long, RequestQueueEntry> entry) -> DhSectionPos.getChebyshevSignedBlockDistance(entry.getKey(), Objects.requireNonNull(this.level.getTargetPosForGeneration()))).reversed()) Iterator<Map.Entry<Long, NetRequestTask>> farestTaskIterator = this.waitingTasksBySectionPos
::iterator) .entrySet().stream()
.sorted(Comparator.comparingInt((Map.Entry<Long, NetRequestTask> entry) ->
{
Long pos = entry.getKey();
DhBlockPos2D targetPos = this.level.getTargetPosForGeneration();
return DhSectionPos.getChebyshevSignedBlockDistance(pos, targetPos);
}).reversed())
.iterator();
while (farestTaskIterator.hasNext())
{ {
Map.Entry<Long, NetRequestTask> mapEntry = farestTaskIterator.next();
long pos = mapEntry.getKey(); long pos = mapEntry.getKey();
RequestQueueEntry entry = mapEntry.getValue(); NetRequestTask entry = mapEntry.getValue();
if (removeIf.accept(pos)) if (removeIf.accept(pos))
{ {
LOGGER.debug("Removing request [" + mapEntry.getKey() + "]...");
entry.future.cancel(false);
if (entry.networkDataSourceFuture != null) if (entry.networkDataSourceFuture != null)
{ {
entry.networkDataSourceFuture.cancel(false); entry.networkDataSourceFuture.cancel(false);
} }
entry.future.cancel(false);
} }
} }
} }
@@ -400,7 +367,7 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
do do
{ {
for (RequestQueueEntry entry : this.waitingTasksBySectionPos.values()) for (NetRequestTask entry : this.waitingTasksBySectionPos.values())
{ {
entry.future.cancel(alsoInterruptRunning); entry.future.cancel(alsoInterruptRunning);
if (entry.networkDataSourceFuture != null && entry.networkDataSourceFuture.cancel(alsoInterruptRunning)) if (entry.networkDataSourceFuture != null && entry.networkDataSourceFuture.cancel(alsoInterruptRunning))
@@ -438,13 +405,31 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
return; return;
} }
for (Map.Entry<Long, RequestQueueEntry> mapEntry : this.waitingTasksBySectionPos.entrySet()) DhBlockPos2D targetPos = this.level.getTargetPosForGeneration();
for (Map.Entry<Long, NetRequestTask> mapEntry : this.waitingTasksBySectionPos.entrySet())
{ {
renderer.renderBox(new DebugRenderer.Box(mapEntry.getKey(), -32f, 64f, 0.05f, long pos = mapEntry.getKey();
mapEntry.getValue().networkDataSourceFuture != null ? Color.red NetRequestTask task = mapEntry.getValue();
: this.isSectionAllowedToGenerate(mapEntry.getKey(), Objects.requireNonNull(this.level.getTargetPosForGeneration())) ? Color.gray
: Color.darkGray Color color;
)); if (task.networkDataSourceFuture != null)
{
color = Color.RED;
}
else
{
boolean taskInAllowedGenRadius = this.sectionInAllowedGenerationRadius(pos, targetPos);
if (taskInAllowedGenRadius)
{
color = Color.GRAY;
}
else
{
color = Color.DARK_GRAY;
}
}
renderer.renderBox(new DebugRenderer.Box(pos, -32f, 64f, 0.05f, color));
} }
} }
@@ -454,11 +439,12 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
// helper classes // // helper classes //
//================// //================//
protected static class RequestQueueEntry protected static class NetRequestTask
{ {
public final long pos;
/** encapsulates the entire request, including client side queuing and the actual server request */ /** encapsulates the entire request, including client side queuing and the actual server request */
public final CompletableFuture<ERequestResult> future = new CompletableFuture<>(); public final CompletableFuture<DataSourceRetrievalResult> future = new CompletableFuture<>();
public final Consumer<FullDataSourceV2> dataSourceConsumer;
/** will be null if we want to retrieve the LOD regardless of when it was last updated */ /** will be null if we want to retrieve the LOD regardless of when it was last updated */
@Nullable @Nullable
public final Long updateTimestamp; public final Long updateTimestamp;
@@ -477,23 +463,14 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
// constructor // // constructor //
//=============// //=============//
public RequestQueueEntry( public NetRequestTask(long pos, @Nullable Long updateTimestamp)
Consumer<FullDataSourceV2> dataSourceConsumer,
@Nullable Long updateTimestamp)
{ {
this.dataSourceConsumer = dataSourceConsumer; this.pos = pos;
this.updateTimestamp = updateTimestamp; this.updateTimestamp = updateTimestamp;
} }
} }
public enum ERequestResult
{
SUCCEEDED,
REQUIRES_SPLITTING,
FAILED,
}
} }
@@ -1,8 +1,10 @@
package com.seibel.distanthorizons.core.multiplayer.client; package com.seibel.distanthorizons.core.multiplayer.client;
import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.MinecraftTextFormat;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig; import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig;
@@ -18,6 +20,7 @@ import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartial
import com.seibel.distanthorizons.core.network.session.NetworkSession; import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.Closeable; import java.io.Closeable;
@@ -29,6 +32,10 @@ public class ClientNetworkState implements Closeable
.fileLevelConfig(Config.Common.Logging.logNetworkEventToFile) .fileLevelConfig(Config.Common.Logging.logNetworkEventToFile)
.build(); .build();
protected static final DhLogger CONFIG_CHANGE_LOGGER = new DhLoggerBuilder()
.fileLevelConfig(Config.Common.Logging.logConnectionConfigChangesToFile)
.build();
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
@@ -44,6 +51,7 @@ public class ClientNetworkState implements Closeable
*/ */
public NetworkSession getSession() { return this.networkSession; } public NetworkSession getSession() { return this.networkSession; }
@NotNull
public SessionConfig sessionConfig = new SessionConfig(); public SessionConfig sessionConfig = new SessionConfig();
private volatile boolean configReceived = false; private volatile boolean configReceived = false;
@@ -89,6 +97,15 @@ public class ClientNetworkState implements Closeable
|| Math.abs(event.protocolVersion - ModInfo.PROTOCOL_VERSION) < this.closestProtocolVersion) || Math.abs(event.protocolVersion - ModInfo.PROTOCOL_VERSION) < this.closestProtocolVersion)
{ {
this.closestProtocolVersion = event.protocolVersion; this.closestProtocolVersion = event.protocolVersion;
if (ModInfo.PROTOCOL_VERSION < event.protocolVersion)
{
ClientApi.INSTANCE.showChatMessageNextFrame(MinecraftTextFormat.ORANGE + "Distant Horizons: Your mod is outdated. Update to receive LODs on this server.");
}
else
{
ClientApi.INSTANCE.showChatMessageNextFrame(MinecraftTextFormat.ORANGE + "Distant Horizons: The server's mod is outdated. Ask the server's owner to update.");
}
} }
}); });
@@ -129,7 +146,9 @@ public class ClientNetworkState implements Closeable
{ {
this.serverSupportStatus = EServerSupportStatus.FULL; this.serverSupportStatus = EServerSupportStatus.FULL;
LOGGER.info("Connection config has been changed: [" + message.config + "]."); String configChanges = this.sessionConfig.getDifferencesAsString(message.config);
CONFIG_CHANGE_LOGGER.info("Connection config has been changed: [" + configChanges + "].");
this.sessionConfig = message.config; this.sessionConfig = message.config;
this.configReceived = true; this.configReceived = true;
}); });
@@ -2,6 +2,7 @@ package com.seibel.distanthorizons.core.multiplayer.client;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue; import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue;
import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.level.DhClientLevel; import com.seibel.distanthorizons.core.level.DhClientLevel;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
@@ -35,12 +36,12 @@ public class SyncOnLoadRequestQueue extends AbstractFullDataNetworkRequestQueue
@Override @Override
protected int getRequestRateLimit() { return this.networkState.sessionConfig.getSyncOnLoginRateLimit(); } protected int getRequestRateLimit() { return this.networkState.sessionConfig.getSyncOnLoginRateLimit(); }
@Override @Override
protected boolean isSectionAllowedToGenerate(long sectionPos, DhBlockPos2D targetPos) protected boolean sectionInAllowedGenerationRadius(long sectionPos, DhBlockPos2D targetPos)
{ {
return DhSectionPos.getChebyshevSignedBlockDistance(sectionPos, targetPos) <= this.networkState.sessionConfig.getMaxSyncOnLoadDistance() * 16; return DhSectionPos.getChebyshevSignedBlockDistance(sectionPos, targetPos) <= this.networkState.sessionConfig.getMaxSyncOnLoadDistance() * 16;
} }
@Override @Override
protected boolean onBeforeRequest(long sectionPos, CompletableFuture<ERequestResult> future) { return true; } protected boolean onBeforeRequest(long sectionPos, CompletableFuture<DataSourceRetrievalResult> future) { return true; }
@Override @Override
protected String getQueueName() { return "Sync On Login Queue"; } protected String getQueueName() { return "Sync On Login Queue"; }
@@ -162,6 +162,34 @@ public class SessionConfig implements INetworkObject
//=========//
// logging //
//=========//
/**
* example: "common.playerBandwidthLimit:[497], " <br>
* Useful to see what was changed when receiving a new config from the server.
*/
public String getDifferencesAsString(SessionConfig that)
{
StringBuilder stringBuilder = new StringBuilder();
for (String key : this.values.keySet())
{
String thisFieldString = this.values.get(key) + "";
String thatFieldString = that.values.get(key) + "";
if (!thisFieldString.equals(thatFieldString))
{
stringBuilder.append(key+":["+thisFieldString+"], ");
}
}
return stringBuilder.toString();
}
//================// //================//
// base overrides // // base overrides //
//================// //================//
@@ -9,7 +9,7 @@ import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMe
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO; import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.buffer.Unpooled;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -36,7 +36,7 @@ public class FullDataPayloadReceiver implements AutoCloseable
{ {
if (message.isFirst) if (message.isFirst)
{ {
composite = UnpooledByteBufAllocator.DEFAULT.compositeBuffer(); composite = Unpooled.compositeBuffer();
LOGGER.debug("Created new full data buffer [" + message.bufferId + "]: [" + composite + "]"); LOGGER.debug("Created new full data buffer [" + message.bufferId + "]: [" + composite + "]");
} }
else if (composite == null) else if (composite == null)
@@ -45,6 +45,7 @@ public class FullDataPayloadReceiver implements AutoCloseable
return null; return null;
} }
message.buffer.readerIndex(0);
composite.addComponent(message.buffer); composite.addComponent(message.buffer);
composite.writerIndex(composite.writerIndex() + message.buffer.writerIndex()); composite.writerIndex(composite.writerIndex() + message.buffer.writerIndex());
LOGGER.debug("Updated full data buffer [" + message.bufferId + "]: [" + composite + "]."); LOGGER.debug("Updated full data buffer [" + message.bufferId + "]: [" + composite + "].");
@@ -55,12 +56,11 @@ public class FullDataPayloadReceiver implements AutoCloseable
public FullDataSourceV2DTO decodeDataSource(FullDataPayload payload) public FullDataSourceV2DTO decodeDataSource(FullDataPayload payload)
{ {
CompositeByteBuf compositeByteBuffer = this.buffersById.get(payload.dtoBufferId); CompositeByteBuf compositeByteBuffer = this.buffersById.get(payload.dtoBufferId);
LodUtil.assertTrue(compositeByteBuffer != null); LodUtil.assertTrue(compositeByteBuffer != null, "decoded data source missing byte buffer");
try try
{ {
FullDataSourceV2DTO dataSourceDto = INetworkObject.decodeToInstance(FullDataSourceV2DTO.CreateEmptyDataSourceForDecoding(), compositeByteBuffer); FullDataSourceV2DTO dataSourceDto = INetworkObject.decodeToInstance(FullDataSourceV2DTO.CreateEmptyDataSourceForDecoding(), compositeByteBuffer);
LOGGER.debug("Buffer {} DTO: {}", payload.dtoBufferId, dataSourceDto);
return dataSourceDto; return dataSourceDto;
} }
finally finally
@@ -174,12 +174,21 @@ public class FullDataSourceRequestHandler implements AutoCloseable
DataSourceRequestGroup requestGroup = this.requestGroupsByPos.computeIfAbsent(requestData.sectionPos(), pos -> DataSourceRequestGroup requestGroup = this.requestGroupsByPos.computeIfAbsent(requestData.sectionPos(), pos ->
{ {
DataSourceRequestGroup newGroup = new DataSourceRequestGroup(pos); DataSourceRequestGroup newGroup = new DataSourceRequestGroup(pos);
newGroup.tryAddRequest(requestData); try
createdNewGroup.set(true); {
newGroup.tryAddRequest(requestData);
createdNewGroup.set(true);
this.tryFulfillDataSourceRequestGroup(newGroup, pos);
LOGGER.debug("[" + this.getLevelIdentifier() + "] Created request group for pos [" + DhSectionPos.toString(pos) + "].");
return newGroup;
}
catch (Exception e)
{
LOGGER.error("Unable to queue request for pos: ["+DhSectionPos.toString(requestData.sectionPos())+"], error: ["+e.getMessage()+"].", e);
}
this.tryFulfillDataSourceRequestGroup(newGroup, pos);
LOGGER.debug("[" + this.getLevelIdentifier() + "] Created request group for pos [" + DhSectionPos.toString(pos) + "].");
return newGroup; return newGroup;
}); });
@@ -229,10 +238,14 @@ public class FullDataSourceRequestHandler implements AutoCloseable
private void tryFulfillDataSourceRequestGroup(DataSourceRequestGroup requestGroup, long pos) private void tryFulfillDataSourceRequestGroup(DataSourceRequestGroup requestGroup, long pos)
{ {
this.fullDataSourceProvider().getAsync(pos).thenAccept(fullDataSource -> final GeneratedFullDataSourceProvider provider = this.fullDataSourceProvider();
provider.getAsync(pos)
.thenAccept((FullDataSourceV2 fullDataSource) ->
{ {
if (this.fullDataSourceProvider().isFullyGenerated(fullDataSource.columnGenerationSteps)) if (provider.generationStepsAreFullyGenerated(fullDataSource.columnGenerationSteps))
{ {
//LOGGER.info("sending - complete [" + DhSectionPos.toString(pos) + "]");
requestGroup.fullDataSource = fullDataSource; requestGroup.fullDataSource = fullDataSource;
return; return;
} }
@@ -247,11 +260,14 @@ public class FullDataSourceRequestHandler implements AutoCloseable
this.requestGroupsByPos.remove(pos); this.requestGroupsByPos.remove(pos);
if (!requestGroup.tryClose()) if (!requestGroup.tryClose())
{ {
//LOGGER.info("closing [" + DhSectionPos.toString(pos) + "]");
return; return;
} }
for (DataSourceRequestGroup.RequestData requestData : requestGroup.requestMessages.values()) for (DataSourceRequestGroup.RequestData requestData : requestGroup.requestMessages.values())
{ {
//LOGGER.info("sending [" + DhSectionPos.toString(pos) + "] - ["+DhSectionPos.toString(requestData.sectionPos())+"]");
this.requestGroupsByFutureId.remove(requestData.futureId()); this.requestGroupsByFutureId.remove(requestData.futureId());
requestData.rateLimiterSet.generationRequestRateLimiter.release(); requestData.rateLimiterSet.generationRequestRateLimiter.release();
requestData.message.sendResponse(new SectionRequiresSplittingException()); requestData.message.sendResponse(new SectionRequiresSplittingException());
@@ -264,7 +280,7 @@ public class FullDataSourceRequestHandler implements AutoCloseable
} }
else else
{ {
//LOGGER.info("sending - queueing [" + DhSectionPos.toString(pos) + "]"); //LOGGER.info("queueing incomplete world gen [" + DhSectionPos.toString(pos) + "]");
this.fullDataSourceProvider().queuePositionForRetrieval(pos); this.fullDataSourceProvider().queuePositionForRetrieval(pos);
} }
}); });
@@ -40,7 +40,9 @@ public abstract class AbstractPhantomArrayList implements AutoCloseable
/** The Array counts can be 0 or greater. */ /** The Array counts can be 0 or greater. */
public AbstractPhantomArrayList(PhantomArrayListPool phantomArrayListPool, int byteArrayCount, int shortArrayCount, int longArrayCount) public AbstractPhantomArrayList(PhantomArrayListPool phantomArrayListPool, int byteArrayCount, int shortArrayCount, int longArrayCount)
{ {
if (byteArrayCount < 0 || shortArrayCount < 0 || longArrayCount < 0) if (byteArrayCount < 0
|| shortArrayCount < 0
|| longArrayCount < 0)
{ {
throw new IllegalArgumentException("Can't get a negative number of pooled arrays."); throw new IllegalArgumentException("Can't get a negative number of pooled arrays.");
} }
@@ -10,6 +10,7 @@ import org.jetbrains.annotations.Nullable;
import java.lang.ref.SoftReference; import java.lang.ref.SoftReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
/** /**
* This keeps track of all the poolable * This keeps track of all the poolable
@@ -33,7 +34,7 @@ public class PhantomArrayListCheckout implements AutoCloseable
/** Will be null if the parent pool doesn't want leak stack tracing */ /** Will be null if the parent pool doesn't want leak stack tracing */
@Nullable @Nullable
public final String allocationStackTrace; public String allocationStackTrace = null;
private final ArrayList<ByteArrayList> byteArrayLists = new ArrayList<>(); private final ArrayList<ByteArrayList> byteArrayLists = new ArrayList<>();
private final ArrayList<ShortArrayList> shortArrayLists = new ArrayList<>(); private final ArrayList<ShortArrayList> shortArrayLists = new ArrayList<>();
@@ -47,21 +48,20 @@ public class PhantomArrayListCheckout implements AutoCloseable
public PhantomArrayListCheckout(@NotNull PhantomArrayListPool owningPool) public PhantomArrayListCheckout(@NotNull PhantomArrayListPool owningPool)
{ {
if (owningPool.logGarbageCollectedStacks)
{
// TODO remove the top 4 or so lines since those will always be the same (relating to the phantom allocations)
// and aren't helpful when debugging
this.allocationStackTrace = StringUtil.join("\n", Thread.currentThread().getStackTrace());
}
else
{
this.allocationStackTrace = null;
}
this.owningPool = owningPool; this.owningPool = owningPool;
this.ownerSoftReference = new SoftReference<>(this); this.ownerSoftReference = new SoftReference<>(this);
} }
public void onCheckout()
{
if (this.owningPool.logGarbageCollectedStacks)
{
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
StackTraceElement[] trimmedElements = Arrays.copyOfRange(stackTraceElements, 4, stackTraceElements.length);
this.allocationStackTrace = StringUtil.join("\n", trimmedElements).intern();
}
}
//=========// //=========//
@@ -2,6 +2,7 @@ package com.seibel.distanthorizons.core.pooling;
import com.seibel.distanthorizons.core.api.internal.ClientApi; import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.enums.MinecraftTextFormat;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen; import com.seibel.distanthorizons.core.logging.f3.F3Screen;
@@ -12,7 +13,6 @@ import com.seibel.distanthorizons.coreapi.util.StringUtil;
import it.unimi.dsi.fastutil.bytes.ByteArrayList; import it.unimi.dsi.fastutil.bytes.ByteArrayList;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.shorts.ShortArrayList; import it.unimi.dsi.fastutil.shorts.ShortArrayList;
import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -159,6 +159,7 @@ public class PhantomArrayListPool
{ {
// pool is empty, create new checkout // pool is empty, create new checkout
checkout = new PhantomArrayListCheckout(this); checkout = new PhantomArrayListCheckout(this);
checkout.onCheckout();
} }
else else
{ {
@@ -166,6 +167,7 @@ public class PhantomArrayListPool
if (checkout != null) if (checkout != null)
{ {
// use pooled checkout // use pooled checkout
checkout.onCheckout();
} }
else else
{ {
@@ -176,8 +178,7 @@ public class PhantomArrayListPool
{ {
lowMemoryWarningLogged = true; lowMemoryWarningLogged = true;
// orange text String message = MinecraftTextFormat.ORANGE + "Distant Horizons: Insufficient memory detected." + MinecraftTextFormat.CLEAR_FORMATTING + "\n" +
String message = "\u00A76" + "Distant Horizons: Insufficient memory detected." + "\u00A7r \n" +
"This may cause stuttering or crashing. \n" + "This may cause stuttering or crashing. \n" +
"Potential causes: \n" + "Potential causes: \n" +
"1. your allocated memory isn't high enough \n" + "1. your allocated memory isn't high enough \n" +
@@ -23,7 +23,9 @@ import com.seibel.distanthorizons.coreapi.util.MathUtil;
import java.util.Objects; import java.util.Objects;
/** immutable */ /** immutable */
@Deprecated // TODO why does this exist vs blockpos2d?
public class Pos2D public class Pos2D
{ {
public static final Pos2D ZERO = new Pos2D(0, 0); public static final Pos2D ZERO = new Pos2D(0, 0);
@@ -75,6 +75,30 @@ public class DhBlockPos2D
public long distSquared(DhBlockPos2D other) { return this.distSquared(other.x, other.z); } public long distSquared(DhBlockPos2D other) { return this.distSquared(other.x, other.z); }
public long distSquared(int x, int z) { return MathUtil.pow2((long) this.x - x) + MathUtil.pow2((long) this.z - z); } public long distSquared(int x, int z) { return MathUtil.pow2((long) this.x - x) + MathUtil.pow2((long) this.z - z); }
/**
* Returns the maximum distance along either the X or Z axis <br><br>
*
* Example chebyshev distance between X and every point around it: <br>
* <code>
* 2 2 2 2 2 <br>
* 2 1 1 1 2 <br>
* 2 1 X 1 2 <br>
* 2 1 1 1 2 <br>
* 2 2 2 2 2 <br>
* </code>
*/
public int chebyshevDist(DhBlockPos2D other) { return Math.max(Math.abs(this.x - other.x), Math.abs(this.z - other.z)); }
/**
* Can be used to quickly determine the rough distance between two points<Br>
* or determine the taxi cab (manhattan) distance between two points. <Br><Br>
*
* Manhattan distance is equivalent to determining the distance between two street intersections,
* where you can only drive along each street, instead of directly to the other point.
*/
public int manhattanDist(DhBlockPos2D other) { return Math.abs(this.x - other.x) + Math.abs(this.z - other.z); }
//===========// //===========//
@@ -20,10 +20,12 @@
package com.seibel.distanthorizons.core.render; package com.seibel.distanthorizons.core.render;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource; import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.LodBufferContainer; import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.LodBufferContainer;
import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.file.fullDatafile.V2.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.fullDatafile.V2.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.generation.tasks.ERetrievalResultState;
import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
@@ -34,12 +36,13 @@ import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.render.renderer.generic.BeaconRenderHandler; import com.seibel.distanthorizons.core.render.renderer.generic.BeaconRenderHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.PerfRecorder;
import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.util.WorldGenUtil;
import com.seibel.distanthorizons.core.util.objects.quadTree.QuadNode; import com.seibel.distanthorizons.core.util.objects.quadTree.QuadNode;
import com.seibel.distanthorizons.core.util.objects.quadTree.QuadTree; import com.seibel.distanthorizons.core.util.objects.quadTree.QuadTree;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.coreapi.util.MathUtil; import com.seibel.distanthorizons.coreapi.util.MathUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongIterator; import it.unimi.dsi.fastutil.longs.LongIterator;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -47,6 +50,7 @@ import javax.annotation.WillNotClose;
import java.awt.*; import java.awt.*;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -57,11 +61,11 @@ import java.util.concurrent.locks.ReentrantLock;
* This quadTree structure is our core data structure and holds * This quadTree structure is our core data structure and holds
* all rendering data. * all rendering data.
*/ */
public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRenderable, AutoCloseable public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRenderable, IConfigListener, AutoCloseable
{ {
private static final DhLogger LOGGER = new DhLoggerBuilder().build(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
/** there should only ever be one {@link LodQuadTree} so having the thread static should be fine */ /** there should only ever be one {@link LodQuadTree} so having the thread static should be fine */
private static final ThreadPoolExecutor FULL_DATA_RETRIEVAL_QUEUE_THREAD = ThreadUtil.makeSingleThreadPool("QuadTree Full Data Retrieval Queue Populator"); private static final ThreadPoolExecutor FULL_DATA_RETRIEVAL_QUEUE_THREAD = ThreadUtil.makeSingleThreadPool("LodQuadTree Data Retrieval Queue");
public final int blockRenderDistanceDiameter; public final int blockRenderDistanceDiameter;
@@ -73,9 +77,13 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
* This is a {@link ConcurrentLinkedQueue} because new sections can be added to this list via the world generator threads. * This is a {@link ConcurrentLinkedQueue} because new sections can be added to this list via the world generator threads.
*/ */
private final ConcurrentLinkedQueue<Long> sectionsToReload = new ConcurrentLinkedQueue<>(); private final ConcurrentLinkedQueue<Long> sectionsToReload = new ConcurrentLinkedQueue<>();
private final IDhClientLevel level; //FIXME: Proper hierarchy to remove this reference! private final IDhClientLevel level;
private final ReentrantLock treeReadWriteLock = new ReentrantLock(); /**
private final AtomicBoolean fullDataRetrievalQueueRunning = new AtomicBoolean(false); * Note: this doesn't lock all operations as some other threads/operations
* that may traverse the tree while it's being modified.
* IE {@link RenderBufferHandler} will walk through the tree each frame.
*/
private final ReentrantLock treeLock = new ReentrantLock();
private ArrayList<LodRenderSection> debugRenderSections = new ArrayList<>(); private ArrayList<LodRenderSection> debugRenderSections = new ArrayList<>();
private ArrayList<LodRenderSection> altDebugRenderSections = new ArrayList<>(); private ArrayList<LodRenderSection> altDebugRenderSections = new ArrayList<>();
@@ -88,7 +96,9 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
* as further sections are loaded before closer ones. * as further sections are loaded before closer ones.
* Only queuing a few of the sections at a time solves this problem. * Only queuing a few of the sections at a time solves this problem.
*/ */
public final AtomicInteger uploadTaskCountRef = new AtomicInteger(0); private final AtomicInteger uploadTaskCountRef = new AtomicInteger(0);
private final AtomicBoolean requeueAllRetrievalTasksRef = new AtomicBoolean(false);
private final AtomicBoolean queueThreadRunningRef = new AtomicBoolean(false);
@Nullable @Nullable
@@ -104,11 +114,18 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
/** used to calculate when a detail drop will occur */ /** used to calculate when a detail drop will occur */
private double detailDropOffLogBase; private double detailDropOffLogBase;
/** the {@link DhSectionPos} that need to be retrieved/generated */
private final Set<Long> missingGenerationPosSet = Collections.newSetFromMap(new ConcurrentHashMap<>()); // concurrency is annoying but required due to needing to add/remove items in the world gen future
private final Set<Long> queuedGenerationPosSet = Collections.newSetFromMap(new ConcurrentHashMap<>());
/** cached array to prevent having to re-allocate it each tick */
private final ArrayList<Long> sortedMissingPosList = new ArrayList<>();
//==============//
// constructors // //=============//
//==============// // constructor //
//=============//
//region constructor
public LodQuadTree( public LodQuadTree(
IDhClientLevel level, int viewDiameterInBlocks, IDhClientLevel level, int viewDiameterInBlocks,
@@ -126,13 +143,18 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
GenericObjectRenderer genericObjectRenderer = this.level.getGenericRenderer(); GenericObjectRenderer genericObjectRenderer = this.level.getGenericRenderer();
this.beaconRenderHandler = (genericObjectRenderer != null) ? new BeaconRenderHandler(genericObjectRenderer) : null; this.beaconRenderHandler = (genericObjectRenderer != null) ? new BeaconRenderHandler(genericObjectRenderer) : null;
Config.Common.WorldGenerator.enableDistantGeneration.addListener(this);
} }
//endregion constructor
//=============// //=============//
// tick update // // tick update //
//=============// //=============//
//region tick update
/** /**
* This function updates the quadTree based on the playerPos and the current game configs (static and global) * This function updates the quadTree based on the playerPos and the current game configs (static and global)
@@ -143,23 +165,33 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
{ {
if (this.level == null) if (this.level == null)
{ {
// the level hasn't finished loading yet // the quad tree was created before a level reference was created
// TODO sometimes null pointers still happen, when logging back into a world (maybe the old level isn't null but isn't valid either?)
return; return;
} }
// this shouldn't be updated while the tree is being iterated through // don't tick the tree if a modification is still going
this.updateDetailLevelVariables(); // TODO is this lock necessary for anything beyond this tick method?
if (this.treeLock.tryLock())
// don't traverse the tree if it is being modified
if (this.treeReadWriteLock.tryLock())
{ {
// this shouldn't be updated while the tree is being iterated through
this.updateDetailLevelVariables();
try try
{ {
// recenter if necessary, removing out of bounds sections // recenter if necessary...
this.setCenterBlockPos(playerPos, LodRenderSection::close); this.setCenterBlockPos(playerPos, (renderSection) ->
{
//...removing out of bounds sections
if (renderSection != null)
{
this.fullDataSourceProvider.removeRetrievalRequestIf((long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
this.missingGenerationPosSet.remove(renderSection.pos);
this.queuedGenerationPosSet.remove(renderSection.pos);
renderSection.close();
}
});
this.updateAllRenderSections(playerPos); this.updateAllRenderSections(playerPos);
} }
@@ -169,7 +201,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
} }
finally finally
{ {
this.treeReadWriteLock.unlock(); this.treeLock.unlock();
} }
} }
} }
@@ -197,7 +229,6 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
// walk through each root node // walk through each root node
HashSet<LodRenderSection> nodesNeedingRetrieval = new HashSet<>();
HashSet<LodRenderSection> nodesNeedingLoading = new HashSet<>(); HashSet<LodRenderSection> nodesNeedingLoading = new HashSet<>();
LongIterator rootPosIterator = this.rootNodePosIterator(); LongIterator rootPosIterator = this.rootNodePosIterator();
while (rootPosIterator.hasNext()) while (rootPosIterator.hasNext())
@@ -210,17 +241,61 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
} }
QuadNode<LodRenderSection> rootNode = this.getNode(rootPos); QuadNode<LodRenderSection> rootNode = this.getNode(rootPos);
this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, rootNode, rootNode.sectionPos, false, nodesNeedingRetrieval, nodesNeedingLoading); LodUtil.assertTrue(rootNode != null, "All root nodes should have been created by this point.");
this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, rootNode, rootNode.sectionPos, false, nodesNeedingLoading);
} }
// queue full data retrieval (world gen) requests if needed // requeue everything if needed
if (nodesNeedingRetrieval.size() != 0 if (this.requeueAllRetrievalTasksRef.get()
&& !this.fullDataRetrievalQueueRunning.get() && !this.queueThreadRunningRef.get())
&& this.fullDataSourceProvider.canQueueRetrieval())
{ {
this.fullDataRetrievalQueueRunning.set(true); this.queueThreadRunningRef.set(true);
FULL_DATA_RETRIEVAL_QUEUE_THREAD.execute(() -> this.queueFullDataRetrievalTasks(playerPos, nodesNeedingRetrieval)); this.requeueAllRetrievalTasksRef.set(false);
// running on a separate thread allows for faster loading
// of finished LODs
FULL_DATA_RETRIEVAL_QUEUE_THREAD.execute(() ->
{
try
{
this.checkAllNodesForRetrievalRequests();
}
catch (Exception e)
{
LOGGER.error("Unexpected error getting new queued retrieval tasks, error: [" + e.getMessage() + "].", e);
}
finally
{
this.queueThreadRunningRef.set(false);
}
});
}
// queue full data retrieval (world gen) requests if needed
if (this.missingGenerationPosSet.size() != 0 //
&& this.fullDataSourceProvider.canQueueRetrievalNow()
&& !this.queueThreadRunningRef.get())
{
this.queueThreadRunningRef.set(true);
// running on a separate thread allows for faster loading
// of finished LODs
FULL_DATA_RETRIEVAL_QUEUE_THREAD.execute(() ->
{
try
{
this.startQueuedRetrievalTasks(playerPos);
}
catch (Exception e)
{
LOGGER.error("Unexpected error starting queued retrieval tasks, error: [" + e.getMessage() + "].", e);
}
finally
{
this.queueThreadRunningRef.set(false);
}
});
} }
@@ -236,7 +311,6 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
DhBlockPos2D playerPos, DhBlockPos2D playerPos,
QuadNode<LodRenderSection> rootNode, QuadNode<LodRenderSection> quadNode, long sectionPos, QuadNode<LodRenderSection> rootNode, QuadNode<LodRenderSection> quadNode, long sectionPos,
boolean parentSectionIsRendering, boolean parentSectionIsRendering,
HashSet<LodRenderSection> nodesNeedingRetrieval,
HashSet<LodRenderSection> nodesNeedingLoading) HashSet<LodRenderSection> nodesNeedingLoading)
{ {
//=====================// //=====================//
@@ -245,7 +319,8 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
//=====================// //=====================//
// create the node // create the node
if (quadNode == null && this.isSectionPosInBounds(sectionPos)) // the position bounds should only fail when at the edge of the user's render distance if (quadNode == null
&& this.isSectionPosInBounds(sectionPos)) // the position bounds should only fail when at the edge of the user's render distance
{ {
rootNode.setValue(sectionPos, new LodRenderSection(sectionPos, this, this.level, this.fullDataSourceProvider, this.uploadTaskCountRef)); rootNode.setValue(sectionPos, new LodRenderSection(sectionPos, this, this.level, this.fullDataSourceProvider, this.uploadTaskCountRef));
quadNode = rootNode.getNode(sectionPos); quadNode = rootNode.getNode(sectionPos);
@@ -288,7 +363,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
QuadNode<LodRenderSection> childNode = quadNode.getChildByIndex(i); QuadNode<LodRenderSection> childNode = quadNode.getChildByIndex(i);
boolean childSectionLoaded = this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, childNode, DhSectionPos.getChildByIndex(sectionPos, i), thisPosIsRendering || parentSectionIsRendering, nodesNeedingRetrieval, nodesNeedingLoading); boolean childSectionLoaded = this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, childNode, DhSectionPos.getChildByIndex(sectionPos, i), thisPosIsRendering || parentSectionIsRendering, nodesNeedingLoading);
allChildrenSectionsAreLoaded = childSectionLoaded && allChildrenSectionsAreLoaded; allChildrenSectionsAreLoaded = childSectionLoaded && allChildrenSectionsAreLoaded;
} }
@@ -347,7 +422,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
QuadNode<LodRenderSection> childNode = quadNode.getChildByIndex(i); QuadNode<LodRenderSection> childNode = quadNode.getChildByIndex(i);
this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, childNode, DhSectionPos.getChildByIndex(sectionPos, i), parentSectionIsRendering, nodesNeedingRetrieval, nodesNeedingLoading); this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, childNode, DhSectionPos.getChildByIndex(sectionPos, i), parentSectionIsRendering, nodesNeedingLoading);
} }
// disabling rendering must be done after the children are enabled // disabling rendering must be done after the children are enabled
@@ -368,23 +443,11 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
// prepare this section for rendering // prepare this section for rendering
if (!renderSection.gpuUploadInProgress() if (!renderSection.gpuUploadInProgress()
&& renderSection.bufferContainer == null && renderSection.bufferContainer == null)
// TODO this is commented out since some users reported LODs refusing to
// load at their expected higher-detail levels
// this check is specifically for N-sized world generators where the higher quality
// data source may not exist yet, this is done to prevent holes while waiting for said generator
//&& renderSection.getFullDataSourceExists()
)
{ {
nodesNeedingLoading.add(renderSection); nodesNeedingLoading.add(renderSection);
} }
// queue world gen if needed
if (!renderSection.isFullyGenerated())
{
nodesNeedingRetrieval.add(renderSection);
}
// update debug if needed // update debug if needed
if (Config.Client.Advanced.Debugging.DebugWireframe.showQuadTreeRenderStatus.get()) if (Config.Client.Advanced.Debugging.DebugWireframe.showQuadTreeRenderStatus.get())
{ {
@@ -394,7 +457,8 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
// wait for the parent to disable before enabling this section, so we don't have a hole // wait for the parent to disable before enabling this section, so we don't have a hole
if (!parentSectionIsRendering && renderSection.canRender()) if (!parentSectionIsRendering
&& renderSection.canRender())
{ {
// if rendering is already enabled we don't have to re-enable it // if rendering is already enabled we don't have to re-enable it
if (!renderSection.getRenderingEnabled()) if (!renderSection.getRenderingEnabled())
@@ -425,6 +489,9 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
// needs to be fired after the children are disabled so beacons render correctly // needs to be fired after the children are disabled so beacons render correctly
renderSection.onRenderingEnabled(); renderSection.onRenderingEnabled();
// since this section wants to render
// check if it needs any generation to do so
this.tryQueuePosForRetrieval(renderSection.pos);
} }
} }
@@ -447,31 +514,23 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
continue; continue;
} }
try // the section only needs to be updated if a buffer is currently present
LodRenderSection renderSection = this.tryGetValue(pos);
if (renderSection != null)
{ {
// the section only needs to be updated if a buffer is currently present if (renderSection.canRender())
LodRenderSection renderSection = this.getValue(pos);
if (renderSection != null)
{ {
// this data source may now exist if (renderSection.gpuUploadInProgress()
renderSection.updateFullDataSourceExists(); || !renderSection.uploadRenderDataToGpuAsync())
if (renderSection.canRender())
{ {
if (renderSection.gpuUploadInProgress() // if a section is already loading or failed to start upload
|| !renderSection.uploadRenderDataToGpuAsync()) // we need to wait to trigger it again
{ // if we don't trigger it again the LOD will be out of date
// if a section is already loading or failed to start upload // and may be invisible/missing
// we need to wait to trigger it again positionsToRequeue.add(pos);
// if we don't trigger it again the LOD will be out of date
// and may be invisible/missing
positionsToRequeue.add(pos);
}
} }
} }
} }
catch (IndexOutOfBoundsException e)
{ /* the section is now out of bounds, it doesn't need to be reloaded */ }
} }
this.sectionsToReload.addAll(positionsToRequeue); this.sectionsToReload.addAll(positionsToRequeue);
} }
@@ -488,18 +547,188 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
for (int i = 0; i < loadSectionList.size(); i++) for (int i = 0; i < loadSectionList.size(); i++)
{ {
LodRenderSection renderSection = loadSectionList.get(i); LodRenderSection renderSection = loadSectionList.get(i);
if (!renderSection.gpuUploadInProgress() && renderSection.bufferContainer == null) if (!renderSection.gpuUploadInProgress()
&& renderSection.bufferContainer == null)
{ {
renderSection.uploadRenderDataToGpuAsync(); renderSection.uploadRenderDataToGpuAsync();
} }
} }
} }
//endregion tick update
//=================================//
// full data retrieval (world gen) //
//=================================//
//region world gen
private void startQueuedRetrievalTasks(DhBlockPos2D playerPos)
{
// sort the nodes from nearest to farthest
this.sortedMissingPosList.clear();
this.sortedMissingPosList.addAll(this.missingGenerationPosSet);
this.sortedMissingPosList.sort((posA, posB) ->
{
int aDist = DhSectionPos.getManhattanBlockDistance(posA, playerPos);
int bDist = DhSectionPos.getManhattanBlockDistance(posB, playerPos);
return Integer.compare(aDist, bDist);
});
//==================================//
// add retrieval tasks to the queue //
//==================================//
for (int i = 0; i < this.sortedMissingPosList.size(); i++)
{
if (!this.fullDataSourceProvider.canQueueRetrievalNow())
{
break;
}
long missingPos = this.sortedMissingPosList.get(i);
// is this position within acceptable generator range?
boolean posInRange = WorldGenUtil.isPosInWorldGenRange(
missingPos,
Config.Common.WorldGenerator.generationCenterChunkX.get(), Config.Common.WorldGenerator.generationCenterChunkZ.get(),
Config.Common.WorldGenerator.generationMaxChunkRadius.get()
);
if (!posInRange)
{
continue;
}
CompletableFuture<DataSourceRetrievalResult> genFuture = this.fullDataSourceProvider.queuePositionForRetrieval(missingPos);
boolean positionQueued = (genFuture != null && !genFuture.isCompletedExceptionally());
if (positionQueued)
{
this.queuedGenerationPosSet.add(missingPos);
this.missingGenerationPosSet.remove(missingPos);
genFuture.exceptionally((Throwable throwable) ->
{
// gen task failed,
// requeue so we can try again in the future
this.queuedGenerationPosSet.remove(missingPos);
this.missingGenerationPosSet.add(missingPos);
return null;
});
genFuture.thenAccept((DataSourceRetrievalResult result) ->
{
// task finished
this.queuedGenerationPosSet.remove(missingPos);
if (result.state == ERetrievalResultState.REQUIRES_SPLITTING)
{
DhSectionPos.forEachChild(missingPos, (long childPos) ->
{
this.tryQueuePosForRetrieval(childPos);
});
}
});
}
}
//==========================//
// calc task count estimate //
//==========================//
// calculate an estimate for the max number of chunks for the queue
int totalWorldGenChunkCount = 0;
int totalWorldGenTaskCount = 0;
for (int i = 0; i < this.sortedMissingPosList.size(); i++)
{
long missingPos = this.sortedMissingPosList.get(i);
// chunk count
int sectionWidthInChunks = DhSectionPos.getChunkWidth(missingPos);
totalWorldGenChunkCount += sectionWidthInChunks * sectionWidthInChunks;
// task count
totalWorldGenTaskCount++;
}
this.fullDataSourceProvider.setEstimatedRemainingRetrievalChunkCount(totalWorldGenChunkCount);
this.fullDataSourceProvider.setTotalRetrievalPositionCount(totalWorldGenTaskCount);
}
@Override
public void onConfigValueSet()
{
boolean generatorEnabled = Config.Common.WorldGenerator.enableDistantGeneration.get();
if (generatorEnabled)
{
// world gen tasks will need to be re-queued
// since all the render sections will already have been loaded
this.requeueAllRetrievalTasksRef.set(true);
}
else
{
// generation is disabled, clear the queues
this.missingGenerationPosSet.clear();
this.queuedGenerationPosSet.clear();
this.requeueAllRetrievalTasksRef.set(false);
}
}
/**
* Needed to get all necessary retrieval requests
* after the quad tree has already been loaded.
*/
private void checkAllNodesForRetrievalRequests()
{
Iterator<QuadNode<LodRenderSection>> nodeIterator = this.nodeIterator();
while (nodeIterator.hasNext())
{
QuadNode<LodRenderSection> node = nodeIterator.next();
if (node != null)
{
LodRenderSection renderSection = node.value;
if (renderSection != null
&& renderSection.getRenderingEnabled())
{
this.tryQueuePosForRetrieval(renderSection.pos);
}
}
}
}
/** Does nothing if the missing positions are already queued. */
private void tryQueuePosForRetrieval(long pos)
{
LongArrayList missingPosList = this.fullDataSourceProvider.getPositionsToRetrieve(pos);
if (missingPosList == null)
{
return;
}
for (int i = 0; i < missingPosList.size(); i++)
{
long missingPos = missingPosList.getLong(i);
if (!this.queuedGenerationPosSet.contains(missingPos))
{
this.missingGenerationPosSet.add(missingPos);
}
}
}
//endregion world gen
//====================// //====================//
// detail level logic // // detail level logic //
//====================// //====================//
//region detail level logic
/** /**
* This method will compute the detail level based on player position and section pos * This method will compute the detail level based on player position and section pos
@@ -553,11 +782,14 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
this.minRootRenderDetailLevel = (byte) Math.max(minSectionDetailLevel, this.maxLeafRenderDetailLevel); // respect the user's selected max resolution if it is lower detail (IE they want 2x2 block, but minSectionDetailLevel is specifically for 1x1 block render resolution) this.minRootRenderDetailLevel = (byte) Math.max(minSectionDetailLevel, this.maxLeafRenderDetailLevel); // respect the user's selected max resolution if it is lower detail (IE they want 2x2 block, but minSectionDetailLevel is specifically for 1x1 block render resolution)
} }
//endregion detail level logic
//=============//
// render data // //==========================//
//=============// // external render requests //
//==========================//
//region external render requests
/** /**
* Re-creates the color, render data. * Re-creates the color, render data.
@@ -565,34 +797,32 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
*/ */
public void clearRenderDataCache() public void clearRenderDataCache()
{ {
if (this.treeReadWriteLock.tryLock()) // TODO make async, can lock render thread try
{ {
try this.treeLock.lock();
LOGGER.info("Disposing render data...");
// clear the tree
Iterator<QuadNode<LodRenderSection>> nodeIterator = this.nodeIterator();
while (nodeIterator.hasNext())
{ {
LOGGER.info("Disposing render data..."); QuadNode<LodRenderSection> quadNode = nodeIterator.next();
if (quadNode.value != null)
// clear the tree
Iterator<QuadNode<LodRenderSection>> nodeIterator = this.nodeIterator();
while (nodeIterator.hasNext())
{ {
QuadNode<LodRenderSection> quadNode = nodeIterator.next(); quadNode.value.close();
if (quadNode.value != null) quadNode.value = null;
{
quadNode.value.close();
quadNode.value = null;
}
} }
LOGGER.info("Render data cleared, please wait a moment for everything to reload...");
}
catch (Exception e)
{
LOGGER.error("Unexpected error when clearing LodQuadTree render cache: " + e.getMessage(), e);
}
finally
{
this.treeReadWriteLock.unlock();
} }
LOGGER.info("Render data cleared, please wait a moment for everything to reload...");
}
catch (Exception e)
{
LOGGER.error("Unexpected error when clearing LodQuadTree render cache: " + e.getMessage(), e);
}
finally
{
this.treeLock.unlock();
} }
} }
@@ -620,79 +850,14 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
} }
} }
//endregion external render requests
//=================================//
// full data retrieval (world gen) //
//=================================//
private void queueFullDataRetrievalTasks(DhBlockPos2D playerPos, HashSet<LodRenderSection> nodesNeedingRetrieval)
{
try
{
// sort the nodes from nearest to farthest
ArrayList<LodRenderSection> nodeList = new ArrayList<>(nodesNeedingRetrieval);
nodeList.sort((a, b) ->
{
int aDist = DhSectionPos.getManhattanBlockDistance(a.pos, playerPos);
int bDist = DhSectionPos.getManhattanBlockDistance(b.pos, playerPos);
return Integer.compare(aDist, bDist);
});
// add retrieval tasks to the queue
for (int i = 0; i < nodeList.size(); i++)
{
LodRenderSection renderSection = nodeList.get(i);
if (!this.fullDataSourceProvider.canQueueRetrieval())
{
break;
}
renderSection.tryQueuingMissingLodRetrieval();
}
// calculate an estimate for the max number of chunks for the queue
int totalWorldGenChunkCount = 0;
int totalWorldGenTaskCount = 0;
for (int i = 0; i < nodeList.size(); i++)
{
LodRenderSection renderSection = nodeList.get(i);
if (!renderSection.missingPositionsCalculated())
{
// chunk count
int sectionWidthInChunks = DhSectionPos.getChunkWidth(renderSection.pos);
totalWorldGenChunkCount += sectionWidthInChunks * sectionWidthInChunks;
// task count
totalWorldGenTaskCount += renderSection.ungeneratedPositionCount();
}
else
{
totalWorldGenChunkCount += renderSection.ungeneratedChunkCount();
// 1 since we assume the position can be generated in a single go
// TODO this is a bad assumption, can we determine what the world gen supports and determine it from that?
totalWorldGenTaskCount += 1;
}
}
this.fullDataSourceProvider.setEstimatedRemainingRetrievalChunkCount(totalWorldGenChunkCount);
this.fullDataSourceProvider.setTotalRetrievalPositionCount(totalWorldGenTaskCount);
}
catch (Exception e)
{
LOGGER.error("Unexpected error: "+e.getMessage(), e);
}
finally
{
this.fullDataRetrievalQueueRunning.set(false);
}
}
//===========// //===========//
// debugging // // debugging //
//===========// //===========//
//region debugging
@Override @Override
public void debugRender(DebugRenderer debugRenderer) public void debugRender(DebugRenderer debugRenderer)
@@ -739,11 +904,14 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
} }
} }
//endregion debugging
//==============// //==============//
// base methods // // base methods //
//==============// //==============//
//region base methods
@Override @Override
public void close() public void close()
@@ -751,13 +919,15 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
LOGGER.info("Shutting down LodQuadTree..."); LOGGER.info("Shutting down LodQuadTree...");
DebugRenderer.unregister(this, Config.Client.Advanced.Debugging.DebugWireframe.showQuadTreeRenderStatus); DebugRenderer.unregister(this, Config.Client.Advanced.Debugging.DebugWireframe.showQuadTreeRenderStatus);
Config.Common.WorldGenerator.enableDistantGeneration.removeListener(this);
ThreadPoolExecutor mainCleanupExecutor = ThreadPoolUtil.getCleanupExecutor(); ThreadPoolExecutor mainCleanupExecutor = ThreadPoolUtil.getCleanupExecutor();
// closing every node may take a few moments // closing every node may take a few moments
// so this is run on a separate thread to prevent lagging the render thread // so this is run on a separate thread to prevent lagging the render thread
mainCleanupExecutor.execute(() -> mainCleanupExecutor.execute(() ->
{ {
this.treeReadWriteLock.lock(); this.treeLock.lock();
try try
{ {
// walk through each node // walk through each node
@@ -775,7 +945,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
} }
finally finally
{ {
this.treeReadWriteLock.unlock(); this.treeLock.unlock();
} }
}); });
@@ -783,6 +953,8 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
LOGGER.info("Finished shutting down LodQuadTree"); LOGGER.info("Finished shutting down LodQuadTree");
} }
//endregion base methods
} }
@@ -19,8 +19,6 @@
package com.seibel.distanthorizons.core.render; package com.seibel.distanthorizons.core.render;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource; import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource;
@@ -42,12 +40,10 @@ import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.render.renderer.generic.BeaconRenderHandler; import com.seibel.distanthorizons.core.render.renderer.generic.BeaconRenderHandler;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO; import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo; import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo;
import com.seibel.distanthorizons.core.util.WorldGenUtil;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker; import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import javax.annotation.WillNotClose; import javax.annotation.WillNotClose;
@@ -81,7 +77,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
* contains the list of beacons currently being rendered in this section * contains the list of beacons currently being rendered in this section
* if this list is modified the {@link LodRenderSection#beaconRenderHandler} should be updated to match. * if this list is modified the {@link LodRenderSection#beaconRenderHandler} should be updated to match.
*/ */
private final List<BeaconBeamDTO> activeBeaconList = new ArrayList<>(); private final ArrayList<BeaconBeamDTO> activeBeaconList = new ArrayList<>();
@Nullable @Nullable
public final BeaconRenderHandler beaconRenderHandler; public final BeaconRenderHandler beaconRenderHandler;
@Nullable @Nullable
@@ -114,19 +110,6 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
* different threads (buffer uploading is on the MC render thread) and need to be canceled separately. * different threads (buffer uploading is on the MC render thread) and need to be canceled separately.
*/ */
private CompletableFuture<LodBufferContainer> bufferUploadFuture = null; private CompletableFuture<LodBufferContainer> bufferUploadFuture = null;
/**
* should be an empty array if no positions need to be generated
*
* @deprecated see the comment where this variable is set
*/
@Nullable
@Deprecated
private Supplier<LongArrayList> missingGenerationPosFunc;
private LongArrayList getMissingGenerationPos() { return this.missingGenerationPosFunc != null ? this.missingGenerationPosFunc.get() : null; }
private boolean checkedIfFullDataSourceExists = false;
private boolean fullDataSourceExists = false;
@@ -150,7 +133,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
this.beaconRenderHandler = this.quadTree.beaconRenderHandler; this.beaconRenderHandler = this.quadTree.beaconRenderHandler;
this.beaconBeamRepo = this.level.getBeaconBeamRepo(); this.beaconBeamRepo = this.level.getBeaconBeamRepo();
DebugRenderer.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionStatus); DebugRenderer.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionStatus);
} }
@@ -258,24 +241,10 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
// get the adjacent positions // get the adjacent positions
// needs to be done async to prevent threads waiting on the same positions to be processed // needs to be done async to prevent threads waiting on the same positions to be processed
final CompletableFuture<ColumnRenderSource>[] adjacentLoadFutures = new CompletableFuture[4]; final CompletableFuture<ColumnRenderSource>[] adjacentLoadFutures = new CompletableFuture[4];
adjacentLoadFutures[0] = this.getRenderSourceForPosAsync(this.pos, EDhDirection.NORTH);
if (Config.Client.Advanced.Graphics.Experimental.onlyLoadCenterLods.get()) adjacentLoadFutures[1] = this.getRenderSourceForPosAsync(this.pos, EDhDirection.SOUTH);
{ adjacentLoadFutures[2] = this.getRenderSourceForPosAsync(this.pos, EDhDirection.EAST);
// TODO temporary test, long term something else should be done to so we can get adjacent lighting data adjacentLoadFutures[3] = this.getRenderSourceForPosAsync(this.pos, EDhDirection.WEST);
// probably a change to the LOD data format
adjacentLoadFutures[0] = CompletableFuture.completedFuture(null);
adjacentLoadFutures[1] = CompletableFuture.completedFuture(null);
adjacentLoadFutures[2] = CompletableFuture.completedFuture(null);
adjacentLoadFutures[3] = CompletableFuture.completedFuture(null);
}
else
{
adjacentLoadFutures[0] = this.getRenderSourceForPosAsync(this.pos, EDhDirection.NORTH);
adjacentLoadFutures[1] = this.getRenderSourceForPosAsync(this.pos, EDhDirection.SOUTH);
adjacentLoadFutures[2] = this.getRenderSourceForPosAsync(this.pos, EDhDirection.EAST);
adjacentLoadFutures[3] = this.getRenderSourceForPosAsync(this.pos, EDhDirection.WEST);
}
return CompletableFuture.allOf(adjacentLoadFutures).thenRun(() -> return CompletableFuture.allOf(adjacentLoadFutures).thenRun(() ->
{ {
try (ColumnRenderSource northRenderSource = adjacentLoadFutures[0].get(); try (ColumnRenderSource northRenderSource = adjacentLoadFutures[0].get();
@@ -453,121 +422,6 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
//=================================//
// full data retrieval (world gen) //
//=================================//
//region full data retrieval
public boolean isFullyGenerated()
{
LongArrayList missingGenerationPos = this.getMissingGenerationPos();
return missingGenerationPos != null && missingGenerationPos.isEmpty();
}
/** Returns true if an LOD exists, regardless of what data is in it */
public boolean getFullDataSourceExists()
{
if (!this.checkedIfFullDataSourceExists)
{
this.fullDataSourceExists = this.fullDataSourceProvider.repo.existsWithKey(this.pos);
this.checkedIfFullDataSourceExists = true;
}
return this.fullDataSourceExists;
}
public void updateFullDataSourceExists()
{
// we don't have any ability to remove LODs so we only
// need to check if an LOD was previously missing
if (!this.fullDataSourceExists)
{
this.checkedIfFullDataSourceExists = false;
this.getFullDataSourceExists();
}
}
public boolean missingPositionsCalculated() { return this.getMissingGenerationPos() != null; }
public int ungeneratedPositionCount()
{
LongArrayList missingGenerationPos = this.getMissingGenerationPos();
return missingGenerationPos != null ? missingGenerationPos.size() : 0;
}
public int ungeneratedChunkCount()
{
LongArrayList missingGenerationPos = this.getMissingGenerationPos();
if (missingGenerationPos == null)
{
return 0;
}
int chunkCount = 0;
// get the number of chunks each position contains
for (int i = 0; i < missingGenerationPos.size(); i++)
{
int chunkWidth = DhSectionPos.getChunkWidth(missingGenerationPos.getLong(i));
chunkCount += (chunkWidth * chunkWidth);
}
return chunkCount;
}
public void tryQueuingMissingLodRetrieval()
{
if (this.fullDataSourceProvider.canRetrieveMissingDataSources()
&& this.fullDataSourceProvider.canQueueRetrieval())
{
// calculate the missing positions if not already done
if (this.missingGenerationPosFunc == null)
{
// TODO memoization is needed for multiplayer, otherwise
// new retrieval requests won't be submitted.
// TODO why is that the case? Shouldn't the missing positions be un-changing?
// TODO setting this value to low can cause world gen to slow down significantly
// due to a race condition where the world gen thinks it is finished, but the results
// haven't been saved to file yet, causing the gen to fire again
this.missingGenerationPosFunc = Suppliers.memoizeWithExpiration(
() -> this.fullDataSourceProvider.getPositionsToRetrieve(this.pos),
10, TimeUnit.MINUTES);
}
LongArrayList missingGenerationPos = this.getMissingGenerationPos();
if (missingGenerationPos != null)
{
// queue from last to first to prevent shifting the array unnecessarily
for (int i = missingGenerationPos.size() - 1; i >= 0; i--)
{
if (!this.fullDataSourceProvider.canQueueRetrieval())
{
// the data source provider isn't accepting any more jobs
break;
}
long pos = missingGenerationPos.removeLong(i);
boolean posInRange = WorldGenUtil.isPosInWorldGenRange(
pos,
Config.Common.WorldGenerator.generationCenterChunkX.get(), Config.Common.WorldGenerator.generationCenterChunkZ.get(),
Config.Common.WorldGenerator.generationMaxChunkRadius.get()
);
if (!posInRange)
{
continue;
}
boolean positionQueued = (this.fullDataSourceProvider.queuePositionForRetrieval(pos) != null);
if (!positionQueued)
{
// shouldn't normally happen, but just in case
missingGenerationPos.add(pos);
}
}
}
}
}
//endregion full data retrieval
//=================// //=================//
// beacon handling // // beacon handling //
//=================// //=================//
@@ -592,20 +446,15 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
// stop rendering current beacons // stop rendering current beacons
for (BeaconBeamDTO beam : this.activeBeaconList) this.beaconRenderHandler.stopRenderingBeacons(this.activeBeaconList);
{
this.beaconRenderHandler.stopRenderingBeaconAtPos(beam.blockPos);
}
// swap old and new active beacon list // swap old and new active beacon list
this.activeBeaconList.clear(); this.activeBeaconList.clear();
this.activeBeaconList.addAll(activeBeacons); this.activeBeaconList.addAll(activeBeacons);
// start rendering new beacon list // start rendering new beacon list
for (BeaconBeamDTO beam : this.activeBeaconList) byte absoluteDetailLevel = (byte)(DhSectionPos.getDetailLevel(this.pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
{ this.beaconRenderHandler.startRenderingBeacons(this.activeBeaconList, absoluteDetailLevel);
this.beaconRenderHandler.startRenderingBeacon(beam);
}
} }
} }
@@ -620,10 +469,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
synchronized (this.activeBeaconList) synchronized (this.activeBeaconList)
{ {
for (BeaconBeamDTO beam : this.activeBeaconList) this.beaconRenderHandler.stopRenderingBeacons(this.activeBeaconList);
{
this.beaconRenderHandler.stopRenderingBeaconAtPos(beam.blockPos);
}
} }
} }
@@ -638,10 +484,8 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
synchronized (this.activeBeaconList) synchronized (this.activeBeaconList)
{ {
for (BeaconBeamDTO beam : this.activeBeaconList) byte absoluteDetailLevel = (byte)(DhSectionPos.getDetailLevel(this.pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
{ this.beaconRenderHandler.startRenderingBeacons(this.activeBeaconList, absoluteDetailLevel);
this.beaconRenderHandler.startRenderingBeacon(beam);
}
} }
} }
@@ -712,13 +556,14 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
{ {
// remove the task from our executor if present // remove the task from our executor if present
// note: don't cancel the task since that prevents cleanup, we just don't want it to run // note: don't cancel the task since that prevents cleanup, we just don't want it to run
PriorityTaskPicker.Executor executor = ThreadPoolUtil.getRenderLoadingExecutor(); PriorityTaskPicker.Executor renderLoaderExecutor = ThreadPoolUtil.getRenderLoadingExecutor();
if (executor != null && !executor.isTerminated()) if (renderLoaderExecutor != null
&& !renderLoaderExecutor.isTerminated())
{ {
Runnable runnable = this.getAndBuildRenderDataRunnable; Runnable runnable = this.getAndBuildRenderDataRunnable;
if (runnable != null) if (runnable != null)
{ {
executor.remove(runnable); renderLoaderExecutor.remove(runnable);
} }
} }
} }
@@ -729,14 +574,6 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
uploadFuture.cancel(true); uploadFuture.cancel(true);
} }
// remove any active world gen requests that may be for this position
ThreadPoolExecutor executor = ThreadPoolUtil.getCleanupExecutor();
// while this should generally be a fast operation
// this is run on a separate thread to prevent lag on the render thread
executor.execute(() -> this.fullDataSourceProvider.removeRetrievalRequestIf((genPos) -> DhSectionPos.contains(this.pos, genPos)));
} }
//endregion base methods //endregion base methods
@@ -0,0 +1,444 @@
package com.seibel.distanthorizons.core.render.renderer;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass;
import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.util.math.Mat4f;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftGLWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import org.joml.Matrix4f;
import org.joml.Vector4f;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.GL46;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.function.Function;
public class DepthCalculator
{
public static DepthCalculator INSTANCE = new DepthCalculator();
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
private static final IMinecraftGLWrapper GLMC = SingletonInjector.INSTANCE.get(IMinecraftGLWrapper.class);
private float[] mcOpaqueDepthTextureValues = new float[1];
private float[] mcTransparentDepthTextureValues = new float[1];
private float[] dhDepthTextureValues = new float[1];
private float[] outDepthTextureValues = new float[1];
private float lastClosestDhDepth = 1.0f;
public float actualMcBlockDistance = 8 * 16; // needs to be non-zero to start DH rendering
@Deprecated // Replace with thread pool and an AtomicBool for running state
public Thread thread = null;
public boolean pause = false;
private boolean gotDhDepthThisFrame = false;
private boolean gotMcDepthThisFrame = false;
//=============//
// constructor //
//=============//
private DepthCalculator() { }
//======//
// test //
//======//
public void getMcOpaqueDepthTexture() { this.gotMcDepthThisFrame = this.trySetDepthTexture(MC_RENDER.getDepthTextureId(), this.mcOpaqueDepthTextureValues); }
public void getMcTransparentDepthTexture() { this.gotMcDepthThisFrame = this.trySetDepthTexture(MC_RENDER.getDepthTextureId(), this.mcTransparentDepthTextureValues); }
public void trySetDhDepthTexture() { this.gotDhDepthThisFrame = this.trySetDepthTexture(LodRenderer.INSTANCE.getActiveDepthTextureId(), this.dhDepthTextureValues); }
private boolean trySetDepthTexture(int id, float[] outputRef)
{
// don't change the texture if a process is already running
if (this.thread != null)
{
return false;
}
this.resizeTexturesIfNeeded();
if (id == -1)
{
return false;
}
// FIXME this is slow and causes frame stuttering
GL46.glGetTextureImage(id, 0, GL32.GL_DEPTH_COMPONENT, GL32.GL_FLOAT, outputRef);
return true;
}
private void resizeTexturesIfNeeded()
{
int width = MC_RENDER.getTargetFramebufferViewportWidth();
int height = MC_RENDER.getTargetFramebufferViewportHeight();
int elementCount = width * height;
if (this.dhDepthTextureValues.length != elementCount)
{
this.mcOpaqueDepthTextureValues = new float[width * height];
this.mcTransparentDepthTextureValues = new float[width * height];
this.dhDepthTextureValues = new float[width * height];
this.outDepthTextureValues = new float[width * height];
}
}
public void tryCalculateAsync()
{
if (this.thread != null)
{
return;
}
//if (!this.gotDhDepthThisFrame
// || !this.gotMcDepthThisFrame)
//{
// return;
//}
if (this.pause)
{
int k = 0;
}
this.thread = new Thread(() ->
{
try
{
this.calculateDepth();
}
catch (Exception e)
{
LOGGER.error("async test: " + e.getMessage(), e);
}
finally
{
this.thread = null;
this.pause = false;
}
});
this.thread.start();
}
public void calculateDepth()
{
int width = MC_RENDER.getTargetFramebufferViewportWidth();
int height = MC_RENDER.getTargetFramebufferViewportHeight();
int elementCount = width * height;
// used to calculate the DH render matrices
RenderParams renderParams =
new RenderParams(
EDhApiRenderPass.OPAQUE,
ClientApi.RENDER_STATE.frameTime,
ClientApi.RENDER_STATE.mcProjectionMatrix, ClientApi.RENDER_STATE.mcModelViewMatrix,
ClientApi.RENDER_STATE.clientLevelWrapper
);
Mat4f dhInvProj = new Mat4f(renderParams.dhProjectionMatrix);
dhInvProj.invert();
Matrix4f dhInvProjJoml = dhInvProj.createJomlMatrix();
Mat4f dhInvMvm = new Mat4f(renderParams.dhModelViewMatrix);
dhInvMvm.invert();
Matrix4f dhInvMvmJoml = dhInvMvm.createJomlMatrix(); // TODO can we use JOML for MC 1.16?
float[] sampledDistances = new float[9];
// find the closest depth value MC hasn't drawn to
float closestDhDepth = 1.0f; float closestDhDistance = Float.MAX_VALUE;
int closeUIndex = 0; int closeVIndex = 0;
for (int u = 0; u < width; u++) // x
{
for (int v = 0; v < height; v++) // y
{
int invertedV = height - 1 - v;
int i = (invertedV * width) + u;
this.outDepthTextureValues[i] = 0.0f;
float mcDepth = Config.Client.dynamicFadeUseOpaqueMcDepth.get()
? this.mcOpaqueDepthTextureValues[i]
: this.mcTransparentDepthTextureValues[i];
if (mcDepth < 1.0f) // ignore positions MC has drawn to
{
continue;
}
float dhDepth = this.dhDepthTextureValues[i];
if (dhDepth == 1.0f) // ignore positions DH has NOT drawn to
{
continue;
}
this.outDepthTextureValues[i] = dhDepth;
// calculate this point's distance from the camera
float ndcU = u / (float)width;
float ndcV = invertedV / (float)height;
float dist = convertDepthToBlockDistance(ndcU, ndcV, dhInvProjJoml, dhInvMvmJoml, dhDepth);
//// sample the 9 surrounding pixels to account for off-by-one errors between the MC and DH depth textures
//int sampleIndex = 0;
//for (int relU = -1; relU < 1; relU++)
//{
// for (int relV = -1; relV < 1; relV++)
// {
// invertedV = height - 1 - v + relV;
// i = (invertedV * width) + u + relU;
// dhDepth = this.dhDepthTextureValues[i];
//
// if (v + relV < 0 || v + relV > height
// || u + relU < 0 || u + relU > width)
// {
// sampledDistances[sampleIndex] = 0.0f;
// sampleIndex++;
// }
//
// // calculate this point's distance from the camera
// float ndcU = u / (float)width;
// float ndcV = invertedV / (float)height;
// float dist = convertDepthToBlockDistance(ndcU, ndcV, dhInvProjJoml, dhInvMvmJoml, dhDepth);
//
// sampledDistances[sampleIndex] = dist;
// sampleIndex++;
// }
//}
//
//
//Arrays.sort(sampledDistances);
//// return the median element
//float dist = sampledDistances[sampledDistances.length / 2];
//if (dist == 0.0f)
//{
// // the median was 0, return the smallest non-zero element
// for (sampleIndex = 0; sampleIndex < sampledDistances.length; sampleIndex++)
// {
// if (sampledDistances[sampleIndex] != 0.0f)
// {
// dist = sampledDistances[sampleIndex];
// break;
// }
// }
//}
if (dist < closestDhDistance)
{
closestDhDepth = dhDepth;
closestDhDistance = dist;
closeUIndex = u;
closeVIndex = invertedV;
}
}
}
if (this.lastClosestDhDepth != closestDhDepth)
{
NumberFormat numForm = NumberFormat.getNumberInstance();
ClientApi.INSTANCE.showChatMessageNextFrame(
"closest: ["+numForm.format(closestDhDepth)+"]-b["+numForm.format(closestDhDistance)+"]c["+numForm.format(closestDhDistance/16)+"] ("+(closeUIndex)+","+(closeVIndex)+")" +
"");
this.actualMcBlockDistance = closestDhDistance;
}
this.lastClosestDhDepth = closestDhDepth;
if (this.pause)
{
// find the range of depth values used by both textures for clearer exporting
float closestMcDepth = 1.0f; float furthestMcDepth = 0.0f;
float minDhDepth = 1.0f; float maxDhDepth = 0.0f;
for (int i = 0; i < elementCount; i++)
{
float mcDepth = this.mcOpaqueDepthTextureValues[i];
if (mcDepth != 0.0f && mcDepth != 1.0f)
{
if (mcDepth < closestMcDepth)
{
closestMcDepth = mcDepth;
}
if (mcDepth > furthestMcDepth)
{
furthestMcDepth = mcDepth;
}
}
float dhDepth = this.dhDepthTextureValues[i];
if (dhDepth != 0.0f && dhDepth != 1.0f)
{
if (dhDepth < minDhDepth)
{
minDhDepth = dhDepth;
}
if (dhDepth > maxDhDepth)
{
maxDhDepth = dhDepth;
}
}
}
String exportPath = Config.Client.dynamicFadeExportPath.get();
ClientApi.INSTANCE.showChatMessageNextFrame("exporting debug textures to: ["+exportPath+"]...");
// mc opaque
createImg(
this.mcOpaqueDepthTextureValues,
closeUIndex, closeVIndex,
(Float depth) -> { return null; },
width, height, closestMcDepth, furthestMcDepth,
exportPath+"mc-opaque_depth.png");
// mc transparent
createImg(
this.mcTransparentDepthTextureValues,
closeUIndex, closeVIndex,
(Float depth) -> { return null; },
width, height, closestMcDepth, furthestMcDepth,
exportPath+"mc-tran_depth.png");
// dh
Function<Float, Color> customColorFunc = (Float depth) ->
{
if (depth == this.lastClosestDhDepth)
{
return Color.RED;
}
//else if (depth <= (lastClosestDhDepth + 0.01f))
//{
// return Color.ORANGE;
//}
return null;
};
createImg(
this.dhDepthTextureValues,
closeUIndex, closeVIndex,
customColorFunc,
width, height, minDhDepth, maxDhDepth,
exportPath+"dh_depth.png");
// temp
createImg(
this.outDepthTextureValues,
closeUIndex, closeVIndex,
(Float depth) -> { return null; },
width, height, minDhDepth, maxDhDepth,
exportPath+"temp_depth.png");
int breakPoint = 0;
}
}
/** NDC (Normalized Device Coordinates) must be between 0.0 and 1.0 (inclusive) */
private static float convertDepthToBlockDistance(float ndcU, float ndcV, Matrix4f invProj, Matrix4f invMvm, float depth)
{
// This assumes depth is scaled to [0, 1]
// Transform depth to clip space Z value
float z = depth * 2.0f - 1.0f;
// Create a vector in clip space
Vector4f clipSpacePosition = new Vector4f(ndcU, ndcV, z, 1.0f);
// Transform to world space
Vector4f worldSpacePosition = clipSpacePosition.mul(invProj);
worldSpacePosition.div(worldSpacePosition.w); // Perform perspective divide
// Finally apply the inverse model-view matrix to get world space coordinates
worldSpacePosition = worldSpacePosition.mul(invMvm);
// calculate distance from the camera
float distance = worldSpacePosition.distance(0, 0, 0, 0);
return distance;
}
private static void createImg(
float[] tex,
int nearestU, int nearestV,
Function<Float, Color> customColorFunc,
int width, int height,
float minDepth, float maxDepth,
String filePath)
{
// Create a BufferedImage
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int u = 0; u < width; u++)
{
for (int v = 0; v < height; v++)
{
int invertedY = height - 1 - v;
// Normalize the depth value to a grayscale pixel
float depthValue = tex[(invertedY * width) + u];
Color color = customColorFunc.apply(depthValue);
if (color == null)
{
float normalizedDepth = (depthValue - minDepth) / (maxDepth - minDepth); // Normalize to 0.0 to 1.0
normalizedDepth = Math.max(0.0f, Math.min(1.0f, normalizedDepth)); // Clamp to valid range
int gray = (int) (normalizedDepth * 255); // Map to 0-255
gray = Math.max(0, Math.min(255, gray)); // Clamp to valid range
color = new Color(gray, gray, gray);
}
if (depthValue == 1.0f)
{
color = new Color(0,0,0,0);
}
if (u == nearestU)
{
color = Color.MAGENTA;
}
else if (invertedY == nearestV)
{
color = Color.ORANGE;
}
image.setRGB(u, v, color.getRGB());
}
}
// Write the image to a file
try
{
ImageIO.write(image, "png", new File(filePath));
}
catch (IOException e)
{
LOGGER.error("Unable to write texture to file, error: ["+e.getMessage()+"].", e);
}
}
}
@@ -23,6 +23,7 @@ import com.seibel.distanthorizons.api.interfaces.override.rendering.IDhApiShader
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.api.objects.math.DhApiVec3f; import com.seibel.distanthorizons.api.objects.math.DhApiVec3f;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.LodQuadBuilder;
import com.seibel.distanthorizons.core.render.glObject.GLProxy; import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.render.glObject.shader.Shader; import com.seibel.distanthorizons.core.render.glObject.shader.Shader;
import com.seibel.distanthorizons.core.render.glObject.shader.ShaderProgram; import com.seibel.distanthorizons.core.render.glObject.shader.ShaderProgram;
@@ -37,6 +38,7 @@ import com.seibel.distanthorizons.core.util.math.Vec3f;
/** /**
* Handles rendering the normal LOD terrain. * Handles rendering the normal LOD terrain.
* @see LodQuadBuilder
*/ */
public class DhTerrainShaderProgram extends ShaderProgram implements IDhApiShaderProgram public class DhTerrainShaderProgram extends ShaderProgram implements IDhApiShaderProgram
{ {
@@ -46,16 +48,14 @@ public class DhTerrainShaderProgram extends ShaderProgram implements IDhApiShade
public int uCombinedMatrix = -1; public int uCombinedMatrix = -1;
public int uModelOffset = -1; public int uModelOffset = -1;
public int uWorldYOffset = -1; public int uWorldYOffset = -1;
public int uDitherDhRendering = -1;
public int uMircoOffset = -1; public int uMircoOffset = -1;
public int uEarthRadius = -1; public int uEarthRadius = -1;
public int uLightMap = -1; public int uLightMap = -1;
// Fog/Clip Uniforms // fragment shader uniforms
public int uClipDistance = -1; public int uClipDistance = -1;
public int uDitherDhRendering = -1;
// Noise Uniforms // Noise Uniforms
public int uNoiseEnabled = -1; public int uNoiseEnabled = -1;
@@ -76,19 +76,16 @@ public class DhTerrainShaderProgram extends ShaderProgram implements IDhApiShade
public DhTerrainShaderProgram() public DhTerrainShaderProgram()
{ {
super( super(
() -> Shader.loadFile(Config.Client.Advanced.Graphics.Experimental.earthCurveRatio.get() != 0 () -> Shader.loadFile("shaders/standard.vert", false, new StringBuilder()).toString(),
? "shaders/curve.vert" () -> Shader.loadFile("shaders/flat_shaded.frag", false, new StringBuilder()).toString(),
: "shaders/standard.vert", "fragColor", new String[]{"vPosition", "color"});
false, new StringBuilder()).toString(),
() -> Shader.loadFile("shaders/flat_shaded.frag", false, new StringBuilder()).toString(),
"fragColor", new String[]{"vPosition", "color"});
this.uCombinedMatrix = this.getUniformLocation("uCombinedMatrix"); this.uCombinedMatrix = this.getUniformLocation("uCombinedMatrix");
this.uModelOffset = this.getUniformLocation("uModelOffset"); this.uModelOffset = this.getUniformLocation("uModelOffset");
this.uWorldYOffset = this.tryGetUniformLocation("uWorldYOffset"); this.uWorldYOffset = this.getUniformLocation("uWorldYOffset");
this.uDitherDhRendering = this.tryGetUniformLocation("uDitherDhRendering"); this.uDitherDhRendering = this.getUniformLocation("uDitherDhRendering");
this.uMircoOffset = this.getUniformLocation("uMircoOffset"); this.uMircoOffset = this.getUniformLocation("uMircoOffset");
this.uEarthRadius = this.tryGetUniformLocation("uEarthRadius"); this.uEarthRadius = this.getUniformLocation("uEarthRadius");
this.uLightMap = this.getUniformLocation("uLightMap"); this.uLightMap = this.getUniformLocation("uLightMap");
@@ -117,10 +114,13 @@ public class DhTerrainShaderProgram extends ShaderProgram implements IDhApiShade
} }
this.vao.bind(); this.vao.bind();
// TODO comment what each attribute represents // short: x, y, z, meta
this.vao.setVertexAttribute(0, 0, VertexPointer.addUnsignedShortsPointer(4, false, true)); // 2+2+2+2 // TODO probably color, blockpos // meta: byte skylight, byte blocklight, byte microOffset
this.vao.setVertexAttribute(0, 1, VertexPointer.addUnsignedBytesPointer(4, true, false)); // +4 // TODO ? this.vao.setVertexAttribute(0, 0, VertexPointer.addUnsignedShortsPointer(4, false, true));
this.vao.setVertexAttribute(0, 2, VertexPointer.addUnsignedBytesPointer(4, true, true)); // +4 // TODO probably normal index and Iris block ID // byte: r, g, b, a
this.vao.setVertexAttribute(0, 1, VertexPointer.addUnsignedBytesPointer(4, true, false));
// byte: iris material ID, normal index, 2 spacers
this.vao.setVertexAttribute(0, 2, VertexPointer.addUnsignedBytesPointer(4, true, true));
try try
{ {
@@ -178,12 +178,21 @@ public class DhTerrainShaderProgram extends ShaderProgram implements IDhApiShade
// setUniform(skyLightUniform, skyLight); // setUniform(skyLightUniform, skyLight);
this.setUniform(this.uLightMap, 0); // TODO this should probably be passed in this.setUniform(this.uLightMap, 0); // TODO this should probably be passed in
if (this.uWorldYOffset != -1) this.setUniform(this.uWorldYOffset, (float) renderParameters.worldYOffset); this.setUniform(this.uWorldYOffset, (float) renderParameters.worldYOffset);
if (this.uDitherDhRendering != -1) this.setUniform(this.uDitherDhRendering, Config.Client.Advanced.Graphics.Quality.ditherDhFade.get()); this.setUniform(this.uDitherDhRendering, Config.Client.Advanced.Graphics.Quality.ditherDhFade.get());
if (this.uEarthRadius != -1) this.setUniform(this.uEarthRadius, float curveRatio = Config.Client.Advanced.Graphics.Experimental.earthCurveRatio.get();
/*6371KM*/ 6371000.0f / Config.Client.Advanced.Graphics.Experimental.earthCurveRatio.get()); if (curveRatio < -1.0f || curveRatio > 1.0f)
{
curveRatio = /*6371KM*/ 6371000.0f / curveRatio;
}
else
{
// disable curvature if the config value is between -1 and 1
curveRatio = 0.0f;
}
this.setUniform(this.uEarthRadius, curveRatio);
// Noise Uniforms // Noise Uniforms
this.setUniform(this.uNoiseEnabled, Config.Client.Advanced.Graphics.NoiseTexture.enableNoiseTexture.get()); this.setUniform(this.uNoiseEnabled, Config.Client.Advanced.Graphics.NoiseTexture.enableNoiseTexture.get());
@@ -195,17 +204,17 @@ public class DhTerrainShaderProgram extends ShaderProgram implements IDhApiShade
this.setUniform(this.uIsWhiteWorld, Config.Client.Advanced.Debugging.enableWhiteWorld.get()); this.setUniform(this.uIsWhiteWorld, Config.Client.Advanced.Debugging.enableWhiteWorld.get());
// Clip Uniform // Clip Uniform
float dhNearClipDistance = RenderUtil.getNearClipPlaneInBlocksForFading(renderParameters.partialTicks); float dhNearClipDistance = 0.1f;//RenderUtil.getNearClipPlaneInBlocksForFading(renderParameters.partialTicks);
if (!Config.Client.Advanced.Debugging.lodOnlyMode.get()) //if (!Config.Client.Advanced.Debugging.lodOnlyMode.get())
{ //{
// this added value prevents the near clip plane and discard circle from touching, which looks bad // // this added value prevents the near clip plane and discard circle from touching, which looks bad
dhNearClipDistance += 16f; // dhNearClipDistance += 16f;
} //}
// if the player is very high up and the near clip plane has been modified, disable the distance clipping // if the player is very high up and the near clip plane has been modified, disable the distance clipping
// we're high enough that nothing will render on top of the player and this can cause issues otherwise // we're high enough that nothing will render on top of the player and this can cause issues otherwise
if (RenderUtil.getHeightBasedNearClipOverride() != -1) if (RenderUtil.getHeightBasedNearClipOverride() != -1)
{ {
dhNearClipDistance = 1.0f; dhNearClipDistance = 1.0f; // TODO does this actually disable anything?
} }
this.setUniform(this.uClipDistance, dhNearClipDistance); this.setUniform(this.uClipDistance, dhNearClipDistance);
} }
@@ -222,6 +222,8 @@ public class LodRenderer
profiler.popPush("LOD Opaque"); profiler.popPush("LOD Opaque");
this.renderLodPass(lodShaderProgram, renderBufferHandler, renderParams, /*opaquePass*/ true); this.renderLodPass(lodShaderProgram, renderBufferHandler, renderParams, /*opaquePass*/ true);
DepthCalculator.INSTANCE.trySetDhDepthTexture();
// custom objects with SSAO // custom objects with SSAO
if (Config.Client.Advanced.Graphics.GenericRendering.enableGenericRendering.get()) if (Config.Client.Advanced.Graphics.GenericRendering.enableGenericRendering.get())
{ {
@@ -434,7 +436,7 @@ public class LodRenderer
// resize the textures if needed // resize the textures if needed
if (MC_RENDER.getTargetFramebufferViewportWidth() != this.textureWidth if (MC_RENDER.getTargetFramebufferViewportWidth() != this.textureWidth
|| MC_RENDER.getTargetFramebufferViewportHeight() != this.textureHeight) || MC_RENDER.getTargetFramebufferViewportHeight() != this.textureHeight)
{ {
// just resizing the textures doesn't work when Optifine is present, // just resizing the textures doesn't work when Optifine is present,
// so recreate the textures with the new size instead // so recreate the textures with the new size instead
@@ -536,7 +538,7 @@ public class LodRenderer
return true; return true;
} }
@SuppressWarnings( "deprecation" ) @SuppressWarnings( "deprecation" ) // done to ignore DhApiColorDepthTextureCreatedEvent
private void createAndBindTextures() private void createAndBindTextures()
{ {
int oldWidth = this.textureWidth; int oldWidth = this.textureWidth;
@@ -30,6 +30,7 @@ public class RenderParams extends DhApiRenderParam
public IDhClientWorld dhClientWorld; public IDhClientWorld dhClientWorld;
public IDhClientLevel dhClientLevel; public IDhClientLevel dhClientLevel;
/** more specific override of the API value {@link DhApiRenderParam#clientLevelWrapper} */
public IClientLevelWrapper clientLevelWrapper; public IClientLevelWrapper clientLevelWrapper;
public ILightMapWrapper lightmap; public ILightMapWrapper lightmap;
public RenderBufferHandler renderBufferHandler; public RenderBufferHandler renderBufferHandler;
@@ -56,7 +57,8 @@ public class RenderParams extends DhApiRenderParam
RenderUtil.getNearClipPlaneDistanceInBlocks(newPartialTicks), RenderUtil.getFarClipPlaneDistanceInBlocks(), RenderUtil.getNearClipPlaneDistanceInBlocks(newPartialTicks), RenderUtil.getFarClipPlaneDistanceInBlocks(),
newMcProjectionMatrix, newMcModelViewMatrix, newMcProjectionMatrix, newMcModelViewMatrix,
RenderUtil.createLodProjectionMatrix(newMcProjectionMatrix, newPartialTicks), RenderUtil.createLodModelViewMatrix(newMcModelViewMatrix), RenderUtil.createLodProjectionMatrix(newMcProjectionMatrix, newPartialTicks), RenderUtil.createLodModelViewMatrix(newMcModelViewMatrix),
clientLevelWrapper.getMinHeight()); clientLevelWrapper.getMinHeight(),
clientLevelWrapper);
this.dhClientWorld = SharedApi.tryGetDhClientWorld(); this.dhClientWorld = SharedApi.tryGetDhClientWorld();
@@ -28,6 +28,7 @@ import com.seibel.distanthorizons.api.objects.render.DhApiRenderableBoxGroupShad
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO; import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
@@ -39,8 +40,7 @@ import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.*;
import java.util.HashSet;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@@ -54,6 +54,8 @@ public class BeaconRenderHandler
/** how often should we check if a beacon should be culled? */ /** how often should we check if a beacon should be culled? */
private static final int MAX_CULLING_FREQUENCY_IN_MS = 1_000; private static final int MAX_CULLING_FREQUENCY_IN_MS = 1_000;
private static final Comparator<BeaconBeamDTO> NEGATIVE_BLOCKPOS_COMPARATOR = new NegativeInfiniteBlockPosComparator();
private final ReentrantLock updateLock = new ReentrantLock(); private final ReentrantLock updateLock = new ReentrantLock();
@@ -89,20 +91,74 @@ public class BeaconRenderHandler
// render handling // // render handling //
//=================// //=================//
public void startRenderingBeacon(BeaconBeamDTO beacon) public void startRenderingBeacons(ArrayList<BeaconBeamDTO> beaconList, byte detailLevel)
{ {
try try
{ {
this.updateLock.lock(); this.updateLock.lock();
if (this.beaconBlockPosSet.add(beacon.blockPos))
// how wide should each beacon be?
int beaconBlockWidth = 1;
if (Config.Client.Advanced.Graphics.GenericRendering.expandDistantBeacons.get())
{ {
beaconBlockWidth = DhSectionPos.getBlockWidth(detailLevel);
}
ArrayList<BeaconBeamDTO> sortedBeaconList = new ArrayList<>(beaconList);
// merge distant beams if requested
if (Config.Client.Advanced.Graphics.GenericRendering.expandDistantBeacons.get())
{
// sort beacons from neg inf -> pos inf
// so we can consistently merge adjacent beacons
sortedBeaconList.sort(NEGATIVE_BLOCKPOS_COMPARATOR);
// go through each beacon...
for (int outerIndex = 0; outerIndex < sortedBeaconList.size(); outerIndex++)
{
BeaconBeamDTO outerBeacon = sortedBeaconList.get(outerIndex);
DhBlockPos outerBlockPos = outerBeacon.blockPos;
// ...and remove any beacons that are within the block width to prevent overlaps
for (int mergeIndex = outerIndex + 1; mergeIndex < sortedBeaconList.size(); mergeIndex++)
{
BeaconBeamDTO beaconToMerge = sortedBeaconList.get(mergeIndex);
DhBlockPos mergeBlockPos = beaconToMerge.blockPos;
int xDiff = mergeBlockPos.getX() - outerBlockPos.getX();
int zDiff = mergeBlockPos.getZ() - outerBlockPos.getZ();
// merge (remove) this beacon if
// it's close to the outer beacon
if (xDiff < beaconBlockWidth
&& zDiff < beaconBlockWidth)
{
sortedBeaconList.remove(mergeIndex);
mergeIndex--; // minus 1 so we don't go past the end of the array when incrementing in the for loop up top
}
}
}
}
// add each beacon to the renderer
for (int i = 0; i < sortedBeaconList.size(); i++)
{
BeaconBeamDTO beacon = sortedBeaconList.get(i);
if (!this.beaconBlockPosSet.add(beacon.blockPos))
{
continue;
}
int maxBeaconBeamHeight = Config.Client.Advanced.Graphics.GenericRendering.beaconRenderHeight.get(); int maxBeaconBeamHeight = Config.Client.Advanced.Graphics.GenericRendering.beaconRenderHeight.get();
DhApiRenderableBox beaconBox = new DhApiRenderableBox( DhApiRenderableBox beaconBox = new DhApiRenderableBox(
new DhApiVec3d(beacon.blockPos.getX(), beacon.blockPos.getY() + 1, beacon.blockPos.getZ()), new DhApiVec3d(beacon.blockPos.getX(), beacon.blockPos.getY() + 1, beacon.blockPos.getZ()),
new DhApiVec3d(beacon.blockPos.getX() + 1, maxBeaconBeamHeight, beacon.blockPos.getZ() + 1), new DhApiVec3d(beacon.blockPos.getX() + beaconBlockWidth, maxBeaconBeamHeight, beacon.blockPos.getZ() + beaconBlockWidth),
beacon.color, beacon.color,
EDhApiBlockMaterial.ILLUMINATED EDhApiBlockMaterial.ILLUMINATED
); );
this.beaconBoxGroup.add(beaconBox); this.beaconBoxGroup.add(beaconBox);
@@ -116,19 +172,26 @@ public class BeaconRenderHandler
} }
} }
public void stopRenderingBeaconAtPos(DhBlockPos beaconPos) public void stopRenderingBeacons(ArrayList<BeaconBeamDTO> beaconList)
{ {
try try
{ {
this.updateLock.lock(); this.updateLock.lock();
if (this.beaconBlockPosSet.remove(beaconPos)) for (int i = 0; i < beaconList.size(); i++)
{ {
BeaconBeamDTO beacon = beaconList.get(i);
DhBlockPos beaconPos = beacon.blockPos;
if (!this.beaconBlockPosSet.remove(beaconPos))
{
continue;
}
Predicate<DhApiRenderableBox> removePredicate = (DhApiRenderableBox box) -> Predicate<DhApiRenderableBox> removePredicate = (DhApiRenderableBox box) ->
{ {
return box.minPos.x == beaconPos.getX() return box.minPos.x == beaconPos.getX()
&& box.minPos.y == beaconPos.getY() + 1 // plus 1 because the beam starts above the beacon && box.minPos.y == beaconPos.getY() + 1 // plus 1 because the beam starts above the beacon
&& box.minPos.z == beaconPos.getZ(); && box.minPos.z == beaconPos.getZ();
}; };
this.beaconBoxGroup.removeIf(removePredicate); this.beaconBoxGroup.removeIf(removePredicate);
this.fullBeaconBoxList.removeIf(removePredicate); this.fullBeaconBoxList.removeIf(removePredicate);
@@ -255,4 +318,28 @@ public class BeaconRenderHandler
} }
//================//
// helper classes //
//================//
private static class NegativeInfiniteBlockPosComparator implements Comparator<BeaconBeamDTO>
{
@Override
public int compare(BeaconBeamDTO beacon1, BeaconBeamDTO beacon2)
{
DhBlockPos blockPos1 = beacon1.blockPos;
DhBlockPos blockPos2 = beacon2.blockPos;
// sort by X, then by Z
if (blockPos1.getX() != blockPos2.getX())
{
return Integer.compare(blockPos1.getX(), blockPos2.getX());
}
return Integer.compare(blockPos1.getZ(), blockPos2.getZ());
}
}
} }
@@ -493,7 +493,7 @@ public class CloudRenderHandler
private static boolean[][] getCloudsFromTexture() throws FileNotFoundException, IOException private static boolean[][] getCloudsFromTexture() throws FileNotFoundException, IOException
{ {
final ClassLoader loader = Thread.currentThread().getContextClassLoader(); final ClassLoader loader = CloudRenderHandler.class.getClassLoader();
boolean[][] whitePixels = null; boolean[][] whitePixels = null;
try(InputStream imageInputStream = loader.getResourceAsStream(CLOUD_RESOURCE_TEXTURE_PATH)) try(InputStream imageInputStream = loader.getResourceAsStream(CLOUD_RESOURCE_TEXTURE_PATH))
@@ -19,6 +19,7 @@
package com.seibel.distanthorizons.core.render.renderer.shaders; package com.seibel.distanthorizons.core.render.renderer.shaders;
import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.render.glObject.shader.ShaderProgram; import com.seibel.distanthorizons.core.render.glObject.shader.ShaderProgram;
@@ -114,7 +115,7 @@ public class VanillaFadeShader extends AbstractShaderRenderer
float dhNearClipDistance = RenderUtil.getNearClipPlaneInBlocksForFading(partialTicks); float dhNearClipDistance = RenderUtil.getNearClipPlaneInBlocksForFading(partialTicks);
// this added value prevents the near clip plane and discard circle from touching, which looks bad // this added value prevents the near clip plane and discard circle from touching, which looks bad
dhNearClipDistance += 16f; //dhNearClipDistance += 16f;
// measured in blocks // measured in blocks
// these multipliers in James' tests should provide a fairly smooth transition // these multipliers in James' tests should provide a fairly smooth transition
@@ -173,7 +173,7 @@ public class DatabaseUpdater
/** @throws NullPointerException if any of the script files failed to be read. */ /** @throws NullPointerException if any of the script files failed to be read. */
private static ArrayList<SqlScript> getAutoUpdateScripts() throws NullPointerException, IOException private static ArrayList<SqlScript> getAutoUpdateScripts() throws NullPointerException, IOException
{ {
final ClassLoader loader = Thread.currentThread().getContextClassLoader(); final ClassLoader loader = DatabaseUpdater.class.getClassLoader();
// get the script list // get the script list
@@ -76,6 +76,9 @@ public class BeaconBeamDTO implements IBaseDTO<DhBlockPos>, INetworkObject
public void close() public void close()
{ /* no closing needed */ } { /* no closing needed */ }
@Override
public String toString() { return this.blockPos + " " + this.color; }
} }
@@ -19,8 +19,10 @@
package com.seibel.distanthorizons.core.util; package com.seibel.distanthorizons.core.util;
import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.render.renderer.DepthCalculator;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
@@ -127,13 +129,16 @@ public class RenderUtil
} }
public static float getNearClipPlaneInBlocksForFading(float partialTicks) public static float getNearClipPlaneInBlocksForFading(float partialTicks)
{ {
float overdraw = getAutoOverdrawPrevention(); //float overdraw = getAutoOverdrawPrevention();
return getNearClipPlaneDistanceInBlocks(partialTicks, overdraw); //return getNearClipPlaneDistanceInBlocks(partialTicks, overdraw);
return DepthCalculator.INSTANCE.actualMcBlockDistance;
} }
private static float getNearClipPlaneDistanceInBlocks(float partialTicks, float overdrawPreventionPercent) private static float getNearClipPlaneDistanceInBlocks(float partialTicks, float overdrawPreventionPercent)
{ {
int chunkRenderDistance = MC_RENDER.getRenderDistance(); int chunkRenderDistance = MC_RENDER.getRenderDistance();
int vanillaBlockRenderedDistance = chunkRenderDistance * LodUtil.CHUNK_WIDTH; //float chunkRenderDistance = ClientApi.actualMcBlockDistance / 16.0f;
float vanillaBlockRenderedDistance = chunkRenderDistance * LodUtil.CHUNK_WIDTH;
float nearClipPlane; float nearClipPlane;
if (Config.Client.Advanced.Debugging.lodOnlyMode.get()) if (Config.Client.Advanced.Debugging.lodOnlyMode.get())
@@ -31,12 +31,10 @@ import com.seibel.distanthorizons.coreapi.util.MathUtil;
import com.seibel.distanthorizons.core.util.gridList.MovableGridRingList; import com.seibel.distanthorizons.core.util.gridList.MovableGridRingList;
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
import it.unimi.dsi.fastutil.longs.LongIterator; import it.unimi.dsi.fastutil.longs.LongIterator;
import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.*;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.LongConsumer; import java.util.function.LongConsumer;
/** /**
@@ -98,9 +96,27 @@ public class QuadTree<T>
// getters and setters // // getters and setters //
//=====================// //=====================//
/** @return the value at the given section position. Null will be returned if the value is missing or the position is out of bounds. */
@Nullable
public final T tryGetValue(long pos)
{
QuadNode<T> node = this.tryGetNode(pos);
if (node != null)
{
return node.value;
}
return null;
}
/** @return the node at the given section position, null if out of bounds */
@Nullable
public final QuadNode<T> tryGetNode(long pos) { return this.getOrSetNode(pos, false, null, false); }
/** @return the node at the given section position */ /** @return the node at the given section position */
@Nullable @Nullable
public final QuadNode<T> getNode(long pos) throws IndexOutOfBoundsException { return this.getOrSetNode(pos, false, null, true); } public final QuadNode<T> getNode(long pos) throws IndexOutOfBoundsException { return this.getOrSetNode(pos, false, null, true); }
/** @return the value at the given section position */ /** @return the value at the given section position */
@Nullable @Nullable
public final T getValue(long pos) throws IndexOutOfBoundsException public final T getValue(long pos) throws IndexOutOfBoundsException
@@ -122,16 +138,24 @@ public class QuadTree<T>
return previousValue; return previousValue;
} }
/** @param runBoundaryChecks should only ever be set to true internally for removing out of bound nodes */ /** @param throwIfOutOfBounds if false returns null */
@Nullable @Nullable
protected final QuadNode<T> getOrSetNode(long pos, boolean setNewValue, T newValue, boolean runBoundaryChecks) throws IndexOutOfBoundsException protected final QuadNode<T> getOrSetNode(long pos, boolean setNewValue, T newValue, boolean throwIfOutOfBounds) throws IndexOutOfBoundsException
{ {
if (runBoundaryChecks && !this.isSectionPosInBounds(pos)) if (!this.isSectionPosInBounds(pos))
{ {
int radius = this.diameterInBlocks() / 2; // how should out-of-bounds positions be handled?
DhBlockPos2D minPos = this.getCenterBlockPos().add(new DhBlockPos2D(-radius, -radius)); if (throwIfOutOfBounds)
DhBlockPos2D maxPos = this.getCenterBlockPos().add(new DhBlockPos2D(radius, radius)); {
throw new IndexOutOfBoundsException("QuadTree GetOrSet failed. Position out of bounds, min pos: " + minPos + ", max pos: " + maxPos + ", min detail level: " + this.treeLeafDetailLevel + ", max detail level: " + this.treeRootDetailLevel + ". Given Position: [" + DhSectionPos.toString(pos) + "] = block pos: " + DhSectionPos.convertToDetailLevel(pos, LodUtil.BLOCK_DETAIL_LEVEL)); int radius = this.diameterInBlocks() / 2;
DhBlockPos2D minBlockPos = this.getCenterBlockPos().add(new DhBlockPos2D(-radius, -radius));
DhBlockPos2D maxBlockPos = this.getCenterBlockPos().add(new DhBlockPos2D(radius, radius));
throw new IndexOutOfBoundsException("QuadTree GetOrSet failed. Position out of bounds, min block pos: [" + minBlockPos + "], max block pos: [" + maxBlockPos + "], leaf detail level: [" + this.treeLeafDetailLevel + "], root detail level: [" + this.treeRootDetailLevel + "]. Requested section pos: [" + DhSectionPos.toString(pos) + "].");
}
else
{
return null;
}
} }
@@ -278,46 +302,6 @@ public class QuadTree<T>
removedItemConsumer.accept(quadNode.value); removedItemConsumer.accept(quadNode.value);
} }
}); });
// // remove out of bound nodes and clean up empty nodes
// // Note: this will iterate over a lot of unnecessary nodes, hopefully speed won't be an issue
// Iterator<DhSectionPos> rootNodePosIterator = this.rootNodePosIterator();
// while (rootNodePosIterator.hasNext())
// {
// // get the root node (regular nodeIterators won't return them if they are out of bounds)
// DhSectionPos rootPos = rootNodePosIterator.next();
// QuadNode<T> rootNode = this.getOrSetNode(rootPos, false, null, false);
// if (rootNode == null)
// {
// continue;
// }
//
// // remove any child nodes that are out of bounds
// Iterator<QuadNode<T>> nodeIterator = this.nodeIterator();
// while (nodeIterator.hasNext())
// {
// QuadNode<T> node = nodeIterator.next();
// if(!this.isSectionPosInBounds(node.sectionPos))
// {
// // node is out of bounds
//
// // FIXME(?) this appears to potentially return large nodes that are partially or entirely in bounds
//
// if (node.getNonNullChildCount() == 0)
// {
// // no child nodes, can be safely removed
// nodeIterator.remove();
// }
// else
// {
// // node can't be removed, but its value can be set to null
// node.value = null;
// }
// }
// }
// }
} }
public final DhBlockPos2D getCenterBlockPos() { return this.centerBlockPos; } public final DhBlockPos2D getCenterBlockPos() { return this.centerBlockPos; }
@@ -544,7 +528,9 @@ public class QuadTree<T>
&& this.rootNodeIterator.hasNext()) && this.rootNodeIterator.hasNext())
{ {
long sectionPos = this.rootNodeIterator.nextLong(); long sectionPos = this.rootNodeIterator.nextLong();
QuadNode<T> rootNode = QuadTree.this.getNode(sectionPos);
// try-get to prevent concurrency errors if the tree is being moved while we walk through it
QuadNode<T> rootNode = QuadTree.this.tryGetNode(sectionPos);
if (rootNode != null) if (rootNode != null)
{ {
nodeIterator = this.onlyReturnLeaves ? rootNode.getLeafNodeIterator() : rootNode.getNodeIterator(this.stopIteratingFunc); nodeIterator = this.onlyReturnLeaves ? rootNode.getLeafNodeIterator() : rootNode.getNodeIterator(this.stopIteratingFunc);
@@ -30,7 +30,7 @@ import java.util.function.Consumer;
public class QuadTreeNodeIterator<T> implements Iterator<QuadNode<T>> public class QuadTreeNodeIterator<T> implements Iterator<QuadNode<T>>
{ {
/** lowest numerical value, inclusive */ /** lowest numerical value, inclusive */
private final byte highestDetailLevel; private final byte leafDetailLevel;
private final Queue<QuadNode<T>> validNodesForDetailLevel = new ArrayDeque<>(); private final Queue<QuadNode<T>> validNodesForDetailLevel = new ArrayDeque<>();
@@ -48,8 +48,7 @@ public class QuadTreeNodeIterator<T> implements Iterator<QuadNode<T>>
{ {
this.onlyReturnLeafValues = onlyReturnLeafValues; this.onlyReturnLeafValues = onlyReturnLeafValues;
this.stopIteratingFunc = stopIteratingFunc; this.stopIteratingFunc = stopIteratingFunc;
// TODO the naming conversion for these are flipped in a lot of places this.leafDetailLevel = rootNode.parentTreeLeafDetailLevel;
this.highestDetailLevel = rootNode.parentTreeLeafDetailLevel;
this.iteratorDetailLevel = DhSectionPos.getDetailLevel(rootNode.sectionPos); this.iteratorDetailLevel = DhSectionPos.getDetailLevel(rootNode.sectionPos);
@@ -110,9 +109,9 @@ public class QuadTreeNodeIterator<T> implements Iterator<QuadNode<T>>
@Override @Override
public QuadNode<T> next() public QuadNode<T> next()
{ {
if (this.iteratorDetailLevel < this.highestDetailLevel) if (this.iteratorDetailLevel < this.leafDetailLevel)
{ {
throw new NoSuchElementException("Highest detail level reached [" + this.highestDetailLevel + "]."); throw new NoSuchElementException("Leaf detail level reached [" + this.leafDetailLevel + "].");
} }
if (this.iteratorNodeQueue.size() == 0) if (this.iteratorNodeQueue.size() == 0)
{ {
@@ -133,7 +132,7 @@ public class QuadTreeNodeIterator<T> implements Iterator<QuadNode<T>>
this.iteratorDetailLevel--; this.iteratorDetailLevel--;
// only continue if we can go down farther // only continue if we can go down farther
if (this.iteratorDetailLevel >= this.highestDetailLevel) if (this.iteratorDetailLevel >= this.leafDetailLevel)
{ {
Queue<QuadNode<T>> parentNodes = new LinkedList<>(this.validNodesForDetailLevel); Queue<QuadNode<T>> parentNodes = new LinkedList<>(this.validNodesForDetailLevel);
this.validNodesForDetailLevel.clear(); this.validNodesForDetailLevel.clear();
@@ -69,10 +69,14 @@ public class ThreadPoolUtil
@Nullable @Nullable
public static ThreadPoolExecutor getBeaconCullingExecutor() { return beaconCullingThreadPool; } public static ThreadPoolExecutor getBeaconCullingExecutor() { return beaconCullingThreadPool; }
// The main distinction between these thread pools is that one for compression has multiple threads and client handler is single-threaded
private static PriorityTaskPicker.Executor networkCompressionThreadPool; private static PriorityTaskPicker.Executor networkCompressionThreadPool;
@Nullable @Nullable
public static PriorityTaskPicker.Executor getNetworkCompressionExecutor() { return networkCompressionThreadPool; } public static PriorityTaskPicker.Executor getNetworkCompressionExecutor() { return networkCompressionThreadPool; }
private static ThreadPoolExecutor networkClientHandlerThreadPool;
@Nullable
public static ThreadPoolExecutor networkClientHandlerExecutor() { return networkClientHandlerThreadPool; }
public static final String FULL_DATA_MIGRATION_THREAD_NAME = "Full Data Migration"; public static final String FULL_DATA_MIGRATION_THREAD_NAME = "Full Data Migration";
@@ -103,7 +107,8 @@ public class ThreadPoolUtil
} }
taskPicker = new PriorityTaskPicker(); taskPicker = new PriorityTaskPicker();
networkCompressionThreadPool = taskPicker.createExecutor("Network"); networkCompressionThreadPool = taskPicker.createExecutor("Network Compression");
networkClientHandlerThreadPool = ThreadUtil.makeSingleThreadPool("Network Client Handler");
fileHandlerThreadPool = taskPicker.createExecutor("IO"); fileHandlerThreadPool = taskPicker.createExecutor("IO");
renderSectionLoadThreadPool = taskPicker.createExecutor("Render Loader"); renderSectionLoadThreadPool = taskPicker.createExecutor("Render Loader");
chunkToLodBuilderThreadPool = taskPicker.createExecutor("LOD Builder"); chunkToLodBuilderThreadPool = taskPicker.createExecutor("LOD Builder");
@@ -133,6 +138,7 @@ public class ThreadPoolUtil
public static void shutdownThreadPools() public static void shutdownThreadPools()
{ {
// standalone threads // standalone threads
networkClientHandlerThreadPool.shutdownNow();
taskPicker.shutdownNow(); taskPicker.shutdownNow();
beaconCullingThreadPool.shutdown(); beaconCullingThreadPool.shutdown();
fullDataMigrationThreadPool.shutdown(); fullDataMigrationThreadPool.shutdown();
@@ -20,6 +20,7 @@
package com.seibel.distanthorizons.core.world; package com.seibel.distanthorizons.core.world;
import com.seibel.distanthorizons.core.api.internal.ClientApi; import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.enums.MinecraftTextFormat;
import com.seibel.distanthorizons.core.level.DhClientServerLevel; import com.seibel.distanthorizons.core.level.DhClientServerLevel;
import com.seibel.distanthorizons.core.util.TimerUtil; import com.seibel.distanthorizons.core.util.TimerUtil;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
@@ -82,7 +83,7 @@ public class DhClientServerWorld extends AbstractDhServerWorld<DhClientServerLev
LOGGER.fatal("Failed to load client-server level, error: ["+e.getMessage()+"].", e); LOGGER.fatal("Failed to load client-server level, error: ["+e.getMessage()+"].", e);
ClientApi.INSTANCE.showChatMessageNextFrame(// red text ClientApi.INSTANCE.showChatMessageNextFrame(// red text
"\u00A7c" + "Distant Horizons: ClientServer level loading failed." + "\u00A7r \n" + MinecraftTextFormat.RED + "Distant Horizons: ClientServer level loading failed." + MinecraftTextFormat.CLEAR_FORMATTING + "\n" +
"Unable to load level ["+levelWrapper.getDhIdentifier()+"], LODs may not appear. See log for more information."); "Unable to load level ["+levelWrapper.getDhIdentifier()+"], LODs may not appear. See log for more information.");
return null; return null;
@@ -20,6 +20,7 @@
package com.seibel.distanthorizons.core.world; package com.seibel.distanthorizons.core.world;
import com.seibel.distanthorizons.core.api.internal.ClientApi; import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.enums.MinecraftTextFormat;
import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure; import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure;
import com.seibel.distanthorizons.core.level.DhClientLevel; import com.seibel.distanthorizons.core.level.DhClientLevel;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
@@ -94,9 +95,9 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
{ {
LOGGER.fatal("Failed to load client level, error: ["+e.getMessage()+"].", e); LOGGER.fatal("Failed to load client level, error: ["+e.getMessage()+"].", e);
ClientApi.INSTANCE.showChatMessageNextFrame(// red text ClientApi.INSTANCE.showChatMessageNextFrame(
"\u00A7c" + "Distant Horizons: Client level loading failed." + "\u00A7r \n" + MinecraftTextFormat.RED + "Distant Horizons: Client level loading failed." + MinecraftTextFormat.CLEAR_FORMATTING + "\n" +
"Unable to load level ["+clientLevelWrapper.getDhIdentifier()+"], LODs may not appear. See log for more information."); "Unable to load level ["+clientLevelWrapper.getDhIdentifier()+"], LODs may not appear. See log for more information.");
return null; return null;
} }
@@ -20,6 +20,7 @@
package com.seibel.distanthorizons.core.world; package com.seibel.distanthorizons.core.world;
import com.seibel.distanthorizons.core.api.internal.ClientApi; import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.enums.MinecraftTextFormat;
import com.seibel.distanthorizons.core.level.DhServerLevel; import com.seibel.distanthorizons.core.level.DhServerLevel;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
@@ -62,9 +63,9 @@ public class DhServerWorld extends AbstractDhServerWorld<DhServerLevel>
{ {
LOGGER.fatal("Failed to load server level, error: ["+e.getMessage()+"].", e); LOGGER.fatal("Failed to load server level, error: ["+e.getMessage()+"].", e);
ClientApi.INSTANCE.showChatMessageNextFrame(// red text ClientApi.INSTANCE.showChatMessageNextFrame(
"\u00A7c" + "Distant Horizons: Server level loading failed." + "\u00A7r \n" + MinecraftTextFormat.RED + "Distant Horizons: Server level loading failed." + MinecraftTextFormat.CLEAR_FORMATTING + "\n" +
"Unable to load level ["+serverLevelWrapper.getDhIdentifier()+"], LODs may not appear. See log for more information."); "Unable to load level ["+serverLevelWrapper.getDhIdentifier()+"], LODs may not appear. See log for more information.");
return null; return null;
} }
@@ -63,6 +63,8 @@ public interface IChunkWrapper extends IBindable
*/ */
int getMaxNonEmptyHeight(); int getMaxNonEmptyHeight();
void createDhHeightMaps();
/** @return The highest y position of a solid block at the given relative chunk position. */ /** @return The highest y position of a solid block at the given relative chunk position. */
int getSolidHeightMapValue(int xRel, int zRel); int getSolidHeightMapValue(int xRel, int zRel);
/** /**
@@ -404,5 +406,8 @@ public interface IChunkWrapper extends IBindable
return beaconTintBlockFound ? new Color(red, green, blue) : Color.WHITE; return beaconTintBlockFound ? new Color(red, green, blue) : Color.WHITE;
} }
IChunkWrapper copy();
} }
@@ -63,7 +63,9 @@ public interface IMinecraftRenderWrapper extends IBindable
/** @return -1 if no valid framebuffer is available yet */ /** @return -1 if no valid framebuffer is available yet */
int getTargetFramebuffer(); // Note: Iris is now hooking onto this for DH + Iris compat, try not to change (unless we wanna deal with some annoyances) int getTargetFramebuffer(); // Note: Iris is now hooking onto this for DH + Iris compat, try not to change (unless we wanna deal with some annoyances)
// Iris commit: https://github.com/IrisShaders/Iris/commit/a76a240527e93780bbcba57c09bef377419d47a7#diff-7b9ded0c79bbcdb130010373387756a28ee8d3640d522c0a5b7acd0abbfc20aeR16 // Iris commit: https://github.com/IrisShaders/Iris/commit/a76a240527e93780bbcba57c09bef377419d47a7#diff-7b9ded0c79bbcdb130010373387756a28ee8d3640d522c0a5b7acd0abbfc20aeR16
/** @return -1 if there was an issue or no texture exists */
int getDepthTextureId(); int getDepthTextureId();
/** @return -1 if there was an issue or no texture exists */
int getColorTextureId(); int getColorTextureId();
int getTargetFramebufferViewportWidth(); int getTargetFramebufferViewportWidth();
int getTargetFramebufferViewportHeight(); int getTargetFramebufferViewportHeight();
@@ -85,8 +85,6 @@ public interface ILevelWrapper extends IDhApiLevelWrapper, IBindable
@Override @Override
int getMinHeight(); int getMinHeight();
default IChunkWrapper tryGetChunk(DhChunkPos pos) { return null; }
/** Fired when the level is being unloaded. Doesn't unload the level. */ /** Fired when the level is being unloaded. Doesn't unload the level. */
void onUnload(); void onUnload();
@@ -84,7 +84,13 @@
"Show The Options Button", "Show The Options Button",
"distanthorizons.config.client.optionsButton.@tooltip": "distanthorizons.config.client.optionsButton.@tooltip":
"Show the config button to the left of the fov button", "Show the config button to the left of the fov button",
"distanthorizons.config.client.dynamicFadeUseOpaqueMcDepth":
"Dynamic Fade Use MC Opaque Depth",
"distanthorizons.config.client.dynamicFadeExportPath":
"Dynamic Fade Export Path",
"distanthorizons.config.client.dynamicFadeExportPath.@tooltip":
"Press 'p' to export the depth textures for troubleshooting",
"distanthorizons.config.client.advanced": "distanthorizons.config.client.advanced":
@@ -227,6 +233,10 @@
"Beacon render height", "Beacon render height",
"distanthorizons.config.client.advanced.graphics.genericRendering.beaconRenderHeight.@tooltip": "distanthorizons.config.client.advanced.graphics.genericRendering.beaconRenderHeight.@tooltip":
"Sets the maximum height at which beacons will render. \nThis will only affect new beacons coming into LOD render distance. \nBeacons currently visible in LOD chunks will not be affected.", "Sets the maximum height at which beacons will render. \nThis will only affect new beacons coming into LOD render distance. \nBeacons currently visible in LOD chunks will not be affected.",
"distanthorizons.config.client.advanced.graphics.genericRendering.expandDistantBeacons":
"Expand Distant Beacons",
"distanthorizons.config.client.advanced.graphics.genericRendering.expandDistantBeacons.@tooltip":
"If true LOD beacon beams will be rendered wider at extreme distances, \nmaking them easier to see. \nIf false all LOD beacon beams will only ever be 1 block wide.",
"distanthorizons.config.client.advanced.graphics.genericRendering.enableBeaconRendering.@tooltip": "distanthorizons.config.client.advanced.graphics.genericRendering.enableBeaconRendering.@tooltip":
"If true LOD beacon beams will be rendered.", "If true LOD beacon beams will be rendered.",
"distanthorizons.config.client.advanced.graphics.genericRendering.enableCloudRendering": "distanthorizons.config.client.advanced.graphics.genericRendering.enableCloudRendering":
@@ -400,14 +410,13 @@
"Experimental", "Experimental",
"distanthorizons.config.client.advanced.graphics.experimental.earthCurveRatio": "distanthorizons.config.client.advanced.graphics.experimental.earthCurveRatio":
"Earth Curve Ratio §6(EXPERIMENTAL)§r", "Earth Curve Ratio",
"distanthorizons.config.client.advanced.graphics.experimental.earthCurveRatio.@tooltip": "distanthorizons.config.client.advanced.graphics.experimental.earthCurveRatio.@tooltip":
"A value of 1 is equivalent to the curvature of Earth in real life. \nThe minimum accepted value is 50 and the maximum value is 5000. \nEverything between 1 and 49 will be rounded up to 50.", "A value of 1 is equivalent to the curvature of Earth in real life. \nThe minimum accepted value is 50 and the maximum value is 5000. \nEverything between 1 and 49 will be rounded up to 50.",
"distanthorizons.config.client.advanced.graphics.experimental.ignoredDimensionCsv":
"distanthorizons.config.client.advanced.graphics.experimental.onlyLoadCenterLods": "Ignored Dimension CSV",
"Only load center LODs", "distanthorizons.config.client.advanced.graphics.experimental.ignoredDimensionCsv.@tooltip":
"distanthorizons.config.client.advanced.graphics.experimental.onlyLoadCenterLods.@tooltip": "A comma separated list of dimension resource locations where DH won't render. Example: \"minecraft:the_nether,minecraft:the_end\" \n\nNote: \nSome DH settings will be disabled and/or changed to improve \nvisuals when DH rendering is disabled.",
"Skips loading adjacent LODs to significantly reduce load times (~5x)\nbut causes lighting on LOD borders to appear as full-bright\nand other graphical bugs.\n",
@@ -718,6 +727,8 @@
"OpenGL Events - Chat", "OpenGL Events - Chat",
"distanthorizons.config.common.logging.logNetworkEventToFile": "distanthorizons.config.common.logging.logNetworkEventToFile":
"Network Events - File", "Network Events - File",
"distanthorizons.config.common.logging.logConnectionConfigChangesToFile":
"Network Connection Config Changes - File",
"distanthorizons.config.common.logging.warning": "distanthorizons.config.common.logging.warning":
"Warnings", "Warnings",
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

@@ -1,79 +0,0 @@
#version 150 core
in uvec4 vPosition;
out vec4 vPos;
in vec4 color;
out vec4 vertexColor;
out vec3 vertexWorldPos;
out float vertexYPos;
uniform bool uWhiteWorld;
uniform mat4 uCombinedMatrix;
uniform vec3 uModelOffset;
uniform float uWorldYOffset;
uniform int uWorldSkyLight;
uniform sampler2D uLightMap;
uniform float uMircoOffset;
uniform float uEarthRadius;
/**
* TODO in the future this and standard.vert should be merged together to prevent inconsistencies between the two
*
* Vertex Shader
*
* author: James Seibel
* author: TomTheFurry
* author: stduhpf
* updated: coolGi
* version: 24-1-2023
*/
void main()
{
vPos = vPosition; // This is so it can be passed to the fragment shader
vertexWorldPos = vPosition.xyz + uModelOffset;
vertexYPos = vPosition.y + uWorldYOffset;
uint meta = vPosition.a;
uint mirco = (meta & 0xFF00u) >> 8u; // mirco offset which is a xyz 2bit value
// 0b00 = no offset
// 0b01 = positive offset
// 0b11 = negative offset
// format is: 0b00zzyyxx
float mx = (mirco & 1u) != 0u ? uMircoOffset : 0.0;
mx = (mirco & 2u) != 0u ? -mx : mx;
float my = (mirco & 4u) != 0u ? uMircoOffset : 0.0;
my = (mirco & 8u) != 0u ? -my : my;
float mz = (mirco & 16u) != 0u ? uMircoOffset : 0.0;
mz = (mirco & 32u) != 0u ? -mz : mz;
vertexWorldPos.x += mx;
vertexWorldPos.y += my;
vertexWorldPos.z += mz;
// vertex transformation logic - stduhpf
float localRadius = uEarthRadius + vertexYPos;
float phi = length(vertexWorldPos.xz) / localRadius;
vertexWorldPos.y += (cos(phi) - 1.0) * localRadius;
vertexWorldPos.xz = vertexWorldPos.xz * sin(phi) / phi;
uint lights = meta & 0xFFu;
float light2 = (mod(float(lights), 16.0) + 0.5) / 16.0;
float light = (float(lights / 16u) + 0.5) / 16.0;
vertexColor = vec4(texture(uLightMap, vec2(light, light2)).xyz, 1.0);
if (!uWhiteWorld)
{
vertexColor *= color;
}
gl_Position = uCombinedMatrix * vec4(vertexWorldPos, 1.0);
}
+31 -17
View File
@@ -17,27 +17,28 @@ uniform float uWorldYOffset;
uniform sampler2D uLightMap; uniform sampler2D uLightMap;
uniform float uMircoOffset; uniform float uMircoOffset;
uniform float uEarthRadius;
/** /**
* TODO in the future this and curve.vert should be merged together to prevent inconsistencies between the two
*
* Vertex Shader * Vertex Shader
* *
* author: James Seibel * author: James Seibel
* updated: TomTheFurry * author: TomTheFurry
* author: stduhpf
* updated: coolGi * updated: coolGi
* version: 2023-6-25 *
* version: 2025-12-22
*/ */
void main() void main()
{ {
vPos = vPosition; // This is so it can be passed to the fragment shader vPos = vPosition; // This is so it can be passed to the fragment shader
vertexWorldPos = vPosition.xyz + uModelOffset; vertexWorldPos = vPosition.xyz + uModelOffset;
vertexYPos = vPosition.y + uWorldYOffset; vertexYPos = vPosition.y + uWorldYOffset;
uint meta = vPosition.a; uint meta = vPosition.a;
uint mirco = (meta & 0xFF00u) >> 8u; // mirco offset which is a xyz 2bit value uint mirco = (meta & 0xFF00u) >> 8u; // mirco offset which is a xyz 2bit value
// 0b00 = no offset // 0b00 = no offset
// 0b01 = positive offset // 0b01 = positive offset
@@ -45,21 +46,34 @@ void main()
// format is: 0b00zzyyxx // format is: 0b00zzyyxx
float mx = (mirco & 1u)!=0u ? uMircoOffset : 0.0; float mx = (mirco & 1u)!=0u ? uMircoOffset : 0.0;
mx = (mirco & 2u)!=0u ? -mx : mx; mx = (mirco & 2u)!=0u ? -mx : mx;
float my = (mirco & 4u)!=0u ? uMircoOffset : 0.0; //float my = (mirco & 4u)!=0u ? uMircoOffset : 0.0;
my = (mirco & 8u)!=0u ? -my : my; //my = (mirco & 8u)!=0u ? -my : my;
float mz = (mirco & 16u)!=0u ? uMircoOffset : 0.0; float mz = (mirco & 16u)!=0u ? uMircoOffset : 0.0;
mz = (mirco & 32u)!=0u ? -mz : mz; mz = (mirco & 32u)!=0u ? -mz : mz;
vertexWorldPos.x += mx;
//vertexWorldPos.y += my;
vertexWorldPos.z += mz;
// apply the earth curvature if needed
if (uEarthRadius < -1.0f || uEarthRadius > 1.0f)
{
// vertex transformation logic - stduhpf
float localRadius = uEarthRadius + vertexYPos;
float phi = length(vertexWorldPos.xz) / localRadius;
vertexWorldPos.y += (cos(phi) - 1.0) * localRadius;
vertexWorldPos.xz = vertexWorldPos.xz * sin(phi) / phi;
}
uint lights = meta & 0xFFu; uint lights = meta & 0xFFu;
float skyLight = (float(lights/16u)+0.5) / 16.0;
float light2 = (mod(float(lights), 16.0)+0.5) / 16.0; float blockLight = (mod(float(lights), 16.0)+0.5) / 16.0;
float light = (float(lights/16u)+0.5) / 16.0; vertexColor = vec4(texture(uLightMap, vec2(skyLight, blockLight)).xyz, 1.0);
vertexColor = vec4(texture(uLightMap, vec2(light, light2)).xyz, 1.0);
if (!uIsWhiteWorld) if (!uIsWhiteWorld)
{ {
vertexColor *= color; vertexColor *= color;
} }
gl_Position = uCombinedMatrix * vec4(vertexWorldPos + vec3(mx, 0, mz), 1.0); gl_Position = uCombinedMatrix * vec4(vertexWorldPos, 1.0);
} }