Compare commits

...

35 Commits

Author SHA1 Message Date
s809 5b746a9534 Incomplete instance based config 2024-09-20 19:49:02 +05:00
James Seibel f7bf05b62f Update network semaphore comments 2024-09-17 07:40:28 -05:00
s809 840b0a7fe2 Bump protocol version because of removed InvalidSectionPosException 2024-09-17 12:41:46 +05:00
s809 5369bf628a Fix naming of some things and comments 2024-09-17 09:42:22 +05:00
James Seibel 79d2269218 Fix very high file handler jobs and pool some render data sources 2024-09-16 20:35:57 -05:00
James Seibel f21c791269 fix comment 2024-09-15 21:15:57 -05:00
James Seibel 11e58eecda Up API version 3.0.1 -> 4.0.0 2024-09-15 20:36:10 -05:00
James Seibel fbf13833a0 Up version 2.2.2 -> 2.3.0 2024-09-15 20:35:54 -05:00
James Seibel 2528f4a725 Merge server side branch and refactor 2024-09-15 20:35:38 -05:00
James Seibel 6d6cbd8a44 add more getDimensinoName() calls and minor cleanup 2024-09-11 17:11:55 -05:00
James Seibel 2fe3c261b0 Add DhClientWorld F3 debug string prep 2024-09-11 17:05:57 -05:00
James Seibel 5086f40d03 Add commit info to F3 screen 2024-09-11 17:04:18 -05:00
James Seibel 7766c49cbd Add DhChunkPos Vec3d constructor 2024-09-11 07:41:56 -05:00
James Seibel c6d86cfa3b remove unneeded IDimensionTypeWrapper.getTeleportationScale() 2024-09-11 07:40:44 -05:00
James Seibel 935cfec3d4 Add network compression thread config and pools 2024-09-11 07:37:39 -05:00
James Seibel ce2e64dc7e ClientOnlySaveStructure rename getDhDataFoldersForDimension -> getDhDataFoldersForLevel 2024-09-11 07:31:24 -05:00
James Seibel 15774ffe2a Only attempt to set Java Swing headless on clients 2024-09-11 07:25:01 -05:00
James Seibel 218cb04696 Don't load the client level in shouldLodsRender()
This might cause issues with the Replay mod, but we'll see
2024-09-11 07:20:29 -05:00
James Seibel 45fc36543b Add ILevelWrapper.getDimensionName() 2024-09-11 07:14:21 -05:00
James Seibel 554bb89690 remove unused message classes 2024-09-10 21:46:47 -05:00
James Seibel 2aa048b0cb Don't send chat messages on dedicated servers for configBasedLogger 2024-09-10 21:46:16 -05:00
James Seibel 570619b114 Ignore config preset UI updating when on the sever 2024-09-10 21:17:50 -05:00
James Seibel fb3e47ec3f Add FullDataSourceRepo.getTimestampForPos() 2024-09-10 20:46:18 -05:00
James Seibel 0f27dd79d7 minor refactoring and style cleanup 2024-09-10 07:24:56 -05:00
James Seibel abe0e284aa Remove unneeded networking classes 2024-09-10 07:15:37 -05:00
James Seibel 7a97b9dcbf Fix low quality LODs not loading when flying in a new straight line 2024-09-09 07:41:12 -05:00
James Seibel 23c98e2253 Clean up LodRenderSection async loading logic 2024-09-09 07:37:08 -05:00
James Seibel 06cce40ac6 hide attempting to... warnings for closed databases 2024-09-09 07:36:19 -05:00
James Seibel ce4259d98f add brown mushrooms to the list of ignored blocks to fix swamp issues 2024-09-07 14:22:41 -05:00
James Seibel d96ba5ae54 Add faster sky light engine from Builderb0y
Closes !67
2024-09-07 12:07:48 -05:00
James Seibel 3bee25053f Add missing LightingTestChunkWrapper methods 2024-09-06 21:55:48 -05:00
James Seibel a75d3ec5b0 Fix unit test compiling 2024-09-06 19:36:32 -05:00
James Seibel d5222ed20f Closes #805 (Hide "Distant Horizons overloaded")
Also update chunks closest to the player first
2024-09-06 18:22:57 -05:00
James Seibel d03a887620 Add BuilderB0y's getBlockState optimization 2024-09-05 07:50:18 -05:00
James Seibel 847cfa3ca9 Add current size to rolling average 2024-09-04 16:40:17 -05:00
134 changed files with 4128 additions and 3295 deletions
+1 -1
View File
@@ -537,7 +537,7 @@ ij_groovy_wrap_chain_calls_after_dot = false
ij_groovy_wrap_long_lines = false ij_groovy_wrap_long_lines = false
[{*.har,*.json,*.png.mcmeta,mcmod.info,pack.mcmeta}] [{*.har,*.json,*.png.mcmeta,mcmod.info,pack.mcmeta}]
indent_size = 2 indent_size = 4
ij_json_array_wrapping = split_into_lines ij_json_array_wrapping = split_into_lines
ij_json_keep_blank_lines_in_code = 0 ij_json_keep_blank_lines_in_code = 0
ij_json_keep_indents_on_empty_lines = false ij_json_keep_indents_on_empty_lines = false
@@ -35,6 +35,7 @@ public interface IDhApiLevelWrapper extends IDhApiUnsafeWrapper
{ {
IDhApiDimensionTypeWrapper getDimensionType(); IDhApiDimensionTypeWrapper getDimensionType();
/** @since API 4.0.0 */
String getDimensionName(); String getDimensionName();
EDhApiLevelType getLevelType(); EDhApiLevelType getLevelType();
@@ -30,27 +30,24 @@ public final class ModInfo
public static final String RESOURCE_NAMESPACE = "distant_horizons"; public static final String RESOURCE_NAMESPACE = "distant_horizons";
public static final String DEDICATED_SERVER_INITIAL_PATH = "dedicated_server_initial"; public static final String DEDICATED_SERVER_INITIAL_PATH = "dedicated_server_initial";
// region Protocol versions /** Incremented every time any packets are added, changed or removed, with a few exceptions. */
// Incremented every time any packets are added, changed or removed, with a few exceptions. public static final int PROTOCOL_VERSION = 4;
public static final int PROTOCOL_VERSION = 3;
public static final String WRAPPER_PACKET_PATH = "message"; public static final String WRAPPER_PACKET_PATH = "message";
// endregion
/** The internal mod name */ /** The internal mod name */
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.2.2-a-dev"; public static final String VERSION = "2.3.0-a-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 boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev"); public static boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev");
/** This version should only be updated when breaking changes are introduced to the DH API */ /** This version should only be updated when breaking changes are introduced to the DH API */
public static final int API_MAJOR_VERSION = 3; public static final int API_MAJOR_VERSION = 4;
/** 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 = 0;
/** 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 = 1; public static final int API_PATCH_VERSION = 0;
/** All DH owned threads should start with this string to allow for easier debugging and profiling. */ /** All DH owned threads should start with this string to allow for easier debugging and profiling. */
public static final String THREAD_NAME_PREFIX = "DH-"; public static final String THREAD_NAME_PREFIX = "DH-";
@@ -41,6 +41,8 @@ import java.awt.*;
public class Initializer public class Initializer
{ {
private static final Logger LOGGER = LogManager.getLogger(ModInfo.NAME + "-" + Initializer.class.getSimpleName()); private static final Logger LOGGER = LogManager.getLogger(ModInfo.NAME + "-" + Initializer.class.getSimpleName());
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
public static void init() public static void init()
{ {
@@ -78,9 +80,9 @@ public class Initializer
throw new RuntimeException(e); throw new RuntimeException(e);
} }
if (SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class) != null) if (MC_CLIENT != null)
{ {
// attempt to setup Swing so we can display dialogs (popup windows) // attempt to set up Swing so we can display dialogs (popup windows)
System.setProperty("java.awt.headless", "false"); System.setProperty("java.awt.headless", "false");
if (GraphicsEnvironment.isHeadless()) if (GraphicsEnvironment.isHeadless())
{ {
@@ -33,19 +33,19 @@ public class DhApiDebuggingConfig implements IDhApiDebuggingConfig
public IDhApiConfigValue<EDhApiDebugRendering> debugRendering() @Override public IDhApiConfigValue<EDhApiDebugRendering> debugRendering()
{ return new DhApiConfigValue<EDhApiDebugRendering, EDhApiDebugRendering>(Config.Client.Advanced.Debugging.debugRendering); } { return new DhApiConfigValue<EDhApiDebugRendering, EDhApiDebugRendering>(Config.Client.Advanced.Debugging.debugRendering); }
public IDhApiConfigValue<Boolean> debugKeybindings() @Override public IDhApiConfigValue<Boolean> debugKeybindings()
{ return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.enableDebugKeybindings); } { return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.enableDebugKeybindings); }
public IDhApiConfigValue<Boolean> renderWireframe() @Override public IDhApiConfigValue<Boolean> renderWireframe()
{ return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.renderWireframe); } { return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.renderWireframe); }
public IDhApiConfigValue<Boolean> lodOnlyMode() @Override public IDhApiConfigValue<Boolean> lodOnlyMode()
{ return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.lodOnlyMode); } { return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.lodOnlyMode); }
public IDhApiConfigValue<Boolean> debugWireframeRendering() @Override public IDhApiConfigValue<Boolean> debugWireframeRendering()
{ return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.DebugWireframe.enableRendering); } { return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.DebugWireframe.enableRendering); }
} }
@@ -33,10 +33,10 @@ public class DhApiMultiplayerConfig implements IDhApiMultiplayerConfig
public IDhApiConfigValue<EDhApiServerFolderNameMode> folderSavingMode() @Override public IDhApiConfigValue<EDhApiServerFolderNameMode> folderSavingMode()
{ return new DhApiConfigValue<EDhApiServerFolderNameMode, EDhApiServerFolderNameMode>(Config.Client.Advanced.Multiplayer.serverFolderNameMode); } { return new DhApiConfigValue<EDhApiServerFolderNameMode, EDhApiServerFolderNameMode>(Config.Client.Advanced.Multiplayer.serverFolderNameMode); }
public IDhApiConfigValue<Double> multiverseSimilarityRequirement() @Override public IDhApiConfigValue<Double> multiverseSimilarityRequirement()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Multiplayer.multiverseSimilarityRequiredPercent); } { return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Multiplayer.multiverseSimilarityRequiredPercent); }
} }
@@ -31,8 +31,8 @@ import com.seibel.distanthorizons.core.util.objects.Pair;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.network.session.Session; 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.EDhApiDebugRendering;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode;
@@ -77,7 +77,7 @@ public class ClientApi
public static final ClientApi INSTANCE = new ClientApi(); public static final ClientApi INSTANCE = new ClientApi();
public static final TestRenderer TEST_RENDERER = new TestRenderer(); public static final TestRenderer TEST_RENDERER = new TestRenderer();
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
public static final long SPAM_LOGGER_FLUSH_NS = TimeUnit.NANOSECONDS.convert(1, TimeUnit.SECONDS); public static final long SPAM_LOGGER_FLUSH_NS = TimeUnit.NANOSECONDS.convert(1, TimeUnit.SECONDS);
@@ -92,9 +92,9 @@ public class ClientApi
private final ClientPluginChannelApi pluginChannelApi = new ClientPluginChannelApi(this::clientLevelLoadEvent, this::clientLevelUnloadEvent); private final ClientPluginChannelApi pluginChannelApi = new ClientPluginChannelApi(this::clientLevelLoadEvent, this::clientLevelUnloadEvent);
// Delay loading the first level to give server some time to respond with level to actually load /** Delay loading the first level to give the server some time to respond with level to actually load */
private Timer firstLevelLoadTimer; private Timer firstLevelLoadTimer;
private static final long FIRST_LEVEL_LOAD_DELAY = 1000; private static final long FIRST_LEVEL_LOAD_DELAY_IN_MS = 1_000;
/** Holds any levels that were loaded before the {@link ClientApi#onClientOnlyConnected} was fired. */ /** Holds any levels that were loaded before the {@link ClientApi#onClientOnlyConnected} was fired. */
public final HashSet<IClientLevelWrapper> waitingClientLevels = new HashSet<>(); public final HashSet<IClientLevelWrapper> waitingClientLevels = new HashSet<>();
@@ -128,8 +128,8 @@ public class ClientApi
public synchronized void onClientOnlyConnected() public synchronized void onClientOnlyConnected()
{ {
// only continue if the client is connected to a different server // only continue if the client is connected to a different server
boolean connectedToServer = MC.clientConnectedToDedicatedServer(); boolean connectedToServer = MC_CLIENT.clientConnectedToDedicatedServer();
boolean connectedToReplay = MC.connectedToReplay(); boolean connectedToReplay = MC_CLIENT.connectedToReplay();
if (connectedToServer || connectedToReplay) if (connectedToServer || connectedToReplay)
{ {
if (connectedToServer) if (connectedToServer)
@@ -142,12 +142,12 @@ public class ClientApi
if (Config.Client.Advanced.Logging.showReplayWarningOnStartup.get()) if (Config.Client.Advanced.Logging.showReplayWarningOnStartup.get())
{ {
MC.sendChatMessage("\u00A76" + "Distant Horizons: Replay detected." + "\u00A7r"); // gold color MC_CLIENT.sendChatMessage("\u00A76" + "Distant Horizons: Replay detected." + "\u00A7r"); // gold color
MC.sendChatMessage("DH may behave strangely or have missing functionality."); MC_CLIENT.sendChatMessage("DH may behave strangely or have missing functionality.");
MC.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.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("\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.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.sendChatMessage(""); MC_CLIENT.sendChatMessage("");
} }
} }
@@ -156,7 +156,7 @@ public class ClientApi
DhClientWorld world = new DhClientWorld(); DhClientWorld world = new DhClientWorld();
SharedApi.setDhWorld(world); SharedApi.setDhWorld(world);
this.pluginChannelApi.onJoin(world.networkState.getSession()); this.pluginChannelApi.onJoinServer(world.networkState.getSession());
world.networkState.sendConfigMessage(); world.networkState.sendConfigMessage();
LOGGER.info("Loading [" + this.waitingClientLevels.size() + "] waiting client level wrappers."); LOGGER.info("Loading [" + this.waitingClientLevels.size() + "] waiting client level wrappers.");
@@ -172,6 +172,7 @@ public class ClientApi
/** Synchronized to prevent a rare issue where multiple disconnect events are triggered on top of each other. */ /** Synchronized to prevent a rare issue where multiple disconnect events are triggered on top of each other. */
public synchronized void onClientOnlyDisconnected() public synchronized void onClientOnlyDisconnected()
{ {
// clear the first time timer
if (this.firstLevelLoadTimer != null) if (this.firstLevelLoadTimer != null)
{ {
this.firstLevelLoadTimer.cancel(); this.firstLevelLoadTimer.cancel();
@@ -201,17 +202,12 @@ public class ClientApi
//==============// //==============//
public void clientLevelUnloadEvent(IClientLevelWrapper level) public void clientLevelUnloadEvent(IClientLevelWrapper level)
{
this.clientLevelUnloadEvent(level, false);
}
public void clientLevelUnloadEvent(IClientLevelWrapper level, boolean respawn)
{ {
try try
{ {
LOGGER.info("Unloading client level [" + level + "]-["+level.getDimensionName()+"]."); LOGGER.info("Unloading client level [" + level + "]-["+level.getDimensionName()+"].");
if (level instanceof IServerKeyedClientLevel && !respawn) if (level instanceof IServerKeyedClientLevel)
{ {
this.pluginChannelApi.onClientLevelUnload(); this.pluginChannelApi.onClientLevelUnload();
} }
@@ -236,7 +232,8 @@ public class ClientApi
public void clientLevelLoadEvent(IClientLevelWrapper level) public void clientLevelLoadEvent(IClientLevelWrapper level)
{ {
if (MC.clientConnectedToDedicatedServer()) // 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 (this.firstLevelLoadTimer == null) if (this.firstLevelLoadTimer == null)
{ {
@@ -244,16 +241,14 @@ public class ClientApi
this.firstLevelLoadTimer.schedule(new TimerTask() this.firstLevelLoadTimer.schedule(new TimerTask()
{ {
@Override @Override
public void run() public void run() { ClientApi.this.clientLevelLoadEvent(level); }
{ }, FIRST_LEVEL_LOAD_DELAY_IN_MS);
ClientApi.this.clientLevelLoadEvent(level);
}
}, FIRST_LEVEL_LOAD_DELAY);
return; return;
} }
this.firstLevelLoadTimer.cancel(); this.firstLevelLoadTimer.cancel();
} }
try try
{ {
LOGGER.info("Loading client level [" + level + "]-["+level.getDimensionName()+"]."); LOGGER.info("Loading client level [" + level + "]-["+level.getDimensionName()+"].");
@@ -265,11 +260,12 @@ public class ClientApi
{ {
LOGGER.info("Levels in this connection are managed by the server, skipping auto-load."); LOGGER.info("Levels in this connection are managed by the server, skipping auto-load.");
// Instead of attempting to load themselves, send config and wait for level key. // Instead of attempting to load themselves, send the config and wait for a server provided level key.
((DhClientWorld) world).networkState.sendConfigMessage(); ((DhClientWorld) world).networkState.sendConfigMessage();
return; return;
} }
world.getOrLoadLevel(level); world.getOrLoadLevel(level);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level)); ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level));
@@ -286,7 +282,6 @@ public class ClientApi
LOGGER.error("Unexpected error in ClientApi.clientLevelLoadEvent(), error: "+e.getMessage(), e); LOGGER.error("Unexpected error in ClientApi.clientLevelLoadEvent(), error: "+e.getMessage(), e);
} }
} }
private void loadWaitingChunksForLevel(IClientLevelWrapper level) private void loadWaitingChunksForLevel(IClientLevelWrapper level)
{ {
HashSet<Pair<IClientLevelWrapper, DhChunkPos>> keysToRemove = new HashSet<>(); HashSet<Pair<IClientLevelWrapper, DhChunkPos>> keysToRemove = new HashSet<>();
@@ -319,7 +314,7 @@ public class ClientApi
{ {
LOGGER.info("Renderer shutting down."); LOGGER.info("Renderer shutting down.");
IProfilerWrapper profiler = MC.getProfiler(); IProfilerWrapper profiler = MC_CLIENT.getProfiler();
profiler.push("DH-RendererShutdown"); profiler.push("DH-RendererShutdown");
profiler.pop(); profiler.pop();
@@ -329,7 +324,7 @@ public class ClientApi
{ {
LOGGER.info("Renderer starting up."); LOGGER.info("Renderer starting up.");
IProfilerWrapper profiler = MC.getProfiler(); IProfilerWrapper profiler = MC_CLIENT.getProfiler();
profiler.push("DH-RendererStartup"); profiler.push("DH-RendererStartup");
// make sure the GLProxy is created before the LodBufferBuilder needs it // make sure the GLProxy is created before the LodBufferBuilder needs it
@@ -339,7 +334,7 @@ public class ClientApi
public void clientTickEvent() public void clientTickEvent()
{ {
IProfilerWrapper profiler = MC.getProfiler(); IProfilerWrapper profiler = MC_CLIENT.getProfiler();
profiler.push("DH-ClientTick"); profiler.push("DH-ClientTick");
try try
@@ -361,7 +356,7 @@ public class ClientApi
// Ignore local world gen, as it's managed by server ticking // Ignore local world gen, as it's managed by server ticking
if (!(clientWorld instanceof DhClientServerWorld)) if (!(clientWorld instanceof DhClientServerWorld))
{ {
SharedApi.worldGenTick(clientWorld::doWorldGen); SharedApi.worldGenTick(clientWorld::worldGenTick);
} }
} }
} }
@@ -380,12 +375,12 @@ public class ClientApi
// networking // // networking //
//============// //============//
public void pluginMessageReceived(@NotNull NetworkMessage message) public void pluginMessageReceived(@NotNull AbstractNetworkMessage message)
{ {
Session session = this.pluginChannelApi.session; NetworkSession networkSession = this.pluginChannelApi.networkSession;
if (session != null) if (networkSession != null)
{ {
session.tryHandleMessage(message); networkSession.tryHandleMessage(message);
} }
} }
@@ -414,7 +409,7 @@ public class ClientApi
this.sendQueuedChatMessages(); this.sendQueuedChatMessages();
IProfilerWrapper profiler = MC.getProfiler(); IProfilerWrapper profiler = MC_CLIENT.getProfiler();
profiler.pop(); // get out of "terrain" profiler.pop(); // get out of "terrain"
profiler.push("DH-RenderLevel"); profiler.push("DH-RenderLevel");
@@ -532,10 +527,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.sendChatMessage("\u00A74\u00A7l\u00A7uERROR: Distant Horizons renderer has encountered an exception!"); MC_CLIENT.sendChatMessage("\u00A74\u00A7l\u00A7uERROR: Distant Horizons renderer has encountered an exception!");
MC.sendChatMessage("\u00A74Renderer disabled to try preventing GL state corruption."); MC_CLIENT.sendChatMessage("\u00A74Renderer disabled to try preventing GL state corruption.");
MC.sendChatMessage("\u00A74Toggle DH rendering via the config UI to re-activate DH rendering."); MC_CLIENT.sendChatMessage("\u00A74Toggle DH rendering via the config UI to re-activate DH rendering.");
MC.sendChatMessage("\u00A74Error: " + e); MC_CLIENT.sendChatMessage("\u00A74Error: " + e);
} }
finally finally
{ {
@@ -575,24 +570,24 @@ public class ClientApi
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())); Config.Client.Advanced.Debugging.debugRendering.set(EDhApiDebugRendering.next(Config.Client.Advanced.Debugging.debugRendering.get()));
MC.sendChatMessage("F8: Set debug mode to " + 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())); Config.Client.Advanced.Debugging.rendererMode.set(EDhApiRendererMode.next(Config.Client.Advanced.Debugging.rendererMode.get()));
MC.sendChatMessage("F6: Set rendering to " + 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; prefLoggerEnabled = !prefLoggerEnabled;
MC.sendChatMessage("P: Debug Pref Logger is " + (prefLoggerEnabled ? "enabled" : "disabled")); MC_CLIENT.sendChatMessage("P: Debug Pref Logger is " + (prefLoggerEnabled ? "enabled" : "disabled"));
} }
} }
private void sendQueuedChatMessages() private void sendQueuedChatMessages()
{ {
// dev build // dev build
if (ModInfo.IS_DEV_BUILD && !this.configOverrideReminderPrinted && MC.playerExists()) if (ModInfo.IS_DEV_BUILD && !this.configOverrideReminderPrinted && MC_CLIENT.playerExists())
{ {
this.configOverrideReminderPrinted = true; this.configOverrideReminderPrinted = true;
@@ -602,7 +597,7 @@ public class ClientApi
"\u00A72" + "Distant Horizons: nightly/unstable build, version: [" + ModInfo.VERSION+"]." + "\u00A7r\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.sendChatMessage(message); MC_CLIENT.sendChatMessage(message);
} }
// memory // memory
@@ -623,7 +618,7 @@ public class ClientApi
"Stuttering or low FPS may occur. \n" + "Stuttering or low FPS may occur. \n" +
"Please increase Minecraft's available memory to 4 gigabytes. \n" + "Please increase Minecraft's available memory to 4 gigabytes. \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";
MC.sendChatMessage(message); MC_CLIENT.sendChatMessage(message);
} }
} }
@@ -636,7 +631,7 @@ public class ClientApi
// done to prevent potential null pointers // done to prevent potential null pointers
message = ""; message = "";
} }
MC.sendChatMessage(message); MC_CLIENT.sendChatMessage(message);
} }
} }
@@ -5,9 +5,9 @@ import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager; import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager;
import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel; import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.network.event.internal.CloseEvent; import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent;
import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage; import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage;
import com.seibel.distanthorizons.core.network.session.Session; 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.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
@@ -31,15 +31,13 @@ public class ClientPluginChannelApi
private final Consumer<IClientLevelWrapper> levelUnloadHandler; private final Consumer<IClientLevelWrapper> levelUnloadHandler;
@Nullable @Nullable
public Session session; public NetworkSession networkSession;
public boolean allowLevelLoading(IClientLevelWrapper level)
{
return (KEYED_CLIENT_LEVEL_MANAGER.isEnabled() && level instanceof IServerKeyedClientLevel)
|| !KEYED_CLIENT_LEVEL_MANAGER.isEnabled();
}
//=============//
// constructor //
//=============//
public ClientPluginChannelApi(Consumer<IServerKeyedClientLevel> levelLoadHandler, Consumer<IClientLevelWrapper> levelUnloadHandler) public ClientPluginChannelApi(Consumer<IServerKeyedClientLevel> levelLoadHandler, Consumer<IClientLevelWrapper> levelUnloadHandler)
{ {
@@ -47,12 +45,32 @@ public class ClientPluginChannelApi
this.levelUnloadHandler = levelUnloadHandler; this.levelUnloadHandler = levelUnloadHandler;
} }
public void onJoin(@NonNull Session session)
//============//
// properties //
//============//
/** @return true if the level loading is handled by the server */
public boolean allowLevelLoading(IClientLevelWrapper level)
{ {
Objects.requireNonNull(session); return (KEYED_CLIENT_LEVEL_MANAGER.hasLevelSet() && level instanceof IServerKeyedClientLevel)
this.session = session; || !KEYED_CLIENT_LEVEL_MANAGER.hasLevelSet();
session.registerHandler(CurrentLevelKeyMessage.class, this::onCurrentLevelKeyMessage); }
session.registerHandler(CloseEvent.class, this::onClose);
//================//
// network events //
//================//
/** fired when this client connects to a server with DH support */
public void onJoinServer(@NonNull NetworkSession networkSession)
{
Objects.requireNonNull(networkSession);
this.networkSession = networkSession;
this.networkSession.registerHandler(CurrentLevelKeyMessage.class, this::onCurrentLevelKeyMessage);
this.networkSession.registerHandler(CloseInternalEvent.class, this::onClose);
} }
private void onCurrentLevelKeyMessage(CurrentLevelKeyMessage msg) private void onCurrentLevelKeyMessage(CurrentLevelKeyMessage msg)
@@ -64,9 +82,10 @@ public class ClientPluginChannelApi
throw new IllegalArgumentException("Server sent invalid level key."); throw new IllegalArgumentException("Server sent invalid level key.");
} }
LOGGER.info("Server level key received: " + msg.levelKey); LOGGER.info("Server level key received: [" + msg.levelKey + "].");
MC.executeOnRenderThread(() -> { MC.executeOnRenderThread(() ->
{
IClientLevelWrapper clientLevel = MC.getWrappedClientLevel(true); IClientLevelWrapper clientLevel = MC.getWrappedClientLevel(true);
IServerKeyedClientLevel existingKeyedClientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel(); IServerKeyedClientLevel existingKeyedClientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel();
@@ -74,7 +93,7 @@ public class ClientPluginChannelApi
{ {
if (!existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey)) if (!existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey))
{ {
LOGGER.info("Unloading previous level with key: " + existingKeyedClientLevel.getServerLevelKey()); LOGGER.info("Unloading previous level with key: [" + existingKeyedClientLevel.getServerLevelKey() + "].");
this.levelUnloadHandler.accept(existingKeyedClientLevel); this.levelUnloadHandler.accept(existingKeyedClientLevel);
} }
else else
@@ -84,33 +103,32 @@ public class ClientPluginChannelApi
} }
else else
{ {
LOGGER.info("Unloading non-keyed level: " + clientLevel.getDimensionName()); LOGGER.info("Unloading non-keyed level: [" + clientLevel.getDimensionName() + "].");
this.levelUnloadHandler.accept(clientLevel); this.levelUnloadHandler.accept(clientLevel);
} }
if (existingKeyedClientLevel == null || !existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey)) if (existingKeyedClientLevel == null || !existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey))
{ {
LOGGER.info("Loading level with key: " + msg.levelKey); LOGGER.info("Loading level with key: [" + msg.levelKey + "].");
IServerKeyedClientLevel keyedLevel = KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel, msg.levelKey); IServerKeyedClientLevel keyedLevel = KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel, msg.levelKey);
this.levelLoadHandler.accept(keyedLevel); this.levelLoadHandler.accept(keyedLevel);
} }
}); });
} }
public void onClientLevelUnload() public void onClientLevelUnload() { KEYED_CLIENT_LEVEL_MANAGER.clearKeyedLevel(); }
{
KEYED_CLIENT_LEVEL_MANAGER.clearServerKeyedLevel();
}
private void onClose(CloseEvent event)
{
this.reset();
}
//==========//
// shutdown //
//==========//
private void onClose(CloseInternalEvent event) { this.reset(); }
public void reset() public void reset()
{ {
this.session = null; this.networkSession = null;
KEYED_CLIENT_LEVEL_MANAGER.disable(); KEYED_CLIENT_LEVEL_MANAGER.clearKeyedLevel();
} }
} }
@@ -21,7 +21,7 @@ package com.seibel.distanthorizons.core.api.internal;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelLoadEvent; import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelLoadEvent;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelUnloadEvent; import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelUnloadEvent;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import com.seibel.distanthorizons.core.world.AbstractDhWorld; import com.seibel.distanthorizons.core.world.AbstractDhWorld;
@@ -67,7 +67,7 @@ public class ServerApi
if (serverWorld != null) if (serverWorld != null)
{ {
serverWorld.serverTick(); serverWorld.serverTick();
SharedApi.worldGenTick(serverWorld::doWorldGen); SharedApi.worldGenTick(serverWorld::worldGenTick);
} }
} }
catch (Exception e) catch (Exception e)
@@ -151,7 +151,7 @@ public class ServerApi
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); IDhServerWorld serverWorld = SharedApi.getIDhServerWorld();
if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well
{ {
LOGGER.info("Creating state for player: " + player.getName()); LOGGER.info("Player [" + player.getName()+ "] joined.");
((DhServerWorld) serverWorld).addPlayer(player); ((DhServerWorld) serverWorld).addPlayer(player);
} }
} }
@@ -160,21 +160,21 @@ public class ServerApi
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); IDhServerWorld serverWorld = SharedApi.getIDhServerWorld();
if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well
{ {
LOGGER.info("Destroying state for player: " + player.getName()); LOGGER.info("Player [" + player.getName() + "] disconnected.");
((DhServerWorld) serverWorld).removePlayer(player); ((DhServerWorld) serverWorld).removePlayer(player);
} }
} }
public void serverPlayerLevelChangeEvent(IServerPlayerWrapper player, IServerLevelWrapper origin, IServerLevelWrapper dest) public void serverPlayerLevelChangeEvent(IServerPlayerWrapper player, IServerLevelWrapper originLevel, IServerLevelWrapper destinationLevel)
{ {
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); IDhServerWorld serverWorld = SharedApi.getIDhServerWorld();
if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well
{ {
LOGGER.info("Player changed level: " + player.getName()); LOGGER.info("Player [" + player.getName() + "] changed level: ["+originLevel.getKeyedLevelDimensionName()+"] -> ["+destinationLevel.getKeyedLevelDimensionName()+"].");
((DhServerWorld) serverWorld).changePlayerLevel(player, origin, dest); ((DhServerWorld) serverWorld).changePlayerLevel(player, originLevel, destinationLevel);
} }
} }
public void pluginMessageReceived(IServerPlayerWrapper player, @NotNull NetworkMessage message) public void pluginMessageReceived(IServerPlayerWrapper player, @NotNull AbstractNetworkMessage message)
{ {
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); IDhServerWorld serverWorld = SharedApi.getIDhServerWorld();
if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well
@@ -31,11 +31,11 @@ import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo; import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.TimerUtil;
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.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.world.*; import com.seibel.distanthorizons.core.world.*;
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.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;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
@@ -44,6 +44,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
/** Contains code and variables used by both {@link ClientApi} and {@link ServerApi} */ /** Contains code and variables used by both {@link ClientApi} and {@link ServerApi} */
public class SharedApi public class SharedApi
@@ -52,18 +53,15 @@ public class SharedApi
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
private static final Set<DhChunkPos> UPDATING_CHUNK_POS_SET = ConcurrentHashMap.newKeySet(); private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
/** how many chunks can be queued for updating per thread, used to prevent updates from infinitely pilling up if the user flys around extremely fast */
private static final UpdateChunkPosManager UPDATE_POS_MANAGER = new UpdateChunkPosManager();
/** how many chunks can be queued for updating per thread, used to prevent updates from infinitely pilling up if the user flies around extremely fast */
private static final int MAX_UPDATING_CHUNK_COUNT_PER_THREAD = 500; private static final int MAX_UPDATING_CHUNK_COUNT_PER_THREAD = 500;
private static final int MIN_MS_BETWEEN_OVERLOADED_LOG_MESSAGE = 30_000;
private static final Timer CHUNK_UPDATE_TIMER = TimerUtil.CreateTimer("ChunkUpdateTimer");
private static AbstractDhWorld currentWorld; private static AbstractDhWorld currentWorld;
private static int lastWorldGenTickDelta = 0; private static int lastWorldGenTickDelta = 0;
private static long lastOverloadedLogMessageMsTime = 0;
@@ -84,7 +82,6 @@ public class SharedApi
public static void setDhWorld(AbstractDhWorld newWorld) public static void setDhWorld(AbstractDhWorld newWorld)
{ {
AbstractDhWorld prevWorld = currentWorld;
currentWorld = newWorld; currentWorld = newWorld;
// starting and stopping the DataRenderTransformer is necessary to prevent attempting to // starting and stopping the DataRenderTransformer is necessary to prevent attempting to
@@ -96,16 +93,12 @@ public class SharedApi
else else
{ {
ThreadPoolUtil.shutdownThreadPools(); ThreadPoolUtil.shutdownThreadPools();
if (prevWorld != null && prevWorld.environment != EWorldEnvironment.Server_Only)
{
DebugRenderer.clearRenderables(); DebugRenderer.clearRenderables();
MC_RENDER.clearTargetFrameBuffer(); MC_RENDER.clearTargetFrameBuffer();
// shouldn't be necessary, but if we missed closing one of the connections this should make sure they're all closed // shouldn't be necessary, but if we missed closing one of the connections this should make sure they're all closed
AbstractDhRepo.closeAllConnections(); AbstractDhRepo.closeAllConnections();
// needs to be closed on world shutdown to clear out un-processed chunks // needs to be closed on world shutdown to clear out un-processed chunks
UPDATING_CHUNK_POS_SET.clear(); UPDATE_POS_MANAGER.clear();
}
// recommend that the garbage collector cleans up any objects from the old world and thread pools // recommend that the garbage collector cleans up any objects from the old world and thread pools
System.gc(); System.gc();
@@ -141,10 +134,10 @@ public class SharedApi
* This is important since asking MC for a chunk is slow and may block the render thread. * This is important since asking MC for a chunk is slow and may block the render thread.
*/ */
public static boolean isChunkAtBlockPosAlreadyUpdating(int blockPosX, int blockPosZ) public static boolean isChunkAtBlockPosAlreadyUpdating(int blockPosX, int blockPosZ)
{ return UPDATING_CHUNK_POS_SET.contains(new DhChunkPos(new DhBlockPos2D(blockPosX, blockPosZ))); } { return UPDATE_POS_MANAGER.contains(new DhChunkPos(new DhBlockPos2D(blockPosX, blockPosZ))); }
public static boolean isChunkAtChunkPosAlreadyUpdating(int chunkPosX, int chunkPosZ) public static boolean isChunkAtChunkPosAlreadyUpdating(int chunkPosX, int chunkPosZ)
{ return UPDATING_CHUNK_POS_SET.contains(new DhChunkPos(chunkPosX, chunkPosZ)); } { return UPDATE_POS_MANAGER.contains(new DhChunkPos(chunkPosX, chunkPosZ)); }
/** handles both block place and break events */ /** handles both block place and break events */
@@ -193,43 +186,6 @@ public class SharedApi
//=====================//
// task limiting check //
//=====================//
int currentQueueCount = UPDATING_CHUNK_POS_SET.size();
int maxQueueCount = MAX_UPDATING_CHUNK_COUNT_PER_THREAD * Config.Client.Advanced.MultiThreading.numberOfLodBuilderThreads.get();
if (currentQueueCount >= maxQueueCount)
{
// The maximum number of chunks are already queued, don't add more.
// This is done to prevent overloading the system if the user fly's extremely fast and queues too many chunks
long msBetweenLastLog = System.currentTimeMillis() - lastOverloadedLogMessageMsTime;
if (msBetweenLastLog >= MIN_MS_BETWEEN_OVERLOADED_LOG_MESSAGE)
{
lastOverloadedLogMessageMsTime = System.currentTimeMillis();
String message = "Distant Horizons overloaded, too many chunks queued for updating. " +
"\nThis may result in holes in your LODs. " +
"\nPlease move through the world slower, decrease your vanilla render distance, slow down your world pre-generator, or increase the Distant Horizons' CPU load config. " +
"\nMax queue count ["+maxQueueCount+"] (["+MAX_UPDATING_CHUNK_COUNT_PER_THREAD+"] per thread).";
ClientApi.INSTANCE.showChatMessageNextFrame(message);
LOGGER.warn(message);
}
return;
}
// prevent duplicate update requests
if (UPDATING_CHUNK_POS_SET.contains(chunkWrapper.getChunkPos()))
{
// this chunk is already being updated
return;
}
UPDATING_CHUNK_POS_SET.add(chunkWrapper.getChunkPos());
//===============================// //===============================//
// update the necessary chunk(s) // // update the necessary chunk(s) //
//===============================// //===============================//
@@ -238,7 +194,7 @@ public class SharedApi
{ {
// only update the center chunk // only update the center chunk
bakeChunkLightingAndSendToLevelAsync(chunkWrapper, null, dhLevel); queueChunkUpdate(chunkWrapper, null, dhLevel);
} }
else else
{ {
@@ -272,27 +228,58 @@ public class SharedApi
// light and send the chunks // light and send the chunks
for (IChunkWrapper litChunk : neighbourChunkList) for (IChunkWrapper litChunk : neighbourChunkList)
{ {
bakeChunkLightingAndSendToLevelAsync(litChunk, neighbourChunkList, dhLevel); queueChunkUpdate(litChunk, neighbourChunkList, dhLevel);
} }
} }
} }
/** returning a {@link CompletableFuture} isn't necessary, but allows Intellij to properly show the full stack trace when debugging. */ private static void queueChunkUpdate(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighbourChunkList, IDhLevel dhLevel)
@SuppressWarnings("UnusedReturnValue")
private static CompletableFuture<Void> bakeChunkLightingAndSendToLevelAsync(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighbourChunkList, IDhLevel dhLevel)
{ {
// lighting the chunk needs to be done on a separate thread to prevent lagging any of the event threads if (MC_CLIENT != null && MC_CLIENT.playerExists())
ThreadPoolExecutor executor = ThreadPoolUtil.getChunkToLodBuilderExecutor();
if (executor == null)
{ {
return CompletableFuture.completedFuture(null); UPDATE_POS_MANAGER.setCenter(MC_CLIENT.getPlayerChunkPos());
UPDATE_POS_MANAGER.maxSize = MAX_UPDATING_CHUNK_COUNT_PER_THREAD * Config.Client.Advanced.MultiThreading.numberOfLodBuilderThreads.get();
} }
UpdateChunkData updateData = new UpdateChunkData(chunkWrapper, neighbourChunkList, dhLevel);
UPDATE_POS_MANAGER.addItem(chunkWrapper.getChunkPos(), updateData);
// queue updates up to the number of CPU cores allocated for the job
// (this prevents doing extra work queuing tasks that may not be necessary)
// and makes sure the chunks closest to the player are updated first
ThreadPoolExecutor executor = ThreadPoolUtil.getChunkToLodBuilderExecutor();
if (executor != null && executor.getQueue().size() < executor.getCorePoolSize())
{
try try
{ {
return CompletableFuture.runAsync(() -> executor.execute(SharedApi::processQueuedChunkUpdate);
}
catch (RejectedExecutionException ignore)
{
// the executor was shut down, it should be back up shortly and able to accept new jobs
}
}
}
/** returning a {@link CompletableFuture} isn't necessary, but allows Intellij to properly show the full stack trace when debugging. */
@SuppressWarnings("UnusedReturnValue")
private static void processQueuedChunkUpdate()
{ {
//LOGGER.trace(chunkWrapper.getChunkPos() + " " + executor.getActiveCount() + " / " + executor.getQueue().size() + " - " + executor.getCompletedTaskCount()); //LOGGER.trace(chunkWrapper.getChunkPos() + " " + executor.getActiveCount() + " / " + executor.getQueue().size() + " - " + executor.getCompletedTaskCount());
UpdateChunkData updateData = UPDATE_POS_MANAGER.popClosest();
if (updateData == null)
{
return;
}
IChunkWrapper chunkWrapper = updateData.chunkWrapper;
@Nullable ArrayList<IChunkWrapper> neighbourChunkList = updateData.neighbourChunkList;
IDhLevel dhLevel = updateData.dhLevel;
try try
{ {
boolean checkChunkHash = !Config.Client.Advanced.LodBuilding.disableUnchangedChunkCheck.get(); boolean checkChunkHash = !Config.Client.Advanced.LodBuilding.disableUnchangedChunkCheck.get();
@@ -329,41 +316,8 @@ public class SharedApi
} }
// chunk light baking is disabled since profiling revealed it used // sky lighting is populated later at the data source level
// roughly the same amount of time as generating the lighting ourselves and DhLightingEngine.INSTANCE.bakeChunkBlockLighting(chunkWrapper, nearbyChunkList, dhLevel.hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT);
// was much more likely to have issues with corrupt (all black or all bright) chunks
boolean tryUsingMcLightingEngine = false;
if (tryUsingMcLightingEngine)
{
// Save or populate the chunk wrapper's lighting
// this is done so we don't have to worry about MC unloading the lighting data for this chunk
boolean chunkLightPopulated = false;
boolean onlyUseDhLighting = Config.Client.Advanced.LodBuilding.onlyUseDhLightingEngine.get();
if (!onlyUseDhLighting && chunkWrapper.isLightCorrect())
{
// If MC's lighting engine isn't thread safe this may cause the server thread to lag
chunkLightPopulated = chunkWrapper.bakeDhLightingUsingMcLightingEngine(dhLevel.getLevelWrapper());
if (!chunkLightPopulated)
{
// clear any existing data to prevent partial or corrupt lighting
// when re-generating it
chunkWrapper.clearDhBlockLighting();
chunkWrapper.clearDhSkyLighting();
}
}
// something went wrong during the baking process so we have to generate the lighting ourselves
if (!chunkLightPopulated)
{
DhLightingEngine.INSTANCE.lightChunk(chunkWrapper, nearbyChunkList, dhLevel.hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT);
}
}
else
{
DhLightingEngine.INSTANCE.lightChunk(chunkWrapper, nearbyChunkList, dhLevel.hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT);
}
dhLevel.updateBeaconBeamsForChunk(chunkWrapper, nearbyChunkList); dhLevel.updateBeaconBeamsForChunk(chunkWrapper, nearbyChunkList);
dhLevel.updateChunkAsync(chunkWrapper, newChunkHash); dhLevel.updateChunkAsync(chunkWrapper, newChunkHash);
@@ -374,29 +328,19 @@ public class SharedApi
} }
finally finally
{ {
// the LOD chunk has finished being updated // queue the next position if there are still positions to process
int updateTimeoutInSec = Config.Client.Advanced.LodBuilding.minTimeBetweenChunkUpdatesInSeconds.get(); ThreadPoolExecutor executor = ThreadPoolUtil.getChunkToLodBuilderExecutor();
if (updateTimeoutInSec != 0) if (executor != null && !UPDATE_POS_MANAGER.positionMap.isEmpty())
{ {
// prevent updating this chunk again until the timeout finishes try
CHUNK_UPDATE_TIMER.schedule(new TimerTask()
{ {
@Override executor.execute(SharedApi::processQueuedChunkUpdate);
public void run() { UPDATING_CHUNK_POS_SET.remove(chunkWrapper.getChunkPos()); }
}, updateTimeoutInSec * 1000L);
}
else
{
// instantly allow this chunk to be updated again
UPDATING_CHUNK_POS_SET.remove(chunkWrapper.getChunkPos());
}
}
}, executor);
} }
catch (RejectedExecutionException ignore) catch (RejectedExecutionException ignore)
{ {
// the executor was shut down, it should be back up shortly and able to accept new jobs // the executor was shut down, it should be back up shortly and able to accept new jobs
return CompletableFuture.completedFuture(null); }
}
} }
} }
@@ -408,11 +352,185 @@ public class SharedApi
public String getDebugMenuString() public String getDebugMenuString()
{ {
int maxUpdateCount = MAX_UPDATING_CHUNK_COUNT_PER_THREAD * Config.Client.Advanced.MultiThreading.numberOfLodBuilderThreads.get(); String updatingCountStr = F3Screen.NUMBER_FORMAT.format(UPDATE_POS_MANAGER.closestQueue.size());
String updatingCountStr = F3Screen.NUMBER_FORMAT.format(UPDATING_CHUNK_POS_SET.size()); String maxUpdateCountStr = F3Screen.NUMBER_FORMAT.format(UPDATE_POS_MANAGER.maxSize);
String maxUpdateCountStr = F3Screen.NUMBER_FORMAT.format(maxUpdateCount);
return "Queued chunk updates: "+updatingCountStr+" / "+maxUpdateCountStr; return "Queued chunk updates: "+updatingCountStr+" / "+maxUpdateCountStr;
} }
//================//
// helper classes //
//================//
/** contains the objects needed to update a chunk */
private static class UpdateChunkData
{
public IChunkWrapper chunkWrapper;
@Nullable
public ArrayList<IChunkWrapper> neighbourChunkList;
public IDhLevel dhLevel;
public UpdateChunkData(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighbourChunkList, IDhLevel dhLevel)
{
this.chunkWrapper = chunkWrapper;
this.neighbourChunkList = neighbourChunkList;
this.dhLevel = dhLevel;
}
}
/** keeps track of which chunks need to be updated */
private static class UpdateChunkPosManager
{
private final PriorityQueue<DhChunkPos> closestQueue;
private final PriorityQueue<DhChunkPos> furthestQueue;
private final HashMap<DhChunkPos, UpdateChunkData> positionMap;
private final ReentrantLock lock = new ReentrantLock();
private DhChunkPos center;
private int maxSize = 500;
//=============//
// constructor //
//=============//
public UpdateChunkPosManager()
{
this.closestQueue = new PriorityQueue<>(Comparator.comparingDouble(pos -> pos.squaredDistance(this.center)));
this.furthestQueue = new PriorityQueue<>(Comparator.comparingDouble(pos -> ((DhChunkPos)pos).squaredDistance(this.center)).reversed());
this.positionMap = new HashMap<>();
// defaulting to 0,0 is fine since it'll be updated once we start adding items
this.center = new DhChunkPos(0, 0);
}
//==================//
// list/set methods //
//==================//
public boolean contains(DhChunkPos pos)
{
try
{
this.lock.lock();
return this.positionMap.containsKey(pos);
}
finally
{
this.lock.unlock();
}
}
public void clear()
{
try
{
this.lock.lock();
this.positionMap.clear();
this.closestQueue.clear();
this.furthestQueue.clear();
}
finally
{
this.lock.unlock();
}
}
public void addItem(DhChunkPos pos, UpdateChunkData updateData)
{
try
{
this.lock.lock();
if (this.positionMap.containsKey(pos))
{
return;
}
if (this.positionMap.size() >= this.maxSize)
{
// Remove item furthest from the center
DhChunkPos furthest = this.furthestQueue.poll();
this.closestQueue.remove(furthest);
this.positionMap.remove(furthest);
}
this.positionMap.put(pos, updateData);
this.closestQueue.add(pos);
this.furthestQueue.add(pos);
}
finally
{
this.lock.unlock();
}
}
//==================//
// position methods //
//==================//
public void setCenter(DhChunkPos newCenter)
{
// if the rebuild time takes too long
// (in James' testing a queue of 500 items only took around 0.1 milliseconds)
// this equation could be changed to only update after moving 2 or 4 chunks from the center
if (newCenter.equals(this.center))
{
return;
}
try
{
this.lock.lock();
this.center = newCenter;
// rebuild the priority queues to match the new center
this.closestQueue.clear();
this.furthestQueue.clear();
for (DhChunkPos pos : this.positionMap.keySet())
{
this.closestQueue.add(pos);
this.furthestQueue.add(pos);
}
}
finally
{
this.lock.unlock();
}
}
public UpdateChunkData popClosest()
{
if (this.closestQueue.isEmpty())
{
return null;
}
try
{
this.lock.lock();
DhChunkPos closest = this.closestQueue.poll();
this.furthestQueue.remove(closest);
return this.positionMap.remove(closest);
}
finally
{
this.lock.unlock();
}
}
}
} }
@@ -20,7 +20,6 @@
package com.seibel.distanthorizons.core.config; package com.seibel.distanthorizons.core.config;
import com.google.common.base.Suppliers;
import com.seibel.distanthorizons.api.DhApi; import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.api.enums.config.*; import com.seibel.distanthorizons.api.enums.config.*;
import com.seibel.distanthorizons.api.enums.config.quickOptions.*; import com.seibel.distanthorizons.api.enums.config.quickOptions.*;
@@ -62,61 +61,236 @@ public class Config
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public static ConfigCategory client = new ConfigCategory.Builder().set(Client.class).build(); private static Config GLOBAL_INSTANCE;
public static void initializeGlobalConfig()
public static class Client
{ {
public static ConfigEntry<Boolean> quickEnableRendering = new ConfigEntry.Builder<Boolean>() if (GLOBAL_INSTANCE != null)
.set(true) {
.comment("" throw new IllegalStateException("Cannot reinitialize the Config");
+ "If true, Distant Horizons will render LODs beyond the vanilla render distance." }
+ "") GLOBAL_INSTANCE = new Config();
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI) }
public static Config getGlobalConfig() { return GLOBAL_INSTANCE; }
public final QuickSettings quickSettings = new QuickSettings();
public final Graphics graphics = new Graphics();
public class QuickSettings
{
public final ConfigEntry<Boolean> enableRendering = new ConfigEntry.Builder<Boolean>()
.computed(
() -> Config.this.debugging.rendererMode.get() != EDhApiRendererMode.DISABLED,
enableRendering -> Config.this.debugging.rendererMode.set(enableRendering
? EDhApiRendererMode.DEFAULT
: EDhApiRendererMode.DISABLED
)
)
.build(); .build();
public static ConfigLinkedEntry quickLodChunkRenderDistance = new ConfigLinkedEntry(Advanced.Graphics.Quality.lodChunkRenderDistanceRadius); public final ConfigEntry<Integer> lodChunkRenderDistanceRadius = new ConfigEntry.Builder<Integer>()
.linkedTo(() -> Config.this.graphics.quality.lodChunkRenderDistanceRadius)
public static ConfigEntry<EDhApiQualityPreset> qualityPresetSetting = new ConfigEntry.Builder<EDhApiQualityPreset>()
.set(EDhApiQualityPreset.MEDIUM) // the default value is set via the listener when accessed
.comment(""
+ "Changing this setting will modify a number of different settings that will change the \n"
+ "visual fidelity of the rendered LODs.\n"
+ "\n"
+ "Higher settings will improve the graphical quality while increasing GPU and memory use.\n"
+ "")
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(RenderQualityPresetConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<EDhApiThreadPreset> threadPresetSetting = new ConfigEntry.Builder<EDhApiThreadPreset>() public final ConfigEntry<EDhApiQualityPreset> qualityPreset = new ConfigEntry.Builder<EDhApiQualityPreset>()
.preset(
new ConfigEntry.PresetBuilder<>(EDhApiQualityPreset.CUSTOM)
.addConfigEntry(() -> Config.this.graphics.quality.maxHorizontalResolution, map -> {
map.put(EDhApiQualityPreset.MINIMUM, EDhApiMaxHorizontalResolution.TWO_BLOCKS);
map.put(EDhApiQualityPreset.LOW, EDhApiMaxHorizontalResolution.BLOCK);
map.put(EDhApiQualityPreset.MEDIUM, EDhApiMaxHorizontalResolution.BLOCK);
map.put(EDhApiQualityPreset.HIGH, EDhApiMaxHorizontalResolution.BLOCK);
map.put(EDhApiQualityPreset.EXTREME, EDhApiMaxHorizontalResolution.BLOCK);
})
.addConfigEntry(() -> Config.this.graphics.quality.verticalQuality, map -> {
map.put(EDhApiQualityPreset.MINIMUM, EDhApiVerticalQuality.HEIGHT_MAP);
map.put(EDhApiQualityPreset.LOW, EDhApiVerticalQuality.LOW);
map.put(EDhApiQualityPreset.MEDIUM, EDhApiVerticalQuality.MEDIUM);
map.put(EDhApiQualityPreset.HIGH, EDhApiVerticalQuality.HIGH);
map.put(EDhApiQualityPreset.EXTREME, EDhApiVerticalQuality.EXTREME);
})
.addConfigEntry(() -> Config.this.graphics.quality.horizontalQuality, map -> {
map.put(EDhApiQualityPreset.MINIMUM, EDhApiHorizontalQuality.LOWEST);
map.put(EDhApiQualityPreset.LOW, EDhApiHorizontalQuality.LOW);
map.put(EDhApiQualityPreset.MEDIUM, EDhApiHorizontalQuality.MEDIUM);
map.put(EDhApiQualityPreset.HIGH, EDhApiHorizontalQuality.HIGH);
map.put(EDhApiQualityPreset.EXTREME, EDhApiHorizontalQuality.EXTREME);
})
.addConfigEntry(() -> Config.this.graphics.quality.transparency, map -> {
map.put(EDhApiQualityPreset.MINIMUM, EDhApiTransparency.DISABLED);
map.put(EDhApiQualityPreset.LOW, EDhApiTransparency.DISABLED); // should be fake if/when fake is fixed
map.put(EDhApiQualityPreset.MEDIUM, EDhApiTransparency.COMPLETE);
map.put(EDhApiQualityPreset.HIGH, EDhApiTransparency.COMPLETE);
map.put(EDhApiQualityPreset.EXTREME, EDhApiTransparency.COMPLETE);
})
.addConfigEntry(() -> Config.this.graphics.quality.ssao.enabled, map -> {
map.put(EDhApiQualityPreset.MINIMUM, false);
map.put(EDhApiQualityPreset.LOW, false);
map.put(EDhApiQualityPreset.MEDIUM, true);
map.put(EDhApiQualityPreset.HIGH, true);
map.put(EDhApiQualityPreset.EXTREME, true);
})
)
.build();
public final ConfigEntry<EDhApiThreadPreset> threadPreset = new ConfigEntry.Builder<EDhApiThreadPreset>()
.set(EDhApiThreadPreset.LOW_IMPACT) // the default value is set via the listener when accessed .set(EDhApiThreadPreset.LOW_IMPACT) // the default value is set via the listener when accessed
.comment(""
+ "Changing this setting will modify a number of different settings that will change \n"
+ "the load that Distant Horizons is allowed to put on your CPU. \n"
+ "\n"
+ "Higher options will improve LOD generation and loading speed, \n"
+ "but will increase CPU load and may introduce stuttering.\n"
+ "\n"
+ "Note: on CPUs with 4 cores or less these settings will be less effective \n"
+ " and some settings will give similar results. \n"
+ "")
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI) .setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ThreadPresetConfigEventHandler.INSTANCE) .addListener(ThreadPresetConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigLinkedEntry quickEnableWorldGenerator = new ConfigLinkedEntry(Advanced.WorldGenerator.enableDistantGeneration); public final ConfigLinkedEntry quickEnableWorldGenerator = new ConfigLinkedEntry(_Client.Advanced.WorldGenerator.enableDistantGeneration);
public static ConfigLinkedEntry quickLodCloudRendering = new ConfigLinkedEntry(Advanced.Graphics.GenericRendering.enableCloudRendering); public final ConfigLinkedEntry quickLodCloudRendering = new ConfigLinkedEntry(_Client.Advanced.Graphics.GenericRendering.enableCloudRendering);
public static ConfigEntry<Boolean> optionsButton = new ConfigEntry.Builder<Boolean>() public final ConfigEntry<Boolean> showOptionsButton = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) .setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.comment("" + .comment("" +
"Should Distant Horizon's config button appear in the options screen next to fov slider?") "Should Distant Horizon's config button appear in the options screen next to fov slider?")
.build(); .build();
}
public static class Graphics
{
public final Quality quality = new Quality();
public static class Quality
{
public ConfigEntry<Integer> lodChunkRenderDistanceRadius = new ConfigEntry.Builder<Integer>()
.setServersideShortName("renderDistanceRadius")
.setMinDefaultMax(32, 128, 4096)
.comment("" +
"The radius of the mod's render distance. (measured in chunks)\n" +
"On server changes the distance players will receive real-time updates for, if enabled." +
"\n" +
"Note for servers:\n" +
"This setting does not prevent players from generating farther out.\n" +
"If you want to limit performance impact, change rate limits\n" +
"and thread count/runtime ratio settings instead.\n" +
"It also does not affect the visuals on clients.")
.setPerformance(EConfigEntryPerformance.HIGH)
.setSide(EConfigEntryRelevantSide.BOTH)
.build();
public ConfigEntry<EDhApiMaxHorizontalResolution> maxHorizontalResolution = new ConfigEntry.Builder<EDhApiMaxHorizontalResolution>()
.set(EDhApiMaxHorizontalResolution.BLOCK)
.comment(""
+ "What is the maximum detail LODs should be drawn at? \n"
+ "Higher settings will increase memory and GPU usage. \n"
+ "\n"
+ EDhApiMaxHorizontalResolution.CHUNK + ": render 1 LOD for each Chunk. \n"
+ EDhApiMaxHorizontalResolution.HALF_CHUNK + ": render 4 LODs for each Chunk. \n"
+ EDhApiMaxHorizontalResolution.FOUR_BLOCKS + ": render 16 LODs for each Chunk. \n"
+ EDhApiMaxHorizontalResolution.TWO_BLOCKS + ": render 64 LODs for each Chunk. \n"
+ EDhApiMaxHorizontalResolution.BLOCK + ": render 256 LODs for each Chunk (width of one block). \n"
+ "\n"
+ "Lowest Quality: " + EDhApiMaxHorizontalResolution.CHUNK + "\n"
+ "Highest Quality: " + EDhApiMaxHorizontalResolution.BLOCK)
.setPerformance(EConfigEntryPerformance.MEDIUM)
.build();
public ConfigEntry<EDhApiVerticalQuality> verticalQuality = new ConfigEntry.Builder<EDhApiVerticalQuality>()
.set(EDhApiVerticalQuality.MEDIUM)
.comment(""
+ "This indicates how well LODs will represent \n"
+ "overhangs, caves, floating islands, etc. \n"
+ "Higher options will make the world more accurate, but"
+ "will increase memory and GPU usage. \n"
+ "\n"
+ "Lowest Quality: " + EDhApiVerticalQuality.HEIGHT_MAP + "\n"
+ "Highest Quality: " + EDhApiVerticalQuality.EXTREME)
.setPerformance(EConfigEntryPerformance.VERY_HIGH)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
public ConfigEntry<EDhApiHorizontalQuality> horizontalQuality = new ConfigEntry.Builder<EDhApiHorizontalQuality>()
.set(EDhApiHorizontalQuality.MEDIUM)
.comment(""
+ "This indicates how quickly LODs decrease in quality the further away they are. \n"
+ "Higher settings will render higher quality fake chunks farther away, \n"
+ "but will increase memory and GPU usage.")
.setPerformance(EConfigEntryPerformance.MEDIUM)
.build();
public ConfigEntry<EDhApiTransparency> transparency = new ConfigEntry.Builder<EDhApiTransparency>()
.set(EDhApiTransparency.COMPLETE)
.comment(""
+ "How should LOD transparency be handled. \n"
+ "\n"
+ EDhApiTransparency.COMPLETE + ": LODs will render transparent. \n"
+ EDhApiTransparency.FAKE + ": LODs will be opaque, but shaded to match the blocks underneath. \n"
+ EDhApiTransparency.DISABLED + ": LODs will be opaque. \n"
+ "")
.setPerformance(EConfigEntryPerformance.MEDIUM)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
public ConfigEntry<EDhApiBlocksToAvoid> blocksToIgnore = new ConfigEntry.Builder<EDhApiBlocksToAvoid>()
.set(EDhApiBlocksToAvoid.NON_COLLIDING)
.comment(""
+ "What blocks shouldn't be rendered as LODs? \n"
+ "\n"
+ EDhApiBlocksToAvoid.NONE + ": Represent all blocks in the LODs \n"
+ EDhApiBlocksToAvoid.NON_COLLIDING + ": Only represent solid blocks in the LODs (tall grass, torches, etc. won't count for a LOD's height) \n"
+ "")
.setPerformance(EConfigEntryPerformance.NONE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
public ConfigEntry<Boolean> tintWithAvoidedBlocks = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "Should the blocks underneath avoided blocks gain the color of the avoided block? \n"
+ "\n"
+ "True: a red flower will tint the grass below it red. \n"
+ "False: skipped blocks will not change color of surface below them. "
+ "")
.setPerformance(EConfigEntryPerformance.NONE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
// TODO fixme
//public static ConfigEntry<Integer> lodBiomeBlending = new ConfigEntry.Builder<Integer>()
// .setMinDefaultMax(0,1,7)
// .comment(""
// + "This is the same as vanilla Biome Blending settings for Lod area. \n"
// + " Note that anything other than '0' will greatly effect Lod building time \n"
// + " and increase triangle count. The cost on chunk generation speed is also \n"
// + " quite large if set too high.\n"
// + "\n"
// + " '0' equals to Vanilla Biome Blending of '1x1' or 'OFF', \n"
// + " '1' equals to Vanilla Biome Blending of '3x3', \n"
// + " '2' equals to Vanilla Biome Blending of '5x5'...")
// .build();
}
}
public final Debugging debugging = new Debugging();
public static class Debugging
{
public ConfigEntry<EDhApiRendererMode> rendererMode = new ConfigEntry.Builder<EDhApiRendererMode>()
.set(EDhApiRendererMode.DEFAULT)
.comment(""
+ "What renderer is active? \n"
+ "\n"
+ EDhApiRendererMode.DEFAULT + ": Default lod renderer \n"
+ EDhApiRendererMode.DEBUG + ": Debug testing renderer \n"
+ EDhApiRendererMode.DISABLED + ": Disable rendering")
.build();
}
public static class _Client
{
public static ConfigCategory advanced = new ConfigCategory.Builder().set(Advanced.class).build(); public static ConfigCategory advanced = new ConfigCategory.Builder().set(Advanced.class).build();
@@ -147,111 +321,6 @@ public class Config
public static class Quality public static class Quality
{ {
public static ConfigEntry<EDhApiMaxHorizontalResolution> maxHorizontalResolution = new ConfigEntry.Builder<EDhApiMaxHorizontalResolution>()
.set(EDhApiMaxHorizontalResolution.BLOCK)
.comment(""
+ "What is the maximum detail LODs should be drawn at? \n"
+ "Higher settings will increase memory and GPU usage. \n"
+ "\n"
+ EDhApiMaxHorizontalResolution.CHUNK + ": render 1 LOD for each Chunk. \n"
+ EDhApiMaxHorizontalResolution.HALF_CHUNK + ": render 4 LODs for each Chunk. \n"
+ EDhApiMaxHorizontalResolution.FOUR_BLOCKS + ": render 16 LODs for each Chunk. \n"
+ EDhApiMaxHorizontalResolution.TWO_BLOCKS + ": render 64 LODs for each Chunk. \n"
+ EDhApiMaxHorizontalResolution.BLOCK + ": render 256 LODs for each Chunk (width of one block). \n"
+ "\n"
+ "Lowest Quality: " + EDhApiMaxHorizontalResolution.CHUNK + "\n"
+ "Highest Quality: " + EDhApiMaxHorizontalResolution.BLOCK)
.setPerformance(EConfigEntryPerformance.MEDIUM)
.build();
public static ConfigEntry<Integer> lodChunkRenderDistanceRadius = new ConfigEntry.Builder<Integer>()
.setServersideShortName("renderDistanceRadius")
.setMinDefaultMax(32, 128, 4096)
.comment("" +
"The radius of the mod's render distance. (measured in chunks)\n" +
"On server changes the distance players will receive real-time updates for, if enabled." +
"\n" +
"Note for servers:\n" +
"This setting does not prevent players from generating farther out.\n" +
"If you want to limit performance impact, change rate limits\n" +
"and thread count/runtime ratio settings instead.\n" +
"It also does not affect the visuals on clients.")
.setPerformance(EConfigEntryPerformance.HIGH)
.build();
public static ConfigEntry<EDhApiVerticalQuality> verticalQuality = new ConfigEntry.Builder<EDhApiVerticalQuality>()
.set(EDhApiVerticalQuality.MEDIUM)
.comment(""
+ "This indicates how well LODs will represent \n"
+ "overhangs, caves, floating islands, etc. \n"
+ "Higher options will make the world more accurate, but"
+ "will increase memory and GPU usage. \n"
+ "\n"
+ "Lowest Quality: " + EDhApiVerticalQuality.HEIGHT_MAP + "\n"
+ "Highest Quality: " + EDhApiVerticalQuality.EXTREME)
.setPerformance(EConfigEntryPerformance.VERY_HIGH)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
public static ConfigEntry<EDhApiHorizontalQuality> horizontalQuality = new ConfigEntry.Builder<EDhApiHorizontalQuality>()
.set(EDhApiHorizontalQuality.MEDIUM)
.comment(""
+ "This indicates how quickly LODs decrease in quality the further away they are. \n"
+ "Higher settings will render higher quality fake chunks farther away, \n"
+ "but will increase memory and GPU usage.")
.setPerformance(EConfigEntryPerformance.MEDIUM)
.build();
public static ConfigEntry<EDhApiTransparency> transparency = new ConfigEntry.Builder<EDhApiTransparency>()
.set(EDhApiTransparency.COMPLETE)
.comment(""
+ "How should LOD transparency be handled. \n"
+ "\n"
+ EDhApiTransparency.COMPLETE + ": LODs will render transparent. \n"
+ EDhApiTransparency.FAKE + ": LODs will be opaque, but shaded to match the blocks underneath. \n"
+ EDhApiTransparency.DISABLED + ": LODs will be opaque. \n"
+ "")
.setPerformance(EConfigEntryPerformance.MEDIUM)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
public static ConfigEntry<EDhApiBlocksToAvoid> blocksToIgnore = new ConfigEntry.Builder<EDhApiBlocksToAvoid>()
.set(EDhApiBlocksToAvoid.NON_COLLIDING)
.comment(""
+ "What blocks shouldn't be rendered as LODs? \n"
+ "\n"
+ EDhApiBlocksToAvoid.NONE + ": Represent all blocks in the LODs \n"
+ EDhApiBlocksToAvoid.NON_COLLIDING + ": Only represent solid blocks in the LODs (tall grass, torches, etc. won't count for a LOD's height) \n"
+ "")
.setPerformance(EConfigEntryPerformance.NONE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
public static ConfigEntry<Boolean> tintWithAvoidedBlocks = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "Should the blocks underneath avoided blocks gain the color of the avoided block? \n"
+ "\n"
+ "True: a red flower will tint the grass below it red. \n"
+ "False: skipped blocks will not change color of surface below them. "
+ "")
.setPerformance(EConfigEntryPerformance.NONE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
// TODO fixme
// public static ConfigEntry<Integer> lodBiomeBlending = new ConfigEntry.Builder<Integer>()
// .setMinDefaultMax(0,1,7)
// .comment(""
// + "This is the same as vanilla Biome Blending settings for Lod area. \n"
// + " Note that anything other than '0' will greatly effect Lod building time \n"
// + " and increase triangle count. The cost on chunk generation speed is also \n"
// + " quite large if set too high.\n"
// + "\n"
// + " '0' equals to Vanilla Biome Blending of '1x1' or 'OFF', \n"
// + " '1' equals to Vanilla Biome Blending of '3x3', \n"
// + " '2' equals to Vanilla Biome Blending of '5x5'...")
// .build();
} }
public static class Fog public static class Fog
@@ -735,6 +804,7 @@ public class Config
.comment("" .comment(""
+ " Should Distant Horizons slowly generate LODs \n" + " Should Distant Horizons slowly generate LODs \n"
+ " outside the vanilla render distance?") + " outside the vanilla render distance?")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static ConfigEntry<EDhApiDistantGeneratorMode> distantGeneratorMode = new ConfigEntry.Builder<EDhApiDistantGeneratorMode>() public static ConfigEntry<EDhApiDistantGeneratorMode> distantGeneratorMode = new ConfigEntry.Builder<EDhApiDistantGeneratorMode>()
@@ -768,15 +838,14 @@ public class Config
+ EDhApiDistantGeneratorMode.FEATURES + " \n" + EDhApiDistantGeneratorMode.FEATURES + " \n"
+ "Generate everything except structures. \n" + "Generate everything except structures. \n"
+ "WARNING: This may cause world generator bugs or instability when paired with certain world generator mods. \n" + "WARNING: This may cause world generator bugs or instability when paired with certain world generator mods. \n"
//not currently implemented
//+ "\n"
//+ EDhApiDistantGeneratorMode.FULL + " \n"
//+ "Ask the local server to generate/load each chunk. \n"
//+ "This is the most compatible, but will cause server/simulation lag. \n"
//+ "- Slow (15-50 ms, with spikes up to 200 ms) \n"
+ "") + "")
/* .setSide(EConfigEntryRelevantSide.BOTH)
// FULL isn't currently implemented
+ "\n"
+ EDhApiDistantGeneratorMode.FULL + " \n"
+ "Ask the local server to generate/load each chunk. \n"
+ "This is the most compatible, but will cause server/simulation lag. \n"
+ "- Slow (15-50 ms, with spikes up to 200 ms) \n"
*/
.build(); .build();
public static ConfigEntry<Integer> worldGenerationTimeoutLengthInSeconds = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> worldGenerationTimeoutLengthInSeconds = new ConfigEntry.Builder<Integer>()
@@ -787,15 +856,18 @@ public class Config
+ "Note: If you are experiencing timeout errors it is better to lower your CPU usage first \n" + "Note: If you are experiencing timeout errors it is better to lower your CPU usage first \n"
+ "via the thread config before changing this value. \n" + "via the thread config before changing this value. \n"
+ "") + "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
} }
public static class LodBuilding public static class LodBuilding
{ {
@Deprecated
public static ConfigEntry<Integer> minTimeBetweenChunkUpdatesInSeconds = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> minTimeBetweenChunkUpdatesInSeconds = new ConfigEntry.Builder<Integer>()
.setServersideShortName("minTimeBetweenChunkUpdates") .setServersideShortName("minTimeBetweenChunkUpdates")
.setMinDefaultMax(0, 1, 60) .setMinDefaultMax(0, 1, 60)
.setAppearance(EConfigEntryAppearance.ONLY_IN_API)
.comment("" .comment(""
+ "Determines how long must pass between LOD chunk updates before another. \n" + "Determines how long must pass between LOD chunk updates before another. \n"
+ "update can occur\n" + "update can occur\n"
@@ -856,7 +928,7 @@ public class Config
+ "unaffected until it needs to be re-written to the database.\n" + "unaffected until it needs to be re-written to the database.\n"
+ "\n" + "\n"
+ EDhApiDataCompressionMode.UNCOMPRESSED + " \n" + EDhApiDataCompressionMode.UNCOMPRESSED + " \n"
+ "Should only be used for testing, is worse in every way vs [" + EDhApiDataCompressionMode.LZ4 + "].\n" + "Should only be used for testing, is worse in every way vs ["+EDhApiDataCompressionMode.LZ4+"].\n"
+ "Expected Compression Ratio: 1.0\n" + "Expected Compression Ratio: 1.0\n"
+ "Estimated average DTO read speed: 1.64 milliseconds\n" + "Estimated average DTO read speed: 1.64 milliseconds\n"
+ "Estimated average DTO write speed: 12.44 milliseconds\n" + "Estimated average DTO write speed: 12.44 milliseconds\n"
@@ -896,7 +968,7 @@ public class Config
.build(); .build();
public static ConfigEntry<String> ignoredRenderBlockCsv = new ConfigEntry.Builder<String>() public static ConfigEntry<String> ignoredRenderBlockCsv = new ConfigEntry.Builder<String>()
.set("minecraft:barrier,minecraft:structure_void,minecraft:light,minecraft:tripwire") .set("minecraft:barrier,minecraft:structure_void,minecraft:light,minecraft:tripwire,minecraft:brown_mushroom")
.comment("" .comment(""
+ "A comma separated list of block resource locations that won't be rendered by DH. \n" + "A comma separated list of block resource locations that won't be rendered by DH. \n"
+ "Note: air is always included in this list. \n" + "Note: air is always included in this list. \n"
@@ -977,14 +1049,7 @@ public class Config
+ "") + "")
.build(); .build();
// not currently implemented
public static ConfigEntry<Boolean> enableMultiverseNetworking = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "If true Distant Horizons will attempt to communicate with the connected \n"
+ "server in order to improve multiverse support. \n"
+ "")
.build();
public static ConfigCategory serverNetworking = new ConfigCategory.Builder().set(ServerNetworking.class).build(); public static ConfigCategory serverNetworking = new ConfigCategory.Builder().set(ServerNetworking.class).build();
@@ -1005,10 +1070,10 @@ public class Config
+ "\n" + "\n"
+ "This should only be used on trusted servers with trusted players!\n" + "This should only be used on trusted servers with trusted players!\n"
+ "") + "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static ConfigEntry<Boolean> sendLevelKeys = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> sendLevelKeys = new ConfigEntry.Builder<Boolean>()
.setServersideShortName("sendLevelKeys") .setServersideShortName("sendLevelKeys")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) .setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
@@ -1017,20 +1082,18 @@ public class Config
+ "Makes the server send level keys for each world.\n" + "Makes the server send level keys for each world.\n"
+ "Disable this if you use alternative ways to send level keys.\n" + "Disable this if you use alternative ways to send level keys.\n"
+ "") + "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static ConfigEntry<String> levelKeyPrefix = new ConfigEntry.Builder<String>() public static ConfigEntry<String> levelKeyPrefix = new ConfigEntry.Builder<String>()
.setServersideShortName("levelKeyPrefix") .setServersideShortName("levelKeyPrefix")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) .setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.set( .set(getDefaultLevelKeyPrefix())
Suppliers.compose(wrapper -> !wrapper.isDedicatedServer() || wrapper.isWorldInitialized(), () -> SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class)).get()
? ""
: "server" + ThreadLocalRandom.current().nextInt(1, 1000)
)
.comment("" .comment(""
+ "Prefix of the level keys sent to the clients.\n" + "Prefix of the level keys sent to the clients.\n"
+ "Should be set to a unique value for each backend server behind a proxy,\n" + "Should be set to a unique value for each backend server behind a proxy,\n"
+ "or empty if you don't use a proxy.\n" + "or empty if you don't use a proxy.\n"
+ "") + "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
@@ -1042,6 +1105,7 @@ public class Config
+ "How many LOD generation requests per second should a client send? \n" + "How many LOD generation requests per second should a client send? \n"
+ "Also limits the amount of player's requests allowed to stay in the server's queue." + "Also limits the amount of player's requests allowed to stay in the server's queue."
+ "") + "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
@@ -1052,9 +1116,11 @@ public class Config
.comment("" .comment(""
+ "If true, the client will receive real-time LOD updates for chunks outside the client's render distance." + "If true, the client will receive real-time LOD updates for chunks outside the client's render distance."
+ "") + "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
// TODO rename
public static ConfigUIComment syncOnLoginSectionNote = new ConfigUIComment(); public static ConfigUIComment syncOnLoginSectionNote = new ConfigUIComment();
public static ConfigEntry<Boolean> synchronizeOnLogin = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> synchronizeOnLogin = new ConfigEntry.Builder<Boolean>()
.setServersideShortName("synchronizeOnLogin") .setServersideShortName("synchronizeOnLogin")
@@ -1062,6 +1128,7 @@ public class Config
.comment("" .comment(""
+ "If true, clients will receive updated LODs on join if any changes occurred since last join." + "If true, clients will receive updated LODs on join if any changes occurred since last join."
+ "") + "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static ConfigEntry<Integer> syncOnLoginRateLimit = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> syncOnLoginRateLimit = new ConfigEntry.Builder<Integer>()
@@ -1071,6 +1138,7 @@ public class Config
+ "How many LOD sync requests per second should a client send? \n" + "How many LOD sync requests per second should a client send? \n"
+ "Also limits the amount of player's requests allowed to stay in the server's queue." + "Also limits the amount of player's requests allowed to stay in the server's queue."
+ "") + "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
} }
@@ -1079,17 +1147,17 @@ public class Config
public static class MultiThreading public static class MultiThreading
{ {
public static final String THREAD_NOTE = "" public static final String THREAD_NOTE = ""
+ "Multi-threading Note:\n" + "Multi-threading Note: \n"
+ "If the total thread count in Distant Horizon's config is more threads than your CPU has cores,\n" + "If the total thread count in Distant Horizon's config is more threads than your CPU has cores, \n"
+ "CPU performance may suffer if Distant Horizons has a lot to load or generate.\n" + "CPU performance may suffer if Distant Horizons has a lot to load or generate. \n"
+ "This can be an issue when first loading into a world, when flying, and/or when generating new terrain."; + "This can be an issue when first loading into a world, when flying, and/or when generating new terrain.";
public static final String THREAD_RUN_TIME_RATIO_NOTE = "" public static final String THREAD_RUN_TIME_RATIO_NOTE = ""
+ "If this value is less than 1.0, it will be treated as a percentage\n" + "If this value is less than 1.0, it will be treated as a percentage \n"
+ "of time each thread can run before going idle.\n" + "of time each thread can run before going idle. \n"
+ "\n" + "\n"
+ "This can be used to reduce CPU usage if the thread count\n" + "This can be used to reduce CPU usage if the thread count \n"
+ "is already set to 1 for the given option, or more finely\n" + "is already set to 1 for the given option, or more finely \n"
+ "tune CPU performance."; + "tune CPU performance.";
@@ -1099,20 +1167,22 @@ public class Config
ThreadPresetConfigEventHandler.getWorldGenDefaultThreadCount(), ThreadPresetConfigEventHandler.getWorldGenDefaultThreadCount(),
Runtime.getRuntime().availableProcessors()) Runtime.getRuntime().availableProcessors())
.comment("" .comment(""
+ "How many threads should be used when generating LOD\n" + "How many threads should be used when generating LOD \n"
+ "chunks outside the normal render distance?\n" + "chunks outside the normal render distance? \n"
+ "\n" + "\n"
+ "If you experience stuttering when generating distant LODs,\n" + "If you experience stuttering when generating distant LODs, \n"
+ "decrease this number.\n" + "decrease this number. \n"
+ "If you want to increase LOD\n" + "If you want to increase LOD \n"
+ "generation speed, increase this number.\n" + "generation speed, increase this number. \n"
+ "\n" + "\n"
+ THREAD_NOTE) + THREAD_NOTE)
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static final ConfigEntry<Double> runTimeRatioForWorldGenerationThreads = new ConfigEntry.Builder<Double>() public static final ConfigEntry<Double> runTimeRatioForWorldGenerationThreads = new ConfigEntry.Builder<Double>()
.setServersideShortName("runTimeRatioForWorldGenerationThreads") .setServersideShortName("runTimeRatioForWorldGenerationThreads")
.setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getWorldGenDefaultRunTimeRatio(), 1.0) .setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getWorldGenDefaultRunTimeRatio(), 1.0)
.comment(THREAD_RUN_TIME_RATIO_NOTE) .comment(THREAD_RUN_TIME_RATIO_NOTE)
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static final ConfigEntry<Integer> numberOfFileHandlerThreads = new ConfigEntry.Builder<Integer>() public static final ConfigEntry<Integer> numberOfFileHandlerThreads = new ConfigEntry.Builder<Integer>()
@@ -1128,11 +1198,13 @@ public class Config
+ "quickly flying through existing LODs. \n" + "quickly flying through existing LODs. \n"
+ "\n" + "\n"
+ THREAD_NOTE) + THREAD_NOTE)
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static final ConfigEntry<Double> runTimeRatioForFileHandlerThreads = new ConfigEntry.Builder<Double>() public static final ConfigEntry<Double> runTimeRatioForFileHandlerThreads = new ConfigEntry.Builder<Double>()
.setServersideShortName("runTimeRatioForFileHandlerThreads") .setServersideShortName("runTimeRatioForFileHandlerThreads")
.setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getFileHandlerDefaultRunTimeRatio(), 1.0) .setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getFileHandlerDefaultRunTimeRatio(), 1.0)
.comment(THREAD_RUN_TIME_RATIO_NOTE) .comment(THREAD_RUN_TIME_RATIO_NOTE)
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static final ConfigEntry<Integer> numberOfUpdatePropagatorThreads = new ConfigEntry.Builder<Integer>() public static final ConfigEntry<Integer> numberOfUpdatePropagatorThreads = new ConfigEntry.Builder<Integer>()
@@ -1172,11 +1244,13 @@ public class Config
+ "certain graphics settings are changed, and when moving around the world. \n" + "certain graphics settings are changed, and when moving around the world. \n"
+ "\n" + "\n"
+ THREAD_NOTE) + THREAD_NOTE)
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static final ConfigEntry<Double> runTimeRatioForLodBuilderThreads = new ConfigEntry.Builder<Double>() public static final ConfigEntry<Double> runTimeRatioForLodBuilderThreads = new ConfigEntry.Builder<Double>()
.setServersideShortName("runTimeRatioForLodBuilderThreads") .setServersideShortName("runTimeRatioForLodBuilderThreads")
.setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getLodBuilderDefaultRunTimeRatio(), 1.0) .setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getLodBuilderDefaultRunTimeRatio(), 1.0)
.comment(THREAD_RUN_TIME_RATIO_NOTE) .comment(THREAD_RUN_TIME_RATIO_NOTE)
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static final ConfigEntry<Boolean> enableLodBuilderThreadLimiting = new ConfigEntry.Builder<Boolean>() public static final ConfigEntry<Boolean> enableLodBuilderThreadLimiting = new ConfigEntry.Builder<Boolean>()
.setServersideShortName("enableLodBuilderThreadLimiting") .setServersideShortName("enableLodBuilderThreadLimiting")
@@ -1187,6 +1261,7 @@ public class Config
+ "\n" + "\n"
+ "Note that if deadlock did occur restarting MC may be necessary to stop the locked threads. \n" + "Note that if deadlock did occur restarting MC may be necessary to stop the locked threads. \n"
+ "") + "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static final ConfigEntry<Integer> numberOfNetworkCompressionThreads = new ConfigEntry.Builder<Integer>() public static final ConfigEntry<Integer> numberOfNetworkCompressionThreads = new ConfigEntry.Builder<Integer>()
@@ -1195,12 +1270,14 @@ public class Config
ThreadPresetConfigEventHandler.getNetworkCompressionDefaultThreadCount(), ThreadPresetConfigEventHandler.getNetworkCompressionDefaultThreadCount(),
Runtime.getRuntime().availableProcessors()) Runtime.getRuntime().availableProcessors())
.comment("" .comment(""
+ "How many threads should be used when building LODs? \n" + "How many threads should be used when (de)compressing LODs \n"
+ "that are received/sent over the network?\n"
+ "\n" + "\n"
+ "These threads run when terrain is generated, when\n" + "This pool doesn't do anything in singleplayer or when connected \n"
+ "certain graphics settings are changed, and when moving around the world. \n" + "to a server that doesn't support DH networking. \n"
+ "\n" + "\n"
+ THREAD_NOTE) + THREAD_NOTE)
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static final ConfigEntry<Double> runTimeRatioForNetworkCompressionThreads = new ConfigEntry.Builder<Double>() public static final ConfigEntry<Double> runTimeRatioForNetworkCompressionThreads = new ConfigEntry.Builder<Double>()
.setServersideShortName("runTimeRatioForNetworkCompressionThreads") .setServersideShortName("runTimeRatioForNetworkCompressionThreads")
@@ -1245,6 +1322,7 @@ public class Config
.comment("" .comment(""
+ "If enabled, the mod will log information about the world generation process. \n" + "If enabled, the mod will log information about the world generation process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logWorldGenPerformance = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logWorldGenPerformance = new ConfigEntry.Builder<EDhApiLoggerMode>()
@@ -1253,6 +1331,7 @@ public class Config
.comment("" .comment(""
+ "If enabled, the mod will log performance about the world generation process. \n" + "If enabled, the mod will log performance about the world generation process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logWorldGenLoadEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logWorldGenLoadEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
@@ -1261,6 +1340,7 @@ public class Config
.comment("" .comment(""
+ "If enabled, the mod will log information about the world generation process. \n" + "If enabled, the mod will log information about the world generation process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logLodBuilderEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logLodBuilderEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
@@ -1269,6 +1349,7 @@ public class Config
.comment("" .comment(""
+ "If enabled, the mod will log information about the LOD generation process. \n" + "If enabled, the mod will log information about the LOD generation process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logRendererBufferEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logRendererBufferEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
@@ -1291,6 +1372,7 @@ public class Config
.comment("" .comment(""
+ "If enabled, the mod will log information about file read/write operations. \n" + "If enabled, the mod will log information about file read/write operations. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logFileSubDimEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logFileSubDimEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
@@ -1299,6 +1381,7 @@ public class Config
.comment("" .comment(""
+ "If enabled, the mod will log information about file sub-dimension operations. \n" + "If enabled, the mod will log information about file sub-dimension operations. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logNetworkEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logNetworkEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
@@ -1307,6 +1390,7 @@ public class Config
.comment("" .comment(""
+ "If enabled, the mod will log information about network operations. \n" + "If enabled, the mod will log information about network operations. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
@@ -1554,8 +1638,8 @@ public class Config
"Can be changed if you experience crashing when loading into a world.\n" + "Can be changed if you experience crashing when loading into a world.\n" +
"\n" + "\n" +
"Defines the OpenGL context type Distant Horizon's will create. \n" + "Defines the OpenGL context type Distant Horizon's will create. \n" +
"Generally this should be left as [" + EDhApiGlProfileMode.CORE + "] unless there is an issue with your GPU driver. \n" + "Generally this should be left as ["+ EDhApiGlProfileMode.CORE+"] unless there is an issue with your GPU driver. \n" +
"Possible values: [" + StringUtil.join("],[", EDhApiGlProfileMode.values()) + "] \n" + "Possible values: ["+ StringUtil.join("],[", EDhApiGlProfileMode.values())+"] \n" +
"") "")
.build(); .build();
public static ConfigEntry<Boolean> enableGlForwardCompatibilityMode = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableGlForwardCompatibilityMode = new ConfigEntry.Builder<Boolean>()
@@ -1718,4 +1802,17 @@ public class Config
} }
} }
private static String getDefaultLevelKeyPrefix()
{
IMinecraftSharedWrapper mcWrapper = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
if (!mcWrapper.isDedicatedServer() || mcWrapper.isWorldNew())
{
return "";
}
else
{
return "server" + ThreadLocalRandom.current().nextInt(1, 1000);
}
}
} }
@@ -0,0 +1,6 @@
package com.seibel.distanthorizons.core.config;
public interface IConfigProvider
{
}
@@ -102,7 +102,7 @@ public class RenderCacheConfigEventHandler
// create a new timer task // create a new timer task
TimerTask timerTask = new TimerTask() TimerTask timerTask = new TimerTask()
{ {
public void run() @Override public void run()
{ {
DhApi.Delayed.renderProxy.clearRenderDataCache(); DhApi.Delayed.renderProxy.clearRenderDataCache();
} }
@@ -32,7 +32,7 @@ public class ResetConfigEventHandler
/** private since we only ever need one handler at a time */ /** private since we only ever need one handler at a time */
private ResetConfigEventHandler() private ResetConfigEventHandler()
{ {
this.configChangeListener = new ConfigChangeListener<>(Config.Client.ResetConfirmation.resetAllSettings, (resetSettings) -> { doStuff(resetSettings); }); this.configChangeListener = new ConfigChangeListener<>(Config.Client.ResetConfirmation.resetAllSettings, (resetSettings) -> { this.doStuff(resetSettings); });
} }
@@ -19,7 +19,6 @@
package com.seibel.distanthorizons.core.config.eventHandlers.presets; package com.seibel.distanthorizons.core.config.eventHandlers.presets;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.ConfigBase; import com.seibel.distanthorizons.core.config.ConfigBase;
import com.seibel.distanthorizons.core.config.ConfigEntryWithPresetOptions; import com.seibel.distanthorizons.core.config.ConfigEntryWithPresetOptions;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener; import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
@@ -30,6 +29,7 @@ import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry;
import com.seibel.distanthorizons.coreapi.util.StringUtil; import com.seibel.distanthorizons.coreapi.util.StringUtil;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.*;
@@ -38,6 +38,7 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
private static final Logger LOGGER = LogManager.getLogger(); private static final Logger LOGGER = LogManager.getLogger();
private static final long MS_DELAY_BEFORE_APPLYING_PRESET = 3_000; private static final long MS_DELAY_BEFORE_APPLYING_PRESET = 3_000;
@Nullable
private static IConfigGui configGui = SingletonInjector.INSTANCE.get(IConfigGui.class); private static IConfigGui configGui = SingletonInjector.INSTANCE.get(IConfigGui.class);
private static boolean guiListenersAdded = false; private static boolean guiListenersAdded = false;
@@ -57,7 +58,9 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
public AbstractPresetConfigEventHandler() public AbstractPresetConfigEventHandler()
{ {
if (configGui != null) { // don't update the UI when running on a server
if (configGui != null)
{
configGui.addOnScreenChangeListener(this::onConfigUiClosed); configGui.addOnScreenChangeListener(this::onConfigUiClosed);
} }
} }
@@ -33,6 +33,7 @@ import org.apache.logging.log4j.Logger;
import java.util.*; import java.util.*;
@SuppressWarnings("FieldCanBeLocal")
public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigEventHandler<EDhApiQualityPreset> public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigEventHandler<EDhApiQualityPreset>
{ {
public static final RenderQualityPresetConfigEventHandler INSTANCE = new RenderQualityPresetConfigEventHandler(); public static final RenderQualityPresetConfigEventHandler INSTANCE = new RenderQualityPresetConfigEventHandler();
@@ -20,6 +20,7 @@
package com.seibel.distanthorizons.core.config.file; package com.seibel.distanthorizons.core.config.file;
import com.electronwill.nightconfig.core.file.CommentedFileConfig; import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryRelevantSide;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.config.ConfigBase; import com.seibel.distanthorizons.core.config.ConfigBase;
import com.seibel.distanthorizons.core.config.types.AbstractConfigType; import com.seibel.distanthorizons.core.config.types.AbstractConfigType;
@@ -41,23 +42,35 @@ import java.nio.file.Path;
*/ */
public class ConfigFileHandling public class ConfigFileHandling
{ {
private static final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
public final ConfigBase configBase; public final ConfigBase configBase;
public final Path configPath; public final Path configPath;
private final Logger LOGGER; private final Logger logger;
/** This is the object for night-config */ /** This is the object for night-config */
private final CommentedFileConfig nightConfig; private final CommentedFileConfig nightConfig;
//=============//
// constructor //
//=============//
public ConfigFileHandling(ConfigBase configBase, Path configPath) public ConfigFileHandling(ConfigBase configBase, Path configPath)
{ {
this.LOGGER = LogManager.getLogger(this.getClass().getSimpleName() + ", " + configBase.modID); this.logger = LogManager.getLogger(this.getClass().getSimpleName() + ", " + configBase.modID);
this.configBase = configBase; this.configBase = configBase;
this.configPath = configPath; this.configPath = configPath;
this.nightConfig = CommentedFileConfig.builder(this.configPath.toFile()).build(); this.nightConfig = CommentedFileConfig.builder(this.configPath.toFile()).build();
} }
/** Saves the entire config to the file */ /** Saves the entire config to the file */
public void saveToFile() public void saveToFile()
{ {
@@ -103,7 +116,7 @@ public class ConfigFileHandling
*/ */
public void loadFromFile() public void loadFromFile()
{ {
int currentCfgVersion = configBase.configVersion; int currentCfgVersion = this.configBase.configVersion;
try try
{ {
// Dont load the real `this.nightConfig`, instead create a tempoary one // Dont load the real `this.nightConfig`, instead create a tempoary one
@@ -114,27 +127,29 @@ public class ConfigFileHandling
tmpNightConfig.close(); tmpNightConfig.close();
} catch (Exception ignored) { } } catch (Exception ignored) { }
if (currentCfgVersion == configBase.configVersion) if (currentCfgVersion == this.configBase.configVersion)
{}
else if (currentCfgVersion > configBase.configVersion)
{ {
LOGGER.warn("Found config version [" + String.valueOf(currentCfgVersion) + "] which is newer than current mods config version of [" + String.valueOf(configBase.configVersion) + "]. You may have downgraded the mod and items may have been moved, you have been warned"); // handle normally
}
else if (currentCfgVersion > this.configBase.configVersion)
{
this.logger.warn("Found config version [" + currentCfgVersion + "] which is newer than current mods config version of [" + this.configBase.configVersion + "]. You may have downgraded the mod and items may have been moved, you have been warned");
} }
else // if (currentCfgVersion < configBase.configVersion) else // if (currentCfgVersion < configBase.configVersion)
{ {
LOGGER.warn(configBase.modName +" config is of an older version, currently there is no config updater... so resetting config"); this.logger.warn(this.configBase.modName +" config is of an older version, currently there is no config updater... so resetting config");
try try
{ {
Files.delete(configPath); Files.delete(this.configPath);
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.error(e); this.logger.error(e);
} }
} }
loadFromFile(nightConfig); this.loadFromFile(this.nightConfig);
nightConfig.set("_version", configBase.configVersion); this.nightConfig.set("_version", this.configBase.configVersion);
} }
/** /**
* Loads the entire config from the file * Loads the entire config from the file
@@ -185,26 +200,34 @@ public class ConfigFileHandling
// Save an entry when only given the entry // Save an entry when only given the entry
public void saveEntry(ConfigEntry<?> entry) public void saveEntry(ConfigEntry<?> entry)
{ {
saveEntry(entry, nightConfig); this.saveEntry(entry, this.nightConfig);
nightConfig.save(); this.nightConfig.save();
} }
/** Save an entry */ /** Save an entry */
public void saveEntry(ConfigEntry<?> entry, CommentedFileConfig workConfig) public void saveEntry(ConfigEntry<?> entry, CommentedFileConfig workConfig)
{ {
if (!entry.getAppearance().showInFile) return; if (!entry.getAppearance().showInFile)
if (SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class).isDedicatedServer() && entry.getServersideShortName() == null) {
return; return;
if (entry.getTrueValue() == null) }
throw new IllegalArgumentException("Entry [" + entry.getNameWCategory() + "] is null, this may be a problem with [" + configBase.modName + "]. Please contact the authors"); else if ((entry.getRelevantSide() == EConfigEntryRelevantSide.CLIENT && MC_SHARED.isDedicatedServer())
|| (entry.getRelevantSide() == EConfigEntryRelevantSide.SERVER && !MC_SHARED.isDedicatedServer()))
{
// don't save server/client specific configs on the opposite
// (this keeps the config file clean of unnecessary items)
return;
}
else if (entry.getTrueValue() == null)
{
// TODO when can this happen?
throw new IllegalArgumentException("Entry [" + entry.getNameWCategory() + "] is null, this may be a problem with [" + this.configBase.modName + "]. Please contact the authors.");
}
workConfig.set(entry.getNameWCategory(), ConfigTypeConverters.attemptToConvertToString(entry.getType(), entry.getTrueValue())); workConfig.set(entry.getNameWCategory(), ConfigTypeConverters.attemptToConvertToString(entry.getType(), entry.getTrueValue()));
} }
/** Loads an entry when only given the entry */ /** Loads an entry when only given the entry */
public void loadEntry(ConfigEntry<?> entry) public void loadEntry(ConfigEntry<?> entry) { this.loadEntry(entry, this.nightConfig); }
{
loadEntry(entry, nightConfig);
}
/** Loads an entry */ /** Loads an entry */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <T> void loadEntry(ConfigEntry<T> entry, CommentedFileConfig nightConfig) public <T> void loadEntry(ConfigEntry<T> entry, CommentedFileConfig nightConfig)
@@ -214,7 +237,7 @@ public class ConfigFileHandling
if (!nightConfig.contains(entry.getNameWCategory())) if (!nightConfig.contains(entry.getNameWCategory()))
{ {
saveEntry(entry, nightConfig); this.saveEntry(entry, nightConfig);
return; return;
} }
@@ -233,7 +256,7 @@ public class ConfigFileHandling
Object convertedValue = ConfigTypeConverters.attemptToConvertFromString(expectedValueClass, value); Object convertedValue = ConfigTypeConverters.attemptToConvertFromString(expectedValueClass, value);
if (!convertedValue.getClass().equals(expectedValueClass)) if (!convertedValue.getClass().equals(expectedValueClass))
{ {
LOGGER.error("Unable to convert config value ["+value+"] from ["+(value != null ? value.getClass() : "NULL")+"] to ["+expectedValueClass+"] for config ["+entry.name+"], " + this.logger.error("Unable to convert config value ["+value+"] from ["+(value != null ? value.getClass() : "NULL")+"] to ["+expectedValueClass+"] for config ["+entry.name+"], " +
"the default config value will be used instead ["+entry.getDefaultValue()+"]. " + "the default config value will be used instead ["+entry.getDefaultValue()+"]. " +
"Make sure a converter is defined in ["+ConfigTypeConverters.class.getSimpleName()+"]."); "Make sure a converter is defined in ["+ConfigTypeConverters.class.getSimpleName()+"].");
convertedValue = entry.getDefaultValue(); convertedValue = entry.getDefaultValue();
@@ -242,34 +265,39 @@ public class ConfigFileHandling
if (entry.getTrueValue() == null) if (entry.getTrueValue() == null)
{ {
LOGGER.warn("Entry [" + entry.getNameWCategory() + "] returned as null from the config. Using default value."); this.logger.warn("Entry [" + entry.getNameWCategory() + "] returned as null from the config. Using default value.");
entry.pureSet(entry.getDefaultValue()); entry.pureSet(entry.getDefaultValue());
} }
} }
catch (Exception e) catch (Exception e)
{ {
// e.printStackTrace(); // e.printStackTrace();
LOGGER.warn("Entry [" + entry.getNameWCategory() + "] had an invalid value when loading the config. Using default value."); this.logger.warn("Entry [" + entry.getNameWCategory() + "] had an invalid value when loading the config. Using default value.");
entry.pureSet(entry.getDefaultValue()); entry.pureSet(entry.getDefaultValue());
} }
} }
// Creates the comment for an entry when only given the entry // Creates the comment for an entry when only given the entry
public void createComment(ConfigEntry<?> entry) public void createComment(ConfigEntry<?> entry) { this.createComment(entry, this.nightConfig); }
{
createComment(entry, nightConfig);
}
// Creates a comment for an entry // Creates a comment for an entry
public void createComment(ConfigEntry<?> entry, CommentedFileConfig nightConfig) public void createComment(ConfigEntry<?> entry, CommentedFileConfig nightConfig)
{ {
if ( if (!entry.getAppearance().showInFile
!entry.getAppearance().showInFile || || entry.getComment() == null)
entry.getComment() == null {
)
return; return;
}
if (SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class).isDedicatedServer() && entry.getServersideShortName() == null)
if ((entry.getRelevantSide() == EConfigEntryRelevantSide.CLIENT && MC_SHARED.isDedicatedServer())
|| (entry.getRelevantSide() == EConfigEntryRelevantSide.SERVER && !MC_SHARED.isDedicatedServer()))
{
// don't save server/client specific configs on the opposite
// (this keeps the config file clean of unnecessary items)
return; return;
}
String comment = entry.getComment().replaceAll("\n", "\n ").trim(); String comment = entry.getComment().replaceAll("\n", "\n ").trim();
// the new line makes it easier to read and separate configs // the new line makes it easier to read and separate configs
@@ -308,7 +336,7 @@ public class ConfigFileHandling
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.warn("Loading file failed because of this expectation:\n" + e); this.logger.warn("Loading file failed because of this expectation:\n" + e);
reCreateFile(this.configPath); reCreateFile(this.configPath);
@@ -318,7 +346,7 @@ public class ConfigFileHandling
catch (Exception ex) catch (Exception ex)
{ {
System.out.println("Creating file failed"); System.out.println("Creating file failed");
LOGGER.error(ex); this.logger.error(ex);
SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class).crashMinecraft("Loading file and resetting config file failed at path [" + configPath + "]. Please check the file is ok and you have the permissions", ex); SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class).crashMinecraft("Loading file and resetting config file failed at path [" + configPath + "]. Please check the file is ok and you have the permissions", ex);
} }
} }
@@ -20,16 +20,23 @@
package com.seibel.distanthorizons.core.config.types; package com.seibel.distanthorizons.core.config.types;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.Table;
import com.seibel.distanthorizons.core.config.NumberUtil; import com.seibel.distanthorizons.core.config.NumberUtil;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener; import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance; import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryPerformance; import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryPerformance;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryRelevantSide;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry; import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/** /**
* Use for making the config variables * Use for making the config variables
@@ -46,6 +53,9 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
private final ArrayList<IConfigListener> listenerList; private final ArrayList<IConfigListener> listenerList;
private final String serversideShortName; private final String serversideShortName;
private final EConfigEntryPerformance performance;
private final EConfigEntryRelevantSide relevantSide;
// API control // // API control //
/** /**
* If true this config can be controlled by the API <br> * If true this config can be controlled by the API <br>
@@ -54,11 +64,15 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
public final boolean allowApiOverride; public final boolean allowApiOverride;
private T apiValue; private T apiValue;
private final EConfigEntryPerformance performance;
/** Creates the entry */ /** Creates the entry */
private ConfigEntry(EConfigEntryAppearance appearance, T value, String comment, T min, T max, String serversideShortName, boolean allowApiOverride, EConfigEntryPerformance performance, ArrayList<IConfigListener> listenerList) private ConfigEntry(
EConfigEntryAppearance appearance,
T value, String comment, T min, T max,
String serversideShortName, boolean allowApiOverride,
EConfigEntryPerformance performance, EConfigEntryRelevantSide relevantSide,
ArrayList<IConfigListener> listenerList)
{ {
super(appearance, value); super(appearance, value);
@@ -68,6 +82,7 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
this.serversideShortName = serversideShortName; this.serversideShortName = serversideShortName;
this.allowApiOverride = allowApiOverride; this.allowApiOverride = allowApiOverride;
this.performance = performance; this.performance = performance;
this.relevantSide = relevantSide;
this.listenerList = listenerList; this.listenerList = listenerList;
} }
@@ -126,9 +141,9 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
@Override @Override
public T get() public T get()
{ {
if (allowApiOverride && apiValue != null) if (this.allowApiOverride && this.apiValue != null)
{ {
return apiValue; return this.apiValue;
} }
return super.get(); return super.get();
@@ -177,10 +192,17 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
public void clampWithinRange(T min, T max) public void clampWithinRange(T min, T max)
{ {
byte validness = this.isValid(min, max); byte validness = this.isValid(min, max);
if (validness == -1) this.value = (T) NumberUtil.getMinimum(this.value.getClass()); if (validness == -1)
if (validness == 1) this.value = (T) NumberUtil.getMaximum(this.value.getClass()); {
this.value = (T) NumberUtil.getMinimum(this.value.getClass());
}
if (validness == 1)
{
this.value = (T) NumberUtil.getMaximum(this.value.getClass());
}
} }
// TODO is this for command line use?
public String getServersideShortName() { return this.serversideShortName; } public String getServersideShortName() { return this.serversideShortName; }
@Override @Override
@@ -190,9 +212,11 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
/** Gets the performance impact of an option */ /** Gets the performance impact of an option */
public EConfigEntryPerformance getPerformance() { return this.performance; } public EConfigEntryPerformance getPerformance() { return this.performance; }
/** Gets whether this config should apply to the client, server, or both */
public EConfigEntryRelevantSide getRelevantSide() { return this.relevantSide; }
/** Fired whenever the config value changes to a new value. */ /** Fired whenever the config value changes to a new value. */
public void addValueChangeListener(Consumer<T> onValueChangeFunc) @Override public void addValueChangeListener(Consumer<T> onValueChangeFunc)
{ {
ConfigChangeListener<T> changeListener = new ConfigChangeListener<>(this, onValueChangeFunc); ConfigChangeListener<T> changeListener = new ConfigChangeListener<>(this, onValueChangeFunc);
this.addListener(changeListener); this.addListener(changeListener);
@@ -223,7 +247,7 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
* <p> -1 == number too low * <p> -1 == number too low
*/ */
@Override @Override
public byte isValid() { return isValid(this.value, this.min, this.max); } public byte isValid() { return this.isValid(this.value, this.min, this.max); }
/** /**
* Checks if a new value is valid * Checks if a new value is valid
* *
@@ -295,107 +319,162 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
} }
/** This should normally not be called since set() automatically calls this */ /** This should normally not be called since set() automatically calls this */
public void save() { configBase.configFileINSTANCE.saveEntry(this); } public void save() { this.configBase.configFileINSTANCE.saveEntry(this); }
/** This should normally not be called except for special circumstances */ /** This should normally not be called except for special circumstances */
public void load() { configBase.configFileINSTANCE.loadEntry(this); } public void load() { this.configBase.configFileINSTANCE.loadEntry(this); }
@Override @Override
public boolean equals(IConfigEntry<?> obj) { return obj.getClass() == ConfigEntry.class && equals((ConfigEntry<?>) obj); } public boolean equals(IConfigEntry<?> obj) { return obj.getClass() == ConfigEntry.class && this.equals((ConfigEntry<?>) obj); }
/** Is the value of this equal to another */ /** Is the value of this equal to another */
public boolean equals(ConfigEntry<?> obj) public boolean equals(ConfigEntry<?> obj)
{ {
// Can all of this just be "return this.value.equals(obj.value)"? // Can all of this just be "return this.value.equals(obj.value)"?
if (Number.class.isAssignableFrom(this.value.getClass())) if (Number.class.isAssignableFrom(this.value.getClass()))
{
return this.value == obj.value; return this.value == obj.value;
}
else else
{
return this.value.equals(obj.value); return this.value.equals(obj.value);
} }
}
public static class Builder<TValue> extends AbstractConfigType.Builder<TValue, Builder<TValue>>
public static class Builder<T> extends AbstractConfigType.Builder<T, Builder<T>>
{ {
private String tmpComment = null; private String tmpComment = null;
private T tmpMin = null; private TValue tmpMin = null;
private T tmpMax = null; private TValue tmpMax = null;
protected String tmpServersideShortName = null; protected String tmpServersideShortName = null;
private boolean tmpUseApiOverwrite = true; private boolean tmpUseApiOverwrite = true;
private EConfigEntryPerformance tmpPerformance = EConfigEntryPerformance.DONT_SHOW; private EConfigEntryPerformance tmpPerformance = EConfigEntryPerformance.DONT_SHOW;
private EConfigEntryRelevantSide tmpRelevantSide = EConfigEntryRelevantSide.CLIENT;
protected ArrayList<IConfigListener> tmpIConfigListener = new ArrayList<>(); protected ArrayList<IConfigListener> tmpIConfigListener = new ArrayList<>();
public Builder<T> comment(String newComment) private Supplier<TValue> computedValueGetter;
private Consumer<TValue> computedValueSetter;
private boolean preventAutoApplyUntilExit;
public Builder<TValue> comment(String newComment)
{ {
this.tmpComment = newComment; this.tmpComment = newComment;
return this; return this;
} }
public Builder<TValue> computed(Supplier<TValue> computedValueGetter, Consumer<TValue> computedValueSetter)
{
this.computedValueGetter = computedValueGetter;
this.computedValueSetter = computedValueSetter;
this.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI);
return this;
}
public Builder<TValue> linkedTo(DeferredAccessor<TValue> otherConfigAccessor)
{
return this.computed(
otherConfigAccessor::get,
otherConfigAccessor::set
);
}
public Builder<TValue> preset(PresetBuilder<TValue> presetBuilder)
{
return this.computed(
() -> {
List<TValue> values = presetBuilder.presetValues.entrySet().stream()
.map(entry -> entry.getValue().inverse().get(entry.getKey().get()))
.distinct().limit(2)
.collect(Collectors.toList());
if (values.size() != 1)
{
return presetBuilder.customStateValue;
}
return values.get(0);
},
value -> {
for (Map.Entry<DeferredAccessor<Object>, BiMap<TValue, Object>> entry : presetBuilder.presetValues.entrySet())
{
entry.getKey().set(entry.getValue().get(value));
}
}
);
}
/** Allows most values to be set by 1 setter */ /** Allows most values to be set by 1 setter */
public Builder<T> setMinDefaultMax(T newMin, T newDefault, T newMax) public Builder<TValue> setMinDefaultMax(TValue newMin, TValue newDefault, TValue newMax)
{ {
this.set(newDefault); this.set(newDefault);
this.setMinMax(newMin, newMax); this.setMinMax(newMin, newMax);
return this; return this;
} }
public Builder<T> setMinMax(T newMin, T newMax) public Builder<TValue> setMinMax(TValue newMin, TValue newMax)
{ {
this.tmpMin = newMin; this.tmpMin = newMin;
this.tmpMax = newMax; this.tmpMax = newMax;
return this; return this;
} }
public Builder<T> setMin(T newMin) public Builder<TValue> setMin(TValue newMin)
{ {
this.tmpMin = newMin; this.tmpMin = newMin;
return this; return this;
} }
public Builder<T> setMax(T newMax) public Builder<TValue> setMax(TValue newMax)
{ {
this.tmpMax = newMax; this.tmpMax = newMax;
return this; return this;
} }
public Builder<T> setServersideShortName(String name) public Builder<TValue> setServersideShortName(String name)
{ {
this.tmpServersideShortName = name; this.tmpServersideShortName = name;
return this; return this;
} }
public Builder<T> setUseApiOverwrite(boolean newUseApiOverwrite) public Builder<TValue> setUseApiOverwrite(boolean newUseApiOverwrite)
{ {
this.tmpUseApiOverwrite = newUseApiOverwrite; this.tmpUseApiOverwrite = newUseApiOverwrite;
return this; return this;
} }
public Builder<T> setPerformance(EConfigEntryPerformance newPerformance) public Builder<TValue> setPerformance(EConfigEntryPerformance newPerformance)
{ {
this.tmpPerformance = newPerformance; this.tmpPerformance = newPerformance;
return this; return this;
} }
public Builder<TValue> setSide(EConfigEntryRelevantSide relevantSide)
{
this.tmpRelevantSide = relevantSide;
return this;
}
public Builder<T> replaceListeners(ArrayList<IConfigListener> newConfigListener)
public Builder<TValue> replaceListeners(ArrayList<IConfigListener> newConfigListener)
{ {
this.tmpIConfigListener = newConfigListener; this.tmpIConfigListener = newConfigListener;
return this; return this;
} }
public Builder<T> addListeners(IConfigListener... newConfigListener) public Builder<TValue> addListeners(IConfigListener... newConfigListener)
{ {
this.tmpIConfigListener.addAll(Arrays.asList(newConfigListener)); this.tmpIConfigListener.addAll(Arrays.asList(newConfigListener));
return this; return this;
} }
public Builder<T> addListener(IConfigListener newConfigListener) public Builder<TValue> addListener(IConfigListener newConfigListener)
{ {
this.tmpIConfigListener.add(newConfigListener); this.tmpIConfigListener.add(newConfigListener);
return this; return this;
} }
public Builder<T> clearListeners() public Builder<TValue> clearListeners()
{ {
this.tmpIConfigListener.clear(); this.tmpIConfigListener.clear();
return this; return this;
@@ -403,9 +482,55 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
public ConfigEntry<T> build() public ConfigEntry<TValue> build()
{ {
return new ConfigEntry<>(this.tmpAppearance, this.tmpValue, this.tmpComment, this.tmpMin, this.tmpMax, this.tmpServersideShortName, this.tmpUseApiOverwrite, this.tmpPerformance, this.tmpIConfigListener); return new ConfigEntry<>(
this.tmpAppearance,
this.tmpValue, this.tmpComment, this.tmpMin, this.tmpMax,
this.tmpServersideShortName, this.tmpUseApiOverwrite,
this.tmpPerformance, this.tmpRelevantSide, this.tmpIConfigListener);
}
}
/**
* Wraps deferred accesses to a ConfigEntry. <br>
* It replaces the direct uses of ConfigEntry in places where Java runtime should not attempt
* to dereference path to said entry, until initialization is done.
*/
@FunctionalInterface
public interface DeferredAccessor<T>
{
ConfigEntry<T> getEntry();
default T get() { return this.getEntry().get(); }
default void set(T value) { this.getEntry().set(value); }
}
public static class PresetBuilder<TValue>
{
public final TValue customStateValue;
public final Map<DeferredAccessor<Object>, BiMap<TValue, Object>> presetValues = new HashMap<>();
public PresetBuilder(TValue customStateValue)
{
this.customStateValue = customStateValue;
}
@SuppressWarnings("unchecked")
public <TControlledEntryValue> PresetBuilder<TValue> addConfigEntry(
DeferredAccessor<TControlledEntryValue> controlledEntrySupplier,
Consumer<Map<TValue, TControlledEntryValue>> presetValueInitializer
)
{
presetValueInitializer.accept(
(Map<TValue, TControlledEntryValue>) this.presetValues.computeIfAbsent(
(DeferredAccessor<Object>) controlledEntrySupplier,
ignored -> HashBiMap.create()
)
);
return this;
} }
} }
@@ -1,63 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.config.types;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance;
/**
* Creates a UI element that copies everything from another element.
* This only effects the UI
*
* @author coolGi
*/
@Deprecated // FIXME doesn't work with localization
public class ConfigLinkedEntry extends AbstractConfigType<AbstractConfigType<?, ?>, ConfigLinkedEntry>
{
public ConfigLinkedEntry(AbstractConfigType<?, ?> value)
{
super(EConfigEntryAppearance.ONLY_IN_GUI, value);
}
/** Appearance shouldn't be changed */
@Override
public void setAppearance(EConfigEntryAppearance newAppearance) { }
/** Value shouldn't be changed after creation */
@Override
public void set(AbstractConfigType<?, ?> newValue) { }
public static class Builder extends AbstractConfigType.Builder<AbstractConfigType<?, ?>, Builder>
{
/** Appearance shouldn't be changed */
@Override
public Builder setAppearance(EConfigEntryAppearance newAppearance)
{
return this;
}
public ConfigLinkedEntry build()
{
return new ConfigLinkedEntry(this.tmpValue);
}
}
}
@@ -17,12 +17,20 @@
* 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.level; package com.seibel.distanthorizons.core.config.types.enums;
import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider; /**
* BOTH, <br>
public interface IDhWorldGenLevel extends IDhLevel, GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener * SERVER, <br>
* CLIENT, <br>
*
* Defines whether this config entry is for the client, server, or both.
* TODO this needs a better name.
*/
public enum EConfigEntryRelevantSide
{ {
void doWorldGen(); BOTH,
SERVER,
CLIENT,
} }
@@ -146,16 +146,16 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
this.columnWorldCompressionMode = columnWorldCompressionMode; this.columnWorldCompressionMode = columnWorldCompressionMode;
} }
public static FullDataSourceV2 createFromChunk(IChunkWrapper chunkWrapper) { return LodDataBuilder.createGeneratedDataSource(chunkWrapper); } public static FullDataSourceV2 createFromChunk(IChunkWrapper chunkWrapper) { return LodDataBuilder.createFromChunk(chunkWrapper); }
public static FullDataSourceV2 createFromLegacyDataSourceV1(FullDataSourceV1 legacyData) public static FullDataSourceV2 createFromLegacyDataSourceV1(FullDataSourceV1 legacyData)
{ {
if (FullDataSourceV1.WIDTH != WIDTH) if (FullDataSourceV1.WIDTH != WIDTH)
{ {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
"Unable to convert [" + FullDataSourceV1.class.getSimpleName() + "] into [" + FullDataSourceV2.class.getSimpleName() + "]. " + "Unable to convert ["+FullDataSourceV1.class.getSimpleName()+"] into ["+FullDataSourceV2.class.getSimpleName()+"]. " +
"Data sources have different data point widths and no converter is present. " + "Data sources have different data point widths and no converter is present. " +
"input width [" + FullDataSourceV1.WIDTH + "], recipient width [" + WIDTH + "]."); "input width ["+ FullDataSourceV1.WIDTH+"], recipient width ["+WIDTH+"].");
} }
@@ -258,7 +258,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
// and would lead to edge cases that don't necessarily need to be supported // and would lead to edge cases that don't necessarily need to be supported
// (IE what do you do when the input is smaller than a single datapoint in the receiving data source?) // (IE what do you do when the input is smaller than a single datapoint in the receiving data source?)
// instead it's better to just percolate the updates up // instead it's better to just percolate the updates up
throw new UnsupportedOperationException("Unsupported data source update. Expected input detail level of [" + thisDetailLevel + "] or [" + (thisDetailLevel + 1) + "], received detail level [" + inputDetailLevel + "]."); throw new UnsupportedOperationException("Unsupported data source update. Expected input detail level of ["+thisDetailLevel+"] or ["+(thisDetailLevel+1)+"], received detail level ["+inputDetailLevel+"].");
} }
// determine if this data source should be applied to its parent // determine if this data source should be applied to its parent
@@ -494,7 +494,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
// special numbers: // special numbers:
// -2 = the column's height hasn't been determined yet // -2 = the column's height hasn't been determined yet
// -1 = we've reached the end of the column // -1 = we've reached the end of the column
int[] currentDatapointIndex = new int[]{-2, -2, -2, -2}; int[] currentDatapointIndex = new int[] { -2, -2, -2, -2 };
int lastId = 0; int lastId = 0;
byte lastBlockLight = 0; byte lastBlockLight = 0;
@@ -791,7 +791,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
if (relX < 0 || relZ < 0 || if (relX < 0 || relZ < 0 ||
relX > WIDTH || relZ > WIDTH) relX > WIDTH || relZ > WIDTH)
{ {
throw new IndexOutOfBoundsException("Relative data source positions must be between [0] and [" + WIDTH + "] (inclusive) the relative pos: [" + relX + "," + relZ + "] is outside of those boundaries."); throw new IndexOutOfBoundsException("Relative data source positions must be between [0] and ["+WIDTH+"] (inclusive) the relative pos: ["+relX+","+relZ+"] is outside of those boundaries.");
} }
return (relX * WIDTH) + relZ; return (relX * WIDTH) + relZ;
@@ -836,7 +836,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
// reverse the array so index 0 is the highest, // reverse the array so index 0 is the highest,
// this is necessary for later logic // this is necessary for later logic
// source: https://stackoverflow.com/questions/2137755/how-do-i-reverse-an-int-array-in-java // source: https://stackoverflow.com/questions/2137755/how-do-i-reverse-an-int-array-in-java
for (int i = 0; i < dataColumn.size() / 2; i++) for(int i = 0; i < dataColumn.size() / 2; i++)
{ {
long temp = dataColumn.getLong(i); long temp = dataColumn.getLong(i);
dataColumn.set(i, dataColumn.getLong(dataColumn.size() - i - 1)); dataColumn.set(i, dataColumn.getLong(dataColumn.size() - i - 1));
@@ -67,9 +67,13 @@ public final class BufferQuad
EDhDirection direction) EDhDirection direction)
{ {
if (widthEastWest == 0 || widthNorthSouthOrUpDown == 0) if (widthEastWest == 0 || widthNorthSouthOrUpDown == 0)
{
throw new IllegalArgumentException("Size 0 quad!"); throw new IllegalArgumentException("Size 0 quad!");
}
if (widthEastWest < 0 || widthNorthSouthOrUpDown < 0) if (widthEastWest < 0 || widthNorthSouthOrUpDown < 0)
{
throw new IllegalArgumentException("Negative sized quad!"); throw new IllegalArgumentException("Negative sized quad!");
}
this.x = x; this.x = x;
this.y = y; this.y = y;
@@ -95,7 +99,9 @@ public final class BufferQuad
public int compare(BufferQuad quad, BufferMergeDirectionEnum compareDirection) public int compare(BufferQuad quad, BufferMergeDirectionEnum compareDirection)
{ {
if (this.direction != quad.direction) if (this.direction != quad.direction)
{
throw new IllegalArgumentException("The other quad is not in the same direction: " + quad.direction + " vs " + this.direction); throw new IllegalArgumentException("The other quad is not in the same direction: " + quad.direction + " vs " + this.direction);
}
if (compareDirection == BufferMergeDirectionEnum.EastWest) if (compareDirection == BufferMergeDirectionEnum.EastWest)
{ {
@@ -150,11 +156,15 @@ public final class BufferQuad
public boolean tryMerge(BufferQuad quad, BufferMergeDirectionEnum mergeDirection) public boolean tryMerge(BufferQuad quad, BufferMergeDirectionEnum mergeDirection)
{ {
if (quad.hasError || this.hasError) if (quad.hasError || this.hasError)
{
return false; return false;
}
// only merge quads that are in the same direction // only merge quads that are in the same direction
if (this.direction != quad.direction) if (this.direction != quad.direction)
{
return false; return false;
}
// make sure these quads share the same perpendicular axis // make sure these quads share the same perpendicular axis
if ((mergeDirection == BufferMergeDirectionEnum.EastWest && this.y != quad.y) || if ((mergeDirection == BufferMergeDirectionEnum.EastWest && this.y != quad.y) ||
@@ -111,7 +111,7 @@ public class ColumnRenderBufferBuilder
/** @link adjData should be null for adjacent sections that cross detail level boundaries */ /** @link adjData should be null for adjacent sections that cross detail level boundaries */
public static CompletableFuture<ColumnRenderBuffer> uploadBuffersAsync( public static CompletableFuture<ColumnRenderBuffer> uploadBuffersAsync(
IDhClientLevel clientLevel, IDhClientLevel clientLevel,
ColumnRenderSource renderSource, long pos,
LodQuadBuilder quadBuilder LodQuadBuilder quadBuilder
) )
{ {
@@ -132,7 +132,7 @@ public class ColumnRenderBufferBuilder
{ {
try try
{ {
ColumnRenderBuffer buffer = new ColumnRenderBuffer(new DhBlockPos(DhSectionPos.getMinCornerBlockX(renderSource.pos), clientLevel.getMinY(), DhSectionPos.getMinCornerBlockZ(renderSource.pos))); ColumnRenderBuffer buffer = new ColumnRenderBuffer(new DhBlockPos(DhSectionPos.getMinCornerBlockX(pos), clientLevel.getMinY(), DhSectionPos.getMinCornerBlockZ(pos)));
try try
{ {
buffer.uploadBuffer(quadBuilder, GLProxy.getInstance().getGpuUploadMethod()); buffer.uploadBuffer(quadBuilder, GLProxy.getInstance().getGpuUploadMethod());
@@ -158,7 +158,7 @@ public class ColumnRenderBufferBuilder
} }
catch (Throwable e3) catch (Throwable e3)
{ {
LOGGER.error("LodNodeBufferBuilder was unable to upload buffer for pos ["+DhSectionPos.toString(renderSource.pos)+"], error: [" + e3.getMessage() + "].", e3); LOGGER.error("LodNodeBufferBuilder was unable to upload buffer for pos ["+DhSectionPos.toString(pos)+"], error: [" + e3.getMessage() + "].", e3);
throw e3; throw e3;
} }
}, bufferUploaderExecutor); }, bufferUploaderExecutor);
@@ -191,14 +191,14 @@ public class LodQuadBuilder
public void addQuadDown(short x, short y, short z, short width, short wz, int color, byte irisBlockMaterialId, byte skylight, byte blocklight) public void addQuadDown(short x, short y, short z, short width, short wz, int color, byte irisBlockMaterialId, byte skylight, byte blocklight)
{ {
BufferQuad quad = new BufferQuad(x, y, z, width, wz, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.DOWN); BufferQuad quad = new BufferQuad(x, y, z, width, wz, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.DOWN);
ArrayList<BufferQuad> qs = (doTransparency && ColorUtil.getAlpha(color) < 255) ArrayList<BufferQuad> qs = (this.doTransparency && ColorUtil.getAlpha(color) < 255)
? transparentQuads[EDhDirection.DOWN.ordinal()] : opaqueQuads[EDhDirection.DOWN.ordinal()]; ? this.transparentQuads[EDhDirection.DOWN.ordinal()] : this.opaqueQuads[EDhDirection.DOWN.ordinal()];
if (!qs.isEmpty() if (!qs.isEmpty()
&& (qs.get(qs.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.EastWest) && (qs.get(qs.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.EastWest)
|| qs.get(qs.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.NorthSouthOrUpDown)) || qs.get(qs.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.NorthSouthOrUpDown))
) )
{ {
premergeCount++; this.premergeCount++;
return; return;
} }
qs.add(quad); qs.add(quad);
@@ -251,7 +251,9 @@ public class LodQuadBuilder
private static long mergeQuadsInternal(ArrayList<BufferQuad>[] list, int directionIndex, BufferMergeDirectionEnum mergeDirection) private static long mergeQuadsInternal(ArrayList<BufferQuad>[] list, int directionIndex, BufferMergeDirectionEnum mergeDirection)
{ {
if (list[directionIndex].size() <= 1) if (list[directionIndex].size() <= 1)
{
return 0; return 0;
}
list[directionIndex].sort((objOne, objTwo) -> objOne.compare(objTwo, mergeDirection)); list[directionIndex].sort((objOne, objTwo) -> objOne.compare(objTwo, mergeDirection));
@@ -423,9 +425,18 @@ public class LodQuadBuilder
// 0b01 = positive offset // 0b01 = positive offset
// 0b11 = negative offset // 0b11 = negative offset
// format is: 0b00zzyyxx // format is: 0b00zzyyxx
if (mx != 0) mirco |= mx > 0 ? 0b01 : 0b11; if (mx != 0)
if (my != 0) mirco |= my > 0 ? 0b0100 : 0b1100; {
if (mz != 0) mirco |= mz > 0 ? 0b010000 : 0b110000; mirco |= mx > 0 ? 0b01 : 0b11;
}
if (my != 0)
{
mirco |= my > 0 ? 0b0100 : 0b1100;
}
if (mz != 0)
{
mirco |= mz > 0 ? 0b010000 : 0b110000;
}
meta |= mirco << 8; meta |= mirco << 8;
bb.putShort(meta); bb.putShort(meta);
@@ -40,6 +40,7 @@ import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IMutableBlockPosWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
@@ -58,12 +59,10 @@ public class LodDataBuilder
// converters // // converters //
//============// //============//
public static FullDataSourceV2 createGeneratedDataSource(IChunkWrapper chunkWrapper) public static FullDataSourceV2 createFromChunk(IChunkWrapper chunkWrapper)
{ {
if (!canGenerateLodFromChunk(chunkWrapper)) // only block lighting is needed here, sky lighting is populated at the data source stage
{ LodUtil.assertTrue(chunkWrapper.isDhBlockLightingCorrect());
return null;
}
@@ -132,6 +131,9 @@ public class LodDataBuilder
try try
{ {
IMutableBlockPosWrapper mcBlockPos = chunkWrapper.getMutableBlockPosWrapper();
IBlockStateWrapper previousBlockState = null;
int minBuildHeight = chunkWrapper.getMinNonEmptyHeight(); int minBuildHeight = chunkWrapper.getMinNonEmptyHeight();
for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++) for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++)
{ {
@@ -149,8 +151,8 @@ public class LodDataBuilder
if (lastY < chunkWrapper.getMaxBuildHeight()) if (lastY < chunkWrapper.getMaxBuildHeight())
{ {
// FIXME: The lastY +1 offset is to reproduce the old behavior. Remove this when we get per-face lighting // FIXME: The lastY +1 offset is to reproduce the old behavior. Remove this when we get per-face lighting
blockLight = (byte) chunkWrapper.getBlockLight(relBlockX, lastY + 1, relBlockZ); blockLight = (byte) chunkWrapper.getDhBlockLight(relBlockX, lastY + 1, relBlockZ);
skyLight = (byte) chunkWrapper.getSkyLight(relBlockX, lastY + 1, relBlockZ); skyLight = (byte) chunkWrapper.getDhSkyLight(relBlockX, lastY + 1, relBlockZ);
} }
else else
{ {
@@ -163,7 +165,7 @@ public class LodDataBuilder
// determine the starting Y Pos // determine the starting Y Pos
int y = chunkWrapper.getLightBlockingHeightMapValue(relBlockX, relBlockZ); int y = chunkWrapper.getLightBlockingHeightMapValue(relBlockX, relBlockZ);
// go up until we reach open air or the world limit // go up until we reach open air or the world limit
IBlockStateWrapper topBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ); IBlockStateWrapper topBlockState = previousBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ, mcBlockPos, previousBlockState);
while (!topBlockState.isAir() && y < chunkWrapper.getMaxBuildHeight()) while (!topBlockState.isAir() && y < chunkWrapper.getMaxBuildHeight())
{ {
try try
@@ -171,7 +173,7 @@ public class LodDataBuilder
// This is necessary in some edge cases with snow layers and some other blocks that may not appear in the height map but do block light. // This is necessary in some edge cases with snow layers and some other blocks that may not appear in the height map but do block light.
// Interestingly this doesn't appear to be the case in the DhLightingEngine, if this same logic is added there the lighting breaks for the affected blocks. // Interestingly this doesn't appear to be the case in the DhLightingEngine, if this same logic is added there the lighting breaks for the affected blocks.
y++; y++;
topBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ); topBlockState = previousBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ, mcBlockPos, previousBlockState);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -190,9 +192,9 @@ public class LodDataBuilder
for (; y >= minBuildHeight; y--) for (; y >= minBuildHeight; y--)
{ {
IBiomeWrapper newBiome = chunkWrapper.getBiome(relBlockX, y, relBlockZ); IBiomeWrapper newBiome = chunkWrapper.getBiome(relBlockX, y, relBlockZ);
IBlockStateWrapper newBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ); IBlockStateWrapper newBlockState = previousBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ, mcBlockPos, previousBlockState);
byte newBlockLight = (byte) chunkWrapper.getBlockLight(relBlockX, y + 1, relBlockZ); byte newBlockLight = (byte) chunkWrapper.getDhBlockLight(relBlockX, y + 1, relBlockZ);
byte newSkyLight = (byte) chunkWrapper.getSkyLight(relBlockX, y + 1, relBlockZ); byte newSkyLight = (byte) chunkWrapper.getDhSkyLight(relBlockX, y + 1, relBlockZ);
// save the biome/block change // save the biome/block change
if (!newBiome.equals(biome) || !newBlockState.equals(blockState)) if (!newBiome.equals(biome) || !newBlockState.equals(blockState))
@@ -435,8 +437,6 @@ public class LodDataBuilder
// helper methods // // helper methods //
//================// //================//
public static boolean canGenerateLodFromChunk(IChunkWrapper chunk) { return chunk != null && chunk.isLightCorrect(); }
public static int getXOrZSectionPosFromChunkPos(int chunkXOrZPos) public static int getXOrZSectionPosFromChunkPos(int chunkXOrZPos)
{ {
// get the section position // get the section position
@@ -205,7 +205,7 @@ public abstract class AbstractDataSourceHandler
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.error("Unexpected error in async data source update, error: " + e.getMessage(), e); LOGGER.error("Unexpected error in async data source update, error: "+e.getMessage(), e);
} }
finally finally
{ {
@@ -269,7 +269,7 @@ public abstract class AbstractDataSourceHandler
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.error("Error updating pos [" + updatePos + "], error: "+e.getMessage(), e); LOGGER.error("Error updating pos ["+updatePos+"], error: "+e.getMessage(), e);
} }
finally finally
{ {
@@ -107,8 +107,12 @@ public class FullDataSourceProviderV2
// TODO only run thread if modifications happened recently // TODO only run thread if modifications happened recently
/** /**
* This isn't in {@link AbstractDataSourceHandler} since we don't need parent updating logic * This isn't in {@link AbstractDataSourceHandler} since we only want to update
* for render data, only full data. * the newest version of the full data, so if we have providers for either
* render data or old full data, we don't want to update them. <br><br>
*
* Will be null on the dedicated server since updates don't need to be propagated,
* only the highest detail level is needed.
*/ */
@Nullable @Nullable
private final ThreadPoolExecutor updateQueueProcessor; private final ThreadPoolExecutor updateQueueProcessor;
@@ -130,12 +134,13 @@ public class FullDataSourceProviderV2
String dimensionName = level.getLevelWrapper().getDimensionName(); String dimensionName = level.getLevelWrapper().getDimensionName();
// start migrating any legacy data sources present in the background // start migrating any legacy data sources present in the background
this.migrationThreadPool = ThreadUtil.makeRateLimitedThreadPool(1, MIGRATION_THREAD_NAME_PREFIX + "[" + dimensionName + "]", Config.Client.Advanced.MultiThreading.runTimeRatioForUpdatePropagatorThreads.get(), Thread.MIN_PRIORITY, (Semaphore) null); this.migrationThreadPool = ThreadUtil.makeRateLimitedThreadPool(1, MIGRATION_THREAD_NAME_PREFIX + "["+dimensionName+"]", Config.Client.Advanced.MultiThreading.runTimeRatioForUpdatePropagatorThreads.get(), Thread.MIN_PRIORITY, (Semaphore) null);
this.migrationThreadPool.execute(this::convertLegacyDataSources); this.migrationThreadPool.execute(this::convertLegacyDataSources);
// update propagation doesn't need to be run on the server since only the highest detail level is needed
if (SharedApi.getEnvironment() != EWorldEnvironment.Server_Only) if (SharedApi.getEnvironment() != EWorldEnvironment.Server_Only)
{ {
this.updateQueueProcessor = ThreadUtil.makeSingleThreadPool("Parent Update Queue [" + dimensionName + "]"); this.updateQueueProcessor = ThreadUtil.makeSingleThreadPool("Parent Update Queue ["+dimensionName+"]");
this.updateQueueProcessor.execute(this::runUpdateQueue); this.updateQueueProcessor.execute(this::runUpdateQueue);
} }
else else
@@ -176,7 +181,7 @@ public class FullDataSourceProviderV2
} }
catch (IOException e) catch (IOException e)
{ {
LOGGER.warn("Unable to create DTO, error: " + e.getMessage(), e); LOGGER.warn("Unable to create DTO, error: "+e.getMessage(), e);
return null; return null;
} }
} }
@@ -188,58 +193,6 @@ public class FullDataSourceProviderV2
@Override @Override
protected FullDataSourceV2 makeEmptyDataSource(long pos) { return FullDataSourceV2.DATA_SOURCE_POOL.getPooledSource(pos, true); } protected FullDataSourceV2 makeEmptyDataSource(long pos) { return FullDataSourceV2.DATA_SOURCE_POOL.getPooledSource(pos, true); }
@Nullable
public Long getTimestampForPos(long pos)
{
try
{
PreparedStatement preparedStatement = this.repo.createPreparedStatement(
"SELECT LastModifiedUnixDateTime " +
"FROM " + this.repo.getTableName() + " " +
"WHERE DetailLevel = ? " +
"AND PosX = ? " +
"AND PosZ = ?;"
);
preparedStatement.setInt(1, DhSectionPos.getDetailLevel(pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
preparedStatement.setInt(2, DhSectionPos.getX(pos));
preparedStatement.setInt(3, DhSectionPos.getZ(pos));
List<Map<String, Object>> row = this.repo.query(preparedStatement);
return !row.isEmpty() ? (Long) row.get(0).get("LastModifiedUnixDateTime") : null;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
public Map<Long, Long> getTimestampsForRange(byte detailLevel, int startPosX, int startPosZ, int endPosX, int endPosZ)
{
try
{
PreparedStatement preparedStatement = this.repo.createPreparedStatement(
"SELECT PosX, PosZ, LastModifiedUnixDateTime " +
"FROM " + this.repo.getTableName() + " " +
"WHERE DetailLevel = ? " +
"AND PosX BETWEEN ? AND ? " +
"AND PosZ BETWEEN ? AND ?;"
);
preparedStatement.setInt(1, detailLevel - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
preparedStatement.setInt(2, startPosX);
preparedStatement.setInt(3, endPosX);
preparedStatement.setInt(4, startPosZ);
preparedStatement.setInt(5, endPosZ);
return this.repo.query(preparedStatement).stream().collect(Collectors.toMap(
row -> DhSectionPos.encode(detailLevel, (int) row.get("PosX"), (int) row.get("PosZ")),
row -> (long) row.get("LastModifiedUnixDateTime"))
);
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
//================// //================//
@@ -375,7 +328,7 @@ public class FullDataSourceProviderV2
} }
} }
LOGGER.info("Update thread [" + Thread.currentThread().getName() + "] terminated."); LOGGER.info("Update thread ["+Thread.currentThread().getName()+"] terminated.");
} }
@@ -387,7 +340,7 @@ public class FullDataSourceProviderV2
private void convertLegacyDataSources() private void convertLegacyDataSources()
{ {
String dimensionName = this.level.getLevelWrapper().getDimensionName(); String dimensionName = this.level.getLevelWrapper().getDimensionName();
LOGGER.info("Attempting to migrate data sources for: [" + dimensionName + "]-[" + this.saveDir + "]..."); LOGGER.info("Attempting to migrate data sources for: ["+dimensionName+"]-["+this.saveDir+"]...");
@@ -406,7 +359,7 @@ public class FullDataSourceProviderV2
this.showMigrationStartMessage(); this.showMigrationStartMessage();
LOGGER.info("deleting [" + dimensionName + "] - [" + totalDeleteCount + "] unused data sources..."); LOGGER.info("deleting [" + dimensionName + "] - ["+totalDeleteCount+"] unused data sources...");
this.legacyDeletionCount = totalDeleteCount; this.legacyDeletionCount = totalDeleteCount;
ArrayList<String> unusedDataPosList = this.legacyFileHandler.repo.getUnusedDataSourcePositionStringList(50); ArrayList<String> unusedDataPosList = this.legacyFileHandler.repo.getUnusedDataSourcePositionStringList(50);
@@ -424,7 +377,7 @@ public class FullDataSourceProviderV2
long endStart = System.currentTimeMillis(); long endStart = System.currentTimeMillis();
long deleteTime = endStart - startTime; long deleteTime = endStart - startTime;
LOGGER.info("Deleting [" + dimensionName + "] - [" + unusedCount + "/" + totalDeleteCount + "] in [" + deleteTime + "]ms ..."); LOGGER.info("Deleting [" + dimensionName + "] - [" + unusedCount + "/" + totalDeleteCount + "] in ["+deleteTime+"]ms ...");
// a slight delay is added to prevent accidentally locking the database when deleting a lot of rows // a slight delay is added to prevent accidentally locking the database when deleting a lot of rows
@@ -435,12 +388,10 @@ public class FullDataSourceProviderV2
// and weak computers wait no time at all // and weak computers wait no time at all
Thread.sleep(deleteTime / 2); Thread.sleep(deleteTime / 2);
} }
catch (InterruptedException ignore) catch (InterruptedException ignore){}
{
}
} }
LOGGER.info("Done deleting [" + dimensionName + "] - ["+totalDeleteCount+"] unused data sources.");
LOGGER.info("Done deleting [" + dimensionName + "] - [" + totalDeleteCount + "] unused data sources.");
} }
@@ -451,7 +402,7 @@ public class FullDataSourceProviderV2
long totalMigrationCount = this.legacyFileHandler.getDataSourceMigrationCount(); long totalMigrationCount = this.legacyFileHandler.getDataSourceMigrationCount();
this.migrationCount = totalMigrationCount; this.migrationCount = totalMigrationCount;
LOGGER.info("Found [" + totalMigrationCount + "] data sources that need migration."); LOGGER.info("Found ["+totalMigrationCount+"] data sources that need migration.");
ArrayList<FullDataSourceV1> legacyDataSourceList = this.legacyFileHandler.getDataSourcesToMigrate(MIGRATION_BATCH_COUNT); ArrayList<FullDataSourceV1> legacyDataSourceList = this.legacyFileHandler.getDataSourcesToMigrate(MIGRATION_BATCH_COUNT);
if (!legacyDataSourceList.isEmpty()) if (!legacyDataSourceList.isEmpty())
@@ -491,9 +442,7 @@ public class FullDataSourceProviderV2
{ {
newDataSource.close(); newDataSource.close();
} }
catch (Exception ignore) catch (Exception ignore) { }
{
}
}); });
} }
catch (Exception e) catch (Exception e)
@@ -536,13 +485,13 @@ public class FullDataSourceProviderV2
{ {
if (this.migrationThreadRunning.get()) if (this.migrationThreadRunning.get())
{ {
LOGGER.info("migration complete for: [" + dimensionName + "]-[" + this.saveDir + "]."); LOGGER.info("migration complete for: ["+dimensionName+"]-["+this.saveDir+"].");
this.showMigrationEndMessage(true); this.showMigrationEndMessage(true);
this.migrationCount = 0; this.migrationCount = 0;
} }
else else
{ {
LOGGER.info("migration stopped for: [" + dimensionName + "]-[" + this.saveDir + "]."); LOGGER.info("migration stopped for: ["+dimensionName+"]-["+this.saveDir+"].");
this.showMigrationEndMessage(false); this.showMigrationEndMessage(false);
this.migrationStoppedWithError = true; this.migrationStoppedWithError = true;
} }
@@ -668,6 +617,19 @@ public class FullDataSourceProviderV2
public boolean fileExists(long pos) { return this.repo.getDataSizeInBytes(pos) > 0; } public boolean fileExists(long pos) { return this.repo.getDataSizeInBytes(pos) > 0; }
//========================//
// multiplayer networking //
//========================//
@Nullable
public Long getTimestampForPos(long pos)
{ return this.repo.getTimestampForPos(pos); }
public Map<Long, Long> getTimestampsForRange(byte detailLevel, int startPosX, int startPosZ, int endPosX, int endPosZ)
{ return this.repo.getTimestampsForRange(detailLevel, startPosX, startPosZ, endPosX, endPosZ); }
//===========// //===========//
// overrides // // overrides //
//===========// //===========//
@@ -23,6 +23,7 @@ import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGeneratio
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.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure;
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.IWorldGenTaskTracker;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult;
@@ -97,7 +98,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// don't log shutdown exceptions // don't log shutdown exceptions
if (!(exception instanceof CancellationException || exception.getCause() instanceof CancellationException)) if (!(exception instanceof CancellationException || exception.getCause() instanceof CancellationException))
{ {
LOGGER.error("Uncaught Gen Task Exception at [" + genTaskResult.pos + "], error: [" + exception.getMessage() + "].", exception); LOGGER.error("Uncaught Gen Task Exception at ["+genTaskResult.pos+"], error: ["+exception.getMessage()+"].", exception);
} }
} }
else if (genTaskResult.success) else if (genTaskResult.success)
@@ -218,7 +219,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
} }
GenTask genTask = new GenTask(genPos); GenTask genTask = new GenTask(genPos);
CompletableFuture<WorldGenResult> worldGenFuture = worldGenQueue.submitGenTask(genPos, (byte) (DhSectionPos.getDetailLevel(genPos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL), genTask); CompletableFuture<WorldGenResult> worldGenFuture = worldGenQueue.submitRetrievalTask(genPos, (byte) (DhSectionPos.getDetailLevel(genPos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL), genTask);
worldGenFuture.whenComplete((genTaskResult, ex) -> this.onWorldGenTaskComplete(genTaskResult, ex)); worldGenFuture.whenComplete((genTaskResult, ex) -> this.onWorldGenTaskComplete(genTaskResult, ex));
return true; return true;
@@ -360,22 +361,6 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
return BitShiftUtil.powerOfTwo(detailLevelDiff); return BitShiftUtil.powerOfTwo(detailLevelDiff);
} }
public Map<Long, Integer> getLoadStates(Iterable<Long> posList)
{
HashMap<Long, Integer> map = new HashMap<>();
for (long pos : posList)
{
map.put(pos,
// Loaded
this.delayedFullDataSourceSaveCache.dataSourceByPosition.containsKey(pos) ? 3
// Unloaded, but exists
: this.fileExists(pos) ? 2
// Not generated
: 1);
}
return map;
}
//=======// //=======//
@@ -411,7 +396,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
public boolean isMemoryAddressValid() { return true; } public boolean isMemoryAddressValid() { return true; }
@Override @Override
public Consumer<FullDataSourceV2> getChunkDataConsumer() public Consumer<FullDataSourceV2> getDataSourceConsumer()
{ {
return (chunkSizedFullDataSource) -> return (chunkSizedFullDataSource) ->
{ {
@@ -421,7 +406,14 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
} }
private void onDataSourceSave(FullDataSourceV2 fullDataSource) private void onDataSourceSave(FullDataSourceV2 fullDataSource)
{ GeneratedFullDataSourceProvider.this.updateDataSourceAsync(fullDataSource); } {
// 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
// allows us to reduce cross-chunk lighting issues by lighting the whole 4x4 LOD at once
DhLightingEngine.INSTANCE.bakeDataSourceSkyLight(fullDataSource, LodUtil.MAX_MC_LIGHT);
GeneratedFullDataSourceProvider.this.updateDataSourceAsync(fullDataSource);
}
@@ -21,7 +21,9 @@ package com.seibel.distanthorizons.core.file.fullDatafile;
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.AbstractSaveStructure; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure;
import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.level.WorldGenModule;
import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoginRequestQueue; import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoginRequestQueue;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -31,28 +33,67 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/**
* Only handles {@link SyncOnLoginRequestQueue} requests (IE updating existing LODs based on a timestamp).
* Missing data is handled by {@link WorldGenModule} and {@link RemoteWorldRetrievalQueue}.
*/
public class RemoteFullDataSourceProvider extends GeneratedFullDataSourceProvider public class RemoteFullDataSourceProvider extends GeneratedFullDataSourceProvider
{ {
@Nullable @Nullable
private final SyncOnLoginRequestQueue syncOnLoginRequestQueue; private final SyncOnLoginRequestQueue syncOnLoginRequestQueue;
private final Set<Long> finishedTaskPositions = ConcurrentHashMap.newKeySet(); private final Set<Long> finishedTaskPositions = ConcurrentHashMap.newKeySet();
public RemoteFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride, @Nullable SyncOnLoginRequestQueue syncOnLoginRequestQueue)
//=============//
// constructor //
//=============//
public RemoteFullDataSourceProvider(
IDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride,
@Nullable SyncOnLoginRequestQueue syncOnLoginRequestQueue)
{ {
super(level, saveStructure, saveDirOverride); super(level, saveStructure, saveDirOverride);
this.syncOnLoginRequestQueue = syncOnLoginRequestQueue; this.syncOnLoginRequestQueue = syncOnLoginRequestQueue;
} }
//==================//
// override methods //
//==================//
@Override @Override
@Nullable @Nullable
public FullDataSourceV2 get(long pos) public FullDataSourceV2 get(long pos)
{ {
//=======================//
// get local data source //
//=======================//
FullDataSourceV2 fullDataSource = super.get(pos); FullDataSourceV2 fullDataSource = super.get(pos);
if (fullDataSource == null || this.syncOnLoginRequestQueue == null) if (fullDataSource == null)
{ {
// we don't have any local data for this position,
// we can't queue updates based on a timestamp
return null;
}
if (this.syncOnLoginRequestQueue == null)
{
// we have local data, but aren't allowed to
// request timestamp updates from the server.
return fullDataSource; return fullDataSource;
} }
//===========================//
// request timestamp updates //
// from server //
//===========================//
// get the timestamp for every maximum detail position in this section
int posToMinimumDetailScale = (DhSectionPos.getDetailLevel(pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL + 1); int posToMinimumDetailScale = (DhSectionPos.getDetailLevel(pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL + 1);
Map<Long, Long> timestamps = this.getTimestampsForRange( Map<Long, Long> timestamps = this.getTimestampsForRange(
DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL, DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL,
@@ -61,17 +102,28 @@ public class RemoteFullDataSourceProvider extends GeneratedFullDataSourceProvide
(DhSectionPos.getX(pos) + 1) * posToMinimumDetailScale - 1, (DhSectionPos.getX(pos) + 1) * posToMinimumDetailScale - 1,
(DhSectionPos.getZ(pos) + 1) * posToMinimumDetailScale - 1 (DhSectionPos.getZ(pos) + 1) * posToMinimumDetailScale - 1
); );
for (Map.Entry<Long, Long> entry : timestamps.entrySet())
// check if the server has newer versions of these LODs
for (Map.Entry<Long, Long> timestampBySectionPos : timestamps.entrySet())
{ {
if (this.finishedTaskPositions.add(entry.getKey())) Long subPos = timestampBySectionPos.getKey();
Long subTimestamp = timestampBySectionPos.getValue();
if (this.finishedTaskPositions.add(subPos))
{ {
this.syncOnLoginRequestQueue.submitRequest(entry.getKey(), entry.getValue(), this.delayedFullDataSourceSaveCache::queueDataSourceForUpdateAndSave); this.syncOnLoginRequestQueue.submitRequest(subPos, subTimestamp, this.delayedFullDataSourceSaveCache::queueDataSourceForUpdateAndSave);
} }
} }
return fullDataSource; return fullDataSource;
} }
//==========//
// shutdown //
//==========//
@Override @Override
public void close() public void close()
{ {
@@ -29,7 +29,6 @@ import com.seibel.distanthorizons.core.util.objects.ParsedIp;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IDimensionTypeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.coreapi.util.StringUtil; import com.seibel.distanthorizons.coreapi.util.StringUtil;
@@ -104,7 +103,7 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
{ {
LOGGER.info("Loading level " + newClientLevelWrapper.getDimensionName()); LOGGER.info("Loading level " + newClientLevelWrapper.getDimensionName());
List<File> levelFolders = this.getDhDataFoldersForDimension(newClientLevelWrapper); List<File> levelFolders = this.getDhDataFoldersForLevel(newClientLevelWrapper);
this.subDimMatcher = new SubDimensionLevelMatcher(newClientLevelWrapper, this.folder, levelFolders); this.subDimMatcher = new SubDimensionLevelMatcher(newClientLevelWrapper, this.folder, levelFolders);
} }
@@ -133,7 +132,7 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
private File getLevelFolderWithoutSimilarityMatching(ILevelWrapper level) private File getLevelFolderWithoutSimilarityMatching(ILevelWrapper level)
{ {
List<File> folders = this.getDhDataFoldersForDimension(level); List<File> folders = this.getDhDataFoldersForLevel(level);
if (!folders.isEmpty() && folders.get(0) != null) if (!folders.isEmpty() && folders.get(0) != null)
{ {
// use the first existing sub-dimension // use the first existing sub-dimension
@@ -149,7 +148,7 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
} }
} }
public List<File> getDhDataFoldersForDimension(ILevelWrapper level) public List<File> getDhDataFoldersForLevel(ILevelWrapper level)
{ {
File[] folders = this.folder.listFiles(); File[] folders = this.folder.listFiles();
if (folders == null) if (folders == null)
@@ -60,6 +60,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
* @author James Seibel * @author James Seibel
* @version 12-17-2022 * @version 12-17-2022
*/ */
@Deprecated
public class SubDimensionLevelMatcher implements AutoCloseable public class SubDimensionLevelMatcher implements AutoCloseable
{ {
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
@@ -184,11 +185,6 @@ public class SubDimensionLevelMatcher implements AutoCloseable
DhLightingEngine.INSTANCE.lightChunk(newlyLoadedChunk, new ArrayList<>(), MC_CLIENT.getWrappedClientLevel().hasSkyLight() ? 15 : 0); DhLightingEngine.INSTANCE.lightChunk(newlyLoadedChunk, new ArrayList<>(), MC_CLIENT.getWrappedClientLevel().hasSkyLight() ? 15 : 0);
// build the chunk LOD // build the chunk LOD
if (!LodDataBuilder.canGenerateLodFromChunk(newlyLoadedChunk))
{
LOGGER.warn("unable to build lod for chunk:"+newlyLoadedChunk.getChunkPos());
return null;
}
FullDataSourceV2 newChunkSizedFullDataView = FullDataSourceV2.createFromChunk(newlyLoadedChunk); FullDataSourceV2 newChunkSizedFullDataView = FullDataSourceV2.createFromChunk(newlyLoadedChunk);
// convert to a data source for easier comparing // convert to a data source for easier comparing
FullDataSourceV2 newDataSource = FullDataSourceV2.createEmpty(DhSectionPos.encodeContaining(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, this.playerData.playerBlockPos)); FullDataSourceV2 newDataSource = FullDataSourceV2.createEmpty(DhSectionPos.encodeContaining(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, this.playerData.playerBlockPos));
@@ -19,6 +19,7 @@
package com.seibel.distanthorizons.core.generation; package com.seibel.distanthorizons.core.generation;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
@@ -26,9 +27,12 @@ import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPosMutable;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.util.FullDataPointUtil;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IMutableBlockPosWrapper;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import java.awt.*; import java.awt.*;
@@ -56,11 +60,23 @@ public class DhLightingEngine
private static final ThreadLocal<DhBlockPosMutable> PRIMARY_BLOCK_POS_REF = ThreadLocal.withInitial(() -> new DhBlockPosMutable()); private static final ThreadLocal<DhBlockPosMutable> PRIMARY_BLOCK_POS_REF = ThreadLocal.withInitial(() -> new DhBlockPosMutable());
private static final ThreadLocal<DhBlockPosMutable> SECONDARY_BLOCK_POS_REF = ThreadLocal.withInitial(() -> new DhBlockPosMutable()); private static final ThreadLocal<DhBlockPosMutable> SECONDARY_BLOCK_POS_REF = ThreadLocal.withInitial(() -> new DhBlockPosMutable());
/** if enabled will render each block light value when the lighting engine is run */ /** if enabled will render each block light value when the chunk lighting engine is run */
private static final boolean RENDER_BLOCK_LIGHT_WIREFRAME = false; private static final boolean RENDER_BLOCK_LIGHT_WIREFRAME = false;
/** if enabled will render each sky light value when the lighting engine is run */ /** if enabled will render each sky light value when the chunk lighting engine is run */
private static final boolean RENDER_SKY_LIGHT_WIREFRAME = false; private static final boolean RENDER_SKY_LIGHT_WIREFRAME = false;
/**
* Used for dataSource lighting. <br>
* Packed as alternating x and z offsets.
*/
private static final byte[] ADJACENT_DIRECTION_OFFSETS = new byte[]
{
-1, 0,
+1, 0,
0, -1,
0, +1
};
//=============// //=============//
@@ -71,9 +87,28 @@ public class DhLightingEngine
//=========// //================//
// methods // // chunk lighting //
//=========// //================//
/**
* Populates both block and sky lighting.
* @see DhLightingEngine#lightChunk(IChunkWrapper, ArrayList, int, boolean, boolean)
*/
public void lightChunk(
@NotNull IChunkWrapper centerChunk, @NotNull ArrayList<IChunkWrapper> nearbyChunkList,
int maxSkyLight)
{ this.lightChunk(centerChunk, nearbyChunkList, maxSkyLight, true, true); }
/**
* Only populates block lights.
* @see DhLightingEngine#lightChunk(IChunkWrapper, ArrayList, int, boolean, boolean)
*/
public void bakeChunkBlockLighting(
@NotNull IChunkWrapper centerChunk, @NotNull ArrayList<IChunkWrapper> nearbyChunkList,
int maxSkyLight)
{ this.lightChunk(centerChunk, nearbyChunkList, maxSkyLight, true, false); }
/** /**
* Note: depending on the implementation of {@link IChunkWrapper#setDhBlockLight(int, int, int, int)} and {@link IChunkWrapper#setDhSkyLight(int, int, int, int)} * Note: depending on the implementation of {@link IChunkWrapper#setDhBlockLight(int, int, int, int)} and {@link IChunkWrapper#setDhSkyLight(int, int, int, int)}
@@ -84,13 +119,13 @@ public class DhLightingEngine
* @param nearbyChunkList should also contain centerChunk * @param nearbyChunkList should also contain centerChunk
* @param maxSkyLight should be a value between 0 and 15 * @param maxSkyLight should be a value between 0 and 15
*/ */
public void lightChunk(@NotNull IChunkWrapper centerChunk, @NotNull ArrayList<IChunkWrapper> nearbyChunkList, int maxSkyLight) private void lightChunk(
@NotNull IChunkWrapper centerChunk, @NotNull ArrayList<IChunkWrapper> nearbyChunkList,
int maxSkyLight, boolean updateBlockLight, boolean updateSkyLight)
{ {
DhChunkPos centerChunkPos = centerChunk.getChunkPos(); DhChunkPos centerChunkPos = centerChunk.getChunkPos();
AdjacentChunkHolder adjacentChunkHolder = new AdjacentChunkHolder(centerChunk); AdjacentChunkHolder adjacentChunkHolder = new AdjacentChunkHolder(centerChunk);
long startTimeNs = System.nanoTime();
// try-finally to handle the stableArray resources // try-finally to handle the stableArray resources
StableLightPosStack blockLightWorldPosQueue = null; StableLightPosStack blockLightWorldPosQueue = null;
@@ -117,7 +152,6 @@ public class DhLightingEngine
// find all adjacent chunks // find all adjacent chunks
// and get any necessary info from them // and get any necessary info from them
boolean warningLogged = false;
for (int chunkIndex = 0; chunkIndex < nearbyChunkList.size(); chunkIndex++) // using iterators in high traffic areas can cause GC issues due to allocating a bunch of iterators, use an indexed for-loop instead for (int chunkIndex = 0; chunkIndex < nearbyChunkList.size(); chunkIndex++) // using iterators in high traffic areas can cause GC issues due to allocating a bunch of iterators, use an indexed for-loop instead
{ {
IChunkWrapper chunk = nearbyChunkList.get(chunkIndex); IChunkWrapper chunk = nearbyChunkList.get(chunkIndex);
@@ -129,15 +163,17 @@ public class DhLightingEngine
// add the adjacent chunk // add the adjacent chunk
adjacentChunkHolder.add(chunk); adjacentChunkHolder.add(chunk);
// get and set the adjacent chunk's initial block lights
final DhBlockPosMutable relLightBlockPos = PRIMARY_BLOCK_POS_REF.get();
//==================// //==================//
// set block lights // // set block lights //
//==================// //==================//
// get and set the adjacent chunk's initial block lights if (updateBlockLight)
final DhBlockPosMutable relLightBlockPos = PRIMARY_BLOCK_POS_REF.get(); {
ArrayList<DhBlockPos> blockLightPosList = chunk.getWorldBlockLightPosList(); ArrayList<DhBlockPos> blockLightPosList = chunk.getWorldBlockLightPosList();
for (int blockLightIndex = 0; blockLightIndex < blockLightPosList.size(); blockLightIndex++) // using iterators in high traffic areas can cause GC issues due to allocating a bunch of iterators, use an indexed for-loop instead for (int blockLightIndex = 0; blockLightIndex < blockLightPosList.size(); blockLightIndex++) // using iterators in high traffic areas can cause GC issues due to allocating a bunch of iterators, use an indexed for-loop instead
{ {
@@ -152,6 +188,7 @@ public class DhLightingEngine
// set the light // set the light
chunk.setDhBlockLight(relLightBlockPos.getX(), relLightBlockPos.getY(), relLightBlockPos.getZ(), lightValue); chunk.setDhBlockLight(relLightBlockPos.getX(), relLightBlockPos.getY(), relLightBlockPos.getZ(), lightValue);
} }
}
@@ -161,8 +198,11 @@ public class DhLightingEngine
// get and set the adjacent chunk's initial skylights, // get and set the adjacent chunk's initial skylights,
// if the dimension has skylights // if the dimension has skylights
if (maxSkyLight > 0) if (updateSkyLight && maxSkyLight > 0)
{ {
IMutableBlockPosWrapper mcBlockPos = chunk.getMutableBlockPosWrapper();
IBlockStateWrapper previousBlockState = null;
int maxY = chunk.getMaxNonEmptyHeight(); int maxY = chunk.getMaxNonEmptyHeight();
int minY = chunk.getMinBuildHeight(); int minY = chunk.getMinBuildHeight();
@@ -174,7 +214,7 @@ public class DhLightingEngine
// set each pos' sky light all the way down until an opaque block is hit // set each pos' sky light all the way down until an opaque block is hit
for (int y = maxY; y >= minY; y--) for (int y = maxY; y >= minY; y--)
{ {
IBlockStateWrapper block = chunk.getBlockState(relX, y, relZ); IBlockStateWrapper block = previousBlockState = chunk.getBlockState(relX, y, relZ, mcBlockPos, previousBlockState);
if (block != null && block.getOpacity() != LodUtil.BLOCK_FULLY_TRANSPARENT) if (block != null && block.getOpacity() != LodUtil.BLOCK_FULLY_TRANSPARENT)
{ {
// keep moving down until we find a non-transparent block // keep moving down until we find a non-transparent block
@@ -204,17 +244,23 @@ public class DhLightingEngine
} }
// block light // block light
this.propagateLightPosList(blockLightWorldPosQueue, adjacentChunkHolder, if (updateBlockLight)
{
this.propagateChunkLightPosList(blockLightWorldPosQueue, adjacentChunkHolder,
(neighbourChunk, relBlockPos) -> neighbourChunk.getDhBlockLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ()), (neighbourChunk, relBlockPos) -> neighbourChunk.getDhBlockLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ()),
(neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhBlockLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ(), newLightValue), (neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhBlockLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ(), newLightValue),
true); true);
}
// sky light // sky light
this.propagateLightPosList(skyLightWorldPosQueue, adjacentChunkHolder, if (updateSkyLight)
{
this.propagateChunkLightPosList(skyLightWorldPosQueue, adjacentChunkHolder,
(neighbourChunk, relBlockPos) -> neighbourChunk.getDhSkyLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ()), (neighbourChunk, relBlockPos) -> neighbourChunk.getDhSkyLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ()),
(neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhSkyLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ(), newLightValue), (neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhSkyLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ(), newLightValue),
false); false);
} }
}
catch (Exception e) catch (Exception e)
{ {
LOGGER.error("Unexpected lighting issue for center chunk: "+centerChunkPos, e); LOGGER.error("Unexpected lighting issue for center chunk: "+centerChunkPos, e);
@@ -226,17 +272,18 @@ public class DhLightingEngine
} }
if (updateBlockLight)
centerChunk.setIsDhLightCorrect(true); {
centerChunk.setUseDhLighting(true); centerChunk.setIsDhBlockLightCorrect(true);
}
long endTimeNs = System.nanoTime(); if (updateSkyLight)
float totalTimeMs = (endTimeNs - startTimeNs) / 1_000_000.0f; {
//LOGGER.trace("Finished generating lighting for chunk: [" + centerChunkPos + "] in ["+totalTimeMs+"] milliseconds"); centerChunk.setIsDhSkyLightCorrect(true);
}
} }
/** Applies each {@link LightPos} from the queue to the given set of {@link IChunkWrapper}'s. */ /** Applies each {@link LightPos} from the queue to the given set of {@link IChunkWrapper}'s. */
private void propagateLightPosList( private void propagateChunkLightPosList(
StableLightPosStack lightPosQueue, AdjacentChunkHolder adjacentChunkHolder, StableLightPosStack lightPosQueue, AdjacentChunkHolder adjacentChunkHolder,
IGetLightFunc getLightFunc, ISetLightFunc setLightFunc, IGetLightFunc getLightFunc, ISetLightFunc setLightFunc,
boolean propagatingBlockLights) boolean propagatingBlockLights)
@@ -247,6 +294,8 @@ public class DhLightingEngine
final DhBlockPosMutable neighbourBlockPos = PRIMARY_BLOCK_POS_REF.get(); final DhBlockPosMutable neighbourBlockPos = PRIMARY_BLOCK_POS_REF.get();
final DhBlockPosMutable relNeighbourBlockPos = SECONDARY_BLOCK_POS_REF.get(); final DhBlockPosMutable relNeighbourBlockPos = SECONDARY_BLOCK_POS_REF.get();
IMutableBlockPosWrapper mcBlockPos = null;
IBlockStateWrapper previousBlockState = null;
// update each light position // update each light position
while (!lightPosQueue.isEmpty()) while (!lightPosQueue.isEmpty())
@@ -290,7 +339,15 @@ public class DhLightingEngine
} }
IBlockStateWrapper neighbourBlockState = neighbourChunk.getBlockState(relNeighbourBlockPos); if (mcBlockPos == null)
{
// it doesn't matter what chunk we get the position object from
// TODO move this getter logic out of ChunkWrapper
mcBlockPos = neighbourChunk.getMutableBlockPosWrapper();
}
IBlockStateWrapper neighbourBlockState = previousBlockState = neighbourChunk.getBlockState(relNeighbourBlockPos, mcBlockPos, previousBlockState);
// Math.max(1, ...) is used so that the propagated light level always drops by at least 1, preventing infinite cycles. // Math.max(1, ...) is used so that the propagated light level always drops by at least 1, preventing infinite cycles.
int targetLevel = lightValue - Math.max(1, neighbourBlockState.getOpacity()); int targetLevel = lightValue - Math.max(1, neighbourBlockState.getOpacity());
if (targetLevel > currentBlockLight) if (targetLevel > currentBlockLight)
@@ -322,6 +379,241 @@ public class DhLightingEngine
//======================//
// data source lighting //
//======================//
/** @author BuilderB0y */
public void bakeDataSourceSkyLight(FullDataSourceV2 dataSource, int maxSkyLight)
{
// create a cache of all the IDs which are completely transparent.
// FullDataPointIdMap is thread-safe with locks, and is also a map lookup,
// and both of these things add a bit of overhead which is not necessary
// in this context.
// note: since IDs map to both biomes and blocks, there can be more than
// one ID which corresponds to air.
BitSet airIDs = new BitSet(dataSource.mapping.size());
for (int id = 0, size = dataSource.mapping.size(); id < size; id++)
{
if (dataSource.mapping.getBlockStateWrapper(id).getOpacity() == 0)
{
airIDs.set(id, true);
}
}
for (int z = 0; z < FullDataSourceV2.WIDTH; z++)
{
for (int x = 0; x < FullDataSourceV2.WIDTH; x++)
{
LongArrayList dataPoints = dataSource.get(x, z);
if (dataPoints != null && !dataPoints.isEmpty())
{
// iterate through the data points in this column top-down
// until we reach light level 0 in some way. at this point,
// no more propagation needs to be performed for this column.
int size = dataPoints.size();
for (int index = 0; index < size; index++)
{
long point = dataPoints.getLong(index);
// if the data point in the column is transparent,
// then fill it with light and then propagate
// that light both horizontally and downwards.
if (airIDs.get(FullDataPointUtil.getId(point)))
{
int skylight;
if (index == 0)
{
// top-most data point in the column.
skylight = maxSkyLight;
}
else
{
// handle down propagation here. sort of.
// down propagation is also handled partially elsewhere.
// basically if the data point above is transparent,
// we copy its light level.
// otherwise, if the data point above is opaque,
// then no light can propagate downwards from it.
// therefore, this data point should be light level 0*
// and no more propagation needs to be performed for this column.
//
// *unless light propagates into it horizontally,
// but that is handled separately.
long above = dataPoints.getLong(index - 1);
if (airIDs.get(FullDataPointUtil.getId(above)))
{
skylight = FullDataPointUtil.getSkyLight(above);
}
else
{
continue;
}
}
// update the data point to contain the correct starting skylight level.
point = FullDataPointUtil.setSkyLight(point, skylight);
dataPoints.set(index, point);
// now for the propagation.
recursivelyLightAdjacentDataPoints(dataSource, airIDs, x, z, point);
}
}
}
}
}
// at this point, all transparent data points have been lit,
// but opaque ones still have light level 0.
// in this loop we make opaque data points copy the light level
// above them if, and only if, the data point above is translucent.
// with one exception: if the data point above is only partially translucent,
// we use a slightly different way of computing how much light it absorbed.
// this is how we handle water and ocean floors.
// note that this alternate logic assumes the
// data point above is being lit from the top.
// this is a fine assumption for water and oceans.
for (LongArrayList list : dataSource.dataPoints)
{
if (list != null)
{
for (int index = 0, size = list.size(); index < size; index++)
{
long dataPoint = list.getLong(index);
if (index == 0)
{
// top data point, assume "above" has the max sky light.
dataPoint = FullDataPointUtil.setSkyLight(dataPoint, maxSkyLight);
list.set(index, dataPoint);
}
else
{
// there is another data point above this one.
// check to see how opaque this data point is first.
// we will check the above one after that.
if (!airIDs.get(FullDataPointUtil.getId(dataPoint)))
{
// this data point is not transparent.
// it should be lit from above.
long above = list.getLong(index - 1);
int aboveLight = FullDataPointUtil.getSkyLight(above);
if (airIDs.get(FullDataPointUtil.getId(above)))
{
// the above data point is transparent,
// and does not absorb any light.
// its light level can be copied as-is.
dataPoint = FullDataPointUtil.setSkyLight(dataPoint, aboveLight);
list.set(index, dataPoint);
}
else
{
// determine how much light should be absorbed by this column
int absorption = dataSource.mapping.getBlockStateWrapper(FullDataPointUtil.getId(above)).getOpacity() * FullDataPointUtil.getHeight(above);
if (absorption < aboveLight)
{
// the above data point is partially translucent,
// and absorbs some light. however, it did not absorb
// enough light to bring the light level down to 0.
// so, the remaining light can still be copied.
dataPoint = FullDataPointUtil.setSkyLight(dataPoint, aboveLight - absorption);
list.set(index, dataPoint);
}
}
}
}
}
}
}
}
/** @author BuilderB0y */
public void recursivelyLightAdjacentDataPoints(
FullDataSourceV2 chunk,
BitSet airIDs,
int relativeX,
int relativeZ,
long dataPoint
)
{
int lightLevel = FullDataPointUtil.getSkyLight(dataPoint);
// early exit condition:
// in this case, propagating light is guaranteed to be 0 at adjacent positions,
// and therefore we do not need to waste time propagating it.
if (lightLevel <= 1)
{
return;
}
int minY = FullDataPointUtil.getBottomY(dataPoint);
int maxY = FullDataPointUtil.getHeight(dataPoint) + minY;
// try to propagate in all 4 directions.
for (int offsetIndex = 0; offsetIndex < ADJACENT_DIRECTION_OFFSETS.length; )
{
int adjacentX = relativeX + ADJACENT_DIRECTION_OFFSETS[offsetIndex++];
int adjacentZ = relativeZ + ADJACENT_DIRECTION_OFFSETS[offsetIndex++];
// check if the adjacent position is within the bounds of this data source...
if (adjacentX >= 0 && adjacentX < FullDataSourceV2.WIDTH && adjacentZ >= 0 && adjacentZ < FullDataSourceV2.WIDTH)
{
LongArrayList adjacentDataPoints = chunk.get(adjacentX, adjacentZ);
// ...and also check to make sure we have some data points
// (potentially transparent ones) to propagate through in the adjacent column.
if (adjacentDataPoints != null)
{
// try to find adjacent data points we can propagate into.
// we go top-down for this, which will be important for some
// later conditions.
int size = adjacentDataPoints.size();
for (int adjacentIndex = 0; adjacentIndex < size; adjacentIndex++)
{
long adjacentDataPoint = adjacentDataPoints.getLong(adjacentIndex);
int adjacentMinY = FullDataPointUtil.getBottomY(adjacentDataPoint);
int adjacentMaxY = FullDataPointUtil.getHeight(adjacentDataPoint) + adjacentMinY;
if (adjacentMinY >= maxY)
{
// if the adjacent data point is completely above this one,
// then there is no overlap between this one and the adjacent one,
// and therefore light cannot propagate here.
// try to propagate to the next data point down from the adjacent one.
continue;
}
else if (adjacentMaxY <= minY)
{
// if the adjacent data point is completely below this one,
// then it also has no overlap and can't propagate,
// but since we're going top-down, neither can any subsequent adjacent data points.
break;
}
else if (!airIDs.get(FullDataPointUtil.getId(adjacentDataPoint)))
{
// assume for now that we cannot propagate into non-transparent data points.
continue; // TODO how does this work with water? Do we care?
}
else
{
// now we can try to propagate.
int adjacentLightLevel = FullDataPointUtil.getSkyLight(adjacentDataPoint);
// if the resulting light level after propagation would INCREASE
// the light level of the adjacent data point, then propagate to it.
// otherwise, don't do that.
if (lightLevel - 1 > adjacentLightLevel)
{
adjacentDataPoint = FullDataPointUtil.setSkyLight(adjacentDataPoint, lightLevel - 1);
adjacentDataPoints.set(adjacentIndex, adjacentDataPoint);
// if propagation succeeded, recursively propagate again starting at the adjacent data point.
recursivelyLightAdjacentDataPoints(chunk, airIDs, adjacentX, adjacentZ, adjacentDataPoint);
}
}
}
}
}
}
}
//===========// //===========//
// debugging // // debugging //
//===========// //===========//
@@ -50,9 +50,17 @@ public interface IFullDataSourceRetrievalQueue extends Closeable
// getters // // getters //
//=========// //=========//
/** the largest numerical detail level */ /**
* The largest numerical detail level. <br>
* Detail level is absolute, not section;
* IE 0 = Block, 1 = 2x2 blocks, etc.
*/
byte lowestDataDetail(); byte lowestDataDetail();
/** the smallest numerical detail level */ /**
* The smallest numerical detail level. <br>
* Detail level is absolute, not section;
* IE 0 = Block, 1 = 2x2 blocks, etc.
*/
byte highestDataDetail(); byte highestDataDetail();
@@ -82,7 +90,7 @@ public interface IFullDataSourceRetrievalQueue extends Closeable
*/ */
void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf); void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf);
CompletableFuture<WorldGenResult> submitGenTask(long pos, byte requiredDataDetail, IWorldGenTaskTracker tracker); CompletableFuture<WorldGenResult> submitRetrievalTask(long pos, byte requiredDataDetail, IWorldGenTaskTracker tracker);
@@ -90,7 +98,10 @@ public interface IFullDataSourceRetrievalQueue extends Closeable
// shutdown // // shutdown //
//==========// //==========//
CompletableFuture<Void> startClosing(boolean cancelCurrentGeneration, boolean alsoInterruptRunning); /** Can be used to let any lingering generation requests finish before fully shutting down the system */
CompletableFuture<Void> startClosingAsync(boolean cancelCurrentGeneration, boolean alsoInterruptRunning);
@Override
void close(); void close();
@@ -5,7 +5,7 @@ import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult;
import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.multiplayer.client.AbstractFullDataRequestQueue; import com.seibel.distanthorizons.core.multiplayer.client.AbstractFullDataNetworkRequestQueue;
import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState; import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState;
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;
@@ -14,62 +14,71 @@ import org.apache.logging.log4j.Logger;
import java.util.concurrent.*; import java.util.concurrent.*;
public class WorldRemoteGenerationQueue extends AbstractFullDataRequestQueue implements IFullDataSourceRetrievalQueue, IDebugRenderable public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQueue implements IFullDataSourceRetrievalQueue, IDebugRenderable
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private int estimatedTotalTaskCount; private int estimatedTotalTaskCount;
@Override
protected int getRequestRateLimit() { return this.networkState.config.getGenerationRequestRateLimit(); } //=============//
// constructor //
//=============//
public RemoteWorldRetrievalQueue(ClientNetworkState networkState, IDhClientLevel level)
{ super(networkState, level, false, Config.Client.Advanced.Debugging.DebugWireframe.showWorldGenQueue); }
//===========================//
// retrieval queue overrides //
//===========================//
@Override @Override
protected String getQueueName() { return "World Remote Generation Queue"; } public void startAndSetTargetPos(DhBlockPos2D targetPos) { super.tick(targetPos); }
@Override
public byte lowestDataDetail() { return LodUtil.BLOCK_DETAIL_LEVEL; }
@Override
public byte highestDataDetail() { return LodUtil.BLOCK_DETAIL_LEVEL; }
public WorldRemoteGenerationQueue(ClientNetworkState networkState, IDhClientLevel level) @Override
public CompletableFuture<WorldGenResult> submitRetrievalTask(long sectionPos, byte requiredDataDetail, IWorldGenTaskTracker tracker)
{ {
super(networkState, level, false, Config.Client.Advanced.Debugging.DebugWireframe.showWorldGenQueue); return super.submitRequest(sectionPos, tracker.getDataSourceConsumer())
} .thenApply(retrievalSuccess -> retrievalSuccess
@Override
public byte lowestDataDetail()
{
return LodUtil.BLOCK_DETAIL_LEVEL;
}
@Override
public byte highestDataDetail()
{
return LodUtil.BLOCK_DETAIL_LEVEL;
}
@Override
public CompletableFuture<WorldGenResult> submitGenTask(long sectionPos, byte requiredDataDetail, IWorldGenTaskTracker tracker)
{
return super.submitRequest(sectionPos, tracker.getChunkDataConsumer())
.thenApply(result -> result
? WorldGenResult.CreateSuccess(sectionPos) ? WorldGenResult.CreateSuccess(sectionPos)
: WorldGenResult.CreateFail()); : WorldGenResult.CreateFail());
} }
@Override @Override
public void startAndSetTargetPos(DhBlockPos2D targetPos) public CompletableFuture<Void> startClosingAsync(boolean cancelCurrentGeneration, boolean alsoInterruptRunning)
{ { return super.startClosingAsync(alsoInterruptRunning); }
super.tick(targetPos);
}
//=================================//
// network request queue overrides //
//=================================//
@Override
protected int getRequestRateLimit() { return this.networkState.sessionConfig.getGenerationRequestRateLimit(); }
@Override
protected String getQueueName() { return "World Remote Generation Queue"; }
//===============//
// debug display //
//===============//
@Override @Override
public int getEstimatedTotalTaskCount() { return this.estimatedTotalTaskCount; } public int getEstimatedTotalTaskCount() { return this.estimatedTotalTaskCount; }
@Override @Override
public void setEstimatedTotalTaskCount(int newEstimate) { this.estimatedTotalTaskCount = newEstimate; } public void setEstimatedTotalTaskCount(int newEstimate) { this.estimatedTotalTaskCount = newEstimate; }
@Override
public CompletableFuture<Void> startClosing(boolean cancelCurrentGeneration, boolean alsoInterruptRunning)
{
return super.startClosing(alsoInterruptRunning);
}
} }
@@ -142,7 +142,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
//=================// //=================//
@Override @Override
public CompletableFuture<WorldGenResult> submitGenTask(long pos, byte requiredDataDetail, IWorldGenTaskTracker tracker) public CompletableFuture<WorldGenResult> submitRetrievalTask(long pos, byte requiredDataDetail, IWorldGenTaskTracker tracker)
{ {
// 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)
@@ -468,7 +468,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
try try
{ {
IChunkWrapper chunk = WRAPPER_FACTORY.createChunkWrapper(generatedObjectArray); IChunkWrapper chunk = WRAPPER_FACTORY.createChunkWrapper(generatedObjectArray);
FullDataSourceV2 dataSource = LodDataBuilder.createGeneratedDataSource(chunk); FullDataSourceV2 dataSource = LodDataBuilder.createFromChunk(chunk);
LodUtil.assertTrue(dataSource != null); LodUtil.assertTrue(dataSource != null);
chunkDataConsumer.accept(dataSource); chunkDataConsumer.accept(dataSource);
} }
@@ -523,8 +523,8 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
// getters / setters // // getters / setters //
//===================// //===================//
public int getWaitingTaskCount() { return this.waitingTasks.size(); } @Override public int getWaitingTaskCount() { return this.waitingTasks.size(); }
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; }
@@ -536,7 +536,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
@Override @Override
public void setEstimatedTotalTaskCount(int newEstimate) { this.estimatedTotalTaskCount = newEstimate; } public void setEstimatedTotalTaskCount(int newEstimate) { this.estimatedTotalTaskCount = newEstimate; }
public void addDebugMenuStringsToList(List<String> messageList) { } @Override public void addDebugMenuStringsToList(List<String> messageList) { }
@@ -544,7 +544,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
// shutdown // // shutdown //
//==========// //==========//
public CompletableFuture<Void> startClosing(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();
@@ -595,7 +595,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
if (this.generatorClosingFuture == null) if (this.generatorClosingFuture == null)
{ {
this.startClosing(true, true); this.startClosingAsync(true, true);
} }
LodUtil.assertTrue(this.generatorClosingFuture != null); LodUtil.assertTrue(this.generatorClosingFuture != null);
@@ -671,14 +671,18 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
{ {
index = 4 * X * X - X - Y; index = 4 * X * X - X - Y;
if (X < Y) if (X < Y)
{
index = index - 2 * (X - Y); index = index - 2 * (X - Y);
} }
}
else else
{ {
index = 4 * Y * Y - X - Y; index = 4 * Y * Y - X - Y;
if (X < Y) if (X < Y)
{
index = index + 2 * (X - Y); index = index + 2 * (X - Y);
} }
}
return index; return index;
} }
@@ -34,6 +34,6 @@ public interface IWorldGenTaskTracker
boolean isMemoryAddressValid(); boolean isMemoryAddressValid();
@Nullable @Nullable
Consumer<FullDataSourceV2> getChunkDataConsumer(); Consumer<FullDataSourceV2> getDataSourceConsumer();
} }
@@ -51,7 +51,7 @@ public final class WorldGenTaskGroup
while (tasks.hasNext()) while (tasks.hasNext())
{ {
WorldGenTask task = tasks.next(); WorldGenTask task = tasks.next();
Consumer<FullDataSourceV2> chunkDataConsumer = task.taskTracker.getChunkDataConsumer(); Consumer<FullDataSourceV2> chunkDataConsumer = task.taskTracker.getDataSourceConsumer();
if (chunkDataConsumer == null) if (chunkDataConsumer == null)
{ {
tasks.remove(); tasks.remove();
@@ -103,10 +103,14 @@ public class DarkModeDetector
{ {
// System.out.println(de); // System.out.println(de);
if (de.contains("gnome-session")) // Gnome uses GTK if (de.contains("gnome-session")) // Gnome uses GTK
{
return GTKChecker(); return GTKChecker();
}
if (de.contains("plasma_session")) // KDE plasma uses QT if (de.contains("plasma_session")) // KDE plasma uses QT
{
return QTChecker(); return QTChecker();
} }
}
return GTKChecker(); // GTK works best with non plasma desktops (desktops includes window managers) return GTKChecker(); // GTK works best with non plasma desktops (desktops includes window managers)
} }
@@ -134,7 +138,9 @@ public class DarkModeDetector
while (themeLine != null) while (themeLine != null)
{ // Go through each line till you find "KWinPalette\activeBackground" { // Go through each line till you find "KWinPalette\activeBackground"
if (themeLine.contains("KWinPalette\\activeBackground")) if (themeLine.contains("KWinPalette\\activeBackground"))
{
break; break;
}
themeLine = reader.readLine(); themeLine = reader.readLine();
} }
reader.close(); reader.close();
@@ -145,10 +151,14 @@ public class DarkModeDetector
short g = (short) Integer.parseInt("" + themeLine.charAt(index + 3) + themeLine.charAt(index + 4), 16); short g = (short) Integer.parseInt("" + themeLine.charAt(index + 3) + themeLine.charAt(index + 4), 16);
short b = (short) Integer.parseInt("" + themeLine.charAt(index + 5) + themeLine.charAt(index + 6), 16); short b = (short) Integer.parseInt("" + themeLine.charAt(index + 5) + themeLine.charAt(index + 6), 16);
if ((r + g + b) / 2 >= 128) if ((r + g + b) / 2 >= 128)
{
return false; return false;
}
else else
{
return true; return true;
} }
}
catch (Exception e) catch (Exception e)
{ {
e.printStackTrace(); return false; e.printStackTrace(); return false;
@@ -170,7 +180,9 @@ public class DarkModeDetector
while ((actualReadLine = reader.readLine()) != null) while ((actualReadLine = reader.readLine()) != null)
{ {
if (stringBuilder.length() != 0) if (stringBuilder.length() != 0)
{
stringBuilder.append('\n'); stringBuilder.append('\n');
}
stringBuilder.append(actualReadLine); stringBuilder.append(actualReadLine);
} }
} }
@@ -22,6 +22,7 @@ package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiChunkModifiedEvent; import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiChunkModifiedEvent;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.fullDatafile.DelayedFullDataSourceSaveCache; import com.seibel.distanthorizons.core.file.fullDatafile.DelayedFullDataSourceSaveCache;
import com.seibel.distanthorizons.core.generation.DhLightingEngine;
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.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
@@ -32,6 +33,7 @@ import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.sql.dto.ChunkHashDTO; import com.seibel.distanthorizons.core.sql.dto.ChunkHashDTO;
import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo; import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo;
import com.seibel.distanthorizons.core.sql.repo.ChunkHashRepo; import com.seibel.distanthorizons.core.sql.repo.ChunkHashRepo;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -168,6 +170,12 @@ public abstract class AbstractDhLevel implements IDhLevel
private void onDataSourceSave(FullDataSourceV2 fullDataSource) private void onDataSourceSave(FullDataSourceV2 fullDataSource)
{ {
// 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
// allows us to reduce cross-chunk lighting issues by lighting the whole 4x4 LOD at once
DhLightingEngine.INSTANCE.bakeDataSourceSkyLight(fullDataSource, this.hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT);
this.updateDataSourcesAsync(fullDataSource).thenRun(() -> this.updateDataSourcesAsync(fullDataSource).thenRun(() ->
{ {
HashSet<DhChunkPos> updatedChunkPosSet = this.updatedChunkPosSetBySectionPos.remove(fullDataSource.getPos()); HashSet<DhChunkPos> updatedChunkPosSet = this.updatedChunkPosSetBySectionPos.remove(fullDataSource.getPos());
@@ -126,9 +126,9 @@ public class ClientLevelModule implements Closeable, AbstractDataSourceHandler.I
boolean isBuffersDirty = false; boolean isBuffersDirty = false;
EDhApiDebugRendering newDebugRendering = Config.Client.Advanced.Debugging.debugRendering.get(); EDhApiDebugRendering newDebugRendering = Config.Client.Advanced.Debugging.debugRendering.get();
if (newDebugRendering != lastDebugRendering) if (newDebugRendering != this.lastDebugRendering)
{ {
lastDebugRendering = newDebugRendering; this.lastDebugRendering = newDebugRendering;
isBuffersDirty = true; isBuffersDirty = true;
} }
if (isBuffersDirty) if (isBuffersDirty)
@@ -225,7 +225,7 @@ public class ClientLevelModule implements Closeable, AbstractDataSourceHandler.I
} }
} }
public void close() @Override public void close()
{ {
// shutdown the renderer // shutdown the renderer
ClientRenderState ClientRenderState = this.ClientRenderStateRef.get(); ClientRenderState ClientRenderState = this.ClientRenderStateRef.get();
@@ -27,7 +27,7 @@ import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.file.fullDatafile.RemoteFullDataSourceProvider; import com.seibel.distanthorizons.core.file.fullDatafile.RemoteFullDataSourceProvider;
import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure;
import com.seibel.distanthorizons.core.generation.WorldRemoteGenerationQueue; import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState; import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState;
import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoginRequestQueue; import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoginRequestQueue;
@@ -60,14 +60,6 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static class WorldGenState extends WorldGenModule.AbstractWorldGenState
{
WorldGenState(IDhClientLevel level, ClientNetworkState networkState)
{
this.worldGenerationQueue = new WorldRemoteGenerationQueue(networkState, level);
}
}
public final ClientLevelModule clientside; public final ClientLevelModule clientside;
public final IClientLevelWrapper levelWrapper; public final IClientLevelWrapper levelWrapper;
public final AbstractSaveStructure saveStructure; public final AbstractSaveStructure saveStructure;
@@ -76,7 +68,7 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
@CheckForNull @CheckForNull
private final ClientNetworkState networkState; private final ClientNetworkState networkState;
@Nullable @Nullable
private final ScopedNetworkEventSource eventSource; private final ScopedNetworkEventSource networkEventSource;
public final WorldGenModule worldGenModule; public final WorldGenModule worldGenModule;
public final AppliedConfigState<Boolean> worldGeneratorEnabledConfig; public final AppliedConfigState<Boolean> worldGeneratorEnabledConfig;
@@ -102,15 +94,15 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
this.saveStructure = saveStructure; this.saveStructure = saveStructure;
this.networkState = networkState; this.networkState = networkState;
if (networkState != null) if (this.networkState != null)
{ {
this.eventSource = new ScopedNetworkEventSource(networkState.getSession()); this.networkEventSource = new ScopedNetworkEventSource(this.networkState.getSession());
this.syncOnLoginRequestQueue = new SyncOnLoginRequestQueue(this, networkState); this.syncOnLoginRequestQueue = new SyncOnLoginRequestQueue(this, this.networkState);
this.registerNetworkHandlers(); this.registerNetworkHandlers();
} }
else else
{ {
this.eventSource = null; this.networkEventSource = null;
this.syncOnLoginRequestQueue = null; this.syncOnLoginRequestQueue = null;
} }
@@ -129,19 +121,18 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
LOGGER.info("Started DHLevel for " + this.levelWrapper + " with saves at " + this.saveStructure); LOGGER.info("Started DHLevel for " + this.levelWrapper + " with saves at " + this.saveStructure);
} }
} }
private void registerNetworkHandlers() private void registerNetworkHandlers()
{ {
assert this.eventSource != null; assert this.networkEventSource != null;
assert this.networkState != null; assert this.networkState != null;
this.eventSource.registerHandler(FullDataPartialUpdateMessage.class, msg -> this.networkEventSource.registerHandler(FullDataPartialUpdateMessage.class, message ->
{ {
try try
{ {
FullDataSourceV2DTO dataSourceDto = this.networkState.decodeDataSourceAndReleaseBuffer(msg.payload); FullDataSourceV2DTO dataSourceDto = this.networkState.decodeDataSourceAndReleaseBuffer(message.payload);
if (!msg.isSameLevelAs(this.levelWrapper)) if (!message.isSameLevelAs(this.levelWrapper))
{ {
return; return;
} }
@@ -155,6 +146,8 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
}); });
} }
//==============// //==============//
// tick methods // // tick methods //
//==============// //==============//
@@ -178,7 +171,7 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
} }
@Override @Override
public void doWorldGen() public void worldGenTick()
{ {
ClientNetworkState networkState = this.networkState; ClientNetworkState networkState = this.networkState;
@@ -190,7 +183,7 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
} }
boolean shouldDoWorldGen = isClientUsable boolean shouldDoWorldGen = isClientUsable
&& networkState.config.isDistantGenerationEnabled() && networkState.sessionConfig.isDistantGenerationEnabled()
&& isAllowedDimension && isAllowedDimension
&& this.clientside.isRendering(); && this.clientside.isRendering();
@@ -214,13 +207,16 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
if (this.worldGenModule.isWorldGenRunning()) if (this.worldGenModule.isWorldGenRunning())
{ {
this.worldGenModule.worldGenTick( this.worldGenModule.worldGenTick(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos()));
new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos())
.scale(MC_CLIENT.getWrappedClientLevel().getDimensionType().getTeleportationScale(this.getLevelWrapper().getDimensionType()))
);
} }
} }
//===========//
// rendering //
//===========//
@Override @Override
public void render(DhApiRenderParam renderEventParam, IProfilerWrapper profiler) public void render(DhApiRenderParam renderEventParam, IProfilerWrapper profiler)
{ this.clientside.render(renderEventParam, profiler); } { this.clientside.render(renderEventParam, profiler); }
@@ -231,9 +227,28 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
//================// //===========//
// level handling // // world gen //
//================// //===========//
@Override
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);
}
//=========//
// getters //
//=========//
@Override @Override
public int computeBaseColor(DhBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper block) { return this.levelWrapper.getBlockColor(pos, biome, block); } public int computeBaseColor(DhBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper block) { return this.levelWrapper.getBlockColor(pos, biome, block); }
@@ -242,10 +257,7 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
public IClientLevelWrapper getClientLevelWrapper() { return this.levelWrapper; } public IClientLevelWrapper getClientLevelWrapper() { return this.levelWrapper; }
@Override @Override
public void clearRenderCache() public void clearRenderCache() { this.clientside.clearRenderCache(); }
{
this.clientside.clearRenderCache();
}
@Override @Override
public ILevelWrapper getLevelWrapper() { return this.levelWrapper; } public ILevelWrapper getLevelWrapper() { return this.levelWrapper; }
@@ -256,6 +268,30 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
@Override @Override
public int getMinY() { return this.levelWrapper.getMinHeight(); } public int getMinY() { return this.levelWrapper.getMinHeight(); }
@Override
public FullDataSourceProviderV2 getFullDataProvider() { return this.dataFileHandler; }
@Override
public AbstractSaveStructure getSaveStructure() { return this.saveStructure; }
@Override
public boolean hasSkyLight() { return this.levelWrapper.hasSkyLight(); }
@Override
public GenericObjectRenderer getGenericRenderer() { return this.clientside.genericRenderer; }
@Override
public RenderBufferHandler getRenderBufferHandler()
{
ClientLevelModule.ClientRenderState renderState = this.clientside.ClientRenderStateRef.get();
return (renderState != null) ? renderState.renderBufferHandler : null;
}
//===========//
// debugging //
//===========//
@Override @Override
public void addDebugMenuStringsToList(List<String> messageList) public void addDebugMenuStringsToList(List<String> messageList)
{ {
@@ -289,13 +325,19 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
if (this.syncOnLoginRequestQueue != null) if (this.syncOnLoginRequestQueue != null)
{ {
assert this.networkState != null; assert this.networkState != null;
if (this.networkState.config.getSynchronizeOnLogin()) if (this.networkState.sessionConfig.getSynchronizeOnLogin())
{ {
this.syncOnLoginRequestQueue.addDebugMenuStringsToList(messageList); this.syncOnLoginRequestQueue.addDebugMenuStringsToList(messageList);
} }
} }
} }
//==========//
// shutdown //
//==========//
@Override @Override
public void close() public void close()
{ {
@@ -304,9 +346,9 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
this.worldGenModule.close(); this.worldGenModule.close();
} }
if (this.eventSource != null) if (this.networkEventSource != null)
{ {
this.eventSource.close(); this.networkEventSource.close();
} }
this.levelWrapper.setParentLevel(null); this.levelWrapper.setParentLevel(null);
@@ -316,35 +358,18 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
LOGGER.info("Closed [" + DhClientLevel.class.getSimpleName() + "] for [" + this.levelWrapper + "]"); LOGGER.info("Closed [" + DhClientLevel.class.getSimpleName() + "] for [" + this.levelWrapper + "]");
} }
@Override
public FullDataSourceProviderV2 getFullDataProvider() { return this.dataFileHandler; }
@Override
public AbstractSaveStructure getSaveStructure() { return this.saveStructure; }
@Override
public boolean hasSkyLight() { return this.levelWrapper.hasSkyLight(); }
@Override //================//
public GenericObjectRenderer getGenericRenderer() { return this.clientside.genericRenderer; } // helper classes //
@Override //================//
public RenderBufferHandler getRenderBufferHandler()
private static class WorldGenState extends WorldGenModule.AbstractWorldGenState
{ {
ClientLevelModule.ClientRenderState renderState = this.clientside.ClientRenderStateRef.get(); WorldGenState(IDhClientLevel level, ClientNetworkState networkState)
return (renderState != null) ? renderState.renderBufferHandler : null; {
this.worldGenerationQueue = new RemoteWorldRetrievalQueue(networkState, level);
}
} }
@Override
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);
}
} }
@@ -98,7 +98,7 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev
public void serverTick() { } public void serverTick() { }
@Override @Override
public void doWorldGen() public void worldGenTick()
{ {
this.serverside.worldGeneratorEnabledConfig.pollNewValue(); // if not called the get() line below may not this.serverside.worldGeneratorEnabledConfig.pollNewValue(); // if not called the get() line below may not
boolean shouldDoWorldGen = this.serverside.worldGeneratorEnabledConfig.get() && this.clientside.isRendering(); boolean shouldDoWorldGen = this.serverside.worldGeneratorEnabledConfig.get() && this.clientside.isRendering();
@@ -154,9 +154,7 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev
public IClientLevelWrapper getClientLevelWrapper() { return MC_CLIENT.getWrappedClientLevel(); } public IClientLevelWrapper getClientLevelWrapper() { return MC_CLIENT.getWrappedClientLevel(); }
@Override @Override
public void clearRenderCache() { public void clearRenderCache() { this.clientside.clearRenderCache(); }
this.clientside.clearRenderCache();
}
@Override @Override
public IServerLevelWrapper getServerLevelWrapper() { return this.serverLevelWrapper; } public IServerLevelWrapper getServerLevelWrapper() { return this.serverLevelWrapper; }
@@ -31,8 +31,8 @@ import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerState;
import com.seibel.distanthorizons.core.network.exceptions.InvalidLevelException; import com.seibel.distanthorizons.core.network.exceptions.InvalidLevelException;
import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException; import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException;
import com.seibel.distanthorizons.core.network.messages.ILevelRelatedMessage; import com.seibel.distanthorizons.core.network.messages.ILevelRelatedMessage;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.network.messages.TrackableMessage; import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPayload; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPayload;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage;
@@ -51,11 +51,9 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import javax.annotation.CheckForNull; import javax.annotation.CheckForNull;
import java.text.MessageFormat;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.function.Consumer;
public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
{ {
@@ -63,7 +61,8 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
private static final ConfigBasedLogger NETWORK_LOGGER = new ConfigBasedLogger(LogManager.getLogger(), private static final ConfigBasedLogger NETWORK_LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Logging.logNetworkEvent.get()); () -> Config.Client.Advanced.Logging.logNetworkEvent.get());
public static final int FULL_DATA_CHUNK_SIZE = 1048000; // 576 bytes left for other contents /** 1 Mebibyte minus 576 bytes for other info */
public static final int FULL_DATA_SPLIT_SIZE_IN_BYTES = 1_048_000;
public final ServerLevelModule serverside; public final ServerLevelModule serverside;
private final IServerLevelWrapper serverLevelWrapper; private final IServerLevelWrapper serverLevelWrapper;
@@ -77,8 +76,9 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
*/ */
private final ConcurrentLinkedQueue<IServerPlayerWrapper> worldGenPlayerCenteringQueue = new ConcurrentLinkedQueue<>(); private final ConcurrentLinkedQueue<IServerPlayerWrapper> worldGenPlayerCenteringQueue = new ConcurrentLinkedQueue<>();
private final ConcurrentMap<Long, DataSourceRequestGroup> requestGroupsByPos = new ConcurrentHashMap<>(); private final ConcurrentMap<Long, DataSourceRequestGroup> requestGroupByPos = new ConcurrentHashMap<>();
private final ConcurrentMap<Long, DataSourceRequestGroup> requestGroupsByFutureId = new ConcurrentHashMap<>(); private final ConcurrentMap<Long, DataSourceRequestGroup> requestGroupByFutureId = new ConcurrentHashMap<>();
//=============// //=============//
@@ -96,181 +96,22 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
this.createAndSetSupportingRepos(this.serverside.fullDataFileHandler.repo.databaseFile); this.createAndSetSupportingRepos(this.serverside.fullDataFileHandler.repo.databaseFile);
this.runRepoReliantSetup(); this.runRepoReliantSetup();
LOGGER.info("Started DHLevel for {} with saves at {}", serverLevelWrapper, saveStructure); LOGGER.info("Started DHLevel for ["+serverLevelWrapper+"] at ["+saveStructure+"].");
this.remotePlayerConnectionHandler = remotePlayerConnectionHandler; this.remotePlayerConnectionHandler = remotePlayerConnectionHandler;
} }
public void registerNetworkHandlers(ServerPlayerState serverPlayerState)
{
serverPlayerState.session.registerHandler(FullDataSourceRequestMessage.class, this.currentLevelOnly(msg ->
{
ServerPlayerState.RateLimiterSet rateLimiterSet = serverPlayerState.getRateLimiterSet(this);
if (msg.clientTimestamp == null)
{
// Normal generation
if (!serverPlayerState.config.isDistantGenerationEnabled())
{
msg.sendResponse(new RequestRejectedException("Operation is disabled from config."));
return;
}
if (!rateLimiterSet.generationRequestRateLimiter.tryAcquire(msg))
{
return;
}
while (true)
{
DataSourceRequestGroup requestGroup = this.requestGroupsByPos.computeIfAbsent(msg.sectionPos, pos ->
{
DataSourceRequestGroup newGroup = new DataSourceRequestGroup();
this.tryFulfillDataSourceRequestGroup(newGroup, pos);
NETWORK_LOGGER.debug("[{}] Created request group for pos {}", this.serverLevelWrapper.getDimensionName(), pos);
return newGroup;
});
// If this fails, loop until either permit is acquired or group is removed to create another one
if (!requestGroup.requestAddSemaphore.tryAcquire())
{
Thread.yield();
continue;
}
this.requestGroupsByFutureId.put(msg.futureId, requestGroup);
requestGroup.requestMessages.put(msg.futureId, msg);
requestGroup.requestAddSemaphore.release();
break;
}
}
else
{
// Sync only
if (!serverPlayerState.config.getSynchronizeOnLogin())
{
msg.sendResponse(new RequestRejectedException("Operation is disabled from config."));
return;
}
if (!rateLimiterSet.syncOnLoginRateLimiter.tryAcquire(msg))
{
return;
}
Long serverTimestamp = this.serverside.fullDataFileHandler.getTimestampForPos(msg.sectionPos);
if (serverTimestamp == null || serverTimestamp <= msg.clientTimestamp)
{
rateLimiterSet.syncOnLoginRateLimiter.release();
msg.sendResponse(new FullDataSourceResponseMessage(null));
return;
}
ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor();
if (executor == null)
{
LOGGER.warn("Unable to send FullDataSourceResponseMessage - getNetworkCompressionExecutor() is null");
return;
}
this.serverside.fullDataFileHandler.getAsync(msg.sectionPos).thenAcceptAsync(fullDataSource ->
{
rateLimiterSet.syncOnLoginRateLimiter.release();
FullDataPayload payload = new FullDataPayload(fullDataSource);
payload.acceptInChunkMessages(FULL_DATA_CHUNK_SIZE, msg.getSession()::sendMessage);
msg.sendResponse(new FullDataSourceResponseMessage(payload));
}, executor);
}
}));
serverPlayerState.session.registerHandler(CancelMessage.class, msg ->
{
DataSourceRequestGroup requestGroup = this.requestGroupsByFutureId.remove(msg.futureId);
if (requestGroup == null)
{
return;
}
// If this fails, group is being removed and completing cancellation is not necessary
if (requestGroup.requestRemoveSemaphore.tryAcquire())
{
// Prevent adding requests in case the group will be removed by this cancellation
requestGroup.requestAddSemaphore.acquireUninterruptibly(Short.MAX_VALUE);
requestGroup.requestRemoveSemaphore.release();
serverPlayerState.getRateLimiterSet(this).generationRequestRateLimiter.release();
FullDataSourceRequestMessage requestMessage = requestGroup.requestMessages.remove(msg.futureId);
if (requestGroup.requestMessages.isEmpty())
{
NETWORK_LOGGER.debug("[{}] Cancelled request group {}", this.serverLevelWrapper.getDimensionName(), requestMessage.sectionPos);
this.requestGroupsByPos.remove(requestMessage.sectionPos);
this.serverside.fullDataFileHandler.removeRetrievalRequestIf(pos -> pos == requestMessage.sectionPos);
}
else
{
requestGroup.requestAddSemaphore.release(Short.MAX_VALUE);
}
}
});
}
//=======//
public <T extends NetworkMessage> Consumer<T> currentLevelOnly(Consumer<T> next) // ticks //
{ //=======//
return msg ->
{
LodUtil.assertTrue(msg instanceof ILevelRelatedMessage, MessageFormat.format("Received message does not implement {0}: {1}", ILevelRelatedMessage.class.getSimpleName(), msg.getClass().getSimpleName()));
// Handle only in requested dimension
if (!((ILevelRelatedMessage) msg).isSameLevelAs(this.getServerLevelWrapper()))
{
return;
}
// If player is not in this dimension and handling multiple dimensions at once is not allowed
assert msg.getSession().serverPlayer != null;
if (msg.getSession().serverPlayer.getLevel() != this.getLevelWrapper())
{
// If the message can be replied to - reply with error, otherwise just ignore
if (msg instanceof TrackableMessage)
{
((TrackableMessage) msg).sendResponse(new InvalidLevelException(MessageFormat.format(
"Generation not allowed. Requested dimension: {0}, player dimension: {1}, handler dimension: {2}",
((ILevelRelatedMessage) msg).getLevelName(),
msg.getSession().serverPlayer.getLevel().getDimensionName(),
this.getLevelWrapper().getDimensionName()
)));
}
return;
}
next.accept(msg);
};
}
//=========//
// methods //
//=========//
public void addPlayer(IServerPlayerWrapper serverPlayer)
{
this.worldGenPlayerCenteringQueue.add(serverPlayer);
}
public void removePlayer(IServerPlayerWrapper serverPlayer)
{
this.worldGenPlayerCenteringQueue.remove(serverPlayer);
}
@Override @Override
public void serverTick() public void serverTick()
{ {
// Send finished data source requests // Send finished data source requests
for (Map.Entry<Long, DataSourceRequestGroup> entry : this.requestGroupsByPos.entrySet()) for (Map.Entry<Long, DataSourceRequestGroup> entry : this.requestGroupByPos.entrySet())
{ {
DataSourceRequestGroup requestGroup = entry.getValue(); DataSourceRequestGroup requestGroup = entry.getValue();
@@ -279,10 +120,10 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
continue; continue;
} }
NETWORK_LOGGER.debug("[{}] Fulfilled request group {}", this.serverLevelWrapper.getDimensionName(), entry.getKey()); NETWORK_LOGGER.debug("["+this.serverLevelWrapper.getDimensionName()+"] Fulfilled request group ["+entry.getKey()+"]");
// Make this group unavailable for adding into // Make this group unavailable for adding into
this.requestGroupsByPos.remove(entry.getKey()); this.requestGroupByPos.remove(entry.getKey());
requestGroup.requestRemoveSemaphore.acquireUninterruptibly(Short.MAX_VALUE); requestGroup.requestRemoveSemaphore.acquireUninterruptibly(Short.MAX_VALUE);
requestGroup.requestAddSemaphore.acquireUninterruptibly(Short.MAX_VALUE); requestGroup.requestAddSemaphore.acquireUninterruptibly(Short.MAX_VALUE);
@@ -297,7 +138,7 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
FullDataPayload payload = new FullDataPayload(requestGroup.fullDataSource); FullDataPayload payload = new FullDataPayload(requestGroup.fullDataSource);
for (FullDataSourceRequestMessage msg : requestGroup.requestMessages.values()) for (FullDataSourceRequestMessage msg : requestGroup.requestMessages.values())
{ {
this.requestGroupsByFutureId.remove(msg.futureId); this.requestGroupByFutureId.remove(msg.futureId);
ServerPlayerState serverPlayerState = this.remotePlayerConnectionHandler.getConnectedPlayer(msg.serverPlayer()); ServerPlayerState serverPlayerState = this.remotePlayerConnectionHandler.getConnectedPlayer(msg.serverPlayer());
if (serverPlayerState == null) if (serverPlayerState == null)
@@ -306,7 +147,7 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
} }
serverPlayerState.getRateLimiterSet(this).generationRequestRateLimiter.release(); serverPlayerState.getRateLimiterSet(this).generationRequestRateLimiter.release();
payload.acceptInChunkMessages(FULL_DATA_CHUNK_SIZE, msg.getSession()::sendMessage); payload.splitAndSend(FULL_DATA_SPLIT_SIZE_IN_BYTES, msg.getSession()::sendMessage);
msg.sendResponse(new FullDataSourceResponseMessage(payload)); msg.sendResponse(new FullDataSourceResponseMessage(payload));
} }
}, executor); }, executor);
@@ -314,65 +155,7 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
} }
@Override @Override
public CompletableFuture<Void> updateDataSourcesAsync(FullDataSourceV2 data) public void worldGenTick()
{
if (!Config.Client.Advanced.Multiplayer.ServerNetworking.enableRealTimeUpdates.get())
{
return this.getFullDataProvider().updateDataSourceAsync(data);
}
ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor();
if (executor == null)
{
LOGGER.warn("Unable to send FullDataPartialUpdateMessage - getNetworkCompressionExecutor() is null");
return this.getFullDataProvider().updateDataSourceAsync(data);
}
CompletableFuture.runAsync(() ->
{
FullDataPayload payload = new FullDataPayload(data);
for (ServerPlayerState serverPlayerState : this.remotePlayerConnectionHandler.getConnectedPlayers())
{
if (serverPlayerState.serverPlayer().getLevel() != this.serverLevelWrapper)
{
continue;
}
if (!serverPlayerState.config.isRealTimeUpdatesEnabled())
{
continue;
}
Vec3d playerPosition = serverPlayerState.serverPlayer().getPosition();
int distanceFromPlayer = DhSectionPos.getManhattanBlockDistance(data.getPos(), new DhBlockPos2D((int) playerPosition.x, (int) playerPosition.z)) / 16;
if (distanceFromPlayer >= serverPlayerState.serverPlayer().getViewDistance() &&
distanceFromPlayer <= serverPlayerState.config.getRenderDistanceRadius())
{
payload.acceptInChunkMessages(FULL_DATA_CHUNK_SIZE, serverPlayerState.session::sendMessage);
serverPlayerState.session.sendMessage(new FullDataPartialUpdateMessage(this.serverLevelWrapper, payload));
}
}
}, executor);
return this.getFullDataProvider().updateDataSourceAsync(data);
}
@Override
public int getMinY()
{
return this.getLevelWrapper().getMinHeight();
}
@Override
public void close()
{
super.close();
this.serverside.close();
LOGGER.info("Closed DHLevel for {}", this.getLevelWrapper());
}
@Override
public void doWorldGen()
{ {
boolean shouldDoWorldGen = true; //todo; boolean shouldDoWorldGen = true; //todo;
boolean isWorldGenRunning = this.serverside.worldGenModule.isWorldGenRunning(); boolean isWorldGenRunning = this.serverside.worldGenModule.isWorldGenRunning();
@@ -405,36 +188,211 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
} }
} }
@Override
public IServerLevelWrapper getServerLevelWrapper()
//==================//
// network handling //
//==================//
public void registerNetworkHandlers(ServerPlayerState serverPlayerState)
{ {
return this.serverLevelWrapper; serverPlayerState.networkSession.registerHandler(FullDataSourceRequestMessage.class, (message) ->
{
if (!this.messagePlayerInThisLevel(message))
{
// we can't handle players in other levels, don't continue
return;
} }
@Override
public ILevelWrapper getLevelWrapper() ServerPlayerState.RateLimiterSet rateLimiterSet = serverPlayerState.getRateLimiterSet(this);
if (message.clientTimestamp == null)
{ {
return this.getServerLevelWrapper(); this.queueWorldGenForRequestMessage(serverPlayerState, message, rateLimiterSet);
}
else
{
this.queueLodSyncForRequestMessage(serverPlayerState, message, rateLimiterSet);
}
});
serverPlayerState.networkSession.registerHandler(CancelMessage.class, msg ->
{
DataSourceRequestGroup requestGroup = this.requestGroupByFutureId.remove(msg.futureId);
if (requestGroup == null)
{
return;
} }
@Override // If this fails, the group is being removed and completing cancellation is not necessary
public FullDataSourceProviderV2 getFullDataProvider() if (requestGroup.requestRemoveSemaphore.tryAcquire())
{ {
return this.serverside.fullDataFileHandler; // Prevent adding requests in case the group will be removed by this cancellation
requestGroup.requestAddSemaphore.acquireUninterruptibly(Short.MAX_VALUE);
requestGroup.requestRemoveSemaphore.release();
serverPlayerState.getRateLimiterSet(this).generationRequestRateLimiter.release();
FullDataSourceRequestMessage requestMessage = requestGroup.requestMessages.remove(msg.futureId);
if (requestGroup.requestMessages.isEmpty())
{
NETWORK_LOGGER.debug("["+this.serverLevelWrapper.getDimensionName()+"] Cancelled request group ["+DhSectionPos.toString(requestMessage.sectionPos)+"].");
this.requestGroupByPos.remove(requestMessage.sectionPos);
this.serverside.fullDataFileHandler.removeRetrievalRequestIf(pos -> pos == requestMessage.sectionPos);
}
else
{
requestGroup.requestAddSemaphore.release(Short.MAX_VALUE);
}
}
});
}
private void queueLodSyncForRequestMessage(ServerPlayerState serverPlayerState, FullDataSourceRequestMessage message, ServerPlayerState.RateLimiterSet rateLimiterSet)
{
if (!serverPlayerState.sessionConfig.getSynchronizeOnLogin())
{
message.sendResponse(new RequestRejectedException("Operation is disabled in config."));
return;
} }
@Override if (!rateLimiterSet.syncOnLoginRateLimiter.tryAcquire(message))
public AbstractSaveStructure getSaveStructure()
{ {
return this.serverside.saveStructure; return;
} }
// the client timestamp will be null if we want to retrieve the LOD regardless of when it was last updated
long clientTimestamp = (message.clientTimestamp != null) ? message.clientTimestamp : -1;
// the server timestamp will be null if no LOD data exists for this position
Long serverTimestamp = this.serverside.fullDataFileHandler.getTimestampForPos(message.sectionPos);
if (serverTimestamp == null
|| serverTimestamp <= clientTimestamp)
{
// either no data exists to sync, or the client is already up to date
rateLimiterSet.syncOnLoginRateLimiter.release();
message.sendResponse(new FullDataSourceResponseMessage(null));
return;
}
ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor();
if (executor == null)
{
// shouldn't normally happen, but just in case
LOGGER.warn("Unable to send FullDataSourceResponseMessage - getNetworkCompressionExecutor() is null");
return;
}
this.serverside.fullDataFileHandler.getAsync(message.sectionPos).thenAcceptAsync(fullDataSource ->
{
rateLimiterSet.syncOnLoginRateLimiter.release();
FullDataPayload payload = new FullDataPayload(fullDataSource);
payload.splitAndSend(FULL_DATA_SPLIT_SIZE_IN_BYTES, message.getSession()::sendMessage);
message.sendResponse(new FullDataSourceResponseMessage(payload));
}, executor);
}
private void queueWorldGenForRequestMessage(ServerPlayerState serverPlayerState, FullDataSourceRequestMessage message, ServerPlayerState.RateLimiterSet rateLimiterSet)
{
if (!serverPlayerState.sessionConfig.isDistantGenerationEnabled())
{
message.sendResponse(new RequestRejectedException("Operation is disabled in config."));
return;
}
if (!rateLimiterSet.generationRequestRateLimiter.tryAcquire(message))
{
return;
}
while (true)
{
DataSourceRequestGroup requestGroup = this.requestGroupByPos.computeIfAbsent(message.sectionPos, pos ->
{
DataSourceRequestGroup newGroup = new DataSourceRequestGroup();
this.tryFulfillDataSourceRequestGroup(newGroup, pos);
NETWORK_LOGGER.debug("["+this.serverLevelWrapper.getDimensionName()+"] Created request group for pos ["+DhSectionPos.toString(pos)+"].");
return newGroup;
});
// If this fails, loop until either a permit is acquired or the group is removed to create another one
if (!requestGroup.requestAddSemaphore.tryAcquire())
{
Thread.yield();
continue;
}
this.requestGroupByFutureId.put(message.futureId, requestGroup);
requestGroup.requestMessages.put(message.futureId, message);
requestGroup.requestAddSemaphore.release();
break;
}
}
/** May send an error message in response if the message is a {@link AbstractTrackableMessage} */
private <T extends AbstractNetworkMessage> boolean messagePlayerInThisLevel(T message)
{
if (!(message instanceof ILevelRelatedMessage))
{
LodUtil.assertNotReach("Received message ["+ILevelRelatedMessage.class.getSimpleName()+"] does not implement ["+message.getClass().getSimpleName()+"]");
}
// Only handle requests for this level
if (!((ILevelRelatedMessage) message).isSameLevelAs(this.getServerLevelWrapper()))
{
return false;
}
LodUtil.assertTrue(message.getSession().serverPlayer != null);
// Check if the player is in this dimension,
// since handling multiple dimensions isn't allowed
if (message.getSession().serverPlayer.getLevel() != this.getLevelWrapper())
{
// If the message can be replied to - reply with an error, otherwise just ignore
if (message instanceof AbstractTrackableMessage)
{
((AbstractTrackableMessage) message).sendResponse(
new InvalidLevelException(
"Generation not allowed. " +
"Requested dimension: ["+((ILevelRelatedMessage) message).getLevelName()+"], " +
"player dimension: ["+message.getSession().serverPlayer.getLevel().getDimensionName()+"], " +
"handler dimension: ["+this.getLevelWrapper().getDimensionName()+"]"
)
);
}
return false;
}
return true;
}
//===========//
// world gen //
//===========//
@Override @Override
public boolean hasSkyLight() { return this.serverLevelWrapper.hasSkyLight(); } public void onWorldGenTaskComplete(long pos)
{
DataSourceRequestGroup requestGroup = this.requestGroupByPos.get(pos);
if (requestGroup != null)
{
this.tryFulfillDataSourceRequestGroup(requestGroup, pos);
}
}
private void tryFulfillDataSourceRequestGroup(DataSourceRequestGroup requestGroup, long pos) private void tryFulfillDataSourceRequestGroup(DataSourceRequestGroup requestGroup, long pos)
{ {
this.serverside.fullDataFileHandler.getAsync(pos).thenAccept(fullDataSource -> { this.serverside.fullDataFileHandler.getAsync(pos).thenAccept(fullDataSource ->
{
if (this.serverside.fullDataFileHandler.isFullyGenerated(fullDataSource.columnGenerationSteps)) if (this.serverside.fullDataFileHandler.isFullyGenerated(fullDataSource.columnGenerationSteps))
{ {
requestGroup.fullDataSource = fullDataSource; requestGroup.fullDataSource = fullDataSource;
@@ -446,15 +404,84 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
}); });
} }
//=================//
// player handling //
//=================//
public void addPlayer(IServerPlayerWrapper serverPlayer) { this.worldGenPlayerCenteringQueue.add(serverPlayer); }
public void removePlayer(IServerPlayerWrapper serverPlayer) { this.worldGenPlayerCenteringQueue.remove(serverPlayer); }
@Override @Override
public void onWorldGenTaskComplete(long pos) public CompletableFuture<Void> updateDataSourcesAsync(FullDataSourceV2 data)
{ {
DataSourceRequestGroup requestGroup = this.requestGroupsByPos.get(pos); if (!Config.Client.Advanced.Multiplayer.ServerNetworking.enableRealTimeUpdates.get())
if (requestGroup != null)
{ {
this.tryFulfillDataSourceRequestGroup(requestGroup, pos); return this.getFullDataProvider().updateDataSourceAsync(data);
}
ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor();
if (executor == null)
{
LOGGER.warn("Unable to send FullDataPartialUpdateMessage - getNetworkCompressionExecutor() is null");
return this.getFullDataProvider().updateDataSourceAsync(data);
}
CompletableFuture.runAsync(() ->
{
FullDataPayload payload = new FullDataPayload(data);
for (ServerPlayerState serverPlayerState : this.remotePlayerConnectionHandler.getConnectedPlayers())
{
if (serverPlayerState.getServerPlayer().getLevel() != this.serverLevelWrapper)
{
continue;
}
if (!serverPlayerState.sessionConfig.isRealTimeUpdatesEnabled())
{
continue;
}
Vec3d playerPosition = serverPlayerState.getServerPlayer().getPosition();
int distanceFromPlayer = DhSectionPos.getManhattanBlockDistance(data.getPos(), new DhBlockPos2D((int) playerPosition.x, (int) playerPosition.z)) / 16;
if (distanceFromPlayer >= serverPlayerState.getServerPlayer().getViewDistance()
&& distanceFromPlayer <= serverPlayerState.sessionConfig.getRenderDistanceRadius())
{
payload.splitAndSend(FULL_DATA_SPLIT_SIZE_IN_BYTES, serverPlayerState.networkSession::sendMessage);
serverPlayerState.networkSession.sendMessage(new FullDataPartialUpdateMessage(this.serverLevelWrapper, payload));
} }
} }
}, executor);
return this.getFullDataProvider().updateDataSourceAsync(data);
}
//=========//
// getters //
//=========//
@Override
public int getMinY() { return this.getLevelWrapper().getMinHeight(); }
@Override
public IServerLevelWrapper getServerLevelWrapper() { return this.serverLevelWrapper; }
@Override
public ILevelWrapper getLevelWrapper() { return this.getServerLevelWrapper(); }
@Override
public FullDataSourceProviderV2 getFullDataProvider() { return this.serverside.fullDataFileHandler; }
@Override
public AbstractSaveStructure getSaveStructure() { return this.serverside.saveStructure; }
@Override
public boolean hasSkyLight() { return this.serverLevelWrapper.hasSkyLight(); }
@Override @Override
public GenericObjectRenderer getGenericRenderer() public GenericObjectRenderer getGenericRenderer()
@@ -470,6 +497,7 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
} }
//===========// //===========//
// debugging // // debugging //
//===========// //===========//
@@ -481,6 +509,26 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
messageList.add("["+dimName+"]"); messageList.add("["+dimName+"]");
} }
//==========//
// shutdown //
//==========//
@Override
public void close()
{
super.close();
this.serverside.close();
LOGGER.info("Closed DHLevel for ["+this.getLevelWrapper()+"].");
}
//================//
// helper classes //
//================//
private static class DataSourceRequestGroup private static class DataSourceRequestGroup
{ {
public final ConcurrentMap<Long, FullDataSourceRequestMessage> requestMessages = new ConcurrentHashMap<>(); public final ConcurrentMap<Long, FullDataSourceRequestMessage> requestMessages = new ConcurrentHashMap<>();
@@ -488,10 +536,17 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
@CheckForNull @CheckForNull
public FullDataSourceV2 fullDataSource; public FullDataSourceV2 fullDataSource;
// Maybe there's a better way to do synchronization, but this should suffice /**
// Why not something like ReentrantReadWriteLock: locks should not be bound to threads * These semaphores prevent a given thread from accidentally locking on the same group
* multiple times, as the semaphore is tied to the given thread. <br>
* Reentrant Lock isn't used since it would allow the thread to lock on the same group. <br>
* the Short.MAX_VALUE is just a very large number that should be larger than the number of
* threads we'll have.
*/
public final Semaphore requestAddSemaphore = new Semaphore(Short.MAX_VALUE, true); public final Semaphore requestAddSemaphore = new Semaphore(Short.MAX_VALUE, true);
/** @see DataSourceRequestGroup#requestAddSemaphore */
public final Semaphore requestRemoveSemaphore = new Semaphore(Short.MAX_VALUE, true); public final Semaphore requestRemoveSemaphore = new Semaphore(Short.MAX_VALUE, true);
} }
} }
@@ -26,7 +26,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrap
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
public interface IDhClientLevel extends IDhWorldGenLevel public interface IDhClientLevel extends IDhLevel
{ {
void clientTick(); void clientTick();
@@ -21,6 +21,7 @@ package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider;
import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.RenderBufferHandler;
@@ -33,8 +34,10 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
public interface IDhLevel extends AutoCloseable public interface IDhLevel extends AutoCloseable, GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener
{ {
void worldGenTick();
int getMinY(); int getMinY();
/** /**
@@ -21,9 +21,10 @@ package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
public interface IDhServerLevel extends IDhWorldGenLevel public interface IDhServerLevel extends IDhLevel
{ {
void serverTick(); void serverTick();
IServerLevelWrapper getServerLevelWrapper(); IServerLevelWrapper getServerLevelWrapper();
} }
@@ -31,9 +31,8 @@ public interface IKeyedClientLevelManager extends IBindable
IServerKeyedClientLevel getServerKeyedLevel(); IServerKeyedClientLevel getServerKeyedLevel();
/** Called when a client level is wrapped by a ServerEnhancedClientLevel, for integration into mod internals. */ /** Called when a client level is wrapped by a ServerEnhancedClientLevel, for integration into mod internals. */
IServerKeyedClientLevel setServerKeyedLevel(IClientLevelWrapper clientLevel, String levelKey); IServerKeyedClientLevel setServerKeyedLevel(IClientLevelWrapper clientLevel, String levelKey);
void clearServerKeyedLevel();
boolean isEnabled(); void clearKeyedLevel();
void disable(); boolean hasLevelSet();
} }
@@ -31,6 +31,10 @@ import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
/**
* Handles both single-player/server-side world gen and client side LOD requests.
* TODO rename
*/
public class WorldGenModule implements Closeable public class WorldGenModule implements Closeable
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
@@ -138,6 +142,7 @@ public class WorldGenModule implements Closeable
public boolean isWorldGenRunning() { return this.worldGenStateRef.get() != null; } public boolean isWorldGenRunning() { return this.worldGenStateRef.get() != null; }
/** mutates a list so it can be added to an existing {@link IDhLevel}'s debug list */
public void addDebugMenuStringsToList(List<String> messageList) public void addDebugMenuStringsToList(List<String> messageList)
{ {
AbstractWorldGenState worldGenState = this.worldGenStateRef.get(); AbstractWorldGenState worldGenState = this.worldGenStateRef.get();
@@ -168,16 +173,16 @@ public class WorldGenModule implements Closeable
CompletableFuture<Void> closeAsync(boolean doInterrupt) CompletableFuture<Void> closeAsync(boolean doInterrupt)
{ {
return this.worldGenerationQueue.startClosing(true, doInterrupt) return this.worldGenerationQueue.startClosingAsync(true, doInterrupt)
.exceptionally(ex -> .exceptionally(e ->
{ {
LOGGER.error("Error closing generation queue", ex); LOGGER.error("Error during first stage of generation queue shutdown, Error: ["+e.getMessage()+"].", e);
return null; return null;
} }
).thenRun(this.worldGenerationQueue::close) ).thenRun(this.worldGenerationQueue::close)
.exceptionally(ex -> .exceptionally(e ->
{ {
LOGGER.error("Error closing world gen", ex); LOGGER.error("Error during second stage of generation queue shutdown, Error: ["+e.getMessage()+"].", e);
return null; return null;
}); });
} }
@@ -101,6 +101,7 @@ public class ConfigBasedLogger
else else
logger.log(logLevel, msgStr); logger.log(logLevel, msgStr);
} }
if (MC != null && mode.levelForChat.isLessSpecificThan(level)) if (MC != null && mode.levelForChat.isLessSpecificThan(level))
{ {
if (param.length > 0 && param[param.length - 1] instanceof Throwable) if (param.length > 0 && param[param.length - 1] instanceof Throwable)
@@ -27,6 +27,7 @@ import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRend
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.world.AbstractDhWorld; import com.seibel.distanthorizons.core.world.AbstractDhWorld;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.coreapi.util.StringUtil;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -49,7 +50,8 @@ public class F3Screen
/** /**
* F3 menu example: <br> * F3 menu example: <br>
<code> <code>
Distant Horizons v: 2.1.1-a-dev <br><br> Distant Horizons v: 2.1.1-a-dev <br>
Build: 7e163ce6 (main) <br><br>
Queued chunk updates: 0 / 1000 <br> Queued chunk updates: 0 / 1000 <br>
World Gen Tasks: 40/5304, (in progress: 7) <br><br> World Gen Tasks: 40/5304, (in progress: 7) <br><br>
@@ -85,8 +87,7 @@ public class F3Screen
messageList.add(ModInfo.READABLE_NAME+": "+ModInfo.VERSION); messageList.add(ModInfo.READABLE_NAME+": "+ModInfo.VERSION);
if (ModInfo.IS_DEV_BUILD) if (ModInfo.IS_DEV_BUILD)
{ {
String shortCommitHash = ModJarInfo.Git_Commit.length() >= 8 ? ModJarInfo.Git_Commit.substring(0, 8) : ModJarInfo.Git_Commit; messageList.add("Build: " + StringUtil.shortenString(ModJarInfo.Git_Commit, 8) + " (" + ModJarInfo.Git_Branch + ")");
messageList.add("Build: " + shortCommitHash + " (" + ModJarInfo.Git_Branch + ")");
} }
messageList.add(""); messageList.add("");
// thread pools // thread pools
@@ -5,6 +5,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.level.DhServerLevel;
import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.logging.ConfigBasedSpamLogger; import com.seibel.distanthorizons.core.logging.ConfigBasedSpamLogger;
import com.seibel.distanthorizons.core.network.exceptions.InvalidLevelException; import com.seibel.distanthorizons.core.network.exceptions.InvalidLevelException;
@@ -38,7 +39,7 @@ import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; import java.util.function.Consumer;
public abstract class AbstractFullDataRequestQueue implements IDebugRenderable, AutoCloseable public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRenderable, AutoCloseable
{ {
private static final ConfigBasedSpamLogger LOGGER = new ConfigBasedSpamLogger(LogManager.getLogger(), private static final ConfigBasedSpamLogger LOGGER = new ConfigBasedSpamLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Logging.logNetworkEvent.get(), 3); () -> Config.Client.Advanced.Logging.logNetworkEvent.get(), 3);
@@ -51,13 +52,22 @@ public abstract class AbstractFullDataRequestQueue implements IDebugRenderable,
protected static final long SHUTDOWN_TIMEOUT_SECONDS = 5; protected static final long SHUTDOWN_TIMEOUT_SECONDS = 5;
public final ClientNetworkState networkState; public final ClientNetworkState networkState;
protected final IDhClientLevel level; protected final IDhClientLevel level;
private final boolean changedOnly; private final boolean changedOnly;
private volatile CompletableFuture<Void> closingFuture = null; private volatile CompletableFuture<Void> closingFuture = null;
protected final ConcurrentMap<Long, RequestQueueEntry> waitingTasks = new ConcurrentHashMap<>(); protected final ConcurrentMap<Long, RequestQueueEntry> waitingTasksBySectionPos = new ConcurrentHashMap<>();
/**
* 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>
* Reentrant Lock isn't used since it would allow the thread to lock on the same group. <br>
* the Short.MAX_VALUE is just a very large number that should be larger than the number of
* threads we'll have.
*/
private final Semaphore pendingTasksSemaphore = new Semaphore(Short.MAX_VALUE, true); private final Semaphore pendingTasksSemaphore = new Semaphore(Short.MAX_VALUE, true);
private final AtomicInteger finishedRequests = new AtomicInteger(); private final AtomicInteger finishedRequests = new AtomicInteger();
@@ -67,12 +77,14 @@ public abstract class AbstractFullDataRequestQueue implements IDebugRenderable,
private final SupplierBasedRateLimiter<Void> rateLimiter = new SupplierBasedRateLimiter<>(this::getRequestRateLimit); private final SupplierBasedRateLimiter<Void> rateLimiter = new SupplierBasedRateLimiter<>(this::getRequestRateLimit);
protected abstract int getRequestRateLimit();
protected abstract String getQueueName(); //=============//
// constructor //
//=============//
public AbstractFullDataNetworkRequestQueue(
public AbstractFullDataRequestQueue(ClientNetworkState networkState, IDhClientLevel level, boolean changedOnly, ConfigEntry<Boolean> showDebugWireframeConfig) ClientNetworkState networkState, IDhClientLevel level,
boolean changedOnly, ConfigEntry<Boolean> showDebugWireframeConfig)
{ {
this.networkState = networkState; this.networkState = networkState;
this.level = level; this.level = level;
@@ -81,32 +93,42 @@ public abstract class AbstractFullDataRequestQueue implements IDebugRenderable,
DebugRenderer.register(this, this.showDebugWireframeConfig); DebugRenderer.register(this, this.showDebugWireframeConfig);
} }
public CompletableFuture<Boolean> submitRequest(long sectionPos, Consumer<FullDataSourceV2> chunkDataConsumer)
{
return this.submitRequest(sectionPos, null, chunkDataConsumer); //==================//
} // abstract methods //
public CompletableFuture<Boolean> submitRequest(long sectionPos, @Nullable Long clientTimestamp, Consumer<FullDataSourceV2> chunkDataConsumer) //==================//
protected abstract int getRequestRateLimit();
protected abstract String getQueueName();
//====================//
// request submitting //
//====================//
public CompletableFuture<Boolean> submitRequest(long sectionPos, Consumer<FullDataSourceV2> dataSourceConsumer)
{ return this.submitRequest(sectionPos, null, dataSourceConsumer); }
public CompletableFuture<Boolean> submitRequest(long sectionPos, @Nullable Long clientTimestamp, Consumer<FullDataSourceV2> dataSourceConsumer)
{ {
LodUtil.assertTrue(DhSectionPos.getDetailLevel(sectionPos) == DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL, "Only highest-detail sections are allowed."); LodUtil.assertTrue(DhSectionPos.getDetailLevel(sectionPos) == DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL, "Only highest-detail sections are allowed.");
RequestQueueEntry entry = new RequestQueueEntry(chunkDataConsumer, clientTimestamp); RequestQueueEntry entry = new RequestQueueEntry(dataSourceConsumer, clientTimestamp);
entry.future.whenComplete((result, throwable) -> entry.future.whenComplete((success, throwable) ->
{ {
this.waitingTasks.remove(sectionPos); this.waitingTasksBySectionPos.remove(sectionPos);
this.finishedRequests.incrementAndGet(); this.finishedRequests.incrementAndGet();
if (!result || throwable != null) if (!success || throwable != null)
{ {
this.failedRequests.incrementAndGet(); this.failedRequests.incrementAndGet();
} }
}); });
this.waitingTasks.put(sectionPos, entry);
return entry.future;
}
protected int posDistanceSquared(DhBlockPos2D targetPos, long pos) this.waitingTasksBySectionPos.put(sectionPos, entry);
{ return entry.future;
return (int) DhSectionPos.getCenterBlockPos(pos).distSquared(targetPos);
} }
public synchronized boolean tick(DhBlockPos2D targetPos) public synchronized boolean tick(DhBlockPos2D targetPos)
@@ -116,7 +138,8 @@ public abstract class AbstractFullDataRequestQueue implements IDebugRenderable,
return false; return false;
} }
while (this.getWaitingTaskCount() > this.getInProgressTaskCount() // queue requests until the queue is full
while (this.getInProgressTaskCount() < this.getWaitingTaskCount()
&& this.getInProgressTaskCount() < this.getRequestRateLimit() && this.getInProgressTaskCount() < this.getRequestRateLimit()
&& this.pendingTasksSemaphore.tryAcquire()) && this.pendingTasksSemaphore.tryAcquire())
{ {
@@ -126,37 +149,16 @@ public abstract class AbstractFullDataRequestQueue implements IDebugRenderable,
break; break;
} }
this.sendNewRequest(targetPos); this.sendNextRequest(targetPos);
} }
return true; return true;
} }
private void sendNextRequest(DhBlockPos2D targetPos)
public void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf)
{ {
for (Map.Entry<Long, RequestQueueEntry> mapEntry : this.waitingTasks.entrySet()) Map.Entry<Long, RequestQueueEntry> mapEntry = this.waitingTasksBySectionPos.entrySet().stream()
{ .filter(task -> task.getValue().networkDataSourceFuture == null)
long pos = mapEntry.getKey(); .min((x, y) -> posDistanceSquared(targetPos, x.getKey()) - posDistanceSquared(targetPos, y.getKey()))
RequestQueueEntry entry = mapEntry.getValue();
if (removeIf.accept(pos))
{
LOGGER.debug("Removing request " + mapEntry.getKey() + "...");
entry.future.cancel(false);
if (entry.request != null)
{
entry.request.cancel(false);
}
}
}
}
private void sendNewRequest(DhBlockPos2D targetPos)
{
Map.Entry<Long, RequestQueueEntry> mapEntry = this.waitingTasks.entrySet().stream()
.filter(task -> task.getValue().request == null)
.min((x, y) -> this.posDistanceSquared(targetPos, x.getKey()) - this.posDistanceSquared(targetPos, y.getKey()))
.orElse(null); .orElse(null);
if (mapEntry == null) if (mapEntry == null)
@@ -168,12 +170,12 @@ public abstract class AbstractFullDataRequestQueue implements IDebugRenderable,
long sectionPos = mapEntry.getKey(); long sectionPos = mapEntry.getKey();
RequestQueueEntry entry = mapEntry.getValue(); RequestQueueEntry entry = mapEntry.getValue();
CompletableFuture<FullDataSourceResponseMessage> request = this.networkState.getSession().sendRequest( CompletableFuture<FullDataSourceResponseMessage> dataSourceFuture = this.networkState.getSession().sendRequest(
new FullDataSourceRequestMessage(this.level.getLevelWrapper(), sectionPos, entry.updateTimestamp), new FullDataSourceRequestMessage(this.level.getLevelWrapper(), sectionPos, entry.updateTimestamp),
FullDataSourceResponseMessage.class FullDataSourceResponseMessage.class
); );
entry.request = request; entry.networkDataSourceFuture = dataSourceFuture;
request.handle((response, throwable) -> dataSourceFuture.handle((response, throwable) ->
{ {
this.pendingTasksSemaphore.release(); this.pendingTasksSemaphore.release();
@@ -199,7 +201,7 @@ public abstract class AbstractFullDataRequestQueue implements IDebugRenderable,
try try
{ {
FullDataSourceV2 fullDataSource = dataSourceDto.createPooledDataSource(this.level.getLevelWrapper()); FullDataSourceV2 fullDataSource = dataSourceDto.createPooledDataSource(this.level.getLevelWrapper());
entry.chunkDataConsumer.accept(fullDataSource); entry.dataSourceConsumer.accept(fullDataSource);
FullDataSourceV2.DATA_SOURCE_POOL.returnPooledDataSource(fullDataSource); FullDataSourceV2.DATA_SOURCE_POOL.returnPooledDataSource(fullDataSource);
} }
catch (IOException | DataCorruptedException | InterruptedException e) catch (IOException | DataCorruptedException | InterruptedException e)
@@ -225,11 +227,12 @@ public abstract class AbstractFullDataRequestQueue implements IDebugRenderable,
} }
catch (RateLimitedException e) catch (RateLimitedException e)
{ {
LOGGER.warn("Rate limited by server, re-queueing task [" + sectionPos + "]: " + e.getMessage()); LOGGER.warn("Rate limited by server, re-queueing task [" + DhSectionPos.toString(sectionPos) + "]: " + e.getMessage());
// Skip 1 second // Skip all requests for 1 second
this.rateLimiter.acquireOrDrain(Integer.MAX_VALUE); this.rateLimiter.acquireAll();
entry.request = null;
entry.networkDataSourceFuture = null;
return null; return null;
} }
catch (Throwable e) catch (Throwable e)
@@ -240,7 +243,7 @@ public abstract class AbstractFullDataRequestQueue implements IDebugRenderable,
// Retry logic // Retry logic
if (entry.retryAttempts > 0) if (entry.retryAttempts > 0)
{ {
entry.request = null; entry.networkDataSourceFuture = null;
return null; return null;
} }
else else
@@ -263,29 +266,60 @@ public abstract class AbstractFullDataRequestQueue implements IDebugRenderable,
}); });
} }
//=========================================//
// IFullDataSourceRetrievalQueue overrides //
//=========================================//
public void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf)
{
for (Map.Entry<Long, RequestQueueEntry> mapEntry : this.waitingTasksBySectionPos.entrySet())
{
long pos = mapEntry.getKey();
RequestQueueEntry entry = mapEntry.getValue();
if (removeIf.accept(pos))
{
LOGGER.debug("Removing request " + mapEntry.getKey() + "...");
entry.future.cancel(false);
if (entry.networkDataSourceFuture != null)
{
entry.networkDataSourceFuture.cancel(false);
}
}
}
}
public void addDebugMenuStringsToList(List<String> messageList) public void addDebugMenuStringsToList(List<String> messageList)
{ {
messageList.add(this.getQueueName() + " [" + this.level.getClientLevelWrapper().getDimensionName() + "]"); messageList.add(this.getQueueName() + " [" + this.level.getClientLevelWrapper().getDimensionName() + "]");
messageList.add("Requests: " + this.finishedRequests + " / " + (this.getWaitingTaskCount() + this.finishedRequests.get()) + " (failed: " + this.failedRequests + ", rate limit: " + this.getRequestRateLimit() + ")"); messageList.add("Requests: " + this.finishedRequests + " / " + (this.getWaitingTaskCount() + this.finishedRequests.get()) + " (failed: " + this.failedRequests + ", rate limit: " + this.getRequestRateLimit() + ")");
} }
public int getWaitingTaskCount() { return this.waitingTasksBySectionPos.size(); }
public int getWaitingTaskCount() { return this.waitingTasks.size(); }
public int getInProgressTaskCount() { return Short.MAX_VALUE - this.pendingTasksSemaphore.availablePermits(); } public int getInProgressTaskCount() { return Short.MAX_VALUE - this.pendingTasksSemaphore.availablePermits(); }
public CompletableFuture<Void> startClosing(boolean alsoInterruptRunning)
//==========//
// shutdown //
//==========//
public CompletableFuture<Void> startClosingAsync(boolean alsoInterruptRunning)
{ {
return this.closingFuture = CompletableFuture.runAsync(() -> { return this.closingFuture = CompletableFuture.runAsync(() -> {
Stopwatch stopwatch = Stopwatch.createStarted(); Stopwatch stopwatch = Stopwatch.createStarted();
do do
{ {
for (RequestQueueEntry entry : this.waitingTasks.values()) for (RequestQueueEntry entry : this.waitingTasksBySectionPos.values())
{ {
entry.future.cancel(alsoInterruptRunning); entry.future.cancel(alsoInterruptRunning);
if (entry.request != null && entry.request.cancel(alsoInterruptRunning)) if (entry.networkDataSourceFuture != null && entry.networkDataSourceFuture.cancel(alsoInterruptRunning))
{ {
this.pendingTasksSemaphore.release(); this.pendingTasksSemaphore.release();
} }
@@ -295,7 +329,7 @@ public abstract class AbstractFullDataRequestQueue implements IDebugRenderable,
if (stopwatch.elapsed(TimeUnit.SECONDS) >= SHUTDOWN_TIMEOUT_SECONDS) if (stopwatch.elapsed(TimeUnit.SECONDS) >= SHUTDOWN_TIMEOUT_SECONDS)
{ {
LOGGER.warn(this.getQueueName() + " for " + this.level.getLevelWrapper() + " did not shutdown in " + SHUTDOWN_TIMEOUT_SECONDS + " seconds! Some unfinished tasks might be left hanging."); LOGGER.warn("The request queue [" + this.getQueueName() + "] for level [" + this.level.getLevelWrapper() + "] did not shutdown in [" + SHUTDOWN_TIMEOUT_SECONDS + "] seconds. Some unfinished tasks might be left hanging.");
} }
}); });
} }
@@ -306,41 +340,76 @@ public abstract class AbstractFullDataRequestQueue implements IDebugRenderable,
DebugRenderer.unregister(this, this.showDebugWireframeConfig); DebugRenderer.unregister(this, this.showDebugWireframeConfig);
} }
//===========//
// debugging //
//===========//
@Override @Override
public void debugRender(DebugRenderer r) public void debugRender(DebugRenderer renderer)
{ {
if (MC_CLIENT.getWrappedClientLevel() != this.level.getClientLevelWrapper()) if (MC_CLIENT.getWrappedClientLevel() != this.level.getClientLevelWrapper())
{ {
return; return;
} }
for (Map.Entry<Long, RequestQueueEntry> mapEntry : this.waitingTasks.entrySet()) for (Map.Entry<Long, RequestQueueEntry> mapEntry : this.waitingTasksBySectionPos.entrySet())
{ {
r.renderBox(new DebugRenderer.Box(mapEntry.getKey(), -32f, 64f, 0.05f, renderer.renderBox(new DebugRenderer.Box(mapEntry.getKey(), -32f, 64f, 0.05f,
mapEntry.getValue().request != null ? Color.red : Color.gray mapEntry.getValue().networkDataSourceFuture != null ? Color.red : Color.gray
)); ));
} }
} }
//================//
// helper methods //
//================//
protected static int posDistanceSquared(DhBlockPos2D targetPos, long pos)
{ return (int) DhSectionPos.getCenterBlockPos(pos).distSquared(targetPos); }
//================//
// helper classes //
//================//
protected static class RequestQueueEntry protected static class RequestQueueEntry
{ {
/** encapsulates the entire request, including client side queuing and the actual server request */
public final CompletableFuture<Boolean> future = new CompletableFuture<>(); public final CompletableFuture<Boolean> future = new CompletableFuture<>();
public final Consumer<FullDataSourceV2> chunkDataConsumer; public final Consumer<FullDataSourceV2> dataSourceConsumer;
/** 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;
/** Will be null until the request has been sent to the server */
@CheckForNull @CheckForNull
public CompletableFuture<?> request; public CompletableFuture<FullDataSourceResponseMessage> networkDataSourceFuture;
/** when this reaches zero then the request will be canceled. */
public int retryAttempts = MAX_RETRY_ATTEMPTS; public int retryAttempts = MAX_RETRY_ATTEMPTS;
//=============//
// constructor //
//=============//
public RequestQueueEntry( public RequestQueueEntry(
Consumer<FullDataSourceV2> chunkDataConsumer, Consumer<FullDataSourceV2> dataSourceConsumer,
@Nullable Long updateTimestamp) @Nullable Long updateTimestamp)
{ {
this.chunkDataConsumer = chunkDataConsumer; this.dataSourceConsumer = dataSourceConsumer;
this.updateTimestamp = updateTimestamp; this.updateTimestamp = updateTimestamp;
} }
} }
} }
@@ -6,15 +6,16 @@ import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig; import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig;
import com.seibel.distanthorizons.core.network.INetworkObject; import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.network.event.ScopedNetworkEventSource; import com.seibel.distanthorizons.core.network.event.ScopedNetworkEventSource;
import com.seibel.distanthorizons.core.network.event.internal.CloseEvent; import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent;
import com.seibel.distanthorizons.core.network.event.internal.IncompatibleMessageEvent; import com.seibel.distanthorizons.core.network.event.internal.IncompatibleMessageInternalEvent;
import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage; import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage;
import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage; import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataChunkMessage; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPayload; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPayload;
import com.seibel.distanthorizons.core.network.session.Session; import com.seibel.distanthorizons.core.network.session.NetworkSession;
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.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.CompositeByteBuf;
@@ -23,7 +24,6 @@ import org.jetbrains.annotations.Nullable;
import java.io.Closeable; import java.io.Closeable;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -32,100 +32,114 @@ public class ClientNetworkState implements Closeable
protected static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(), protected static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Logging.logNetworkEvent.get()); () -> Config.Client.Advanced.Logging.logNetworkEvent.get());
private final Session session = new Session(null);
private final ConcurrentMap<Integer, CompositeByteBuf> fullDataBufferById = CacheBuilder.newBuilder()
.expireAfterAccess(10, TimeUnit.SECONDS)
.<Integer, CompositeByteBuf>build()
.asMap();
private final SessionConfig.AnyChangeListener configAnyChangeListener = new SessionConfig.AnyChangeListener(this::sendConfigMessage);
private final NetworkSession networkSession = new NetworkSession(null);
/** /**
* Returns the client used by this instance. <p> * Returns the client used by this instance. <p>
* If you need to subscribe to any packet events, create an instance of {@link ScopedNetworkEventSource} using the returned instance. * If you need to subscribe to any packet events, create an instance of {@link ScopedNetworkEventSource} using the returned instance.
*/ */
public Session getSession() { return this.session; } public NetworkSession getSession() { return this.networkSession; }
public SessionConfig sessionConfig = new SessionConfig();
public SessionConfig config = new SessionConfig();
private volatile boolean configReceived = false; private volatile boolean configReceived = false;
private final SessionConfig.ChangeListener configChangeListener = new SessionConfig.ChangeListener(this::sendConfigMessage);
public boolean isReady() { return this.configReceived; } public boolean isReady() { return this.configReceived; }
private EServerSupportStatus serverSupportStatus = EServerSupportStatus.NONE; private EServerSupportStatus serverSupportStatus = EServerSupportStatus.NONE;
/** Protocol version closest to supported by this mod version */ /** Protocol version closest to supported by this mod version */
@Nullable @Nullable
private Integer closestProtocolVersion; private Integer closestProtocolVersion;
private final ConcurrentMap<Integer, CompositeByteBuf> fullDataBuffers = CacheBuilder.newBuilder()
.expireAfterAccess(10, TimeUnit.SECONDS)
.<Integer, CompositeByteBuf>build()
.asMap();
//=============//
// constructor //
//=============//
/**
* Constructs a new instance.
*/
public ClientNetworkState() public ClientNetworkState()
{ {
this.session.registerHandler(IncompatibleMessageEvent.class, event -> this.networkSession.registerHandler(IncompatibleMessageInternalEvent.class, event ->
{ {
if (this.closestProtocolVersion == null || Math.abs(event.protocolVersion - ModInfo.PROTOCOL_VERSION) < this.closestProtocolVersion) if (this.closestProtocolVersion == null
|| Math.abs(event.protocolVersion - ModInfo.PROTOCOL_VERSION) < this.closestProtocolVersion)
{ {
this.closestProtocolVersion = event.protocolVersion; this.closestProtocolVersion = event.protocolVersion;
} }
}); });
this.session.registerHandler(CurrentLevelKeyMessage.class, msg -> this.networkSession.registerHandler(CurrentLevelKeyMessage.class, message ->
{ {
// we will also receive this message when we have full support
if (this.serverSupportStatus == EServerSupportStatus.NONE) if (this.serverSupportStatus == EServerSupportStatus.NONE)
{ {
this.serverSupportStatus = EServerSupportStatus.LEVELS_ONLY; this.serverSupportStatus = EServerSupportStatus.LEVELS_ONLY;
} }
}); });
this.session.registerHandler(SessionConfigMessage.class, msg -> this.networkSession.registerHandler(SessionConfigMessage.class, message ->
{ {
this.serverSupportStatus = EServerSupportStatus.FULL; this.serverSupportStatus = EServerSupportStatus.FULL;
LOGGER.info("Connection config has been changed: {}", msg.config); LOGGER.info("Connection config has been changed: ["+message.config+"].");
this.config = msg.config; this.sessionConfig = message.config;
this.configReceived = true; this.configReceived = true;
}); });
this.session.registerHandler(CloseEvent.class, msg -> this.networkSession.registerHandler(CloseInternalEvent.class, message ->
{ {
this.configReceived = false; this.configReceived = false;
}); });
this.session.registerHandler(FullDataChunkMessage.class, msg -> this.networkSession.registerHandler(FullDataSplitMessage.class, message ->
{ {
if (msg.isFirst) if (message.isFirst)
{ {
CompositeByteBuf composite = this.fullDataBuffers.remove(msg.bufferId); CompositeByteBuf composite = this.fullDataBufferById.remove(message.bufferId);
if (composite != null) if (composite != null)
{ {
composite.release(); composite.release();
LOGGER.debug("Released full data buffer {}: {}", msg.bufferId, composite); LOGGER.debug("Released full data buffer ["+message.bufferId+"]: ["+composite+"]");
} }
} }
CompositeByteBuf composite = this.fullDataBuffers.computeIfAbsent(msg.bufferId, bufferId -> ByteBufAllocator.DEFAULT.compositeBuffer()); CompositeByteBuf byteBuffer = this.fullDataBufferById.computeIfAbsent(message.bufferId, bufferId -> ByteBufAllocator.DEFAULT.compositeBuffer());
composite.addComponent(true, msg.buffer); byteBuffer.addComponent(true, message.buffer);
LOGGER.debug("Full data buffer {}: {}", msg.bufferId, composite); LOGGER.debug("Full data buffer ["+message.bufferId+"]: ["+byteBuffer+"].");
}); });
this.session.registerHandler(FullDataPartialUpdateMessage.class, msg -> this.networkSession.registerHandler(FullDataPartialUpdateMessage.class, msg ->
{ {
// Dummy handler to prevent unhandled message warnings // Dummy handler to prevent unhandled message warnings
}); });
} }
//==============//
// send message //
//==============//
public FullDataSourceV2DTO decodeDataSourceAndReleaseBuffer(FullDataPayload msg) public FullDataSourceV2DTO decodeDataSourceAndReleaseBuffer(FullDataPayload msg)
{ {
CompositeByteBuf composite = this.fullDataBuffers.remove(msg.dtoBufferId); CompositeByteBuf compositeByteBuffer = this.fullDataBufferById.remove(msg.dtoBufferId);
Objects.requireNonNull(composite); LodUtil.assertTrue(compositeByteBuffer != null);
try try
{ {
return INetworkObject.decodeToInstance(new FullDataSourceV2DTO(), composite); return INetworkObject.decodeToInstance(FullDataSourceV2DTO.CreateEmptyDataSource(), compositeByteBuffer);
} }
finally finally
{ {
composite.release(); compositeByteBuffer.release();
} }
} }
@@ -135,30 +149,53 @@ public class ClientNetworkState implements Closeable
this.getSession().sendMessage(new SessionConfigMessage(new SessionConfig())); this.getSession().sendMessage(new SessionConfigMessage(new SessionConfig()));
} }
//===========//
// debugging //
//===========//
public void addDebugMenuStringsToList(List<String> messageList) public void addDebugMenuStringsToList(List<String> messageList)
{ {
if (this.session.isClosed()) if (this.networkSession.isClosed())
{ {
messageList.add("Session closed: " + this.session.getCloseReason().getMessage()); messageList.add("NetworkSession closed: " + this.networkSession.getCloseReason().getMessage());
return; return;
} }
if (this.serverSupportStatus == EServerSupportStatus.NONE && this.closestProtocolVersion != null) if (this.serverSupportStatus == EServerSupportStatus.NONE && this.closestProtocolVersion != null)
{ {
messageList.add("Incompatible protocol version: " + this.closestProtocolVersion + ", required: " + ModInfo.PROTOCOL_VERSION); messageList.add("Incompatible protocol version: [" + this.closestProtocolVersion + "], required: [" + ModInfo.PROTOCOL_VERSION+ "]");
return; return;
} }
messageList.add(this.serverSupportStatus.message); messageList.add(this.serverSupportStatus.message);
} }
//==========//
// shutdown //
//==========//
@Override @Override
public void close() public void close()
{ {
this.configChangeListener.close(); this.configAnyChangeListener.close();
this.session.close(); this.networkSession.close();
} }
//================//
// helper classes //
//================//
/**
* NONE, <br>
* LEVELS_ONLY, <br>
* FULL, <br>
*/
private enum EServerSupportStatus private enum EServerSupportStatus
{ {
NONE("Server does not support DH"), NONE("Server does not support DH"),
@@ -167,9 +204,7 @@ public class ClientNetworkState implements Closeable
public final String message; public final String message;
EServerSupportStatus(String message) EServerSupportStatus(String message) { this.message = message; }
{
this.message = message;
}
} }
} }
@@ -1,30 +1,57 @@
package com.seibel.distanthorizons.core.multiplayer.client; 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.level.IDhClientLevel; import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
public class SyncOnLoginRequestQueue extends AbstractFullDataRequestQueue /**
* This queue only handles LOD updates for
* LODs that were changed when the player wasn't online
* and the player already loaded the LODs once.
* {@link RemoteWorldRetrievalQueue} is used for all other requests.
*
* @see Config.Client.Advanced.Multiplayer.ServerNetworking#synchronizeOnLogin
* @see RemoteWorldRetrievalQueue
*/
public class SyncOnLoginRequestQueue extends AbstractFullDataNetworkRequestQueue
{ {
//=============//
// constructor //
//=============//
public SyncOnLoginRequestQueue(IDhClientLevel level, ClientNetworkState networkState) public SyncOnLoginRequestQueue(IDhClientLevel level, ClientNetworkState networkState)
{ { super(networkState, level, true, Config.Client.Advanced.Debugging.DebugWireframe.showWorldGenQueue); }
super(networkState, level, true, Config.Client.Advanced.Debugging.DebugWireframe.showWorldGenQueue);
}
//=========//
// getters //
//=========//
@Override @Override
protected int getRequestRateLimit() { return this.networkState.config.getSyncOnLoginRateLimit(); } protected int getRequestRateLimit() { return this.networkState.sessionConfig.getSyncOnLoginRateLimit(); }
@Override @Override
protected String getQueueName() { return "Sync On Login Queue"; } protected String getQueueName() { return "Sync On Login Queue"; }
//==================//
// request handling //
//==================//
@Override @Override
public boolean tick(DhBlockPos2D targetPos) public boolean tick(DhBlockPos2D targetPos)
{ {
if (!this.networkState.config.getSynchronizeOnLogin()) if (!this.networkState.sessionConfig.getSynchronizeOnLogin())
{ {
return false; return false;
} }
return super.tick(targetPos); return super.tick(targetPos);
} }
} }
@@ -1,6 +1,7 @@
package com.seibel.distanthorizons.core.multiplayer.config; package com.seibel.distanthorizons.core.multiplayer.config;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
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.config.types.ConfigEntry; import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.network.INetworkObject; import com.seibel.distanthorizons.core.network.INetworkObject;
@@ -12,43 +13,68 @@ import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.seibel.distanthorizons.core.config.Config.Client.Advanced.*;
public class SessionConfig implements INetworkObject public class SessionConfig implements INetworkObject
{ {
private static final LinkedHashMap<String, Entry> CONFIG_ENTRIES = new LinkedHashMap<>(); private static final LinkedHashMap<String, Entry> CONFIG_ENTRIES = new LinkedHashMap<>();
private static <T> void registerConfigEntry(ConfigEntry<T> configEntry, BiFunction<T, T, T> valueConstrainer)
{
CONFIG_ENTRIES.put(Objects.requireNonNull(configEntry.getServersideShortName()), new Entry(configEntry, valueConstrainer));
}
private final LinkedHashMap<String, Object> values = new LinkedHashMap<>(); private final LinkedHashMap<String, Object> values = new LinkedHashMap<>();
public SessionConfig constrainingConfig; public SessionConfig constrainingConfig;
//=============//
// constructor //
//=============//
static static
{ {
// Note: config values are ordered by serversideShortName when transmitted // Note: config values are ordered by serversideShortName when transmitted
registerConfigEntry(Graphics.Quality.lodChunkRenderDistanceRadius, Math::min); registerConfigEntry(Config.Client.Advanced.Graphics.Quality.lodChunkRenderDistanceRadius, Math::min);
registerConfigEntry(WorldGenerator.enableDistantGeneration, (x, y) -> x && y); registerConfigEntry(Config.Client.Advanced.WorldGenerator.enableDistantGeneration, (x, y) -> x && y);
registerConfigEntry(Multiplayer.ServerNetworking.generationRequestRateLimit, Math::min); registerConfigEntry(Config.Client.Advanced.Multiplayer.ServerNetworking.generationRequestRateLimit, Math::min);
registerConfigEntry(Multiplayer.ServerNetworking.enableRealTimeUpdates, (x, y) -> x && y); registerConfigEntry(Config.Client.Advanced.Multiplayer.ServerNetworking.enableRealTimeUpdates, (x, y) -> x && y);
registerConfigEntry(Multiplayer.ServerNetworking.synchronizeOnLogin, (x, y) -> x && y); registerConfigEntry(Config.Client.Advanced.Multiplayer.ServerNetworking.synchronizeOnLogin, (x, y) -> x && y);
registerConfigEntry(Multiplayer.ServerNetworking.syncOnLoginRateLimit, Math::min); registerConfigEntry(Config.Client.Advanced.Multiplayer.ServerNetworking.syncOnLoginRateLimit, Math::min);
} }
public int getRenderDistanceRadius() { return this.getValue(Graphics.Quality.lodChunkRenderDistanceRadius); } public SessionConfig() {}
public boolean isDistantGenerationEnabled() { return this.getValue(WorldGenerator.enableDistantGeneration); }
public int getGenerationRequestRateLimit() { return this.getValue(Multiplayer.ServerNetworking.generationRequestRateLimit); }
public boolean isRealTimeUpdatesEnabled() { return this.getValue(Multiplayer.ServerNetworking.enableRealTimeUpdates); }
public boolean getSynchronizeOnLogin() { return this.getValue(Multiplayer.ServerNetworking.synchronizeOnLogin); }
public int getSyncOnLoginRateLimit() { return this.getValue(Multiplayer.ServerNetworking.syncOnLoginRateLimit); }
//===============//
// public values //
//===============//
public int getRenderDistanceRadius() { return this.getValue(Config.Client.Advanced.Graphics.Quality.lodChunkRenderDistanceRadius); }
public boolean isDistantGenerationEnabled() { return this.getValue(Config.Client.Advanced.WorldGenerator.enableDistantGeneration); }
public int getGenerationRequestRateLimit() { return this.getValue(Config.Client.Advanced.Multiplayer.ServerNetworking.generationRequestRateLimit); }
public boolean isRealTimeUpdatesEnabled() { return this.getValue(Config.Client.Advanced.Multiplayer.ServerNetworking.enableRealTimeUpdates); }
public boolean getSynchronizeOnLogin() { return this.getValue(Config.Client.Advanced.Multiplayer.ServerNetworking.synchronizeOnLogin); }
public int getSyncOnLoginRateLimit() { return this.getValue(Config.Client.Advanced.Multiplayer.ServerNetworking.syncOnLoginRateLimit); }
//====================//
// entry registration //
//====================//
private static <T> void registerConfigEntry(ConfigEntry<T> configEntry, BiFunction<T, T, T> valueConstrainer)
{
CONFIG_ENTRIES.put(Objects.requireNonNull(configEntry.getServersideShortName()), new Entry(configEntry, valueConstrainer));
}
//==================//
// internal getters //
//==================//
private <T> T getValue(ConfigEntry<T> configEntry) { return this.getValue(configEntry.getServersideShortName()); }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T> T getValue(String name) private <T> T getValue(String name)
{ {
@@ -64,10 +90,6 @@ public class SessionConfig implements INetworkObject
? (T) entry.valueConstrainer.apply(value, this.constrainingConfig.getValue(name)) ? (T) entry.valueConstrainer.apply(value, this.constrainingConfig.getValue(name))
: value); : value);
} }
private <T> T getValue(ConfigEntry<T> configEntry)
{
return this.getValue(configEntry.getServersideShortName());
}
private Map<String, ?> getValues() private Map<String, ?> getValues()
{ {
@@ -80,24 +102,31 @@ public class SessionConfig implements INetworkObject
} }
@Override
public void encode(ByteBuf out) //===============//
{ // serialization //
this.writeFixedLengthCollection(out, this.getValues().values()); //===============//
}
@Override @Override
public void decode(ByteBuf in) public void encode(ByteBuf outBuffer) { this.writeFixedLengthCollection(outBuffer, this.getValues().values()); }
@Override
public void decode(ByteBuf inBuffer)
{ {
for (String key : CONFIG_ENTRIES.keySet()) for (String key : CONFIG_ENTRIES.keySet())
{ {
Object currentValue = this.getValue(key); Object currentValue = this.getValue(key);
Object newValue = Codec.getCodec(currentValue.getClass()).decode.apply(currentValue, in); Object newValue = Codec.getCodec(currentValue.getClass()).decode.apply(currentValue, inBuffer);
this.values.put(key, newValue); this.values.put(key, newValue);
} }
} }
//================//
// base overrides //
//================//
@Override @Override
public String toString() public String toString()
{ {
@@ -107,6 +136,11 @@ public class SessionConfig implements INetworkObject
} }
//================//
// helper classes //
//================//
private static class Entry private static class Entry
{ {
public final ConfigEntry<Object> supplier; public final ConfigEntry<Object> supplier;
@@ -121,11 +155,12 @@ public class SessionConfig implements INetworkObject
} }
public static class ChangeListener implements Closeable /** fires if any config value was changed */
public static class AnyChangeListener implements Closeable
{ {
private final ArrayList<ConfigChangeListener<?>> changeListeners; private final ArrayList<ConfigChangeListener<?>> changeListeners;
public ChangeListener(Runnable runnable) public AnyChangeListener(Runnable runnable)
{ {
this.changeListeners = new ArrayList<>(CONFIG_ENTRIES.size()); this.changeListeners = new ArrayList<>(CONFIG_ENTRIES.size());
for (Entry entry : CONFIG_ENTRIES.values()) for (Entry entry : CONFIG_ENTRIES.values())
@@ -1,8 +1,9 @@
package com.seibel.distanthorizons.core.multiplayer.server; package com.seibel.distanthorizons.core.multiplayer.server;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.network.session.Session; import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import org.jetbrains.annotations.Nullable;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@@ -11,60 +12,74 @@ import java.util.concurrent.ConcurrentMap;
public class RemotePlayerConnectionHandler public class RemotePlayerConnectionHandler
{ {
private final ConcurrentMap<IServerPlayerWrapper, ServerPlayerState> connectedPlayers = new ConcurrentHashMap<>(); private final ConcurrentMap<IServerPlayerWrapper, ServerPlayerState> connectedPlayerStateByPlayerWrapper = new ConcurrentHashMap<>();
private final ConcurrentMap<IServerPlayerWrapper, Queue<NetworkMessage>> messageQueue = new ConcurrentHashMap<>(); private final ConcurrentMap<IServerPlayerWrapper, Queue<AbstractNetworkMessage>> messageQueueByPlayerWrapper = new ConcurrentHashMap<>();
public void handlePluginMessage(IServerPlayerWrapper player, NetworkMessage message)
{
ServerPlayerState playerState = this.connectedPlayers.get(player);
if (playerState != null)
{
playerState.session.tryHandleMessage(message);
}
else
{
this.messageQueue.computeIfAbsent(player, k -> new ConcurrentLinkedQueue<>()).add(message);
}
}
public ServerPlayerState getConnectedPlayer(IServerPlayerWrapper player)
{
return this.connectedPlayers.get(player);
}
public Iterable<ServerPlayerState> getConnectedPlayers()
{
return this.connectedPlayers.values();
}
//========================//
// player joining/leaving //
//========================//
public ServerPlayerState registerJoinedPlayer(IServerPlayerWrapper serverPlayer) public ServerPlayerState registerJoinedPlayer(IServerPlayerWrapper serverPlayer)
{ {
ServerPlayerState state = new ServerPlayerState(serverPlayer); ServerPlayerState playerState = new ServerPlayerState(serverPlayer);
this.connectedPlayers.put(serverPlayer, state); this.connectedPlayerStateByPlayerWrapper.put(serverPlayer, playerState);
Queue<NetworkMessage> queuedMessages = this.messageQueue.get(serverPlayer); Queue<AbstractNetworkMessage> queuedMessages = this.messageQueueByPlayerWrapper.get(serverPlayer);
if (queuedMessages != null) if (queuedMessages != null)
{ {
Session session = state.session; NetworkSession networkSession = playerState.networkSession;
for (NetworkMessage message : queuedMessages) for (AbstractNetworkMessage message : queuedMessages)
{ {
session.tryHandleMessage(message); networkSession.tryHandleMessage(message);
} }
this.messageQueue.remove(serverPlayer); this.messageQueueByPlayerWrapper.remove(serverPlayer);
} }
return state; return playerState;
} }
public void unregisterLeftPlayer(IServerPlayerWrapper serverPlayer) public void unregisterLeftPlayer(IServerPlayerWrapper serverPlayer)
{ {
ServerPlayerState playerState = this.connectedPlayers.remove(serverPlayer); ServerPlayerState playerState = this.connectedPlayerStateByPlayerWrapper.remove(serverPlayer);
if (playerState != null) if (playerState != null)
{ {
playerState.close(); playerState.close();
} }
} }
//==========//
// messages //
//==========//
public void handlePluginMessage(IServerPlayerWrapper player, AbstractNetworkMessage message)
{
ServerPlayerState playerState = this.connectedPlayerStateByPlayerWrapper.get(player);
if (playerState != null)
{
playerState.networkSession.tryHandleMessage(message);
}
else
{
this.messageQueueByPlayerWrapper.computeIfAbsent(player, k -> new ConcurrentLinkedQueue<>()).add(message);
}
}
//=========//
// getters //
//=========//
@Nullable
public ServerPlayerState getConnectedPlayer(IServerPlayerWrapper player) { return this.connectedPlayerStateByPlayerWrapper.get(player); }
public Iterable<ServerPlayerState> getConnectedPlayers() { return this.connectedPlayerStateByPlayerWrapper.values(); }
} }
@@ -1,103 +1,120 @@
package com.seibel.distanthorizons.core.multiplayer.server; package com.seibel.distanthorizons.core.multiplayer.server;
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.level.DhServerLevel; import com.seibel.distanthorizons.core.level.DhServerLevel;
import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig; import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig;
import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage; import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage;
import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage; import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage;
import com.seibel.distanthorizons.core.network.event.internal.CloseEvent; import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent;
import com.seibel.distanthorizons.core.network.exceptions.RateLimitedException; import com.seibel.distanthorizons.core.network.exceptions.RateLimitedException;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage;
import com.seibel.distanthorizons.core.network.session.Session; import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.util.ratelimiting.SupplierBasedRateAndConcurrencyLimiter; import com.seibel.distanthorizons.core.util.ratelimiting.SupplierBasedRateAndConcurrencyLimiter;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.Closeable;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import static com.seibel.distanthorizons.core.config.Config.Client.Advanced.Multiplayer.ServerNetworking; public class ServerPlayerState implements Closeable
public class ServerPlayerState
{ {
public final Session session; private final ConfigChangeListener<String> levelKeyPrefixChangeListener
public IServerPlayerWrapper serverPlayer() { return this.session.serverPlayer; } = new ConfigChangeListener<>(Config.Client.Advanced.Multiplayer.ServerNetworking.levelKeyPrefix, this::onLevelKeyPrefixConfigChanged);
private final SessionConfig.AnyChangeListener configAnyChangeListener = new SessionConfig.AnyChangeListener(this::onSessionConfigChanged);
@NotNull
public SessionConfig config = new SessionConfig();
private final SessionConfig.ChangeListener configChangeListener = new SessionConfig.ChangeListener(this::onConfigChanged);
private String lastLevelKey = ""; private String lastLevelKey = "";
private final ConfigChangeListener<String> levelKeyPrefixChangeListener = new ConfigChangeListener<>(ServerNetworking.levelKeyPrefix, this::sendLevelKey);
public final NetworkSession networkSession;
public IServerPlayerWrapper getServerPlayer() { return this.networkSession.serverPlayer; }
@NotNull
public final SessionConfig sessionConfig = new SessionConfig();
private final ConcurrentHashMap<DhServerLevel, RateLimiterSet> rateLimiterSets = new ConcurrentHashMap<>(); private final ConcurrentHashMap<DhServerLevel, RateLimiterSet> rateLimiterSets = new ConcurrentHashMap<>();
public RateLimiterSet getRateLimiterSet(DhServerLevel level) public RateLimiterSet getRateLimiterSet(DhServerLevel level) { return this.rateLimiterSets.computeIfAbsent(level, ignored -> new RateLimiterSet()); }
{ public void clearRateLimiterSets() { this.rateLimiterSets.clear(); }
return this.rateLimiterSets.computeIfAbsent(level, ignored -> new RateLimiterSet());
}
public void clearRateLimiterSets()
{
this.rateLimiterSets.clear();
}
//==============//
// constructors //
//==============//
public ServerPlayerState(IServerPlayerWrapper serverPlayer) public ServerPlayerState(IServerPlayerWrapper serverPlayer)
{ {
this.session = new Session(serverPlayer); this.networkSession = new NetworkSession(serverPlayer);
this.session.registerHandler(SessionConfigMessage.class, sessionConfigMessage -> this.networkSession.registerHandler(SessionConfigMessage.class, (sessionConfigMessage) ->
{ {
this.config.constrainingConfig = sessionConfigMessage.config; this.sessionConfig.constrainingConfig = sessionConfigMessage.config;
this.sendLevelKey(null); this.sendLevelKey();
this.session.sendMessage(new SessionConfigMessage(this.config)); this.networkSession.sendMessage(new SessionConfigMessage(this.sessionConfig));
}); });
this.session.registerHandler(CloseEvent.class, event -> { this.networkSession.registerHandler(CloseInternalEvent.class, event -> {
// No-op. removes "Unhandled message" log entries // No-op. prevents "Unhandled message" log entries
}); });
} }
private void sendLevelKey(String ignored)
//=================//
// client updating //
//=================//
private void onLevelKeyPrefixConfigChanged(String newLevelKey) { this.sendLevelKey(); }
private void sendLevelKey()
{ {
if (ServerNetworking.sendLevelKeys.get()) if (Config.Client.Advanced.Multiplayer.ServerNetworking.sendLevelKeys.get())
{ {
String levelKey = this.serverPlayer().getLevel().getKeyedLevelDimensionName(); // let the client's know about the change
String levelKey = this.getServerPlayer().getLevel().getKeyedLevelDimensionName();
if (!levelKey.equals(this.lastLevelKey)) if (!levelKey.equals(this.lastLevelKey))
{ {
this.lastLevelKey = levelKey; this.lastLevelKey = levelKey;
this.session.sendMessage(new CurrentLevelKeyMessage(levelKey)); this.networkSession.sendMessage(new CurrentLevelKeyMessage(levelKey));
} }
} }
} }
private void onConfigChanged() private void onSessionConfigChanged() { this.networkSession.sendMessage(new SessionConfigMessage(this.sessionConfig)); }
{
this.session.sendMessage(new SessionConfigMessage(this.config));
}
//==========//
// shutdown //
//==========//
@Override
public void close() public void close()
{ {
this.levelKeyPrefixChangeListener.close(); this.levelKeyPrefixChangeListener.close();
this.configChangeListener.close(); this.configAnyChangeListener.close();
this.session.close(); this.networkSession.close();
} }
//================//
// helper classes //
//================//
public class RateLimiterSet public class RateLimiterSet
{ {
public final SupplierBasedRateAndConcurrencyLimiter<FullDataSourceRequestMessage> generationRequestRateLimiter = new SupplierBasedRateAndConcurrencyLimiter<>( public final SupplierBasedRateAndConcurrencyLimiter<FullDataSourceRequestMessage> generationRequestRateLimiter = new SupplierBasedRateAndConcurrencyLimiter<>(
() -> ServerNetworking.generationRequestRateLimit.get(), () -> Config.Client.Advanced.Multiplayer.ServerNetworking.generationRequestRateLimit.get(),
msg -> { msg -> {
msg.sendResponse(new RateLimitedException("Full data request rate limit: " + ServerPlayerState.this.config.getGenerationRequestRateLimit())); msg.sendResponse(new RateLimitedException("Full data request rate limit: " + ServerPlayerState.this.sessionConfig.getGenerationRequestRateLimit()));
} }
); );
public final SupplierBasedRateAndConcurrencyLimiter<FullDataSourceRequestMessage> syncOnLoginRateLimiter = new SupplierBasedRateAndConcurrencyLimiter<>( public final SupplierBasedRateAndConcurrencyLimiter<FullDataSourceRequestMessage> syncOnLoginRateLimiter = new SupplierBasedRateAndConcurrencyLimiter<>(
() -> ServerNetworking.syncOnLoginRateLimit.get(), () -> Config.Client.Advanced.Multiplayer.ServerNetworking.syncOnLoginRateLimit.get(),
msg -> { msg -> {
msg.sendResponse(new RateLimitedException("Sync on login rate limit: " + ServerPlayerState.this.config.getSyncOnLoginRateLimit())); msg.sendResponse(new RateLimitedException("Sync on login rate limit: " + ServerPlayerState.this.sessionConfig.getSyncOnLoginRateLimit()));
} }
); );
@@ -29,18 +29,31 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.function.*; import java.util.function.*;
/** The base for any object that can be sent over the network. */
public interface INetworkObject public interface INetworkObject
{ {
/** Serializes this object into the given {@link ByteBuf} */
void encode(ByteBuf out); void encode(ByteBuf out);
/** Populates this object's from the given {@link ByteBuf} */
void decode(ByteBuf in); void decode(ByteBuf in);
//========================//
// default/static methods //
//========================//
/**
* @param obj the empty object that will be populated by the incoming {@link ByteBuf}
*/
static <T extends INetworkObject> T decodeToInstance(T obj, ByteBuf inputByteBuf) static <T extends INetworkObject> T decodeToInstance(T obj, ByteBuf inputByteBuf)
{ {
obj.decode(inputByteBuf); obj.decode(inputByteBuf);
return obj; return obj;
} }
@Contract("_, null -> false; _, !null -> true") @Contract("_, null -> false; _, !null -> true")
default boolean writeOptional(ByteBuf outputByteBuf, Object value) default boolean writeOptional(ByteBuf outputByteBuf, Object value)
{ {
@@ -56,7 +69,6 @@ public interface INetworkObject
? decoder.get() ? decoder.get()
: null; : null;
} }
default void readOptional(ByteBuf inputByteBuf, Runnable decoder) default void readOptional(ByteBuf inputByteBuf, Runnable decoder)
{ {
if (inputByteBuf.readBoolean()) if (inputByteBuf.readBoolean())
@@ -65,16 +77,10 @@ public interface INetworkObject
} }
} }
default void writeString(String inputString, ByteBuf outputByteBuf)
{
INetworkObject.writeStringStatic(inputString, outputByteBuf);
}
default String readString(ByteBuf inputByteBuf) // strings //
{
return INetworkObject.readStringStatic(inputByteBuf);
}
default void writeString(String inputString, ByteBuf outputByteBuf) { INetworkObject.writeStringStatic(inputString, outputByteBuf); }
static void writeStringStatic(String inputString, ByteBuf outputByteBuf) static void writeStringStatic(String inputString, ByteBuf outputByteBuf)
{ {
byte[] bytes = inputString.getBytes(StandardCharsets.UTF_8); byte[] bytes = inputString.getBytes(StandardCharsets.UTF_8);
@@ -82,12 +88,16 @@ public interface INetworkObject
outputByteBuf.writeBytes(bytes); outputByteBuf.writeBytes(bytes);
} }
default String readString(ByteBuf inputByteBuf) { return INetworkObject.readStringStatic(inputByteBuf); }
static String readStringStatic(ByteBuf inputByteBuf) static String readStringStatic(ByteBuf inputByteBuf)
{ {
int length = inputByteBuf.readUnsignedShort(); int length = inputByteBuf.readUnsignedShort();
return inputByteBuf.readSlice(length).toString(StandardCharsets.UTF_8); return inputByteBuf.readSlice(length).toString(StandardCharsets.UTF_8);
} }
// collections //
default <T> void writeCollection(ByteBuf outputByteBuf, Collection<T> collection) default <T> void writeCollection(ByteBuf outputByteBuf, Collection<T> collection)
{ {
outputByteBuf.writeInt(collection.size()); outputByteBuf.writeInt(collection.size());
@@ -133,39 +143,57 @@ public interface INetworkObject
} }
} }
//================//
// helper classes //
//================//
/** /**
* Defines (de)serialization for different classes,
* specifically for base classes like {@link Integer}, {@link Boolean}, and {@link String}. <br><br>
*
* Should only be used for non-editable classes; * Should only be used for non-editable classes;
* otherwise, you may want to implement {@link INetworkObject} and use its methods where applicable. * otherwise, you may want to implement {@link INetworkObject} and use its methods where applicable.
*/ */
class Codec class Codec
{ {
private static final ConcurrentMap<Class<?>, Codec> codecMap = new ConcurrentHashMap<Class<?>, Codec>() private static final ConcurrentMap<Class<?>, Codec> CODEC_MAP = new ConcurrentHashMap<Class<?>, Codec>()
{{ {{
// Primitives must be added manually here // Primitives must be added manually here
this.put(Integer.class, new Codec((obj, out) -> out.writeInt((int)obj), (obj, in) -> in.readInt())); this.put(Integer.class, new Codec((obj, outByteBuff) -> outByteBuff.writeInt((int)obj), (obj, inByteBuff) -> inByteBuff.readInt()));
this.put(Boolean.class, new Codec((obj, out) -> out.writeBoolean((boolean) obj), (obj, in) -> in.readBoolean())); this.put(Boolean.class, new Codec((obj, outByteBuff) -> outByteBuff.writeBoolean((boolean) obj), (obj, inByteBuff) -> inByteBuff.readBoolean()));
this.put(String.class, new Codec((obj, out) -> INetworkObject.writeStringStatic((String) obj, out), (obj, in) -> INetworkObject.readStringStatic(in))); this.put(String.class, new Codec((obj, outByteBuff) -> INetworkObject.writeStringStatic((String) obj, outByteBuff), (obj, inByteBuff) -> INetworkObject.readStringStatic(inByteBuff)));
this.put(INetworkObject.class, new Codec(INetworkObject::encode, INetworkObject::decodeToInstance)); this.put(INetworkObject.class, new Codec(INetworkObject::encode, INetworkObject::decodeToInstance));
this.put(Map.Entry.class, new Codec( this.put(Map.Entry.class, new Codec(
(obj, out) -> { (obj, outByteBuff) ->
{
Map.Entry<?, ?> entry = (Entry<?, ?>) obj; Map.Entry<?, ?> entry = (Entry<?, ?>) obj;
getCodec(entry.getKey().getClass()).encode.accept(entry.getKey(), out); getCodec(entry.getKey().getClass()).encode.accept(entry.getKey(), outByteBuff);
getCodec(entry.getValue().getClass()).encode.accept(entry.getValue(), out); getCodec(entry.getValue().getClass()).encode.accept(entry.getValue(), outByteBuff);
}, },
(obj, in) -> { (obj, inByteBuff) ->
{
Map.Entry<?, ?> entry = (Entry<?, ?>) obj; Map.Entry<?, ?> entry = (Entry<?, ?>) obj;
return new SimpleEntry<>( return new SimpleEntry<>(
getCodec(entry.getKey().getClass()).decode.apply(entry.getKey(), in), getCodec(entry.getKey().getClass()).decode.apply(entry.getKey(), inByteBuff),
getCodec(entry.getValue().getClass()).decode.apply(entry.getValue(), in) getCodec(entry.getValue().getClass()).decode.apply(entry.getValue(), inByteBuff)
); );
} }
)); ));
}}; }};
public final BiConsumer<Object, ByteBuf> encode; public final BiConsumer<Object, ByteBuf> encode;
public final BiFunction<Object, ByteBuf, Object> decode; public final BiFunction<Object, ByteBuf, Object> decode;
//=============//
// constructor //
//=============//
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <T> Codec(BiConsumer<T, ByteBuf> encode, BiFunction<T, ByteBuf, T> decode) public <T> Codec(BiConsumer<T, ByteBuf> encode, BiFunction<T, ByteBuf, T> decode)
{ {
@@ -173,10 +201,18 @@ public interface INetworkObject
this.decode = (BiFunction<Object, ByteBuf, Object>) decode; this.decode = (BiFunction<Object, ByteBuf, Object>) decode;
} }
//================//
// static methods //
//================//
public static <T> Codec getCodec(Class<T> clazz) public static <T> Codec getCodec(Class<T> clazz)
{ {
return codecMap.computeIfAbsent(clazz, ignored -> { return CODEC_MAP.computeIfAbsent(clazz, ignored ->
for (Map.Entry<Class<?>, Codec> entry : codecMap.entrySet()) {
// TODO when would this ever return true?
for (Map.Entry<Class<?>, Codec> entry : CODEC_MAP.entrySet())
{ {
if (entry.getKey().isAssignableFrom(clazz)) if (entry.getKey().isAssignableFrom(clazz))
{ {
@@ -0,0 +1,235 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.event;
import com.google.common.cache.CacheBuilder;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.network.event.internal.AbstractInternalEvent;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage;
import com.seibel.distanthorizons.core.network.messages.MessageRegistry;
import com.seibel.distanthorizons.core.network.session.SessionClosedException;
import com.seibel.distanthorizons.core.network.messages.requests.CancelMessage;
import com.seibel.distanthorizons.core.network.messages.requests.ExceptionMessage;
import com.seibel.distanthorizons.coreapi.ModInfo;
import org.apache.logging.log4j.LogManager;
import java.io.InvalidClassException;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.*;
import java.util.function.Consumer;
public abstract class AbstractNetworkEventSource
{
private static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Logging.logNetworkEvent.get());
/**
* Contains all message handlers. <br>
* Grouped by: <br>
* - {@link AbstractNetworkMessage} type <br>
* - {@link AbstractNetworkEventSource} <br>
*/
private final ConcurrentHashMap<
Class<? extends AbstractNetworkMessage>,
ConcurrentMap<AbstractNetworkEventSource, Set<INetworkMessageConsumer>>
> networkHandlerSetByMessageClass = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, FutureResponseData> pendingFutureById = new ConcurrentHashMap<>();
/** automatically forgets about ID's after a set time span. */
private final Set<Long> cancelledFutureIdSet = Collections.newSetFromMap(
CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.<Long, Boolean>build()
.asMap());
//=============//
// constructor //
//=============//
protected void handleMessage(AbstractNetworkMessage message)
{
boolean handled = false;
ConcurrentMap<AbstractNetworkEventSource, Set<INetworkMessageConsumer>> handlersByEventSource = this.networkHandlerSetByMessageClass.get(message.getClass());
if (handlersByEventSource != null)
{
for (Set<INetworkMessageConsumer> handlerSet : handlersByEventSource.values())
{
for (INetworkMessageConsumer handler : handlerSet)
{
handled = true;
handler.accept(message);
}
}
}
if (message instanceof AbstractTrackableMessage)
{
AbstractTrackableMessage trackableMessage = (AbstractTrackableMessage) message;
FutureResponseData responseData = this.pendingFutureById.get(trackableMessage.futureId);
if (responseData != null)
{
handled = true;
if (message instanceof ExceptionMessage)
{
responseData.future.completeExceptionally(((ExceptionMessage) message).exception);
}
else if (message.getClass() != responseData.responseClass)
{
responseData.future.completeExceptionally(new InvalidClassException("Response with invalid type: expected " + responseData.responseClass.getSimpleName() + ", got:" + message));
}
else
{
responseData.future.complete(trackableMessage);
}
}
else if (this.cancelledFutureIdSet.remove(trackableMessage.futureId))
{
handled = true;
}
}
if (!handled && ModInfo.IS_DEV_BUILD)
{
LOGGER.warn("Unhandled message: [{}].", message);
}
}
//==================//
// abstract methods //
//==================//
public abstract <T extends AbstractNetworkMessage> void registerHandler(Class<T> handlerClass, Consumer<T> handlerImplementation);
//==================//
// message handlers //
//==================//
protected final <T extends AbstractNetworkMessage> void registerHandler(AbstractNetworkEventSource eventSource, Class<T> handlerClass, Consumer<T> handlerImplementation)
{
if (!AbstractInternalEvent.class.isAssignableFrom(handlerClass))
{
MessageRegistry.INSTANCE.getMessageId(handlerClass);
}
//noinspection unchecked
this.networkHandlerSetByMessageClass
.computeIfAbsent(handlerClass, missingHandlerClass -> new ConcurrentHashMap<>())
.computeIfAbsent(eventSource, missingEventSource -> ConcurrentHashMap.newKeySet())
.add((m) -> handlerImplementation.accept((T) m));
}
protected void removeAllHandlers(AbstractNetworkEventSource eventSource)
{
for (ConcurrentMap<AbstractNetworkEventSource, Set<INetworkMessageConsumer>> handlerMap : this.networkHandlerSetByMessageClass.values())
{
handlerMap.remove(eventSource);
}
}
//================//
// create message //
//================//
protected <TResponse extends AbstractTrackableMessage> CompletableFuture<TResponse> createRequest(AbstractTrackableMessage msg, Class<TResponse> responseClass)
{
CompletableFuture<TResponse> responseFuture = new CompletableFuture<>();
responseFuture.whenComplete((response, throwable) ->
{
if (throwable instanceof CancellationException)
{
this.cancelledFutureIdSet.add(msg.futureId);
msg.sendResponse(new CancelMessage());
}
if (!(throwable instanceof SessionClosedException))
{
this.pendingFutureById.remove(msg.futureId);
}
});
this.pendingFutureById.put(msg.futureId, new FutureResponseData(responseClass, responseFuture));
return responseFuture;
}
//==========//
// shutdown //
//==========//
public void close()
{
this.networkHandlerSetByMessageClass.clear();
this.completeAllFuturesExceptionally(new SessionClosedException(this.getClass().getSimpleName() + " is closed."));
}
private void completeAllFuturesExceptionally(Throwable cause)
{
for (FutureResponseData responseData : this.pendingFutureById.values())
{
responseData.future.completeExceptionally(cause);
}
}
//================//
// helper classes //
//================//
private static class FutureResponseData
{
public final Class<? extends AbstractTrackableMessage> responseClass;
public final CompletableFuture<AbstractTrackableMessage> future;
private <T extends AbstractTrackableMessage> FutureResponseData(Class<T> responseClass, CompletableFuture<T> future)
{
this.responseClass = responseClass;
//noinspection unchecked
this.future = (CompletableFuture<AbstractTrackableMessage>) future;
}
}
/** Simple wrapper just to make the code here a bit more readable */
@FunctionalInterface
private interface INetworkMessageConsumer
{
void accept(AbstractNetworkMessage message);
}
}
@@ -1,186 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.event;
import com.google.common.cache.CacheBuilder;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.network.event.internal.InternalEvent;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage;
import com.seibel.distanthorizons.core.network.messages.TrackableMessage;
import com.seibel.distanthorizons.core.network.messages.MessageRegistry;
import com.seibel.distanthorizons.core.network.session.SessionClosedException;
import com.seibel.distanthorizons.core.network.messages.requests.CancelMessage;
import com.seibel.distanthorizons.core.network.messages.requests.ExceptionMessage;
import com.seibel.distanthorizons.coreapi.ModInfo;
import org.apache.logging.log4j.LogManager;
import java.io.InvalidClassException;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.*;
import java.util.function.Consumer;
public abstract class NetworkEventSource
{
private static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Logging.logNetworkEvent.get());
protected final ConcurrentMap<
Class<? extends NetworkMessage>,
ConcurrentMap<
NetworkEventSource,
Set<Consumer<NetworkMessage>>
>
> handlers = new ConcurrentHashMap<>();
private final ConcurrentMap<Long, FutureResponseData> pendingFutures = new ConcurrentHashMap<>();
private final Set<Long> cancelledFutures = Collections.newSetFromMap(CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.<Long, Boolean>build()
.asMap());
protected void handleMessage(NetworkMessage message)
{
boolean handled = false;
ConcurrentMap<NetworkEventSource, Set<Consumer<NetworkMessage>>> handlersByEventSource = this.handlers.get(message.getClass());
if (handlersByEventSource != null)
{
for (Set<Consumer<NetworkMessage>> handlerSet : handlersByEventSource.values())
{
for (Consumer<NetworkMessage> handler : handlerSet)
{
handled = true;
handler.accept(message);
}
}
}
if (message instanceof TrackableMessage)
{
TrackableMessage trackableMessage = (TrackableMessage) message;
FutureResponseData responseData = this.pendingFutures.get(trackableMessage.futureId);
if (responseData != null)
{
handled = true;
if (message instanceof ExceptionMessage)
{
responseData.future.completeExceptionally(((ExceptionMessage) message).exception);
}
else if (message.getClass() != responseData.responseClass)
{
responseData.future.completeExceptionally(new InvalidClassException("Response with invalid type: expected " + responseData.responseClass.getSimpleName() + ", got:" + message));
}
else
{
responseData.future.complete(trackableMessage);
}
}
else if (this.cancelledFutures.remove(trackableMessage.futureId))
{
handled = true;
}
}
if (!handled && ModInfo.IS_DEV_BUILD)
{
LOGGER.warn("Unhandled message: {}", message);
}
}
public abstract <T extends NetworkMessage> void registerHandler(Class<T> handlerClass, Consumer<T> handlerImplementation);
protected final <T extends NetworkMessage> void registerHandler(NetworkEventSource instance, Class<T> handlerClass, Consumer<T> handlerImplementation)
{
if (!InternalEvent.class.isAssignableFrom(handlerClass))
{
MessageRegistry.INSTANCE.getMessageId(handlerClass);
}
//noinspection unchecked
this.handlers.computeIfAbsent(handlerClass, missingHandlerClass -> new ConcurrentHashMap<>())
.computeIfAbsent(instance, _instance -> ConcurrentHashMap.newKeySet())
.add((Consumer<NetworkMessage>) handlerImplementation);
}
protected void removeAllHandlers(NetworkEventSource childInstance)
{
for (ConcurrentMap<NetworkEventSource, Set<Consumer<NetworkMessage>>> handlerMap : this.handlers.values())
{
handlerMap.remove(childInstance);
}
}
protected <TResponse extends TrackableMessage> CompletableFuture<TResponse> createRequest(TrackableMessage msg, Class<TResponse> responseClass)
{
CompletableFuture<TResponse> responseFuture = new CompletableFuture<>();
responseFuture.whenComplete((response, throwable) ->
{
if (throwable instanceof CancellationException)
{
this.cancelledFutures.add(msg.futureId);
msg.sendResponse(new CancelMessage());
}
if (!(throwable instanceof SessionClosedException))
{
this.pendingFutures.remove(msg.futureId);
}
});
this.pendingFutures.put(msg.futureId, new FutureResponseData(responseClass, responseFuture));
return responseFuture;
}
protected final void completeAllFuturesExceptionally(Throwable cause)
{
for (FutureResponseData responseData : this.pendingFutures.values())
{
responseData.future.completeExceptionally(cause);
}
}
public void close()
{
this.handlers.clear();
this.completeAllFuturesExceptionally(new SessionClosedException(this.getClass().getSimpleName() + " is closed."));
}
private static class FutureResponseData
{
public final Class<? extends TrackableMessage> responseClass;
public final CompletableFuture<TrackableMessage> future;
private <T extends TrackableMessage> FutureResponseData(Class<T> responseClass, CompletableFuture<T> future)
{
this.responseClass = responseClass;
//noinspection unchecked
this.future = (CompletableFuture<TrackableMessage>) future;
}
}
}
@@ -19,27 +19,40 @@
package com.seibel.distanthorizons.core.network.event; package com.seibel.distanthorizons.core.network.event;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import java.util.function.Consumer; import java.util.function.Consumer;
/** Provides a way to register network message handlers which are expected to be removed later. */ /**
public final class ScopedNetworkEventSource extends NetworkEventSource * Provides a way to register network message handlers which are expected to be removed later. <br><br>
*
* In other words, listeners can be added to this {@link AbstractNetworkEventSource} and when
* you no longer need any of those listeners you can {@link ScopedNetworkEventSource#close()}
* this handler to remove all of them.
*/
public final class ScopedNetworkEventSource extends AbstractNetworkEventSource
{ {
public final NetworkEventSource parent; public final AbstractNetworkEventSource parent;
private boolean isClosed = false; private boolean isClosed = false;
private final Consumer<NetworkMessage> actualHandleMessageStable = this::handleMessage; private final Consumer<AbstractNetworkMessage> actualHandleMessageStable = this::handleMessage;
public ScopedNetworkEventSource(NetworkEventSource parent)
{
this.parent = parent;
}
//=============//
// constructor //
//=============//
public ScopedNetworkEventSource(AbstractNetworkEventSource parent) { this.parent = parent; }
//==================//
// message handlers //
//==================//
@Override @Override
public <T extends NetworkMessage> void registerHandler(Class<T> handlerClass, Consumer<T> handlerImplementation) public <T extends AbstractNetworkMessage> void registerHandler(Class<T> handlerClass, Consumer<T> handlerImplementation)
{ {
if (this.isClosed) if (this.isClosed)
{ {
@@ -53,10 +66,16 @@ public final class ScopedNetworkEventSource extends NetworkEventSource
} }
//==========//
// shutdown //
//==========//
@Override @Override
public void close() public void close()
{ {
this.isClosed = true; this.isClosed = true;
this.parent.removeAllHandlers(this); this.parent.removeAllHandlers(this);
} }
} }
@@ -0,0 +1,17 @@
package com.seibel.distanthorizons.core.network.event.internal;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import io.netty.buffer.ByteBuf;
/** internal events are messages sent from the client/sever back to themselves. */
public abstract class AbstractInternalEvent extends AbstractNetworkMessage
{
@Override
public void encode(ByteBuf out)
{ throw new UnsupportedOperationException(this.getClass().getSimpleName() + " is an internal event, and cannot be sent."); }
@Override
public void decode(ByteBuf in)
{ throw new UnsupportedOperationException(this.getClass().getSimpleName() + " is an internal event, and cannot be received."); }
}
@@ -3,7 +3,7 @@ package com.seibel.distanthorizons.core.network.event.internal;
/** /**
* This event is used to indicate a disconnect. * This event is used to indicate a disconnect.
*/ */
public class CloseEvent extends InternalEvent public class CloseInternalEvent extends AbstractInternalEvent
{ {
} }
@@ -3,11 +3,11 @@ package com.seibel.distanthorizons.core.network.event.internal;
/** /**
* This event is received instead of a message if its protocol version is incompatible with version the mod uses. * This event is received instead of a message if its protocol version is incompatible with version the mod uses.
*/ */
public class IncompatibleMessageEvent extends InternalEvent public class IncompatibleMessageInternalEvent extends AbstractInternalEvent
{ {
public final int protocolVersion; public final int protocolVersion;
public IncompatibleMessageEvent(int protocolVersion) public IncompatibleMessageInternalEvent(int protocolVersion)
{ {
this.protocolVersion = protocolVersion; this.protocolVersion = protocolVersion;
} }
@@ -1,20 +0,0 @@
package com.seibel.distanthorizons.core.network.event.internal;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage;
import io.netty.buffer.ByteBuf;
public abstract class InternalEvent extends NetworkMessage
{
@Override
public void encode(ByteBuf out)
{
throw new UnsupportedOperationException(this.getClass().getSimpleName() + " is an internal event, and cannot be sent.");
}
@Override
public void decode(ByteBuf in)
{
throw new UnsupportedOperationException(this.getClass().getSimpleName() + " is an internal event, and cannot be received.");
}
}
@@ -1,19 +1,19 @@
package com.seibel.distanthorizons.core.network.event.internal; package com.seibel.distanthorizons.core.network.event.internal;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
/** /**
* This event is used to indicate that encoding or decoding of a message threw an exception. * This event is used to indicate that encoding or decoding of a message threw an exception.
*/ */
public class ProtocolErrorEvent extends InternalEvent public class ProtocolErrorInternalEvent extends AbstractInternalEvent
{ {
public final Throwable reason; public final Throwable reason;
@Nullable @Nullable
public final NetworkMessage message; public final AbstractNetworkMessage message;
public final boolean replyWithCloseReason; public final boolean replyWithCloseReason;
public ProtocolErrorEvent(Throwable reason, @Nullable NetworkMessage message, boolean replyWithCloseReason) public ProtocolErrorInternalEvent(Throwable reason, @Nullable AbstractNetworkMessage message, boolean replyWithCloseReason)
{ {
this.reason = reason; this.reason = reason;
this.message = message; this.message = message;
@@ -19,10 +19,8 @@
package com.seibel.distanthorizons.core.network.exceptions; package com.seibel.distanthorizons.core.network.exceptions;
/** Fired if a user attempts to run an operation in a level they aren't currently in. */
public class InvalidLevelException extends Exception public class InvalidLevelException extends Exception
{ {
public InvalidLevelException(String message) public InvalidLevelException(String message) { super(message); }
{
super(message);
}
} }
@@ -1,28 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.exceptions;
public class InvalidSectionPosException extends Exception
{
public InvalidSectionPosException(String message)
{
super(message);
}
}
@@ -19,10 +19,9 @@
package com.seibel.distanthorizons.core.network.exceptions; package com.seibel.distanthorizons.core.network.exceptions;
/** Fired if the client attempts to queue more tasks than the server is willing to handle. */
public class RateLimitedException extends Exception public class RateLimitedException extends Exception
{ {
public RateLimitedException(String message) public RateLimitedException(String message) { super(message); }
{
super(message);
}
} }
@@ -1,10 +1,11 @@
package com.seibel.distanthorizons.core.network.exceptions; package com.seibel.distanthorizons.core.network.exceptions;
/**
* Fired if the client attempts an operation currently forbidden by the server. <Br>
* For example attempting to request LODs when world generation is disabled on the server.
*/
public class RequestRejectedException extends Exception public class RequestRejectedException extends Exception
{ {
public RequestRejectedException(String message) public RequestRejectedException(String message) { super(message); }
{
super(message);
}
} }
@@ -0,0 +1,31 @@
package com.seibel.distanthorizons.core.network.messages;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
/** Any new implementing classes should be registered in {@link MessageRegistry} */
public abstract class AbstractNetworkMessage implements INetworkObject
{
//============//
// properties //
//============//
private NetworkSession networkSession = null;
public NetworkSession getSession() { return this.networkSession; }
public void setSession(NetworkSession networkSession) { this.networkSession = networkSession; }
public IServerPlayerWrapper serverPlayer() { return this.networkSession.serverPlayer; }
//================//
// base overrides //
//================//
@Override
public String toString() { return this.toStringHelper().toString(); }
public MoreObjects.ToStringHelper toStringHelper() { return MoreObjects.toStringHelper(this); }
}
@@ -0,0 +1,162 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.network.messages.requests.ExceptionMessage;
import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.world.EWorldEnvironment;
import io.netty.buffer.ByteBuf;
import java.util.concurrent.atomic.AtomicInteger;
public abstract class AbstractTrackableMessage extends AbstractNetworkMessage
{
/** Tracks every message we've sent */
private static final AtomicInteger LAST_MESSAGE_ID_REF = new AtomicInteger();
/**
* 32 bits - NetworkSession ID (not transmitted) <br>
* 1 bit - Requesting side (client - 0, server - 1) <br>
* 31 bits - Request/Message ID <br><br>
*
* SI = NetworkSession ID <br>
* CS = Client/Server flag <br>
* MI = Request/Message ID <br><br>
*
* <code>
* =======Bit layout======= <br>
* SI SI SI SI SI SI SI SI <-- Top bits <br>
* SI SI SI SI SI SI SI SI <br>
* SI SI SI SI SI SI SI SI <br>
* SI SI SI SI SI SI SI SI <br>
* CS MI MI MI MI MI MI MI <br>
* MI MI MI MI MI MI MI MI <br>
* MI MI MI MI MI MI MI MI <br>
* MI MI MI MI MI MI MI MI <-- Bottom bits <br>
* </code>
*/
public long futureId;
//=============//
// constructor //
//=============//
public AbstractTrackableMessage()
{
EWorldEnvironment worldEnvironment = SharedApi.getEnvironment();
LodUtil.assertTrue(worldEnvironment != null, "Message can't be created if no world is loaded.");
// message/Request ID written as the least significant bits
long id = LAST_MESSAGE_ID_REF.getAndIncrement();
// write requesting side at bit 32
id |= ((worldEnvironment == EWorldEnvironment.Server_Only) ? 1 : 0) << 31;
this.futureId = id;
}
//==================//
// abstract methods //
//==================//
protected abstract void encodeInternal(ByteBuf out) throws Exception;
protected abstract void decodeInternal(ByteBuf in) throws Exception;
//=================//
// getters/setters //
//=================//
@Override
public void setSession(NetworkSession networkSession)
{
super.setSession(networkSession);
// Session ID is written in the most significant bits
this.futureId |= (long) networkSession.id << 32;
}
//==============//
// send message //
//==============//
public void sendResponse(AbstractTrackableMessage responseMessage)
{
responseMessage.futureId = this.futureId;
this.getSession().sendMessage(responseMessage);
}
public void sendResponse(Exception e) { this.sendResponse(new ExceptionMessage(e)); }
//=============//
// serializing //
//=============//
@Override
public final void encode(ByteBuf out)
{
try
{
out.writeInt((int) this.futureId);
this.encodeInternal(out);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
@Override
public final void decode(ByteBuf in)
{
try
{
this.futureId = in.readInt();
this.decodeInternal(in);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
//================//
// base overrides //
//================//
@Override
public MoreObjects.ToStringHelper toStringHelper()
{
return super.toStringHelper()
.add("futureId", this.futureId);
}
}
@@ -3,13 +3,12 @@ package com.seibel.distanthorizons.core.network.messages;
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;
/** Implemented by messages that handle level data */
public interface ILevelRelatedMessage public interface ILevelRelatedMessage
{ {
String getLevelName(); String getLevelName();
/** /** Checks whether the message's level matches the given level. */
* Checks whether the message's level matches the given level.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted") @SuppressWarnings("BooleanMethodIsAlwaysInverted")
default boolean isSameLevelAs(ILevelWrapper levelWrapper) default boolean isSameLevelAs(ILevelWrapper levelWrapper)
{ {
@@ -24,7 +24,7 @@ import com.google.common.collect.HashBiMap;
import com.seibel.distanthorizons.core.network.messages.base.CodecCrashMessage; import com.seibel.distanthorizons.core.network.messages.base.CodecCrashMessage;
import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage; import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage;
import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage; import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataChunkMessage; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMessage;
import com.seibel.distanthorizons.core.network.messages.requests.CancelMessage; import com.seibel.distanthorizons.core.network.messages.requests.CancelMessage;
import com.seibel.distanthorizons.core.network.messages.base.CloseReasonMessage; import com.seibel.distanthorizons.core.network.messages.base.CloseReasonMessage;
import com.seibel.distanthorizons.core.network.messages.requests.ExceptionMessage; import com.seibel.distanthorizons.core.network.messages.requests.ExceptionMessage;
@@ -37,22 +37,25 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
/** Keeps track of every {@link AbstractNetworkMessage} so they can be (De)serialized. */
public class MessageRegistry public class MessageRegistry
{ {
public static final boolean DEBUG_ENABLE_CODEC_CRASH_MESSAGE = ModInfo.IS_DEV_BUILD; public static final boolean DEBUG_CODEC_CRASH_MESSAGE = ModInfo.IS_DEV_BUILD;
public static final MessageRegistry INSTANCE = new MessageRegistry(); public static final MessageRegistry INSTANCE = new MessageRegistry();
private final Map<Integer, Supplier<? extends NetworkMessage>> idToSupplier = new HashMap<>(); private final Map<Integer, Supplier<? extends AbstractNetworkMessage>> messageConstructorById = new HashMap<>();
private final BiMap<Class<? extends NetworkMessage>, Integer> classToId = HashBiMap.create(); private final BiMap<Class<? extends AbstractNetworkMessage>, Integer> messageClassById = HashBiMap.create();
//=============//
// constructor //
//=============//
private MessageRegistry() private MessageRegistry()
{ {
// Note: Messages must have parameterless constructors // Note: Messages must have parameterless constructors
// When the communication is about to be stopped, either side can send this message
// There may be messages after this, but they should be ignored if it's possible
this.registerMessage(CloseReasonMessage.class, CloseReasonMessage::new); this.registerMessage(CloseReasonMessage.class, CloseReasonMessage::new);
// Level keys // Level keys
@@ -69,10 +72,10 @@ public class MessageRegistry
this.registerMessage(FullDataSourceRequestMessage.class, FullDataSourceRequestMessage::new); this.registerMessage(FullDataSourceRequestMessage.class, FullDataSourceRequestMessage::new);
this.registerMessage(FullDataSourceResponseMessage.class, FullDataSourceResponseMessage::new); this.registerMessage(FullDataSourceResponseMessage.class, FullDataSourceResponseMessage::new);
this.registerMessage(FullDataPartialUpdateMessage.class, FullDataPartialUpdateMessage::new); this.registerMessage(FullDataPartialUpdateMessage.class, FullDataPartialUpdateMessage::new);
this.registerMessage(FullDataChunkMessage.class, FullDataChunkMessage::new); this.registerMessage(FullDataSplitMessage.class, FullDataSplitMessage::new);
// Debug messages are always last, and not included into release builds. // Debug messages are always last, and not included in release builds.
if (DEBUG_ENABLE_CODEC_CRASH_MESSAGE) if (DEBUG_CODEC_CRASH_MESSAGE)
{ {
this.registerMessage(CodecCrashMessage.class, CodecCrashMessage::new); this.registerMessage(CodecCrashMessage.class, CodecCrashMessage::new);
} }
@@ -80,18 +83,23 @@ public class MessageRegistry
public <T extends NetworkMessage> void registerMessage(Class<T> clazz, Supplier<T> supplier) //==================//
// message handling //
//==================//
public <T extends AbstractNetworkMessage> void registerMessage(Class<T> clazz, Supplier<T> supplier)
{ {
int id = this.idToSupplier.size() + 1; int id = this.messageConstructorById.size() + 1;
this.idToSupplier.put(id, supplier); this.messageConstructorById.put(id, supplier);
this.classToId.put(clazz, id); this.messageClassById.put(clazz, id);
} }
public NetworkMessage createMessage(int messageId) throws IllegalArgumentException /** used when decoding messages */
public AbstractNetworkMessage createMessage(int messageId) throws IllegalArgumentException
{ {
try try
{ {
return this.idToSupplier.get(messageId).get(); return this.messageConstructorById.get(messageId).get();
} }
catch (NullPointerException e) catch (NullPointerException e)
{ {
@@ -99,20 +107,22 @@ public class MessageRegistry
} }
} }
public int getMessageId(NetworkMessage message)
{
return this.getMessageId(message.getClass());
}
public int getMessageId(Class<? extends NetworkMessage> messageClass)
//=========//
// getters //
//=========//
public int getMessageId(AbstractNetworkMessage message) { return this.getMessageId(message.getClass()); }
public int getMessageId(Class<? extends AbstractNetworkMessage> messageClass)
{ {
try try
{ {
return this.classToId.get(messageClass); return this.messageClassById.get(messageClass);
} }
catch (NullPointerException e) catch (NullPointerException e)
{ {
throw new IllegalArgumentException("Message does not have ID assigned to it: " + messageClass.getSimpleName()); throw new IllegalArgumentException("Message does not have ID assigned to it: [" + messageClass.getSimpleName() + "].");
} }
} }
@@ -1,35 +0,0 @@
package com.seibel.distanthorizons.core.network.messages;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.network.session.Session;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
public abstract class NetworkMessage implements INetworkObject
{
private Session session = null;
public IServerPlayerWrapper serverPlayer() { return this.session.serverPlayer; }
public Session getSession()
{
return this.session;
}
public void setSession(Session session)
{
this.session = session;
}
@Override
public String toString()
{
return this.toStringHelper().toString();
}
public MoreObjects.ToStringHelper toStringHelper()
{
return MoreObjects.toStringHelper(this);
}
}
@@ -1,98 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.network.messages.requests.ExceptionMessage;
import com.seibel.distanthorizons.core.network.session.Session;
import com.seibel.distanthorizons.core.world.EWorldEnvironment;
import io.netty.buffer.ByteBuf;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
public abstract class TrackableMessage extends NetworkMessage
{
private static final AtomicInteger lastId = new AtomicInteger();
// 32 bits - Session ID (not transmitted)
// 1 bit - Requesting side (client - 0, server - 1)
// 31 bits - Request ID
public long futureId = lastId.getAndIncrement()
| ((Objects.requireNonNull(SharedApi.getEnvironment()) == EWorldEnvironment.Server_Only ? 1 : 0) << 31);
@Override
public void setSession(Session session)
{
super.setSession(session);
this.futureId |= (long) session.id << 32;
}
public void sendResponse(TrackableMessage responseMessage)
{
responseMessage.futureId = this.futureId;
this.getSession().sendMessage(responseMessage);
}
public void sendResponse(Exception e)
{
this.sendResponse(new ExceptionMessage(e));
}
@Override
public final void encode(ByteBuf out)
{
try
{
out.writeInt((int) this.futureId);
this.encode0(out);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
@Override
public final void decode(ByteBuf in)
{
try
{
this.futureId = in.readInt();
this.decode0(in);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
protected abstract void encode0(ByteBuf out) throws Exception;
protected abstract void decode0(ByteBuf in) throws Exception;
@Override public MoreObjects.ToStringHelper toStringHelper()
{
return super.toStringHelper()
.add("futureId", this.futureId);
}
}
@@ -20,26 +20,43 @@
package com.seibel.distanthorizons.core.network.messages.base; package com.seibel.distanthorizons.core.network.messages.base;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
public class CloseReasonMessage extends NetworkMessage /**
* When the communication is about to be stopped, either side can send this message
* There may be messages after this, but they should be ignored if possible.
*/
public class CloseReasonMessage extends AbstractNetworkMessage
{ {
public String reason; public String reason;
//==============//
// constructors //
//==============//
public CloseReasonMessage() { } public CloseReasonMessage() { }
public CloseReasonMessage(String reason) { this.reason = reason; } public CloseReasonMessage(String reason) { this.reason = reason; }
@Override
public void encode(ByteBuf out)
{
this.writeString(this.reason, out);
}
//===============//
// serialization //
//===============//
@Override
public void encode(ByteBuf out) { this.writeString(this.reason, out); }
@Override @Override
public void decode(ByteBuf in) { this.reason = this.readString(in); } public void decode(ByteBuf in) { this.reason = this.readString(in); }
//================//
// base overrides //
//================//
@Override @Override
public MoreObjects.ToStringHelper toStringHelper() public MoreObjects.ToStringHelper toStringHelper()
{ {
@@ -20,16 +20,28 @@
package com.seibel.distanthorizons.core.network.messages.base; package com.seibel.distanthorizons.core.network.messages.base;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
public class CodecCrashMessage extends NetworkMessage public class CodecCrashMessage extends AbstractNetworkMessage
{ {
public ECrashPhase crashPhase; public ECrashPhase crashPhase;
//==============//
// constructors //
//==============//
public CodecCrashMessage() { } public CodecCrashMessage() { }
public CodecCrashMessage(ECrashPhase crashPhase) { this.crashPhase = crashPhase; } public CodecCrashMessage(ECrashPhase crashPhase) { this.crashPhase = crashPhase; }
//===============//
// serialization //
//===============//
@Override @Override
public void encode(ByteBuf out) public void encode(ByteBuf out)
{ {
@@ -40,12 +52,14 @@ public class CodecCrashMessage extends NetworkMessage
} }
@Override @Override
public void decode(ByteBuf in) public void decode(ByteBuf in) { throw new RuntimeException("decode force crash"); }
{
throw new RuntimeException("decode force crash");
}
//================//
// base overrides //
//================//
@Override @Override
public MoreObjects.ToStringHelper toStringHelper() public MoreObjects.ToStringHelper toStringHelper()
{ {
@@ -53,6 +67,16 @@ public class CodecCrashMessage extends NetworkMessage
.add("crashPhase", this.crashPhase); .add("crashPhase", this.crashPhase);
} }
//================//
// helper classes //
//================//
/**
* ENCODE, <br>
* DECODE, <br>
*/
public enum ECrashPhase public enum ECrashPhase
{ {
ENCODE, ENCODE,
@@ -1,32 +1,40 @@
package com.seibel.distanthorizons.core.network.messages.base; package com.seibel.distanthorizons.core.network.messages.base;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
public class CurrentLevelKeyMessage extends NetworkMessage public class CurrentLevelKeyMessage extends AbstractNetworkMessage
{ {
public String levelKey; public String levelKey;
//==============//
// constructors //
//==============//
public CurrentLevelKeyMessage() { } public CurrentLevelKeyMessage() { }
public CurrentLevelKeyMessage(String levelKey) public CurrentLevelKeyMessage(String levelKey) { this.levelKey = levelKey; }
{
this.levelKey = levelKey;
}
//===============//
// serialization //
//===============//
@Override @Override
public void encode(ByteBuf out) public void encode(ByteBuf out) { this.writeString(this.levelKey, out); }
{
this.writeString(this.levelKey, out);
}
@Override @Override
public void decode(ByteBuf in) public void decode(ByteBuf in) { this.levelKey = this.readString(in); }
{
this.levelKey = this.readString(in);
}
//================//
// base overrides //
//================//
@Override @Override
public MoreObjects.ToStringHelper toStringHelper() public MoreObjects.ToStringHelper toStringHelper()
{ {
@@ -22,16 +22,29 @@ package com.seibel.distanthorizons.core.network.messages.base;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig; import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig;
import com.seibel.distanthorizons.core.network.INetworkObject; import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
public class SessionConfigMessage extends NetworkMessage /** used for full DH support */
public class SessionConfigMessage extends AbstractNetworkMessage
{ {
public SessionConfig config; public SessionConfig config;
//=============//
// constructor //
//=============//
public SessionConfigMessage() { } public SessionConfigMessage() { }
public SessionConfigMessage(SessionConfig config) { this.config = config; } public SessionConfigMessage(SessionConfig config) { this.config = config; }
//===============//
// serialization //
//===============//
@Override @Override
public void encode(ByteBuf out) { this.config.encode(out); } public void encode(ByteBuf out) { this.config.encode(out); }
@@ -39,6 +52,11 @@ public class SessionConfigMessage extends NetworkMessage
public void decode(ByteBuf in) { this.config = INetworkObject.decodeToInstance(new SessionConfig(), in); } public void decode(ByteBuf in) { this.config = INetworkObject.decodeToInstance(new SessionConfig(), in); }
//================//
// base overrides //
//================//
@Override @Override
public MoreObjects.ToStringHelper toStringHelper() public MoreObjects.ToStringHelper toStringHelper()
{ {
@@ -22,19 +22,24 @@ package com.seibel.distanthorizons.core.network.messages.fullData;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.INetworkObject; import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.network.messages.ILevelRelatedMessage; import com.seibel.distanthorizons.core.network.messages.ILevelRelatedMessage;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
public class FullDataPartialUpdateMessage extends NetworkMessage implements ILevelRelatedMessage public class FullDataPartialUpdateMessage extends AbstractNetworkMessage implements ILevelRelatedMessage
{ {
public FullDataPayload payload;
private String levelName; private String levelName;
@Override @Override
public String getLevelName() { return this.levelName; } public String getLevelName() { return this.levelName; }
public FullDataPayload payload;
//==============//
// constructors //
//==============//
public FullDataPartialUpdateMessage() { } public FullDataPartialUpdateMessage() { }
public FullDataPartialUpdateMessage(IServerLevelWrapper level, FullDataPayload payload) public FullDataPartialUpdateMessage(IServerLevelWrapper level, FullDataPayload payload)
{ {
@@ -43,6 +48,11 @@ public class FullDataPartialUpdateMessage extends NetworkMessage implements ILev
} }
//===============//
// serialization //
//===============//
@Override @Override
public void encode(ByteBuf out) public void encode(ByteBuf out)
{ {
@@ -58,6 +68,11 @@ public class FullDataPartialUpdateMessage extends NetworkMessage implements ILev
} }
//================//
// base overrides //
//================//
@Override @Override
public MoreObjects.ToStringHelper toStringHelper() public MoreObjects.ToStringHelper toStringHelper()
{ {
@@ -18,7 +18,9 @@ import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; import java.util.function.Consumer;
/**
* @see FullDataSplitMessage
*/
public class FullDataPayload implements INetworkObject public class FullDataPayload implements INetworkObject
{ {
private static final AtomicInteger lastBufferId = new AtomicInteger(); private static final AtomicInteger lastBufferId = new AtomicInteger();
@@ -30,6 +32,11 @@ public class FullDataPayload implements INetworkObject
public ByteBuf dtoBuffer; public ByteBuf dtoBuffer;
//==============//
// constructors //
//==============//
public FullDataPayload() { } public FullDataPayload() { }
public FullDataPayload(@NotNull FullDataSourceV2 fullDataSource) public FullDataPayload(@NotNull FullDataSourceV2 fullDataSource)
{ {
@@ -61,23 +68,10 @@ public class FullDataPayload implements INetworkObject
} }
public void acceptInChunkMessages(int chunkSize, Consumer<FullDataChunkMessage> chunkMessageConsumer)
{
for (int chunkNum = 0; ; chunkNum++)
{
int offset = chunkNum * chunkSize;
int actualChunkSize = Math.min(this.dtoBuffer.writerIndex() - offset, chunkSize);
if (actualChunkSize <= 0)
{
break;
}
FullDataChunkMessage chunk = new FullDataChunkMessage(this.dtoBufferId, chunkNum == 0, this.dtoBuffer.slice(offset, actualChunkSize));
chunkMessageConsumer.accept(chunk);
}
}
//===============//
// serialization //
//===============//
@Override @Override
public void encode(ByteBuf out) public void encode(ByteBuf out)
@@ -91,6 +85,35 @@ public class FullDataPayload implements INetworkObject
this.dtoBufferId = in.readInt(); this.dtoBufferId = in.readInt();
} }
/**
* Used to send {@link FullDataPayload}'s since the data they contain may be larger
* than what a single packet could contain.
*
* @param payloadChunkSizeInBytes how many bytes can be sent in a single message
*/
public void splitAndSend(int payloadChunkSizeInBytes, Consumer<FullDataSplitMessage> sendMessageConsumer)
{
// chunk in this context means chunk of data, not a MC chunk
for (int payloadChunkNum = 0; ; payloadChunkNum++)
{
int offset = payloadChunkNum * payloadChunkSizeInBytes;
int actualChunkSize = Math.min(this.dtoBuffer.writerIndex() - offset, payloadChunkSizeInBytes);
if (actualChunkSize <= 0)
{
break;
}
FullDataSplitMessage chunk = new FullDataSplitMessage(this.dtoBufferId, payloadChunkNum == 0, this.dtoBuffer.slice(offset, actualChunkSize));
sendMessageConsumer.accept(chunk);
}
}
//================//
// base overrides //
//================//
@Override @Override
public String toString() public String toString()
@@ -21,25 +21,30 @@ package com.seibel.distanthorizons.core.network.messages.fullData;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.messages.ILevelRelatedMessage; import com.seibel.distanthorizons.core.network.messages.ILevelRelatedMessage;
import com.seibel.distanthorizons.core.network.messages.TrackableMessage; import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import javax.annotation.Nullable; import javax.annotation.Nullable;
public class FullDataSourceRequestMessage extends TrackableMessage implements ILevelRelatedMessage public class FullDataSourceRequestMessage extends AbstractTrackableMessage implements ILevelRelatedMessage
{ {
private String levelName;
public long sectionPos; public long sectionPos;
/** Only present when requesting for changes. */ /** Only present when requesting changes. */
@Nullable @Nullable
public Long clientTimestamp; public Long clientTimestamp;
private String levelName;
@Override @Override
public String getLevelName() { return this.levelName; } public String getLevelName() { return this.levelName; }
//==============//
// constructors //
//==============//
public FullDataSourceRequestMessage() {} public FullDataSourceRequestMessage() {}
public FullDataSourceRequestMessage(ILevelWrapper levelWrapper, long sectionPos, @Nullable Long clientTimestamp) public FullDataSourceRequestMessage(ILevelWrapper levelWrapper, long sectionPos, @Nullable Long clientTimestamp)
{ {
@@ -48,8 +53,14 @@ public class FullDataSourceRequestMessage extends TrackableMessage implements IL
this.clientTimestamp = clientTimestamp; this.clientTimestamp = clientTimestamp;
} }
//===============//
// serialization //
//===============//
@Override @Override
public void encode0(ByteBuf out) public void encodeInternal(ByteBuf out)
{ {
this.writeString(this.levelName, out); this.writeString(this.levelName, out);
out.writeLong(this.sectionPos); out.writeLong(this.sectionPos);
@@ -60,7 +71,7 @@ public class FullDataSourceRequestMessage extends TrackableMessage implements IL
} }
@Override @Override
public void decode0(ByteBuf in) public void decodeInternal(ByteBuf in)
{ {
this.levelName = this.readString(in); this.levelName = this.readString(in);
this.sectionPos = in.readLong(); this.sectionPos = in.readLong();
@@ -68,6 +79,11 @@ public class FullDataSourceRequestMessage extends TrackableMessage implements IL
} }
//================//
// base overrides //
//================//
@Override @Override
public MoreObjects.ToStringHelper toStringHelper() public MoreObjects.ToStringHelper toStringHelper()
{ {
@@ -21,20 +21,25 @@ package com.seibel.distanthorizons.core.network.messages.fullData;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.INetworkObject; import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.network.messages.TrackableMessage; import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
/** /**
* Response message, containing the requested full data source, * Response message, containing the requested full data source,
* or nothing if requested in updates-only mode and the data was not updated. * or null if requested in updates-only mode and the data was not updated.
*/ */
public class FullDataSourceResponseMessage extends TrackableMessage public class FullDataSourceResponseMessage extends AbstractTrackableMessage
{ {
@Nullable @Nullable
public FullDataPayload payload; public FullDataPayload payload;
//=============//
// constructor //
//=============//
public FullDataSourceResponseMessage() { } public FullDataSourceResponseMessage() { }
public FullDataSourceResponseMessage(@Nullable FullDataPayload payload) public FullDataSourceResponseMessage(@Nullable FullDataPayload payload)
{ {
@@ -44,8 +49,14 @@ public class FullDataSourceResponseMessage extends TrackableMessage
} }
} }
//===============//
// serialization //
//===============//
@Override @Override
public void encode0(ByteBuf out) public void encodeInternal(ByteBuf out)
{ {
if (this.writeOptional(out, this.payload)) if (this.writeOptional(out, this.payload))
{ {
@@ -54,12 +65,14 @@ public class FullDataSourceResponseMessage extends TrackableMessage
} }
@Override @Override
public void decode0(ByteBuf in) public void decodeInternal(ByteBuf in) { this.payload = this.readOptional(in, () -> INetworkObject.decodeToInstance(new FullDataPayload(), in)); }
{
this.payload = this.readOptional(in, () -> INetworkObject.decodeToInstance(new FullDataPayload(), in));
}
//================//
// base overrides //
//================//
@Override @Override
public MoreObjects.ToStringHelper toStringHelper() public MoreObjects.ToStringHelper toStringHelper()
{ {
@@ -20,18 +20,28 @@
package com.seibel.distanthorizons.core.network.messages.fullData; package com.seibel.distanthorizons.core.network.messages.fullData;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
public class FullDataChunkMessage extends NetworkMessage /**
* Used to send part of a {@link FullDataPayload}.
*
* @see FullDataPayload
*/
public class FullDataSplitMessage extends AbstractNetworkMessage
{ {
public int bufferId; public int bufferId;
public ByteBuf buffer; public ByteBuf buffer;
public boolean isFirst; public boolean isFirst;
public FullDataChunkMessage() { }
public FullDataChunkMessage(int bufferId, boolean isFirst, ByteBuf buffer) //==============//
// constructors //
//==============//
public FullDataSplitMessage() { }
public FullDataSplitMessage(int bufferId, boolean isFirst, ByteBuf buffer)
{ {
this.bufferId = bufferId; this.bufferId = bufferId;
this.buffer = buffer; this.buffer = buffer;
@@ -39,6 +49,11 @@ public class FullDataChunkMessage extends NetworkMessage
} }
//===============//
// serialization //
//===============//
@Override @Override
public void encode(ByteBuf out) public void encode(ByteBuf out)
{ {
@@ -62,6 +77,11 @@ public class FullDataChunkMessage extends NetworkMessage
} }
//================//
// base overrides //
//================//
@Override @Override
public MoreObjects.ToStringHelper toStringHelper() public MoreObjects.ToStringHelper toStringHelper()
{ {
@@ -19,20 +19,18 @@
package com.seibel.distanthorizons.core.network.messages.requests; package com.seibel.distanthorizons.core.network.messages.requests;
import com.seibel.distanthorizons.core.network.messages.TrackableMessage; import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
public class CancelMessage extends TrackableMessage public class CancelMessage extends AbstractTrackableMessage
{ {
public CancelMessage() { } public CancelMessage() { }
@Override
public void encode0(ByteBuf out)
{
}
@Override @Override
public void decode0(ByteBuf in) public void encodeInternal(ByteBuf out) { }
{ @Override
} public void decodeInternal(ByteBuf in) { }
} }
@@ -21,49 +21,63 @@ package com.seibel.distanthorizons.core.network.messages.requests;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.exceptions.InvalidLevelException; import com.seibel.distanthorizons.core.network.exceptions.InvalidLevelException;
import com.seibel.distanthorizons.core.network.exceptions.InvalidSectionPosException;
import com.seibel.distanthorizons.core.network.exceptions.RateLimitedException; import com.seibel.distanthorizons.core.network.exceptions.RateLimitedException;
import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException; import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException;
import com.seibel.distanthorizons.core.network.messages.TrackableMessage; import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
// TODO appears to be useless yelling at user // TODO appears to be useless yelling at user
public class ExceptionMessage extends TrackableMessage public class ExceptionMessage extends AbstractTrackableMessage
{ {
private static final List<Class<? extends Exception>> exceptionMap = new ArrayList<Class<? extends Exception>>() private static final List<Class<? extends Exception>> EXCEPTION_LIST = new ArrayList<Class<? extends Exception>>()
{{ {{
// All exceptions here must include constructor: (String) // All exceptions here must include constructor: (String)
this.add(RateLimitedException.class); this.add(RateLimitedException.class);
this.add(InvalidLevelException.class); this.add(InvalidLevelException.class);
this.add(InvalidSectionPosException.class);
this.add(RequestRejectedException.class); this.add(RequestRejectedException.class);
}}; }};
public Exception exception; public Exception exception;
public ExceptionMessage() { }
public ExceptionMessage(Exception exception)
{
this.exception = exception;
}
@Override protected void encode0(ByteBuf out)
//==============//
// constructors //
//==============//
public ExceptionMessage() { }
public ExceptionMessage(Exception exception) { this.exception = exception; }
//===============//
// serialization //
//===============//
@Override
protected void encodeInternal(ByteBuf out)
{ {
out.writeInt(exceptionMap.indexOf(this.exception.getClass())); out.writeInt(EXCEPTION_LIST.indexOf(this.exception.getClass()));
this.writeString(this.exception.getMessage(), out); this.writeString(this.exception.getMessage(), out);
} }
@Override protected void decode0(ByteBuf in) throws Exception @Override
protected void decodeInternal(ByteBuf in) throws Exception
{ {
int id = in.readInt(); int id = in.readInt();
String message = this.readString(in); String message = this.readString(in);
this.exception = exceptionMap.get(id).getDeclaredConstructor(String.class).newInstance(message); this.exception = EXCEPTION_LIST.get(id).getDeclaredConstructor(String.class).newInstance(message);
} }
//================//
// base overrides //
//================//
@Override @Override
public MoreObjects.ToStringHelper toStringHelper() public MoreObjects.ToStringHelper toStringHelper()
{ {
@@ -3,11 +3,11 @@ package com.seibel.distanthorizons.core.network.session;
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.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.network.event.NetworkEventSource; import com.seibel.distanthorizons.core.network.event.AbstractNetworkEventSource;
import com.seibel.distanthorizons.core.network.event.internal.CloseEvent; import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent;
import com.seibel.distanthorizons.core.network.event.internal.ProtocolErrorEvent; import com.seibel.distanthorizons.core.network.event.internal.ProtocolErrorInternalEvent;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.network.messages.TrackableMessage; import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage;
import com.seibel.distanthorizons.core.network.messages.base.CloseReasonMessage; import com.seibel.distanthorizons.core.network.messages.base.CloseReasonMessage;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IPluginPacketSender; import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IPluginPacketSender;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
@@ -19,7 +19,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer; import java.util.function.Consumer;
public class Session extends NetworkEventSource public class NetworkSession extends AbstractNetworkEventSource
{ {
private static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(), private static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Logging.logNetworkEvent.get()); () -> Config.Client.Advanced.Logging.logNetworkEvent.get());
@@ -41,7 +41,13 @@ public class Session extends NetworkEventSource
@Nullable @Nullable
public final IServerPlayerWrapper serverPlayer; public final IServerPlayerWrapper serverPlayer;
public Session(@Nullable IServerPlayerWrapper serverPlayer)
//=============//
// constructor //
//=============//
public NetworkSession(@Nullable IServerPlayerWrapper serverPlayer)
{ {
this.serverPlayer = serverPlayer; this.serverPlayer = serverPlayer;
@@ -50,7 +56,7 @@ public class Session extends NetworkEventSource
this.close(new SessionClosedException(msg.reason)); this.close(new SessionClosedException(msg.reason));
}); });
this.registerHandler(ProtocolErrorEvent.class, event -> this.registerHandler(ProtocolErrorInternalEvent.class, event ->
{ {
if (event.replyWithCloseReason) if (event.replyWithCloseReason)
{ {
@@ -62,7 +68,12 @@ public class Session extends NetworkEventSource
} }
public void tryHandleMessage(NetworkMessage message)
//==================//
// message handling //
//==================//
public void tryHandleMessage(AbstractNetworkMessage message)
{ {
if (this.closeReason.get() != null) if (this.closeReason.get() != null)
{ {
@@ -73,19 +84,19 @@ public class Session extends NetworkEventSource
try try
{ {
LOGGER.debug("Received message: {}", message); LOGGER.debug("Received message: ["+message+"].");
this.handleMessage(message); this.handleMessage(message);
} }
catch (Throwable e) catch (Throwable e)
{ {
LOGGER.error("Failed to handle the message. New messages will be ignored.", e); LOGGER.error("Failed to handle the message. New messages will be ignored.", e);
LOGGER.error("Message: {}", message); LOGGER.error("Message: ["+message+"]");
this.close(); this.close();
} }
} }
@Override @Override
public <T extends NetworkMessage> void registerHandler(Class<T> handlerClass, Consumer<T> handlerImplementation) public <T extends AbstractNetworkMessage> void registerHandler(Class<T> handlerClass, Consumer<T> handlerImplementation)
{ {
if (this.closeReason.get() != null) if (this.closeReason.get() != null)
{ {
@@ -95,7 +106,13 @@ public class Session extends NetworkEventSource
this.registerHandler(this, handlerClass, handlerImplementation); this.registerHandler(this, handlerClass, handlerImplementation);
} }
public <TResponse extends TrackableMessage> CompletableFuture<TResponse> sendRequest(TrackableMessage msg, Class<TResponse> responseClass)
//==============//
// send message //
//==============//
public <TResponse extends AbstractTrackableMessage> CompletableFuture<TResponse> sendRequest(AbstractTrackableMessage msg, Class<TResponse> responseClass)
{ {
msg.setSession(this); msg.setSession(this);
CompletableFuture<TResponse> responseFuture = this.createRequest(msg, responseClass); CompletableFuture<TResponse> responseFuture = this.createRequest(msg, responseClass);
@@ -103,35 +120,41 @@ public class Session extends NetworkEventSource
return responseFuture; return responseFuture;
} }
public void sendMessage(NetworkMessage message) public void sendMessage(AbstractNetworkMessage message)
{ {
if (this.closeReason.get() != null) if (this.closeReason.get() != null)
{ {
return; return;
} }
LOGGER.debug("Sending message: {}", message); LOGGER.debug("Sending message: ["+message+"]");
message.setSession(this); message.setSession(this);
try try
{ {
if (this.serverPlayer != null) if (this.serverPlayer != null)
{ {
PACKET_SENDER.sendPluginPacketServer(this.serverPlayer, message); PACKET_SENDER.sendToClient(this.serverPlayer, message);
} }
else else
{ {
PACKET_SENDER.sendPluginPacketClient(message); PACKET_SENDER.sendToServer(message);
} }
} }
catch (Throwable throwable) catch (Throwable throwable)
{ {
LOGGER.info("Failed to send a message", throwable); LOGGER.info("Failed to send a message", throwable);
LOGGER.info("Message: {}", message); LOGGER.info("Message: ["+message+"]");
this.close(throwable); this.close(throwable);
} }
} }
//==========//
// shutdown //
//==========//
public void close(Throwable closeReason) public void close(Throwable closeReason)
{ {
if (!this.closeReason.compareAndSet(null, closeReason)) if (!this.closeReason.compareAndSet(null, closeReason))
@@ -141,11 +164,9 @@ public class Session extends NetworkEventSource
try try
{ {
this.handleMessage(new CloseEvent()); this.handleMessage(new CloseInternalEvent());
}
catch (Throwable ignored)
{
} }
catch (Throwable ignored) { }
super.close(); super.close();
} }
@@ -2,10 +2,9 @@ package com.seibel.distanthorizons.core.network.session;
import java.io.IOException; import java.io.IOException;
/** The exception thrown if DH's networking session has been shut down. */
public class SessionClosedException extends IOException public class SessionClosedException extends IOException
{ {
public SessionClosedException(String message) public SessionClosedException(String message) { super(message); }
{
super(message);
}
} }
@@ -33,10 +33,10 @@ import com.seibel.distanthorizons.core.util.math.Vec3d;
public class DhChunkPos public class DhChunkPos
{ {
private final int x; private final int x;
public int getX() { return x; } public int getX() { return this.x; }
private final int z; private final int z;
public int getZ() { return z; } public int getZ() { return this.z; }
/** cached to improve hashing speed */ /** cached to improve hashing speed */
public final int hashCode; public final int hashCode;
@@ -67,6 +67,7 @@ public class DhChunkPos
} }
public DhChunkPos(Vec3d pos) public DhChunkPos(Vec3d pos)
{ {
// >> 4 is the Same as div 16
this(((int)pos.x) >> 4, ((int)pos.z) >> 4); this(((int)pos.x) >> 4, ((int)pos.z) >> 4);
} }
@@ -95,11 +96,11 @@ public class DhChunkPos
&& minBlockZ <= pos.getZ() && pos.getZ() < maxBlockZ; && minBlockZ <= pos.getZ() && pos.getZ() < maxBlockZ;
} }
public double distance(DhChunkPos other) public double distance(DhChunkPos other)
{ { return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.z - other.z, 2)); }
return Math.sqrt(Math.pow(x - other.x, 2) + Math.pow(z - other.z, 2)); public double squaredDistance(DhChunkPos other)
} { return Math.pow(this.x - other.x, 2) + Math.pow(this.z - other.z, 2); }
//================// //================//
@@ -61,8 +61,6 @@ public class DhBlockPos2D
public DhBlockPos2D subtract(DhBlockPos2D other) { return new DhBlockPos2D(this.x - other.x, this.z - other.z); } public DhBlockPos2D subtract(DhBlockPos2D other) { return new DhBlockPos2D(this.x - other.x, this.z - other.z); }
public DhBlockPos2D scale(double scale) { return new DhBlockPos2D((int) (this.x * scale), (int) (this.z * scale)); }
public Pos2D toPos2D() { return new Pos2D(this.x, this.z); } public Pos2D toPos2D() { return new Pos2D(this.x, this.z); }
@@ -186,8 +186,11 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
try try
{ {
LodRenderSection renderSection = this.getValue(pos); LodRenderSection renderSection = this.getValue(pos);
if (renderSection != null && renderSection.getRenderingEnabled()) if (renderSection != null)
{ {
// We need to update every non-null section, including those that aren't currently rendering.
// If this isn't done, and the player moves so a lower quality section is now being rendered,
// that section will not have updated correctly and may refuse to load in at all.
renderSection.uploadRenderDataToGpuAsync(); renderSection.uploadRenderDataToGpuAsync();
} }
} }
@@ -27,6 +27,7 @@ import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.LodQuad
import com.seibel.distanthorizons.core.dataObjects.transformers.FullDataToRenderDataTransformer; import com.seibel.distanthorizons.core.dataObjects.transformers.FullDataToRenderDataTransformer;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.file.DataSourcePool;
import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
@@ -36,11 +37,12 @@ import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.ColumnRenderBuffer; import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.ColumnRenderBuffer;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.util.ListUtil;
import com.seibel.distanthorizons.core.util.LodUtil;
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 it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import javax.annotation.WillNotClose; import javax.annotation.WillNotClose;
import java.awt.*; import java.awt.*;
@@ -97,9 +99,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
private final ReentrantLock getRenderSourceLock = new ReentrantLock(); private final ReentrantLock getRenderSourceLock = new ReentrantLock();
/** Stored as a class variable so we can reuse it's result across multiple LOD loads if necessary */ /** Stored as a class variable so we can reuse it's result across multiple LOD loads if necessary */
private ReferencedFutureWrapper renderSourceLoadingRefFuture = null; private ReferencedRenderSourceFutureWrapper renderSourceLoadingRefFuture = null;
/** Stored as a class variable so we can decrement reference counts as each {@link LodRenderSection} finishes using them. */
private ReferencedFutureWrapper[] adjacentLoadRefFutures;
private boolean missingPositionsCalculated = false; private boolean missingPositionsCalculated = false;
/** should be an empty array if no positions need to be generated */ /** should be an empty array if no positions need to be generated */
@@ -123,11 +123,10 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
//===============================// //======================================//
// render data loading/uploading // // render data generation and uploading //
//===============================// //======================================//
// TODO cleanup, there's a lot of nested futures and duplicate error handling here and it's hard to read
public synchronized void uploadRenderDataToGpuAsync() public synchronized void uploadRenderDataToGpuAsync()
{ {
if (!GLProxy.hasInstance()) if (!GLProxy.hasInstance())
@@ -143,70 +142,106 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
return; return;
} }
ThreadPoolExecutor executor = ThreadPoolUtil.getFileHandlerExecutor(); ThreadPoolExecutor executor = ThreadPoolUtil.getFileHandlerExecutor();
if (executor == null || executor.isTerminated()) if (executor == null || executor.isTerminated())
{ {
return; return;
} }
try
{
this.buildAndUploadRenderDataToGpuFuture = CompletableFuture.runAsync(() -> this.buildAndUploadRenderDataToGpuFuture = CompletableFuture.runAsync(() ->
{ {
//==================// try
// load render data // {
//==================// this.loadRenderDataAsync()
.thenCompose((loadedRenderSources) ->
this.tryDecrementingLoadFutureArray(this.adjacentLoadRefFutures); {
try
ReferencedFutureWrapper thisRenderSourceLoadFuture = this.getRenderSourceAsync(); {
ReferencedFutureWrapper[] adjRenderSourceLoadRefFutures = this.getNeighborRenderSourcesAsync(); ColumnRenderSource thisRenderSource = loadedRenderSources.getThisRenderSource();
if (thisRenderSource != null && !thisRenderSource.isEmpty())
{
CompletableFuture<LodQuadBuilder> buildDataFuture = this.buildNewRenderDataAsync(thisRenderSource, loadedRenderSources);
buildDataFuture.thenRun(loadedRenderSources::decrementRefCounts);
return buildDataFuture;
}
else
{
// nothing needs to be rendered
this.canRender = false;
this.buildAndUploadRenderDataToGpuFuture = null;
this.bufferBuildFuture = null;
return CompletableFuture.completedFuture(null);
}
}
catch (Exception e)
{
// exception handling is done here since attempting to do so in the final future's
// .exceptionally() block doesn't return the correct stack traces, making debugging impossible
this.handleException(e);
throw e;
}
})
.thenCompose((lodQuadBuilder) ->
{
try
{
// can be null if there was a problem or if there's nothing to render
if (lodQuadBuilder != null)
{
return this.uploadToGpuAsync(lodQuadBuilder);
}
else
{
return CompletableFuture.completedFuture(null);
}
}
catch (Exception e)
{
this.handleException(e);
throw e;
}
});
}
catch (Exception e)
{
// this catch is just for the first loadRenderDataAsync(),
// each subsequent method has their own handleException() block.
this.handleException(e);
}
}, executor);
}
catch (RejectedExecutionException ignore)
{ /* the thread pool was probably shut down because it's size is being changed, just wait a sec and it should be back */ }
}
private CompletableFuture<LoadedRenderSourcesFutureWrapper> loadRenderDataAsync()
{
ReferencedRenderSourceFutureWrapper thisRenderSourceLoadFuture = this.getRenderSourceAsync();
ArrayList<ReferencedRenderSourceFutureWrapper> adjRenderSourceLoadRefFutures = this.getNeighborRenderSourcesAsync();
// wait for all futures to complete together, // wait for all futures to complete together,
// merging the futures makes loading significantly faster than loading this position then loading its neighbors // merging the futures makes loading significantly faster than loading this position then loading its neighbors
ArrayList<CompletableFuture<ColumnRenderSource>> futureList = new ArrayList<>(); ArrayList<CompletableFuture<ColumnRenderSource>> futureList = new ArrayList<>();
futureList.add(thisRenderSourceLoadFuture.future); futureList.add(thisRenderSourceLoadFuture.future);
for (ReferencedFutureWrapper refFuture : adjRenderSourceLoadRefFutures) for(ReferencedRenderSourceFutureWrapper futureWrapper : adjRenderSourceLoadRefFutures)
{ {
futureList.add(refFuture.future); futureList.add(futureWrapper.future);
} }
CompletableFuture<Void> allLoadedFuture = CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0]));
CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).thenAccept((voidObj) -> return allLoadedFuture.thenApply((voidObj) -> new LoadedRenderSourcesFutureWrapper(thisRenderSourceLoadFuture, adjRenderSourceLoadRefFutures));
{
try
{
ColumnRenderSource renderSource = thisRenderSourceLoadFuture.future.get();
if (renderSource == null || renderSource.isEmpty())
{
thisRenderSourceLoadFuture.decrementRefCount();
for (ReferencedFutureWrapper futureWrapper : adjRenderSourceLoadRefFutures)
{
futureWrapper.decrementRefCount();
} }
private CompletableFuture<LodQuadBuilder> buildNewRenderDataAsync(
// nothing needs to be rendered ColumnRenderSource thisRenderSource,
this.canRender = false; LoadedRenderSourcesFutureWrapper loadedRenderSources)
this.buildAndUploadRenderDataToGpuFuture = null;
this.bufferBuildFuture = null;
return;
}
//=======================//
// build new render data //
//=======================//
try
{ {
ColumnRenderBuffer previousBuffer = this.renderBuffer;
ColumnRenderSource[] adjacentRenderSections = new ColumnRenderSource[EDhDirection.ADJ_DIRECTIONS.length]; ColumnRenderSource[] adjacentRenderSections = new ColumnRenderSource[EDhDirection.ADJ_DIRECTIONS.length];
boolean[] adjIsSameDetailLevel = new boolean[EDhDirection.ADJ_DIRECTIONS.length]; boolean[] adjIsSameDetailLevel = new boolean[EDhDirection.ADJ_DIRECTIONS.length];
for (int i = 0; i < EDhDirection.ADJ_DIRECTIONS.length; i++) for (int i = 0; i < EDhDirection.ADJ_DIRECTIONS.length; i++)
{ {
adjacentRenderSections[i] = adjRenderSourceLoadRefFutures[i].future.getNow(null); adjacentRenderSections[i] = loadedRenderSources.getAdjacentRenderSource(i);
// if the adjacent position isn't the same detail level the buffer building logic // if the adjacent position isn't the same detail level the buffer building logic
// will need to be slightly different in order to reduce holes in the LODs // will need to be slightly different in order to reduce holes in the LODs
@@ -220,71 +255,49 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
// prevents the CPU from working on something that won't be used // prevents the CPU from working on something that won't be used
this.bufferBuildFuture.cancel(true); this.bufferBuildFuture.cancel(true);
} }
this.bufferBuildFuture = ColumnRenderBufferBuilder.buildBuffersAsync(this.level, renderSource, adjacentRenderSections, adjIsSameDetailLevel); this.bufferBuildFuture = ColumnRenderBufferBuilder.buildBuffersAsync(this.level, thisRenderSource, adjacentRenderSections, adjIsSameDetailLevel);
this.bufferBuildFuture.thenAccept((lodQuadBuilder) -> return this.bufferBuildFuture;
}
private CompletableFuture<Void> uploadToGpuAsync(LodQuadBuilder lodQuadBuilder)
{ {
//===================================//
// upload new render data to the GPU //
//===================================//
if (this.bufferUploadFuture != null) if (this.bufferUploadFuture != null)
{ {
// shouldn't normally happen, but just in case canceling the previous future // shouldn't normally happen, but just in case canceling the previous future
// prevents the CPU from working on something that won't be used // prevents the CPU from working on something that won't be used
this.bufferUploadFuture.cancel(true); this.bufferUploadFuture.cancel(true);
} }
this.bufferUploadFuture = ColumnRenderBufferBuilder.uploadBuffersAsync(this.level, renderSource, lodQuadBuilder);
this.bufferUploadFuture.thenAccept((buffer) -> this.bufferUploadFuture = ColumnRenderBufferBuilder.uploadBuffersAsync(this.level, this.pos, lodQuadBuilder);
return this.bufferUploadFuture.thenCompose((buffer) ->
{ {
ColumnRenderBuffer previousBuffer = this.renderBuffer;
// upload complete, clean up the old data if // upload complete, clean up the old data if
this.renderBuffer = buffer; this.renderBuffer = buffer;
this.canRender = (buffer != null); this.canRender = (buffer != null);
this.buildAndUploadRenderDataToGpuFuture = null; this.buildAndUploadRenderDataToGpuFuture = null;
this.bufferBuildFuture = null; this.bufferBuildFuture = null;
if (previousBuffer != null) if (previousBuffer != null)
{ {
previousBuffer.close(); previousBuffer.close();
} }
thisRenderSourceLoadFuture.decrementRefCount(); return null;
this.tryDecrementingLoadFutureArray(adjRenderSourceLoadRefFutures);
this.adjacentLoadRefFutures = null;
});
}); });
} }
catch (Exception e)
{
thisRenderSourceLoadFuture.decrementRefCount();
this.tryDecrementingLoadFutureArray(adjRenderSourceLoadRefFutures);
this.adjacentLoadRefFutures = null;
LOGGER.error("Unexpected error in LodRenderSection loading, Error: "+e.getMessage(), e);
this.buildAndUploadRenderDataToGpuFuture = null;
this.bufferBuildFuture = null;
}
}
catch (Exception e)
{
thisRenderSourceLoadFuture.decrementRefCount();
this.tryDecrementingLoadFutureArray(adjRenderSourceLoadRefFutures);
this.adjacentLoadRefFutures = null;
LOGGER.error("Unexpected error in LodRenderSection loading, Error: "+e.getMessage(), e);
this.buildAndUploadRenderDataToGpuFuture = null; //=====================//
this.bufferBuildFuture = null; // render data helpers //
} //=====================//
});
}, executor);
}
/** Should be called on the {@link ThreadPoolUtil#getFileHandlerExecutor()} */ /** Should be called on the {@link ThreadPoolUtil#getFileHandlerExecutor()} */
private ReferencedFutureWrapper[] getNeighborRenderSourcesAsync() private ArrayList<ReferencedRenderSourceFutureWrapper> getNeighborRenderSourcesAsync()
{ {
ReferencedFutureWrapper[] futureArray = new ReferencedFutureWrapper[EDhDirection.ADJ_DIRECTIONS.length]; ArrayList<ReferencedRenderSourceFutureWrapper> futureList = ListUtil.createEmptyList(EDhDirection.ADJ_DIRECTIONS.length);
for (int i = 0; i < EDhDirection.ADJ_DIRECTIONS.length; i++) for (int i = 0; i < EDhDirection.ADJ_DIRECTIONS.length; i++)
{ {
EDhDirection direction = EDhDirection.ADJ_DIRECTIONS[i]; EDhDirection direction = EDhDirection.ADJ_DIRECTIONS[i];
@@ -296,46 +309,43 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
LodRenderSection adjRenderSection = this.quadTree.getValue(adjPos); LodRenderSection adjRenderSection = this.quadTree.getValue(adjPos);
if (adjRenderSection != null) if (adjRenderSection != null)
{ {
futureArray[arrayIndex] = adjRenderSection.getRenderSourceAsync(); futureList.set(arrayIndex, adjRenderSection.getRenderSourceAsync());
} }
} }
catch (IndexOutOfBoundsException ignore) {} catch (IndexOutOfBoundsException ignore) {}
if (futureArray[arrayIndex] == null) if (futureList.get(arrayIndex) == null)
{ {
futureArray[arrayIndex] = new ReferencedFutureWrapper(CompletableFuture.completedFuture(null)); futureList.set(arrayIndex, new ReferencedRenderSourceFutureWrapper(CompletableFuture.completedFuture(null)));
} }
} }
this.adjacentLoadRefFutures = futureArray; return futureList;
return futureArray;
} }
/** Will try to return the same {@link CompletableFuture} if multiple requests are made for the same position */ /** Will try to return the same {@link CompletableFuture} if multiple requests are made for the same position */
private ReferencedFutureWrapper getRenderSourceAsync() private ReferencedRenderSourceFutureWrapper getRenderSourceAsync()
{ {
try try
{ {
this.getRenderSourceLock.lock(); this.getRenderSourceLock.lock();
// use the already loading future if one is present
// if a load is already in progress, use that existing one ReferencedRenderSourceFutureWrapper oldFuture = this.renderSourceLoadingRefFuture;
// (this reduces the number of duplicate loads that may happen when initially loading the world) if (oldFuture != null)
if (this.renderSourceLoadingRefFuture != null)
{ {
// increment the number of objects needing this future oldFuture.incrementRefCount();
this.renderSourceLoadingRefFuture.incrementRefCount(); return oldFuture;
return this.renderSourceLoadingRefFuture;
} }
ThreadPoolExecutor executor = ThreadPoolUtil.getFileHandlerExecutor(); ThreadPoolExecutor executor = ThreadPoolUtil.getFileHandlerExecutor();
if (executor == null || executor.isTerminated()) if (executor == null || executor.isTerminated())
{ {
return new ReferencedFutureWrapper(CompletableFuture.completedFuture(null)); return new ReferencedRenderSourceFutureWrapper(CompletableFuture.completedFuture(null));
} }
this.renderSourceLoadingRefFuture = new ReferencedFutureWrapper(CompletableFuture.supplyAsync(() -> this.renderSourceLoadingRefFuture = new ReferencedRenderSourceFutureWrapper(CompletableFuture.supplyAsync(() ->
{ {
try (FullDataSourceV2 fullDataSource = this.fullDataSourceProvider.get(this.pos)) try (FullDataSourceV2 fullDataSource = this.fullDataSourceProvider.get(this.pos))
{ {
@@ -362,26 +372,14 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
long adjPos = DhSectionPos.getAdjacentPos(this.pos, direction); long adjPos = DhSectionPos.getAdjacentPos(this.pos, direction);
byte detailLevel = this.quadTree.calculateExpectedDetailLevel(new DhBlockPos2D(MC.getPlayerBlockPos()), adjPos); byte detailLevel = this.quadTree.calculateExpectedDetailLevel(new DhBlockPos2D(MC.getPlayerBlockPos()), adjPos);
detailLevel += DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; detailLevel += DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL;
boolean adjacentIsSameDetailLevel = (detailLevel == DhSectionPos.getDetailLevel(this.pos)); return detailLevel == DhSectionPos.getDetailLevel(this.pos);
return adjacentIsSameDetailLevel;
} }
private void handleException(Throwable e)
/**
* Note: can cause issues with neighboring LOD sections
* if only some (vs all) futures are canceled.
*/
public void cancelGpuUpload()
{ {
CompletableFuture<Void> future = this.buildAndUploadRenderDataToGpuFuture; LOGGER.error("Unexpected error in LodRenderSection loading, Error: " + e.getMessage(), e);
this.buildAndUploadRenderDataToGpuFuture = null; this.buildAndUploadRenderDataToGpuFuture = null;
this.bufferBuildFuture = null; this.bufferBuildFuture = null;
if (future != null)
{
// interrupting the future speeds things up, but also causes
// some LODs to never load in properly
future.cancel(false);
}
} }
@@ -430,7 +428,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
// full data retrieval (world gen) // // full data retrieval (world gen) //
//=================================// //=================================//
public boolean isFullyGenerated() { return this.missingPositionsCalculated && this.missingGenerationPos.size() == 0; } public boolean isFullyGenerated() { return this.missingPositionsCalculated && this.missingGenerationPos.isEmpty(); }
public boolean missingPositionsCalculated() { return this.missingPositionsCalculated; } public boolean missingPositionsCalculated() { return this.missingPositionsCalculated; }
public int ungeneratedPositionCount() { return (this.missingGenerationPos != null) ? this.missingGenerationPos.size() : 0; } public int ungeneratedPositionCount() { return (this.missingGenerationPos != null) ? this.missingGenerationPos.size() : 0; }
@@ -475,27 +473,6 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
//=========//
// cleanup //
//=========//
/** does nothing if the passed in value is null. */
private void tryDecrementingLoadFutureArray(@Nullable ReferencedFutureWrapper[] refFutures)
{
if (refFutures != null)
{
for (ReferencedFutureWrapper futureWrapper : refFutures)
{
if (futureWrapper != null)
{
futureWrapper.decrementRefCount();
}
}
}
}
//==============// //==============//
// base methods // // base methods //
//==============// //==============//
@@ -546,12 +523,6 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
this.bufferUploadFuture.cancel(true); this.bufferUploadFuture.cancel(true);
} }
// this render section won't be rendering, we don't need to load any data for it
this.tryDecrementingLoadFutureArray(this.adjacentLoadRefFutures);
if (this.renderSourceLoadingRefFuture != null)
{
this.renderSourceLoadingRefFuture.decrementRefCount();
}
// remove any active world gen requests that may be for this position // remove any active world gen requests that may be for this position
@@ -596,14 +567,48 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
// helper classes // // helper classes //
//================// //================//
/** Used to easily pass around the loaded {@link ColumnRenderSource}'s. */
private static class LoadedRenderSourcesFutureWrapper
{
private final ReferencedRenderSourceFutureWrapper thisRenderSourceFutureRef;
private final ArrayList<ReferencedRenderSourceFutureWrapper> adjacentRenderSourceFutureRefList;
public LoadedRenderSourcesFutureWrapper(ReferencedRenderSourceFutureWrapper thisRenderSourceFutureRef, ArrayList<ReferencedRenderSourceFutureWrapper> adjacentRenderSourceFutureRefList)
{
this.thisRenderSourceFutureRef = thisRenderSourceFutureRef;
this.adjacentRenderSourceFutureRefList = adjacentRenderSourceFutureRefList;
}
public ColumnRenderSource getThisRenderSource() { return (this.thisRenderSourceFutureRef != null && this.thisRenderSourceFutureRef.future != null) ? this.thisRenderSourceFutureRef.future.getNow(null) : null; }
public ColumnRenderSource getAdjacentRenderSource(int i)
{
ReferencedRenderSourceFutureWrapper futureWrapper = this.adjacentRenderSourceFutureRefList.get(i);
return (futureWrapper != null && futureWrapper.future != null) ? futureWrapper.future.getNow(null) : null;
}
public void decrementRefCounts()
{
this.thisRenderSourceFutureRef.decrementRefCount();
for (int i = 0; i < this.adjacentRenderSourceFutureRefList.size(); i++)
{
this.adjacentRenderSourceFutureRefList.get(i).decrementRefCount();
}
}
}
/** /**
* Used to keep track of whether a {@link ColumnRenderSource} {@link CompletableFuture} * Used to keep track of whether a {@link ColumnRenderSource} {@link CompletableFuture}
* is in use or not, and if not in use cancels the future. <br> <br> * is in use or not, and if not in use returns the data source to the {@link DataSourcePool}. <br> <br>
* *
* This helps speed up LOD loading by canceling loads that are no longer needed, * This reduces GC overhead by pooling shared {@link ColumnRenderSource}.
* IE out of range or in an unloaded dimension.
*/ */
private static class ReferencedFutureWrapper private static class ReferencedRenderSourceFutureWrapper
{ {
public final CompletableFuture<ColumnRenderSource> future; public final CompletableFuture<ColumnRenderSource> future;
// starts at 1 since the constructing method is referencing this future // starts at 1 since the constructing method is referencing this future
@@ -611,20 +616,25 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
public ReferencedFutureWrapper(CompletableFuture<ColumnRenderSource> future) { this.future = future; } public ReferencedRenderSourceFutureWrapper(CompletableFuture<ColumnRenderSource> future) { this.future = future; }
public void incrementRefCount() { this.refCount.incrementAndGet(); } public void incrementRefCount() { this.refCount.incrementAndGet(); }
public void decrementRefCount() public void decrementRefCount()
{ {
// automatically clean up this future if no one else is referencing it int refCount = this.refCount.decrementAndGet();
if (this.refCount.decrementAndGet() <= 0) if (refCount <= 0)
{ {
if (this.future != null) // this render section should only be released once
if (refCount < 0)
{ {
if (!this.future.isDone()) LodUtil.assertNotReach("ReferencedRenderSourceFutureWrapper was released more than once! Ref Count ["+refCount+"].");
{
this.future.cancel(true);
} }
// return data source to the pool
ColumnRenderSource source = this.future.getNow(null);
if (source != null)
{
ColumnRenderSource.DATA_SOURCE_POOL.returnPooledDataSource(source);
} }
} }
} }
@@ -636,4 +646,5 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
} }
} }
@@ -76,19 +76,19 @@ public class LodFogConfig
private LodFogConfig(EDhApiFogDrawMode fogDrawMode) private LodFogConfig(EDhApiFogDrawMode fogDrawMode)
{ {
// TODO: Move these out of here // TODO: Move these out of here
earthCurveRatio = Config.Client.Advanced.Graphics.AdvancedGraphics.earthCurveRatio.get(); this.earthCurveRatio = Config.Client.Advanced.Graphics.AdvancedGraphics.earthCurveRatio.get();
noiseEnable = Config.Client.Advanced.Graphics.NoiseTextureSettings.noiseEnabled.get(); this.noiseEnable = Config.Client.Advanced.Graphics.NoiseTextureSettings.noiseEnabled.get();
noiseSteps = Config.Client.Advanced.Graphics.NoiseTextureSettings.noiseSteps.get(); this.noiseSteps = Config.Client.Advanced.Graphics.NoiseTextureSettings.noiseSteps.get();
noiseIntensity = Config.Client.Advanced.Graphics.NoiseTextureSettings.noiseIntensity.get().floatValue(); this.noiseIntensity = Config.Client.Advanced.Graphics.NoiseTextureSettings.noiseIntensity.get().floatValue();
noiseDropoff = Config.Client.Advanced.Graphics.NoiseTextureSettings.noiseDropoff.get(); this.noiseDropoff = Config.Client.Advanced.Graphics.NoiseTextureSettings.noiseDropoff.get();
if (fogDrawMode != EDhApiFogDrawMode.FOG_DISABLED) if (fogDrawMode != EDhApiFogDrawMode.FOG_DISABLED)
{ {
// fog should be drawn // fog should be drawn
farFogSetting = new FogSettings( this.farFogSetting = new FogSettings(
Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogStart.get(), Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogStart.get(),
Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogEnd.get(), Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogEnd.get(),
Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogMin.get(), Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogMin.get(),
@@ -97,20 +97,20 @@ public class LodFogConfig
Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogFalloff.get() Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogFalloff.get()
); );
heightFogMixMode = Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMixMode.get(); this.heightFogMixMode = Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMixMode.get();
if (heightFogMixMode == EDhApiHeightFogMixMode.IGNORE_HEIGHT || heightFogMixMode == EDhApiHeightFogMixMode.BASIC) if (this.heightFogMixMode == EDhApiHeightFogMixMode.IGNORE_HEIGHT || this.heightFogMixMode == EDhApiHeightFogMixMode.BASIC)
{ {
// basic fog mixing // basic fog mixing
heightFogSetting = null; this.heightFogSetting = null;
heightFogMode = null; this.heightFogMode = null;
heightFogHeight = 0.f; this.heightFogHeight = 0.f;
} }
else else
{ {
// advanced fog mixing // advanced fog mixing
heightFogSetting = new FogSettings( this.heightFogSetting = new FogSettings(
Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogDensity.get(), Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogDensity.get(),
Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogEnd.get(), Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogEnd.get(),
Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMin.get(), Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMin.get(),
@@ -119,15 +119,15 @@ public class LodFogConfig
Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogFalloff.get() Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogFalloff.get()
); );
heightFogMode = Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMode.get(); this.heightFogMode = Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMode.get();
if (heightFogMode.basedOnCamera) if (this.heightFogMode.basedOnCamera)
{ {
heightFogHeight = 0.f; this.heightFogHeight = 0.f;
} }
else else
{ {
heightFogHeight = Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogBaseHeight.get().floatValue(); this.heightFogHeight = Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogBaseHeight.get().floatValue();
} }
} }
} }
@@ -135,11 +135,11 @@ public class LodFogConfig
{ {
// fog disabled // fog disabled
farFogSetting = null; this.farFogSetting = null;
heightFogMixMode = null; this.heightFogMixMode = null;
heightFogMode = null; this.heightFogMode = null;
heightFogSetting = null; this.heightFogSetting = null;
heightFogHeight = 0.f; this.heightFogHeight = 0.f;
} }
} }
@@ -177,7 +177,7 @@ public class LodFogConfig
"} \n"); "} \n");
if (farFogSetting == null) if (this.farFogSetting == null)
{ {
str.append("\n" + str.append("\n" +
"float getFarFogThickness(float dist) { return 0.0; } \n" + "float getFarFogThickness(float dist) { return 0.0; } \n" +
@@ -195,7 +195,7 @@ public class LodFogConfig
str.append("" + str.append("" +
"float getFarFogThickness(float dist) \n" + "float getFarFogThickness(float dist) \n" +
"{ \n" + "{ \n" +
getFarFogMethod(farFogSetting.fogType) + "\n" + getFarFogMethod(this.farFogSetting.fogType) + "\n" +
"} \n"); "} \n");
@@ -203,7 +203,7 @@ public class LodFogConfig
str.append("" + str.append("" +
"float getHeightFogThickness(float dist) \n" + "float getHeightFogThickness(float dist) \n" +
"{ \n" + "{ \n" +
(heightFogSetting != null ? getHeightFogMethod(heightFogSetting.fogType) : " return 0.0;") + "\n" + (this.heightFogSetting != null ? getHeightFogMethod(this.heightFogSetting.fogType) : " return 0.0;") + "\n" +
"} \n"); "} \n");
@@ -211,7 +211,7 @@ public class LodFogConfig
str.append("" + str.append("" +
"float calculateHeightFogDepth(float vertical, float realY) \n" + "float calculateHeightFogDepth(float vertical, float realY) \n" +
"{ \n" + "{ \n" +
(heightFogSetting != null ? getHeightDepthMethod(heightFogMode, heightFogHeight) : " return 0.0;") + "\n" + (this.heightFogSetting != null ? getHeightDepthMethod(this.heightFogMode, this.heightFogHeight) : " return 0.0;") + "\n" +
"} \n"); "} \n");
@@ -219,7 +219,7 @@ public class LodFogConfig
str.append("" + str.append("" +
"float calculateFarFogDepth(float horizontal, float dist, float uNearFogStart) \n" + "float calculateFarFogDepth(float horizontal, float dist, float uNearFogStart) \n" +
"{ \n" + "{ \n" +
" return " + (heightFogMixMode == EDhApiHeightFogMixMode.BASIC ? " return " + (this.heightFogMixMode == EDhApiHeightFogMixMode.BASIC ?
"(dist - uNearFogStart)/(1.0 - uNearFogStart);" : "(dist - uNearFogStart)/(1.0 - uNearFogStart);" :
"(horizontal - uNearFogStart)/(1.0 - uNearFogStart);") + "(horizontal - uNearFogStart)/(1.0 - uNearFogStart);") +
"} \n"); "} \n");
@@ -228,7 +228,7 @@ public class LodFogConfig
str.append("" + str.append("" +
"float mixFogThickness(float near, float far, float height) \n" + "float mixFogThickness(float near, float far, float height) \n" +
"{ \n" + "{ \n" +
getMixFogLine(heightFogMixMode) + "\n" + getMixFogLine(this.heightFogMixMode) + "\n" +
"} \n"); "} \n");
} }
} }
@@ -398,23 +398,27 @@ public class LodFogConfig
public boolean equals(Object o) public boolean equals(Object o)
{ {
if (this == o) if (this == o)
{
return true; return true;
if (o == null || getClass() != o.getClass()) }
if (o == null || this.getClass() != o.getClass())
{
return false; return false;
}
LodFogConfig that = (LodFogConfig) o; LodFogConfig that = (LodFogConfig) o;
return Float.compare(that.heightFogHeight, heightFogHeight) == 0 && return Float.compare(that.heightFogHeight, this.heightFogHeight) == 0 &&
Objects.equals(farFogSetting, that.farFogSetting) && Objects.equals(this.farFogSetting, that.farFogSetting) &&
Objects.equals(heightFogSetting, that.heightFogSetting) && heightFogMixMode == that.heightFogMixMode && Objects.equals(this.heightFogSetting, that.heightFogSetting) && this.heightFogMixMode == that.heightFogMixMode &&
heightFogMode == that.heightFogMode this.heightFogMode == that.heightFogMode
// TODO: Move these out of here // TODO: Move these out of here
&& earthCurveRatio == that.earthCurveRatio && this.earthCurveRatio == that.earthCurveRatio
&& noiseEnable == that.noiseEnable && noiseSteps == that.noiseSteps && noiseIntensity == that.noiseIntensity && noiseDropoff == that.noiseDropoff; && this.noiseEnable == that.noiseEnable && this.noiseSteps == that.noiseSteps && this.noiseIntensity == that.noiseIntensity && this.noiseDropoff == that.noiseDropoff;
} }
@Override @Override
public int hashCode() public int hashCode()
{ {
return Objects.hash(farFogSetting, heightFogSetting, heightFogMixMode, heightFogMode, heightFogHeight, earthCurveRatio, noiseEnable, noiseSteps, noiseIntensity, noiseDropoff); return Objects.hash(this.farFogSetting, this.heightFogSetting, this.heightFogMixMode, this.heightFogMode, this.heightFogHeight, this.earthCurveRatio, this.noiseEnable, this.noiseSteps, this.noiseIntensity, this.noiseDropoff);
} }
} }
@@ -130,8 +130,12 @@ public class DhTerrainShaderProgram extends ShaderProgram implements IDhApiShade
throw e; throw e;
} }
if (this.uEarthRadius != -1) this.setUniform(this.uEarthRadius, if (this.uEarthRadius != -1)
{
this.setUniform(this.uEarthRadius,
/*6371KM*/ 6371000.0f / Config.Client.Advanced.Graphics.AdvancedGraphics.earthCurveRatio.get()); /*6371KM*/ 6371000.0f / Config.Client.Advanced.Graphics.AdvancedGraphics.earthCurveRatio.get());
}
// Noise Uniforms // Noise Uniforms
@@ -185,7 +189,10 @@ 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); if (this.uWorldYOffset != -1)
{
this.setUniform(this.uWorldYOffset, (float) renderParameters.worldYOffset);
}
// Debug // Debug
this.setUniform(this.uWhiteWorld, Config.Client.Advanced.Debugging.enableWhiteWorld.get()); this.setUniform(this.uWhiteWorld, Config.Client.Advanced.Debugging.enableWhiteWorld.get());
@@ -845,15 +845,23 @@ public class LodRenderer
} }
if (this.quadIBO != null) if (this.quadIBO != null)
{
this.quadIBO.destroyAsync(); this.quadIBO.destroyAsync();
}
// Delete framebuffer, color texture, and depth texture // Delete framebuffer, color texture, and depth texture
if (this.framebuffer != null && !this.usingMcFrameBuffer) if (this.framebuffer != null && !this.usingMcFrameBuffer)
{
this.framebuffer.destroy(); this.framebuffer.destroy();
}
if (this.nullableColorTexture != null) if (this.nullableColorTexture != null)
{
this.nullableColorTexture.destroy(); this.nullableColorTexture.destroy();
}
if (this.depthTexture != null) if (this.depthTexture != null)
{
this.depthTexture.destroy(); this.depthTexture.destroy();
}
EVENT_LOGGER.info("Renderer Cleanup Complete"); EVENT_LOGGER.info("Renderer Cleanup Complete");
}); });
@@ -115,14 +115,32 @@ public class FogShader extends AbstractShaderRenderer
int lodDrawDistance = Config.Client.Advanced.Graphics.Quality.lodChunkRenderDistanceRadius.get() * LodUtil.CHUNK_WIDTH; int lodDrawDistance = Config.Client.Advanced.Graphics.Quality.lodChunkRenderDistanceRadius.get() * LodUtil.CHUNK_WIDTH;
// Fog // Fog
if (this.uFullFogMode != -1) this.shader.setUniform(this.uFullFogMode, MC_RENDER.isFogStateSpecial() ? 1 : 0); if (this.uFullFogMode != -1)
if (this.uFogColor != -1) this.shader.setUniform(this.uFogColor, MC_RENDER.isFogStateSpecial() ? this.getSpecialFogColor(partialTicks) : this.getFogColor(partialTicks)); {
this.shader.setUniform(this.uFullFogMode, MC_RENDER.isFogStateSpecial() ? 1 : 0);
}
if (this.uFogColor != -1)
{
this.shader.setUniform(this.uFogColor, MC_RENDER.isFogStateSpecial() ? this.getSpecialFogColor(partialTicks) : this.getFogColor(partialTicks));
}
float nearFogStart = (VERSION_CONSTANTS.isVanillaRenderedChunkSquare() ? (float) Math.sqrt(2.0) : 1.0f) / lodDrawDistance; float nearFogStart = (VERSION_CONSTANTS.isVanillaRenderedChunkSquare() ? (float) Math.sqrt(2.0) : 1.0f) / lodDrawDistance;
if (this.uNearFogStart != -1) this.shader.setUniform(this.uNearFogStart, nearFogStart); if (this.uNearFogStart != -1)
if (this.uNearFogLength != -1) this.shader.setUniform(this.uNearFogLength, 0.0f); {
if (this.uFogScale != -1) this.shader.setUniform(this.uFogScale, 1.f / lodDrawDistance); this.shader.setUniform(this.uNearFogStart, nearFogStart);
if (this.uFogVerticalScale != -1) this.shader.setUniform(this.uFogVerticalScale, 1.f / MC.getWrappedClientLevel().getMaxHeight()); }
if (this.uNearFogLength != -1)
{
this.shader.setUniform(this.uNearFogLength, 0.0f);
}
if (this.uFogScale != -1)
{
this.shader.setUniform(this.uFogScale, 1.f / lodDrawDistance);
}
if (this.uFogVerticalScale != -1)
{
this.shader.setUniform(this.uFogVerticalScale, 1.f / MC.getWrappedClientLevel().getMaxHeight());
}
} }
private Color getFogColor(float partialTicks) private Color getFogColor(float partialTicks)
{ {
@@ -94,7 +94,8 @@ public class FullDataSourceV2DTO implements IBaseDTO<Long>, INetworkObject
} }
/** Should only be used for subsequent decoding */ /** Should only be used for subsequent decoding */
public FullDataSourceV2DTO() { } public static FullDataSourceV2DTO CreateEmptyDataSource() { return new FullDataSourceV2DTO(); }
private FullDataSourceV2DTO() { }
public FullDataSourceV2DTO( public FullDataSourceV2DTO(
long pos, long pos,
@@ -148,7 +149,7 @@ public class FullDataSourceV2DTO implements IBaseDTO<Long>, INetworkObject
{ {
if (FullDataSourceV2.DATA_FORMAT_VERSION != this.dataFormatVersion) if (FullDataSourceV2.DATA_FORMAT_VERSION != this.dataFormatVersion)
{ {
throw new IllegalStateException("There should only be one data format [" + FullDataSourceV2.DATA_FORMAT_VERSION + "]."); throw new IllegalStateException("There should only be one data format ["+FullDataSourceV2.DATA_FORMAT_VERSION+"].");
} }
@@ -357,6 +358,12 @@ public class FullDataSourceV2DTO implements IBaseDTO<Long>, INetworkObject
return mapping; return mapping;
} }
//============//
// networking //
//============//
@Override @Override
public void encode(ByteBuf out) public void encode(ByteBuf out)
{ {
@@ -220,7 +220,7 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
} }
catch (DbConnectionClosedException ignored) catch (DbConnectionClosedException ignored)
{ {
LOGGER.warn("Attempted to insert ["+this.dtoClass.getSimpleName()+"] with primary key ["+(dto != null ? dto.getKeyDisplayString() : "NULL")+"] on closed repo ["+this.connectionString+"]."); //LOGGER.warn("Attempted to insert ["+this.dtoClass.getSimpleName()+"] with primary key ["+(dto != null ? dto.getKeyDisplayString() : "NULL")+"] on closed repo ["+this.connectionString+"].");
} }
catch (SQLException e) catch (SQLException e)
{ {
@@ -237,7 +237,7 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
} }
catch (DbConnectionClosedException e) catch (DbConnectionClosedException e)
{ {
LOGGER.warn("Attempted to update ["+this.dtoClass.getSimpleName()+"] with primary key ["+(dto != null ? dto.getKeyDisplayString() : "NULL")+"] on closed repo ["+this.connectionString+"]."); //LOGGER.warn("Attempted to update ["+this.dtoClass.getSimpleName()+"] with primary key ["+(dto != null ? dto.getKeyDisplayString() : "NULL")+"] on closed repo ["+this.connectionString+"].");
} }
catch (SQLException e) catch (SQLException e)
{ {
@@ -283,6 +283,17 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
return new ArrayList<>(); return new ArrayList<>();
} }
} }
public List<Map<String, Object>> queryDictionary(PreparedStatement preparedStatement)
{
try
{
return this.query(preparedStatement);
}
catch (DbConnectionClosedException e)
{
return new ArrayList<>();
}
}
@Nullable @Nullable
public Map<String, Object> queryDictionaryFirst(String sql) public Map<String, Object> queryDictionaryFirst(String sql)
{ {
@@ -296,10 +307,23 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
return null; return null;
} }
} }
@Nullable
public Map<String, Object> queryDictionaryFirst(PreparedStatement preparedStatement)
{
try
{
List<Map<String, Object>> objectList = this.query(preparedStatement);
return !objectList.isEmpty() ? objectList.get(0) : null;
}
catch (DbConnectionClosedException e)
{
return null;
}
}
/** note: this can only handle 1 command at a time */ /** note: this can only handle 1 command at a time */
public List<Map<String, Object>> query(PreparedStatement statement) throws RuntimeException, DbConnectionClosedException private List<Map<String, Object>> query(PreparedStatement statement) throws RuntimeException, DbConnectionClosedException
{ {
try try
{ {
@@ -559,7 +583,7 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
@Nullable @Nullable
public abstract TDTO convertDictionaryToDto(Map<String, Object> objectMap) throws ClassCastException; public abstract TDTO convertDictionaryToDto(Map<String, Object> objectMap) throws ClassCastException;
public String createSelectByKeySql(TKey key) { return "SELECT * FROM " + this.getTableName() + " WHERE " + this.createWhereStatement(key); } public String createSelectByKeySql(TKey key) { return "SELECT * FROM "+this.getTableName()+" WHERE "+this.createWhereStatement(key); }
/** /**
* Example: * Example:
* <code> Id = '0' </code> * <code> Id = '0' </code>
@@ -27,6 +27,7 @@ import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream; import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
@@ -35,6 +36,7 @@ import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2DTO> public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2DTO>
{ {
@@ -112,7 +114,7 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
public PreparedStatement createInsertStatement(FullDataSourceV2DTO dto) throws SQLException public PreparedStatement createInsertStatement(FullDataSourceV2DTO dto) throws SQLException
{ {
String sql = String sql =
"INSERT INTO " + this.getTableName() + " (\n" + "INSERT INTO "+this.getTableName() + " (\n" +
" DetailLevel, PosX, PosZ, \n" + " DetailLevel, PosX, PosZ, \n" +
" MinY, DataChecksum, \n" + " MinY, DataChecksum, \n" +
" Data, ColumnGenerationStep, ColumnWorldCompressionMode, Mapping, \n" + " Data, ColumnGenerationStep, ColumnWorldCompressionMode, Mapping, \n" +
@@ -154,7 +156,7 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
public PreparedStatement createUpdateStatement(FullDataSourceV2DTO dto) throws SQLException public PreparedStatement createUpdateStatement(FullDataSourceV2DTO dto) throws SQLException
{ {
String sql = String sql =
"UPDATE " + this.getTableName() + " \n" + "UPDATE "+this.getTableName()+" \n" +
"SET \n" + "SET \n" +
" MinY = ? \n" + " MinY = ? \n" +
" ,DataChecksum = ? \n" + " ,DataChecksum = ? \n" +
@@ -199,15 +201,17 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
//=========//
// updates // // updates //
//=========//
public void setApplyToParent(long pos, boolean applyToParent) throws SQLException public void setApplyToParent(long pos, boolean applyToParent) throws SQLException
{ {
int detailLevel = DhSectionPos.getDetailLevel(pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; int detailLevel = DhSectionPos.getDetailLevel(pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL;
String sql = String sql =
"UPDATE " + this.getTableName() + " \n" + "UPDATE "+this.getTableName()+" \n" +
"SET ApplyToParent = " + applyToParent + " \n" + "SET ApplyToParent = "+applyToParent+" \n" +
"WHERE DetailLevel = "+detailLevel+" AND PosX = "+ DhSectionPos.getX(pos)+" AND PosZ = "+ DhSectionPos.getZ(pos); "WHERE DetailLevel = "+detailLevel+" AND PosX = "+ DhSectionPos.getX(pos)+" AND PosZ = "+ DhSectionPos.getZ(pos);
this.queryDictionaryFirst(sql); this.queryDictionaryFirst(sql);
@@ -219,9 +223,9 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
List<Map<String, Object>> resultMapList = this.queryDictionary( List<Map<String, Object>> resultMapList = this.queryDictionary(
"select DetailLevel, PosX, PosZ " + "select DetailLevel, PosX, PosZ " +
"from " + this.getTableName() + " " + "from "+this.getTableName()+" " +
"where ApplyToParent = 1 " + "where ApplyToParent = 1 " +
"order by DetailLevel asc LIMIT " + returnCount + ";"); "order by DetailLevel asc LIMIT "+returnCount+";");
for (Map<String, Object> resultMap : resultMapList) for (Map<String, Object> resultMap : resultMapList)
{ {
@@ -244,7 +248,7 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
Map<String, Object> resultMap = this.queryDictionaryFirst( Map<String, Object> resultMap = this.queryDictionaryFirst(
"select ColumnGenerationStep, CompressionMode " + "select ColumnGenerationStep, CompressionMode " +
"from " + this.getTableName() + " " + "from "+this.getTableName()+" " +
"WHERE DetailLevel = "+detailLevel+" AND PosX = "+ DhSectionPos.getX(pos)+" AND PosZ = "+ DhSectionPos.getZ(pos)); "WHERE DetailLevel = "+detailLevel+" AND PosX = "+ DhSectionPos.getX(pos)+" AND PosZ = "+ DhSectionPos.getZ(pos));
if (resultMap != null) if (resultMap != null)
@@ -279,6 +283,70 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
//=============//
// multiplayer //
//=============//
@Nullable
public Long getTimestampForPos(long pos)
{
try
{
PreparedStatement preparedStatement = this.createPreparedStatement(
"SELECT LastModifiedUnixDateTime " +
"FROM " + this.getTableName() + " " +
"WHERE DetailLevel = ? " +
"AND PosX = ? " +
"AND PosZ = ?;"
);
int i = 1;
preparedStatement.setInt(i++, DhSectionPos.getDetailLevel(pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
preparedStatement.setInt(i++, DhSectionPos.getX(pos));
preparedStatement.setInt(i++, DhSectionPos.getZ(pos));
List<Map<String, Object>> row = this.queryDictionary(preparedStatement);
return !row.isEmpty() ? (Long) row.get(0).get("LastModifiedUnixDateTime") : null;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
public Map<Long, Long> getTimestampsForRange(byte detailLevel, int startPosX, int startPosZ, int endPosX, int endPosZ)
{
try
{
PreparedStatement preparedStatement = this.createPreparedStatement(
"SELECT PosX, PosZ, LastModifiedUnixDateTime " +
"FROM " + this.getTableName() + " " +
"WHERE DetailLevel = ? " +
"AND PosX BETWEEN ? AND ? " +
"AND PosZ BETWEEN ? AND ?;"
);
int i = 1;
preparedStatement.setInt(i++, detailLevel - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
preparedStatement.setInt(i++, startPosX);
preparedStatement.setInt(i++, endPosX);
preparedStatement.setInt(i++, startPosZ);
preparedStatement.setInt(i++, endPosZ);
return this.queryDictionary(preparedStatement)
.stream().collect(Collectors.toMap(
row -> DhSectionPos.encode(detailLevel, (int) row.get("PosX"), (int) row.get("PosZ")),
row -> (long) row.get("LastModifiedUnixDateTime"))
);
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
//===================// //===================//
// compression tests // // compression tests //
//===================// //===================//
@@ -290,7 +358,7 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
List<Map<String, Object>> resultMapList = this.queryDictionary( List<Map<String, Object>> resultMapList = this.queryDictionary(
"select DetailLevel, PosX, PosZ " + "select DetailLevel, PosX, PosZ " +
"from " + this.getTableName() + "; "); "from "+this.getTableName()+"; ");
for (Map<String, Object> resultMap : resultMapList) for (Map<String, Object> resultMap : resultMapList)
{ {
@@ -316,7 +384,7 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
Map<String, Object> resultMap = this.queryDictionaryFirst( Map<String, Object> resultMap = this.queryDictionaryFirst(
"select LENGTH(Data) as dataSize " + "select LENGTH(Data) as dataSize " +
"from " + this.getTableName() + " " + "from "+this.getTableName()+" " +
"WHERE DetailLevel = "+detailLevel+" AND PosX = "+ DhSectionPos.getX(pos)+" AND PosZ = "+ DhSectionPos.getZ(pos)); "WHERE DetailLevel = "+detailLevel+" AND PosX = "+ DhSectionPos.getX(pos)+" AND PosZ = "+ DhSectionPos.getZ(pos));
if (resultMap != null && resultMap.get("dataSize") != null) if (resultMap != null && resultMap.get("dataSize") != null)
@@ -338,7 +406,7 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
{ {
Map<String, Object> resultMap = this.queryDictionaryFirst( Map<String, Object> resultMap = this.queryDictionaryFirst(
"select SUM(LENGTH(Data)) as dataSize " + "select SUM(LENGTH(Data)) as dataSize " +
"from " + this.getTableName() + "; "); "from "+this.getTableName()+"; ");
if (resultMap != null && resultMap.get("dataSize") != null) if (resultMap != null && resultMap.get("dataSize") != null)
{ {

Some files were not shown because too many files have changed in this diff Show More