From eeae8dbdc56837cfe7f7be67f0fbe17f585cdad6 Mon Sep 17 00:00:00 2001 From: s809 <43530948+s809@users.noreply.github.com> Date: Tue, 12 Nov 2024 12:25:29 +0500 Subject: [PATCH] WIP generation distance limiting --- .../distanthorizons/core/config/Config.java | 72 ++++++++++++++----- .../generation/RemoteWorldRetrievalQueue.java | 2 + .../core/level/AbstractDhServerLevel.java | 15 +++- .../AbstractFullDataNetworkRequestQueue.java | 41 ++++++----- .../client/SyncOnLoadRequestQueue.java | 2 + .../multiplayer/config/SessionConfig.java | 27 +++++-- ...n.java => RequestOutOfRangeException.java} | 7 +- .../messages/requests/ExceptionMessage.java | 4 +- .../core/pos/DhSectionPos.java | 10 ++- 9 files changed, 127 insertions(+), 53 deletions(-) rename core/src/main/java/com/seibel/distanthorizons/core/network/exceptions/{InvalidLevelException.java => RequestOutOfRangeException.java} (80%) 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 848b0b856..709b185e3 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 @@ -33,7 +33,6 @@ import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; -import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper; import com.seibel.distanthorizons.coreapi.ModInfo; import org.apache.logging.log4j.Logger; @@ -43,7 +42,6 @@ import java.awt.*; import java.io.File; import java.util.*; import java.util.List; -import java.util.concurrent.ThreadLocalRandom; /** * This handles any configuration the user has access to.

@@ -1512,23 +1510,9 @@ public class Config } - public static class Server + public static class Server { - public static ConfigEntry realTimeUpdateDistanceRadiusInChunks = new ConfigEntry.Builder() - .setServersideShortName("renderDistanceRadius") - .setMinDefaultMax(32, 256, 4096) - .comment("" + - "Defines the distance players will receive real-time updates for if enabled. \n" + - "\n" + - "Note: \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. \n" + - "") - .setPerformance(EConfigEntryPerformance.HIGH) - .build(); - + // Level keys public static ConfigEntry sendLevelKeys = new ConfigEntry.Builder() .setServersideShortName("sendLevelKeys") .setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) @@ -1549,6 +1533,8 @@ public class Config + "") .build(); + + // Generation public static ConfigEntry generationRequestRateLimit = new ConfigEntry.Builder() .setServersideShortName("generationRequestRateLimit") .setMinDefaultMax(1, 20, 100) @@ -1558,6 +1544,23 @@ public class Config + "") .build(); + public static ConfigEntry maxGenerationRequestDistance = new ConfigEntry.Builder() + .setServersideShortName("maxGenerationRequestDistance") + .setMinDefaultMax(32, 1024, 4096) + .comment("" + + "Defines the distance players will receive real-time updates for if enabled. \n" + + "\n" + + "Note: \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. \n" + + "") + .setPerformance(EConfigEntryPerformance.HIGH) + .build(); + + + // Real-time updates public static ConfigEntry enableRealTimeUpdates = new ConfigEntry.Builder() .setServersideShortName("enableRealTimeUpdates") .set(true) @@ -1566,7 +1569,23 @@ public class Config + "") .build(); + public static ConfigEntry realTimeUpdateDistanceRadiusInChunks = new ConfigEntry.Builder() + .setServersideShortName("realTimeUpdateDistanceRadius") + .setMinDefaultMax(32, 256, 4096) + .comment("" + + "Defines the distance players will receive real-time updates for if enabled. \n" + + "\n" + + "Note: \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. \n" + + "") + .setPerformance(EConfigEntryPerformance.HIGH) + .build(); + + // Sync on load public static ConfigEntry synchronizeOnLoad = new ConfigEntry.Builder() .setServersideShortName("synchronizeOnLoad") .set(true) @@ -1584,6 +1603,23 @@ public class Config + "") .build(); + public static ConfigEntry maxSyncOnLoadRequestDistance = new ConfigEntry.Builder() + .setServersideShortName("maxSyncOnLoadRequestDistance") + .setMinDefaultMax(32, 1024, 4096) + .comment("" + + "Defines the distance players will receive real-time updates for if enabled. \n" + + "\n" + + "Note: \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. \n" + + "") + .setPerformance(EConfigEntryPerformance.HIGH) + .build(); + + + // Common public static ConfigEntry maxDataTransferSpeed = new ConfigEntry.Builder() .setServersideShortName("maxDataTransferSpeed") .setMinDefaultMax(0, 500, 1000000 /* 1 GB/s */) 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 index 56b23490f..18a5c4ad9 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/RemoteWorldRetrievalQueue.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/RemoteWorldRetrievalQueue.java @@ -64,6 +64,8 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue @Override protected int getRequestRateLimit() { return this.networkState.sessionConfig.getGenerationRequestRateLimit(); } + @Override + protected int getMaxRequestDistance() { return this.networkState.sessionConfig.getMaxGenerationRequestDistance(); } @Override protected String getQueueName() { return "World Remote Generation Queue"; } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhServerLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhServerLevel.java index 059697189..47cfc5e98 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhServerLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhServerLevel.java @@ -9,7 +9,7 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.f3.F3Screen; import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerState; import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerStateManager; -import com.seibel.distanthorizons.core.network.exceptions.InvalidLevelException; +import com.seibel.distanthorizons.core.network.exceptions.RequestOutOfRangeException; import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage; @@ -194,6 +194,13 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I return; } + Vec3d playerPosition = serverPlayerState.getServerPlayer().getPosition(); + int distanceFromPlayer = DhSectionPos.getChebyshevSignedBlockDistance(message.sectionPos, new DhBlockPos2D((int) playerPosition.x, (int) playerPosition.z)) / 16; + if (distanceFromPlayer > Config.Server.maxGenerationRequestDistance.get()) + { + message.sendResponse(new RequestOutOfRangeException("Distance too large: " + distanceFromPlayer + " > " + Config.Server.maxGenerationRequestDistance.get())); + return; + } ServerPlayerState.RateLimiterSet rateLimiterSet = serverPlayerState.getRateLimiterSet(this); @@ -241,6 +248,8 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I } private void queueLodSyncForRequestMessage(ServerPlayerState serverPlayerState, FullDataSourceRequestMessage message, ServerPlayerState.RateLimiterSet rateLimiterSet) { + + if (!serverPlayerState.sessionConfig.getSynchronizeOnLoad()) { message.sendResponse(new RequestRejectedException("Operation is disabled in config.")); @@ -350,7 +359,7 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I if (message instanceof AbstractTrackableMessage) { ((AbstractTrackableMessage) message).sendResponse( - new InvalidLevelException( + new RequestRejectedException( "Generation not allowed. " + "Requested dimension: ["+((ILevelRelatedMessage) message).getLevelName()+"], " + "player dimension: [" + message.getSession().serverPlayer.getLevel().getDhIdentifier() + "], " + @@ -446,7 +455,7 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I } Vec3d playerPosition = serverPlayerState.getServerPlayer().getPosition(); - int distanceFromPlayer = DhSectionPos.getChebyshevBlockDistance(data.getPos(), new DhBlockPos2D((int) playerPosition.x, (int) playerPosition.z)) / 16; + int distanceFromPlayer = DhSectionPos.getChebyshevSignedBlockDistance(data.getPos(), new DhBlockPos2D((int) playerPosition.x, (int) playerPosition.z)) / 16; if (distanceFromPlayer >= serverPlayerState.getServerPlayer().getViewDistance() && distanceFromPlayer <= serverPlayerState.sessionConfig.getMaxUpdateDistanceRadius()) { 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 index 70779b70d..7812129a9 100644 --- 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 @@ -7,8 +7,8 @@ import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSour import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.level.DhClientLevel; 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.RequestOutOfRangeException; 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; @@ -71,6 +71,8 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende private final SupplierBasedRateLimiter rateLimiter = new SupplierBasedRateLimiter<>(this::getRequestRateLimit); + private DhBlockPos2D lastTargetPos = new DhBlockPos2D(0, 0); + //=============// @@ -95,6 +97,7 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende //==================// protected abstract int getRequestRateLimit(); + protected abstract int getMaxRequestDistance(); protected abstract String getQueueName(); @@ -138,6 +141,8 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende return false; } + this.lastTargetPos = targetPos; + // queue requests until the queue is full while (this.getInProgressTaskCount() < this.getWaitingTaskCount() && this.getInProgressTaskCount() < this.getRequestRateLimit() @@ -158,7 +163,8 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende { Map.Entry mapEntry = this.waitingTasksBySectionPos.entrySet().stream() .filter(task -> task.getValue().networkDataSourceFuture == null) - .min(Comparator.comparingInt(x -> posDistanceSquared(targetPos, x.getKey()))) + .filter(task -> DhSectionPos.getChebyshevSignedBlockDistance(task.getKey(), targetPos) <= this.getMaxRequestDistance()) + .min(Comparator.comparingInt(task -> DhSectionPos.getChebyshevSignedBlockDistance(task.getKey(), targetPos))) .orElse(null); if (mapEntry == null) @@ -220,16 +226,15 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende 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 (RequestRejectedException e) + { + LOGGER.info("Request rejected by the server: " + e.getMessage()); + return entry.future.complete(false); + } catch (RateLimitedException e) { LOGGER.warn("Rate limited by server, re-queueing task [" + DhSectionPos.toString(sectionPos) + "]: " + e.getMessage()); @@ -240,6 +245,13 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende entry.networkDataSourceFuture = null; return null; } + catch (RequestOutOfRangeException e) + { + LOGGER.warn("Out of range, re-queueing task [" + DhSectionPos.toString(sectionPos) + "]: " + e.getMessage()); + + entry.networkDataSourceFuture = null; + return null; + } catch (Throwable e) { entry.retryAttempts--; @@ -352,22 +364,15 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende 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 + mapEntry.getValue().networkDataSourceFuture != null ? Color.red + : DhSectionPos.getChebyshevSignedBlockDistance(mapEntry.getKey(), this.lastTargetPos) <= this.getMaxRequestDistance() ? Color.gray + : Color.lightGray )); } } - //================// - // helper methods // - //================// - - protected static int posDistanceSquared(DhBlockPos2D targetPos, long pos) - { return (int) DhSectionPos.getCenterBlockPos(pos).distSquared(targetPos); } - - - //================// // helper classes // //================// diff --git a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/SyncOnLoadRequestQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/SyncOnLoadRequestQueue.java index 5ae421c75..522f04d54 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/SyncOnLoadRequestQueue.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/SyncOnLoadRequestQueue.java @@ -31,6 +31,8 @@ public class SyncOnLoadRequestQueue extends AbstractFullDataNetworkRequestQueue @Override protected int getRequestRateLimit() { return this.networkState.sessionConfig.getSyncOnLoginRateLimit(); } + @Override + protected int getMaxRequestDistance() { return this.networkState.sessionConfig.getMaxSyncOnLoadDistance(); } @Override protected String getQueueName() { return "Sync On Login Queue"; } 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 index 790bffccc..6ace4c042 100644 --- 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 @@ -31,14 +31,15 @@ public class SessionConfig implements INetworkObject { // Note: config values are transmitted in the insertion order - registerConfigEntry(Config.Server.realTimeUpdateDistanceRadiusInChunks, Math::min); - - registerConfigEntry(Config.Common.WorldGenerator.enableDistantGeneration, (x, y) -> x && y); + registerConfigEntry(Config.Common.WorldGenerator.enableDistantGeneration, Boolean::logicalAnd); + registerConfigEntry(Config.Server.maxGenerationRequestDistance, Math::min); registerConfigEntry(Config.Server.generationRequestRateLimit, Math::min); - registerConfigEntry(Config.Server.enableRealTimeUpdates, (x, y) -> x && y); + registerConfigEntry(Config.Server.enableRealTimeUpdates, Boolean::logicalAnd); + registerConfigEntry(Config.Server.realTimeUpdateDistanceRadiusInChunks, Math::min); - registerConfigEntry(Config.Server.synchronizeOnLoad, (x, y) -> x && y); + registerConfigEntry(Config.Server.synchronizeOnLoad, Boolean::logicalAnd); + registerConfigEntry(Config.Server.maxSyncOnLoadRequestDistance, Math::min); registerConfigEntry(Config.Server.syncOnLoadRateLimit, Math::min); registerConfigEntry(Config.Server.maxDataTransferSpeed, (x, y) -> { @@ -62,12 +63,17 @@ public class SessionConfig implements INetworkObject // public values // //===============// - public int getMaxUpdateDistanceRadius() { return this.getValue(Config.Server.realTimeUpdateDistanceRadiusInChunks); } public boolean isDistantGenerationEnabled() { return this.getValue(Config.Common.WorldGenerator.enableDistantGeneration); } + public int getMaxGenerationRequestDistance() { return this.getValue(Config.Server.maxGenerationRequestDistance); } public int getGenerationRequestRateLimit() { return this.getValue(Config.Server.generationRequestRateLimit); } + public boolean isRealTimeUpdatesEnabled() { return this.getValue(Config.Server.enableRealTimeUpdates); } + public int getMaxUpdateDistanceRadius() { return this.getValue(Config.Server.realTimeUpdateDistanceRadiusInChunks); } + public boolean getSynchronizeOnLoad() { return this.getValue(Config.Server.synchronizeOnLoad); } + public int getMaxSyncOnLoadDistance() { return this.getValue(Config.Server.maxSyncOnLoadRequestDistance); } public int getSyncOnLoginRateLimit() { return this.getValue(Config.Server.syncOnLoadRateLimit); } + public int getMaxDataTransferSpeed() { return this.getValue(Config.Server.maxDataTransferSpeed); } @@ -78,7 +84,14 @@ public class SessionConfig implements INetworkObject private static void registerConfigEntry(ConfigEntry configEntry, BiFunction valueConstrainer) { - CONFIG_ENTRIES.put(Objects.requireNonNull(configEntry.getServersideShortName()), new Entry(configEntry, valueConstrainer)); + CONFIG_ENTRIES.compute(Objects.requireNonNull(configEntry.getServersideShortName()), (key, existingEntry) -> { + if (existingEntry != null) + { + throw new IllegalArgumentException("Attempted to register config entry with duplicate serversideShortName: " + key); + } + + return new Entry(configEntry, valueConstrainer); + }); } 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/RequestOutOfRangeException.java similarity index 80% rename from core/src/main/java/com/seibel/distanthorizons/core/network/exceptions/InvalidLevelException.java rename to core/src/main/java/com/seibel/distanthorizons/core/network/exceptions/RequestOutOfRangeException.java index 9a7d20a80..fdd32e611 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/network/exceptions/InvalidLevelException.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/network/exceptions/RequestOutOfRangeException.java @@ -19,8 +19,9 @@ 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 +/** Fired if the client attempts to request an LOD out of allowed range. */ +public class RequestOutOfRangeException extends Exception { - public InvalidLevelException(String message) { super(message); } + public RequestOutOfRangeException(String message) { super(message); } + } 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 index c6b4bae46..c0679fc40 100644 --- 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 @@ -20,8 +20,8 @@ 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.RequestOutOfRangeException; import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException; import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage; import io.netty.buffer.ByteBuf; @@ -36,7 +36,7 @@ public class ExceptionMessage extends AbstractTrackableMessage {{ // All exceptions here must include constructor: (String) this.add(RateLimitedException.class); - this.add(InvalidLevelException.class); + this.add(RequestOutOfRangeException.class); this.add(RequestRejectedException.class); }}; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/pos/DhSectionPos.java b/core/src/main/java/com/seibel/distanthorizons/core/pos/DhSectionPos.java index 3fb1252af..64bf74423 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/pos/DhSectionPos.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/pos/DhSectionPos.java @@ -261,12 +261,18 @@ public class DhSectionPos + Math.abs(getCenterBlockPosZ(pos) - blockPos.z); } - public static int getChebyshevBlockDistance(long pos, DhBlockPos2D blockPos) + /** + * Returns the signed distance from a given block to a given section.
+ * Essentially acts like a distance from the block to the nearest edge of the section, + * except inside the section it's negative.
+ * Useful for detail level insensitive distance comparisons. + */ + public static int getChebyshevSignedBlockDistance(long pos, DhBlockPos2D blockPos) { return Math.max( Math.abs(getCenterBlockPosX(pos) - blockPos.x), Math.abs(getCenterBlockPosZ(pos) - blockPos.z) - ); + ) - getBlockWidth(pos) / 2; }