WIP generation distance limiting

This commit is contained in:
s809
2024-11-12 12:25:29 +05:00
parent ea55750c4b
commit eeae8dbdc5
9 changed files with 127 additions and 53 deletions
@@ -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. <br><br>
@@ -1512,23 +1510,9 @@ public class Config
}
public static class Server
public static class Server
{
public static ConfigEntry<Integer> realTimeUpdateDistanceRadiusInChunks = new ConfigEntry.Builder<Integer>()
.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<Boolean> sendLevelKeys = new ConfigEntry.Builder<Boolean>()
.setServersideShortName("sendLevelKeys")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
@@ -1549,6 +1533,8 @@ public class Config
+ "")
.build();
// Generation
public static ConfigEntry<Integer> generationRequestRateLimit = new ConfigEntry.Builder<Integer>()
.setServersideShortName("generationRequestRateLimit")
.setMinDefaultMax(1, 20, 100)
@@ -1558,6 +1544,23 @@ public class Config
+ "")
.build();
public static ConfigEntry<Integer> maxGenerationRequestDistance = new ConfigEntry.Builder<Integer>()
.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<Boolean> enableRealTimeUpdates = new ConfigEntry.Builder<Boolean>()
.setServersideShortName("enableRealTimeUpdates")
.set(true)
@@ -1566,7 +1569,23 @@ public class Config
+ "")
.build();
public static ConfigEntry<Integer> realTimeUpdateDistanceRadiusInChunks = new ConfigEntry.Builder<Integer>()
.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<Boolean> synchronizeOnLoad = new ConfigEntry.Builder<Boolean>()
.setServersideShortName("synchronizeOnLoad")
.set(true)
@@ -1584,6 +1603,23 @@ public class Config
+ "")
.build();
public static ConfigEntry<Integer> maxSyncOnLoadRequestDistance = new ConfigEntry.Builder<Integer>()
.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<Integer> maxDataTransferSpeed = new ConfigEntry.Builder<Integer>()
.setServersideShortName("maxDataTransferSpeed")
.setMinDefaultMax(0, 500, 1000000 /* 1 GB/s */)
@@ -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"; }
@@ -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())
{
@@ -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<Void> 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<Long, RequestQueueEntry> 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<Long, RequestQueueEntry> 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 //
//================//
@@ -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"; }
@@ -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 <T> void registerConfigEntry(ConfigEntry<T> configEntry, BiFunction<T, T, T> 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);
});
}
@@ -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); }
}
@@ -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);
}};
@@ -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. <br>
* Essentially acts like a distance from the block to the nearest edge of the section,
* except inside the section it's negative. <br>
* 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;
}