Compare commits

..

7 Commits

Author SHA1 Message Date
s809 ad995544f7 Use bytesReceived instead of decreasing multiplicatively 2025-04-20 23:59:34 +05:00
s809 d521e931f4 Change data send tick rate 4 -> 20 2025-04-20 18:26:07 +05:00
s809 dd30a8274a Add a config entry and refactor 2025-04-20 18:25:27 +05:00
s809 3ca5efadc9 Adaptive data transfer speed 2025-04-20 03:02:18 +05:00
Ran 09174c2d2a Improve LodDataBuilder.java
- Use bitwise modulo
- Don't compute certain things 256 times when they can be computed once.
- Removed expressions that are always false
- Improved comments
2025-04-11 11:24:16 +10:00
James Seibel e079b28e77 maybe break n-sized rendering but fix LOD loading getting stuck 2025-04-07 06:56:53 -05:00
James Seibel 136124a703 up version number 2.3.2 -> 2.3.3 2025-04-05 09:11:19 -05:00
10 changed files with 236 additions and 131 deletions
@@ -38,7 +38,7 @@ public final class ModInfo
public static final String NAME = "DistantHorizons"; public static final String NAME = "DistantHorizons";
/** Human-readable version of NAME */ /** Human-readable version of NAME */
public static final String READABLE_NAME = "Distant Horizons"; public static final String READABLE_NAME = "Distant Horizons";
public static final String VERSION = "2.3.2-b"; public static final String VERSION = "2.3.3-b-dev";
/** Returns true if the current build is an unstable developer build, false otherwise. */ /** Returns true if the current build is an unstable developer build, false otherwise. */
public static final boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev"); public static final boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev");
@@ -1713,6 +1713,15 @@ public class Config
+ "Value of 0 disables the limit." + "Value of 0 disables the limit."
+ "") + "")
.build(); .build();
public static ConfigEntry<Boolean> enableAdaptiveTransferSpeed = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "Enables adaptive transfer speed based on client performance.\n"
+ "If true, DH will automatically adjust transfer rate to minimize connection lag.\n"
+ "If false, transfer speed will remain fixed.\n"
+ "")
.build();
public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build(); public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build();
@@ -65,8 +65,6 @@ public class LodDataBuilder
// only block lighting is needed here, sky lighting is populated at the data source stage // only block lighting is needed here, sky lighting is populated at the data source stage
LodUtil.assertTrue(chunkWrapper.isDhBlockLightingCorrect()); LodUtil.assertTrue(chunkWrapper.isDhBlockLightingCorrect());
int sectionPosX = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getX()); int sectionPosX = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getX());
int sectionPosZ = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getZ()); int sectionPosZ = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getZ());
long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ); long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ);
@@ -80,47 +78,31 @@ public class LodDataBuilder
// compute the chunk dataSource offset // compute the chunk dataSource offset
// this offset is used to determine where in the dataSource this chunk's data should go // this offset is used to determine where in the dataSource this chunk's data should go
int chunkOffsetX = chunkWrapper.getChunkPos().getX();
if (chunkWrapper.getChunkPos().getX() < 0)
{
// expected offset positions:
// chunkPos -> offset
// 5 -> 1
// 4 -> 0 ---
// 3 -> 3
// 2 -> 2
// 1 -> 1
// 0 -> 0 ===
// -1 -> 3
// -2 -> 2
// -3 -> 1
// -4 -> 0 ---
// -5 -> 3
chunkOffsetX = ((chunkOffsetX) % FullDataSourceV2.NUMB_OF_CHUNKS_WIDE);
if (chunkOffsetX != 0)
{
chunkOffsetX += FullDataSourceV2.NUMB_OF_CHUNKS_WIDE;
}
}
else
{
chunkOffsetX %= FullDataSourceV2.NUMB_OF_CHUNKS_WIDE;
}
chunkOffsetX *= LodUtil.CHUNK_WIDTH;
int chunkOffsetZ = chunkWrapper.getChunkPos().getZ(); // expected offset positions:
if (chunkWrapper.getChunkPos().getZ() < 0) // chunkPos -> offset
{ // 5 -> 1
chunkOffsetZ = ((chunkOffsetZ) % FullDataSourceV2.NUMB_OF_CHUNKS_WIDE); // 4 -> 0 ---
if (chunkOffsetZ != 0) // 3 -> 3
{ // 2 -> 2
chunkOffsetZ += FullDataSourceV2.NUMB_OF_CHUNKS_WIDE; // 1 -> 1
} // 0 -> 0 ===
} // -1 -> 3
else // -2 -> 2
{ // -3 -> 1
chunkOffsetZ %= FullDataSourceV2.NUMB_OF_CHUNKS_WIDE; // -4 -> 0 ---
} // -5 -> 3
// Fast modulo calculation using bitwise AND since NUMB_OF_CHUNKS_WIDE is a power of 2 (4)
// For any number n: n & (2^k - 1) is equivalent to Math.floorMod(n, 2^k)
// Original: Math.floorMod(x, 4) - Handles negative numbers, gives non-negative result in range [0,3]
// Bitwise: x & (4-1) - Also gives non-negative result in range [0,3]
// Example: -5 & 3 = 3, which equals Math.floorMod(-5, 4) = 3
int chunkOffsetX = chunkWrapper.getChunkPos().getX() & (FullDataSourceV2.NUMB_OF_CHUNKS_WIDE - 1);
int chunkOffsetZ = chunkWrapper.getChunkPos().getZ() & (FullDataSourceV2.NUMB_OF_CHUNKS_WIDE - 1);
// Convert from chunk coordinates to block coordinates
chunkOffsetX *= LodUtil.CHUNK_WIDTH;
chunkOffsetZ *= LodUtil.CHUNK_WIDTH; chunkOffsetZ *= LodUtil.CHUNK_WIDTH;
@@ -138,54 +120,49 @@ public class LodDataBuilder
IBlockStateWrapper previousBlockState = null; IBlockStateWrapper previousBlockState = null;
int minBuildHeight = chunkWrapper.getMinNonEmptyHeight(); int minBuildHeight = chunkWrapper.getMinNonEmptyHeight();
int exclusiveMaxBuildHeight = chunkWrapper.getExclusiveMaxBuildHeight();
int inclusiveMinBuildHeight = chunkWrapper.getInclusiveMinBuildHeight();
int dataCapacity = chunkWrapper.getHeight() / 4;
for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++) for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++)
{ {
for (int relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++) for (int relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++)
{ {
LongArrayList longs = dataSource.get( // Calculate column position
relBlockX + chunkOffsetX, int columnX = relBlockX + chunkOffsetX;
relBlockZ + chunkOffsetZ); int columnZ = relBlockZ + chunkOffsetZ;
// Get column data
LongArrayList longs = dataSource.get(columnX, columnZ);
if (longs == null) if (longs == null)
{ {
longs = new LongArrayList(chunkWrapper.getHeight() / 4); longs = new LongArrayList(dataCapacity);
} }
else else
{ {
longs.clear(); longs.clear();
} }
int lastY = chunkWrapper.getExclusiveMaxBuildHeight(); int lastY = exclusiveMaxBuildHeight;
IBiomeWrapper biome = chunkWrapper.getBiome(relBlockX, lastY, relBlockZ); IBiomeWrapper biome = chunkWrapper.getBiome(relBlockX, lastY, relBlockZ);
IBlockStateWrapper blockState = AIR; IBlockStateWrapper blockState = AIR;
int mappedId = dataSource.mapping.addIfNotPresentAndGetId(biome, blockState); int mappedId = dataSource.mapping.addIfNotPresentAndGetId(biome, blockState);
// Determine lighting (we are at the height limit. There are no torches here, and sky is not obscured.) // TODO: Per face lighting someday?
byte blockLight = LodUtil.MIN_MC_LIGHT;
byte skyLight = LodUtil.MAX_MC_LIGHT;
byte blockLight; // Get the maximum height from both heightmaps
byte skyLight;
if (lastY < chunkWrapper.getExclusiveMaxBuildHeight())
{
// FIXME: The lastY +1 offset is to reproduce the old behavior. Remove this when we get per-face lighting
blockLight = (byte) chunkWrapper.getDhBlockLight(relBlockX, lastY + 1, relBlockZ);
skyLight = (byte) chunkWrapper.getDhSkyLight(relBlockX, lastY + 1, relBlockZ);
}
else
{
//we are at the height limit. There are no torches here, and sky is not obscured.
blockLight = LodUtil.MIN_MC_LIGHT;
skyLight = LodUtil.MAX_MC_LIGHT;
}
// determine the starting Y Pos
int y = Math.max( int y = Math.max(
// max between both heightmaps to account for solid invisible blocks (glass) // max between both heightmaps to account for solid invisible blocks (glass)
// and non-solid opaque blocks (at one point this was stairs, not sure what would fit this now) // and non-solid opaque blocks (at one point this was stairs, not sure what would fit this now)
chunkWrapper.getLightBlockingHeightMapValue(relBlockX, relBlockZ), chunkWrapper.getLightBlockingHeightMapValue(relBlockX, relBlockZ),
chunkWrapper.getSolidHeightMapValue(relBlockX, relBlockZ) chunkWrapper.getSolidHeightMapValue(relBlockX, relBlockZ)
); );
// go up until we reach open air or the world limit
// Go up until we reach open air or the world limit
IBlockStateWrapper topBlockState = previousBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ, mcBlockPos, previousBlockState); IBlockStateWrapper topBlockState = previousBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ, mcBlockPos, previousBlockState);
while (!topBlockState.isAir() && y < chunkWrapper.getExclusiveMaxBuildHeight()) while (!topBlockState.isAir() && y < exclusiveMaxBuildHeight)
{ {
try try
{ {
@@ -198,7 +175,7 @@ public class LodDataBuilder
{ {
if (!getTopErrorLogged) if (!getTopErrorLogged)
{ {
LOGGER.warn("Unexpected issue in LodDataBuilder, future errors won't be logged. Chunk [" + chunkWrapper.getChunkPos() + "] with max height: [" + chunkWrapper.getExclusiveMaxBuildHeight() + "] had issue getting block at pos [" + relBlockX + "," + y + "," + relBlockZ + "] error: " + e.getMessage(), e); LOGGER.warn("Unexpected issue in LodDataBuilder, future errors won't be logged. Chunk [" + chunkWrapper.getChunkPos() + "] with max height: [" + exclusiveMaxBuildHeight + "] had issue getting block at pos [" + relBlockX + "," + y + "," + relBlockZ + "] error: " + e.getMessage(), e);
getTopErrorLogged = true; getTopErrorLogged = true;
} }
@@ -207,7 +184,7 @@ public class LodDataBuilder
} }
} }
// Process blocks from top to bottom
for (; y >= minBuildHeight; y--) for (; y >= minBuildHeight; y--)
{ {
IBiomeWrapper newBiome = chunkWrapper.getBiome(relBlockX, y, relBlockZ); IBiomeWrapper newBiome = chunkWrapper.getBiome(relBlockX, y, relBlockZ);
@@ -215,10 +192,10 @@ public class LodDataBuilder
byte newBlockLight = (byte) chunkWrapper.getDhBlockLight(relBlockX, y + 1, relBlockZ); byte newBlockLight = (byte) chunkWrapper.getDhBlockLight(relBlockX, y + 1, relBlockZ);
byte newSkyLight = (byte) chunkWrapper.getDhSkyLight(relBlockX, y + 1, relBlockZ); byte newSkyLight = (byte) chunkWrapper.getDhSkyLight(relBlockX, y + 1, relBlockZ);
// save the biome/block change // Save the biome/block change if different from previous
if (!newBiome.equals(biome) || !newBlockState.equals(blockState)) if (!newBiome.equals(biome) || !newBlockState.equals(blockState))
{ {
longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - chunkWrapper.getInclusiveMinBuildHeight(), blockLight, skyLight)); longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - inclusiveMinBuildHeight, blockLight, skyLight));
biome = newBiome; biome = newBiome;
blockState = newBlockState; blockState = newBlockState;
@@ -228,13 +205,12 @@ public class LodDataBuilder
lastY = y; lastY = y;
} }
} }
longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - chunkWrapper.getInclusiveMinBuildHeight(), blockLight, skyLight));
dataSource.setSingleColumn(longs, // Add the final data point
relBlockX + chunkOffsetX, longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - inclusiveMinBuildHeight, blockLight, skyLight));
relBlockZ + chunkOffsetZ,
EDhApiWorldGenerationStep.LIGHT, // Set the column in the data source
worldCompressionMode); dataSource.setSingleColumn(longs, columnX, columnZ, EDhApiWorldGenerationStep.LIGHT, worldCompressionMode);
} }
} }
@@ -275,7 +251,7 @@ public class LodDataBuilder
{ {
long currentPoint = centerColumn.getLong(centerIndex); long currentPoint = centerColumn.getLong(centerIndex);
// translucent data points are not eligible to be culled. // Translucent data points are not eligible to be culled.
if (isTranslucent(dataSource, currentPoint)) if (isTranslucent(dataSource, currentPoint))
{ {
continue; continue;
@@ -283,8 +259,8 @@ public class LodDataBuilder
// the top segment should never be culled. // the top segment should never be culled.
if (centerIndex == 0 if (centerIndex == 0
|| isTranslucent(dataSource, centerColumn.getLong(centerIndex - 1)) || isTranslucent(dataSource, centerColumn.getLong(centerIndex - 1))
) )
{ {
continue; continue;
} }
@@ -293,8 +269,8 @@ public class LodDataBuilder
// assume it will not be seen from below, // assume it will not be seen from below,
// because this would imply the player is in the void. // because this would imply the player is in the void.
if (centerIndex + 1 < centerColumn.size() if (centerIndex + 1 < centerColumn.size()
&& isTranslucent(dataSource, centerColumn.getLong(centerIndex + 1)) && isTranslucent(dataSource, centerColumn.getLong(centerIndex + 1))
) )
{ {
continue; continue;
} }
@@ -327,9 +303,11 @@ public class LodDataBuilder
continue; continue;
} }
// current point is fully surrounded. remove it. // Current point is fully surrounded. remove it.
centerColumn.removeLong(centerIndex); centerColumn.removeLong(centerIndex);
// make the above data point cover the area that the current point used to occupy.
// Make the above data point cover the area that the current point used to occupy.
// The element that was at `centerIndex - 1` is still at that position even after removal of centerIndex.
long above = centerColumn.getLong(centerIndex - 1); long above = centerColumn.getLong(centerIndex - 1);
above = FullDataPointUtil.setBottomY(above, FullDataPointUtil.getBottomY(currentPoint)); above = FullDataPointUtil.setBottomY(above, FullDataPointUtil.getBottomY(currentPoint));
above = FullDataPointUtil.setHeight(above, FullDataPointUtil.getHeight(currentPoint) + FullDataPointUtil.getHeight(above)); above = FullDataPointUtil.setHeight(above, FullDataPointUtil.getHeight(currentPoint) + FullDataPointUtil.getHeight(above));
@@ -340,29 +318,29 @@ public class LodDataBuilder
} }
/** /**
checks if centerPoint is "covered" by opaque data points in adjacentColumn. checks if centerPoint is "covered" by opaque data points in adjacentColumn.
centerPoint counts as covered if, and only if, for all Y levels in its height range, centerPoint counts as covered if, and only if, for all Y levels in its height range,
there exists an opaque data point in adjacentColumn which overlaps with that Y level. there exists an opaque data point in adjacentColumn which overlaps with that Y level.
@param source used to lookup blocks (and their opacities) based on their IDs. @param source used to lookup blocks (and their opacities) based on their IDs.
@param centerPoint the point being checked to see if it's fully covered. @param centerPoint the point being checked to see if it's fully covered.
@param adjacentColumn the data points which might cover centerPoint. @param adjacentColumn the data points which might cover centerPoint.
@param adjacentIndex the starting index in adjacentColumn to start scanning at. @param adjacentIndex the starting index in adjacentColumn to start scanning at.
indices greater than adjacentIndex have already been checked and confirmed to indices greater than adjacentIndex have already been checked and confirmed to
not overlap or only overlap partially with centerPoint's Y range. not overlap or only overlap partially with centerPoint's Y range.
@return if centerPoint is covered, returns the index of the segment which finishes covering it. @return if centerPoint is covered, returns the index of the segment which finishes covering it.
the start of the covering may be a smaller index. in this case, the returned index may be used the start of the covering may be a smaller index. in this case, the returned index may be used
as the adjacentIndex provided to this method on the next iteration which yields a new centerPoint. as the adjacentIndex provided to this method on the next iteration which yields a new centerPoint.
if centerPoint is NOT covered, returns the bitwise negation of the index of the if centerPoint is NOT covered, returns the bitwise negation of the index of the
segment which did not cover it. this guarantees that the returned value is negative. segment which did not cover it. this guarantees that the returned value is negative.
the caller should check for negative return values and manually un-negate them to proceed with the loop. the caller should check for negative return values and manually un-negate them to proceed with the loop.
in other words, this function returns the index of the next adjacent data in other words, this function returns the index of the next adjacent data
point to use in the loop, AND a boolean indicating whether or not the point to use in the loop, AND a boolean indicating whether or not the
centerPoint is covered; both are packed into the same int, and returned. centerPoint is covered; both are packed into the same int, and returned.
*/ */
private static int checkOcclusion(FullDataSourceV2 source, long centerPoint, LongArrayList adjacentColumn, int adjacentIndex) private static int checkOcclusion(FullDataSourceV2 source, long centerPoint, LongArrayList adjacentColumn, int adjacentIndex)
{ {
int bottomOfCenter = FullDataPointUtil.getBottomY(centerPoint); int bottomOfCenter = FullDataPointUtil.getBottomY(centerPoint);
@@ -402,9 +380,13 @@ public class LodDataBuilder
int sectionPosZ = getXOrZSectionPosFromChunkPos(apiChunk.chunkPosZ); int sectionPosZ = getXOrZSectionPosFromChunkPos(apiChunk.chunkPosZ);
long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ); long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ);
// chunk relative block position in the data source // Fast modulo calculation using bitwise AND since NUMB_OF_CHUNKS_WIDE is a power of 2 (4)
int relSourceBlockX = Math.floorMod(apiChunk.chunkPosX, 4) * LodUtil.CHUNK_WIDTH; // For any number n: n & (2^k - 1) is equivalent to Math.floorMod(n, 2^k)
int relSourceBlockZ = Math.floorMod(apiChunk.chunkPosZ, 4) * LodUtil.CHUNK_WIDTH; // Original: Math.floorMod(x, 4) - Handles negative numbers, gives non-negative result in range [0,3]
// Bitwise: x & (4-1) - Also gives non-negative result in range [0,3]
// Example: -5 & 3 = 3, which equals Math.floorMod(-5, 4) = 3
int relSourceBlockX = (apiChunk.chunkPosX & (FullDataSourceV2.NUMB_OF_CHUNKS_WIDE - 1)) * LodUtil.CHUNK_WIDTH;
int relSourceBlockZ = (apiChunk.chunkPosZ & (FullDataSourceV2.NUMB_OF_CHUNKS_WIDE - 1)) * LodUtil.CHUNK_WIDTH;
FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos); FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos);
for (int relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++) for (int relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++)
@@ -533,7 +515,7 @@ public class LodDataBuilder
} }
// is there a gap between the last datapoint? // is there a gap between the last datapoint?
if (topYPos != lastBottomYPos if (topYPos != lastBottomYPos
&& lastBottomYPos != Integer.MIN_VALUE) && lastBottomYPos != Integer.MIN_VALUE)
{ {
throw new IllegalArgumentException("DhApiTerrainDataPoint ["+i+"] has a gap between it and index ["+(i-1)+"]. Empty spaces should be filled by air, otherwise DH's downsampling won't calculate lighting correctly."); throw new IllegalArgumentException("DhApiTerrainDataPoint ["+i+"] has a gap between it and index ["+(i-1)+"]. Empty spaces should be filled by air, otherwise DH's downsampling won't calculate lighting correctly.");
} }
@@ -0,0 +1,71 @@
package com.seibel.distanthorizons.core.multiplayer.client;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMessage;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BooleanSupplier;
import java.util.function.IntConsumer;
import java.util.function.IntSupplier;
public class ClientCongestionControl
{
private static final double ADDITIVE_INCREASE = 50000;
private static final long INTERVAL_MS = 1000;
private final Runnable rateUpdateHandler;
private final AtomicLong bytesReceived = new AtomicLong(0);
private double desiredRate;
private long lastAdjustTime;
public ClientCongestionControl(
Runnable rateUpdateHandler
)
{
this.rateUpdateHandler = rateUpdateHandler;
this.reset();
}
public void reset()
{
this.desiredRate = ADDITIVE_INCREASE;
this.lastAdjustTime = System.currentTimeMillis();
this.bytesReceived.set(0);
}
public void onPayloadReceived(FullDataSplitMessage message)
{
long now = System.currentTimeMillis();
if (now - this.lastAdjustTime >= INTERVAL_MS)
{
this.adjustRate(now);
}
this.bytesReceived.addAndGet(message.buffer.readableBytes());
}
private void adjustRate(long now)
{
double throughput = this.bytesReceived.getAndSet(0);
if (throughput >= this.desiredRate)
{
this.desiredRate += ADDITIVE_INCREASE;
}
else
{
this.desiredRate = Math.max(throughput - ADDITIVE_INCREASE / 2, 1000);
}
this.lastAdjustTime = now;
this.rateUpdateHandler.run();
}
public int getDesiredRate()
{
return (int) (this.desiredRate / 1000);
}
}
@@ -1,6 +1,7 @@
package com.seibel.distanthorizons.core.multiplayer.client; package com.seibel.distanthorizons.core.multiplayer.client;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig; import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig;
@@ -56,6 +57,23 @@ public class ClientNetworkState implements Closeable
private long serverTimeOffset = 0; private long serverTimeOffset = 0;
public long getServerTimeOffset() { return this.serverTimeOffset; } public long getServerTimeOffset() { return this.serverTimeOffset; }
private final ClientCongestionControl congestionControl = new ClientCongestionControl(
() -> {
if (Config.Server.enableAdaptiveTransferSpeed.get())
{
this.sendConfigMessage(false);
}
}
);
private final ConfigChangeListener<Boolean> adaptiveTransferSpeedListener = new ConfigChangeListener<>(Config.Server.enableAdaptiveTransferSpeed, isEnabled -> {
if (isEnabled)
{
this.congestionControl.reset();
}
this.sendConfigMessage();
});
//=============// //=============//
@@ -116,6 +134,7 @@ public class ClientNetworkState implements Closeable
}); });
this.networkSession.registerHandler(FullDataSplitMessage.class, this.fullDataPayloadReceiver::receiveChunk); this.networkSession.registerHandler(FullDataSplitMessage.class, this.fullDataPayloadReceiver::receiveChunk);
this.networkSession.registerHandler(FullDataSplitMessage.class, this.congestionControl::onPayloadReceived);
} }
} }
@@ -127,10 +146,22 @@ public class ClientNetworkState implements Closeable
public void sendConfigMessage() public void sendConfigMessage() { this.sendConfigMessage(true); }
public void sendConfigMessage(boolean blocking)
{ {
this.configReceived = false; SessionConfig sessionConfig = new SessionConfig();
this.getSession().sendMessage(new SessionConfigMessage(new SessionConfig()));
if (Config.Server.enableAdaptiveTransferSpeed.get())
{
sessionConfig.constrainValue(Config.Server.maxDataTransferSpeed, this.congestionControl.getDesiredRate());
}
if (blocking)
{
this.configReceived = false;
}
this.getSession().sendMessage(new SessionConfigMessage(sessionConfig));
} }
@@ -166,6 +197,7 @@ public class ClientNetworkState implements Closeable
public void close() public void close()
{ {
this.fullDataPayloadReceiver.close(); this.fullDataPayloadReceiver.close();
this.adaptiveTransferSpeedListener.close();
this.configAnyChangeListener.close(); this.configAnyChangeListener.close();
this.networkSession.close(); this.networkSession.close();
} }
@@ -18,7 +18,7 @@ public class SessionConfig implements INetworkObject
private static final LinkedHashMap<String, Entry> CONFIG_ENTRIES = new LinkedHashMap<>(); private static final LinkedHashMap<String, Entry> CONFIG_ENTRIES = new LinkedHashMap<>();
private final LinkedHashMap<String, Object> values = new LinkedHashMap<>(); private final HashMap<String, Object> values = new HashMap<>();
public SessionConfig constrainingConfig; public SessionConfig constrainingConfig;
@@ -123,6 +123,13 @@ public class SessionConfig implements INetworkObject
: value); : value);
} }
public <T> void constrainValue(ConfigEntry<T> configEntry, T value) { this.constrainValue(configEntry.getChatCommandName(), value); }
private void constrainValue(String name, Object value)
{
Entry entry = CONFIG_ENTRIES.get(name);
this.values.put(name, entry.valueConstrainer.apply(this.getValue(name), value));
}
private Map<String, ?> getValues() private Map<String, ?> getValues()
{ {
return CONFIG_ENTRIES.keySet().stream().collect(Collectors.toMap( return CONFIG_ENTRIES.keySet().stream().collect(Collectors.toMap(
@@ -34,8 +34,6 @@ public class FullDataPayloadReceiver implements AutoCloseable
}) })
.build().asMap(); .build().asMap();
@Override @Override
public void close() public void close()
{ {
@@ -13,7 +13,7 @@ import java.util.function.*;
public class FullDataPayloadSender implements AutoCloseable public class FullDataPayloadSender implements AutoCloseable
{ {
private static final int TICK_RATE = 4; private static final int TICK_RATE = 20;
/** 1 Mebibyte minus 576 bytes for other info */ /** 1 Mebibyte minus 576 bytes for other info */
public static final int FULL_DATA_SPLIT_SIZE_IN_BYTES = 1_048_000; public static final int FULL_DATA_SPLIT_SIZE_IN_BYTES = 1_048_000;
@@ -399,9 +399,11 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
// prepare this section for rendering // prepare this section for rendering
if (!renderSection.gpuUploadInProgress() if (!renderSection.gpuUploadInProgress()
&& renderSection.renderBuffer == null && renderSection.renderBuffer == null
// TODO this is commented out since some users reported LODs refusing to
// load at their expected higher-detail levels
// this check is specifically for N-sized world generators where the higher quality // this check is specifically for N-sized world generators where the higher quality
// data source may not exist yet, this is done to prevent holes while waiting for said generator // data source may not exist yet, this is done to prevent holes while waiting for said generator
&& renderSection.getFullDataSourceExists() //&& renderSection.getFullDataSourceExists()
) )
{ {
nodesNeedingLoading.add(renderSection); nodesNeedingLoading.add(renderSection);
@@ -746,6 +746,10 @@
"Maximum Data Transfer Speed, KB/s", "Maximum Data Transfer Speed, KB/s",
"distanthorizons.config.server.maxDataTransferSpeed.@tooltip": "distanthorizons.config.server.maxDataTransferSpeed.@tooltip":
"Maximum speed for uploading LODs to the clients, in KB/s.\nValue of 0 disables the limit.", "Maximum speed for uploading LODs to the clients, in KB/s.\nValue of 0 disables the limit.",
"distanthorizons.config.server.enableAdaptiveTransferSpeed":
"Enable Adaptive Transfer Speed",
"distanthorizons.config.server.enableAdaptiveTransferSpeed.@tooltip":
"Enables adaptive transfer speed based on client performance.\nIf true, DH will automatically adjust transfer rate to minimize connection lag.\nIf false, transfer speed will remain fixed.",
"distanthorizons.config.server.experimental": "distanthorizons.config.server.experimental":