Compare commits

..

3 Commits

Author SHA1 Message Date
James Seibel 1e6eb4c096 Add complete multidraw 2024-07-17 17:39:52 -05:00
James Seibel 618707533b add multidraw 2024-07-17 17:09:56 -05:00
James Seibel 2d7e2e2444 Merge render VBOs 2024-07-17 15:15:43 -05:00
198 changed files with 5711 additions and 7981 deletions
+1 -1
View File
@@ -537,7 +537,7 @@ ij_groovy_wrap_chain_calls_after_dot = false
ij_groovy_wrap_long_lines = false ij_groovy_wrap_long_lines = false
[{*.har,*.json,*.png.mcmeta,mcmod.info,pack.mcmeta}] [{*.har,*.json,*.png.mcmeta,mcmod.info,pack.mcmeta}]
indent_size = 2 indent_size = 4
ij_json_array_wrapping = split_into_lines ij_json_array_wrapping = split_into_lines
ij_json_keep_blank_lines_in_code = 0 ij_json_keep_blank_lines_in_code = 0
ij_json_keep_indents_on_empty_lines = false ij_json_keep_indents_on_empty_lines = false
@@ -180,7 +180,7 @@ public class DhApi
* This version should be updated whenever non-breaking fixes are added to the Distant Horizons API. * This version should be updated whenever non-breaking fixes are added to the Distant Horizons API.
* @since API 1.0.0 * @since API 1.0.0
*/ */
public static int getApiPatchVersion() { return ModInfo.API_PATCH_VERSION; } public static int getApiPatchVersion() { return ModInfo.API_PATH_VERSION; }
/** /**
* Returns the mod's semantic version number in the format: Major.Minor.Patch * Returns the mod's semantic version number in the format: Major.Minor.Patch
@@ -210,4 +210,4 @@ public class DhApi
*/ */
public static boolean isDhThread() { return Thread.currentThread().getName().startsWith(ModInfo.THREAD_NAME_PREFIX); } public static boolean isDhThread() { return Thread.currentThread().getName().startsWith(ModInfo.THREAD_NAME_PREFIX); }
} }
@@ -24,7 +24,7 @@ package com.seibel.distanthorizons.api.enums;
* CHUNK - Detail Level: 4, width 16 block, <br> * CHUNK - Detail Level: 4, width 16 block, <br>
* REGION - Detail Level: 9, width 512 block <br> <br> * REGION - Detail Level: 9, width 512 block <br> <br>
* *
* Detail levels in Distant Horizons represent how large a LOD * Detail levels in Distant Horizons represent how large a section (of either LODs or MC chunks)
* is, with the smallest being 0 (1 block wide). <br> * is, with the smallest being 0 (1 block wide). <br>
* The width of a detail level can be calculated by putting the detail level to the power of 2. <br> * The width of a detail level can be calculated by putting the detail level to the power of 2. <br>
* Example for the chunk detail level (4): 2^4 = 16 blocks wide <Br><br> * Example for the chunk detail level (4): 2^4 = 16 blocks wide <Br><br>
@@ -23,12 +23,13 @@ package com.seibel.distanthorizons.api.enums.config;
* AUTO, <br> * AUTO, <br>
* BUFFER_STORAGE, <br> * BUFFER_STORAGE, <br>
* SUB_DATA, <br> * SUB_DATA, <br>
* BUFFER_MAPPING, <br>
* DATA <br> * DATA <br>
* *
* @author Leetom * @author Leetom
* @author James Seibel * @author James Seibel
* @version 2024-4-6 * @version 2024-4-6
* @since API 3.0.0 * @since API 2.0.0
*/ */
public enum EDhApiGpuUploadMethod public enum EDhApiGpuUploadMethod
{ {
@@ -48,10 +49,7 @@ public enum EDhApiGpuUploadMethod
* May end up storing buffers in System memory. <br> * May end up storing buffers in System memory. <br>
* Fast rending if in GPU memory, slow if in system memory, <br> * Fast rending if in GPU memory, slow if in system memory, <br>
* but won't stutter when uploading. * but won't stutter when uploading.
*
* @deprecated not currently supported
*/ */
@Deprecated
BUFFER_MAPPING(true, false), BUFFER_MAPPING(true, false),
/** Fast rendering but may stutter when uploading. */ /** Fast rendering but may stutter when uploading. */
@@ -21,7 +21,6 @@ package com.seibel.distanthorizons.api.enums.rendering;
* AIR, <br> * AIR, <br>
* ILLUMINATED, <br> * ILLUMINATED, <br>
* *
* @author IMS
* @author James Seibel * @author James Seibel
* @since API 3.0.0 * @since API 3.0.0
* @version 2024-7-11 * @version 2024-7-11
@@ -19,7 +19,6 @@
package com.seibel.distanthorizons.api.interfaces.block; package com.seibel.distanthorizons.api.interfaces.block;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiBlockMaterial;
import com.seibel.distanthorizons.api.interfaces.IDhApiUnsafeWrapper; import com.seibel.distanthorizons.api.interfaces.IDhApiUnsafeWrapper;
/** /**
@@ -45,11 +44,7 @@ public interface IDhApiBlockStateWrapper extends IDhApiUnsafeWrapper
* @since API 3.0.0 * @since API 3.0.0
*/ */
String getSerialString(); String getSerialString();
/** /** @since API 3.0.0 */
* Returns the byte value representing the {@link EDhApiBlockMaterial} enum.
* @see EDhApiBlockMaterial
* @since API 3.0.0
*/
byte getMaterialId(); byte getMaterialId();
} }
@@ -36,6 +36,7 @@ public interface IDhApiConfig
IDhApiWorldGenerationConfig worldGenerator(); IDhApiWorldGenerationConfig worldGenerator();
IDhApiMultiplayerConfig multiplayer(); IDhApiMultiplayerConfig multiplayer();
IDhApiMultiThreadingConfig multiThreading(); IDhApiMultiThreadingConfig multiThreading();
IDhApiGpuBuffersConfig gpuBuffers();
// note: DON'T add the Auto Updater to this API. We only want the user's to have the ability to control when things are downloaded to their machines. // note: DON'T add the Auto Updater to this API. We only want the user's to have the ability to control when things are downloaded to their machines.
//IDhApiLoggingConfig logging(); // TODO implement //IDhApiLoggingConfig logging(); // TODO implement
IDhApiDebuggingConfig debugging(); IDhApiDebuggingConfig debugging();
@@ -0,0 +1,39 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.api.interfaces.config.client;
import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigGroup;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
/**
* Distant Horizons' OpenGL buffer configuration.
*
* @author James Seibel
* @version 2023-6-14
* @since API 1.0.0
*/
public interface IDhApiGpuBuffersConfig extends IDhApiConfigGroup
{
/** Defines how geometry data is uploaded to the GPU. */
IDhApiConfigValue<EDhApiGpuUploadMethod> gpuUploadMethod();
}
@@ -79,34 +79,11 @@ public interface IDhApiWrapperFactory
/** ///**
* Constructs a {@link IDhApiBiomeWrapper} for use by other DhApi methods. // * Specifically designed to be used with the API.
* // *
* @param resourceLocationString example: "minecraft:plains" // * @throws ClassCastException with instructions on expected objects if the object couldn't be cast
* // */
* @param levelWrapper Expects a {@link IDhApiLevelWrapper} returned by one of DH's {@link DhApi.Delayed#worldProxy} methods. <br> //IChunkWrapper createChunkWrapper(Object[] objectArray) throws ClassCastException;
* A custom implementation of {@link IDhApiLevelWrapper} will not be accepted.
*
* @throws IOException if the resourceLocationString wasn't able to be parsed or converted into a valid {@link IDhApiBiomeWrapper}
* @throws ClassCastException if the wrong levelWrapper type was given
*
* @since API 3.0.0
*/
IDhApiBiomeWrapper getBiomeWrapper(String resourceLocationString, IDhApiLevelWrapper levelWrapper) throws IOException, ClassCastException;
/**
* Constructs a {@link IDhApiBlockStateWrapper} for use by other DhApi methods.
* This returns the default blockstate for the given resource location.
*
* @param resourceLocationString examples: "minecraft:bedrock", "minecraft:stone", "minecraft:grass_block"
* @param levelWrapper Expects a {@link IDhApiBlockStateWrapper} returned by one of DH's {@link DhApi.Delayed#worldProxy} methods. <br>
* A custom implementation of {@link IDhApiBlockStateWrapper} will not be accepted.
*
* @throws IOException if the resourceLocationString wasn't able to be parsed or converted into a valid {@link IDhApiBlockStateWrapper}
* @throws ClassCastException if the wrong levelWrapper type was given
*
* @since API 3.0.0
*/
IDhApiBlockStateWrapper getDefaultBlockStateWrapper(String resourceLocationString, IDhApiLevelWrapper levelWrapper) throws IOException, ClassCastException;
} }
@@ -22,8 +22,6 @@ package com.seibel.distanthorizons.api.interfaces.override.worldGenerator;
import com.seibel.distanthorizons.api.enums.EDhApiDetailLevel; import com.seibel.distanthorizons.api.enums.EDhApiDetailLevel;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorMode; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorMode;
import com.seibel.distanthorizons.api.interfaces.override.IDhApiOverrideable; import com.seibel.distanthorizons.api.interfaces.override.IDhApiOverrideable;
import com.seibel.distanthorizons.api.objects.data.DhApiChunk;
import com.seibel.distanthorizons.api.objects.data.DhApiTerrainDataPoint;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import java.io.Closeable; import java.io.Closeable;
@@ -79,41 +77,13 @@ public abstract class AbstractDhApiChunkWorldGenerator implements Closeable, IDh
}, worldGeneratorThreadPool); }, worldGeneratorThreadPool);
} }
@Override
public final CompletableFuture<Void> generateApiChunks(
int chunkPosMinX,
int chunkPosMinZ,
byte granularity,
byte targetDataDetail,
EDhApiDistantGeneratorMode generatorMode,
ExecutorService worldGeneratorThreadPool,
Consumer<DhApiChunk> resultConsumer
)
{
return CompletableFuture.runAsync(() ->
{
// TODO what does this mean?
int genChunkWidth = BitShiftUtil.powerOfTwo(granularity - 4);
for (int chunkX = chunkPosMinX; chunkX < chunkPosMinX + genChunkWidth; chunkX++)
{
for (int chunkZ = chunkPosMinZ; chunkZ < chunkPosMinZ + genChunkWidth; chunkZ++)
{
DhApiChunk apiChunk = this.generateApiChunk(chunkX, chunkZ, generatorMode);
resultConsumer.accept(apiChunk);
}
}
}, worldGeneratorThreadPool);
}
/** /**
* This method is called to generate terrain over a given area * This method is called to generate terrain over a given area
* from a thread defined by Distant Horizons. <br><br> * from a thread defined by Distant Horizons. <br><br>
* *
* @param chunkPosX the chunk X position in the level (not to be confused with the chunk's BlockPos in the level) * @param chunkPosX the chunk X position in the level (not to be confused with the chunk's BlockPos in the level)
* @param chunkPosZ the chunk Z position in the level (not to be confused with the chunk's BlockPos in the level) * @param chunkPosZ the chunk Z position in the level (not to be confused with the chunk's BlockPos in the level)
* @param generatorMode how far into the world gen pipeline this method should run. See {@link EDhApiDistantGeneratorMode} for additional documentation. * @param generatorMode how far into the world gen pipeline this method run. See {@link EDhApiDistantGeneratorMode} for additional documentation.
* *
* @return See {@link IDhApiWorldGenerator#generateChunks(int, int, byte, byte, EDhApiDistantGeneratorMode, ExecutorService, Consumer) IDhApiWorldGenerator.generateChunks} * @return See {@link IDhApiWorldGenerator#generateChunks(int, int, byte, byte, EDhApiDistantGeneratorMode, ExecutorService, Consumer) IDhApiWorldGenerator.generateChunks}
* for the list of Object's this method should return along with additional documentation. * for the list of Object's this method should return along with additional documentation.
@@ -122,21 +92,4 @@ public abstract class AbstractDhApiChunkWorldGenerator implements Closeable, IDh
*/ */
public abstract Object[] generateChunk(int chunkPosX, int chunkPosZ, EDhApiDistantGeneratorMode generatorMode); public abstract Object[] generateChunk(int chunkPosX, int chunkPosZ, EDhApiDistantGeneratorMode generatorMode);
/**
* This method is called to generate terrain over a given area
* from a thread defined by Distant Horizons. <br><br>
*
* @param chunkPosX the chunk X position in the level (not to be confused with the chunk's BlockPos in the level)
* @param chunkPosZ the chunk Z position in the level (not to be confused with the chunk's BlockPos in the level)
* @param generatorMode how far into the world gen pipeline this method should run. See {@link EDhApiDistantGeneratorMode} for additional documentation.
*
* @return A {@link DhApiChunk} with the generated {@link DhApiTerrainDataPoint} including air blocks.
* Note: if air blocks aren't included with the proper lighting, lower detail levels will appear as black/unlit.
*
* @see IDhApiWorldGenerator#generateApiChunks(int, int, byte, byte, EDhApiDistantGeneratorMode, ExecutorService, Consumer)
*
* @since API 3.0.0
*/
public abstract DhApiChunk generateApiChunk(int chunkPosX, int chunkPosZ, EDhApiDistantGeneratorMode generatorMode);
} }
@@ -90,29 +90,11 @@ public interface IDhApiWorldGenerator extends Closeable, IDhApiOverrideable
*/ */
default byte getMaxGenerationGranularity() { return (byte) (EDhApiDetailLevel.CHUNK.detailLevel + 2); } default byte getMaxGenerationGranularity() { return (byte) (EDhApiDetailLevel.CHUNK.detailLevel + 2); }
/**
* Starting in API 3.0.0 DH now handles future queuing/management internally. <br><br>
*
* Previous description: <br>
* true if the generator is unable to accept new generation requests. <br>
*
* @since API 1.0.0
* @deprecated API 3.0.0
*/
@Deprecated
default boolean isBusy() { return false; }
/** /**
* Only used if {@link #getReturnType()} returns {@link EDhApiWorldGeneratorReturnType#API_CHUNKS}. <Br> * @return true if the generator is unable to accept new generation requests.
* If true DH will run additional validation on the {@link DhApiChunk}'s returned. <Br> * @since API 1.0.0
* This should be disabled during release but should be enabled during development to help spot issues with your data format.
*
* @see #getReturnType()
* @see DhApiChunk
* @see EDhApiWorldGeneratorReturnType#API_CHUNKS
* @since API 3.0.0
*/ */
default boolean runApiChunkValidation() { return true; } boolean isBusy();
@@ -172,7 +154,6 @@ public interface IDhApiWorldGenerator extends Closeable, IDhApiOverrideable
* *
* After the {@link DhApiChunk} has been generated, it should be passed into the * After the {@link DhApiChunk} has been generated, it should be passed into the
* resultConsumer's {@link Consumer#accept(Object)} method. * resultConsumer's {@link Consumer#accept(Object)} method.
* Note: if air blocks aren't included in the with the {@link DhApiChunk} with proper lighting, lower detail levels will appear as black/unlit.
* *
* @implNote the default implementation of this method throws an {@link UnsupportedOperationException}, * @implNote the default implementation of this method throws an {@link UnsupportedOperationException},
* and must be overridden when {@link #getReturnType()} returns {@link EDhApiWorldGeneratorReturnType#API_CHUNKS}. * and must be overridden when {@link #getReturnType()} returns {@link EDhApiWorldGeneratorReturnType#API_CHUNKS}.
@@ -26,7 +26,7 @@ import com.seibel.distanthorizons.api.objects.DhApiResult;
* Used to interact with Distant Horizons' rendering system. * Used to interact with Distant Horizons' rendering system.
* *
* @author James Seibel * @author James Seibel
* @version 2024-7-27 * @version 2023-10-13
* @since API 1.0.0 * @since API 1.0.0
*/ */
public interface IDhApiRenderProxy public interface IDhApiRenderProxy
@@ -39,8 +39,10 @@ public interface IDhApiRenderProxy
* If this is called on a dedicated server it won't do anything and will return {@link DhApiResult#success} = false <Br><Br> * If this is called on a dedicated server it won't do anything and will return {@link DhApiResult#success} = false <Br><Br>
* *
* Background: <Br> * Background: <Br>
* When rendering Distant Horizons bakes each block's color into the geometry that's rendered. <Br> * Distant Horizons has two different file formats: Full data and Render data. <Br>
* This improves rendering speed and VRAM size, but prevents dynamically changing LOD colors. <Br> * - Full data files store the block, biome, etc. information and is the result of loading or generating new chunks. <Br>
* - Render data files store LOD colors and are created using the Full data and currently loaded resource packs. <Br>
* This is the data cleared by this method.
*/ */
DhApiResult<Boolean> clearRenderDataCache(); DhApiResult<Boolean> clearRenderDataCache();
@@ -28,8 +28,10 @@ import com.seibel.distanthorizons.api.interfaces.IDhApiUnsafeWrapper;
*/ */
public interface IDhApiDimensionTypeWrapper extends IDhApiUnsafeWrapper public interface IDhApiDimensionTypeWrapper extends IDhApiUnsafeWrapper
{ {
String getDimensionName();
boolean hasCeiling(); boolean hasCeiling();
boolean hasSkyLight(); boolean hasSkyLight();
} }
@@ -28,33 +28,20 @@ import com.seibel.distanthorizons.api.interfaces.render.IDhApiCustomRenderRegist
* A level is equivalent to a dimension in vanilla Minecraft. * A level is equivalent to a dimension in vanilla Minecraft.
* *
* @author James Seibel * @author James Seibel
* @version 2024-7-28 * @version 2022-7-14
* @since API 1.0.0 * @since API 1.0.0
*/ */
public interface IDhApiLevelWrapper extends IDhApiUnsafeWrapper public interface IDhApiLevelWrapper extends IDhApiUnsafeWrapper
{ {
IDhApiDimensionTypeWrapper getDimensionType(); IDhApiDimensionTypeWrapper getDimensionType();
String getDimensionName();
EDhApiLevelType getLevelType(); EDhApiLevelType getLevelType();
boolean hasCeiling(); boolean hasCeiling();
boolean hasSkyLight(); boolean hasSkyLight();
/** /** Returns the max block height of the level(?) */
* Deprecated, use {@link IDhApiLevelWrapper#getMaxHeight} instead. <br>
* Returns the max block height of the level.
*
* @see IDhApiLevelWrapper#getMaxHeight
*/
@Deprecated
default int getHeight() { return this.getMaxHeight(); }
/**
* Returns the max block height of the level
* @since API 3.0.0
*/
int getMaxHeight(); int getMaxHeight();
/** /**
@@ -66,9 +53,7 @@ public interface IDhApiLevelWrapper extends IDhApiUnsafeWrapper
/** /**
* Will return null if called on the server, * Will return null if called on the server,
* or if called before the renderer has been set up. * or if called before the renderer has been set up.
*
* @since API 3.0.0
*/ */
IDhApiCustomRenderRegister getRenderRegister(); IDhApiCustomRenderRegister getRenderRegister();
} }
@@ -21,17 +21,15 @@ package com.seibel.distanthorizons.api.objects.data;
import com.seibel.distanthorizons.api.interfaces.factories.IDhApiWrapperFactory; import com.seibel.distanthorizons.api.interfaces.factories.IDhApiWrapperFactory;
import com.seibel.distanthorizons.api.interfaces.override.worldGenerator.IDhApiWorldGenerator; import com.seibel.distanthorizons.api.interfaces.override.worldGenerator.IDhApiWorldGenerator;
import org.apache.logging.log4j.LogManager;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
* Contains a list of {@link DhApiTerrainDataPoint} representing the blocks in a Minecraft chunk. * Contains a list of {@link DhApiTerrainDataPoint} representing the blocks in a Minecraft chunk.
* *
* @author Builderb0y, James Seibel * @author Builderb0y, James Seibel
* @version 2024-7-21 * @version 2023-12-21
* @since API 2.0.0 * @since API 2.0.0
* *
* @see IDhApiWrapperFactory * @see IDhApiWrapperFactory
@@ -43,8 +41,8 @@ public class DhApiChunk
public final int chunkPosX; public final int chunkPosX;
public final int chunkPosZ; public final int chunkPosZ;
public final int bottomYBlockPos;
public final int topYBlockPos; public final int topYBlockPos;
public final int bottomYBlockPos;
private final List<List<DhApiTerrainDataPoint>> dataPoints; private final List<List<DhApiTerrainDataPoint>> dataPoints;
@@ -54,32 +52,12 @@ public class DhApiChunk
// constructors // // constructors //
//==============// //==============//
/**
* Deprecated due to the topYBlockPos and bottomYBlockPos variables being put in the wrong order.
* They should have been in bottom -> top order.
*
* @see DhApiChunk#create(int, int, int, int)
*/
@Deprecated
public DhApiChunk(int chunkPosX, int chunkPosZ, int topYBlockPos, int bottomYBlockPos) public DhApiChunk(int chunkPosX, int chunkPosZ, int topYBlockPos, int bottomYBlockPos)
{ this(chunkPosX, chunkPosZ, bottomYBlockPos, topYBlockPos, false); }
/**
* @since API 3.0.0
*/
public static DhApiChunk create(int chunkPosX, int chunkPosZ, int bottomYBlockPos, int topYBlockPos)
{ return new DhApiChunk(chunkPosX, chunkPosZ, bottomYBlockPos, topYBlockPos, false); }
/**
* Only visible to internal DH methods
* @param ignoredParameter is only present to differentiate the two constructors and isn't actually used
*/
private DhApiChunk(int chunkPosX, int chunkPosZ, int bottomYBlockPos, int topYBlockPos, boolean ignoredParameter)
{ {
this.chunkPosX = chunkPosX; this.chunkPosX = chunkPosX;
this.chunkPosZ = chunkPosZ; this.chunkPosZ = chunkPosZ;
this.bottomYBlockPos = bottomYBlockPos;
this.topYBlockPos = topYBlockPos; this.topYBlockPos = topYBlockPos;
this.bottomYBlockPos = bottomYBlockPos;
// populate the array to prevent null pointers // populate the array to prevent null pointers
this.dataPoints = new ArrayList<>(16 * 16); // 256 this.dataPoints = new ArrayList<>(16 * 16); // 256
@@ -116,34 +94,27 @@ public class DhApiChunk
*/ */
public void setDataPoints(int relX, int relZ, List<DhApiTerrainDataPoint> dataPoints) throws IndexOutOfBoundsException, IllegalArgumentException public void setDataPoints(int relX, int relZ, List<DhApiTerrainDataPoint> dataPoints) throws IndexOutOfBoundsException, IllegalArgumentException
{ {
//==================//
// basic validation //
//==================//
// heavier validation is done in the world generator if requested
int internalArrayIndex = (relZ << 4) | relX;
throwIfRelativePosOutOfBounds(relX, relZ); throwIfRelativePosOutOfBounds(relX, relZ);
if (dataPoints == null) // validate the incoming datapoints
if (dataPoints != null)
{ {
// we don't allow null columns for (int i = 0; i < dataPoints.size(); i++) // standard for-loop used instead of an enhanced for-loop to slightly reduce GC overhead due to iterator allocation
throw new IllegalArgumentException("Null columns aren't allowed. If you want to remove all data from a column please clear the list or pass in an empty list."); {
DhApiTerrainDataPoint dataPoint = dataPoints.get(i);
if (dataPoint == null)
{
throw new IllegalArgumentException("Null DhApiTerrainDataPoints are not allowed. If you want to represent empty terrain, please use AIR.");
}
if (dataPoint.detailLevel != 0)
{
throw new IllegalArgumentException("DhApiTerrainDataPoints has the wrong detail level ["+dataPoint.detailLevel+"], all data points must be block sized; IE their detail level must be [0].");
}
}
} }
this.dataPoints.set((relZ << 4) | relX, dataPoints);
//================//
// set datapoints //
//================//
List<DhApiTerrainDataPoint> column = this.dataPoints.get(internalArrayIndex);
if (column == null)
{
column = new ArrayList<>();
this.dataPoints.set(internalArrayIndex, column);
}
column.addAll(dataPoints);
} }
@@ -19,17 +19,14 @@
package com.seibel.distanthorizons.api.objects.data; package com.seibel.distanthorizons.api.objects.data;
import com.seibel.distanthorizons.api.enums.EDhApiDetailLevel;
import com.seibel.distanthorizons.api.interfaces.block.IDhApiBiomeWrapper; import com.seibel.distanthorizons.api.interfaces.block.IDhApiBiomeWrapper;
import com.seibel.distanthorizons.api.interfaces.block.IDhApiBlockStateWrapper; import com.seibel.distanthorizons.api.interfaces.block.IDhApiBlockStateWrapper;
import java.util.ArrayList;
/** /**
* Holds a single datapoint of terrain data. * Holds a single datapoint of terrain data.
* *
* @author James Seibel * @author James Seibel
* @version 2024-7-20 * @version 2022-11-13
* @since API 1.0.0 * @since API 1.0.0
*/ */
public class DhApiTerrainDataPoint public class DhApiTerrainDataPoint
@@ -40,79 +37,27 @@ public class DhApiTerrainDataPoint
* 2 = 4x4 blocks <br> * 2 = 4x4 blocks <br>
* 4 = chunk (16x16 blocks) <br> * 4 = chunk (16x16 blocks) <br>
* 9 = region (512x512 blocks) <br> * 9 = region (512x512 blocks) <br>
*
* @see EDhApiDetailLevel
*/ */
public final byte detailLevel; public final byte detailLevel;
public final int blockLightLevel; public final int blockLightLevel;
public final int skyLightLevel; public final int skyLightLevel;
public final int bottomYBlockPos;
public final int topYBlockPos; public final int topYBlockPos;
public final int bottomYBlockPos;
public final IDhApiBlockStateWrapper blockStateWrapper; public final IDhApiBlockStateWrapper blockStateWrapper;
public final IDhApiBiomeWrapper biomeWrapper; public final IDhApiBiomeWrapper biomeWrapper;
//==============// public DhApiTerrainDataPoint(byte detailLevel, int blockLightLevel, int skyLightLevel, int topYBlockPos, int bottomYBlockPos, IDhApiBlockStateWrapper blockStateWrapper, IDhApiBiomeWrapper biomeWrapper)
// constructors //
//==============//
/**
* Deprecated due to the topYBlockPos and bottomYBlockPos variables being put in the wrong order.
* They should have been in bottom -> top order.
*
* @see DhApiTerrainDataPoint#create(byte, int, int, int, int, IDhApiBlockStateWrapper, IDhApiBiomeWrapper)
*/
@Deprecated
public DhApiTerrainDataPoint(
byte detailLevel,
int blockLightLevel, int skyLightLevel,
int topYBlockPos, int bottomYBlockPos,
IDhApiBlockStateWrapper blockStateWrapper, IDhApiBiomeWrapper biomeWrapper)
{
this(detailLevel, blockLightLevel, skyLightLevel,
bottomYBlockPos, topYBlockPos,
blockStateWrapper, biomeWrapper,
false);
}
/**
* @since API 3.0.0
*/
public static DhApiTerrainDataPoint create(
byte detailLevel,
int blockLightLevel, int skyLightLevel,
int bottomYBlockPos, int topYBlockPos,
IDhApiBlockStateWrapper blockStateWrapper, IDhApiBiomeWrapper biomeWrapper
)
{
return new DhApiTerrainDataPoint(
detailLevel, blockLightLevel, skyLightLevel,
bottomYBlockPos, topYBlockPos,
blockStateWrapper, biomeWrapper,
false);
}
/**
* Only visible to internal DH methods
* @param ignoredParameter is only present to differentiate the two constructors and isn't actually used
*/
private DhApiTerrainDataPoint(
byte detailLevel,
int blockLightLevel, int skyLightLevel,
int bottomYBlockPos, int topYBlockPos,
IDhApiBlockStateWrapper blockStateWrapper, IDhApiBiomeWrapper biomeWrapper,
boolean ignoredParameter
)
{ {
this.detailLevel = detailLevel; this.detailLevel = detailLevel;
this.blockLightLevel = blockLightLevel; this.blockLightLevel = blockLightLevel;
this.skyLightLevel = skyLightLevel; this.skyLightLevel = skyLightLevel;
this.bottomYBlockPos = bottomYBlockPos;
this.topYBlockPos = topYBlockPos; this.topYBlockPos = topYBlockPos;
this.bottomYBlockPos = bottomYBlockPos;
this.blockStateWrapper = blockStateWrapper; this.blockStateWrapper = blockStateWrapper;
this.biomeWrapper = biomeWrapper; this.biomeWrapper = biomeWrapper;
@@ -26,22 +26,15 @@ package com.seibel.distanthorizons.coreapi;
public final class ModInfo public final class ModInfo
{ {
public static final String ID = "distanthorizons"; public static final String ID = "distanthorizons";
/** The internal protocol version used for networking */
public static final String RESOURCE_NAMESPACE = "distant_horizons"; public static final int PROTOCOL_VERSION = 1;
public static final String DEDICATED_SERVER_INITIAL_PATH = "dedicated_server_initial"; /** The protocol version used for multiverse networking */
public static final int MULTIVERSE_PLUGIN_PROTOCOL_VERSION = 1;
// region Protocol versions
// Incremented every time any packets are added, changed or removed, with a few exceptions.
public static final int PROTOCOL_VERSION = 3;
public static final String WRAPPER_PACKET_PATH = "message";
// endregion
/** The internal mod name */ /** The internal mod name */
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.2.2-a-dev"; public static final String VERSION = "2.1.3-a-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 boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev"); public static boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev");
@@ -50,9 +43,12 @@ public final class ModInfo
/** This version should be updated whenever new methods are added to the DH API */ /** This version should be updated whenever new methods are added to the DH API */
public static final int API_MINOR_VERSION = 0; public static final int API_MINOR_VERSION = 0;
/** This version should be updated whenever non-breaking fixes are added to the DH API */ /** This version should be updated whenever non-breaking fixes are added to the DH API */
public static final int API_PATCH_VERSION = 1; public static final int API_PATH_VERSION = 0;
public static final String NETWORKING_RESOURCE_NAMESPACE = "distant_horizons";
public static final String MULTIVERSE_PLUGIN_NAMESPACE = "world_control";
/** All DH owned threads should start with this string to allow for easier debugging and profiling. */ /** All DH owned threads should start with this string to allow for easier debugging and profiling. */
public static final String THREAD_NAME_PREFIX = "DH-"; public static final String THREAD_NAME_PREFIX = "DH-";
} }
@@ -67,11 +67,10 @@ public interface IConfigEntry<T>
* Checks if the option is valid * Checks if the option is valid
* *
* 0 == valid * 0 == valid
* 2 == invalid
* 1 == number too high * 1 == number too high
* -1 == number too low * -1 == number too low
*/ */
byte isValid(); // TODO replace with an enum byte isValid();
/** Checks if a value is valid */ /** Checks if a value is valid */
byte isValid(T value); byte isValid(T value);
@@ -19,17 +19,18 @@
package com.seibel.distanthorizons.coreapi.util; package com.seibel.distanthorizons.coreapi.util;
import java.util.ArrayList; import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.Arrays; import java.util.Arrays;
/** /**
* Miscellaneous string helper functions. * Miscellaneous string helper functions.
*
* @author James Seibel
* @version 2022-7-19
*/ */
public class StringUtil public class StringUtil
{ {
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
/** /**
* Returns the n-th index of the given string. <br> <br> * Returns the n-th index of the given string. <br> <br>
* *
@@ -67,6 +68,8 @@ public class StringUtil
return stringBuilder.toString(); return stringBuilder.toString();
} }
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
/** /**
* Converts the given byte array into a hex string representation. <br> * Converts the given byte array into a hex string representation. <br>
* source: https://stackoverflow.com/a/9855338 * source: https://stackoverflow.com/a/9855338
@@ -84,19 +87,24 @@ public class StringUtil
} }
/** /**
* Returns a shortened version of the given string that is no longer than maxLength. <br> * Source:
* If null returns the empty string. * https://stackoverflow.com/questions/3758606/how-can-i-convert-byte-size-into-a-human-readable-format-in-java#3758880
*/ */
public static String shortenString(String str, int maxLength) public static String convertByteCountToHumanReadableSI(long bytes)
{ {
if (str == null) if (-1000 < bytes && bytes < 1000)
{ {
return ""; return bytes + " B";
} }
else
CharacterIterator ci = new StringCharacterIterator("kMGTPE");
while (bytes <= -999_950 || bytes >= 999_950)
{ {
return str.substring(0, Math.min(str.length(), maxLength)); bytes /= 1000;
ci.next();
} }
return String.format("%.1f %cB", bytes / 1000.0, ci.current());
} }
} }
@@ -23,7 +23,6 @@ import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericRenderObjectFactory; import com.seibel.distanthorizons.core.render.renderer.generic.GenericRenderObjectFactory;
import com.seibel.distanthorizons.core.sql.DatabaseUpdater; import com.seibel.distanthorizons.core.sql.DatabaseUpdater;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.core.world.DhApiWorldProxy; import com.seibel.distanthorizons.core.world.DhApiWorldProxy;
import com.seibel.distanthorizons.core.api.external.methods.config.DhApiConfig; import com.seibel.distanthorizons.core.api.external.methods.config.DhApiConfig;
@@ -78,18 +77,16 @@ public class Initializer
throw new RuntimeException(e); throw new RuntimeException(e);
} }
if (SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class) != null)
// attempt to setup Swing so we can display dialogs (popup windows)
System.setProperty("java.awt.headless", "false");
if (GraphicsEnvironment.isHeadless())
{ {
// attempt to setup Swing so we can display dialogs (popup windows) LOGGER.warn("Java.awt.headless is false. This means Distant Horizons can't display error and info dialog windows.");
System.setProperty("java.awt.headless", "false"); }
if (GraphicsEnvironment.isHeadless()) else
{ {
LOGGER.warn("Java.awt.headless is false. This means Distant Horizons can't display error and info dialog windows."); LOGGER.info("Java.awt.headless set to true. Distant Horizons can correctly display error and info dialog windows.");
}
else
{
LOGGER.info("Java.awt.headless set to true. Distant Horizons can correctly display error and info dialog windows.");
}
} }
// link Core's config to the API // link Core's config to the API
@@ -106,4 +103,4 @@ public class Initializer
} }
} }
@@ -42,6 +42,8 @@ public class DhApiConfig implements IDhApiConfig
@Override @Override
public IDhApiMultiThreadingConfig multiThreading() { return DhApiMultiThreadingConfig.INSTANCE; } public IDhApiMultiThreadingConfig multiThreading() { return DhApiMultiThreadingConfig.INSTANCE; }
@Override @Override
public IDhApiGpuBuffersConfig gpuBuffers() { return DhApiGpuBuffersConfig.INSTANCE; }
@Override
public IDhApiDebuggingConfig debugging() { return DhApiDebuggingConfig.INSTANCE; } public IDhApiDebuggingConfig debugging() { return DhApiDebuggingConfig.INSTANCE; }
} }
@@ -0,0 +1,39 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.api.external.methods.config.client;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiGpuBuffersConfig;
import com.seibel.distanthorizons.api.objects.config.DhApiConfigValue;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod;
public class DhApiGpuBuffersConfig implements IDhApiGpuBuffersConfig
{
public static DhApiGpuBuffersConfig INSTANCE = new DhApiGpuBuffersConfig();
private DhApiGpuBuffersConfig() { }
public IDhApiConfigValue<EDhApiGpuUploadMethod> gpuUploadMethod()
{ return new DhApiConfigValue<>(Config.Client.Advanced.GpuBuffers.gpuUploadMethod); }
}
@@ -326,10 +326,9 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
int height = FullDataPointUtil.getHeight(dataPoint); int height = FullDataPointUtil.getHeight(dataPoint);
int topY = bottomY + height; int topY = bottomY + height;
return DhApiTerrainDataPoint.create( return new DhApiTerrainDataPoint(detailLevel,
detailLevel,
FullDataPointUtil.getBlockLight(dataPoint), FullDataPointUtil.getSkyLight(dataPoint), FullDataPointUtil.getBlockLight(dataPoint), FullDataPointUtil.getSkyLight(dataPoint),
bottomY, topY, topY, bottomY,
blockState, biomeWrapper); blockState, biomeWrapper);
} }
@@ -23,21 +23,18 @@ import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.*; import com.seibel.distanthorizons.api.methods.events.abstractEvents.*;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure; import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.render.DhApiRenderProxy; import com.seibel.distanthorizons.core.render.DhApiRenderProxy;
import com.seibel.distanthorizons.core.util.TimerUtil;
import com.seibel.distanthorizons.core.util.objects.Pair; import com.seibel.distanthorizons.core.util.objects.Pair;
import com.seibel.distanthorizons.core.world.*;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage;
import com.seibel.distanthorizons.core.network.session.Session;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering; import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.logging.ConfigBasedSpamLogger; import com.seibel.distanthorizons.core.logging.ConfigBasedSpamLogger;
import com.seibel.distanthorizons.core.logging.SpamReducedLogger; import com.seibel.distanthorizons.core.logging.SpamReducedLogger;
@@ -45,21 +42,20 @@ import com.seibel.distanthorizons.core.util.math.Mat4f;
import com.seibel.distanthorizons.core.render.glObject.GLProxy; import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.render.renderer.TestRenderer; import com.seibel.distanthorizons.core.render.renderer.TestRenderer;
import com.seibel.distanthorizons.core.util.RenderUtil; import com.seibel.distanthorizons.core.util.RenderUtil;
import com.seibel.distanthorizons.core.world.AbstractDhWorld;
import com.seibel.distanthorizons.core.world.DhClientServerWorld;
import com.seibel.distanthorizons.core.world.DhClientWorld;
import com.seibel.distanthorizons.core.world.IDhClientWorld;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
//import io.netty.buffer.ByteBuf;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import java.io.File; import java.util.HashMap;
import java.util.*; import java.util.HashSet;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -78,6 +74,8 @@ public class ClientApi
public static final TestRenderer TEST_RENDERER = new TestRenderer(); public static final TestRenderer TEST_RENDERER = new TestRenderer();
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
private static final IKeyedClientLevelManager KEYED_CLIENT_LEVEL_MANAGER = SingletonInjector.INSTANCE.get(IKeyedClientLevelManager.class);
public static final long SPAM_LOGGER_FLUSH_NS = TimeUnit.NANOSECONDS.convert(1, TimeUnit.SECONDS); public static final long SPAM_LOGGER_FLUSH_NS = TimeUnit.NANOSECONDS.convert(1, TimeUnit.SECONDS);
@@ -90,11 +88,10 @@ public class ClientApi
private long lastFlushNanoTime = 0; private long lastFlushNanoTime = 0;
private final ClientPluginChannelApi pluginChannelApi = new ClientPluginChannelApi(this::clientLevelLoadEvent, this::clientLevelUnloadEvent); private boolean isServerCommunicationEnabled = false;
// Delay loading the first level to give server some time to respond with level to actually load /** set to true if any unexpected responses are received from the server */
private Timer firstLevelLoadTimer; private boolean serverNetworkingIsMalformed = false;
private static final long FIRST_LEVEL_LOAD_DELAY = 1000;
/** Holds any levels that were loaded before the {@link ClientApi#onClientOnlyConnected} was fired. */ /** Holds any levels that were loaded before the {@link ClientApi#onClientOnlyConnected} was fired. */
public final HashSet<IClientLevelWrapper> waitingClientLevels = new HashSet<>(); public final HashSet<IClientLevelWrapper> waitingClientLevels = new HashSet<>();
@@ -128,36 +125,14 @@ public class ClientApi
public synchronized void onClientOnlyConnected() public synchronized void onClientOnlyConnected()
{ {
// only continue if the client is connected to a different server // only continue if the client is connected to a different server
boolean connectedToServer = MC.clientConnectedToDedicatedServer(); if (MC.clientConnectedToDedicatedServer())
boolean connectedToReplay = MC.connectedToReplay();
if (connectedToServer || connectedToReplay)
{ {
if (connectedToServer) LOGGER.info("Client on ClientOnly mode connecting.");
{
LOGGER.info("Client on ClientOnly mode connecting.");
}
else
{
LOGGER.info("Replay on ClientServer mode connecting.");
if (Config.Client.Advanced.Logging.showReplayWarningOnStartup.get())
{
MC.sendChatMessage("\u00A76" + "Distant Horizons: Replay detected." + "\u00A7r"); // gold color
MC.sendChatMessage("DH may behave strangely or have missing functionality.");
MC.sendChatMessage("In order to use pre-generated LODs, put your DH database(s) in:");
MC.sendChatMessage("\u00A77"+".Minecraft" + File.separator + ClientOnlySaveStructure.SERVER_DATA_FOLDER_NAME + File.separator + ClientOnlySaveStructure.REPLAY_SERVER_FOLDER_NAME + File.separator + "DIMENSION_NAME"+"\u00A7r"); // light gray color
MC.sendChatMessage("This can be disabled in DH's config under Advanced -> Logging.");
MC.sendChatMessage("");
}
}
// firing after clientLevelLoadEvent // firing after clientLevelLoadEvent
// TODO if level has prepped to load it should fire level load event // TODO if level has prepped to load it should fire level load event
DhClientWorld world = new DhClientWorld(); SharedApi.setDhWorld(new DhClientWorld());
SharedApi.setDhWorld(world);
this.pluginChannelApi.onJoin(world.networkState.getSession());
world.networkState.sendConfigMessage();
LOGGER.info("Loading [" + this.waitingClientLevels.size() + "] waiting client level wrappers."); LOGGER.info("Loading [" + this.waitingClientLevels.size() + "] waiting client level wrappers.");
for (IClientLevelWrapper level : this.waitingClientLevels) for (IClientLevelWrapper level : this.waitingClientLevels)
@@ -172,12 +147,6 @@ public class ClientApi
/** Synchronized to prevent a rare issue where multiple disconnect events are triggered on top of each other. */ /** Synchronized to prevent a rare issue where multiple disconnect events are triggered on top of each other. */
public synchronized void onClientOnlyDisconnected() public synchronized void onClientOnlyDisconnected()
{ {
if (this.firstLevelLoadTimer != null)
{
this.firstLevelLoadTimer.cancel();
this.firstLevelLoadTimer = null;
}
AbstractDhWorld world = SharedApi.getAbstractDhWorld(); AbstractDhWorld world = SharedApi.getAbstractDhWorld();
if (world != null) if (world != null)
{ {
@@ -187,7 +156,11 @@ public class ClientApi
SharedApi.setDhWorld(null); SharedApi.setDhWorld(null);
} }
this.pluginChannelApi.reset(); // clear the previous server's information
this.isServerCommunicationEnabled = false;
this.serverNetworkingIsMalformed = false;
KEYED_CLIENT_LEVEL_MANAGER.setUseOverrideWrapper(false);
KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(null);
// remove any waiting items // remove any waiting items
this.waitingChunkByClientLevelAndPos.clear(); this.waitingChunkByClientLevelAndPos.clear();
@@ -200,21 +173,16 @@ public class ClientApi
// level events // // level events //
//==============// //==============//
public void clientLevelUnloadEvent(IClientLevelWrapper level) public void clientLevelUnloadEvent(@Nullable IClientLevelWrapper level)
{
this.clientLevelUnloadEvent(level, false);
}
public void clientLevelUnloadEvent(IClientLevelWrapper level, boolean respawn)
{ {
try try
{ {
LOGGER.info("Unloading client level [" + level + "]-["+level.getDimensionName()+"]."); if (level == null)
if (level instanceof IServerKeyedClientLevel && !respawn)
{ {
this.pluginChannelApi.onClientLevelUnload(); // can happen on certain multiverse servers
return;
} }
LOGGER.info("Unloading client level [" + level + "]-["+level.getDimensionType().getDimensionName()+"].");
AbstractDhWorld world = SharedApi.getAbstractDhWorld(); AbstractDhWorld world = SharedApi.getAbstractDhWorld();
if (world != null) if (world != null)
@@ -234,42 +202,30 @@ public class ClientApi
} }
} }
public void clientLevelLoadEvent(IClientLevelWrapper level) public void clientLevelLoadEvent(@Nullable IClientLevelWrapper level) { this.clientLevelLoadEvent(level, false); }
public void multiverseClientLevelLoadEvent(@Nullable IClientLevelWrapper level) { this.clientLevelLoadEvent(level, true); }
private void clientLevelLoadEvent(@Nullable IClientLevelWrapper level, boolean isServerCommunication)
{ {
if (MC.clientConnectedToDedicatedServer())
{
if (this.firstLevelLoadTimer == null)
{
this.firstLevelLoadTimer = TimerUtil.CreateTimer("FirstLevelLoadTimer");
this.firstLevelLoadTimer.schedule(new TimerTask()
{
@Override
public void run()
{
ClientApi.this.clientLevelLoadEvent(level);
}
}, FIRST_LEVEL_LOAD_DELAY);
return;
}
this.firstLevelLoadTimer.cancel();
}
try try
{ {
LOGGER.info("Loading client level [" + level + "]-["+level.getDimensionName()+"]."); if (this.isServerCommunicationEnabled && !isServerCommunication)
{
LOGGER.info("Server supports communication, deferring loading.");
return;
}
if (level == null)
{
// can happen on certain multiverse servers
return;
}
LOGGER.info("Loading " + (isServerCommunication ? "Multiverse" : "") + " client level [" + level + "]-["+level.getDimensionType().getDimensionName()+"].");
AbstractDhWorld world = SharedApi.getAbstractDhWorld(); AbstractDhWorld world = SharedApi.getAbstractDhWorld();
if (world != null) if (world != null)
{ {
if (!this.pluginChannelApi.allowLevelLoading(level))
{
LOGGER.info("Levels in this connection are managed by the server, skipping auto-load.");
// Instead of attempting to load themselves, send config and wait for level key.
((DhClientWorld) world).networkState.sendConfigMessage();
return;
}
world.getOrLoadLevel(level); world.getOrLoadLevel(level);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level)); ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level));
@@ -286,7 +242,6 @@ public class ClientApi
LOGGER.error("Unexpected error in ClientApi.clientLevelLoadEvent(), error: "+e.getMessage(), e); LOGGER.error("Unexpected error in ClientApi.clientLevelLoadEvent(), error: "+e.getMessage(), e);
} }
} }
private void loadWaitingChunksForLevel(IClientLevelWrapper level) private void loadWaitingChunksForLevel(IClientLevelWrapper level)
{ {
HashSet<Pair<IClientLevelWrapper, DhChunkPos>> keysToRemove = new HashSet<>(); HashSet<Pair<IClientLevelWrapper, DhChunkPos>> keysToRemove = new HashSet<>();
@@ -379,15 +334,124 @@ public class ClientApi
//============// //============//
// networking // // networking //
//============// //============//
public void pluginMessageReceived(@NotNull NetworkMessage message) // /** @param byteBuf is Netty's {@link ByteBuffer} wrapper. */
{ // public void serverMessageReceived(ByteBuf byteBuf)
Session session = this.pluginChannelApi.session; // {
if (session != null) // if (!Config.Client.Advanced.Multiplayer.enableMultiverseNetworking.get())
{ // {
session.tryHandleMessage(message); // // multiverse networking disabled, ignore anything sent from the server
} // return;
} // }
//
//
//
// // either value can be set to true to debug the received byte stream
// boolean stopAndDisplayInputAsByteArray = false;
// boolean stopAndDisplayInputAsString = false;
// if (stopAndDisplayInputAsByteArray || stopAndDisplayInputAsString)
// {
// String messageString = "";
// if (stopAndDisplayInputAsByteArray)
// {
// int byteCount = byteBuf.readableBytes();
// byte[] arr = new byte[byteCount];
// StringBuilder stringBuilder = new StringBuilder("Server message received: [");
// for (int i = 0; i < byteCount; i++)
// {
// arr[i] = byteBuf.readByte();
// stringBuilder.append(arr[i]);
// }
// stringBuilder.append("]");
//
// messageString = stringBuilder.toString();
// }
// else if (stopAndDisplayInputAsString)
// {
// messageString = byteBuf.toString(StandardCharsets.UTF_8);
// }
//
// // this is logged as an error so it is easier to see in an Intellij log
// LOGGER.error(messageString);
// return;
// }
//
//
//
//
// // It is important to ensure malicious server input is ignored.
// if (this.serverNetworkingIsMalformed)
// {
// return;
// }
//
// // check that the incoming message is within the expected size
// short commandLength = byteBuf.readShort();
// if (commandLength < 1 || commandLength > 32)
// {
// LOGGER.error("Server command length ["+commandLength+"] outside the expected range of 1 to 32 (inclusive).");
// ClientApi.INSTANCE.serverNetworkingIsMalformed = true;
// return;
// }
//
// // parse the command
// String eventType;
// try
// {
// eventType = byteBuf.readCharSequence(commandLength, StandardCharsets.UTF_8).toString();
// }
// catch (Exception e)
// {
// LOGGER.error("Server sent un-parsable command. Error: "+e.getMessage());
// return;
// }
//
// switch (eventType)
// {
// case "ServerCommsEnabled":
// LOGGER.info("Server supports DH multiverse protocol.");
// ClientApi.INSTANCE.isServerCommunicationEnabled = true;
// KEYED_CLIENT_LEVEL_MANAGER.setUseOverrideWrapper(true);
// MC.executeOnRenderThread(() ->
// {
// // Unload the current world, since it may be wrong.
// // A followup WorldChanged event should be received from the server soon after this.
// LOGGER.info("Unloading current client level so the server can define the correct multiverse level.");
// this.clientLevelUnloadEvent((IClientLevelWrapper) MC.getWrappedClientWorld());
// });
// break;
//
// case "LevelChanged":
// short levelKeyLength = byteBuf.readShort();
// if (levelKeyLength < 1 || levelKeyLength > 128) // TODO 128 should be put into a constant somewhere
// {
// LOGGER.error("Server [LevelChanged] command length ["+commandLength+"] outside the expected range of 1 to 128 (inclusive).");
// this.serverNetworkingIsMalformed = true;
// return;
// }
//
// String levelKey = byteBuf.readCharSequence(levelKeyLength, StandardCharsets.UTF_8).toString();
// if (!levelKey.matches("[a-zA-Z0-9_]+"))
// {
// LOGGER.error("Server sent invalid world key name, and is being ignored.");
// this.isServerCommunicationEnabled = false;
// this.serverNetworkingIsMalformed = true;
// return;
// }
//
// LOGGER.info("Server level change event received, changing the level to ["+levelKey+"].");
// MC.executeOnRenderThread(() -> {
// if (MC.getWrappedClientWorld() != null)
// {
// this.clientLevelUnloadEvent((IClientLevelWrapper) MC.getWrappedClientWorld());
// }
// IServerKeyedClientLevel clientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel(MC.getWrappedClientWorld(), levelKey);
// KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel);
// this.multiverseClientLevelLoadEvent(clientLevel);
// });
// break;
// }
// }
@@ -412,7 +476,7 @@ public class ClientApi
{ {
// logging // // logging //
this.sendQueuedChatMessages(); this.sendChatMessagesNow();
IProfilerWrapper profiler = MC.getProfiler(); IProfilerWrapper profiler = MC.getProfiler();
profiler.pop(); // get out of "terrain" profiler.pop(); // get out of "terrain"
@@ -465,24 +529,10 @@ public class ClientApi
{ {
return; return;
} }
IDhClientLevel level = dhClientWorld.getOrLoadClientLevel(levelWrapper);
IDhClientLevel level = (IDhClientLevel) dhClientWorld.getLevel(levelWrapper);
if (level == null)
{
return;
}
if (this.rendererDisabledBecauseOfExceptions) if (this.rendererDisabledBecauseOfExceptions)
{ {
// re-enable rendering if the user toggles DH rendering
if (!Config.Client.quickEnableRendering.get())
{
LOGGER.info("DH Renderer re-enabled after exception. Some rendering issues may occur. Please reboot Minecraft if you see any rendering issues.");
this.rendererDisabledBecauseOfExceptions = false;
Config.Client.quickEnableRendering.set(true);
}
return; return;
} }
@@ -533,9 +583,9 @@ public class ClientApi
LOGGER.error("Unexpected Renderer error in render pass [" + renderPass + "]. Error: " + e.getMessage(), e); LOGGER.error("Unexpected Renderer error in render pass [" + renderPass + "]. Error: " + e.getMessage(), e);
MC.sendChatMessage("\u00A74\u00A7l\u00A7uERROR: Distant Horizons renderer has encountered an exception!"); MC.sendChatMessage("\u00A74\u00A7l\u00A7uERROR: Distant Horizons renderer has encountered an exception!");
MC.sendChatMessage("\u00A74Renderer disabled to try preventing GL state corruption."); MC.sendChatMessage("\u00A74Renderer is now disabled to prevent further issues.");
MC.sendChatMessage("\u00A74Toggle DH rendering via the config UI to re-activate DH rendering."); MC.sendChatMessage("\u00A74Please restart your game to re-enable Distant Horizons' LOD rendering.");
MC.sendChatMessage("\u00A74Error: " + e); MC.sendChatMessage("\u00A74Exception detail: " + e);
} }
finally finally
{ {
@@ -589,7 +639,7 @@ public class ClientApi
} }
} }
private void sendQueuedChatMessages() private void sendChatMessagesNow()
{ {
// dev build // dev build
if (ModInfo.IS_DEV_BUILD && !this.configOverrideReminderPrinted && MC.playerExists()) if (ModInfo.IS_DEV_BUILD && !this.configOverrideReminderPrinted && MC.playerExists())
@@ -597,12 +647,10 @@ public class ClientApi
this.configOverrideReminderPrinted = true; this.configOverrideReminderPrinted = true;
// remind the user that this is a development build // remind the user that this is a development build
String message = MC.sendChatMessage("\u00A72" + "Distant Horizons: nightly/unstable build, version: [" + ModInfo.VERSION+"]." + "\u00A7r");
// green text MC.sendChatMessage("Issues may occur with this version.");
"\u00A72" + "Distant Horizons: nightly/unstable build, version: [" + ModInfo.VERSION+"]." + "\u00A7r\n" + MC.sendChatMessage("Here be dragons!");
"Issues may occur with this version.\n" + MC.sendChatMessage("");
"Here be dragons!\n";
MC.sendChatMessage(message);
} }
// memory // memory
@@ -617,13 +665,11 @@ public class ClientApi
long maxMemoryInBytes = Runtime.getRuntime().maxMemory(); long maxMemoryInBytes = Runtime.getRuntime().maxMemory();
if (maxMemoryInBytes < minimumRecommendedMemoryInBytes) if (maxMemoryInBytes < minimumRecommendedMemoryInBytes)
{ {
String message = MC.sendChatMessage("\u00A76" + "Distant Horizons: Low memory detected." + "\u00A7r");
// orange text MC.sendChatMessage("Stuttering or low FPS may occur.");
"\u00A76" + "Distant Horizons: Low memory detected." + "\u00A7r \n" + MC.sendChatMessage("Please increase Minecraft's available memory to 4 gigabytes.");
"Stuttering or low FPS may occur. \n" + MC.sendChatMessage("This warning can be disabled in DH's config under Advanced -> Logging.");
"Please increase Minecraft's available memory to 4 gigabytes. \n" + MC.sendChatMessage("");
"This warning can be disabled in DH's config under Advanced -> Logging. \n";
MC.sendChatMessage(message);
} }
} }
@@ -646,4 +692,4 @@ public class ClientApi
*/ */
public void showChatMessageNextFrame(String chatMessage) { this.chatMessageQueueForNextFrame.add(chatMessage); } public void showChatMessageNextFrame(String chatMessage) { this.chatMessageQueueForNextFrame.add(chatMessage); }
} }
@@ -1,116 +0,0 @@
package com.seibel.distanthorizons.core.api.internal;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager;
import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.network.event.internal.CloseEvent;
import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage;
import com.seibel.distanthorizons.core.network.session.Session;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import org.apache.logging.log4j.LogManager;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.function.Consumer;
/**
* This class is used to manage the level keys.
*/
public class ClientPluginChannelApi
{
private static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Logging.logNetworkEvent.get());
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IKeyedClientLevelManager KEYED_CLIENT_LEVEL_MANAGER = SingletonInjector.INSTANCE.get(IKeyedClientLevelManager.class);
private final Consumer<IServerKeyedClientLevel> levelLoadHandler;
private final Consumer<IClientLevelWrapper> levelUnloadHandler;
@Nullable
public Session session;
public boolean allowLevelLoading(IClientLevelWrapper level)
{
return (KEYED_CLIENT_LEVEL_MANAGER.isEnabled() && level instanceof IServerKeyedClientLevel)
|| !KEYED_CLIENT_LEVEL_MANAGER.isEnabled();
}
public ClientPluginChannelApi(Consumer<IServerKeyedClientLevel> levelLoadHandler, Consumer<IClientLevelWrapper> levelUnloadHandler)
{
this.levelLoadHandler = levelLoadHandler;
this.levelUnloadHandler = levelUnloadHandler;
}
public void onJoin(@NonNull Session session)
{
Objects.requireNonNull(session);
this.session = session;
session.registerHandler(CurrentLevelKeyMessage.class, this::onCurrentLevelKeyMessage);
session.registerHandler(CloseEvent.class, this::onClose);
}
private void onCurrentLevelKeyMessage(CurrentLevelKeyMessage msg)
{
// prefix@namespace:path
// 1-50 characters in total, all parts except namespace can be omitted
if (!msg.levelKey.matches("^(?=.{1,50}$)([a-zA-Z0-9-_]+@)?[a-zA-Z0-9-_]+(:[a-zA-Z0-9-_]+)?$"))
{
throw new IllegalArgumentException("Server sent invalid level key.");
}
LOGGER.info("Server level key received: " + msg.levelKey);
MC.executeOnRenderThread(() -> {
IClientLevelWrapper clientLevel = MC.getWrappedClientLevel(true);
IServerKeyedClientLevel existingKeyedClientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel();
if (existingKeyedClientLevel != null)
{
if (!existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey))
{
LOGGER.info("Unloading previous level with key: " + existingKeyedClientLevel.getServerLevelKey());
this.levelUnloadHandler.accept(existingKeyedClientLevel);
}
else
{
LOGGER.info("Level key matches the previous level key, ignoring the message.");
}
}
else
{
LOGGER.info("Unloading non-keyed level: " + clientLevel.getDimensionName());
this.levelUnloadHandler.accept(clientLevel);
}
if (existingKeyedClientLevel == null || !existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey))
{
LOGGER.info("Loading level with key: " + msg.levelKey);
IServerKeyedClientLevel keyedLevel = KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel, msg.levelKey);
this.levelLoadHandler.accept(keyedLevel);
}
});
}
public void onClientLevelUnload()
{
KEYED_CLIENT_LEVEL_MANAGER.clearServerKeyedLevel();
}
private void onClose(CloseEvent event)
{
this.reset();
}
public void reset()
{
this.session = null;
KEYED_CLIENT_LEVEL_MANAGER.disable();
}
}
@@ -21,9 +21,13 @@ package com.seibel.distanthorizons.core.api.internal;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelLoadEvent; import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelLoadEvent;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelUnloadEvent; import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelUnloadEvent;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.world.AbstractDhWorld; import com.seibel.distanthorizons.core.world.AbstractDhWorld;
import com.seibel.distanthorizons.core.world.DhClientServerWorld; import com.seibel.distanthorizons.core.world.DhClientServerWorld;
import com.seibel.distanthorizons.core.world.DhServerWorld; import com.seibel.distanthorizons.core.world.DhServerWorld;
@@ -33,7 +37,6 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
/** /**
* This holds the methods that should be called by the host mod loader (Fabric, * This holds the methods that should be called by the host mod loader (Fabric,
@@ -151,7 +154,7 @@ public class ServerApi
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); IDhServerWorld serverWorld = SharedApi.getIDhServerWorld();
if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well
{ {
LOGGER.info("Creating state for player: " + player.getName()); LOGGER.debug("Waiting for player to connect: " + player.getUUID());
((DhServerWorld) serverWorld).addPlayer(player); ((DhServerWorld) serverWorld).addPlayer(player);
} }
} }
@@ -160,27 +163,9 @@ public class ServerApi
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); IDhServerWorld serverWorld = SharedApi.getIDhServerWorld();
if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well
{ {
LOGGER.info("Destroying state for player: " + player.getName()); LOGGER.debug("Removing player from connect wait list: " + player.getUUID());
((DhServerWorld) serverWorld).removePlayer(player); ((DhServerWorld) serverWorld).removePlayer(player);
} }
} }
public void serverPlayerLevelChangeEvent(IServerPlayerWrapper player, IServerLevelWrapper origin, IServerLevelWrapper dest)
{
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld();
if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well
{
LOGGER.info("Player changed level: " + player.getName());
((DhServerWorld) serverWorld).changePlayerLevel(player, origin, dest);
}
}
public void pluginMessageReceived(IServerPlayerWrapper player, @NotNull NetworkMessage message) }
{
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld();
if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well
{
((DhServerWorld) serverWorld).remotePlayerConnectionHandler.handlePluginMessage(player, message);
}
}
}
@@ -26,11 +26,11 @@ import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen; import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo; import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.TimerUtil; import com.seibel.distanthorizons.core.util.TimerUtil;
import com.seibel.distanthorizons.core.util.objects.Pair; import com.seibel.distanthorizons.core.util.objects.Pair;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
@@ -43,7 +43,9 @@ import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
/** Contains code and variables used by both {@link ClientApi} and {@link ServerApi} */ /** Contains code and variables used by both {@link ClientApi} and {@link ServerApi} */
public class SharedApi public class SharedApi
@@ -60,7 +62,6 @@ public class SharedApi
private static final Timer CHUNK_UPDATE_TIMER = TimerUtil.CreateTimer("ChunkUpdateTimer"); private static final Timer CHUNK_UPDATE_TIMER = TimerUtil.CreateTimer("ChunkUpdateTimer");
private static AbstractDhWorld currentWorld; private static AbstractDhWorld currentWorld;
private static int lastWorldGenTickDelta = 0; private static int lastWorldGenTickDelta = 0;
private static long lastOverloadedLogMessageMsTime = 0; private static long lastOverloadedLogMessageMsTime = 0;
@@ -84,7 +85,6 @@ public class SharedApi
public static void setDhWorld(AbstractDhWorld newWorld) public static void setDhWorld(AbstractDhWorld newWorld)
{ {
AbstractDhWorld prevWorld = currentWorld;
currentWorld = newWorld; currentWorld = newWorld;
// starting and stopping the DataRenderTransformer is necessary to prevent attempting to // starting and stopping the DataRenderTransformer is necessary to prevent attempting to
@@ -96,16 +96,12 @@ public class SharedApi
else else
{ {
ThreadPoolUtil.shutdownThreadPools(); ThreadPoolUtil.shutdownThreadPools();
DebugRenderer.clearRenderables();
if (prevWorld != null && prevWorld.environment != EWorldEnvironment.Server_Only) MC_RENDER.clearTargetFrameBuffer();
{ // shouldn't be necessary, but if we missed closing one of the connections this should make sure they're all closed
DebugRenderer.clearRenderables();
MC_RENDER.clearTargetFrameBuffer();
// shouldn't be necessary, but if we missed closing one of the connections this should make sure they're all closed
AbstractDhRepo.closeAllConnections(); AbstractDhRepo.closeAllConnections();
// needs to be closed on world shutdown to clear out un-processed chunks // needs to be closed on world shutdown to clear out un-processed chunks
UPDATING_CHUNK_POS_SET.clear(); UPDATING_CHUNK_POS_SET.clear();
}
// recommend that the garbage collector cleans up any objects from the old world and thread pools // recommend that the garbage collector cleans up any objects from the old world and thread pools
System.gc(); System.gc();
@@ -143,13 +139,23 @@ public class SharedApi
public static boolean isChunkAtBlockPosAlreadyUpdating(int blockPosX, int blockPosZ) public static boolean isChunkAtBlockPosAlreadyUpdating(int blockPosX, int blockPosZ)
{ return UPDATING_CHUNK_POS_SET.contains(new DhChunkPos(new DhBlockPos2D(blockPosX, blockPosZ))); } { return UPDATING_CHUNK_POS_SET.contains(new DhChunkPos(new DhBlockPos2D(blockPosX, blockPosZ))); }
public static boolean isChunkAtChunkPosAlreadyUpdating(int chunkPosX, int chunkPosZ)
{ return UPDATING_CHUNK_POS_SET.contains(new DhChunkPos(chunkPosX, chunkPosZ)); }
/** handles both block place and break events */ /** handles both block place and break events */
public void chunkBlockChangedEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true); } public void chunkBlockChangedEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true); }
public void chunkLoadEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, false); } public void chunkLoadEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, false); }
public void chunkUnloadEvent(IChunkWrapper chunk, ILevelWrapper level)
{
// temporarily disabled since this was originally incorrectly designated as "chunkSaveEvent"
// but didn't actually fire on chunk save
// and generally this is unnecessary and drastically reduces LOD building performance
// when traveling around the world
if (false)
{
this.applyChunkUpdate(chunk, level, false);
}
}
public void applyChunkUpdate(IChunkWrapper chunkWrapper, ILevelWrapper level, boolean updateNeighborChunks) public void applyChunkUpdate(IChunkWrapper chunkWrapper, ILevelWrapper level, boolean updateNeighborChunks)
{ {
@@ -259,7 +265,7 @@ public class SharedApi
else else
{ {
// neighboring chunk // neighboring chunk
DhChunkPos neighbourPos = new DhChunkPos(chunkWrapper.getChunkPos().getX() + xOffset, chunkWrapper.getChunkPos().getZ() + zOffset); DhChunkPos neighbourPos = new DhChunkPos(chunkWrapper.getChunkPos().x + xOffset, chunkWrapper.getChunkPos().z + zOffset);
IChunkWrapper neighbourChunk = dhLevel.getLevelWrapper().tryGetChunk(neighbourPos); IChunkWrapper neighbourChunk = dhLevel.getLevelWrapper().tryGetChunk(neighbourPos);
if (neighbourChunk != null) if (neighbourChunk != null)
{ {
@@ -276,43 +282,36 @@ public class SharedApi
} }
} }
} }
/** returning a {@link CompletableFuture} isn't necessary, but allows Intellij to properly show the full stack trace when debugging. */ private static void bakeChunkLightingAndSendToLevelAsync(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighbourChunkList, IDhLevel dhLevel)
@SuppressWarnings("UnusedReturnValue")
private static CompletableFuture<Void> bakeChunkLightingAndSendToLevelAsync(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighbourChunkList, IDhLevel dhLevel)
{ {
// lighting the chunk needs to be done on a separate thread to prevent lagging any of the event threads // lighting the chunk needs to be done on a separate thread to prevent lagging any of the event threads
ThreadPoolExecutor executor = ThreadPoolUtil.getChunkToLodBuilderExecutor(); ThreadPoolExecutor executor = ThreadPoolUtil.getLightPopulatorExecutor();
if (executor == null) if (executor == null)
{ {
return CompletableFuture.completedFuture(null); return;
} }
try try
{ {
return CompletableFuture.runAsync(() -> executor.execute(() ->
{ {
//LOGGER.trace(chunkWrapper.getChunkPos() + " " + executor.getActiveCount() + " / " + executor.getQueue().size() + " - " + executor.getCompletedTaskCount()); //LOGGER.trace(chunkWrapper.getChunkPos() + " " + executor.getActiveCount() + " / " + executor.getQueue().size() + " - " + executor.getCompletedTaskCount());
try try
{ {
boolean checkChunkHash = !Config.Client.Advanced.LodBuilding.disableUnchangedChunkCheck.get();
// check if this chunk has been converted into an LOD already // check if this chunk has been converted into an LOD already
int oldChunkHash = dhLevel.getChunkHash(chunkWrapper.getChunkPos()); // shouldn't happen on the render thread since it may take a few moments to run int oldChunkHash = dhLevel.getChunkHash(chunkWrapper.getChunkPos()); // shouldn't happen on the render thread since it may take a few moments to run
int newChunkHash = chunkWrapper.getBlockBiomeHashCode(); int newChunkHash = chunkWrapper.getBlockBiomeHashCode();
if (checkChunkHash) if (oldChunkHash == newChunkHash)
{ {
if (oldChunkHash == newChunkHash) // if the chunk hashes are the same then we don't need to bother with lighting the chunk
{ // or creating/updating the LODs
// if the chunk hashes are the same then we don't need to bother with lighting the chunk //LOGGER.info("skipping: "+chunkWrapper.getChunkPos()+" "+newChunkHash);
// or creating/updating the LODs return;
//LOGGER.info("skipping: "+chunkWrapper.getChunkPos()+" "+newChunkHash); }
return; else
} {
else //LOGGER.info("g: "+chunkWrapper.getChunkPos()+" "+newChunkHash);
{
//LOGGER.info("g: "+chunkWrapper.getChunkPos()+" "+newChunkHash);
}
} }
@@ -329,44 +328,36 @@ public class SharedApi
} }
// chunk light baking is disabled since profiling revealed it used // Save or populate the chunk wrapper's lighting
// roughly the same amount of time as generating the lighting ourselves and // this is done so we don't have to worry about MC unloading the lighting data for this chunk
// was much more likely to have issues with corrupt (all black or all bright) chunks boolean onlyUseDhLighting = Config.Client.Advanced.LodBuilding.onlyUseDhLightingEngine.get();
boolean tryUsingMcLightingEngine = false; if (!onlyUseDhLighting && chunkWrapper.isLightCorrect())
if (tryUsingMcLightingEngine)
{ {
// Save or populate the chunk wrapper's lighting try
// this is done so we don't have to worry about MC unloading the lighting data for this chunk
boolean chunkLightPopulated = false;
boolean onlyUseDhLighting = Config.Client.Advanced.LodBuilding.onlyUseDhLightingEngine.get();
if (!onlyUseDhLighting && chunkWrapper.isLightCorrect())
{ {
// If MC's lighting engine isn't thread safe this may cause the server thread to lag // If MC's lighting engine isn't thread safe this may cause the server thread to lag
chunkLightPopulated = chunkWrapper.bakeDhLightingUsingMcLightingEngine(dhLevel.getLevelWrapper()); chunkWrapper.bakeDhLightingUsingMcLightingEngine();
if (!chunkLightPopulated)
{
// clear any existing data to prevent partial or corrupt lighting
// when re-generating it
chunkWrapper.clearDhBlockLighting();
chunkWrapper.clearDhSkyLighting();
}
} }
catch (IllegalStateException e)
// something went wrong during the baking process so we have to generate the lighting ourselves
if (!chunkLightPopulated)
{ {
DhLightingEngine.INSTANCE.lightChunk(chunkWrapper, nearbyChunkList, dhLevel.hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT); LOGGER.warn("Chunk light baking error: " + e.getMessage(), e);
} }
} }
else else
{ {
DhLightingEngine.INSTANCE.lightChunk(chunkWrapper, nearbyChunkList, dhLevel.hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT); // generate the chunk's lighting, using neighboring chunks if present
DhLightingEngine.INSTANCE.lightChunk(chunkWrapper, nearbyChunkList, dhLevel.hasSkyLight() ? 15 : 0);
} }
dhLevel.updateBeaconBeamsForChunk(chunkWrapper, nearbyChunkList); // get this chunk's active beacons
dhLevel.updateChunkAsync(chunkWrapper, newChunkHash); List<BeaconBeamDTO> beaconBeamList = chunkWrapper.getAllActiveBeacons(nearbyChunkList);
dhLevel.setBeaconBeamsForChunk(chunkWrapper.getChunkPos(), beaconBeamList);
dhLevel.updateChunkAsync(chunkWrapper);
dhLevel.setChunkHash(chunkWrapper.getChunkPos(), newChunkHash);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -391,13 +382,9 @@ public class SharedApi
UPDATING_CHUNK_POS_SET.remove(chunkWrapper.getChunkPos()); UPDATING_CHUNK_POS_SET.remove(chunkWrapper.getChunkPos());
} }
} }
}, executor); });
}
catch (RejectedExecutionException ignore)
{
// the executor was shut down, it should be back up shortly and able to accept new jobs
return CompletableFuture.completedFuture(null);
} }
catch (RejectedExecutionException ignore) { /* the executor was shut down, it should be back up shortly and able to accept new jobs */ }
} }
@@ -415,4 +402,4 @@ public class SharedApi
} }
} }
@@ -20,21 +20,17 @@
package com.seibel.distanthorizons.core.config; package com.seibel.distanthorizons.core.config;
import com.google.common.base.Suppliers;
import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.api.enums.config.*; import com.seibel.distanthorizons.api.enums.config.*;
import com.seibel.distanthorizons.api.enums.config.quickOptions.*; import com.seibel.distanthorizons.api.enums.config.quickOptions.*;
import com.seibel.distanthorizons.api.enums.rendering.*; import com.seibel.distanthorizons.api.enums.rendering.*;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorMode; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorMode;
import com.seibel.distanthorizons.core.config.eventHandlers.*; import com.seibel.distanthorizons.core.config.eventHandlers.*;
import com.seibel.distanthorizons.core.config.eventHandlers.presets.*; import com.seibel.distanthorizons.core.config.eventHandlers.presets.*;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.config.types.*; import com.seibel.distanthorizons.core.config.types.*;
import com.seibel.distanthorizons.core.config.types.enums.*; import com.seibel.distanthorizons.core.config.types.enums.*;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.coreapi.util.StringUtil; import com.seibel.distanthorizons.coreapi.util.StringUtil;
@@ -44,7 +40,6 @@ import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
/** /**
@@ -56,6 +51,7 @@ import java.util.concurrent.ThreadLocalRandom;
* Otherwise, you will have issues where only some of the config entries will exist when your listener is created. * Otherwise, you will have issues where only some of the config entries will exist when your listener is created.
* *
* @author coolGi * @author coolGi
* @version 2023-7-16
*/ */
public class Config public class Config
@@ -108,8 +104,6 @@ public class Config
public static ConfigLinkedEntry quickEnableWorldGenerator = new ConfigLinkedEntry(Advanced.WorldGenerator.enableDistantGeneration); public static ConfigLinkedEntry quickEnableWorldGenerator = new ConfigLinkedEntry(Advanced.WorldGenerator.enableDistantGeneration);
public static ConfigLinkedEntry quickLodCloudRendering = new ConfigLinkedEntry(Advanced.Graphics.GenericRendering.enableCloudRendering);
public static ConfigEntry<Boolean> optionsButton = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> optionsButton = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) .setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
@@ -129,6 +123,7 @@ public class Config
public static ConfigCategory multiplayer = new ConfigCategory.Builder().set(Multiplayer.class).build(); public static ConfigCategory multiplayer = new ConfigCategory.Builder().set(Multiplayer.class).build();
public static ConfigCategory lodBuilding = new ConfigCategory.Builder().set(LodBuilding.class).build(); public static ConfigCategory lodBuilding = new ConfigCategory.Builder().set(LodBuilding.class).build();
public static ConfigCategory multiThreading = new ConfigCategory.Builder().set(MultiThreading.class).build(); public static ConfigCategory multiThreading = new ConfigCategory.Builder().set(MultiThreading.class).build();
public static ConfigCategory buffers = new ConfigCategory.Builder().set(GpuBuffers.class).build();
public static ConfigCategory autoUpdater = new ConfigCategory.Builder().set(AutoUpdater.class).build(); public static ConfigCategory autoUpdater = new ConfigCategory.Builder().set(AutoUpdater.class).build();
public static ConfigCategory logging = new ConfigCategory.Builder().set(Logging.class).build(); public static ConfigCategory logging = new ConfigCategory.Builder().set(Logging.class).build();
@@ -165,17 +160,8 @@ public class Config
.build(); .build();
public static ConfigEntry<Integer> lodChunkRenderDistanceRadius = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> lodChunkRenderDistanceRadius = new ConfigEntry.Builder<Integer>()
.setServersideShortName("renderDistanceRadius")
.setMinDefaultMax(32, 128, 4096) .setMinDefaultMax(32, 128, 4096)
.comment("" + .comment("The radius of the mod's render distance. (measured in chunks)")
"The radius of the mod's render distance. (measured in chunks)\n" +
"On server changes the distance players will receive real-time updates for, if enabled." +
"\n" +
"Note for servers:\n" +
"This setting does not prevent players from generating farther out.\n" +
"If you want to limit performance impact, change rate limits\n" +
"and thread count/runtime ratio settings instead.\n" +
"It also does not affect the visuals on clients.")
.setPerformance(EConfigEntryPerformance.HIGH) .setPerformance(EConfigEntryPerformance.HIGH)
.build(); .build();
@@ -190,7 +176,6 @@ public class Config
+ "Lowest Quality: " + EDhApiVerticalQuality.HEIGHT_MAP + "\n" + "Lowest Quality: " + EDhApiVerticalQuality.HEIGHT_MAP + "\n"
+ "Highest Quality: " + EDhApiVerticalQuality.EXTREME) + "Highest Quality: " + EDhApiVerticalQuality.EXTREME)
.setPerformance(EConfigEntryPerformance.VERY_HIGH) .setPerformance(EConfigEntryPerformance.VERY_HIGH)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<EDhApiHorizontalQuality> horizontalQuality = new ConfigEntry.Builder<EDhApiHorizontalQuality>() public static ConfigEntry<EDhApiHorizontalQuality> horizontalQuality = new ConfigEntry.Builder<EDhApiHorizontalQuality>()
@@ -212,7 +197,6 @@ public class Config
+ EDhApiTransparency.DISABLED + ": LODs will be opaque. \n" + EDhApiTransparency.DISABLED + ": LODs will be opaque. \n"
+ "") + "")
.setPerformance(EConfigEntryPerformance.MEDIUM) .setPerformance(EConfigEntryPerformance.MEDIUM)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<EDhApiBlocksToAvoid> blocksToIgnore = new ConfigEntry.Builder<EDhApiBlocksToAvoid>() public static ConfigEntry<EDhApiBlocksToAvoid> blocksToIgnore = new ConfigEntry.Builder<EDhApiBlocksToAvoid>()
@@ -224,7 +208,6 @@ public class Config
+ EDhApiBlocksToAvoid.NON_COLLIDING + ": Only represent solid blocks in the LODs (tall grass, torches, etc. won't count for a LOD's height) \n" + EDhApiBlocksToAvoid.NON_COLLIDING + ": Only represent solid blocks in the LODs (tall grass, torches, etc. won't count for a LOD's height) \n"
+ "") + "")
.setPerformance(EConfigEntryPerformance.NONE) .setPerformance(EConfigEntryPerformance.NONE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<Boolean> tintWithAvoidedBlocks = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> tintWithAvoidedBlocks = new ConfigEntry.Builder<Boolean>()
@@ -236,7 +219,6 @@ public class Config
+ "False: skipped blocks will not change color of surface below them. " + "False: skipped blocks will not change color of surface below them. "
+ "") + "")
.setPerformance(EConfigEntryPerformance.NONE) .setPerformance(EConfigEntryPerformance.NONE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
// TODO fixme // TODO fixme
@@ -602,7 +584,6 @@ public class Config
+ "0 = black \n" + "0 = black \n"
+ "1 = normal \n" + "1 = normal \n"
+ "2 = near white") + "2 = near white")
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<Double> saturationMultiplier = new ConfigEntry.Builder<Double>() // TODO: Make this a float (the ClassicConfigGUI doesnt support floats) public static ConfigEntry<Double> saturationMultiplier = new ConfigEntry.Builder<Double>() // TODO: Make this a float (the ClassicConfigGUI doesnt support floats)
@@ -613,7 +594,6 @@ public class Config
+ "0 = black and white \n" + "0 = black and white \n"
+ "1 = normal \n" + "1 = normal \n"
+ "2 = very saturated") + "2 = very saturated")
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<Boolean> enableCaveCulling = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableCaveCulling = new ConfigEntry.Builder<Boolean>()
@@ -628,15 +608,12 @@ public class Config
+ "Additional Info: Currently this cull all faces \n" + "Additional Info: Currently this cull all faces \n"
+ " with skylight value of 0 in dimensions that \n" + " with skylight value of 0 in dimensions that \n"
+ " does not have a ceiling.") + " does not have a ceiling.")
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<Integer> caveCullingHeight = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> caveCullingHeight = new ConfigEntry.Builder<Integer>()
.setMinDefaultMax(-4096, 60, 4096) .setMinDefaultMax(-4096, 40, 4096)
.comment("" .comment(""
+ "At what Y value should cave culling start? \n" + "At what Y value should cave culling start?")
+ "Lower this value if you get walls for areas with 0 light.")
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<Integer> earthCurveRatio = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> earthCurveRatio = new ConfigEntry.Builder<Integer>()
@@ -674,7 +651,6 @@ public class Config
+ EDhApiLodShading.DISABLED + ": All LOD sides will be rendered with the same brightness. \n" + EDhApiLodShading.DISABLED + ": All LOD sides will be rendered with the same brightness. \n"
+ "") + "")
.setPerformance(EConfigEntryPerformance.NONE) .setPerformance(EConfigEntryPerformance.NONE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<Boolean> disableFrustumCulling = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> disableFrustumCulling = new ConfigEntry.Builder<Boolean>()
@@ -709,18 +685,6 @@ public class Config
+ EDhApiGrassSideRendering.AS_DIRT + ": sides render entirely as dirt. \n" + EDhApiGrassSideRendering.AS_DIRT + ": sides render entirely as dirt. \n"
+ "") + "")
.setPerformance(EConfigEntryPerformance.NONE) .setPerformance(EConfigEntryPerformance.NONE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
public static ConfigEntry<Boolean> disableBeaconDistanceCulling = new ConfigEntry.Builder<Boolean>()
.set(false)
.comment(""
+ "If true all beacons near the camera won't be drawn to prevent vanilla overdraw. \n"
+ "If false all beacons will be rendered. \n"
+ "\n"
+ "Generally this should be left as false. It's main purpose is for debugging\n"
+ "beacon updating/rendering.\n"
+ "")
.build(); .build();
} }
@@ -730,15 +694,16 @@ public class Config
public static class WorldGenerator public static class WorldGenerator
{ {
public static ConfigEntry<Boolean> enableDistantGeneration = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableDistantGeneration = new ConfigEntry.Builder<Boolean>()
.setServersideShortName("enableDistantGeneration")
.set(true) .set(true)
.comment("" .comment(""
+ " Should Distant Horizons slowly generate LODs \n" + " Should Distant Horizons slowly generate LODs \n"
+ " outside the vanilla render distance?") + " outside the vanilla render distance?\n"
+ "\n"
+ " Note: when on a server, distant generation isn't supported \n"
+ " and will always be disabled.")
.build(); .build();
public static ConfigEntry<EDhApiDistantGeneratorMode> distantGeneratorMode = new ConfigEntry.Builder<EDhApiDistantGeneratorMode>() public static ConfigEntry<EDhApiDistantGeneratorMode> distantGeneratorMode = new ConfigEntry.Builder<EDhApiDistantGeneratorMode>()
.setServersideShortName("distantGeneratorMode")
.set(EDhApiDistantGeneratorMode.FEATURES) .set(EDhApiDistantGeneratorMode.FEATURES)
.comment("" .comment(""
+ "How detailed should LODs be generated outside the vanilla render distance? \n" + "How detailed should LODs be generated outside the vanilla render distance? \n"
@@ -780,7 +745,6 @@ public class Config
.build(); .build();
public static ConfigEntry<Integer> worldGenerationTimeoutLengthInSeconds = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> worldGenerationTimeoutLengthInSeconds = new ConfigEntry.Builder<Integer>()
.setServersideShortName("worldGenerationTimeout")
.setMinDefaultMax(5, 60 * 3, 60 * 10/*10 minutes*/ ) .setMinDefaultMax(5, 60 * 3, 60 * 10/*10 minutes*/ )
.comment("" .comment(""
+ "How long should a world generator thread run for before timing out? \n" + "How long should a world generator thread run for before timing out? \n"
@@ -794,7 +758,6 @@ public class Config
public static class LodBuilding public static class LodBuilding
{ {
public static ConfigEntry<Integer> minTimeBetweenChunkUpdatesInSeconds = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> minTimeBetweenChunkUpdatesInSeconds = new ConfigEntry.Builder<Integer>()
.setServersideShortName("minTimeBetweenChunkUpdates")
.setMinDefaultMax(0, 1, 60) .setMinDefaultMax(0, 1, 60)
.comment("" .comment(""
+ "Determines how long must pass between LOD chunk updates before another. \n" + "Determines how long must pass between LOD chunk updates before another. \n"
@@ -805,23 +768,8 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Boolean> disableUnchangedChunkCheck = new ConfigEntry.Builder<Boolean>()
.set(false)
.comment(""
+ "Normally DH will attempt to skip creating LODs for chunks it's already seen\n"
+ "and that haven't changed.\n"
+ "\n"
+ "However sometimes that logic incorrecly prevents LODs from being updated.\n"
+ "Disabling this check may fix issues where LODs aren't updated after\n"
+ "blocks have been changed.\n"
+ "")
.build();
/** Currently we always use the DH lighting engine because there's a high likelyhood of MC returning incorrect lighting otherwise */
@Deprecated
public static ConfigEntry<Boolean> onlyUseDhLightingEngine = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> onlyUseDhLightingEngine = new ConfigEntry.Builder<Boolean>()
.set(false) .set(false)
.setAppearance(EConfigEntryAppearance.ONLY_IN_API)
.comment("" .comment(""
+ "If false LODs will be lit by Minecraft's lighting engine when possible \n" + "If false LODs will be lit by Minecraft's lighting engine when possible \n"
+ "and fall back to the DH lighting engine only when necessary. \n" + "and fall back to the DH lighting engine only when necessary. \n"
@@ -856,7 +804,7 @@ public class Config
+ "unaffected until it needs to be re-written to the database.\n" + "unaffected until it needs to be re-written to the database.\n"
+ "\n" + "\n"
+ EDhApiDataCompressionMode.UNCOMPRESSED + " \n" + EDhApiDataCompressionMode.UNCOMPRESSED + " \n"
+ "Should only be used for testing, is worse in every way vs [" + EDhApiDataCompressionMode.LZ4 + "].\n" + "Should only be used for testing, is worse in every way vs ["+EDhApiDataCompressionMode.LZ4+"].\n"
+ "Expected Compression Ratio: 1.0\n" + "Expected Compression Ratio: 1.0\n"
+ "Estimated average DTO read speed: 1.64 milliseconds\n" + "Estimated average DTO read speed: 1.64 milliseconds\n"
+ "Estimated average DTO write speed: 12.44 milliseconds\n" + "Estimated average DTO write speed: 12.44 milliseconds\n"
@@ -884,58 +832,24 @@ public class Config
+ "unaffected until it is modified or re-loaded.\n" + "unaffected until it is modified or re-loaded.\n"
+ "\n" + "\n"
+ EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS + " \n" + EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS + " \n"
+ "Every block/biome change is recorded in the database. \n" + "Every block/biome change is recorded in the database. \n"
+ "This is what DH 2.0 and 2.0.1 all used by default and will store a lot of data. \n" + "This is what DH 2.0 and 2.0.1 all used by default and will store a lot of data. \n"
+ "Expected Compression Ratio: 1.0\n" + "Expected Compression Ratio: 1.0\n"
+ "\n" + "\n"
+ EDhApiWorldCompressionMode.VISUALLY_EQUAL + " \n" + EDhApiWorldCompressionMode.VISUALLY_EQUAL + " \n"
+ "Only visible block/biome changes are recorded in the database. \n" + "Only visible block/biome changes are recorded in the database. \n"
+ "Hidden blocks (IE ores) are ignored. \n" + "Hidden blocks (IE ores) are ignored. \n"
+ "Expected Compression Ratio: 0.7\n" + "Expected Compression Ratio: 0.7\n"
+ "") + "")
.build(); .build();
public static ConfigEntry<String> ignoredRenderBlockCsv = new ConfigEntry.Builder<String>() //public static ConfigEntry<Boolean> showMigrationChatWarning = new ConfigEntry.Builder<Boolean>()
.set("minecraft:barrier,minecraft:structure_void,minecraft:light,minecraft:tripwire") // .set(true)
.comment("" // .comment(""
+ "A comma separated list of block resource locations that won't be rendered by DH. \n" // + "Determines if a message should be displayed in the chat when LOD migration starts. \n"
+ "Note: air is always included in this list. \n" // + "")
+ "") // .build();
.build();
public static ConfigEntry<String> ignoredRenderCaveBlockCsv = new ConfigEntry.Builder<String>()
.set("minecraft:glow_lichen,minecraft:rail,minecraft:water,minecraft:lava,minecraft:bubble_column")
.comment(""
+ "A comma separated list of block resource locations that shouldn't be rendered \n"
+ "if they are in a 0 sky light underground area. \n"
+ "Note: air is always included in this list. \n"
+ "")
.build();
static
{
ignoredRenderBlockCsv.addListener(new ConfigChangeListener<String>(Config.Client.Advanced.LodBuilding.ignoredRenderBlockCsv,
(blockCsv) ->
{
IWrapperFactory wrapperFactory = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
if (wrapperFactory != null)
{
wrapperFactory.resetRendererIgnoredBlocksSet();
DhApi.Delayed.renderProxy.clearRenderDataCache();
}
}));
ignoredRenderCaveBlockCsv.addListener(new ConfigChangeListener<String>(Config.Client.Advanced.LodBuilding.ignoredRenderCaveBlockCsv,
(blockCsv) ->
{
IWrapperFactory wrapperFactory = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
if (wrapperFactory != null)
{
wrapperFactory.resetRendererIgnoredCaveBlocks();
DhApi.Delayed.renderProxy.clearRenderDataCache();
}
}));
}
} }
public static class Multiplayer public static class Multiplayer
@@ -978,7 +892,7 @@ public class Config
.build(); .build();
// not currently implemented // not currently implemented
public static ConfigEntry<Boolean> enableMultiverseNetworking = new ConfigEntry.Builder<Boolean>() private static ConfigEntry<Boolean> enableMultiverseNetworking = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.comment("" .comment(""
+ "If true Distant Horizons will attempt to communicate with the connected \n" + "If true Distant Horizons will attempt to communicate with the connected \n"
@@ -986,137 +900,59 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigCategory serverNetworking = new ConfigCategory.Builder().set(ServerNetworking.class).build(); // not currently implemented
private static ConfigEntry<Boolean> enableServerNetworking = new ConfigEntry.Builder<Boolean>()
public static class ServerNetworking .set(false)
{ .comment(""
public static ConfigUIComment generalSectionNote = new ConfigUIComment(); + "Attention: this is only for developers and hasn't been implemented.\n"
public static ConfigEntry<Boolean> enableServerNetworking = new ConfigEntry.Builder<Boolean>() + "\n"
.setServersideShortName("enableServerNetworking") + "If true Distant Horizons will attempt to communicate with the connected \n"
.set(true) + "server in order to load LODs outside your vanilla render distance. \n"
.comment("" + "\n"
+ "WARNING!\n" + "Note: This requires DH to be installed on the server in order to function. \n"
+ "Server-client networking is not yet fully implemented!\n" + "")
+ "Both the server and client must be running the server-side fork with this option enabled\n" .build();
+ "for Distant Horizons data to be transceived.\n"
+ "\n"
+ "If true, the server and client will attempt to communicate to transceive Distant Horizons data.\n"
+ "This allows for further distant generation and LOD updates on all clients.\n"
+ "\n"
+ "This should only be used on trusted servers with trusted players!\n"
+ "")
.build();
public static ConfigEntry<Boolean> sendLevelKeys = new ConfigEntry.Builder<Boolean>()
.setServersideShortName("sendLevelKeys")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.set(true)
.comment(""
+ "Makes the server send level keys for each world.\n"
+ "Disable this if you use alternative ways to send level keys.\n"
+ "")
.build();
public static ConfigEntry<String> levelKeyPrefix = new ConfigEntry.Builder<String>()
.setServersideShortName("levelKeyPrefix")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.set(
Suppliers.compose(wrapper -> !wrapper.isDedicatedServer() || wrapper.isWorldInitialized(), () -> SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class)).get()
? ""
: "server" + ThreadLocalRandom.current().nextInt(1, 1000)
)
.comment(""
+ "Prefix of the level keys sent to the clients.\n"
+ "Should be set to a unique value for each backend server behind a proxy,\n"
+ "or empty if you don't use a proxy.\n"
+ "")
.build();
public static ConfigUIComment generationSectionNote = new ConfigUIComment();
public static ConfigEntry<Integer> generationRequestRateLimit = new ConfigEntry.Builder<Integer>()
.setServersideShortName("generationRequestRateLimit")
.setMinDefaultMax(1, 20, 100)
.comment(""
+ "How many LOD generation requests per second should a client send? \n"
+ "Also limits the amount of player's requests allowed to stay in the server's queue."
+ "")
.build();
public static ConfigUIComment realTimeUpdatesSectionNote = new ConfigUIComment();
public static ConfigEntry<Boolean> enableRealTimeUpdates = new ConfigEntry.Builder<Boolean>()
.setServersideShortName("enableRealTimeUpdates")
.set(false)
.comment(""
+ "If true, the client will receive real-time LOD updates for chunks outside the client's render distance."
+ "")
.build();
public static ConfigUIComment syncOnLoginSectionNote = new ConfigUIComment();
public static ConfigEntry<Boolean> synchronizeOnLogin = new ConfigEntry.Builder<Boolean>()
.setServersideShortName("synchronizeOnLogin")
.set(false)
.comment(""
+ "If true, clients will receive updated LODs on join if any changes occurred since last join."
+ "")
.build();
public static ConfigEntry<Integer> syncOnLoginRateLimit = new ConfigEntry.Builder<Integer>()
.setServersideShortName("syncOnLoginRateLimit")
.setMinDefaultMax(1, 50, 100)
.comment(""
+ "How many LOD sync requests per second should a client send? \n"
+ "Also limits the amount of player's requests allowed to stay in the server's queue."
+ "")
.build();
}
} }
public static class MultiThreading public static class MultiThreading
{ {
public static final String THREAD_NOTE = "" public static final String THREAD_NOTE = ""
+ "Multi-threading Note:\n" + "Multi-threading Note: \n"
+ "If the total thread count in Distant Horizon's config is more threads than your CPU has cores,\n" + "If the total thread count in Distant Horizon's config is more threads than your CPU has cores, \n"
+ "CPU performance may suffer if Distant Horizons has a lot to load or generate.\n" + "CPU performance may suffer if Distant Horizons has a lot to load or generate. \n"
+ "This can be an issue when first loading into a world, when flying, and/or when generating new terrain."; + "This can be an issue when first loading into a world, when flying, and/or when generating new terrain.";
public static final String THREAD_RUN_TIME_RATIO_NOTE = "" public static final String THREAD_RUN_TIME_RATIO_NOTE = ""
+ "If this value is less than 1.0, it will be treated as a percentage\n" + "If this value is less than 1.0, it will be treated as a percentage \n"
+ "of time each thread can run before going idle.\n" + "of time each thread can run before going idle. \n"
+ "\n" + "\n"
+ "This can be used to reduce CPU usage if the thread count\n" + "This can be used to reduce CPU usage if the thread count \n"
+ "is already set to 1 for the given option, or more finely\n" + "is already set to 1 for the given option, or more finely \n"
+ "tune CPU performance."; + "tune CPU performance.";
public static final ConfigEntry<Integer> numberOfWorldGenerationThreads = new ConfigEntry.Builder<Integer>() public static final ConfigEntry<Integer> numberOfWorldGenerationThreads = new ConfigEntry.Builder<Integer>()
.setServersideShortName("numberOfWorldGenerationThreads")
.setMinDefaultMax(1, .setMinDefaultMax(1,
ThreadPresetConfigEventHandler.getWorldGenDefaultThreadCount(), ThreadPresetConfigEventHandler.getWorldGenDefaultThreadCount(),
Runtime.getRuntime().availableProcessors()) Runtime.getRuntime().availableProcessors())
.comment("" .comment(""
+ "How many threads should be used when generating LOD\n" + "How many threads should be used when generating LOD \n"
+ "chunks outside the normal render distance?\n" + "chunks outside the normal render distance? \n"
+ "\n" + "\n"
+ "If you experience stuttering when generating distant LODs,\n" + "If you experience stuttering when generating distant LODs, \n"
+ "decrease this number.\n" + "decrease this number. \n"
+ "If you want to increase LOD\n" + "If you want to increase LOD \n"
+ "generation speed, increase this number.\n" + "generation speed, increase this number. \n"
+ "\n" + "\n"
+ THREAD_NOTE) + THREAD_NOTE)
.build(); .build();
public static final ConfigEntry<Double> runTimeRatioForWorldGenerationThreads = new ConfigEntry.Builder<Double>() public static final ConfigEntry<Double> runTimeRatioForWorldGenerationThreads = new ConfigEntry.Builder<Double>()
.setServersideShortName("runTimeRatioForWorldGenerationThreads")
.setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getWorldGenDefaultRunTimeRatio(), 1.0) .setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getWorldGenDefaultRunTimeRatio(), 1.0)
.comment(THREAD_RUN_TIME_RATIO_NOTE) .comment(THREAD_RUN_TIME_RATIO_NOTE)
.build(); .build();
public static final ConfigEntry<Integer> numberOfFileHandlerThreads = new ConfigEntry.Builder<Integer>() public static final ConfigEntry<Integer> numberOfFileHandlerThreads = new ConfigEntry.Builder<Integer>()
.setServersideShortName("numberOfFileHandlerThreads")
.setMinDefaultMax(1, .setMinDefaultMax(1,
ThreadPresetConfigEventHandler.getFileHandlerDefaultThreadCount(), ThreadPresetConfigEventHandler.getFileHandlerDefaultThreadCount(),
Runtime.getRuntime().availableProcessors()) Runtime.getRuntime().availableProcessors())
@@ -1130,7 +966,6 @@ public class Config
+ THREAD_NOTE) + THREAD_NOTE)
.build(); .build();
public static final ConfigEntry<Double> runTimeRatioForFileHandlerThreads = new ConfigEntry.Builder<Double>() public static final ConfigEntry<Double> runTimeRatioForFileHandlerThreads = new ConfigEntry.Builder<Double>()
.setServersideShortName("runTimeRatioForFileHandlerThreads")
.setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getFileHandlerDefaultRunTimeRatio(), 1.0) .setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getFileHandlerDefaultRunTimeRatio(), 1.0)
.comment(THREAD_RUN_TIME_RATIO_NOTE) .comment(THREAD_RUN_TIME_RATIO_NOTE)
.build(); .build();
@@ -1161,7 +996,6 @@ public class Config
.build(); .build();
public static final ConfigEntry<Integer> numberOfLodBuilderThreads = new ConfigEntry.Builder<Integer>() public static final ConfigEntry<Integer> numberOfLodBuilderThreads = new ConfigEntry.Builder<Integer>()
.setServersideShortName("numberOfLodBuilderThreads")
.setMinDefaultMax(1, .setMinDefaultMax(1,
ThreadPresetConfigEventHandler.getLodBuilderDefaultThreadCount(), ThreadPresetConfigEventHandler.getLodBuilderDefaultThreadCount(),
Runtime.getRuntime().availableProcessors()) Runtime.getRuntime().availableProcessors())
@@ -1174,12 +1008,10 @@ public class Config
+ THREAD_NOTE) + THREAD_NOTE)
.build(); .build();
public static final ConfigEntry<Double> runTimeRatioForLodBuilderThreads = new ConfigEntry.Builder<Double>() public static final ConfigEntry<Double> runTimeRatioForLodBuilderThreads = new ConfigEntry.Builder<Double>()
.setServersideShortName("runTimeRatioForLodBuilderThreads")
.setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getLodBuilderDefaultRunTimeRatio(), 1.0) .setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getLodBuilderDefaultRunTimeRatio(), 1.0)
.comment(THREAD_RUN_TIME_RATIO_NOTE) .comment(THREAD_RUN_TIME_RATIO_NOTE)
.build(); .build();
public static final ConfigEntry<Boolean> enableLodBuilderThreadLimiting = new ConfigEntry.Builder<Boolean>() public static final ConfigEntry<Boolean> enableLodBuilderThreadLimiting = new ConfigEntry.Builder<Boolean>()
.setServersideShortName("enableLodBuilderThreadLimiting")
.set(true) .set(true)
.comment("" .comment(""
+ "Should only be disabled if deadlock occurs and LODs refuse to update. \n" + "Should only be disabled if deadlock occurs and LODs refuse to update. \n"
@@ -1189,24 +1021,39 @@ public class Config
+ "") + "")
.build(); .build();
public static final ConfigEntry<Integer> numberOfNetworkCompressionThreads = new ConfigEntry.Builder<Integer>() }
.setServersideShortName("numberOfNetworkCompressionThreads")
.setMinDefaultMax(1, public static class GpuBuffers
ThreadPresetConfigEventHandler.getNetworkCompressionDefaultThreadCount(), {
Runtime.getRuntime().availableProcessors()) public static ConfigEntry<EDhApiGpuUploadMethod> gpuUploadMethod = new ConfigEntry.Builder<EDhApiGpuUploadMethod>()
.set(EDhApiGpuUploadMethod.AUTO)
.comment("" .comment(""
+ "How many threads should be used when building LODs? \n" + "What method should be used to upload geometry to the GPU? \n"
+ "\n" + "\n"
+ "These threads run when terrain is generated, when\n" + EDhApiGpuUploadMethod.AUTO + ": Picks the best option based on the GPU you have. \n"
+ "certain graphics settings are changed, and when moving around the world. \n"
+ "\n" + "\n"
+ THREAD_NOTE) + EDhApiGpuUploadMethod.BUFFER_STORAGE + ": Default if OpenGL 4.5 is supported. \n"
.build(); + " Fast rendering, no stuttering. \n"
public static final ConfigEntry<Double> runTimeRatioForNetworkCompressionThreads = new ConfigEntry.Builder<Double>() + "\n"
.setServersideShortName("runTimeRatioForNetworkCompressionThreads") + EDhApiGpuUploadMethod.SUB_DATA + ": Backup option for NVIDIA. \n"
.setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getNetworkCompressionDefaultRunTimeRatio(), 1.0) + " Fast rendering but may stutter when uploading. \n"
.comment(THREAD_RUN_TIME_RATIO_NOTE) + "\n"
+ EDhApiGpuUploadMethod.BUFFER_MAPPING + ": Slow rendering but won't stutter when uploading. \n"
+ " Generally the best option for integrated GPUs. \n"
+ " Default option for AMD/Intel if OpenGL 4.5 isn't supported. \n"
+ " May end up storing buffers in System memory. \n"
+ " Fast rendering if in GPU memory, slow if in system memory, \n"
+ " but won't stutter when uploading. \n"
+ "\n"
+ EDhApiGpuUploadMethod.DATA + ": Fast rendering but will stutter when uploading. \n"
+ " Backup option for AMD/Intel. \n"
+ " Fast rendering but may stutter when uploading. \n"
+ "\n"
+ "If you don't see any difference when changing these settings, \n"
+ "or the world looks corrupted: restart your game."
+ "")
.build(); .build();
} }
public static class AutoUpdater public static class AutoUpdater
@@ -1240,70 +1087,63 @@ public class Config
// TODO add change all option // TODO add change all option
// TODO default to error chat and info file // TODO default to error chat and info file
public static ConfigEntry<EDhApiLoggerMode> logWorldGenEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logWorldGenEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setServersideShortName("logWorldGenEvent") .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT)
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE)
.comment("" .comment(""
+ "If enabled, the mod will log information about the world generation process. \n" + "If enabled, the mod will log information about the world generation process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logWorldGenPerformance = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logWorldGenPerformance = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setServersideShortName("logWorldGenPerformance") .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT)
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE)
.comment("" .comment(""
+ "If enabled, the mod will log performance about the world generation process. \n" + "If enabled, the mod will log performance about the world generation process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logWorldGenLoadEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logWorldGenLoadEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setServersideShortName("logWorldGenPerformance") .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT)
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE)
.comment("" .comment(""
+ "If enabled, the mod will log information about the world generation process. \n" + "If enabled, the mod will log information about the world generation process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logLodBuilderEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logLodBuilderEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setServersideShortName("logLodBuilderEvent") .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT)
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE)
.comment("" .comment(""
+ "If enabled, the mod will log information about the LOD generation process. \n" + "If enabled, the mod will log information about the LOD generation process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logRendererBufferEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logRendererBufferEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT)
.comment("" .comment(""
+ "If enabled, the mod will log information about the renderer buffer process. \n" + "If enabled, the mod will log information about the renderer buffer process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logRendererGLEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logRendererGLEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT)
.comment("" .comment(""
+ "If enabled, the mod will log information about the renderer OpenGL process. \n" + "If enabled, the mod will log information about the renderer OpenGL process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logFileReadWriteEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logFileReadWriteEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setServersideShortName("logFileReadWriteEvent") .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT)
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE)
.comment("" .comment(""
+ "If enabled, the mod will log information about file read/write operations. \n" + "If enabled, the mod will log information about file read/write operations. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logFileSubDimEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logFileSubDimEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setServersideShortName("logFileSubDimEvent") .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT)
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE)
.comment("" .comment(""
+ "If enabled, the mod will log information about file sub-dimension operations. \n" + "If enabled, the mod will log information about file sub-dimension operations. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logNetworkEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logNetworkEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setServersideShortName("logNetworkEvent") .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT)
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE)
.comment("" .comment(""
+ "If enabled, the mod will log information about network operations. \n" + "If enabled, the mod will log information about network operations. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
@@ -1317,20 +1157,6 @@ public class Config
+ "memory allocated to run DH well.") + "memory allocated to run DH well.")
.build(); .build();
public static ConfigEntry<Boolean> showReplayWarningOnStartup = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "If enabled, a chat message will be displayed when a replay is started \n"
+ "giving some basic information about how DH will function.")
.build();
public static ConfigEntry<Boolean> showModCompatibilityWarningsOnStartup = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "If enabled, a chat message will be displayed when a potentially problematic \n"
+ "mod is installed alongside DH.")
.build();
} }
public static class Debugging public static class Debugging
@@ -1417,38 +1243,22 @@ public class Config
public static ConfigEntry<Boolean> columnBuilderDebugEnable = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> columnBuilderDebugEnable = new ConfigEntry.Builder<Boolean>()
.set(false) .set(false)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI) .setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE) .addListener(DebugColumnConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<Integer> columnBuilderDebugDetailLevel = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> columnBuilderDebugDetailLevel = new ConfigEntry.Builder<Integer>()
.set((int) DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL) .set((int) DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI) .setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE) .addListener(DebugColumnConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<Integer> columnBuilderDebugXPos = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> columnBuilderDebugXPos = new ConfigEntry.Builder<Integer>()
.set(0) .set(0)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI) .setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE) .addListener(DebugColumnConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<Integer> columnBuilderDebugZPos = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> columnBuilderDebugZPos = new ConfigEntry.Builder<Integer>()
.set(0) .set(0)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI) .setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE) .addListener(DebugColumnConfigEventHandler.INSTANCE)
.build();
public static ConfigEntry<Integer> columnBuilderDebugXRow = new ConfigEntry.Builder<Integer>()
.set(-1)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
public static ConfigEntry<Integer> columnBuilderDebugZRow = new ConfigEntry.Builder<Integer>()
.set(-1)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
public static ConfigEntry<Integer> columnBuilderDebugColumnIndex = new ConfigEntry.Builder<Integer>()
.set(-1)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
@@ -1554,8 +1364,8 @@ public class Config
"Can be changed if you experience crashing when loading into a world.\n" + "Can be changed if you experience crashing when loading into a world.\n" +
"\n" + "\n" +
"Defines the OpenGL context type Distant Horizon's will create. \n" + "Defines the OpenGL context type Distant Horizon's will create. \n" +
"Generally this should be left as [" + EDhApiGlProfileMode.CORE + "] unless there is an issue with your GPU driver. \n" + "Generally this should be left as ["+ EDhApiGlProfileMode.CORE+"] unless there is an issue with your GPU driver. \n" +
"Possible values: [" + StringUtil.join("],[", EDhApiGlProfileMode.values()) + "] \n" + "Possible values: ["+ StringUtil.join("],[", EDhApiGlProfileMode.values())+"] \n" +
"") "")
.build(); .build();
public static ConfigEntry<Boolean> enableGlForwardCompatibilityMode = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableGlForwardCompatibilityMode = new ConfigEntry.Builder<Boolean>()
@@ -1634,9 +1444,9 @@ public class Config
.set(new HashMap<String, String>()) .set(new HashMap<String, String>())
.build(); .build();
public static ConfigUIButton uiButtonTest = new ConfigUIButton(() -> public static ConfigUIButton uiButtonTest = new ConfigUIButton(() ->
{ {
new Thread(() -> new Thread(() ->
{ {
if (!GraphicsEnvironment.isHeadless()) if (!GraphicsEnvironment.isHeadless())
{ {
@@ -1718,4 +1528,4 @@ public class Config
} }
} }
} }
@@ -23,9 +23,9 @@ import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.api.interfaces.render.IDhApiRenderProxy; import com.seibel.distanthorizons.api.interfaces.render.IDhApiRenderProxy;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener; import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
public class ReloadLodsConfigEventHandler implements IConfigListener public class DebugColumnConfigEventHandler implements IConfigListener
{ {
public static ReloadLodsConfigEventHandler INSTANCE = new ReloadLodsConfigEventHandler(); public static DebugColumnConfigEventHandler INSTANCE = new DebugColumnConfigEventHandler();
@Override @Override
public void onConfigValueSet() public void onConfigValueSet()
@@ -57,9 +57,7 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
public AbstractPresetConfigEventHandler() public AbstractPresetConfigEventHandler()
{ {
if (configGui != null) { configGui.addOnScreenChangeListener(() -> this.onConfigUiClosed());
configGui.addOnScreenChangeListener(this::onConfigUiClosed);
}
} }
@@ -170,7 +168,7 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
if (newPreset != currentPreset) if (newPreset != currentPreset)
{ {
this.getPresetConfigEntry().set(newPreset); this.getPresetConfigEntry().set(this.getCustomPresetEnum());
} }
} }
@@ -32,7 +32,6 @@ import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@SuppressWarnings("FieldCanBeLocal")
public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHandler<EDhApiThreadPreset> public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHandler<EDhApiThreadPreset>
{ {
public static final ThreadPresetConfigEventHandler INSTANCE = new ThreadPresetConfigEventHandler(); public static final ThreadPresetConfigEventHandler INSTANCE = new ThreadPresetConfigEventHandler();
@@ -129,28 +128,6 @@ public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHan
}}); }});
public static int getNetworkCompressionDefaultThreadCount() { return getThreadCountByPercent(0.3); }
private final ConfigEntryWithPresetOptions<EDhApiThreadPreset, Integer> networkCompressionThreadCount = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.numberOfNetworkCompressionThreads,
new HashMap<EDhApiThreadPreset, Integer>()
{{
this.put(EDhApiThreadPreset.MINIMAL_IMPACT, 1);
this.put(EDhApiThreadPreset.LOW_IMPACT, getNetworkCompressionDefaultThreadCount());
this.put(EDhApiThreadPreset.BALANCED, getThreadCountByPercent(0.4));
this.put(EDhApiThreadPreset.AGGRESSIVE, getThreadCountByPercent(0.6));
this.put(EDhApiThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, getThreadCountByPercent(0.8));
}});
public static double getNetworkCompressionDefaultRunTimeRatio() { return 0.5; }
private final ConfigEntryWithPresetOptions<EDhApiThreadPreset, Double> networkCompressionRunTime = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.runTimeRatioForNetworkCompressionThreads,
new HashMap<EDhApiThreadPreset, Double>()
{{
this.put(EDhApiThreadPreset.MINIMAL_IMPACT, 0.25);
this.put(EDhApiThreadPreset.LOW_IMPACT, getNetworkCompressionDefaultRunTimeRatio());
this.put(EDhApiThreadPreset.BALANCED, 0.75);
this.put(EDhApiThreadPreset.AGGRESSIVE, 1.0);
this.put(EDhApiThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, 1.0);
}});
//==============// //==============//
// constructors // // constructors //
@@ -172,9 +149,6 @@ public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHan
this.configList.add(this.lodBuilderThreadCount); this.configList.add(this.lodBuilderThreadCount);
this.configList.add(this.lodBuilderRunTime); this.configList.add(this.lodBuilderRunTime);
this.configList.add(this.networkCompressionThreadCount);
this.configList.add(this.networkCompressionRunTime);
for (ConfigEntryWithPresetOptions<EDhApiThreadPreset, ?> config : this.configList) for (ConfigEntryWithPresetOptions<EDhApiThreadPreset, ?> config : this.configList)
{ {
@@ -25,7 +25,6 @@ import com.seibel.distanthorizons.core.config.ConfigBase;
import com.seibel.distanthorizons.core.config.types.AbstractConfigType; import com.seibel.distanthorizons.core.config.types.AbstractConfigType;
import com.seibel.distanthorizons.core.config.types.ConfigEntry; import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -192,8 +191,6 @@ public class ConfigFileHandling
public void saveEntry(ConfigEntry<?> entry, CommentedFileConfig workConfig) public void saveEntry(ConfigEntry<?> entry, CommentedFileConfig workConfig)
{ {
if (!entry.getAppearance().showInFile) return; if (!entry.getAppearance().showInFile) return;
if (SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class).isDedicatedServer() && entry.getServersideShortName() == null)
return;
if (entry.getTrueValue() == null) if (entry.getTrueValue() == null)
throw new IllegalArgumentException("Entry [" + entry.getNameWCategory() + "] is null, this may be a problem with [" + configBase.modName + "]. Please contact the authors"); throw new IllegalArgumentException("Entry [" + entry.getNameWCategory() + "] is null, this may be a problem with [" + configBase.modName + "]. Please contact the authors");
@@ -268,9 +265,6 @@ public class ConfigFileHandling
) )
return; return;
if (SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class).isDedicatedServer() && entry.getServersideShortName() == null)
return;
String comment = entry.getComment().replaceAll("\n", "\n ").trim(); String comment = entry.getComment().replaceAll("\n", "\n ").trim();
// the new line makes it easier to read and separate configs // the new line makes it easier to read and separate configs
// the space makes sure the first word of a comment isn't directly in line with the "#" // the space makes sure the first word of a comment isn't directly in line with the "#"
@@ -397,4 +391,4 @@ public class ConfigFileHandling
} }
} }
*/ */
} }
@@ -44,7 +44,6 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
private T min; private T min;
private T max; private T max;
private final ArrayList<IConfigListener> listenerList; private final ArrayList<IConfigListener> listenerList;
private final String serversideShortName;
// API control // // API control //
/** /**
@@ -58,14 +57,13 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
/** Creates the entry */ /** Creates the entry */
private ConfigEntry(EConfigEntryAppearance appearance, T value, String comment, T min, T max, String serversideShortName, boolean allowApiOverride, EConfigEntryPerformance performance, ArrayList<IConfigListener> listenerList) private ConfigEntry(EConfigEntryAppearance appearance, T value, String comment, T min, T max, boolean allowApiOverride, EConfigEntryPerformance performance, ArrayList<IConfigListener> listenerList)
{ {
super(appearance, value); super(appearance, value);
this.comment = comment; this.comment = comment;
this.min = min; this.min = min;
this.max = max; this.max = max;
this.serversideShortName = serversideShortName;
this.allowApiOverride = allowApiOverride; this.allowApiOverride = allowApiOverride;
this.performance = performance; this.performance = performance;
this.listenerList = listenerList; this.listenerList = listenerList;
@@ -181,8 +179,6 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
if (validness == 1) this.value = (T) NumberUtil.getMaximum(this.value.getClass()); if (validness == 1) this.value = (T) NumberUtil.getMaximum(this.value.getClass());
} }
public String getServersideShortName() { return this.serversideShortName; }
@Override @Override
public String getComment() { return this.comment; } public String getComment() { return this.comment; }
@Override @Override
@@ -260,38 +256,20 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
public byte isValid(T value, T min, T max) public byte isValid(T value, T min, T max)
{ {
if (this.configBase.disableMinMax) if (this.configBase.disableMinMax)
{
return 0; return 0;
}
else if (min == null && max == null) if (value == null || this.value == null || value.getClass() != this.value.getClass()) // If the 2 variables aren't the same type then it will be invalid
{
// no validation is needed for this field
return 0;
}
else if (value == null || this.value == null
|| value.getClass() != this.value.getClass())
{
// If the 2 variables aren't the same type then it will be invalid
return 2; return 2;
} if (Number.class.isAssignableFrom(value.getClass()))
else if (Number.class.isAssignableFrom(value.getClass())) { // Only check min max if it is a number
{
// Only check min max if it is a number
if (max != null && NumberUtil.greaterThan((Number) value, (Number) max)) if (max != null && NumberUtil.greaterThan((Number) value, (Number) max))
{
return 1; return 1;
}
if (min != null && NumberUtil.lessThan((Number) value, (Number) min)) if (min != null && NumberUtil.lessThan((Number) value, (Number) min))
{
return -1; return -1;
}
return 0; return 0;
} }
else return 0;
{
return 0;
}
} }
/** This should normally not be called since set() automatically calls this */ /** This should normally not be called since set() automatically calls this */
@@ -319,7 +297,6 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
private String tmpComment = null; private String tmpComment = null;
private T tmpMin = null; private T tmpMin = null;
private T tmpMax = null; private T tmpMax = null;
protected String tmpServersideShortName = null;
private boolean tmpUseApiOverwrite = true; private boolean tmpUseApiOverwrite = true;
private EConfigEntryPerformance tmpPerformance = EConfigEntryPerformance.DONT_SHOW; private EConfigEntryPerformance tmpPerformance = EConfigEntryPerformance.DONT_SHOW;
protected ArrayList<IConfigListener> tmpIConfigListener = new ArrayList<>(); protected ArrayList<IConfigListener> tmpIConfigListener = new ArrayList<>();
@@ -357,12 +334,6 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
return this; return this;
} }
public Builder<T> setServersideShortName(String name)
{
this.tmpServersideShortName = name;
return this;
}
public Builder<T> setUseApiOverwrite(boolean newUseApiOverwrite) public Builder<T> setUseApiOverwrite(boolean newUseApiOverwrite)
{ {
this.tmpUseApiOverwrite = newUseApiOverwrite; this.tmpUseApiOverwrite = newUseApiOverwrite;
@@ -405,7 +376,7 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
public ConfigEntry<T> build() public ConfigEntry<T> build()
{ {
return new ConfigEntry<>(this.tmpAppearance, this.tmpValue, this.tmpComment, this.tmpMin, this.tmpMax, this.tmpServersideShortName, this.tmpUseApiOverwrite, this.tmpPerformance, this.tmpIConfigListener); return new ConfigEntry<>(this.tmpAppearance, this.tmpValue, this.tmpComment, this.tmpMin, this.tmpMax, this.tmpUseApiOverwrite, this.tmpPerformance, this.tmpIConfigListener);
} }
} }
@@ -44,7 +44,7 @@ import java.util.Arrays;
/** /**
* This data source contains every datapoint over its given {@link DhSectionPos}. <br><br> * This data source contains every datapoint over its given {@link DhSectionPos}. <br><br>
* *
* @see FullDataPointUtil * @see FullDataPointUtil
* @see FullDataSourceV1 * @see FullDataSourceV1
*/ */
@@ -53,17 +53,15 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
/** useful for debugging, but can slow down update operations quite a bit due to being called so often. */ /** useful for debugging, but can slow down update operations quite a bit due to being called so often. */
private static final boolean RUN_UPDATE_DEV_VALIDATION = false; private static final boolean RUN_UPDATE_DEV_VALIDATION = false;
/** /**
* If the data column order isn't correct * If the data column order isn't correct
* block lighting may appear broken * block lighting may appear broken
* and/or certain detail level LODs may not appear at all. * and/or certain detail level LODs may not appear at all.
*/ */
private static final boolean RUN_DATA_ORDER_VALIDATION = ModInfo.IS_DEV_BUILD; private static final boolean RUN_DATA_ORDER_VALIDATION = ModInfo.IS_DEV_BUILD;
/** measured in data columns */ /** measured in data columns */
public static final int WIDTH = 64; public static final int WIDTH = 64;
/** how many chunks wide this datasource is. */
public static final int NUMB_OF_CHUNKS_WIDE = WIDTH / LodUtil.CHUNK_WIDTH;
public static final byte DATA_FORMAT_VERSION = 1; public static final byte DATA_FORMAT_VERSION = 1;
@@ -88,20 +86,18 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
public int levelMinY; public int levelMinY;
/** /**
* stores how far each column has been generated should start with {@link EDhApiWorldGenerationStep#EMPTY} * stores how far each column has been generated should start with {@link EDhApiWorldGenerationStep#EMPTY}
* * @see EDhApiWorldGenerationStep
* @see EDhApiWorldGenerationStep
*/ */
public byte[] columnGenerationSteps; public byte[] columnGenerationSteps;
/** /**
* stores what world compression was used for each column. * stores what world compression was used for each column.
* * @see EDhApiWorldCompressionMode
* @see EDhApiWorldCompressionMode
*/ */
public byte[] columnWorldCompressionMode; public byte[] columnWorldCompressionMode;
/** /**
* stored x/z, y <br> * stored x/z, y <br>
* The y data should be sorted from top to bottom <br> * The y data should be sorted from top to bottom <br>
* TODO that ordering feels weird, it'd be nice to reverse that order, unfortunately * TODO that ordering feels weird, it'd be nice to reverse that order, unfortunately
@@ -153,9 +149,9 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
if (FullDataSourceV1.WIDTH != WIDTH) if (FullDataSourceV1.WIDTH != WIDTH)
{ {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
"Unable to convert [" + FullDataSourceV1.class.getSimpleName() + "] into [" + FullDataSourceV2.class.getSimpleName() + "]. " + "Unable to convert ["+FullDataSourceV1.class.getSimpleName()+"] into ["+FullDataSourceV2.class.getSimpleName()+"]. " +
"Data sources have different data point widths and no converter is present. " + "Data sources have different data point widths and no converter is present. " +
"input width [" + FullDataSourceV1.WIDTH + "], recipient width [" + WIDTH + "]."); "input width ["+ FullDataSourceV1.WIDTH+"], recipient width ["+WIDTH+"].");
} }
@@ -182,7 +178,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
boolean isAir = legacyData.mapping.getBlockStateWrapper(FullDataPointUtil.getId(dataPoint)).isAir(); boolean isAir = legacyData.mapping.getBlockStateWrapper(FullDataPointUtil.getId(dataPoint)).isAir();
byte blockLight = (byte) FullDataPointUtil.getBlockLight(dataPoint); byte blockLight = (byte) FullDataPointUtil.getBlockLight(dataPoint);
if (isAir) if (isAir)
{ {
// air shouldn't have any light, otherwise down sampling will look weird // air shouldn't have any light, otherwise down sampling will look weird
@@ -258,7 +254,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
// and would lead to edge cases that don't necessarily need to be supported // and would lead to edge cases that don't necessarily need to be supported
// (IE what do you do when the input is smaller than a single datapoint in the receiving data source?) // (IE what do you do when the input is smaller than a single datapoint in the receiving data source?)
// instead it's better to just percolate the updates up // instead it's better to just percolate the updates up
throw new UnsupportedOperationException("Unsupported data source update. Expected input detail level of [" + thisDetailLevel + "] or [" + (thisDetailLevel + 1) + "], received detail level [" + inputDetailLevel + "]."); throw new UnsupportedOperationException("Unsupported data source update. Expected input detail level of ["+thisDetailLevel+"] or ["+(thisDetailLevel+1)+"], received detail level ["+inputDetailLevel+"].");
} }
// determine if this data source should be applied to its parent // determine if this data source should be applied to its parent
@@ -296,7 +292,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
byte inputGenState = inputDataSource.columnGenerationSteps[index]; byte inputGenState = inputDataSource.columnGenerationSteps[index];
if (inputGenState != EDhApiWorldGenerationStep.EMPTY.value if (inputGenState != EDhApiWorldGenerationStep.EMPTY.value
&& thisGenState <= inputGenState) && thisGenState <= inputGenState)
{ {
// check if the data changed // check if the data changed
if (this.dataPoints[index] == null) if (this.dataPoints[index] == null)
@@ -381,7 +377,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
for (int x = 0; x < WIDTH; x += 2) for (int x = 0; x < WIDTH; x += 2)
{ {
for (int z = 0; z < WIDTH; z += 2) for (int z = 0; z < WIDTH; z += 2)
{ {
int recipientX = (x / 2) + recipientOffsetX; int recipientX = (x / 2) + recipientOffsetX;
int recipientZ = (z / 2) + recipientOffsetZ; int recipientZ = (z / 2) + recipientOffsetZ;
int recipientIndex = relativePosToIndex(recipientX, recipientZ); int recipientIndex = relativePosToIndex(recipientX, recipientZ);
@@ -449,7 +445,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
return dataChanged; return dataChanged;
} }
/** /**
* The minimum value is used because we don't want to accidentally record that * The minimum value is used because we don't want to accidentally record that
* something was generated when it wasn't. * something was generated when it wasn't.
*/ */
@@ -468,7 +464,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
} }
return minWorldGenStepValue; return minWorldGenStepValue;
} }
/** /**
* The minimum value is used because we don't want to accidentally record that * The minimum value is used because we don't want to accidentally record that
* something was generated when it wasn't. * something was generated when it wasn't.
*/ */
@@ -494,7 +490,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
// special numbers: // special numbers:
// -2 = the column's height hasn't been determined yet // -2 = the column's height hasn't been determined yet
// -1 = we've reached the end of the column // -1 = we've reached the end of the column
int[] currentDatapointIndex = new int[]{-2, -2, -2, -2}; int[] currentDatapointIndex = new int[] { -2, -2, -2, -2 };
int lastId = 0; int lastId = 0;
byte lastBlockLight = 0; byte lastBlockLight = 0;
@@ -514,10 +510,10 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
{ {
// if each column has reached the end of their data, nothing more needs to be done // if each column has reached the end of their data, nothing more needs to be done
if (currentDatapointIndex[0] == -1 if (currentDatapointIndex[0] == -1
&& currentDatapointIndex[1] == -1 && currentDatapointIndex[1] == -1
&& currentDatapointIndex[2] == -1 && currentDatapointIndex[2] == -1
&& currentDatapointIndex[3] == -1 && currentDatapointIndex[3] == -1
) )
{ {
break; break;
} }
@@ -614,12 +610,12 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
int id = determineMostValueInColumnSlice(mergeIds, inputDataSource.mapping); int id = determineMostValueInColumnSlice(mergeIds, inputDataSource.mapping);
byte blockLight = (byte) determineAverageValueInColumnSlice(mergeBlockLights); byte blockLight = (byte) determineAverageValueInColumnSlice(mergeBlockLights);
byte skyLight = (byte) determineAverageValueInColumnSlice(mergeSkyLights); byte skyLight = (byte) determineAverageValueInColumnSlice(mergeSkyLights);
// if this slice is different then the last one, create a new one // if this slice is different then the last one, create a new one
if (id != lastId if (id != lastId
// block and sky light might not be necessary // block and sky light might not be necessary
|| blockLight != lastBlockLight || blockLight != lastBlockLight
|| skyLight != lastSkyLight) || skyLight != lastSkyLight)
{ {
if (height != 0) if (height != 0)
{ {
@@ -667,7 +663,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
} }
/** /**
* Only update the ID once it's been added to this data source. * Only update the ID once it's been added to this data source.
* Updating the incoming data source will cause issues if it is applied * Updating the incoming data source will cause issues if it is applied
* to anything else due to multiple remapping. * to anything else due to multiple remapping.
*/ */
private void remapDataColumn(int dataPointIndex, int[] remappedIds) private void remapDataColumn(int dataPointIndex, int[] remappedIds)
@@ -700,7 +696,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
{ {
LodUtil.assertTrue(sliceArray.length == 4, "Column Slice should only contain 4 values."); LodUtil.assertTrue(sliceArray.length == 4, "Column Slice should only contain 4 values.");
} }
int value0 = sliceArray[0]; int value0 = sliceArray[0];
int count0 = 0; int count0 = 0;
int value1 = sliceArray[1]; int value1 = sliceArray[1];
@@ -708,7 +704,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
int value2 = sliceArray[2]; int value2 = sliceArray[2];
int count2 = 0; int count2 = 0;
int value3 = sliceArray[3]; int value3 = sliceArray[3];
int count3 = 0; int count3 = 0;
// count the occurrences of each value // count the occurrences of each value
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
@@ -721,42 +717,26 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
} }
if (value == value0) if (value == value0)
{
count0++; count0++;
}
else if (value == value1) else if (value == value1)
{
count1++; count1++;
}
else if (value == value2) else if (value == value2)
{
count2++; count2++;
}
else else
{
count3++; count3++;
}
} }
// return the most common occurance // return the most common occurance
int maxCount = Math.max(count0, Math.max(count1, Math.max(count2, count3))); int maxCount = Math.max(count0, Math.max(count1, Math.max(count2, count3)));
if (maxCount == count0) if (maxCount == count0)
// if the max count is 1 then we'll just go with the first column // if the max count is 1 then we'll just go with the first column
{
return value0; return value0;
}
else if (maxCount == count1) else if (maxCount == count1)
{
return value1; return value1;
}
else if (maxCount == count2) else if (maxCount == count2)
{
return value2; return value2;
} else
else
{
return value3; return value3;
}
} }
private static int determineAverageValueInColumnSlice(int[] sliceArray) private static int determineAverageValueInColumnSlice(int[] sliceArray)
{ {
@@ -782,27 +762,27 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
// helper methods // // helper methods //
//================// //================//
/** /**
* Usually this should just be used internally, but there may be instances * Usually this should just be used internally, but there may be instances
* where the raw data arrays are available without the data source object. * where the raw data arrays are available without the data source object.
*/ */
public static int relativePosToIndex(int relX, int relZ) throws IndexOutOfBoundsException public static int relativePosToIndex(int relX, int relZ) throws IndexOutOfBoundsException
{ {
if (relX < 0 || relZ < 0 || if (relX < 0 || relZ < 0 ||
relX > WIDTH || relZ > WIDTH) relX > WIDTH || relZ > WIDTH)
{ {
throw new IndexOutOfBoundsException("Relative data source positions must be between [0] and [" + WIDTH + "] (inclusive) the relative pos: [" + relX + "," + relZ + "] is outside of those boundaries."); throw new IndexOutOfBoundsException("Relative data source positions must be between [0] and ["+WIDTH+"] (inclusive) the relative pos: ["+relX+","+relZ+"] is outside of those boundaries.");
} }
return (relX * WIDTH) + relZ; return (relX * WIDTH) + relZ;
} }
/** /**
* Throws an exception if the given * Throws an exception if the given
* full data column array is in the wrong order * full data column array is in the wrong order
* IE if the first data point is the lowest and the last data point is the highest. * IE if the first data point is the lowest and the last data point is the highest.
* Data columns should be in reverse order, IE the first data point should be the highest data point. * Data columns should be in reverse order, IE the first data point should be the highest data point.
* *
* @see FullDataSourceV2#dataPoints * @see FullDataSourceV2#dataPoints
*/ */
public static void throwIfDataColumnInWrongOrder(long pos, LongArrayList dataArray) throws IllegalStateException public static void throwIfDataColumnInWrongOrder(long pos, LongArrayList dataArray) throws IllegalStateException
@@ -836,7 +816,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
// reverse the array so index 0 is the highest, // reverse the array so index 0 is the highest,
// this is necessary for later logic // this is necessary for later logic
// source: https://stackoverflow.com/questions/2137755/how-do-i-reverse-an-int-array-in-java // source: https://stackoverflow.com/questions/2137755/how-do-i-reverse-an-int-array-in-java
for (int i = 0; i < dataColumn.size() / 2; i++) for(int i = 0; i < dataColumn.size() / 2; i++)
{ {
long temp = dataColumn.getLong(i); long temp = dataColumn.getLong(i);
dataColumn.set(i, dataColumn.getLong(dataColumn.size() - i - 1)); dataColumn.set(i, dataColumn.getLong(dataColumn.size() - i - 1));
@@ -884,18 +864,18 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
@Override @Override
public byte getDataDetailLevel() { return (byte) (DhSectionPos.getDetailLevel(this.pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); } public byte getDataDetailLevel() { return (byte) (DhSectionPos.getDetailLevel(this.pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); }
public EDhApiWorldGenerationStep getWorldGenStepAtRelativePos(int relX, int relZ) public EDhApiWorldGenerationStep getWorldGenStepAtRelativePos(int relX, int relZ)
{ {
int index = relativePosToIndex(relX, relZ); int index = relativePosToIndex(relX, relZ);
return EDhApiWorldGenerationStep.fromValue(this.columnGenerationSteps[index]); return EDhApiWorldGenerationStep.fromValue(this.columnGenerationSteps[index]);
} }
public void setSingleColumn(LongArrayList longArray, int relX, int relZ, EDhApiWorldGenerationStep worldGenStep, EDhApiWorldCompressionMode worldCompressionMode) public void setSingleColumn(LongArrayList longArray, int relX, int relZ, EDhApiWorldGenerationStep worldGenStep, EDhApiWorldCompressionMode worldCompressionMode)
{ {
int index = relativePosToIndex(relX, relZ); int index = relativePosToIndex(relX, relZ);
this.dataPoints[index] = longArray; this.dataPoints[index] = longArray;
this.columnGenerationSteps[index] = worldGenStep.value; this.columnGenerationSteps[index] = worldGenStep.value;
this.columnWorldCompressionMode[index] = worldCompressionMode.value; this.columnWorldCompressionMode[index] = worldCompressionMode.value;
if (RUN_UPDATE_DEV_VALIDATION) if (RUN_UPDATE_DEV_VALIDATION)
@@ -923,7 +903,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
@Override @Override
public String toString() { return DhSectionPos.toString(this.pos); } public String toString() { return DhSectionPos.toString(this.pos); }
@Override @Override
public int hashCode() public int hashCode()
{ {
if (this.cachedHashCode == 0) if (this.cachedHashCode == 0)
@@ -942,7 +922,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
this.cachedHashCode = result; this.cachedHashCode = result;
} }
@Override @Override
public boolean equals(Object obj) public boolean equals(Object obj)
{ {
if (!(obj instanceof FullDataSourceV2)) if (!(obj instanceof FullDataSourceV2))
@@ -960,7 +940,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
// the positions are the same, use the hash as a quick method // the positions are the same, use the hash as a quick method
// to determine if the data inside is the same. // to determine if the data inside is the same.
// Note: this isn't perfect, but should work well enough for our use case. // Note: this isn't perfect, but should work well enough for our use case.
return other.hashCode() == this.hashCode(); return other.hashCode() == this.hashCode();
} }
} }
@@ -970,4 +950,4 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
DATA_SOURCE_POOL.returnPooledDataSource(this); DATA_SOURCE_POOL.returnPooledDataSource(this);
} }
} }
@@ -25,7 +25,7 @@ import com.seibel.distanthorizons.core.dataObjects.transformers.FullDataToRender
import com.seibel.distanthorizons.core.file.DataSourcePool; import com.seibel.distanthorizons.core.file.DataSourcePool;
import com.seibel.distanthorizons.core.file.IDataSource; import com.seibel.distanthorizons.core.file.IDataSource;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView;
@@ -182,7 +182,7 @@ public class ColumnRenderSource implements IDataSource<IDhClientLevel>
EDhApiWorldGenerationStep worldGenStep = inputFullDataSource.getWorldGenStepAtRelativePos(x, z); EDhApiWorldGenerationStep worldGenStep = inputFullDataSource.getWorldGenStepAtRelativePos(x, z);
if (dataColumn != null && worldGenStep != EDhApiWorldGenerationStep.EMPTY) if (dataColumn != null && worldGenStep != EDhApiWorldGenerationStep.EMPTY)
{ {
FullDataToRenderDataTransformer.updateOrReplaceRenderDataViewColumnWithFullDataColumn( FullDataToRenderDataTransformer.convertColumnData(
level, inputFullDataSource.mapping, level, inputFullDataSource.mapping,
minBlockPos.x + x, minBlockPos.x + x,
minBlockPos.z + z, minBlockPos.z + z,
@@ -288,7 +288,7 @@ public class ColumnRenderSource implements IDataSource<IDhClientLevel>
String SUBDATA_DELIMITER = ","; String SUBDATA_DELIMITER = ",";
StringBuilder stringBuilder = new StringBuilder(); StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(DhSectionPos.toString(this.pos)); stringBuilder.append(this.pos);
stringBuilder.append(LINE_DELIMITER); stringBuilder.append(LINE_DELIMITER);
int size = 1; int size = 1;
@@ -19,60 +19,29 @@
package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding; package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.api.enums.rendering.EDhApiBlockMaterial;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.util.ColorUtil; import com.seibel.distanthorizons.core.util.ColorUtil;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.RenderDataPointUtil; import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView;
import com.seibel.distanthorizons.core.render.renderer.LodRenderer; import com.seibel.distanthorizons.core.render.renderer.LodRenderer;
import com.seibel.distanthorizons.coreapi.util.MathUtil; import com.seibel.distanthorizons.coreapi.util.MathUtil;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
public class ColumnBox public class ColumnBox
{ {
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
/**
* if the skylight has this value that means
* no data is expected
*/
private static final byte SKYLIGHT_EMPTY = -1;
/**
* if the skylight has this value that means
* that block position is covered/occuled by an adjacent block/column.
*/
private static final byte SKYLIGHT_COVERED = -2;
private static final ThreadLocal<byte[]> THREAD_LOCAL_SKY_LIGHT_ARRAY = ThreadLocal.withInitial(() ->
{
byte[] array = new byte[RenderDataPointUtil.MAX_WORLD_Y_SIZE];
Arrays.fill(array, SKYLIGHT_EMPTY);
return array;
});
//=========//
// builder //
//=========//
public static void addBoxQuadsToBuilder( public static void addBoxQuadsToBuilder(
LodQuadBuilder builder, IDhClientLevel clientLevel, LodQuadBuilder builder,
short xSize, short ySize, short zSize, short xSize, short ySize, short zSize,
short x, short minY, short z, short x, short minY, short z,
int color, byte irisBlockMaterialId, byte skyLight, byte blockLight, int color, byte irisBlockMaterialId, byte skyLight, byte blockLight,
long topData, long bottomData, ColumnArrayView[] adjData, boolean[] isAdjDataSameDetailLevel) long topData, long bottomData, ColumnArrayView[][] adjData)
{ {
//================//
// variable setup //
//================//
short maxX = (short) (x + xSize); short maxX = (short) (x + xSize);
short maxY = (short) (minY + ySize); short maxY = (short) (minY + ySize);
short maxZ = (short) (z + zSize); short maxZ = (short) (z + zSize);
@@ -84,24 +53,33 @@ public class ColumnBox
boolean isTopTransparent = RenderDataPointUtil.getAlpha(topData) < 255 && LodRenderer.transparencyEnabled; boolean isTopTransparent = RenderDataPointUtil.getAlpha(topData) < 255 && LodRenderer.transparencyEnabled;
boolean isBottomTransparent = RenderDataPointUtil.getAlpha(bottomData) < 255 && LodRenderer.transparencyEnabled; boolean isBottomTransparent = RenderDataPointUtil.getAlpha(bottomData) < 255 && LodRenderer.transparencyEnabled;
// defaulting to a value far below what we can normally render means we
// don't need to have an additional "is cave culling enabled" check
int caveCullingMaxY = Integer.MIN_VALUE;
if (Config.Client.Advanced.Graphics.AdvancedGraphics.enableCaveCulling.get())
{
caveCullingMaxY = Config.Client.Advanced.Graphics.AdvancedGraphics.caveCullingHeight.get() - clientLevel.getMinY();
}
// if there isn't any data below this LOD, make this LOD's color opaque to prevent seeing void through transparent blocks // if there isn't any data below this LOD, make this LOD's color opaque to prevent seeing void through transparent blocks
// Note: this LOD should still be considered transparent for this method's checks, otherwise rendering bugs may occur // Note: this LOD should still be considered transparent for this method's checks, otherwise rendering bugs may occur
// FIXME this transparency change should be applied before this point since this could affect other areas
// This may also be better than handling the LOD as transparent, but that is TBD
if (!RenderDataPointUtil.doesDataPointExist(bottomData)) if (!RenderDataPointUtil.doesDataPointExist(bottomData))
{ {
color = ColorUtil.setAlpha(color, 255); color = ColorUtil.setAlpha(color, 255);
} }
// cave culling prevention
// prevents certain faces from being culled underground that should be allowed
if (builder.skipQuadsWithZeroSkylight
&& 0 == skyLight
&& builder.skyLightCullingBelow > maxY
&& (
(RenderDataPointUtil.getAlpha(topData) < 255 && RenderDataPointUtil.getYMax(topData) >= builder.skyLightCullingBelow)
|| (RenderDataPointUtil.getYMin(topData) >= builder.skyLightCullingBelow)
|| !RenderDataPointUtil.doesDataPointExist(topData)
)
)
{
maxY = builder.skyLightCullingBelow;
}
// fake ocean transparency // fake ocean transparency
if (LodRenderer.transparencyEnabled && LodRenderer.fakeOceanFloor) if (LodRenderer.transparencyEnabled && LodRenderer.fakeOceanFloor)
{ {
@@ -121,9 +99,7 @@ public class ColumnBox
//==========================// // add top and bottom faces if requested //
// add top and bottom faces //
//==========================//
boolean skipTop = RenderDataPointUtil.doesDataPointExist(topData) && (RenderDataPointUtil.getYMin(topData) == maxY) && !isTopTransparent; boolean skipTop = RenderDataPointUtil.doesDataPointExist(topData) && (RenderDataPointUtil.getYMin(topData) == maxY) && !isTopTransparent;
if (!skipTop) if (!skipTop)
@@ -138,291 +114,409 @@ public class ColumnBox
} }
// add North, south, east, and west faces if requested //
//========================================// // TODO merge duplicate code
// add North, south, east, and west faces // //NORTH face vertex creation
//========================================//
// NORTH face
{ {
ColumnArrayView adjCol = adjData[EDhDirection.NORTH.ordinal() - 2]; // TODO can we use something other than ordinal-2? ColumnArrayView[] adjDataNorth = adjData[EDhDirection.NORTH.ordinal() - 2]; // TODO can we use something other than ordinal-2?
boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.NORTH.ordinal() - 2]; int adjOverlapNorth = ColorUtil.INVISIBLE;
// if the adjacent column is null that generally means the adjacent area hasn't been generated yet if (adjDataNorth == null)
if (adjCol == null)
{ {
// Add an adjacent face if this is opaque face or transparent over the void. // add an adjacent face if this is opaque face or transparent over the void
if (!isTransparent || overVoid) if (!isTransparent || overVoid)
{ {
builder.addQuadAdj(EDhDirection.NORTH, x, minY, z, xSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); builder.addQuadAdj(EDhDirection.NORTH, x, minY, z, xSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight);
} }
} }
else if (adjDataNorth.length == 1)
{
makeAdjVerticalQuad(builder, adjDataNorth[0], EDhDirection.NORTH, x, minY, z, xSize, ySize,
color, adjOverlapNorth, irisBlockMaterialId, skyLightTop, blockLight,
topData, bottomData);
}
else else
{ {
makeAdjVerticalQuad(builder, adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.NORTH, x, minY, z, xSize, ySize, makeAdjVerticalQuad(builder, adjDataNorth[0], EDhDirection.NORTH, x, minY, z, (short) (xSize / 2), ySize,
color, irisBlockMaterialId, blockLight); color, adjOverlapNorth, irisBlockMaterialId, skyLightTop, blockLight,
topData, bottomData);
makeAdjVerticalQuad(builder, adjDataNorth[1], EDhDirection.NORTH, (short) (x + xSize / 2), minY, z, (short) (xSize / 2), ySize,
color, adjOverlapNorth, irisBlockMaterialId, skyLightTop, blockLight,
topData, bottomData);
} }
} }
// SOUTH face //SOUTH face vertex creation
{ {
ColumnArrayView adjCol = adjData[EDhDirection.SOUTH.ordinal() - 2]; ColumnArrayView[] adjDataSouth = adjData[EDhDirection.SOUTH.ordinal() - 2];
boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.SOUTH.ordinal() - 2]; int adjOverlapSouth = ColorUtil.INVISIBLE;
if (adjCol == null) if (adjDataSouth == null)
{ {
if (!isTransparent || overVoid) if (!isTransparent || overVoid)
{
builder.addQuadAdj(EDhDirection.SOUTH, x, minY, maxZ, xSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); builder.addQuadAdj(EDhDirection.SOUTH, x, minY, maxZ, xSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight);
} }
else if (adjDataSouth.length == 1)
{
makeAdjVerticalQuad(builder, adjDataSouth[0], EDhDirection.SOUTH, x, minY, maxZ, xSize, ySize,
color, adjOverlapSouth, irisBlockMaterialId, skyLightTop, blockLight,
topData, bottomData);
} }
else else
{ {
makeAdjVerticalQuad(builder, adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.SOUTH, x, minY, maxZ, xSize, ySize, makeAdjVerticalQuad(builder, adjDataSouth[0], EDhDirection.SOUTH, x, minY, maxZ, (short) (xSize / 2), ySize,
color, irisBlockMaterialId, blockLight); color, adjOverlapSouth, irisBlockMaterialId, skyLightTop, blockLight,
topData, bottomData);
makeAdjVerticalQuad(builder, adjDataSouth[1], EDhDirection.SOUTH, (short) (x + xSize / 2), minY, maxZ, (short) (xSize / 2), ySize,
color, adjOverlapSouth, irisBlockMaterialId, skyLightTop, blockLight,
topData, bottomData);
} }
} }
// WEST face //WEST face vertex creation
{ {
ColumnArrayView adjCol = adjData[EDhDirection.WEST.ordinal() - 2]; ColumnArrayView[] adjDataWest = adjData[EDhDirection.WEST.ordinal() - 2];
boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.WEST.ordinal() - 2]; int adjOverlapWest = ColorUtil.INVISIBLE;
if (adjCol == null) if (adjDataWest == null)
{ {
if (!isTransparent || overVoid) if (!isTransparent || overVoid)
{
builder.addQuadAdj(EDhDirection.WEST, x, minY, z, zSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); builder.addQuadAdj(EDhDirection.WEST, x, minY, z, zSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight);
} }
else if (adjDataWest.length == 1)
{
makeAdjVerticalQuad(builder, adjDataWest[0], EDhDirection.WEST, x, minY, z, zSize, ySize,
color, adjOverlapWest, irisBlockMaterialId, skyLightTop, blockLight,
topData, bottomData);
} }
else else
{ {
makeAdjVerticalQuad(builder, adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.WEST, x, minY, z, zSize, ySize, makeAdjVerticalQuad(builder, adjDataWest[0], EDhDirection.WEST, x, minY, z, (short) (zSize / 2), ySize,
color, irisBlockMaterialId, blockLight); color, adjOverlapWest, irisBlockMaterialId, skyLightTop, blockLight,
topData, bottomData);
makeAdjVerticalQuad(builder, adjDataWest[1], EDhDirection.WEST, x, minY, (short) (z + zSize / 2), (short) (zSize / 2), ySize,
color, adjOverlapWest, irisBlockMaterialId, skyLightTop, blockLight,
topData, bottomData);
} }
} }
// EAST face //EAST face vertex creation
{ {
ColumnArrayView adjCol = adjData[EDhDirection.EAST.ordinal() - 2]; ColumnArrayView[] adjDataEast = adjData[EDhDirection.EAST.ordinal() - 2];
boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.EAST.ordinal() - 2]; int adjOverlapEast = ColorUtil.INVISIBLE;
if (adjCol == null) if (adjData[EDhDirection.EAST.ordinal() - 2] == null)
{ {
if (!isTransparent || overVoid) if (!isTransparent || overVoid)
{
builder.addQuadAdj(EDhDirection.EAST, maxX, minY, z, zSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); builder.addQuadAdj(EDhDirection.EAST, maxX, minY, z, zSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight);
} }
else if (adjDataEast.length == 1)
{
makeAdjVerticalQuad(builder, adjDataEast[0], EDhDirection.EAST, maxX, minY, z, zSize, ySize,
color, adjOverlapEast, irisBlockMaterialId, skyLightTop, blockLight,
topData, bottomData);
} }
else else
{ {
makeAdjVerticalQuad(builder, adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.EAST, maxX, minY, z, zSize, ySize, makeAdjVerticalQuad(builder, adjDataEast[0], EDhDirection.EAST, maxX, minY, z, (short) (zSize / 2), ySize,
color, irisBlockMaterialId, blockLight); color, adjOverlapEast, irisBlockMaterialId, skyLightTop, blockLight,
topData, bottomData);
makeAdjVerticalQuad(builder, adjDataEast[1], EDhDirection.EAST, maxX, minY, (short) (z + zSize / 2), (short) (zSize / 2), ySize,
color, adjOverlapEast, irisBlockMaterialId, skyLightTop, blockLight,
topData, bottomData);
} }
} }
} }
// the overlap color can be used to see faces that shouldn't be rendered
private static void makeAdjVerticalQuad( private static void makeAdjVerticalQuad(
LodQuadBuilder builder, @NotNull ColumnArrayView adjColumnView, boolean adjacentIsSameDetailLevel, int caveCullingMaxY, EDhDirection direction, LodQuadBuilder builder, ColumnArrayView adjColumnView, EDhDirection direction,
short x, short yMin, short z, short horizontalWidth, short ySize, short x, short yMin, short z, short horizontalWidth, short ySize,
int color, byte irisBlockMaterialId, byte blockLight) int color, int debugOverlapColor, byte irisBlockMaterialId, byte skyLightTop, byte blockLight,
long topData, long bottomData)
{ {
//==================//
// create face with //
// no adjacent data //
//==================//
color = ColorUtil.applyShade(color, MC.getShade(direction)); color = ColorUtil.applyShade(color, MC.getShade(direction));
// if there isn't any data adjacent to this LOD, if (adjColumnView == null || adjColumnView.size == 0 || RenderDataPointUtil.isVoid(adjColumnView.get(0)))
// just add the full vertical quad
if (adjColumnView.size == 0 || RenderDataPointUtil.isVoid(adjColumnView.get(0)))
{ {
// there isn't any data adjacent to this LOD, add the vertical quad
builder.addQuadAdj(direction, x, yMin, z, horizontalWidth, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); builder.addQuadAdj(direction, x, yMin, z, horizontalWidth, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight);
return; return;
} }
int yMax = yMin + ySize;
//===========================// int adjIndex;
// Determine face visibility // boolean firstFace = true;
// based on it's neighbors // boolean inputAboveAdjLods = true;
//===========================// short previousAdjDepth = -1;
byte nextTopSkyLight = skyLightTop;
boolean inputTransparent = ColorUtil.getAlpha(color) < 255 && LodRenderer.transparencyEnabled;
boolean lastAdjWasTransparent = false;
short yMax = (short) (yMin + ySize); // min is inclusive, max is exclusive
byte[] skyLightAtInputPos = THREAD_LOCAL_SKY_LIGHT_ARRAY.get();
try
if (!RenderDataPointUtil.doesDataPointExist(bottomData))
{ {
// set the initial sky-lights for this face, // there isn't anything under this LOD,
// if nothing overlaps or overhangs the face should have max sky light // to prevent seeing through the world, make it opaque
Arrays.fill(skyLightAtInputPos, yMin, yMax, LodUtil.MAX_MC_LIGHT); color = ColorUtil.setAlpha(color, 255);
}
// Add adjacent faces if this LOD is surrounded by transparent LODs
// (prevents invisible sides underwater)
int adjCount = adjColumnView.size();
for (adjIndex = 0; // iterates top down
adjIndex < adjCount
&& RenderDataPointUtil.doesDataPointExist(adjColumnView.get(adjIndex))
&& !RenderDataPointUtil.isVoid(adjColumnView.get(adjIndex));
adjIndex++)
{
long adjPoint = adjColumnView.get(adjIndex);
// iterate top down // if the adjacent data point is over the void
int adjCount = adjColumnView.size(); // don't consider it as transparent
for (int adjIndex = 0; adjIndex < adjCount; adjIndex++) // FIXME this transparency change should be applied before this point since this could affect other areas
boolean adjOverVoid = false;
if (adjIndex > 0)
{ {
long adjPoint = adjColumnView.get(adjIndex); long adjBellowPoint = adjColumnView.get(adjIndex-1);
short adjMinY = RenderDataPointUtil.getYMin(adjPoint); adjOverVoid = !RenderDataPointUtil.doesDataPointExist(adjBellowPoint);
short adjMaxY = RenderDataPointUtil.getYMax(adjPoint); }
boolean adjTransparent = !adjOverVoid && RenderDataPointUtil.getAlpha(adjPoint) < 255 && LodRenderer.transparencyEnabled;
// continue if this data point is transparent or the adjacent point is not
if (inputTransparent || !adjTransparent) // TODO inputIsTransparent may be unnecessary
{
short adjYMin = RenderDataPointUtil.getYMin(adjPoint);
short adjYMax = RenderDataPointUtil.getYMax(adjPoint);
// skip empty adjacent datapoints
if (!RenderDataPointUtil.doesDataPointExist(adjPoint) // if fake transparency is enabled, allow for 1 block of transparency,
|| RenderDataPointUtil.isVoid(adjPoint)) // everything under that should be opaque
if (LodRenderer.transparencyEnabled && LodRenderer.fakeOceanFloor)
{ {
continue; if (lastAdjWasTransparent && !adjTransparent)
}
// skip this adjacent datapoint if it's above the input datapoint (since it can't affect the input data point)
if (yMax <= adjMinY)
{
continue;
}
long adjAbovePoint = (adjIndex != 0) ? adjColumnView.get(adjIndex - 1) : RenderDataPointUtil.EMPTY_DATA;
long adjBelowPoint = (adjIndex + 1 < adjCount) ? adjColumnView.get(adjIndex + 1) : RenderDataPointUtil.EMPTY_DATA;
// if the adjacent data point is over the void
// don't consider it as transparent
boolean adjOverVoid = !RenderDataPointUtil.doesDataPointExist(adjBelowPoint);
boolean adjTransparent = !adjOverVoid && RenderDataPointUtil.getAlpha(adjPoint) < 255 && LodRenderer.transparencyEnabled;
//=================================//
// set sky light based on adjacent //
//=================================//
// set light based on overlapping adjacent
if (!adjTransparent)
{
// adj opaque
// mark positions adjacent is covering
byte adjSkyLight = RenderDataPointUtil.getLightSky(adjPoint);
for (int i = adjMinY; i < adjMaxY; i++)
{ {
byte skyLightAtPos = skyLightAtInputPos[i]; adjYMax = (short) (RenderDataPointUtil.getYMax(adjColumnView.get(adjIndex - 1)) - 1);
// if the adjacent is a different detail level, we want to render adjacent opaque
// faces to try and reduce the chance of holes on detail level borders
boolean adjacentCoversThis =
// if the adjacent is the same detail level, no special handling is necessary
!adjacentIsSameDetailLevel
// if the adjacent face is underground we probably don't need it
&& RenderDataPointUtil.getYMax(adjPoint) >= caveCullingMaxY
// check if this face is on a border
&&
(
(x == 0 && direction == EDhDirection.WEST)
|| (z == 0 && direction == EDhDirection.NORTH)
// TODO why does 256 represent a border? aren't LODs only 64 datapoints wide?
|| (x == 256 && direction == EDhDirection.EAST)
|| (z == 256 && direction == EDhDirection.SOUTH)
);
byte newSkyLightAtPos = adjacentCoversThis ? adjSkyLight : SKYLIGHT_COVERED;
skyLightAtInputPos[i] = (byte) Math.min(newSkyLightAtPos, skyLightAtPos);
} }
else if (adjTransparent && (adjIndex + 1) < adjCount)
{
if (RenderDataPointUtil.getAlpha(adjColumnView.get(adjIndex + 1)) == 255)
{
adjYMin = (short) (adjYMax - 1);
}
}
}
if (yMax <= adjYMin)
{
// the adjacent LOD is above the input LOD and won't affect its rendering,
// skip to the next adjacent
continue;
}
inputAboveAdjLods = false;
if (adjYMax < yMin)
{
// the adjacent LOD is below the input LOD
// getting the skylight is more complicated
// since LODs can be adjacent to water, which changes how skylight works
byte skyLight;
if (adjIndex == 0)
{
// this adj LOD is at the highest position,
// its sky lighting won't be affected by anything above it
skyLight = RenderDataPointUtil.getLightSky(adjPoint);
}
else
{
// TODO improve the comments here, this is a bit confusing
long aboveAdjPoint = adjColumnView.get(adjIndex - 1);
if (RenderDataPointUtil.getAlpha(aboveAdjPoint) != 255)
{
// above adjacent LOD is transparent...
boolean inputMaxHigherThanAboveAdj = yMax > RenderDataPointUtil.getYMax(aboveAdjPoint);
if (inputMaxHigherThanAboveAdj)
{
// ...and higher than the input yMax,
// use its sky light
skyLight = RenderDataPointUtil.getLightSky(aboveAdjPoint);
}
else
{
// ...and at or below the input yMax,
skyLight = RenderDataPointUtil.getLightSky(adjPoint);
}
}
else
{
// LOD above adjacent is opaque, use the adj LOD's skylight
skyLight = RenderDataPointUtil.getLightSky(adjPoint);
}
}
if (firstFace)
{
builder.addQuadAdj(direction, x, yMin, z, horizontalWidth, ySize, color, irisBlockMaterialId, skyLight, blockLight);
}
else
{
// Now: adjMaxHeight < y < previousAdjDepth < yMax
if (previousAdjDepth == -1)
{
// TODO why is this an error?
throw new RuntimeException("Loop error");
}
builder.addQuadAdj(direction, x, yMin, z, horizontalWidth, (short) (previousAdjDepth - yMin), color, irisBlockMaterialId, skyLight, blockLight);
previousAdjDepth = -1;
}
// TODO why break here?
break;
}
if (adjYMin <= yMin)
{
// the adjacent LOD's base is at or below the input's base
if (yMax <= adjYMax)
{
// The input face is completely inside the adj's face, don't render it
if (debugOverlapColor != 0)
{
builder.addQuadAdj(direction, x, yMin, z, horizontalWidth, ySize, debugOverlapColor, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, LodUtil.MAX_MC_LIGHT);
}
}
else
{
// the adj data intersects the lower part of the input data, don't render below the intersection
if (adjYMax > yMin && debugOverlapColor != 0)
{
builder.addQuadAdj(direction, x, yMin, z, horizontalWidth, (short) (adjYMax - yMin), debugOverlapColor, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, LodUtil.MAX_MC_LIGHT);
}
// if this is the only face, use the yMax and break,
// if there was another face finish the last one and then break
if (firstFace)
{
builder.addQuadAdj(direction, x, adjYMax, z, horizontalWidth, (short) (yMax - adjYMax), color, irisBlockMaterialId,
RenderDataPointUtil.getLightSky(adjPoint), blockLight);
}
else
{
// Now: depth <= y <= height <= previousAdjDepth < yMax
if (previousAdjDepth == -1)
{
// TODO why is this an error?
throw new RuntimeException("Loop error");
}
if (previousAdjDepth > adjYMax)
{
builder.addQuadAdj(direction, x, adjYMax, z, horizontalWidth, (short) (previousAdjDepth - adjYMax), color, irisBlockMaterialId,
RenderDataPointUtil.getLightSky(adjPoint), blockLight);
}
previousAdjDepth = -1;
}
}
// we don't need to check any other adjacent LODs
// since this one completely covers the input
break;
}
// In here always true: y < adjYMin < yMax
// _________________&&: y < ________ (height and yMax)
if (adjYMax >= yMax)
{
// Basically: y _______ < yMax <= height
// _______&&: y < depth < yMax
// the adj data intersects the higher part of the current data
if (debugOverlapColor != 0)
{
builder.addQuadAdj(direction, x, adjYMin, z, horizontalWidth, (short) (yMax - adjYMin), debugOverlapColor, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, LodUtil.MAX_MC_LIGHT);
}
// we start the creation of a new face
} }
else else
{ {
// adjacent is transparent, // Otherwise: y < _____ height < yMax
// use datapoint below adjacent for lighting // _______&&: y < depth ______ < yMax
byte belowSkyLight = RenderDataPointUtil.getLightSky(adjBelowPoint); if (debugOverlapColor != 0)
for (int i = adjMinY; i < adjMaxY; i++)
{ {
byte skyLightAtPos = skyLightAtInputPos[i]; builder.addQuadAdj(direction, x, adjYMin, z, horizontalWidth, (short) (adjYMax - adjYMin), debugOverlapColor, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, LodUtil.MAX_MC_LIGHT);
skyLightAtInputPos[i] = (byte) Math.min(belowSkyLight, skyLightAtPos); }
if (firstFace)
{
builder.addQuadAdj(direction, x, adjYMax, z, horizontalWidth, (short) (yMax - adjYMax), color, irisBlockMaterialId,
RenderDataPointUtil.getLightSky(adjPoint), blockLight);
}
else
{
// Now: y < depth < height <= previousAdjDepth < yMax
if (previousAdjDepth == -1)
throw new RuntimeException("Loop error");
if (previousAdjDepth > adjYMax)
{
if (irisBlockMaterialId == EDhApiBlockMaterial.GRASS.index)
{
// this LOD is underneath another, grass will never show here
irisBlockMaterialId = EDhApiBlockMaterial.DIRT.index;
}
builder.addQuadAdj(direction, x, adjYMax, z, horizontalWidth, (short) (previousAdjDepth - adjYMax), color, irisBlockMaterialId,
RenderDataPointUtil.getLightSky(adjPoint), blockLight);
}
previousAdjDepth = -1;
} }
} }
// fill in sky light up to the next DP, // set next top as current depth
// this is done to handle overhangs previousAdjDepth = adjYMin;
byte adjSkyLight = RenderDataPointUtil.getLightSky(adjPoint); firstFace = false;
int adjAboveMinY = RenderDataPointUtil.getYMin(adjAbovePoint); nextTopSkyLight = skyLightTop;
for (int i = adjMaxY; i < adjAboveMinY; i++)
if (adjIndex + 1 < adjColumnView.size() && RenderDataPointUtil.doesDataPointExist(adjColumnView.get(adjIndex + 1)))
{ {
byte skyLightAtPos = skyLightAtInputPos[i]; nextTopSkyLight = RenderDataPointUtil.getLightSky(adjColumnView.get(adjIndex + 1));
skyLightAtInputPos[i] = (byte) Math.min(adjSkyLight, skyLightAtPos);
}
}
//=======================//
// create vertical faces //
//=======================//
boolean inputTransparent = ColorUtil.getAlpha(color) < 255 && LodRenderer.transparencyEnabled;
byte lastSkyLight = skyLightAtInputPos[yMin];
int quadBottomY = yMin;
int quadTopY = -1;
// walk up the sky lights and create a new face
// whenever the light changes to different valid value
for (int i = yMin; i < yMax; i++)
{
byte skyLight = skyLightAtInputPos[i];
if (skyLight != lastSkyLight)
{
// the sky light changed, create the in-progress face
tryAddVerticalFaceWithSkyLightToBuilder(
builder, direction,
x, z, horizontalWidth,
color, irisBlockMaterialId, blockLight,
lastSkyLight, inputTransparent, quadTopY, quadBottomY
);
lastSkyLight = skyLight;
quadBottomY = i;
} }
quadTopY = (i + 1); lastAdjWasTransparent = adjTransparent;
}
// add the in-progress face if present
if (quadTopY != -1)
{
tryAddVerticalFaceWithSkyLightToBuilder(
builder, direction,
x, z, horizontalWidth,
color, irisBlockMaterialId, blockLight,
lastSkyLight, inputTransparent, quadTopY, quadBottomY
);
} }
} }
finally
if (inputAboveAdjLods)
{ {
// clean up the array before the next thread uses it // the input LOD is above all adjacent LODs and won't be affected
// (may be unnecessary since we only work between the yMin-yMax anyway, but is helpful for debugging) // by them, add the vertical quad using the input's lighting and height
Arrays.fill(skyLightAtInputPos, yMin, yMax, SKYLIGHT_EMPTY); builder.addQuadAdj(direction, x, yMin, z, horizontalWidth, ySize, color, irisBlockMaterialId, skyLightTop, blockLight);
}
else if (previousAdjDepth != -1)
{
// We need to finish the last quad.
builder.addQuadAdj(direction, x, yMin, z, horizontalWidth, (short) (previousAdjDepth - yMin), color, irisBlockMaterialId, nextTopSkyLight, blockLight);
} }
} }
private static void tryAddVerticalFaceWithSkyLightToBuilder(
LodQuadBuilder builder, EDhDirection direction,
short x, short z, short horizontalWidth,
int color, byte irisBlockMaterialId, byte blockLight,
byte lastSkyLight, boolean inputTransparent, int quadTopY, int quadBottomY
)
{
// invalid positions will have a negative skylight
if (lastSkyLight >= 0)
{
// Don't add transparent vertical faces
// unless the adjacent position is empty.
// This is done to prevent walls between water blocks in the ocean.
if (!inputTransparent
|| (lastSkyLight == LodUtil.MAX_MC_LIGHT))
{
// don't add negative/empty height faces
short height = (short) (quadTopY - quadBottomY);
if (height > 0)
{
builder.addQuadAdj(direction, x, (short) quadBottomY, z, horizontalWidth, height, color, irisBlockMaterialId, lastSkyLight, blockLight);
}
}
}
}
} }
@@ -19,23 +19,19 @@
package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding; package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding;
import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.pos.DhBlockPos;
import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.render.glObject.buffer.GLVertexBuffer;
import com.seibel.distanthorizons.core.render.renderer.LodRenderer; import com.seibel.distanthorizons.core.render.renderer.LodRenderer;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.objects.StatsMap;
import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod; import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod;
import com.seibel.distanthorizons.core.util.*;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.Iterator;
import java.util.concurrent.*; import java.util.concurrent.*;
/** /**
@@ -53,19 +49,18 @@ public class ColumnRenderBuffer implements AutoCloseable
/** number of bytes a single quad takes */ /** number of bytes a single quad takes */
public static final int QUADS_BYTE_SIZE = LodUtil.LOD_VERTEX_FORMAT.getByteSize() * 4; public static final int QUADS_BYTE_SIZE = LodUtil.LOD_VERTEX_FORMAT.getByteSize() * 4;
/** how big a single VBO can be in bytes */ /** how big a single VBO can be in bytes */
public static final int MAX_VBO_BYTE_SIZE = 10 * 1024 * 1024; // 10 MB public static final int MAX_VBO_BYTE_SIZE = 1024 * 1024; // 1 MB
public static final int MAX_QUADS_PER_BUFFER = MAX_VBO_BYTE_SIZE / QUADS_BYTE_SIZE; public static final int MAX_QUADS_PER_BUFFER = MAX_VBO_BYTE_SIZE / QUADS_BYTE_SIZE;
public static final int FULL_SIZED_BUFFER = MAX_QUADS_PER_BUFFER * QUADS_BYTE_SIZE; public static final int FULL_SIZED_BUFFER = MAX_QUADS_PER_BUFFER * QUADS_BYTE_SIZE;
public final DhBlockPos pos; public final DhBlockPos pos;
public boolean buffersUploaded = false; public boolean buffersUploaded = false;
private GLVertexBuffer[] vbos; public SharedVbo.BufferSlice[] opaqueBufferSlices;
private GLVertexBuffer[] vbosTransparent; public SharedVbo.BufferSlice[] transparentBufferSlices;
@@ -76,14 +71,12 @@ public class ColumnRenderBuffer implements AutoCloseable
public ColumnRenderBuffer(DhBlockPos pos) public ColumnRenderBuffer(DhBlockPos pos)
{ {
this.pos = pos; this.pos = pos;
this.vbos = new GLVertexBuffer[0]; this.opaqueBufferSlices = new SharedVbo.BufferSlice[0];
this.vbosTransparent = new GLVertexBuffer[0]; this.transparentBufferSlices = new SharedVbo.BufferSlice[0];
} }
//==================// //==================//
// buffer uploading // // buffer uploading //
//==================// //==================//
@@ -91,7 +84,7 @@ public class ColumnRenderBuffer implements AutoCloseable
/** Should be run on a DH thread. */ /** Should be run on a DH thread. */
public void uploadBuffer(LodQuadBuilder builder, EDhApiGpuUploadMethod gpuUploadMethod) throws InterruptedException public void uploadBuffer(LodQuadBuilder builder, EDhApiGpuUploadMethod gpuUploadMethod) throws InterruptedException
{ {
LodUtil.assertTrue(DhApi.isDhThread(), "Buffer uploading needs to be done on a DH thread to prevent locking up any MC threads."); LodUtil.assertTrue(Thread.currentThread().getName().startsWith(ThreadUtil.THREAD_NAME_PREFIX), "Buffer uploading needs to be done on a DH thread to prevent locking up any MC threads.");
// upload on MC's render thread // upload on MC's render thread
@@ -100,7 +93,7 @@ public class ColumnRenderBuffer implements AutoCloseable
{ {
try try
{ {
this.uploadBuffers(builder, gpuUploadMethod); this.uploadBuffersUsingUploadMethod(builder, gpuUploadMethod);
uploadFuture.complete(null); uploadFuture.complete(null);
} }
catch (InterruptedException e) catch (InterruptedException e)
@@ -126,231 +119,163 @@ public class ColumnRenderBuffer implements AutoCloseable
//LOGGER.warn("Error uploading builder ["+builder+"] synchronously. Error: "+e.getMessage(), e); //LOGGER.warn("Error uploading builder ["+builder+"] synchronously. Error: "+e.getMessage(), e);
} }
} }
private void uploadBuffers(LodQuadBuilder builder, EDhApiGpuUploadMethod method) throws InterruptedException private void uploadBuffersUsingUploadMethod(LodQuadBuilder builder, EDhApiGpuUploadMethod gpuUploadMethod) throws InterruptedException
{ {
// uploading mapped buffers used to be done here, //if (gpuUploadMethod.useEarlyMapping)
// however due to a memory leak and complication with the previous code, //{
// now we only allow direct uploading. // this.uploadBuffersMapped(builder, gpuUploadMethod);
// (There's also insufficient data to state whether mapped buffers are necessary //}
// for DH to upload without stuttering the main thread) //else
//{
this.vbos = makeAndUploadBuffers(builder, method, this.vbos, builder.makeOpaqueVertexBuffers()); this.uploadBuffersDirect(builder);
this.vbosTransparent = makeAndUploadBuffers(builder, method, this.vbosTransparent, builder.makeTransparentVertexBuffers()); //}
this.buffersUploaded = true; this.buffersUploaded = true;
} }
/** This resizes and returns the vbo array if necessary based on the amount of data needed for this area. */
private static GLVertexBuffer[] makeAndUploadBuffers(LodQuadBuilder builder, EDhApiGpuUploadMethod method, GLVertexBuffer[] vbos, ArrayList<ByteBuffer> buffers) throws InterruptedException
//private void uploadBuffersMapped(LodQuadBuilder builder, EDhApiGpuUploadMethod method)
//{
// // opaque vbos //
//
// this.vbos = ColumnRenderBufferBuilder.resizeBuffer(this.vbos, builder.getCurrentNeededOpaqueVertexBufferCount());
// for (int i = 0; i < this.vbos.length; i++)
// {
// if (this.vbos[i] == null)
// {
// this.vbos[i] = new GLVertexBuffer(method.useBufferStorage);
// }
// }
// LodQuadBuilder.BufferFiller func = builder.makeOpaqueBufferFiller(method);
// for (GLVertexBuffer vbo : this.vbos)
// {
// func.fill(vbo);
// }
//
//
// // transparent vbos //
//
// this.vbosTransparent = ColumnRenderBufferBuilder.resizeBuffer(this.vbosTransparent, builder.getCurrentNeededTransparentVertexBufferCount());
// for (int i = 0; i < this.vbosTransparent.length; i++)
// {
// if (this.vbosTransparent[i] == null)
// {
// this.vbosTransparent[i] = new GLVertexBuffer(method.useBufferStorage);
// }
// }
// LodQuadBuilder.BufferFiller transparentFillerFunc = builder.makeTransparentBufferFiller(method);
// for (GLVertexBuffer vbo : this.vbosTransparent)
// {
// transparentFillerFunc.fill(vbo);
// }
//}
private void uploadBuffersDirect(LodQuadBuilder builder)
{ {
try int opaqueSliceCount = builder.getCurrentNeededOpaqueVertexBufferCount();
if (this.opaqueBufferSlices.length != opaqueSliceCount)
{ {
vbos = resizeBuffer(vbos, buffers.size()); SharedVbo.OPAQUE.deallocate(this.opaqueBufferSlices);
uploadBuffersDirect(vbos, buffers, method); this.opaqueBufferSlices = new SharedVbo.BufferSlice[opaqueSliceCount];
}
finally
{
// all the buffers must be manually freed to prevent memory leaks
if (buffers != null)
{
for (ByteBuffer buffer : buffers)
{
MemoryUtil.memFree(buffer);
}
}
} }
uploadBuffersDirect(SharedVbo.OPAQUE, this.opaqueBufferSlices, builder.makeOpaqueVertexBuffers());
// return the array in case it was resized
return vbos; int transparentSliceCount = builder.getCurrentNeededTransparentVertexBufferCount();
} if (this.transparentBufferSlices.length != transparentSliceCount)
private static void uploadBuffersDirect(GLVertexBuffer[] vbos, ArrayList<ByteBuffer> byteBuffers, EDhApiGpuUploadMethod method) throws InterruptedException
{
int vboIndex = 0;
for (int i = 0; i < byteBuffers.size(); i++)
{ {
if (vboIndex >= vbos.length) SharedVbo.TRANSPARENT.deallocate(this.transparentBufferSlices);
this.transparentBufferSlices = new SharedVbo.BufferSlice[transparentSliceCount];
}
uploadBuffersDirect(SharedVbo.TRANSPARENT, this.transparentBufferSlices, builder.makeTransparentVertexBuffers());
}
private static void uploadBuffersDirect(SharedVbo handler, SharedVbo.BufferSlice[] bufferSlices, Iterator<ByteBuffer> iter)
{
int i = 0;
while (iter.hasNext())
{
if (i >= bufferSlices.length)
{ {
throw new RuntimeException("Too many vertex buffers!!"); throw new RuntimeException("Too many vertex buffers!!");
} }
// get or create the buffer
// get or create the VBO if (bufferSlices[i] == null)
if (vbos[vboIndex] == null)
{ {
vbos[vboIndex] = new GLVertexBuffer(method.useBufferStorage); bufferSlices[i] = handler.allocateSlice();
} }
GLVertexBuffer vbo = vbos[vboIndex]; SharedVbo.BufferSlice slice = bufferSlices[i];
ByteBuffer buffer = byteBuffers.get(i); ByteBuffer buffer = iter.next();
int size = buffer.limit() - buffer.position();
try try
{ {
vbo.bind(); handler.updateBuffer(slice, buffer);
vbo.uploadBuffer(buffer, size / LodUtil.LOD_VERTEX_FORMAT.getByteSize(), method, FULL_SIZED_BUFFER);
} }
catch (Exception e) catch (Exception e)
{ {
vbos[vboIndex] = null; bufferSlices[i] = null;
vbo.close(); handler.deallocate(slice);
LOGGER.error("Failed to upload buffer: ", e); LOGGER.error("Failed to upload buffer. Error: ["+e.getMessage()+"].", e);
} }
vboIndex++; i++;
} }
if (vboIndex < vbos.length) if (i < bufferSlices.length)
{ {
throw new RuntimeException("Too few vertex buffers!!"); throw new RuntimeException("Too few buffer chunks!");
} }
} }
//========// //========//
// render // // render //
//========// //========//
/** @return true if something was rendered, false otherwise */ //public void renderOpaque(LodRenderer renderContext, DhApiRenderParam renderEventParam)
public boolean renderOpaque(LodRenderer renderContext, DhApiRenderParam renderEventParam) //{
{ // renderContext.setModelViewMatrixOffset(this.pos, renderEventParam);
boolean hasRendered = false; // renderContext.drawSharedVbo(SharedVbo.OPAQUE, this.opaqueBufferSlices);
renderContext.setModelViewMatrixOffset(this.pos, renderEventParam); //}
for (GLVertexBuffer vbo : this.vbos) //
{ //public void renderTransparent(LodRenderer renderContext, DhApiRenderParam renderEventParam)
if (vbo == null) //{
{ // renderContext.setModelViewMatrixOffset(this.pos, renderEventParam);
continue; // renderContext.drawSharedVbo(SharedVbo.TRANSPARENT, this.transparentBufferSlices);
} //}
if (vbo.getVertexCount() == 0)
{
continue;
}
hasRendered = true;
renderContext.drawVbo(vbo);
//LodRenderer.tickLogger.info("Vertex buffer: {}", vbo);
}
return hasRendered;
}
/** @return true if something was rendered, false otherwise */
public boolean renderTransparent(LodRenderer renderContext, DhApiRenderParam renderEventParam)
{
boolean hasRendered = false;
try
{
// can throw an IllegalStateException if the GL program was freed before it should've been
renderContext.setModelViewMatrixOffset(this.pos, renderEventParam);
for (GLVertexBuffer vbo : this.vbosTransparent)
{
if (vbo == null)
{
continue;
}
if (vbo.getVertexCount() == 0)
{
continue;
}
hasRendered = true;
renderContext.drawVbo(vbo);
//LodRenderer.tickLogger.info("Vertex buffer: {}", vbo);
}
}
catch (IllegalStateException e)
{
LOGGER.error("renderContext program doesn't exist for pos: "+this.pos, e);
}
return hasRendered;
}
//================// //==============//
// helper methods // // misc methods //
//================// //==============//
// TODO
/** can be used when debugging */
public boolean hasNonNullVbos() { return false; } //this.vbos != null || this.vbosTransparent != null; }
/** can be used when debugging */ /** can be used when debugging */
public boolean hasNonNullVbos() { return this.vbos != null || this.vbosTransparent != null; } public int bufferSliceCount()
/** can be used when debugging */
public int vboBufferCount()
{ {
int count = 0; int count = 0;
if (this.vbos != null) if (this.opaqueBufferSlices != null)
{ {
count += this.vbos.length; count += this.opaqueBufferSlices.length;
} }
if (this.vbosTransparent != null) if (this.transparentBufferSlices != null)
{ {
count += this.vbosTransparent.length; count += this.transparentBufferSlices.length;
} }
return count; return count;
} }
public void debugDumpStats(StatsMap statsMap)
{
statsMap.incStat("RenderBuffers");
statsMap.incStat("SimpleRenderBuffers");
for (GLVertexBuffer vertexBuffer : vbos)
{
if (vertexBuffer != null)
{
statsMap.incStat("VBOs");
if (vertexBuffer.getSize() == FULL_SIZED_BUFFER)
{
statsMap.incStat("FullsizedVBOs");
}
if (vertexBuffer.getSize() == 0)
{
GLProxy.GL_LOGGER.warn("VBO with size 0");
}
statsMap.incBytesStat("TotalUsage", vertexBuffer.getSize());
}
}
}
public static GLVertexBuffer[] resizeBuffer(GLVertexBuffer[] vbos, int newSize)
{
if (vbos.length == newSize)
{
return vbos;
}
GLVertexBuffer[] newVbos = new GLVertexBuffer[newSize];
System.arraycopy(vbos, 0, newVbos, 0, Math.min(vbos.length, newSize));
if (newSize < vbos.length)
{
for (int i = newSize; i < vbos.length; i++)
{
if (vbos[i] != null)
{
vbos[i].close();
}
}
}
return newVbos;
}
//================//
// base overrides //
//================//
/** /**
* This method is called when object is no longer in use. * This method is called when object is no longer in use.
* Called either after uploadBuffers() returned false (On buffer Upload * Called either after uploadBuffers() returned false (On buffer Upload
@@ -362,24 +287,8 @@ public class ColumnRenderBuffer implements AutoCloseable
{ {
this.buffersUploaded = false; this.buffersUploaded = false;
GLProxy.getInstance().queueRunningOnRenderThread(() -> SharedVbo.OPAQUE.deallocate(this.opaqueBufferSlices);
{ SharedVbo.TRANSPARENT.deallocate(this.transparentBufferSlices);
for (GLVertexBuffer buffer : this.vbos)
{
if (buffer != null)
{
buffer.destroyAsync();
}
}
for (GLVertexBuffer buffer : this.vbosTransparent)
{
if (buffer != null)
{
buffer.destroyAsync();
}
}
});
} }
} }
@@ -19,7 +19,6 @@
package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding; package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiBlockMaterial;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering; import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering;
import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
@@ -27,16 +26,15 @@ import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource;
import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.pos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.glObject.GLProxy; import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.util.ColorUtil; import com.seibel.distanthorizons.core.render.glObject.buffer.GLVertexBuffer;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.RenderDataPointUtil; import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
import com.seibel.distanthorizons.core.util.objects.UncheckedInterruptedException; import com.seibel.distanthorizons.core.util.objects.UncheckedInterruptedException;
import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -61,21 +59,21 @@ public class ColumnRenderBufferBuilder
// vbo building // // vbo building //
//==============// //==============//
public static CompletableFuture<LodQuadBuilder> buildBuffersAsync( public static CompletableFuture<ColumnRenderBuffer> buildAndUploadBuffersAsync(
IDhClientLevel clientLevel, IDhClientLevel clientLevel,
ColumnRenderSource renderSource, ColumnRenderSource[] adjData, boolean[] isSameDetailLevel ColumnRenderSource renderSource, ColumnRenderSource[] adjData)
)
{ {
ThreadPoolExecutor bufferBuilderExecutor = ThreadPoolUtil.getBufferBuilderExecutor(); ThreadPoolExecutor bufferBuilderExecutor = ThreadPoolUtil.getBufferBuilderExecutor();
if (bufferBuilderExecutor == null || bufferBuilderExecutor.isTerminated()) ThreadPoolExecutor bufferUploaderExecutor = ThreadPoolUtil.getBufferUploaderExecutor();
if ((bufferBuilderExecutor == null || bufferBuilderExecutor.isTerminated()) ||
(bufferUploaderExecutor == null || bufferUploaderExecutor.isTerminated()))
{ {
// one or more of the thread pools has been shut down // one or more of the thread pools has been shut down
CompletableFuture<LodQuadBuilder> future = new CompletableFuture<>(); CompletableFuture<ColumnRenderBuffer> future = new CompletableFuture<>();
future.cancel(true); future.cancel(true);
return future; return future;
} }
try try
{ {
return CompletableFuture.supplyAsync(() -> return CompletableFuture.supplyAsync(() ->
@@ -83,8 +81,35 @@ public class ColumnRenderBufferBuilder
try try
{ {
boolean enableTransparency = Config.Client.Advanced.Graphics.Quality.transparency.get().transparencyEnabled; boolean enableTransparency = Config.Client.Advanced.Graphics.Quality.transparency.get().transparencyEnabled;
LodQuadBuilder builder = new LodQuadBuilder(enableTransparency, clientLevel.getClientLevelWrapper());
makeLodRenderData(builder, renderSource, clientLevel, adjData, isSameDetailLevel); //EVENT_LOGGER.trace("RenderRegion start QuadBuild @ " + renderSource.sectionPos);
boolean enableSkyLightCulling =
Config.Client.Advanced.Graphics.AdvancedGraphics.enableCaveCulling.get()
&& (
// dimensions with a ceiling will be all caves so we don't want cave culling
!clientLevel.getLevelWrapper().hasCeiling()
// the end has a lot of overhangs with 0 lighting above the void, which look broken with
// the current cave culling logic (this could probably be improved, but just skipping it works best for now)
&& !clientLevel.getLevelWrapper().getDimensionType().isTheEnd()
// FIXME temporary fix
// Cave culling is currently broken for any detail level above 0
&& DhSectionPos.getDetailLevel(renderSource.pos) == DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL
);
int skyLightCullingBelow = Config.Client.Advanced.Graphics.AdvancedGraphics.caveCullingHeight.get();
// FIXME: Clamp also to the max world height.
skyLightCullingBelow = Math.max(skyLightCullingBelow, clientLevel.getMinY());
long builderStartTime = System.currentTimeMillis();
LodQuadBuilder builder = new LodQuadBuilder(enableSkyLightCulling, (short) (skyLightCullingBelow - clientLevel.getMinY()), enableTransparency, clientLevel.getClientLevelWrapper());
makeLodRenderData(builder, renderSource, adjData);
long builderEndTime = System.currentTimeMillis();
long buildMs = builderEndTime - builderStartTime;
LOGGER.debug("RenderRegion end QuadBuild @ " + renderSource.pos + " took: " + buildMs);
return builder; return builder;
} }
catch (UncheckedInterruptedException e) catch (UncheckedInterruptedException e)
@@ -93,42 +118,11 @@ public class ColumnRenderBufferBuilder
} }
catch (Throwable e3) catch (Throwable e3)
{ {
LOGGER.error("LodNodeBufferBuilder was unable to build quads for pos ["+DhSectionPos.toString(renderSource.pos)+"], error: ["+ e3.getMessage()+"].", e3); LOGGER.error("\"LodNodeBufferBuilder\" was unable to build quads: ", e3);
throw e3; throw e3;
} }
}, bufferBuilderExecutor); }, bufferBuilderExecutor)
} .thenApplyAsync((quadBuilder) ->
catch (RejectedExecutionException ignore)
{
// the thread pool was probably shut down because it's size is being changed, just wait a sec and it should be back
CompletableFuture<LodQuadBuilder> future = new CompletableFuture<>();
future.cancel(true);
return future;
}
}
/** @link adjData should be null for adjacent sections that cross detail level boundaries */
public static CompletableFuture<ColumnRenderBuffer> uploadBuffersAsync(
IDhClientLevel clientLevel,
ColumnRenderSource renderSource,
LodQuadBuilder quadBuilder
)
{
// TODO put into a single future/thread so it can be easily canceled
ThreadPoolExecutor bufferUploaderExecutor = ThreadPoolUtil.getBufferUploaderExecutor();
if (bufferUploaderExecutor == null || bufferUploaderExecutor.isTerminated())
{
// one or more of the thread pools has been shut down
CompletableFuture<ColumnRenderBuffer> future = new CompletableFuture<>();
future.cancel(true);
return future;
}
try
{
return CompletableFuture.supplyAsync(() ->
{ {
try try
{ {
@@ -136,15 +130,8 @@ public class ColumnRenderBufferBuilder
try try
{ {
buffer.uploadBuffer(quadBuilder, GLProxy.getInstance().getGpuUploadMethod()); buffer.uploadBuffer(quadBuilder, GLProxy.getInstance().getGpuUploadMethod());
if (buffer.buffersUploaded) LodUtil.assertTrue(buffer.buffersUploaded);
{ return buffer;
return buffer;
}
else
{
buffer.close();
return null;
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -158,38 +145,35 @@ public class ColumnRenderBufferBuilder
} }
catch (Throwable e3) catch (Throwable e3)
{ {
LOGGER.error("LodNodeBufferBuilder was unable to upload buffer for pos ["+DhSectionPos.toString(renderSource.pos)+"], error: [" + e3.getMessage() + "].", e3); LOGGER.error("LodNodeBufferBuilder was unable to upload buffer: " + e3.getMessage(), e3);
throw e3; throw e3;
} }
}, bufferUploaderExecutor); }, bufferUploaderExecutor);
} }
catch (RejectedExecutionException ignore) catch (RejectedExecutionException ignore)
{ {
// shouldn't happen, but just in case // the thread pool was probably shut down because it's size is being changed, just wait a sec and it should be back
CompletableFuture<ColumnRenderBuffer> future = new CompletableFuture<>(); CompletableFuture<ColumnRenderBuffer> future = new CompletableFuture<>();
future.cancel(true); future.cancel(true);
return future; return future;
} }
} }
private static void makeLodRenderData( private static void makeLodRenderData(LodQuadBuilder quadBuilder, ColumnRenderSource renderSource, ColumnRenderSource[] adjRegions)
LodQuadBuilder quadBuilder, ColumnRenderSource renderSource, IDhClientLevel clientLevel,
ColumnRenderSource[] adjRegions, boolean[] isSameDetailLevel)
{ {
//=============// // Variable initialization
// debug check // EDhApiDebugRendering debugMode = Config.Client.Advanced.Debugging.debugRendering.get();
//=============//
// can be used to limit which section positions are build and thus, rendered // can be used to limit which section positions are build and thus, rendered
// useful when debugging a specific section // useful when debugging a specific section
boolean columnBuilderDebugEnabled = Config.Client.Advanced.Debugging.columnBuilderDebugEnable.get(); boolean enableColumnBufferLimit = Config.Client.Advanced.Debugging.columnBuilderDebugEnable.get();
if (columnBuilderDebugEnabled) if (enableColumnBufferLimit)
{ {
if (DhSectionPos.getDetailLevel(renderSource.pos) == Config.Client.Advanced.Debugging.columnBuilderDebugDetailLevel.get() if (DhSectionPos.getDetailLevel(renderSource.pos) == Config.Client.Advanced.Debugging.columnBuilderDebugDetailLevel.get()
&& DhSectionPos.getX(renderSource.pos) == Config.Client.Advanced.Debugging.columnBuilderDebugXPos.get() && DhSectionPos.getX(renderSource.pos) == Config.Client.Advanced.Debugging.columnBuilderDebugXPos.get()
&& DhSectionPos.getZ(renderSource.pos) == Config.Client.Advanced.Debugging.columnBuilderDebugZPos.get()) && DhSectionPos.getZ(renderSource.pos) == Config.Client.Advanced.Debugging.columnBuilderDebugZPos.get())
{ {
int breakpoint = 0; int test = 0;
} }
else else
{ {
@@ -197,22 +181,24 @@ public class ColumnRenderBufferBuilder
} }
} }
byte detailLevel = renderSource.getDataDetailLevel();
for (int x = 0; x < ColumnRenderSource.SECTION_SIZE; x++)
//===================//
// build each column //
//===================//
byte thisDetailLevel = renderSource.getDataDetailLevel();
for (int relX = 0; relX < ColumnRenderSource.SECTION_SIZE; relX++)
{ {
for (int relZ = 0; relZ < ColumnRenderSource.SECTION_SIZE; relZ++) for (int z = 0; z < ColumnRenderSource.SECTION_SIZE; z++)
{ {
// stop the builder if requested // TODO make a config for this
// can be uncommented to limit the buffer building to a specific
// relative position in this section.
// useful for debugging a single column's rendering
// if (x != 0 || (z != 0 && z != 1))
// {
// continue;
// }
UncheckedInterruptedException.throwIfInterrupted(); UncheckedInterruptedException.throwIfInterrupted();
// ignore empty/null columns ColumnArrayView columnRenderData = renderSource.getVerticalDataPointView(x, z);
ColumnArrayView columnRenderData = renderSource.getVerticalDataPointView(relX, relZ);
if (columnRenderData.size() == 0 if (columnRenderData.size() == 0
|| !RenderDataPointUtil.doesDataPointExist(columnRenderData.get(0)) || !RenderDataPointUtil.doesDataPointExist(columnRenderData.get(0))
|| RenderDataPointUtil.isVoid(columnRenderData.get(0))) || RenderDataPointUtil.isVoid(columnRenderData.get(0)))
@@ -220,66 +206,43 @@ public class ColumnRenderBufferBuilder
continue; continue;
} }
ColumnRenderSource.DebugSourceFlag debugSourceFlag = renderSource.debugGetFlag(x, z);
ColumnArrayView[][] adjColumnViews = new ColumnArrayView[4][];
// We extract the adj data in the four cardinal direction
// we first reset the adjShadeDisabled. This is used to disable the shade on the
// border when we have transparent block like water or glass
// to avoid having a "darker border" underground
// Arrays.fill(adjShadeDisabled, false);
//=============// // We check every adj block in each direction
// debug limit //
//=============//
// can be used to limit the buffer building to a specific relative position. // If the adj block is rendered in the same region and with same detail
// useful for debugging a single column // and is positioned in a place that is not going to be rendered by vanilla game
if (columnBuilderDebugEnabled) // then we can set this position as adj
{ // We avoid cases where the adjPosition is in player chunk while the position is
int wantedX = Config.Client.Advanced.Debugging.columnBuilderDebugXRow.get(); // not
if (wantedX >= 0 && relX != wantedX) // to always have a wall underwater
{
continue;
}
int wantedZ = Config.Client.Advanced.Debugging.columnBuilderDebugZRow.get();
if (wantedZ >= 0 && relZ != wantedZ)
{
continue;
}
}
//==================================//
// get adjacent render data columns //
//==================================//
ColumnArrayView[] adjColumnViews = new ColumnArrayView[EDhDirection.ADJ_DIRECTIONS.length];
for (EDhDirection lodDirection : EDhDirection.ADJ_DIRECTIONS) for (EDhDirection lodDirection : EDhDirection.ADJ_DIRECTIONS)
{ {
try try
{ {
int xAdj = relX + lodDirection.getNormal().x; int xAdj = x + lodDirection.getNormal().x;
int zAdj = relZ + lodDirection.getNormal().z; int zAdj = z + lodDirection.getNormal().z;
boolean isCrossRenderSourceBoundary = boolean isCrossRegionBoundary =
(xAdj < 0 || xAdj >= ColumnRenderSource.SECTION_SIZE) || (xAdj < 0 || xAdj >= ColumnRenderSource.SECTION_SIZE) ||
(zAdj < 0 || zAdj >= ColumnRenderSource.SECTION_SIZE); (zAdj < 0 || zAdj >= ColumnRenderSource.SECTION_SIZE);
ColumnRenderSource adjRenderSource; ColumnRenderSource adjRenderSource;
byte adjDetailLevel; byte adjDetailLevel;
//we check if the detail of the adjPos is equal to the correct one (region border fix)
//or if the detail is wrong by 1 value (region+circle border fix)
//=========================// if (isCrossRegionBoundary)
// get the adjacent render //
// source if present //
//=========================//
if (!isCrossRenderSourceBoundary)
{ {
// the adjacent position is inside this same render source //we compute at which detail that position should be rendered
adjRenderSource = renderSource;
adjDetailLevel = thisDetailLevel;
}
else
{
// the adjacent position is outside this render source
// skip empty sections
adjRenderSource = adjRegions[lodDirection.ordinal() - 2]; adjRenderSource = adjRegions[lodDirection.ordinal() - 2];
if (adjRenderSource == null) if (adjRenderSource == null)
{ {
@@ -287,70 +250,67 @@ public class ColumnRenderBufferBuilder
} }
adjDetailLevel = adjRenderSource.getDataDetailLevel(); adjDetailLevel = adjRenderSource.getDataDetailLevel();
if (adjDetailLevel == thisDetailLevel) if (adjDetailLevel != detailLevel)
{
//TODO: Implement this
}
else
{ {
// if the adjacent position is outside this render source,
// wrap the position around so it's inside the adjacent source
if (xAdj < 0) if (xAdj < 0)
{
xAdj += ColumnRenderSource.SECTION_SIZE; xAdj += ColumnRenderSource.SECTION_SIZE;
}
if (xAdj >= ColumnRenderSource.SECTION_SIZE)
{
xAdj -= ColumnRenderSource.SECTION_SIZE;
}
if (zAdj < 0) if (zAdj < 0)
{
zAdj += ColumnRenderSource.SECTION_SIZE; zAdj += ColumnRenderSource.SECTION_SIZE;
}
if (xAdj >= ColumnRenderSource.SECTION_SIZE)
xAdj -= ColumnRenderSource.SECTION_SIZE;
if (zAdj >= ColumnRenderSource.SECTION_SIZE) if (zAdj >= ColumnRenderSource.SECTION_SIZE)
{
zAdj -= ColumnRenderSource.SECTION_SIZE; zAdj -= ColumnRenderSource.SECTION_SIZE;
}
} }
} }
else
{
adjRenderSource = renderSource;
adjDetailLevel = detailLevel;
}
if (adjDetailLevel < detailLevel - 1 || adjDetailLevel > detailLevel + 1)
{
continue;
}
if (adjDetailLevel == detailLevel || adjDetailLevel > detailLevel)
//========================// {
// get the adjacent views // adjColumnViews[lodDirection.ordinal() - 2] = new ColumnArrayView[1];
//========================// adjColumnViews[lodDirection.ordinal() - 2][0] = adjRenderSource.getVerticalDataPointView(xAdj, zAdj);
}
// the old logic handled additional cases, but they never appeared to fire, else
// so just these two cases should be fine {
LodUtil.assertTrue(adjDetailLevel == thisDetailLevel || adjDetailLevel > thisDetailLevel); adjColumnViews[lodDirection.ordinal() - 2] = new ColumnArrayView[2];
adjColumnViews[lodDirection.ordinal() - 2][0] = adjRenderSource.getVerticalDataPointView(xAdj, zAdj);
adjColumnViews[lodDirection.ordinal() - 2] = adjRenderSource.getVerticalDataPointView(xAdj, zAdj); adjColumnViews[lodDirection.ordinal() - 2][1] = adjRenderSource.getVerticalDataPointView(
xAdj + (lodDirection.getAxis() == EDhDirection.Axis.X ? 0 : 1),
zAdj + (lodDirection.getAxis() == EDhDirection.Axis.Z ? 0 : 1));
}
} }
catch (RuntimeException e) catch (RuntimeException e)
{ {
EVENT_LOGGER.warn("Failed to get adj data for relative pos: [" + thisDetailLevel + ":" + relX + "," + relZ + "] at [" + lodDirection + "], Error: "+e.getMessage(), e); EVENT_LOGGER.warn("Failed to get adj data for [" + detailLevel + ":" + x + "," + z + "] at [" + lodDirection + "], Error: "+e.getMessage(), e);
} }
} // for adjacent directions } // for adjacent directions
//==========================//
// build this render column //
//==========================//
ColumnRenderSource.DebugSourceFlag debugSourceFlag = renderSource.debugGetFlag(relX, relZ);
// We render every vertical lod present in this position // We render every vertical lod present in this position
// We only stop when we find a block that is void or non-existing block // We only stop when we find a block that is void or non-existing block
for (int i = 0; i < columnRenderData.size(); i++) for (int i = 0; i < columnRenderData.size(); i++)
{ {
// TODO make a config for this
// can be uncommented to limit which vertical LOD is generated // can be uncommented to limit which vertical LOD is generated
if (Config.Client.Advanced.Debugging.columnBuilderDebugEnable.get()) // if (i != 0)
{ // {
int wantedColumnIndex = Config.Client.Advanced.Debugging.columnBuilderDebugColumnIndex.get(); // continue;
if (wantedColumnIndex >= 0 && i != wantedColumnIndex) // }
{
continue;
}
}
long data = columnRenderData.get(i); long data = columnRenderData.get(i);
// If the data is not render-able (Void or non-existing) we stop since there is // If the data is not render-able (Void or non-existing) we stop since there is
@@ -363,12 +323,8 @@ public class ColumnRenderBufferBuilder
long topDataPoint = (i - 1) >= 0 ? columnRenderData.get(i - 1) : RenderDataPointUtil.EMPTY_DATA; long topDataPoint = (i - 1) >= 0 ? columnRenderData.get(i - 1) : RenderDataPointUtil.EMPTY_DATA;
long bottomDataPoint = (i + 1) < columnRenderData.size() ? columnRenderData.get(i + 1) : RenderDataPointUtil.EMPTY_DATA; long bottomDataPoint = (i + 1) < columnRenderData.size() ? columnRenderData.get(i + 1) : RenderDataPointUtil.EMPTY_DATA;
addLodToBuffer( CubicLodTemplate.addLodToBuffer(data, topDataPoint, bottomDataPoint, adjColumnViews, detailLevel,
clientLevel, x, z, quadBuilder, debugMode, debugSourceFlag);
data, topDataPoint, bottomDataPoint,
adjColumnViews, isSameDetailLevel,
thisDetailLevel, relX, relZ,
quadBuilder, debugSourceFlag);
} }
}// for z }// for z
@@ -376,147 +332,5 @@ public class ColumnRenderBufferBuilder
quadBuilder.finalizeData(); quadBuilder.finalizeData();
} }
private static void addLodToBuffer(
IDhClientLevel clientLevel,
long data, long topData, long bottomData,
ColumnArrayView[] adjColumnViews, boolean[] isSameDetailLevel,
byte detailLevel, int renderSourceOffsetPosX, int renderSourceOffsetPosZ,
LodQuadBuilder quadBuilder, ColumnRenderSource.DebugSourceFlag debugSource)
{
long sectionPos = DhSectionPos.encode(detailLevel, renderSourceOffsetPosX, renderSourceOffsetPosZ);
short width = (short) BitShiftUtil.powerOfTwo(detailLevel);
short x = (short) DhSectionPos.getMinCornerBlockX(sectionPos);
short yMin = RenderDataPointUtil.getYMin(data);
short z = (short) DhSectionPos.getMinCornerBlockZ(sectionPos);
short ySize = (short) (RenderDataPointUtil.getYMax(data) - yMin);
if (ySize == 0)
{
return;
}
else if (ySize < 0)
{
throw new IllegalArgumentException("Negative y size for the data! Data: [" + RenderDataPointUtil.toString(data) + "].");
}
byte blockMaterialId = RenderDataPointUtil.getBlockMaterialId(data);
int color;
boolean fullBright = false;
EDhApiDebugRendering debugging = Config.Client.Advanced.Debugging.debugRendering.get();
switch (debugging)
{
case OFF:
{
float saturationMultiplier = Config.Client.Advanced.Graphics.AdvancedGraphics.saturationMultiplier.get().floatValue();
float brightnessMultiplier = Config.Client.Advanced.Graphics.AdvancedGraphics.brightnessMultiplier.get().floatValue();
if (saturationMultiplier == 1.0 && brightnessMultiplier == 1.0)
{
color = RenderDataPointUtil.getColor(data);
}
else
{
float[] ahsv = ColorUtil.argbToAhsv(RenderDataPointUtil.getColor(data));
color = ColorUtil.ahsvToArgb(ahsv[0], ahsv[1], ahsv[2] * saturationMultiplier, ahsv[3] * brightnessMultiplier);
}
break;
}
case SHOW_DETAIL:
{
color = LodUtil.DEBUG_DETAIL_LEVEL_COLORS[detailLevel];
fullBright = true;
break;
}
case SHOW_BLOCK_MATERIAL:
{
switch (EDhApiBlockMaterial.getFromIndex(blockMaterialId))
{
case UNKNOWN:
case AIR: // shouldn't normally be rendered, but just in case
color = ColorUtil.HOT_PINK;
break;
case LEAVES:
color = ColorUtil.DARK_GREEN;
break;
case STONE:
color = ColorUtil.GRAY;
break;
case WOOD:
color = ColorUtil.BROWN;
break;
case METAL:
color = ColorUtil.DARK_GRAY;
break;
case DIRT:
color = ColorUtil.LIGHT_BROWN;
break;
case LAVA:
color = ColorUtil.ORANGE;
break;
case DEEPSLATE:
color = ColorUtil.BLACK;
break;
case SNOW:
color = ColorUtil.WHITE;
break;
case SAND:
color = ColorUtil.TAN;
break;
case TERRACOTTA:
color = ColorUtil.DARK_ORANGE;
break;
case NETHER_STONE:
color = ColorUtil.DARK_RED;
break;
case WATER:
color = ColorUtil.BLUE;
break;
case GRASS:
color = ColorUtil.GREEN;
break;
case ILLUMINATED:
color = ColorUtil.YELLOW;
break;
default:
// undefined color
color = ColorUtil.CYAN;
break;
}
fullBright = true;
break;
}
case SHOW_OVERLAPPING_QUADS:
{
color = ColorUtil.WHITE;
fullBright = true;
break;
}
case SHOW_RENDER_SOURCE_FLAG:
{
color = debugSource == null ? ColorUtil.RED : debugSource.color;
fullBright = true;
break;
}
default:
throw new IllegalArgumentException("Unknown debug mode: " + debugging);
}
ColumnBox.addBoxQuadsToBuilder(
quadBuilder, clientLevel,
width, ySize, width,
x, yMin, z,
color,
blockMaterialId,
RenderDataPointUtil.getLightSky(data),
fullBright ? 15 : RenderDataPointUtil.getLightBlock(data),
topData, bottomData, adjColumnViews, isSameDetailLevel);
}
} }
@@ -0,0 +1,186 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiBlockMaterial;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource;
import com.seibel.distanthorizons.core.pos.DhLodPos;
import com.seibel.distanthorizons.core.util.ColorUtil;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering;
import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
/**
* Builds LODs as rectangular prisms.
*
* @author James Seibel
* @version 2022-1-2
*/
public class CubicLodTemplate
{
public static void addLodToBuffer(
long data, long topData, long bottomData, ColumnArrayView[][] adjColumnViews,
byte detailLevel, int offsetPosX, int offsetOosZ, LodQuadBuilder quadBuilder,
EDhApiDebugRendering debugging, ColumnRenderSource.DebugSourceFlag debugSource)
{
DhLodPos blockOffsetPos = new DhLodPos(detailLevel, offsetPosX, offsetOosZ).convertToDetailLevel(LodUtil.BLOCK_DETAIL_LEVEL);
short width = (short) BitShiftUtil.powerOfTwo(detailLevel);
short x = (short) blockOffsetPos.x;
short yMin = RenderDataPointUtil.getYMin(data);
short z = (short) (short) blockOffsetPos.z;
short ySize = (short) (RenderDataPointUtil.getYMax(data) - yMin);
if (ySize == 0)
{
return;
}
else if (ySize < 0)
{
throw new IllegalArgumentException("Negative y size for the data! Data: " + RenderDataPointUtil.toString(data));
}
byte blockMaterialId = RenderDataPointUtil.getBlockMaterialId(data);
int color;
boolean fullBright = false;
switch (debugging)
{
case OFF:
{
float saturationMultiplier = Config.Client.Advanced.Graphics.AdvancedGraphics.saturationMultiplier.get().floatValue();
float brightnessMultiplier = Config.Client.Advanced.Graphics.AdvancedGraphics.brightnessMultiplier.get().floatValue();
if (saturationMultiplier == 1.0 && brightnessMultiplier == 1.0)
{
color = RenderDataPointUtil.getColor(data);
}
else
{
float[] ahsv = ColorUtil.argbToAhsv(RenderDataPointUtil.getColor(data));
color = ColorUtil.ahsvToArgb(ahsv[0], ahsv[1], ahsv[2] * saturationMultiplier, ahsv[3] * brightnessMultiplier);
//ApiShared.LOGGER.info("Raw color:[{}], AHSV:{}, Out color:[{}]",
// ColorUtil.toString(DataPointUtil.getColor(data)),
// ahsv, ColorUtil.toString(color));
}
break;
}
case SHOW_DETAIL:
{
color = LodUtil.DEBUG_DETAIL_LEVEL_COLORS[detailLevel];
fullBright = true;
break;
}
case SHOW_BLOCK_MATERIAL:
{
switch (EDhApiBlockMaterial.getFromIndex(blockMaterialId))
{
case UNKNOWN:
case AIR: // shouldn't normally be rendered, but just in case
color = ColorUtil.HOT_PINK;
break;
case LEAVES:
color = ColorUtil.DARK_GREEN;
break;
case STONE:
color = ColorUtil.GRAY;
break;
case WOOD:
color = ColorUtil.BROWN;
break;
case METAL:
color = ColorUtil.DARK_GRAY;
break;
case DIRT:
color = ColorUtil.LIGHT_BROWN;
break;
case LAVA:
color = ColorUtil.ORANGE;
break;
case DEEPSLATE:
color = ColorUtil.BLACK;
break;
case SNOW:
color = ColorUtil.WHITE;
break;
case SAND:
color = ColorUtil.TAN;
break;
case TERRACOTTA:
color = ColorUtil.DARK_ORANGE;
break;
case NETHER_STONE:
color = ColorUtil.DARK_RED;
break;
case WATER:
color = ColorUtil.BLUE;
break;
case GRASS:
color = ColorUtil.GREEN;
break;
case ILLUMINATED:
color = ColorUtil.YELLOW;
break;
default:
// undefined color
color = ColorUtil.CYAN;
break;
}
fullBright = true;
break;
}
case SHOW_OVERLAPPING_QUADS:
{
color = ColorUtil.WHITE;
fullBright = true;
break;
}
case SHOW_RENDER_SOURCE_FLAG:
{
color = debugSource == null ? ColorUtil.RED : debugSource.color;
fullBright = true;
break;
}
default:
throw new IllegalArgumentException("Unknown debug mode: " + debugging);
}
ColumnBox.addBoxQuadsToBuilder(
quadBuilder, // buffer
width, ySize, width, // setWidth
x, yMin, z, // setOffset
color, // setColor
blockMaterialId, // irisBlockMaterialId
RenderDataPointUtil.getLightSky(data), // setSkyLights
fullBright ? 15 : RenderDataPointUtil.getLightBlock(data), // setBlockLights
topData, bottomData, adjColumnViews); // setAdjData
}
}
@@ -38,7 +38,8 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftCli
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.coreapi.util.MathUtil; import com.seibel.distanthorizons.coreapi.util.MathUtil;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.lwjgl.system.MemoryUtil;
//TODO: Recheck this class for refactoring
/** /**
* Used to create the quads before they are converted to render-able buffers. <br><br> * Used to create the quads before they are converted to render-able buffers. <br><br>
@@ -50,6 +51,9 @@ public class LodQuadBuilder
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
public final boolean skipQuadsWithZeroSkylight;
public final short skyLightCullingBelow;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private final ArrayList<BufferQuad>[] opaqueQuads = (ArrayList<BufferQuad>[]) new ArrayList[6]; private final ArrayList<BufferQuad>[] opaqueQuads = (ArrayList<BufferQuad>[]) new ArrayList[6];
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -119,7 +123,7 @@ public class LodQuadBuilder
// constructor // // constructor //
//=============// //=============//
public LodQuadBuilder(boolean doTransparency, IClientLevelWrapper clientLevelWrapper) public LodQuadBuilder(boolean enableSkylightCulling, short skyLightCullingBelow, boolean doTransparency, IClientLevelWrapper clientLevelWrapper)
{ {
this.doTransparency = doTransparency; this.doTransparency = doTransparency;
for (int i = 0; i < 6; i++) for (int i = 0; i < 6; i++)
@@ -128,6 +132,8 @@ public class LodQuadBuilder
this.transparentQuads[i] = new ArrayList<>(); this.transparentQuads[i] = new ArrayList<>();
} }
this.skipQuadsWithZeroSkylight = enableSkylightCulling;
this.skyLightCullingBelow = skyLightCullingBelow;
this.clientLevelWrapper = clientLevelWrapper; this.clientLevelWrapper = clientLevelWrapper;
this.debugRenderingMode = Config.Client.Advanced.Debugging.debugRendering.get(); this.debugRenderingMode = Config.Client.Advanced.Debugging.debugRendering.get();
@@ -151,6 +157,11 @@ public class LodQuadBuilder
throw new IllegalArgumentException("addQuadAdj() is only for adj direction! Not UP or Down!"); throw new IllegalArgumentException("addQuadAdj() is only for adj direction! Not UP or Down!");
} }
if (this.skipQuadsWithZeroSkylight && skyLight == 0 && y + widthNorthSouthOrUpDown < this.skyLightCullingBelow)
{
return;
}
BufferQuad quad = new BufferQuad(x, y, z, widthEastWest, widthNorthSouthOrUpDown, color, irisBlockMaterialId, skyLight, blockLight, dir); BufferQuad quad = new BufferQuad(x, y, z, widthEastWest, widthNorthSouthOrUpDown, color, irisBlockMaterialId, skyLight, blockLight, dir);
ArrayList<BufferQuad> quadList = (this.doTransparency && ColorUtil.getAlpha(color) < 255) ? this.transparentQuads[dir.ordinal()] : this.opaqueQuads[dir.ordinal()]; ArrayList<BufferQuad> quadList = (this.doTransparency && ColorUtil.getAlpha(color) < 255) ? this.transparentQuads[dir.ordinal()] : this.opaqueQuads[dir.ordinal()];
if (!quadList.isEmpty() && if (!quadList.isEmpty() &&
@@ -169,6 +180,12 @@ public class LodQuadBuilder
// XZ // XZ
public void addQuadUp(short x, short maxY, short z, short widthEastWest, short widthNorthSouthOrUpDown, int color, byte irisBlockMaterialId, byte skylight, byte blocklight) // TODO argument names are wrong public void addQuadUp(short x, short maxY, short z, short widthEastWest, short widthNorthSouthOrUpDown, int color, byte irisBlockMaterialId, byte skylight, byte blocklight) // TODO argument names are wrong
{ {
// cave culling
if (this.skipQuadsWithZeroSkylight && skylight == 0 && maxY < this.skyLightCullingBelow)
{
return;
}
BufferQuad quad = new BufferQuad(x, maxY, z, widthEastWest, widthNorthSouthOrUpDown, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.UP); BufferQuad quad = new BufferQuad(x, maxY, z, widthEastWest, widthNorthSouthOrUpDown, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.UP);
boolean isTransparent = (this.doTransparency && ColorUtil.getAlpha(color) < 255); boolean isTransparent = (this.doTransparency && ColorUtil.getAlpha(color) < 255);
ArrayList<BufferQuad> quadList = isTransparent ? this.transparentQuads[EDhDirection.UP.ordinal()] : this.opaqueQuads[EDhDirection.UP.ordinal()]; ArrayList<BufferQuad> quadList = isTransparent ? this.transparentQuads[EDhDirection.UP.ordinal()] : this.opaqueQuads[EDhDirection.UP.ordinal()];
@@ -190,13 +207,15 @@ public class LodQuadBuilder
public void addQuadDown(short x, short y, short z, short width, short wz, int color, byte irisBlockMaterialId, byte skylight, byte blocklight) public void addQuadDown(short x, short y, short z, short width, short wz, int color, byte irisBlockMaterialId, byte skylight, byte blocklight)
{ {
if (skipQuadsWithZeroSkylight && skylight == 0 && y < skyLightCullingBelow)
return;
BufferQuad quad = new BufferQuad(x, y, z, width, wz, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.DOWN); BufferQuad quad = new BufferQuad(x, y, z, width, wz, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.DOWN);
ArrayList<BufferQuad> qs = (doTransparency && ColorUtil.getAlpha(color) < 255) ArrayList<BufferQuad> qs = (doTransparency && ColorUtil.getAlpha(color) < 255)
? transparentQuads[EDhDirection.DOWN.ordinal()] : opaqueQuads[EDhDirection.DOWN.ordinal()]; ? transparentQuads[EDhDirection.DOWN.ordinal()] : opaqueQuads[EDhDirection.DOWN.ordinal()];
if (!qs.isEmpty() if (!qs.isEmpty() &&
&& (qs.get(qs.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.EastWest) (qs.get(qs.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.EastWest)
|| qs.get(qs.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.NorthSouthOrUpDown)) || qs.get(qs.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.NorthSouthOrUpDown))
) )
{ {
premergeCount++; premergeCount++;
return; return;
@@ -206,124 +225,10 @@ public class LodQuadBuilder
//=================//
// data finalizing //
//=================//
/** runs any final data cleanup, merging, etc. */
public void finalizeData() { this.mergeQuads(); }
/** Uses Greedy meshing to merge this builder's Quads. */
public void mergeQuads()
{
long mergeCount = 0; // can be used for debugging
long preQuadsCount = this.getCurrentOpaqueQuadsCount() + this.getCurrentTransparentQuadsCount();
if (preQuadsCount <= 1)
{
return;
}
for (int directionIndex = 0; directionIndex < 6; directionIndex++)
{
mergeCount += mergeQuadsInternal(this.opaqueQuads, directionIndex, BufferMergeDirectionEnum.EastWest);
if (this.doTransparency)
{
mergeCount += mergeQuadsInternal(this.transparentQuads, directionIndex, BufferMergeDirectionEnum.EastWest);
}
// only run the second merge if the face is the top or bottom
if (directionIndex == EDhDirection.UP.ordinal() || directionIndex == EDhDirection.DOWN.ordinal())
{
mergeCount += mergeQuadsInternal(this.opaqueQuads, directionIndex, BufferMergeDirectionEnum.NorthSouthOrUpDown);
if (this.doTransparency)
{
mergeCount += mergeQuadsInternal(this.transparentQuads, directionIndex, BufferMergeDirectionEnum.NorthSouthOrUpDown);
}
}
}
//long postQuadsCount = this.getCurrentOpaqueQuadsCount() + this.getCurrentTransparentQuadsCount();
//LOGGER.trace("Merged "+mergeCount+"/"+preQuadsCount+"("+(mergeCount / (double) preQuadsCount)+") quads");
}
/** Merges all of this builder's quads for the given directionIndex (up, down, left, etc.) in the given direction */
private static long mergeQuadsInternal(ArrayList<BufferQuad>[] list, int directionIndex, BufferMergeDirectionEnum mergeDirection)
{
if (list[directionIndex].size() <= 1)
return 0;
list[directionIndex].sort((objOne, objTwo) -> objOne.compare(objTwo, mergeDirection));
long mergeCount = 0;
ListIterator<BufferQuad> iter = list[directionIndex].listIterator();
BufferQuad currentQuad = iter.next();
while (iter.hasNext())
{
BufferQuad nextQuad = iter.next();
if (currentQuad.tryMerge(nextQuad, mergeDirection))
{
// merge successful, attempt to merge the next quad
mergeCount++;
iter.set(null);
}
else
{
// merge fail, move on to the next quad
currentQuad = nextQuad;
}
}
list[directionIndex].removeIf(Objects::isNull);
return mergeCount;
}
//==============// //==============//
// buffer setup // // add vertices //
//==============// //==============//
public ArrayList<ByteBuffer> makeOpaqueVertexBuffers() { return this.makeVertexBuffers(this.opaqueQuads); }
public ArrayList<ByteBuffer> makeTransparentVertexBuffers() { return this.makeVertexBuffers(this.transparentQuads); }
private ArrayList<ByteBuffer> makeVertexBuffers(ArrayList<BufferQuad>[] quadList)
{
ArrayList<ByteBuffer> byteBufferList = new ArrayList<>(3);
ByteBuffer buffer = null;
for (int directionIndex = 0; directionIndex < 6; directionIndex++)
{
// ignore empty directions
if (quadList[directionIndex].isEmpty())
{
continue;
}
// put all the quads in this direction into the buffer
for (int quadIndex = 0; quadIndex < quadList[directionIndex].size(); quadIndex++)
{
// if this is the first iteration or the buffer is full,
// create a new buffer
if (buffer == null || !buffer.hasRemaining())
{
buffer = MemoryUtil.memAlloc(ColumnRenderBuffer.FULL_SIZED_BUFFER);
byteBufferList.add(buffer);
}
this.putQuad(buffer, quadList[directionIndex].get(quadIndex));
}
}
// rewind all the buffers so they can be read from
for (int i = 0; i < byteBufferList.size(); i++)
{
buffer = byteBufferList.get(i);
buffer.limit(buffer.position());
buffer.rewind();
}
return byteBufferList;
}
private void putQuad(ByteBuffer bb, BufferQuad quad) private void putQuad(ByteBuffer bb, BufferQuad quad)
{ {
int[][] quadBase = DIRECTION_VERTEX_IBO_QUAD[quad.direction.ordinal()]; int[][] quadBase = DIRECTION_VERTEX_IBO_QUAD[quad.direction.ordinal()];
@@ -381,10 +286,10 @@ public class LodQuadBuilder
if (quad.direction.getAxis().isHorizontal() || quad.direction == EDhDirection.DOWN) if (quad.direction.getAxis().isHorizontal() || quad.direction == EDhDirection.DOWN)
{ {
if (this.grassSideRenderingMode == EDhApiGrassSideRendering.AS_DIRT if (this.grassSideRenderingMode == EDhApiGrassSideRendering.AS_DIRT
// if we want the color to fade, only apply the dirt color to the bottom vertices // if we want the color to fade, only apply the dirt color to the bottom vertices
|| (this.grassSideRenderingMode == EDhApiGrassSideRendering.FADE_TO_DIRT && quadBase[i][1] == 0) || (this.grassSideRenderingMode == EDhApiGrassSideRendering.FADE_TO_DIRT && quadBase[i][1] == 0)
// always render the bottom as dirt // always render the bottom as dirt
|| quad.direction == EDhDirection.DOWN) || quad.direction == EDhDirection.DOWN)
{ {
// for horizontal and bottom faces of grass blocks, use the dirt color to // for horizontal and bottom faces of grass blocks, use the dirt color to
// prevent green cliff walls // prevent green cliff walls
@@ -392,7 +297,7 @@ public class LodQuadBuilder
color = ColorUtil.applyShade(color, MC.getShade(quad.direction)); color = ColorUtil.applyShade(color, MC.getShade(quad.direction));
} }
} }
} }
} }
} }
@@ -406,6 +311,7 @@ public class LodQuadBuilder
mx, my, mz); mx, my, mz);
} }
} }
private void putVertex(ByteBuffer bb, short x, short y, short z, int color, byte normalIndex, byte irisBlockMaterialId, byte skylight, byte blocklight, int mx, int my, int mz) private void putVertex(ByteBuffer bb, short x, short y, short z, int color, byte normalIndex, byte irisBlockMaterialId, byte skylight, byte blocklight, int mx, int my, int mz)
{ {
skylight %= 16; skylight %= 16;
@@ -446,6 +352,389 @@ public class LodQuadBuilder
//=================//
// data finalizing //
//=================//
/** runs any final data cleanup, merging, etc. */
public void finalizeData() { this.mergeQuads(); }
/** Uses Greedy meshing to merge this builder's Quads. */
public void mergeQuads()
{
long mergeCount = 0;
long preQuadsCount = this.getCurrentOpaqueQuadsCount() + this.getCurrentTransparentQuadsCount();
if (preQuadsCount <= 1)
{
return;
}
for (int directionIndex = 0; directionIndex < 6; directionIndex++)
{
mergeCount += mergeQuadsInternal(this.opaqueQuads, directionIndex, BufferMergeDirectionEnum.EastWest);
if (this.doTransparency)
{
mergeCount += mergeQuadsInternal(this.transparentQuads, directionIndex, BufferMergeDirectionEnum.EastWest);
}
// only run the second merge if the face is the top or bottom
if (directionIndex == EDhDirection.UP.ordinal() || directionIndex == EDhDirection.DOWN.ordinal())
{
mergeCount += mergeQuadsInternal(this.opaqueQuads, directionIndex, BufferMergeDirectionEnum.NorthSouthOrUpDown);
if (this.doTransparency)
{
mergeCount += mergeQuadsInternal(this.transparentQuads, directionIndex, BufferMergeDirectionEnum.NorthSouthOrUpDown);
}
}
}
long postQuadsCount = this.getCurrentOpaqueQuadsCount() + this.getCurrentTransparentQuadsCount();
LOGGER.debug("Merged "+mergeCount+"/"+preQuadsCount+"("+(mergeCount / (double) preQuadsCount)+") quads");
}
/** Merges all of this builder's quads for the given directionIndex (up, down, left, etc.) in the given direction */
private static long mergeQuadsInternal(ArrayList<BufferQuad>[] list, int directionIndex, BufferMergeDirectionEnum mergeDirection)
{
if (list[directionIndex].size() <= 1)
return 0;
list[directionIndex].sort((objOne, objTwo) -> objOne.compare(objTwo, mergeDirection));
long mergeCount = 0;
ListIterator<BufferQuad> iter = list[directionIndex].listIterator();
BufferQuad currentQuad = iter.next();
while (iter.hasNext())
{
BufferQuad nextQuad = iter.next();
if (currentQuad.tryMerge(nextQuad, mergeDirection))
{
// merge successful, attempt to merge the next quad
mergeCount++;
iter.set(null);
}
else
{
// merge fail, move on to the next quad
currentQuad = nextQuad;
}
}
list[directionIndex].removeIf(Objects::isNull);
return mergeCount;
}
//==============//
// buffer setup //
//==============//
public Iterator<ByteBuffer> makeOpaqueVertexBuffers()
{
return new Iterator<ByteBuffer>()
{
final ByteBuffer bb = ByteBuffer.allocateDirect(ColumnRenderBuffer.FULL_SIZED_BUFFER)
.order(ByteOrder.nativeOrder());
int dir = skipEmpty(0);
int quad = 0;
private int skipEmpty(int d)
{
while (d < 6 && opaqueQuads[d].isEmpty())
{
d++;
}
return d;
}
@Override
public boolean hasNext()
{
return dir < 6;
}
@Override
public ByteBuffer next()
{
if (dir >= 6)
{
return null;
}
bb.clear();
bb.limit(ColumnRenderBuffer.FULL_SIZED_BUFFER);
while (bb.hasRemaining() && dir < 6)
{
writeData();
}
bb.limit(bb.position());
bb.rewind();
return bb;
}
private void writeData()
{
int i = quad;
for (; i < opaqueQuads[dir].size(); i++)
{
if (!bb.hasRemaining())
{
break;
}
putQuad(bb, opaqueQuads[dir].get(i));
}
if (i >= opaqueQuads[dir].size())
{
quad = 0;
dir++;
dir = skipEmpty(dir);
}
else
{
quad = i;
}
}
};
}
public Iterator<ByteBuffer> makeTransparentVertexBuffers()
{
return new Iterator<ByteBuffer>()
{
final ByteBuffer bb = ByteBuffer.allocateDirect(ColumnRenderBuffer.FULL_SIZED_BUFFER)
.order(ByteOrder.nativeOrder());
int directionIndex = this.skipEmptyDirectionIndices(0);
int quad = 0;
private int skipEmptyDirectionIndices(int directionIndex)
{
while (directionIndex < 6 &&
(LodQuadBuilder.this.transparentQuads[directionIndex] == null
|| LodQuadBuilder.this.transparentQuads[directionIndex].isEmpty()))
{
directionIndex++;
}
return directionIndex;
}
@Override
public boolean hasNext() { return this.directionIndex < 6; }
@Override
public ByteBuffer next()
{
if (this.directionIndex >= 6)
{
return null;
}
this.bb.clear();
this.bb.limit(ColumnRenderBuffer.FULL_SIZED_BUFFER);
while (this.bb.hasRemaining() && this.directionIndex < 6)
{
this.writeData();
}
this.bb.limit(this.bb.position());
this.bb.rewind();
return this.bb;
}
private void writeData()
{
int i = this.quad;
for (; i < LodQuadBuilder.this.transparentQuads[this.directionIndex].size(); i++)
{
if (!this.bb.hasRemaining())
{
break;
}
putQuad(this.bb, LodQuadBuilder.this.transparentQuads[this.directionIndex].get(i));
}
if (i >= LodQuadBuilder.this.transparentQuads[this.directionIndex].size())
{
this.quad = 0;
this.directionIndex++;
this.directionIndex = this.skipEmptyDirectionIndices(this.directionIndex);
}
else
{
this.quad = i;
}
}
};
}
public interface BufferFiller
{
/** If true: more data needs to be filled */
boolean fill(GLVertexBuffer vbo);
}
public BufferFiller makeOpaqueBufferFiller(EDhApiGpuUploadMethod method)
{
return new BufferFiller()
{
int dir = 0;
int quad = 0;
public boolean fill(GLVertexBuffer vbo)
{
if (dir >= 6)
{
vbo.setVertexCount(0);
return false;
}
int numOfQuads = _countRemainingQuads();
if (numOfQuads > ColumnRenderBuffer.MAX_QUADS_PER_BUFFER)
numOfQuads = ColumnRenderBuffer.MAX_QUADS_PER_BUFFER;
if (numOfQuads == 0)
{
vbo.setVertexCount(0);
return false;
}
ByteBuffer bb = vbo.mapBuffer(numOfQuads * ColumnRenderBuffer.QUADS_BYTE_SIZE, method,
ColumnRenderBuffer.FULL_SIZED_BUFFER);
if (bb == null)
throw new NullPointerException("mapBuffer returned null");
bb.clear();
bb.limit(numOfQuads * ColumnRenderBuffer.QUADS_BYTE_SIZE);
while (bb.hasRemaining() && dir < 6)
{
writeData(bb);
}
bb.rewind();
vbo.unmapBuffer();
vbo.setVertexCount(numOfQuads * 4);
return dir < 6;
}
private int _countRemainingQuads()
{
int a = opaqueQuads[dir].size() - quad;
for (int i = dir + 1; i < opaqueQuads.length; i++)
{
a += opaqueQuads[i].size();
}
return a;
}
private void writeData(ByteBuffer bb)
{
int startQ = quad;
int i = startQ;
for (i = startQ; i < opaqueQuads[dir].size(); i++)
{
if (!bb.hasRemaining())
{
break;
}
putQuad(bb, opaqueQuads[dir].get(i));
}
if (i >= opaqueQuads[dir].size())
{
quad = 0;
dir++;
while (dir < 6 && opaqueQuads[dir].isEmpty())
{
dir++;
}
}
else
{
quad = i;
}
}
};
}
public BufferFiller makeTransparentBufferFiller(EDhApiGpuUploadMethod method)
{
return new BufferFiller()
{
int dir = 0;
int quad = 0;
public boolean fill(GLVertexBuffer vbo)
{
if (dir >= 6)
{
vbo.setVertexCount(0);
return false;
}
int numOfQuads = _countRemainingQuads();
if (numOfQuads > ColumnRenderBuffer.MAX_QUADS_PER_BUFFER)
numOfQuads = ColumnRenderBuffer.MAX_QUADS_PER_BUFFER;
if (numOfQuads == 0)
{
vbo.setVertexCount(0);
return false;
}
ByteBuffer bb = vbo.mapBuffer(numOfQuads * ColumnRenderBuffer.QUADS_BYTE_SIZE, method,
ColumnRenderBuffer.FULL_SIZED_BUFFER);
if (bb == null)
throw new NullPointerException("mapBuffer returned null");
bb.clear();
bb.limit(numOfQuads * ColumnRenderBuffer.QUADS_BYTE_SIZE);
while (bb.hasRemaining() && dir < 6)
{
writeData(bb);
}
bb.rewind();
vbo.unmapBuffer();
vbo.setVertexCount(numOfQuads * 4);
return dir < 6;
}
private int _countRemainingQuads()
{
int a = transparentQuads[dir].size() - quad;
for (int i = dir + 1; i < transparentQuads.length; i++)
{
a += transparentQuads[i].size();
}
return a;
}
private void writeData(ByteBuffer bb)
{
int startQ = quad;
int i = startQ;
for (i = startQ; i < transparentQuads[dir].size(); i++)
{
if (!bb.hasRemaining())
{
break;
}
putQuad(bb, transparentQuads[dir].get(i));
}
if (i >= transparentQuads[dir].size())
{
quad = 0;
dir++;
while (dir < 6 && transparentQuads[dir].isEmpty())
{
dir++;
}
}
else
{
quad = i;
}
}
};
}
//=========// //=========//
// getters // // getters //
//=========// //=========//
@@ -0,0 +1,188 @@
package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.coreapi.util.StringUtil;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.opengl.GL32;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
/** Used to allow multiple {@link ColumnRenderBuffer}'s to be put in a single VBO. */
public class SharedVbo
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public static final SharedVbo OPAQUE = new SharedVbo(2_000_000_000L/*1GB*/, 1024 * 1024/*1MB*/);
public static final SharedVbo TRANSPARENT = new SharedVbo(2_000_000_000L/*1GB*/, 1024 * 1024/*1MB*/);
public final int vboId;
private final long bufferTotalByteSize;
/** the length of a single chunk of this VBO in bytes. */
private final int chunkByteSize;
private final ConcurrentHashMap<Long, BufferSlice> bufferSliceByStartingIndex = new ConcurrentHashMap<>();
private long nextMemoryAddress = 0L;
private final Queue<BufferSlice> availableSlices = new ArrayDeque<>();
//=============//
// constructor //
//=============//
public SharedVbo(long totalSize, int chunkByteSize)
{
LodUtil.assertTrue(GLProxy.getInstance().runningOnRenderThread(), "Buffer Handler has to be created on the render thread.");
this.bufferTotalByteSize = totalSize;
this.chunkByteSize = chunkByteSize;
// Generate and bind the VBO
this.vboId = GL32.glGenBuffers();
GL32.glBindBuffer(GL32.GL_ARRAY_BUFFER, this.vboId);
GL32.glBufferData(GL32.GL_ARRAY_BUFFER, this.bufferTotalByteSize, GL32.GL_DYNAMIC_DRAW);
GL32.glBindBuffer(GL32.GL_ARRAY_BUFFER, 0);
}
//============//
// allocation //
//============//
@Nullable
public SharedVbo.BufferSlice allocateSlice()
{
BufferSlice availableSlice = this.availableSlices.poll();
if (availableSlice != null)
{
return availableSlice;
}
// Find the first free chunk
for (long startingIndex = this.nextMemoryAddress; startingIndex < this.bufferTotalByteSize; startingIndex += this.chunkByteSize)
{
// check if this section is free
BufferSlice newSlice = new BufferSlice(startingIndex, this.chunkByteSize);
if (this.bufferSliceByStartingIndex.putIfAbsent(startingIndex, newSlice) == null)
{
this.nextMemoryAddress = startingIndex;
return newSlice;
}
}
return null; // No free chunk found
}
public void deallocate(BufferSlice[] slices)
{
if (slices != null)
{
for (BufferSlice slice : slices)
{
if (slice != null)
{
this.bufferSliceByStartingIndex.remove(slice.startIndex);
this.availableSlices.add(slice);
}
}
}
}
public void deallocate(BufferSlice slice)
{
if (slice != null)
{
this.bufferSliceByStartingIndex.remove(slice.startIndex);
this.availableSlices.add(slice);
}
}
public void clear()
{
this.bufferSliceByStartingIndex.clear();
}
//=================//
// buffer handling //
//=================//
public void updateBuffer(BufferSlice chunk, ByteBuffer buffer)
{
int size = buffer.limit() - buffer.position();
if (size > chunk.length)
{
// if this was fired that means we didn't split up the buffer into the right size
// if this isn't stopped the buffer will overwrite an adjacent section and cause incorrect rendering
throw new RuntimeException("Programmer error: Uploaded buffer bigger than the allocated area. Allocated: ["+chunk.length+"], buffer: ["+size+"]");
}
GL32.glBindBuffer(GL32.GL_ARRAY_BUFFER, this.vboId);
GL32.glBufferSubData(GL32.GL_ARRAY_BUFFER, chunk.startIndex, buffer);
GL32.glBindBuffer(GL32.GL_ARRAY_BUFFER, 0);
chunk.vertexCount = size / LodUtil.LOD_VERTEX_FORMAT.getByteSize();
}
public void bind() { GL32.glBindBuffer(GL32.GL_ARRAY_BUFFER, this.vboId); }
public void unbind() { GL32.glBindBuffer(GL32.GL_ARRAY_BUFFER, 0); }
//=======//
// debug //
//=======//
public String getDebugMenuString()
{
long maxChunkCount = (this.bufferTotalByteSize / this.chunkByteSize);
long chunkCount = 0;
long allocatedBytes = 0;
for (BufferSlice slice : this.bufferSliceByStartingIndex.values())
{
allocatedBytes += slice.length;
chunkCount++;
}
return "Slices: ["+F3Screen.NUMBER_FORMAT.format(chunkCount)+"/"+F3Screen.NUMBER_FORMAT.format(maxChunkCount)+"], " +
"Mem: ["+StringUtil.convertByteCountToHumanReadableSI(allocatedBytes)+"/"+StringUtil.convertByteCountToHumanReadableSI(this.bufferTotalByteSize)+"]";
}
//================//
// helper classes //
//================//
/** represents a single allocated slice of the parent VBO */
public static class BufferSlice
{
public final long startIndex;
public final int length;
public int vertexCount;
//=============//
// constructor //
//=============//
public BufferSlice(long startIndex, int length)
{
this.startIndex = startIndex;
this.length = length;
}
}
}
@@ -20,7 +20,6 @@
package com.seibel.distanthorizons.core.dataObjects.render.columnViews; package com.seibel.distanthorizons.core.dataObjects.render.columnViews;
import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource;
import com.seibel.distanthorizons.core.util.RenderDataPointUtil; import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
@@ -29,60 +28,41 @@ import java.util.Arrays;
public final class ColumnArrayView implements IColumnDataView public final class ColumnArrayView implements IColumnDataView
{ {
public final LongArrayList data; public final LongArrayList data;
/**
* How many data points are currently being represented by this view. <br>
* Will be equal to or less than {@link ColumnArrayView#verticalSize}.
*/
public final int size; public final int size;
/** public final int offset; // offset in longs
* Vertical size in data points. <Br> /** can be 0 if this column was created for an empty data source */
* Can be 0 if this column was created for an empty data source. public final int vertSize; // vertical size in longs
*/
public final int verticalSize;
/**
* Where the relative starting index is in the {@link ColumnArrayView#data} array
* if this view is representing part of a {@link ColumnRenderSource}.
*/
public final int offset;
//=============// public ColumnArrayView(LongArrayList data, int size, int offset, int vertSize)
// constructor //
//=============//
public ColumnArrayView(LongArrayList data, int size, int offset, int verticalSize)
{ {
this.data = data; this.data = data;
this.size = size; this.size = size;
this.offset = offset; this.offset = offset;
this.verticalSize = verticalSize; this.vertSize = vertSize;
} }
//=====================//
// getters and setters //
//=====================//
@Override @Override
public long get(int index) { return data.getLong(index + offset); } public long get(int index) { return data.getLong(index + offset); }
public void set(int index, long value) { data.set(index + offset, value); } public void set(int index, long value) { data.set(index + offset, value); }
@Override @Override
public int size() { return size; } public int size() { return size; }
@Override
public int verticalSize() { return verticalSize; }
@Override @Override
public int dataCount() { return (this.verticalSize != 0) ? (this.size / this.verticalSize) : 0; } // TODO what does the divide by mean? public int verticalSize() { return vertSize; }
@Override
public int dataCount() { return (this.vertSize != 0) ? (this.size / this.vertSize) : 0; }
@Override @Override
public ColumnArrayView subView(int dataIndexStart, int dataCount) public ColumnArrayView subView(int dataIndexStart, int dataCount)
{ {
return new ColumnArrayView(data, dataCount * verticalSize, offset + dataIndexStart * verticalSize, verticalSize); return new ColumnArrayView(data, dataCount * vertSize, offset + dataIndexStart * vertSize, vertSize);
} }
public void fill(long value) { Arrays.fill(data.elements(), offset, offset + size, value); } public void fill(long value) { Arrays.fill(data.elements(), offset, offset + size, value); }
@@ -90,7 +70,7 @@ public final class ColumnArrayView implements IColumnDataView
public void copyFrom(IColumnDataView source) { copyFrom(source, 0); } public void copyFrom(IColumnDataView source) { copyFrom(source, 0); }
public void copyFrom(IColumnDataView source, int outputDataIndexOffset) public void copyFrom(IColumnDataView source, int outputDataIndexOffset)
{ {
if (source.verticalSize() > verticalSize) if (source.verticalSize() > vertSize)
{ {
throw new IllegalArgumentException("source verticalSize must be <= self's verticalSize to copy"); throw new IllegalArgumentException("source verticalSize must be <= self's verticalSize to copy");
} }
@@ -98,19 +78,19 @@ public final class ColumnArrayView implements IColumnDataView
{ {
throw new IllegalArgumentException("dataIndexStart + source.dataCount() must be <= self.dataCount() to copy"); throw new IllegalArgumentException("dataIndexStart + source.dataCount() must be <= self.dataCount() to copy");
} }
else if (source.verticalSize() != verticalSize) else if (source.verticalSize() != vertSize)
{ {
for (int i = 0; i < source.dataCount(); i++) for (int i = 0; i < source.dataCount(); i++)
{ {
int outputOffset = offset + outputDataIndexOffset * verticalSize + i * verticalSize; int outputOffset = offset + outputDataIndexOffset * vertSize + i * vertSize;
source.subView(i, 1).copyTo(data.elements(), outputOffset, source.verticalSize()); source.subView(i, 1).copyTo(data.elements(), outputOffset, source.verticalSize());
Arrays.fill(data.elements(), outputOffset + source.verticalSize(), Arrays.fill(data.elements(), outputOffset + source.verticalSize(),
outputOffset + verticalSize, 0); outputOffset + vertSize, 0);
} }
} }
else else
{ {
source.copyTo(data.elements(), offset + outputDataIndexOffset * verticalSize, source.size()); source.copyTo(data.elements(), offset + outputDataIndexOffset * vertSize, source.size());
} }
} }
@@ -123,19 +103,19 @@ public final class ColumnArrayView implements IColumnDataView
{ {
throw new IllegalArgumentException("Cannot merge views of different sizes"); throw new IllegalArgumentException("Cannot merge views of different sizes");
} }
if (verticalSize != source.verticalSize) if (vertSize != source.vertSize)
{ {
throw new IllegalArgumentException("Cannot merge views of different vertical sizes"); throw new IllegalArgumentException("Cannot merge views of different vertical sizes");
} }
boolean anyChange = false; boolean anyChange = false;
for (int o = 0; o < (source.size() * verticalSize); o += verticalSize) for (int o = 0; o < (source.size() * vertSize); o += vertSize)
{ {
if (override) if (override)
{ {
if (RenderDataPointUtil.compareDatapointPriority(source.get(o), get(o)) >= 0) if (RenderDataPointUtil.compareDatapointPriority(source.get(o), get(o)) >= 0)
{ {
anyChange = true; anyChange = true;
System.arraycopy(source.data, source.offset + o, data, offset + o, verticalSize); System.arraycopy(source.data, source.offset + o, data, offset + o, vertSize);
} }
} }
else else
@@ -143,7 +123,7 @@ public final class ColumnArrayView implements IColumnDataView
if (RenderDataPointUtil.compareDatapointPriority(source.get(o), get(o)) > 0) if (RenderDataPointUtil.compareDatapointPriority(source.get(o), get(o)) > 0)
{ {
anyChange = true; anyChange = true;
System.arraycopy(source.data, source.offset + o, data, offset + o, verticalSize); System.arraycopy(source.data, source.offset + o, data, offset + o, vertSize);
} }
} }
} }
@@ -157,7 +137,7 @@ public final class ColumnArrayView implements IColumnDataView
throw new IllegalArgumentException("Cannot copy and resize to views with different dataCounts"); throw new IllegalArgumentException("Cannot copy and resize to views with different dataCounts");
} }
if (this.verticalSize >= source.verticalSize()) if (this.vertSize >= source.verticalSize())
{ {
this.copyFrom(source); this.copyFrom(source);
} }
@@ -180,18 +160,12 @@ public final class ColumnArrayView implements IColumnDataView
RenderDataPointUtil.mergeMultiData(source, this); RenderDataPointUtil.mergeMultiData(source, this);
} }
//================//
// base overrides //
//================//
@Override @Override
public String toString() public String toString()
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("S:").append(size); sb.append("S:").append(size);
sb.append(" V:").append(verticalSize); sb.append(" V:").append(vertSize);
sb.append(" O:").append(offset); sb.append(" O:").append(offset);
sb.append(" ["); sb.append(" [");
@@ -208,7 +182,6 @@ public final class ColumnArrayView implements IColumnDataView
return sb.toString(); return sb.toString();
} }
public int getDataHash() public int getDataHash()
{ {
return arrayHash(data, offset, size); return arrayHash(data, offset, size);
@@ -0,0 +1,253 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.dataObjects.transformers;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import org.apache.logging.log4j.LogManager;
public class ChunkToLodBuilder implements AutoCloseable
{
public static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(), () -> Config.Client.Advanced.Logging.logLodBuilderEvent.get());
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
public static final long MAX_TICK_TIME_NS = 1000000000L / 20L;
private final ConcurrentHashMap<DhChunkPos, IChunkWrapper> concurrentChunkToBuildByChunkPos = new ConcurrentHashMap<>();
private final ConcurrentLinkedDeque<Task> concurrentTaskToBuildList = new ConcurrentLinkedDeque<>();
private final AtomicInteger runningCount = new AtomicInteger(0);
//==============//
// constructors //
//==============//
public ChunkToLodBuilder() { }
//=================//
// data generation //
//=================//
public CompletableFuture<FullDataSourceV2> tryGenerateData(IChunkWrapper chunkWrapper)
{
if (chunkWrapper == null)
{
throw new NullPointerException("ChunkWrapper cannot be null!");
}
IChunkWrapper oldChunk = this.concurrentChunkToBuildByChunkPos.put(chunkWrapper.getChunkPos(), chunkWrapper); // an Exchange operation
// If there's old chunk, that means we just replaced an unprocessed old request on generating data on this pos.
// if so, we can just return null to signal this, as the old request's future will instead be the proper one
// that will return the latest generated data.
if (oldChunk != null)
{
return null;
}
// Otherwise, it means we're the first to do so. Let's submit our task to this entry.
CompletableFuture<FullDataSourceV2> future = new CompletableFuture<>();
this.concurrentTaskToBuildList.addLast(new Task(chunkWrapper.getChunkPos(), future));
return future;
}
// TODO why on tick?
public void tick()
{
int threadCount = ThreadPoolUtil.getWorkerThreadCount();
if (this.runningCount.get() >= threadCount)
{
return;
}
else if (this.concurrentTaskToBuildList.isEmpty())
{
return;
}
else if (MC == null || !MC.playerExists())
{
// TODO handle server side properly
// MC hasn't finished loading (or is currently unloaded)
// can be uncommented if tasks aren't being cleared correctly
//this.clearCurrentTasks();
return;
}
ThreadPoolExecutor lodBuilderExecutor = ThreadPoolUtil.getChunkToLodBuilderExecutor();
if (lodBuilderExecutor == null)
{
return;
}
for (int i = 0; i < threadCount; i++)
{
this.runningCount.incrementAndGet();
try
{
CompletableFuture.runAsync(() ->
{
try
{
this.tickThreadTask();
}
finally
{
this.runningCount.decrementAndGet();
}
}, lodBuilderExecutor);
}
catch (RejectedExecutionException ignore) { /* the thread pool was probably shut down because it's size is being changed, just wait a sec and it should be back */ }
}
}
private void tickThreadTask()
{
long time = System.nanoTime();
int count = 0;
boolean allDone = false;
while (true)
{
// run until we either run out of time, or all tasks are complete
if (System.nanoTime() - time > MAX_TICK_TIME_NS && !this.concurrentTaskToBuildList.isEmpty())
{
break;
}
Task task = this.concurrentTaskToBuildList.pollFirst();
if (task == null)
{
allDone = true;
break;
}
count++;
IChunkWrapper latestChunk = this.concurrentChunkToBuildByChunkPos.remove(task.chunkPos); // Basically an Exchange operation
if (latestChunk == null)
{
LOGGER.error("Somehow Task at " + task.chunkPos + " has latestChunk as null. Skipping task.");
task.future.complete(null);
continue;
}
try
{
if (LodDataBuilder.canGenerateLodFromChunk(latestChunk))
{
FullDataSourceV2 dataSource = LodDataBuilder.createGeneratedDataSource(latestChunk);
if (dataSource != null)
{
task.future.complete(dataSource);
continue;
}
}
else if (task.generationAttemptExpirationTimeMs < System.currentTimeMillis())
{
// this task won't be re-queued
//LOGGER.trace("removed chunk "+task.chunkPos);
continue;
}
}
catch (Exception ex)
{
LOGGER.error("Error while processing Task at " + task.chunkPos, ex);
}
// Failed to build due to chunk not meeting requirement,
// re-add it to the queue so it can be tested next time
IChunkWrapper casChunk = this.concurrentChunkToBuildByChunkPos.putIfAbsent(task.chunkPos, latestChunk); // CAS operation with expected=null
if (casChunk == null || latestChunk.isStillValid()) // That means CAS have been successful
{
this.concurrentTaskToBuildList.addLast(task); // Then add back the same old task.
}
else // Else, it means someone managed to sneak in a new gen request in this pos. Then lets drop this old task.
{
task.future.complete(null);
}
count--;
}
long time2 = System.nanoTime();
if (!allDone)
{
//LOGGER.info("Completed {} tasks in {} in this tick", count, Duration.ofNanos(time2 - time));
}
else if (count > 0)
{
//LOGGER.info("Completed all {} tasks in {}", count, Duration.ofNanos(time2 - time));
}
}
/**
* should be called whenever changing levels/worlds
* to prevent trying to generate LODs for chunk(s) that are no longer loaded
* (which can cause exceptions)
*/
public void clearCurrentTasks()
{
this.concurrentTaskToBuildList.clear();
this.concurrentChunkToBuildByChunkPos.clear();
}
//==============//
// base methods //
//==============//
@Override
public void close() { this.clearCurrentTasks(); }
//================//
// helper classes //
//================//
private static class Task
{
public final DhChunkPos chunkPos;
public final CompletableFuture<FullDataSourceV2> future;
/** This is tracked so impossible tasks can be removed from the queue */
public long generationAttemptExpirationTimeMs = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(10);
Task(DhChunkPos chunkPos, CompletableFuture<FullDataSourceV2> future)
{
this.chunkPos = chunkPos;
this.future = future;
}
}
}
@@ -28,7 +28,7 @@ import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArra
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.pos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.util.ColorUtil; import com.seibel.distanthorizons.core.util.ColorUtil;
import com.seibel.distanthorizons.core.util.FullDataPointUtil; import com.seibel.distanthorizons.core.util.FullDataPointUtil;
@@ -38,7 +38,6 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -111,260 +110,33 @@ public class FullDataToRenderDataTransformer
} }
columnSource.markNotEmpty(); columnSource.markNotEmpty();
int baseX = DhSectionPos.getMinCornerBlockX(pos);
int baseZ = DhSectionPos.getMinCornerBlockZ(pos);
for (int x = 0; x < DhSectionPos.getWidthCountForLowerDetailedSection(pos, dataDetail); x++) if (dataDetail == columnSource.getDataDetailLevel())
{ {
for (int z = 0; z < DhSectionPos.getWidthCountForLowerDetailedSection(pos, dataDetail); z++) int baseX = DhSectionPos.getMinCornerBlockX(pos);
{ int baseZ = DhSectionPos.getMinCornerBlockZ(pos);
throwIfThreadInterrupted();
for (int x = 0; x < DhSectionPos.getWidthCountForLowerDetailedSection(pos, dataDetail); x++)
ColumnArrayView columnArrayView = columnSource.getVerticalDataPointView(x, z); {
LongArrayList dataColumn = fullDataSource.get(x, z); for (int z = 0; z < DhSectionPos.getWidthCountForLowerDetailedSection(pos, dataDetail); z++)
{
updateOrReplaceRenderDataViewColumnWithFullDataColumn( throwIfThreadInterrupted();
level, fullDataSource.mapping,
// bitshift is to account for LODs with a detail level greater than 0 so the block pos is correct ColumnArrayView columnArrayView = columnSource.getVerticalDataPointView(x, z);
baseX + BitShiftUtil.pow(x,dataDetail), baseZ + BitShiftUtil.pow(z,dataDetail), LongArrayList dataColumn = fullDataSource.get(x, z);
columnArrayView, dataColumn); convertColumnData(level, fullDataSource.mapping, baseX + x, baseZ + z, columnArrayView, dataColumn);
} }
} }
columnSource.fillDebugFlag(0, 0, ColumnRenderSource.SECTION_SIZE, ColumnRenderSource.SECTION_SIZE, ColumnRenderSource.DebugSourceFlag.FULL); columnSource.fillDebugFlag(0, 0, ColumnRenderSource.SECTION_SIZE, ColumnRenderSource.SECTION_SIZE, ColumnRenderSource.DebugSourceFlag.FULL);
return columnSource;
}
/** Updates the given {@link ColumnArrayView} to match the incoming Full data {@link LongArrayList} */
public static void updateOrReplaceRenderDataViewColumnWithFullDataColumn(
IDhClientLevel level,
FullDataPointIdMap fullDataMapping, int blockX, int blockZ,
ColumnArrayView columnArrayView,
LongArrayList fullDataColumn)
{
// we can't do anything if the full data is missing or empty
if (fullDataColumn == null || fullDataColumn.size() == 0)
{
return;
}
int fullDataLength = fullDataColumn.size();
if (fullDataLength <= columnArrayView.verticalSize())
{
// Directly use the arrayView since it fits.
setRenderColumnView(level, fullDataMapping, blockX, blockZ, columnArrayView, fullDataColumn);
} }
else else
{ {
// expand the ColumnArrayView to fit the new larger max vertical size throw new UnsupportedOperationException("To be implemented");
ColumnArrayView newColumnArrayView = new ColumnArrayView(new LongArrayList(new long[fullDataLength]), fullDataLength, 0, fullDataLength); //FIXME: Implement different size creation of renderData
setRenderColumnView(level, fullDataMapping, blockX, blockZ, newColumnArrayView, fullDataColumn);
columnArrayView.changeVerticalSizeFrom(newColumnArrayView);
}
}
private static void setRenderColumnView(
IDhClientLevel level, FullDataPointIdMap fullDataMapping,
int blockX, int blockZ,
ColumnArrayView renderColumnData, LongArrayList fullColumnData)
{
//===============//
// config values //
//===============//
boolean ignoreNonCollidingBlocks = (Config.Client.Advanced.Graphics.Quality.blocksToIgnore.get() == EDhApiBlocksToAvoid.NON_COLLIDING);
boolean colorBelowWithAvoidedBlocks = Config.Client.Advanced.Graphics.Quality.tintWithAvoidedBlocks.get();
HashSet<IBlockStateWrapper> blockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredBlocks(level.getLevelWrapper());
HashSet<IBlockStateWrapper> caveBlockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredCaveBlocks(level.getLevelWrapper());
int caveCullingMaxY = Config.Client.Advanced.Graphics.AdvancedGraphics.caveCullingHeight.get() - level.getMinY();
boolean caveCullingEnabled =
Config.Client.Advanced.Graphics.AdvancedGraphics.enableCaveCulling.get()
&& (
// dimensions with a ceiling will be all caves so we don't want cave culling
!level.getLevelWrapper().hasCeiling()
// the end has a lot of overhangs with 0 lighting above the void, which look broken with
// the current cave culling logic (this could probably be improved, but just skipping it works best for now)
&& !level.getLevelWrapper().getDimensionType().isTheEnd()
);
boolean isColumnVoid = true;
int colorToApplyToNextBlock = -1;
int lastColor = 0;
int lastBottom = -10_000;
int skylightToApplyToNextBlock = -1;
int blocklightToApplyToNextBlock = -1;
int renderDataIndex = 0;
//==================================//
// convert full data to render data //
//==================================//
// goes from the top down
for (int fullDataIndex = 0; fullDataIndex < fullColumnData.size(); fullDataIndex++)
{
long fullData = fullColumnData.getLong(fullDataIndex);
int bottomY = FullDataPointUtil.getBottomY(fullData);
int blockHeight = FullDataPointUtil.getHeight(fullData);
int topY = bottomY + blockHeight;
int id = FullDataPointUtil.getId(fullData);
int blockLight = FullDataPointUtil.getBlockLight(fullData);
int skyLight = FullDataPointUtil.getSkyLight(fullData);
IBiomeWrapper biome;
IBlockStateWrapper block;
try
{
biome = fullDataMapping.getBiomeWrapper(id);
block = fullDataMapping.getBlockStateWrapper(id);
}
catch (IndexOutOfBoundsException e)
{
if (!brokenPos.contains(fullDataMapping.getPos()))
{
brokenPos.add(fullDataMapping.getPos());
String dimName = level.getLevelWrapper().getDimensionName();
LOGGER.warn("Unable to get data point with id ["+id+"] " +
"(Max possible ID: ["+fullDataMapping.getMaxValidId()+"]) " +
"for pos ["+fullDataMapping.getPos()+"] in dimension ["+dimName+"]. " +
"Error: ["+e.getMessage()+"]. " +
"Further errors for this position won't be logged.");
}
// don't render broken data
continue;
}
//====================//
// ignored block and //
// cave culling check //
//====================//
boolean ignoreBlock = blockStatesToIgnore.contains(block);
boolean caveBlock = caveBlockStatesToIgnore.contains(block);
if (caveBlock)
{
if (caveCullingEnabled
// assume this data point is underground if it has no sky-light
&& skyLight == LodUtil.MIN_MC_LIGHT
// ignore caves above a certain height to prevent floating islands from having walls underneath them
&& topY < caveCullingMaxY
// cave culling shouldn't happen when at the top of the world
&& renderDataIndex != 0 && fullDataIndex != 0
// cave culling can't happen when at the bottom of the world
&& (fullDataIndex+1) < fullColumnData.size())
{
// we need to get the next sky/block lights because
// the air block here will always have a light of 0/0 due to only the top of the LOD's light being saved.
long nextFullData = fullColumnData.getLong(fullDataIndex+1);
int nextSkyLight = FullDataPointUtil.getSkyLight(nextFullData);
if (nextSkyLight == LodUtil.MIN_MC_LIGHT
&& ColorUtil.getAlpha(lastColor) == 255)
{
// replace the previous block with new bottom
long columnData = renderColumnData.get(renderDataIndex - 1);
columnData = RenderDataPointUtil.setYMin(columnData, bottomY);
renderColumnData.set(renderDataIndex - 1, columnData);
}
continue;
}
if (ignoreBlock)
{
// this is a merged block and a cave block, so it should never be rendered
continue;
}
}
else if (ignoreBlock)
{
// this is an ignored block, but shouldn't be merged like a cave block
continue;
}
//=======================//
// non-solid block check //
//=======================//
if (ignoreNonCollidingBlocks
&& !block.isSolid() && !block.isLiquid() && block.getOpacity() != LodUtil.BLOCK_FULLY_OPAQUE)
{
if (colorBelowWithAvoidedBlocks)
{
int tempColor = level.computeBaseColor(new DhBlockPos(blockX, bottomY + level.getMinY(), blockZ), biome, block);
// don't transfer the color when alpha is 0
// this prevents issues if grass is transparent
if (ColorUtil.getAlpha(tempColor) != 0)
{
colorToApplyToNextBlock = ColorUtil.setAlpha(tempColor,255);
skylightToApplyToNextBlock = skyLight;
blocklightToApplyToNextBlock = blockLight;
}
}
// skip this non-colliding block
continue;
}
int color;
if (colorToApplyToNextBlock == -1)
{
// use this block's color
color = level.computeBaseColor(new DhBlockPos(blockX, bottomY + level.getMinY(), blockZ), biome, block);
}
else
{
// use the previous block's color
color = colorToApplyToNextBlock;
colorToApplyToNextBlock = -1;
skyLight = skylightToApplyToNextBlock;
blockLight = blocklightToApplyToNextBlock;
}
//=============================//
// merge same-colored adjacent //
//=============================//
// check if they share a top-bottom face and if they have same color
if (color == lastColor && bottomY + blockHeight == lastBottom && renderDataIndex > 0)
{
//replace the previous block with new bottom
long columnData = renderColumnData.get(renderDataIndex - 1);
columnData = RenderDataPointUtil.setYMin(columnData, bottomY);
renderColumnData.set(renderDataIndex - 1, columnData);
}
else
{
// add the block
isColumnVoid = false;
long columnData = RenderDataPointUtil.createDataPoint(bottomY + blockHeight, bottomY, color, skyLight, blockLight, block.getMaterialId());
renderColumnData.set(renderDataIndex, columnData);
renderDataIndex++;
}
lastBottom = bottomY;
lastColor = color;
}
if (isColumnVoid)
{
renderColumnData.set(0, RenderDataPointUtil.EMPTY_DATA);
} }
return columnSource;
} }
@@ -386,4 +158,168 @@ public class FullDataToRenderDataTransformer
} }
} }
}
// TODO what does this mean?
private static void iterateAndConvert(
IDhClientLevel level, FullDataPointIdMap fullDataMapping,
int blockX, int blockZ,
ColumnArrayView renderColumnData, LongArrayList fullColumnData)
{
boolean avoidSolidBlocks = (Config.Client.Advanced.Graphics.Quality.blocksToIgnore.get() == EDhApiBlocksToAvoid.NON_COLLIDING);
boolean colorBelowWithAvoidedBlocks = Config.Client.Advanced.Graphics.Quality.tintWithAvoidedBlocks.get();
HashSet<IBlockStateWrapper> blockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredBlocks(level.getLevelWrapper());
boolean isVoid = true;
int colorToApplyToNextBlock = -1;
int lastColor = 0;
int lastBottom = -10000;
int skylightToApplyToNextBlock = -1;
int blocklightToApplyToNextBlock = -1;
int columnOffset = 0;
// goes from the top down
for (int i = 0; i < fullColumnData.size(); i++)
{
long fullData = fullColumnData.getLong(i);
int bottomY = FullDataPointUtil.getBottomY(fullData);
int blockHeight = FullDataPointUtil.getHeight(fullData);
int id = FullDataPointUtil.getId(fullData);
int blockLight = FullDataPointUtil.getBlockLight(fullData);
int skyLight = FullDataPointUtil.getSkyLight(fullData);
// TODO how should corrupted data be handled?
// TODO why is the full data corrupted in the first place? FullDataPointUtil hasn't been changed in a long time, could one of the full data point objects be corrupted?
// TODO if either of these happen the ID might also be invalid
//if (bottomY + blockHeight > 300)
//{
// // this data point is too tall, it's probably a monolith
// int k = 0;
// throw new RuntimeException();
//}
//if (light > 16 || light < 0)
//{
// // light is out of range
// throw new RuntimeException();
//}
IBiomeWrapper biome;
IBlockStateWrapper block;
try
{
biome = fullDataMapping.getBiomeWrapper(id);
block = fullDataMapping.getBlockStateWrapper(id);
}
catch (IndexOutOfBoundsException e)
{
// FIXME sometimes the data map has a length of 0
if (!brokenPos.contains(fullDataMapping.getPos()))
{
brokenPos.add(fullDataMapping.getPos());
String dimName = level.getLevelWrapper().getDimensionType().getDimensionName();
LOGGER.warn("Unable to get data point with id ["+id+"] " +
"(Max possible ID: ["+fullDataMapping.getMaxValidId()+"]) " +
"for pos ["+fullDataMapping.getPos()+"] in dimension ["+dimName+"]. " +
"Error: ["+e.getMessage()+"]. " +
"Further errors for this position won't be logged.");
}
// skip rendering broken data
continue;
}
if (blockStatesToIgnore.contains(block))
{
// Don't render: air, barriers, light blocks, etc.
continue;
}
// solid block check
if (avoidSolidBlocks && !block.isSolid() && !block.isLiquid() && block.getOpacity() != LodUtil.BLOCK_FULLY_OPAQUE)
{
if (colorBelowWithAvoidedBlocks)
{
int tempColor = level.computeBaseColor(new DhBlockPos(blockX, bottomY + level.getMinY(), blockZ), biome, block);
if (ColorUtil.getAlpha(tempColor) == 0)
{
//make sure to not transfer the color when alpha is 0
continue;
}
//mare sure to not trnasfer alpha if for some reason grass is semi transparent
colorToApplyToNextBlock = ColorUtil.setAlpha(tempColor,255);
skylightToApplyToNextBlock = skyLight;
blocklightToApplyToNextBlock = blockLight;
}
// don't add this block
continue;
}
int color;
if (colorToApplyToNextBlock == -1)
{
// use this block's color
color = level.computeBaseColor(new DhBlockPos(blockX, bottomY + level.getMinY(), blockZ), biome, block);
}
else
{
// use the previous block's color
color = colorToApplyToNextBlock;
colorToApplyToNextBlock = -1;
skyLight = skylightToApplyToNextBlock;
blockLight = blocklightToApplyToNextBlock;
}
//check if they share a top-bottom face and if they have same collor
if (color == lastColor && bottomY + blockHeight == lastBottom && columnOffset > 0)
{
//replace the previus block with new bottom
long columnData = renderColumnData.get(columnOffset - 1);
columnData = RenderDataPointUtil.setYMin(columnData, bottomY);
renderColumnData.set(columnOffset - 1, columnData);
}
else
{
// add the block
isVoid = false;
long columnData = RenderDataPointUtil.createDataPoint(bottomY + blockHeight, bottomY, color, skyLight, blockLight, block.getMaterialId());
renderColumnData.set(columnOffset, columnData);
columnOffset++;
}
lastBottom = bottomY;
lastColor = color;
}
if (isVoid)
{
renderColumnData.set(0, RenderDataPointUtil.createVoidDataPoint());
}
}
// TODO what does this mean?
public static void convertColumnData(IDhClientLevel level, FullDataPointIdMap fullDataMapping, int blockX, int blockZ, ColumnArrayView columnArrayView, LongArrayList fullDataColumn)
{
if (fullDataColumn == null || fullDataColumn.size() == 0)
{
return;
}
int dataTotalLength = fullDataColumn.size();
if (dataTotalLength > columnArrayView.verticalSize())
{
ColumnArrayView totalColumnData = new ColumnArrayView(new LongArrayList(new long[dataTotalLength]), dataTotalLength, 0, dataTotalLength);
iterateAndConvert(level, fullDataMapping, blockX, blockZ, totalColumnData, fullDataColumn);
columnArrayView.changeVerticalSizeFrom(totalColumnData);
}
else
{
iterateAndConvert(level, fullDataMapping, blockX, blockZ, columnArrayView, fullDataColumn); //Directly use the arrayView since it fits.
}
}
}
@@ -19,7 +19,6 @@
package com.seibel.distanthorizons.core.dataObjects.transformers; package com.seibel.distanthorizons.core.dataObjects.transformers;
import java.util.Collections;
import java.util.List; import java.util.List;
import com.seibel.distanthorizons.api.enums.config.EDhApiWorldCompressionMode; import com.seibel.distanthorizons.api.enums.config.EDhApiWorldCompressionMode;
@@ -31,12 +30,11 @@ import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSour
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.pos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPosMutable; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.util.FullDataPointUtil; import com.seibel.distanthorizons.core.util.FullDataPointUtil;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
@@ -49,6 +47,8 @@ public class LodDataBuilder
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IBlockStateWrapper AIR = SingletonInjector.INSTANCE.get(IWrapperFactory.class).getAirBlockStateWrapper(); private static final IBlockStateWrapper AIR = SingletonInjector.INSTANCE.get(IWrapperFactory.class).getAirBlockStateWrapper();
/** how many chunks wide the {@link FullDataSourceV2} is. */
private static final int NUMB_OF_CHUNKS_WIDE = FullDataSourceV2.WIDTH / LodUtil.CHUNK_WIDTH;
private static boolean getTopErrorLogged = false; private static boolean getTopErrorLogged = false;
@@ -67,8 +67,12 @@ public class LodDataBuilder
int sectionPosX = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getX()); // get the section position
int sectionPosZ = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getZ()); int sectionPosX = chunkWrapper.getChunkPos().x;
// negative positions start at -1 so the logic there is slightly different
sectionPosX = (sectionPosX < 0) ? ((sectionPosX + 1) / NUMB_OF_CHUNKS_WIDE) - 1 : (sectionPosX / NUMB_OF_CHUNKS_WIDE);
int sectionPosZ = chunkWrapper.getChunkPos().z;
sectionPosZ = (sectionPosZ < 0) ? ((sectionPosZ + 1) / NUMB_OF_CHUNKS_WIDE) - 1 : (sectionPosZ / NUMB_OF_CHUNKS_WIDE);
long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ); long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ);
FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos); FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos);
@@ -78,8 +82,8 @@ 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(); int chunkOffsetX = chunkWrapper.getChunkPos().x;
if (chunkWrapper.getChunkPos().getX() < 0) if (chunkWrapper.getChunkPos().x < 0)
{ {
// expected offset positions: // expected offset positions:
// chunkPos -> offset // chunkPos -> offset
@@ -94,30 +98,30 @@ public class LodDataBuilder
// -3 -> 1 // -3 -> 1
// -4 -> 0 --- // -4 -> 0 ---
// -5 -> 3 // -5 -> 3
chunkOffsetX = ((chunkOffsetX) % FullDataSourceV2.NUMB_OF_CHUNKS_WIDE); chunkOffsetX = ((chunkOffsetX) % NUMB_OF_CHUNKS_WIDE);
if (chunkOffsetX != 0) if (chunkOffsetX != 0)
{ {
chunkOffsetX += FullDataSourceV2.NUMB_OF_CHUNKS_WIDE; chunkOffsetX += NUMB_OF_CHUNKS_WIDE;
} }
} }
else else
{ {
chunkOffsetX %= FullDataSourceV2.NUMB_OF_CHUNKS_WIDE; chunkOffsetX %= NUMB_OF_CHUNKS_WIDE;
} }
chunkOffsetX *= LodUtil.CHUNK_WIDTH; chunkOffsetX *= LodUtil.CHUNK_WIDTH;
int chunkOffsetZ = chunkWrapper.getChunkPos().getZ(); int chunkOffsetZ = chunkWrapper.getChunkPos().z;
if (chunkWrapper.getChunkPos().getZ() < 0) if (chunkWrapper.getChunkPos().z < 0)
{ {
chunkOffsetZ = ((chunkOffsetZ) % FullDataSourceV2.NUMB_OF_CHUNKS_WIDE); chunkOffsetZ = ((chunkOffsetZ) % NUMB_OF_CHUNKS_WIDE);
if (chunkOffsetZ != 0) if (chunkOffsetZ != 0)
{ {
chunkOffsetZ += FullDataSourceV2.NUMB_OF_CHUNKS_WIDE; chunkOffsetZ += NUMB_OF_CHUNKS_WIDE;
} }
} }
else else
{ {
chunkOffsetZ %= FullDataSourceV2.NUMB_OF_CHUNKS_WIDE; chunkOffsetZ %= NUMB_OF_CHUNKS_WIDE;
} }
chunkOffsetZ *= LodUtil.CHUNK_WIDTH; chunkOffsetZ *= LodUtil.CHUNK_WIDTH;
@@ -155,8 +159,8 @@ public class LodDataBuilder
else else
{ {
//we are at the height limit. There are no torches here, and sky is not obscured. //we are at the height limit. There are no torches here, and sky is not obscured.
blockLight = LodUtil.MIN_MC_LIGHT; blockLight = 0;
skyLight = LodUtil.MAX_MC_LIGHT; skyLight = 15;
} }
@@ -238,7 +242,7 @@ public class LodDataBuilder
private static boolean blockVisible(IChunkWrapper chunkWrapper, int relBlockX, int blockY, int relBlockZ) private static boolean blockVisible(IChunkWrapper chunkWrapper, int relBlockX, int blockY, int relBlockZ)
{ {
DhBlockPos originalBlockPos = new DhBlockPos(relBlockX,blockY,relBlockZ); DhBlockPos originalBlockPos = new DhBlockPos(relBlockX,blockY,relBlockZ);
final DhBlockPosMutable testBlockPos = new DhBlockPosMutable(relBlockX,blockY,relBlockZ); DhBlockPos testBlockPos = new DhBlockPos(relBlockX,blockY,relBlockZ);
// up/down // up/down
if (blockInDirectionVisible(chunkWrapper, EDhDirection.UP, originalBlockPos, testBlockPos)) if (blockInDirectionVisible(chunkWrapper, EDhDirection.UP, originalBlockPos, testBlockPos))
@@ -273,20 +277,20 @@ public class LodDataBuilder
return false; return false;
} }
private static boolean blockInDirectionVisible(IChunkWrapper chunkWrapper, EDhDirection direction, DhBlockPos originalBlockPos, DhBlockPosMutable testBlockPos) private static boolean blockInDirectionVisible(IChunkWrapper chunkWrapper, EDhDirection direction, DhBlockPos originalBlockPos, DhBlockPos testBlockPos)
{ {
originalBlockPos.mutateOffset(direction, testBlockPos); originalBlockPos.mutateOffset(direction, testBlockPos);
// if the block is next to the border of a chunk, assume it's visible // if the block is next to the border of a chunk, assume it's visible
if (testBlockPos.getX() < 0 || testBlockPos.getX() >= LodUtil.CHUNK_WIDTH) if (testBlockPos.x < 0 || testBlockPos.x >= LodUtil.CHUNK_WIDTH)
{ {
return true; return true;
} }
if (testBlockPos.getZ() < 0 || testBlockPos.getZ() >= LodUtil.CHUNK_WIDTH) if (testBlockPos.z < 0 || testBlockPos.z >= LodUtil.CHUNK_WIDTH)
{ {
return true; return true;
} }
if (testBlockPos.getY() < chunkWrapper.getMinBuildHeight() || testBlockPos.getY() > chunkWrapper.getMaxBuildHeight()) if (testBlockPos.y < chunkWrapper.getMinBuildHeight() || testBlockPos.y > chunkWrapper.getMaxBuildHeight())
{ {
return true; return true;
} }
@@ -298,27 +302,14 @@ public class LodDataBuilder
/** @throws ClassCastException if an API user returns the wrong object type(s) */ /** @throws ClassCastException if an API user returns the wrong object type(s) */
public static FullDataSourceV2 createFromApiChunkData(DhApiChunk apiChunk, boolean runAdditionalValidation) throws ClassCastException, DataCorruptedException, IllegalArgumentException public static FullDataSourceV2 createFromApiChunkData(DhApiChunk dataPoints) throws ClassCastException, DataCorruptedException
{ {
// get the section position FullDataSourceV2 accessor = FullDataSourceV2.createEmpty(DhSectionPos.encode(new DhChunkPos(dataPoints.chunkPosX, dataPoints.chunkPosZ)));
int sectionPosX = getXOrZSectionPosFromChunkPos(apiChunk.chunkPosX); for (int relZ = 0; relZ < LodUtil.CHUNK_WIDTH; relZ++)
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;
FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos);
for (int relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++)
{ {
for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++) for (int relX = 0; relX < LodUtil.CHUNK_WIDTH; relX++)
{ {
List<DhApiTerrainDataPoint> columnDataPoints = apiChunk.getDataPoints(relBlockX, relBlockZ); List<DhApiTerrainDataPoint> columnDataPoints = dataPoints.getDataPoints(relX, relZ);
if (runAdditionalValidation)
{
validateOrThrowDataColumn(columnDataPoints);
}
// this null check does 2 nice things at the same time: // this null check does 2 nice things at the same time:
@@ -327,14 +318,12 @@ public class LodDataBuilder
// AND the below loop won't run. // AND the below loop won't run.
int size = (columnDataPoints != null) ? columnDataPoints.size() : 0; int size = (columnDataPoints != null) ? columnDataPoints.size() : 0;
// TODO make missing air LODs
// TODO merge duplicate datapoints
LongArrayList packedDataPoints = new LongArrayList(new long[size]); LongArrayList packedDataPoints = new LongArrayList(new long[size]);
for (int index = 0; index < size; index++) for (int index = 0; index < size; index++)
{ {
DhApiTerrainDataPoint dataPoint = columnDataPoints.get(index); DhApiTerrainDataPoint dataPoint = columnDataPoints.get(index);
int id = dataSource.mapping.addIfNotPresentAndGetId( int id = accessor.mapping.addIfNotPresentAndGetId(
(IBiomeWrapper) (dataPoint.biomeWrapper), (IBiomeWrapper) (dataPoint.biomeWrapper),
(IBlockStateWrapper) (dataPoint.blockStateWrapper) (IBlockStateWrapper) (dataPoint.blockStateWrapper)
); );
@@ -342,7 +331,7 @@ public class LodDataBuilder
packedDataPoints.set(index, FullDataPointUtil.encode( packedDataPoints.set(index, FullDataPointUtil.encode(
id, id,
dataPoint.topYBlockPos - dataPoint.bottomYBlockPos, dataPoint.topYBlockPos - dataPoint.bottomYBlockPos,
dataPoint.bottomYBlockPos - apiChunk.bottomYBlockPos, dataPoint.bottomYBlockPos - dataPoints.topYBlockPos,
(byte) (dataPoint.blockLightLevel), (byte) (dataPoint.blockLightLevel),
(byte) (dataPoint.skyLightLevel) (byte) (dataPoint.skyLightLevel)
)); ));
@@ -350,84 +339,12 @@ public class LodDataBuilder
// TODO add the ability for API users to define a different compression mode // TODO add the ability for API users to define a different compression mode
// or add a "unkown" compression mode // or add a "unkown" compression mode
dataSource.setSingleColumn( accessor.setSingleColumn(packedDataPoints, relX, relZ, EDhApiWorldGenerationStep.LIGHT, EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS);
packedDataPoints,
relBlockX + relSourceBlockX, relBlockZ + relSourceBlockZ,
EDhApiWorldGenerationStep.LIGHT, EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS);
dataSource.isEmpty = false;
} }
} }
return dataSource;
return accessor;
} }
private static void validateOrThrowDataColumn(List<DhApiTerrainDataPoint> dataPoints) throws IllegalArgumentException
{
// order doesn't need to be checked if there is 0 or 1 items
if (dataPoints.size() > 1)
{
// DH expects datapoints to be in a top-down order
DhApiTerrainDataPoint first = dataPoints.get(0);
DhApiTerrainDataPoint last = dataPoints.get(dataPoints.size() - 1);
if (first.bottomYBlockPos < last.bottomYBlockPos)
{
// flip the array if it's in bottom-up order
Collections.reverse(dataPoints);
}
}
// check that each datapoint is valid
int lastBottomYPos = Integer.MIN_VALUE;
for (int i = 0; i < dataPoints.size(); i++) // standard for-loop used instead of an enhanced for-loop to slightly reduce GC overhead due to iterator allocation
{
DhApiTerrainDataPoint dataPoint = dataPoints.get(i);
if (dataPoint == null)
{
throw new IllegalArgumentException("Datapoint: ["+i+"] is null DhApiTerrainDataPoints are not allowed. If you want to represent empty terrain, please use AIR.");
}
if (dataPoint.detailLevel != 0)
{
throw new IllegalArgumentException("Datapoint: ["+i+"] has the wrong detail level ["+dataPoint.detailLevel+"], all data points must be block sized; IE their detail level must be [0].");
}
int bottomYPos = dataPoint.bottomYBlockPos;
int topYPos = dataPoint.topYBlockPos;
int height = (dataPoint.topYBlockPos - dataPoint.bottomYBlockPos);
// is the datapoint right side up?
if (bottomYPos > topYPos)
{
throw new IllegalArgumentException("Datapoint: ["+i+"] is upside down. Top Pos: ["+topYPos+"], bottom pos: ["+bottomYPos+"].");
}
// valid height?
if (height <= 0 || height >= RenderDataPointUtil.MAX_WORLD_Y_SIZE)
{
throw new IllegalArgumentException("Datapoint: ["+i+"] has invalid height. Height must be in the range [1 - "+RenderDataPointUtil.MAX_WORLD_Y_SIZE+"] (inclusive).");
}
// is this datapoint overlapping the last one?
if (lastBottomYPos > topYPos)
{
throw new IllegalArgumentException("DhApiTerrainDataPoint ["+i+"] is overlapping with the last datapoint, this top Y: ["+topYPos+"], lastBottomYPos: ["+lastBottomYPos+"].");
}
// is there a gap between the last datapoint?
if (topYPos != lastBottomYPos
&& 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;
}
}
@@ -437,13 +354,4 @@ public class LodDataBuilder
public static boolean canGenerateLodFromChunk(IChunkWrapper chunk) { return chunk != null && chunk.isLightCorrect(); } public static boolean canGenerateLodFromChunk(IChunkWrapper chunk) { return chunk != null && chunk.isLightCorrect(); }
public static int getXOrZSectionPosFromChunkPos(int chunkXOrZPos)
{
// get the section position
int sectionPos = chunkXOrZPos;
// negative positions start at -1 so the logic there is slightly different
sectionPos = (sectionPos < 0) ? ((sectionPos + 1) / FullDataSourceV2.NUMB_OF_CHUNKS_WIDE) - 1 : (sectionPos / FullDataSourceV2.NUMB_OF_CHUNKS_WIDE);
return sectionPos;
}
} }
@@ -25,7 +25,7 @@ import java.util.concurrent.locks.ReentrantLock;
// TODO is there a reason this is separate from FullDataSourceProviderV2? // TODO is there a reason this is separate from FullDataSourceProviderV2?
// We shouldn't need multiple data source handlers // We shouldn't need multiple data source handlers
public abstract class AbstractDataSourceHandler public abstract class AbstractDataSourceHandler
<TDataSource extends IDataSource<TDhLevel>, <TDataSource extends IDataSource<TDhLevel>,
TDTO extends IBaseDTO<Long>, TDTO extends IBaseDTO<Long>,
TRepo extends AbstractDhRepo<Long, TDTO>, TRepo extends AbstractDhRepo<Long, TDTO>,
TDhLevel extends IDhLevel> TDhLevel extends IDhLevel>
@@ -38,20 +38,20 @@ public abstract class AbstractDataSourceHandler
/** /**
* The highest numerical detail level possible. * The highest numerical detail level possible.
* Used when determining which positions to update. * Used when determining which positions to update.
* *
* @see AbstractDataSourceHandler#MIN_SECTION_DETAIL_LEVEL * @see AbstractDataSourceHandler#MIN_SECTION_DETAIL_LEVEL
*/ */
public static final byte TOP_SECTION_DETAIL_LEVEL = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL + LodUtil.REGION_DETAIL_LEVEL; public static final byte TOP_SECTION_DETAIL_LEVEL = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL + LodUtil.REGION_DETAIL_LEVEL;
/** /**
* The lowest numerical detail level possible. * The lowest numerical detail level possible.
* *
* @see AbstractDataSourceHandler#TOP_SECTION_DETAIL_LEVEL * @see AbstractDataSourceHandler#TOP_SECTION_DETAIL_LEVEL
*/ * */
public static final byte MIN_SECTION_DETAIL_LEVEL = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; public static final byte MIN_SECTION_DETAIL_LEVEL = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL;
protected final PositionalLockProvider updateLockProvider = new PositionalLockProvider(); protected final PositionalLockProvider updateLockProvider = new PositionalLockProvider();
/** /**
* generally just used for debugging, * generally just used for debugging,
* keeps track of which positions are currently locked. * keeps track of which positions are currently locked.
*/ */
@@ -123,7 +123,7 @@ public abstract class AbstractDataSourceHandler
{ {
return CompletableFuture.supplyAsync(() -> this.get(pos), executor); return CompletableFuture.supplyAsync(() -> this.get(pos), executor);
} }
catch (RejectedExecutionException ignore) catch (RejectedExecutionException ignore)
{ {
// the thread pool was probably shut down because it's size is being changed, just wait a sec and it should be back // the thread pool was probably shut down because it's size is being changed, just wait a sec and it should be back
return CompletableFuture.completedFuture(null); return CompletableFuture.completedFuture(null);
@@ -205,7 +205,7 @@ public abstract class AbstractDataSourceHandler
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.error("Unexpected error in async data source update, error: " + e.getMessage(), e); LOGGER.error("Unexpected error in async data source update, error: "+e.getMessage(), e);
} }
finally finally
{ {
@@ -221,10 +221,9 @@ public abstract class AbstractDataSourceHandler
} }
} }
/** /**
* After this method returns the inputData will be written to file. * After this method returns the inputData will be written to file.
* * @param updatePos the position to update
* @param updatePos the position to update
*/ */
protected void updateDataSourceAtPos(long updatePos, @NotNull FullDataSourceV2 inputData, boolean lockOnUpdatePos) protected void updateDataSourceAtPos(long updatePos, @NotNull FullDataSourceV2 inputData, boolean lockOnUpdatePos)
{ {
@@ -269,7 +268,7 @@ public abstract class AbstractDataSourceHandler
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.error("Error updating pos [" + updatePos + "], error: "+e.getMessage(), e); LOGGER.error("Error updating pos ["+updatePos+"], error: "+e.getMessage(), e);
} }
finally finally
{ {
@@ -354,4 +353,4 @@ public abstract class AbstractDataSourceHandler
void OnDataSourceUpdated(TDataSource updatedFullDataSource); void OnDataSourceUpdated(TDataSource updatedFullDataSource);
} }
} }
@@ -21,7 +21,6 @@ package com.seibel.distanthorizons.core.file.fullDatafile;
import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode; import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode;
import com.seibel.distanthorizons.core.api.internal.ClientApi; import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV1; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV1;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
@@ -38,7 +37,6 @@ import com.seibel.distanthorizons.core.sql.repo.FullDataSourceV2Repo;
import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.world.EWorldEnvironment;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -47,21 +45,18 @@ import org.jetbrains.annotations.Nullable;
import java.awt.*; import java.awt.*;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.*; import java.util.*;
import java.util.List;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
/** /**
* Handles reading/writing {@link FullDataSourceV2} * Handles reading/writing {@link FullDataSourceV2}
* to and from the database. * to and from the database.
*/ */
public class FullDataSourceProviderV2 public class FullDataSourceProviderV2
extends AbstractDataSourceHandler<FullDataSourceV2, FullDataSourceV2DTO, FullDataSourceV2Repo, IDhLevel> extends AbstractDataSourceHandler<FullDataSourceV2, FullDataSourceV2DTO, FullDataSourceV2Repo, IDhLevel>
implements IDebugRenderable implements IDebugRenderable
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
@@ -77,7 +72,7 @@ public class FullDataSourceProviderV2
/** how many data sources should be pulled down for migration at once */ /** how many data sources should be pulled down for migration at once */
private static final int MIGRATION_BATCH_COUNT = NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD; private static final int MIGRATION_BATCH_COUNT = NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD;
private static final String MIGRATION_THREAD_NAME_PREFIX = "Full Data Migration Thread: "; private static final String MIGRATION_THREAD_NAME_PREFIX = "Full Data Migration Thread: ";
/** /**
* 5 minutes <br> * 5 minutes <br>
* This should be much longer than any update should take. This is just * This should be much longer than any update should take. This is just
* to make sure the thread doesn't get stuck. * to make sure the thread doesn't get stuck.
@@ -86,9 +81,9 @@ public class FullDataSourceProviderV2
protected final ThreadPoolExecutor migrationThreadPool; protected final ThreadPoolExecutor migrationThreadPool;
/** /**
* Interrupting the migration thread pool doesn't work well and may corrupt the database * Interrupting the migration thread pool doesn't work well and may corrupt the database
* vs gracefully shutting down the thread ourselves. * vs gracefully shutting down the thread ourselves.
*/ */
protected final AtomicBoolean migrationThreadRunning = new AtomicBoolean(true); protected final AtomicBoolean migrationThreadRunning = new AtomicBoolean(true);
protected final FullDataSourceProviderV1<IDhLevel> legacyFileHandler; protected final FullDataSourceProviderV1<IDhLevel> legacyFileHandler;
@@ -99,18 +94,17 @@ public class FullDataSourceProviderV2
protected long migrationCount = -1; protected long migrationCount = -1;
protected boolean migrationStoppedWithError = false; protected boolean migrationStoppedWithError = false;
/** /**
* Tracks which positions are currently being updated * Tracks which positions are currently being updated
* to prevent duplicate concurrent updates. * to prevent duplicate concurrent updates.
*/ */
public final Set<Long> parentUpdatingPosSet = ConcurrentHashMap.newKeySet(); public final Set<Long> parentUpdatingPosSet = ConcurrentHashMap.newKeySet();
// TODO only run thread if modifications happened recently // TODO only run thread if modifications happened recently
/** /**
* This isn't in {@link AbstractDataSourceHandler} since we don't need parent updating logic * This isn't in {@link AbstractDataSourceHandler} since we don't need parent updating logic
* for render data, only full data. * for render data, only full data.
*/ */
@Nullable
private final ThreadPoolExecutor updateQueueProcessor; private final ThreadPoolExecutor updateQueueProcessor;
@@ -120,28 +114,21 @@ public class FullDataSourceProviderV2
//=============// //=============//
public FullDataSourceProviderV2(IDhLevel level, AbstractSaveStructure saveStructure) { this(level, saveStructure, null); } public FullDataSourceProviderV2(IDhLevel level, AbstractSaveStructure saveStructure) { this(level, saveStructure, null); }
public FullDataSourceProviderV2(IDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride) public FullDataSourceProviderV2(IDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride)
{ {
super(level, saveStructure, saveDirOverride); super(level, saveStructure, saveDirOverride);
this.legacyFileHandler = new FullDataSourceProviderV1<>(level, saveStructure, saveDirOverride); this.legacyFileHandler = new FullDataSourceProviderV1<>(level, saveStructure, saveDirOverride);
DebugRenderer.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showFullDataUpdateStatus); DebugRenderer.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showFullDataUpdateStatus);
String dimensionName = level.getLevelWrapper().getDimensionName(); String dimensionName = level.getLevelWrapper().getDimensionType().getDimensionName();
// start migrating any legacy data sources present in the background // start migrating any legacy data sources present in the background
this.migrationThreadPool = ThreadUtil.makeRateLimitedThreadPool(1, MIGRATION_THREAD_NAME_PREFIX + "[" + dimensionName + "]", Config.Client.Advanced.MultiThreading.runTimeRatioForUpdatePropagatorThreads.get(), Thread.MIN_PRIORITY, (Semaphore) null); this.migrationThreadPool = ThreadUtil.makeRateLimitedThreadPool(1, MIGRATION_THREAD_NAME_PREFIX +"["+dimensionName+"]", Config.Client.Advanced.MultiThreading.runTimeRatioForUpdatePropagatorThreads.get(), Thread.MIN_PRIORITY, (Semaphore)null);
this.migrationThreadPool.execute(this::convertLegacyDataSources); this.migrationThreadPool.execute(() -> this.convertLegacyDataSources());
if (SharedApi.getEnvironment() != EWorldEnvironment.Server_Only) this.updateQueueProcessor = ThreadUtil.makeSingleThreadPool("Parent Update Queue ["+dimensionName+"]");
{ this.updateQueueProcessor.execute(() -> this.runUpdateQueue());
this.updateQueueProcessor = ThreadUtil.makeSingleThreadPool("Parent Update Queue [" + dimensionName + "]");
this.updateQueueProcessor.execute(this::runUpdateQueue);
}
else
{
this.updateQueueProcessor = null;
}
} }
@@ -165,7 +152,7 @@ public class FullDataSourceProviderV2
} }
} }
@Override @Override
protected FullDataSourceV2DTO createDtoFromDataSource(FullDataSourceV2 dataSource) protected FullDataSourceV2DTO createDtoFromDataSource(FullDataSourceV2 dataSource)
{ {
try try
@@ -176,7 +163,7 @@ public class FullDataSourceProviderV2
} }
catch (IOException e) catch (IOException e)
{ {
LOGGER.warn("Unable to create DTO, error: " + e.getMessage(), e); LOGGER.warn("Unable to create DTO, error: "+e.getMessage(), e);
return null; return null;
} }
} }
@@ -188,58 +175,6 @@ public class FullDataSourceProviderV2
@Override @Override
protected FullDataSourceV2 makeEmptyDataSource(long pos) { return FullDataSourceV2.DATA_SOURCE_POOL.getPooledSource(pos, true); } protected FullDataSourceV2 makeEmptyDataSource(long pos) { return FullDataSourceV2.DATA_SOURCE_POOL.getPooledSource(pos, true); }
@Nullable
public Long getTimestampForPos(long pos)
{
try
{
PreparedStatement preparedStatement = this.repo.createPreparedStatement(
"SELECT LastModifiedUnixDateTime " +
"FROM " + this.repo.getTableName() + " " +
"WHERE DetailLevel = ? " +
"AND PosX = ? " +
"AND PosZ = ?;"
);
preparedStatement.setInt(1, DhSectionPos.getDetailLevel(pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
preparedStatement.setInt(2, DhSectionPos.getX(pos));
preparedStatement.setInt(3, DhSectionPos.getZ(pos));
List<Map<String, Object>> row = this.repo.query(preparedStatement);
return !row.isEmpty() ? (Long) row.get(0).get("LastModifiedUnixDateTime") : null;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
public Map<Long, Long> getTimestampsForRange(byte detailLevel, int startPosX, int startPosZ, int endPosX, int endPosZ)
{
try
{
PreparedStatement preparedStatement = this.repo.createPreparedStatement(
"SELECT PosX, PosZ, LastModifiedUnixDateTime " +
"FROM " + this.repo.getTableName() + " " +
"WHERE DetailLevel = ? " +
"AND PosX BETWEEN ? AND ? " +
"AND PosZ BETWEEN ? AND ?;"
);
preparedStatement.setInt(1, detailLevel - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
preparedStatement.setInt(2, startPosX);
preparedStatement.setInt(3, endPosX);
preparedStatement.setInt(4, startPosZ);
preparedStatement.setInt(5, endPosZ);
return this.repo.query(preparedStatement).stream().collect(Collectors.toMap(
row -> DhSectionPos.encode(detailLevel, (int) row.get("PosX"), (int) row.get("PosZ")),
row -> (long) row.get("LastModifiedUnixDateTime"))
);
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
//================// //================//
@@ -264,7 +199,7 @@ public class FullDataSourceProviderV2
// queue parent updates // queue parent updates
if (executor.getQueue().size() < MAX_UPDATE_TASK_COUNT if (executor.getQueue().size() < MAX_UPDATE_TASK_COUNT
&& this.parentUpdatingPosSet.size() < MAX_UPDATE_TASK_COUNT) && this.parentUpdatingPosSet.size() < MAX_UPDATE_TASK_COUNT)
{ {
// get the positions that need to be applied to their parents // get the positions that need to be applied to their parents
LongArrayList parentUpdatePosList = this.repo.getPositionsToUpdate(MAX_UPDATE_TASK_COUNT); LongArrayList parentUpdatePosList = this.repo.getPositionsToUpdate(MAX_UPDATE_TASK_COUNT);
@@ -273,7 +208,7 @@ public class FullDataSourceProviderV2
HashMap<Long, HashSet<Long>> updatePosByParentPos = new HashMap<>(); HashMap<Long, HashSet<Long>> updatePosByParentPos = new HashMap<>();
for (Long pos : parentUpdatePosList) for (Long pos : parentUpdatePosList)
{ {
updatePosByParentPos.compute(DhSectionPos.getParentPos(pos), (parentPos, updatePosSet) -> updatePosByParentPos.compute(DhSectionPos.getParentPos(pos), (parentPos, updatePosSet) ->
{ {
if (updatePosSet == null) if (updatePosSet == null)
{ {
@@ -289,7 +224,7 @@ public class FullDataSourceProviderV2
{ {
// stop if there are already a bunch of updates queued // stop if there are already a bunch of updates queued
if (this.parentUpdatingPosSet.size() > MAX_UPDATE_TASK_COUNT if (this.parentUpdatingPosSet.size() > MAX_UPDATE_TASK_COUNT
|| !this.parentUpdatingPosSet.add(parentUpdatePos)) || !this.parentUpdatingPosSet.add(parentUpdatePos))
{ {
break; break;
} }
@@ -354,8 +289,7 @@ public class FullDataSourceProviderV2
} }
}); });
} }
catch (RejectedExecutionException ignore) catch (RejectedExecutionException ignore) { /* the executor was shut down, it should be back up shortly and able to accept new jobs */ }
{ /* the executor was shut down, it should be back up shortly and able to accept new jobs */ }
catch (Exception e) catch (Exception e)
{ {
this.parentUpdatingPosSet.remove(parentUpdatePos); this.parentUpdatingPosSet.remove(parentUpdatePos);
@@ -365,17 +299,14 @@ public class FullDataSourceProviderV2
} }
} }
catch (InterruptedException ignored) catch (InterruptedException ignored) { Thread.currentThread().interrupt(); }
{
Thread.currentThread().interrupt();
}
catch (Exception e) catch (Exception e)
{ {
LOGGER.error("Unexpected error in the parent update queue thread. Error: " + e.getMessage(), e); LOGGER.error("Unexpected error in the parent update queue thread. Error: " + e.getMessage(), e);
} }
} }
LOGGER.info("Update thread [" + Thread.currentThread().getName() + "] terminated."); LOGGER.info("Update thread ["+Thread.currentThread().getName()+"] terminated.");
} }
@@ -386,8 +317,8 @@ public class FullDataSourceProviderV2
private void convertLegacyDataSources() private void convertLegacyDataSources()
{ {
String dimensionName = this.level.getLevelWrapper().getDimensionName(); String dimensionName = this.level.getLevelWrapper().getDimensionType().getDimensionName();
LOGGER.info("Attempting to migrate data sources for: [" + dimensionName + "]-[" + this.saveDir + "]..."); LOGGER.info("Attempting to migrate data sources for: ["+dimensionName+"]-["+this.saveDir+"]...");
@@ -406,9 +337,9 @@ public class FullDataSourceProviderV2
this.showMigrationStartMessage(); this.showMigrationStartMessage();
LOGGER.info("deleting [" + dimensionName + "] - [" + totalDeleteCount + "] unused data sources..."); LOGGER.info("deleting [" + dimensionName + "] - ["+totalDeleteCount+"] unused data sources...");
this.legacyDeletionCount = totalDeleteCount; this.legacyDeletionCount = totalDeleteCount;
ArrayList<String> unusedDataPosList = this.legacyFileHandler.repo.getUnusedDataSourcePositionStringList(50); ArrayList<String> unusedDataPosList = this.legacyFileHandler.repo.getUnusedDataSourcePositionStringList(50);
while (unusedDataPosList.size() != 0) while (unusedDataPosList.size() != 0)
{ {
@@ -424,7 +355,7 @@ public class FullDataSourceProviderV2
long endStart = System.currentTimeMillis(); long endStart = System.currentTimeMillis();
long deleteTime = endStart - startTime; long deleteTime = endStart - startTime;
LOGGER.info("Deleting [" + dimensionName + "] - [" + unusedCount + "/" + totalDeleteCount + "] in [" + deleteTime + "]ms ..."); LOGGER.info("Deleting [" + dimensionName + "] - [" + unusedCount + "/" + totalDeleteCount + "] in ["+deleteTime+"]ms ...");
// a slight delay is added to prevent accidentally locking the database when deleting a lot of rows // a slight delay is added to prevent accidentally locking the database when deleting a lot of rows
@@ -435,12 +366,10 @@ public class FullDataSourceProviderV2
// and weak computers wait no time at all // and weak computers wait no time at all
Thread.sleep(deleteTime / 2); Thread.sleep(deleteTime / 2);
} }
catch (InterruptedException ignore) catch (InterruptedException ignore){}
{
}
} }
LOGGER.info("Done deleting [" + dimensionName + "] - [" + totalDeleteCount + "] unused data sources."); LOGGER.info("Done deleting [" + dimensionName + "] - ["+totalDeleteCount+"] unused data sources.");
} }
@@ -451,7 +380,7 @@ public class FullDataSourceProviderV2
long totalMigrationCount = this.legacyFileHandler.getDataSourceMigrationCount(); long totalMigrationCount = this.legacyFileHandler.getDataSourceMigrationCount();
this.migrationCount = totalMigrationCount; this.migrationCount = totalMigrationCount;
LOGGER.info("Found [" + totalMigrationCount + "] data sources that need migration."); LOGGER.info("Found ["+totalMigrationCount+"] data sources that need migration.");
ArrayList<FullDataSourceV1> legacyDataSourceList = this.legacyFileHandler.getDataSourcesToMigrate(MIGRATION_BATCH_COUNT); ArrayList<FullDataSourceV1> legacyDataSourceList = this.legacyFileHandler.getDataSourcesToMigrate(MIGRATION_BATCH_COUNT);
if (!legacyDataSourceList.isEmpty()) if (!legacyDataSourceList.isEmpty())
@@ -491,40 +420,40 @@ public class FullDataSourceProviderV2
{ {
newDataSource.close(); newDataSource.close();
} }
catch (Exception ignore) catch (Exception ignore)
{ {
} }
}); });
}
catch (Exception e)
{
Long migrationPos = legacyDataSource.getPos();
LOGGER.warn("Unexpected issue migrating data source at pos " + migrationPos + ". Error: " + e.getMessage(), e);
this.legacyFileHandler.markMigrationFailed(migrationPos);
}
} }
catch (Exception e)
try
{ {
Long migrationPos = legacyDataSource.getPos(); // wait for each thread to finish updating
LOGGER.warn("Unexpected issue migrating data source at pos " + migrationPos + ". Error: " + e.getMessage(), e); CompletableFuture<Void> combinedFutures = CompletableFuture.allOf(updateFutureList.toArray(new CompletableFuture[0]));
this.legacyFileHandler.markMigrationFailed(migrationPos); combinedFutures.get(MIGRATION_MAX_UPDATE_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS);
} }
catch (InterruptedException | TimeoutException e)
{
LOGGER.warn("Migration update timed out after [" + MIGRATION_MAX_UPDATE_TIMEOUT_IN_MS + "] milliseconds. Migration will re-try the same positions again in a moment..", e);
}
catch (ExecutionException e)
{
LOGGER.warn("Migration update failed. Migration will re-try the same positions again. Error:" + e.getMessage(), e);
}
legacyDataSourceList = this.legacyFileHandler.getDataSourcesToMigrate(MIGRATION_BATCH_COUNT);
progressCount += legacyDataSourceList.size();
this.migrationCount -= legacyDataSourceList.size();
} }
try
{
// wait for each thread to finish updating
CompletableFuture<Void> combinedFutures = CompletableFuture.allOf(updateFutureList.toArray(new CompletableFuture[0]));
combinedFutures.get(MIGRATION_MAX_UPDATE_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS);
}
catch (InterruptedException | TimeoutException e)
{
LOGGER.warn("Migration update timed out after [" + MIGRATION_MAX_UPDATE_TIMEOUT_IN_MS + "] milliseconds. Migration will re-try the same positions again in a moment..", e);
}
catch (ExecutionException e)
{
LOGGER.warn("Migration update failed. Migration will re-try the same positions again. Error:" + e.getMessage(), e);
}
legacyDataSourceList = this.legacyFileHandler.getDataSourcesToMigrate(MIGRATION_BATCH_COUNT);
progressCount += legacyDataSourceList.size();
this.migrationCount -= legacyDataSourceList.size();
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -534,16 +463,16 @@ public class FullDataSourceProviderV2
} }
finally finally
{ {
if (this.migrationThreadRunning.get()) if (this.migrationThreadRunning.get())
{ {
LOGGER.info("migration complete for: [" + dimensionName + "]-[" + this.saveDir + "]."); LOGGER.info("migration complete for: ["+dimensionName+"]-["+this.saveDir+"].");
this.showMigrationEndMessage(true); this.showMigrationEndMessage(true);
this.migrationCount = 0; this.migrationCount = 0;
} }
else else
{ {
LOGGER.info("migration stopped for: [" + dimensionName + "]-[" + this.saveDir + "]."); LOGGER.info("migration stopped for: ["+dimensionName+"]-["+this.saveDir+"].");
this.showMigrationEndMessage(false); this.showMigrationEndMessage(false);
this.migrationStoppedWithError = true; this.migrationStoppedWithError = true;
} }
} }
@@ -569,7 +498,7 @@ public class FullDataSourceProviderV2
} }
this.migrationStartMessageQueued = true; this.migrationStartMessageQueued = true;
String dimName = this.level.getLevelWrapper().getDimensionName(); String dimName = this.level.getLevelWrapper().getDimensionType().getDimensionName();
ClientApi.INSTANCE.showChatMessageNextFrame( ClientApi.INSTANCE.showChatMessageNextFrame(
"Old Distant Horizons data is being migrated for ["+dimName+"]. \n" + "Old Distant Horizons data is being migrated for ["+dimName+"]. \n" +
"While migrating LODs may load slowly \n" + "While migrating LODs may load slowly \n" +
@@ -580,7 +509,7 @@ public class FullDataSourceProviderV2
private void showMigrationEndMessage(boolean success) private void showMigrationEndMessage(boolean success)
{ {
String dimName = this.level.getLevelWrapper().getDimensionName(); String dimName = this.level.getLevelWrapper().getDimensionType().getDimensionName();
if (success) if (success)
{ {
@@ -605,27 +534,27 @@ public class FullDataSourceProviderV2
* Returns true if this provider can generate or retrieve * Returns true if this provider can generate or retrieve
* {@link FullDataSourceV2}'s that aren't currently in the database. * {@link FullDataSourceV2}'s that aren't currently in the database.
*/ */
public boolean canRetrieveMissingDataSources() public boolean canRetrieveMissingDataSources()
{ {
// the base handler just handles basic reading/writing // the base handler just handles basic reading/writing
// to the database and as such can't retrieve anything else. // to the database and as such can't retrieve anything else.
return false; return false;
} }
/** /**
* Returns false if this provider isn't accepting new requests, * Returns false if this provider isn't accepting new requests,
* this can be due to having a full queue or some other * this can be due to having a full queue or some other
* limiting factor. <br><br> * limiting factor. <br><br>
* *
* Note: when overriding make sure to add: <br> * Note: when overriding make sure to add: <br>
* <code> * <code>
* if (!super.canQueueRetrieval()) <br> * if (!super.canQueueRetrieval()) <br>
* { <br> * { <br>
* return false; <br> * return false; <br>
* } <br> * } <br>
* </code> * </code>
* to the beginning of your override. * to the beginning of your override.
* Otherwise, parent retrieval limits will be ignored. * Otherwise, parent retrieval limits will be ignored.
*/ */
public boolean canQueueRetrieval() public boolean canQueueRetrieval()
{ {
@@ -635,18 +564,18 @@ public class FullDataSourceProviderV2
return !this.migrationThreadRunning.get(); return !this.migrationThreadRunning.get();
} }
/** /**
* @return null if this provider can't generate any positions and * @return null if this provider can't generate any positions and
* an empty array if all positions were generated * an empty array if all positions were generated
*/ */
@Nullable @Nullable
public LongArrayList getPositionsToRetrieve(Long pos) { return null; } public LongArrayList getPositionsToRetrieve(Long pos) { return null; }
/** /**
* Returns how many positions could potentially be generated for this position assuming the position is empty. * Returns how many positions could potentially be generated for this position assuming the position is empty.
* Used when estimating the total number of retrieval requests. * Used when estimating the total number of retrieval requests.
*/ */
public int getMaxPossibleRetrievalPositionCountForPos(Long pos) { return -1; } public int getMaxPossibleRetrievalPositionCountForPos(Long pos) { return -1; }
/** @return true if the position was queued, false if not */ /** @return true if the position was queued, false if not */
public boolean queuePositionForRetrieval(Long genPos) { return false; } public boolean queuePositionForRetrieval(Long genPos) { return false; }
@@ -656,16 +585,15 @@ public class FullDataSourceProviderV2
public void clearRetrievalQueue() { } public void clearRetrievalQueue() { }
/** Can be used to display how many total retrieval requests might be available. */ /** Can be used to display how many total retrieval requests might be available. */
public void setTotalRetrievalPositionCount(int newCount) { } public void setTotalRetrievalPositionCount(int newCount) { }
/** /**
* Returns how many data sources are currently in memory and haven't * Returns how many data sources are currently in memory and haven't
* been saved to the database. * been saved to the database.
* Returns -1 if this provider never stores data sources to memory. * Returns -1 if this provider never stores data sources to memory.
*/ */
public int getUnsavedDataSourceCount() { return -1; } public int getUnsavedDataSourceCount() { return -1; }
public boolean fileExists(long pos) { return this.repo.getDataSizeInBytes(pos) > 0; }
//===========// //===========//
@@ -684,14 +612,11 @@ public class FullDataSourceProviderV2
.forEach((pos) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 80f, 0.20f, Color.MAGENTA)); }); .forEach((pos) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 80f, 0.20f, Color.MAGENTA)); });
} }
@Override @Override
public void close() public void close()
{ {
super.close(); super.close();
if (this.updateQueueProcessor != null) this.updateQueueProcessor.shutdownNow();
{
this.updateQueueProcessor.shutdownNow();
}
this.legacyFileHandler.close(); this.legacyFileHandler.close();
@@ -699,4 +624,4 @@ public class FullDataSourceProviderV2
this.migrationThreadPool.shutdown(); this.migrationThreadPool.shutdown();
} }
} }
@@ -36,15 +36,12 @@ import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import java.awt.*; import java.awt.*;
import java.io.File;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.IntStream;
public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 implements IDebugRenderable public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 implements IDebugRenderable
{ {
@@ -73,7 +70,6 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
//=============// //=============//
public GeneratedFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure) { super(level, saveStructure); } public GeneratedFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure) { super(level, saveStructure); }
public GeneratedFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride) { super(level, saveStructure, saveDirOverride); }
@@ -97,7 +93,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// don't log shutdown exceptions // don't log shutdown exceptions
if (!(exception instanceof CancellationException || exception.getCause() instanceof CancellationException)) if (!(exception instanceof CancellationException || exception.getCause() instanceof CancellationException))
{ {
LOGGER.error("Uncaught Gen Task Exception at [" + genTaskResult.pos + "], error: [" + exception.getMessage() + "].", exception); LOGGER.error("Uncaught Gen Task Exception at [" + genTaskResult.pos + "], error: ["+ exception.getMessage() + "].", exception);
} }
} }
else if (genTaskResult.success) else if (genTaskResult.success)
@@ -136,21 +132,21 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
//===================================// //===================================//
/** /**
* Assigns the queue for handling world gen and does first time setup as well. <br> * Assigns the queue for handling world gen and does first time setup as well. <br>
* Assumes there isn't a pre-existing queue. * Assumes there isn't a pre-existing queue.
*/ */
public void setWorldGenerationQueue(IFullDataSourceRetrievalQueue newWorldGenQueue) public void setWorldGenerationQueue(IFullDataSourceRetrievalQueue newWorldGenQueue)
{ {
boolean oldQueueExists = this.worldGenQueueRef.compareAndSet(null, newWorldGenQueue); boolean oldQueueExists = this.worldGenQueueRef.compareAndSet(null, newWorldGenQueue);
LodUtil.assertTrue(oldQueueExists, "previous world gen queue is still here!"); LodUtil.assertTrue(oldQueueExists, "previous world gen queue is still here!");
LOGGER.info("Set world gen queue for level [" + this.level.getLevelWrapper().getDimensionName() + "]."); LOGGER.info("Set world gen queue for level ["+this.level+"].");
} }
@Override @Override
public boolean canRetrieveMissingDataSources() { return true; } public boolean canRetrieveMissingDataSources() { return true; }
@Override @Override
public void setTotalRetrievalPositionCount(int newCount) public void setTotalRetrievalPositionCount(int newCount)
{ {
IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get(); IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get();
if (worldGenQueue != null) if (worldGenQueue != null)
@@ -205,7 +201,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// don't queue additional world gen requests beyond the max allotted count // don't queue additional world gen requests beyond the max allotted count
return worldGenQueue.getWaitingTaskCount() < maxQueueCount; return worldGenQueue.getWaitingTaskCount() < maxQueueCount;
} }
@Override @Override
@@ -241,12 +237,6 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
public int getUnsavedDataSourceCount() { return this.delayedFullDataSourceSaveCache.getUnsavedCount(); } public int getUnsavedDataSourceCount() { return this.delayedFullDataSourceSaveCache.getUnsavedCount(); }
public boolean isFullyGenerated(byte[] columnGenerationSteps)
{
return IntStream.range(0, columnGenerationSteps.length)
.noneMatch(i -> columnGenerationSteps[i] == EDhApiWorldGenerationStep.EMPTY.value);
}
@Override @Override
public LongArrayList getPositionsToRetrieve(Long pos) public LongArrayList getPositionsToRetrieve(Long pos)
{ {
@@ -360,22 +350,6 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
return BitShiftUtil.powerOfTwo(detailLevelDiff); return BitShiftUtil.powerOfTwo(detailLevelDiff);
} }
public Map<Long, Integer> getLoadStates(Iterable<Long> posList)
{
HashMap<Long, Integer> map = new HashMap<>();
for (long pos : posList)
{
map.put(pos,
// Loaded
this.delayedFullDataSourceSaveCache.dataSourceByPosition.containsKey(pos) ? 3
// Unloaded, but exists
: this.fileExists(pos) ? 2
// Not generated
: 1);
}
return map;
}
//=======// //=======//
@@ -418,9 +392,8 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
GeneratedFullDataSourceProvider.this.delayedFullDataSourceSaveCache.queueDataSourceForUpdateAndSave(chunkSizedFullDataSource); GeneratedFullDataSourceProvider.this.delayedFullDataSourceSaveCache.queueDataSourceForUpdateAndSave(chunkSizedFullDataSource);
}; };
} }
} }
private void onDataSourceSave(FullDataSourceV2 fullDataSource) private void onDataSourceSave(FullDataSourceV2 fullDataSource)
{ GeneratedFullDataSourceProvider.this.updateDataSourceAsync(fullDataSource); } { GeneratedFullDataSourceProvider.this.updateDataSourceAsync(fullDataSource); }
@@ -434,4 +407,4 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
} }
} }
@@ -19,67 +19,15 @@
package com.seibel.distanthorizons.core.file.fullDatafile; package com.seibel.distanthorizons.core.file.fullDatafile;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoginRequestQueue;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class RemoteFullDataSourceProvider extends GeneratedFullDataSourceProvider public class RemoteFullDataSourceProvider extends FullDataSourceProviderV2
{ {
@Nullable public RemoteFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure) { super(level, saveStructure); }
private final SyncOnLoginRequestQueue syncOnLoginRequestQueue; public RemoteFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride) { super(level, saveStructure, saveDirOverride); }
private final Set<Long> finishedTaskPositions = ConcurrentHashMap.newKeySet();
public RemoteFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride, @Nullable SyncOnLoginRequestQueue syncOnLoginRequestQueue) }
{
super(level, saveStructure, saveDirOverride);
this.syncOnLoginRequestQueue = syncOnLoginRequestQueue;
}
@Override
@Nullable
public FullDataSourceV2 get(long pos)
{
FullDataSourceV2 fullDataSource = super.get(pos);
if (fullDataSource == null || this.syncOnLoginRequestQueue == null)
{
return fullDataSource;
}
int posToMinimumDetailScale = (DhSectionPos.getDetailLevel(pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL + 1);
Map<Long, Long> timestamps = this.getTimestampsForRange(
DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL,
DhSectionPos.getX(pos) * posToMinimumDetailScale,
DhSectionPos.getZ(pos) * posToMinimumDetailScale,
(DhSectionPos.getX(pos) + 1) * posToMinimumDetailScale - 1,
(DhSectionPos.getZ(pos) + 1) * posToMinimumDetailScale - 1
);
for (Map.Entry<Long, Long> entry : timestamps.entrySet())
{
if (this.finishedTaskPositions.add(entry.getKey()))
{
this.syncOnLoginRequestQueue.submitRequest(entry.getKey(), entry.getValue(), this.delayedFullDataSourceSaveCache::queueDataSourceForUpdateAndSave);
}
}
return fullDataSource;
}
@Override
public void close()
{
if (this.syncOnLoginRequestQueue != null)
{
this.syncOnLoginRequestQueue.close();
}
super.close();
}
}
@@ -26,12 +26,12 @@ import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.api.enums.config.EDhApiServerFolderNameMode; import com.seibel.distanthorizons.api.enums.config.EDhApiServerFolderNameMode;
import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel; import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel;
import com.seibel.distanthorizons.core.util.objects.ParsedIp; import com.seibel.distanthorizons.core.util.objects.ParsedIp;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IDimensionTypeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IDimensionTypeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.coreapi.util.StringUtil;
import java.io.File; import java.io.File;
import java.util.*; import java.util.*;
@@ -41,17 +41,13 @@ import java.util.*;
*/ */
public class ClientOnlySaveStructure extends AbstractSaveStructure public class ClientOnlySaveStructure extends AbstractSaveStructure
{ {
public static final String SERVER_DATA_FOLDER_NAME = "Distant_Horizons_server_data"; final File folder;
public static final String REPLAY_SERVER_FOLDER_NAME = "REPLAY";
public static final String INVALID_FILE_CHARACTERS_REGEX = "[\\\\/:*?\"<>|]";
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class); private static final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
public static final String INVALID_FILE_CHARACTERS_REGEX = "[\\\\/:*?\"<>|]";
SubDimensionLevelMatcher subDimMatcher = null;
private SubDimensionLevelMatcher subDimMatcher = null; final HashMap<ILevelWrapper, File> levelWrapperToFileMap = new HashMap<>();
private final File folder;
private final HashMap<ILevelWrapper, File> levelWrapperToFileMap = new HashMap<>();
@@ -88,9 +84,9 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
if (newLevelWrapper instanceof IServerKeyedClientLevel) if (newLevelWrapper instanceof IServerKeyedClientLevel)
{ {
IServerKeyedClientLevel keyedClientLevel = (IServerKeyedClientLevel) newLevelWrapper; IServerKeyedClientLevel keyedClientLevel = (IServerKeyedClientLevel) newLevelWrapper;
LOGGER.info("Loading level " + newLevelWrapper.getDimensionName() + " with key: " + keyedClientLevel.getServerLevelKey()); LOGGER.info("Loading level " + newLevelWrapper.getDimensionType().getDimensionName() + " with key: " + keyedClientLevel.getServerLevelKey());
// This world was identified by the server directly, so we can know for sure which folder to use. // This world was identified by the server directly, so we can know for sure which folder to use.
return new File(getSaveStructureFolderPath() + File.separatorChar + keyedClientLevel.getServerLevelKey().replaceAll(":", "@@")); return new File(getSaveStructureFolderPath() + File.separatorChar + keyedClientLevel.getServerLevelKey());
} }
@@ -102,9 +98,9 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
// create the matcher if one doesn't exist // create the matcher if one doesn't exist
if (this.subDimMatcher == null || !this.subDimMatcher.isFindingLevel(newClientLevelWrapper)) if (this.subDimMatcher == null || !this.subDimMatcher.isFindingLevel(newClientLevelWrapper))
{ {
LOGGER.info("Loading level " + newClientLevelWrapper.getDimensionName()); LOGGER.info("Loading level " + newClientLevelWrapper.getDimensionType().getDimensionName());
List<File> levelFolders = this.getDhDataFoldersForDimension(newClientLevelWrapper); List<File> levelFolders = this.getDhDataFoldersForDimension(newClientLevelWrapper.getDimensionType());
this.subDimMatcher = new SubDimensionLevelMatcher(newClientLevelWrapper, this.folder, levelFolders); this.subDimMatcher = new SubDimensionLevelMatcher(newClientLevelWrapper, this.folder, levelFolders);
} }
@@ -133,23 +129,23 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
private File getLevelFolderWithoutSimilarityMatching(ILevelWrapper level) private File getLevelFolderWithoutSimilarityMatching(ILevelWrapper level)
{ {
List<File> folders = this.getDhDataFoldersForDimension(level); List<File> folders = this.getDhDataFoldersForDimension(level.getDimensionType());
if (!folders.isEmpty() && folders.get(0) != null) if (!folders.isEmpty() && folders.get(0) != null)
{ {
// use the first existing sub-dimension // use the first existing sub-dimension
String folderName = folders.get(0).getName(); String folderName = folders.get(0).getName();
LOGGER.info("Default Sub Dimension set to: [" + StringUtil.shortenString(folderName, 8) + "...]"); LOGGER.info("Default Sub Dimension set to: [" + LodUtil.shortenString(folderName, 8) + "...]");
return folders.get(0); return folders.get(0);
} }
else else
{ {
// no valid sub dimension was found, create a new one // no valid sub dimension was found, create a new one
LOGGER.info("Default Sub Dimension not found. Creating: [" + level.getDimensionName() + "]"); LOGGER.info("Default Sub Dimension not found. Creating: [" + level.getDimensionType().getDimensionName() + "]");
return new File(this.folder, level.getDimensionName().replaceAll(":", "@@")); return new File(this.folder, level.getDimensionType().getDimensionName());
} }
} }
public List<File> getDhDataFoldersForDimension(ILevelWrapper level) public List<File> getDhDataFoldersForDimension(IDimensionTypeWrapper dimensionType)
{ {
File[] folders = this.folder.listFiles(); File[] folders = this.folder.listFiles();
if (folders == null) if (folders == null)
@@ -158,7 +154,7 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
} }
// filter by dimension name // filter by dimension name
String expectedDimName = level.getDimensionName(); String expectedDimName = dimensionType.getDimensionName();
ArrayList<File> possibleDimFolders = new ArrayList<>(); ArrayList<File> possibleDimFolders = new ArrayList<>();
for (File dimFolder : folders) for (File dimFolder : folders)
{ {
@@ -241,7 +237,7 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
private static String getSaveStructureFolderPath() private static String getSaveStructureFolderPath()
{ {
String path = MC_SHARED.getInstallationDirectory().getPath() + File.separatorChar String path = MC_SHARED.getInstallationDirectory().getPath() + File.separatorChar
+ SERVER_DATA_FOLDER_NAME + File.separatorChar + "Distant_Horizons_server_data" + File.separatorChar
+ getServerFolderName(); + getServerFolderName();
return path; return path;
} }
@@ -249,14 +245,6 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
/** Generated from the server the client is currently connected to. */ /** Generated from the server the client is currently connected to. */
private static String getServerFolderName() private static String getServerFolderName()
{ {
// if connected to a replay we won't have any server info
// use the dedicated replay server folder
if (MC_CLIENT.connectedToReplay())
{
return REPLAY_SERVER_FOLDER_NAME;
}
// parse the current server's IP // parse the current server's IP
ParsedIp parsedIp = new ParsedIp(MC_CLIENT.getCurrentServerIp()); ParsedIp parsedIp = new ParsedIp(MC_CLIENT.getCurrentServerIp());
String serverIpCleaned = parsedIp.ip.replaceAll(INVALID_FILE_CHARACTERS_REGEX, ""); String serverIpCleaned = parsedIp.ip.replaceAll(INVALID_FILE_CHARACTERS_REGEX, "");
@@ -309,4 +297,4 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
@Override @Override
public String toString() { return "[" + this.getClass().getSimpleName() + "@" + this.folder.getName() + "]"; } public String toString() { return "[" + this.getClass().getSimpleName() + "@" + this.folder.getName() + "]"; }
} }
@@ -40,7 +40,6 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftCli
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.coreapi.util.StringUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
@@ -93,7 +92,7 @@ public class SubDimensionLevelMatcher implements AutoCloseable
if (potentialLevelFolders.size() == 0) if (potentialLevelFolders.size() == 0)
{ {
String newId = UUID.randomUUID().toString(); String newId = UUID.randomUUID().toString();
LOGGER.info("No potential level files found. Creating a new sub dimension with the ID ["+ StringUtil.shortenString(newId, 8)+"]..."); LOGGER.info("No potential level files found. Creating a new sub dimension with the ID ["+LodUtil.shortenString(newId, 8)+"]...");
this.foundLevelFile = this.CreateSubDimFolder(newId); this.foundLevelFile = this.CreateSubDimFolder(newId);
} }
} }
@@ -191,7 +190,7 @@ public class SubDimensionLevelMatcher implements AutoCloseable
} }
FullDataSourceV2 newChunkSizedFullDataView = FullDataSourceV2.createFromChunk(newlyLoadedChunk); FullDataSourceV2 newChunkSizedFullDataView = FullDataSourceV2.createFromChunk(newlyLoadedChunk);
// convert to a data source for easier comparing // convert to a data source for easier comparing
FullDataSourceV2 newDataSource = FullDataSourceV2.createEmpty(DhSectionPos.encodeContaining(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, this.playerData.playerBlockPos)); FullDataSourceV2 newDataSource = FullDataSourceV2.createEmpty(DhSectionPos.encode(this.playerData.playerBlockPos));
newDataSource.update(newChunkSizedFullDataView); newDataSource.update(newChunkSizedFullDataView);
@@ -201,22 +200,22 @@ public class SubDimensionLevelMatcher implements AutoCloseable
//================================// //================================//
// log the start of this attempt // log the start of this attempt
LOGGER.info("Attempting to determine sub-dimension for [" + MC_CLIENT.getWrappedClientLevel().getDimensionName() + "]"); LOGGER.info("Attempting to determine sub-dimension for [" + MC_CLIENT.getWrappedClientLevel().getDimensionType().getDimensionName() + "]");
LOGGER.info("Player block pos in dimension: [" + this.playerData.playerBlockPos.getX() + "," + this.playerData.playerBlockPos.getY() + "," + this.playerData.playerBlockPos.getZ() + "]"); LOGGER.info("Player block pos in dimension: [" + this.playerData.playerBlockPos.x + "," + this.playerData.playerBlockPos.y + "," + this.playerData.playerBlockPos.z + "]");
LOGGER.info("Potential Sub Dimension folders: [" + this.potentialLevelFolders.size() + "]"); LOGGER.info("Potential Sub Dimension folders: [" + this.potentialLevelFolders.size() + "]");
SubDimCompare mostSimilarSubDim = null; SubDimCompare mostSimilarSubDim = null;
for (File testLevelFolder : this.potentialLevelFolders) for (File testLevelFolder : this.potentialLevelFolders)
{ {
LOGGER.info("Testing level folder: [" + StringUtil.shortenString(testLevelFolder.getName(), 8) + "]"); LOGGER.info("Testing level folder: [" + LodUtil.shortenString(testLevelFolder.getName(), 8) + "]");
FullDataSourceV2 testFullDataSource = null; FullDataSourceV2 testFullDataSource = null;
try try
{ {
// get the data source to compare against // get the data source to compare against
try (IDhLevel tempLevel = new DhClientLevel(new ClientOnlySaveStructure(), this.currentClientLevel, testLevelFolder, false, null)) try (IDhLevel tempLevel = new DhClientLevel(new ClientOnlySaveStructure(), this.currentClientLevel, testLevelFolder, false))
{ {
testFullDataSource = tempLevel.getFullDataProvider().getAsync(DhSectionPos.encodeContaining(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, this.playerData.playerBlockPos)).join(); testFullDataSource = tempLevel.getFullDataProvider().getAsync(DhSectionPos.encode(this.playerData.playerBlockPos)).join();
if (testFullDataSource == null) if (testFullDataSource == null)
{ {
continue; continue;
@@ -315,7 +314,7 @@ public class SubDimensionLevelMatcher implements AutoCloseable
// get the player data for this dimension folder // get the player data for this dimension folder
SubDimensionPlayerData testPlayerData = new SubDimensionPlayerData(testLevelFolder); SubDimensionPlayerData testPlayerData = new SubDimensionPlayerData(testLevelFolder);
LOGGER.info("Last known player pos: [" + testPlayerData.playerBlockPos.getX() + "," + testPlayerData.playerBlockPos.getY() + "," + testPlayerData.playerBlockPos.getZ() + "]"); LOGGER.info("Last known player pos: [" + testPlayerData.playerBlockPos.x + "," + testPlayerData.playerBlockPos.y + "," + testPlayerData.playerBlockPos.z + "]");
// check if the block positions are close // check if the block positions are close
int playerBlockDist = testPlayerData.playerBlockPos.getManhattanDistance(this.playerData.playerBlockPos); int playerBlockDist = testPlayerData.playerBlockPos.getManhattanDistance(this.playerData.playerBlockPos);
@@ -329,8 +328,8 @@ public class SubDimensionLevelMatcher implements AutoCloseable
} }
String subDimShortName = StringUtil.shortenString(testLevelFolder.getName(), 8); // variables are separated out for easier debugging String subDimShortName = LodUtil.shortenString(testLevelFolder.getName(), 8); // variables are separated out for easier debugging
String equalPercent = StringUtil.shortenString(mostSimilarSubDim.getPercentEqual()+"", 5); String equalPercent = LodUtil.shortenString(mostSimilarSubDim.getPercentEqual()+"", 5);
LOGGER.info("Sub dimension ["+subDimShortName+"...] is current dimension probability: "+equalPercent+" ("+equalDataPoints+"/"+totalDataPointCount+")"); LOGGER.info("Sub dimension ["+subDimShortName+"...] is current dimension probability: "+equalPercent+" ("+equalDataPoints+"/"+totalDataPointCount+")");
} }
catch (Exception e) catch (Exception e)
@@ -360,7 +359,7 @@ public class SubDimensionLevelMatcher implements AutoCloseable
{ {
// we found a sub dim folder that is similar, use it // we found a sub dim folder that is similar, use it
LOGGER.info("Sub Dimension set to: [" + StringUtil.shortenString(mostSimilarSubDim.folder.getName(), 8) + "...] with an equality of [" + mostSimilarSubDim.getPercentEqual() + "]"); LOGGER.info("Sub Dimension set to: [" + LodUtil.shortenString(mostSimilarSubDim.folder.getName(), 8) + "...] with an equality of [" + mostSimilarSubDim.getPercentEqual() + "]");
return mostSimilarSubDim.folder; return mostSimilarSubDim.folder;
} }
else else
@@ -370,7 +369,7 @@ public class SubDimensionLevelMatcher implements AutoCloseable
String newId = UUID.randomUUID().toString(); String newId = UUID.randomUUID().toString();
double highestEqualityPercent = mostSimilarSubDim != null ? mostSimilarSubDim.getPercentEqual() : 0; double highestEqualityPercent = mostSimilarSubDim != null ? mostSimilarSubDim.getPercentEqual() : 0;
String message = "No suitable sub dimension found. The highest equality was [" + StringUtil.shortenString(highestEqualityPercent + "", 5) + "]. Creating a new sub dimension with ID: " + StringUtil.shortenString(newId, 8) + "..."; String message = "No suitable sub dimension found. The highest equality was [" + LodUtil.shortenString(highestEqualityPercent + "", 5) + "]. Creating a new sub dimension with ID: " + LodUtil.shortenString(newId, 8) + "...";
LOGGER.info(message); LOGGER.info(message);
File folder = this.CreateSubDimFolder(newId); File folder = this.CreateSubDimFolder(newId);
@@ -380,9 +379,9 @@ public class SubDimensionLevelMatcher implements AutoCloseable
} }
private File CreateSubDimFolder(String subDimId) { return new File(this.levelsFolder.getPath() + File.separatorChar + this.currentClientLevel.getDimensionName(), subDimId); } private File CreateSubDimFolder(String subDimId) { return new File(this.levelsFolder.getPath() + File.separatorChar + this.currentClientLevel.getDimensionType().getDimensionName(), subDimId); }
@Override @Override
public void close() { this.matcherThread.shutdownNow(); } public void close() { this.matcherThread.shutdownNow(); }
} }
@@ -22,7 +22,7 @@ package com.seibel.distanthorizons.core.file.subDimMatching;
import com.electronwill.nightconfig.core.file.CommentedFileConfig; import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.pos.DhBlockPos;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
@@ -127,9 +127,9 @@ public class SubDimensionPlayerData
public void toTomlFile(CommentedFileConfig toml) public void toTomlFile(CommentedFileConfig toml)
{ {
// player block pos // player block pos
toml.add(PLAYER_BLOCK_POS_X_PATH, this.playerBlockPos.getX()); toml.add(PLAYER_BLOCK_POS_X_PATH, this.playerBlockPos.x);
toml.add(PLAYER_BLOCK_POS_Y_PATH, this.playerBlockPos.getY()); toml.add(PLAYER_BLOCK_POS_Y_PATH, this.playerBlockPos.y);
toml.add(PLAYER_BLOCK_POS_Z_PATH, this.playerBlockPos.getZ()); toml.add(PLAYER_BLOCK_POS_Z_PATH, this.playerBlockPos.z);
toml.save(); toml.save();
} }
@@ -138,7 +138,7 @@ public class SubDimensionPlayerData
@Override @Override
public String toString() public String toString()
{ {
return "PlayerBlockPos: [" + this.playerBlockPos.getX() + "," + this.playerBlockPos.getY() + "," + this.playerBlockPos.getZ() + "]"; return "PlayerBlockPos: [" + this.playerBlockPos.x + "," + this.playerBlockPos.y + "," + this.playerBlockPos.z + "]";
} }
} }
@@ -33,7 +33,7 @@ public class AdjacentChunkHolder
{ {
for (int zOffset = -1; zOffset <= 1; zOffset++) for (int zOffset = -1; zOffset <= 1; zOffset++)
{ {
DhChunkPos adjacentPos = new DhChunkPos(centerChunkPos.getX() + xOffset, centerChunkPos.getZ() + zOffset); DhChunkPos adjacentPos = new DhChunkPos(centerChunkPos.x + xOffset, centerChunkPos.z + zOffset);
requestedAdjacentPositions.add(adjacentPos); requestedAdjacentPositions.add(adjacentPos);
} }
} }
@@ -69,13 +69,13 @@ public class AdjacentChunkHolder
DhChunkPos centerPos = this.chunkArray[4].getChunkPos(); DhChunkPos centerPos = this.chunkArray[4].getChunkPos();
DhChunkPos offsetPos = centerWrapper.getChunkPos(); DhChunkPos offsetPos = centerWrapper.getChunkPos();
int offsetX = offsetPos.getX() - centerPos.getX(); int offsetX = offsetPos.x - centerPos.x;
if (offsetX < -1 || offsetX > 1) if (offsetX < -1 || offsetX > 1)
{ {
return; return;
} }
int offsetZ = offsetPos.getZ() - centerPos.getZ(); int offsetZ = offsetPos.z - centerPos.z;
if (offsetZ < -1 || offsetZ > 1) if (offsetZ < -1 || offsetZ > 1)
{ {
return; return;
@@ -91,18 +91,18 @@ public class AdjacentChunkHolder
int chunkZ = BitShiftUtil.divideByPowerOfTwo(blockZ, 4); int chunkZ = BitShiftUtil.divideByPowerOfTwo(blockZ, 4);
IChunkWrapper centerChunk = this.chunkArray[4]; IChunkWrapper centerChunk = this.chunkArray[4];
DhChunkPos centerPos = centerChunk.getChunkPos(); DhChunkPos centerPos = centerChunk.getChunkPos();
if (centerPos.getX() == chunkX && centerPos.getZ() == chunkZ) if (centerPos.x == chunkX && centerPos.z == chunkZ)
{ {
return centerChunk; return centerChunk;
} }
int offsetX = chunkX - centerPos.getX(); int offsetX = chunkX - centerPos.x;
if (offsetX < -1 || offsetX > 1) if (offsetX < -1 || offsetX > 1)
{ {
return null; return null;
} }
int offsetZ = chunkZ - centerPos.getZ(); int offsetZ = chunkZ - centerPos.z;
if (offsetZ < -1 || offsetZ > 1) if (offsetZ < -1 || offsetZ > 1)
{ {
return null; return null;
@@ -47,6 +47,15 @@ public class BatchGenerator implements IDhApiWorldGenerator
private static final IWrapperFactory FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class); private static final IWrapperFactory FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
/**
* Defines how many tasks can be queued per thread. <br><br>
*
* TODO the multiplier here should change dynamically based on how fast the generator is vs the queuing thread,
* if this is too high it may cause issues when moving,
* but if it is too low the generator threads won't have enough tasks to work on
*/
private static final int MAX_QUEUED_TASKS_PER_THREAD = 3;
public AbstractBatchGenerationEnvironmentWrapper generationEnvironment; public AbstractBatchGenerationEnvironmentWrapper generationEnvironment;
public IDhLevel targetDhLevel; public IDhLevel targetDhLevel;
@@ -138,6 +147,14 @@ public class BatchGenerator implements IDhApiWorldGenerator
@Override @Override
public void preGeneratorTaskStart() { this.generationEnvironment.updateAllFutures(); } public void preGeneratorTaskStart() { this.generationEnvironment.updateAllFutures(); }
@Override
public boolean isBusy()
{
int worldGenThreadCount = Math.max(Config.Client.Advanced.MultiThreading.numberOfWorldGenerationThreads.get(), 1);
int maxWorldGenTaskCount = worldGenThreadCount * MAX_QUEUED_TASKS_PER_THREAD;
return this.generationEnvironment.getEventCount() > maxWorldGenTaskCount;
}
//=========// //=========//
@@ -21,17 +21,13 @@ package com.seibel.distanthorizons.core.generation;
import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.pos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPosMutable;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import java.awt.*;
import java.util.*; import java.util.*;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@@ -53,28 +49,15 @@ public class DhLightingEngine
* Since these objects are always mutated anyway, using a {@link ThreadLocal} will allow us to * Since these objects are always mutated anyway, using a {@link ThreadLocal} will allow us to
* only create as many of these {@link DhBlockPos} as necessary. * only create as many of these {@link DhBlockPos} as necessary.
*/ */
private static final ThreadLocal<DhBlockPosMutable> PRIMARY_BLOCK_POS_REF = ThreadLocal.withInitial(() -> new DhBlockPosMutable()); private static final ThreadLocal<DhBlockPos> PRIMARY_BLOCK_POS_REF = ThreadLocal.withInitial(() -> new DhBlockPos());
private static final ThreadLocal<DhBlockPosMutable> SECONDARY_BLOCK_POS_REF = ThreadLocal.withInitial(() -> new DhBlockPosMutable()); private static final ThreadLocal<DhBlockPos> SECONDARY_BLOCK_POS_REF = ThreadLocal.withInitial(() -> new DhBlockPos());
/** if enabled will render each block light value when the lighting engine is run */
private static final boolean RENDER_BLOCK_LIGHT_WIREFRAME = false;
/** if enabled will render each sky light value when the lighting engine is run */
private static final boolean RENDER_SKY_LIGHT_WIREFRAME = false;
//=============//
// constructor //
//=============//
private DhLightingEngine() { } private DhLightingEngine() { }
//=========//
// methods //
//=========//
/** /**
* Note: depending on the implementation of {@link IChunkWrapper#setDhBlockLight(int, int, int, int)} and {@link IChunkWrapper#setDhSkyLight(int, int, int, int)} * Note: depending on the implementation of {@link IChunkWrapper#setDhBlockLight(int, int, int, int)} and {@link IChunkWrapper#setDhSkyLight(int, int, int, int)}
* the light values may be stored in the wrapper itself instead of the wrapped chunk object. * the light values may be stored in the wrapper itself instead of the wrapped chunk object.
@@ -93,12 +76,12 @@ public class DhLightingEngine
// try-finally to handle the stableArray resources // try-finally to handle the stableArray resources
StableLightPosStack blockLightWorldPosQueue = null; StableLightPosStack blockLightPosQueue = null;
StableLightPosStack skyLightWorldPosQueue = null; StableLightPosStack skyLightPosQueue = null;
try try
{ {
blockLightWorldPosQueue = StableLightPosStack.borrowStableLightPosArray(); blockLightPosQueue = StableLightPosStack.borrowStableLightPosArray();
skyLightWorldPosQueue = StableLightPosStack.borrowStableLightPosArray(); skyLightPosQueue = StableLightPosStack.borrowStableLightPosArray();
@@ -109,7 +92,7 @@ public class DhLightingEngine
{ {
for (int zOffset = -1; zOffset <= 1; zOffset++) for (int zOffset = -1; zOffset <= 1; zOffset++)
{ {
DhChunkPos adjacentPos = new DhChunkPos(centerChunkPos.getX() + xOffset, centerChunkPos.getZ() + zOffset); DhChunkPos adjacentPos = new DhChunkPos(centerChunkPos.x + xOffset, centerChunkPos.z + zOffset);
requestedAdjacentPositions.add(adjacentPos); requestedAdjacentPositions.add(adjacentPos);
} }
} }
@@ -131,14 +114,11 @@ public class DhLightingEngine
//==================//
// set block lights //
//==================//
// get and set the adjacent chunk's initial block lights // get and set the adjacent chunk's initial block lights
final DhBlockPosMutable relLightBlockPos = PRIMARY_BLOCK_POS_REF.get(); final DhBlockPos relLightBlockPos = PRIMARY_BLOCK_POS_REF.get();
final DhBlockPos relBlockPos = SECONDARY_BLOCK_POS_REF.get();
ArrayList<DhBlockPos> blockLightPosList = chunk.getWorldBlockLightPosList(); ArrayList<DhBlockPos> blockLightPosList = chunk.getBlockLightPosList();
for (int blockLightIndex = 0; blockLightIndex < blockLightPosList.size(); blockLightIndex++) // using iterators in high traffic areas can cause GC issues due to allocating a bunch of iterators, use an indexed for-loop instead for (int blockLightIndex = 0; blockLightIndex < blockLightPosList.size(); blockLightIndex++) // using iterators in high traffic areas can cause GC issues due to allocating a bunch of iterators, use an indexed for-loop instead
{ {
DhBlockPos blockLightPos = blockLightPosList.get(blockLightIndex); DhBlockPos blockLightPos = blockLightPosList.get(blockLightIndex);
@@ -147,18 +127,14 @@ public class DhLightingEngine
// get the light // get the light
IBlockStateWrapper blockState = chunk.getBlockState(relLightBlockPos); IBlockStateWrapper blockState = chunk.getBlockState(relLightBlockPos);
int lightValue = blockState.getLightEmission(); int lightValue = blockState.getLightEmission();
blockLightWorldPosQueue.push(blockLightPos.getX(), blockLightPos.getY(), blockLightPos.getZ(), lightValue); blockLightPosQueue.push(blockLightPos.x, blockLightPos.y, blockLightPos.z, lightValue);
// set the light // set the light
chunk.setDhBlockLight(relLightBlockPos.getX(), relLightBlockPos.getY(), relLightBlockPos.getZ(), lightValue); blockLightPos.mutateToChunkRelativePos(relBlockPos);
chunk.setDhBlockLight(relBlockPos.x, relBlockPos.y, relBlockPos.z, lightValue);
} }
//================//
// set sky lights //
//================//
// get and set the adjacent chunk's initial skylights, // get and set the adjacent chunk's initial skylights,
// if the dimension has skylights // if the dimension has skylights
if (maxSkyLight > 0) if (maxSkyLight > 0)
@@ -171,7 +147,7 @@ public class DhLightingEngine
{ {
for (int relZ = 0; relZ < LodUtil.CHUNK_WIDTH; relZ++) for (int relZ = 0; relZ < LodUtil.CHUNK_WIDTH; relZ++)
{ {
// set each pos' sky light all the way down until an opaque block is hit // set each pos' sky light all the way down until a opaque block is hit
for (int y = maxY; y >= minY; y--) for (int y = maxY; y >= minY; y--)
{ {
IBlockStateWrapper block = chunk.getBlockState(relX, y, relZ); IBlockStateWrapper block = chunk.getBlockState(relX, y, relZ);
@@ -184,11 +160,11 @@ public class DhLightingEngine
// add sky light to the queue // add sky light to the queue
DhBlockPos skyLightPos = new DhBlockPos(chunk.getMinBlockX() + relX, y, chunk.getMinBlockZ() + relZ); DhBlockPos skyLightPos = new DhBlockPos(chunk.getMinBlockX() + relX, y, chunk.getMinBlockZ() + relZ);
skyLightWorldPosQueue.push(skyLightPos.getX(), skyLightPos.getY(), skyLightPos.getZ(), maxSkyLight); skyLightPosQueue.push(skyLightPos.x, skyLightPos.y, skyLightPos.z, maxSkyLight);
// set the chunk's sky light // set the chunk's sky light
skyLightPos.mutateToChunkRelativePos(relLightBlockPos); skyLightPos.mutateToChunkRelativePos(relBlockPos);
chunk.setDhSkyLight(relLightBlockPos.getX(), relLightBlockPos.getY(), relLightBlockPos.getZ(), maxSkyLight); chunk.setDhSkyLight(relBlockPos.x, relBlockPos.y, relBlockPos.z, maxSkyLight);
} }
} }
} }
@@ -204,16 +180,14 @@ public class DhLightingEngine
} }
// block light // block light
this.propagateLightPosList(blockLightWorldPosQueue, adjacentChunkHolder, this.propagateLightPosList(blockLightPosQueue, adjacentChunkHolder,
(neighbourChunk, relBlockPos) -> neighbourChunk.getDhBlockLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ()), (neighbourChunk, relBlockPos) -> neighbourChunk.getDhBlockLight(relBlockPos.x, relBlockPos.y, relBlockPos.z),
(neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhBlockLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ(), newLightValue), (neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhBlockLight(relBlockPos.x, relBlockPos.y, relBlockPos.z, newLightValue));
true);
// sky light // sky light
this.propagateLightPosList(skyLightWorldPosQueue, adjacentChunkHolder, this.propagateLightPosList(skyLightPosQueue, adjacentChunkHolder,
(neighbourChunk, relBlockPos) -> neighbourChunk.getDhSkyLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ()), (neighbourChunk, relBlockPos) -> neighbourChunk.getDhSkyLight(relBlockPos.x, relBlockPos.y, relBlockPos.z),
(neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhSkyLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ(), newLightValue), (neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhSkyLight(relBlockPos.x, relBlockPos.y, relBlockPos.z, newLightValue));
false);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -221,8 +195,8 @@ public class DhLightingEngine
} }
finally finally
{ {
StableLightPosStack.returnStableLightPosArray(blockLightWorldPosQueue); StableLightPosStack.returnStableLightPosArray(blockLightPosQueue);
StableLightPosStack.returnStableLightPosArray(skyLightWorldPosQueue); StableLightPosStack.returnStableLightPosArray(skyLightPosQueue);
} }
@@ -238,14 +212,13 @@ public class DhLightingEngine
/** Applies each {@link LightPos} from the queue to the given set of {@link IChunkWrapper}'s. */ /** Applies each {@link LightPos} from the queue to the given set of {@link IChunkWrapper}'s. */
private void propagateLightPosList( private void propagateLightPosList(
StableLightPosStack lightPosQueue, AdjacentChunkHolder adjacentChunkHolder, StableLightPosStack lightPosQueue, AdjacentChunkHolder adjacentChunkHolder,
IGetLightFunc getLightFunc, ISetLightFunc setLightFunc, IGetLightFunc getLightFunc, ISetLightFunc setLightFunc)
boolean propagatingBlockLights)
{ {
// these objects are saved so they can be mutated throughout the method, // these objects are saved so they can be mutated throughout the method,
// this reduces the number of allocations necessary, reducing GC pressure // this reduces the number of allocations necessary, reducing GC pressure
final LightPos lightPos = new LightPos(0, 0, 0, 0); final LightPos lightPos = new LightPos(0, 0, 0, 0);
final DhBlockPosMutable neighbourBlockPos = PRIMARY_BLOCK_POS_REF.get(); final DhBlockPos neighbourBlockPos = PRIMARY_BLOCK_POS_REF.get();
final DhBlockPosMutable relNeighbourBlockPos = SECONDARY_BLOCK_POS_REF.get(); final DhBlockPos relNeighbourBlockPos = SECONDARY_BLOCK_POS_REF.get();
// update each light position // update each light position
@@ -266,14 +239,14 @@ public class DhLightingEngine
// only continue if the light position is inside one of our chunks // only continue if the light position is inside one of our chunks
IChunkWrapper neighbourChunk = adjacentChunkHolder.getByBlockPos(neighbourBlockPos.getX(), neighbourBlockPos.getZ()); IChunkWrapper neighbourChunk = adjacentChunkHolder.getByBlockPos(neighbourBlockPos.x, neighbourBlockPos.z);
if (neighbourChunk == null) if (neighbourChunk == null)
{ {
// the light pos is outside our generator's range, ignore it // the light pos is outside our generator's range, ignore it
continue; continue;
} }
if (relNeighbourBlockPos.getY() < neighbourChunk.getMinNonEmptyHeight() || relNeighbourBlockPos.getY() > neighbourChunk.getMaxBuildHeight()) if (relNeighbourBlockPos.y < neighbourChunk.getMinNonEmptyHeight() || relNeighbourBlockPos.y > neighbourChunk.getMaxBuildHeight())
{ {
// the light pos is outside the chunk's min/max height, // the light pos is outside the chunk's min/max height,
// this can happen if given a chunk that hasn't finished generating // this can happen if given a chunk that hasn't finished generating
@@ -300,103 +273,16 @@ public class DhLightingEngine
// now that light has been propagated to this blockPos // now that light has been propagated to this blockPos
// we need to queue it up so its neighbours can be propagated as well // we need to queue it up so its neighbours can be propagated as well
lightPosQueue.push(neighbourBlockPos.getX(), neighbourBlockPos.getY(), neighbourBlockPos.getZ(), targetLevel); lightPosQueue.push(neighbourBlockPos.x, neighbourBlockPos.y, neighbourBlockPos.z, targetLevel);
} }
} }
} }
// can be enable if troubleshooting lighting issues
if (RENDER_BLOCK_LIGHT_WIREFRAME && propagatingBlockLights)
{
RenderDhLightValuesAsWireframe(adjacentChunkHolder, true);
}
else if (RENDER_SKY_LIGHT_WIREFRAME && !propagatingBlockLights)
{
RenderDhLightValuesAsWireframe(adjacentChunkHolder, false);
}
// propagation complete // propagation complete
} }
//===========//
// debugging //
//===========//
/** Draw a wireframe representing each block's light value */
private static void RenderDhLightValuesAsWireframe(AdjacentChunkHolder adjacentChunkHolder, boolean renderBlockLights)
{
for (IChunkWrapper chunk : adjacentChunkHolder.chunkArray)
{
if (chunk == null)
{
continue;
}
int chunkMinX = chunk.getMinBlockX();
int chunkMinZ = chunk.getMinBlockZ();
int minY = chunk.getMinNonEmptyHeight();
int maxY = chunk.getMaxNonEmptyHeight();
// check each position's light
for (int x = 0; x < LodUtil.CHUNK_WIDTH; x++)
{
for (int z = 0; z < LodUtil.CHUNK_WIDTH; z++)
{
for (int y = minY; y < maxY; y++)
{
int lightValue = renderBlockLights? chunk.getDhBlockLight(x, y, z) : chunk.getDhSkyLight(x, y, z);
if (lightValue != LodUtil.MIN_MC_LIGHT)
{
// hotter colors for more intense light
Color color;
if (lightValue >= 14)
{
color = Color.WHITE;
}
else if (lightValue >= 10)
{
color = Color.PINK;
}
else if (lightValue >= 6)
{
color = Color.YELLOW;
}
else if (lightValue >= 4)
{
color = Color.ORANGE;
}
else
{
color = Color.RED;
}
// a color can be set to null if you only want to troubleshoot up to a certain light level
if (color != null)
{
DebugRenderer.makeParticle(
new DebugRenderer.BoxParticle(
new DebugRenderer.Box(DhSectionPos.encode((byte) 0, chunkMinX + x, chunkMinZ + z), y, y + 1, 0.2f, color),
10.0, 0f
)
);
}
}
}
}
}
}
}
//================// //================//
// helper classes // // helper classes //
//================// //================//
@@ -407,7 +293,7 @@ public class DhLightingEngine
@FunctionalInterface @FunctionalInterface
interface ISetLightFunc { void setLight(IChunkWrapper chunk, DhBlockPos pos, int lightValue); } interface ISetLightFunc { void setLight(IChunkWrapper chunk, DhBlockPos pos, int lightValue); }
private static class LightPos extends DhBlockPosMutable private static class LightPos extends DhBlockPos
{ {
public int lightValue; public int lightValue;
@@ -417,11 +303,6 @@ public class DhLightingEngine
this.lightValue = lightValue; this.lightValue = lightValue;
} }
@Override
public String toString() { return this.lightValue+" - ["+ this.x +", "+ this.y +", "+ this.z +"]"; }
} }
/** /**
@@ -521,9 +402,9 @@ public class DhLightingEngine
{ {
int subIndex = this.index * INTS_PER_LIGHT_POS; int subIndex = this.index * INTS_PER_LIGHT_POS;
pos.setX(this.lightPositions.getInt(subIndex)); pos.x = this.lightPositions.getInt(subIndex);
pos.setY(this.lightPositions.getInt(subIndex + 1)); pos.y = this.lightPositions.getInt(subIndex + 1);
pos.setZ(this.lightPositions.getInt(subIndex + 2)); pos.z = this.lightPositions.getInt(subIndex + 2);
pos.lightValue = this.lightPositions.getInt(subIndex + 3); pos.lightValue = this.lightPositions.getInt(subIndex + 3);
this.index--; this.index--;
@@ -21,12 +21,11 @@ package com.seibel.distanthorizons.core.generation;
import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.LodQuadTree; import com.seibel.distanthorizons.core.render.LodQuadTree;
import java.io.Closeable; import java.io.Closeable;
import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
/** /**
@@ -105,8 +104,6 @@ public interface IFullDataSourceRetrievalQueue extends Closeable
/** used for rendering to the F3 menu */ /** used for rendering to the F3 menu */
int getEstimatedTotalTaskCount(); int getEstimatedTotalTaskCount();
void setEstimatedTotalTaskCount(int newEstimate); void setEstimatedTotalTaskCount(int newEstimate);
void addDebugMenuStringsToList(List<String> messageList);
} }
@@ -27,7 +27,7 @@ import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSour
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.generation.tasks.*; import com.seibel.distanthorizons.core.generation.tasks.*;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
@@ -47,7 +47,6 @@ import org.apache.logging.log4j.Logger;
import java.awt.*; import java.awt.*;
import java.util.*; import java.util.*;
import java.util.List;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -56,16 +55,6 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class); private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
/**
* Defines how many tasks can be queued per thread. <br><br>
*
* TODO the multiplier here should change dynamically based on how fast the generator is vs the queuing thread,
* if this is too high it may cause issues when moving,
* but if it is too low the generator threads won't have enough tasks to work on
*/
private static final int MAX_QUEUED_TASKS_PER_THREAD = 3;
private final IDhApiWorldGenerator generator; private final IDhApiWorldGenerator generator;
/** contains the positions that need to be generated */ /** contains the positions that need to be generated */
@@ -89,7 +78,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
// TODO this logic isn't great and can cause a limit to how many threads could be used for world generation, // TODO this logic isn't great and can cause a limit to how many threads could be used for world generation,
// however it won't cause duplicate requests or concurrency issues, so it will be good enough for now. // however it won't cause duplicate requests or concurrency issues, so it will be good enough for now.
// A good long term fix may be to either: // A good long term fix may be to either:
// 1. allow the generator to deal with larger sections (let the generator threads split up larger tasks into smaller ones // 1. allow the generator to deal with larger sections (let the generator threads split up larger tasks into smaller one
// 2. batch requests better. instead of sending 4 individual tasks of detail level N, send 1 task of detail level n+1 // 2. batch requests better. instead of sending 4 individual tasks of detail level N, send 1 task of detail level n+1
private final ExecutorService queueingThread = ThreadUtil.makeSingleThreadPool("World Gen Queue"); private final ExecutorService queueingThread = ThreadUtil.makeSingleThreadPool("World Gen Queue");
private boolean generationQueueRunning = false; private boolean generationQueueRunning = false;
@@ -217,7 +206,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
// queue generation tasks until the generator is full, or there are no more tasks to generate // queue generation tasks until the generator is full, or there are no more tasks to generate
boolean taskStarted = true; boolean taskStarted = true;
while (!this.isGeneratorBusy() && taskStarted) while (!this.generator.isBusy() && taskStarted)
{ {
taskStarted = this.startNextWorldGenTask(this.generationTargetPos); taskStarted = this.startNextWorldGenTask(this.generationTargetPos);
if (!taskStarted) if (!taskStarted)
@@ -237,26 +226,10 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
catch (Exception e) catch (Exception e)
{ {
LOGGER.error("queueing exception: " + e.getMessage(), e); LOGGER.error("queueing exception: " + e.getMessage(), e);
}
finally
{
this.generationQueueRunning = false; this.generationQueueRunning = false;
} }
}); });
} }
public boolean isGeneratorBusy()
{
ThreadPoolExecutor executor = ThreadPoolUtil.getWorldGenExecutor();
if (executor == null)
{
// shouldn't happen, but just in case, don't queue more tasks
return true;
}
int worldGenThreadCount = Math.max(Config.Client.Advanced.MultiThreading.numberOfWorldGenerationThreads.get(), 1);
int maxWorldGenTaskCount = worldGenThreadCount * MAX_QUEUED_TASKS_PER_THREAD;
return executor.getQueue().size() > maxWorldGenTaskCount;
}
/** /**
* @param targetPos the position to center the generation around * @param targetPos the position to center the generation around
@@ -400,7 +373,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
// don't log the shutdown exceptions // don't log the shutdown exceptions
if (!LodUtil.isInterruptOrReject(exception)) if (!LodUtil.isInterruptOrReject(exception))
{ {
LOGGER.error("Error generating data for pos: " + DhSectionPos.toString(taskPos), exception); LOGGER.error("Error generating data for section " + taskPos, exception);
} }
newTaskGroup.group.worldGenTasks.forEach(worldGenTask -> worldGenTask.future.complete(WorldGenResult.CreateFail())); newTaskGroup.group.worldGenTasks.forEach(worldGenTask -> worldGenTask.future.complete(WorldGenResult.CreateFail()));
@@ -410,11 +383,11 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
newTaskGroup.group.worldGenTasks.forEach(worldGenTask -> worldGenTask.future.complete(WorldGenResult.CreateSuccess(DhSectionPos.encode(granularity, DhSectionPos.getX(taskPos), DhSectionPos.getZ(taskPos))))); newTaskGroup.group.worldGenTasks.forEach(worldGenTask -> worldGenTask.future.complete(WorldGenResult.CreateSuccess(DhSectionPos.encode(granularity, DhSectionPos.getX(taskPos), DhSectionPos.getZ(taskPos)))));
} }
boolean worked = this.inProgressGenTasksByLodPos.remove(taskPos, newTaskGroup); boolean worked = this.inProgressGenTasksByLodPos.remove(taskPos, newTaskGroup);
LodUtil.assertTrue(worked, "Unable to find in progress generator task with position ["+DhSectionPos.toString(taskPos)+"]"); LodUtil.assertTrue(worked);
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.error("Unexpected error completing world gen task at pos: ["+DhSectionPos.toString(taskPos)+"].", e); LOGGER.error("Unexpected error completing world gen task: "+taskPos, e);
} }
}); });
@@ -457,8 +430,8 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
case VANILLA_CHUNKS: case VANILLA_CHUNKS:
{ {
return this.generator.generateChunks( return this.generator.generateChunks(
chunkPosMin.getX(), chunkPosMin.x,
chunkPosMin.getZ(), chunkPosMin.z,
granularity, granularity,
targetDataDetail, targetDataDetail,
generatorMode, generatorMode,
@@ -483,8 +456,8 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
case API_CHUNKS: case API_CHUNKS:
{ {
return this.generator.generateApiChunks( return this.generator.generateApiChunks(
chunkPosMin.getX(), chunkPosMin.x,
chunkPosMin.getZ(), chunkPosMin.z,
granularity, granularity,
targetDataDetail, targetDataDetail,
generatorMode, generatorMode,
@@ -493,10 +466,10 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
{ {
try try
{ {
FullDataSourceV2 dataSource = LodDataBuilder.createFromApiChunkData(dataPoints, this.generator.runApiChunkValidation()); FullDataSourceV2 dataSource = LodDataBuilder.createFromApiChunkData(dataPoints);
chunkDataConsumer.accept(dataSource); chunkDataConsumer.accept(dataSource);
} }
catch (DataCorruptedException | IllegalArgumentException e) catch (DataCorruptedException e)
{ {
LOGGER.error("World generator returned a corrupt chunk. Error: [" + e.getMessage() + "]. World generator disabled.", e); LOGGER.error("World generator returned a corrupt chunk. Error: [" + e.getMessage() + "]. World generator disabled.", e);
Config.Client.Advanced.WorldGenerator.enableDistantGeneration.set(false); Config.Client.Advanced.WorldGenerator.enableDistantGeneration.set(false);
@@ -536,8 +509,6 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
@Override @Override
public void setEstimatedTotalTaskCount(int newEstimate) { this.estimatedTotalTaskCount = newEstimate; } public void setEstimatedTotalTaskCount(int newEstimate) { this.estimatedTotalTaskCount = newEstimate; }
public void addDebugMenuStringsToList(List<String> messageList) { }
//==========// //==========//
@@ -701,4 +672,4 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
} }
} }
@@ -1,75 +0,0 @@
package com.seibel.distanthorizons.core.generation;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult;
import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.multiplayer.client.AbstractFullDataRequestQueue;
import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.util.LodUtil;
import org.apache.logging.log4j.Logger;
import java.util.concurrent.*;
public class WorldRemoteGenerationQueue extends AbstractFullDataRequestQueue implements IFullDataSourceRetrievalQueue, IDebugRenderable
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private int estimatedTotalTaskCount;
@Override
protected int getRequestRateLimit() { return this.networkState.config.getGenerationRequestRateLimit(); }
@Override
protected String getQueueName() { return "World Remote Generation Queue"; }
public WorldRemoteGenerationQueue(ClientNetworkState networkState, IDhClientLevel level)
{
super(networkState, level, false, Config.Client.Advanced.Debugging.DebugWireframe.showWorldGenQueue);
}
@Override
public byte lowestDataDetail()
{
return LodUtil.BLOCK_DETAIL_LEVEL;
}
@Override
public byte highestDataDetail()
{
return LodUtil.BLOCK_DETAIL_LEVEL;
}
@Override
public CompletableFuture<WorldGenResult> submitGenTask(long sectionPos, byte requiredDataDetail, IWorldGenTaskTracker tracker)
{
return super.submitRequest(sectionPos, tracker.getChunkDataConsumer())
.thenApply(result -> result
? WorldGenResult.CreateSuccess(sectionPos)
: WorldGenResult.CreateFail());
}
@Override
public void startAndSetTargetPos(DhBlockPos2D targetPos)
{
super.tick(targetPos);
}
@Override
public int getEstimatedTotalTaskCount() { return this.estimatedTotalTaskCount; }
@Override
public void setEstimatedTotalTaskCount(int newEstimate) { this.estimatedTotalTaskCount = newEstimate; }
@Override
public CompletableFuture<Void> startClosing(boolean cancelCurrentGeneration, boolean alsoInterruptRunning)
{
return super.startClosing(alsoInterruptRunning);
}
}
@@ -21,7 +21,6 @@ package com.seibel.distanthorizons.core.generation.tasks;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import javax.annotation.Nullable;
import java.util.function.Consumer; import java.util.function.Consumer;
/** /**
@@ -33,7 +32,6 @@ public interface IWorldGenTaskTracker
/** Returns true if the task hasn't been garbage collected. */ /** Returns true if the task hasn't been garbage collected. */
boolean isMemoryAddressValid(); boolean isMemoryAddressValid();
@Nullable
Consumer<FullDataSourceV2> getChunkDataConsumer(); Consumer<FullDataSourceV2> getChunkDataConsumer();
} }
@@ -36,4 +36,4 @@ public final class InProgressWorldGenTaskGroup
this.group = group; this.group = group;
} }
} }
@@ -19,19 +19,28 @@
package com.seibel.distanthorizons.core.level; package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.api.interfaces.render.IDhApiRenderableBoxGroup;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiChunkModifiedEvent; import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiChunkModifiedEvent;
import com.seibel.distanthorizons.api.objects.math.DhApiVec3f;
import com.seibel.distanthorizons.api.objects.render.DhApiRenderableBox;
import com.seibel.distanthorizons.api.objects.render.DhApiRenderableBoxGroupShading;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dataObjects.transformers.ChunkToLodBuilder;
import com.seibel.distanthorizons.core.file.fullDatafile.DelayedFullDataSourceSaveCache; import com.seibel.distanthorizons.core.file.fullDatafile.DelayedFullDataSourceSaveCache;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.renderer.generic.BeaconRenderHandler; import com.seibel.distanthorizons.core.render.renderer.generic.BeaconRenderHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.CloudRenderHandler; import com.seibel.distanthorizons.core.render.renderer.generic.CloudRenderHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericRenderObjectFactory;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO; import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.sql.dto.ChunkHashDTO; import com.seibel.distanthorizons.core.sql.dto.ChunkHashDTO;
import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo; import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo;
import com.seibel.distanthorizons.core.sql.repo.ChunkHashRepo; import com.seibel.distanthorizons.core.sql.repo.ChunkHashRepo;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -40,14 +49,18 @@ import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public abstract class AbstractDhLevel implements IDhLevel public abstract class AbstractDhLevel implements IDhLevel
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public final ChunkToLodBuilder chunkToLodBuilder;
/** if this is null then the other handler is probably null too, but just in case */ /** if this is null then the other handler is probably null too, but just in case */
@Nullable @Nullable
public ChunkHashRepo chunkHashRepo; public ChunkHashRepo chunkHashRepo;
@@ -55,10 +68,9 @@ public abstract class AbstractDhLevel implements IDhLevel
@Nullable @Nullable
public BeaconBeamRepo beaconBeamRepo; public BeaconBeamRepo beaconBeamRepo;
protected final DelayedFullDataSourceSaveCache delayedFullDataSourceSaveCache = new DelayedFullDataSourceSaveCache(this::onDataSourceSave, 500); protected final DelayedFullDataSourceSaveCache delayedFullDataSourceSaveCache = new DelayedFullDataSourceSaveCache(this::onDataSourceSave, 2_000);
/** contains the {@link DhChunkPos} for each {@link DhSectionPos} that are queued to save via {@link AbstractDhLevel#delayedFullDataSourceSaveCache} */ /** contains the {@link DhChunkPos} for each {@link DhSectionPos} that are queued to save via {@link AbstractDhLevel#delayedFullDataSourceSaveCache} */
protected final ConcurrentHashMap<Long, HashSet<DhChunkPos>> updatedChunkPosSetBySectionPos = new ConcurrentHashMap<>(); protected final ConcurrentHashMap<Long, HashSet<DhChunkPos>> updatedChunkPosSetBySectionPos = new ConcurrentHashMap<>();
protected final ConcurrentHashMap<DhChunkPos, Integer> updatedChunkHashesByChunkPos = new ConcurrentHashMap<>();
/** Will be null if clouds shouldn't be rendered for this level. */ /** Will be null if clouds shouldn't be rendered for this level. */
@Nullable @Nullable
@@ -71,7 +83,10 @@ public abstract class AbstractDhLevel implements IDhLevel
// constructor // // constructor //
//=============// //=============//
protected AbstractDhLevel() { } protected AbstractDhLevel()
{
this.chunkToLodBuilder = new ChunkToLodBuilder();
}
/** /**
* Creating the repos requires access to the level file, which isn't * Creating the repos requires access to the level file, which isn't
@@ -111,15 +126,11 @@ public abstract class AbstractDhLevel implements IDhLevel
GenericObjectRenderer genericRenderer = this.getGenericRenderer(); GenericObjectRenderer genericRenderer = this.getGenericRenderer();
if (genericRenderer != null) if (genericRenderer != null)
{ {
// only client levels can render clouds // only add clouds for certain dimension types
if (this instanceof IDhClientLevel) if (!this.getLevelWrapper().hasCeiling()
&& !this.getLevelWrapper().getDimensionType().isTheEnd())
{ {
// only add clouds for certain dimension types this.cloudRenderHandler = new CloudRenderHandler(this, genericRenderer);
if (!this.getLevelWrapper().hasCeiling()
&& !this.getLevelWrapper().getDimensionType().isTheEnd())
{
this.cloudRenderHandler = new CloudRenderHandler((IDhClientLevel)this, genericRenderer);
}
} }
@@ -141,7 +152,7 @@ public abstract class AbstractDhLevel implements IDhLevel
public int getUnsavedDataSourceCount() { return this.delayedFullDataSourceSaveCache.getUnsavedCount(); } public int getUnsavedDataSourceCount() { return this.delayedFullDataSourceSaveCache.getUnsavedCount(); }
@Override @Override
public void updateChunkAsync(IChunkWrapper chunkWrapper, int chunkHash) public void updateChunkAsync(IChunkWrapper chunkWrapper)
{ {
FullDataSourceV2 dataSource = FullDataSourceV2.createFromChunk(chunkWrapper); FullDataSourceV2 dataSource = FullDataSourceV2.createFromChunk(chunkWrapper);
if (dataSource == null) if (dataSource == null)
@@ -151,7 +162,7 @@ public abstract class AbstractDhLevel implements IDhLevel
} }
this.updatedChunkPosSetBySectionPos.compute(dataSource.getPos(), (dataSourcePos, chunkPosSet) -> this.updatedChunkPosSetBySectionPos.compute(dataSource.getPos(), (dataSourcePos, chunkPosSet) ->
{ {
if (chunkPosSet == null) if (chunkPosSet == null)
{ {
@@ -160,7 +171,6 @@ public abstract class AbstractDhLevel implements IDhLevel
chunkPosSet.add(chunkWrapper.getChunkPos()); chunkPosSet.add(chunkWrapper.getChunkPos());
return chunkPosSet; return chunkPosSet;
}); });
this.updatedChunkHashesByChunkPos.put(chunkWrapper.getChunkPos(), chunkHash);
// batch updates to reduce overhead when flying around or breaking/placing a lot of blocks in an area // batch updates to reduce overhead when flying around or breaking/placing a lot of blocks in an area
this.delayedFullDataSourceSaveCache.queueDataSourceForUpdateAndSave(dataSource); this.delayedFullDataSourceSaveCache.queueDataSourceForUpdateAndSave(dataSource);
@@ -168,23 +178,16 @@ public abstract class AbstractDhLevel implements IDhLevel
private void onDataSourceSave(FullDataSourceV2 fullDataSource) private void onDataSourceSave(FullDataSourceV2 fullDataSource)
{ {
this.updateDataSourcesAsync(fullDataSource).thenRun(() -> this.updateDataSourcesAsync(fullDataSource).thenRun(() ->
{ {
HashSet<DhChunkPos> updatedChunkPosSet = this.updatedChunkPosSetBySectionPos.remove(fullDataSource.getPos()); HashSet<DhChunkPos> updatedChunkPosSet = this.updatedChunkPosSetBySectionPos.remove(fullDataSource.getPos());
if (updatedChunkPosSet != null) if (updatedChunkPosSet != null)
{ {
for (DhChunkPos chunkPos : updatedChunkPosSet) for (DhChunkPos chunkPos : updatedChunkPosSet)
{ {
// save after the data source has been updated to prevent saving the hash without the associated datasource
Integer chunkHash = this.updatedChunkHashesByChunkPos.remove(chunkPos);
if (this.chunkHashRepo != null && chunkHash != null)
{
this.chunkHashRepo.save(new ChunkHashDTO(chunkPos, chunkHash));
}
ApiEventInjector.INSTANCE.fireAllEvents( ApiEventInjector.INSTANCE.fireAllEvents(
DhApiChunkModifiedEvent.class, DhApiChunkModifiedEvent.class,
new DhApiChunkModifiedEvent.EventParam(this.getLevelWrapper(), chunkPos.getX(), chunkPos.getZ())); new DhApiChunkModifiedEvent.EventParam(this.getLevelWrapper(), chunkPos.x, chunkPos.z));
} }
} }
}); });
@@ -209,6 +212,14 @@ public abstract class AbstractDhLevel implements IDhLevel
ChunkHashDTO dto = this.chunkHashRepo.getByKey(pos); ChunkHashDTO dto = this.chunkHashRepo.getByKey(pos);
return (dto != null) ? dto.chunkHash : 0; return (dto != null) ? dto.chunkHash : 0;
} }
@Override
public void setChunkHash(DhChunkPos pos, int chunkHash)
{
if (this.chunkHashRepo != null)
{
this.chunkHashRepo.save(new ChunkHashDTO(pos, chunkHash));
}
}
@@ -217,12 +228,11 @@ public abstract class AbstractDhLevel implements IDhLevel
//=================// //=================//
@Override @Override
public void updateBeaconBeamsForChunk(IChunkWrapper chunkToUpdate, ArrayList<IChunkWrapper> nearbyChunkList) public void setBeaconBeamsForChunk(DhChunkPos chunkPos, List<BeaconBeamDTO> newBeamList)
{ {
if (this.beaconRenderHandler != null) if (this.beaconRenderHandler != null)
{ {
List<BeaconBeamDTO> activeBeamList = chunkToUpdate.getAllActiveBeacons(nearbyChunkList); this.beaconRenderHandler.setBeaconBeamsForChunk(chunkPos, newBeamList);
this.beaconRenderHandler.setBeaconBeamsForChunk(chunkToUpdate.getChunkPos(), activeBeamList);
} }
} }
@@ -252,6 +262,8 @@ public abstract class AbstractDhLevel implements IDhLevel
@Override @Override
public void close() public void close()
{ {
this.chunkToLodBuilder.close();
if (this.chunkHashRepo != null) if (this.chunkHashRepo != null)
{ {
this.chunkHashRepo.close(); this.chunkHashRepo.close();
@@ -262,4 +274,4 @@ public abstract class AbstractDhLevel implements IDhLevel
} }
} }
} }
@@ -27,7 +27,7 @@ import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.file.AbstractDataSourceHandler; import com.seibel.distanthorizons.core.file.AbstractDataSourceHandler;
import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.DhBlockPos2D;
import com.seibel.distanthorizons.core.render.LodQuadTree; import com.seibel.distanthorizons.core.render.LodQuadTree;
import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.RenderBufferHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
@@ -258,8 +258,6 @@ public class ClientLevelModule implements Closeable, AbstractDataSourceHandler.I
public void clearRenderCache() public void clearRenderCache()
{ {
this.clientLevel.getClientLevelWrapper().clearBlockColorCache();
ClientRenderState ClientRenderState = this.ClientRenderStateRef.get(); ClientRenderState ClientRenderState = this.ClientRenderStateRef.get();
if (ClientRenderState != null && ClientRenderState.quadtree != null) if (ClientRenderState != null && ClientRenderState.quadtree != null)
{ {
@@ -20,27 +20,15 @@
package com.seibel.distanthorizons.core.level; package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.core.config.AppliedConfigState;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.file.fullDatafile.RemoteFullDataSourceProvider; import com.seibel.distanthorizons.core.file.fullDatafile.RemoteFullDataSourceProvider;
import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure;
import com.seibel.distanthorizons.core.generation.WorldRemoteGenerationQueue;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState; import com.seibel.distanthorizons.core.pos.DhBlockPos;
import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoginRequestQueue;
import com.seibel.distanthorizons.core.network.event.ScopedNetworkEventSource;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.RenderBufferHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
@@ -48,8 +36,6 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import javax.annotation.CheckForNull;
import java.awt.*;
import java.io.File; import java.io.File;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -58,66 +44,25 @@ import java.util.concurrent.CompletableFuture;
public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static class WorldGenState extends WorldGenModule.AbstractWorldGenState
{
WorldGenState(IDhClientLevel level, ClientNetworkState networkState)
{
this.worldGenerationQueue = new WorldRemoteGenerationQueue(networkState, level);
}
}
public final ClientLevelModule clientside; public final ClientLevelModule clientside;
public final IClientLevelWrapper levelWrapper; public final IClientLevelWrapper levelWrapper;
public final AbstractSaveStructure saveStructure; public final AbstractSaveStructure saveStructure;
public final RemoteFullDataSourceProvider dataFileHandler; public final RemoteFullDataSourceProvider dataFileHandler;
@CheckForNull
private final ClientNetworkState networkState;
@Nullable
private final ScopedNetworkEventSource eventSource;
public final WorldGenModule worldGenModule;
public final AppliedConfigState<Boolean> worldGeneratorEnabledConfig;
@Nullable
private final SyncOnLoginRequestQueue syncOnLoginRequestQueue;
//=============// //=============//
// constructor // // constructor //
//=============// //=============//
public DhClientLevel(AbstractSaveStructure saveStructure, IClientLevelWrapper clientLevelWrapper, @Nullable ClientNetworkState networkState) { this(saveStructure, clientLevelWrapper, null, true, networkState); } public DhClientLevel(AbstractSaveStructure saveStructure, IClientLevelWrapper clientLevelWrapper) { this(saveStructure, clientLevelWrapper, null, true); }
public DhClientLevel(AbstractSaveStructure saveStructure, IClientLevelWrapper clientLevelWrapper, @Nullable File fullDataSaveDirOverride, boolean enableRendering, @Nullable ClientNetworkState networkState) public DhClientLevel(AbstractSaveStructure saveStructure, IClientLevelWrapper clientLevelWrapper, @Nullable File fullDataSaveDirOverride, boolean enableRendering)
{ {
if (saveStructure.getFullDataFolder(clientLevelWrapper).mkdirs())
{
LOGGER.warn("unable to create data folder.");
}
this.levelWrapper = clientLevelWrapper; this.levelWrapper = clientLevelWrapper;
this.levelWrapper.setParentLevel(this); this.levelWrapper.setParentLevel(this);
this.saveStructure = saveStructure; this.saveStructure = saveStructure;
this.dataFileHandler = new RemoteFullDataSourceProvider(this, saveStructure, fullDataSaveDirOverride);
this.networkState = networkState;
if (networkState != null)
{
this.eventSource = new ScopedNetworkEventSource(networkState.getSession());
this.syncOnLoginRequestQueue = new SyncOnLoginRequestQueue(this, networkState);
this.registerNetworkHandlers();
}
else
{
this.eventSource = null;
this.syncOnLoginRequestQueue = null;
}
this.dataFileHandler = new RemoteFullDataSourceProvider(this, saveStructure, fullDataSaveDirOverride, this.syncOnLoginRequestQueue);
this.worldGeneratorEnabledConfig = new AppliedConfigState<>(Config.Client.Advanced.WorldGenerator.enableDistantGeneration);
this.worldGenModule = new WorldGenModule(this);
this.clientside = new ClientLevelModule(this); this.clientside = new ClientLevelModule(this);
this.createAndSetSupportingRepos(this.dataFileHandler.repo.databaseFile); this.createAndSetSupportingRepos(this.dataFileHandler.repo.databaseFile);
@@ -130,30 +75,7 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
} }
} }
private void registerNetworkHandlers()
{
assert this.eventSource != null;
assert this.networkState != null;
this.eventSource.registerHandler(FullDataPartialUpdateMessage.class, msg ->
{
try
{
FullDataSourceV2DTO dataSourceDto = this.networkState.decodeDataSourceAndReleaseBuffer(msg.payload);
if (!msg.isSameLevelAs(this.levelWrapper))
{
return;
}
this.updateDataSourcesAsync(dataSourceDto.createPooledDataSource(this.levelWrapper));
}
catch (Exception e)
{
LOGGER.error("Error while updating full data source", e);
}
});
}
//==============// //==============//
// tick methods // // tick methods //
@@ -164,12 +86,8 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
{ {
try try
{ {
this.chunkToLodBuilder.tick();
this.clientside.clientTick(); this.clientside.clientTick();
if (this.syncOnLoginRequestQueue != null)
{
this.syncOnLoginRequestQueue.tick(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos()));
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -177,50 +95,6 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
} }
} }
@Override
public void doWorldGen()
{
ClientNetworkState networkState = this.networkState;
boolean isClientUsable = false, isAllowedDimension = false;
if (networkState != null)
{
isClientUsable = networkState.isReady();
isAllowedDimension = MC_CLIENT.getWrappedClientLevel() == this.levelWrapper;
}
boolean shouldDoWorldGen = isClientUsable
&& networkState.config.isDistantGenerationEnabled()
&& isAllowedDimension
&& this.clientside.isRendering();
boolean isWorldGenRunning = this.worldGenModule.isWorldGenRunning();
if (shouldDoWorldGen && !isWorldGenRunning)
{
// start world gen
this.worldGenModule.startWorldGen(this.dataFileHandler, new WorldGenState(this, networkState));
// populate the queue based on the current rendering tree
ClientLevelModule.ClientRenderState renderState = this.clientside.ClientRenderStateRef.get();
renderState.quadtree.leafNodeIterator().forEachRemaining(node -> {
this.dataFileHandler.getAsync(node.sectionPos);
});
}
else if (!shouldDoWorldGen && isWorldGenRunning)
{
// stop world gen
this.worldGenModule.stopWorldGen(this.dataFileHandler);
}
if (this.worldGenModule.isWorldGenRunning())
{
this.worldGenModule.worldGenTick(
new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos())
.scale(MC_CLIENT.getWrappedClientLevel().getDimensionType().getTeleportationScale(this.getLevelWrapper().getDimensionType()))
);
}
}
@Override @Override
public void render(DhApiRenderParam renderEventParam, IProfilerWrapper profiler) public void render(DhApiRenderParam renderEventParam, IProfilerWrapper profiler)
{ this.clientside.render(renderEventParam, profiler); } { this.clientside.render(renderEventParam, profiler); }
@@ -236,16 +110,13 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
//================// //================//
@Override @Override
public int computeBaseColor(DhBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper block) { return this.levelWrapper.getBlockColor(pos, biome, block); } public int computeBaseColor(DhBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper block) { return this.levelWrapper.computeBaseColor(pos, biome, block); }
@Override @Override
public IClientLevelWrapper getClientLevelWrapper() { return this.levelWrapper; } public IClientLevelWrapper getClientLevelWrapper() { return this.levelWrapper; }
@Override @Override
public void clearRenderCache() public void clearRenderCache() { this.clientside.clearRenderCache(); }
{
this.clientside.clearRenderCache();
}
@Override @Override
public ILevelWrapper getLevelWrapper() { return this.levelWrapper; } public ILevelWrapper getLevelWrapper() { return this.levelWrapper; }
@@ -259,7 +130,7 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
@Override @Override
public void addDebugMenuStringsToList(List<String> messageList) public void addDebugMenuStringsToList(List<String> messageList)
{ {
String dimName = this.levelWrapper.getDimensionName(); String dimName = this.levelWrapper.getDimensionType().getDimensionName();
boolean rendering = this.clientside.isRendering(); boolean rendering = this.clientside.isRendering();
messageList.add("["+dimName+"] rendering: "+(rendering ? "yes" : "no")); messageList.add("["+dimName+"] rendering: "+(rendering ? "yes" : "no"));
@@ -282,33 +153,11 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
{ {
messageList.add(" Migration Failed"); messageList.add(" Migration Failed");
} }
// world gen
this.worldGenModule.addDebugMenuStringsToList(messageList);
if (this.syncOnLoginRequestQueue != null)
{
assert this.networkState != null;
if (this.networkState.config.getSynchronizeOnLogin())
{
this.syncOnLoginRequestQueue.addDebugMenuStringsToList(messageList);
}
}
} }
@Override @Override
public void close() public void close()
{ {
if (this.worldGenModule != null)
{
this.worldGenModule.close();
}
if (this.eventSource != null)
{
this.eventSource.close();
}
this.levelWrapper.setParentLevel(null); this.levelWrapper.setParentLevel(null);
this.clientside.close(); this.clientside.close();
super.close(); super.close();
@@ -335,16 +184,4 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
return (renderState != null) ? renderState.renderBufferHandler : null; return (renderState != null) ? renderState.renderBufferHandler : null;
} }
@Override }
public void onWorldGenTaskComplete(long pos)
{
DebugRenderer.makeParticle(
new DebugRenderer.BoxParticle(
new DebugRenderer.Box(pos, 128f, 156f, 0.09f, Color.red.darker()),
0.2, 32f
)
);
this.clientside.reloadPos(pos);
}
}
@@ -28,8 +28,8 @@ import com.seibel.distanthorizons.core.render.RenderBufferHandler;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.pos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.DhBlockPos2D;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper;
@@ -95,7 +95,7 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev
{ this.clientside.renderDeferred(renderEventParam, profiler); } { this.clientside.renderDeferred(renderEventParam, profiler); }
@Override @Override
public void serverTick() { } public void serverTick() { this.chunkToLodBuilder.tick(); }
@Override @Override
public void doWorldGen() public void doWorldGen()
@@ -146,7 +146,7 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev
} }
else else
{ {
return clientLevel.getBlockColor(pos, biome, block); return clientLevel.computeBaseColor(pos, biome, block);
} }
} }
@@ -154,14 +154,15 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev
public IClientLevelWrapper getClientLevelWrapper() { return MC_CLIENT.getWrappedClientLevel(); } public IClientLevelWrapper getClientLevelWrapper() { return MC_CLIENT.getWrappedClientLevel(); }
@Override @Override
public void clearRenderCache() { public void clearRenderCache()
this.clientside.clearRenderCache(); {
clientside.clearRenderCache();
} }
@Override @Override
public IServerLevelWrapper getServerLevelWrapper() { return this.serverLevelWrapper; } public IServerLevelWrapper getServerLevelWrapper() { return serverLevelWrapper; }
@Override @Override
public ILevelWrapper getLevelWrapper() { return this.getServerLevelWrapper(); } public ILevelWrapper getLevelWrapper() { return getServerLevelWrapper(); }
@Override @Override
public FullDataSourceProviderV2 getFullDataProvider() { return this.serverside.fullDataFileHandler; } public FullDataSourceProviderV2 getFullDataProvider() { return this.serverside.fullDataFileHandler; }
@@ -169,7 +170,7 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev
@Override @Override
public AbstractSaveStructure getSaveStructure() public AbstractSaveStructure getSaveStructure()
{ {
return this.serverside.saveStructure; return serverside.saveStructure;
} }
@Override @Override
@@ -191,7 +192,7 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev
public void addDebugMenuStringsToList(List<String> messageList) public void addDebugMenuStringsToList(List<String> messageList)
{ {
// header // header
String dimName = this.serverLevelWrapper.getDimensionName(); String dimName = this.serverLevelWrapper.getDimensionType().getDimensionName();
boolean rendering = this.clientside.isRendering(); boolean rendering = this.clientside.isRendering();
messageList.add("["+dimName+"] rendering: "+(rendering ? "yes" : "no")); messageList.add("["+dimName+"] rendering: "+(rendering ? "yes" : "no"));
@@ -218,7 +219,12 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev
// world gen // world gen
this.serverside.worldGenModule.addDebugMenuStringsToList(messageList); WorldGenModule worldGenState = this.serverside.worldGenModule;
String worldGenDisplayString = worldGenState.getDebugMenuString();
if (worldGenDisplayString != null)
{
messageList.add(worldGenDisplayString);
}
} }
@@ -259,4 +265,4 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev
this.clientside.reloadPos(pos); this.clientside.reloadPos(pos);
} }
} }
@@ -19,73 +19,33 @@
package com.seibel.distanthorizons.core.level; package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.DhBlockPos2D;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.multiplayer.server.RemotePlayerConnectionHandler;
import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerState;
import com.seibel.distanthorizons.core.network.exceptions.InvalidLevelException;
import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException;
import com.seibel.distanthorizons.core.network.messages.ILevelRelatedMessage;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage;
import com.seibel.distanthorizons.core.network.messages.TrackableMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPayload;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceResponseMessage;
import com.seibel.distanthorizons.core.network.messages.requests.CancelMessage;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.RenderBufferHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.math.Vec3d;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import javax.annotation.CheckForNull;
import java.text.MessageFormat;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.*;
import java.util.function.Consumer;
public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final ConfigBasedLogger NETWORK_LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Logging.logNetworkEvent.get());
public static final int FULL_DATA_CHUNK_SIZE = 1048000; // 576 bytes left for other contents
public final ServerLevelModule serverside; public final ServerLevelModule serverside;
private final IServerLevelWrapper serverLevelWrapper; private final IServerLevelWrapper serverLevelWrapper;
private final RemotePlayerConnectionHandler remotePlayerConnectionHandler;
/**
* This queue is used for ensuring fair generation speed for each player. <br>
* Every tick the first player gets used for centering generation, and then is immediately moved into the back of the queue. <br>
* TODO only add players that actually have something to generate
*/
private final ConcurrentLinkedQueue<IServerPlayerWrapper> worldGenPlayerCenteringQueue = new ConcurrentLinkedQueue<>();
private final ConcurrentMap<Long, DataSourceRequestGroup> requestGroupsByPos = new ConcurrentHashMap<>();
private final ConcurrentMap<Long, DataSourceRequestGroup> requestGroupsByFutureId = new ConcurrentHashMap<>();
//=============// //=============//
// constructor // // constructor //
//=============// //=============//
public DhServerLevel(AbstractSaveStructure saveStructure, IServerLevelWrapper serverLevelWrapper, RemotePlayerConnectionHandler remotePlayerConnectionHandler) public DhServerLevel(AbstractSaveStructure saveStructure, IServerLevelWrapper serverLevelWrapper)
{ {
if (saveStructure.getFullDataFolder(serverLevelWrapper).mkdirs()) if (saveStructure.getFullDataFolder(serverLevelWrapper).mkdirs())
{ {
@@ -97,363 +57,74 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
this.runRepoReliantSetup(); this.runRepoReliantSetup();
LOGGER.info("Started DHLevel for {} with saves at {}", serverLevelWrapper, saveStructure); LOGGER.info("Started DHLevel for {} with saves at {}", serverLevelWrapper, saveStructure);
this.remotePlayerConnectionHandler = remotePlayerConnectionHandler;
}
public void registerNetworkHandlers(ServerPlayerState serverPlayerState)
{
serverPlayerState.session.registerHandler(FullDataSourceRequestMessage.class, this.currentLevelOnly(msg ->
{
ServerPlayerState.RateLimiterSet rateLimiterSet = serverPlayerState.getRateLimiterSet(this);
if (msg.clientTimestamp == null)
{
// Normal generation
if (!serverPlayerState.config.isDistantGenerationEnabled())
{
msg.sendResponse(new RequestRejectedException("Operation is disabled from config."));
return;
}
if (!rateLimiterSet.generationRequestRateLimiter.tryAcquire(msg))
{
return;
}
while (true)
{
DataSourceRequestGroup requestGroup = this.requestGroupsByPos.computeIfAbsent(msg.sectionPos, pos ->
{
DataSourceRequestGroup newGroup = new DataSourceRequestGroup();
this.tryFulfillDataSourceRequestGroup(newGroup, pos);
NETWORK_LOGGER.debug("[{}] Created request group for pos {}", this.serverLevelWrapper.getDimensionName(), pos);
return newGroup;
});
// If this fails, loop until either permit is acquired or group is removed to create another one
if (!requestGroup.requestAddSemaphore.tryAcquire())
{
Thread.yield();
continue;
}
this.requestGroupsByFutureId.put(msg.futureId, requestGroup);
requestGroup.requestMessages.put(msg.futureId, msg);
requestGroup.requestAddSemaphore.release();
break;
}
}
else
{
// Sync only
if (!serverPlayerState.config.getSynchronizeOnLogin())
{
msg.sendResponse(new RequestRejectedException("Operation is disabled from config."));
return;
}
if (!rateLimiterSet.syncOnLoginRateLimiter.tryAcquire(msg))
{
return;
}
Long serverTimestamp = this.serverside.fullDataFileHandler.getTimestampForPos(msg.sectionPos);
if (serverTimestamp == null || serverTimestamp <= msg.clientTimestamp)
{
rateLimiterSet.syncOnLoginRateLimiter.release();
msg.sendResponse(new FullDataSourceResponseMessage(null));
return;
}
ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor();
if (executor == null)
{
LOGGER.warn("Unable to send FullDataSourceResponseMessage - getNetworkCompressionExecutor() is null");
return;
}
this.serverside.fullDataFileHandler.getAsync(msg.sectionPos).thenAcceptAsync(fullDataSource ->
{
rateLimiterSet.syncOnLoginRateLimiter.release();
FullDataPayload payload = new FullDataPayload(fullDataSource);
payload.acceptInChunkMessages(FULL_DATA_CHUNK_SIZE, msg.getSession()::sendMessage);
msg.sendResponse(new FullDataSourceResponseMessage(payload));
}, executor);
}
}));
serverPlayerState.session.registerHandler(CancelMessage.class, msg ->
{
DataSourceRequestGroup requestGroup = this.requestGroupsByFutureId.remove(msg.futureId);
if (requestGroup == null)
{
return;
}
// If this fails, group is being removed and completing cancellation is not necessary
if (requestGroup.requestRemoveSemaphore.tryAcquire())
{
// Prevent adding requests in case the group will be removed by this cancellation
requestGroup.requestAddSemaphore.acquireUninterruptibly(Short.MAX_VALUE);
requestGroup.requestRemoveSemaphore.release();
serverPlayerState.getRateLimiterSet(this).generationRequestRateLimiter.release();
FullDataSourceRequestMessage requestMessage = requestGroup.requestMessages.remove(msg.futureId);
if (requestGroup.requestMessages.isEmpty())
{
NETWORK_LOGGER.debug("[{}] Cancelled request group {}", this.serverLevelWrapper.getDimensionName(), requestMessage.sectionPos);
this.requestGroupsByPos.remove(requestMessage.sectionPos);
this.serverside.fullDataFileHandler.removeRetrievalRequestIf(pos -> pos == requestMessage.sectionPos);
}
else
{
requestGroup.requestAddSemaphore.release(Short.MAX_VALUE);
}
}
});
} }
public <T extends NetworkMessage> Consumer<T> currentLevelOnly(Consumer<T> next)
{
return msg ->
{
LodUtil.assertTrue(msg instanceof ILevelRelatedMessage, MessageFormat.format("Received message does not implement {0}: {1}", ILevelRelatedMessage.class.getSimpleName(), msg.getClass().getSimpleName()));
// Handle only in requested dimension
if (!((ILevelRelatedMessage) msg).isSameLevelAs(this.getServerLevelWrapper()))
{
return;
}
// If player is not in this dimension and handling multiple dimensions at once is not allowed
assert msg.getSession().serverPlayer != null;
if (msg.getSession().serverPlayer.getLevel() != this.getLevelWrapper())
{
// If the message can be replied to - reply with error, otherwise just ignore
if (msg instanceof TrackableMessage)
{
((TrackableMessage) msg).sendResponse(new InvalidLevelException(MessageFormat.format(
"Generation not allowed. Requested dimension: {0}, player dimension: {1}, handler dimension: {2}",
((ILevelRelatedMessage) msg).getLevelName(),
msg.getSession().serverPlayer.getLevel().getDimensionName(),
this.getLevelWrapper().getDimensionName()
)));
}
return;
}
next.accept(msg);
};
}
//=========// //=========//
// methods // // methods //
//=========// //=========//
public void addPlayer(IServerPlayerWrapper serverPlayer) public void serverTick() { this.chunkToLodBuilder.tick(); }
{
this.worldGenPlayerCenteringQueue.add(serverPlayer);
}
public void removePlayer(IServerPlayerWrapper serverPlayer)
{
this.worldGenPlayerCenteringQueue.remove(serverPlayer);
}
@Override @Override
public void serverTick() public CompletableFuture<Void> updateDataSourcesAsync(FullDataSourceV2 data) { return this.getFullDataProvider().updateDataSourceAsync(data); }
{
// Send finished data source requests
for (Map.Entry<Long, DataSourceRequestGroup> entry : this.requestGroupsByPos.entrySet())
{
DataSourceRequestGroup requestGroup = entry.getValue();
if (requestGroup.fullDataSource == null)
{
continue;
}
NETWORK_LOGGER.debug("[{}] Fulfilled request group {}", this.serverLevelWrapper.getDimensionName(), entry.getKey());
// Make this group unavailable for adding into
this.requestGroupsByPos.remove(entry.getKey());
requestGroup.requestRemoveSemaphore.acquireUninterruptibly(Short.MAX_VALUE);
requestGroup.requestAddSemaphore.acquireUninterruptibly(Short.MAX_VALUE);
ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor();
if (executor == null)
{
LOGGER.warn("Unable to send FullDataSourceResponseMessage - getNetworkCompressionExecutor() is null");
continue;
}
CompletableFuture.runAsync(() ->
{
FullDataPayload payload = new FullDataPayload(requestGroup.fullDataSource);
for (FullDataSourceRequestMessage msg : requestGroup.requestMessages.values())
{
this.requestGroupsByFutureId.remove(msg.futureId);
ServerPlayerState serverPlayerState = this.remotePlayerConnectionHandler.getConnectedPlayer(msg.serverPlayer());
if (serverPlayerState == null)
{
continue;
}
serverPlayerState.getRateLimiterSet(this).generationRequestRateLimiter.release();
payload.acceptInChunkMessages(FULL_DATA_CHUNK_SIZE, msg.getSession()::sendMessage);
msg.sendResponse(new FullDataSourceResponseMessage(payload));
}
}, executor);
}
}
@Override @Override
public CompletableFuture<Void> updateDataSourcesAsync(FullDataSourceV2 data) public int getMinY() { return getLevelWrapper().getMinHeight(); }
{
if (!Config.Client.Advanced.Multiplayer.ServerNetworking.enableRealTimeUpdates.get())
{
return this.getFullDataProvider().updateDataSourceAsync(data);
}
ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor();
if (executor == null)
{
LOGGER.warn("Unable to send FullDataPartialUpdateMessage - getNetworkCompressionExecutor() is null");
return this.getFullDataProvider().updateDataSourceAsync(data);
}
CompletableFuture.runAsync(() ->
{
FullDataPayload payload = new FullDataPayload(data);
for (ServerPlayerState serverPlayerState : this.remotePlayerConnectionHandler.getConnectedPlayers())
{
if (serverPlayerState.serverPlayer().getLevel() != this.serverLevelWrapper)
{
continue;
}
if (!serverPlayerState.config.isRealTimeUpdatesEnabled())
{
continue;
}
Vec3d playerPosition = serverPlayerState.serverPlayer().getPosition();
int distanceFromPlayer = DhSectionPos.getManhattanBlockDistance(data.getPos(), new DhBlockPos2D((int) playerPosition.x, (int) playerPosition.z)) / 16;
if (distanceFromPlayer >= serverPlayerState.serverPlayer().getViewDistance() &&
distanceFromPlayer <= serverPlayerState.config.getRenderDistanceRadius())
{
payload.acceptInChunkMessages(FULL_DATA_CHUNK_SIZE, serverPlayerState.session::sendMessage);
serverPlayerState.session.sendMessage(new FullDataPartialUpdateMessage(this.serverLevelWrapper, payload));
}
}
}, executor);
return this.getFullDataProvider().updateDataSourceAsync(data);
}
@Override
public int getMinY()
{
return this.getLevelWrapper().getMinHeight();
}
@Override @Override
public void close() public void close()
{ {
super.close(); super.close();
this.serverside.close(); serverside.close();
LOGGER.info("Closed DHLevel for {}", this.getLevelWrapper()); LOGGER.info("Closed DHLevel for {}", getLevelWrapper());
} }
@Override @Override
public void doWorldGen() public void doWorldGen()
{ {
boolean shouldDoWorldGen = true; //todo; boolean shouldDoWorldGen = true; //todo;
boolean isWorldGenRunning = this.serverside.worldGenModule.isWorldGenRunning(); boolean isWorldGenRunning = serverside.worldGenModule.isWorldGenRunning();
if (shouldDoWorldGen && !isWorldGenRunning) if (shouldDoWorldGen && !isWorldGenRunning)
{ {
// start world gen // start world gen
this.serverside.worldGenModule.startWorldGen(this.serverside.fullDataFileHandler, new ServerLevelModule.WorldGenState(this)); serverside.worldGenModule.startWorldGen(serverside.fullDataFileHandler, new ServerLevelModule.WorldGenState(this));
} }
else if (!shouldDoWorldGen && isWorldGenRunning) else if (!shouldDoWorldGen && isWorldGenRunning)
{ {
// stop world gen // stop world gen
this.serverside.worldGenModule.stopWorldGen(this.serverside.fullDataFileHandler); serverside.worldGenModule.stopWorldGen(serverside.fullDataFileHandler);
} }
if (this.serverside.worldGenModule.isWorldGenRunning()) if (serverside.worldGenModule.isWorldGenRunning())
{ {
IServerPlayerWrapper firstPlayer = this.worldGenPlayerCenteringQueue.peek(); serverside.worldGenModule.worldGenTick(new DhBlockPos2D(0, 0)); // todo;
if (firstPlayer == null)
{
return;
}
// Put first player in back before removing from front, so it can be removed by other thread without blocking
// - if it gets removed, remove() below will remove the item we just put instead
this.worldGenPlayerCenteringQueue.add(firstPlayer);
this.worldGenPlayerCenteringQueue.remove(firstPlayer);
Vec3d position = firstPlayer.getPosition();
this.serverside.worldGenModule.worldGenTick(new DhBlockPos2D((int) position.x, (int) position.z));
} }
} }
@Override @Override
public IServerLevelWrapper getServerLevelWrapper() public IServerLevelWrapper getServerLevelWrapper() { return serverLevelWrapper; }
{
return this.serverLevelWrapper;
}
@Override @Override
public ILevelWrapper getLevelWrapper() public ILevelWrapper getLevelWrapper() { return getServerLevelWrapper(); }
{
return this.getServerLevelWrapper();
}
@Override @Override
public FullDataSourceProviderV2 getFullDataProvider() public FullDataSourceProviderV2 getFullDataProvider() { return this.serverside.fullDataFileHandler; }
{
return this.serverside.fullDataFileHandler;
}
@Override @Override
public AbstractSaveStructure getSaveStructure() public AbstractSaveStructure getSaveStructure()
{ {
return this.serverside.saveStructure; return serverside.saveStructure;
} }
@Override @Override
public boolean hasSkyLight() { return this.serverLevelWrapper.hasSkyLight(); } public boolean hasSkyLight() { return this.serverLevelWrapper.hasSkyLight(); }
private void tryFulfillDataSourceRequestGroup(DataSourceRequestGroup requestGroup, long pos)
{
this.serverside.fullDataFileHandler.getAsync(pos).thenAccept(fullDataSource -> {
if (this.serverside.fullDataFileHandler.isFullyGenerated(fullDataSource.columnGenerationSteps))
{
requestGroup.fullDataSource = fullDataSource;
}
else
{
this.serverside.fullDataFileHandler.queuePositionForRetrieval(pos);
}
});
}
@Override @Override
public void onWorldGenTaskComplete(long pos) public void onWorldGenTaskComplete(long pos)
{ {
DataSourceRequestGroup requestGroup = this.requestGroupsByPos.get(pos); //TODO: Send packet to client
if (requestGroup != null)
{
this.tryFulfillDataSourceRequestGroup(requestGroup, pos);
}
} }
@Override @Override
@@ -477,21 +148,8 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
@Override @Override
public void addDebugMenuStringsToList(List<String> messageList) public void addDebugMenuStringsToList(List<String> messageList)
{ {
String dimName = this.serverLevelWrapper.getDimensionName(); String dimName = this.serverLevelWrapper.getDimensionType().getDimensionName();
messageList.add("["+dimName+"]"); messageList.add("["+dimName+"]");
} }
private static class DataSourceRequestGroup }
{
public final ConcurrentMap<Long, FullDataSourceRequestMessage> requestMessages = new ConcurrentHashMap<>();
@CheckForNull
public FullDataSourceV2 fullDataSource;
// Maybe there's a better way to do synchronization, but this should suffice
// Why not something like ReentrantReadWriteLock: locks should not be bound to threads
public final Semaphore requestAddSemaphore = new Semaphore(Short.MAX_VALUE, true);
public final Semaphore requestRemoveSemaphore = new Semaphore(Short.MAX_VALUE, true);
}
}
@@ -20,13 +20,13 @@
package com.seibel.distanthorizons.core.level; package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.pos.DhBlockPos;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
public interface IDhClientLevel extends IDhWorldGenLevel public interface IDhClientLevel extends IDhLevel
{ {
void clientTick(); void clientTick();
@@ -25,11 +25,11 @@ import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.RenderBufferHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -45,10 +45,11 @@ public interface IDhLevel extends AutoCloseable
/** @return 0 if no hash is known */ /** @return 0 if no hash is known */
int getChunkHash(DhChunkPos pos); int getChunkHash(DhChunkPos pos);
void updateChunkAsync(IChunkWrapper chunk, int newChunkHash); void setChunkHash(DhChunkPos pos, int chunkHash);
void updateChunkAsync(IChunkWrapper chunk);
void loadBeaconBeamsInPos(long pos); void loadBeaconBeamsInPos(long pos);
void updateBeaconBeamsForChunk(IChunkWrapper chunkToUpdate, ArrayList<IChunkWrapper> nearbyChunkList); void setBeaconBeamsForChunk(DhChunkPos chunkPos, List<BeaconBeamDTO> beamList);
void unloadBeaconBeamsInPos(long pos); void unloadBeaconBeamsInPos(long pos);
FullDataSourceProviderV2 getFullDataProvider(); FullDataSourceProviderV2 getFullDataProvider();
@@ -59,8 +60,8 @@ public interface IDhLevel extends AutoCloseable
CompletableFuture<Void> updateDataSourcesAsync(FullDataSourceV2 data); CompletableFuture<Void> updateDataSourcesAsync(FullDataSourceV2 data);
/** /**
* this number is generally related to how many data sources have been updated * this number is generally related to how many data sources have been updated
* due to chunk modifications or loads. * due to chunk modifications or loads.
*/ */
int getUnsavedDataSourceCount(); int getUnsavedDataSourceCount();
@@ -80,4 +81,4 @@ public interface IDhLevel extends AutoCloseable
@Nullable @Nullable
RenderBufferHandler getRenderBufferHandler(); RenderBufferHandler getRenderBufferHandler();
} }
@@ -19,7 +19,7 @@
package com.seibel.distanthorizons.core.level; package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable; import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable;
/** /**
@@ -28,12 +28,15 @@ import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindab
*/ */
public interface IKeyedClientLevelManager extends IBindable public interface IKeyedClientLevelManager extends IBindable
{ {
IServerKeyedClientLevel getServerKeyedLevel();
/** Called when a client level is wrapped by a ServerEnhancedClientLevel, for integration into mod internals. */ /** Called when a client level is wrapped by a ServerEnhancedClientLevel, for integration into mod internals. */
IServerKeyedClientLevel setServerKeyedLevel(IClientLevelWrapper clientLevel, String levelKey); void setServerKeyedLevel(IServerKeyedClientLevel clientLevel);
void clearServerKeyedLevel(); IServerKeyedClientLevel getOverrideWrapper();
boolean isEnabled(); /** Returns a new instance of a ServerEnhancedClientLevel. */
void disable(); IServerKeyedClientLevel getServerKeyedLevel(ILevelWrapper level, String serverLevelKey);
/** Sets the LOD engine to use the override wrapper, if the server has communication enabled. */
void setUseOverrideWrapper(boolean useOverrideWrapper);
boolean getUseOverrideWrapper();
} }
@@ -23,11 +23,10 @@ import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSource
import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue; import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen; import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.DhBlockPos2D;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import java.io.Closeable; import java.io.Closeable;
import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@@ -138,21 +137,20 @@ public class WorldGenModule implements Closeable
public boolean isWorldGenRunning() { return this.worldGenStateRef.get() != null; } public boolean isWorldGenRunning() { return this.worldGenStateRef.get() != null; }
public void addDebugMenuStringsToList(List<String> messageList) public String getDebugMenuString()
{ {
AbstractWorldGenState worldGenState = this.worldGenStateRef.get(); AbstractWorldGenState worldGenState = this.worldGenStateRef.get();
if (worldGenState == null) if (worldGenState == null)
{ {
return; return null;
} }
String waitingCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getWaitingTaskCount()); String waitingCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getWaitingTaskCount());
String inProgressCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getInProgressTaskCount()); String inProgressCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getInProgressTaskCount());
String totalCountEstimateStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getEstimatedTotalTaskCount()); String totalCountEstimateStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getEstimatedTotalTaskCount());
messageList.add("World Gen Tasks: "+waitingCountStr+"/"+totalCountEstimateStr+" (in progress: "+inProgressCountStr+")");
return "World Gen Tasks: "+waitingCountStr+"/"+totalCountEstimateStr+" (in progress: "+inProgressCountStr+")";
worldGenState.worldGenerationQueue.addDebugMenuStringsToList(messageList);
} }
@@ -187,4 +185,4 @@ public class WorldGenModule implements Closeable
{ this.worldGenerationQueue.startAndSetTargetPos(targetPosForGeneration); } { this.worldGenerationQueue.startAndSetTargetPos(targetPosForGeneration); }
} }
} }
@@ -101,7 +101,7 @@ public class ConfigBasedLogger
else else
logger.log(logLevel, msgStr); logger.log(logLevel, msgStr);
} }
if (MC != null && mode.levelForChat.isLessSpecificThan(level)) if (mode.levelForChat.isLessSpecificThan(level))
{ {
if (param.length > 0 && param[param.length - 1] instanceof Throwable) if (param.length > 0 && param[param.length - 1] instanceof Throwable)
MC.logToChat(level, msgStr + "\n" + MC.logToChat(level, msgStr + "\n" +
@@ -20,7 +20,7 @@
package com.seibel.distanthorizons.core.logging.f3; package com.seibel.distanthorizons.core.logging.f3;
import com.seibel.distanthorizons.core.api.internal.SharedApi; import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.jar.ModJarInfo; import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.SharedVbo;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.RenderBufferHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
@@ -61,9 +61,9 @@ public class F3Screen
Parent Update #: 12 <br><br> Parent Update #: 12 <br><br>
Client_Server World with 3 levels <br> Client_Server World with 3 levels <br>
[minecraft:overworld] rendering: Active <br> [overworld] rendering: Active <br>
[minecraft:the_end] rendering: Inactive <br> [the_end] rendering: Inactive <br>
[minecraft:the_nether] rendering: Inactive <br><br> [the_nether] rendering: Inactive <br><br>
VBO Render Count: 199/374 <br> VBO Render Count: 199/374 <br>
</code> </code>
@@ -73,9 +73,6 @@ public class F3Screen
ThreadPoolExecutor worldGenPool = ThreadPoolUtil.getWorldGenExecutor(); ThreadPoolExecutor worldGenPool = ThreadPoolUtil.getWorldGenExecutor();
ThreadPoolExecutor fileHandlerPool = ThreadPoolUtil.getFileHandlerExecutor(); ThreadPoolExecutor fileHandlerPool = ThreadPoolUtil.getFileHandlerExecutor();
ThreadPoolExecutor updatePool = ThreadPoolUtil.getUpdatePropagatorExecutor(); ThreadPoolExecutor updatePool = ThreadPoolUtil.getUpdatePropagatorExecutor();
ThreadPoolExecutor lodBuilderPool = ThreadPoolUtil.getChunkToLodBuilderExecutor();
ThreadPoolExecutor bufferBuilderPool = ThreadPoolUtil.getBufferBuilderExecutor();
ThreadPoolExecutor bufferUploaderPool = ThreadPoolUtil.getBufferUploaderExecutor();
AbstractDhWorld world = SharedApi.getAbstractDhWorld(); AbstractDhWorld world = SharedApi.getAbstractDhWorld();
Iterable<? extends IDhLevel> levelIterator = world.getAllLoadedLevels(); Iterable<? extends IDhLevel> levelIterator = world.getAllLoadedLevels();
@@ -83,25 +80,17 @@ public class F3Screen
messageList.add(""); messageList.add("");
messageList.add(ModInfo.READABLE_NAME+": "+ModInfo.VERSION); messageList.add(ModInfo.READABLE_NAME+": "+ModInfo.VERSION);
if (ModInfo.IS_DEV_BUILD)
{
String shortCommitHash = ModJarInfo.Git_Commit.length() >= 8 ? ModJarInfo.Git_Commit.substring(0, 8) : ModJarInfo.Git_Commit;
messageList.add("Build: " + shortCommitHash + " (" + ModJarInfo.Git_Branch + ")");
}
messageList.add(""); messageList.add("");
// thread pools // thread pools
messageList.add(getThreadPoolStatString("World Gen", worldGenPool));//"World Gen Tasks: 40/5304, (in progress: 7)"); messageList.add(getThreadPoolStatString("World Gen", worldGenPool));//"World Gen Tasks: 40/5304, (in progress: 7)");
messageList.add(getThreadPoolStatString("File Handler", fileHandlerPool)); messageList.add(getThreadPoolStatString("File Handler", fileHandlerPool));
messageList.add(getThreadPoolStatString("Update Propagator", updatePool)); messageList.add(getThreadPoolStatString("Update Propagator", updatePool));
messageList.add(getThreadPoolStatString("LOD Builder", lodBuilderPool));
messageList.add(getThreadPoolStatString("Buffer Builder", bufferBuilderPool));
messageList.add(getThreadPoolStatString("Buffer Uploader", bufferUploaderPool));
messageList.add(""); messageList.add("");
// chunk updates // chunk updates
messageList.add(SharedApi.INSTANCE.getDebugMenuString()); messageList.add(SharedApi.INSTANCE.getDebugMenuString());
messageList.add(""); messageList.add("");
// world / levels // world / levels
world.addDebugMenuStringsToList(messageList); messageList.add(world.GetDebugMenuString());
for (IDhLevel level : levelIterator) for (IDhLevel level : levelIterator)
{ {
level.addDebugMenuStringsToList(messageList); level.addDebugMenuStringsToList(messageList);
@@ -123,6 +112,11 @@ public class F3Screen
messageList.add(genericRenderer.getVboRenderDebugMenuString()); messageList.add(genericRenderer.getVboRenderDebugMenuString());
} }
} }
messageList.add("");
// GPU memory
messageList.add("GPU Opaque " + SharedVbo.OPAQUE.getDebugMenuString());
messageList.add("GPU Transp " + SharedVbo.TRANSPARENT.getDebugMenuString());
messageList.add("");
} }
@@ -141,4 +135,4 @@ public class F3Screen
} }
@@ -1,346 +0,0 @@
package com.seibel.distanthorizons.core.multiplayer.client;
import com.google.common.base.Stopwatch;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.logging.ConfigBasedSpamLogger;
import com.seibel.distanthorizons.core.network.exceptions.InvalidLevelException;
import com.seibel.distanthorizons.core.network.exceptions.RateLimitedException;
import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException;
import com.seibel.distanthorizons.core.network.session.SessionClosedException;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceResponseMessage;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.TimerUtil;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.core.util.ratelimiting.SupplierBasedRateLimiter;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import org.apache.logging.log4j.LogManager;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import java.awt.*;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
public abstract class AbstractFullDataRequestQueue implements IDebugRenderable, AutoCloseable
{
private static final ConfigBasedSpamLogger LOGGER = new ConfigBasedSpamLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Logging.logNetworkEvent.get(), 3);
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final Timer TASK_FINISH_TIMER = TimerUtil.CreateTimer("RequestTaskFinishTimer");
private static final int MAX_RETRY_ATTEMPTS = 3;
protected static final long SHUTDOWN_TIMEOUT_SECONDS = 5;
public final ClientNetworkState networkState;
protected final IDhClientLevel level;
private final boolean changedOnly;
private volatile CompletableFuture<Void> closingFuture = null;
protected final ConcurrentMap<Long, RequestQueueEntry> waitingTasks = new ConcurrentHashMap<>();
private final Semaphore pendingTasksSemaphore = new Semaphore(Short.MAX_VALUE, true);
private final AtomicInteger finishedRequests = new AtomicInteger();
private final AtomicInteger failedRequests = new AtomicInteger();
private final ConfigEntry<Boolean> showDebugWireframeConfig;
private final SupplierBasedRateLimiter<Void> rateLimiter = new SupplierBasedRateLimiter<>(this::getRequestRateLimit);
protected abstract int getRequestRateLimit();
protected abstract String getQueueName();
public AbstractFullDataRequestQueue(ClientNetworkState networkState, IDhClientLevel level, boolean changedOnly, ConfigEntry<Boolean> showDebugWireframeConfig)
{
this.networkState = networkState;
this.level = level;
this.changedOnly = changedOnly;
this.showDebugWireframeConfig = showDebugWireframeConfig;
DebugRenderer.register(this, this.showDebugWireframeConfig);
}
public CompletableFuture<Boolean> submitRequest(long sectionPos, Consumer<FullDataSourceV2> chunkDataConsumer)
{
return this.submitRequest(sectionPos, null, chunkDataConsumer);
}
public CompletableFuture<Boolean> submitRequest(long sectionPos, @Nullable Long clientTimestamp, Consumer<FullDataSourceV2> chunkDataConsumer)
{
LodUtil.assertTrue(DhSectionPos.getDetailLevel(sectionPos) == DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL, "Only highest-detail sections are allowed.");
RequestQueueEntry entry = new RequestQueueEntry(chunkDataConsumer, clientTimestamp);
entry.future.whenComplete((result, throwable) ->
{
this.waitingTasks.remove(sectionPos);
this.finishedRequests.incrementAndGet();
if (!result || throwable != null)
{
this.failedRequests.incrementAndGet();
}
});
this.waitingTasks.put(sectionPos, entry);
return entry.future;
}
protected int posDistanceSquared(DhBlockPos2D targetPos, long pos)
{
return (int) DhSectionPos.getCenterBlockPos(pos).distSquared(targetPos);
}
public synchronized boolean tick(DhBlockPos2D targetPos)
{
if (this.closingFuture != null || !this.networkState.isReady())
{
return false;
}
while (this.getWaitingTaskCount() > this.getInProgressTaskCount()
&& this.getInProgressTaskCount() < this.getRequestRateLimit()
&& this.pendingTasksSemaphore.tryAcquire())
{
if (!this.rateLimiter.tryAcquire())
{
this.pendingTasksSemaphore.release();
break;
}
this.sendNewRequest(targetPos);
}
return true;
}
public void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf)
{
for (Map.Entry<Long, RequestQueueEntry> mapEntry : this.waitingTasks.entrySet())
{
long pos = mapEntry.getKey();
RequestQueueEntry entry = mapEntry.getValue();
if (removeIf.accept(pos))
{
LOGGER.debug("Removing request " + mapEntry.getKey() + "...");
entry.future.cancel(false);
if (entry.request != null)
{
entry.request.cancel(false);
}
}
}
}
private void sendNewRequest(DhBlockPos2D targetPos)
{
Map.Entry<Long, RequestQueueEntry> mapEntry = this.waitingTasks.entrySet().stream()
.filter(task -> task.getValue().request == null)
.min((x, y) -> this.posDistanceSquared(targetPos, x.getKey()) - this.posDistanceSquared(targetPos, y.getKey()))
.orElse(null);
if (mapEntry == null)
{
this.pendingTasksSemaphore.release();
return;
}
long sectionPos = mapEntry.getKey();
RequestQueueEntry entry = mapEntry.getValue();
CompletableFuture<FullDataSourceResponseMessage> request = this.networkState.getSession().sendRequest(
new FullDataSourceRequestMessage(this.level.getLevelWrapper(), sectionPos, entry.updateTimestamp),
FullDataSourceResponseMessage.class
);
entry.request = request;
request.handle((response, throwable) ->
{
this.pendingTasksSemaphore.release();
try
{
if (throwable != null)
{
throw throwable;
}
if (response.payload != null)
{
FullDataSourceV2DTO dataSourceDto = this.networkState.decodeDataSourceAndReleaseBuffer(response.payload);
ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor();
if (executor == null)
{
LOGGER.warn("Unable to handle FullDataPayload - getNetworkCompressionExecutor() is null");
return null;
}
CompletableFuture.runAsync(() ->
{
try
{
FullDataSourceV2 fullDataSource = dataSourceDto.createPooledDataSource(this.level.getLevelWrapper());
entry.chunkDataConsumer.accept(fullDataSource);
FullDataSourceV2.DATA_SOURCE_POOL.returnPooledDataSource(fullDataSource);
}
catch (IOException | DataCorruptedException | InterruptedException e)
{
throw new RuntimeException(e);
}
}, executor);
}
else
{
LodUtil.assertTrue(this.changedOnly, "Received empty data source response for not changes-only request");
}
}
catch (InvalidLevelException | RequestRejectedException ignored)
{
// We're too late / some cases might trigger a bunch of expected rejections
return entry.future.complete(false);
}
catch (SessionClosedException | CancellationException ignored)
{
// Triggered when level is unloaded
return entry.future.cancel(false);
}
catch (RateLimitedException e)
{
LOGGER.warn("Rate limited by server, re-queueing task [" + sectionPos + "]: " + e.getMessage());
// Skip 1 second
this.rateLimiter.acquireOrDrain(Integer.MAX_VALUE);
entry.request = null;
return null;
}
catch (Throwable e)
{
entry.retryAttempts--;
LOGGER.error("Error while fetching full data source, attempts left: {} / {}", entry.retryAttempts, MAX_RETRY_ATTEMPTS, e);
// Retry logic
if (entry.retryAttempts > 0)
{
entry.request = null;
return null;
}
else
{
return entry.future.complete(false);
}
}
// Hack to work around a race condition
// If you finish the request too quickly, the section will never render
TASK_FINISH_TIMER.schedule(new TimerTask()
{
@Override
public void run()
{
entry.future.complete(true);
}
}, 10000);
return null;
});
}
public void addDebugMenuStringsToList(List<String> messageList)
{
messageList.add(this.getQueueName() + " [" + this.level.getClientLevelWrapper().getDimensionName() + "]");
messageList.add("Requests: " + this.finishedRequests + " / " + (this.getWaitingTaskCount() + this.finishedRequests.get()) + " (failed: " + this.failedRequests + ", rate limit: " + this.getRequestRateLimit() + ")");
}
public int getWaitingTaskCount() { return this.waitingTasks.size(); }
public int getInProgressTaskCount() { return Short.MAX_VALUE - this.pendingTasksSemaphore.availablePermits(); }
public CompletableFuture<Void> startClosing(boolean alsoInterruptRunning)
{
return this.closingFuture = CompletableFuture.runAsync(() -> {
Stopwatch stopwatch = Stopwatch.createStarted();
do
{
for (RequestQueueEntry entry : this.waitingTasks.values())
{
entry.future.cancel(alsoInterruptRunning);
if (entry.request != null && entry.request.cancel(alsoInterruptRunning))
{
this.pendingTasksSemaphore.release();
}
}
}
while (!this.pendingTasksSemaphore.tryAcquire(Short.MAX_VALUE) && stopwatch.elapsed(TimeUnit.SECONDS) < SHUTDOWN_TIMEOUT_SECONDS);
if (stopwatch.elapsed(TimeUnit.SECONDS) >= SHUTDOWN_TIMEOUT_SECONDS)
{
LOGGER.warn(this.getQueueName() + " for " + this.level.getLevelWrapper() + " did not shutdown in " + SHUTDOWN_TIMEOUT_SECONDS + " seconds! Some unfinished tasks might be left hanging.");
}
});
}
@Override
public void close()
{
DebugRenderer.unregister(this, this.showDebugWireframeConfig);
}
@Override
public void debugRender(DebugRenderer r)
{
if (MC_CLIENT.getWrappedClientLevel() != this.level.getClientLevelWrapper())
{
return;
}
for (Map.Entry<Long, RequestQueueEntry> mapEntry : this.waitingTasks.entrySet())
{
r.renderBox(new DebugRenderer.Box(mapEntry.getKey(), -32f, 64f, 0.05f,
mapEntry.getValue().request != null ? Color.red : Color.gray
));
}
}
protected static class RequestQueueEntry
{
public final CompletableFuture<Boolean> future = new CompletableFuture<>();
public final Consumer<FullDataSourceV2> chunkDataConsumer;
@Nullable
public final Long updateTimestamp;
@CheckForNull
public CompletableFuture<?> request;
public int retryAttempts = MAX_RETRY_ATTEMPTS;
public RequestQueueEntry(
Consumer<FullDataSourceV2> chunkDataConsumer,
@Nullable Long updateTimestamp)
{
this.chunkDataConsumer = chunkDataConsumer;
this.updateTimestamp = updateTimestamp;
}
}
}
@@ -1,175 +0,0 @@
package com.seibel.distanthorizons.core.multiplayer.client;
import com.google.common.cache.CacheBuilder;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig;
import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.network.event.ScopedNetworkEventSource;
import com.seibel.distanthorizons.core.network.event.internal.CloseEvent;
import com.seibel.distanthorizons.core.network.event.internal.IncompatibleMessageEvent;
import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage;
import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataChunkMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPayload;
import com.seibel.distanthorizons.core.network.session.Session;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.coreapi.ModInfo;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.CompositeByteBuf;
import org.apache.logging.log4j.LogManager;
import org.jetbrains.annotations.Nullable;
import java.io.Closeable;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
public class ClientNetworkState implements Closeable
{
protected static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Logging.logNetworkEvent.get());
private final Session session = new Session(null);
/**
* Returns the client used by this instance. <p>
* If you need to subscribe to any packet events, create an instance of {@link ScopedNetworkEventSource} using the returned instance.
*/
public Session getSession() { return this.session; }
public SessionConfig config = new SessionConfig();
private volatile boolean configReceived = false;
private final SessionConfig.ChangeListener configChangeListener = new SessionConfig.ChangeListener(this::sendConfigMessage);
public boolean isReady() { return this.configReceived; }
private EServerSupportStatus serverSupportStatus = EServerSupportStatus.NONE;
/** Protocol version closest to supported by this mod version */
@Nullable
private Integer closestProtocolVersion;
private final ConcurrentMap<Integer, CompositeByteBuf> fullDataBuffers = CacheBuilder.newBuilder()
.expireAfterAccess(10, TimeUnit.SECONDS)
.<Integer, CompositeByteBuf>build()
.asMap();
/**
* Constructs a new instance.
*/
public ClientNetworkState()
{
this.session.registerHandler(IncompatibleMessageEvent.class, event ->
{
if (this.closestProtocolVersion == null || Math.abs(event.protocolVersion - ModInfo.PROTOCOL_VERSION) < this.closestProtocolVersion)
{
this.closestProtocolVersion = event.protocolVersion;
}
});
this.session.registerHandler(CurrentLevelKeyMessage.class, msg ->
{
if (this.serverSupportStatus == EServerSupportStatus.NONE)
{
this.serverSupportStatus = EServerSupportStatus.LEVELS_ONLY;
}
});
this.session.registerHandler(SessionConfigMessage.class, msg ->
{
this.serverSupportStatus = EServerSupportStatus.FULL;
LOGGER.info("Connection config has been changed: {}", msg.config);
this.config = msg.config;
this.configReceived = true;
});
this.session.registerHandler(CloseEvent.class, msg ->
{
this.configReceived = false;
});
this.session.registerHandler(FullDataChunkMessage.class, msg ->
{
if (msg.isFirst)
{
CompositeByteBuf composite = this.fullDataBuffers.remove(msg.bufferId);
if (composite != null)
{
composite.release();
LOGGER.debug("Released full data buffer {}: {}", msg.bufferId, composite);
}
}
CompositeByteBuf composite = this.fullDataBuffers.computeIfAbsent(msg.bufferId, bufferId -> ByteBufAllocator.DEFAULT.compositeBuffer());
composite.addComponent(true, msg.buffer);
LOGGER.debug("Full data buffer {}: {}", msg.bufferId, composite);
});
this.session.registerHandler(FullDataPartialUpdateMessage.class, msg ->
{
// Dummy handler to prevent unhandled message warnings
});
}
public FullDataSourceV2DTO decodeDataSourceAndReleaseBuffer(FullDataPayload msg)
{
CompositeByteBuf composite = this.fullDataBuffers.remove(msg.dtoBufferId);
Objects.requireNonNull(composite);
try
{
return INetworkObject.decodeToInstance(new FullDataSourceV2DTO(), composite);
}
finally
{
composite.release();
}
}
public void sendConfigMessage()
{
this.configReceived = false;
this.getSession().sendMessage(new SessionConfigMessage(new SessionConfig()));
}
public void addDebugMenuStringsToList(List<String> messageList)
{
if (this.session.isClosed())
{
messageList.add("Session closed: " + this.session.getCloseReason().getMessage());
return;
}
if (this.serverSupportStatus == EServerSupportStatus.NONE && this.closestProtocolVersion != null)
{
messageList.add("Incompatible protocol version: " + this.closestProtocolVersion + ", required: " + ModInfo.PROTOCOL_VERSION);
return;
}
messageList.add(this.serverSupportStatus.message);
}
@Override
public void close()
{
this.configChangeListener.close();
this.session.close();
}
private enum EServerSupportStatus
{
NONE("Server does not support DH"),
LEVELS_ONLY("Server supports shared level keys"),
FULL("Server has full DH support");
public final String message;
EServerSupportStatus(String message)
{
this.message = message;
}
}
}
@@ -1,30 +0,0 @@
package com.seibel.distanthorizons.core.multiplayer.client;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
public class SyncOnLoginRequestQueue extends AbstractFullDataRequestQueue
{
public SyncOnLoginRequestQueue(IDhClientLevel level, ClientNetworkState networkState)
{
super(networkState, level, true, Config.Client.Advanced.Debugging.DebugWireframe.showWorldGenQueue);
}
@Override
protected int getRequestRateLimit() { return this.networkState.config.getSyncOnLoginRateLimit(); }
@Override
protected String getQueueName() { return "Sync On Login Queue"; }
@Override
public boolean tick(DhBlockPos2D targetPos)
{
if (!this.networkState.config.getSynchronizeOnLogin())
{
return false;
}
return super.tick(targetPos);
}
}
@@ -1,149 +0,0 @@
package com.seibel.distanthorizons.core.multiplayer.config;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.network.INetworkObject;
import io.netty.buffer.ByteBuf;
import java.io.Closeable;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.seibel.distanthorizons.core.config.Config.Client.Advanced.*;
public class SessionConfig implements INetworkObject
{
private static final LinkedHashMap<String, Entry> CONFIG_ENTRIES = new LinkedHashMap<>();
private static <T> void registerConfigEntry(ConfigEntry<T> configEntry, BiFunction<T, T, T> valueConstrainer)
{
CONFIG_ENTRIES.put(Objects.requireNonNull(configEntry.getServersideShortName()), new Entry(configEntry, valueConstrainer));
}
private final LinkedHashMap<String, Object> values = new LinkedHashMap<>();
public SessionConfig constrainingConfig;
static
{
// Note: config values are ordered by serversideShortName when transmitted
registerConfigEntry(Graphics.Quality.lodChunkRenderDistanceRadius, Math::min);
registerConfigEntry(WorldGenerator.enableDistantGeneration, (x, y) -> x && y);
registerConfigEntry(Multiplayer.ServerNetworking.generationRequestRateLimit, Math::min);
registerConfigEntry(Multiplayer.ServerNetworking.enableRealTimeUpdates, (x, y) -> x && y);
registerConfigEntry(Multiplayer.ServerNetworking.synchronizeOnLogin, (x, y) -> x && y);
registerConfigEntry(Multiplayer.ServerNetworking.syncOnLoginRateLimit, Math::min);
}
public int getRenderDistanceRadius() { return this.getValue(Graphics.Quality.lodChunkRenderDistanceRadius); }
public boolean isDistantGenerationEnabled() { return this.getValue(WorldGenerator.enableDistantGeneration); }
public int getGenerationRequestRateLimit() { return this.getValue(Multiplayer.ServerNetworking.generationRequestRateLimit); }
public boolean isRealTimeUpdatesEnabled() { return this.getValue(Multiplayer.ServerNetworking.enableRealTimeUpdates); }
public boolean getSynchronizeOnLogin() { return this.getValue(Multiplayer.ServerNetworking.synchronizeOnLogin); }
public int getSyncOnLoginRateLimit() { return this.getValue(Multiplayer.ServerNetworking.syncOnLoginRateLimit); }
@SuppressWarnings("unchecked")
private <T> T getValue(String name)
{
Entry entry = CONFIG_ENTRIES.get(name);
T value = (T) this.values.get(name);
if (value == null)
{
value = (T) entry.supplier.get();
}
return (this.constrainingConfig != null
? (T) entry.valueConstrainer.apply(value, this.constrainingConfig.getValue(name))
: value);
}
private <T> T getValue(ConfigEntry<T> configEntry)
{
return this.getValue(configEntry.getServersideShortName());
}
private Map<String, ?> getValues()
{
return CONFIG_ENTRIES.keySet().stream().collect(Collectors.toMap(
Function.identity(),
this::getValue,
(x, y) -> x,
LinkedHashMap::new
));
}
@Override
public void encode(ByteBuf out)
{
this.writeFixedLengthCollection(out, this.getValues().values());
}
@Override
public void decode(ByteBuf in)
{
for (String key : CONFIG_ENTRIES.keySet())
{
Object currentValue = this.getValue(key);
Object newValue = Codec.getCodec(currentValue.getClass()).decode.apply(currentValue, in);
this.values.put(key, newValue);
}
}
@Override
public String toString()
{
return MoreObjects.toStringHelper(this)
.add("values", this.getValues())
.toString();
}
private static class Entry
{
public final ConfigEntry<Object> supplier;
public final BiFunction<Object, Object, Object> valueConstrainer;
@SuppressWarnings("unchecked")
private <T> Entry(ConfigEntry<T> supplier, BiFunction<T, T, T> valueConstrainer)
{
this.supplier = (ConfigEntry<Object>) supplier;
this.valueConstrainer = (BiFunction<Object, Object, Object>) valueConstrainer;
}
}
public static class ChangeListener implements Closeable
{
private final ArrayList<ConfigChangeListener<?>> changeListeners;
public ChangeListener(Runnable runnable)
{
this.changeListeners = new ArrayList<>(CONFIG_ENTRIES.size());
for (Entry entry : CONFIG_ENTRIES.values())
{
this.changeListeners.add(new ConfigChangeListener<>(entry.supplier, ignored -> runnable.run()));
}
}
@Override
public void close()
{
for (ConfigChangeListener<?> changeListener : this.changeListeners)
{
changeListener.close();
}
this.changeListeners.clear();
}
}
}
@@ -1,70 +0,0 @@
package com.seibel.distanthorizons.core.multiplayer.server;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage;
import com.seibel.distanthorizons.core.network.session.Session;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
public class RemotePlayerConnectionHandler
{
private final ConcurrentMap<IServerPlayerWrapper, ServerPlayerState> connectedPlayers = new ConcurrentHashMap<>();
private final ConcurrentMap<IServerPlayerWrapper, Queue<NetworkMessage>> messageQueue = new ConcurrentHashMap<>();
public void handlePluginMessage(IServerPlayerWrapper player, NetworkMessage message)
{
ServerPlayerState playerState = this.connectedPlayers.get(player);
if (playerState != null)
{
playerState.session.tryHandleMessage(message);
}
else
{
this.messageQueue.computeIfAbsent(player, k -> new ConcurrentLinkedQueue<>()).add(message);
}
}
public ServerPlayerState getConnectedPlayer(IServerPlayerWrapper player)
{
return this.connectedPlayers.get(player);
}
public Iterable<ServerPlayerState> getConnectedPlayers()
{
return this.connectedPlayers.values();
}
public ServerPlayerState registerJoinedPlayer(IServerPlayerWrapper serverPlayer)
{
ServerPlayerState state = new ServerPlayerState(serverPlayer);
this.connectedPlayers.put(serverPlayer, state);
Queue<NetworkMessage> queuedMessages = this.messageQueue.get(serverPlayer);
if (queuedMessages != null)
{
Session session = state.session;
for (NetworkMessage message : queuedMessages)
{
session.tryHandleMessage(message);
}
this.messageQueue.remove(serverPlayer);
}
return state;
}
public void unregisterLeftPlayer(IServerPlayerWrapper serverPlayer)
{
ServerPlayerState playerState = this.connectedPlayers.remove(serverPlayer);
if (playerState != null)
{
playerState.close();
}
}
}
@@ -1,106 +0,0 @@
package com.seibel.distanthorizons.core.multiplayer.server;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.level.DhServerLevel;
import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig;
import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage;
import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage;
import com.seibel.distanthorizons.core.network.event.internal.CloseEvent;
import com.seibel.distanthorizons.core.network.exceptions.RateLimitedException;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage;
import com.seibel.distanthorizons.core.network.session.Session;
import com.seibel.distanthorizons.core.util.ratelimiting.SupplierBasedRateAndConcurrencyLimiter;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.ConcurrentHashMap;
import static com.seibel.distanthorizons.core.config.Config.Client.Advanced.Multiplayer.ServerNetworking;
public class ServerPlayerState
{
public final Session session;
public IServerPlayerWrapper serverPlayer() { return this.session.serverPlayer; }
@NotNull
public SessionConfig config = new SessionConfig();
private final SessionConfig.ChangeListener configChangeListener = new SessionConfig.ChangeListener(this::onConfigChanged);
private String lastLevelKey = "";
private final ConfigChangeListener<String> levelKeyPrefixChangeListener = new ConfigChangeListener<>(ServerNetworking.levelKeyPrefix, this::sendLevelKey);
private final ConcurrentHashMap<DhServerLevel, RateLimiterSet> rateLimiterSets = new ConcurrentHashMap<>();
public RateLimiterSet getRateLimiterSet(DhServerLevel level)
{
return this.rateLimiterSets.computeIfAbsent(level, ignored -> new RateLimiterSet());
}
public void clearRateLimiterSets()
{
this.rateLimiterSets.clear();
}
public ServerPlayerState(IServerPlayerWrapper serverPlayer)
{
this.session = new Session(serverPlayer);
this.session.registerHandler(SessionConfigMessage.class, sessionConfigMessage ->
{
this.config.constrainingConfig = sessionConfigMessage.config;
this.sendLevelKey(null);
this.session.sendMessage(new SessionConfigMessage(this.config));
});
this.session.registerHandler(CloseEvent.class, event -> {
// No-op. removes "Unhandled message" log entries
});
}
private void sendLevelKey(String ignored)
{
if (ServerNetworking.sendLevelKeys.get())
{
String levelKey = this.serverPlayer().getLevel().getKeyedLevelDimensionName();
if (!levelKey.equals(this.lastLevelKey))
{
this.lastLevelKey = levelKey;
this.session.sendMessage(new CurrentLevelKeyMessage(levelKey));
}
}
}
private void onConfigChanged()
{
this.session.sendMessage(new SessionConfigMessage(this.config));
}
public void close()
{
this.levelKeyPrefixChangeListener.close();
this.configChangeListener.close();
this.session.close();
}
public class RateLimiterSet
{
public final SupplierBasedRateAndConcurrencyLimiter<FullDataSourceRequestMessage> generationRequestRateLimiter = new SupplierBasedRateAndConcurrencyLimiter<>(
() -> ServerNetworking.generationRequestRateLimit.get(),
msg -> {
msg.sendResponse(new RateLimitedException("Full data request rate limit: " + ServerPlayerState.this.config.getGenerationRequestRateLimit()));
}
);
public final SupplierBasedRateAndConcurrencyLimiter<FullDataSourceRequestMessage> syncOnLoginRateLimiter = new SupplierBasedRateAndConcurrencyLimiter<>(
() -> ServerNetworking.syncOnLoginRateLimit.get(),
msg -> {
msg.sendResponse(new RateLimitedException("Sync on login rate limit: " + ServerPlayerState.this.config.getSyncOnLoginRateLimit()));
}
);
}
}
@@ -1,192 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network;
import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.Contract;
import javax.annotation.Nullable;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.*;
public interface INetworkObject
{
void encode(ByteBuf out);
void decode(ByteBuf in);
static <T extends INetworkObject> T decodeToInstance(T obj, ByteBuf inputByteBuf)
{
obj.decode(inputByteBuf);
return obj;
}
@Contract("_, null -> false; _, !null -> true")
default boolean writeOptional(ByteBuf outputByteBuf, Object value)
{
boolean isNull = value != null;
outputByteBuf.writeBoolean(isNull);
return isNull;
}
@Nullable
default <T> T readOptional(ByteBuf inputByteBuf, Supplier<T> decoder)
{
return inputByteBuf.readBoolean()
? decoder.get()
: null;
}
default void readOptional(ByteBuf inputByteBuf, Runnable decoder)
{
if (inputByteBuf.readBoolean())
{
decoder.run();
}
}
default void writeString(String inputString, ByteBuf outputByteBuf)
{
INetworkObject.writeStringStatic(inputString, outputByteBuf);
}
default String readString(ByteBuf inputByteBuf)
{
return INetworkObject.readStringStatic(inputByteBuf);
}
static void writeStringStatic(String inputString, ByteBuf outputByteBuf)
{
byte[] bytes = inputString.getBytes(StandardCharsets.UTF_8);
outputByteBuf.writeShort(bytes.length);
outputByteBuf.writeBytes(bytes);
}
static String readStringStatic(ByteBuf inputByteBuf)
{
int length = inputByteBuf.readUnsignedShort();
return inputByteBuf.readSlice(length).toString(StandardCharsets.UTF_8);
}
default <T> void writeCollection(ByteBuf outputByteBuf, Collection<T> collection)
{
outputByteBuf.writeInt(collection.size());
this.writeFixedLengthCollection(outputByteBuf, collection);
}
default <T> void writeFixedLengthCollection(ByteBuf outputByteBuf, Collection<T> collection)
{
for (T item : collection)
{
Codec codec = Codec.getCodec(item.getClass());
codec.encode.accept(item, outputByteBuf);
}
}
default <T> void readCollection(ByteBuf inputByteBuf, Collection<T> collection, Supplier<T> innerValueConstructor)
{
int size = inputByteBuf.readInt();
Codec codec = null;
for (int i = 0; i < size; i++)
{
T item = innerValueConstructor.get();
if (codec == null)
{
codec = Codec.getCodec(item.getClass());
}
//noinspection unchecked
item = (T) codec.decode.apply(item, inputByteBuf);
collection.add(item);
}
}
default <K, V> void readMap(ByteBuf inputByteBuf, Map<K, V> map, Supplier<K> keySupplier, Supplier<V> valueSupplier)
{
ArrayList<Map.Entry<K, V>> entryList = new ArrayList<>();
this.readCollection(inputByteBuf, entryList, () -> new AbstractMap.SimpleEntry<>(keySupplier.get(), valueSupplier.get()));
for (Map.Entry<K, V> entry : entryList)
{
map.put(entry.getKey(), entry.getValue());
}
}
/**
* Should only be used for non-editable classes;
* otherwise, you may want to implement {@link INetworkObject} and use its methods where applicable.
*/
class Codec
{
private static final ConcurrentMap<Class<?>, Codec> codecMap = new ConcurrentHashMap<Class<?>, Codec>()
{{
// Primitives must be added manually here
this.put(Integer.class, new Codec((obj, out) -> out.writeInt((int)obj), (obj, in) -> in.readInt()));
this.put(Boolean.class, new Codec((obj, out) -> out.writeBoolean((boolean) obj), (obj, in) -> in.readBoolean()));
this.put(String.class, new Codec((obj, out) -> INetworkObject.writeStringStatic((String) obj, out), (obj, in) -> INetworkObject.readStringStatic(in)));
this.put(INetworkObject.class, new Codec(INetworkObject::encode, INetworkObject::decodeToInstance));
this.put(Map.Entry.class, new Codec(
(obj, out) -> {
Map.Entry<?, ?> entry = (Entry<?, ?>) obj;
getCodec(entry.getKey().getClass()).encode.accept(entry.getKey(), out);
getCodec(entry.getValue().getClass()).encode.accept(entry.getValue(), out);
},
(obj, in) -> {
Map.Entry<?, ?> entry = (Entry<?, ?>) obj;
return new SimpleEntry<>(
getCodec(entry.getKey().getClass()).decode.apply(entry.getKey(), in),
getCodec(entry.getValue().getClass()).decode.apply(entry.getValue(), in)
);
}
));
}};
public final BiConsumer<Object, ByteBuf> encode;
public final BiFunction<Object, ByteBuf, Object> decode;
@SuppressWarnings("unchecked")
public <T> Codec(BiConsumer<T, ByteBuf> encode, BiFunction<T, ByteBuf, T> decode)
{
this.encode = (BiConsumer<Object, ByteBuf>) encode;
this.decode = (BiFunction<Object, ByteBuf, Object>) decode;
}
public static <T> Codec getCodec(Class<T> clazz)
{
return codecMap.computeIfAbsent(clazz, ignored -> {
for (Map.Entry<Class<?>, Codec> entry : codecMap.entrySet())
{
if (entry.getKey().isAssignableFrom(clazz))
{
return entry.getValue();
}
}
throw new AssertionError("Class has no compatible codec: " + clazz.getSimpleName());
});
}
}
}
@@ -0,0 +1,175 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.network.messages.CloseMessage;
import com.seibel.distanthorizons.core.network.messages.CloseReasonMessage;
import com.seibel.distanthorizons.core.network.messages.HelloMessage;
import com.seibel.distanthorizons.core.network.protocol.NetworkChannelInitializer;
//import io.netty.bootstrap.Bootstrap;
//import io.netty.channel.Channel;
//import io.netty.channel.ChannelFuture;
//import io.netty.channel.ChannelOption;
//import io.netty.channel.EventLoopGroup;
//import io.netty.channel.nio.NioEventLoopGroup;
//import io.netty.channel.socket.nio.NioSocketChannel;
import org.apache.logging.log4j.Logger;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
public class NetworkClient //extends NetworkEventSource implements AutoCloseable
{
// private static final Logger LOGGER = DhLoggerBuilder.getLogger();
//
// private enum EConnectionState
// {
// OPEN,
// RECONNECT,
// RECONNECT_FORCE,
// CLOSE_WAIT,
// CLOSED
// }
//
// private static final int FAILURE_RECONNECT_DELAY_SEC = 5;
// private static final int FAILURE_RECONNECT_ATTEMPTS = 3;
//
// // TODO move to payload of some sort
// private final InetSocketAddress address;
//
// private final EventLoopGroup workerGroup = new NioEventLoopGroup();
// private final Bootstrap clientBootstrap = new Bootstrap()
// .group(this.workerGroup)
// .channel(NioSocketChannel.class)
// .option(ChannelOption.SO_KEEPALIVE, true)
// .handler(new NetworkChannelInitializer(this.messageHandler));
//
// private EConnectionState connectionState;
// private Channel channel;
// private int reconnectAttempts = FAILURE_RECONNECT_ATTEMPTS;
//
//
//
// public NetworkClient(String host, int port)
// {
// this.address = new InetSocketAddress(host, port);
//
// this.registerHandlers();
// this.connect();
// }
//
// private void registerHandlers()
// {
// this.registerHandler(HelloMessage.class, (helloMessage, channelContext) ->
// {
// LOGGER.info("Connected to server: "+channelContext.channel().remoteAddress());
// });
//
// this.registerHandler(CloseReasonMessage.class, (closeReasonMessage, channelContext) ->
// {
// LOGGER.info(closeReasonMessage.reason);
// this.connectionState = EConnectionState.CLOSE_WAIT;
// });
//
// this.registerHandler(CloseMessage.class, (closeMessage, channelContext) ->
// {
// LOGGER.info("Disconnected from server: "+channelContext.channel().remoteAddress());
// if (this.connectionState == EConnectionState.CLOSE_WAIT)
// {
// this.close();
// }
// });
// }
//
// private void connect()
// {
// LOGGER.info("Connecting to server: "+this.address);
// this.connectionState = EConnectionState.OPEN;
//
// // FIXME sometimes this causes the MC connection to crash
// // this might happen if the URL can't be converted to a IP (IE UnknownHostException)
// ChannelFuture connectFuture = this.clientBootstrap.connect(this.address);
// connectFuture.addListener((ChannelFuture channelFuture) ->
// {
// if (!channelFuture.isSuccess())
// {
// LOGGER.warn("Connection failed: "+channelFuture.cause());
// return;
// }
//
// this.channel.writeAndFlush(new HelloMessage());
// });
//
// this.channel = connectFuture.channel();
// this. channel.closeFuture().addListener((ChannelFuture channelFuture) ->
// {
// switch (this.connectionState)
// {
// case OPEN:
// this.reconnectAttempts--;
// LOGGER.info("Reconnection attempts left: ["+this.reconnectAttempts+"] of ["+FAILURE_RECONNECT_ATTEMPTS+"].");
// if (this.reconnectAttempts == 0)
// {
// this.connectionState = EConnectionState.CLOSE_WAIT;
// return;
// }
//
// this.connectionState = EConnectionState.RECONNECT;
// this.workerGroup.schedule(this::connect, FAILURE_RECONNECT_DELAY_SEC, TimeUnit.SECONDS);
// break;
//
// case RECONNECT_FORCE:
// LOGGER.info("Reconnecting forcefully.");
// this.reconnectAttempts = FAILURE_RECONNECT_ATTEMPTS;
//
// this.connectionState = EConnectionState.RECONNECT;
// this.workerGroup.schedule(this::connect, 0, TimeUnit.SECONDS);
// break;
// }
// });
// }
//
// /** Kills the current connection, triggering auto-reconnection immediately. */
// public void reconnect()
// {
// this.connectionState = EConnectionState.RECONNECT_FORCE;
// this.channel.disconnect();
// }
//
// @Override
// public void close()
// {
// if (this.closeReason != null)
// {
// LOGGER.error(this.closeReason);
// }
//
// if (this.connectionState == EConnectionState.CLOSED)
// {
// return;
// }
//
// this.connectionState = EConnectionState.CLOSED;
// this.workerGroup.shutdownGracefully().syncUninterruptibly();
// this.channel.close().syncUninterruptibly();
// }
}
@@ -0,0 +1,82 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.network.messages.AckMessage;
import com.seibel.distanthorizons.core.network.messages.HelloMessage;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import com.seibel.distanthorizons.core.network.protocol.MessageHandler;
import com.seibel.distanthorizons.coreapi.ModInfo;
//import io.netty.channel.ChannelHandlerContext;
import org.apache.logging.log4j.Logger;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public abstract class NetworkEventSource implements AutoCloseable
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
protected final MessageHandler messageHandler = new MessageHandler();
protected String closeReason = null;
// public NetworkEventSource()
// {
// this.registerHandler(HelloMessage.class, (helloMessage, channelContext) ->
// {
// if (helloMessage.version != ModInfo.PROTOCOL_VERSION)
// {
// try
// {
// String closeReason = "Ignoring message from channel ["+channelContext.name()+"], due to version mismatch. Expected version: ["+ModInfo.PROTOCOL_VERSION+"], received version: ["+helloMessage.version+"].";
// LOGGER.info(closeReason);
// this.close(closeReason);
// }
// catch (Exception e)
// {
// throw new RuntimeException(e);
// }
// }
// });
// }
//
// public <T extends INetworkMessage> void registerHandler(Class<T> clazz, BiConsumer<T, ChannelHandlerContext> handler) { this.messageHandler.registerHandler(clazz, handler); }
//
// public <T extends INetworkMessage> void registerAckHandler(Class<T> clazz, Consumer<ChannelHandlerContext> handler)
// {
// this.messageHandler.registerHandler(AckMessage.class, (ackMessage, channelContext) ->
// {
// if (ackMessage.messageType == clazz)
// {
// handler.accept(channelContext);
// }
// });
// }
//
// public void close(String reason) throws Exception
// {
// this.closeReason = reason;
// this.close();
// }
}
@@ -0,0 +1,122 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.network.messages.CloseReasonMessage;
import com.seibel.distanthorizons.core.network.messages.CloseMessage;
import com.seibel.distanthorizons.core.network.messages.HelloMessage;
import com.seibel.distanthorizons.core.network.protocol.NetworkChannelInitializer;
//import io.netty.bootstrap.ServerBootstrap;
//import io.netty.channel.*;
//import io.netty.channel.nio.NioEventLoopGroup;
//import io.netty.channel.socket.nio.NioServerSocketChannel;
//import io.netty.handler.logging.LogLevel;
//import io.netty.handler.logging.LoggingHandler;
import org.apache.logging.log4j.Logger;
public class NetworkServer //extends NetworkEventSource implements AutoCloseable
{
// private static final Logger LOGGER = DhLoggerBuilder.getLogger();
//
// // TODO move to the config
// private final int port;
//
// private final EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// private final EventLoopGroup workerGroup = new NioEventLoopGroup();
// private Channel channel;
// private boolean isClosed = false;
//
//
//
// public NetworkServer(int port)
// {
// this.port = port;
//
// LOGGER.info("Starting server on port "+port);
// this.registerHandlers();
// this.bind();
// }
//
// private void registerHandlers()
// {
// this.registerHandler(HelloMessage.class, (helloMessage, channelContext) ->
// {
// LOGGER.info("Client connected: "+channelContext.channel().remoteAddress());
// channelContext.channel().writeAndFlush(new HelloMessage());
// });
//
// this.registerHandler(CloseMessage.class, (closeMessage, channelContext) ->
// {
// LOGGER.info("Client disconnected: "+channelContext.channel().remoteAddress());
// });
// }
//
// private void bind()
// {
// ServerBootstrap bootstrap = new ServerBootstrap()
// .group(this.bossGroup, this.workerGroup)
// .channel(NioServerSocketChannel.class)
// .handler(new LoggingHandler(LogLevel.DEBUG))
// .childHandler(new NetworkChannelInitializer(this.messageHandler));
//
// ChannelFuture bindFuture = bootstrap.bind(this.port);
// bindFuture.addListener((ChannelFuture channelFuture) ->
// {
// if (!channelFuture.isSuccess())
// {
// throw new RuntimeException("Failed to bind: " + channelFuture.cause());
// }
//
// LOGGER.info("Server is started on port "+this.port);
// });
//
// this.channel = bindFuture.channel();
// this.channel.closeFuture().addListener(future -> this.close());
// }
//
// public void disconnectClient(ChannelHandlerContext ctx, String reason)
// {
// ctx.channel().config().setAutoRead(false);
// ctx.writeAndFlush(new CloseReasonMessage(reason))
// .addListener(ChannelFutureListener.CLOSE);
// }
//
// @Override
// public void close()
// {
// if (this.closeReason != null)
// {
// LOGGER.error(this.closeReason);
// }
//
// if (this.isClosed)
// {
// return;
// }
// this.isClosed = true;
//
// LOGGER.info("Shutting down the network server.");
// this.workerGroup.shutdownGracefully().syncUninterruptibly();
// this.bossGroup.shutdownGracefully().syncUninterruptibly();
// LOGGER.info("Network server has been closed.");
// }
}
@@ -1,186 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.event;
import com.google.common.cache.CacheBuilder;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.network.event.internal.InternalEvent;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage;
import com.seibel.distanthorizons.core.network.messages.TrackableMessage;
import com.seibel.distanthorizons.core.network.messages.MessageRegistry;
import com.seibel.distanthorizons.core.network.session.SessionClosedException;
import com.seibel.distanthorizons.core.network.messages.requests.CancelMessage;
import com.seibel.distanthorizons.core.network.messages.requests.ExceptionMessage;
import com.seibel.distanthorizons.coreapi.ModInfo;
import org.apache.logging.log4j.LogManager;
import java.io.InvalidClassException;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.*;
import java.util.function.Consumer;
public abstract class NetworkEventSource
{
private static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Logging.logNetworkEvent.get());
protected final ConcurrentMap<
Class<? extends NetworkMessage>,
ConcurrentMap<
NetworkEventSource,
Set<Consumer<NetworkMessage>>
>
> handlers = new ConcurrentHashMap<>();
private final ConcurrentMap<Long, FutureResponseData> pendingFutures = new ConcurrentHashMap<>();
private final Set<Long> cancelledFutures = Collections.newSetFromMap(CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.<Long, Boolean>build()
.asMap());
protected void handleMessage(NetworkMessage message)
{
boolean handled = false;
ConcurrentMap<NetworkEventSource, Set<Consumer<NetworkMessage>>> handlersByEventSource = this.handlers.get(message.getClass());
if (handlersByEventSource != null)
{
for (Set<Consumer<NetworkMessage>> handlerSet : handlersByEventSource.values())
{
for (Consumer<NetworkMessage> handler : handlerSet)
{
handled = true;
handler.accept(message);
}
}
}
if (message instanceof TrackableMessage)
{
TrackableMessage trackableMessage = (TrackableMessage) message;
FutureResponseData responseData = this.pendingFutures.get(trackableMessage.futureId);
if (responseData != null)
{
handled = true;
if (message instanceof ExceptionMessage)
{
responseData.future.completeExceptionally(((ExceptionMessage) message).exception);
}
else if (message.getClass() != responseData.responseClass)
{
responseData.future.completeExceptionally(new InvalidClassException("Response with invalid type: expected " + responseData.responseClass.getSimpleName() + ", got:" + message));
}
else
{
responseData.future.complete(trackableMessage);
}
}
else if (this.cancelledFutures.remove(trackableMessage.futureId))
{
handled = true;
}
}
if (!handled && ModInfo.IS_DEV_BUILD)
{
LOGGER.warn("Unhandled message: {}", message);
}
}
public abstract <T extends NetworkMessage> void registerHandler(Class<T> handlerClass, Consumer<T> handlerImplementation);
protected final <T extends NetworkMessage> void registerHandler(NetworkEventSource instance, Class<T> handlerClass, Consumer<T> handlerImplementation)
{
if (!InternalEvent.class.isAssignableFrom(handlerClass))
{
MessageRegistry.INSTANCE.getMessageId(handlerClass);
}
//noinspection unchecked
this.handlers.computeIfAbsent(handlerClass, missingHandlerClass -> new ConcurrentHashMap<>())
.computeIfAbsent(instance, _instance -> ConcurrentHashMap.newKeySet())
.add((Consumer<NetworkMessage>) handlerImplementation);
}
protected void removeAllHandlers(NetworkEventSource childInstance)
{
for (ConcurrentMap<NetworkEventSource, Set<Consumer<NetworkMessage>>> handlerMap : this.handlers.values())
{
handlerMap.remove(childInstance);
}
}
protected <TResponse extends TrackableMessage> CompletableFuture<TResponse> createRequest(TrackableMessage msg, Class<TResponse> responseClass)
{
CompletableFuture<TResponse> responseFuture = new CompletableFuture<>();
responseFuture.whenComplete((response, throwable) ->
{
if (throwable instanceof CancellationException)
{
this.cancelledFutures.add(msg.futureId);
msg.sendResponse(new CancelMessage());
}
if (!(throwable instanceof SessionClosedException))
{
this.pendingFutures.remove(msg.futureId);
}
});
this.pendingFutures.put(msg.futureId, new FutureResponseData(responseClass, responseFuture));
return responseFuture;
}
protected final void completeAllFuturesExceptionally(Throwable cause)
{
for (FutureResponseData responseData : this.pendingFutures.values())
{
responseData.future.completeExceptionally(cause);
}
}
public void close()
{
this.handlers.clear();
this.completeAllFuturesExceptionally(new SessionClosedException(this.getClass().getSimpleName() + " is closed."));
}
private static class FutureResponseData
{
public final Class<? extends TrackableMessage> responseClass;
public final CompletableFuture<TrackableMessage> future;
private <T extends TrackableMessage> FutureResponseData(Class<T> responseClass, CompletableFuture<T> future)
{
this.responseClass = responseClass;
//noinspection unchecked
this.future = (CompletableFuture<TrackableMessage>) future;
}
}
}
@@ -1,62 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.event;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage;
import java.util.function.Consumer;
/** Provides a way to register network message handlers which are expected to be removed later. */
public final class ScopedNetworkEventSource extends NetworkEventSource
{
public final NetworkEventSource parent;
private boolean isClosed = false;
private final Consumer<NetworkMessage> actualHandleMessageStable = this::handleMessage;
public ScopedNetworkEventSource(NetworkEventSource parent)
{
this.parent = parent;
}
@Override
public <T extends NetworkMessage> void registerHandler(Class<T> handlerClass, Consumer<T> handlerImplementation)
{
if (this.isClosed)
{
return;
}
//noinspection unchecked
this.parent.registerHandler(this, handlerClass, (Consumer<T>) this.actualHandleMessageStable);
super.registerHandler(this, handlerClass, handlerImplementation);
}
@Override
public void close()
{
this.isClosed = true;
this.parent.removeAllHandlers(this);
}
}
@@ -1,9 +0,0 @@
package com.seibel.distanthorizons.core.network.event.internal;
/**
* This event is used to indicate a disconnect.
*/
public class CloseEvent extends InternalEvent
{
}
@@ -1,15 +0,0 @@
package com.seibel.distanthorizons.core.network.event.internal;
/**
* This event is received instead of a message if its protocol version is incompatible with version the mod uses.
*/
public class IncompatibleMessageEvent extends InternalEvent
{
public final int protocolVersion;
public IncompatibleMessageEvent(int protocolVersion)
{
this.protocolVersion = protocolVersion;
}
}
@@ -1,20 +0,0 @@
package com.seibel.distanthorizons.core.network.event.internal;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage;
import io.netty.buffer.ByteBuf;
public abstract class InternalEvent extends NetworkMessage
{
@Override
public void encode(ByteBuf out)
{
throw new UnsupportedOperationException(this.getClass().getSimpleName() + " is an internal event, and cannot be sent.");
}
@Override
public void decode(ByteBuf in)
{
throw new UnsupportedOperationException(this.getClass().getSimpleName() + " is an internal event, and cannot be received.");
}
}
@@ -1,23 +0,0 @@
package com.seibel.distanthorizons.core.network.event.internal;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage;
import org.jetbrains.annotations.Nullable;
/**
* This event is used to indicate that encoding or decoding of a message threw an exception.
*/
public class ProtocolErrorEvent extends InternalEvent
{
public final Throwable reason;
@Nullable
public final NetworkMessage message;
public final boolean replyWithCloseReason;
public ProtocolErrorEvent(Throwable reason, @Nullable NetworkMessage message, boolean replyWithCloseReason)
{
this.reason = reason;
this.message = message;
this.replyWithCloseReason = replyWithCloseReason;
}
}
@@ -1,10 +0,0 @@
package com.seibel.distanthorizons.core.network.exceptions;
public class RequestRejectedException extends Exception
{
public RequestRejectedException(String message)
{
super(message);
}
}
@@ -0,0 +1,45 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import com.seibel.distanthorizons.core.network.protocol.MessageRegistry;
//import io.netty.buffer.ByteBuf;
/**
* Simple empty response message.
* This message is not sent automatically.
*/
public class AckMessage implements INetworkMessage
{
public Class<? extends INetworkMessage> messageType;
public AckMessage() { }
public AckMessage(Class<? extends INetworkMessage> messageType) { this.messageType = messageType; }
// @Override
// public void encode(ByteBuf out) { out.writeInt(MessageRegistry.INSTANCE.getMessageId(this.messageType)); }
//
// @Override
// public void decode(ByteBuf in) { this.messageType = MessageRegistry.INSTANCE.getMessageClassById(in.readInt()); }
}
@@ -0,0 +1,38 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
//import io.netty.buffer.ByteBuf;
/**
* This is not a "real" message, and only used to indicate a disconnection.
* To send a "disconnect reason" message, use {@link CloseReasonMessage}.
*/
public class CloseMessage implements INetworkMessage
{
// @Override
// public void encode(ByteBuf out) { throw new UnsupportedOperationException("CloseMessage is not a real message, and must not be sent."); }
//
// @Override
// public void decode(ByteBuf in) { throw new UnsupportedOperationException("CloseMessage is not a real message, and must not be received."); }
}
@@ -17,34 +17,25 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.seibel.distanthorizons.core.network.messages.base; package com.seibel.distanthorizons.core.network.messages;
import com.google.common.base.MoreObjects; import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage; import com.seibel.distanthorizons.core.network.protocol.INetworkObject;
import io.netty.buffer.ByteBuf; //import io.netty.buffer.ByteBuf;
public class CloseReasonMessage extends NetworkMessage public class CloseReasonMessage implements INetworkMessage
{ {
public String reason; public String reason;
public CloseReasonMessage() { } public CloseReasonMessage() { }
public CloseReasonMessage(String reason) { this.reason = reason; } public CloseReasonMessage(String reason) { this.reason = reason; }
@Override // @Override
public void encode(ByteBuf out) // public void encode(ByteBuf out) { INetworkObject.encodeString(this.reason, out); }
{ //
this.writeString(this.reason, out); // @Override
} // public void decode(ByteBuf in) { this.reason = INetworkObject.decodeString(in); }
@Override
public void decode(ByteBuf in) { this.reason = this.readString(in); }
}
@Override
public MoreObjects.ToStringHelper toStringHelper()
{
return super.toStringHelper()
.add("reason", this.reason);
}
}
@@ -17,12 +17,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.seibel.distanthorizons.core.network.exceptions; package com.seibel.distanthorizons.core.network.messages;
public class InvalidSectionPosException extends Exception import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import com.seibel.distanthorizons.coreapi.ModInfo;
//import io.netty.buffer.ByteBuf;
public class HelloMessage implements INetworkMessage
{ {
public InvalidSectionPosException(String message) public int version = ModInfo.PROTOCOL_VERSION;
{
super(message);
}
// @Override
// public void encode(ByteBuf out) { out.writeInt(this.version); }
//
// @Override
// public void decode(ByteBuf in) { this.version = in.readInt(); }
} }
@@ -1,24 +0,0 @@
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
public interface ILevelRelatedMessage
{
String getLevelName();
/**
* Checks whether the message's level matches the given level.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
default boolean isSameLevelAs(ILevelWrapper levelWrapper)
{
if (levelWrapper instanceof IServerLevelWrapper)
{
return this.getLevelName().equals(((IServerLevelWrapper) levelWrapper).getKeyedLevelDimensionName());
}
return this.getLevelName().equals(levelWrapper.getDimensionName());
}
}
@@ -1,119 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.seibel.distanthorizons.core.network.messages.base.CodecCrashMessage;
import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage;
import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataChunkMessage;
import com.seibel.distanthorizons.core.network.messages.requests.CancelMessage;
import com.seibel.distanthorizons.core.network.messages.base.CloseReasonMessage;
import com.seibel.distanthorizons.core.network.messages.requests.ExceptionMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceResponseMessage;
import com.seibel.distanthorizons.coreapi.ModInfo;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
public class MessageRegistry
{
public static final boolean DEBUG_ENABLE_CODEC_CRASH_MESSAGE = ModInfo.IS_DEV_BUILD;
public static final MessageRegistry INSTANCE = new MessageRegistry();
private final Map<Integer, Supplier<? extends NetworkMessage>> idToSupplier = new HashMap<>();
private final BiMap<Class<? extends NetworkMessage>, Integer> classToId = HashBiMap.create();
private MessageRegistry()
{
// Note: Messages must have parameterless constructors
// When the communication is about to be stopped, either side can send this message
// There may be messages after this, but they should be ignored if it's possible
this.registerMessage(CloseReasonMessage.class, CloseReasonMessage::new);
// Level keys
this.registerMessage(CurrentLevelKeyMessage.class, CurrentLevelKeyMessage::new);
// Config (for full DH support)
this.registerMessage(SessionConfigMessage.class, SessionConfigMessage::new);
// Requests
this.registerMessage(CancelMessage.class, CancelMessage::new);
this.registerMessage(ExceptionMessage.class, ExceptionMessage::new);
// Full data requests & updates
this.registerMessage(FullDataSourceRequestMessage.class, FullDataSourceRequestMessage::new);
this.registerMessage(FullDataSourceResponseMessage.class, FullDataSourceResponseMessage::new);
this.registerMessage(FullDataPartialUpdateMessage.class, FullDataPartialUpdateMessage::new);
this.registerMessage(FullDataChunkMessage.class, FullDataChunkMessage::new);
// Debug messages are always last, and not included into release builds.
if (DEBUG_ENABLE_CODEC_CRASH_MESSAGE)
{
this.registerMessage(CodecCrashMessage.class, CodecCrashMessage::new);
}
}
public <T extends NetworkMessage> void registerMessage(Class<T> clazz, Supplier<T> supplier)
{
int id = this.idToSupplier.size() + 1;
this.idToSupplier.put(id, supplier);
this.classToId.put(clazz, id);
}
public NetworkMessage createMessage(int messageId) throws IllegalArgumentException
{
try
{
return this.idToSupplier.get(messageId).get();
}
catch (NullPointerException e)
{
throw new IllegalArgumentException("Invalid message ID: " + messageId);
}
}
public int getMessageId(NetworkMessage message)
{
return this.getMessageId(message.getClass());
}
public int getMessageId(Class<? extends NetworkMessage> messageClass)
{
try
{
return this.classToId.get(messageClass);
}
catch (NullPointerException e)
{
throw new IllegalArgumentException("Message does not have ID assigned to it: " + messageClass.getSimpleName());
}
}
}
@@ -1,35 +0,0 @@
package com.seibel.distanthorizons.core.network.messages;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.network.session.Session;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
public abstract class NetworkMessage implements INetworkObject
{
private Session session = null;
public IServerPlayerWrapper serverPlayer() { return this.session.serverPlayer; }
public Session getSession()
{
return this.session;
}
public void setSession(Session session)
{
this.session = session;
}
@Override
public String toString()
{
return this.toStringHelper().toString();
}
public MoreObjects.ToStringHelper toStringHelper()
{
return MoreObjects.toStringHelper(this);
}
}
@@ -0,0 +1,46 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
//import io.netty.buffer.ByteBuf;
import java.util.UUID;
public class PlayerUUIDMessage implements INetworkMessage
{
public UUID playerUUID;
public PlayerUUIDMessage() { }
public PlayerUUIDMessage(UUID playerUUID) { this.playerUUID = playerUUID; }
// @Override
// public void encode(ByteBuf out)
// {
// out.writeLong(this.playerUUID.getMostSignificantBits());
// out.writeLong(this.playerUUID.getLeastSignificantBits());
// }
//
// @Override
// public void decode(ByteBuf in) { this.playerUUID = new UUID(in.readLong(), in.readLong()); }
}
@@ -0,0 +1,42 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.protocol.INetworkObject;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import com.seibel.distanthorizons.core.network.objects.RemotePlayer;
//import io.netty.buffer.ByteBuf;
public class RemotePlayerConfigMessage implements INetworkMessage
{
public RemotePlayer.Payload payload;
public RemotePlayerConfigMessage() { }
public RemotePlayerConfigMessage(RemotePlayer.Payload payload) { this.payload = payload; }
// @Override
// public void encode(ByteBuf out) { this.payload.encode(out); }
//
// @Override
// public void decode(ByteBuf in) { this.payload = INetworkObject.decode(new RemotePlayer.Payload(), in); }
}
@@ -17,22 +17,24 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.seibel.distanthorizons.core.network.messages.requests; package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.messages.TrackableMessage; import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import io.netty.buffer.ByteBuf; //import io.netty.buffer.ByteBuf;
public class CancelMessage extends TrackableMessage public class RequestChunksMessage implements INetworkMessage
{ {
public CancelMessage() { }
// @Override
// public void encode(ByteBuf out)
// {
//
// }
//
// @Override
// public void decode(ByteBuf in)
// {
//
// }
@Override }
public void encode0(ByteBuf out)
{
}
@Override
public void decode0(ByteBuf in)
{
}
}
@@ -1,98 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.network.messages.requests.ExceptionMessage;
import com.seibel.distanthorizons.core.network.session.Session;
import com.seibel.distanthorizons.core.world.EWorldEnvironment;
import io.netty.buffer.ByteBuf;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
public abstract class TrackableMessage extends NetworkMessage
{
private static final AtomicInteger lastId = new AtomicInteger();
// 32 bits - Session ID (not transmitted)
// 1 bit - Requesting side (client - 0, server - 1)
// 31 bits - Request ID
public long futureId = lastId.getAndIncrement()
| ((Objects.requireNonNull(SharedApi.getEnvironment()) == EWorldEnvironment.Server_Only ? 1 : 0) << 31);
@Override
public void setSession(Session session)
{
super.setSession(session);
this.futureId |= (long) session.id << 32;
}
public void sendResponse(TrackableMessage responseMessage)
{
responseMessage.futureId = this.futureId;
this.getSession().sendMessage(responseMessage);
}
public void sendResponse(Exception e)
{
this.sendResponse(new ExceptionMessage(e));
}
@Override
public final void encode(ByteBuf out)
{
try
{
out.writeInt((int) this.futureId);
this.encode0(out);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
@Override
public final void decode(ByteBuf in)
{
try
{
this.futureId = in.readInt();
this.decode0(in);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
protected abstract void encode0(ByteBuf out) throws Exception;
protected abstract void decode0(ByteBuf in) throws Exception;
@Override public MoreObjects.ToStringHelper toStringHelper()
{
return super.toStringHelper()
.add("futureId", this.futureId);
}
}
@@ -1,62 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages.base;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.messages.NetworkMessage;
import io.netty.buffer.ByteBuf;
public class CodecCrashMessage extends NetworkMessage
{
public ECrashPhase crashPhase;
public CodecCrashMessage() { }
public CodecCrashMessage(ECrashPhase crashPhase) { this.crashPhase = crashPhase; }
@Override
public void encode(ByteBuf out)
{
if (this.crashPhase == ECrashPhase.ENCODE)
{
throw new RuntimeException("encode force crash");
}
}
@Override
public void decode(ByteBuf in)
{
throw new RuntimeException("decode force crash");
}
@Override
public MoreObjects.ToStringHelper toStringHelper()
{
return super.toStringHelper()
.add("crashPhase", this.crashPhase);
}
public enum ECrashPhase
{
ENCODE,
DECODE
}
}

Some files were not shown because too many files have changed in this diff Show More