Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad995544f7 | |||
| d521e931f4 | |||
| dd30a8274a | |||
| 3ca5efadc9 | |||
| 09174c2d2a | |||
| e079b28e77 | |||
| 136124a703 |
@@ -38,7 +38,7 @@ public final class ModInfo
|
||||
public static final String NAME = "DistantHorizons";
|
||||
/** Human-readable version of NAME */
|
||||
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. */
|
||||
public static final boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev");
|
||||
|
||||
|
||||
@@ -1713,6 +1713,15 @@ public class Config
|
||||
+ "Value of 0 disables the limit."
|
||||
+ "")
|
||||
.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();
|
||||
|
||||
|
||||
+104
-122
@@ -65,8 +65,6 @@ public class LodDataBuilder
|
||||
// only block lighting is needed here, sky lighting is populated at the data source stage
|
||||
LodUtil.assertTrue(chunkWrapper.isDhBlockLightingCorrect());
|
||||
|
||||
|
||||
|
||||
int sectionPosX = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getX());
|
||||
int sectionPosZ = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getZ());
|
||||
long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ);
|
||||
@@ -80,47 +78,31 @@ public class LodDataBuilder
|
||||
|
||||
// compute the chunk dataSource offset
|
||||
// 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();
|
||||
if (chunkWrapper.getChunkPos().getZ() < 0)
|
||||
{
|
||||
chunkOffsetZ = ((chunkOffsetZ) % FullDataSourceV2.NUMB_OF_CHUNKS_WIDE);
|
||||
if (chunkOffsetZ != 0)
|
||||
{
|
||||
chunkOffsetZ += FullDataSourceV2.NUMB_OF_CHUNKS_WIDE;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
chunkOffsetZ %= FullDataSourceV2.NUMB_OF_CHUNKS_WIDE;
|
||||
}
|
||||
// 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
|
||||
|
||||
// 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;
|
||||
|
||||
|
||||
@@ -138,54 +120,49 @@ public class LodDataBuilder
|
||||
IBlockStateWrapper previousBlockState = null;
|
||||
|
||||
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 relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++)
|
||||
{
|
||||
LongArrayList longs = dataSource.get(
|
||||
relBlockX + chunkOffsetX,
|
||||
relBlockZ + chunkOffsetZ);
|
||||
// Calculate column position
|
||||
int columnX = relBlockX + chunkOffsetX;
|
||||
int columnZ = relBlockZ + chunkOffsetZ;
|
||||
|
||||
// Get column data
|
||||
LongArrayList longs = dataSource.get(columnX, columnZ);
|
||||
if (longs == null)
|
||||
{
|
||||
longs = new LongArrayList(chunkWrapper.getHeight() / 4);
|
||||
longs = new LongArrayList(dataCapacity);
|
||||
}
|
||||
else
|
||||
{
|
||||
longs.clear();
|
||||
}
|
||||
|
||||
int lastY = chunkWrapper.getExclusiveMaxBuildHeight();
|
||||
int lastY = exclusiveMaxBuildHeight;
|
||||
IBiomeWrapper biome = chunkWrapper.getBiome(relBlockX, lastY, relBlockZ);
|
||||
IBlockStateWrapper blockState = AIR;
|
||||
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;
|
||||
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
|
||||
// Get the maximum height from both heightmaps
|
||||
int y = Math.max(
|
||||
// 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)
|
||||
chunkWrapper.getLightBlockingHeightMapValue(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);
|
||||
while (!topBlockState.isAir() && y < chunkWrapper.getExclusiveMaxBuildHeight())
|
||||
while (!topBlockState.isAir() && y < exclusiveMaxBuildHeight)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -198,7 +175,7 @@ public class LodDataBuilder
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -207,7 +184,7 @@ public class LodDataBuilder
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Process blocks from top to bottom
|
||||
for (; y >= minBuildHeight; y--)
|
||||
{
|
||||
IBiomeWrapper newBiome = chunkWrapper.getBiome(relBlockX, y, relBlockZ);
|
||||
@@ -215,10 +192,10 @@ public class LodDataBuilder
|
||||
byte newBlockLight = (byte) chunkWrapper.getDhBlockLight(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))
|
||||
{
|
||||
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;
|
||||
blockState = newBlockState;
|
||||
|
||||
@@ -228,17 +205,16 @@ public class LodDataBuilder
|
||||
lastY = y;
|
||||
}
|
||||
}
|
||||
longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - chunkWrapper.getInclusiveMinBuildHeight(), blockLight, skyLight));
|
||||
|
||||
dataSource.setSingleColumn(longs,
|
||||
relBlockX + chunkOffsetX,
|
||||
relBlockZ + chunkOffsetZ,
|
||||
EDhApiWorldGenerationStep.LIGHT,
|
||||
worldCompressionMode);
|
||||
// Add the final data point
|
||||
longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - inclusiveMinBuildHeight, blockLight, skyLight));
|
||||
|
||||
// Set the column in the data source
|
||||
dataSource.setSingleColumn(longs, columnX, columnZ, EDhApiWorldGenerationStep.LIGHT, worldCompressionMode);
|
||||
}
|
||||
}
|
||||
|
||||
if (ignoreHiddenBlocks)
|
||||
if (ignoreHiddenBlocks)
|
||||
{
|
||||
cullHiddenBlocks(dataSource, chunkOffsetX, chunkOffsetZ);
|
||||
}
|
||||
@@ -275,16 +251,16 @@ public class LodDataBuilder
|
||||
{
|
||||
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))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// the top segment should never be culled.
|
||||
if (centerIndex == 0
|
||||
|| isTranslucent(dataSource, centerColumn.getLong(centerIndex - 1))
|
||||
)
|
||||
if (centerIndex == 0
|
||||
|| isTranslucent(dataSource, centerColumn.getLong(centerIndex - 1))
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -292,9 +268,9 @@ public class LodDataBuilder
|
||||
// the bottom segment can sometimes be culled.
|
||||
// assume it will not be seen from below,
|
||||
// because this would imply the player is in the void.
|
||||
if (centerIndex + 1 < centerColumn.size()
|
||||
&& isTranslucent(dataSource, centerColumn.getLong(centerIndex + 1))
|
||||
)
|
||||
if (centerIndex + 1 < centerColumn.size()
|
||||
&& isTranslucent(dataSource, centerColumn.getLong(centerIndex + 1))
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -327,9 +303,11 @@ public class LodDataBuilder
|
||||
continue;
|
||||
}
|
||||
|
||||
// current point is fully surrounded. remove it.
|
||||
// Current point is fully surrounded. remove it.
|
||||
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);
|
||||
above = FullDataPointUtil.setBottomY(above, FullDataPointUtil.getBottomY(currentPoint));
|
||||
above = FullDataPointUtil.setHeight(above, FullDataPointUtil.getHeight(currentPoint) + FullDataPointUtil.getHeight(above));
|
||||
@@ -338,31 +316,31 @@ public class LodDataBuilder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
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,
|
||||
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 centerPoint the point being checked to see if it's fully covered.
|
||||
@param adjacentColumn the data points which might cover centerPoint.
|
||||
@param adjacentIndex the starting index in adjacentColumn to start scanning at.
|
||||
indices greater than adjacentIndex have already been checked and confirmed to
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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
|
||||
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.
|
||||
*/
|
||||
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,
|
||||
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 centerPoint the point being checked to see if it's fully covered.
|
||||
@param adjacentColumn the data points which might cover centerPoint.
|
||||
@param adjacentIndex the starting index in adjacentColumn to start scanning at.
|
||||
indices greater than adjacentIndex have already been checked and confirmed to
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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
|
||||
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.
|
||||
*/
|
||||
private static int checkOcclusion(FullDataSourceV2 source, long centerPoint, LongArrayList adjacentColumn, int adjacentIndex)
|
||||
{
|
||||
int bottomOfCenter = FullDataPointUtil.getBottomY(centerPoint);
|
||||
@@ -387,12 +365,12 @@ public class LodDataBuilder
|
||||
|
||||
throw new LodUtil.AssertFailureException("Adjacent column ends before center column does.");
|
||||
}
|
||||
|
||||
|
||||
private static boolean isTranslucent(FullDataSourceV2 source, long point) {
|
||||
return source.mapping.getBlockStateWrapper(FullDataPointUtil.getId(point)).getOpacity() < LodUtil.BLOCK_FULLY_OPAQUE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/** @throws ClassCastException if an API user returns the wrong object type(s) */
|
||||
public static FullDataSourceV2 createFromApiChunkData(DhApiChunk apiChunk, boolean runAdditionalValidation) throws ClassCastException, DataCorruptedException, IllegalArgumentException
|
||||
@@ -402,9 +380,13 @@ public class LodDataBuilder
|
||||
int sectionPosZ = getXOrZSectionPosFromChunkPos(apiChunk.chunkPosZ);
|
||||
long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ);
|
||||
|
||||
// chunk relative block position in the data source
|
||||
int relSourceBlockX = Math.floorMod(apiChunk.chunkPosX, 4) * LodUtil.CHUNK_WIDTH;
|
||||
int relSourceBlockZ = Math.floorMod(apiChunk.chunkPosZ, 4) * LodUtil.CHUNK_WIDTH;
|
||||
// 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 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);
|
||||
for (int relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++)
|
||||
@@ -423,8 +405,8 @@ public class LodDataBuilder
|
||||
// TODO add the ability for API users to define a different compression mode
|
||||
// or add a "unkown" compression mode
|
||||
dataSource.setSingleColumn(
|
||||
packedDataPoints,
|
||||
relBlockX + relSourceBlockX, relBlockZ + relSourceBlockZ,
|
||||
packedDataPoints,
|
||||
relBlockX + relSourceBlockX, relBlockZ + relSourceBlockZ,
|
||||
EDhApiWorldGenerationStep.LIGHT, EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS);
|
||||
dataSource.isEmpty = false;
|
||||
}
|
||||
@@ -440,7 +422,7 @@ public class LodDataBuilder
|
||||
|
||||
/** @see FullDataPointUtil */
|
||||
public static LongArrayList convertApiDataPointListToPackedLongArray(
|
||||
@Nullable List<DhApiTerrainDataPoint> columnDataPoints, FullDataSourceV2 dataSource,
|
||||
@Nullable List<DhApiTerrainDataPoint> columnDataPoints, FullDataSourceV2 dataSource,
|
||||
int bottomYBlockPos) throws DataCorruptedException
|
||||
{
|
||||
// this null check does 2 nice things at the same time:
|
||||
@@ -533,13 +515,13 @@ public class LodDataBuilder
|
||||
}
|
||||
// is there a gap between the last datapoint?
|
||||
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.");
|
||||
}
|
||||
|
||||
|
||||
lastBottomYPos = bottomYPos;
|
||||
lastBottomYPos = bottomYPos;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+71
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+35
-3
@@ -1,6 +1,7 @@
|
||||
package com.seibel.distanthorizons.core.multiplayer.client;
|
||||
|
||||
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.logging.ConfigBasedLogger;
|
||||
import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig;
|
||||
@@ -56,6 +57,23 @@ public class ClientNetworkState implements Closeable
|
||||
private long serverTimeOffset = 0;
|
||||
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.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;
|
||||
this.getSession().sendMessage(new SessionConfigMessage(new SessionConfig()));
|
||||
SessionConfig sessionConfig = 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()
|
||||
{
|
||||
this.fullDataPayloadReceiver.close();
|
||||
this.adaptiveTransferSpeedListener.close();
|
||||
this.configAnyChangeListener.close();
|
||||
this.networkSession.close();
|
||||
}
|
||||
|
||||
+8
-1
@@ -18,7 +18,7 @@ public class SessionConfig implements INetworkObject
|
||||
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;
|
||||
|
||||
|
||||
@@ -123,6 +123,13 @@ public class SessionConfig implements INetworkObject
|
||||
: 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()
|
||||
{
|
||||
return CONFIG_ENTRIES.keySet().stream().collect(Collectors.toMap(
|
||||
|
||||
-2
@@ -34,8 +34,6 @@ public class FullDataPayloadReceiver implements AutoCloseable
|
||||
})
|
||||
.build().asMap();
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import java.util.function.*;
|
||||
|
||||
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 */
|
||||
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
|
||||
if (!renderSection.gpuUploadInProgress()
|
||||
&& 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
|
||||
// data source may not exist yet, this is done to prevent holes while waiting for said generator
|
||||
&& renderSection.getFullDataSourceExists()
|
||||
//&& renderSection.getFullDataSourceExists()
|
||||
)
|
||||
{
|
||||
nodesNeedingLoading.add(renderSection);
|
||||
|
||||
@@ -746,6 +746,10 @@
|
||||
"Maximum Data Transfer Speed, KB/s",
|
||||
"distanthorizons.config.server.maxDataTransferSpeed.@tooltip":
|
||||
"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":
|
||||
|
||||
Reference in New Issue
Block a user