From 2528f4a7255b9a2c0c29d22ebe91c86506a16ac2 Mon Sep 17 00:00:00 2001 From: James Seibel Date: Sun, 15 Sep 2024 20:35:38 -0500 Subject: [PATCH] Merge server side branch and refactor --- .../distanthorizons/coreapi/ModInfo.java | 15 +- .../core/api/internal/ClientApi.java | 285 ++++------- .../api/internal/ClientPluginChannelApi.java | 134 ++++++ .../core/api/internal/ServerApi.java | 31 +- .../core/api/internal/SharedApi.java | 6 +- .../distanthorizons/core/config/Config.java | 192 ++++++-- .../core/config/file/ConfigFileHandling.java | 68 +-- .../core/config/types/ConfigEntry.java | 40 +- .../types/enums/EConfigEntryRelevantSide.java | 36 ++ .../FullDataSourceProviderV2.java | 51 +- .../GeneratedFullDataSourceProvider.java | 14 +- .../RemoteFullDataSourceProvider.java | 112 ++++- .../structure/ClientOnlySaveStructure.java | 2 +- .../SubDimensionLevelMatcher.java | 3 +- .../IFullDataSourceRetrievalQueue.java | 22 +- .../generation/RemoteWorldRetrievalQueue.java | 84 ++++ .../core/generation/WorldGenerationQueue.java | 9 +- .../tasks/IWorldGenTaskTracker.java | 2 +- .../generation/tasks/WorldGenTaskGroup.java | 2 +- .../core/jar/DarkModeDetector.java | 2 +- .../core/level/DhClientLevel.java | 229 ++++++++- .../core/level/DhClientServerLevel.java | 9 +- .../core/level/DhServerLevel.java | 450 ++++++++++++++++-- .../distanthorizons/core/level/IDhLevel.java | 5 +- .../core/level/IDhServerLevel.java | 3 +- .../core/level/IKeyedClientLevelManager.java | 14 +- .../core/level/WorldGenModule.java | 39 +- .../AbstractFullDataNetworkRequestQueue.java | 407 ++++++++++++++++ .../client/ClientNetworkState.java | 210 ++++++++ .../client/SyncOnLoginRequestQueue.java | 57 +++ .../multiplayer/config/SessionConfig.java | 184 +++++++ .../server/RemotePlayerConnectionHandler.java | 85 ++++ .../multiplayer/server/ServerPlayerState.java | 123 +++++ .../core/network/INetworkObject.java | 228 +++++++++ .../event/AbstractNetworkEventSource.java | 235 +++++++++ .../event/ScopedNetworkEventSource.java | 81 ++++ .../event/internal/AbstractInternalEvent.java | 17 + .../event/internal/CloseInternalEvent.java | 9 + .../IncompatibleMessageInternalEvent.java | 15 + .../internal/ProtocolErrorInternalEvent.java | 23 + .../exceptions/InvalidLevelException.java | 26 + .../exceptions/RateLimitedException.java} | 9 +- .../exceptions/RequestRejectedException.java | 11 + .../messages/AbstractNetworkMessage.java | 31 ++ .../messages/AbstractTrackableMessage.java | 162 +++++++ .../messages/ILevelRelatedMessage.java | 23 + .../network/messages/MessageRegistry.java | 129 +++++ .../messages/base/CloseReasonMessage.java | 67 +++ .../messages/base/CodecCrashMessage.java | 86 ++++ .../messages/base/CurrentLevelKeyMessage.java | 45 ++ .../messages/base/SessionConfigMessage.java | 67 +++ .../FullDataPartialUpdateMessage.java | 84 ++++ .../messages/fullData/FullDataPayload.java | 127 +++++ .../FullDataSourceRequestMessage.java | 96 ++++ .../FullDataSourceResponseMessage.java | 83 ++++ .../fullData/FullDataSplitMessage.java | 94 ++++ .../messages/requests/CancelMessage.java | 36 ++ .../messages/requests/ExceptionMessage.java | 88 ++++ .../core/network/session/NetworkSession.java | 174 +++++++ .../session/SessionClosedException.java | 10 + .../core/sql/dto/FullDataSourceV2DTO.java | 86 +++- .../SupplierBasedConcurrencyLimiter.java | 53 +++ ...upplierBasedRateAndConcurrencyLimiter.java | 55 +++ .../SupplierBasedRateLimiter.java | 77 +++ .../core/world/DhClientServerWorld.java | 4 +- .../core/world/DhClientWorld.java | 62 +-- .../core/world/DhServerWorld.java | 127 ++--- .../core/world/IDhClientWorld.java | 2 - .../core/world/IDhServerWorld.java | 1 - .../distanthorizons/core/world/IDhWorld.java | 2 + .../minecraft/IMinecraftClientWrapper.java | 5 + .../minecraft/IMinecraftSharedWrapper.java | 3 + .../misc/IPluginPacketSender.java | 13 + .../misc/IServerPlayerWrapper.java | 15 +- .../world/IServerLevelWrapper.java | 18 +- .../assets/distanthorizons/lang/en_us.json | 24 +- 76 files changed, 4993 insertions(+), 535 deletions(-) create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientPluginChannelApi.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/config/types/enums/EConfigEntryRelevantSide.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/generation/RemoteWorldRetrievalQueue.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/AbstractFullDataNetworkRequestQueue.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/ClientNetworkState.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/SyncOnLoginRequestQueue.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/multiplayer/config/SessionConfig.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/RemotePlayerConnectionHandler.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/ServerPlayerState.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/INetworkObject.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/event/AbstractNetworkEventSource.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/event/ScopedNetworkEventSource.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/event/internal/AbstractInternalEvent.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/event/internal/CloseInternalEvent.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/event/internal/IncompatibleMessageInternalEvent.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/event/internal/ProtocolErrorInternalEvent.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/exceptions/InvalidLevelException.java rename core/src/main/java/com/seibel/distanthorizons/core/{level/IDhWorldGenLevel.java => network/exceptions/RateLimitedException.java} (74%) create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/exceptions/RequestRejectedException.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/messages/AbstractNetworkMessage.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/messages/AbstractTrackableMessage.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/messages/ILevelRelatedMessage.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/messages/MessageRegistry.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/CloseReasonMessage.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/CodecCrashMessage.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/CurrentLevelKeyMessage.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/SessionConfigMessage.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataPartialUpdateMessage.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataPayload.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataSourceRequestMessage.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataSourceResponseMessage.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataSplitMessage.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/messages/requests/CancelMessage.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/messages/requests/ExceptionMessage.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/session/NetworkSession.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/network/session/SessionClosedException.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/util/ratelimiting/SupplierBasedConcurrencyLimiter.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/util/ratelimiting/SupplierBasedRateAndConcurrencyLimiter.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/util/ratelimiting/SupplierBasedRateLimiter.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/misc/IPluginPacketSender.java diff --git a/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java b/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java index 92282b9d4..4ebef2246 100644 --- a/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java +++ b/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java @@ -26,10 +26,14 @@ package com.seibel.distanthorizons.coreapi; public final class ModInfo { public static final String ID = "distanthorizons"; - /** The internal protocol version used for networking */ - public static final int PROTOCOL_VERSION = 1; - /** The protocol version used for multiverse networking */ - public static final int MULTIVERSE_PLUGIN_PROTOCOL_VERSION = 1; + + public static final String RESOURCE_NAMESPACE = "distant_horizons"; + public static final String DEDICATED_SERVER_INITIAL_PATH = "dedicated_server_initial"; + + /** Incremented every time any packets are added, changed or removed, with a few exceptions. */ + public static final int PROTOCOL_VERSION = 3; + public static final String WRAPPER_PACKET_PATH = "message"; + /** The internal mod name */ public static final String NAME = "DistantHorizons"; /** Human-readable version of NAME */ @@ -45,9 +49,6 @@ public final class ModInfo /** 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 String NETWORKING_RESOURCE_NAMESPACE = "distant_horizons"; - public static final String MULTIVERSE_PLUGIN_NAMESPACE = "world_control"; - /** All DH owned threads should start with this string to allow for easier debugging and profiling. */ public static final String THREAD_NAME_PREFIX = "DH-"; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java index 7e49a4c80..1984d2b36 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java @@ -24,18 +24,20 @@ import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass; import com.seibel.distanthorizons.api.methods.events.abstractEvents.*; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure; -import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager; import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.render.DhApiRenderProxy; +import com.seibel.distanthorizons.core.util.TimerUtil; import com.seibel.distanthorizons.core.util.objects.Pair; -import com.seibel.distanthorizons.core.world.*; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.config.Config; +import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; +import com.seibel.distanthorizons.core.network.session.NetworkSession; import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; +import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel; import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.ConfigBasedSpamLogger; import com.seibel.distanthorizons.core.logging.SpamReducedLogger; @@ -43,21 +45,21 @@ import com.seibel.distanthorizons.core.util.math.Mat4f; import com.seibel.distanthorizons.core.render.glObject.GLProxy; import com.seibel.distanthorizons.core.render.renderer.TestRenderer; import com.seibel.distanthorizons.core.util.RenderUtil; +import com.seibel.distanthorizons.core.world.AbstractDhWorld; +import com.seibel.distanthorizons.core.world.DhClientServerWorld; +import com.seibel.distanthorizons.core.world.DhClientWorld; +import com.seibel.distanthorizons.core.world.IDhClientWorld; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; -import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; -//import io.netty.buffer.ByteBuf; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; import org.lwjgl.glfw.GLFW; import java.io.File; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Queue; +import java.util.*; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -75,9 +77,7 @@ public class ClientApi public static final ClientApi INSTANCE = new ClientApi(); public static final TestRenderer TEST_RENDERER = new TestRenderer(); - private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); - private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); - private static final IKeyedClientLevelManager KEYED_CLIENT_LEVEL_MANAGER = SingletonInjector.INSTANCE.get(IKeyedClientLevelManager.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); @@ -90,10 +90,11 @@ public class ClientApi private long lastFlushNanoTime = 0; - private boolean isServerCommunicationEnabled = false; + private final ClientPluginChannelApi pluginChannelApi = new ClientPluginChannelApi(this::clientLevelLoadEvent, this::clientLevelUnloadEvent); - /** set to true if any unexpected responses are received from the server */ - private boolean serverNetworkingIsMalformed = false; + /** Delay loading the first level to give the server some time to respond with level to actually load */ + private Timer firstLevelLoadTimer; + 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. */ public final HashSet waitingClientLevels = new HashSet<>(); @@ -127,8 +128,8 @@ public class ClientApi public synchronized void onClientOnlyConnected() { // only continue if the client is connected to a different server - boolean connectedToServer = MC.clientConnectedToDedicatedServer(); - boolean connectedToReplay = MC.connectedToReplay(); + boolean connectedToServer = MC_CLIENT.clientConnectedToDedicatedServer(); + boolean connectedToReplay = MC_CLIENT.connectedToReplay(); if (connectedToServer || connectedToReplay) { if (connectedToServer) @@ -141,19 +142,22 @@ public class ClientApi if (Config.Client.Advanced.Logging.showReplayWarningOnStartup.get()) { - MC.sendChatMessage("\u00A76" + "Distant Horizons: Replay detected." + "\u00A7r"); // gold color - MC.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.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.sendChatMessage(""); + MC_CLIENT.sendChatMessage("\u00A76" + "Distant Horizons: Replay detected." + "\u00A7r"); // gold color + MC_CLIENT.sendChatMessage("DH may behave strangely or have missing functionality."); + MC_CLIENT.sendChatMessage("In order to use pre-generated LODs, put your DH database(s) in:"); + MC_CLIENT.sendChatMessage("\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("This can be disabled in DH's config under Advanced -> Logging."); + MC_CLIENT.sendChatMessage(""); } } // firing after clientLevelLoadEvent // TODO if level has prepped to load it should fire level load event - SharedApi.setDhWorld(new DhClientWorld()); + DhClientWorld world = new DhClientWorld(); + SharedApi.setDhWorld(world); + this.pluginChannelApi.onJoinServer(world.networkState.getSession()); + world.networkState.sendConfigMessage(); LOGGER.info("Loading [" + this.waitingClientLevels.size() + "] waiting client level wrappers."); for (IClientLevelWrapper level : this.waitingClientLevels) @@ -168,6 +172,13 @@ public class ClientApi /** Synchronized to prevent a rare issue where multiple disconnect events are triggered on top of each other. */ public synchronized void onClientOnlyDisconnected() { + // clear the first time timer + if (this.firstLevelLoadTimer != null) + { + this.firstLevelLoadTimer.cancel(); + this.firstLevelLoadTimer = null; + } + AbstractDhWorld world = SharedApi.getAbstractDhWorld(); if (world != null) { @@ -177,11 +188,7 @@ public class ClientApi SharedApi.setDhWorld(null); } - // clear the previous server's information - this.isServerCommunicationEnabled = false; - this.serverNetworkingIsMalformed = false; - KEYED_CLIENT_LEVEL_MANAGER.setUseOverrideWrapper(false); - KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(null); + this.pluginChannelApi.reset(); // remove any waiting items this.waitingChunkByClientLevelAndPos.clear(); @@ -194,16 +201,16 @@ public class ClientApi // level events // //==============// - public void clientLevelUnloadEvent(@Nullable IClientLevelWrapper level) + public void clientLevelUnloadEvent(IClientLevelWrapper level) { try { - if (level == null) + LOGGER.info("Unloading client level [" + level + "]-["+level.getDimensionName()+"]."); + + if (level instanceof IServerKeyedClientLevel) { - // can happen on certain multiverse servers - return; + this.pluginChannelApi.onClientLevelUnload(); } - LOGGER.info("Unloading client level [" + level + "]-["+level.getDimensionType().getDimensionName()+"]."); AbstractDhWorld world = SharedApi.getAbstractDhWorld(); if (world != null) @@ -223,30 +230,42 @@ public class ClientApi } } - public void clientLevelLoadEvent(@Nullable IClientLevelWrapper level) { this.clientLevelLoadEvent(level, false); } - public void multiverseClientLevelLoadEvent(@Nullable IClientLevelWrapper level) { this.clientLevelLoadEvent(level, true); } - private void clientLevelLoadEvent(@Nullable IClientLevelWrapper level, boolean isServerCommunication) + public void clientLevelLoadEvent(IClientLevelWrapper level) { + // 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) + { + this.firstLevelLoadTimer = TimerUtil.CreateTimer("FirstLevelLoadTimer"); + this.firstLevelLoadTimer.schedule(new TimerTask() + { + @Override + public void run() { ClientApi.this.clientLevelLoadEvent(level); } + }, FIRST_LEVEL_LOAD_DELAY_IN_MS); + return; + } + this.firstLevelLoadTimer.cancel(); + } + + try { - if (this.isServerCommunicationEnabled && !isServerCommunication) - { - LOGGER.info("Server supports communication, deferring loading."); - return; - } - if (level == null) - { - // can happen on certain multiverse servers - return; - } - - - - LOGGER.info("Loading " + (isServerCommunication ? "Multiverse" : "") + " client level [" + level + "]-["+level.getDimensionType().getDimensionName()+"]."); + LOGGER.info("Loading client level [" + level + "]-["+level.getDimensionName()+"]."); AbstractDhWorld world = SharedApi.getAbstractDhWorld(); if (world != null) { + if (!this.pluginChannelApi.allowLevelLoading(level)) + { + LOGGER.info("Levels in this connection are managed by the server, skipping auto-load."); + + // Instead of attempting to load themselves, send the config and wait for a server provided level key. + ((DhClientWorld) world).networkState.sendConfigMessage(); + return; + } + + world.getOrLoadLevel(level); ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level)); @@ -295,7 +314,7 @@ public class ClientApi { LOGGER.info("Renderer shutting down."); - IProfilerWrapper profiler = MC.getProfiler(); + IProfilerWrapper profiler = MC_CLIENT.getProfiler(); profiler.push("DH-RendererShutdown"); profiler.pop(); @@ -305,7 +324,7 @@ public class ClientApi { LOGGER.info("Renderer starting up."); - IProfilerWrapper profiler = MC.getProfiler(); + IProfilerWrapper profiler = MC_CLIENT.getProfiler(); profiler.push("DH-RendererStartup"); // make sure the GLProxy is created before the LodBufferBuilder needs it @@ -315,7 +334,7 @@ public class ClientApi public void clientTickEvent() { - IProfilerWrapper profiler = MC.getProfiler(); + IProfilerWrapper profiler = MC_CLIENT.getProfiler(); profiler.push("DH-ClientTick"); try @@ -337,7 +356,7 @@ public class ClientApi // Ignore local world gen, as it's managed by server ticking if (!(clientWorld instanceof DhClientServerWorld)) { - SharedApi.worldGenTick(clientWorld::doWorldGen); + SharedApi.worldGenTick(clientWorld::worldGenTick); } } } @@ -355,124 +374,15 @@ public class ClientApi //============// // networking // //============// - -// /** @param byteBuf is Netty's {@link ByteBuffer} wrapper. */ -// public void serverMessageReceived(ByteBuf byteBuf) -// { -// if (!Config.Client.Advanced.Multiplayer.enableMultiverseNetworking.get()) -// { -// // multiverse networking disabled, ignore anything sent from the server -// return; -// } -// -// -// -// // either value can be set to true to debug the received byte stream -// boolean stopAndDisplayInputAsByteArray = false; -// boolean stopAndDisplayInputAsString = false; -// if (stopAndDisplayInputAsByteArray || stopAndDisplayInputAsString) -// { -// String messageString = ""; -// if (stopAndDisplayInputAsByteArray) -// { -// int byteCount = byteBuf.readableBytes(); -// byte[] arr = new byte[byteCount]; -// StringBuilder stringBuilder = new StringBuilder("Server message received: ["); -// for (int i = 0; i < byteCount; i++) -// { -// arr[i] = byteBuf.readByte(); -// stringBuilder.append(arr[i]); -// } -// stringBuilder.append("]"); -// -// messageString = stringBuilder.toString(); -// } -// else if (stopAndDisplayInputAsString) -// { -// messageString = byteBuf.toString(StandardCharsets.UTF_8); -// } -// -// // this is logged as an error so it is easier to see in an Intellij log -// LOGGER.error(messageString); -// return; -// } -// -// -// -// -// // It is important to ensure malicious server input is ignored. -// if (this.serverNetworkingIsMalformed) -// { -// return; -// } -// -// // check that the incoming message is within the expected size -// short commandLength = byteBuf.readShort(); -// if (commandLength < 1 || commandLength > 32) -// { -// LOGGER.error("Server command length ["+commandLength+"] outside the expected range of 1 to 32 (inclusive)."); -// ClientApi.INSTANCE.serverNetworkingIsMalformed = true; -// return; -// } -// -// // parse the command -// String eventType; -// try -// { -// eventType = byteBuf.readCharSequence(commandLength, StandardCharsets.UTF_8).toString(); -// } -// catch (Exception e) -// { -// LOGGER.error("Server sent un-parsable command. Error: "+e.getMessage()); -// return; -// } -// -// switch (eventType) -// { -// case "ServerCommsEnabled": -// LOGGER.info("Server supports DH multiverse protocol."); -// ClientApi.INSTANCE.isServerCommunicationEnabled = true; -// KEYED_CLIENT_LEVEL_MANAGER.setUseOverrideWrapper(true); -// MC.executeOnRenderThread(() -> -// { -// // Unload the current world, since it may be wrong. -// // A followup WorldChanged event should be received from the server soon after this. -// LOGGER.info("Unloading current client level so the server can define the correct multiverse level."); -// this.clientLevelUnloadEvent((IClientLevelWrapper) MC.getWrappedClientWorld()); -// }); -// break; -// -// case "LevelChanged": -// short levelKeyLength = byteBuf.readShort(); -// if (levelKeyLength < 1 || levelKeyLength > 128) // TODO 128 should be put into a constant somewhere -// { -// LOGGER.error("Server [LevelChanged] command length ["+commandLength+"] outside the expected range of 1 to 128 (inclusive)."); -// this.serverNetworkingIsMalformed = true; -// return; -// } -// -// String levelKey = byteBuf.readCharSequence(levelKeyLength, StandardCharsets.UTF_8).toString(); -// if (!levelKey.matches("[a-zA-Z0-9_]+")) -// { -// LOGGER.error("Server sent invalid world key name, and is being ignored."); -// this.isServerCommunicationEnabled = false; -// this.serverNetworkingIsMalformed = true; -// return; -// } -// -// LOGGER.info("Server level change event received, changing the level to ["+levelKey+"]."); -// MC.executeOnRenderThread(() -> { -// if (MC.getWrappedClientWorld() != null) -// { -// this.clientLevelUnloadEvent((IClientLevelWrapper) MC.getWrappedClientWorld()); -// } -// IServerKeyedClientLevel clientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel(MC.getWrappedClientWorld(), levelKey); -// KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel); -// this.multiverseClientLevelLoadEvent(clientLevel); -// }); -// break; -// } -// } + + public void pluginMessageReceived(@NotNull AbstractNetworkMessage message) + { + NetworkSession networkSession = this.pluginChannelApi.networkSession; + if (networkSession != null) + { + networkSession.tryHandleMessage(message); + } + } @@ -499,7 +409,7 @@ public class ClientApi this.sendQueuedChatMessages(); - IProfilerWrapper profiler = MC.getProfiler(); + IProfilerWrapper profiler = MC_CLIENT.getProfiler(); profiler.pop(); // get out of "terrain" profiler.push("DH-RenderLevel"); @@ -550,7 +460,12 @@ public class ClientApi { return; } - IDhClientLevel level = dhClientWorld.getOrLoadClientLevel(levelWrapper); + + IDhClientLevel level = (IDhClientLevel) dhClientWorld.getLevel(levelWrapper); + if (level == null) + { + return; + } if (this.rendererDisabledBecauseOfExceptions) @@ -612,10 +527,10 @@ public class ClientApi this.rendererDisabledBecauseOfExceptions = true; 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.sendChatMessage("\u00A74Renderer disabled to try preventing GL state corruption."); - MC.sendChatMessage("\u00A74Toggle DH rendering via the config UI to re-activate DH rendering."); - MC.sendChatMessage("\u00A74Error: " + e); + MC_CLIENT.sendChatMessage("\u00A74\u00A7l\u00A7uERROR: Distant Horizons renderer has encountered an exception!"); + MC_CLIENT.sendChatMessage("\u00A74Renderer disabled to try preventing GL state corruption."); + MC_CLIENT.sendChatMessage("\u00A74Toggle DH rendering via the config UI to re-activate DH rendering."); + MC_CLIENT.sendChatMessage("\u00A74Error: " + e); } finally { @@ -655,24 +570,24 @@ public class ClientApi if (glfwKey == GLFW.GLFW_KEY_F8) { 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) { 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) { 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() { // dev build - if (ModInfo.IS_DEV_BUILD && !this.configOverrideReminderPrinted && MC.playerExists()) + if (ModInfo.IS_DEV_BUILD && !this.configOverrideReminderPrinted && MC_CLIENT.playerExists()) { this.configOverrideReminderPrinted = true; @@ -682,7 +597,7 @@ public class ClientApi "\u00A72" + "Distant Horizons: nightly/unstable build, version: [" + ModInfo.VERSION+"]." + "\u00A7r\n" + "Issues may occur with this version.\n" + "Here be dragons!\n"; - MC.sendChatMessage(message); + MC_CLIENT.sendChatMessage(message); } // memory @@ -703,7 +618,7 @@ public class ClientApi "Stuttering or low FPS may occur. \n" + "Please increase Minecraft's available memory to 4 gigabytes. \n" + "This warning can be disabled in DH's config under Advanced -> Logging. \n"; - MC.sendChatMessage(message); + MC_CLIENT.sendChatMessage(message); } } @@ -716,7 +631,7 @@ public class ClientApi // done to prevent potential null pointers message = ""; } - MC.sendChatMessage(message); + MC_CLIENT.sendChatMessage(message); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientPluginChannelApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientPluginChannelApi.java new file mode 100644 index 000000000..cc45ea3dc --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientPluginChannelApi.java @@ -0,0 +1,134 @@ +package com.seibel.distanthorizons.core.api.internal; + +import com.seibel.distanthorizons.core.config.Config; +import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; +import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager; +import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel; +import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; +import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent; +import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage; +import com.seibel.distanthorizons.core.network.session.NetworkSession; +import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; +import org.apache.logging.log4j.LogManager; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.function.Consumer; + +/** + * This class is used to manage the level keys. + */ +public class ClientPluginChannelApi +{ + private static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(), + () -> Config.Client.Advanced.Logging.logNetworkEvent.get()); + private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); + private static final IKeyedClientLevelManager KEYED_CLIENT_LEVEL_MANAGER = SingletonInjector.INSTANCE.get(IKeyedClientLevelManager.class); + + private final Consumer levelLoadHandler; + private final Consumer levelUnloadHandler; + + @Nullable + public NetworkSession networkSession; + + + + //=============// + // constructor // + //=============// + + public ClientPluginChannelApi(Consumer levelLoadHandler, Consumer levelUnloadHandler) + { + this.levelLoadHandler = levelLoadHandler; + this.levelUnloadHandler = levelUnloadHandler; + } + + + + //============// + // properties // + //============// + + /** @return true if the level loading is handled by the server */ + public boolean allowLevelLoading(IClientLevelWrapper level) + { + return (KEYED_CLIENT_LEVEL_MANAGER.hasLevelSet() && level instanceof IServerKeyedClientLevel) + || !KEYED_CLIENT_LEVEL_MANAGER.hasLevelSet(); + } + + + + //================// + // 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) + { + // prefix@namespace:path + // 1-50 characters in total, all parts except namespace can be omitted + if (!msg.levelKey.matches("^(?=.{1,50}$)([a-zA-Z0-9-_]+@)?[a-zA-Z0-9-_]+(:[a-zA-Z0-9-_]+)?$")) + { + throw new IllegalArgumentException("Server sent invalid level key."); + } + + LOGGER.info("Server level key received: [" + msg.levelKey + "]."); + + MC.executeOnRenderThread(() -> + { + IClientLevelWrapper clientLevel = MC.getWrappedClientLevel(true); + IServerKeyedClientLevel existingKeyedClientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel(); + + if (existingKeyedClientLevel != null) + { + if (!existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey)) + { + LOGGER.info("Unloading previous level with key: [" + existingKeyedClientLevel.getServerLevelKey() + "]."); + this.levelUnloadHandler.accept(existingKeyedClientLevel); + } + else + { + LOGGER.info("Level key matches the previous level key, ignoring the message."); + } + } + else + { + LOGGER.info("Unloading non-keyed level: [" + clientLevel.getDimensionName() + "]."); + this.levelUnloadHandler.accept(clientLevel); + } + + if (existingKeyedClientLevel == null || !existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey)) + { + LOGGER.info("Loading level with key: [" + msg.levelKey + "]."); + IServerKeyedClientLevel keyedLevel = KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel, msg.levelKey); + this.levelLoadHandler.accept(keyedLevel); + } + }); + } + + public void onClientLevelUnload() { KEYED_CLIENT_LEVEL_MANAGER.clearKeyedLevel(); } + + + + //==========// + // shutdown // + //==========// + + private void onClose(CloseInternalEvent event) { this.reset(); } + public void reset() + { + this.networkSession = null; + KEYED_CLIENT_LEVEL_MANAGER.clearKeyedLevel(); + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ServerApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ServerApi.java index 962c7b323..b2aa3e9ec 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ServerApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ServerApi.java @@ -21,13 +21,9 @@ package com.seibel.distanthorizons.core.api.internal; import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelLoadEvent; import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelUnloadEvent; -import com.seibel.distanthorizons.core.config.Config; -import com.seibel.distanthorizons.core.generation.DhLightingEngine; -import com.seibel.distanthorizons.core.util.ThreadUtil; +import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; -import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; -import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.world.AbstractDhWorld; import com.seibel.distanthorizons.core.world.DhClientServerWorld; import com.seibel.distanthorizons.core.world.DhServerWorld; @@ -37,6 +33,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; /** * This holds the methods that should be called by the host mod loader (Fabric, @@ -70,7 +67,7 @@ public class ServerApi if (serverWorld != null) { serverWorld.serverTick(); - SharedApi.worldGenTick(serverWorld::doWorldGen); + SharedApi.worldGenTick(serverWorld::worldGenTick); } } catch (Exception e) @@ -154,7 +151,7 @@ public class ServerApi IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well { - LOGGER.debug("Waiting for player to connect: " + player.getUUID()); + LOGGER.info("Player [" + player.getName()+ "] joined."); ((DhServerWorld) serverWorld).addPlayer(player); } } @@ -163,9 +160,27 @@ public class ServerApi IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well { - LOGGER.debug("Removing player from connect wait list: " + player.getUUID()); + LOGGER.info("Player [" + player.getName() + "] disconnected."); ((DhServerWorld) serverWorld).removePlayer(player); } } + public void serverPlayerLevelChangeEvent(IServerPlayerWrapper player, IServerLevelWrapper originLevel, IServerLevelWrapper destinationLevel) + { + IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); + if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well + { + LOGGER.info("Player [" + player.getName() + "] changed level: ["+originLevel.getKeyedLevelDimensionName()+"] -> ["+destinationLevel.getKeyedLevelDimensionName()+"]."); + ((DhServerWorld) serverWorld).changePlayerLevel(player, originLevel, destinationLevel); + } + } + + public void pluginMessageReceived(IServerPlayerWrapper player, @NotNull AbstractNetworkMessage message) + { + IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); + if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well + { + ((DhServerWorld) serverWorld).remotePlayerConnectionHandler.handlePluginMessage(player, message); + } + } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java index 4df42a8d3..40afa8b85 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java @@ -53,7 +53,7 @@ public class SharedApi private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); - private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); + private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); 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 */ @@ -234,9 +234,9 @@ public class SharedApi } private static void queueChunkUpdate(IChunkWrapper chunkWrapper, @Nullable ArrayList neighbourChunkList, IDhLevel dhLevel) { - if (MC.playerExists()) + if (MC_CLIENT != null && MC_CLIENT.playerExists()) { - UPDATE_POS_MANAGER.setCenter(MC.getPlayerChunkPos()); + UPDATE_POS_MANAGER.setCenter(MC_CLIENT.getPlayerChunkPos()); UPDATE_POS_MANAGER.maxSize = MAX_UPDATING_CHUNK_COUNT_PER_THREAD * Config.Client.Advanced.MultiThreading.numberOfLodBuilderThreads.get(); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java b/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java index a90ace090..b517223d7 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java @@ -43,6 +43,7 @@ import javax.swing.*; import java.awt.*; import java.util.*; import java.util.List; +import java.util.concurrent.ThreadLocalRandom; /** @@ -163,9 +164,19 @@ public class Config .build(); public static ConfigEntry lodChunkRenderDistanceRadius = new ConfigEntry.Builder() + .setServersideShortName("renderDistanceRadius") .setMinDefaultMax(32, 128, 4096) - .comment("The radius of the mod's render distance. (measured in chunks)") + .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 static ConfigEntry verticalQuality = new ConfigEntry.Builder() @@ -719,16 +730,16 @@ public class Config public static class WorldGenerator { public static ConfigEntry enableDistantGeneration = new ConfigEntry.Builder() + .setServersideShortName("enableDistantGeneration") .set(true) .comment("" + " Should Distant Horizons slowly generate LODs \n" - + " outside the vanilla render distance?\n" - + "\n" - + " Note: when on a server, distant generation isn't supported \n" - + " and will always be disabled.") + + " outside the vanilla render distance?") + .setSide(EConfigEntryRelevantSide.BOTH) .build(); public static ConfigEntry distantGeneratorMode = new ConfigEntry.Builder() + .setServersideShortName("distantGeneratorMode") .set(EDhApiDistantGeneratorMode.FEATURES) .comment("" + "How detailed should LODs be generated outside the vanilla render distance? \n" @@ -758,24 +769,25 @@ public class Config + EDhApiDistantGeneratorMode.FEATURES + " \n" + "Generate everything except structures. \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" + "") - /* - // 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" - */ + .setSide(EConfigEntryRelevantSide.BOTH) .build(); public static ConfigEntry worldGenerationTimeoutLengthInSeconds = new ConfigEntry.Builder() + .setServersideShortName("worldGenerationTimeout") .setMinDefaultMax(5, 60 * 3, 60 * 10/*10 minutes*/ ) .comment("" + "How long should a world generator thread run for before timing out? \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" + "") + .setSide(EConfigEntryRelevantSide.BOTH) .build(); } @@ -784,6 +796,7 @@ public class Config { @Deprecated public static ConfigEntry minTimeBetweenChunkUpdatesInSeconds = new ConfigEntry.Builder() + .setServersideShortName("minTimeBetweenChunkUpdates") .setMinDefaultMax(0, 1, 60) .setAppearance(EConfigEntryAppearance.ONLY_IN_API) .comment("" @@ -967,27 +980,98 @@ public class Config + "") .build(); - // not currently implemented - private static ConfigEntry enableMultiverseNetworking = new ConfigEntry.Builder() - .set(true) - .comment("" - + "If true Distant Horizons will attempt to communicate with the connected \n" - + "server in order to improve multiverse support. \n" - + "") - .build(); - // not currently implemented - private static ConfigEntry enableServerNetworking = new ConfigEntry.Builder() - .set(false) - .comment("" - + "Attention: this is only for developers and hasn't been implemented.\n" - + "\n" - + "If true Distant Horizons will attempt to communicate with the connected \n" - + "server in order to load LODs outside your vanilla render distance. \n" - + "\n" - + "Note: This requires DH to be installed on the server in order to function. \n" - + "") - .build(); + + public static ConfigCategory serverNetworking = new ConfigCategory.Builder().set(ServerNetworking.class).build(); + + public static class ServerNetworking + { + public static ConfigUIComment generalSectionNote = new ConfigUIComment(); + public static ConfigEntry enableServerNetworking = new ConfigEntry.Builder() + .setServersideShortName("enableServerNetworking") + .set(true) + .comment("" + + "WARNING!\n" + + "Server-client networking is not yet fully implemented!\n" + + "Both the server and client must be running the server-side fork with this option enabled\n" + + "for Distant Horizons data to be transceived.\n" + + "\n" + + "If true, the server and client will attempt to communicate to transceive Distant Horizons data.\n" + + "This allows for further distant generation and LOD updates on all clients.\n" + + "\n" + + "This should only be used on trusted servers with trusted players!\n" + + "") + .setSide(EConfigEntryRelevantSide.BOTH) + .build(); + + + public static ConfigEntry sendLevelKeys = new ConfigEntry.Builder() + .setServersideShortName("sendLevelKeys") + .setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) + .set(true) + .comment("" + + "Makes the server send level keys for each world.\n" + + "Disable this if you use alternative ways to send level keys.\n" + + "") + .setSide(EConfigEntryRelevantSide.BOTH) + .build(); + public static ConfigEntry levelKeyPrefix = new ConfigEntry.Builder() + .setServersideShortName("levelKeyPrefix") + .setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) + .set(getDefaultLevelKeyPrefix()) + .comment("" + + "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" + + "or empty if you don't use a proxy.\n" + + "") + .setSide(EConfigEntryRelevantSide.BOTH) + .build(); + + + public static ConfigUIComment generationSectionNote = new ConfigUIComment(); + public static ConfigEntry generationRequestRateLimit = new ConfigEntry.Builder() + .setServersideShortName("generationRequestRateLimit") + .setMinDefaultMax(1, 20, 100) + .comment("" + + "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." + + "") + .setSide(EConfigEntryRelevantSide.BOTH) + .build(); + + + public static ConfigUIComment realTimeUpdatesSectionNote = new ConfigUIComment(); + public static ConfigEntry enableRealTimeUpdates = new ConfigEntry.Builder() + .setServersideShortName("enableRealTimeUpdates") + .set(false) + .comment("" + + "If true, the client will receive real-time LOD updates for chunks outside the client's render distance." + + "") + .setSide(EConfigEntryRelevantSide.BOTH) + .build(); + + + // TODO rename + public static ConfigUIComment syncOnLoginSectionNote = new ConfigUIComment(); + public static ConfigEntry synchronizeOnLogin = new ConfigEntry.Builder() + .setServersideShortName("synchronizeOnLogin") + .set(false) + .comment("" + + "If true, clients will receive updated LODs on join if any changes occurred since last join." + + "") + .setSide(EConfigEntryRelevantSide.BOTH) + .build(); + + public static ConfigEntry syncOnLoginRateLimit = new ConfigEntry.Builder() + .setServersideShortName("syncOnLoginRateLimit") + .setMinDefaultMax(1, 50, 100) + .comment("" + + "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." + + "") + .setSide(EConfigEntryRelevantSide.BOTH) + .build(); + } } @@ -1009,6 +1093,7 @@ public class Config public static final ConfigEntry numberOfWorldGenerationThreads = new ConfigEntry.Builder() + .setServersideShortName("numberOfWorldGenerationThreads") .setMinDefaultMax(1, ThreadPresetConfigEventHandler.getWorldGenDefaultThreadCount(), Runtime.getRuntime().availableProcessors()) @@ -1022,13 +1107,17 @@ public class Config + "generation speed, increase this number. \n" + "\n" + THREAD_NOTE) + .setSide(EConfigEntryRelevantSide.BOTH) .build(); public static final ConfigEntry runTimeRatioForWorldGenerationThreads = new ConfigEntry.Builder() + .setServersideShortName("runTimeRatioForWorldGenerationThreads") .setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getWorldGenDefaultRunTimeRatio(), 1.0) .comment(THREAD_RUN_TIME_RATIO_NOTE) + .setSide(EConfigEntryRelevantSide.BOTH) .build(); public static final ConfigEntry numberOfFileHandlerThreads = new ConfigEntry.Builder() + .setServersideShortName("numberOfFileHandlerThreads") .setMinDefaultMax(1, ThreadPresetConfigEventHandler.getFileHandlerDefaultThreadCount(), Runtime.getRuntime().availableProcessors()) @@ -1040,10 +1129,13 @@ public class Config + "quickly flying through existing LODs. \n" + "\n" + THREAD_NOTE) + .setSide(EConfigEntryRelevantSide.BOTH) .build(); public static final ConfigEntry runTimeRatioForFileHandlerThreads = new ConfigEntry.Builder() + .setServersideShortName("runTimeRatioForFileHandlerThreads") .setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getFileHandlerDefaultRunTimeRatio(), 1.0) .comment(THREAD_RUN_TIME_RATIO_NOTE) + .setSide(EConfigEntryRelevantSide.BOTH) .build(); public static final ConfigEntry numberOfUpdatePropagatorThreads = new ConfigEntry.Builder() @@ -1072,6 +1164,7 @@ public class Config .build(); public static final ConfigEntry numberOfLodBuilderThreads = new ConfigEntry.Builder() + .setServersideShortName("numberOfLodBuilderThreads") .setMinDefaultMax(1, ThreadPresetConfigEventHandler.getLodBuilderDefaultThreadCount(), Runtime.getRuntime().availableProcessors()) @@ -1082,12 +1175,16 @@ public class Config + "certain graphics settings are changed, and when moving around the world. \n" + "\n" + THREAD_NOTE) + .setSide(EConfigEntryRelevantSide.BOTH) .build(); public static final ConfigEntry runTimeRatioForLodBuilderThreads = new ConfigEntry.Builder() + .setServersideShortName("runTimeRatioForLodBuilderThreads") .setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getLodBuilderDefaultRunTimeRatio(), 1.0) .comment(THREAD_RUN_TIME_RATIO_NOTE) + .setSide(EConfigEntryRelevantSide.BOTH) .build(); public static final ConfigEntry enableLodBuilderThreadLimiting = new ConfigEntry.Builder() + .setServersideShortName("enableLodBuilderThreadLimiting") .set(true) .comment("" + "Should only be disabled if deadlock occurs and LODs refuse to update. \n" @@ -1095,6 +1192,7 @@ public class Config + "\n" + "Note that if deadlock did occur restarting MC may be necessary to stop the locked threads. \n" + "") + .setSide(EConfigEntryRelevantSide.BOTH) .build(); public static final ConfigEntry numberOfNetworkCompressionThreads = new ConfigEntry.Builder() @@ -1110,6 +1208,7 @@ public class Config + "to a server that doesn't support DH networking. \n" + "\n" + THREAD_NOTE) + .setSide(EConfigEntryRelevantSide.BOTH) .build(); public static final ConfigEntry runTimeRatioForNetworkCompressionThreads = new ConfigEntry.Builder() .setServersideShortName("runTimeRatioForNetworkCompressionThreads") @@ -1149,31 +1248,39 @@ public class Config // TODO add change all option // TODO default to error chat and info file public static ConfigEntry logWorldGenEvent = new ConfigEntry.Builder() + .setServersideShortName("logWorldGenEvent") .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .comment("" + "If enabled, the mod will log information about the world generation process. \n" + "This can be useful for debugging.") + .setSide(EConfigEntryRelevantSide.BOTH) .build(); public static ConfigEntry logWorldGenPerformance = new ConfigEntry.Builder() + .setServersideShortName("logWorldGenPerformance") .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .comment("" + "If enabled, the mod will log performance about the world generation process. \n" + "This can be useful for debugging.") + .setSide(EConfigEntryRelevantSide.BOTH) .build(); public static ConfigEntry logWorldGenLoadEvent = new ConfigEntry.Builder() + .setServersideShortName("logWorldGenPerformance") .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .comment("" + "If enabled, the mod will log information about the world generation process. \n" + "This can be useful for debugging.") + .setSide(EConfigEntryRelevantSide.BOTH) .build(); public static ConfigEntry logLodBuilderEvent = new ConfigEntry.Builder() + .setServersideShortName("logLodBuilderEvent") .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .comment("" + "If enabled, the mod will log information about the LOD generation process. \n" + "This can be useful for debugging.") + .setSide(EConfigEntryRelevantSide.BOTH) .build(); public static ConfigEntry logRendererBufferEvent = new ConfigEntry.Builder() @@ -1191,24 +1298,30 @@ public class Config .build(); public static ConfigEntry logFileReadWriteEvent = new ConfigEntry.Builder() + .setServersideShortName("logFileReadWriteEvent") .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .comment("" + "If enabled, the mod will log information about file read/write operations. \n" + "This can be useful for debugging.") + .setSide(EConfigEntryRelevantSide.BOTH) .build(); public static ConfigEntry logFileSubDimEvent = new ConfigEntry.Builder() + .setServersideShortName("logFileSubDimEvent") .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .comment("" + "If enabled, the mod will log information about file sub-dimension operations. \n" + "This can be useful for debugging.") + .setSide(EConfigEntryRelevantSide.BOTH) .build(); public static ConfigEntry logNetworkEvent = new ConfigEntry.Builder() + .setServersideShortName("logNetworkEvent") .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .comment("" + "If enabled, the mod will log information about network operations. \n" + "This can be useful for debugging.") + .setSide(EConfigEntryRelevantSide.BOTH) .build(); @@ -1620,4 +1733,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); + } + } + } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/file/ConfigFileHandling.java b/core/src/main/java/com/seibel/distanthorizons/core/config/file/ConfigFileHandling.java index ff5098250..12fb71a2a 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/config/file/ConfigFileHandling.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/config/file/ConfigFileHandling.java @@ -20,6 +20,7 @@ package com.seibel.distanthorizons.core.config.file; 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.config.ConfigBase; import com.seibel.distanthorizons.core.config.types.AbstractConfigType; @@ -47,7 +48,7 @@ public class ConfigFileHandling public final ConfigBase configBase; public final Path configPath; - private final Logger LOGGER; + private final Logger logger; /** This is the object for night-config */ private final CommentedFileConfig nightConfig; @@ -60,7 +61,7 @@ public class ConfigFileHandling 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.configPath = configPath; @@ -115,7 +116,7 @@ public class ConfigFileHandling */ public void loadFromFile() { - int currentCfgVersion = configBase.configVersion; + int currentCfgVersion = this.configBase.configVersion; try { // Dont load the real `this.nightConfig`, instead create a tempoary one @@ -126,27 +127,29 @@ public class ConfigFileHandling tmpNightConfig.close(); } catch (Exception ignored) { } - if (currentCfgVersion == configBase.configVersion) - {} - else if (currentCfgVersion > configBase.configVersion) + if (currentCfgVersion == this.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) { - 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 { - Files.delete(configPath); + Files.delete(this.configPath); } catch (Exception e) { - LOGGER.error(e); + this.logger.error(e); } } - loadFromFile(nightConfig); - nightConfig.set("_version", configBase.configVersion); + this.loadFromFile(this.nightConfig); + this.nightConfig.set("_version", this.configBase.configVersion); } /** * Loads the entire config from the file @@ -197,8 +200,8 @@ public class ConfigFileHandling // Save an entry when only given the entry public void saveEntry(ConfigEntry entry) { - saveEntry(entry, nightConfig); - nightConfig.save(); + this.saveEntry(entry, this.nightConfig); + this.nightConfig.save(); } /** Save an entry */ public void saveEntry(ConfigEntry entry, CommentedFileConfig workConfig) @@ -207,25 +210,24 @@ public class ConfigFileHandling { return; } - else if (MC_SHARED.isDedicatedServer() && entry.getServersideShortName() == null) + else if ((entry.getRelevantSide() == EConfigEntryRelevantSide.CLIENT && MC_SHARED.isDedicatedServer()) + || (entry.getRelevantSide() == EConfigEntryRelevantSide.SERVER && !MC_SHARED.isDedicatedServer())) { - // don't save server configs on the client + // 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 [" + configBase.modName + "]. Please contact the authors."); + 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())); } /** Loads an entry when only given the entry */ - public void loadEntry(ConfigEntry entry) - { - loadEntry(entry, nightConfig); - } + public void loadEntry(ConfigEntry entry) { this.loadEntry(entry, this.nightConfig); } /** Loads an entry */ @SuppressWarnings("unchecked") public void loadEntry(ConfigEntry entry, CommentedFileConfig nightConfig) @@ -235,7 +237,7 @@ public class ConfigFileHandling if (!nightConfig.contains(entry.getNameWCategory())) { - saveEntry(entry, nightConfig); + this.saveEntry(entry, nightConfig); return; } @@ -254,7 +256,7 @@ public class ConfigFileHandling Object convertedValue = ConfigTypeConverters.attemptToConvertFromString(expectedValueClass, value); if (!convertedValue.getClass().equals(expectedValueClass)) { - LOGGER.error("Unable to convert config value ["+value+"] from ["+(value != null ? value.getClass() : "NULL")+"] to ["+expectedValueClass+"] for config ["+entry.name+"], " + + 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()+"]. " + "Make sure a converter is defined in ["+ConfigTypeConverters.class.getSimpleName()+"]."); convertedValue = entry.getDefaultValue(); @@ -263,23 +265,20 @@ public class ConfigFileHandling 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()); } } catch (Exception e) { // 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()); } } // Creates the comment for an entry when only given the entry - public void createComment(ConfigEntry entry) - { - createComment(entry, nightConfig); - } + public void createComment(ConfigEntry entry) { this.createComment(entry, this.nightConfig); } // Creates a comment for an entry public void createComment(ConfigEntry entry, CommentedFileConfig nightConfig) { @@ -289,8 +288,13 @@ public class ConfigFileHandling return; } - if (MC_SHARED.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; } @@ -332,7 +336,7 @@ public class ConfigFileHandling } 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); @@ -342,7 +346,7 @@ public class ConfigFileHandling catch (Exception ex) { 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); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/types/ConfigEntry.java b/core/src/main/java/com/seibel/distanthorizons/core/config/types/ConfigEntry.java index 5ef257666..fa4269dad 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/config/types/ConfigEntry.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/config/types/ConfigEntry.java @@ -25,6 +25,7 @@ import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; 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.EConfigEntryPerformance; +import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryRelevantSide; import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry; import java.util.ArrayList; @@ -44,6 +45,10 @@ public class ConfigEntry extends AbstractConfigType> implem private T min; private T max; private final ArrayList listenerList; + private final String serversideShortName; + + private final EConfigEntryPerformance performance; + private final EConfigEntryRelevantSide relevantSide; // API control // /** @@ -53,19 +58,25 @@ public class ConfigEntry extends AbstractConfigType> implem public final boolean allowApiOverride; private T apiValue; - private final EConfigEntryPerformance performance; /** Creates the entry */ - private ConfigEntry(EConfigEntryAppearance appearance, T value, String comment, T min, T max, boolean allowApiOverride, EConfigEntryPerformance performance, ArrayList listenerList) + private ConfigEntry( + EConfigEntryAppearance appearance, + T value, String comment, T min, T max, + String serversideShortName, boolean allowApiOverride, + EConfigEntryPerformance performance, EConfigEntryRelevantSide relevantSide, + ArrayList listenerList) { super(appearance, value); this.comment = comment; this.min = min; this.max = max; + this.serversideShortName = serversideShortName; this.allowApiOverride = allowApiOverride; this.performance = performance; + this.relevantSide = relevantSide; this.listenerList = listenerList; } @@ -179,6 +190,9 @@ public class ConfigEntry extends AbstractConfigType> implem if (validness == 1) this.value = (T) NumberUtil.getMaximum(this.value.getClass()); } + // TODO is this for command line use? + public String getServersideShortName() { return this.serversideShortName; } + @Override public String getComment() { return this.comment; } @Override @@ -186,6 +200,8 @@ public class ConfigEntry extends AbstractConfigType> implem /** Gets the performance impact of an option */ 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. */ public void addValueChangeListener(Consumer onValueChangeFunc) @@ -315,8 +331,10 @@ public class ConfigEntry extends AbstractConfigType> implem private String tmpComment = null; private T tmpMin = null; private T tmpMax = null; + protected String tmpServersideShortName = null; private boolean tmpUseApiOverwrite = true; private EConfigEntryPerformance tmpPerformance = EConfigEntryPerformance.DONT_SHOW; + private EConfigEntryRelevantSide tmpRelevantSide = EConfigEntryRelevantSide.CLIENT; protected ArrayList tmpIConfigListener = new ArrayList<>(); public Builder comment(String newComment) @@ -352,6 +370,12 @@ public class ConfigEntry extends AbstractConfigType> implem return this; } + public Builder setServersideShortName(String name) + { + this.tmpServersideShortName = name; + return this; + } + public Builder setUseApiOverwrite(boolean newUseApiOverwrite) { this.tmpUseApiOverwrite = newUseApiOverwrite; @@ -364,6 +388,12 @@ public class ConfigEntry extends AbstractConfigType> implem return this; } + public Builder setSide(EConfigEntryRelevantSide relevantSide) + { + this.tmpRelevantSide = relevantSide; + return this; + } + public Builder replaceListeners(ArrayList newConfigListener) @@ -394,7 +424,11 @@ public class ConfigEntry extends AbstractConfigType> implem public ConfigEntry build() { - return new ConfigEntry<>(this.tmpAppearance, this.tmpValue, this.tmpComment, this.tmpMin, this.tmpMax, 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); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/types/enums/EConfigEntryRelevantSide.java b/core/src/main/java/com/seibel/distanthorizons/core/config/types/enums/EConfigEntryRelevantSide.java new file mode 100644 index 000000000..821663238 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/config/types/enums/EConfigEntryRelevantSide.java @@ -0,0 +1,36 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.config.types.enums; + +/** + * BOTH,
+ * SERVER,
+ * CLIENT,
+ * + * Defines whether this config entry is for the client, server, or both. + * TODO this needs a better name. + */ +public enum EConfigEntryRelevantSide +{ + BOTH, + SERVER, + CLIENT, + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV2.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV2.java index 873cbbb56..e2cf3624b 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV2.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV2.java @@ -21,6 +21,7 @@ package com.seibel.distanthorizons.core.file.fullDatafile; import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode; import com.seibel.distanthorizons.core.api.internal.ClientApi; +import com.seibel.distanthorizons.core.api.internal.SharedApi; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV1; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; @@ -37,6 +38,7 @@ import com.seibel.distanthorizons.core.sql.repo.FullDataSourceV2Repo; import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; +import com.seibel.distanthorizons.core.world.EWorldEnvironment; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import it.unimi.dsi.fastutil.longs.LongArrayList; import org.apache.logging.log4j.Logger; @@ -45,11 +47,14 @@ import org.jetbrains.annotations.Nullable; import java.awt.*; import java.io.File; import java.io.IOException; +import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.*; +import java.util.List; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; /** * Handles reading/writing {@link FullDataSourceV2} @@ -102,9 +107,14 @@ public class FullDataSourceProviderV2 // TODO only run thread if modifications happened recently /** - * This isn't in {@link AbstractDataSourceHandler} since we don't need parent updating logic - * for render data, only full data. + * This isn't in {@link AbstractDataSourceHandler} since we only want to update + * 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.

+ * + * Will be null on the dedicated server since updates don't need to be propagated, + * only the highest detail level is needed. */ + @Nullable private final ThreadPoolExecutor updateQueueProcessor; @@ -124,11 +134,19 @@ public class FullDataSourceProviderV2 String dimensionName = level.getLevelWrapper().getDimensionName(); // 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.execute(() -> this.convertLegacyDataSources()); + 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.updateQueueProcessor = ThreadUtil.makeSingleThreadPool("Parent Update Queue ["+dimensionName+"]"); - this.updateQueueProcessor.execute(() -> this.runUpdateQueue()); + // 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) + { + this.updateQueueProcessor = ThreadUtil.makeSingleThreadPool("Parent Update Queue ["+dimensionName+"]"); + this.updateQueueProcessor.execute(this::runUpdateQueue); + } + else + { + this.updateQueueProcessor = null; + } } @@ -336,7 +354,7 @@ public class FullDataSourceProviderV2 long totalDeleteCount = this.legacyFileHandler.repo.getUnusedDataSourceCount(); if (totalDeleteCount != 0) { - // this should only be shown once per session but should be shown during + // this should only be shown once per networkSession but should be shown during // either when the deletion or migration phases start this.showMigrationStartMessage(); @@ -596,6 +614,20 @@ public class FullDataSourceProviderV2 */ public int getUnsavedDataSourceCount() { return -1; } + 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 getTimestampsForRange(byte detailLevel, int startPosX, int startPosZ, int endPosX, int endPosZ) + { return this.repo.getTimestampsForRange(detailLevel, startPosX, startPosZ, endPosX, endPosZ); } + //===========// @@ -618,7 +650,10 @@ public class FullDataSourceProviderV2 public void close() { super.close(); - this.updateQueueProcessor.shutdownNow(); + if (this.updateQueueProcessor != null) + { + this.updateQueueProcessor.shutdownNow(); + } this.legacyFileHandler.close(); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java index 6771fec20..02b5a39be 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java @@ -37,12 +37,15 @@ import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; import it.unimi.dsi.fastutil.longs.LongArrayList; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; import java.awt.*; +import java.io.File; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import java.util.stream.IntStream; public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 implements IDebugRenderable { @@ -71,6 +74,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im //=============// public GeneratedFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure) { super(level, saveStructure); } + public GeneratedFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride) { super(level, saveStructure, saveDirOverride); } @@ -215,7 +219,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im } GenTask genTask = new GenTask(genPos); - CompletableFuture worldGenFuture = worldGenQueue.submitGenTask(genPos, (byte) (DhSectionPos.getDetailLevel(genPos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL), genTask); + CompletableFuture worldGenFuture = worldGenQueue.submitRetrievalTask(genPos, (byte) (DhSectionPos.getDetailLevel(genPos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL), genTask); worldGenFuture.whenComplete((genTaskResult, ex) -> this.onWorldGenTaskComplete(genTaskResult, ex)); return true; @@ -238,6 +242,12 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im public int getUnsavedDataSourceCount() { return this.delayedFullDataSourceSaveCache.getUnsavedCount(); } + public boolean isFullyGenerated(byte[] columnGenerationSteps) + { + return IntStream.range(0, columnGenerationSteps.length) + .noneMatch(i -> columnGenerationSteps[i] == EDhApiWorldGenerationStep.EMPTY.value); + } + @Override public LongArrayList getPositionsToRetrieve(Long pos) { @@ -386,7 +396,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im public boolean isMemoryAddressValid() { return true; } @Override - public Consumer getChunkDataConsumer() + public Consumer getDataSourceConsumer() { return (chunkSizedFullDataSource) -> { diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/RemoteFullDataSourceProvider.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/RemoteFullDataSourceProvider.java index efdf28122..05daf25e7 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/RemoteFullDataSourceProvider.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/RemoteFullDataSourceProvider.java @@ -19,15 +19,119 @@ package com.seibel.distanthorizons.core.file.fullDatafile; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; 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.WorldGenModule; +import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoginRequestQueue; +import com.seibel.distanthorizons.core.pos.DhSectionPos; import org.jetbrains.annotations.Nullable; import java.io.File; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; -public class RemoteFullDataSourceProvider extends FullDataSourceProviderV2 +/** + * 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 RemoteFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure) { super(level, saveStructure); } - public RemoteFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride) { super(level, saveStructure, saveDirOverride); } + @Nullable + private final SyncOnLoginRequestQueue syncOnLoginRequestQueue; + private final Set finishedTaskPositions = ConcurrentHashMap.newKeySet(); -} + + + //=============// + // constructor // + //=============// + + public RemoteFullDataSourceProvider( + IDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride, + @Nullable SyncOnLoginRequestQueue syncOnLoginRequestQueue) + { + super(level, saveStructure, saveDirOverride); + this.syncOnLoginRequestQueue = syncOnLoginRequestQueue; + } + + + + //==================// + // override methods // + //==================// + + @Override + @Nullable + public FullDataSourceV2 get(long pos) + { + //=======================// + // get local data source // + //=======================// + + FullDataSourceV2 fullDataSource = super.get(pos); + 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; + } + + + + //===========================// + // 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); + Map timestamps = this.getTimestampsForRange( + DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL, + DhSectionPos.getX(pos) * posToMinimumDetailScale, + DhSectionPos.getZ(pos) * posToMinimumDetailScale, + (DhSectionPos.getX(pos) + 1) * posToMinimumDetailScale - 1, + (DhSectionPos.getZ(pos) + 1) * posToMinimumDetailScale - 1 + ); + + // check if the server has newer versions of these LODs + for (Map.Entry timestampBySectionPos : timestamps.entrySet()) + { + Long subPos = timestampBySectionPos.getKey(); + Long subTimestamp = timestampBySectionPos.getValue(); + + if (this.finishedTaskPositions.add(subPos)) + { + this.syncOnLoginRequestQueue.submitRequest(subPos, subTimestamp, this.delayedFullDataSourceSaveCache::queueDataSourceForUpdateAndSave); + } + } + + return fullDataSource; + } + + + + //==========// + // shutdown // + //==========// + + @Override + public void close() + { + if (this.syncOnLoginRequestQueue != null) + { + this.syncOnLoginRequestQueue.close(); + } + super.close(); + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/structure/ClientOnlySaveStructure.java b/core/src/main/java/com/seibel/distanthorizons/core/file/structure/ClientOnlySaveStructure.java index 61352b306..da0493643 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/structure/ClientOnlySaveStructure.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/structure/ClientOnlySaveStructure.java @@ -89,7 +89,7 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure IServerKeyedClientLevel keyedClientLevel = (IServerKeyedClientLevel) newLevelWrapper; LOGGER.info("Loading level " + newLevelWrapper.getDimensionName() + " with key: " + keyedClientLevel.getServerLevelKey()); // This world was identified by the server directly, so we can know for sure which folder to use. - return new File(getSaveStructureFolderPath() + File.separatorChar + keyedClientLevel.getServerLevelKey()); + return new File(getSaveStructureFolderPath() + File.separatorChar + keyedClientLevel.getServerLevelKey().replaceAll(":", "@@")); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/subDimMatching/SubDimensionLevelMatcher.java b/core/src/main/java/com/seibel/distanthorizons/core/file/subDimMatching/SubDimensionLevelMatcher.java index 0e2bb8ee2..a183a6010 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/subDimMatching/SubDimensionLevelMatcher.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/subDimMatching/SubDimensionLevelMatcher.java @@ -60,6 +60,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * @author James Seibel * @version 12-17-2022 */ +@Deprecated public class SubDimensionLevelMatcher implements AutoCloseable { private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); @@ -209,7 +210,7 @@ public class SubDimensionLevelMatcher implements AutoCloseable try { // get the data source to compare against - try (IDhLevel tempLevel = new DhClientLevel(new ClientOnlySaveStructure(), this.currentClientLevel, testLevelFolder, false)) + try (IDhLevel tempLevel = new DhClientLevel(new ClientOnlySaveStructure(), this.currentClientLevel, testLevelFolder, false, null)) { testFullDataSource = tempLevel.getFullDataProvider().getAsync(DhSectionPos.encodeContaining(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, this.playerData.playerBlockPos)).join(); if (testFullDataSource == null) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/IFullDataSourceRetrievalQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/IFullDataSourceRetrievalQueue.java index 076721f79..05a103e83 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/IFullDataSourceRetrievalQueue.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/IFullDataSourceRetrievalQueue.java @@ -26,6 +26,7 @@ import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.render.LodQuadTree; import java.io.Closeable; +import java.util.List; import java.util.concurrent.CompletableFuture; /** @@ -49,9 +50,17 @@ public interface IFullDataSourceRetrievalQueue extends Closeable // getters // //=========// - /** the largest numerical detail level */ + /** + * The largest numerical detail level.
+ * Detail level is absolute, not section; + * IE 0 = Block, 1 = 2x2 blocks, etc. + */ byte lowestDataDetail(); - /** the smallest numerical detail level */ + /** + * The smallest numerical detail level.
+ * Detail level is absolute, not section; + * IE 0 = Block, 1 = 2x2 blocks, etc. + */ byte highestDataDetail(); @@ -81,7 +90,7 @@ public interface IFullDataSourceRetrievalQueue extends Closeable */ void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf); - CompletableFuture submitGenTask(long pos, byte requiredDataDetail, IWorldGenTaskTracker tracker); + CompletableFuture submitRetrievalTask(long pos, byte requiredDataDetail, IWorldGenTaskTracker tracker); @@ -89,7 +98,10 @@ public interface IFullDataSourceRetrievalQueue extends Closeable // shutdown // //==========// - CompletableFuture startClosing(boolean cancelCurrentGeneration, boolean alsoInterruptRunning); + /** Can be used to let any lingering generation requests finish before fully shutting down the system */ + CompletableFuture startClosingAsync(boolean cancelCurrentGeneration, boolean alsoInterruptRunning); + + @Override void close(); @@ -104,6 +116,8 @@ public interface IFullDataSourceRetrievalQueue extends Closeable /** used for rendering to the F3 menu */ int getEstimatedTotalTaskCount(); void setEstimatedTotalTaskCount(int newEstimate); + + void addDebugMenuStringsToList(List messageList); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/RemoteWorldRetrievalQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/RemoteWorldRetrievalQueue.java new file mode 100644 index 000000000..48a7ef395 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/RemoteWorldRetrievalQueue.java @@ -0,0 +1,84 @@ +package com.seibel.distanthorizons.core.generation; + +import com.seibel.distanthorizons.core.config.Config; +import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; +import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; +import com.seibel.distanthorizons.core.level.IDhClientLevel; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.multiplayer.client.AbstractFullDataNetworkRequestQueue; +import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState; +import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; +import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; +import com.seibel.distanthorizons.core.util.LodUtil; +import org.apache.logging.log4j.Logger; + +import java.util.concurrent.*; + +public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQueue implements IFullDataSourceRetrievalQueue, IDebugRenderable +{ + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + + private int estimatedTotalTaskCount; + + + + //=============// + // constructor // + //=============// + + public RemoteWorldRetrievalQueue(ClientNetworkState networkState, IDhClientLevel level) + { super(networkState, level, false, Config.Client.Advanced.Debugging.DebugWireframe.showWorldGenQueue); } + + + + //===========================// + // retrieval queue overrides // + //===========================// + + @Override + 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; } + + @Override + public CompletableFuture submitRetrievalTask(long sectionPos, byte requiredDataDetail, IWorldGenTaskTracker tracker) + { + return super.submitRequest(sectionPos, tracker.getDataSourceConsumer()) + .thenApply(retrievalSuccess -> retrievalSuccess + ? WorldGenResult.CreateSuccess(sectionPos) + : WorldGenResult.CreateFail()); + } + + @Override + public CompletableFuture startClosingAsync(boolean cancelCurrentGeneration, boolean alsoInterruptRunning) + { return super.startClosingAsync(alsoInterruptRunning); } + + + + //=================================// + // 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 + public int getEstimatedTotalTaskCount() { return this.estimatedTotalTaskCount; } + @Override + public void setEstimatedTotalTaskCount(int newEstimate) { this.estimatedTotalTaskCount = newEstimate; } + + + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java index d4814d412..67e90d450 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java @@ -47,6 +47,7 @@ import org.apache.logging.log4j.Logger; import java.awt.*; import java.util.*; +import java.util.List; import java.util.concurrent.*; import java.util.function.Consumer; @@ -141,7 +142,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb //=================// @Override - public CompletableFuture submitGenTask(long pos, byte requiredDataDetail, IWorldGenTaskTracker tracker) + public CompletableFuture submitRetrievalTask(long pos, byte requiredDataDetail, IWorldGenTaskTracker tracker) { // the generator is shutting down, don't add new tasks if (this.generatorClosingFuture != null) @@ -535,13 +536,15 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb @Override public void setEstimatedTotalTaskCount(int newEstimate) { this.estimatedTotalTaskCount = newEstimate; } + public void addDebugMenuStringsToList(List messageList) { } + //==========// // shutdown // //==========// - public CompletableFuture startClosing(boolean cancelCurrentGeneration, boolean alsoInterruptRunning) + public CompletableFuture startClosingAsync(boolean cancelCurrentGeneration, boolean alsoInterruptRunning) { LOGGER.info("Closing world gen queue"); this.queueingThread.shutdownNow(); @@ -592,7 +595,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb if (this.generatorClosingFuture == null) { - this.startClosing(true, true); + this.startClosingAsync(true, true); } LodUtil.assertTrue(this.generatorClosingFuture != null); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/tasks/IWorldGenTaskTracker.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/tasks/IWorldGenTaskTracker.java index b37cff85a..1bb46f1a9 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/tasks/IWorldGenTaskTracker.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/tasks/IWorldGenTaskTracker.java @@ -34,6 +34,6 @@ public interface IWorldGenTaskTracker boolean isMemoryAddressValid(); @Nullable - Consumer getChunkDataConsumer(); + Consumer getDataSourceConsumer(); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/tasks/WorldGenTaskGroup.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/tasks/WorldGenTaskGroup.java index c8130b330..859363122 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/tasks/WorldGenTaskGroup.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/tasks/WorldGenTaskGroup.java @@ -51,7 +51,7 @@ public final class WorldGenTaskGroup while (tasks.hasNext()) { WorldGenTask task = tasks.next(); - Consumer chunkDataConsumer = task.taskTracker.getChunkDataConsumer(); + Consumer chunkDataConsumer = task.taskTracker.getDataSourceConsumer(); if (chunkDataConsumer == null) { tasks.remove(); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/jar/DarkModeDetector.java b/core/src/main/java/com/seibel/distanthorizons/core/jar/DarkModeDetector.java index e6cdc8c61..dc90c5d00 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/jar/DarkModeDetector.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/jar/DarkModeDetector.java @@ -102,7 +102,7 @@ public class DarkModeDetector for (String de : de_location.list()) { // System.out.println(de); - if (de.contains("gnome-session")) // Gnome uses GTK + if (de.contains("gnome-networkSession")) // Gnome uses GTK return GTKChecker(); if (de.contains("plasma_session")) // KDE plasma uses QT return QTChecker(); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java index 0452cfec9..c409b502f 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java @@ -20,15 +20,27 @@ package com.seibel.distanthorizons.core.level; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; +import com.seibel.distanthorizons.core.config.AppliedConfigState; +import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; +import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.fullDatafile.RemoteFullDataSourceProvider; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; +import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState; +import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoginRequestQueue; +import com.seibel.distanthorizons.core.network.event.ScopedNetworkEventSource; +import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; +import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; +import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; +import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; @@ -36,6 +48,8 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; +import javax.annotation.CheckForNull; +import java.awt.*; import java.io.File; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -44,25 +58,58 @@ import java.util.concurrent.CompletableFuture; public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); public final ClientLevelModule clientside; public final IClientLevelWrapper levelWrapper; public final AbstractSaveStructure saveStructure; public final RemoteFullDataSourceProvider dataFileHandler; + @CheckForNull + private final ClientNetworkState networkState; + @Nullable + private final ScopedNetworkEventSource networkEventSource; + + public final WorldGenModule worldGenModule; + public final AppliedConfigState worldGeneratorEnabledConfig; + + @Nullable + private final SyncOnLoginRequestQueue syncOnLoginRequestQueue; + //=============// // constructor // //=============// - public DhClientLevel(AbstractSaveStructure saveStructure, IClientLevelWrapper clientLevelWrapper) { this(saveStructure, clientLevelWrapper, null, true); } - public DhClientLevel(AbstractSaveStructure saveStructure, IClientLevelWrapper clientLevelWrapper, @Nullable File fullDataSaveDirOverride, boolean enableRendering) + public DhClientLevel(AbstractSaveStructure saveStructure, IClientLevelWrapper clientLevelWrapper, @Nullable ClientNetworkState networkState) { this(saveStructure, clientLevelWrapper, null, true, networkState); } + public DhClientLevel(AbstractSaveStructure saveStructure, IClientLevelWrapper clientLevelWrapper, @Nullable File fullDataSaveDirOverride, boolean enableRendering, @Nullable ClientNetworkState networkState) { + if (saveStructure.getFullDataFolder(clientLevelWrapper).mkdirs()) + { + LOGGER.warn("unable to create data folder."); + } this.levelWrapper = clientLevelWrapper; this.levelWrapper.setParentLevel(this); this.saveStructure = saveStructure; - this.dataFileHandler = new RemoteFullDataSourceProvider(this, saveStructure, fullDataSaveDirOverride); + + this.networkState = networkState; + if (this.networkState != null) + { + this.networkEventSource = new ScopedNetworkEventSource(this.networkState.getSession()); + this.syncOnLoginRequestQueue = new SyncOnLoginRequestQueue(this, this.networkState); + this.registerNetworkHandlers(); + } + else + { + this.networkEventSource = null; + this.syncOnLoginRequestQueue = null; + } + + this.dataFileHandler = new RemoteFullDataSourceProvider(this, saveStructure, fullDataSaveDirOverride, this.syncOnLoginRequestQueue); + this.worldGeneratorEnabledConfig = new AppliedConfigState<>(Config.Client.Advanced.WorldGenerator.enableDistantGeneration); + this.worldGenModule = new WorldGenModule(this); + this.clientside = new ClientLevelModule(this); this.createAndSetSupportingRepos(this.dataFileHandler.repo.databaseFile); @@ -74,6 +121,30 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel LOGGER.info("Started DHLevel for " + this.levelWrapper + " with saves at " + this.saveStructure); } } + private void registerNetworkHandlers() + { + assert this.networkEventSource != null; + assert this.networkState != null; + + this.networkEventSource.registerHandler(FullDataPartialUpdateMessage.class, message -> + { + try + { + FullDataSourceV2DTO dataSourceDto = this.networkState.decodeDataSourceAndReleaseBuffer(message.payload); + + if (!message.isSameLevelAs(this.levelWrapper)) + { + return; + } + + this.updateDataSourcesAsync(dataSourceDto.createPooledDataSource(this.levelWrapper)); + } + catch (Exception e) + { + LOGGER.error("Error while updating full data source", e); + } + }); + } @@ -87,6 +158,11 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel try { this.clientside.clientTick(); + + if (this.syncOnLoginRequestQueue != null) + { + this.syncOnLoginRequestQueue.tick(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos())); + } } catch (Exception e) { @@ -94,6 +170,53 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel } } + @Override + public void worldGenTick() + { + ClientNetworkState networkState = this.networkState; + + boolean isClientUsable = false, isAllowedDimension = false; + if (networkState != null) + { + isClientUsable = networkState.isReady(); + isAllowedDimension = MC_CLIENT.getWrappedClientLevel() == this.levelWrapper; + } + + boolean shouldDoWorldGen = isClientUsable + && networkState.sessionConfig.isDistantGenerationEnabled() + && isAllowedDimension + && this.clientside.isRendering(); + + boolean isWorldGenRunning = this.worldGenModule.isWorldGenRunning(); + if (shouldDoWorldGen && !isWorldGenRunning) + { + // start world gen + this.worldGenModule.startWorldGen(this.dataFileHandler, new WorldGenState(this, networkState)); + + // populate the queue based on the current rendering tree + ClientLevelModule.ClientRenderState renderState = this.clientside.ClientRenderStateRef.get(); + renderState.quadtree.leafNodeIterator().forEachRemaining(node -> { + this.dataFileHandler.getAsync(node.sectionPos); + }); + } + else if (!shouldDoWorldGen && isWorldGenRunning) + { + // stop world gen + this.worldGenModule.stopWorldGen(this.dataFileHandler); + } + + if (this.worldGenModule.isWorldGenRunning()) + { + this.worldGenModule.worldGenTick(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos())); + } + } + + + + //===========// + // rendering // + //===========// + @Override public void render(DhApiRenderParam renderEventParam, IProfilerWrapper profiler) { this.clientside.render(renderEventParam, profiler); } @@ -104,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 public int computeBaseColor(DhBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper block) { return this.levelWrapper.getBlockColor(pos, biome, block); } @@ -126,6 +268,30 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel @Override 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 public void addDebugMenuStringsToList(List messageList) { @@ -152,11 +318,39 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel { messageList.add(" Migration Failed"); } + + + // world gen + this.worldGenModule.addDebugMenuStringsToList(messageList); + if (this.syncOnLoginRequestQueue != null) + { + assert this.networkState != null; + if (this.networkState.sessionConfig.getSynchronizeOnLogin()) + { + this.syncOnLoginRequestQueue.addDebugMenuStringsToList(messageList); + } + } } + + + //==========// + // shutdown // + //==========// + @Override public void close() { + if (this.worldGenModule != null) + { + this.worldGenModule.close(); + } + + if (this.networkEventSource != null) + { + this.networkEventSource.close(); + } + this.levelWrapper.setParentLevel(null); this.clientside.close(); super.close(); @@ -164,23 +358,18 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel 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; } - @Override - public RenderBufferHandler getRenderBufferHandler() + //================// + // helper classes // + //================// + + private static class WorldGenState extends WorldGenModule.AbstractWorldGenState { - ClientLevelModule.ClientRenderState renderState = this.clientside.ClientRenderStateRef.get(); - return (renderState != null) ? renderState.renderBufferHandler : null; + WorldGenState(IDhClientLevel level, ClientNetworkState networkState) + { + this.worldGenerationQueue = new RemoteWorldRetrievalQueue(networkState, level); + } } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientServerLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientServerLevel.java index 3e308447f..ef307e589 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientServerLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientServerLevel.java @@ -98,7 +98,7 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev public void serverTick() { } @Override - public void doWorldGen() + public void worldGenTick() { this.serverside.worldGeneratorEnabledConfig.pollNewValue(); // if not called the get() line below may not boolean shouldDoWorldGen = this.serverside.worldGeneratorEnabledConfig.get() && this.clientside.isRendering(); @@ -216,12 +216,7 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev // world gen - WorldGenModule worldGenState = this.serverside.worldGenModule; - String worldGenDisplayString = worldGenState.getDebugMenuString(); - if (worldGenDisplayString != null) - { - messageList.add(worldGenDisplayString); - } + this.serverside.worldGenModule.addDebugMenuStringsToList(messageList); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/DhServerLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/DhServerLevel.java index 3c538c9b7..4ea422400 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/DhServerLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/DhServerLevel.java @@ -19,33 +19,73 @@ package com.seibel.distanthorizons.core.level; +import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; +import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.multiplayer.server.RemotePlayerConnectionHandler; +import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerState; +import com.seibel.distanthorizons.core.network.exceptions.InvalidLevelException; +import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException; +import com.seibel.distanthorizons.core.network.messages.ILevelRelatedMessage; +import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; +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.FullDataPayload; +import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage; +import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceResponseMessage; +import com.seibel.distanthorizons.core.network.messages.requests.CancelMessage; +import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; +import com.seibel.distanthorizons.core.util.LodUtil; +import com.seibel.distanthorizons.core.util.math.Vec3d; +import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; +import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import javax.annotation.CheckForNull; import java.util.List; -import java.util.concurrent.CompletableFuture; +import java.util.Map; +import java.util.concurrent.*; public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + private static final ConfigBasedLogger NETWORK_LOGGER = new ConfigBasedLogger(LogManager.getLogger(), + () -> Config.Client.Advanced.Logging.logNetworkEvent.get()); + + /** 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; private final IServerLevelWrapper serverLevelWrapper; + private final RemotePlayerConnectionHandler remotePlayerConnectionHandler; + + /** + * This queue is used for ensuring fair generation speed for each player.
+ * Every tick the first player gets used for centering generation, and then is immediately moved into the back of the queue.
+ * TODO only add players that actually have something to generate + */ + private final ConcurrentLinkedQueue worldGenPlayerCenteringQueue = new ConcurrentLinkedQueue<>(); + + private final ConcurrentMap requestGroupByPos = new ConcurrentHashMap<>(); + private final ConcurrentMap requestGroupByFutureId = new ConcurrentHashMap<>(); + //=============// // constructor // //=============// - public DhServerLevel(AbstractSaveStructure saveStructure, IServerLevelWrapper serverLevelWrapper) + public DhServerLevel(AbstractSaveStructure saveStructure, IServerLevelWrapper serverLevelWrapper, RemotePlayerConnectionHandler remotePlayerConnectionHandler) { if (saveStructure.getFullDataFolder(serverLevelWrapper).mkdirs()) { @@ -56,53 +96,378 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel this.createAndSetSupportingRepos(this.serverside.fullDataFileHandler.repo.databaseFile); this.runRepoReliantSetup(); - LOGGER.info("Started DHLevel for {} with saves at {}", serverLevelWrapper, saveStructure); + LOGGER.info("Started DHLevel for ["+serverLevelWrapper+"] at ["+saveStructure+"]."); + + this.remotePlayerConnectionHandler = remotePlayerConnectionHandler; } - //=========// - // methods // - //=========// - - public void serverTick() { } + //=======// + // ticks // + //=======// @Override - public CompletableFuture updateDataSourcesAsync(FullDataSourceV2 data) { return this.getFullDataProvider().updateDataSourceAsync(data); } - - @Override - public int getMinY() { return getLevelWrapper().getMinHeight(); } - - @Override - public void close() + public void serverTick() { - super.close(); - this.serverside.close(); - LOGGER.info("Closed DHLevel for {}", this.getLevelWrapper()); + // Send finished data source requests + for (Map.Entry entry : this.requestGroupByPos.entrySet()) + { + DataSourceRequestGroup requestGroup = entry.getValue(); + + if (requestGroup.fullDataSource == null) + { + continue; + } + + NETWORK_LOGGER.debug("["+this.serverLevelWrapper.getDimensionName()+"] Fulfilled request group ["+entry.getKey()+"]"); + + // Make this group unavailable for adding into + this.requestGroupByPos.remove(entry.getKey()); + requestGroup.requestRemoveSemaphore.acquireUninterruptibly(Short.MAX_VALUE); + requestGroup.requestAddSemaphore.acquireUninterruptibly(Short.MAX_VALUE); + + ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor(); + if (executor == null) + { + LOGGER.warn("Unable to send FullDataSourceResponseMessage - getNetworkCompressionExecutor() is null"); + continue; + } + CompletableFuture.runAsync(() -> + { + FullDataPayload payload = new FullDataPayload(requestGroup.fullDataSource); + for (FullDataSourceRequestMessage msg : requestGroup.requestMessages.values()) + { + this.requestGroupByFutureId.remove(msg.futureId); + + ServerPlayerState serverPlayerState = this.remotePlayerConnectionHandler.getConnectedPlayer(msg.serverPlayer()); + if (serverPlayerState == null) + { + continue; + } + + serverPlayerState.getRateLimiterSet(this).generationRequestRateLimiter.release(); + payload.splitAndSend(FULL_DATA_SPLIT_SIZE_IN_BYTES, msg.getSession()::sendMessage); + msg.sendResponse(new FullDataSourceResponseMessage(payload)); + } + }, executor); + } } @Override - public void doWorldGen() + public void worldGenTick() { boolean shouldDoWorldGen = true; //todo; boolean isWorldGenRunning = this.serverside.worldGenModule.isWorldGenRunning(); if (shouldDoWorldGen && !isWorldGenRunning) { // start world gen - serverside.worldGenModule.startWorldGen(serverside.fullDataFileHandler, new ServerLevelModule.WorldGenState(this)); + this.serverside.worldGenModule.startWorldGen(this.serverside.fullDataFileHandler, new ServerLevelModule.WorldGenState(this)); } else if (!shouldDoWorldGen && isWorldGenRunning) { // stop world gen - serverside.worldGenModule.stopWorldGen(serverside.fullDataFileHandler); + this.serverside.worldGenModule.stopWorldGen(this.serverside.fullDataFileHandler); } if (this.serverside.worldGenModule.isWorldGenRunning()) { - serverside.worldGenModule.worldGenTick(new DhBlockPos2D(0, 0)); // todo; + IServerPlayerWrapper firstPlayer = this.worldGenPlayerCenteringQueue.peek(); + if (firstPlayer == null) + { + return; + } + + // Put first player in back before removing from front, so it can be removed by other thread without blocking + // - if it gets removed, remove() below will remove the item we just put instead + this.worldGenPlayerCenteringQueue.add(firstPlayer); + this.worldGenPlayerCenteringQueue.remove(firstPlayer); + + Vec3d position = firstPlayer.getPosition(); + this.serverside.worldGenModule.worldGenTick(new DhBlockPos2D((int) position.x, (int) position.z)); } } + + + //==================// + // network handling // + //==================// + + public void registerNetworkHandlers(ServerPlayerState serverPlayerState) + { + serverPlayerState.networkSession.registerHandler(FullDataSourceRequestMessage.class, (message) -> + { + if (!this.messagePlayerInThisLevel(message)) + { + // we can't handle players in other levels, don't continue + return; + } + + + ServerPlayerState.RateLimiterSet rateLimiterSet = serverPlayerState.getRateLimiterSet(this); + + if (message.clientTimestamp == null) + { + 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; + } + + // If this fails, the 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("["+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; + } + + if (!rateLimiterSet.syncOnLoginRateLimiter.tryAcquire(message)) + { + 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 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 + 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) + { + this.serverside.fullDataFileHandler.getAsync(pos).thenAccept(fullDataSource -> + { + if (this.serverside.fullDataFileHandler.isFullyGenerated(fullDataSource.columnGenerationSteps)) + { + requestGroup.fullDataSource = fullDataSource; + } + else + { + this.serverside.fullDataFileHandler.queuePositionForRetrieval(pos); + } + }); + } + + + + //=================// + // player handling // + //=================// + + public void addPlayer(IServerPlayerWrapper serverPlayer) { this.worldGenPlayerCenteringQueue.add(serverPlayer); } + public void removePlayer(IServerPlayerWrapper serverPlayer) { this.worldGenPlayerCenteringQueue.remove(serverPlayer); } + + + + @Override + public CompletableFuture updateDataSourcesAsync(FullDataSourceV2 data) + { + 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.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; } @@ -118,12 +483,6 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel @Override public boolean hasSkyLight() { return this.serverLevelWrapper.hasSkyLight(); } - @Override - public void onWorldGenTaskComplete(long pos) - { - //TODO: Send packet to client - } - @Override public GenericObjectRenderer getGenericRenderer() { @@ -138,6 +497,7 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel } + //===========// // debugging // //===========// @@ -149,4 +509,38 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel 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 + { + public final ConcurrentMap requestMessages = new ConcurrentHashMap<>(); + + @CheckForNull + 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 + public final Semaphore requestAddSemaphore = new Semaphore(Short.MAX_VALUE, true); + public final Semaphore requestRemoveSemaphore = new Semaphore(Short.MAX_VALUE, true); + + } + } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/IDhLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/IDhLevel.java index a56aafc44..861db29dd 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/IDhLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/IDhLevel.java @@ -21,6 +21,7 @@ package com.seibel.distanthorizons.core.level; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; 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.pos.DhChunkPos; import com.seibel.distanthorizons.core.render.RenderBufferHandler; @@ -33,8 +34,10 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; -public interface IDhLevel extends AutoCloseable +public interface IDhLevel extends AutoCloseable, GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener { + void worldGenTick(); + int getMinY(); /** diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/IDhServerLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/IDhServerLevel.java index bfca9b22b..1af771b65 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/IDhServerLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/IDhServerLevel.java @@ -21,9 +21,10 @@ package com.seibel.distanthorizons.core.level; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; -public interface IDhServerLevel extends IDhWorldGenLevel +public interface IDhServerLevel extends IDhLevel { void serverTick(); IServerLevelWrapper getServerLevelWrapper(); + } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/IKeyedClientLevelManager.java b/core/src/main/java/com/seibel/distanthorizons/core/level/IKeyedClientLevelManager.java index ac8654c82..c8544d71e 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/IKeyedClientLevelManager.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/IKeyedClientLevelManager.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.core.level; -import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable; /** @@ -28,15 +28,11 @@ import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindab */ public interface IKeyedClientLevelManager extends IBindable { + IServerKeyedClientLevel getServerKeyedLevel(); /** Called when a client level is wrapped by a ServerEnhancedClientLevel, for integration into mod internals. */ - void setServerKeyedLevel(IServerKeyedClientLevel clientLevel); - IServerKeyedClientLevel getOverrideWrapper(); + IServerKeyedClientLevel setServerKeyedLevel(IClientLevelWrapper clientLevel, String levelKey); - /** Returns a new instance of a ServerEnhancedClientLevel. */ - IServerKeyedClientLevel getServerKeyedLevel(ILevelWrapper level, String serverLevelKey); - - /** Sets the LOD engine to use the override wrapper, if the server has communication enabled. */ - void setUseOverrideWrapper(boolean useOverrideWrapper); - boolean getUseOverrideWrapper(); + void clearKeyedLevel(); + boolean hasLevelSet(); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java b/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java index 39b16f300..5ddf8729a 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java @@ -27,9 +27,14 @@ import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import org.apache.logging.log4j.Logger; import java.io.Closeable; +import java.util.List; import java.util.concurrent.CompletableFuture; 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 { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); @@ -137,20 +142,22 @@ public class WorldGenModule implements Closeable public boolean isWorldGenRunning() { return this.worldGenStateRef.get() != null; } - public String getDebugMenuString() + /** mutates a list so it can be added to an existing {@link IDhLevel}'s debug list */ + public void addDebugMenuStringsToList(List messageList) { AbstractWorldGenState worldGenState = this.worldGenStateRef.get(); if (worldGenState == null) { - return null; + return; } String waitingCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getWaitingTaskCount()); String inProgressCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getInProgressTaskCount()); String totalCountEstimateStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getEstimatedTotalTaskCount()); - - return "World Gen Tasks: "+waitingCountStr+"/"+totalCountEstimateStr+" (in progress: "+inProgressCountStr+")"; + messageList.add("World Gen Tasks: "+waitingCountStr+"/"+totalCountEstimateStr+" (in progress: "+inProgressCountStr+")"); + + worldGenState.worldGenerationQueue.addDebugMenuStringsToList(messageList); } @@ -166,18 +173,18 @@ public class WorldGenModule implements Closeable CompletableFuture closeAsync(boolean doInterrupt) { - return this.worldGenerationQueue.startClosing(true, doInterrupt) - .exceptionally(ex -> - { - LOGGER.error("Error closing generation queue", ex); - return null; - } - ).thenRun(this.worldGenerationQueue::close) - .exceptionally(ex -> - { - LOGGER.error("Error closing world gen", ex); - return null; - }); + return this.worldGenerationQueue.startClosingAsync(true, doInterrupt) + .exceptionally(e -> + { + LOGGER.error("Error during first stage of generation queue shutdown, Error: ["+e.getMessage()+"].", e); + return null; + } + ).thenRun(this.worldGenerationQueue::close) + .exceptionally(e -> + { + LOGGER.error("Error during second stage of generation queue shutdown, Error: ["+e.getMessage()+"].", e); + return null; + }); } /** @param targetPosForGeneration the position that world generation should be centered around */ diff --git a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/AbstractFullDataNetworkRequestQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/AbstractFullDataNetworkRequestQueue.java new file mode 100644 index 000000000..1eb2fac5c --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/AbstractFullDataNetworkRequestQueue.java @@ -0,0 +1,407 @@ +package com.seibel.distanthorizons.core.multiplayer.client; + +import com.google.common.base.Stopwatch; +import com.seibel.distanthorizons.core.config.Config; +import com.seibel.distanthorizons.core.config.types.ConfigEntry; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; +import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; +import com.seibel.distanthorizons.core.level.IDhClientLevel; +import com.seibel.distanthorizons.core.logging.ConfigBasedSpamLogger; +import com.seibel.distanthorizons.core.network.exceptions.InvalidLevelException; +import com.seibel.distanthorizons.core.network.exceptions.RateLimitedException; +import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException; +import com.seibel.distanthorizons.core.network.session.SessionClosedException; +import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage; +import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceResponseMessage; +import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; +import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; +import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; +import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO; +import com.seibel.distanthorizons.core.util.LodUtil; +import com.seibel.distanthorizons.core.util.TimerUtil; +import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; +import com.seibel.distanthorizons.core.util.ratelimiting.SupplierBasedRateLimiter; +import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; +import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; +import org.apache.logging.log4j.LogManager; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import java.awt.*; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRenderable, AutoCloseable +{ + private static final ConfigBasedSpamLogger LOGGER = new ConfigBasedSpamLogger(LogManager.getLogger(), + () -> Config.Client.Advanced.Logging.logNetworkEvent.get(), 3); + + private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); + + private static final Timer TASK_FINISH_TIMER = TimerUtil.CreateTimer("RequestTaskFinishTimer"); + + private static final int MAX_RETRY_ATTEMPTS = 3; + + protected static final long SHUTDOWN_TIMEOUT_SECONDS = 5; + + + + public final ClientNetworkState networkState; + protected final IDhClientLevel level; + private final boolean changedOnly; + + private volatile CompletableFuture closingFuture = null; + + protected final ConcurrentMap waitingTasksBySectionPos = new ConcurrentHashMap<>(); + private final Semaphore pendingTasksSemaphore = new Semaphore(Short.MAX_VALUE, true); + + private final AtomicInteger finishedRequests = new AtomicInteger(); + private final AtomicInteger failedRequests = new AtomicInteger(); + private final ConfigEntry showDebugWireframeConfig; + + private final SupplierBasedRateLimiter rateLimiter = new SupplierBasedRateLimiter<>(this::getRequestRateLimit); + + + + //=============// + // constructor // + //=============// + + public AbstractFullDataNetworkRequestQueue( + ClientNetworkState networkState, IDhClientLevel level, + boolean changedOnly, ConfigEntry showDebugWireframeConfig) + { + this.networkState = networkState; + this.level = level; + this.changedOnly = changedOnly; + this.showDebugWireframeConfig = showDebugWireframeConfig; + DebugRenderer.register(this, this.showDebugWireframeConfig); + } + + + + //==================// + // abstract methods // + //==================// + + protected abstract int getRequestRateLimit(); + + protected abstract String getQueueName(); + + + + //====================// + // request submitting // + //====================// + + public CompletableFuture submitRequest(long sectionPos, Consumer dataSourceConsumer) + { return this.submitRequest(sectionPos, null, dataSourceConsumer); } + public CompletableFuture submitRequest(long sectionPos, @Nullable Long clientTimestamp, Consumer dataSourceConsumer) + { + LodUtil.assertTrue(DhSectionPos.getDetailLevel(sectionPos) == DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL, "Only highest-detail sections are allowed."); + + RequestQueueEntry entry = new RequestQueueEntry(dataSourceConsumer, clientTimestamp); + entry.future.whenComplete((success, throwable) -> + { + this.waitingTasksBySectionPos.remove(sectionPos); + + this.finishedRequests.incrementAndGet(); + if (!success || throwable != null) + { + this.failedRequests.incrementAndGet(); + } + }); + + this.waitingTasksBySectionPos.put(sectionPos, entry); + return entry.future; + } + + public synchronized boolean tick(DhBlockPos2D targetPos) + { + if (this.closingFuture != null || !this.networkState.isReady()) + { + return false; + } + + // queue requests until the queue is full + while (this.getInProgressTaskCount() < this.getWaitingTaskCount() + && this.getInProgressTaskCount() < this.getRequestRateLimit() + && this.pendingTasksSemaphore.tryAcquire()) + { + if (!this.rateLimiter.tryAcquire()) + { + this.pendingTasksSemaphore.release(); + break; + } + + this.sendNextRequest(targetPos); + } + + return true; + } + private void sendNextRequest(DhBlockPos2D targetPos) + { + Map.Entry mapEntry = this.waitingTasksBySectionPos.entrySet().stream() + .filter(task -> task.getValue().networkDataSourceFuture == null) + .min((x, y) -> posDistanceSquared(targetPos, x.getKey()) - posDistanceSquared(targetPos, y.getKey())) + .orElse(null); + + if (mapEntry == null) + { + this.pendingTasksSemaphore.release(); + return; + } + + long sectionPos = mapEntry.getKey(); + RequestQueueEntry entry = mapEntry.getValue(); + + CompletableFuture dataSourceFuture = this.networkState.getSession().sendRequest( + new FullDataSourceRequestMessage(this.level.getLevelWrapper(), sectionPos, entry.updateTimestamp), + FullDataSourceResponseMessage.class + ); + entry.networkDataSourceFuture = dataSourceFuture; + dataSourceFuture.handle((response, throwable) -> + { + this.pendingTasksSemaphore.release(); + + try + { + if (throwable != null) + { + throw throwable; + } + + if (response.payload != null) + { + FullDataSourceV2DTO dataSourceDto = this.networkState.decodeDataSourceAndReleaseBuffer(response.payload); + + ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor(); + if (executor == null) + { + LOGGER.warn("Unable to handle FullDataPayload - getNetworkCompressionExecutor() is null"); + return null; + } + CompletableFuture.runAsync(() -> + { + try + { + FullDataSourceV2 fullDataSource = dataSourceDto.createPooledDataSource(this.level.getLevelWrapper()); + entry.dataSourceConsumer.accept(fullDataSource); + FullDataSourceV2.DATA_SOURCE_POOL.returnPooledDataSource(fullDataSource); + } + catch (IOException | DataCorruptedException | InterruptedException e) + { + throw new RuntimeException(e); + } + }, executor); + } + else + { + LodUtil.assertTrue(this.changedOnly, "Received empty data source response for not changes-only request"); + } + } + catch (InvalidLevelException | RequestRejectedException ignored) + { + // We're too late / some cases might trigger a bunch of expected rejections + return entry.future.complete(false); + } + catch (SessionClosedException | CancellationException ignored) + { + // Triggered when level is unloaded + return entry.future.cancel(false); + } + catch (RateLimitedException e) + { + LOGGER.warn("Rate limited by server, re-queueing task [" + DhSectionPos.toString(sectionPos) + "]: " + e.getMessage()); + + // Skip all requests for 1 second + this.rateLimiter.acquireAll(); + + entry.networkDataSourceFuture = null; + return null; + } + catch (Throwable e) + { + entry.retryAttempts--; + LOGGER.error("Error while fetching full data source, attempts left: {} / {}", entry.retryAttempts, MAX_RETRY_ATTEMPTS, e); + + // Retry logic + if (entry.retryAttempts > 0) + { + entry.networkDataSourceFuture = null; + return null; + } + else + { + return entry.future.complete(false); + } + } + + // Hack to work around a race condition + // If you finish the request too quickly, the section will never render + TASK_FINISH_TIMER.schedule(new TimerTask() + { + @Override + public void run() + { + entry.future.complete(true); + } + }, 10000); + return null; + }); + } + + + + + //=========================================// + // IFullDataSourceRetrievalQueue overrides // + //=========================================// + + public void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf) + { + for (Map.Entry 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 messageList) + { + 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() + ")"); + } + + public int getWaitingTaskCount() { return this.waitingTasksBySectionPos.size(); } + public int getInProgressTaskCount() { return Short.MAX_VALUE - this.pendingTasksSemaphore.availablePermits(); } + + + + //==========// + // shutdown // + //==========// + + + public CompletableFuture startClosingAsync(boolean alsoInterruptRunning) + { + return this.closingFuture = CompletableFuture.runAsync(() -> { + Stopwatch stopwatch = Stopwatch.createStarted(); + + do + { + for (RequestQueueEntry entry : this.waitingTasksBySectionPos.values()) + { + entry.future.cancel(alsoInterruptRunning); + if (entry.networkDataSourceFuture != null && entry.networkDataSourceFuture.cancel(alsoInterruptRunning)) + { + this.pendingTasksSemaphore.release(); + } + } + } + while (!this.pendingTasksSemaphore.tryAcquire(Short.MAX_VALUE) && stopwatch.elapsed(TimeUnit.SECONDS) < SHUTDOWN_TIMEOUT_SECONDS); + + if (stopwatch.elapsed(TimeUnit.SECONDS) >= SHUTDOWN_TIMEOUT_SECONDS) + { + 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."); + } + }); + } + + @Override + public void close() + { + DebugRenderer.unregister(this, this.showDebugWireframeConfig); + } + + + + //===========// + // debugging // + //===========// + + @Override + public void debugRender(DebugRenderer renderer) + { + if (MC_CLIENT.getWrappedClientLevel() != this.level.getClientLevelWrapper()) + { + return; + } + + for (Map.Entry mapEntry : this.waitingTasksBySectionPos.entrySet()) + { + renderer.renderBox(new DebugRenderer.Box(mapEntry.getKey(), -32f, 64f, 0.05f, + 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 + { + /** encapsulates the entire request, including client side queuing and the actual server request */ + public final CompletableFuture future = new CompletableFuture<>(); + public final Consumer dataSourceConsumer; + /** will be null if we want to retrieve the LOD regardless of when it was last updated */ + @Nullable + public final Long updateTimestamp; + + + /** Will be null until the request has been sent to the server */ + @CheckForNull + public CompletableFuture networkDataSourceFuture; + + /** when this reaches zero then the request will be canceled. */ + public int retryAttempts = MAX_RETRY_ATTEMPTS; + + + + //=============// + // constructor // + //=============// + + public RequestQueueEntry( + Consumer dataSourceConsumer, + @Nullable Long updateTimestamp) + { + this.dataSourceConsumer = dataSourceConsumer; + this.updateTimestamp = updateTimestamp; + } + + } + + + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/ClientNetworkState.java b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/ClientNetworkState.java new file mode 100644 index 000000000..cc5e646c5 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/ClientNetworkState.java @@ -0,0 +1,210 @@ +package com.seibel.distanthorizons.core.multiplayer.client; + +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.multiplayer.config.SessionConfig; +import com.seibel.distanthorizons.core.network.INetworkObject; +import com.seibel.distanthorizons.core.network.event.ScopedNetworkEventSource; +import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent; +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.SessionConfigMessage; +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.FullDataPayload; +import com.seibel.distanthorizons.core.network.session.NetworkSession; +import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO; +import com.seibel.distanthorizons.core.util.LodUtil; +import com.seibel.distanthorizons.coreapi.ModInfo; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import org.apache.logging.log4j.LogManager; +import org.jetbrains.annotations.Nullable; + +import java.io.Closeable; +import java.util.List; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +public class ClientNetworkState implements Closeable +{ + protected static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(), + () -> Config.Client.Advanced.Logging.logNetworkEvent.get()); + + + private final ConcurrentMap fullDataBufferById = CacheBuilder.newBuilder() + .expireAfterAccess(10, TimeUnit.SECONDS) + .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.

+ * If you need to subscribe to any packet events, create an instance of {@link ScopedNetworkEventSource} using the returned instance. + */ + public NetworkSession getSession() { return this.networkSession; } + + public SessionConfig sessionConfig = new SessionConfig(); + + private volatile boolean configReceived = false; + public boolean isReady() { return this.configReceived; } + + private EServerSupportStatus serverSupportStatus = EServerSupportStatus.NONE; + + /** Protocol version closest to supported by this mod version */ + @Nullable + private Integer closestProtocolVersion; + + + + //=============// + // constructor // + //=============// + + public ClientNetworkState() + { + this.networkSession.registerHandler(IncompatibleMessageInternalEvent.class, event -> + { + if (this.closestProtocolVersion == null + || Math.abs(event.protocolVersion - ModInfo.PROTOCOL_VERSION) < this.closestProtocolVersion) + { + this.closestProtocolVersion = event.protocolVersion; + } + }); + + this.networkSession.registerHandler(CurrentLevelKeyMessage.class, message -> + { + // we will also receive this message when we have full support + if (this.serverSupportStatus == EServerSupportStatus.NONE) + { + this.serverSupportStatus = EServerSupportStatus.LEVELS_ONLY; + } + }); + + this.networkSession.registerHandler(SessionConfigMessage.class, message -> + { + this.serverSupportStatus = EServerSupportStatus.FULL; + + LOGGER.info("Connection config has been changed: ["+message.config+"]."); + this.sessionConfig = message.config; + this.configReceived = true; + }); + + this.networkSession.registerHandler(CloseInternalEvent.class, message -> + { + this.configReceived = false; + }); + + this.networkSession.registerHandler(FullDataSplitMessage.class, message -> + { + if (message.isFirst) + { + CompositeByteBuf composite = this.fullDataBufferById.remove(message.bufferId); + if (composite != null) + { + composite.release(); + LOGGER.debug("Released full data buffer ["+message.bufferId+"]: ["+composite+"]"); + } + } + + CompositeByteBuf byteBuffer = this.fullDataBufferById.computeIfAbsent(message.bufferId, bufferId -> ByteBufAllocator.DEFAULT.compositeBuffer()); + byteBuffer.addComponent(true, message.buffer); + LOGGER.debug("Full data buffer ["+message.bufferId+"]: ["+byteBuffer+"]."); + }); + + this.networkSession.registerHandler(FullDataPartialUpdateMessage.class, msg -> + { + // Dummy handler to prevent unhandled message warnings + }); + } + + + + //==============// + // send message // + //==============// + + public FullDataSourceV2DTO decodeDataSourceAndReleaseBuffer(FullDataPayload msg) + { + CompositeByteBuf compositeByteBuffer = this.fullDataBufferById.remove(msg.dtoBufferId); + LodUtil.assertTrue(compositeByteBuffer != null); + + try + { + return INetworkObject.decodeToInstance(FullDataSourceV2DTO.CreateEmptyDataSource(), compositeByteBuffer); + } + finally + { + compositeByteBuffer.release(); + } + } + + public void sendConfigMessage() + { + this.configReceived = false; + this.getSession().sendMessage(new SessionConfigMessage(new SessionConfig())); + } + + + + //===========// + // debugging // + //===========// + + public void addDebugMenuStringsToList(List messageList) + { + if (this.networkSession.isClosed()) + { + messageList.add("NetworkSession closed: " + this.networkSession.getCloseReason().getMessage()); + return; + } + + if (this.serverSupportStatus == EServerSupportStatus.NONE && this.closestProtocolVersion != null) + { + messageList.add("Incompatible protocol version: [" + this.closestProtocolVersion + "], required: [" + ModInfo.PROTOCOL_VERSION+ "]"); + return; + } + + messageList.add(this.serverSupportStatus.message); + } + + + + //==========// + // shutdown // + //==========// + + @Override + public void close() + { + this.configAnyChangeListener.close(); + this.networkSession.close(); + } + + + + //================// + // helper classes // + //================// + + /** + * NONE,
+ * LEVELS_ONLY,
+ * FULL,
+ */ + private enum EServerSupportStatus + { + NONE("Server does not support DH"), + LEVELS_ONLY("Server supports shared level keys"), + FULL("Server has full DH support"); + + public final String message; + + EServerSupportStatus(String message) { this.message = message; } + + } +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/SyncOnLoginRequestQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/SyncOnLoginRequestQueue.java new file mode 100644 index 000000000..efc68baba --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/SyncOnLoginRequestQueue.java @@ -0,0 +1,57 @@ +package com.seibel.distanthorizons.core.multiplayer.client; + +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.pos.blockPos.DhBlockPos2D; + +/** + * 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) + { super(networkState, level, true, Config.Client.Advanced.Debugging.DebugWireframe.showWorldGenQueue); } + + + + //=========// + // getters // + //=========// + + @Override + protected int getRequestRateLimit() { return this.networkState.sessionConfig.getSyncOnLoginRateLimit(); } + + @Override + protected String getQueueName() { return "Sync On Login Queue"; } + + + + //==================// + // request handling // + //==================// + + @Override + public boolean tick(DhBlockPos2D targetPos) + { + if (!this.networkState.sessionConfig.getSynchronizeOnLogin()) + { + return false; + } + + return super.tick(targetPos); + } + + + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/config/SessionConfig.java b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/config/SessionConfig.java new file mode 100644 index 000000000..3dca5d7e0 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/config/SessionConfig.java @@ -0,0 +1,184 @@ +package com.seibel.distanthorizons.core.multiplayer.config; + +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.types.ConfigEntry; +import com.seibel.distanthorizons.core.network.INetworkObject; +import io.netty.buffer.ByteBuf; + +import java.io.Closeable; +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class SessionConfig implements INetworkObject +{ + private static final LinkedHashMap CONFIG_ENTRIES = new LinkedHashMap<>(); + + + private final LinkedHashMap values = new LinkedHashMap<>(); + public SessionConfig constrainingConfig; + + + + //=============// + // constructor // + //=============// + + static + { + // Note: config values are ordered by serversideShortName when transmitted + + registerConfigEntry(Config.Client.Advanced.Graphics.Quality.lodChunkRenderDistanceRadius, Math::min); + + registerConfigEntry(Config.Client.Advanced.WorldGenerator.enableDistantGeneration, (x, y) -> x && y); + registerConfigEntry(Config.Client.Advanced.Multiplayer.ServerNetworking.generationRequestRateLimit, Math::min); + + registerConfigEntry(Config.Client.Advanced.Multiplayer.ServerNetworking.enableRealTimeUpdates, (x, y) -> x && y); + + registerConfigEntry(Config.Client.Advanced.Multiplayer.ServerNetworking.synchronizeOnLogin, (x, y) -> x && y); + registerConfigEntry(Config.Client.Advanced.Multiplayer.ServerNetworking.syncOnLoginRateLimit, Math::min); + } + + public SessionConfig() {} + + + + //===============// + // 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 void registerConfigEntry(ConfigEntry configEntry, BiFunction valueConstrainer) + { + CONFIG_ENTRIES.put(Objects.requireNonNull(configEntry.getServersideShortName()), new Entry(configEntry, valueConstrainer)); + } + + + + //==================// + // internal getters // + //==================// + + private T getValue(ConfigEntry configEntry) { return this.getValue(configEntry.getServersideShortName()); } + @SuppressWarnings("unchecked") + private T getValue(String name) + { + Entry entry = CONFIG_ENTRIES.get(name); + + T value = (T) this.values.get(name); + if (value == null) + { + value = (T) entry.supplier.get(); + } + + return (this.constrainingConfig != null + ? (T) entry.valueConstrainer.apply(value, this.constrainingConfig.getValue(name)) + : value); + } + + private Map getValues() + { + return CONFIG_ENTRIES.keySet().stream().collect(Collectors.toMap( + Function.identity(), + this::getValue, + (x, y) -> x, + LinkedHashMap::new + )); + } + + + + //===============// + // serialization // + //===============// + + @Override + public void encode(ByteBuf outBuffer) { this.writeFixedLengthCollection(outBuffer, this.getValues().values()); } + + @Override + public void decode(ByteBuf inBuffer) + { + for (String key : CONFIG_ENTRIES.keySet()) + { + Object currentValue = this.getValue(key); + Object newValue = Codec.getCodec(currentValue.getClass()).decode.apply(currentValue, inBuffer); + this.values.put(key, newValue); + } + } + + + + //================// + // base overrides // + //================// + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("values", this.getValues()) + .toString(); + } + + + + //================// + // helper classes // + //================// + + private static class Entry + { + public final ConfigEntry supplier; + public final BiFunction valueConstrainer; + + @SuppressWarnings("unchecked") + private Entry(ConfigEntry supplier, BiFunction valueConstrainer) + { + this.supplier = (ConfigEntry) supplier; + this.valueConstrainer = (BiFunction) valueConstrainer; + } + + } + + /** fires if any config value was changed */ + public static class AnyChangeListener implements Closeable + { + private final ArrayList> changeListeners; + + public AnyChangeListener(Runnable runnable) + { + this.changeListeners = new ArrayList<>(CONFIG_ENTRIES.size()); + for (Entry entry : CONFIG_ENTRIES.values()) + { + this.changeListeners.add(new ConfigChangeListener<>(entry.supplier, ignored -> runnable.run())); + } + } + + @Override + public void close() + { + for (ConfigChangeListener changeListener : this.changeListeners) + { + changeListener.close(); + } + this.changeListeners.clear(); + } + + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/RemotePlayerConnectionHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/RemotePlayerConnectionHandler.java new file mode 100644 index 000000000..2ae9e1290 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/RemotePlayerConnectionHandler.java @@ -0,0 +1,85 @@ +package com.seibel.distanthorizons.core.multiplayer.server; + +import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; +import com.seibel.distanthorizons.core.network.session.NetworkSession; +import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; +import org.jetbrains.annotations.Nullable; + +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; + +public class RemotePlayerConnectionHandler +{ + private final ConcurrentMap connectedPlayerStateByPlayerWrapper = new ConcurrentHashMap<>(); + private final ConcurrentMap> messageQueueByPlayerWrapper = new ConcurrentHashMap<>(); + + + + //========================// + // player joining/leaving // + //========================// + + public ServerPlayerState registerJoinedPlayer(IServerPlayerWrapper serverPlayer) + { + ServerPlayerState playerState = new ServerPlayerState(serverPlayer); + this.connectedPlayerStateByPlayerWrapper.put(serverPlayer, playerState); + + Queue queuedMessages = this.messageQueueByPlayerWrapper.get(serverPlayer); + if (queuedMessages != null) + { + NetworkSession networkSession = playerState.networkSession; + for (AbstractNetworkMessage message : queuedMessages) + { + networkSession.tryHandleMessage(message); + } + + this.messageQueueByPlayerWrapper.remove(serverPlayer); + } + + return playerState; + } + + public void unregisterLeftPlayer(IServerPlayerWrapper serverPlayer) + { + ServerPlayerState playerState = this.connectedPlayerStateByPlayerWrapper.remove(serverPlayer); + if (playerState != null) + { + 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 getConnectedPlayers() { return this.connectedPlayerStateByPlayerWrapper.values(); } + + + + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/ServerPlayerState.java b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/ServerPlayerState.java new file mode 100644 index 000000000..dfb1205a4 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/ServerPlayerState.java @@ -0,0 +1,123 @@ +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.level.DhServerLevel; +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.SessionConfigMessage; +import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent; +import com.seibel.distanthorizons.core.network.exceptions.RateLimitedException; +import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage; +import com.seibel.distanthorizons.core.network.session.NetworkSession; +import com.seibel.distanthorizons.core.util.ratelimiting.SupplierBasedRateAndConcurrencyLimiter; +import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; +import org.jetbrains.annotations.NotNull; + +import java.io.Closeable; +import java.util.concurrent.ConcurrentHashMap; + +public class ServerPlayerState implements Closeable +{ + private final ConfigChangeListener levelKeyPrefixChangeListener + = new ConfigChangeListener<>(Config.Client.Advanced.Multiplayer.ServerNetworking.levelKeyPrefix, this::onLevelKeyPrefixConfigChanged); + private final SessionConfig.AnyChangeListener configAnyChangeListener = new SessionConfig.AnyChangeListener(this::onSessionConfigChanged); + + + private String lastLevelKey = ""; + + + public final NetworkSession networkSession; + public IServerPlayerWrapper getServerPlayer() { return this.networkSession.serverPlayer; } + + @NotNull + public final SessionConfig sessionConfig = new SessionConfig(); + + private final ConcurrentHashMap rateLimiterSets = new ConcurrentHashMap<>(); + public RateLimiterSet getRateLimiterSet(DhServerLevel level) { return this.rateLimiterSets.computeIfAbsent(level, ignored -> new RateLimiterSet()); } + public void clearRateLimiterSets() { this.rateLimiterSets.clear(); } + + + + //==============// + // constructors // + //==============// + + public ServerPlayerState(IServerPlayerWrapper serverPlayer) + { + this.networkSession = new NetworkSession(serverPlayer); + + this.networkSession.registerHandler(SessionConfigMessage.class, (sessionConfigMessage) -> + { + this.sessionConfig.constrainingConfig = sessionConfigMessage.config; + this.sendLevelKey(); + this.networkSession.sendMessage(new SessionConfigMessage(this.sessionConfig)); + }); + + this.networkSession.registerHandler(CloseInternalEvent.class, event -> { + // No-op. prevents "Unhandled message" log entries + }); + } + + + + //=================// + // client updating // + //=================// + + private void onLevelKeyPrefixConfigChanged(String newLevelKey) { this.sendLevelKey(); } + private void sendLevelKey() + { + if (Config.Client.Advanced.Multiplayer.ServerNetworking.sendLevelKeys.get()) + { + // let the client's know about the change + String levelKey = this.getServerPlayer().getLevel().getKeyedLevelDimensionName(); + if (!levelKey.equals(this.lastLevelKey)) + { + this.lastLevelKey = levelKey; + this.networkSession.sendMessage(new CurrentLevelKeyMessage(levelKey)); + } + } + } + + private void onSessionConfigChanged() { this.networkSession.sendMessage(new SessionConfigMessage(this.sessionConfig)); } + + + + //==========// + // shutdown // + //==========// + + @Override + public void close() + { + this.levelKeyPrefixChangeListener.close(); + this.configAnyChangeListener.close(); + this.networkSession.close(); + } + + + + //================// + // helper classes // + //================// + + public class RateLimiterSet + { + public final SupplierBasedRateAndConcurrencyLimiter generationRequestRateLimiter = new SupplierBasedRateAndConcurrencyLimiter<>( + () -> Config.Client.Advanced.Multiplayer.ServerNetworking.generationRequestRateLimit.get(), + msg -> { + msg.sendResponse(new RateLimitedException("Full data request rate limit: " + ServerPlayerState.this.sessionConfig.getGenerationRequestRateLimit())); + } + ); + + public final SupplierBasedRateAndConcurrencyLimiter syncOnLoginRateLimiter = new SupplierBasedRateAndConcurrencyLimiter<>( + () -> Config.Client.Advanced.Multiplayer.ServerNetworking.syncOnLoginRateLimit.get(), + msg -> { + msg.sendResponse(new RateLimitedException("Sync on login rate limit: " + ServerPlayerState.this.sessionConfig.getSyncOnLoginRateLimit())); + } + ); + + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/INetworkObject.java b/core/src/main/java/com/seibel/distanthorizons/core/network/INetworkObject.java new file mode 100644 index 000000000..e464efa74 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/INetworkObject.java @@ -0,0 +1,228 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.network; + +import io.netty.buffer.ByteBuf; +import org.jetbrains.annotations.Contract; + +import javax.annotation.Nullable; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.*; + +/** The base for any object that can be sent over the network. */ +public interface INetworkObject +{ + /** Serializes this object into the given {@link ByteBuf} */ + void encode(ByteBuf out); + /** Populates this object's from the given {@link ByteBuf} */ + void decode(ByteBuf in); + + + + //========================// + // default/static methods // + //========================// + + /** + * @param obj the empty object that will be populated by the incoming {@link ByteBuf} + */ + static T decodeToInstance(T obj, ByteBuf inputByteBuf) + { + obj.decode(inputByteBuf); + return obj; + } + + + + @Contract("_, null -> false; _, !null -> true") + default boolean tryWrite(ByteBuf outputByteBuf, Object value) + { + boolean isNull = value != null; + outputByteBuf.writeBoolean(isNull); + return isNull; + } + + @Nullable + default T tryRead(ByteBuf inputByteBuf, Supplier decoder) + { + return inputByteBuf.readBoolean() + ? decoder.get() + : null; + } + default void tryRead(ByteBuf inputByteBuf, Runnable decoder) + { + if (inputByteBuf.readBoolean()) + { + decoder.run(); + } + } + + + // strings // + + default void writeString(String inputString, ByteBuf outputByteBuf) { INetworkObject.writeStringStatic(inputString, outputByteBuf); } + static void writeStringStatic(String inputString, ByteBuf outputByteBuf) + { + byte[] bytes = inputString.getBytes(StandardCharsets.UTF_8); + outputByteBuf.writeShort(bytes.length); + outputByteBuf.writeBytes(bytes); + } + + default String readString(ByteBuf inputByteBuf) { return INetworkObject.readStringStatic(inputByteBuf); } + static String readStringStatic(ByteBuf inputByteBuf) + { + int length = inputByteBuf.readUnsignedShort(); + return inputByteBuf.readSlice(length).toString(StandardCharsets.UTF_8); + } + + + // collections // + + default void writeCollection(ByteBuf outputByteBuf, Collection collection) + { + outputByteBuf.writeInt(collection.size()); + this.writeFixedLengthCollection(outputByteBuf, collection); + } + default void writeFixedLengthCollection(ByteBuf outputByteBuf, Collection collection) + { + for (T item : collection) + { + Codec codec = Codec.getCodec(item.getClass()); + codec.encode.accept(item, outputByteBuf); + } + } + + default void readCollection(ByteBuf inputByteBuf, Collection collection, Supplier innerValueConstructor) + { + int size = inputByteBuf.readInt(); + + Codec codec = null; + for (int i = 0; i < size; i++) + { + T item = innerValueConstructor.get(); + + if (codec == null) + { + codec = Codec.getCodec(item.getClass()); + } + //noinspection unchecked + item = (T) codec.decode.apply(item, inputByteBuf); + + collection.add(item); + } + } + + default void readMap(ByteBuf inputByteBuf, Map map, Supplier keySupplier, Supplier valueSupplier) + { + ArrayList> entryList = new ArrayList<>(); + + this.readCollection(inputByteBuf, entryList, () -> new AbstractMap.SimpleEntry<>(keySupplier.get(), valueSupplier.get())); + for (Map.Entry entry : entryList) + { + map.put(entry.getKey(), entry.getValue()); + } + } + + + + //================// + // helper classes // + //================// + + /** + * Defines (de)serialization for different classes, + * specifically for base classes like {@link Integer}, {@link Boolean}, and {@link String}.

+ * + * Should only be used for non-editable classes; + * otherwise, you may want to implement {@link INetworkObject} and use its methods where applicable. + */ + class Codec + { + private static final ConcurrentMap, Codec> CODEC_MAP = new ConcurrentHashMap, Codec>() + {{ + // Primitives must be added manually here + this.put(Integer.class, new Codec((obj, outByteBuff) -> outByteBuff.writeInt((int)obj), (obj, inByteBuff) -> inByteBuff.readInt())); + this.put(Boolean.class, new Codec((obj, outByteBuff) -> outByteBuff.writeBoolean((boolean) obj), (obj, inByteBuff) -> inByteBuff.readBoolean())); + 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(Map.Entry.class, new Codec( + (obj, outByteBuff) -> + { + Map.Entry entry = (Entry) obj; + getCodec(entry.getKey().getClass()).encode.accept(entry.getKey(), outByteBuff); + getCodec(entry.getValue().getClass()).encode.accept(entry.getValue(), outByteBuff); + }, + (obj, inByteBuff) -> + { + Map.Entry entry = (Entry) obj; + return new SimpleEntry<>( + getCodec(entry.getKey().getClass()).decode.apply(entry.getKey(), inByteBuff), + getCodec(entry.getValue().getClass()).decode.apply(entry.getValue(), inByteBuff) + ); + } + )); + }}; + + + public final BiConsumer encode; + public final BiFunction decode; + + + + //=============// + // constructor // + //=============// + + @SuppressWarnings("unchecked") + public Codec(BiConsumer encode, BiFunction decode) + { + this.encode = (BiConsumer) encode; + this.decode = (BiFunction) decode; + } + + + + //================// + // static methods // + //================// + + public static Codec getCodec(Class clazz) + { + return CODEC_MAP.computeIfAbsent(clazz, ignored -> + { + // TODO when would this ever return true? + for (Map.Entry, Codec> entry : CODEC_MAP.entrySet()) + { + if (entry.getKey().isAssignableFrom(clazz)) + { + return entry.getValue(); + } + } + + throw new AssertionError("Class has no compatible codec: " + clazz.getSimpleName()); + }); + } + + } +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/event/AbstractNetworkEventSource.java b/core/src/main/java/com/seibel/distanthorizons/core/network/event/AbstractNetworkEventSource.java new file mode 100644 index 000000000..65f6b2017 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/event/AbstractNetworkEventSource.java @@ -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 . + */ + +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.
+ * Grouped by:
+ * - {@link AbstractNetworkMessage} type
+ * - {@link AbstractNetworkEventSource}
+ */ + private final ConcurrentHashMap< + Class, + ConcurrentMap> + > networkHandlerSetByMessageClass = new ConcurrentHashMap<>(); + + + private final ConcurrentHashMap pendingFutureById = new ConcurrentHashMap<>(); + /** automatically forgets about ID's after a set time span. */ + private final Set cancelledFutureIdSet = Collections.newSetFromMap( + CacheBuilder.newBuilder() + .expireAfterWrite(10, TimeUnit.SECONDS) + .build() + .asMap()); + + + + //=============// + // constructor // + //=============// + + protected void handleMessage(AbstractNetworkMessage message) + { + boolean handled = false; + + ConcurrentMap> handlersByEventSource = this.networkHandlerSetByMessageClass.get(message.getClass()); + if (handlersByEventSource != null) + { + for (Set 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 void registerHandler(Class handlerClass, Consumer handlerImplementation); + + + + //==================// + // message handlers // + //==================// + + protected final void registerHandler(AbstractNetworkEventSource eventSource, Class handlerClass, Consumer 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> handlerMap : this.networkHandlerSetByMessageClass.values()) + { + handlerMap.remove(eventSource); + } + } + + + + //================// + // create message // + //================// + + protected CompletableFuture createRequest(AbstractTrackableMessage msg, Class responseClass) + { + CompletableFuture 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 responseClass; + public final CompletableFuture future; + + private FutureResponseData(Class responseClass, CompletableFuture future) + { + this.responseClass = responseClass; + //noinspection unchecked + this.future = (CompletableFuture) future; + } + + } + + /** Simple wrapper just to make the code here a bit more readable */ + @FunctionalInterface + private interface INetworkMessageConsumer + { + void accept(AbstractNetworkMessage message); + } + + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/event/ScopedNetworkEventSource.java b/core/src/main/java/com/seibel/distanthorizons/core/network/event/ScopedNetworkEventSource.java new file mode 100644 index 000000000..f25981482 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/event/ScopedNetworkEventSource.java @@ -0,0 +1,81 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.network.event; + +import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; + +import java.util.function.Consumer; + +/** + * Provides a way to register network message handlers which are expected to be removed later.

+ * + * 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 AbstractNetworkEventSource parent; + private boolean isClosed = false; + + private final Consumer actualHandleMessageStable = this::handleMessage; + + + + //=============// + // constructor // + //=============// + + public ScopedNetworkEventSource(AbstractNetworkEventSource parent) { this.parent = parent; } + + + + //==================// + // message handlers // + //==================// + + @Override + public void registerHandler(Class handlerClass, Consumer handlerImplementation) + { + if (this.isClosed) + { + return; + } + + //noinspection unchecked + this.parent.registerHandler(this, handlerClass, (Consumer) this.actualHandleMessageStable); + + super.registerHandler(this, handlerClass, handlerImplementation); + } + + + + //==========// + // shutdown // + //==========// + + @Override + public void close() + { + this.isClosed = true; + this.parent.removeAllHandlers(this); + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/event/internal/AbstractInternalEvent.java b/core/src/main/java/com/seibel/distanthorizons/core/network/event/internal/AbstractInternalEvent.java new file mode 100644 index 000000000..e7e45bab8 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/event/internal/AbstractInternalEvent.java @@ -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."); } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/event/internal/CloseInternalEvent.java b/core/src/main/java/com/seibel/distanthorizons/core/network/event/internal/CloseInternalEvent.java new file mode 100644 index 000000000..619d164fe --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/event/internal/CloseInternalEvent.java @@ -0,0 +1,9 @@ +package com.seibel.distanthorizons.core.network.event.internal; + +/** + * This event is used to indicate a disconnect. + */ +public class CloseInternalEvent extends AbstractInternalEvent +{ + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/event/internal/IncompatibleMessageInternalEvent.java b/core/src/main/java/com/seibel/distanthorizons/core/network/event/internal/IncompatibleMessageInternalEvent.java new file mode 100644 index 000000000..d40defb06 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/event/internal/IncompatibleMessageInternalEvent.java @@ -0,0 +1,15 @@ +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. + */ +public class IncompatibleMessageInternalEvent extends AbstractInternalEvent +{ + public final int protocolVersion; + + public IncompatibleMessageInternalEvent(int protocolVersion) + { + this.protocolVersion = protocolVersion; + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/event/internal/ProtocolErrorInternalEvent.java b/core/src/main/java/com/seibel/distanthorizons/core/network/event/internal/ProtocolErrorInternalEvent.java new file mode 100644 index 000000000..46ead68f6 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/event/internal/ProtocolErrorInternalEvent.java @@ -0,0 +1,23 @@ +package com.seibel.distanthorizons.core.network.event.internal; + +import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; +import org.jetbrains.annotations.Nullable; + +/** + * This event is used to indicate that encoding or decoding of a message threw an exception. + */ +public class ProtocolErrorInternalEvent extends AbstractInternalEvent +{ + public final Throwable reason; + @Nullable + public final AbstractNetworkMessage message; + public final boolean replyWithCloseReason; + + public ProtocolErrorInternalEvent(Throwable reason, @Nullable AbstractNetworkMessage message, boolean replyWithCloseReason) + { + this.reason = reason; + this.message = message; + this.replyWithCloseReason = replyWithCloseReason; + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/exceptions/InvalidLevelException.java b/core/src/main/java/com/seibel/distanthorizons/core/network/exceptions/InvalidLevelException.java new file mode 100644 index 000000000..9a7d20a80 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/exceptions/InvalidLevelException.java @@ -0,0 +1,26 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.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 InvalidLevelException(String message) { super(message); } +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/IDhWorldGenLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/network/exceptions/RateLimitedException.java similarity index 74% rename from core/src/main/java/com/seibel/distanthorizons/core/level/IDhWorldGenLevel.java rename to core/src/main/java/com/seibel/distanthorizons/core/network/exceptions/RateLimitedException.java index 287d6ad62..dba86e939 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/IDhWorldGenLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/exceptions/RateLimitedException.java @@ -17,12 +17,11 @@ * along with this program. If not, see . */ -package com.seibel.distanthorizons.core.level; +package com.seibel.distanthorizons.core.network.exceptions; -import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider; - -public interface IDhWorldGenLevel extends IDhLevel, GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener +/** Fired if the client attempts to queue more tasks than the server is willing to handle. */ +public class RateLimitedException extends Exception { - void doWorldGen(); + public RateLimitedException(String message) { super(message); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/exceptions/RequestRejectedException.java b/core/src/main/java/com/seibel/distanthorizons/core/network/exceptions/RequestRejectedException.java new file mode 100644 index 000000000..a91dd094a --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/exceptions/RequestRejectedException.java @@ -0,0 +1,11 @@ +package com.seibel.distanthorizons.core.network.exceptions; + +/** + * Fired if the client attempts an operation currently forbidden by the server.
+ * For example attempting to request LODs when world generation is disabled on the server. + */ +public class RequestRejectedException extends Exception +{ + public RequestRejectedException(String message) { super(message); } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/AbstractNetworkMessage.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/AbstractNetworkMessage.java new file mode 100644 index 000000000..eee1b4d45 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/AbstractNetworkMessage.java @@ -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); } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/AbstractTrackableMessage.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/AbstractTrackableMessage.java new file mode 100644 index 000000000..adf99f251 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/AbstractTrackableMessage.java @@ -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 . + */ + +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)
+ * 1 bit - Requesting side (client - 0, server - 1)
+ * 31 bits - Request/Message ID

+ * + * SI = NetworkSession ID
+ * CS = Client/Server flag
+ * MI = Request/Message ID

+ * + * + * =======Bit layout=======
+ * SI SI SI SI SI SI SI SI <-- Top bits
+ * SI SI SI SI SI SI SI SI
+ * SI SI SI SI SI SI SI SI
+ * SI SI SI SI SI SI SI SI
+ * CS MI MI MI MI MI MI MI
+ * MI MI MI MI MI MI MI MI
+ * MI MI MI MI MI MI MI MI
+ * MI MI MI MI MI MI MI MI <-- Bottom bits
+ *
+ */ + 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); + // networkSession 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); + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/ILevelRelatedMessage.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/ILevelRelatedMessage.java new file mode 100644 index 000000000..43d5bd37d --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/ILevelRelatedMessage.java @@ -0,0 +1,23 @@ +package com.seibel.distanthorizons.core.network.messages; + +import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; + +/** Implemented by messages that handle level data */ +public interface ILevelRelatedMessage +{ + String getLevelName(); + + /** Checks whether the message's level matches the given level. */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + default boolean isSameLevelAs(ILevelWrapper levelWrapper) + { + if (levelWrapper instanceof IServerLevelWrapper) + { + return this.getLevelName().equals(((IServerLevelWrapper) levelWrapper).getKeyedLevelDimensionName()); + } + + return this.getLevelName().equals(levelWrapper.getDimensionName()); + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/MessageRegistry.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/MessageRegistry.java new file mode 100644 index 000000000..241f77807 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/MessageRegistry.java @@ -0,0 +1,129 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.network.messages; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +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.SessionConfigMessage; +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.base.CloseReasonMessage; +import com.seibel.distanthorizons.core.network.messages.requests.ExceptionMessage; +import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage; +import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage; +import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceResponseMessage; +import com.seibel.distanthorizons.coreapi.ModInfo; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +/** Keeps track of every {@link AbstractNetworkMessage} so they can be (De)serialized. */ +public class MessageRegistry +{ + public static final boolean DEBUG_CODEC_CRASH_MESSAGE = ModInfo.IS_DEV_BUILD; + public static final MessageRegistry INSTANCE = new MessageRegistry(); + + private final Map> messageConstructorById = new HashMap<>(); + private final BiMap, Integer> messageClassById = HashBiMap.create(); + + + + //=============// + // constructor // + //=============// + + private MessageRegistry() + { + // Note: Messages must have parameterless constructors + + this.registerMessage(CloseReasonMessage.class, CloseReasonMessage::new); + + // Level keys + this.registerMessage(CurrentLevelKeyMessage.class, CurrentLevelKeyMessage::new); + + // Config (for full DH support) + this.registerMessage(SessionConfigMessage.class, SessionConfigMessage::new); + + // Requests + this.registerMessage(CancelMessage.class, CancelMessage::new); + this.registerMessage(ExceptionMessage.class, ExceptionMessage::new); + + // Full data requests & updates + this.registerMessage(FullDataSourceRequestMessage.class, FullDataSourceRequestMessage::new); + this.registerMessage(FullDataSourceResponseMessage.class, FullDataSourceResponseMessage::new); + this.registerMessage(FullDataPartialUpdateMessage.class, FullDataPartialUpdateMessage::new); + this.registerMessage(FullDataSplitMessage.class, FullDataSplitMessage::new); + + // Debug messages are always last, and not included in release builds. + if (DEBUG_CODEC_CRASH_MESSAGE) + { + this.registerMessage(CodecCrashMessage.class, CodecCrashMessage::new); + } + } + + + + //==================// + // message handling // + //==================// + + public void registerMessage(Class clazz, Supplier supplier) + { + int id = this.messageConstructorById.size() + 1; + this.messageConstructorById.put(id, supplier); + this.messageClassById.put(clazz, id); + } + + /** used when decoding messages */ + public AbstractNetworkMessage createMessage(int messageId) throws IllegalArgumentException + { + try + { + return this.messageConstructorById.get(messageId).get(); + } + catch (NullPointerException e) + { + throw new IllegalArgumentException("Invalid message ID: " + messageId); + } + } + + + + //=========// + // getters // + //=========// + + public int getMessageId(AbstractNetworkMessage message) { return this.getMessageId(message.getClass()); } + public int getMessageId(Class messageClass) + { + try + { + return this.messageClassById.get(messageClass); + } + catch (NullPointerException e) + { + throw new IllegalArgumentException("Message does not have ID assigned to it: [" + messageClass.getSimpleName() + "]."); + } + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/CloseReasonMessage.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/CloseReasonMessage.java new file mode 100644 index 000000000..bc5395e6f --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/CloseReasonMessage.java @@ -0,0 +1,67 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.network.messages.base; + +import com.google.common.base.MoreObjects; +import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; +import io.netty.buffer.ByteBuf; + +/** + * 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; + + + + //==============// + // constructors // + //==============// + + public CloseReasonMessage() { } + public CloseReasonMessage(String reason) { this.reason = reason; } + + + + //===============// + // serialization // + //===============// + + @Override + public void encode(ByteBuf out) { this.writeString(this.reason, out); } + @Override + public void decode(ByteBuf in) { this.reason = this.readString(in); } + + + + //================// + // base overrides // + //================// + + @Override + public MoreObjects.ToStringHelper toStringHelper() + { + return super.toStringHelper() + .add("reason", this.reason); + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/CodecCrashMessage.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/CodecCrashMessage.java new file mode 100644 index 000000000..ddc383d21 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/CodecCrashMessage.java @@ -0,0 +1,86 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.network.messages.base; + +import com.google.common.base.MoreObjects; +import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; +import io.netty.buffer.ByteBuf; + +public class CodecCrashMessage extends AbstractNetworkMessage +{ + public ECrashPhase crashPhase; + + + + //==============// + // constructors // + //==============// + + public CodecCrashMessage() { } + public CodecCrashMessage(ECrashPhase crashPhase) { this.crashPhase = crashPhase; } + + + + //===============// + // serialization // + //===============// + + @Override + public void encode(ByteBuf out) + { + if (this.crashPhase == ECrashPhase.ENCODE) + { + throw new RuntimeException("encode force crash"); + } + } + + @Override + public void decode(ByteBuf in) { throw new RuntimeException("decode force crash"); } + + + + //================// + // base overrides // + //================// + + @Override + public MoreObjects.ToStringHelper toStringHelper() + { + return super.toStringHelper() + .add("crashPhase", this.crashPhase); + } + + + + //================// + // helper classes // + //================// + + /** + * ENCODE,
+ * DECODE,
+ */ + public enum ECrashPhase + { + ENCODE, + DECODE + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/CurrentLevelKeyMessage.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/CurrentLevelKeyMessage.java new file mode 100644 index 000000000..50d05fd3c --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/CurrentLevelKeyMessage.java @@ -0,0 +1,45 @@ +package com.seibel.distanthorizons.core.network.messages.base; + +import com.google.common.base.MoreObjects; +import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; +import io.netty.buffer.ByteBuf; + +public class CurrentLevelKeyMessage extends AbstractNetworkMessage +{ + public String levelKey; + + + + //==============// + // constructors // + //==============// + + public CurrentLevelKeyMessage() { } + public CurrentLevelKeyMessage(String levelKey) { this.levelKey = levelKey; } + + + + //===============// + // serialization // + //===============// + + @Override + public void encode(ByteBuf out) { this.writeString(this.levelKey, out); } + + @Override + public void decode(ByteBuf in) { this.levelKey = this.readString(in); } + + + + //================// + // base overrides // + //================// + + @Override + public MoreObjects.ToStringHelper toStringHelper() + { + return super.toStringHelper() + .add("levelKey", this.levelKey); + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/SessionConfigMessage.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/SessionConfigMessage.java new file mode 100644 index 000000000..317e380a0 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/base/SessionConfigMessage.java @@ -0,0 +1,67 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.network.messages.base; + +import com.google.common.base.MoreObjects; +import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig; +import com.seibel.distanthorizons.core.network.INetworkObject; +import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; +import io.netty.buffer.ByteBuf; + +/** used for full DH support */ +public class SessionConfigMessage extends AbstractNetworkMessage +{ + public SessionConfig config; + + + + //=============// + // constructor // + //=============// + + public SessionConfigMessage() { } + public SessionConfigMessage(SessionConfig config) { this.config = config; } + + + + //===============// + // serialization // + //===============// + + @Override + public void encode(ByteBuf out) { this.config.encode(out); } + + @Override + public void decode(ByteBuf in) { this.config = INetworkObject.decodeToInstance(new SessionConfig(), in); } + + + + //================// + // base overrides // + //================// + + @Override + public MoreObjects.ToStringHelper toStringHelper() + { + return super.toStringHelper() + .add("config", this.config); + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataPartialUpdateMessage.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataPartialUpdateMessage.java new file mode 100644 index 000000000..86e6e7eaa --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataPartialUpdateMessage.java @@ -0,0 +1,84 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.network.messages.fullData; + +import com.google.common.base.MoreObjects; +import com.seibel.distanthorizons.core.network.INetworkObject; +import com.seibel.distanthorizons.core.network.messages.ILevelRelatedMessage; +import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; +import io.netty.buffer.ByteBuf; + +public class FullDataPartialUpdateMessage extends AbstractNetworkMessage implements ILevelRelatedMessage +{ + public FullDataPayload payload; + + private String levelName; + @Override + public String getLevelName() { return this.levelName; } + + + + //==============// + // constructors // + //==============// + + public FullDataPartialUpdateMessage() { } + public FullDataPartialUpdateMessage(IServerLevelWrapper level, FullDataPayload payload) + { + this.levelName = level.getKeyedLevelDimensionName(); + this.payload = payload; + } + + + + //===============// + // serialization // + //===============// + + @Override + public void encode(ByteBuf out) + { + this.writeString(this.levelName, out); + this.payload.encode(out); + } + + @Override + public void decode(ByteBuf in) + { + this.levelName = this.readString(in); + this.payload = INetworkObject.decodeToInstance(new FullDataPayload(), in); + } + + + + //================// + // base overrides // + //================// + + @Override + public MoreObjects.ToStringHelper toStringHelper() + { + return super.toStringHelper() + .add("levelName", this.levelName) + .add("payload", this.payload); + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataPayload.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataPayload.java new file mode 100644 index 000000000..1a8a2ad39 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataPayload.java @@ -0,0 +1,127 @@ +package com.seibel.distanthorizons.core.network.messages.fullData; + +import com.google.common.base.MoreObjects; +import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode; +import com.seibel.distanthorizons.core.config.Config; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; +import com.seibel.distanthorizons.core.network.INetworkObject; +import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO; +import com.seibel.distanthorizons.core.util.TimerUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.Objects; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +/** + * @see FullDataSplitMessage + */ +public class FullDataPayload implements INetworkObject +{ + private static final AtomicInteger lastBufferId = new AtomicInteger(); + + // Reference counting is unreliable here for some reason so this is a "fix" + private static final Timer bufferCleanupTimer = TimerUtil.CreateTimer("FullDataBufferCleanupTimer"); + + public int dtoBufferId; + public ByteBuf dtoBuffer; + + + + //==============// + // constructors // + //==============// + + public FullDataPayload() { } + public FullDataPayload(@NotNull FullDataSourceV2 fullDataSource) + { + Objects.requireNonNull(fullDataSource); + + this.dtoBufferId = lastBufferId.getAndIncrement(); + + try + { + EDhApiDataCompressionMode compressionMode = Config.Client.Advanced.LodBuilding.dataCompression.get(); + FullDataSourceV2DTO dataSourceDto = FullDataSourceV2DTO.CreateFromDataSource(fullDataSource, compressionMode); + + this.dtoBuffer = ByteBufAllocator.DEFAULT.buffer(); + dataSourceDto.encode(this.dtoBuffer); + + bufferCleanupTimer.schedule(new TimerTask() + { + @Override + public void run() + { + FullDataPayload.this.dtoBuffer.release(); + } + }, 5000L); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + + + //===============// + // serialization // + //===============// + + @Override + public void encode(ByteBuf out) + { + out.writeInt(this.dtoBufferId); + } + + @Override + public void decode(ByteBuf in) + { + 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 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 + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("dtoBufferId", this.dtoBufferId) + .add("dtoBuffer", this.dtoBuffer) + .toString(); + } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataSourceRequestMessage.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataSourceRequestMessage.java new file mode 100644 index 000000000..c4e9b5cfc --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataSourceRequestMessage.java @@ -0,0 +1,96 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.network.messages.fullData; + +import com.google.common.base.MoreObjects; +import com.seibel.distanthorizons.core.network.messages.ILevelRelatedMessage; +import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; +import io.netty.buffer.ByteBuf; + +import javax.annotation.Nullable; + +public class FullDataSourceRequestMessage extends AbstractTrackableMessage implements ILevelRelatedMessage +{ + public long sectionPos; + + /** Only present when requesting changes. */ + @Nullable + public Long clientTimestamp; + + private String levelName; + @Override + public String getLevelName() { return this.levelName; } + + + + //==============// + // constructors // + //==============// + + public FullDataSourceRequestMessage() {} + public FullDataSourceRequestMessage(ILevelWrapper levelWrapper, long sectionPos, @Nullable Long clientTimestamp) + { + this.levelName = levelWrapper.getDimensionName(); + this.sectionPos = sectionPos; + this.clientTimestamp = clientTimestamp; + } + + + + //===============// + // serialization // + //===============// + + @Override + public void encodeInternal(ByteBuf out) + { + this.writeString(this.levelName, out); + out.writeLong(this.sectionPos); + if (this.tryWrite(out, this.clientTimestamp)) + { + out.writeLong(this.clientTimestamp); + } + } + + @Override + public void decodeInternal(ByteBuf in) + { + this.levelName = this.readString(in); + this.sectionPos = in.readLong(); + this.clientTimestamp = this.tryRead(in, in::readLong); + } + + + + //================// + // base overrides // + //================// + + @Override + public MoreObjects.ToStringHelper toStringHelper() + { + return super.toStringHelper() + .add("levelName", this.levelName) + .add("sectionPos", this.sectionPos) + .add("clientTimestamp", this.clientTimestamp); + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataSourceResponseMessage.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataSourceResponseMessage.java new file mode 100644 index 000000000..102b654a7 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataSourceResponseMessage.java @@ -0,0 +1,83 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.network.messages.fullData; + +import com.google.common.base.MoreObjects; +import com.seibel.distanthorizons.core.network.INetworkObject; +import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage; +import io.netty.buffer.ByteBuf; +import org.jetbrains.annotations.Nullable; + +/** + * Response message, containing the requested full data source, + * or null if requested in updates-only mode and the data was not updated. + */ +public class FullDataSourceResponseMessage extends AbstractTrackableMessage +{ + @Nullable + public FullDataPayload payload; + + + + //=============// + // constructor // + //=============// + + public FullDataSourceResponseMessage() { } + public FullDataSourceResponseMessage(@Nullable FullDataPayload payload) + { + if (payload != null) + { + this.payload = payload; + } + } + + + + //===============// + // serialization // + //===============// + + @Override + public void encodeInternal(ByteBuf out) + { + if (this.tryWrite(out, this.payload)) + { + this.payload.encode(out); + } + } + + @Override + public void decodeInternal(ByteBuf in) { this.payload = this.tryRead(in, () -> INetworkObject.decodeToInstance(new FullDataPayload(), in)); } + + + + //================// + // base overrides // + //================// + + @Override + public MoreObjects.ToStringHelper toStringHelper() + { + return super.toStringHelper() + .add("payload", this.payload); + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataSplitMessage.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataSplitMessage.java new file mode 100644 index 000000000..ae94d7ac1 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/fullData/FullDataSplitMessage.java @@ -0,0 +1,94 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.network.messages.fullData; + +import com.google.common.base.MoreObjects; +import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; +import io.netty.buffer.ByteBuf; + +/** + * Used to send part of a {@link FullDataPayload}. + * + * @see FullDataPayload + */ +public class FullDataSplitMessage extends AbstractNetworkMessage +{ + public int bufferId; + public ByteBuf buffer; + public boolean isFirst; + + + + //==============// + // constructors // + //==============// + + public FullDataSplitMessage() { } + public FullDataSplitMessage(int bufferId, boolean isFirst, ByteBuf buffer) + { + this.bufferId = bufferId; + this.buffer = buffer; + this.isFirst = isFirst; + } + + + + //===============// + // serialization // + //===============// + + @Override + public void encode(ByteBuf out) + { + out.writeInt(this.bufferId); + + out.writeInt(this.buffer.writerIndex()); + out.writeBytes(this.buffer.readerIndex(0)); + + out.writeBoolean(this.isFirst); + } + + @Override + public void decode(ByteBuf in) + { + this.bufferId = in.readInt(); + + int bufferSize = in.readInt(); + this.buffer = in.readBytes(bufferSize); + + this.isFirst = in.readBoolean(); + } + + + + //================// + // base overrides // + //================// + + @Override + public MoreObjects.ToStringHelper toStringHelper() + { + return super.toStringHelper() + .add("bufferId", this.bufferId) + .add("buffer", this.buffer) + .add("isFirst", this.isFirst); + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/requests/CancelMessage.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/requests/CancelMessage.java new file mode 100644 index 000000000..58546ecbc --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/requests/CancelMessage.java @@ -0,0 +1,36 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.network.messages.requests; + +import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage; +import io.netty.buffer.ByteBuf; + +public class CancelMessage extends AbstractTrackableMessage +{ + public CancelMessage() { } + + + + @Override + public void encodeInternal(ByteBuf out) { } + @Override + public void decodeInternal(ByteBuf in) { } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/messages/requests/ExceptionMessage.java b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/requests/ExceptionMessage.java new file mode 100644 index 000000000..c6b4bae46 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/messages/requests/ExceptionMessage.java @@ -0,0 +1,88 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.network.messages.requests; + +import com.google.common.base.MoreObjects; +import com.seibel.distanthorizons.core.network.exceptions.InvalidLevelException; +import com.seibel.distanthorizons.core.network.exceptions.RateLimitedException; +import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException; +import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage; +import io.netty.buffer.ByteBuf; + +import java.util.ArrayList; +import java.util.List; + +// TODO appears to be useless yelling at user +public class ExceptionMessage extends AbstractTrackableMessage +{ + private static final List> EXCEPTION_LIST = new ArrayList>() + {{ + // All exceptions here must include constructor: (String) + this.add(RateLimitedException.class); + this.add(InvalidLevelException.class); + this.add(RequestRejectedException.class); + }}; + + public Exception exception; + + + + //==============// + // constructors // + //==============// + + public ExceptionMessage() { } + public ExceptionMessage(Exception exception) { this.exception = exception; } + + + + //===============// + // serialization // + //===============// + + @Override + protected void encodeInternal(ByteBuf out) + { + out.writeInt(EXCEPTION_LIST.indexOf(this.exception.getClass())); + this.writeString(this.exception.getMessage(), out); + } + + @Override + protected void decodeInternal(ByteBuf in) throws Exception + { + int id = in.readInt(); + String message = this.readString(in); + this.exception = EXCEPTION_LIST.get(id).getDeclaredConstructor(String.class).newInstance(message); + } + + + + //================// + // base overrides // + //================// + + @Override + public MoreObjects.ToStringHelper toStringHelper() + { + return super.toStringHelper() + .add("exception", this.exception); + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/session/NetworkSession.java b/core/src/main/java/com/seibel/distanthorizons/core/network/session/NetworkSession.java new file mode 100644 index 000000000..c1afdc488 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/session/NetworkSession.java @@ -0,0 +1,174 @@ +package com.seibel.distanthorizons.core.network.session; + +import com.seibel.distanthorizons.core.config.Config; +import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; +import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; +import com.seibel.distanthorizons.core.network.event.AbstractNetworkEventSource; +import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent; +import com.seibel.distanthorizons.core.network.event.internal.ProtocolErrorInternalEvent; +import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; +import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage; +import com.seibel.distanthorizons.core.network.messages.base.CloseReasonMessage; +import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IPluginPacketSender; +import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; +import org.apache.logging.log4j.LogManager; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +public class NetworkSession extends AbstractNetworkEventSource +{ + private static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(), + () -> Config.Client.Advanced.Logging.logNetworkEvent.get()); + + private static final IPluginPacketSender PACKET_SENDER = SingletonInjector.INSTANCE.get(IPluginPacketSender.class); + + private static final AtomicInteger lastId = new AtomicInteger(); + public final int id = lastId.getAndIncrement(); + + /** + * When non-null, any received data will be ignored.
+ * This does not include wrong versions, which are ignored without setting this flag, + * to allow multi-compat servers. + */ + private final AtomicReference closeReason = new AtomicReference<>(); + public Throwable getCloseReason() { return this.closeReason.get(); } + public boolean isClosed() { return this.closeReason.get() != null; } + + @Nullable + public final IServerPlayerWrapper serverPlayer; + + + + //=============// + // constructor // + //=============// + + public NetworkSession(@Nullable IServerPlayerWrapper serverPlayer) + { + this.serverPlayer = serverPlayer; + + this.registerHandler(CloseReasonMessage.class, msg -> + { + this.close(new SessionClosedException(msg.reason)); + }); + + this.registerHandler(ProtocolErrorInternalEvent.class, event -> + { + if (event.replyWithCloseReason) + { + this.sendMessage(new CloseReasonMessage("Internal error on other side")); + } + + this.close(event.reason); + }); + } + + + + //==================// + // message handling // + //==================// + + public void tryHandleMessage(AbstractNetworkMessage message) + { + if (this.closeReason.get() != null) + { + return; + } + + message.setSession(this); + + try + { + LOGGER.debug("Received message: ["+message+"]."); + this.handleMessage(message); + } + catch (Throwable e) + { + LOGGER.error("Failed to handle the message. New messages will be ignored.", e); + LOGGER.error("Message: ["+message+"]"); + this.close(); + } + } + + @Override + public void registerHandler(Class handlerClass, Consumer handlerImplementation) + { + if (this.closeReason.get() != null) + { + return; + } + + this.registerHandler(this, handlerClass, handlerImplementation); + } + + + + //==============// + // send message // + //==============// + + public CompletableFuture sendRequest(AbstractTrackableMessage msg, Class responseClass) + { + msg.setSession(this); + CompletableFuture responseFuture = this.createRequest(msg, responseClass); + this.sendMessage(msg); + return responseFuture; + } + + public void sendMessage(AbstractNetworkMessage message) + { + if (this.closeReason.get() != null) + { + return; + } + + LOGGER.debug("Sending message: ["+message+"]"); + message.setSession(this); + + try + { + if (this.serverPlayer != null) + { + PACKET_SENDER.sendPluginServerPacket(this.serverPlayer, message); + } + else + { + PACKET_SENDER.sendPluginClientPacket(message); + } + } + catch (Throwable throwable) + { + LOGGER.info("Failed to send a message", throwable); + LOGGER.info("Message: ["+message+"]"); + this.close(throwable); + } + } + + + + //==========// + // shutdown // + //==========// + + public void close(Throwable closeReason) + { + if (!this.closeReason.compareAndSet(null, closeReason)) + { + return; + } + + try + { + this.handleMessage(new CloseInternalEvent()); + } + catch (Throwable ignored) { } + + super.close(); + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/network/session/SessionClosedException.java b/core/src/main/java/com/seibel/distanthorizons/core/network/session/SessionClosedException.java new file mode 100644 index 000000000..9c8dc9c93 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/session/SessionClosedException.java @@ -0,0 +1,10 @@ +package com.seibel.distanthorizons.core.network.session; + +import java.io.IOException; + +/** The exception thrown if DH's networking networkSession has been shut down. */ +public class SessionClosedException extends IOException +{ + public SessionClosedException(String message) { super(message); } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/sql/dto/FullDataSourceV2DTO.java b/core/src/main/java/com/seibel/distanthorizons/core/sql/dto/FullDataSourceV2DTO.java index 7b3cf1e0c..ebe3fcca8 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/sql/dto/FullDataSourceV2DTO.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/sql/dto/FullDataSourceV2DTO.java @@ -19,18 +19,21 @@ package com.seibel.distanthorizons.core.sql.dto; +import com.google.common.base.MoreObjects; import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode; import com.seibel.distanthorizons.api.enums.config.EDhApiWorldCompressionMode; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.network.INetworkObject; import com.seibel.distanthorizons.core.util.FullDataPointUtil; import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream; import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataOutputStream; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; +import io.netty.buffer.ByteBuf; import it.unimi.dsi.fastutil.longs.LongArrayList; import org.jetbrains.annotations.NotNull; @@ -39,7 +42,7 @@ import java.util.zip.Adler32; import java.util.zip.CheckedOutputStream; /** handles storing {@link FullDataSourceV2}'s in the database. */ -public class FullDataSourceV2DTO implements IBaseDTO +public class FullDataSourceV2DTO implements IBaseDTO, INetworkObject { public static final boolean VALIDATE_INPUT_DATAPOINTS = true; @@ -90,6 +93,10 @@ public class FullDataSourceV2DTO implements IBaseDTO ); } + /** Should only be used for subsequent decoding */ + public static FullDataSourceV2DTO CreateEmptyDataSource() { return new FullDataSourceV2DTO(); } + private FullDataSourceV2DTO() { } + public FullDataSourceV2DTO( long pos, int dataChecksum, byte[] compressedColumnGenStepByteArray, byte[] compressedWorldCompressionModeByteArray, byte dataFormatVersion, byte compressionModeValue, byte[] compressedDataByteArray, @@ -353,6 +360,64 @@ public class FullDataSourceV2DTO implements IBaseDTO + //============// + // networking // + //============// + + @Override + public void encode(ByteBuf out) + { + out.writeLong(this.pos); + out.writeInt(this.dataChecksum); + + out.writeInt(this.compressedDataByteArray.length); + out.writeBytes(this.compressedDataByteArray); + + out.writeInt(this.compressedColumnGenStepByteArray.length); + out.writeBytes(this.compressedColumnGenStepByteArray); + out.writeInt(this.compressedWorldCompressionModeByteArray.length); + out.writeBytes(this.compressedWorldCompressionModeByteArray); + + out.writeInt(this.compressedMappingByteArray.length); + out.writeBytes(this.compressedMappingByteArray); + + out.writeByte(this.dataFormatVersion); + out.writeByte(this.compressionModeValue); + + out.writeBoolean(this.applyToParent); + + out.writeLong(this.lastModifiedUnixDateTime); + out.writeLong(this.createdUnixDateTime); + } + + @Override + public void decode(ByteBuf in) + { + this.pos = in.readLong(); + this.dataChecksum = in.readInt(); + + this.compressedDataByteArray = new byte[in.readInt()]; + in.readBytes(this.compressedDataByteArray); + + this.compressedColumnGenStepByteArray = new byte[in.readInt()]; + in.readBytes(this.compressedColumnGenStepByteArray); + this.compressedWorldCompressionModeByteArray = new byte[in.readInt()]; + in.readBytes(this.compressedWorldCompressionModeByteArray); + + this.compressedMappingByteArray = new byte[in.readInt()]; + in.readBytes(this.compressedMappingByteArray); + + this.dataFormatVersion = in.readByte(); + this.compressionModeValue = in.readByte(); + + this.applyToParent = in.readBoolean(); + + this.lastModifiedUnixDateTime = in.readLong(); + this.createdUnixDateTime = in.readLong(); + } + + + //===========// // overrides // //===========// @@ -362,7 +427,24 @@ public class FullDataSourceV2DTO implements IBaseDTO @Override public String getKeyDisplayString() { return DhSectionPos.toString(this.pos); } - + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("levelMinY", this.levelMinY) + .add("pos", this.pos) + .add("dataChecksum", this.dataChecksum) + .add("compressedDataByteArray length", this.compressedDataByteArray.length) + .add("compressedColumnGenStepByteArray length", this.compressedColumnGenStepByteArray.length) + .add("compressedWorldCompressionModeByteArray length", this.compressedWorldCompressionModeByteArray.length) + .add("compressedMappingByteArray length", this.compressedMappingByteArray.length) + .add("dataFormatVersion", this.dataFormatVersion) + .add("compressionModeValue", this.compressionModeValue) + .add("applyToParent", this.applyToParent) + .add("lastModifiedUnixDateTime", this.lastModifiedUnixDateTime) + .add("createdUnixDateTime", this.createdUnixDateTime) + .toString(); + } //================// // helper methods // diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/ratelimiting/SupplierBasedConcurrencyLimiter.java b/core/src/main/java/com/seibel/distanthorizons/core/util/ratelimiting/SupplierBasedConcurrencyLimiter.java new file mode 100644 index 000000000..2573e0cc6 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/ratelimiting/SupplierBasedConcurrencyLimiter.java @@ -0,0 +1,53 @@ +package com.seibel.distanthorizons.core.util.ratelimiting; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Limits concurrent tasks based on a given limit supplier.
+ * If the limit of concurrent tasks is exceeded, acquisitions will fail and the provided failure handler will be called instead. + * @param Type of the object used as context for the failure handler. + */ +public class SupplierBasedConcurrencyLimiter +{ + private final Supplier maxConcurrentTasksSupplier; + private final Consumer onFailureConsumer; + + private final AtomicInteger pendingTasks = new AtomicInteger(); + + + + //=============// + // constructor // + //=============// + + public SupplierBasedConcurrencyLimiter(Supplier maxConcurrentTasksSupplier, Consumer onFailureConsumer) + { + this.maxConcurrentTasksSupplier = maxConcurrentTasksSupplier; + this.onFailureConsumer = onFailureConsumer; + } + + + + //===============// + // lock handling // + //===============// + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean tryAcquire(TFailObj context) + { + if (this.pendingTasks.incrementAndGet() > this.maxConcurrentTasksSupplier.get()) + { + this.pendingTasks.decrementAndGet(); + this.onFailureConsumer.accept(context); + return false; + } + + return true; + } + + public void release() { this.pendingTasks.decrementAndGet(); } + + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/ratelimiting/SupplierBasedRateAndConcurrencyLimiter.java b/core/src/main/java/com/seibel/distanthorizons/core/util/ratelimiting/SupplierBasedRateAndConcurrencyLimiter.java new file mode 100644 index 000000000..0d18f40e6 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/ratelimiting/SupplierBasedRateAndConcurrencyLimiter.java @@ -0,0 +1,55 @@ +package com.seibel.distanthorizons.core.util.ratelimiting; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Combines both a {@link SupplierBasedRateLimiter} and a {@link SupplierBasedConcurrencyLimiter} for combined limiting. + * + * @param Type of the object used as context for the failure handler. + * @see SupplierBasedRateLimiter + * @see SupplierBasedConcurrencyLimiter + */ +public class SupplierBasedRateAndConcurrencyLimiter +{ + private final SupplierBasedRateLimiter rateLimiter; + private final SupplierBasedConcurrencyLimiter concurrencyLimiter; + + + + //=============// + // constructor // + //=============// + + public SupplierBasedRateAndConcurrencyLimiter(Supplier maxRateSupplier, Consumer onFailureConsumer) + { + this.rateLimiter = new SupplierBasedRateLimiter<>(maxRateSupplier, onFailureConsumer); + this.concurrencyLimiter = new SupplierBasedConcurrencyLimiter<>(maxRateSupplier, onFailureConsumer); + } + + + + //===============// + // lock handling // + //===============// + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean tryAcquire(TFailObj context) + { + if (!this.concurrencyLimiter.tryAcquire(context)) + { + return false; + } + + if (!this.rateLimiter.tryAcquire(context)) + { + this.concurrencyLimiter.release(); + return false; + } + + return true; + } + + public void release() { this.concurrencyLimiter.release(); } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/ratelimiting/SupplierBasedRateLimiter.java b/core/src/main/java/com/seibel/distanthorizons/core/util/ratelimiting/SupplierBasedRateLimiter.java new file mode 100644 index 000000000..cc09ac1b1 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/ratelimiting/SupplierBasedRateLimiter.java @@ -0,0 +1,77 @@ +package com.seibel.distanthorizons.core.util.ratelimiting; + +import com.google.common.annotations.Beta; +import com.google.common.util.concurrent.RateLimiter; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Limits rate of tasks based on a given limit supplier.
+ * If the rate limit is exceeded, acquisitions will fail and the provided failure handler will be called instead. + * @param Type of the object sent to the failure handler. + * + * @apiNote UnstableApiUsage warning suppression is due to Google having marked {@link RateLimiter} as {@link Beta} + * for the past 5 years. Considering how long it's been "unstable" we probably don't have anything to worry about.
+ * https://github.com/google/guava/issues/2797 + */ +@SuppressWarnings("UnstableApiUsage") +public class SupplierBasedRateLimiter +{ + private final Supplier maxRateSupplier; + private final Consumer onFailureConsumer; + + private final RateLimiter rateLimiter = RateLimiter.create(/*permits per second*/Double.POSITIVE_INFINITY); + + + + //=============// + // constructor // + //=============// + + public SupplierBasedRateLimiter(Supplier maxRateSupplier) { this(maxRateSupplier, ignored -> { }); } + public SupplierBasedRateLimiter(Supplier maxRateSupplier, Consumer onFailureConsumer) + { + this.maxRateSupplier = maxRateSupplier; + this.onFailureConsumer = onFailureConsumer; + } + + + + //==================// + // lock acquisition // + //==================// + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean tryAcquire(TFailObject failContext) { return this.tryAcquire(failContext, 1); } + public boolean tryAcquire() { return this.tryAcquire(null, 1); } + public boolean tryAcquire(TFailObject failContext, int permits) + { + this.rateLimiter.setRate(this.maxRateSupplier.get()); + + if (!this.rateLimiter.tryAcquire(permits)) + { + this.onFailureConsumer.accept(failContext); + return false; + } + + return true; + } + + /** can be used to prevent any locks from being acquired for one second */ + public void acquireAll() { int ignored = this.acquireOrDrain(Integer.MAX_VALUE); } + /** @return the number of locks acquired */ + public int acquireOrDrain(int requestedPermits) + { + this.rateLimiter.setRate(this.maxRateSupplier.get()); + + int acquiredCount = 0; + while (requestedPermits > 0 && this.rateLimiter.tryAcquire()) + { + acquiredCount++; + requestedPermits--; + } + return acquiredCount; + } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientServerWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientServerWorld.java index 10cccc3cc..a9d238398 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientServerWorld.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientServerWorld.java @@ -21,7 +21,6 @@ package com.seibel.distanthorizons.core.world; import com.seibel.distanthorizons.core.file.structure.LocalSaveStructure; import com.seibel.distanthorizons.core.level.IDhLevel; -import com.seibel.distanthorizons.core.logging.f3.F3Screen; import com.seibel.distanthorizons.core.level.DhClientServerLevel; import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.objects.EventLoop; @@ -34,7 +33,6 @@ import org.jetbrains.annotations.NotNull; import java.io.File; import java.util.HashMap; import java.util.HashSet; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; public class DhClientServerWorld extends AbstractDhWorld implements IDhClientWorld, IDhServerWorld @@ -150,7 +148,7 @@ public class DhClientServerWorld extends AbstractDhWorld implements IDhClientWor public void serverTick() { this.dhLevels.forEach(DhClientServerLevel::serverTick); } - public void doWorldGen() { this.dhLevels.forEach(DhClientServerLevel::doWorldGen); } + public void worldGenTick() { this.dhLevels.forEach(DhClientServerLevel::worldGenTick); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientWorld.java index 17cb245e1..9d98c6f1f 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientWorld.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientWorld.java @@ -19,13 +19,12 @@ package com.seibel.distanthorizons.core.world; -import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure; -import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.DhClientLevel; +import com.seibel.distanthorizons.core.level.IDhLevel; +import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState; import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.objects.EventLoop; -import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import org.jetbrains.annotations.NotNull; @@ -37,12 +36,9 @@ import java.util.concurrent.ExecutorService; public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld { - private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); - private final ConcurrentHashMap levels; public final ClientOnlySaveStructure saveStructure; - -// private final NetworkClient networkClient; + public final ClientNetworkState networkState = new ClientNetworkState(); public ExecutorService dhTickerThread = ThreadUtil.makeSingleThreadPool("Client World Ticker Thread"); public EventLoop eventLoop = new EventLoop(this.dhTickerThread, this::_clientTick); @@ -60,44 +56,9 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld this.saveStructure = new ClientOnlySaveStructure(); this.levels = new ConcurrentHashMap<>(); - //if (Config.Client.Advanced.Multiplayer.enableServerNetworking.get()) - //{ - // // TODO server specific configs - // this.networkClient = new NetworkClient(MC_CLIENT.getCurrentServerIp(), 25049); - // this.registerNetworkHandlers(); - //} - //else - //{ - // this.networkClient = null; - //} - LOGGER.info("Started DhWorld of type " + this.environment); } - private void registerNetworkHandlers() - { -// this.networkClient.registerHandler(HelloMessage.class, (msg, ctx) -> -// { -// ctx.writeAndFlush(new PlayerUUIDMessage(MC_CLIENT.getPlayerUUID())); -// }); -// -// // TODO Proper payload handling -// this.networkClient.registerAckHandler(PlayerUUIDMessage.class, ctx -> -// { -// ctx.writeAndFlush(new RemotePlayerConfigMessage(new RemotePlayer.Payload())); -// }); -// this.networkClient.registerHandler(RemotePlayerConfigMessage.class, (msg, ctx) -> -// { -// -// }); -// -// this.networkClient.registerAckHandler(RemotePlayerConfigMessage.class, ctx -> -// { -// // TODO Actually request chunks -// ctx.writeAndFlush(new RequestChunksMessage()); -// }); - } - //=========// @@ -121,7 +82,7 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld return null; } - return new DhClientLevel(this.saveStructure, clientLevelWrapper); + return new DhClientLevel(this.saveStructure, clientLevelWrapper, this.networkState); }); } @@ -162,26 +123,25 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld @Override public void clientTick() { this.eventLoop.tick(); } - public void doWorldGen() { - // Not implemented + @Override + public void worldGenTick() { this.levels.values().forEach(DhClientLevel::worldGenTick); } + @Override public void addDebugMenuStringsToList(List messageList) { super.addDebugMenuStringsToList(messageList); + this.networkState.addDebugMenuStringsToList(messageList); } @Override public void close() { -// if (this.networkClient != null) -// { -//// this.networkClient.close(); -// } + this.networkState.close(); for (DhClientLevel dhClientLevel : this.levels.values()) { - LOGGER.info("Unloading level " + dhClientLevel.getLevelWrapper().getDimensionName()); + LOGGER.info("Unloading level [" + dhClientLevel.getLevelWrapper().getDimensionName() + "]."); // level wrapper shouldn't be null, but just in case IClientLevelWrapper clientLevelWrapper = dhClientLevel.getClientLevelWrapper(); @@ -195,7 +155,7 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld this.levels.clear(); this.eventLoop.close(); - LOGGER.info("Closed DhWorld of type " + this.environment); + LOGGER.info("Closed DhWorld of type [" + this.environment + "]."); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java index dc140279c..f7b4bd6d7 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java @@ -19,35 +19,26 @@ package com.seibel.distanthorizons.core.world; -import com.google.common.collect.BiMap; -import com.google.common.collect.HashBiMap; import com.seibel.distanthorizons.core.file.structure.LocalSaveStructure; import com.seibel.distanthorizons.core.level.DhServerLevel; import com.seibel.distanthorizons.core.level.IDhLevel; -import com.seibel.distanthorizons.core.network.NetworkServer; -import com.seibel.distanthorizons.core.network.messages.*; -import com.seibel.distanthorizons.core.network.messages.RequestChunksMessage; -import com.seibel.distanthorizons.core.network.objects.RemotePlayer; +import com.seibel.distanthorizons.core.multiplayer.server.RemotePlayerConnectionHandler; +import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerState; import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; -import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import org.jetbrains.annotations.NotNull; -//import io.netty.channel.ChannelHandlerContext; import java.io.File; import java.util.HashMap; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld { private final HashMap levels; public final LocalSaveStructure saveStructure; - -// private final NetworkServer networkServer; -// private final HashMap playersByUUID; -// private final BiMap playersByConnection; + + public final RemotePlayerConnectionHandler remotePlayerConnectionHandler; @@ -62,82 +53,48 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld this.saveStructure = new LocalSaveStructure(); this.levels = new HashMap<>(); - // TODO move to global payload once server specific configs are implemented -// this.networkServer = new NetworkServer(25049); -// this.playersByUUID = new HashMap<>(); -// this.playersByConnection = HashBiMap.create(); -// this.registerNetworkHandlers(); - - LOGGER.info("Started " + DhServerWorld.class.getSimpleName() + " of type " + this.environment); - } - - private void registerNetworkHandlers() - { -// this.networkServer.registerHandler(CloseMessage.class, (closeMessage, channelContext) -> -// { -// RemotePlayer dhPlayer = this.playersByConnection.remove(channelContext); -// if (dhPlayer != null) -// { -// dhPlayer.channelContext = null; -// } -// }); -// -// this.networkServer.registerHandler(PlayerUUIDMessage.class, (playerUUIDMessage, channelContext) -> -// { -// RemotePlayer dhPlayer = this.playersByUUID.get(playerUUIDMessage.playerUUID); -// -// if (dhPlayer == null) -// { -// this.networkServer.disconnectClient(channelContext, "Player is not logged in."); -// return; -// } -// -// if (dhPlayer.channelContext != null) -// { -// this.networkServer.disconnectClient(channelContext, "Another connection is already in use."); -// return; -// } -// -// dhPlayer.channelContext = channelContext; -// this.playersByConnection.put(channelContext, dhPlayer); -// -// channelContext.writeAndFlush(new AckMessage(PlayerUUIDMessage.class)); -// }); -// -// this.networkServer.registerHandler(RemotePlayerConfigMessage.class, (dhRemotePlayerConfigMessage, channelContext) -> -// { -// // TODO Take notice of received payload and possibly echo back a constrained version -// channelContext.writeAndFlush(new AckMessage(RemotePlayerConfigMessage.class)); -// }); -// -// this.networkServer.registerHandler(RequestChunksMessage.class, (msg, ctx) -> -// { -// LOGGER.info("RequestChunksMessage"); -// // hasReceivedChunkRequest should be false somewhere ??? -// // to avoid sending updates until client says at least something about its state -// }); + this.remotePlayerConnectionHandler = new RemotePlayerConnectionHandler(); + + LOGGER.info("Started ["+DhServerWorld.class.getSimpleName()+"] of type ["+this.environment+"]."); } - //=========// - // methods // - //=========// + //=================// + // player handling // + //=================// public void addPlayer(IServerPlayerWrapper serverPlayer) { - //this.playersByUUID.put(serverPlayer.getUUID(), new RemotePlayer(serverPlayer)); + ServerPlayerState playerState = this.remotePlayerConnectionHandler.registerJoinedPlayer(serverPlayer); + this.getLevel(serverPlayer.getLevel()).addPlayer(serverPlayer); + + for (DhServerLevel level : this.levels.values()) + { + level.registerNetworkHandlers(playerState); + } } + public void removePlayer(IServerPlayerWrapper serverPlayer) { -// RemotePlayer dhPlayer = this.playersByUUID.remove(serverPlayer.getUUID()); -// ChannelHandlerContext channelContext = this.playersByConnection.inverse().remove(dhPlayer); -// if (channelContext != null) -// { -// this.networkServer.disconnectClient(channelContext, "You are being disconnected."); -// } + this.getLevel(serverPlayer.getLevel()).removePlayer(serverPlayer); + this.remotePlayerConnectionHandler.unregisterLeftPlayer(serverPlayer); + + // If player's left, networkSession is already closed } + public void changePlayerLevel(IServerPlayerWrapper player, IServerLevelWrapper originLevel, IServerLevelWrapper destinationLevel) + { + this.getLevel(destinationLevel).addPlayer(player); + this.getLevel(originLevel).removePlayer(player); + } + + + + //================// + // level handling // + //================// + @Override public DhServerLevel getOrLoadLevel(@NotNull ILevelWrapper wrapper) { @@ -150,7 +107,7 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld { File levelFile = this.saveStructure.getLevelFolder(wrapper); LodUtil.assertTrue(levelFile != null); - return new DhServerLevel(this.saveStructure, serverLevelWrapper); + return new DhServerLevel(this.saveStructure, serverLevelWrapper, this.remotePlayerConnectionHandler); }); } @@ -186,11 +143,17 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld } } + + + //==============// + // tick methods // + //==============// + @Override public void serverTick() { this.levels.values().forEach(DhServerLevel::serverTick); } @Override - public void doWorldGen() { this.levels.values().forEach(DhServerLevel::doWorldGen); } + public void worldGenTick() { this.levels.values().forEach(DhServerLevel::worldGenTick); } @@ -203,7 +166,7 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld { for (DhServerLevel level : this.levels.values()) { - LOGGER.info("Unloading level " + level.getLevelWrapper().getDimensionName()); + LOGGER.info("Unloading level [" + level.getLevelWrapper().getDimensionName() + "]."); // level wrapper shouldn't be null, but just in case IServerLevelWrapper serverLevelWrapper = level.getServerLevelWrapper(); @@ -216,7 +179,7 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld } this.levels.clear(); - LOGGER.info("Closed DhWorld of type " + this.environment); + LOGGER.info("Closed DhWorld of type [" + this.environment + "]."); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/IDhClientWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/IDhClientWorld.java index 832ef08e7..43b2952ab 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/world/IDhClientWorld.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/world/IDhClientWorld.java @@ -26,8 +26,6 @@ public interface IDhClientWorld extends IDhWorld { void clientTick(); - void doWorldGen(); - default IDhClientLevel getOrLoadClientLevel(ILevelWrapper levelWrapper) { return (IDhClientLevel) this.getOrLoadLevel(levelWrapper); } default IDhClientLevel getClientLevel(ILevelWrapper levelWrapper) { return (IDhClientLevel) this.getLevel(levelWrapper); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/IDhServerWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/IDhServerWorld.java index 9b74df4ff..17bd2fcbf 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/world/IDhServerWorld.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/world/IDhServerWorld.java @@ -26,7 +26,6 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; public interface IDhServerWorld extends IDhWorld { void serverTick(); - void doWorldGen(); default IDhServerLevel getOrLoadServerLevel(ILevelWrapper levelWrapper) { return (IDhServerLevel) this.getOrLoadLevel(levelWrapper); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/IDhWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/IDhWorld.java index c3bf59165..0821defd0 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/world/IDhWorld.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/world/IDhWorld.java @@ -35,4 +35,6 @@ public interface IDhWorld void unloadLevel(@NotNull ILevelWrapper levelWrapper); + void worldGenTick(); + } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftClientWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftClientWrapper.java index 3869e457f..52dd7e0d8 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftClientWrapper.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftClientWrapper.java @@ -85,6 +85,11 @@ public interface IMinecraftClientWrapper extends IBindable * Returns null if the client isn't in a level. */ IClientLevelWrapper getWrappedClientLevel(); + /** + * Returns the level the client is currently in.
+ * Returns null if the client isn't in a level. + */ + IClientLevelWrapper getWrappedClientLevel(boolean bypassLevelKeyManager); IProfilerWrapper getProfiler(); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftSharedWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftSharedWrapper.java index 7f7594c8b..a4538ea92 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftSharedWrapper.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftSharedWrapper.java @@ -30,4 +30,7 @@ public interface IMinecraftSharedWrapper extends IBindable File getInstallationDirectory(); + /** @return true if this is the first time loading this world */ + boolean isWorldNew(); + } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/misc/IPluginPacketSender.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/misc/IPluginPacketSender.java new file mode 100644 index 000000000..9487483b1 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/misc/IPluginPacketSender.java @@ -0,0 +1,13 @@ +package com.seibel.distanthorizons.core.wrapperInterfaces.misc; + +import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; +import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable; + +public interface IPluginPacketSender extends IBindable +{ + /** Sends a packet from the client */ + void sendPluginClientPacket(AbstractNetworkMessage message); + /** Sends a packet from the server */ + void sendPluginServerPacket(IServerPlayerWrapper serverPlayer, AbstractNetworkMessage message); + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/misc/IServerPlayerWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/misc/IServerPlayerWrapper.java index c56f04216..e67f1b07d 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/misc/IServerPlayerWrapper.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/misc/IServerPlayerWrapper.java @@ -20,11 +20,22 @@ package com.seibel.distanthorizons.core.wrapperInterfaces.misc; import com.seibel.distanthorizons.api.interfaces.IDhApiUnsafeWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; +import com.seibel.distanthorizons.core.util.math.Vec3d; -import java.util.UUID; +import java.net.SocketAddress; public interface IServerPlayerWrapper extends IDhApiUnsafeWrapper { - UUID getUUID(); + String getName(); + + IServerLevelWrapper getLevel(); + + Vec3d getPosition(); + + /** measured in chunks */ + int getViewDistance(); + + SocketAddress getRemoteAddress(); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/IServerLevelWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/IServerLevelWrapper.java index 3b4a00b60..dcc1ed1e5 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/IServerLevelWrapper.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/IServerLevelWrapper.java @@ -19,11 +19,27 @@ package com.seibel.distanthorizons.core.wrapperInterfaces.world; -import org.jetbrains.annotations.Nullable; +import com.seibel.distanthorizons.core.config.Config; import java.io.File; public interface IServerLevelWrapper extends ILevelWrapper { File getSaveFolder(); + default String getKeyedLevelDimensionName() + { + String dimensionName = this.getDimensionName(); + + if (Config.Client.Advanced.Multiplayer.ServerNetworking.sendLevelKeys.get()) + { + String levelKeyPrefix = Config.Client.Advanced.Multiplayer.ServerNetworking.levelKeyPrefix.get(); + if (!levelKeyPrefix.isEmpty()) + { + return levelKeyPrefix + "@" + dimensionName; + } + } + + return dimensionName; + } + } diff --git a/core/src/main/resources/assets/distanthorizons/lang/en_us.json b/core/src/main/resources/assets/distanthorizons/lang/en_us.json index bded71d0a..ebc185c72 100644 --- a/core/src/main/resources/assets/distanthorizons/lang/en_us.json +++ b/core/src/main/resources/assets/distanthorizons/lang/en_us.json @@ -398,6 +398,21 @@ "distanthorizons.config.client.advanced.multiplayer": "Multiplayer", + "distanthorizons.config.client.advanced.multiplayer.serverNetworking": "Server Networking", + "distanthorizons.config.client.advanced.multiplayer.serverNetworking.generalSectionNote": " \u25cf General", + "distanthorizons.config.client.advanced.multiplayer.serverNetworking.enableServerNetworking": "Enable Server Networking", + "distanthorizons.config.client.advanced.multiplayer.serverNetworking.enableServerNetworking.@tooltip": "§6Attention:§r this feature is not fully implemented. \n\nIf true Distant Horizons will attempt to communicate with the connected \nserver in order to load LODs outside your vanilla render distance. \n\nNote: This requires DH to be installed on the server in order to function.", + "distanthorizons.config.client.advanced.multiplayer.serverNetworking.generationSectionNote": " \u25cf Generation", + "distanthorizons.config.client.advanced.multiplayer.serverNetworking.generationRequestRateLimit": "Rate Limit for Generation Requests", + "distanthorizons.config.client.advanced.multiplayer.serverNetworking.generationRequestRateLimit.@tooltip": "How many LOD generation requests per second should a client send? \nAlso limits the amount of player's requests allowed to stay in the server's queue.", + "distanthorizons.config.client.advanced.multiplayer.serverNetworking.realTimeUpdatesSectionNote": " \u25cf Real-time Updates", + "distanthorizons.config.client.advanced.multiplayer.serverNetworking.enableRealTimeUpdates": "Enable Real-time Updates", + "distanthorizons.config.client.advanced.multiplayer.serverNetworking.enableRealTimeUpdates.@tooltip": "If true, the client will receive real-time LOD updates for chunks outside the client's render distance.", + "distanthorizons.config.client.advanced.multiplayer.serverNetworking.syncOnLoginSectionNote": " \u25cf Synchronization on Login", + "distanthorizons.config.client.advanced.multiplayer.serverNetworking.synchronizeOnLogin": "Synchronize LODs on Login", + "distanthorizons.config.client.advanced.multiplayer.serverNetworking.synchronizeOnLogin.@tooltip": "If true, clients will receive updated LODs on join if any changes occurred since last join.", + "distanthorizons.config.client.advanced.multiplayer.serverNetworking.syncOnLoginRateLimit": "Rate Limit for Sync on Login", + "distanthorizons.config.client.advanced.multiplayer.serverNetworking.syncOnLoginRateLimit.@tooltip": "How many LOD sync requests per second should a client send? \nAlso limits the amount of player's requests allowed to stay in the server's queue.", "distanthorizons.config.client.advanced.multiplayer.serverFolderNameMode": "Server Folder Mode", "distanthorizons.config.client.advanced.multiplayer.serverFolderNameMode.@tooltip": @@ -410,13 +425,8 @@ "Networked Multiverse Support", "distanthorizons.config.client.advanced.multiplayer.enableMultiverseNetworking.@tooltip": "If true Distant Horizons will attempt to communicate with the connected \nserver in order to improve multiverse support.", - "distanthorizons.config.client.advanced.multiplayer.enableServerNetworking": - "§4Unimplemented, Dev Use Only§r - Server Support", - "distanthorizons.config.client.advanced.multiplayer.enableServerNetworking.@tooltip": - "§6Attention:§r this is only for developers and hasn't been implemented. \n\nIf true Distant Horizons will attempt to communicate with the connected \nserver in order to load LODs outside your vanilla render distance. \n\nNote: This requires DH to be installed on the server in order to function.", - - - + + "distanthorizons.config.client.advanced.multiThreading": "Threading",