diff --git a/.gitignore b/.gitignore index 42889b9cc..eeff2dc36 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ forge*changelog.txt # Sqlite databases *.sqlite +*.sqlite-journal +*.sqlite-shm +*.sqlite-wal diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EBufferRebuildTimes.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EBufferRebuildTimes.java deleted file mode 100644 index b34c005c7..000000000 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EBufferRebuildTimes.java +++ /dev/null @@ -1,56 +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 . - */ - -package com.seibel.distanthorizons.api.enums.config; - -/** - * CONSTANT
- * FREQUENT
- * NORMAL
- * RARE

- * - * Determines how fast the buffers should be regenerated - * - * @author Leonardo Amato - * @version 9-25-2021 - */ -@Deprecated // not currently in use, if the config this enum represents is re-implemented, the deprecated flag can be removed -public enum EBufferRebuildTimes -{ - CONSTANT(0, 0, 0, 1), - - FREQUENT(1000, 500, 2500, 1), - - NORMAL(2000, 1000, 5000, 4), - - RARE(5000, 2000, 10000, 16); - - public final int playerMoveTimeout; - public final int renderedChunkTimeout; - public final int chunkChangeTimeout; - public final int playerMoveDistance; - - EBufferRebuildTimes(int playerMoveTimeout, int renderedChunkTimeout, int chunkChangeTimeout, int playerMoveDistance) - { - this.playerMoveTimeout = playerMoveTimeout; - this.renderedChunkTimeout = renderedChunkTimeout; - this.chunkChangeTimeout = chunkChangeTimeout; - this.playerMoveDistance = playerMoveDistance; - } -} diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EBlocksToAvoid.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiBlocksToAvoid.java similarity index 87% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/config/EBlocksToAvoid.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiBlocksToAvoid.java index 2f26d3d71..92388135d 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EBlocksToAvoid.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiBlocksToAvoid.java @@ -23,9 +23,10 @@ package com.seibel.distanthorizons.api.enums.config; * NONE,
* NON_COLLIDING,
* - * @since API 1.0.0 + * @since API 1.1.0 + * @version 2024-4-6 */ -public enum EBlocksToAvoid +public enum EDhApiBlocksToAvoid { // Reminder: // when adding items up the API minor version @@ -36,6 +37,6 @@ public enum EBlocksToAvoid public final boolean noCollision; - EBlocksToAvoid(boolean noCollision) { this.noCollision = noCollision; } + EDhApiBlocksToAvoid(boolean noCollision) { this.noCollision = noCollision; } } \ No newline at end of file diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiDataCompressionMode.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiDataCompressionMode.java new file mode 100644 index 000000000..ce277088c --- /dev/null +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiDataCompressionMode.java @@ -0,0 +1,100 @@ +/* + * 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 . + */ + +package com.seibel.distanthorizons.api.enums.config; + +/** + * UNCOMPRESSED
+ * LZ4
+ * ZSTD
+ * XZ

+ * + * Note: speed and compression ratios are examples + * and should only be used for estimated comparisons. + * + * @version 2024-3-16 + * @since API 1.1.0 + */ +public enum EDhApiDataCompressionMode +{ + // Reminder: + // when adding items up the API minor version + // when removing items up the API major version + + /** + * Should only be used internally and for unit testing.

+ * + * Read Speed: 1.64 MS / DTO
+ * Write Speed: 12.44 MS / DTO
+ * Compression ratio: 1.0
+ */ + @DisallowSelectingViaConfigGui + UNCOMPRESSED(0), + + /** + * Extremely fast (often faster than uncompressed), but generally poor compression.

+ * + * Read Speed: 1.85 MS / DTO
+ * Write Speed: 9.46 MS / DTO
+ * Compression ratio: 0.3638
+ */ + LZ4(1), + + /** + * Decent speed and good compression.

+ * + * Read Speed: 11.78 MS / DTO
+ * Write Speed: 16.76 MS / DTO
+ * Compression ratio: 0.2199
+ */ + Z_STD(2), + + /** + * Extremely slow, but very good compression.

+ * + * Read Speed: 12.25 MS / DTO
+ * Write Speed: 490.07 MS / DTO
+ * Compression ratio: 0.1242
+ */ + LZMA2(3); + + + + /** More stable than using the ordinal of the enum */ + public final byte value; + + EDhApiDataCompressionMode(int value) { this.value = (byte) value; } + + + public static EDhApiDataCompressionMode getFromValue(byte value) + { + EDhApiDataCompressionMode[] enumList = EDhApiDataCompressionMode.values(); + for (int i = 0; i < enumList.length; i++) + { + if (enumList[i].value == value) + { + return enumList[i]; + } + } + + throw new IllegalArgumentException("No compression mode with the value ["+value+"]"); + } + + +} diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EGLErrorHandlingMode.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiGLErrorHandlingMode.java similarity index 91% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/config/EGLErrorHandlingMode.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiGLErrorHandlingMode.java index 00719a140..5e5013613 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EGLErrorHandlingMode.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiGLErrorHandlingMode.java @@ -20,9 +20,10 @@ package com.seibel.distanthorizons.api.enums.config; /** - * @since API 1.0.0 + * @since API 1.1.0 + * @version 2024-4-6 */ -public enum EGLErrorHandlingMode +public enum EDhApiGLErrorHandlingMode { IGNORE, LOG, diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EGlProfileMode.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiGlProfileMode.java similarity index 92% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/config/EGlProfileMode.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiGlProfileMode.java index b4247d66f..3e383e758 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EGlProfileMode.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiGlProfileMode.java @@ -20,9 +20,10 @@ package com.seibel.distanthorizons.api.enums.config; /** - * @since API 1.0.0 + * @since API 1.1.0 + * @version 2024-4-6 */ -public enum EGlProfileMode +public enum EDhApiGlProfileMode { CORE, COMPAT, diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EGpuUploadMethod.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiGpuUploadMethod.java similarity index 92% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/config/EGpuUploadMethod.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiGpuUploadMethod.java index d87d8738d..8f3f5ec9c 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EGpuUploadMethod.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiGpuUploadMethod.java @@ -28,10 +28,10 @@ package com.seibel.distanthorizons.api.enums.config; * * @author Leetom * @author James Seibel - * @version 2022-7-2 - * @since API 1.0.0 + * @version 2024-4-6 + * @since API 1.1.0 */ -public enum EGpuUploadMethod +public enum EDhApiGpuUploadMethod { /** Picks the best option based on the GPU the user has. */ AUTO(false, false), @@ -60,7 +60,7 @@ public enum EGpuUploadMethod public final boolean useEarlyMapping; public final boolean useBufferStorage; - EGpuUploadMethod(boolean useEarlyMapping, boolean useBufferStorage) + EDhApiGpuUploadMethod(boolean useEarlyMapping, boolean useBufferStorage) { this.useEarlyMapping = useEarlyMapping; this.useBufferStorage = useBufferStorage; diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/ELightGenerationMode.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiGrassSideRendering.java similarity index 85% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/config/ELightGenerationMode.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiGrassSideRendering.java index cc57d08b7..2f2f1a8fa 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/ELightGenerationMode.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiGrassSideRendering.java @@ -20,21 +20,21 @@ package com.seibel.distanthorizons.api.enums.config; /** - * DISTANT_HORIZONS,
- * MINECRAFT, + * AS_GRASS
+ * FADE_TO_DIRT
+ * AS_DIRT
* - * @author Leetom - * @version 2023-6-7 - * @since API 1.0.0 + * @since API 1.1.0 + * @version 2024-4-7 */ -public enum ELightGenerationMode +public enum EDhApiGrassSideRendering { // Reminder: // when adding items up the API minor version // when removing items up the API major version - DISTANT_HORIZONS, - - MINECRAFT + AS_GRASS, + FADE_TO_DIRT, + AS_DIRT; } diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EHorizontalQuality.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiHorizontalQuality.java similarity index 88% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/config/EHorizontalQuality.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiHorizontalQuality.java index 3fdcddd83..bdf7967cf 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EHorizontalQuality.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiHorizontalQuality.java @@ -26,9 +26,10 @@ package com.seibel.distanthorizons.api.enums.config; * HIGH
* UNLIMITED
* - * @since API 1.0.0 + * @since API 1.1.0 + * @version 2024-4-6 */ -public enum EHorizontalQuality +public enum EDhApiHorizontalQuality { // Reminder: // when adding items up the API minor version @@ -42,17 +43,16 @@ public enum EHorizontalQuality LOWEST(2.0f, 4), LOW(2.0f, 8), MEDIUM(2.0f, 12), - HIGH(2.2f, 24), - EXTREME(2.4f, 64), - - UNLIMITED(-1, -1); + HIGH(2.2f, 16), + EXTREME(2.4f, 32), + ; public final double quadraticBase; public final int distanceUnitInBlocks; - EHorizontalQuality(double quadraticBase, int distanceUnitInBlocks) + EDhApiHorizontalQuality(double quadraticBase, int distanceUnitInBlocks) { this.quadraticBase = quadraticBase; this.distanceUnitInBlocks = distanceUnitInBlocks; diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/ELodShading.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiLodShading.java similarity index 77% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/config/ELodShading.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiLodShading.java index b098bbab0..b3ca135b8 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/ELodShading.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiLodShading.java @@ -20,27 +20,33 @@ package com.seibel.distanthorizons.api.enums.config; /** - * MINECRAFT
- * OLD_LIGHTING
- * NONE
+ * AUTO
+ * ENABLED
+ * DISABLED
* - * @since API 1.0.0 + * @since API 1.1.0 + * @version 2024-4-6 */ -public enum ELodShading +public enum EDhApiLodShading { // Reminder: // when adding items up the API minor version // when removing items up the API major version - /** Uses Minecraft's shading for LODs */ - MINECRAFT, /** - * Simulates Minecraft's shading. + * Uses Minecraft's shading for LODs.
+ * This means if Minecraft's shading is disabled DH's shading will be as well. + */ + AUTO, + + /** + * Simulates Minecraft's shading.
* This is most useful for shaders that disable Minecraft's shading * but still require shading on LODs. */ - OLD_LIGHTING, + ENABLED, + /** LODs will have no shading */ - NONE; + DISABLED; } diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/ELoggerMode.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiLoggerMode.java similarity index 92% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/config/ELoggerMode.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiLoggerMode.java index 3a16c88ed..eb9043a2e 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/ELoggerMode.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiLoggerMode.java @@ -22,9 +22,10 @@ package com.seibel.distanthorizons.api.enums.config; import org.apache.logging.log4j.Level; /** - * @since API 1.0.0 + * @since API 1.1.0 + * @version 2024-4-6 */ -public enum ELoggerMode +public enum EDhApiLoggerMode { DISABLED(Level.OFF, Level.OFF), LOG_ALL_TO_FILE(Level.ALL, Level.OFF), @@ -40,11 +41,14 @@ public enum ELoggerMode LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE(Level.INFO, Level.WARN), LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE(Level.INFO, Level.ERROR), ; + public final Level levelForFile; public final Level levelForChat; - ELoggerMode(Level levelForFile, Level levelForChat) + + EDhApiLoggerMode(Level levelForFile, Level levelForChat) { this.levelForFile = levelForFile; this.levelForChat = levelForChat; } + } diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EMaxHorizontalResolution.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiMaxHorizontalResolution.java similarity index 80% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/config/EMaxHorizontalResolution.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiMaxHorizontalResolution.java index e9139c20b..922932c1b 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EMaxHorizontalResolution.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiMaxHorizontalResolution.java @@ -33,10 +33,10 @@ import com.seibel.distanthorizons.coreapi.util.MathUtil; * * @author James Seibel * @author Leonardo Amato - * @version 2023-6-14 - * @since API 1.0.0 + * @since API 1.1.0 + * @version 2024-4-6 */ -public enum EMaxHorizontalResolution +public enum EDhApiMaxHorizontalResolution { /** render 256 LODs for each chunk */ BLOCK(16, 0), @@ -84,12 +84,12 @@ public enum EMaxHorizontalResolution * 2nd dimension: An array of all LodDetails that are less than or
* equal to that detailLevel */ - private static EMaxHorizontalResolution[][] lowerDetailArrays; + private static EDhApiMaxHorizontalResolution[][] lowerDetailArrays; - EMaxHorizontalResolution(int newLengthCount, int newDetailLevel) + EDhApiMaxHorizontalResolution(int newLengthCount, int newDetailLevel) { this.detailLevel = (byte) newDetailLevel; this.dataPointLengthCount = newLengthCount; @@ -129,20 +129,20 @@ public enum EMaxHorizontalResolution * Returns an array of all LodDetails that have a detail level * that is less than or equal to the given LodDetail */ - public static EMaxHorizontalResolution[] getSelfAndLowerDetails(EMaxHorizontalResolution detail) + public static EDhApiMaxHorizontalResolution[] getSelfAndLowerDetails(EDhApiMaxHorizontalResolution detail) { if (lowerDetailArrays == null) { // run first time setup - lowerDetailArrays = new EMaxHorizontalResolution[EMaxHorizontalResolution.values().length][]; + lowerDetailArrays = new EDhApiMaxHorizontalResolution[EDhApiMaxHorizontalResolution.values().length][]; // go through each LodDetail - for (EMaxHorizontalResolution currentDetail : EMaxHorizontalResolution.values()) + for (EDhApiMaxHorizontalResolution currentDetail : EDhApiMaxHorizontalResolution.values()) { - ArrayList lowerDetails = new ArrayList<>(); + ArrayList lowerDetails = new ArrayList<>(); // find the details lower than currentDetail - for (EMaxHorizontalResolution compareDetail : EMaxHorizontalResolution.values()) + for (EDhApiMaxHorizontalResolution compareDetail : EDhApiMaxHorizontalResolution.values()) { if (currentDetail.detailLevel <= compareDetail.detailLevel) { @@ -154,7 +154,7 @@ public enum EMaxHorizontalResolution Collections.sort(lowerDetails); Collections.reverse(lowerDetails); - lowerDetailArrays[currentDetail.detailLevel] = lowerDetails.toArray(new EMaxHorizontalResolution[lowerDetails.size()]); + lowerDetailArrays[currentDetail.detailLevel] = lowerDetails.toArray(new EDhApiMaxHorizontalResolution[lowerDetails.size()]); } } @@ -162,9 +162,9 @@ public enum EMaxHorizontalResolution } /** Returns what detail level should be used at a given distance and maxDistance. */ - public static EMaxHorizontalResolution getDetailForDistance(EMaxHorizontalResolution maxDetailLevel, int distance, int maxDistance) + public static EDhApiMaxHorizontalResolution getDetailForDistance(EDhApiMaxHorizontalResolution maxDetailLevel, int distance, int maxDistance) { - EMaxHorizontalResolution[] lowerDetails = getSelfAndLowerDetails(maxDetailLevel); + EDhApiMaxHorizontalResolution[] lowerDetails = getSelfAndLowerDetails(maxDetailLevel); int distanceBetweenDetails = maxDistance / lowerDetails.length; int index = MathUtil.clamp(0, distance / distanceBetweenDetails, lowerDetails.length - 1); diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EServerFolderNameMode.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiServerFolderNameMode.java similarity index 92% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/config/EServerFolderNameMode.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiServerFolderNameMode.java index 944e61457..69a8ea619 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EServerFolderNameMode.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiServerFolderNameMode.java @@ -22,6 +22,7 @@ package com.seibel.distanthorizons.api.enums.config; /** * NAME_ONLY,
+ * IP_ONLY,
* NAME_IP,
* NAME_IP_PORT,
* NAME_IP_PORT_MC_VERSION,

@@ -29,10 +30,10 @@ package com.seibel.distanthorizons.api.enums.config; * Determines how the multiplayer folders should be named. * * @author James Seibel - * @version 2022-7-1 - * @since API 1.0.0 + * @since API 1.1.0 + * @version 2024-4-6 */ -public enum EServerFolderNameMode +public enum EDhApiServerFolderNameMode { // Reminder: // when adding items up the API minor version @@ -42,6 +43,9 @@ public enum EServerFolderNameMode /** Only use the server name */ NAME_ONLY, + /** Only use the server IP */ + IP_ONLY, + /** * {SERVER_NAME} IP {IP}
* Example: Minecraft Server IP 192.168.1.40 diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiUpdateBranch.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiUpdateBranch.java new file mode 100644 index 000000000..e7eab618f --- /dev/null +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiUpdateBranch.java @@ -0,0 +1,14 @@ +package com.seibel.distanthorizons.api.enums.config; + +/** + * STABLE,
+ * NIGHTLY,

+ * + * @since API 1.1.0 + * @version 2024-4-6 + */ +public enum EDhApiUpdateBranch +{ + STABLE, + NIGHTLY +} diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EVanillaOverdraw.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiVanillaOverdraw.java similarity index 95% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/config/EVanillaOverdraw.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiVanillaOverdraw.java index c9f9124ab..ed321d493 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EVanillaOverdraw.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiVanillaOverdraw.java @@ -28,10 +28,11 @@ package com.seibel.distanthorizons.api.enums.config; * the vanilla Minecraft terrain. * * @author James Seibel - * @version 2022-6-30 + * @since API 1.1.0 + * @version 2024-4-6 */ @Deprecated // not currently in use, if the config this enum represents is re-implemented, the deprecated flag can be removed -public enum EVanillaOverdraw +public enum EDhApiVanillaOverdraw { // Reminder: // when adding items up the API minor version diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EVerticalQuality.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiVerticalQuality.java similarity index 70% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/config/EVerticalQuality.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiVerticalQuality.java index c20b86b70..0917ab82b 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EVerticalQuality.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiVerticalQuality.java @@ -29,23 +29,25 @@ import com.seibel.distanthorizons.coreapi.util.MathUtil; * EXTREME
* * @author Leonardo Amato - * @version 2023-2-5 - * @since API 1.0.0 + * @version 2024-4-6 + * @since API 1.1.0 */ -public enum EVerticalQuality +public enum EDhApiVerticalQuality { - HEIGHT_MAP(new int[]{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}), - LOW(new int[]{4, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1}), - MEDIUM(new int[]{6, 4, 3, 2, 2, 1, 1, 1, 1, 1, 1}), - HIGH(new int[]{8, 6, 4, 2, 2, 2, 2, 1, 1, 1, 1}), - EXTREME(new int[]{16, 8, 4, 2, 2, 2, 2, 1, 1, 1, 1}); + HEIGHT_MAP( new int[]{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}), + LOW( new int[]{4, 3, 3, 2, 2, 1, 1, 1, 1, 1, 1}), + MEDIUM( new int[]{6, 4, 3, 3, 3, 3, 3, 2, 2, 2, 1}), + HIGH( new int[]{16, 8, 4, 3, 3, 3, 3, 3, 3, 3, 1}), + VERY_HIGH( new int[]{32, 16, 8, 4, 4, 3, 3, 3, 3, 3, 1}), + EXTREME( new int[]{64, 32, 8, 4, 4, 3, 3, 3, 3, 3, 1}), + PIXEL_ART( new int[]{512, 64, 16, 8, 4, 3, 3, 3, 3, 3, 1}); /** represents how many LODs can be rendered in a single vertical slice */ public final int[] maxVerticalData; - EVerticalQuality(int[] maxVerticalData) { this.maxVerticalData = maxVerticalData; } + EDhApiVerticalQuality(int[] maxVerticalData) { this.maxVerticalData = maxVerticalData; } diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiWorldCompressionMode.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiWorldCompressionMode.java new file mode 100644 index 000000000..d8abc9eb0 --- /dev/null +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EDhApiWorldCompressionMode.java @@ -0,0 +1,70 @@ +/* + * 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 . + */ + +package com.seibel.distanthorizons.api.enums.config; + +/** + * MERGE_SAME_BLOCKS
+ * VISUALLY_EQUAL

+ * + * @version 2024-3-27 + * @since API 1.1.0 + */ +public enum EDhApiWorldCompressionMode +{ + // Reminder: + // when adding items up the API minor version + // when removing items up the API major version + + /** + * Every block/biome change is recorded in the database.
+ * This is what DH 2.0 and 2.0.1 all used by default and will store a lot of data. + */ + MERGE_SAME_BLOCKS(0), + + /** + * Only visible block/biome changes are recorded in the database.
+ * Hidden blocks (IE ores) are ignored. + */ + VISUALLY_EQUAL(1); + + + + /** More stable than using the ordinal of the enum */ + public final byte value; + + EDhApiWorldCompressionMode(int value) { this.value = (byte) value; } + + + public static EDhApiWorldCompressionMode getFromValue(byte value) + { + EDhApiWorldCompressionMode[] enumList = EDhApiWorldCompressionMode.values(); + for (int i = 0; i < enumList.length; i++) + { + if (enumList[i].value == value) + { + return enumList[i]; + } + } + + throw new IllegalArgumentException("No lossy compression mode with the value ["+value+"]"); + } + + +} diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EGenerationPriority.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EGenerationPriority.java deleted file mode 100644 index 39271c3ee..000000000 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EGenerationPriority.java +++ /dev/null @@ -1,48 +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 . - */ - -package com.seibel.distanthorizons.api.enums.config; - -/** - * AUTO
- * Near_First
- * Far_First

- * - * Determines which LODs should have priority when generating - * outside the normal view distance. - * - * @author Leonardo Amato - * @version 12-1-2021 - */ -@Deprecated // not currently in use, if the config this enum represents is re-implemented, the deprecated flag can be removed -public enum EGenerationPriority -{ - // Reminder: - // when adding items up the API minor version - // when removing items up the API major version - - /** NEAR_FIRST when connected to servers and BALANCED when on single player */ - AUTO, - - NEAR_FIRST, - - BALANCED, - - FAR_FIRST -} diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EOverdrawPrevention.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EOverdrawPrevention.java deleted file mode 100644 index 46f82868b..000000000 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EOverdrawPrevention.java +++ /dev/null @@ -1,51 +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 . - */ - -package com.seibel.distanthorizons.api.enums.config; - -/** - * NONE
- * LIGHT
- * MEDIUM
- * HEAVY
- * - * CUSTOM
- * - * @since API 1.0.0 - * @deprecated will be removed when DH updates to MC 1.21
- * After removal a float value will be used to control overdraw instead. - */ -@Deprecated -public enum EOverdrawPrevention -{ - // Reminder: - // when adding items up the API minor version - // when removing items up the API major version - - NONE, - LIGHT, - MEDIUM, - HEAVY, - - /** - * Should not be passed in.
- * Is returned if the overdraw value doesn't match any of the enums defined here. - */ - CUSTOM; -} diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EUpdateBranch.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EUpdateBranch.java deleted file mode 100644 index 09da806d5..000000000 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/EUpdateBranch.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.seibel.distanthorizons.api.enums.config; - -public enum EUpdateBranch -{ - STABLE, - NIGHTLY -} diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/quickOptions/EQualityPreset.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/quickOptions/EDhApiQualityPreset.java similarity index 94% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/config/quickOptions/EQualityPreset.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/config/quickOptions/EDhApiQualityPreset.java index 7936f3176..7bca56868 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/quickOptions/EQualityPreset.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/quickOptions/EDhApiQualityPreset.java @@ -30,9 +30,10 @@ import com.seibel.distanthorizons.api.enums.config.DisallowSelectingViaConfigGui * HIGH,
* EXTREME,
* - * @since API 1.0.0 + * @since API 1.1.0 + * @version 2024-4-6 */ -public enum EQualityPreset +public enum EDhApiQualityPreset { // Reminder: // when adding items up the API minor version diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/quickOptions/EThreadPreset.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/quickOptions/EDhApiThreadPreset.java similarity index 92% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/config/quickOptions/EThreadPreset.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/config/quickOptions/EDhApiThreadPreset.java index 8773ff1a4..b10468e4f 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/config/quickOptions/EThreadPreset.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/config/quickOptions/EDhApiThreadPreset.java @@ -29,9 +29,10 @@ import com.seibel.distanthorizons.api.enums.config.DisallowSelectingViaConfigGui * BALANCED,
* AGGRESSIVE,
* - * @since API 1.0.0 + * @since API 1.1.0 + * @version 2024-4-6 */ -public enum EThreadPreset +public enum EDhApiThreadPreset { // Reminder: // when adding items up the API minor version @@ -44,8 +45,6 @@ public enum EThreadPreset LOW_IMPACT, BALANCED, AGGRESSIVE, - - // temporarily removed due to stability concerns I_PAID_FOR_THE_WHOLE_CPU, } \ No newline at end of file diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDebugRendering.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiDebugRendering.java similarity index 86% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDebugRendering.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiDebugRendering.java index 62ee27786..646f9f25a 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDebugRendering.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiDebugRendering.java @@ -28,10 +28,10 @@ package com.seibel.distanthorizons.api.enums.rendering; * * @author Leetom * @author James Seibel - * @version 2023-6-7 - * @since API 1.0.0 + * @version 2024-4-6 + * @since API 1.1.0 */ -public enum EDebugRendering +public enum EDhApiDebugRendering { // Reminder: // when adding items up the API minor version @@ -44,10 +44,6 @@ public enum EDebugRendering /** LOD colors are based on their detail */ SHOW_DETAIL, - @Deprecated - /** LOD colors are based on their gen mode. */ - SHOW_GENMODE, - /** Block Materials are often used by Iris shaders to determine how LODs should be rendered */ SHOW_BLOCK_MATERIAL, @@ -58,7 +54,7 @@ public enum EDebugRendering SHOW_RENDER_SOURCE_FLAG; - public static EDebugRendering next(EDebugRendering type) + public static EDhApiDebugRendering next(EDhApiDebugRendering type) { switch (type) { @@ -75,7 +71,7 @@ public enum EDebugRendering } } - public static EDebugRendering previous(EDebugRendering type) + public static EDhApiDebugRendering previous(EDhApiDebugRendering type) { switch (type) { @@ -84,8 +80,6 @@ public enum EDebugRendering case SHOW_RENDER_SOURCE_FLAG: return SHOW_OVERLAPPING_QUADS; case SHOW_OVERLAPPING_QUADS: - return SHOW_GENMODE; - case SHOW_GENMODE: return SHOW_DETAIL; default: return OFF; diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EFogColorMode.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiFogColorMode.java similarity index 95% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EFogColorMode.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiFogColorMode.java index ab0831d49..8900574c5 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EFogColorMode.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiFogColorMode.java @@ -24,10 +24,10 @@ package com.seibel.distanthorizons.api.enums.rendering; * USE_SKY_COLOR,
* * @author James Seibel - * @version 2022-6-9 - * @since API 1.0.0 + * @version 2024-4-6 + * @since API 1.1.0 */ -public enum EFogColorMode +public enum EDhApiFogColorMode { // Reminder: // when adding items: up the API minor version @@ -44,4 +44,5 @@ public enum EFogColorMode * https://www.curseforge.com/minecraft/mc-mods/clear-skies-forge-port */ USE_SKY_COLOR, + } diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EFogDrawMode.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiFogDrawMode.java similarity index 96% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EFogDrawMode.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiFogDrawMode.java index 83c732142..1bc322109 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EFogDrawMode.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiFogDrawMode.java @@ -25,10 +25,10 @@ package com.seibel.distanthorizons.api.enums.rendering; * FOG_DISABLED
* * @author James Seibel + * @since API 1.1.0 * @version 2022-6-2 - * @since API 1.0.0 */ -public enum EFogDrawMode +public enum EDhApiFogDrawMode { // Reminder: // when adding items up the API minor version diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EFogFalloff.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiFogFalloff.java similarity index 93% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EFogFalloff.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiFogFalloff.java index c1de07ece..4f310bf17 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EFogFalloff.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiFogFalloff.java @@ -26,9 +26,9 @@ package com.seibel.distanthorizons.api.enums.rendering; * * @author Leetom * @version 2022-6-30 - * @since API 1.0.0 + * @since API 1.1.0 */ -public enum EFogFalloff +public enum EDhApiFogFalloff { // Reminder: // when adding items up the API minor version @@ -38,5 +38,5 @@ public enum EFogFalloff LINEAR, EXPONENTIAL, EXPONENTIAL_SQUARED, - // TEXTURE_BASED, // TODO: Impl this + } diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EHeightFogMixMode.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiHeightFogMixMode.java similarity index 94% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EHeightFogMixMode.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiHeightFogMixMode.java index b688ebda8..13ab79dbb 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EHeightFogMixMode.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiHeightFogMixMode.java @@ -32,10 +32,10 @@ package com.seibel.distanthorizons.api.enums.rendering; * AVERAGE
* * @author Leetom - * @version 2022-4-14 - * @since API 1.0.0 + * @version 2024-4-6 + * @since API 1.1.0 */ -public enum EHeightFogMixMode +public enum EDhApiHeightFogMixMode { BASIC, IGNORE_HEIGHT, diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EHeightFogMode.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiHeightFogMode.java similarity index 91% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EHeightFogMode.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiHeightFogMode.java index 5aebcc8cd..99e538603 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EHeightFogMode.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiHeightFogMode.java @@ -28,10 +28,10 @@ package com.seibel.distanthorizons.api.enums.rendering; * ABOVE_AND_BELOW_SET_HEIGHT,
* * @author Leetom - * @version 6-30-2022 - * @since API 1.0.0 + * @version 2024-4-6 + * @since API 1.1.0 */ -public enum EHeightFogMode +public enum EDhApiHeightFogMode { // Reminder: // when adding items up the API minor version @@ -49,7 +49,7 @@ public enum EHeightFogMode public final boolean above; public final boolean below; - EHeightFogMode(boolean basedOnCamera, boolean above, boolean below) + EDhApiHeightFogMode(boolean basedOnCamera, boolean above, boolean below) { this.basedOnCamera = basedOnCamera; this.above = above; diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/ERendererMode.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiRendererMode.java similarity index 87% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/ERendererMode.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiRendererMode.java index a6a9c6c85..87a7099b1 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/ERendererMode.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiRendererMode.java @@ -24,10 +24,10 @@ package com.seibel.distanthorizons.api.enums.rendering; * Debug
* Disabled
* - * @version 2022-6-2 - * @since API 1.0.0 + * @since API 1.1.0 + * @version 2024-4-6 */ -public enum ERendererMode +public enum EDhApiRendererMode { // Reminder: // when adding items up the API minor version @@ -40,7 +40,7 @@ public enum ERendererMode /** Used by the config GUI to cycle through the available rendering options */ - public static ERendererMode next(ERendererMode type) + public static EDhApiRendererMode next(EDhApiRendererMode type) { switch (type) { @@ -54,7 +54,7 @@ public enum ERendererMode } /** Used by the config GUI to cycle through the available rendering options */ - public static ERendererMode previous(ERendererMode type) + public static EDhApiRendererMode previous(EDhApiRendererMode type) { switch (type) { diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/ETransparency.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiTransparency.java similarity index 89% rename from api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/ETransparency.java rename to api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiTransparency.java index aefc14473..495c8fe6e 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/ETransparency.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EDhApiTransparency.java @@ -24,9 +24,10 @@ package com.seibel.distanthorizons.api.enums.rendering; * FAKE,
* COMPLETE,
* - * @since API 1.0.0 + * @since API 1.1.0 + * @version 2024-4-6 */ -public enum ETransparency +public enum EDhApiTransparency { // Reminder: // when adding items up the API minor version @@ -40,7 +41,7 @@ public enum ETransparency public final boolean transparencyEnabled; public final boolean fakeTransparencyEnabled; - ETransparency(boolean transparencyEnabled, boolean fakeTransparencyEnabled) + EDhApiTransparency(boolean transparencyEnabled, boolean fakeTransparencyEnabled) { this.transparencyEnabled = transparencyEnabled; this.fakeTransparencyEnabled = fakeTransparencyEnabled; diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EFogDistance.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EFogDistance.java deleted file mode 100644 index 5196ad696..000000000 --- a/api/src/main/java/com/seibel/distanthorizons/api/enums/rendering/EFogDistance.java +++ /dev/null @@ -1,40 +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 . - */ - -package com.seibel.distanthorizons.api.enums.rendering; - -/** - * NEAR,
- * FAR,
- * NEAR_AND_FAR
- * - * @author James Seibel - * @version 2022-6-2 - * @since API 1.0.0 - */ -public enum EFogDistance -{ - // Reminder: - // when adding items up the API minor version - // when removing items up the API major version - - NEAR, - FAR, - NEAR_AND_FAR -} \ No newline at end of file diff --git a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/both/IDhApiWorldGenerationConfig.java b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/both/IDhApiWorldGenerationConfig.java index d52733634..9ea48a2ac 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/both/IDhApiWorldGenerationConfig.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/both/IDhApiWorldGenerationConfig.java @@ -19,7 +19,6 @@ package com.seibel.distanthorizons.api.interfaces.config.both; -import com.seibel.distanthorizons.api.enums.config.ELightGenerationMode; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorMode; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigGroup; diff --git a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiAmbientOcclusionConfig.java b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiAmbientOcclusionConfig.java index d20bac76e..8be9af11a 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiAmbientOcclusionConfig.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiAmbientOcclusionConfig.java @@ -19,9 +19,6 @@ package com.seibel.distanthorizons.api.interfaces.config.client; -import com.seibel.distanthorizons.api.enums.rendering.EFogColorMode; -import com.seibel.distanthorizons.api.enums.rendering.EFogDistance; -import com.seibel.distanthorizons.api.enums.rendering.EFogDrawMode; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigGroup; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; diff --git a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiDebuggingConfig.java b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiDebuggingConfig.java index 18f2aea84..ad45b6d61 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiDebuggingConfig.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiDebuggingConfig.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.api.interfaces.config.client; -import com.seibel.distanthorizons.api.enums.rendering.EDebugRendering; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigGroup; @@ -33,7 +33,7 @@ import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigGroup; public interface IDhApiDebuggingConfig extends IDhApiConfigGroup { /** Can be used to debug the standard fake chunk rendering. */ - IDhApiConfigValue debugRendering(); + IDhApiConfigValue debugRendering(); /** If enabled debug keybindings can be used. */ IDhApiConfigValue debugKeybindings(); diff --git a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiFarFogConfig.java b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiFarFogConfig.java index 95f0d0a3d..e50e76cff 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiFarFogConfig.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiFarFogConfig.java @@ -19,8 +19,7 @@ package com.seibel.distanthorizons.api.interfaces.config.client; -import com.seibel.distanthorizons.api.enums.rendering.EFogFalloff; -import com.seibel.distanthorizons.api.enums.rendering.*; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiFogFalloff; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigGroup; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; @@ -64,7 +63,7 @@ public interface IDhApiFarFogConfig extends IDhApiConfigGroup IDhApiConfigValue farFogMaxThickness(); /** Defines how the fog changes in thickness. */ - IDhApiConfigValue farFogFalloff(); + IDhApiConfigValue farFogFalloff(); /** Defines the fog density. */ IDhApiConfigValue farFogDensity(); diff --git a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiFogConfig.java b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiFogConfig.java index f7ad27c84..3303615b1 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiFogConfig.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiFogConfig.java @@ -19,10 +19,8 @@ package com.seibel.distanthorizons.api.interfaces.config.client; -import com.seibel.distanthorizons.api.enums.rendering.EFogColorMode; -import com.seibel.distanthorizons.api.enums.rendering.EFogDistance; -import com.seibel.distanthorizons.api.enums.rendering.EFogDrawMode; -import com.seibel.distanthorizons.api.enums.rendering.*; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiFogColorMode; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiFogDrawMode; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigGroup; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; @@ -58,14 +56,11 @@ public interface IDhApiFogConfig extends IDhApiConfigGroup // basic fog settings // //====================// - /** Defines at what distance fog is rendered on fake chunks. */ - IDhApiConfigValue distance(); - /** Should be used to enable/disable fog rendering. */ - IDhApiConfigValue drawMode(); + IDhApiConfigValue drawMode(); /** Can be used to enable support with mods that change vanilla MC's fog color. */ - IDhApiConfigValue color(); + IDhApiConfigValue color(); /** * If enabled attempts to disable vanilla MC's fog on real chunks.
diff --git a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiGpuBuffersConfig.java b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiGpuBuffersConfig.java index 56f6febc7..4b0433f7a 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiGpuBuffersConfig.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiGpuBuffersConfig.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.api.interfaces.config.client; -import com.seibel.distanthorizons.api.enums.config.EGpuUploadMethod; +import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigGroup; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; @@ -34,7 +34,7 @@ public interface IDhApiGpuBuffersConfig extends IDhApiConfigGroup { /** Defines how geometry data is uploaded to the GPU. */ - IDhApiConfigValue gpuUploadMethod(); + IDhApiConfigValue gpuUploadMethod(); /** * Defines how long we should wait after uploading one diff --git a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiGraphicsConfig.java b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiGraphicsConfig.java index eae828efc..eaf51d691 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiGraphicsConfig.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiGraphicsConfig.java @@ -20,9 +20,8 @@ package com.seibel.distanthorizons.api.interfaces.config.client; import com.seibel.distanthorizons.api.enums.config.*; -import com.seibel.distanthorizons.api.enums.config.*; -import com.seibel.distanthorizons.api.enums.rendering.ERendererMode; -import com.seibel.distanthorizons.api.enums.rendering.ETransparency; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiTransparency; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigGroup; @@ -68,7 +67,7 @@ public interface IDhApiGraphicsConfig extends IDhApiConfigGroup * * Changing this config also changes {@link IDhApiGraphicsConfig#renderingEnabled()}'s value. */ - IDhApiConfigValue renderingMode(); + IDhApiConfigValue renderingMode(); @@ -77,18 +76,18 @@ public interface IDhApiGraphicsConfig extends IDhApiConfigGroup //==================// /** Defines how detailed fake chunks are in the horizontal direction */ - IDhApiConfigValue maxHorizontalResolution(); + IDhApiConfigValue maxHorizontalResolution(); /** Defines how detailed fake chunks are in the vertical direction */ - IDhApiConfigValue verticalQuality(); + IDhApiConfigValue verticalQuality(); /** Modifies the quadratic function fake chunks use for horizontal quality drop-off. */ - IDhApiConfigValue horizontalQuality(); + IDhApiConfigValue horizontalQuality(); - IDhApiConfigValue transparency(); + IDhApiConfigValue transparency(); /** Defines what blocks won't be rendered as LODs. */ - IDhApiConfigValue blocksToAvoid(); + IDhApiConfigValue blocksToAvoid(); /** * Defines if the color of avoided blocks will color the block below them.
@@ -112,17 +111,6 @@ public interface IDhApiGraphicsConfig extends IDhApiConfigGroup // advanced graphic settings // //===========================// - /** - * Sets the distance used by the near clip plane to reduce - * overdraw.
- * Disabling this reduces holes in the world due to the near clip plane - * being too close to the camera and the terrain not being covered by vanilla terrain. - * - * @deprecated Use {@link IDhApiGraphicsConfig#overdrawPreventionRadius()} instead. - */ - @Deprecated - IDhApiConfigValue overdrawPrevention(); - /** * Sets the radius used by the near clip shader to reduce * overdraw.
@@ -172,7 +160,7 @@ public interface IDhApiGraphicsConfig extends IDhApiConfigGroup * * @since API 1.1.0 */ - IDhApiConfigValue lodShading(); + IDhApiConfigValue lodShading(); /** * Sets whether LODs outside the view frustum culling will diff --git a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiHeightFogConfig.java b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiHeightFogConfig.java index 4e1c98753..754cc01a9 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiHeightFogConfig.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiHeightFogConfig.java @@ -19,9 +19,9 @@ package com.seibel.distanthorizons.api.interfaces.config.client; -import com.seibel.distanthorizons.api.enums.rendering.EFogFalloff; -import com.seibel.distanthorizons.api.enums.rendering.EHeightFogMixMode; -import com.seibel.distanthorizons.api.enums.rendering.EHeightFogMode; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiFogFalloff; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiHeightFogMixMode; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiHeightFogMode; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigGroup; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; @@ -40,10 +40,10 @@ public interface IDhApiHeightFogConfig extends IDhApiConfigGroup { /** Defines how the height fog mixes. */ - IDhApiConfigValue heightFogMixMode(); + IDhApiConfigValue heightFogMixMode(); /** Defines how the height fog is drawn relative to the camera or world. */ - IDhApiConfigValue heightFogMode(); + IDhApiConfigValue heightFogMode(); /** * Defines the height fog's base height if {@link IDhApiHeightFogConfig#heightFogMode()} @@ -64,7 +64,7 @@ public interface IDhApiHeightFogConfig extends IDhApiConfigGroup IDhApiConfigValue heightFogMaxThickness(); /** Defines how the height fog changes in thickness. */ - IDhApiConfigValue heightFogFalloff(); + IDhApiConfigValue heightFogFalloff(); /** Defines the height fog's density. */ IDhApiConfigValue heightFogDensity(); diff --git a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiMultiplayerConfig.java b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiMultiplayerConfig.java index 39edf49a5..20397f392 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiMultiplayerConfig.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/config/client/IDhApiMultiplayerConfig.java @@ -20,7 +20,7 @@ package com.seibel.distanthorizons.api.interfaces.config.client; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; -import com.seibel.distanthorizons.api.enums.config.EServerFolderNameMode; +import com.seibel.distanthorizons.api.enums.config.EDhApiServerFolderNameMode; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigGroup; /** @@ -37,7 +37,7 @@ public interface IDhApiMultiplayerConfig extends IDhApiConfigGroup * Defines how multiplayer server folders are named.
* Note: Changing this while connected to a multiplayer world will cause undefined behavior! */ - IDhApiConfigValue folderSavingMode(); + IDhApiConfigValue folderSavingMode(); /** * Defines the necessary similarity (as a percent) that two potential levels diff --git a/api/src/main/java/com/seibel/distanthorizons/api/methods/events/abstractEvents/DhApiScreenResizeEvent.java b/api/src/main/java/com/seibel/distanthorizons/api/methods/events/abstractEvents/DhApiColorDepthTextureCreatedEvent.java similarity index 83% rename from api/src/main/java/com/seibel/distanthorizons/api/methods/events/abstractEvents/DhApiScreenResizeEvent.java rename to api/src/main/java/com/seibel/distanthorizons/api/methods/events/abstractEvents/DhApiColorDepthTextureCreatedEvent.java index 387981aad..c67cd547d 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/methods/events/abstractEvents/DhApiScreenResizeEvent.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/methods/events/abstractEvents/DhApiColorDepthTextureCreatedEvent.java @@ -23,16 +23,16 @@ import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEvent; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiEventParam; /** - * Called before Distant Horizons starts rendering a buffer.
- * This event cannot be cancelled, use {@link DhApiBeforeRenderEvent} if you want to cancel rendering. + * Called whenever Distant Horizons (re)creates + * the color and depth textures it renders to.
* * @author James Seibel - * @version 2023-1-23 + * @version 2024-3-2 * @since API 1.1.0 */ -public abstract class DhApiScreenResizeEvent implements IDhApiEvent +public abstract class DhApiColorDepthTextureCreatedEvent implements IDhApiEvent { - /** Fired immediately before Distant Horizons renders any transparent buffers. */ + /** Fired before Distant Horizons creates. */ public abstract void onResize(DhApiEventParam event); diff --git a/api/src/main/java/com/seibel/distanthorizons/api/objects/data/DhApiTerrainDataPoint.java b/api/src/main/java/com/seibel/distanthorizons/api/objects/data/DhApiTerrainDataPoint.java index d78ca58e2..0c70dfbfb 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/objects/data/DhApiTerrainDataPoint.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/objects/data/DhApiTerrainDataPoint.java @@ -40,7 +40,8 @@ public class DhApiTerrainDataPoint */ public final byte detailLevel; - public final int lightLevel; + public final int blockLightLevel; + public final int skyLightLevel; public final int topYBlockPos; public final int bottomYBlockPos; @@ -49,11 +50,12 @@ public class DhApiTerrainDataPoint - public DhApiTerrainDataPoint(byte detailLevel, int lightLevel, int topYBlockPos, int bottomYBlockPos, IDhApiBlockStateWrapper blockStateWrapper, IDhApiBiomeWrapper biomeWrapper) + public DhApiTerrainDataPoint(byte detailLevel, int blockLightLevel, int skyLightLevel, int topYBlockPos, int bottomYBlockPos, IDhApiBlockStateWrapper blockStateWrapper, IDhApiBiomeWrapper biomeWrapper) { this.detailLevel = detailLevel; - this.lightLevel = lightLevel; + this.blockLightLevel = blockLightLevel; + this.skyLightLevel = skyLightLevel; this.topYBlockPos = topYBlockPos; this.bottomYBlockPos = bottomYBlockPos; diff --git a/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java b/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java index 6665b77be..723187c4e 100644 --- a/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java +++ b/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java @@ -34,7 +34,7 @@ public final class ModInfo public static final String NAME = "DistantHorizons"; /** Human-readable version of NAME */ public static final String READABLE_NAME = "Distant Horizons"; - public static final String VERSION = "2.0.2-a-dev"; + public static final String VERSION = "2.0.3-a-dev"; /** Returns true if the current build is an unstable developer build, false otherwise. */ public static boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev"); diff --git a/api/src/main/java/com/seibel/distanthorizons/coreapi/util/converters/RenderModeEnabledConverter.java b/api/src/main/java/com/seibel/distanthorizons/coreapi/util/converters/RenderModeEnabledConverter.java index 2bb47acfe..207d3c8b0 100644 --- a/api/src/main/java/com/seibel/distanthorizons/coreapi/util/converters/RenderModeEnabledConverter.java +++ b/api/src/main/java/com/seibel/distanthorizons/coreapi/util/converters/RenderModeEnabledConverter.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.coreapi.util.converters; -import com.seibel.distanthorizons.api.enums.rendering.ERendererMode; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode; import com.seibel.distanthorizons.coreapi.interfaces.config.IConverter; /** @@ -28,17 +28,17 @@ import com.seibel.distanthorizons.coreapi.interfaces.config.IConverter; * @author James Seibel * @version 2022-6-30 */ -public class RenderModeEnabledConverter implements IConverter +public class RenderModeEnabledConverter implements IConverter { - @Override public ERendererMode convertToCoreType(Boolean renderingEnabled) + @Override public EDhApiRendererMode convertToCoreType(Boolean renderingEnabled) { - return renderingEnabled ? ERendererMode.DEFAULT : ERendererMode.DISABLED; + return renderingEnabled ? EDhApiRendererMode.DEFAULT : EDhApiRendererMode.DISABLED; } - @Override public Boolean convertToApiType(ERendererMode renderingMode) + @Override public Boolean convertToApiType(EDhApiRendererMode renderingMode) { - return renderingMode == ERendererMode.DEFAULT; + return renderingMode == EDhApiRendererMode.DEFAULT; } } diff --git a/core/src/test/java/tests/ExampleTest.java b/api/src/test/java/tests/ExampleApiTest.java similarity index 97% rename from core/src/test/java/tests/ExampleTest.java rename to api/src/test/java/tests/ExampleApiTest.java index 63e1ecf89..ebf7c52a1 100644 --- a/core/src/test/java/tests/ExampleTest.java +++ b/api/src/test/java/tests/ExampleApiTest.java @@ -28,7 +28,7 @@ import org.junit.Test; * @author James Seibel * @version 2022-9-5 */ -public class ExampleTest +public class ExampleApiTest { @Test diff --git a/core/build.gradle b/core/build.gradle new file mode 100644 index 000000000..172f997d8 --- /dev/null +++ b/core/build.gradle @@ -0,0 +1,122 @@ +plugins { + id "java" + id "com.github.johnrengelman.shadow" version '7.1.2' apply false // Set this to true if you're using the standalone Core project +} + +apply plugin: "application" + +application { + mainClass.set("com.seibel.distanthorizons.core.jar.JarMain") +} + +configurations { + shadowedArtifact // Used by DH to specify that we want to implement the shadowed core JAR file instead of the regular JAR file + shade + implementation.extendsFrom shade +} + +OperatingSystem os = org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.currentOperatingSystem; + +// Set the OS lwjgl is using to the current os +project.ext.lwjglNatives = "natives-" + os.toFamilyName() + +dependencies { // All of these dependencies are in Vanilla Minecraft, but we need to depend on it as we arent importing Minecraft in the core + // Imports most of lwjgl's libraries (well, only the ones that we need) + implementation platform("org.lwjgl:lwjgl-bom:${rootProject.lwjgl_version}") // TODO: Use Minecraft's version for lwjgl_version (which changes in nearly every version) instead of a hard defined version for all versions + + // REMEMBER: Dont shadow stuff here, these are just the libs that are included in Minecraft so that the core can use + implementation "org.lwjgl:lwjgl" + implementation "org.lwjgl:lwjgl-assimp" + implementation "org.lwjgl:lwjgl-glfw" + implementation "org.lwjgl:lwjgl-openal" + implementation "org.lwjgl:lwjgl-opengl" + implementation "org.lwjgl:lwjgl-stb" + implementation "org.lwjgl:lwjgl-tinyfd" + runtimeOnly "org.lwjgl:lwjgl::$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-assimp::$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-glfw::$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-openal::$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-opengl::$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-stb::$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-tinyfd::$lwjglNatives" + + + // fast util + shade("it.unimi.dsi:fastutil:${rootProject.fastutil_version}") + + // Compression + shade("org.lz4:lz4-java:${rootProject.lz4_version}") // LZ4 + shade("com.github.luben:zstd-jni:${rootProject.zstd_version}") // Zstd + shade("org.tukaani:xz:${rootProject.xz_version}") // LZMA + + // Sqlite Database + shade("org.xerial:sqlite-jdbc:${rootProject.sqlite_jdbc_version}") + + // Netty + shade("io.netty:netty-all:${rootProject.netty_version}") + + // NightConfig (includes Toml & Json) + // needed in both common and core + shade("com.electronwill.night-config:toml:${rootProject.nightconfig_version}") + shade("com.electronwill.night-config:json:${rootProject.nightconfig_version}") + + + // needed for the standalone jar + shade("org.apache.logging.log4j:log4j-core:2.23.1") + shade("org.apache.logging.log4j:log4j-api:2.23.1") + + // SVG (not needed atm) + //shade("com.formdev:svgSalamander:${rootProject.svgSalamander_version}") + + + + + // Some other dependencies + implementation("org.jetbrains:annotations:16.0.2") + implementation("com.google.code.findbugs:jsr305:3.0.2") + implementation("com.google.common:google-collect:0.5") + implementation("com.google.guava:guava:31.1-jre") + +} + +artifacts { + shade shadowJar + shadowedArtifact shadowJar // Setup the configuration shadowedArtifact to be the shadowJar +} + +shadowJar { + configurations = [project.configurations.shade] + + def librariesLocation = "distanthorizons.libraries" + + + relocate "it.unimi.dsi.fastutil", "${librariesLocation}.unimi.dsi.fastutil" + + // LWJGL + // Only ever shadow the dependencies we use otherwise some stuff would break when running on an external client + relocate "org.lwjgl.system.jawt", "${librariesLocation}.lwjgl.system.jawt" + + // Compression + relocate "net.jpountz", "${librariesLocation}.jpountz" + relocate "com.github.luben", "${librariesLocation}.github.luben" + relocate "org.tukaani", "${librariesLocation}.tukaani" + + // Sqlite Database + //At the moment, there is a bug in this library which doesnt allow it to be relocated +// relocate "org.sqlite", "${librariesLocation}.sqlite" + + // JOML + if (project.hasProperty("embed_joml") && embed_joml == "true") + relocate "org.joml", "${librariesLocation}.joml" + + // NightConfig (includes Toml & Json) + relocate "com.electronwill.nightconfig", "${librariesLocation}.electronwill.nightconfig" + + // Netty + relocate "io.netty", "${librariesLocation}.netty" + + relocate "org.apache.logging", "${librariesLocation}.apache.logging" + + + mergeServiceFiles() +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/Initializer.java b/core/src/main/java/com/seibel/distanthorizons/core/Initializer.java index e74d7bedd..b84c721ef 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/Initializer.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/Initializer.java @@ -26,17 +26,14 @@ import com.seibel.distanthorizons.coreapi.ModInfo; 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.data.DhApiTerrainDataRepo; -import com.seibel.distanthorizons.core.dataObjects.fullData.loader.CompleteFullDataSourceLoader; -import com.seibel.distanthorizons.core.dataObjects.fullData.loader.HighDetailIncompleteFullDataSourceLoader; import com.seibel.distanthorizons.api.DhApi; -import com.seibel.distanthorizons.core.dataObjects.fullData.loader.LowDetailIncompleteFullDataSourceLoader; import com.seibel.distanthorizons.core.render.DhApiRenderProxy; -//import io.netty.buffer.ByteBuf; -import net.jpountz.lz4.LZ4Compressor; +import io.netty.buffer.ByteBuf; +import net.jpountz.lz4.LZ4FrameOutputStream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.io.InputStream; +import java.awt.*; /** Handles first time Core setup. */ public class Initializer @@ -50,11 +47,11 @@ public class Initializer { // if any library isn't present in the jar its class // will throw an error (not an exception) - Class compressor = LZ4Compressor.class; - //Class networking = ByteBuf.class; + Class compressor = LZ4FrameOutputStream.class; + Class networking = ByteBuf.class; Class toml = com.electronwill.nightconfig.core.Config.class; } - catch (NoClassDefFoundError e) + catch (Throwable e) { LOGGER.fatal("Critical programmer error: One or more libraries aren't present. Error: [" + e.getMessage() + "]."); throw e; @@ -76,10 +73,16 @@ public class Initializer } - - CompleteFullDataSourceLoader unused2 = new CompleteFullDataSourceLoader(); // Auto register into the loader system - HighDetailIncompleteFullDataSourceLoader unused3 = new HighDetailIncompleteFullDataSourceLoader(); // Auto register - LowDetailIncompleteFullDataSourceLoader unused4 = new LowDetailIncompleteFullDataSourceLoader(); // Auto register + // attempt to setup Swing so we can display dialogs (popup windows) + System.setProperty("java.awt.headless", "false"); + if (GraphicsEnvironment.isHeadless()) + { + LOGGER.warn("Java.awt.headless is false. This means Distant Horizons can't 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 DhApi.Delayed.configs = DhApiConfig.INSTANCE; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiDebuggingConfig.java b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiDebuggingConfig.java index e34b964b9..925dfc3da 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiDebuggingConfig.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiDebuggingConfig.java @@ -23,7 +23,7 @@ import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiDebuggingConfig; import com.seibel.distanthorizons.api.objects.config.DhApiConfigValue; import com.seibel.distanthorizons.core.config.Config; -import com.seibel.distanthorizons.api.enums.rendering.EDebugRendering; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering; public class DhApiDebuggingConfig implements IDhApiDebuggingConfig { @@ -33,8 +33,8 @@ public class DhApiDebuggingConfig implements IDhApiDebuggingConfig - public IDhApiConfigValue debugRendering() - { return new DhApiConfigValue(Config.Client.Advanced.Debugging.debugRendering); } + public IDhApiConfigValue debugRendering() + { return new DhApiConfigValue(Config.Client.Advanced.Debugging.debugRendering); } public IDhApiConfigValue debugKeybindings() { return new DhApiConfigValue(Config.Client.Advanced.Debugging.enableDebugKeybindings); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiFarFogConfig.java b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiFarFogConfig.java index 1bacc727c..0393ccbdb 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiFarFogConfig.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiFarFogConfig.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.core.api.external.methods.config.client; -import com.seibel.distanthorizons.api.enums.rendering.EFogFalloff; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiFogFalloff; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiFarFogConfig; import com.seibel.distanthorizons.api.objects.config.DhApiConfigValue; @@ -50,8 +50,8 @@ public class DhApiFarFogConfig implements IDhApiFarFogConfig { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogMax); } @Override - public IDhApiConfigValue farFogFalloff() - { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogFalloff); } + public IDhApiConfigValue farFogFalloff() + { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogFalloff); } @Override public IDhApiConfigValue farFogDensity() diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiFogConfig.java b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiFogConfig.java index 9889723f2..cc89c53de 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiFogConfig.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiFogConfig.java @@ -19,9 +19,8 @@ package com.seibel.distanthorizons.core.api.external.methods.config.client; -import com.seibel.distanthorizons.api.enums.rendering.EFogColorMode; -import com.seibel.distanthorizons.api.enums.rendering.EFogDistance; -import com.seibel.distanthorizons.api.enums.rendering.EFogDrawMode; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiFogColorMode; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiFogDrawMode; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiFarFogConfig; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiFogConfig; @@ -53,15 +52,11 @@ public class DhApiFogConfig implements IDhApiFogConfig //====================// @Override - public IDhApiConfigValue distance() - { return new DhApiConfigValue<>(Config.Client.Advanced.Graphics.Fog.distance); } - - @Override - public IDhApiConfigValue drawMode() + public IDhApiConfigValue drawMode() { return new DhApiConfigValue<>(Config.Client.Advanced.Graphics.Fog.drawMode); } @Override - public IDhApiConfigValue color() + public IDhApiConfigValue color() { return new DhApiConfigValue<>(Config.Client.Advanced.Graphics.Fog.colorMode); } @Override diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiGpuBuffersConfig.java b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiGpuBuffersConfig.java index 7069e5df4..22f4852b0 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiGpuBuffersConfig.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiGpuBuffersConfig.java @@ -23,7 +23,7 @@ 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.EGpuUploadMethod; +import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod; public class DhApiGpuBuffersConfig implements IDhApiGpuBuffersConfig { @@ -33,7 +33,7 @@ public class DhApiGpuBuffersConfig implements IDhApiGpuBuffersConfig - public IDhApiConfigValue gpuUploadMethod() + public IDhApiConfigValue gpuUploadMethod() { return new DhApiConfigValue<>(Config.Client.Advanced.GpuBuffers.gpuUploadMethod); } public IDhApiConfigValue gpuUploadPerMegabyteInMilliseconds() diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiGraphicsConfig.java b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiGraphicsConfig.java index 63b810521..27c1d48eb 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiGraphicsConfig.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiGraphicsConfig.java @@ -20,14 +20,14 @@ package com.seibel.distanthorizons.core.api.external.methods.config.client; import com.seibel.distanthorizons.api.enums.config.*; -import com.seibel.distanthorizons.api.enums.rendering.ETransparency; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiTransparency; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiAmbientOcclusionConfig; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiFogConfig; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiGraphicsConfig; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiNoiseTextureConfig; import com.seibel.distanthorizons.api.objects.config.DhApiConfigValue; -import com.seibel.distanthorizons.api.enums.rendering.ERendererMode; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode; import com.seibel.distanthorizons.core.config.Config; public class DhApiGraphicsConfig implements IDhApiGraphicsConfig @@ -61,8 +61,8 @@ public class DhApiGraphicsConfig implements IDhApiGraphicsConfig { return new DhApiConfigValue(Config.Client.quickEnableRendering); } @Override - public IDhApiConfigValue renderingMode() - { return new DhApiConfigValue(Config.Client.Advanced.Debugging.rendererMode); } + public IDhApiConfigValue renderingMode() + { return new DhApiConfigValue(Config.Client.Advanced.Debugging.rendererMode); } @@ -71,24 +71,24 @@ public class DhApiGraphicsConfig implements IDhApiGraphicsConfig //==================// @Override - public IDhApiConfigValue maxHorizontalResolution() - { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Quality.maxHorizontalResolution); } + public IDhApiConfigValue maxHorizontalResolution() + { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Quality.maxHorizontalResolution); } @Override - public IDhApiConfigValue verticalQuality() - { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Quality.verticalQuality); } + public IDhApiConfigValue verticalQuality() + { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Quality.verticalQuality); } @Override - public IDhApiConfigValue horizontalQuality() - { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Quality.horizontalQuality); } + public IDhApiConfigValue horizontalQuality() + { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Quality.horizontalQuality); } @Override - public IDhApiConfigValue transparency() - { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Quality.transparency); } + public IDhApiConfigValue transparency() + { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Quality.transparency); } @Override - public IDhApiConfigValue blocksToAvoid() - { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Quality.blocksToIgnore); } + public IDhApiConfigValue blocksToAvoid() + { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Quality.blocksToIgnore); } @Override public IDhApiConfigValue tintWithAvoidedBlocks() @@ -105,11 +105,6 @@ public class DhApiGraphicsConfig implements IDhApiGraphicsConfig // advanced graphic settings // //===========================// - @Deprecated - @Override - public IDhApiConfigValue overdrawPrevention() - { return new DhApiConfigValue(Config.Client.Advanced.Graphics.AdvancedGraphics.overdrawPreventionPreset); } - @Override public IDhApiConfigValue overdrawPreventionRadius() { return new DhApiConfigValue(Config.Client.Advanced.Graphics.AdvancedGraphics.overdrawPrevention); } @@ -143,8 +138,8 @@ public class DhApiGraphicsConfig implements IDhApiGraphicsConfig { return new DhApiConfigValue(Config.Client.Advanced.Graphics.AdvancedGraphics.lodBias); } @Override - public IDhApiConfigValue lodShading() - { return new DhApiConfigValue(Config.Client.Advanced.Graphics.AdvancedGraphics.lodShading); } + public IDhApiConfigValue lodShading() + { return new DhApiConfigValue(Config.Client.Advanced.Graphics.AdvancedGraphics.lodShading); } @Override public IDhApiConfigValue disableFrustumCulling() diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiHeightFogConfig.java b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiHeightFogConfig.java index 866137bc0..ecd1090a2 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiHeightFogConfig.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiHeightFogConfig.java @@ -19,9 +19,9 @@ package com.seibel.distanthorizons.core.api.external.methods.config.client; -import com.seibel.distanthorizons.api.enums.rendering.EFogFalloff; -import com.seibel.distanthorizons.api.enums.rendering.EHeightFogMixMode; -import com.seibel.distanthorizons.api.enums.rendering.EHeightFogMode; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiFogFalloff; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiHeightFogMixMode; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiHeightFogMode; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiHeightFogConfig; import com.seibel.distanthorizons.api.objects.config.DhApiConfigValue; @@ -36,12 +36,12 @@ public class DhApiHeightFogConfig implements IDhApiHeightFogConfig @Override - public IDhApiConfigValue heightFogMixMode() - { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMixMode); } + public IDhApiConfigValue heightFogMixMode() + { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMixMode); } @Override - public IDhApiConfigValue heightFogMode() - { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMode); } + public IDhApiConfigValue heightFogMode() + { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMode); } @Override public IDhApiConfigValue heightFogBaseHeight() @@ -64,8 +64,8 @@ public class DhApiHeightFogConfig implements IDhApiHeightFogConfig { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMax); } @Override - public IDhApiConfigValue heightFogFalloff() - { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogFalloff); } + public IDhApiConfigValue heightFogFalloff() + { return new DhApiConfigValue(Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogFalloff); } @Override public IDhApiConfigValue heightFogDensity() diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiMultiplayerConfig.java b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiMultiplayerConfig.java index e09d70244..1c16671d0 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiMultiplayerConfig.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiMultiplayerConfig.java @@ -23,7 +23,7 @@ import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiMultiplayerConfig; import com.seibel.distanthorizons.api.objects.config.DhApiConfigValue; import com.seibel.distanthorizons.core.config.Config; -import com.seibel.distanthorizons.api.enums.config.EServerFolderNameMode; +import com.seibel.distanthorizons.api.enums.config.EDhApiServerFolderNameMode; public class DhApiMultiplayerConfig implements IDhApiMultiplayerConfig { @@ -33,8 +33,8 @@ public class DhApiMultiplayerConfig implements IDhApiMultiplayerConfig - public IDhApiConfigValue folderSavingMode() - { return new DhApiConfigValue(Config.Client.Advanced.Multiplayer.serverFolderNameMode); } + public IDhApiConfigValue folderSavingMode() + { return new DhApiConfigValue(Config.Client.Advanced.Multiplayer.serverFolderNameMode); } public IDhApiConfigValue multiverseSimilarityRequirement() { return new DhApiConfigValue(Config.Client.Advanced.Multiplayer.multiverseSimilarityRequiredPercent); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/common/DhApiWorldGenerationConfig.java b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/common/DhApiWorldGenerationConfig.java index 121909395..547f395b1 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/common/DhApiWorldGenerationConfig.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/common/DhApiWorldGenerationConfig.java @@ -19,7 +19,6 @@ package com.seibel.distanthorizons.core.api.external.methods.config.common; -import com.seibel.distanthorizons.api.enums.config.ELightGenerationMode; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.both.IDhApiWorldGenerationConfig; import com.seibel.distanthorizons.api.objects.config.DhApiConfigValue; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/data/DhApiTerrainDataRepo.java b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/data/DhApiTerrainDataRepo.java index e2d2c8d66..e3f4f5210 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/data/DhApiTerrainDataRepo.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/data/DhApiTerrainDataRepo.java @@ -27,8 +27,7 @@ import com.seibel.distanthorizons.api.interfaces.data.IDhApiTerrainDataRepo; import com.seibel.distanthorizons.api.objects.math.DhApiVec3i; import com.seibel.distanthorizons.core.api.internal.SharedApi; import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.SingleColumnFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.pos.DhLodPos; @@ -47,6 +46,7 @@ import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; import com.seibel.distanthorizons.coreapi.util.math.Vec3d; import com.seibel.distanthorizons.coreapi.util.math.Vec3f; import com.seibel.distanthorizons.coreapi.util.math.Vec3i; +import it.unimi.dsi.fastutil.longs.LongArrayList; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -212,7 +212,7 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo try { // attempt to get/generate the data source for this section - IFullDataSource dataSource = level.getFileHandler().getAsync(sectionPos).get(); + FullDataSourceV2 dataSource = level.getFullDataProvider().getAsync(sectionPos).get(); if (dataSource == null) { return DhApiResult.createFail("Unable to find/generate any data at the " + DhSectionPos.class.getSimpleName() + " [" + sectionPos + "]."); @@ -220,11 +220,11 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo else { // attempt to get the LOD data from the data source - FullDataPointIdMap mapping = dataSource.getMapping(); - SingleColumnFullDataAccessor dataColumn = dataSource.tryGet(relativePos.x, relativePos.z); + FullDataPointIdMap mapping = dataSource.mapping; + LongArrayList dataColumn = dataSource.get(relativePos.x, relativePos.z); if (dataColumn != null) { - int dataColumnIndexCount = dataColumn.getSingleLength(); + int dataColumnIndexCount = dataColumn.size(); DhApiTerrainDataPoint[] returnArray = new DhApiTerrainDataPoint[dataColumnIndexCount]; long dataPoint; @@ -235,7 +235,7 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo // search for a datapoint that contains the block y position for (int i = 0; i < dataColumnIndexCount; i++) { - dataPoint = dataColumn.getSingle(i); + dataPoint = dataColumn.getLong(i); if (!getSpecificYCoordinate) { @@ -290,7 +290,8 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo int topY = bottomY + height; return new DhApiTerrainDataPoint(detailLevel, - FullDataPointUtil.getLight(dataPoint), topY, bottomY, + FullDataPointUtil.getBlockLight(dataPoint), FullDataPointUtil.getSkyLight(dataPoint), + topY, bottomY, blockState, biomeWrapper); } @@ -460,7 +461,7 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo // this will throw a cast exception if the chunk object array isn't correct IChunkWrapper chunk = SingletonInjector.INSTANCE.get(IWrapperFactory.class).createChunkWrapper(chunkObjectArray); - dhLevel.updateChunkAsync(chunk); + SharedApi.INSTANCE.applyChunkUpdate(chunk, dhLevel.getLevelWrapper(), true); return DhApiResult.createSuccess(); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java index 4a81bd1c1..c29fcfeef 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java @@ -21,7 +21,6 @@ package com.seibel.distanthorizons.core.api.internal; import com.seibel.distanthorizons.api.DhApi; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass; -import com.seibel.distanthorizons.api.interfaces.override.rendering.IDhApiFramebuffer; import com.seibel.distanthorizons.api.methods.events.abstractEvents.*; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager; @@ -32,10 +31,9 @@ import com.seibel.distanthorizons.core.world.*; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.config.Config; -import com.seibel.distanthorizons.coreapi.DependencyInjection.OverrideInjector; import com.seibel.distanthorizons.coreapi.ModInfo; -import com.seibel.distanthorizons.api.enums.rendering.EDebugRendering; -import com.seibel.distanthorizons.api.enums.rendering.ERendererMode; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.ConfigBasedSpamLogger; @@ -54,7 +52,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; -import org.lwjgl.opengl.GL32; import java.util.HashMap; import java.util.HashSet; @@ -81,6 +78,10 @@ public class ClientApi public static final long SPAM_LOGGER_FLUSH_NS = TimeUnit.NANOSECONDS.convert(1, TimeUnit.SECONDS); private boolean configOverrideReminderPrinted = false; + + private boolean migrationMessageShown = false; + private boolean showMigrationMessageNextFrame = false; + public boolean rendererDisabledBecauseOfExceptions = false; private long lastFlushNanoTime = 0; @@ -179,7 +180,7 @@ public class ClientApi // can happen on certain multiverse servers return; } - LOGGER.info("Unloading client level [" + level + "]."); + LOGGER.info("Unloading client level [" + level + "]-["+level.getDimensionType().getDimensionName()+"]."); AbstractDhWorld world = SharedApi.getAbstractDhWorld(); if (world != null) @@ -218,7 +219,7 @@ public class ClientApi - LOGGER.info("Loading " + (isServerCommunication ? "Multiverse" : "") + " client level [" + level + "]."); + LOGGER.info("Loading " + (isServerCommunication ? "Multiverse" : "") + " client level [" + level + "]-["+level.getDimensionType().getDimensionName()+"]."); AbstractDhWorld world = SharedApi.getAbstractDhWorld(); if (world != null) @@ -473,14 +474,29 @@ public class ClientApi { // logging // + // dev build if (ModInfo.IS_DEV_BUILD && !this.configOverrideReminderPrinted && MC.playerExists()) { this.configOverrideReminderPrinted = true; // remind the user that this is a development build - MC.sendChatMessage(ModInfo.READABLE_NAME + " experimental build " + ModInfo.VERSION); + MC.sendChatMessage("Distant Horizons nightly experimental build version [" + ModInfo.VERSION+"]."); MC.sendChatMessage("You are running an unsupported version of Distant Horizons!"); MC.sendChatMessage("Here be dragons!"); + MC.sendChatMessage(""); + } + + // data migration + if (this.showMigrationMessageNextFrame + && !this.migrationMessageShown + && Config.Client.Advanced.LodBuilding.showMigrationChatWarning.get()) + { + this.showMigrationMessageNextFrame = false; + this.migrationMessageShown = true; + + MC.sendChatMessage("Old Distant Horizons data is being migrated."); + MC.sendChatMessage("During migration LODs may load slowly and DH world gen is disabled."); + MC.sendChatMessage(""); } IProfilerWrapper profiler = MC.getProfiler(); @@ -547,7 +563,7 @@ public class ClientApi if (!renderingDeferredLayer) { - if (Config.Client.Advanced.Debugging.rendererMode.get() == ERendererMode.DEFAULT) + if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT) { this.renderingCancelledForThisFrame = ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeRenderEvent.class, renderEventParam); if (!this.renderingCancelledForThisFrame) @@ -560,7 +576,7 @@ public class ClientApi ApiEventInjector.INSTANCE.fireAllEvents(DhApiAfterRenderEvent.class, renderEventParam); } } - else if (Config.Client.Advanced.Debugging.rendererMode.get() == ERendererMode.DEBUG) + else if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEBUG) { profiler.push("Render Debug"); ClientApi.testRenderer.render(); @@ -617,12 +633,12 @@ public class ClientApi if (glfwKey == GLFW.GLFW_KEY_F8) { - Config.Client.Advanced.Debugging.debugRendering.set(EDebugRendering.next(Config.Client.Advanced.Debugging.debugRendering.get())); + Config.Client.Advanced.Debugging.debugRendering.set(EDhApiDebugRendering.next(Config.Client.Advanced.Debugging.debugRendering.get())); MC.sendChatMessage("F8: Set debug mode to " + Config.Client.Advanced.Debugging.debugRendering.get()); } else if (glfwKey == GLFW.GLFW_KEY_F6) { - Config.Client.Advanced.Debugging.rendererMode.set(ERendererMode.next(Config.Client.Advanced.Debugging.rendererMode.get())); + Config.Client.Advanced.Debugging.rendererMode.set(EDhApiRendererMode.next(Config.Client.Advanced.Debugging.rendererMode.get())); MC.sendChatMessage("F6: Set rendering to " + Config.Client.Advanced.Debugging.rendererMode.get()); } else if (glfwKey == GLFW.GLFW_KEY_P) @@ -632,5 +648,7 @@ public class ClientApi } } + // TODO there's probably a better way of handling chat messages + public void showMigrationMessageOnNextFrame() { this.showMigrationMessageNextFrame = true; } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ServerApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ServerApi.java index d85dd8639..0d1e97862 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ServerApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ServerApi.java @@ -38,11 +38,6 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import org.apache.logging.log4j.Logger; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.ThreadPoolExecutor; - /** * This holds the methods that should be called by the host mod loader (Fabric, * Forge, etc.). Specifically server events. diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java index b0499375d..c8ffe6de8 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java @@ -32,7 +32,7 @@ import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.TimerUtil; import com.seibel.distanthorizons.core.util.objects.Pair; -import com.seibel.distanthorizons.core.util.threading.ThreadPools; +import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.world.*; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; @@ -46,6 +46,7 @@ import java.util.Set; import java.util.Timer; import java.util.TimerTask; 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} */ @@ -102,11 +103,11 @@ public class SharedApi // access the MC level at inappropriate times, which can cause exceptions if (currentWorld != null) { - ThreadPools.setupThreadPools(); + ThreadPoolUtil.setupThreadPools(); } else { - ThreadPools.shutdownThreadPools(); + ThreadPoolUtil.shutdownThreadPools(); DebugRenderer.clearRenderables(); MC_RENDER.clearTargetFrameBuffer(); @@ -285,77 +286,81 @@ public class SharedApi private static void bakeChunkLightingAndSendToLevelAsync(IChunkWrapper chunkWrapper, @Nullable ArrayList neighbourChunkList, IDhLevel dhLevel) { // lighting the chunk needs to be done on a separate thread to prevent lagging any of the event threads - ThreadPoolExecutor executor = ThreadPools.getLightPopulatorExecutor(); + ThreadPoolExecutor executor = ThreadPoolUtil.getLightPopulatorExecutor(); if (executor == null) { return; } - executor.execute(() -> + try { - LOGGER.trace(chunkWrapper.getChunkPos() + " " + executor.getActiveCount() + " / " + executor.getQueue().size() + " - " + executor.getCompletedTaskCount()); - - try + executor.execute(() -> { - // Save or populate the chunk wrapper's lighting - // this is done so we don't have to worry about MC unloading the lighting data for this chunk - boolean onlyUseDhLighting = Config.Client.Advanced.LodBuilding.onlyUseDhLightingEngine.get(); - if (!onlyUseDhLighting && chunkWrapper.isLightCorrect()) + //LOGGER.trace(chunkWrapper.getChunkPos() + " " + executor.getActiveCount() + " / " + executor.getQueue().size() + " - " + executor.getCompletedTaskCount()); + + try { - try + // Save or populate the chunk wrapper's lighting + // this is done so we don't have to worry about MC unloading the lighting data for this chunk + 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 - chunkWrapper.bakeDhLightingUsingMcLightingEngine(); - } - catch (IllegalStateException e) - { - LOGGER.warn("Chunk light baking error: " + e.getMessage(), e); - } - } - else - { - // generate the chunk's lighting, using neighboring chunks if present - - ArrayList nearbyChunkList; - if (neighbourChunkList != null) - { - nearbyChunkList = neighbourChunkList; + try + { + // If MC's lighting engine isn't thread safe this may cause the server thread to lag + chunkWrapper.bakeDhLightingUsingMcLightingEngine(); + } + catch (IllegalStateException e) + { + LOGGER.warn("Chunk light baking error: " + e.getMessage(), e); + } } else { - nearbyChunkList = new ArrayList<>(1); - nearbyChunkList.add(chunkWrapper); + // generate the chunk's lighting, using neighboring chunks if present + + ArrayList nearbyChunkList; + if (neighbourChunkList != null) + { + nearbyChunkList = neighbourChunkList; + } + else + { + nearbyChunkList = new ArrayList<>(1); + nearbyChunkList.add(chunkWrapper); + } + + DhLightingEngine.INSTANCE.lightChunk(chunkWrapper, nearbyChunkList, dhLevel.hasSkyLight() ? 15 : 0); } - DhLightingEngine.INSTANCE.lightChunk(chunkWrapper, nearbyChunkList, dhLevel.hasSkyLight() ? 15 : 0); + dhLevel.updateChunkAsync(chunkWrapper); } - - dhLevel.updateChunkAsync(chunkWrapper); - } - catch (Exception e) - { - LOGGER.error("Unexpected error when updating chunk at pos: ["+chunkWrapper.getChunkPos()+"]", e); - } - finally - { - // the LOD chunk has finished being updated - int updateTimeoutInSec = Config.Client.Advanced.LodBuilding.minTimeBetweenChunkUpdatesInSeconds.get(); - if (updateTimeoutInSec != 0) + catch (Exception e) { - // prevent updating this chunk again until the timeout finishes - CHUNK_UPDATE_TIMER.schedule(new TimerTask() + LOGGER.error("Unexpected error when updating chunk at pos: [" + chunkWrapper.getChunkPos() + "]", e); + } + finally + { + // the LOD chunk has finished being updated + int updateTimeoutInSec = Config.Client.Advanced.LodBuilding.minTimeBetweenChunkUpdatesInSeconds.get(); + if (updateTimeoutInSec != 0) { - @Override - public void run() { UPDATING_CHUNK_POS_SET.remove(chunkWrapper.getChunkPos()); } - }, updateTimeoutInSec * 1000L); + // prevent updating this chunk again until the timeout finishes + CHUNK_UPDATE_TIMER.schedule(new TimerTask() + { + @Override + public void run() { UPDATING_CHUNK_POS_SET.remove(chunkWrapper.getChunkPos()); } + }, updateTimeoutInSec * 1000L); + } + else + { + // instantly allow this chunk to be updated again + UPDATING_CHUNK_POS_SET.remove(chunkWrapper.getChunkPos()); + } } - else - { - // instantly allow this chunk to be updated again - UPDATING_CHUNK_POS_SET.remove(chunkWrapper.getChunkPos()); - } - } - }); + }); + } + catch (RejectedExecutionException ignore) { /* the executor was shut down, it should be back up shortly and able to accept new jobs */ } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java b/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java index f81eb8ce9..ac2f0bb33 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java @@ -37,7 +37,9 @@ import com.seibel.distanthorizons.coreapi.util.StringUtil; import org.apache.logging.log4j.Logger; import javax.swing.*; +import java.awt.*; import java.util.*; +import java.util.List; /** @@ -72,8 +74,8 @@ public class Config public static ConfigLinkedEntry quickLodChunkRenderDistance = new ConfigLinkedEntry(Advanced.Graphics.Quality.lodChunkRenderDistanceRadius); - public static ConfigEntry qualityPresetSetting = new ConfigEntry.Builder() - .set(EQualityPreset.MEDIUM) // the default value is set via the listener when accessed + public static ConfigEntry qualityPresetSetting = new ConfigEntry.Builder() + .set(EDhApiQualityPreset.MEDIUM) // the default value is set via the listener when accessed .comment("" + "Changing this setting will modify a number of different settings that will change the \n" + "visual fidelity of the rendered LODs.\n" @@ -84,8 +86,8 @@ public class Config .addListener(RenderQualityPresetConfigEventHandler.INSTANCE) .build(); - public static ConfigEntry threadPresetSetting = new ConfigEntry.Builder() - .set(EThreadPreset.LOW_IMPACT) // the default value is set via the listener when accessed + public static ConfigEntry threadPresetSetting = new ConfigEntry.Builder() + .set(EDhApiThreadPreset.LOW_IMPACT) // the default value is set via the listener when accessed .comment("" + "Changing this setting will modify a number of different settings that will change \n" + "the load that Distant Horizons is allowed to put on your CPU. \n" @@ -138,20 +140,20 @@ public class Config public static class Quality { - public static ConfigEntry maxHorizontalResolution = new ConfigEntry.Builder() - .set(EMaxHorizontalResolution.BLOCK) + public static ConfigEntry maxHorizontalResolution = new ConfigEntry.Builder() + .set(EDhApiMaxHorizontalResolution.BLOCK) .comment("" + "What is the maximum detail LODs should be drawn at? \n" + "Higher settings will increase memory and GPU usage. \n" + "\n" - + EMaxHorizontalResolution.CHUNK + ": render 1 LOD for each Chunk. \n" - + EMaxHorizontalResolution.HALF_CHUNK + ": render 4 LODs for each Chunk. \n" - + EMaxHorizontalResolution.FOUR_BLOCKS + ": render 16 LODs for each Chunk. \n" - + EMaxHorizontalResolution.TWO_BLOCKS + ": render 64 LODs for each Chunk. \n" - + EMaxHorizontalResolution.BLOCK + ": render 256 LODs for each Chunk (width of one block). \n" + + EDhApiMaxHorizontalResolution.CHUNK + ": render 1 LOD for each Chunk. \n" + + EDhApiMaxHorizontalResolution.HALF_CHUNK + ": render 4 LODs for each Chunk. \n" + + EDhApiMaxHorizontalResolution.FOUR_BLOCKS + ": render 16 LODs for each Chunk. \n" + + EDhApiMaxHorizontalResolution.TWO_BLOCKS + ": render 64 LODs for each Chunk. \n" + + EDhApiMaxHorizontalResolution.BLOCK + ": render 256 LODs for each Chunk (width of one block). \n" + "\n" - + "Lowest Quality: " + EMaxHorizontalResolution.CHUNK + "\n" - + "Highest Quality: " + EMaxHorizontalResolution.BLOCK) + + "Lowest Quality: " + EDhApiMaxHorizontalResolution.CHUNK + "\n" + + "Highest Quality: " + EDhApiMaxHorizontalResolution.BLOCK) .setPerformance(EConfigEntryPerformance.MEDIUM) .build(); @@ -161,21 +163,21 @@ public class Config .setPerformance(EConfigEntryPerformance.HIGH) .build(); - public static ConfigEntry verticalQuality = new ConfigEntry.Builder() - .set(EVerticalQuality.MEDIUM) + public static ConfigEntry verticalQuality = new ConfigEntry.Builder() + .set(EDhApiVerticalQuality.MEDIUM) .comment("" + "This indicates how well LODs will represent \n" + "overhangs, caves, floating islands, etc. \n" + "Higher options will make the world more accurate, but" + "will increase memory and GPU usage. \n" + "\n" - + "Lowest Quality: " + EVerticalQuality.HEIGHT_MAP + "\n" - + "Highest Quality: " + EVerticalQuality.EXTREME) + + "Lowest Quality: " + EDhApiVerticalQuality.HEIGHT_MAP + "\n" + + "Highest Quality: " + EDhApiVerticalQuality.EXTREME) .setPerformance(EConfigEntryPerformance.VERY_HIGH) .build(); - public static ConfigEntry horizontalQuality = new ConfigEntry.Builder() - .set(EHorizontalQuality.MEDIUM) + public static ConfigEntry horizontalQuality = new ConfigEntry.Builder() + .set(EDhApiHorizontalQuality.MEDIUM) .comment("" + "This indicates how quickly LODs decrease in quality the further away they are. \n" + "Higher settings will render higher quality fake chunks farther away, \n" @@ -183,25 +185,25 @@ public class Config .setPerformance(EConfigEntryPerformance.MEDIUM) .build(); - public static ConfigEntry transparency = new ConfigEntry.Builder() - .set(ETransparency.COMPLETE) + public static ConfigEntry transparency = new ConfigEntry.Builder() + .set(EDhApiTransparency.COMPLETE) .comment("" + "How should LOD transparency be handled. \n" + "\n" - + ETransparency.COMPLETE + ": LODs will render transparent. \n" - + ETransparency.FAKE + ": LODs will be opaque, but shaded to match the blocks underneath. \n" - + ETransparency.DISABLED + ": LODs will be opaque. \n" + + EDhApiTransparency.COMPLETE + ": LODs will render transparent. \n" + + EDhApiTransparency.FAKE + ": LODs will be opaque, but shaded to match the blocks underneath. \n" + + EDhApiTransparency.DISABLED + ": LODs will be opaque. \n" + "") .setPerformance(EConfigEntryPerformance.MEDIUM) .build(); - public static ConfigEntry blocksToIgnore = new ConfigEntry.Builder() - .set(EBlocksToAvoid.NON_COLLIDING) + public static ConfigEntry blocksToIgnore = new ConfigEntry.Builder() + .set(EDhApiBlocksToAvoid.NON_COLLIDING) .comment("" + "What blocks shouldn't be rendered as LODs? \n" + "\n" - + EBlocksToAvoid.NONE + ": Represent all blocks in the LODs \n" - + EBlocksToAvoid.NON_COLLIDING + ": Only represent solid blocks in the LODs (tall grass, torches, etc. won't count for a LOD's height) \n" + + EDhApiBlocksToAvoid.NONE + ": Represent all blocks in the LODs \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) .build(); @@ -234,33 +236,27 @@ public class Config public static class Fog { - public static ConfigEntry drawMode = new ConfigEntry.Builder() - .set(EFogDrawMode.FOG_ENABLED) + public static ConfigEntry drawMode = new ConfigEntry.Builder() + .set(EDhApiFogDrawMode.FOG_ENABLED) .comment("" + "When should fog be drawn? \n" + "\n" - + EFogDrawMode.USE_OPTIFINE_SETTING + ": Use whatever Fog setting Optifine is using.\n" - + "If Optifine isn't installed this defaults to " + EFogDrawMode.FOG_ENABLED + ". \n" - + EFogDrawMode.FOG_ENABLED + ": Never draw fog on the LODs \n" - + EFogDrawMode.FOG_DISABLED + ": Always draw fast fog on the LODs \n" + + EDhApiFogDrawMode.USE_OPTIFINE_SETTING + ": Use whatever Fog setting Optifine is using.\n" + + "If Optifine isn't installed this defaults to " + EDhApiFogDrawMode.FOG_ENABLED + ". \n" + + EDhApiFogDrawMode.FOG_ENABLED + ": Never draw fog on the LODs \n" + + EDhApiFogDrawMode.FOG_DISABLED + ": Always draw fast fog on the LODs \n" + "\n" + "Disabling fog will improve GPU performance.") .setPerformance(EConfigEntryPerformance.VERY_LOW) .build(); - public static ConfigEntry distance = new ConfigEntry.Builder() - .set(EFogDistance.FAR) - .comment("At what distance should Fog be drawn on the LODs?") - .setPerformance(EConfigEntryPerformance.NONE) - .build(); - - public static ConfigEntry colorMode = new ConfigEntry.Builder() - .set(EFogColorMode.USE_WORLD_FOG_COLOR) + public static ConfigEntry colorMode = new ConfigEntry.Builder() + .set(EDhApiFogColorMode.USE_WORLD_FOG_COLOR) .comment("" + "What color should fog use? \n" + "\n" - + EFogColorMode.USE_WORLD_FOG_COLOR + ": Use the world's fog color. \n" - + EFogColorMode.USE_SKY_COLOR + ": Use the sky's color.") + + EDhApiFogColorMode.USE_WORLD_FOG_COLOR + ": Use the world's fog color. \n" + + EDhApiFogColorMode.USE_SKY_COLOR + ": Use the sky's color.") .setPerformance(EConfigEntryPerformance.NONE) .build(); @@ -320,14 +316,14 @@ public class Config + "1.0: Fully opaque fog.") .build(); - public static ConfigEntry farFogFalloff = new ConfigEntry.Builder() - .set(EFogFalloff.EXPONENTIAL_SQUARED) + public static ConfigEntry farFogFalloff = new ConfigEntry.Builder() + .set(EDhApiFogFalloff.EXPONENTIAL_SQUARED) .comment("" + "How should the fog thickness should be calculated? \n" + "\n" - + EFogFalloff.LINEAR + ": Linear based on distance (will ignore 'density')\n" - + EFogFalloff.EXPONENTIAL + ": 1/(e^(distance*density)) \n" - + EFogFalloff.EXPONENTIAL_SQUARED + ": 1/(e^((distance*density)^2)") + + EDhApiFogFalloff.LINEAR + ": Linear based on distance (will ignore 'density')\n" + + EDhApiFogFalloff.EXPONENTIAL + ": 1/(e^(distance*density)) \n" + + EDhApiFogFalloff.EXPONENTIAL_SQUARED + ": 1/(e^((distance*density)^2)") .build(); public static ConfigEntry farFogDensity = new ConfigEntry.Builder() @@ -343,37 +339,37 @@ public class Config public static class HeightFog { - public static ConfigEntry heightFogMixMode = new ConfigEntry.Builder() - .set(EHeightFogMixMode.BASIC) + public static ConfigEntry heightFogMixMode = new ConfigEntry.Builder() + .set(EDhApiHeightFogMixMode.BASIC) .comment("" + "How should height effect the fog thickness? \n" + "Note: height fog is combined with the other fog settings. \n" + "\n" - + EHeightFogMixMode.BASIC + ": No special height fog effect. Fog is calculated based on camera distance \n" - + EHeightFogMixMode.IGNORE_HEIGHT + ": Ignore height completely. Fog is only calculated with horizontal distance \n" - + EHeightFogMixMode.ADDITION + ": heightFog + farFog \n" - + EHeightFogMixMode.MAX + ": max(heightFog, farFog) \n" - + EHeightFogMixMode.MULTIPLY + ": heightFog * farFog \n" - + EHeightFogMixMode.INVERSE_MULTIPLY + ": 1 - (1-heightFog) * (1-farFog) \n" - + EHeightFogMixMode.LIMITED_ADDITION + ": farFog + max(farFog, heightFog) \n" - + EHeightFogMixMode.MULTIPLY_ADDITION + ": farFog + farFog * heightFog \n" - + EHeightFogMixMode.INVERSE_MULTIPLY_ADDITION + ": farFog + 1 - (1-heightFog) * (1-farFog) \n" - + EHeightFogMixMode.AVERAGE + ": farFog*0.5 + heightFog*0.5 \n" + + EDhApiHeightFogMixMode.BASIC + ": No special height fog effect. Fog is calculated based on camera distance \n" + + EDhApiHeightFogMixMode.IGNORE_HEIGHT + ": Ignore height completely. Fog is only calculated with horizontal distance \n" + + EDhApiHeightFogMixMode.ADDITION + ": heightFog + farFog \n" + + EDhApiHeightFogMixMode.MAX + ": max(heightFog, farFog) \n" + + EDhApiHeightFogMixMode.MULTIPLY + ": heightFog * farFog \n" + + EDhApiHeightFogMixMode.INVERSE_MULTIPLY + ": 1 - (1-heightFog) * (1-farFog) \n" + + EDhApiHeightFogMixMode.LIMITED_ADDITION + ": farFog + max(farFog, heightFog) \n" + + EDhApiHeightFogMixMode.MULTIPLY_ADDITION + ": farFog + farFog * heightFog \n" + + EDhApiHeightFogMixMode.INVERSE_MULTIPLY_ADDITION + ": farFog + 1 - (1-heightFog) * (1-farFog) \n" + + EDhApiHeightFogMixMode.AVERAGE + ": farFog*0.5 + heightFog*0.5 \n" + "\n" - + "Note: height fog settings are ignored if '" + EHeightFogMixMode.BASIC + "' or '" + EHeightFogMixMode.IGNORE_HEIGHT + "' are selected.") + + "Note: height fog settings are ignored if '" + EDhApiHeightFogMixMode.BASIC + "' or '" + EDhApiHeightFogMixMode.IGNORE_HEIGHT + "' are selected.") .build(); - public static ConfigEntry heightFogMode = new ConfigEntry.Builder() - .set(EHeightFogMode.ABOVE_AND_BELOW_CAMERA) + public static ConfigEntry heightFogMode = new ConfigEntry.Builder() + .set(EDhApiHeightFogMode.ABOVE_AND_BELOW_CAMERA) .comment("" + "Where should the height fog start? \n" + "\n" - + EHeightFogMode.ABOVE_CAMERA + ": Height fog starts at the camera and goes towards the sky \n" - + EHeightFogMode.BELOW_CAMERA + ": Height fog starts at the camera and goes towards the void \n" - + EHeightFogMode.ABOVE_AND_BELOW_CAMERA + ": Height fog starts from the camera to goes towards both the sky and void \n" - + EHeightFogMode.ABOVE_SET_HEIGHT + ": Height fog starts from a set height and goes towards the sky \n" - + EHeightFogMode.BELOW_SET_HEIGHT + ": Height fog starts from a set height and goes towards the void \n" - + EHeightFogMode.ABOVE_AND_BELOW_SET_HEIGHT + ": Height fog starts from a set height and goes towards both the sky and void") + + EDhApiHeightFogMode.ABOVE_CAMERA + ": Height fog starts at the camera and goes towards the sky \n" + + EDhApiHeightFogMode.BELOW_CAMERA + ": Height fog starts at the camera and goes towards the void \n" + + EDhApiHeightFogMode.ABOVE_AND_BELOW_CAMERA + ": Height fog starts from the camera to goes towards both the sky and void \n" + + EDhApiHeightFogMode.ABOVE_SET_HEIGHT + ": Height fog starts from a set height and goes towards the sky \n" + + EDhApiHeightFogMode.BELOW_SET_HEIGHT + ": Height fog starts from a set height and goes towards the void \n" + + EDhApiHeightFogMode.ABOVE_AND_BELOW_SET_HEIGHT + ": Height fog starts from a set height and goes towards both the sky and void") .build(); public static ConfigEntry heightFogBaseHeight = new ConfigEntry.Builder() @@ -417,14 +413,14 @@ public class Config + "1.0: Fully opaque fog.") .build(); - public static ConfigEntry heightFogFalloff = new ConfigEntry.Builder() - .set(EFogFalloff.EXPONENTIAL_SQUARED) + public static ConfigEntry heightFogFalloff = new ConfigEntry.Builder() + .set(EDhApiFogFalloff.EXPONENTIAL_SQUARED) .comment("" + "How should the height fog thickness should be calculated? \n" + "\n" - + EFogFalloff.LINEAR + ": Linear based on height (will ignore 'density')\n" - + EFogFalloff.EXPONENTIAL + ": 1/(e^(height*density)) \n" - + EFogFalloff.EXPONENTIAL_SQUARED + ": 1/(e^((height*density)^2)") + + EDhApiFogFalloff.LINEAR + ": Linear based on height (will ignore 'density')\n" + + EDhApiFogFalloff.EXPONENTIAL + ": 1/(e^(height*density)) \n" + + EDhApiFogFalloff.EXPONENTIAL_SQUARED + ": 1/(e^((height*density)^2)") .build(); public static ConfigEntry heightFogDensity = new ConfigEntry.Builder() @@ -534,18 +530,6 @@ public class Config public static class AdvancedGraphics { - /** - * @deprecated Use overdrawPrevention instead, will be removed when DH updates to MC 1.21
- * After removal a float value will be used to control overdraw instead.
- */ - @Deprecated - public static ConfigEntry overdrawPreventionPreset = new ConfigEntry.Builder() - .set(EOverdrawPrevention.MEDIUM) - .comment("") - .setAppearance(EConfigEntryAppearance.ONLY_IN_API) - .setPerformance(EConfigEntryPerformance.NONE) - .build(); - public static ConfigEntry overdrawPrevention = new ConfigEntry.Builder() .setMinDefaultMax(0.0, 0.4, 1.0) .comment("" @@ -561,21 +545,6 @@ public class Config .setPerformance(EConfigEntryPerformance.NONE) .build(); - @Deprecated // TODO remove failed experiment - public static ConfigEntry seamlessOverdraw = new ConfigEntry.Builder() - .set(false) - .comment("" - + "Buggy experimental option that will attempt to match up \n" - + "Distant Horizons' and Minecraft's near/far clip planes, \n" - + "reducing overdraw. \n" - + "\n" - + "Only functional on Fabric.\n" - + "Works best with an overdraw prevention setting of " + EOverdrawPrevention.MEDIUM + " or higher \n" - + " and cave culling is disabled. \n" - + "") - .setPerformance(EConfigEntryPerformance.NONE) - .build(); - // move into "shader compatibility" public static ConfigEntry brightnessMultiplier = new ConfigEntry.Builder() // TODO: Make this a float (the ClassicConfigGUI doesnt support floats) .set(1.0) @@ -641,15 +610,15 @@ public class Config + "If set to 0 the mod wont overwrite vanilla's default (which so happens to also be 0)") .build(); - public static ConfigEntry lodShading = new ConfigEntry.Builder() - .set(ELodShading.MINECRAFT) + public static ConfigEntry lodShading = new ConfigEntry.Builder() + .set(EDhApiLodShading.AUTO) .comment("" + "How should LODs be shaded? \n" + "\n" - + ELodShading.MINECRAFT + ": Uses the same side shading as vanilla Minecraft blocks. \n" - + ELodShading.OLD_LIGHTING + ": Simulates Minecraft's block shading for LODs. \n" + + EDhApiLodShading.AUTO + ": Uses the same side shading as vanilla Minecraft blocks. \n" + + EDhApiLodShading.ENABLED + ": Simulates Minecraft's block shading for LODs. \n" + " Can be used to force LOD shading when using some shaders. \n" - + ELodShading.NONE + ": 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) .build(); @@ -676,6 +645,17 @@ public class Config + "Disable this if shadows render incorrectly.") .build(); + public static ConfigEntry grassSideRendering = new ConfigEntry.Builder() + .set(EDhApiGrassSideRendering.FADE_TO_DIRT) + .comment("" + + "How should the sides and bottom of grass block LODs render? \n" + + "\n" + + EDhApiGrassSideRendering.AS_GRASS + ": all sides of dirt LOD's render using the top (green) color. \n" + + EDhApiGrassSideRendering.FADE_TO_DIRT + ": sides fade from grass to dirt. \n" + + EDhApiGrassSideRendering.AS_DIRT + ": sides render entirely as dirt. \n" + + "") + .setPerformance(EConfigEntryPerformance.NONE) + .build(); } @@ -743,36 +723,6 @@ public class Config + "") .build(); - // deprecated and not implemented, can be made public if we ever re-implement it - @Deprecated - private static ConfigEntry generationPriority = new ConfigEntry.Builder() - .set(EGenerationPriority.NEAR_FIRST) - .comment("" - + "In what priority should fake chunks be generated outside the vanilla render distance? \n" - + "\n" - + EGenerationPriority.FAR_FIRST + " \n" - + "Fake chunks are generated from lowest to highest detail \n" - + " with a priority for far away regions. \n" - + "This fills in the world fastest, but you will have large low detail \n" - + " blocks for a while while the generation happens. \n" - + "\n" - + EGenerationPriority.NEAR_FIRST + " \n" - + "Fake chunks are generated around the player \n" - + " in a spiral, similar to vanilla minecraft. \n" - + "Best used when on a server since we can't generate \n" - + " fake chunks. \n" - + "\n" - + EGenerationPriority.BALANCED + " \n" - + "A mix between " + EGenerationPriority.NEAR_FIRST + "and" + EGenerationPriority.FAR_FIRST + ". \n" - + "First prioritise completing nearby highest detail chunks, \n" - + " then focus on filling in the low detail areas away from the player. \n" - + "\n" - + EGenerationPriority.AUTO + " \n" - + "Uses " + EGenerationPriority.BALANCED + " when on a single player world \n" - + " and " + EGenerationPriority.NEAR_FIRST + " when connected to a server.") - .setPerformance(EConfigEntryPerformance.NONE) - .build(); - } public static class LodBuilding @@ -801,19 +751,81 @@ public class Config + "") .build(); + public static ConfigEntry dataCompression = new ConfigEntry.Builder() + .set(EDhApiDataCompressionMode.LZMA2) + .comment("" + + "What algorithm should be used to compress new LOD data? \n" + + "This setting will only affect new or updated LOD data, \n" + + "any data already generated when this setting is changed will be\n" + + "unaffected until it needs to be re-written to the database.\n" + + "\n" + + EDhApiDataCompressionMode.UNCOMPRESSED + " \n" + + "Should only be used for testing, is worse in every way vs ["+EDhApiDataCompressionMode.LZ4+"].\n" + + "Expected Compression Ratio: 1.0\n" + + "Estimated average DTO read speed: 1.64 milliseconds\n" + + "Estimated average DTO write speed: 12.44 milliseconds\n" + + "\n" + + EDhApiDataCompressionMode.LZ4 + " \n" + + "A good option if you're CPU limited and have plenty of hard drive space.\n" + + "Expected Compression Ratio: 0.36\n" + + "Estimated average DTO read speed: 1.85 ms\n" + + "Estimated average DTO write speed: 9.46 ms\n" + + "\n" + + EDhApiDataCompressionMode.Z_STD + " \n" + + "A good middle ground between speed and compression.\n" + + "Expected Compression Ratio: 0.21\n" + + "Estimated average DTO read speed: 11.78 ms\n" + + "Estimated average DTO write speed: 16.77 ms\n" + + "\n" + + EDhApiDataCompressionMode.LZMA2 + " \n" + + "Slow but very good compression.\n" + + "Expected Compression Ratio: 0.14\n" + + "Estimated average DTO read speed: 11.89 ms\n" + + "Estimated average DTO write speed: 192.01 ms\n" + + "") + .build(); + + public static ConfigEntry worldCompression = new ConfigEntry.Builder() + .set(EDhApiWorldCompressionMode.VISUALLY_EQUAL) + .comment("" + + "How should block data be compressed when creating LOD data? \n" + + "This setting will only affect new or updated LOD data, \n" + + "any data already generated when this setting is changed will be\n" + + "unaffected until it is modified or re-loaded.\n" + + "\n" + + EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS + " \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" + + "Expected Compression Ratio: 1.0\n" + + "\n" + + EDhApiWorldCompressionMode.VISUALLY_EQUAL + " \n" + + "Only visible block/biome changes are recorded in the database. \n" + + "Hidden blocks (IE ores) are ignored. \n" + + "Expected Compression Ratio: 0.7\n" + + "") + .build(); + + public static ConfigEntry showMigrationChatWarning = new ConfigEntry.Builder() + .set(true) + .comment("" + + "Determines if a message should be displayed in the chat when LOD migration starts. \n" + + "") + .build(); + } public static class Multiplayer { - public static ConfigEntry serverFolderNameMode = new ConfigEntry.Builder() - .set(EServerFolderNameMode.NAME_ONLY) + public static ConfigEntry serverFolderNameMode = new ConfigEntry.Builder() + .set(EDhApiServerFolderNameMode.NAME_ONLY) .comment("" + "How should multiplayer save folders should be named? \n" + "\n" - + EServerFolderNameMode.NAME_ONLY + ": Example: \"Minecraft Server\" \n" - + EServerFolderNameMode.NAME_IP + ": Example: \"Minecraft Server IP 192.168.1.40\" \n" - + EServerFolderNameMode.NAME_IP_PORT + ": Example: \"Minecraft Server IP 192.168.1.40:25565\"" - + EServerFolderNameMode.NAME_IP_PORT_MC_VERSION + ": Example: \"Minecraft Server IP 192.168.1.40:25565 GameVersion 1.16.5\"") + + EDhApiServerFolderNameMode.NAME_ONLY + ": Example: \"Minecraft Server\" \n" + + EDhApiServerFolderNameMode.IP_ONLY + ": Example: \"192.168.1.40\" \n" + + EDhApiServerFolderNameMode.NAME_IP + ": Example: \"Minecraft Server IP 192.168.1.40\" \n" + + EDhApiServerFolderNameMode.NAME_IP_PORT + ": Example: \"Minecraft Server IP 192.168.1.40:25565\"" + + EDhApiServerFolderNameMode.NAME_IP_PORT_MC_VERSION + ": Example: \"Minecraft Server IP 192.168.1.40:25565 GameVersion 1.16.5\"") .build(); public static ConfigEntry multiverseSimilarityRequiredPercent = new ConfigEntry.Builder() @@ -907,7 +919,7 @@ public class Config ThreadPresetConfigEventHandler.getFileHandlerDefaultThreadCount(), Runtime.getRuntime().availableProcessors()) .comment("" - + "How many threads should be used when reading in LOD data from disk? \n" + + "How many threads should be used when reading/writing LOD data to/from disk? \n" + "\n" + "Increasing this number will cause LODs to load in faster, \n" + "but may cause lag when loading a new world or when \n" @@ -920,6 +932,31 @@ public class Config .comment(THREAD_RUN_TIME_RATIO_NOTE) .build(); + public static final ConfigEntry numberOfUpdatePropagatorThreads = new ConfigEntry.Builder() + .setMinDefaultMax(1, + ThreadPresetConfigEventHandler.getUpdatePropagatorDefaultThreadCount(), + Runtime.getRuntime().availableProcessors()) + .comment("" + + "How many threads should be used when applying LOD updates? \n" + + "An LOD update is the operation of down-sampling a high detail LOD \n" + + "into a lower detail one.\n" + + "\n" + + "This config can have a much higher number of threads \n" + + "assigned and much lower run time ratio vs other thread pools \n" + + "because the amount of time any particular thread may run is relatively low.\n" + + "\n" + + "This is because LOD updating only only partially thread safe, \n" + + "so between 40% and 60% of the time a given thread may end up \n" + + "waiting on another thread to finish updating the same LOD it also wants\n" + + "to work on.\n" + + "\n" + + THREAD_NOTE) + .build(); + public static final ConfigEntry runTimeRatioForUpdatePropagatorThreads = new ConfigEntry.Builder() + .setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getUpdatePropagatorDefaultRunTimeRatio(), 1.0) + .comment(THREAD_RUN_TIME_RATIO_NOTE) + .build(); + public static final ConfigEntry numberOfLodBuilderThreads = new ConfigEntry.Builder() .setMinDefaultMax(1, ThreadPresetConfigEventHandler.getLodBuilderDefaultThreadCount(), @@ -950,27 +987,27 @@ public class Config public static class GpuBuffers { - public static ConfigEntry gpuUploadMethod = new ConfigEntry.Builder() - .set(EGpuUploadMethod.AUTO) + public static ConfigEntry gpuUploadMethod = new ConfigEntry.Builder() + .set(EDhApiGpuUploadMethod.AUTO) .comment("" + "What method should be used to upload geometry to the GPU? \n" + "\n" - + EGpuUploadMethod.AUTO + ": Picks the best option based on the GPU you have. \n" + + EDhApiGpuUploadMethod.AUTO + ": Picks the best option based on the GPU you have. \n" + "\n" - + EGpuUploadMethod.BUFFER_STORAGE + ": Default if OpenGL 4.5 is supported. \n" + + EDhApiGpuUploadMethod.BUFFER_STORAGE + ": Default if OpenGL 4.5 is supported. \n" + " Fast rendering, no stuttering. \n" + "\n" - + EGpuUploadMethod.SUB_DATA + ": Backup option for NVIDIA. \n" + + EDhApiGpuUploadMethod.SUB_DATA + ": Backup option for NVIDIA. \n" + " Fast rendering but may stutter when uploading. \n" + "\n" - + EGpuUploadMethod.BUFFER_MAPPING + ": Slow rendering but won't stutter when uploading. \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" - + EGpuUploadMethod.DATA + ": Fast rendering but will stutter when uploading. \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" @@ -1003,15 +1040,6 @@ public class Config + "") .build(); - // deprecated and not implemented, can be made public if we ever re-implement it - @Deprecated - private static ConfigEntry rebuildTimes = new ConfigEntry.Builder() - .set(EBufferRebuildTimes.NORMAL) - .comment("" - + "How frequently should vertex buffers (geometry) be rebuilt and sent to the GPU? \n" - + "Higher settings may cause stuttering, but will prevent holes in the world") - .build(); - } public static class AutoUpdater @@ -1030,9 +1058,9 @@ public class Config + "Should Distant Horizons silently, automatically download and install new versions?") .build(); - public static ConfigEntry updateBranch = new ConfigEntry.Builder() + public static ConfigEntry updateBranch = new ConfigEntry.Builder() .set( - ModInfo.IS_DEV_BUILD? EUpdateBranch.NIGHTLY: EUpdateBranch.STABLE // If it's already a nightly build, then download the nightly build ofc + ModInfo.IS_DEV_BUILD? EDhApiUpdateBranch.NIGHTLY: EDhApiUpdateBranch.STABLE // If it's already a nightly build, then download the nightly build ofc ) .comment("" + " If DH should use the nightly (provided by Gitlab), or stable (provided by Modrinth) build") @@ -1043,64 +1071,64 @@ public class Config { // TODO add change all option // TODO default to error chat and info file - public static ConfigEntry logWorldGenEvent = new ConfigEntry.Builder() - .set(ELoggerMode.LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE) + public static ConfigEntry logWorldGenEvent = new ConfigEntry.Builder() + .set(EDhApiLoggerMode.LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE) .comment("" + "If enabled, the mod will log information about the world generation process. \n" + "This can be useful for debugging.") .build(); - public static ConfigEntry logWorldGenPerformance = new ConfigEntry.Builder() - .set(ELoggerMode.LOG_WARNING_TO_CHAT_AND_FILE) + public static ConfigEntry logWorldGenPerformance = new ConfigEntry.Builder() + .set(EDhApiLoggerMode.LOG_WARNING_TO_CHAT_AND_FILE) .comment("" + "If enabled, the mod will log performance about the world generation process. \n" + "This can be useful for debugging.") .build(); - public static ConfigEntry logWorldGenLoadEvent = new ConfigEntry.Builder() - .set(ELoggerMode.LOG_WARNING_TO_CHAT_AND_FILE) + public static ConfigEntry logWorldGenLoadEvent = new ConfigEntry.Builder() + .set(EDhApiLoggerMode.LOG_WARNING_TO_CHAT_AND_FILE) .comment("" + "If enabled, the mod will log information about the world generation process. \n" + "This can be useful for debugging.") .build(); - public static ConfigEntry logLodBuilderEvent = new ConfigEntry.Builder() - .set(ELoggerMode.LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE) + public static ConfigEntry logLodBuilderEvent = new ConfigEntry.Builder() + .set(EDhApiLoggerMode.LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE) .comment("" + "If enabled, the mod will log information about the LOD generation process. \n" + "This can be useful for debugging.") .build(); - public static ConfigEntry logRendererBufferEvent = new ConfigEntry.Builder() - .set(ELoggerMode.LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE) + public static ConfigEntry logRendererBufferEvent = new ConfigEntry.Builder() + .set(EDhApiLoggerMode.LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE) .comment("" + "If enabled, the mod will log information about the renderer buffer process. \n" + "This can be useful for debugging.") .build(); - public static ConfigEntry logRendererGLEvent = new ConfigEntry.Builder() - .set(ELoggerMode.LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE) + public static ConfigEntry logRendererGLEvent = new ConfigEntry.Builder() + .set(EDhApiLoggerMode.LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE) .comment("" + "If enabled, the mod will log information about the renderer OpenGL process. \n" + "This can be useful for debugging.") .build(); - public static ConfigEntry logFileReadWriteEvent = new ConfigEntry.Builder() - .set(ELoggerMode.LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE) + public static ConfigEntry logFileReadWriteEvent = new ConfigEntry.Builder() + .set(EDhApiLoggerMode.LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE) .comment("" + "If enabled, the mod will log information about file read/write operations. \n" + "This can be useful for debugging.") .build(); - public static ConfigEntry logFileSubDimEvent = new ConfigEntry.Builder() - .set(ELoggerMode.LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE) + public static ConfigEntry logFileSubDimEvent = new ConfigEntry.Builder() + .set(EDhApiLoggerMode.LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE) .comment("" + "If enabled, the mod will log information about file sub-dimension operations. \n" + "This can be useful for debugging.") .build(); - public static ConfigEntry logNetworkEvent = new ConfigEntry.Builder() - .set(ELoggerMode.LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE) + public static ConfigEntry logNetworkEvent = new ConfigEntry.Builder() + .set(EDhApiLoggerMode.LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE) .comment("" + "If enabled, the mod will log information about network operations. \n" + "This can be useful for debugging.") @@ -1110,25 +1138,25 @@ public class Config public static class Debugging { - public static ConfigEntry rendererMode = new ConfigEntry.Builder() - .set(ERendererMode.DEFAULT) + public static ConfigEntry rendererMode = new ConfigEntry.Builder() + .set(EDhApiRendererMode.DEFAULT) .comment("" + "What renderer is active? \n" + "\n" - + ERendererMode.DEFAULT + ": Default lod renderer \n" - + ERendererMode.DEBUG + ": Debug testing renderer \n" - + ERendererMode.DISABLED + ": Disable rendering") + + EDhApiRendererMode.DEFAULT + ": Default lod renderer \n" + + EDhApiRendererMode.DEBUG + ": Debug testing renderer \n" + + EDhApiRendererMode.DISABLED + ": Disable rendering") .build(); - public static ConfigEntry debugRendering = new ConfigEntry.Builder() - .set(EDebugRendering.OFF) + public static ConfigEntry debugRendering = new ConfigEntry.Builder() + .set(EDhApiDebugRendering.OFF) .comment("" + "Should specialized colors/rendering modes be used? \n" + "\n" - + EDebugRendering.OFF + ": LODs will be drawn with their normal colors. \n" - + EDebugRendering.SHOW_DETAIL + ": LODs' color will be based on their detail level. \n" - + EDebugRendering.SHOW_BLOCK_MATERIAL + ": LODs' color will be based on their material. \n" - + EDebugRendering.SHOW_OVERLAPPING_QUADS + ": LODs will be drawn with total white, but overlapping quads will be drawn with red. \n" + + EDhApiDebugRendering.OFF + ": LODs will be drawn with their normal colors. \n" + + EDhApiDebugRendering.SHOW_DETAIL + ": LODs' color will be based on their detail level. \n" + + EDhApiDebugRendering.SHOW_BLOCK_MATERIAL + ": LODs' color will be based on their material. \n" + + EDhApiDebugRendering.SHOW_OVERLAPPING_QUADS + ": LODs will be drawn with total white, but overlapping quads will be drawn with red. \n" + "") .build(); @@ -1165,6 +1193,14 @@ public class Config + "Useful for debugging shaders") .build(); + public static ConfigEntry showOverlappingQuadErrors = new ConfigEntry.Builder() + .set(false) + .comment("" + + "If true overlapping quads will be rendered as bright red for easy identification. \n" + + "If false the quads will be rendered normally. \n" + + "") + .build(); + // Note: This will reset on game restart, and should have a warning on the tooltip public static ConfigEntry allowUnsafeValues = new ConfigEntry.Builder() .set(false) @@ -1222,19 +1258,9 @@ public class Config .comment("Render LOD section status?") .build(); - public static ConfigEntry showFullDataFileStatus = new ConfigEntry.Builder() + public static ConfigEntry showFullDataUpdateStatus = new ConfigEntry.Builder() .set(false) - .comment("Render full data file status?") - .build(); - - public static ConfigEntry showFullDataFileSampling = new ConfigEntry.Builder() - .set(false) - .comment("Render full data file sampling progress?") - .build(); - - public static ConfigEntry showRenderDataFileStatus = new ConfigEntry.Builder() - .set(false) - .comment("Render render data file status?") + .comment("Render full data update/lock status?") .build(); } @@ -1254,15 +1280,15 @@ public class Config + "") .build(); - public static ConfigEntry glErrorHandlingMode = new ConfigEntry.Builder() - .set(ModInfo.IS_DEV_BUILD ? EGLErrorHandlingMode.LOG : EGLErrorHandlingMode.IGNORE) + public static ConfigEntry glErrorHandlingMode = new ConfigEntry.Builder() + .set(ModInfo.IS_DEV_BUILD ? EDhApiGLErrorHandlingMode.LOG : EDhApiGLErrorHandlingMode.IGNORE) .comment("" + "Defines how OpenGL errors are handled. \n" + "May incorrectly catch OpenGL errors thrown by other mods. \n" + "\n" - + EGLErrorHandlingMode.IGNORE + ": Do nothing. \n" - + EGLErrorHandlingMode.LOG + ": write an error to the log. \n" - + EGLErrorHandlingMode.LOG_THROW + ": write to the log and throw an exception. \n" + + EDhApiGLErrorHandlingMode.IGNORE + ": Do nothing. \n" + + EDhApiGLErrorHandlingMode.LOG + ": write an error to the log. \n" + + EDhApiGLErrorHandlingMode.LOG_THROW + ": write to the log and throw an exception. \n" + " Warning: this should only be enabled when debugging the LOD renderer \n" + " as it may break Minecraft's renderer when an exception is thrown. \n" + "") @@ -1295,14 +1321,14 @@ public class Config "") .build(); - public static ConfigEntry glProfileMode = new ConfigEntry.Builder() - .set(EGlProfileMode.CORE) + public static ConfigEntry glProfileMode = new ConfigEntry.Builder() + .set(EDhApiGlProfileMode.CORE) .comment("" + "Can be changed if you experience crashing when loading into a world.\n" + "\n" + "Defines the OpenGL context type Distant Horizon's will create. \n" + - "Generally this should be left as ["+EGlProfileMode.CORE+"] unless there is an issue with your GPU driver. \n" + - "Possible values: ["+ StringUtil.join("],[", EGlProfileMode.values())+"] \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" + "") .build(); public static ConfigEntry enableGlForwardCompatibilityMode = new ConfigEntry.Builder() @@ -1381,10 +1407,20 @@ public class Config .set(new HashMap()) .build(); - public static ConfigUIButton uiButtonTest = new ConfigUIButton(() -> { new Thread(() -> { - System.setProperty("java.awt.headless", "false"); // Required to make it work - JOptionPane.showMessageDialog(null, "Button pressed!", "UITester dialog", JOptionPane.INFORMATION_MESSAGE); - });}); + public static ConfigUIButton uiButtonTest = new ConfigUIButton(() -> + { + new Thread(() -> + { + if (!GraphicsEnvironment.isHeadless()) + { + JOptionPane.showMessageDialog(null, "Button pressed!", "UITester dialog", JOptionPane.INFORMATION_MESSAGE); + } + else + { + LOGGER.info("button pressed!"); + } + }); + }); public static ConfigCategory categoryTest = new ConfigCategory.Builder().set(CategoryTest.class).build(); @@ -1440,23 +1476,12 @@ public class Config { complicatedListenerSetupComplete = true; - try - { - // listener can only be added after the config has finished initializing - Client.Advanced.Graphics.AdvancedGraphics.overdrawPreventionPreset.addListener(OverdrawPreventionPresetConfigEventHandler.INSTANCE); - } - catch (Exception e) - { - LOGGER.error("Unexpected exception when running config delayed listener setup. Error: [" + e.getMessage() + "].", e); - } - try { // TODO automatically get all instances of AbstractPresetConfigEventHandler and fire "setUiOnlyConfigValues" ThreadPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues(); RenderQualityPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues(); QuickRenderToggleConfigEventHandler.INSTANCE.setUiOnlyConfigValues(); - OverdrawPreventionPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues(); RenderCacheConfigEventHandler.getInstance(); } catch (Exception e) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/DebugColumnConfigEventHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/DebugColumnConfigEventHandler.java index 9e014f45f..93f55820d 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/DebugColumnConfigEventHandler.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/DebugColumnConfigEventHandler.java @@ -20,24 +20,8 @@ package com.seibel.distanthorizons.core.config.eventHandlers; import com.seibel.distanthorizons.api.DhApi; -import com.seibel.distanthorizons.api.enums.config.EHorizontalQuality; -import com.seibel.distanthorizons.api.enums.config.EMaxHorizontalResolution; -import com.seibel.distanthorizons.api.enums.config.EVerticalQuality; -import com.seibel.distanthorizons.api.enums.config.quickOptions.EQualityPreset; -import com.seibel.distanthorizons.api.enums.rendering.ETransparency; import com.seibel.distanthorizons.api.interfaces.render.IDhApiRenderProxy; -import com.seibel.distanthorizons.core.config.Config; -import com.seibel.distanthorizons.core.config.ConfigEntryWithPresetOptions; -import com.seibel.distanthorizons.core.config.eventHandlers.presets.AbstractPresetConfigEventHandler; -import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; import com.seibel.distanthorizons.core.config.listeners.IConfigListener; -import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; public class DebugColumnConfigEventHandler implements IConfigListener { diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/QuickRenderToggleConfigEventHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/QuickRenderToggleConfigEventHandler.java index 1991603e3..8b6f3cb6d 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/QuickRenderToggleConfigEventHandler.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/QuickRenderToggleConfigEventHandler.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.core.config.eventHandlers; -import com.seibel.distanthorizons.api.enums.rendering.ERendererMode; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode; import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; import com.seibel.distanthorizons.core.config.Config; @@ -28,15 +28,15 @@ public class QuickRenderToggleConfigEventHandler public static QuickRenderToggleConfigEventHandler INSTANCE = new QuickRenderToggleConfigEventHandler(); private final ConfigChangeListener quickRenderChangeListener; - private final ConfigChangeListener rendererModeChangeListener; + private final ConfigChangeListener rendererModeChangeListener; /** private since we only ever need one handler at a time */ private QuickRenderToggleConfigEventHandler() { - this.quickRenderChangeListener = new ConfigChangeListener<>(Config.Client.quickEnableRendering, (val) -> { Config.Client.Advanced.Debugging.rendererMode.set(Config.Client.quickEnableRendering.get() ? ERendererMode.DEFAULT : ERendererMode.DISABLED); }); - this.rendererModeChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Debugging.rendererMode, (val) -> { Config.Client.quickEnableRendering.set(Config.Client.Advanced.Debugging.rendererMode.get() != ERendererMode.DISABLED); }); + this.quickRenderChangeListener = new ConfigChangeListener<>(Config.Client.quickEnableRendering, (val) -> { Config.Client.Advanced.Debugging.rendererMode.set(Config.Client.quickEnableRendering.get() ? EDhApiRendererMode.DEFAULT : EDhApiRendererMode.DISABLED); }); + this.rendererModeChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Debugging.rendererMode, (val) -> { Config.Client.quickEnableRendering.set(Config.Client.Advanced.Debugging.rendererMode.get() != EDhApiRendererMode.DISABLED); }); } /** @@ -45,7 +45,7 @@ public class QuickRenderToggleConfigEventHandler */ public void setUiOnlyConfigValues() { - Config.Client.quickEnableRendering.set(Config.Client.Advanced.Debugging.rendererMode.get() != ERendererMode.DISABLED); + Config.Client.quickEnableRendering.set(Config.Client.Advanced.Debugging.rendererMode.get() != EDhApiRendererMode.DISABLED); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/RenderCacheConfigEventHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/RenderCacheConfigEventHandler.java index 1a169aa97..24b613e9b 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/RenderCacheConfigEventHandler.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/RenderCacheConfigEventHandler.java @@ -20,13 +20,10 @@ package com.seibel.distanthorizons.core.config.eventHandlers; import com.seibel.distanthorizons.api.DhApi; -import com.seibel.distanthorizons.api.enums.config.EBlocksToAvoid; -import com.seibel.distanthorizons.api.enums.config.ELodShading; -import com.seibel.distanthorizons.api.enums.config.EMaxHorizontalResolution; -import com.seibel.distanthorizons.api.enums.config.EVerticalQuality; -import com.seibel.distanthorizons.api.enums.rendering.ETransparency; +import com.seibel.distanthorizons.api.enums.config.*; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiTransparency; import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; -import com.seibel.distanthorizons.core.config.listeners.IConfigListener; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.util.TimerUtil; @@ -45,15 +42,18 @@ public class RenderCacheConfigEventHandler // previous values used to check if a watched setting was actually modified - private final ConfigChangeListener horizontalResolutionChangeListener; - private final ConfigChangeListener verticalQualityChangeListener; - private final ConfigChangeListener transparencyChangeListener; - private final ConfigChangeListener blocksToIgnoreChangeListener; + private final ConfigChangeListener horizontalResolutionChangeListener; + private final ConfigChangeListener verticalQualityChangeListener; + private final ConfigChangeListener transparencyChangeListener; + private final ConfigChangeListener blocksToIgnoreChangeListener; private final ConfigChangeListener tintWithAvoidedBlocksChangeListener; private final ConfigChangeListener brightnessMultiplierChangeListener; private final ConfigChangeListener saturationMultiplierChangeListener; - private final ConfigChangeListener lodShadingChangeListener; + private final ConfigChangeListener lodShadingChangeListener; + private final ConfigChangeListener grassSideChangeListener; + + private final ConfigChangeListener debugRenderingChangeListener; /** how long to wait in milliseconds before applying the config changes */ private static final long TIMEOUT_IN_MS = 4_000L; @@ -83,6 +83,9 @@ public class RenderCacheConfigEventHandler this.brightnessMultiplierChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Graphics.AdvancedGraphics.brightnessMultiplier, (newValue) -> this.refreshRenderDataAfterTimeout()); this.saturationMultiplierChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Graphics.AdvancedGraphics.saturationMultiplier, (newValue) -> this.refreshRenderDataAfterTimeout()); this.lodShadingChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Graphics.AdvancedGraphics.lodShading, (newValue) -> this.refreshRenderDataAfterTimeout()); + this.grassSideChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Graphics.AdvancedGraphics.grassSideRendering, (newValue) -> this.refreshRenderDataAfterTimeout()); + + this.debugRenderingChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Debugging.debugRendering, (newValue) -> this.refreshRenderDataAfterTimeout()); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/presets/OverdrawPreventionPresetConfigEventHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/presets/OverdrawPreventionPresetConfigEventHandler.java deleted file mode 100644 index 216238d53..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/presets/OverdrawPreventionPresetConfigEventHandler.java +++ /dev/null @@ -1,86 +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 . - */ - -package com.seibel.distanthorizons.core.config.eventHandlers.presets; - -import com.seibel.distanthorizons.api.enums.config.EMaxHorizontalResolution; -import com.seibel.distanthorizons.api.enums.config.EOverdrawPrevention; -import com.seibel.distanthorizons.api.enums.config.quickOptions.EQualityPreset; -import com.seibel.distanthorizons.core.config.Config; -import com.seibel.distanthorizons.core.config.ConfigEntryWithPresetOptions; -import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; -import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; - -public class OverdrawPreventionPresetConfigEventHandler extends AbstractPresetConfigEventHandler -{ - public static final OverdrawPreventionPresetConfigEventHandler INSTANCE = new OverdrawPreventionPresetConfigEventHandler(); - - private static final Logger LOGGER = LogManager.getLogger(); - - - private final ConfigEntryWithPresetOptions overdrawPrevention = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.AdvancedGraphics.overdrawPrevention, - new HashMap() - {{ - this.put(EOverdrawPrevention.HEAVY, 0.6); - this.put(EOverdrawPrevention.MEDIUM, 0.4); - this.put(EOverdrawPrevention.LIGHT, 0.25); - this.put(EOverdrawPrevention.NONE, 0.0); - }}); - - - - //==============// - // constructors // - //==============// - - /** private since we only ever need one handler at a time */ - private OverdrawPreventionPresetConfigEventHandler() - { - // add each config used by this preset - this.configList.add(this.overdrawPrevention); - - - for (ConfigEntryWithPresetOptions config : this.configList) - { - // ignore try-using, the listener should only ever be added once and should never be removed - new ConfigChangeListener<>(config.configEntry, (val) -> { this.onConfigValueChanged(); }); - } - } - - - - //==============// - // enum getters // - //==============// - - @Override - protected IConfigEntry getPresetConfigEntry() { return Config.Client.Advanced.Graphics.AdvancedGraphics.overdrawPreventionPreset; } - - @Override - protected List getPresetEnumList() { return Arrays.asList(EOverdrawPrevention.values()); } - @Override - protected EOverdrawPrevention getCustomPresetEnum() { return EOverdrawPrevention.CUSTOM; } - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/presets/RenderQualityPresetConfigEventHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/presets/RenderQualityPresetConfigEventHandler.java index 3ac0f6f78..85f1e1b40 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/presets/RenderQualityPresetConfigEventHandler.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/presets/RenderQualityPresetConfigEventHandler.java @@ -19,11 +19,11 @@ package com.seibel.distanthorizons.core.config.eventHandlers.presets; -import com.seibel.distanthorizons.api.enums.config.EHorizontalQuality; -import com.seibel.distanthorizons.api.enums.config.EMaxHorizontalResolution; -import com.seibel.distanthorizons.api.enums.config.EVerticalQuality; -import com.seibel.distanthorizons.api.enums.config.quickOptions.EQualityPreset; -import com.seibel.distanthorizons.api.enums.rendering.ETransparency; +import com.seibel.distanthorizons.api.enums.config.EDhApiHorizontalQuality; +import com.seibel.distanthorizons.api.enums.config.EDhApiMaxHorizontalResolution; +import com.seibel.distanthorizons.api.enums.config.EDhApiVerticalQuality; +import com.seibel.distanthorizons.api.enums.config.quickOptions.EDhApiQualityPreset; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiTransparency; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.ConfigEntryWithPresetOptions; import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; @@ -33,57 +33,57 @@ import org.apache.logging.log4j.Logger; import java.util.*; -public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigEventHandler +public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigEventHandler { public static final RenderQualityPresetConfigEventHandler INSTANCE = new RenderQualityPresetConfigEventHandler(); private static final Logger LOGGER = LogManager.getLogger(); - private final ConfigEntryWithPresetOptions drawResolution = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Quality.maxHorizontalResolution, - new HashMap() + private final ConfigEntryWithPresetOptions drawResolution = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Quality.maxHorizontalResolution, + new HashMap() {{ - this.put(EQualityPreset.MINIMUM, EMaxHorizontalResolution.TWO_BLOCKS); - this.put(EQualityPreset.LOW, EMaxHorizontalResolution.BLOCK); - this.put(EQualityPreset.MEDIUM, EMaxHorizontalResolution.BLOCK); - this.put(EQualityPreset.HIGH, EMaxHorizontalResolution.BLOCK); - this.put(EQualityPreset.EXTREME, EMaxHorizontalResolution.BLOCK); + this.put(EDhApiQualityPreset.MINIMUM, EDhApiMaxHorizontalResolution.TWO_BLOCKS); + this.put(EDhApiQualityPreset.LOW, EDhApiMaxHorizontalResolution.BLOCK); + this.put(EDhApiQualityPreset.MEDIUM, EDhApiMaxHorizontalResolution.BLOCK); + this.put(EDhApiQualityPreset.HIGH, EDhApiMaxHorizontalResolution.BLOCK); + this.put(EDhApiQualityPreset.EXTREME, EDhApiMaxHorizontalResolution.BLOCK); }}); - private final ConfigEntryWithPresetOptions verticalQuality = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Quality.verticalQuality, - new HashMap() + private final ConfigEntryWithPresetOptions verticalQuality = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Quality.verticalQuality, + new HashMap() {{ - this.put(EQualityPreset.MINIMUM, EVerticalQuality.HEIGHT_MAP); - this.put(EQualityPreset.LOW, EVerticalQuality.LOW); - this.put(EQualityPreset.MEDIUM, EVerticalQuality.MEDIUM); - this.put(EQualityPreset.HIGH, EVerticalQuality.HIGH); - this.put(EQualityPreset.EXTREME, EVerticalQuality.EXTREME); + this.put(EDhApiQualityPreset.MINIMUM, EDhApiVerticalQuality.HEIGHT_MAP); + this.put(EDhApiQualityPreset.LOW, EDhApiVerticalQuality.LOW); + this.put(EDhApiQualityPreset.MEDIUM, EDhApiVerticalQuality.MEDIUM); + this.put(EDhApiQualityPreset.HIGH, EDhApiVerticalQuality.HIGH); + this.put(EDhApiQualityPreset.EXTREME, EDhApiVerticalQuality.EXTREME); }}); - private final ConfigEntryWithPresetOptions horizontalQuality = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Quality.horizontalQuality, - new HashMap() + private final ConfigEntryWithPresetOptions horizontalQuality = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Quality.horizontalQuality, + new HashMap() {{ - this.put(EQualityPreset.MINIMUM, EHorizontalQuality.LOWEST); - this.put(EQualityPreset.LOW, EHorizontalQuality.LOW); - this.put(EQualityPreset.MEDIUM, EHorizontalQuality.MEDIUM); - this.put(EQualityPreset.HIGH, EHorizontalQuality.HIGH); - this.put(EQualityPreset.EXTREME, EHorizontalQuality.EXTREME); + this.put(EDhApiQualityPreset.MINIMUM, EDhApiHorizontalQuality.LOWEST); + this.put(EDhApiQualityPreset.LOW, EDhApiHorizontalQuality.LOW); + this.put(EDhApiQualityPreset.MEDIUM, EDhApiHorizontalQuality.MEDIUM); + this.put(EDhApiQualityPreset.HIGH, EDhApiHorizontalQuality.HIGH); + this.put(EDhApiQualityPreset.EXTREME, EDhApiHorizontalQuality.EXTREME); }}); - private final ConfigEntryWithPresetOptions transparency = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Quality.transparency, - new HashMap() + private final ConfigEntryWithPresetOptions transparency = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Quality.transparency, + new HashMap() {{ - this.put(EQualityPreset.MINIMUM, ETransparency.DISABLED); - this.put(EQualityPreset.LOW, ETransparency.DISABLED); // should be fake if/when fake is fixed - this.put(EQualityPreset.MEDIUM, ETransparency.COMPLETE); - this.put(EQualityPreset.HIGH, ETransparency.COMPLETE); - this.put(EQualityPreset.EXTREME, ETransparency.COMPLETE); + this.put(EDhApiQualityPreset.MINIMUM, EDhApiTransparency.DISABLED); + this.put(EDhApiQualityPreset.LOW, EDhApiTransparency.DISABLED); // should be fake if/when fake is fixed + this.put(EDhApiQualityPreset.MEDIUM, EDhApiTransparency.COMPLETE); + this.put(EDhApiQualityPreset.HIGH, EDhApiTransparency.COMPLETE); + this.put(EDhApiQualityPreset.EXTREME, EDhApiTransparency.COMPLETE); }}); - private final ConfigEntryWithPresetOptions ssaoEnabled = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Ssao.enabled, - new HashMap() + private final ConfigEntryWithPresetOptions ssaoEnabled = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Ssao.enabled, + new HashMap() {{ - this.put(EQualityPreset.MINIMUM, false); - this.put(EQualityPreset.LOW, false); - this.put(EQualityPreset.MEDIUM, true); - this.put(EQualityPreset.HIGH, true); - this.put(EQualityPreset.EXTREME, true); + this.put(EDhApiQualityPreset.MINIMUM, false); + this.put(EDhApiQualityPreset.LOW, false); + this.put(EDhApiQualityPreset.MEDIUM, true); + this.put(EDhApiQualityPreset.HIGH, true); + this.put(EDhApiQualityPreset.EXTREME, true); }}); @@ -103,7 +103,7 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE this.configList.add(this.ssaoEnabled); - for (ConfigEntryWithPresetOptions config : this.configList) + for (ConfigEntryWithPresetOptions config : this.configList) { // ignore try-using, the listener should only ever be added once and should never be removed new ConfigChangeListener<>(config.configEntry, (val) -> { this.onConfigValueChanged(); }); @@ -117,11 +117,11 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE //==============// @Override - protected IConfigEntry getPresetConfigEntry() { return Config.Client.qualityPresetSetting; } + protected IConfigEntry getPresetConfigEntry() { return Config.Client.qualityPresetSetting; } @Override - protected List getPresetEnumList() { return Arrays.asList(EQualityPreset.values()); } + protected List getPresetEnumList() { return Arrays.asList(EDhApiQualityPreset.values()); } @Override - protected EQualityPreset getCustomPresetEnum() { return EQualityPreset.CUSTOM; } + protected EDhApiQualityPreset getCustomPresetEnum() { return EDhApiQualityPreset.CUSTOM; } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/presets/ThreadPresetConfigEventHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/presets/ThreadPresetConfigEventHandler.java index 67b7e2010..c6bbf1120 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/presets/ThreadPresetConfigEventHandler.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/config/eventHandlers/presets/ThreadPresetConfigEventHandler.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.core.config.eventHandlers.presets; -import com.seibel.distanthorizons.api.enums.config.quickOptions.EThreadPreset; +import com.seibel.distanthorizons.api.enums.config.quickOptions.EDhApiThreadPreset; import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.ConfigEntryWithPresetOptions; @@ -32,7 +32,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; -public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHandler +public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHandler { public static final ThreadPresetConfigEventHandler INSTANCE = new ThreadPresetConfigEventHandler(); @@ -42,68 +42,90 @@ public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHan public static int getWorldGenDefaultThreadCount() { return getThreadCountByPercent(0.15); } - private final ConfigEntryWithPresetOptions worldGenThreadCount = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.numberOfWorldGenerationThreads, - new HashMap() + private final ConfigEntryWithPresetOptions worldGenThreadCount = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.numberOfWorldGenerationThreads, + new HashMap() {{ - this.put(EThreadPreset.MINIMAL_IMPACT, 1); - this.put(EThreadPreset.LOW_IMPACT, getWorldGenDefaultThreadCount()); - this.put(EThreadPreset.BALANCED, getThreadCountByPercent(0.25)); - this.put(EThreadPreset.AGGRESSIVE, getThreadCountByPercent(0.5)); - this.put(EThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, getThreadCountByPercent(1.0)); + this.put(EDhApiThreadPreset.MINIMAL_IMPACT, 1); + this.put(EDhApiThreadPreset.LOW_IMPACT, getWorldGenDefaultThreadCount()); + this.put(EDhApiThreadPreset.BALANCED, getThreadCountByPercent(0.25)); + this.put(EDhApiThreadPreset.AGGRESSIVE, getThreadCountByPercent(0.5)); + this.put(EDhApiThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, getThreadCountByPercent(1.0)); }}); public static double getWorldGenDefaultRunTimeRatio() { return LOW_THREAD_COUNT_CPU ? 0.5 : 0.75; } - private final ConfigEntryWithPresetOptions worldGenRunTime = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.runTimeRatioForWorldGenerationThreads, - new HashMap() + private final ConfigEntryWithPresetOptions worldGenRunTime = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.runTimeRatioForWorldGenerationThreads, + new HashMap() {{ - this.put(EThreadPreset.MINIMAL_IMPACT, LOW_THREAD_COUNT_CPU ? 0.1 : 0.25); - this.put(EThreadPreset.LOW_IMPACT, getWorldGenDefaultRunTimeRatio()); - this.put(EThreadPreset.BALANCED, LOW_THREAD_COUNT_CPU ? 0.5 : 0.75); - this.put(EThreadPreset.AGGRESSIVE, LOW_THREAD_COUNT_CPU ? 0.75 : 1.0); - this.put(EThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, 1.0); + this.put(EDhApiThreadPreset.MINIMAL_IMPACT, LOW_THREAD_COUNT_CPU ? 0.1 : 0.25); + this.put(EDhApiThreadPreset.LOW_IMPACT, getWorldGenDefaultRunTimeRatio()); + this.put(EDhApiThreadPreset.BALANCED, LOW_THREAD_COUNT_CPU ? 0.5 : 0.75); + this.put(EDhApiThreadPreset.AGGRESSIVE, LOW_THREAD_COUNT_CPU ? 0.75 : 1.0); + this.put(EDhApiThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, 1.0); }}); public static int getFileHandlerDefaultThreadCount() { return getThreadCountByPercent(0.1); } - private final ConfigEntryWithPresetOptions fileHandlerThreadCount = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.numberOfFileHandlerThreads, - new HashMap() + private final ConfigEntryWithPresetOptions fileHandlerThreadCount = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.numberOfFileHandlerThreads, + new HashMap() {{ - this.put(EThreadPreset.MINIMAL_IMPACT, 1); - this.put(EThreadPreset.LOW_IMPACT, getFileHandlerDefaultThreadCount()); - this.put(EThreadPreset.BALANCED, getThreadCountByPercent(0.2)); - this.put(EThreadPreset.AGGRESSIVE, getThreadCountByPercent(0.2)); - this.put(EThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, getThreadCountByPercent(1.0)); + this.put(EDhApiThreadPreset.MINIMAL_IMPACT, 1); + this.put(EDhApiThreadPreset.LOW_IMPACT, getFileHandlerDefaultThreadCount()); + this.put(EDhApiThreadPreset.BALANCED, getThreadCountByPercent(0.2)); + this.put(EDhApiThreadPreset.AGGRESSIVE, getThreadCountByPercent(0.2)); + this.put(EDhApiThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, getThreadCountByPercent(1.0)); }}); - public static double getFileHandlerDefaultRunTimeRatio() { return 0.5; } - private final ConfigEntryWithPresetOptions fileHandlerRunTime = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.runTimeRatioForFileHandlerThreads, - new HashMap() + public static double getFileHandlerDefaultRunTimeRatio() { return 0.75; } + private final ConfigEntryWithPresetOptions fileHandlerRunTime = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.runTimeRatioForFileHandlerThreads, + new HashMap() {{ - this.put(EThreadPreset.MINIMAL_IMPACT, 0.25); - this.put(EThreadPreset.LOW_IMPACT, getFileHandlerDefaultRunTimeRatio()); - this.put(EThreadPreset.BALANCED, 0.75); - this.put(EThreadPreset.AGGRESSIVE, 1.0); - this.put(EThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, 1.0); + this.put(EDhApiThreadPreset.MINIMAL_IMPACT, 0.50); + this.put(EDhApiThreadPreset.LOW_IMPACT, getFileHandlerDefaultRunTimeRatio()); + this.put(EDhApiThreadPreset.BALANCED, 1.0); + this.put(EDhApiThreadPreset.AGGRESSIVE, 1.0); + this.put(EDhApiThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, 1.0); + }}); + + + public static int getUpdatePropagatorDefaultThreadCount() { return getThreadCountByPercent(0.25); } + private final ConfigEntryWithPresetOptions UpdatePropagatorThreadCount = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.numberOfUpdatePropagatorThreads, + new HashMap() + {{ + this.put(EDhApiThreadPreset.MINIMAL_IMPACT, 1); + this.put(EDhApiThreadPreset.LOW_IMPACT, getUpdatePropagatorDefaultThreadCount()); + this.put(EDhApiThreadPreset.BALANCED, getThreadCountByPercent(0.5)); + this.put(EDhApiThreadPreset.AGGRESSIVE, getThreadCountByPercent(0.75)); + this.put(EDhApiThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, getThreadCountByPercent(1.0)); + }}); + public static double getUpdatePropagatorDefaultRunTimeRatio() { return 0.5; } + private final ConfigEntryWithPresetOptions UpdatePropagatorRunTime = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.runTimeRatioForUpdatePropagatorThreads, + new HashMap() + {{ + this.put(EDhApiThreadPreset.MINIMAL_IMPACT, 0.25); + this.put(EDhApiThreadPreset.LOW_IMPACT, getUpdatePropagatorDefaultRunTimeRatio()); + this.put(EDhApiThreadPreset.BALANCED, 0.75); + this.put(EDhApiThreadPreset.AGGRESSIVE, 1.0); + this.put(EDhApiThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, 1.0); }}); public static int getLodBuilderDefaultThreadCount() { return getThreadCountByPercent(0.1); } - private final ConfigEntryWithPresetOptions lodBuilderThreadCount = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.numberOfLodBuilderThreads, - new HashMap() + private final ConfigEntryWithPresetOptions lodBuilderThreadCount = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.numberOfLodBuilderThreads, + new HashMap() {{ - this.put(EThreadPreset.MINIMAL_IMPACT, 1); - this.put(EThreadPreset.LOW_IMPACT, getLodBuilderDefaultThreadCount()); - this.put(EThreadPreset.BALANCED, getThreadCountByPercent(0.2)); - this.put(EThreadPreset.AGGRESSIVE, getThreadCountByPercent(0.4)); - this.put(EThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, getThreadCountByPercent(1.0)); + this.put(EDhApiThreadPreset.MINIMAL_IMPACT, 1); + this.put(EDhApiThreadPreset.LOW_IMPACT, getLodBuilderDefaultThreadCount()); + this.put(EDhApiThreadPreset.BALANCED, getThreadCountByPercent(0.2)); + this.put(EDhApiThreadPreset.AGGRESSIVE, getThreadCountByPercent(0.4)); + this.put(EDhApiThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, getThreadCountByPercent(1.0)); }}); public static double getLodBuilderDefaultRunTimeRatio() { return LOW_THREAD_COUNT_CPU ? 0.25 : 0.5; } - private final ConfigEntryWithPresetOptions lodBuilderRunTime = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.runTimeRatioForLodBuilderThreads, - new HashMap() + private final ConfigEntryWithPresetOptions lodBuilderRunTime = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.runTimeRatioForLodBuilderThreads, + new HashMap() {{ - this.put(EThreadPreset.MINIMAL_IMPACT, 0.1); - this.put(EThreadPreset.LOW_IMPACT, getLodBuilderDefaultRunTimeRatio()); - this.put(EThreadPreset.BALANCED, LOW_THREAD_COUNT_CPU ? 0.5 : 0.75); - this.put(EThreadPreset.AGGRESSIVE, 1.0); - this.put(EThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, 1.0); + this.put(EDhApiThreadPreset.MINIMAL_IMPACT, 0.1); + this.put(EDhApiThreadPreset.LOW_IMPACT, getLodBuilderDefaultRunTimeRatio()); + this.put(EDhApiThreadPreset.BALANCED, LOW_THREAD_COUNT_CPU ? 0.5 : 0.75); + this.put(EDhApiThreadPreset.AGGRESSIVE, 1.0); + this.put(EDhApiThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, 1.0); }}); @@ -122,11 +144,14 @@ public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHan this.configList.add(this.fileHandlerThreadCount); this.configList.add(this.fileHandlerRunTime); + this.configList.add(this.UpdatePropagatorThreadCount); + this.configList.add(this.UpdatePropagatorRunTime); + this.configList.add(this.lodBuilderThreadCount); this.configList.add(this.lodBuilderRunTime); - for (ConfigEntryWithPresetOptions config : this.configList) + for (ConfigEntryWithPresetOptions config : this.configList) { // ignore try-using, the listeners should only ever be added once and should never be removed new ConfigChangeListener<>(config.configEntry, (val) -> { this.onConfigValueChanged(); }); @@ -171,11 +196,11 @@ public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHan //==============// @Override - protected IConfigEntry getPresetConfigEntry() { return Config.Client.threadPresetSetting; } + protected IConfigEntry getPresetConfigEntry() { return Config.Client.threadPresetSetting; } @Override - protected List getPresetEnumList() { return Arrays.asList(EThreadPreset.values()); } + protected List getPresetEnumList() { return Arrays.asList(EDhApiThreadPreset.values()); } @Override - protected EThreadPreset getCustomPresetEnum() { return EThreadPreset.CUSTOM; } + protected EDhApiThreadPreset getCustomPresetEnum() { return EDhApiThreadPreset.CUSTOM; } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/gui/JavaScreenHandlerScreen.java b/core/src/main/java/com/seibel/distanthorizons/core/config/gui/JavaScreenHandlerScreen.java index 48c6b2077..06c626c1f 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/config/gui/JavaScreenHandlerScreen.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/config/gui/JavaScreenHandlerScreen.java @@ -37,7 +37,8 @@ public class JavaScreenHandlerScreen extends AbstractScreen static { - // Required to run this + // Needs to be called before any Swing code is called, otherwise + // Swing will get stuck thinking it's headless System.setProperty("java.awt.headless", "false"); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/gui/OpenGLConfigScreen.java b/core/src/main/java/com/seibel/distanthorizons/core/config/gui/OpenGLConfigScreen.java index 71dad5506..35a6cf353 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/config/gui/OpenGLConfigScreen.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/config/gui/OpenGLConfigScreen.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.core.config.gui; -import com.seibel.distanthorizons.api.enums.config.EGpuUploadMethod; +import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod; import com.seibel.distanthorizons.core.render.glObject.GLProxy; import com.seibel.distanthorizons.core.render.glObject.GLState; import com.seibel.distanthorizons.core.render.glObject.buffer.GLVertexBuffer; @@ -76,7 +76,7 @@ public class OpenGLConfigScreen extends AbstractScreen buffer.rewind(); GLVertexBuffer vbo = new GLVertexBuffer(false); vbo.bind(); - vbo.uploadBuffer(buffer, 4, EGpuUploadMethod.DATA, vertices.length * Float.BYTES); + vbo.uploadBuffer(buffer, 4, EDhApiGpuUploadMethod.DATA, vertices.length * Float.BYTES); return vbo; } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/FullDataDownSampler.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/FullDataDownSampler.java deleted file mode 100644 index ac8862668..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/FullDataDownSampler.java +++ /dev/null @@ -1,159 +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 . - */ - -package com.seibel.distanthorizons.core.dataObjects.fullData; - -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.SingleColumnFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.CompleteFullDataSource; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; -import com.seibel.distanthorizons.core.file.fullDatafile.IFullDataSourceProvider; -import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; -import com.seibel.distanthorizons.core.pos.DhLodPos; -import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.util.LodUtil; -import org.apache.logging.log4j.Logger; - -import java.util.ArrayList; -import java.util.concurrent.CompletableFuture; - -public class FullDataDownSampler -{ - private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - public static CompletableFuture createDownSamplingFuture(DhSectionPos newTarget, IFullDataSourceProvider provider) - { - // TODO: Make this future somehow run with lowest priority (to ensure ram usage stays low) - return createDownSamplingFuture(CompleteFullDataSource.createEmpty(newTarget), provider); - } - - public static CompletableFuture createDownSamplingFuture(CompleteFullDataSource target, IFullDataSourceProvider provider) - { - int sectionSizeNeeded = 1 << target.getDataDetailLevel(); - - ArrayList> futures; - DhLodPos basePos = target.getSectionPos().getSectionBBoxPos().getCornerLodPos(CompleteFullDataSource.SECTION_SIZE_OFFSET); - - - if (sectionSizeNeeded <= CompleteFullDataSource.SECTION_SIZE_OFFSET) - { - futures = new ArrayList<>(sectionSizeNeeded * sectionSizeNeeded); - for (int xOffset = 0; xOffset < sectionSizeNeeded; xOffset++) - { - for (int zOffset = 0; zOffset < sectionSizeNeeded; zOffset++) - { - CompletableFuture future = provider.getAsync(new DhSectionPos( - CompleteFullDataSource.SECTION_SIZE_OFFSET, basePos.x + xOffset, basePos.z + zOffset)); - future = future.whenComplete((source, ex) -> { - if (ex == null && source != null && source instanceof CompleteFullDataSource) - { - downSample(target, (CompleteFullDataSource) source); - } - else if (ex != null) - { - LOGGER.error("Error while down sampling", ex); - } - }); - futures.add(future); - } - } - } - else - { - futures = new ArrayList<>(CompleteFullDataSource.WIDTH * CompleteFullDataSource.WIDTH); - int multiplier = sectionSizeNeeded / CompleteFullDataSource.WIDTH; - for (int xOffset = 0; xOffset < CompleteFullDataSource.WIDTH; xOffset++) - { - for (int zOffset = 0; zOffset < CompleteFullDataSource.WIDTH; zOffset++) - { - CompletableFuture future = provider.getAsync(new DhSectionPos( - CompleteFullDataSource.SECTION_SIZE_OFFSET, basePos.x + xOffset * multiplier, basePos.z + zOffset * multiplier)); - future = future.whenComplete((source, ex) -> { - if (ex == null && source != null && source instanceof CompleteFullDataSource) - { - downSample(target, (CompleteFullDataSource) source); - } - else if (ex != null) - { - LOGGER.error("Error while down sampling", ex); - } - }); - futures.add(future); - } - } - } - return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenApply(v -> target); - } - - public static void downSample(CompleteFullDataSource target, CompleteFullDataSource source) - { - LodUtil.assertTrue(target.getSectionPos().overlapsExactly(source.getSectionPos())); - LodUtil.assertTrue(target.getDataDetailLevel() > source.getDataDetailLevel()); - - byte detailDiff = (byte) (target.getDataDetailLevel() - source.getDataDetailLevel()); - DhSectionPos trgPos = target.getSectionPos(); - DhSectionPos srcPos = source.getSectionPos(); - - if (detailDiff >= CompleteFullDataSource.SECTION_SIZE_OFFSET) - { - // The source occupies only 1 datapoint in the target - // FIXME: TEMP method for down-sampling: take only the corner column - int sourceSectionPerTargetData = 1 << (detailDiff - CompleteFullDataSource.SECTION_SIZE_OFFSET); - if (srcPos.getX() % sourceSectionPerTargetData != 0 || srcPos.getZ() % sourceSectionPerTargetData != 0) - { - return; - } - DhLodPos trgOffset = trgPos.getMinCornerLodPos(target.getDataDetailLevel()); - DhLodPos srcOffset = srcPos.getSectionBBoxPos().convertToDetailLevel(target.getDataDetailLevel()); - int offsetX = trgOffset.x - srcOffset.x; - int offsetZ = trgOffset.z - srcOffset.z; - LodUtil.assertTrue(offsetX >= 0 && offsetX < CompleteFullDataSource.WIDTH - && offsetZ >= 0 && offsetZ < CompleteFullDataSource.WIDTH); - target.markNotEmpty(); - source.get(0, 0).deepCopyTo(target.get(offsetX, offsetZ)); - - } - else if (detailDiff > 0) - { - // The source occupies multiple data-points in the target - int srcDataPerTrgData = 1 << detailDiff; - int overlappedTrgDataSize = CompleteFullDataSource.WIDTH / srcDataPerTrgData; - - DhLodPos trgOffset = trgPos.getMinCornerLodPos(target.getDataDetailLevel()); - DhLodPos srcOffset = srcPos.getSectionBBoxPos().getCornerLodPos(target.getDataDetailLevel()); - int offsetX = trgOffset.x - srcOffset.x; - int offsetZ = trgOffset.z - srcOffset.z; - LodUtil.assertTrue(offsetX >= 0 && offsetX < CompleteFullDataSource.WIDTH - && offsetZ >= 0 && offsetZ < CompleteFullDataSource.WIDTH); - target.markNotEmpty(); - - for (int ox = 0; ox < overlappedTrgDataSize; ox++) - { - for (int oz = 0; oz < overlappedTrgDataSize; oz++) - { - SingleColumnFullDataAccessor column = target.get(ox + offsetX, oz + offsetZ); - column.downsampleFrom(source.subView(srcDataPerTrgData, ox * srcDataPerTrgData, oz * srcDataPerTrgData)); - } - } - } - else - { - LodUtil.assertNotReach(); - } - } - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/FullDataPointIdMap.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/FullDataPointIdMap.java index b276882b0..ce362a4e6 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/FullDataPointIdMap.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/FullDataPointIdMap.java @@ -27,19 +27,25 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrappe import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; +import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.*; -import java.util.ArrayList; -import java.util.HashMap; +import java.util.*; import java.util.concurrent.locks.ReentrantReadWriteLock; /** - * WARNING: This is not THREAD-SAFE! - *

- * Used to map a numerical IDs to a Biome/BlockState pair. - * + * WARNING: This is not THREAD-SAFE!

+ * + * Used to map a numerical IDs to a Biome/BlockState pair.

+ * + * TODO the serializing of this map might be really big + * since it stringifies every block and biome name, which is quite bulky. + * It might be worth while to have a biome and block ID that then both get mapped + * to the data point ID to reduce file size. + * And/or it would be good to dynamically remove IDs that aren't currently in use. + * * @author Leetom */ public class FullDataPointIdMap @@ -55,7 +61,6 @@ public class FullDataPointIdMap private static final String BLOCK_STATE_SEPARATOR_STRING = "_DH-BSW_"; - // FIXME: Improve performance maybe? /** used when the data point map is running normally */ private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); @@ -112,6 +117,7 @@ public class FullDataPointIdMap /** @return -1 if the list is empty */ public int getMaxValidId() { return this.entryList.size() - 1; } + public boolean isEmpty() { return this.entryList.isEmpty(); } public DhSectionPos getPos() { return this.pos; } @@ -125,7 +131,7 @@ public class FullDataPointIdMap * If an entry with the given values already exists nothing will * be added but the existing item's ID will still be returned. */ - public int addIfNotPresentAndGetId(IBiomeWrapper biome, IBlockStateWrapper blockState) { return this.addIfNotPresentAndGetId(new Entry(biome, blockState), true); } + public int addIfNotPresentAndGetId(IBiomeWrapper biome, IBlockStateWrapper blockState) { return this.addIfNotPresentAndGetId(Entry.getEntry(biome, blockState), true); } /** @param useWriteLocks should only be false if this method is already in a write lock to prevent unlocking at the wrong time */ private int addIfNotPresentAndGetId(Entry biomeBlockStateEntry, boolean useWriteLocks) { @@ -165,19 +171,23 @@ public class FullDataPointIdMap /** * Adds each entry from the given map to this map. + * + * Note: when using this function be careful about re-mapping the + * same data source multiple times. + * Doing so may cause indexOutOfBounds issues. * * @return an array of each added entry's ID in this map in order */ - public int[] mergeAndReturnRemappedEntityIds(FullDataPointIdMap target) + public int[] mergeAndReturnRemappedEntityIds(FullDataPointIdMap inputMap) { try { - LOGGER.trace("merging {" + this.pos + ", " + this.entryList.size() + "} and {" + target.pos + ", " + target.entryList.size() + "}"); + //LOGGER.trace("merging {" + this.pos + ", " + this.entryList.size() + "} and {" + inputMap.pos + ", " + inputMap.entryList.size() + "}"); - target.readWriteLock.readLock().lock(); + inputMap.readWriteLock.readLock().lock(); this.readWriteLock.writeLock().lock(); - ArrayList entriesToMerge = target.entryList; + ArrayList entriesToMerge = inputMap.entryList; int[] remappedEntryIds = new int[entriesToMerge.size()]; for (int i = 0; i < entriesToMerge.size(); i++) { @@ -191,9 +201,9 @@ public class FullDataPointIdMap finally { this.readWriteLock.writeLock().unlock(); - target.readWriteLock.readLock().unlock(); + inputMap.readWriteLock.readLock().unlock(); - LOGGER.trace("finished merging {" + this.pos + ", " + this.entryList.size() + "} and {" + target.pos + ", " + target.entryList.size() + "}"); + //LOGGER.trace("finished merging {" + this.pos + ", " + this.entryList.size() + "} and {" + inputMap.pos + ", " + inputMap.entryList.size() + "}"); } } @@ -244,7 +254,7 @@ public class FullDataPointIdMap finally { this.readWriteLock.readLock().unlock(); - LOGGER.trace("serialize " + this.pos + " " + this.entryList.size()); + //LOGGER.trace("serialize " + this.pos + " " + this.entryList.size()); } } @@ -277,7 +287,7 @@ public class FullDataPointIdMap } } - LOGGER.trace("deserialized " + pos + " " + newMap.entryList.size() + "-" + entityCount); + //LOGGER.trace("deserialized " + pos + " " + newMap.entryList.size() + "-" + entityCount); return newMap; } @@ -312,15 +322,81 @@ public class FullDataPointIdMap { private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class); + private static final Int2ReferenceOpenHashMap> ENTRY_POOL = new Int2ReferenceOpenHashMap<>(); + /** lock is necessary since {@link Int2ReferenceOpenHashMap} isn't concurrent and concurrent threads can cause infinite loops */ + private static final ReentrantReadWriteLock ENTRY_POOL_LOCK = new ReentrantReadWriteLock(); + public final IBiomeWrapper biome; public final IBlockStateWrapper blockState; private Integer hashCode = null; - // constructor // - public Entry(IBiomeWrapper biome, IBlockStateWrapper blockState) + //=============// + // constructor // + //=============// + + public static Entry getEntry(IBiomeWrapper biome, IBlockStateWrapper blockState) + { + int entryHash = getHashCode(biome, blockState); + + // try getting the existing entry + try + { + ENTRY_POOL_LOCK.readLock().lock(); + + // check if an entry already exists + ArrayList entryList = ENTRY_POOL.get(entryHash); + if (entryList != null) + { + // at least one entry exists with this hash code + for (int i = 0; i < entryList.size(); i++) + { + Entry entry = entryList.get(i); + if (entry.biome.equals(biome) && entry.blockState.equals(blockState)) + { + return entry; + } + } + + // if we got here, then there was a hash collision and this entry wasn't present in the array + } + } + finally + { + ENTRY_POOL_LOCK.readLock().unlock(); + } + + + // no entry exists, + // create a new one + try + { + ENTRY_POOL_LOCK.writeLock().lock(); + + ArrayList entryList = ENTRY_POOL.get(entryHash); + if (entryList == null) + { + // no entries exist for this hash code + + // we assume that hash collisions should basically never happen, + // so the array starts with an initial capacity of 1. + // However, since collisions will eventually happen, using an arrayList prevents unexpected bugs caused by collisions. + entryList = new ArrayList<>(1); + ENTRY_POOL.put(entryHash, entryList); + } + + Entry newEntry = new Entry(biome, blockState); + entryList.add(newEntry); + return newEntry; + } + finally + { + ENTRY_POOL_LOCK.writeLock().unlock(); + } + } + private Entry(IBiomeWrapper biome, IBlockStateWrapper blockState) { this.biome = biome; this.blockState = blockState; @@ -328,15 +404,29 @@ public class FullDataPointIdMap - // methods // + //===========// + // overrides // + //===========// + public static int getHashCode(Entry entry) { return getHashCode(entry.biome, entry.blockState); } + public static int getHashCode(IBiomeWrapper biome, IBlockStateWrapper blockState) + { + final int prime = 31; + + int result = 1; + // the biome and blockstate hashcode should be already calculated by the time + // we get here, so this operation should be very fast + result = prime * result + (biome == null ? 0 : biome.hashCode()); + result = prime * result + (blockState == null ? 0 : blockState.hashCode()); + return result; + } @Override public int hashCode() { // cache the hash code to improve speed if (this.hashCode == null) { - this.hashCode = this.serialize().hashCode(); + this.hashCode = getHashCode(this); } return this.hashCode; @@ -361,6 +451,10 @@ public class FullDataPointIdMap + //=================// + // (de)serializing // + //=================// + public String serialize() { return this.biome.getSerialString() + BLOCK_STATE_SEPARATOR_STRING + this.blockState.getSerialString(); } public static Entry deserialize(String str, ILevelWrapper levelWrapper) throws IOException, InterruptedException @@ -379,7 +473,7 @@ public class FullDataPointIdMap IBiomeWrapper biome = WRAPPER_FACTORY.deserializeBiomeWrapper(stringArray[0], levelWrapper); IBlockStateWrapper blockState = WRAPPER_FACTORY.deserializeBlockStateWrapper(stringArray[1], levelWrapper); - return new Entry(biome, blockState); + return Entry.getEntry(biome, blockState); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/accessor/ChunkSizedFullDataAccessor.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/accessor/ChunkSizedFullDataAccessor.java deleted file mode 100644 index dcc02e536..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/accessor/ChunkSizedFullDataAccessor.java +++ /dev/null @@ -1,79 +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 . - */ - -package com.seibel.distanthorizons.core.dataObjects.fullData.accessor; - -import com.seibel.distanthorizons.core.pos.DhChunkPos; -import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; -import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.util.FullDataPointUtil; -import com.seibel.distanthorizons.core.util.LodUtil; - -/** - * A more specific version of {@link FullDataArrayAccessor} - * that only contains full data for a single chunk. - * - * @see FullDataPointUtil - */ -public class ChunkSizedFullDataAccessor extends FullDataArrayAccessor -{ - public final DhChunkPos chunkPos; - public final DhSectionPos sectionPos; - - // TODO replace this var with LodUtil.BLOCK_DETAIL_LEVEL - public final byte detailLevel = LodUtil.BLOCK_DETAIL_LEVEL; - - - - public ChunkSizedFullDataAccessor(DhChunkPos chunkPos) - { - super(new FullDataPointIdMap(new DhSectionPos(chunkPos)), - new long[LodUtil.CHUNK_WIDTH * LodUtil.CHUNK_WIDTH][0], - LodUtil.CHUNK_WIDTH); - - this.chunkPos = chunkPos; - // TODO the fact this is using a LodUtil detail level instead of the DhSectionPos detail level may cause confusion and trouble down the line - this.sectionPos = new DhSectionPos(LodUtil.CHUNK_DETAIL_LEVEL, this.chunkPos.x, this.chunkPos.z); - } - - - - public void setSingleColumn(long[] data, int xRelative, int zRelative) { this.dataArrays[xRelative * LodUtil.CHUNK_WIDTH + zRelative] = data; } - - public long nonEmptyCount() - { - long count = 0; - for (long[] data : this.dataArrays) - { - if (data.length != 0) - { - count += 1; - } - } - return count; - } - - public long emptyCount() { return (LodUtil.CHUNK_WIDTH * LodUtil.CHUNK_WIDTH) - this.nonEmptyCount(); } - - public DhSectionPos getSectionPos() { return this.sectionPos; } - - @Override - public String toString() { return this.chunkPos + " " + this.nonEmptyCount(); } - -} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/accessor/FullDataArrayAccessor.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/accessor/FullDataArrayAccessor.java deleted file mode 100644 index db8eec620..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/accessor/FullDataArrayAccessor.java +++ /dev/null @@ -1,189 +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 . - */ - -package com.seibel.distanthorizons.core.dataObjects.fullData.accessor; - -import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; -import com.seibel.distanthorizons.core.util.FullDataPointUtil; -import com.seibel.distanthorizons.core.util.LodUtil; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.CompleteFullDataSource; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.LowDetailIncompleteFullDataSource; - -/** - * Contains Full Data points and basic methods for getting and setting them.
- * Can be used standalone or as the base for Full data sources. - * - * @see CompleteFullDataSource - * @see LowDetailIncompleteFullDataSource - */ -public class FullDataArrayAccessor implements IFullDataAccessor -{ - protected final FullDataPointIdMap mapping; - - /** A flattened 2D array (for the X and Z directions) containing an array for the Y direction. */ - protected final long[][] dataArrays; - - /** measured in data points */ - protected final int width; - /** measured in data points */ - protected final int dataWidth; - - /** index offset used when getting/setting data in {@link FullDataArrayAccessor#dataArrays}. */ - protected final int offset; - - - - //==============// - // constructors // - //==============// - - public FullDataArrayAccessor(FullDataPointIdMap mapping, long[][] dataArrays, int width) - { - if (dataArrays.length != width * width) - { - throw new IllegalArgumentException("tried constructing dataArrayView with invalid input!"); - } - - this.dataArrays = dataArrays; - this.width = width; - this.dataWidth = width; - this.mapping = mapping; - this.offset = 0; - } - - public FullDataArrayAccessor(FullDataArrayAccessor source, int width, int offsetX, int offsetZ) - { - if (source.width < width || source.width < width + offsetX || source.width < width + offsetZ) - { - throw new IllegalArgumentException("tried constructing dataArrayView subview with invalid input!"); - } - - this.dataArrays = source.dataArrays; - this.width = width; - this.dataWidth = source.dataWidth; - this.mapping = source.mapping; - this.offset = source.offset + offsetX * this.dataWidth + offsetZ; - } - - - - //=========// - // methods // - //=========// - - @Override - public FullDataArrayAccessor subView(int width, int xOffset, int zOffset) { return new FullDataArrayAccessor(this, width, xOffset, zOffset); } - - /** WARNING: This will potentially share the underlying array object! */ - public void shadowCopyTo(FullDataArrayAccessor target) - { - if (target.width != this.width) - { - throw new IllegalArgumentException("Target view must have same size as this view"); - } - - - if (target.mapping.equals(this.mapping)) - { - for (int x = 0; x < this.width; x++) - { - System.arraycopy(this.dataArrays, this.offset + x * this.dataWidth, - target.dataArrays, target.offset + x * target.dataWidth, this.width); - } - } - else - { - int[] remappedIds = target.mapping.mergeAndReturnRemappedEntityIds(this.mapping); - for (int x = 0; x < this.width; x++) - { - for (int z = 0; z < this.width; z++) - { - long[] currentData = this.dataArrays[this.offset + x * this.dataWidth + z]; - // may be null if no data exists for this column yet - if (currentData != null) - { - long[] newData = new long[currentData.length]; // TODO what to do if null? - for (int dataPointIndex = 0; dataPointIndex < newData.length; dataPointIndex++) - { - newData[dataPointIndex] = FullDataPointUtil.remap(remappedIds, currentData[dataPointIndex]); - } - - target.dataArrays[target.offset + x * target.dataWidth + z] = newData; - } - } - } - } - } - - /** - * Takes a higher detail {@link FullDataArrayAccessor}'s and converts the data to a lower detail level. - * - * @param incomingFullDataAccessor must be larger than this {@link FullDataArrayAccessor} and its width must a power of two larger (example: this.width = 4, other.width = 8) - */ - public void downsampleFrom(FullDataArrayAccessor incomingFullDataAccessor) - { - // validate that the incoming data isn't smaller than this accessor - LodUtil.assertTrue(incomingFullDataAccessor.width >= this.width && incomingFullDataAccessor.width % this.width == 0); - - int dataPointsPerWidthUnit = incomingFullDataAccessor.width / this.width; - for (int xOffset = 0; xOffset < this.width; xOffset++) - { - for (int zOffset = 0; zOffset < this.width; zOffset++) - { - FullDataArrayAccessor subView = incomingFullDataAccessor.subView(dataPointsPerWidthUnit, - xOffset * dataPointsPerWidthUnit, - zOffset * dataPointsPerWidthUnit); - - SingleColumnFullDataAccessor column = this.get(xOffset, zOffset); - column.downsampleFrom(subView); - } - } - } - - - - //=========// - // getters // - //=========// - - @Override - public FullDataPointIdMap getMapping() { return this.mapping; } - - @Override - public SingleColumnFullDataAccessor get(int index) { return this.get(index / this.width, index % this.width); } - @Override - public SingleColumnFullDataAccessor get(int relativeX, int relativeZ) - { - int dataArrayIndex = (relativeX * this.width) + relativeZ + this.offset; - if (dataArrayIndex >= this.dataArrays.length) - { - LodUtil.assertNotReach( - "FullDataArrayAccessor.get() called with a relative position that is outside the data source. \n" + - "source width: [" + this.width + "] source offset: [" + this.offset + "]\n" + - "given relative pos X: [" + relativeX + "] Z: [" + relativeZ + "]\n" + - "dataArrays.length: [" + this.dataArrays.length + "] dataArrayIndex: [" + dataArrayIndex + "]."); - } - - return new SingleColumnFullDataAccessor(this.mapping, this.dataArrays, dataArrayIndex); - } - - @Override - public int width() { return this.width; } - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/accessor/IFullDataAccessor.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/accessor/IFullDataAccessor.java deleted file mode 100644 index 6f45e1707..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/accessor/IFullDataAccessor.java +++ /dev/null @@ -1,77 +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 . - */ - -package com.seibel.distanthorizons.core.dataObjects.fullData.accessor; - -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; -import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; -import com.seibel.distanthorizons.core.util.LodUtil; -import com.seibel.distanthorizons.core.util.FullDataPointUtil; - -import java.util.Iterator; - -/** - * Contains raw full data points, which must be interpreted by the {@link FullDataPointUtil}.
- * Often used by {@link IFullDataSource}'s. - * - * @see IFullDataSource - * @see FullDataArrayAccessor - * @see FullDataPointUtil - */ -public interface IFullDataAccessor -{ - FullDataPointIdMap getMapping(); - - /** generally used for iterating through the whole data set */ - SingleColumnFullDataAccessor get(int index); - SingleColumnFullDataAccessor get(int relativeX, int relativeZ); - - /** measured in full data points */ - int width(); - - /** - * Creates a new {@link IFullDataAccessor} with the given width and starting at the given X and Z offsets.
- * The returned object will use the same underlining data structure (IE memory addresses) as the source {@link IFullDataAccessor}. - */ - IFullDataAccessor subView(int width, int xOffset, int zOffset); - - - - - /** Returns an iterator that goes over each data column */ - default Iterator iterator() - { - return new Iterator() - { - private int index = 0; - private final int size = width() * width(); - - @Override - public boolean hasNext() { return this.index < this.size; } - - @Override - public SingleColumnFullDataAccessor next() - { - LodUtil.assertTrue(this.hasNext(), "No more data to iterate!"); - return get(this.index++); - } - }; - } - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/accessor/SingleColumnFullDataAccessor.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/accessor/SingleColumnFullDataAccessor.java deleted file mode 100644 index b9f66e242..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/accessor/SingleColumnFullDataAccessor.java +++ /dev/null @@ -1,182 +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 . - */ - -package com.seibel.distanthorizons.core.dataObjects.fullData.accessor; - -import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; -import com.seibel.distanthorizons.core.util.FullDataPointUtil; -import com.seibel.distanthorizons.core.util.LodUtil; - -/** - * Represents a single column of Full LOD data. - * - * @see FullDataPointUtil - */ -public class SingleColumnFullDataAccessor implements IFullDataAccessor -{ - /** - * A flattened 2D array (for the X and Z directions) containing an array for the Y direction. - * TODO the flattened array is probably to reduce garbage collection overhead, but is doing it this way worth while? Having a 3D array would be much easier to understand - * - * @see FullDataArrayAccessor#dataArrays - */ - private final long[][] dataArrays; - /** indicates what index of the {@link SingleColumnFullDataAccessor#dataArrays} is used by this accessor */ - private final int dataArrayIndex; - private final FullDataPointIdMap mapping; - - - - //=============// - // constructor // - //=============// - - public SingleColumnFullDataAccessor(FullDataPointIdMap mapping, long[][] dataArrays, int dataArrayIndex) - { - this.dataArrays = dataArrays; - this.dataArrayIndex = dataArrayIndex; - this.mapping = mapping; - - LodUtil.assertTrue(this.dataArrayIndex < this.dataArrays.length, "dataArrays.length [" + this.dataArrays.length + "] is less than the dataArrayIndex [" + this.dataArrayIndex + "]."); - } - - - - //=========// - // methods // - //=========// - - /** @return true if any data exists in this column. */ - public boolean doesColumnExist() - { - long[] dataColumn = this.dataArrays[this.dataArrayIndex]; - return dataColumn != null && dataColumn.length != 0; - } - - @Override - public FullDataPointIdMap getMapping() { return this.mapping; } - - @Override - public SingleColumnFullDataAccessor get(int index) - { - if (index != 0) - { - throw new IllegalArgumentException("Only contains 1 column of full data!"); - } - - return this; - } - - @Override - public SingleColumnFullDataAccessor get(int relativeX, int relativeZ) - { - if (relativeX != 0 || relativeZ != 0) - { - throw new IllegalArgumentException("Only contains 1 column of full data!"); - } - - return this; - } - - /** @return the entire array of raw full data points. */ - public long[] getRaw() { return this.dataArrays[this.dataArrayIndex]; } - - public long getSingle(int yIndex) { return this.dataArrays[this.dataArrayIndex][yIndex]; } - public void setSingle(int yIndex, long fullDataPoint) { this.dataArrays[this.dataArrayIndex][yIndex] = fullDataPoint; } - - public void setNew(long[] newArray) { this.dataArrays[this.dataArrayIndex] = newArray; } - - /** @return how many data points are in this column */ - public int getSingleLength() - { - long[] singleDataArray = this.dataArrays[this.dataArrayIndex]; - return singleDataArray != null ? singleDataArray.length : 0; - } - - @Override - public int width() { return 1; } - - @Override - public IFullDataAccessor subView(int width, int xOffset, int zOffset) - { - if (width != 1 || xOffset != 1 || zOffset != 1) - { - throw new IllegalArgumentException("Getting invalid range of subView from SingleColumnFullDataAccessor!"); - } - - return this; - } - - /** WARNING: This may potentially share the underlying array objects! */ - public void shadowCopyTo(SingleColumnFullDataAccessor target) - { - if (target.mapping.equals(this.mapping)) - { - target.dataArrays[target.dataArrayIndex] = this.dataArrays[this.dataArrayIndex]; - } - else - { - int[] remappedEntryIds = target.mapping.mergeAndReturnRemappedEntityIds(this.mapping); - long[] sourceData = this.dataArrays[this.dataArrayIndex]; - long[] newData = new long[sourceData.length]; - - for (int i = 0; i < newData.length; i++) - { - newData[i] = FullDataPointUtil.remap(remappedEntryIds, sourceData[i]); - } - target.dataArrays[target.dataArrayIndex] = newData; - } - } - - /** Copies both ID data and mapping data. */ - public void deepCopyTo(SingleColumnFullDataAccessor target) - { - if (target.mapping.equals(this.mapping)) - { - System.arraycopy(this.dataArrays[this.dataArrayIndex], 0, target.dataArrays[target.dataArrayIndex], 0, this.dataArrays[this.dataArrayIndex].length); - } - else - { - int[] remappedEntryIds = target.mapping.mergeAndReturnRemappedEntityIds(this.mapping); - long[] sourceData = this.dataArrays[this.dataArrayIndex]; - // FIXME sourceData.length != 0 may not be a good solution and may end up breaking issues down the line, but fixes exceptions being fired here - if (sourceData != null && sourceData.length != 0) - { - long[] newData = new long[sourceData.length]; - for (int i = 0; i < newData.length; i++) - { - newData[i] = FullDataPointUtil.remap(remappedEntryIds, sourceData[i]); - } - target.dataArrays[target.dataArrayIndex] = newData; - } - } - } - - /** - * Replaces this column's data with data from the input {@link IFullDataAccessor}.
- * This is used to convert higher detail LOD data to lower detail LOD data. - */ - public void downsampleFrom(IFullDataAccessor source) - { - //TODO: average the data instead of just picking the first column - SingleColumnFullDataAccessor firstColumn = source.get(0); - firstColumn.deepCopyTo(this); - } - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/loader/AbstractFullDataSourceLoader.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/loader/AbstractFullDataSourceLoader.java deleted file mode 100644 index 406d64b97..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/loader/AbstractFullDataSourceLoader.java +++ /dev/null @@ -1,209 +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 . - */ - -package com.seibel.distanthorizons.core.dataObjects.fullData.loader; - -import com.google.common.collect.HashMultimap; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; -import com.seibel.distanthorizons.core.level.IDhLevel; -import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.sql.DataSourceDto; - -import java.io.IOException; -import java.util.*; -import java.util.concurrent.locks.ReentrantLock; - -public abstract class AbstractFullDataSourceLoader -{ - public static final HashMultimap, AbstractFullDataSourceLoader> LOADER_REGISTRY = HashMultimap.create(); - public static final HashMap> DATATYPE_REGISTRY = new HashMap<>(); - - - public final Class fullDataSourceClass; - - public final String datatype; - public final byte[] loaderSupportedVersions; - - /** used when pooling data sources */ - private final ArrayList cachedSources = new ArrayList<>(); - private final ReentrantLock cacheLock = new ReentrantLock(); - - - - //=============// - // constructor // - //=============// - - public AbstractFullDataSourceLoader(Class fullDataSourceClass, String datatype, byte[] loaderSupportedVersions) - { - this.datatype = datatype; - this.loaderSupportedVersions = loaderSupportedVersions; - Arrays.sort(loaderSupportedVersions); // sort to allow fast access - this.fullDataSourceClass = fullDataSourceClass; - - if (DATATYPE_REGISTRY.containsKey(datatype) && DATATYPE_REGISTRY.get(datatype) != fullDataSourceClass) - { - throw new IllegalArgumentException("Loader for datatype: [" + datatype + "] already registered with different class: " - + DATATYPE_REGISTRY.get(datatype) + " != " + fullDataSourceClass); - } - - Set loaders = LOADER_REGISTRY.get(fullDataSourceClass); - if (loaders.stream().anyMatch(other -> - { - // see if any loaderSupportsVersion conflicts with this one - for (byte otherVer : other.loaderSupportedVersions) - { - if (Arrays.binarySearch(loaderSupportedVersions, otherVer) >= 0) - { - return true; - } - } - return false; - })) - { - throw new IllegalArgumentException("Loader for class " + fullDataSourceClass + " that supports one of the version in " - + Arrays.toString(loaderSupportedVersions) + " already registered!"); - } - - DATATYPE_REGISTRY.put(datatype, fullDataSourceClass); - LOADER_REGISTRY.put(fullDataSourceClass, this); - } - - - - //================// - // loader getters // - //================// - - public static AbstractFullDataSourceLoader getLoader(String dataType, byte dataVersion) - { - return LOADER_REGISTRY.get(DATATYPE_REGISTRY.get(dataType)).stream() - .filter(loader -> Arrays.binarySearch(loader.loaderSupportedVersions, dataVersion) >= 0) - .findFirst().orElse(null); - } - - public static AbstractFullDataSourceLoader getLoader(Class clazz, byte dataVersion) - { - return LOADER_REGISTRY.get(clazz).stream() - .filter(loader -> Arrays.binarySearch(loader.loaderSupportedVersions, dataVersion) >= 0) - .findFirst().orElse(null); - } - - - - //==================// - // abstract methods // - //==================// - - protected abstract IFullDataSource createEmptyDataSource(DhSectionPos pos); - - - - //==============// - // data loading // - //==============// - - /** Should be used in conjunction with {@link AbstractFullDataSourceLoader#returnPooledDataSource} to return the pooled sources. */ - public IFullDataSource loadTemporaryDataSource(DataSourceDto dto, IDhLevel level) throws IOException, InterruptedException - { - IFullDataSource dataSource = this.tryGetPooledSource(); - if (dataSource != null) - { - dataSource.repopulateFromStream(dto, dto.getInputStream(), level); - } - else - { - dataSource = this.loadDataSource(dto, level); - } - - return dataSource; - } - - /** - * Can return null if any of the requirements aren't met. - * - * @throws InterruptedException if the loader thread is interrupted, generally happens when the level is shutting down - */ - public IFullDataSource loadDataSource(DataSourceDto dto, IDhLevel level) throws IOException, InterruptedException - { - IFullDataSource dataSource = this.createEmptyDataSource(dto.pos); - dataSource.populateFromStream(dto, dto.getInputStream(), level); - return dataSource; - } - - - - //=====================// - // data source pooling // - //=====================// - - /** @return null if no pooled source exists */ - public IFullDataSource tryGetPooledSource() - { - try - { - this.cacheLock.lock(); - - int index = this.cachedSources.size() - 1; - if (index == -1) - { - return null; - } - else - { - return this.cachedSources.remove(index); - } - } - finally - { - this.cacheLock.unlock(); - } - } - - /** - * Doesn't have to be called, if a data source isn't returned, nothing will be leaked. - * It just means a new source must be constructed next time {@link AbstractFullDataSourceLoader#tryGetPooledSource} is called. - */ - public void returnPooledDataSource(IFullDataSource dataSource) - { - if (dataSource == null) - { - return; - } - else if (dataSource.getClass() != this.fullDataSourceClass) - { - return; - } - else if (this.cachedSources.size() > 25) - { - return; - } - - try - { - this.cacheLock.lock(); - this.cachedSources.add(dataSource); - } - finally - { - this.cacheLock.unlock(); - } - } - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/loader/CompleteFullDataSourceLoader.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/loader/CompleteFullDataSourceLoader.java deleted file mode 100644 index d9088d20d..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/loader/CompleteFullDataSourceLoader.java +++ /dev/null @@ -1,33 +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 . - */ - -package com.seibel.distanthorizons.core.dataObjects.fullData.loader; - -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; -import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.CompleteFullDataSource; - -public class CompleteFullDataSourceLoader extends AbstractFullDataSourceLoader -{ - public CompleteFullDataSourceLoader() { super(CompleteFullDataSource.class, CompleteFullDataSource.DATA_TYPE_NAME, new byte[]{CompleteFullDataSource.DATA_FORMAT_VERSION}); } - - @Override - protected IFullDataSource createEmptyDataSource(DhSectionPos pos) { return CompleteFullDataSource.createEmpty(pos); } - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/loader/HighDetailIncompleteFullDataSourceLoader.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/loader/HighDetailIncompleteFullDataSourceLoader.java deleted file mode 100644 index 9dcfb4f58..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/loader/HighDetailIncompleteFullDataSourceLoader.java +++ /dev/null @@ -1,33 +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 . - */ - -package com.seibel.distanthorizons.core.dataObjects.fullData.loader; - -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; -import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.HighDetailIncompleteFullDataSource; - -public class HighDetailIncompleteFullDataSourceLoader extends AbstractFullDataSourceLoader -{ - public HighDetailIncompleteFullDataSourceLoader() { super(HighDetailIncompleteFullDataSource.class, HighDetailIncompleteFullDataSource.DATA_TYPE_NAME, new byte[]{HighDetailIncompleteFullDataSource.DATA_FORMAT_VERSION}); } - - @Override - protected IFullDataSource createEmptyDataSource(DhSectionPos pos) { return HighDetailIncompleteFullDataSource.createEmpty(pos); } - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/loader/LowDetailIncompleteFullDataSourceLoader.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/loader/LowDetailIncompleteFullDataSourceLoader.java deleted file mode 100644 index 49793c958..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/loader/LowDetailIncompleteFullDataSourceLoader.java +++ /dev/null @@ -1,33 +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 . - */ - -package com.seibel.distanthorizons.core.dataObjects.fullData.loader; - -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; -import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.LowDetailIncompleteFullDataSource; - -public class LowDetailIncompleteFullDataSourceLoader extends AbstractFullDataSourceLoader -{ - public LowDetailIncompleteFullDataSourceLoader() { super(LowDetailIncompleteFullDataSource.class, LowDetailIncompleteFullDataSource.DATA_TYPE_NAME, new byte[]{LowDetailIncompleteFullDataSource.DATA_FORMAT_VERSION}); } - - @Override - protected IFullDataSource createEmptyDataSource(DhSectionPos pos) { return LowDetailIncompleteFullDataSource.createEmpty(pos); } - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/CompleteFullDataSource.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/CompleteFullDataSource.java deleted file mode 100644 index c0edc5612..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/CompleteFullDataSource.java +++ /dev/null @@ -1,447 +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 . - */ - -package com.seibel.distanthorizons.core.dataObjects.fullData.sources; - -import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.FullDataArrayAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.SingleColumnFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IStreamableFullDataSource; -import com.seibel.distanthorizons.core.level.IDhLevel; -import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; -import com.seibel.distanthorizons.core.pos.DhBlockPos2D; -import com.seibel.distanthorizons.core.pos.DhLodPos; -import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.sql.DataSourceDto; -import com.seibel.distanthorizons.core.util.FullDataPointUtil; -import com.seibel.distanthorizons.core.util.LodUtil; -import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream; -import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataOutputStream; -import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; -import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; -import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; -import org.apache.logging.log4j.Logger; - -import java.io.*; -import java.util.Arrays; - -/** - * This data source contains every datapoint over its given {@link DhSectionPos}. - * - * @see FullDataPointUtil - * @see LowDetailIncompleteFullDataSource - * @see HighDetailIncompleteFullDataSource - */ -public class CompleteFullDataSource extends FullDataArrayAccessor implements IFullDataSource, IStreamableFullDataSource -{ - private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - - public static final byte SECTION_SIZE_OFFSET = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; - /** measured in dataPoints */ - public static final int WIDTH = BitShiftUtil.powerOfTwo(SECTION_SIZE_OFFSET); - - public static final byte DATA_FORMAT_VERSION = 3; - public static final String DATA_TYPE_NAME = "CompleteFullDataSource"; - @Override - public String getDataTypeName() { return DATA_TYPE_NAME; } - - private DhSectionPos sectionPos; - - private boolean isEmpty = true; - public EDhApiWorldGenerationStep worldGenStep = EDhApiWorldGenerationStep.EMPTY; - - - - //==============// - // constructors // - //==============// - - public static CompleteFullDataSource createEmpty(DhSectionPos pos) { return new CompleteFullDataSource(pos); } - private CompleteFullDataSource(DhSectionPos sectionPos) - { - super(new FullDataPointIdMap(sectionPos), new long[WIDTH * WIDTH][0], WIDTH); - this.sectionPos = sectionPos; - } - - public CompleteFullDataSource(DhSectionPos pos, FullDataPointIdMap mapping, long[][] data) - { - super(mapping, data, WIDTH); - LodUtil.assertTrue(data.length == WIDTH * WIDTH); - - this.sectionPos = pos; - this.isEmpty = false; - } - - - - //=================// - // stream handling // - //=================// - - @Override - public void writeSourceSummaryInfo(IDhLevel level, DhDataOutputStream outputStream) throws IOException - { - outputStream.writeInt(this.getDataDetailLevel()); - outputStream.writeInt(this.width); - outputStream.writeInt(level.getMinY()); - outputStream.writeByte(this.worldGenStep.value); - - } - @Override - public FullDataSourceSummaryData readSourceSummaryInfo(DataSourceDto dto, DhDataInputStream inputStream, IDhLevel level) throws IOException - { - int dataDetail = inputStream.readInt(); - if (dataDetail != dto.dataDetailLevel) - { - throw new IOException(LodUtil.formatLog("Data level mismatch. Expected: ["+dto.dataDetailLevel+"], found ["+dataDetail+"].")); - } - - int width = inputStream.readInt(); - if (width != WIDTH) - { - throw new IOException(LodUtil.formatLog("Section width mismatch: " + width + " != " + WIDTH + " (Currently only 1 section width is supported)")); - } - - int minY = inputStream.readInt(); - if (minY != level.getMinY()) - { - LOGGER.warn("Data minY mismatch: " + minY + " != " + level.getMinY() + ". Will ignore data's y level"); - } - - byte worldGenByte = inputStream.readByte(); - EDhApiWorldGenerationStep worldGenStep = EDhApiWorldGenerationStep.fromValue(worldGenByte); - if (worldGenStep == null) - { - worldGenStep = EDhApiWorldGenerationStep.SURFACE; - LOGGER.warn("Missing WorldGenStep, defaulting to: " + worldGenStep.name()); - } - - - return new FullDataSourceSummaryData(width, worldGenStep); - } - public void setSourceSummaryData(FullDataSourceSummaryData summaryData) - { - this.worldGenStep = summaryData.worldGenStep; - } - - - @Override - public boolean writeDataPoints(DhDataOutputStream outputStream) throws IOException - { - if (this.isEmpty()) - { - outputStream.writeInt(IFullDataSource.NO_DATA_FLAG_BYTE); - return false; - } - outputStream.writeInt(IFullDataSource.DATA_GUARD_BYTE); - - - - // Data array length - for (int x = 0; x < this.width; x++) - { - for (int z = 0; z < this.width; z++) - { - outputStream.writeInt(this.get(x, z).getSingleLength()); - } - } - - - - // Data array content (only on non-empty columns) - outputStream.writeInt(IFullDataSource.DATA_GUARD_BYTE); - for (int x = 0; x < this.width; x++) - { - for (int z = 0; z < this.width; z++) - { - SingleColumnFullDataAccessor columnAccessor = this.get(x, z); - if (columnAccessor.doesColumnExist()) - { - long[] dataPointArray = columnAccessor.getRaw(); - for (long dataPoint : dataPointArray) - { - outputStream.writeLong(dataPoint); - } - } - } - } - - - return true; - } - @Override - public long[][] readDataPoints(DataSourceDto dto, int width, DhDataInputStream dataInputStream) throws IOException - { - // Data array length - int dataPresentFlag = dataInputStream.readInt(); - if (dataPresentFlag == IFullDataSource.NO_DATA_FLAG_BYTE) - { - // Section is empty - return null; - } - else if (dataPresentFlag != IFullDataSource.DATA_GUARD_BYTE) - { - throw new IOException("Invalid file format. Data Points guard byte expected: (no data) [" + IFullDataSource.NO_DATA_FLAG_BYTE + "] or (data present) [" + IFullDataSource.DATA_GUARD_BYTE + "], but found [" + dataPresentFlag + "]."); - } - - - - long[][] dataPointArrays; - if (this.width == width) // attempt to use the existing dataArrays if possible - { - dataPointArrays = this.dataArrays; - } - else - { - dataPointArrays = new long[width * width][]; - } - - for (int x = 0; x < width; x++) - { - for (int z = 0; z < width; z++) - { - int requestedArrayLength = dataInputStream.readInt(); - int arrayIndex = x * width + z; - - // attempt to use the existing dataArrays if possible - if (dataPointArrays[arrayIndex] == null || dataPointArrays[arrayIndex].length != requestedArrayLength) - { - dataPointArrays[arrayIndex] = new long[requestedArrayLength]; - } - else - { - // clear the existing array to prevent any data leakage - Arrays.fill(dataPointArrays[arrayIndex], 0); - } - } - } - - - - // check if the array start flag is present - int arrayStartFlag = dataInputStream.readInt(); - if (arrayStartFlag != IFullDataSource.DATA_GUARD_BYTE) - { - throw new IOException("invalid data length end guard"); - } - - for (int xz = 0; xz < dataPointArrays.length; xz++) // x and z are combined - { - if (dataPointArrays[xz].length != 0) - { - for (int y = 0; y < dataPointArrays[xz].length; y++) - { - dataPointArrays[xz][y] = dataInputStream.readLong(); - } - } - } - - - - return dataPointArrays; - } - @Override - public void setDataPoints(long[][] dataPoints) - { - LodUtil.assertTrue(this.dataArrays.length == dataPoints.length, "Data point array length mismatch."); - - this.isEmpty = false; - System.arraycopy(dataPoints, 0, this.dataArrays, 0, dataPoints.length); - } - - - @Override - public void writeIdMappings(DhDataOutputStream outputStream) throws IOException - { - outputStream.writeInt(IFullDataSource.DATA_GUARD_BYTE); - this.mapping.serialize(outputStream); - } - @Override - public FullDataPointIdMap readIdMappings(long[][] dataPoints, DhDataInputStream inputStream, ILevelWrapper levelWrapper) throws IOException, InterruptedException - { - int guardByte = inputStream.readInt(); - if (guardByte != IFullDataSource.DATA_GUARD_BYTE) - { - throw new IOException("Invalid data content end guard for ID mapping"); - } - - return FullDataPointIdMap.deserialize(inputStream, this.sectionPos, levelWrapper); - } - @Override - public void setIdMapping(FullDataPointIdMap mappings) { this.mapping.mergeAndReturnRemappedEntityIds(mappings); } - - - - //======// - // data // - //======// - - @Override - public SingleColumnFullDataAccessor tryGet(int relativeX, int relativeZ) { return this.get(relativeX, relativeZ); } - @Override - public SingleColumnFullDataAccessor getOrCreate(int relativeX, int relativeZ) { return this.get(relativeX, relativeZ); } - - @Override - public void update(ChunkSizedFullDataAccessor chunkDataView) - { - LodUtil.assertTrue(this.sectionPos.overlapsExactly(chunkDataView.getSectionPos())); - if (this.getDataDetailLevel() == LodUtil.BLOCK_DETAIL_LEVEL) - { - DhBlockPos2D chunkBlockPos = new DhBlockPos2D(chunkDataView.chunkPos.x * LodUtil.CHUNK_WIDTH, chunkDataView.chunkPos.z * LodUtil.CHUNK_WIDTH); - DhBlockPos2D blockOffset = chunkBlockPos.subtract(this.sectionPos.getMinCornerLodPos().getCornerBlockPos()); - LodUtil.assertTrue(blockOffset.x >= 0 && blockOffset.x < WIDTH && blockOffset.z >= 0 && blockOffset.z < WIDTH); - this.isEmpty = false; - - chunkDataView.shadowCopyTo(this.subView(LodUtil.CHUNK_WIDTH, blockOffset.x, blockOffset.z)); - - // DEBUG ASSERTION - { - for (int x = 0; x < LodUtil.CHUNK_WIDTH; x++) - { - for (int z = 0; z < LodUtil.CHUNK_WIDTH; z++) - { - SingleColumnFullDataAccessor column = this.get(x + blockOffset.x, z + blockOffset.z); - LodUtil.assertTrue(column.doesColumnExist()); - } - } - } - } - else if (this.getDataDetailLevel() < LodUtil.CHUNK_DETAIL_LEVEL) - { - int dataPerFull = 1 << this.getDataDetailLevel(); - int fullSize = LodUtil.CHUNK_WIDTH / dataPerFull; - DhLodPos dataOffset = chunkDataView.getSectionPos().getMinCornerLodPos(this.getDataDetailLevel()); - DhLodPos baseOffset = this.sectionPos.getMinCornerLodPos(this.getDataDetailLevel()); - - int offsetX = dataOffset.x - baseOffset.x; - int offsetZ = dataOffset.z - baseOffset.z; - LodUtil.assertTrue(offsetX >= 0 && offsetX < WIDTH && offsetZ >= 0 && offsetZ < WIDTH); - - this.isEmpty = false; - for (int xOffset = 0; xOffset < fullSize; xOffset++) - { - for (int zOffset = 0; zOffset < fullSize; zOffset++) - { - SingleColumnFullDataAccessor column = this.get(xOffset + offsetX, zOffset + offsetZ); - column.downsampleFrom(chunkDataView.subView(dataPerFull, xOffset * dataPerFull, zOffset * dataPerFull)); - } - } - } - else if (this.getDataDetailLevel() >= LodUtil.CHUNK_DETAIL_LEVEL) - { - //FIXME: TEMPORARY - int chunkPerFull = 1 << (this.getDataDetailLevel() - LodUtil.CHUNK_DETAIL_LEVEL); - if (chunkDataView.chunkPos.x % chunkPerFull != 0 || chunkDataView.chunkPos.z % chunkPerFull != 0) - { - return; - } - - DhLodPos baseOffset = this.sectionPos.getMinCornerLodPos(this.getDataDetailLevel()); - DhSectionPos dataOffset = chunkDataView.getSectionPos().convertNewToDetailLevel(this.getDataDetailLevel()); - - int offsetX = dataOffset.getX() - baseOffset.x; - int offsetZ = dataOffset.getZ() - baseOffset.z; - LodUtil.assertTrue(offsetX >= 0 && offsetX < WIDTH && offsetZ >= 0 && offsetZ < WIDTH); - - this.isEmpty = false; - chunkDataView.get(0, 0).deepCopyTo(this.get(offsetX, offsetZ)); - } - else - { - LodUtil.assertNotReach(); - //TODO - } - - } - - - - //================// - // helper methods // - //================// - - /** Returns whether data at the given posToWrite can effect the target region file at posToTest. */ - public static boolean firstDataPosCanAffectSecond(DhSectionPos posToWrite, DhSectionPos posToTest) - { - if (!posToWrite.overlapsExactly(posToTest)) - { - // the testPosition is outside the writePosition - return false; - } - else if (posToTest.getDetailLevel() > posToWrite.getDetailLevel()) - { - // the testPosition is larger (aka is less detailed) than the writePosition, - // more detailed sections shouldn't be updated by lower detail sections - return false; - } - else if (posToWrite.getDetailLevel() - posToTest.getDetailLevel() <= SECTION_SIZE_OFFSET) - { - // if the difference in detail levels is very large, the posToWrite - // may be skipped, due to how we sample large detail levels by only - // getting the corners. - - // In this case the difference isn't very large, so return true - return true; - } - else - { - // the difference in detail levels is very large, - // check if the posToWrite is in a corner of posToTest - byte sectPerData = (byte) BitShiftUtil.powerOfTwo(posToWrite.getDetailLevel() - posToTest.getDetailLevel() - SECTION_SIZE_OFFSET); - LodUtil.assertTrue(sectPerData != 0); - return posToTest.getX() % sectPerData == 0 && posToTest.getZ() % sectPerData == 0; - } - } - - - - //=====================// - // setters and getters // - //=====================// - - @Override - public DhSectionPos getSectionPos() { return this.sectionPos; } - - @Override - public void resizeDataStructuresForRepopulation(DhSectionPos pos) - { - // no data structures need to be changed, only the source's position - this.sectionPos = pos; - } - - @Override - public byte getDataDetailLevel() { return (byte) (this.sectionPos.getDetailLevel() - SECTION_SIZE_OFFSET); } - - @Override - public byte getDataFormatVersion() { return DATA_FORMAT_VERSION; } - - @Override - public EDhApiWorldGenerationStep getWorldGenStep() { return this.worldGenStep; } - - @Override - public boolean isEmpty() { return this.isEmpty; } - @Override - public void markNotEmpty() { this.isEmpty = false; } - - @Override - public int getWidthInDataPoints() { return this.width; } - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/FullDataSourceV1.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/FullDataSourceV1.java new file mode 100644 index 000000000..37cdf0452 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/FullDataSourceV1.java @@ -0,0 +1,409 @@ +/* + * 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 . + */ + +package com.seibel.distanthorizons.core.dataObjects.fullData.sources; + +import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; +import com.seibel.distanthorizons.core.file.IDataSource; +import com.seibel.distanthorizons.core.level.IDhLevel; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV1DTO; +import com.seibel.distanthorizons.core.util.FullDataPointUtil; +import com.seibel.distanthorizons.core.util.LodUtil; +import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream; +import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataOutputStream; +import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; +import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; +import org.apache.logging.log4j.Logger; + +import java.io.*; +import java.util.Arrays; + +/** + * Formerly "CompleteFullDataSource".
+ * Should be fully populated, containing 1 data point for each column.

+ * + * Replaced by {@link FullDataSourceV2}. + * + * @see FullDataPointUtil + * @see FullDataSourceV2 + */ +public class FullDataSourceV1 implements IDataSource +{ + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + + public static final byte SECTION_SIZE_OFFSET = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; + /** measured in dataPoints */ + public static final int WIDTH = BitShiftUtil.powerOfTwo(SECTION_SIZE_OFFSET); + + public static final byte DATA_FORMAT_VERSION = 3; + /** never used but should stay here. */ + public static final String DATA_TYPE_NAME = "CompleteFullDataSource"; + + /** + * This is the byte put between different sections in the binary save file. + * The presence and absence of this byte indicates if the file is correctly formatted. + */ + private static final int DATA_GUARD_BYTE = 0xFFFFFFFF; + /** indicates the binary save file represents an empty data source */ + private static final int NO_DATA_FLAG_BYTE = 0x00000001; + + + public final FullDataPointIdMap mapping; + public EDhApiWorldGenerationStep worldGenStep = EDhApiWorldGenerationStep.EMPTY; + + + /** A flattened 2D array (for the X and Z directions) containing an array for the Y direction. */ + private final long[][] dataArrays; + + private DhSectionPos sectionPos; + + private boolean isEmpty = true; + + + + //==============// + // constructors // + //==============// + + public static FullDataSourceV1 createEmpty(DhSectionPos pos) { return new FullDataSourceV1(pos); } + private FullDataSourceV1(DhSectionPos sectionPos) + { + this.dataArrays = new long[WIDTH * WIDTH][0]; + this.mapping = new FullDataPointIdMap(sectionPos); + this.sectionPos = sectionPos; + } + + + + + //======// + // data // + //======// + + @Deprecated + @Override + public boolean update(FullDataSourceV2 dataSource, IDhLevel level) { throw new UnsupportedOperationException("Deprecated"); } + + + + //=====================// + // setters and getters // + //=====================// + + @Override + public DhSectionPos getKey() { return this.sectionPos; } + + @Override + public DhSectionPos getPos() { return this.sectionPos; } + + public void resizeDataStructuresForRepopulation(DhSectionPos pos) + { + // no data structures need to be changed, only the source's position + this.sectionPos = pos; + } + + @Override + public byte getDataDetailLevel() { return (byte) (this.sectionPos.getDetailLevel() - SECTION_SIZE_OFFSET); } + + public boolean isEmpty() { return this.isEmpty; } + + + public long[] get(int index) { return this.get(index / WIDTH, index % WIDTH); } + public long[] get(int relativeX, int relativeZ) + { + int dataArrayIndex = (relativeX * WIDTH) + relativeZ; + if (dataArrayIndex >= this.dataArrays.length) + { + LodUtil.assertNotReach( + "FullDataArrayAccessor.get() called with a relative position that is outside the data source. \n" + + "given relative pos X: [" + relativeX + "] Z: [" + relativeZ + "]\n" + + "dataArrays.length: [" + this.dataArrays.length + "] dataArrayIndex: [" + dataArrayIndex + "]."); + } + + return this.dataArrays[dataArrayIndex]; + } + + + + //=================// + // stream handling // + //=================// + + /** + * Clears and then overwrites any data in this object with the data from the given file and stream. + * This is expected to be used with an existing {@link FullDataSourceV1} and can be used in place of a constructor to reuse an existing {@link FullDataSourceV1} object. + */ + public void repopulateFromStream(FullDataSourceV1DTO dto, DhDataInputStream inputStream, IDhLevel level) throws IOException, InterruptedException + { + // clear/overwrite the old data + this.resizeDataStructuresForRepopulation(dto.pos); + this.mapping.clear(dto.pos); + + // set the new data + this.populateFromStream(dto, inputStream, level); + } + + /** + * Overwrites any data in this object with the data from the given file and stream. + * This is expected to be used with an empty {@link FullDataSourceV1} and functions similar to a constructor. + */ + public void populateFromStream(FullDataSourceV1DTO dto, DhDataInputStream inputStream, IDhLevel level) throws IOException, InterruptedException + { + FullDataSourceSummaryData summaryData = this.readSourceSummaryInfo(dto, inputStream, level); + this.setSourceSummaryData(summaryData); + + + long[][] dataPoints = this.readDataPoints(summaryData.dataWidth, inputStream); + if (dataPoints == null) + { + return; + } + this.setDataPoints(dataPoints); + + + FullDataPointIdMap mapping = this.readIdMappings(inputStream, level.getLevelWrapper()); + this.setIdMapping(mapping); + + } + + + // low level stream methods // + + /** unused, just here for reference as to how the data was written */ + @Deprecated + public void writeSourceSummaryInfo(IDhLevel level, DhDataOutputStream outputStream) throws IOException + { + outputStream.writeInt(this.getDataDetailLevel()); + outputStream.writeInt(WIDTH); + outputStream.writeInt(level.getMinY()); + outputStream.writeByte(this.worldGenStep.value); + + } + public FullDataSourceSummaryData readSourceSummaryInfo(FullDataSourceV1DTO dto, DhDataInputStream inputStream, IDhLevel level) throws IOException + { + int dataDetail = inputStream.readInt(); + if (dataDetail != dto.dataDetailLevel) + { + throw new IOException(LodUtil.formatLog("Data level mismatch. Expected: ["+dto.dataDetailLevel+"], found ["+dataDetail+"].")); + } + + int width = inputStream.readInt(); + if (width != WIDTH) + { + throw new IOException(LodUtil.formatLog("Section width mismatch: " + width + " != " + WIDTH + " (Currently only 1 section width is supported)")); + } + + int minY = inputStream.readInt(); + if (minY != level.getMinY()) + { + LOGGER.warn("Data minY mismatch: " + minY + " != " + level.getMinY() + ". Will ignore data's y level"); + } + + byte worldGenByte = inputStream.readByte(); + EDhApiWorldGenerationStep worldGenStep = EDhApiWorldGenerationStep.fromValue(worldGenByte); + if (worldGenStep == null) + { + worldGenStep = EDhApiWorldGenerationStep.SURFACE; + LOGGER.warn("Missing WorldGenStep, defaulting to: " + worldGenStep.name()); + } + + + return new FullDataSourceSummaryData(width, worldGenStep); + } + public void setSourceSummaryData(FullDataSourceSummaryData summaryData) { this.worldGenStep = summaryData.worldGenStep; } + + /** unused, just here for reference as to how the data was written */ + @Deprecated + public boolean writeDataPoints(DhDataOutputStream outputStream) throws IOException + { + if (this.isEmpty()) + { + outputStream.writeInt(NO_DATA_FLAG_BYTE); + return false; + } + outputStream.writeInt(DATA_GUARD_BYTE); + + + + // Data array length + for (int x = 0; x < WIDTH; x++) + { + for (int z = 0; z < WIDTH; z++) + { + outputStream.writeInt(this.get(x, z).length); + } + } + + + + // Data array content (only on non-empty columns) + outputStream.writeInt(DATA_GUARD_BYTE); + for (int x = 0; x < WIDTH; x++) + { + for (int z = 0; z < WIDTH; z++) + { + long[] dataColumn = this.get(x, z); + if (dataColumn != null) + { + for (long dataPoint : dataColumn) + { + outputStream.writeLong(dataPoint); + } + } + } + } + + + return true; + } + public long[][] readDataPoints(int width, DhDataInputStream dataInputStream) throws IOException + { + // Data array length + int dataPresentFlag = dataInputStream.readInt(); + if (dataPresentFlag == NO_DATA_FLAG_BYTE) + { + // Section is empty + return null; + } + else if (dataPresentFlag != DATA_GUARD_BYTE) + { + throw new IOException("Invalid file format. Data Points guard byte expected: (no data) [" + NO_DATA_FLAG_BYTE + "] or (data present) [" + DATA_GUARD_BYTE + "], but found [" + dataPresentFlag + "]."); + } + + + + long[][] dataPointArrays; + if (WIDTH == width) // attempt to use the existing dataArrays if possible + { + dataPointArrays = this.dataArrays; + } + else + { + dataPointArrays = new long[width * width][]; + } + + for (int x = 0; x < width; x++) + { + for (int z = 0; z < width; z++) + { + int requestedArrayLength = dataInputStream.readInt(); + int arrayIndex = x * width + z; + + // attempt to use the existing dataArrays if possible + if (dataPointArrays[arrayIndex] == null || dataPointArrays[arrayIndex].length != requestedArrayLength) + { + dataPointArrays[arrayIndex] = new long[requestedArrayLength]; + } + else + { + // clear the existing array to prevent any data leakage + Arrays.fill(dataPointArrays[arrayIndex], 0); + } + } + } + + + + // check if the array start flag is present + int arrayStartFlag = dataInputStream.readInt(); + if (arrayStartFlag != DATA_GUARD_BYTE) + { + throw new IOException("invalid data length end guard"); + } + + for (int xz = 0; xz < dataPointArrays.length; xz++) // x and z are combined + { + if (dataPointArrays[xz].length != 0) + { + for (int y = 0; y < dataPointArrays[xz].length; y++) + { + dataPointArrays[xz][y] = dataInputStream.readLong(); + } + } + } + + + + return dataPointArrays; + } + public void setDataPoints(long[][] dataPoints) + { + LodUtil.assertTrue(this.dataArrays.length == dataPoints.length, "Data point array length mismatch."); + + this.isEmpty = false; + System.arraycopy(dataPoints, 0, this.dataArrays, 0, dataPoints.length); + } + + + /** unused, just here for reference as to how the data was written */ + @Deprecated + public void writeIdMappings(DhDataOutputStream outputStream) throws IOException + { + outputStream.writeInt(DATA_GUARD_BYTE); + this.mapping.serialize(outputStream); + } + public FullDataPointIdMap readIdMappings(DhDataInputStream inputStream, ILevelWrapper levelWrapper) throws IOException, InterruptedException + { + int guardByte = inputStream.readInt(); + if (guardByte != DATA_GUARD_BYTE) + { + throw new IOException("Invalid data content end guard for ID mapping"); + } + + return FullDataPointIdMap.deserialize(inputStream, this.sectionPos, levelWrapper); + } + public void setIdMapping(FullDataPointIdMap mappings) { this.mapping.mergeAndReturnRemappedEntityIds(mappings); } + + + //==================// + // override methods // + //==================// + + @Override + public void close() throws Exception + { /* not currently needed */ } + + + + //================// + // helper classes // + //================// + + /** + * This holds information that is relevant to the entire source and isn't stored in the data points.
+ * Example: minimum height, detail level, source type, etc. + */ + private static class FullDataSourceSummaryData + { + public final int dataWidth; + public EDhApiWorldGenerationStep worldGenStep; + + + public FullDataSourceSummaryData(int dataWidth, EDhApiWorldGenerationStep worldGenStep) + { + this.dataWidth = dataWidth; + this.worldGenStep = worldGenStep; + } + + } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/FullDataSourceV2.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/FullDataSourceV2.java new file mode 100644 index 000000000..8846ba085 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/FullDataSourceV2.java @@ -0,0 +1,933 @@ +/* + * 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 . + */ + +package com.seibel.distanthorizons.core.dataObjects.fullData.sources; + +import com.seibel.distanthorizons.api.enums.config.EDhApiWorldCompressionMode; +import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; +import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; +import com.seibel.distanthorizons.core.dataObjects.transformers.LodDataBuilder; +import com.seibel.distanthorizons.core.file.AbstractDataSourceHandler; +import com.seibel.distanthorizons.core.file.DataSourcePool; +import com.seibel.distanthorizons.core.file.IDataSource; +import com.seibel.distanthorizons.core.level.IDhLevel; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.util.FullDataPointUtil; +import com.seibel.distanthorizons.core.util.LodUtil; +import com.seibel.distanthorizons.core.util.RenderDataPointUtil; +import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; +import com.seibel.distanthorizons.coreapi.ModInfo; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; +import java.util.Arrays; + +/** + * This data source contains every datapoint over its given {@link DhSectionPos}.

+ * + * @see FullDataPointUtil + * @see FullDataSourceV1 + */ +public class FullDataSourceV2 implements IDataSource +{ + 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. */ + private static final boolean RUN_UPDATE_DEV_VALIDATION = false; + /** + * If the data column order isn't correct + * block lighting may appear broken + * and/or certain detail level LODs may not appear at all. + */ + private static final boolean RUN_DATA_ORDER_VALIDATION = ModInfo.IS_DEV_BUILD; + + /** measured in data columns */ + public static final int WIDTH = 64; + + public static final byte DATA_FORMAT_VERSION = 1; + + public static final DataSourcePool DATA_SOURCE_POOL = new DataSourcePool<>(FullDataSourceV2::createEmpty, FullDataSourceV2::prepPooledDataSource); + + + + private int cachedHashCode = 0; + + private DhSectionPos pos; + @Override + public DhSectionPos getKey() { return this.pos; } + + + public final FullDataPointIdMap mapping; + + + public long lastModifiedUnixDateTime; + public long createdUnixDateTime; + + public int levelMinY; + + /** + * stores how far each column has been generated should start with {@link EDhApiWorldGenerationStep#EMPTY} + * @see EDhApiWorldGenerationStep + */ + public byte[] columnGenerationSteps; + /** + * stores what world compression was used for each column. + * @see EDhApiWorldCompressionMode + */ + public byte[] columnWorldCompressionMode; + + /** + * stored x/z, y
+ * The y data should be sorted from top to bottom
+ * TODO that ordering feels weird, it'd be nice to reverse that order, unfortunately + * there's something in the render data logic that expects this order so we can't change it right now + */ + public LongArrayList[] dataPoints; + + public boolean isEmpty; + public boolean applyToParent = false; + + + + //==============// + // constructors // + //==============// + + public static FullDataSourceV2 createEmpty(DhSectionPos pos) { return new FullDataSourceV2(pos); } + private FullDataSourceV2(DhSectionPos pos) + { + this.pos = pos; + this.dataPoints = new LongArrayList[WIDTH * WIDTH]; + this.mapping = new FullDataPointIdMap(pos); + this.isEmpty = true; + + // doesn't need to be populated since nothing has been generated yet + // the default value of all 0's is adequate + this.columnGenerationSteps = new byte[WIDTH * WIDTH]; + this.columnWorldCompressionMode = new byte[WIDTH * WIDTH]; + } + + public static FullDataSourceV2 createWithData(DhSectionPos pos, FullDataPointIdMap mapping, LongArrayList[] data, byte[] columnGenerationStep, byte[] columnWorldCompressionMode) { return new FullDataSourceV2(pos, mapping, data, columnGenerationStep, columnWorldCompressionMode); } + private FullDataSourceV2(DhSectionPos pos, FullDataPointIdMap mapping, LongArrayList[] data, byte[] columnGenerationSteps, byte[] columnWorldCompressionMode) + { + LodUtil.assertTrue(data.length == WIDTH * WIDTH); + + this.pos = pos; + this.dataPoints = data; + this.mapping = mapping; + this.isEmpty = false; + + this.columnGenerationSteps = columnGenerationSteps; + this.columnWorldCompressionMode = columnWorldCompressionMode; + } + + public static FullDataSourceV2 createFromChunk(IChunkWrapper chunkWrapper) { return LodDataBuilder.createGeneratedDataSource(chunkWrapper); } + + public static FullDataSourceV2 createFromLegacyDataSourceV1(FullDataSourceV1 legacyData) + { + if (FullDataSourceV1.WIDTH != WIDTH) + { + throw new UnsupportedOperationException( + "Unable to convert ["+FullDataSourceV1.class.getSimpleName()+"] into ["+FullDataSourceV2.class.getSimpleName()+"]. " + + "Data sources have different data point widths and no converter is present. " + + "input width ["+ FullDataSourceV1.WIDTH+"], recipient width ["+WIDTH+"]."); + } + + + // Note: this logic only works if the data point data is the same between both versions + byte[] columnGenerationSteps = new byte[WIDTH * WIDTH]; + byte[] columnWorldCompressionMode = new byte[WIDTH * WIDTH]; + LongArrayList[] dataPoints = new LongArrayList[WIDTH * WIDTH]; + for (int x = 0; x < WIDTH; x++) + { + for (int z = 0; z < WIDTH; z++) + { + long[] legacyDataColumn = legacyData.get(x, z); + if (legacyDataColumn != null && legacyDataColumn.length != 0) + { + int index = relativePosToIndex(x, z); + LongArrayList newDataColumn = new LongArrayList(legacyDataColumn); + + + // convert the data point format + boolean columnHasNonAirBlock = false; + for (int i = 0; i < legacyDataColumn.length; i++) + { + long dataPoint = legacyDataColumn[i]; + + boolean isAir = legacyData.mapping.getBlockStateWrapper(FullDataPointUtil.getId(dataPoint)).isAir(); + byte blockLight = (byte) FullDataPointUtil.getBlockLight(dataPoint); + + if (isAir) + { + // air shouldn't have any light, otherwise down sampling will look weird + blockLight = 0; + } + + dataPoint = FullDataPointUtil.setBlockLight(dataPoint, blockLight); + newDataColumn.set(i, dataPoint); + + + // check if this datapoint is air + if (!columnHasNonAirBlock && !isAir) + { + columnHasNonAirBlock = true; + } + } + + + // save the converted data point + ensureDataColumnOrder(newDataColumn); + dataPoints[index] = newDataColumn; + + // the old data sources didn't have a generation step written down + // if the column has any data points, assume it's fully generated, otherwise assume it's empty + columnGenerationSteps[index] = (columnHasNonAirBlock ? EDhApiWorldGenerationStep.LIGHT.value : EDhApiWorldGenerationStep.EMPTY.value); + columnWorldCompressionMode[index] = EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS.value; + } + } + } + + FullDataSourceV2 fullDataSource = FullDataSourceV2.createWithData(legacyData.getPos(), legacyData.mapping, dataPoints, columnGenerationSteps, columnWorldCompressionMode); + return fullDataSource; + } + + + + //======// + // data // + //======// + + public LongArrayList get(int relX, int relZ) throws IndexOutOfBoundsException { return this.dataPoints[relativePosToIndex(relX, relZ)]; } + + @Override + public boolean update(@NotNull FullDataSourceV2 inputDataSource, @Nullable IDhLevel level) { return this.update(inputDataSource); } + public boolean update(@NotNull FullDataSourceV2 inputDataSource) + { + // don't try updating if the input is empty + if (inputDataSource.mapping.isEmpty()) + { + return false; + } + + + byte thisDetailLevel = this.pos.getDetailLevel(); + byte inputDetailLevel = inputDataSource.pos.getDetailLevel(); + + + // determine the mapping changes necessary for the input to map onto this datasource + int[] remappedIds = this.mapping.mergeAndReturnRemappedEntityIds(inputDataSource.mapping); + + boolean dataChanged; + if (inputDetailLevel == thisDetailLevel) + { + dataChanged = this.updateFromSameDetailLevel(inputDataSource, remappedIds); + } + else if (inputDetailLevel + 1 == thisDetailLevel) + { + dataChanged = this.updateFromOneBelowDetailLevel(inputDataSource, remappedIds); + } + else + { + // other detail levels aren't supported since it would be more difficult to maintain + // 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?) + // 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+"]."); + } + + // determine if this data source should be applied to its parent + this.applyToParent = (dataChanged && this.pos.getDetailLevel() < AbstractDataSourceHandler.TOP_SECTION_DETAIL_LEVEL); + + if (dataChanged) + { + // update the hash code + this.generateHashCode(); + } + + return dataChanged; + } + public boolean updateFromSameDetailLevel(FullDataSourceV2 inputDataSource, int[] remappedIds) + { + // both data sources should have the same detail level + if (inputDataSource.pos.getDetailLevel() != this.pos.getDetailLevel()) + { + throw new IllegalArgumentException("Both data sources must have the same detail level. Expected ["+this.pos.getDetailLevel()+"], received ["+inputDataSource.pos.getDetailLevel()+"]."); + } + + // copy over everything from the input data source into this one + // provided there is data to copy and the world generation step is the same or more complete + boolean dataChanged = false; + for (int x = 0; x < WIDTH; x++) + { + for (int z = 0; z < WIDTH; z++) + { + int index = relativePosToIndex(x, z); + + LongArrayList inputDataArray = inputDataSource.dataPoints[index]; + if (inputDataArray != null) + { + byte thisGenState = this.columnGenerationSteps[index]; + byte inputGenState = inputDataSource.columnGenerationSteps[index]; + + if (inputGenState != EDhApiWorldGenerationStep.EMPTY.value + && thisGenState <= inputGenState) + { + // check if the data changed + if (this.dataPoints[index] == null) + { + // no data was present previously + this.dataPoints[index] = new LongArrayList(inputDataArray); + dataChanged = true; + } + else if (this.dataPoints[index].size() != inputDataArray.size()) + { + // data is present, but the size is different + dataChanged = true; + } + + int oldDataHash = 0; + if (!dataChanged) + { + // some old data existed with the same length, + // we'll have to compare the caches + oldDataHash = this.dataPoints[index].hashCode(); + } + + + // copy over the new data + this.dataPoints[index].clear(); + this.dataPoints[index].addAll(inputDataArray); + this.remapDataColumn(index, remappedIds); + + if (RUN_DATA_ORDER_VALIDATION) + { + throwIfDataColumnInWrongOrder(inputDataSource.pos, this.dataPoints[index]); + } + + + + if (!dataChanged) + { + // check if the identical length data column hashes are the same + // hashes need to be compared after the ID's have been remapped otherwise the ID's won't match even if the data is the same + if (oldDataHash != this.dataPoints[index].hashCode()) + { + // the hashes are different, something was changed + dataChanged = true; + } + } + + + this.columnGenerationSteps[index] = inputGenState; + // always overwrite the compression mode since we're replacing this column + this.columnWorldCompressionMode[index] = inputDataSource.columnWorldCompressionMode[index]; + this.isEmpty = false; + } + } + } + } + + return dataChanged; + } + public boolean updateFromOneBelowDetailLevel(FullDataSourceV2 inputDataSource, int[] remappedIds) + { + if (inputDataSource.pos.getDetailLevel() + 1 != this.pos.getDetailLevel()) + { + throw new IllegalArgumentException("Input data source must be exactly 1 detail level below this data source. Expected [" + (this.pos.getDetailLevel() - 1) + "], received [" + inputDataSource.pos.getDetailLevel() + "]."); + } + + // input is one detail level lower (higher detail) + // so 2x2 input data points will be converted into 1 recipient data point + + + // determine where in the input data source should be written to + // since the input is one detail level below it will be one of this position's 4 children + int minChildXPos = this.pos.getChildByIndex(0).getX(); + int recipientOffsetX = (inputDataSource.pos.getX() == minChildXPos) ? 0 : (WIDTH / 2); + int minChildZPos = this.pos.getChildByIndex(0).getZ(); + int recipientOffsetZ = (inputDataSource.pos.getZ() == minChildZPos) ? 0 : (WIDTH / 2); + + + + // merge the input's data points + // into this data source's + boolean dataChanged = false; + for (int x = 0; x < WIDTH; x += 2) + { + for (int z = 0; z < WIDTH; z += 2) + { + int recipientX = (x / 2) + recipientOffsetX; + int recipientZ = (z / 2) + recipientOffsetZ; + int recipientIndex = relativePosToIndex(recipientX, recipientZ); + + + // world gen // + byte inputGenStep = determineMinWorldGenStepForTwoByTwoColumn(inputDataSource.columnGenerationSteps, x, z); + this.columnGenerationSteps[recipientIndex] = inputGenStep; + + + // world compression // + byte worldCompressionMode = determineHighestWorldCompressionForTwoByTwoColumn(inputDataSource.columnWorldCompressionMode, x, z); + this.columnWorldCompressionMode[recipientIndex] = worldCompressionMode; + + + + // data points // + LongArrayList mergedInputDataArray = mergeInputTwoByTwoDataColumn(inputDataSource, x, z); + + // check if the data changed + if (this.dataPoints[recipientIndex] == null) + { + // no data was present previously + dataChanged = true; + } + else if (this.dataPoints[recipientIndex].size() != mergedInputDataArray.size()) + { + // data is present, but the size is different + dataChanged = true; + } + + int oldDataHash = 0; + if (!dataChanged) + { + // some old data existed with the same length, + // we'll have to compare the caches + oldDataHash = this.dataPoints[recipientIndex].hashCode(); + } + + + this.dataPoints[recipientIndex] = mergedInputDataArray; + this.remapDataColumn(recipientIndex, remappedIds); + + if (RUN_DATA_ORDER_VALIDATION) + { + throwIfDataColumnInWrongOrder(inputDataSource.pos, this.dataPoints[recipientIndex]); + } + + + + if (!dataChanged) + { + // check if the identical length data column hashes are the same + // hashes need to be compared after the ID's have been remapped otherwise the ID's won't match even if the data is the same + if (oldDataHash != this.dataPoints[recipientIndex].hashCode()) + { + // the hashes are different, something was changed + dataChanged = true; + } + } + + this.isEmpty = false; + } + } + + return dataChanged; + } + /** + * The minimum value is used because we don't want to accidentally record that + * something was generated when it wasn't. + */ + private static byte determineMinWorldGenStepForTwoByTwoColumn(byte[] columnGenerationSteps, int relX, int relZ) + { + // TODO merge similar logic with determineHighestWorldCompressionForTwoByTwoColumn + byte minWorldGenStepValue = Byte.MAX_VALUE; + for (int x = 0; x < 2; x++) + { + for (int z = 0; z < 2; z++) + { + int index = relativePosToIndex(x + relX, z + relZ); + byte worldGenStepValue = columnGenerationSteps[index]; + minWorldGenStepValue = (byte) Math.min(minWorldGenStepValue, worldGenStepValue); + } + } + return minWorldGenStepValue; + } + /** + * The minimum value is used because we don't want to accidentally record that + * something was generated when it wasn't. + */ + private static byte determineHighestWorldCompressionForTwoByTwoColumn(byte[] columnCompressionMode, int relX, int relZ) + { + // TODO merge similar logic with determineMinWorldGenStepForTwoByTwoColumn + byte minWorldGenStepValue = Byte.MIN_VALUE; + for (int x = 0; x < 2; x++) + { + for (int z = 0; z < 2; z++) + { + int index = relativePosToIndex(x + relX, z + relZ); + byte worldGenStepValue = columnCompressionMode[index]; + minWorldGenStepValue = (byte) Math.max(minWorldGenStepValue, worldGenStepValue); + } + } + return minWorldGenStepValue; + } + private static LongArrayList mergeInputTwoByTwoDataColumn(FullDataSourceV2 inputDataSource, int x, int z) + { + LongArrayList newColumnList = new LongArrayList(); + + // special numbers: + // -2 = the column's height hasn't been determined yet + // -1 = we've reached the end of the column + int[] currentDatapointIndex = new int[] { -2, -2, -2, -2 }; + + int lastId = 0; + byte lastBlockLight = 0; + byte lastSkyLight = 0; + int height = 0; + int minY = 0; + + + // these arrays will be reused quite often, so re-using them helps reduce some GC pressure + long[] datapointsForYSlice = new long[4]; + int[] mergeIds = new int[4]; + int[] mergeBlockLights = new int[4]; + int[] mergeSkyLights = new int[4]; + + + for (int blockY = 0; blockY < RenderDataPointUtil.MAX_WORLD_Y_SIZE; blockY++, height++) + { + // if each column has reached the end of their data, nothing more needs to be done + if (currentDatapointIndex[0] == -1 + && currentDatapointIndex[1] == -1 + && currentDatapointIndex[2] == -1 + && currentDatapointIndex[3] == -1 + ) + { + break; + } + + + // scary double loop but, + // this will only ever loop 4 times, + // once for each of the 4 input columns + Arrays.fill(datapointsForYSlice, 0L); + int colIndex = 0; + for (int inputX = x; inputX < x + 2; inputX++) + { + for (int inputZ = z; inputZ < z + 2; inputZ++, colIndex++) + { + // TODO throw an assertion if the column isn't in top-down order or just fix it... + LongArrayList inputDataArray = inputDataSource.dataPoints[relativePosToIndex(inputX, inputZ)]; + if (inputDataArray == null || inputDataArray.size() == 0) + { + currentDatapointIndex[colIndex] = -1; + continue; + } + + // determine the last index (the lowest data point) for each column + if (currentDatapointIndex[colIndex] == -2) + { + currentDatapointIndex[colIndex] = inputDataArray.size() - 1; + + if (RUN_DATA_ORDER_VALIDATION) + { + throwIfDataColumnInWrongOrder(inputDataSource.pos, inputDataArray); + } + } + + + int dataPointIndex = currentDatapointIndex[colIndex]; + if (dataPointIndex == -1) + { + // went over the end + continue; + } + long datapoint = inputDataArray.getLong(dataPointIndex); + + int datapointMinY = FullDataPointUtil.getBottomY(datapoint); + int numbOfBlocksTall = FullDataPointUtil.getHeight(datapoint); + int datapointMaxY = (datapointMinY + numbOfBlocksTall); + + + // check if y position is inside this datapoint + if (blockY < datapointMinY) + { + // this y-slice is below this datapoint, nothing can be added + continue; + } + else if (blockY >= datapointMaxY) + { + // this y-slice is above the current datapoint, + // try the next data point + + int newDatapointIndex = currentDatapointIndex[colIndex] - 1; + if (newDatapointIndex < 0) + { + // went to far, no additional data present + newDatapointIndex = -1; + } + currentDatapointIndex[colIndex] = newDatapointIndex; + + + // try again with the next data point + inputZ--; + colIndex--; + continue; + } + + + + datapointsForYSlice[colIndex] = datapoint; + } + } + + + + Arrays.fill(mergeIds, 0); + Arrays.fill(mergeBlockLights, 0); + Arrays.fill(mergeSkyLights, 0); + for (int i = 0; i < 4; i++) + { + mergeIds[i] = FullDataPointUtil.getId(datapointsForYSlice[i]); + mergeBlockLights[i] = FullDataPointUtil.getBlockLight(datapointsForYSlice[i]); + mergeSkyLights[i] = FullDataPointUtil.getSkyLight(datapointsForYSlice[i]); + } + + + // determine the most common values for this slice + int id = determineMostValueInColumnSlice(mergeIds, inputDataSource.mapping); + byte blockLight = (byte) determineAverageValueInColumnSlice(mergeBlockLights); + byte skyLight = (byte) determineAverageValueInColumnSlice(mergeSkyLights); + + // if this slice is different then the last one, create a new one + if (id != lastId + // block and sky light might not be necessary + || blockLight != lastBlockLight + || skyLight != lastSkyLight) + { + if (height != 0) + { + newColumnList.add(FullDataPointUtil.encode(lastId, height, minY, lastBlockLight, lastSkyLight)); + } + + lastId = id; + lastBlockLight = blockLight; + lastSkyLight = skyLight; + height = 0; + minY = blockY; + } + } + + // add the last slice if present + if (height != 0) + { + newColumnList.add(FullDataPointUtil.encode(lastId, height, minY, lastBlockLight, lastSkyLight)); + } + + + // flip the array if necessary + // TODO why is this sometimes necessary? What did I (James) screw up that causes the mergedInputDataArray + // to sometimes be in a different order? Is it potentially related to what detail level is coming in? + ensureDataColumnOrder(newColumnList); + + return newColumnList; + } + /** + * 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 + * to anything else due to multiple remapping. + */ + private void remapDataColumn(int dataPointIndex, int[] remappedIds) + { + LongArrayList dataColumn = this.dataPoints[dataPointIndex]; + for (int i = 0; i < dataColumn.size(); i++) + { + dataColumn.set(i, FullDataPointUtil.remap(remappedIds, dataColumn.getLong(i))); + } + } + private static boolean areDataColumnsDifferent(long[] oldDataArray, long[] newDataArray) + { + if (oldDataArray == null || oldDataArray.length != newDataArray.length) + { + // new data was added/removed + return true; + } + else + { + // check if the new column data is different + int oldArrayHash = Arrays.hashCode(oldDataArray); + int newArrayHash = Arrays.hashCode(newDataArray); + return (newArrayHash != oldArrayHash); + } + } + /** @param mapping can be included to ignore air ID's, otherwise all 4 values are treated equally */ + private static int determineMostValueInColumnSlice(int[] sliceArray, @Nullable FullDataPointIdMap mapping) + { + if (RUN_UPDATE_DEV_VALIDATION) + { + LodUtil.assertTrue(sliceArray.length == 4, "Column Slice should only contain 4 values."); + } + + int value0 = sliceArray[0]; + int count0 = 0; + int value1 = sliceArray[1]; + int count1 = 0; + int value2 = sliceArray[2]; + int count2 = 0; + int value3 = sliceArray[3]; + int count3 = 0; + + // count the occurrences of each value + for (int i = 0; i < 4; i++) + { + int value = sliceArray[i]; + if (mapping != null && mapping.getBlockStateWrapper(value).isAir()) + { + // always overwrite air to prevent holes in hollow structures + continue; + } + + if (value == value0) + count0++; + else if (value == value1) + count1++; + else if (value == value2) + count2++; + else + count3++; + } + + // return the most common occurance + int maxCount = Math.max(count0, Math.max(count1, Math.max(count2, count3))); + if (maxCount == count0) + // if the max count is 1 then we'll just go with the first column + return value0; + else if (maxCount == count1) + return value1; + else if (maxCount == count2) + return value2; + else + return value3; + } + private static int determineAverageValueInColumnSlice(int[] sliceArray) + { + if (RUN_UPDATE_DEV_VALIDATION) + { + LodUtil.assertTrue(sliceArray.length == 4, "Column Slice should only contain 4 values."); + } + + + int value = 0; + for (int i = 0; i < 4; i++) + { + value += sliceArray[i]; + } + + value /= 4; + return value; + } + + + + //================// + // helper methods // + //================// + + /** + * Usually this should just be used internally, but there may be instances + * where the raw data arrays are available without the data source object. + */ + public static int relativePosToIndex(int relX, int relZ) throws IndexOutOfBoundsException + { + if (relX < 0 || relZ < 0 || + 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."); + } + + return (relX * WIDTH) + relZ; + } + + /** + * Throws an exception if the given + * 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. + * Data columns should be in reverse order, IE the first data point should be the highest data point. + * + * @see FullDataSourceV2#dataPoints + */ + public static void throwIfDataColumnInWrongOrder(DhSectionPos pos, LongArrayList dataArray) throws IllegalStateException + { + long firstDataPoint = dataArray.getLong(0); + int firstBottomY = FullDataPointUtil.getBottomY(firstDataPoint); + + long lastDataPoint = dataArray.getLong(dataArray.size() - 1); + int lastBottomY = FullDataPointUtil.getBottomY(lastDataPoint); + + if (firstBottomY < lastBottomY) + { + throw new IllegalStateException("Incorrect data point order at pos: "+pos+", first datapoint bottom Y ["+firstBottomY+"], last datapoint bottom Y ["+lastBottomY+"]."); + } + } + + /** + * Ensures the given data column is in the correct Y order, specifically + * top-to-bottom. + */ + private static void ensureDataColumnOrder(LongArrayList dataColumn) + { + long firstDataPoint = dataColumn.getLong(0); + int firstBottomY = FullDataPointUtil.getBottomY(firstDataPoint); + + long lastDataPoint = dataColumn.getLong(dataColumn.size() - 1); + int lastBottomY = FullDataPointUtil.getBottomY(lastDataPoint); + + if (firstBottomY < lastBottomY) + { + // reverse the array so index 0 is the highest, + // this is necessary for later logic + // source: https://stackoverflow.com/questions/2137755/how-do-i-reverse-an-int-array-in-java + for(int i = 0; i < dataColumn.size() / 2; i++) + { + long temp = dataColumn.getLong(i); + dataColumn.set(i, dataColumn.getLong(dataColumn.size() - i - 1)); + dataColumn.set(dataColumn.size() - i - 1, temp); + } + } + } + + + + //=========// + // pooling // + //=========// + + private static void prepPooledDataSource(DhSectionPos pos, boolean clearData, FullDataSourceV2 dataSource) + { + dataSource.pos = pos; + + if (clearData) + { + dataSource.mapping.clear(pos); + + for (int i = 0; i < dataSource.dataPoints.length; i++) + { + if (dataSource.dataPoints[i] != null) + { + dataSource.dataPoints[i].clear(); + } + } + + Arrays.fill(dataSource.columnGenerationSteps, (byte) 0); + Arrays.fill(dataSource.columnWorldCompressionMode, (byte) 0); + } + } + + + + //=====================// + // setters and getters // + //=====================// + + @Override + public DhSectionPos getPos() { return this.pos; } + + @Override + public byte getDataDetailLevel() { return (byte) (this.pos.getDetailLevel() - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); } + + public EDhApiWorldGenerationStep getWorldGenStepAtRelativePos(int relX, int relZ) + { + int index = relativePosToIndex(relX, relZ); + return EDhApiWorldGenerationStep.fromValue(this.columnGenerationSteps[index]); + } + + public void setSingleColumn(LongArrayList longArray, int relX, int relZ, EDhApiWorldGenerationStep worldGenStep, EDhApiWorldCompressionMode worldCompressionMode) + { + int index = relativePosToIndex(relX, relZ); + this.dataPoints[index] = longArray; + this.columnGenerationSteps[index] = worldGenStep.value; + this.columnWorldCompressionMode[index] = worldCompressionMode.value; + + + if (RUN_UPDATE_DEV_VALIDATION) + { + // validate the incoming ID's + int maxValidId = this.mapping.getMaxValidId(); + for (int i = 0; i < longArray.size(); i++) + { + long dataPoint = longArray.getLong(i); + int id = FullDataPointUtil.getId(dataPoint); + if (id > maxValidId) + { + LodUtil.assertNotReach("Column set with higher than possible ID. ID [" + id + "], max valid ID [" + maxValidId + "]."); + } + } + } + } + + + + //================// + // base overrides // + //================// + + @Override + public String toString() { return this.pos.toString(); } + + @Override + public int hashCode() + { + if (this.cachedHashCode == 0) + { + this.generateHashCode(); + } + return this.cachedHashCode; + } + private void generateHashCode() + { + int result = this.pos.hashCode(); + result = 31 * result + Arrays.deepHashCode(this.dataPoints); + result = 17 * result + Arrays.hashCode(this.columnGenerationSteps); + result = 43 * result + Arrays.hashCode(this.columnWorldCompressionMode); + + this.cachedHashCode = result; + } + + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof FullDataSourceV2)) + { + return false; + } + FullDataSourceV2 other = (FullDataSourceV2) obj; + + if (!other.pos.equals(this.pos)) + { + return false; + } + else + { + // the positions are the same, use the hash as a quick method + // to determine if the data inside is the same. + // Note: this isn't perfect, but should work well enough for our use case. + return other.hashCode() == this.hashCode(); + } + } + + @Override + public void close() throws Exception + { + DATA_SOURCE_POOL.returnPooledDataSource(this); + } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/HighDetailIncompleteFullDataSource.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/HighDetailIncompleteFullDataSource.java deleted file mode 100644 index b239a8c18..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/HighDetailIncompleteFullDataSource.java +++ /dev/null @@ -1,583 +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 . - */ - -package com.seibel.distanthorizons.core.dataObjects.fullData.sources; - -import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.FullDataArrayAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.SingleColumnFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IIncompleteFullDataSource; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IStreamableFullDataSource; -import com.seibel.distanthorizons.core.level.IDhLevel; -import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; -import com.seibel.distanthorizons.core.pos.DhLodPos; -import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.sql.DataSourceDto; -import com.seibel.distanthorizons.core.util.FullDataPointUtil; -import com.seibel.distanthorizons.core.util.LodUtil; -import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream; -import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataOutputStream; -import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; -import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; -import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; -import org.apache.logging.log4j.Logger; - -import java.io.*; -import java.util.BitSet; - -/** - * Used for small incomplete LOD blocks.
- * Handles incomplete full data with a detail level equal to or lower than - * {@link HighDetailIncompleteFullDataSource#MAX_SECTION_DETAIL}.

- * - * Compared to other {@link IIncompleteFullDataSource}'s, this object doesn't extend {@link FullDataArrayAccessor}, - * instead it contains several "sections" of data, represented by {@link FullDataArrayAccessor}s.

- * - * Formerly "SparseFullDataSource". - * - * @see LowDetailIncompleteFullDataSource - * @see CompleteFullDataSource - * @see FullDataPointUtil - */ -public class HighDetailIncompleteFullDataSource implements IIncompleteFullDataSource, IStreamableFullDataSource -{ - private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - - // TODO James would like to rename, comment, and potentially remove some of these constants. - // But he doesn't currently have the understanding to do so. - public static final byte SPARSE_UNIT_DETAIL = LodUtil.CHUNK_DETAIL_LEVEL; - public static final byte SPARSE_UNIT_SIZE = (byte) BitShiftUtil.powerOfTwo(SPARSE_UNIT_DETAIL); - - public static final byte SECTION_SIZE_OFFSET = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; - public static final int SECTION_SIZE = (byte) BitShiftUtil.powerOfTwo(SECTION_SIZE_OFFSET); - /** aka max detail level */ - public static final byte MAX_SECTION_DETAIL = SECTION_SIZE_OFFSET + SPARSE_UNIT_DETAIL; - - public static final byte DATA_FORMAT_VERSION = 3; - public static final String DATA_TYPE_NAME = "HighDetailIncompleteFullDataSource"; - @Override - public String getDataTypeName() { return DATA_TYPE_NAME; } - - - protected final FullDataPointIdMap mapping; - private DhSectionPos sectionPos; - private FullDataArrayAccessor[] sparseData; - private DhLodPos chunkPos; - - public int sectionCount; - public int dataPointsPerSection; - public boolean isEmpty = true; - public EDhApiWorldGenerationStep worldGenStep = EDhApiWorldGenerationStep.EMPTY; - private boolean isPromoted = false; - - - - //==============// - // constructors // - //==============// - - public static HighDetailIncompleteFullDataSource createEmpty(DhSectionPos pos) { return new HighDetailIncompleteFullDataSource(pos); } - private HighDetailIncompleteFullDataSource(DhSectionPos sectionPos) - { - LodUtil.assertTrue(sectionPos.getDetailLevel() > SPARSE_UNIT_DETAIL); - LodUtil.assertTrue(sectionPos.getDetailLevel() <= MAX_SECTION_DETAIL); - - this.sectionPos = sectionPos; - this.sectionCount = BitShiftUtil.powerOfTwo(sectionPos.getDetailLevel() - SPARSE_UNIT_DETAIL); - this.dataPointsPerSection = SECTION_SIZE / this.sectionCount; - - this.sparseData = new FullDataArrayAccessor[this.sectionCount * this.sectionCount]; - this.chunkPos = sectionPos.getMinCornerLodPos(SPARSE_UNIT_DETAIL); - this.mapping = new FullDataPointIdMap(sectionPos); - } - - protected HighDetailIncompleteFullDataSource(DhSectionPos sectionPos, FullDataPointIdMap mapping, FullDataArrayAccessor[] data) - { - LodUtil.assertTrue(sectionPos.getDetailLevel() > SPARSE_UNIT_DETAIL); - LodUtil.assertTrue(sectionPos.getDetailLevel() <= MAX_SECTION_DETAIL); - - this.sectionPos = sectionPos; - this.sectionCount = 1 << (byte) (sectionPos.getDetailLevel() - SPARSE_UNIT_DETAIL); - this.dataPointsPerSection = SECTION_SIZE / this.sectionCount; - - LodUtil.assertTrue(this.sectionCount * this.sectionCount == data.length); - this.sparseData = data; - this.chunkPos = sectionPos.getMinCornerLodPos(SPARSE_UNIT_DETAIL); - this.isEmpty = false; - this.mapping = mapping; - } - - - - //=================// - // stream handling // - //=================// - - - @Override - public void writeSourceSummaryInfo(IDhLevel level, DhDataOutputStream dataOutputStream) throws IOException - { - dataOutputStream.writeShort(this.getDataDetailLevel()); - dataOutputStream.writeShort(SPARSE_UNIT_DETAIL); - dataOutputStream.writeInt(SECTION_SIZE); - dataOutputStream.writeInt(level.getMinY()); - dataOutputStream.writeByte(this.worldGenStep.value); - - } - @Override - public FullDataSourceSummaryData readSourceSummaryInfo(DataSourceDto dto, DhDataInputStream inputStream, IDhLevel level) throws IOException - { - LodUtil.assertTrue(dto.pos.getDetailLevel() > SPARSE_UNIT_DETAIL); - LodUtil.assertTrue(dto.pos.getDetailLevel() <= MAX_SECTION_DETAIL); - - int dataDetailLevel = inputStream.readShort(); - if (dataDetailLevel != dto.dataDetailLevel) - { - throw new IOException("Data level mismatch: ["+dataDetailLevel+"] != ["+dto.dataDetailLevel+"]"); - } - - // confirm that the detail level is correct - int sparseDetail = inputStream.readShort(); - if (sparseDetail != SPARSE_UNIT_DETAIL) - { - throw new IOException("Unexpected sparse detail level: ["+sparseDetail+"] != ["+SPARSE_UNIT_DETAIL+"]"); - } - - // confirm the scale of the data points is correct - int sectionSize = inputStream.readInt(); - if (sectionSize != SECTION_SIZE) - { - throw new IOException("Section size mismatch: ["+sectionSize+"] != ["+SECTION_SIZE+"] (Currently only 1 section size is supported)"); - } - - int minY = inputStream.readInt(); - if (minY != level.getMinY()) - { - LOGGER.warn("Data minY mismatch: [" + minY + "] != [" + level.getMinY() + "]. Will ignore data's y level"); - } - - EDhApiWorldGenerationStep worldGenStep = EDhApiWorldGenerationStep.fromValue(inputStream.readByte()); - if (worldGenStep == null) - { - worldGenStep = EDhApiWorldGenerationStep.SURFACE; - LOGGER.warn("Missing WorldGenStep, defaulting to: " + worldGenStep.name()); - } - - - return new FullDataSourceSummaryData(-1, worldGenStep); - } - public void setSourceSummaryData(FullDataSourceSummaryData summaryData) { this.worldGenStep = summaryData.worldGenStep; } - - - @Override - public boolean writeDataPoints(DhDataOutputStream dataOutputStream) throws IOException - { - if (this.isEmpty) - { - dataOutputStream.writeInt(IFullDataSource.NO_DATA_FLAG_BYTE); - return false; - } - dataOutputStream.writeInt(IFullDataSource.DATA_GUARD_BYTE); - - - // sparse array existence bitset - BitSet dataArrayIndexHasData = new BitSet(this.sparseData.length); - for (int i = 0; i < this.sparseData.length; i++) - { - dataArrayIndexHasData.set(i, this.sparseData[i] != null); - } - byte[] bytes = dataArrayIndexHasData.toByteArray(); - dataOutputStream.writeInt(bytes.length); - dataOutputStream.write(bytes); - - - // Data array content (only non-empty data is written) - dataOutputStream.writeInt(IFullDataSource.DATA_GUARD_BYTE); - for (int dataArrayIndex = dataArrayIndexHasData.nextSetBit(0); - dataArrayIndex >= 0; - dataArrayIndex = dataArrayIndexHasData.nextSetBit(dataArrayIndex + 1)) - { - // column data length - FullDataArrayAccessor array = this.sparseData[dataArrayIndex]; - LodUtil.assertTrue(array != null); - for (int x = 0; x < array.width(); x++) - { - for (int z = 0; z < array.width(); z++) - { - SingleColumnFullDataAccessor columnAccessor = array.get(x, z); - int columnLength = 0; - if (columnAccessor != null) - { - columnLength = columnAccessor.getSingleLength(); - } - dataOutputStream.writeInt(columnLength); - } - } - - // column data - for (int x = 0; x < array.width(); x++) - { - for (int z = 0; z < array.width(); z++) - { - SingleColumnFullDataAccessor column = array.get(x, z); - LodUtil.assertTrue(column.getMapping() == this.mapping); // the mappings must be exactly equal! - - if (column.doesColumnExist()) - { - long[] rawDataPoints = column.getRaw(); - for (long dataPoint : rawDataPoints) - { - dataOutputStream.writeLong(dataPoint); - } - } - } - } - } - - - return true; - } - @Override - public long[][][] readDataPoints(DataSourceDto dto, int width, DhDataInputStream inputStream) throws IOException - { - // calculate the number of chunks and dataPoints based on the sparseDetail and sectionSize - // TODO these values should be constant, should we still be calculating them like this? - int chunks = BitShiftUtil.powerOfTwo(dto.pos.getDetailLevel() - SPARSE_UNIT_DETAIL); - int dataPointsPerChunk = SECTION_SIZE / chunks; - - - // check if this file has any data - int dataPresentFlag = inputStream.readInt(); - if (dataPresentFlag == IFullDataSource.NO_DATA_FLAG_BYTE) - { - // this file is empty - return null; - } - else if (dataPresentFlag != IFullDataSource.DATA_GUARD_BYTE) - { - // the file format is incorrect - throw new IOException("Invalid file format. Data Points guard byte expected: (no data) [" + IFullDataSource.NO_DATA_FLAG_BYTE + "] or (data present) [" + IFullDataSource.DATA_GUARD_BYTE + "], but found [" + dataPresentFlag + "]."); - } - - - // get the number of columns (IE the bitSet from before) - int numberOfDataColumns = inputStream.readInt(); - // validate the number of data columns - int maxNumberOfDataColumns = (chunks * chunks / 8 + 64) * 2; // TODO what do these values represent? - if (numberOfDataColumns < 0 || numberOfDataColumns > maxNumberOfDataColumns) - { - throw new IOException(LodUtil.formatLog("Sparse Flag BitSet size outside reasonable range: {} (expects {} to {})", - numberOfDataColumns, 1, maxNumberOfDataColumns)); - } - - // read in the presence of each data column - byte[] bytes = new byte[numberOfDataColumns]; - inputStream.readFully(bytes, 0, numberOfDataColumns); - BitSet dataArrayIndexHasData = BitSet.valueOf(bytes); - - - - //====================// - // Data array content // - //====================// - - // (only on non-empty columns) - int dataArrayStartByte = inputStream.readInt(); - // confirm the column data is starting - if (dataArrayStartByte != IFullDataSource.DATA_GUARD_BYTE) - { - // the file format is incorrect - throw new IOException("invalid data length end guard"); - } - - - // read in each column that has data written to it - long[][][] rawFullDataArrays = new long[chunks * chunks][][]; - for (int fullDataIndex = dataArrayIndexHasData.nextSetBit(0); - fullDataIndex >= 0 && // TODO why does this happen? - fullDataIndex < rawFullDataArrays.length; - fullDataIndex = dataArrayIndexHasData.nextSetBit(fullDataIndex + 1)) - { - long[][] dataColumn = new long[dataPointsPerChunk * dataPointsPerChunk][]; - - // get the column data lengths - rawFullDataArrays[fullDataIndex] = dataColumn; - for (int x = 0; x < dataColumn.length; x++) - { - // this should be zero if the column doesn't have any data - int dataColumnLength = inputStream.readInt(); - dataColumn[x] = new long[dataColumnLength]; - } - - // get the column data - for (int x = 0; x < dataColumn.length; x++) - { - if (dataColumn[x].length != 0) - { - // read in the data columns - for (int z = 0; z < dataColumn[x].length; z++) - { - dataColumn[x][z] = inputStream.readLong(); - } - } - } - } - - - return rawFullDataArrays; - } - @Override - public void setDataPoints(long[][][] dataPoints) - { - LodUtil.assertTrue(this.sparseData.length == dataPoints.length, "Data point array length mismatch."); - - this.isEmpty = false; - - - for (int arrayAccessorIndex = 0; arrayAccessorIndex < dataPoints.length; arrayAccessorIndex++) - { - if (dataPoints[arrayAccessorIndex] == null) - { - this.sparseData[arrayAccessorIndex] = null; - } - else if (this.sparseData[arrayAccessorIndex] == null) - { - int width = (int) Math.sqrt(dataPoints[arrayAccessorIndex].length); - this.sparseData[arrayAccessorIndex] = new FullDataArrayAccessor(this.mapping, dataPoints[arrayAccessorIndex], width); - } - else - { - for (int dataPointColIndex = 0; dataPointColIndex < dataPoints[arrayAccessorIndex].length; dataPointColIndex++) - { - long[] incomingColumn = dataPoints[arrayAccessorIndex][dataPointColIndex]; - long[] destinationColumn = this.sparseData[arrayAccessorIndex].get(dataPointColIndex).getRaw(); - - // use the existing arrays if possible - if (incomingColumn.length == destinationColumn.length) - { - System.arraycopy(incomingColumn, 0, destinationColumn, 0, incomingColumn.length); - } - else - { - this.sparseData[arrayAccessorIndex].get(dataPointColIndex).setNew(incomingColumn); - } - } - } - } - } - - - @Override - public FullDataPointIdMap readIdMappings(long[][][] dataPoints, DhDataInputStream inputStream, ILevelWrapper levelWrapper) throws IOException, InterruptedException - { - // mark the start of the ID data - int idMappingStartByte = inputStream.readInt(); - if (idMappingStartByte != DATA_GUARD_BYTE) - { - // the file format is incorrect - throw new IOException("invalid data content end guard"); - } - - // deserialize the ID data - return FullDataPointIdMap.deserialize(inputStream, this.sectionPos, levelWrapper); - } - @Override - public void writeIdMappings(DhDataOutputStream dataOutputStream) throws IOException - { - dataOutputStream.writeInt(IFullDataSource.DATA_GUARD_BYTE); - this.mapping.serialize(dataOutputStream); - } - @Override - public void setIdMapping(FullDataPointIdMap mappings) { this.mapping.mergeAndReturnRemappedEntityIds(mappings); } - - - - //======// - // data // - //======// - - @Override - public SingleColumnFullDataAccessor tryGet(int relativeX, int relativeZ) { return this.tryGetOrCreate(relativeX, relativeZ, false); } - @Override - public SingleColumnFullDataAccessor getOrCreate(int relativeX, int relativeZ) { return this.tryGetOrCreate(relativeX, relativeZ, true); } - private SingleColumnFullDataAccessor tryGetOrCreate(int relativeX, int relativeZ, boolean createIfMissing) - { - LodUtil.assertTrue(relativeX >= 0 && relativeX < SECTION_SIZE && relativeZ >= 0 && relativeZ < SECTION_SIZE); - int chunkX = relativeX / this.dataPointsPerSection; - int chunkZ = relativeZ / this.dataPointsPerSection; - FullDataArrayAccessor accessor = this.sparseData[chunkX * this.sectionCount + chunkZ]; - if (accessor == null) - { - if (createIfMissing) - { - // create the missing data so the following get() will succeed - accessor = new FullDataArrayAccessor(this.mapping, new long[this.dataPointsPerSection * this.dataPointsPerSection][], this.dataPointsPerSection); - this.sparseData[chunkX * this.sectionCount + chunkZ] = accessor; - } - else - { - return null; - } - } - - return accessor.get(relativeX % this.dataPointsPerSection, relativeZ % this.dataPointsPerSection); - } - - - - //=========// - // getters // - //=========// - - @Override - public DhSectionPos getSectionPos() { return this.sectionPos; } - - @Override - public void resizeDataStructuresForRepopulation(DhSectionPos pos) - { - // update the position - this.sectionPos = pos; - this.sectionCount = BitShiftUtil.powerOfTwo(this.sectionPos.getDetailLevel() - SPARSE_UNIT_DETAIL); - this.dataPointsPerSection = SECTION_SIZE / this.sectionCount; - - this.chunkPos = this.sectionPos.getMinCornerLodPos(SPARSE_UNIT_DETAIL); - - - // update the data container - int dataPointCount = this.sectionCount * this.sectionCount; - if (this.sparseData.length != dataPointCount) - { - this.sparseData = new FullDataArrayAccessor[this.sectionCount * this.sectionCount]; - } - - } - - @Override - public byte getDataDetailLevel() { return (byte) (this.sectionPos.getDetailLevel() - SECTION_SIZE_OFFSET); } - - @Override - public byte getDataFormatVersion() { return DATA_FORMAT_VERSION; } - - @Override - public EDhApiWorldGenerationStep getWorldGenStep() { return this.worldGenStep; } - - @Override - public FullDataPointIdMap getMapping() { return this.mapping; } - - @Override - public boolean isEmpty() { return this.isEmpty; } - @Override - public void markNotEmpty() { this.isEmpty = false; } - - @Override - public int getWidthInDataPoints() { return SECTION_SIZE; } - - - private int calculateOffset(int chunkX, int chunkZ) - { - int offsetX = chunkX - this.chunkPos.x; - int offsetZ = chunkZ - this.chunkPos.z; - LodUtil.assertTrue(offsetX >= 0 && offsetZ >= 0 && offsetX < this.sectionCount && offsetZ < this.sectionCount); - return offsetX * this.sectionCount + offsetZ; - } - - - - //=============// - // data update // - //=============// - - @Override - public void update(ChunkSizedFullDataAccessor chunkDataView) - { - int arrayOffset = this.calculateOffset(chunkDataView.chunkPos.x, chunkDataView.chunkPos.z); - FullDataArrayAccessor newArray = new FullDataArrayAccessor(this.mapping, new long[this.dataPointsPerSection * this.dataPointsPerSection][], this.dataPointsPerSection); - if (this.getDataDetailLevel() == chunkDataView.detailLevel) - { - chunkDataView.shadowCopyTo(newArray); - } - else - { - int count = this.dataPointsPerSection; - int dataPerCount = SPARSE_UNIT_SIZE / this.dataPointsPerSection; - - for (int xOffset = 0; xOffset < count; xOffset++) - { - for (int zOffset = 0; zOffset < count; zOffset++) - { - SingleColumnFullDataAccessor column = newArray.get(xOffset, zOffset); - column.downsampleFrom(chunkDataView.subView(dataPerCount, xOffset * dataPerCount, zOffset * dataPerCount)); - } - } - } - - this.isEmpty = false; - this.sparseData[arrayOffset] = newArray; - } - - - // data sampling // - - private void applyToFullDataSource(CompleteFullDataSource dataSource) - { - LodUtil.assertTrue(dataSource.getSectionPos().equals(this.sectionPos)); - LodUtil.assertTrue(dataSource.getDataDetailLevel() == this.getDataDetailLevel()); - for (int x = 0; x < this.sectionCount; x++) - { - for (int z = 0; z < this.sectionCount; z++) - { - FullDataArrayAccessor array = this.sparseData[x * this.sectionCount + z]; - if (array == null) - continue; - - // Otherwise, apply data to dataSource - dataSource.markNotEmpty(); - FullDataArrayAccessor view = dataSource.subView(this.dataPointsPerSection, x * this.dataPointsPerSection, z * this.dataPointsPerSection); - array.shadowCopyTo(view); - } - } - } - - public IFullDataSource tryPromotingToCompleteDataSource() - { - if (this.isEmpty) - { - return this; - } - - // promotion can only succeed if every data column is present - for (FullDataArrayAccessor array : this.sparseData) - { - if (array == null) - { - return this; - } - } - this.isPromoted = true; - CompleteFullDataSource fullDataSource = CompleteFullDataSource.createEmpty(this.sectionPos); - this.applyToFullDataSource(fullDataSource); - return fullDataSource; - } - - @Override - public boolean hasBeenPromoted() { return this.isPromoted; } - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/LowDetailIncompleteFullDataSource.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/LowDetailIncompleteFullDataSource.java deleted file mode 100644 index a2d188453..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/LowDetailIncompleteFullDataSource.java +++ /dev/null @@ -1,436 +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 . - */ - -package com.seibel.distanthorizons.core.dataObjects.fullData.sources; - -import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.FullDataArrayAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.SingleColumnFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IIncompleteFullDataSource; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IStreamableFullDataSource; -import com.seibel.distanthorizons.core.level.IDhLevel; -import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; -import com.seibel.distanthorizons.core.pos.DhLodPos; -import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.sql.DataSourceDto; -import com.seibel.distanthorizons.core.util.FullDataPointUtil; -import com.seibel.distanthorizons.core.util.LodUtil; -import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream; -import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataOutputStream; -import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; -import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; -import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; -import org.apache.logging.log4j.Logger; - -import java.io.*; -import java.util.BitSet; - -/** - * Used for large incomplete LOD blocks.
- * Handles incomplete full data with a detail level higher than - * {@link HighDetailIncompleteFullDataSource#MAX_SECTION_DETAIL}.

- * - * Formerly "SpottyFullDataSource". - * - * @see HighDetailIncompleteFullDataSource - * @see CompleteFullDataSource - * @see FullDataPointUtil - */ -public class LowDetailIncompleteFullDataSource extends FullDataArrayAccessor implements IIncompleteFullDataSource, IStreamableFullDataSource -{ - private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - - public static final byte SECTION_SIZE_OFFSET = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; - /** measured in dataPoints */ - public static final int WIDTH = BitShiftUtil.powerOfTwo(SECTION_SIZE_OFFSET); - - public static final byte DATA_FORMAT_VERSION = 3; - public static final String DATA_TYPE_NAME = "LowDetailIncompleteFullDataSource"; - @Override - public String getDataTypeName() { return DATA_TYPE_NAME; } - - private DhSectionPos sectionPos; - - private final BitSet isColumnNotEmpty; - - private boolean isEmpty = true; - public EDhApiWorldGenerationStep worldGenStep = EDhApiWorldGenerationStep.EMPTY; - private boolean isPromoted = false; - - - - //==============// - // constructors // - //==============// - - public static LowDetailIncompleteFullDataSource createEmpty(DhSectionPos pos) { return new LowDetailIncompleteFullDataSource(pos); } - private LowDetailIncompleteFullDataSource(DhSectionPos sectionPos) - { - super(new FullDataPointIdMap(sectionPos), new long[WIDTH * WIDTH][0], WIDTH); - LodUtil.assertTrue(sectionPos.getDetailLevel() > HighDetailIncompleteFullDataSource.MAX_SECTION_DETAIL); - - this.sectionPos = sectionPos; - this.isColumnNotEmpty = new BitSet(WIDTH * WIDTH); - this.worldGenStep = EDhApiWorldGenerationStep.EMPTY; - } - - private LowDetailIncompleteFullDataSource(DhSectionPos pos, FullDataPointIdMap mapping, EDhApiWorldGenerationStep worldGenStep, BitSet isColumnNotEmpty, long[][] data) - { - super(mapping, data, WIDTH); - LodUtil.assertTrue(data.length == WIDTH * WIDTH); - - this.sectionPos = pos; - this.isColumnNotEmpty = isColumnNotEmpty; - this.worldGenStep = worldGenStep; - this.isEmpty = false; - } - - - - //=================// - // stream handling // - //=================// - - - @Override - public void writeSourceSummaryInfo(IDhLevel level, DhDataOutputStream outputStream) throws IOException - { - outputStream.writeInt(this.getDataDetailLevel()); - outputStream.writeInt(this.width); - outputStream.writeInt(level.getMinY()); - outputStream.writeByte(this.worldGenStep.value); - - } - @Override - public FullDataSourceSummaryData readSourceSummaryInfo(DataSourceDto dto, DhDataInputStream inputStream, IDhLevel level) throws IOException - { - int dataDetailLevel = inputStream.readInt(); - if (dataDetailLevel != dto.dataDetailLevel) - { - throw new IOException(LodUtil.formatLog("Data level mismatch: " + dataDetailLevel + " != " + dto.dataDetailLevel)); - } - - int width = inputStream.readInt(); - if (width != WIDTH) - { - throw new IOException(LodUtil.formatLog("Section size mismatch: " + width + " != " + WIDTH + " (Currently only 1 section size is supported)")); - } - - int minY = inputStream.readInt(); - if (minY != level.getMinY()) - { - LOGGER.warn("Data minY mismatch: " + minY + " != " + level.getMinY() + ". Will ignore data's y level"); - } - - EDhApiWorldGenerationStep worldGenStep = EDhApiWorldGenerationStep.fromValue(inputStream.readByte()); - if (worldGenStep == null) - { - worldGenStep = EDhApiWorldGenerationStep.SURFACE; - LOGGER.warn("Missing WorldGenStep, defaulting to: " + worldGenStep.name()); - } - - - return new FullDataSourceSummaryData(this.width, worldGenStep); - } - public void setSourceSummaryData(FullDataSourceSummaryData summaryData) - { - this.worldGenStep = summaryData.worldGenStep; - } - - - @Override - public boolean writeDataPoints(DhDataOutputStream dataOutputStream) throws IOException - { - if (this.isEmpty) - { - dataOutputStream.writeInt(IFullDataSource.NO_DATA_FLAG_BYTE); - return false; - } - dataOutputStream.writeInt(IFullDataSource.DATA_GUARD_BYTE); - - - // data column presence - byte[] bytes = this.isColumnNotEmpty.toByteArray(); - dataOutputStream.writeInt(bytes.length); - dataOutputStream.write(bytes); - - - // Data content - dataOutputStream.writeInt(IFullDataSource.DATA_GUARD_BYTE); - for (int i = this.isColumnNotEmpty.nextSetBit(0); i >= 0; i = this.isColumnNotEmpty.nextSetBit(i + 1)) - { - dataOutputStream.writeByte(this.dataArrays[i].length); - for (long dataPoint : this.dataArrays[i]) - { - dataOutputStream.writeLong(dataPoint); - } - } - - - return true; - } - @Override - public StreamDataPointContainer readDataPoints(DataSourceDto dto, int width, DhDataInputStream inputStream) throws IOException - { - // is source empty flag - int dataPresentFlag = inputStream.readInt(); - if (dataPresentFlag == IFullDataSource.NO_DATA_FLAG_BYTE) - { - // Section is empty - return null; - } - else if (dataPresentFlag != IFullDataSource.DATA_GUARD_BYTE) - { - throw new IOException("Invalid file format. Data Points guard byte expected: (no data) [" + IFullDataSource.NO_DATA_FLAG_BYTE + "] or (data present) [" + IFullDataSource.DATA_GUARD_BYTE + "], but found [" + dataPresentFlag + "]."); - } - - - // data column presence - int length = inputStream.readInt(); - if (length < 0 || length > (WIDTH * WIDTH / 8 + 64) * 2) // TODO replace magic numbers or comment what they mean - { - throw new IOException(LodUtil.formatLog("Spotty Flag BitSet size outside reasonable range: {} (expects {} to {})", - length, 1, WIDTH * WIDTH / 8 + 63)); - } - - byte[] bytes = new byte[length]; - inputStream.readFully(bytes, 0, length); - BitSet isColumnNotEmpty = BitSet.valueOf(bytes); - - - - // Data array content - long[][] dataPointArray = new long[WIDTH * WIDTH][]; - dataPresentFlag = inputStream.readInt(); - if (dataPresentFlag != IFullDataSource.DATA_GUARD_BYTE) - { - throw new IOException("invalid spotty flag end guard"); - } - - for (int xz = isColumnNotEmpty.nextSetBit(0); xz >= 0; xz = isColumnNotEmpty.nextSetBit(xz + 1)) - { - long[] array = new long[inputStream.readByte()]; - for (int y = 0; y < array.length; y++) - { - array[y] = inputStream.readLong(); - } - dataPointArray[xz] = array; - } - - - return new StreamDataPointContainer(dataPointArray, isColumnNotEmpty); - } - @Override - public void setDataPoints(StreamDataPointContainer streamDataPointContainer) - { - long[][] dataPoints = streamDataPointContainer.dataPoints; - - // copy over the datapoints - LodUtil.assertTrue(this.dataArrays.length == dataPoints.length, "Data point array length mismatch."); - System.arraycopy(dataPoints, 0, this.dataArrays, 0, dataPoints.length); - - // overwrite the bitset - for (int i = 0; i < streamDataPointContainer.isColumnNotEmpty.length(); i++) - { - this.isColumnNotEmpty.set(i, streamDataPointContainer.isColumnNotEmpty.get(i)); - } - - this.isEmpty = false; - } - - - @Override - public void writeIdMappings(DhDataOutputStream outputStream) throws IOException - { - outputStream.writeInt(IFullDataSource.DATA_GUARD_BYTE); - this.mapping.serialize(outputStream); - - } - @Override - public FullDataPointIdMap readIdMappings(StreamDataPointContainer streamDataPointContainer, DhDataInputStream inputStream, ILevelWrapper levelWrapper) throws IOException, InterruptedException - { - // Id mapping - int dataPresentFlag = inputStream.readInt(); - if (dataPresentFlag != IFullDataSource.DATA_GUARD_BYTE) - { - throw new IOException("invalid ID mapping end guard"); - } - return FullDataPointIdMap.deserialize(inputStream, this.sectionPos, levelWrapper); - } - @Override - public void setIdMapping(FullDataPointIdMap mappings) { this.mapping.mergeAndReturnRemappedEntityIds(mappings); } - - - - //======// - // data // - //======// - - @Override - public SingleColumnFullDataAccessor tryGet(int relativeX, int relativeZ) { return this.tryGetOrCreate(relativeX, relativeZ, false); } - @Override - public SingleColumnFullDataAccessor getOrCreate(int relativeX, int relativeZ) { return this.tryGetOrCreate(relativeX, relativeZ, true); } - private SingleColumnFullDataAccessor tryGetOrCreate(int relativeX, int relativeZ, boolean createIfMissing) - { - int notEmptyIndex = relativeX * WIDTH + relativeZ; - boolean columnEmpty = this.isColumnNotEmpty.get(notEmptyIndex); - - // "create" the missing column if necessary - if (columnEmpty && createIfMissing) - { - this.isColumnNotEmpty.set(notEmptyIndex, true); - columnEmpty = false; - } - - return !columnEmpty ? this.get(relativeX, relativeZ) : null; - } - - - - //=====================// - // getters and setters // - //=====================// - - @Override - public DhSectionPos getSectionPos() { return this.sectionPos; } - - @Override - public void resizeDataStructuresForRepopulation(DhSectionPos pos) - { - // no data structures need to be changed, only the source's position - this.sectionPos = pos; - } - - @Override - public byte getDataDetailLevel() { return (byte) (this.sectionPos.getDetailLevel() - SECTION_SIZE_OFFSET); } - @Override - public byte getDataFormatVersion() { return DATA_FORMAT_VERSION; } - - @Override - public EDhApiWorldGenerationStep getWorldGenStep() { return this.worldGenStep; } - - @Override - public boolean isEmpty() { return this.isEmpty; } - @Override - public void markNotEmpty() { this.isEmpty = false; } - - @Override - public int getWidthInDataPoints() { return WIDTH; } - - - - //===============// - // Data updating // - //===============// - - @Override - public void update(ChunkSizedFullDataAccessor data) - { - LodUtil.assertTrue(this.sectionPos.overlapsExactly(data.getSectionPos())); - - if (this.getDataDetailLevel() >= 4) - { - //FIXME: TEMPORARY - int chunkPerFull = 1 << (this.getDataDetailLevel() - 4); - if (data.chunkPos.x % chunkPerFull != 0 || data.chunkPos.z % chunkPerFull != 0) - { - return; - } - - DhLodPos baseOffset = this.sectionPos.getMinCornerLodPos(this.getDataDetailLevel()); - DhSectionPos dataOffset = data.getSectionPos().convertNewToDetailLevel(this.getDataDetailLevel()); - int offsetX = dataOffset.getX() - baseOffset.x; - int offsetZ = dataOffset.getZ() - baseOffset.z; - LodUtil.assertTrue(offsetX >= 0 && offsetX < WIDTH && offsetZ >= 0 && offsetZ < WIDTH); - this.isEmpty = false; - - SingleColumnFullDataAccessor columnFullDataAccessor = this.get(offsetX, offsetZ); - data.get(0, 0).deepCopyTo(columnFullDataAccessor); - - this.isColumnNotEmpty.set(offsetX * WIDTH + offsetZ, columnFullDataAccessor.doesColumnExist()); - } - else - { - LodUtil.assertNotReach(); - //TODO; - } - - } - - @Override - public IFullDataSource tryPromotingToCompleteDataSource() - { - // promotion can only be completed if every column has data - if (this.isEmpty) - { - return this; - } - else if (this.isColumnNotEmpty.cardinality() != WIDTH * WIDTH) - { - return this; - } - this.isPromoted = true; - return new CompleteFullDataSource(this.sectionPos, this.mapping, this.dataArrays); - } - - @Override - public boolean hasBeenPromoted() { return this.isPromoted; } - - - - //================// - // helper classes // - //================// - - /** used when reading the datapoints to and from the {@link IStreamableFullDataSource} */ - public static class StreamDataPointContainer - { - public long[][] dataPoints; - public BitSet isColumnNotEmpty; - - public StreamDataPointContainer(long[][] dataPoints, BitSet isColumnNotEmpty) - { - this.dataPoints = dataPoints; - this.isColumnNotEmpty = isColumnNotEmpty; - } - - } - - - - //========// - // unused // - //========// - - public static boolean neededForPosition(DhSectionPos posToWrite, DhSectionPos posToTest) - { - if (!posToWrite.overlapsExactly(posToTest)) - return false; - if (posToTest.getDetailLevel() > posToWrite.getDetailLevel()) - return false; - if (posToWrite.getDetailLevel() - posToTest.getDetailLevel() <= SECTION_SIZE_OFFSET) - return true; - byte sectPerData = (byte) (1 << (posToWrite.getDetailLevel() - posToTest.getDetailLevel() - SECTION_SIZE_OFFSET)); - return posToTest.getX() % sectPerData == 0 && posToTest.getZ() % sectPerData == 0; - } - - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/interfaces/IFullDataSource.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/interfaces/IFullDataSource.java deleted file mode 100644 index 1dcaeed37..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/interfaces/IFullDataSource.java +++ /dev/null @@ -1,107 +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 . - */ - -package com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces; - -import com.seibel.distanthorizons.core.file.IDataSource; -import com.seibel.distanthorizons.core.level.IDhLevel; -import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.IFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.SingleColumnFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource; -import com.seibel.distanthorizons.core.sql.DataSourceDto; -import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; - -/** - * Base for all Full Data Source objects.

- * - * Contains full DH data, methods related to file/stream reading/writing, and the data necessary to create {@link ColumnRenderSource}'s.
- * {@link IFullDataSource}'s will either implement or contain {@link IFullDataAccessor}'s. - * - * @see IFullDataAccessor - * @see IIncompleteFullDataSource - * @see IStreamableFullDataSource - */ -public interface IFullDataSource extends IDataSource -{ - /** - * This is the byte put between different sections in the binary save file. - * The presence and absence of this byte indicates if the file is correctly formatted. - */ - int DATA_GUARD_BYTE = 0xFFFFFFFF; - /** indicates the binary save file represents an empty data source */ - int NO_DATA_FLAG_BYTE = 0x00000001; - - - - default void update(ChunkSizedFullDataAccessor chunkData, IDhLevel level) { this.update(chunkData); } - void update(ChunkSizedFullDataAccessor data); - - boolean isEmpty(); - void markNotEmpty(); - - /** AKA; the max relative position that {@link IFullDataSource#tryGet(int, int)} can accept for either X or Z */ - int getWidthInDataPoints(); - - - - //======// - // data // - //======// - - /** - * Attempts to get the data column for the given relative x and z position. - * - * @return null if the data doesn't exist - */ - @Nullable - SingleColumnFullDataAccessor tryGet(int relativeX, int relativeZ); - /** - * Attempts to get the data column for the given relative x and z position.
- * If no data exists yet an empty data column will be created. - */ - SingleColumnFullDataAccessor getOrCreate(int relativeX, int relativeZ); - - FullDataPointIdMap getMapping(); - - - - //=======================// - // basic stream handling // - //=======================// - - /** - * Should only be implemented by {@link IStreamableFullDataSource} to prevent potential stream read/write inconsistencies. - * - * @see IStreamableFullDataSource#populateFromStream(DataSourceDto, DhDataInputStream, IDhLevel) - */ - void populateFromStream(DataSourceDto dto, DhDataInputStream inputStream, IDhLevel level) throws IOException, InterruptedException; - - /** - * Should only be implemented by {@link IStreamableFullDataSource} to prevent potential stream read/write inconsistencies. - * - * @see IStreamableFullDataSource#repopulateFromStream(DataSourceDto, DhDataInputStream, IDhLevel) - */ - void repopulateFromStream(DataSourceDto dto, DhDataInputStream inputStream, IDhLevel level) throws IOException, InterruptedException; - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/interfaces/IIncompleteFullDataSource.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/interfaces/IIncompleteFullDataSource.java deleted file mode 100644 index 4514e5f45..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/interfaces/IIncompleteFullDataSource.java +++ /dev/null @@ -1,86 +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 . - */ - -package com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces; - -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.SingleColumnFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.CompleteFullDataSource; -import com.seibel.distanthorizons.core.pos.DhLodPos; -import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.util.LodUtil; - -public interface IIncompleteFullDataSource extends IFullDataSource -{ - /** - * Overwrites data in this object with non-null data from the input {@link IFullDataSource}.

- * - * This can be used to either merge same sized data sources or downsample to - */ - default void sampleFrom(IFullDataSource inputSource) - { - DhSectionPos inputPos = inputSource.getSectionPos(); - DhSectionPos thisPos = this.getSectionPos(); - LodUtil.assertTrue(inputPos.getDetailLevel() < thisPos.getDetailLevel(), "input data source at pos: ["+inputPos+"] has a lower detail level than this: ["+thisPos+"]."); - LodUtil.assertTrue(inputPos.overlapsExactly(this.getSectionPos()), "input source at pos: ["+inputPos+"] (converted to ["+inputPos.convertNewToDetailLevel(thisPos.getDetailLevel())+"]) doesn't overlap with this source's pos: ["+thisPos+"]."); - - if (inputSource.isEmpty()) - { - return; - } - - - this.markNotEmpty(); - - DhLodPos baseOffset = thisPos.getMinCornerLodPos(this.getDataDetailLevel()); - DhSectionPos inputOffset = inputPos.convertNewToDetailLevel(this.getDataDetailLevel()); - int offsetX = inputOffset.getX() - baseOffset.x; - int offsetZ = inputOffset.getZ() - baseOffset.z; - - - int numberOfDataPointsToUpdate = this.getWidthInDataPoints() / thisPos.getWidthCountForLowerDetailedSection(inputSource.getSectionPos().getDetailLevel()); // can be 0 if the input source is significantly smaller than this data source - // should be 1 at minimum, to prevent divide by zero errors (and because trying to get 0 or a fractional data point doesn't make any sense) - numberOfDataPointsToUpdate = Math.max(1, numberOfDataPointsToUpdate); - - - int inputFractionWidth = inputSource.getWidthInDataPoints() / numberOfDataPointsToUpdate; - for (int x = 0; x < numberOfDataPointsToUpdate; x++) - { - for (int z = 0; z < numberOfDataPointsToUpdate; z++) - { - SingleColumnFullDataAccessor thisDataColumn = this.getOrCreate(offsetX + x, offsetZ + z); - SingleColumnFullDataAccessor inputDataColumn = inputSource.tryGet(inputFractionWidth * x, inputFractionWidth * z); - - if (inputDataColumn != null) - { - inputDataColumn.deepCopyTo(thisDataColumn); - } - } - } - } - - /** - * Attempts to convert this {@link IIncompleteFullDataSource} into a {@link CompleteFullDataSource}. - * - * @return a new {@link CompleteFullDataSource} if successful, returns itself if not. - */ - IFullDataSource tryPromotingToCompleteDataSource(); - - boolean hasBeenPromoted(); - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/interfaces/IStreamableFullDataSource.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/interfaces/IStreamableFullDataSource.java deleted file mode 100644 index e1ce32bef..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/interfaces/IStreamableFullDataSource.java +++ /dev/null @@ -1,161 +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 . - */ - -package com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces; - -import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; -import com.seibel.distanthorizons.core.level.IDhLevel; -import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; -import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.sql.DataSourceDto; -import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream; -import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataOutputStream; -import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; - -import java.io.IOException; - -/** - * This interface holds the complete method list necessary for reading and writing a {@link IFullDataSource} - * to and from data streams.

- * - * This interface's purpose is to reduce the chance of accidentally mismatching read/write operation data types or content by splitting - * up each read/write method into small easy to understand chunks. - * - * @param defines the object holding this data source's summary data, extends {@link IStreamableFullDataSource.FullDataSourceSummaryData}. - * @param defines the object holding the data points, probably long[][] or long[][][]. - * {@link IStreamableFullDataSource#populateFromStream(DataSourceDto, DhDataInputStream, IDhLevel) populateFromStream} - * for the full reasoning. - */ -public interface IStreamableFullDataSource extends IFullDataSource -{ - - //=================// - // stream handling // - //=================// - - /** - * Clears and then overwrites any data in this object with the data from the given file and stream. - * This is expected to be used with an existing {@link IStreamableFullDataSource} and can be used in place of a constructor to reuse an existing {@link IStreamableFullDataSource} object. - * - * @see IStreamableFullDataSource#populateFromStream - */ - @Override - default void repopulateFromStream(DataSourceDto dto, DhDataInputStream inputStream, IDhLevel level) throws IOException, InterruptedException - { - // clear/overwrite the old data - this.resizeDataStructuresForRepopulation(dto.pos); - this.getMapping().clear(dto.pos); - - // set the new data - this.populateFromStream(dto, inputStream, level); - } - - /** - * Overwrites any data in this object with the data from the given file and stream. - * This is expected to be used with an empty {@link IStreamableFullDataSource} and functions similar to a constructor. - */ - @Override - default void populateFromStream(DataSourceDto dto, DhDataInputStream inputStream, IDhLevel level) throws IOException, InterruptedException - { - SummaryDataType summaryData = this.readSourceSummaryInfo(dto, inputStream, level); - this.setSourceSummaryData(summaryData); - - - DataContainerType dataPoints = this.readDataPoints(dto, summaryData.dataWidth, inputStream); - if (dataPoints == null) - { - return; - } - this.setDataPoints(dataPoints); - - - FullDataPointIdMap mapping = this.readIdMappings(dataPoints, inputStream, level.getLevelWrapper()); - this.setIdMapping(mapping); - - } - - @Override - default void writeToStream(DhDataOutputStream outputStream, IDhLevel level) throws IOException - { - this.writeSourceSummaryInfo(level, outputStream); - - boolean hasData = this.writeDataPoints(outputStream); - if (!hasData) - { - return; - } - - this.writeIdMappings(outputStream); - } - - - - /** Note: this should only be used if the data source is being reused. Normally data sources shouldn't change. */ - void resizeDataStructuresForRepopulation(DhSectionPos pos); - - /** - * Includes information about the source file that doesn't need to be saved in each data point. Like the source's size and y-level. - */ - void writeSourceSummaryInfo(IDhLevel level, DhDataOutputStream outputStream) throws IOException; - /** - * Confirms that the given {@link DataSourceDto} is valid for this {@link IStreamableFullDataSource}.
- * This specifically checks any fields that should be set when the {@link IStreamableFullDataSource} was first constructed. - * - * @throws IOException if the {@link DataSourceDto} isn't valid for this object. - */ - SummaryDataType readSourceSummaryInfo(DataSourceDto dto, DhDataInputStream inputStream, IDhLevel level) throws IOException; - void setSourceSummaryData(SummaryDataType summaryData); - - - /** @return true if any data points were present and written, false if this object was empty */ - boolean writeDataPoints(DhDataOutputStream outputStream) throws IOException; - /** @return null if no data points were present */ - DataContainerType readDataPoints(DataSourceDto dto, int width, DhDataInputStream inputStream) throws IOException; - void setDataPoints(DataContainerType dataPoints); - - - void writeIdMappings(DhDataOutputStream outputStream) throws IOException; - FullDataPointIdMap readIdMappings(DataContainerType dataPoints, DhDataInputStream inputStream, ILevelWrapper levelWrapper) throws IOException, InterruptedException; - void setIdMapping(FullDataPointIdMap mappings); - - - - //================// - // helper classes // - //================// - - /** - * This holds information that is relevant to the entire source and isn't stored in the data points.
- * Example: minimum height, detail level, source type, etc. - */ - class FullDataSourceSummaryData - { - public final int dataWidth; - public EDhApiWorldGenerationStep worldGenStep; - - - public FullDataSourceSummaryData(int dataWidth, EDhApiWorldGenerationStep worldGenStep) - { - this.dataWidth = dataWidth; - this.worldGenStep = worldGenStep; - } - - } - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/ColumnRenderSource.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/ColumnRenderSource.java index 08ef20f1d..453f5b490 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/ColumnRenderSource.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/ColumnRenderSource.java @@ -20,27 +20,24 @@ package com.seibel.distanthorizons.core.dataObjects.render; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.SingleColumnFullDataAccessor; -import com.seibel.distanthorizons.core.file.IDataSource; -import com.seibel.distanthorizons.core.level.IDhLevel; -import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; -import com.seibel.distanthorizons.core.pos.DhLodPos; -import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.transformers.FullDataToRenderDataTransformer; -import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataOutputStream; +import com.seibel.distanthorizons.core.file.DataSourcePool; +import com.seibel.distanthorizons.core.file.IDataSource; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.pos.DhBlockPos2D; +import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnQuadView; -import com.seibel.distanthorizons.core.dataObjects.render.columnViews.IColumnDataView; import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; import com.seibel.distanthorizons.core.util.ColorUtil; import com.seibel.distanthorizons.core.util.RenderDataPointUtil; -import com.seibel.distanthorizons.core.util.LodUtil; +import it.unimi.dsi.fastutil.longs.LongArrayList; import org.apache.logging.log4j.Logger; -import java.io.*; +import java.util.Arrays; import java.util.concurrent.atomic.AtomicLong; /** @@ -56,34 +53,20 @@ public class ColumnRenderSource implements IDataSource public static final byte SECTION_SIZE_OFFSET = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; public static final int SECTION_SIZE = BitShiftUtil.powerOfTwo(SECTION_SIZE_OFFSET); - public static final byte DATA_FORMAT_VERSION = 1; - @Override - public byte getDataFormatVersion() { return DATA_FORMAT_VERSION; } - - public static final String DATA_NAME = "ColumnRenderSource"; - @Override - public String getDataTypeName() { return DATA_NAME; } - - /** - * This is the byte put between different sections in the binary save file. - * The presence and absence of this byte indicates if the file is correctly formatted. - */ - public static final int DATA_GUARD_BYTE = 0xFFFFFFFF; - /** indicates the binary save file represents an empty data source */ - public static final int NO_DATA_FLAG_BYTE = 0x00000001; + public static final DataSourcePool DATA_SOURCE_POOL = new DataSourcePool<>(ColumnRenderSource::createEmptyRenderSource, null /* data source prep/cleanup needs to be done outside the pool since it requires additional inputs */); + /** will be zero if an empty data source was created */ public int verticalDataCount; - public final DhSectionPos sectionPos; - public final int yOffset; + public DhSectionPos pos; + public int yOffset; - public long[] renderDataContainer; + public LongArrayList renderDataContainer; public final DebugSourceFlag[] debugSourceFlags; private boolean isEmpty = true; - public EDhApiWorldGenerationStep worldGenStep; public AtomicLong localVersion = new AtomicLong(0); // used to track changes to the data source, so that buffers can be updated when necessary @@ -93,44 +76,53 @@ public class ColumnRenderSource implements IDataSource // constructors // //==============// - public static ColumnRenderSource createEmptyRenderSource(DhSectionPos sectionPos) { return new ColumnRenderSource(sectionPos, 0, 0); } + /** + * This is separate from {@link DataSourcePool#getPooledSource(DhSectionPos, boolean)} + * because we need to pass in a couple extra values, + * specifically maxVerticalSize and yOffset. + */ + public static ColumnRenderSource getPooledRenderSource(DhSectionPos pos, int maxVerticalSize, int yOffset, boolean clearData) + { + ColumnRenderSource renderSource = DATA_SOURCE_POOL.getPooledSource(pos); + + // set necessary properties + renderSource.pos = pos; + renderSource.verticalDataCount = maxVerticalSize; + renderSource.yOffset = yOffset; + + + // resize the array if necessary + int dataArraySize = SECTION_SIZE * SECTION_SIZE * maxVerticalSize; + renderSource.renderDataContainer.ensureCapacity(dataArraySize); + while (renderSource.renderDataContainer.size() < dataArraySize) + { + renderSource.renderDataContainer.add(0); + } + + if (clearData) + { + Arrays.fill(renderSource.renderDataContainer.elements(), 0); + Arrays.fill(renderSource.debugSourceFlags, null); + } + + return renderSource; + } + + + private static ColumnRenderSource createEmptyRenderSource(DhSectionPos sectionPos) { return new ColumnRenderSource(sectionPos, 0, 0); } /** * Creates an empty ColumnRenderSource. * - * @param sectionPos the relative position of the container + * @param pos the relative position of the container * @param maxVerticalSize the maximum vertical size of the container */ - public ColumnRenderSource(DhSectionPos sectionPos, int maxVerticalSize, int yOffset) + private ColumnRenderSource(DhSectionPos pos, int maxVerticalSize, int yOffset) { this.verticalDataCount = maxVerticalSize; - this.renderDataContainer = new long[SECTION_SIZE * SECTION_SIZE * this.verticalDataCount]; + this.renderDataContainer = new LongArrayList(new long[SECTION_SIZE * SECTION_SIZE * this.verticalDataCount]); this.debugSourceFlags = new DebugSourceFlag[SECTION_SIZE * SECTION_SIZE]; - this.sectionPos = sectionPos; + this.pos = pos; this.yOffset = yOffset; - this.worldGenStep = EDhApiWorldGenerationStep.EMPTY; - } - - /** - * Creates a new ColumnRenderSource from the parsedColumnData. - * - * @throws IOException if the DataInputStream's detail level isn't what was expected - */ - public ColumnRenderSource(DhSectionPos sectionPos, ColumnRenderSourceLoader.ParsedColumnData parsedColumnData, IDhLevel level) throws IOException - { - if (sectionPos.getDetailLevel() - SECTION_SIZE_OFFSET != parsedColumnData.detailLevel) - { - throw new IOException("Invalid data: detail level does not match"); - } - - this.sectionPos = sectionPos; - this.yOffset = level.getMinY(); - this.verticalDataCount = parsedColumnData.verticalSize; - this.renderDataContainer = parsedColumnData.dataContainer; - this.worldGenStep = parsedColumnData.worldGenStep; - this.isEmpty = parsedColumnData.isEmpty; - - this.debugSourceFlags = new DebugSourceFlag[SECTION_SIZE * SECTION_SIZE]; - this.fillDebugFlag(0, 0, SECTION_SIZE, SECTION_SIZE, DebugSourceFlag.FILE); } @@ -139,65 +131,7 @@ public class ColumnRenderSource implements IDataSource // datapoint manipulation // //========================// - public void clearDataPoint(int posX, int posZ) - { - for (int verticalIndex = 0; verticalIndex < this.verticalDataCount; verticalIndex++) - { - this.renderDataContainer[posX * SECTION_SIZE * this.verticalDataCount + posZ * this.verticalDataCount + verticalIndex] = RenderDataPointUtil.EMPTY_DATA; - } - } - - public boolean setDataPoint(long data, int posX, int posZ, int verticalIndex) - { - this.renderDataContainer[posX * SECTION_SIZE * this.verticalDataCount + posZ * this.verticalDataCount + verticalIndex] = data; - return true; - } - - public boolean copyVerticalData(IColumnDataView newData, int posX, int posZ, boolean overwriteDataWithSameGenerationMode) - { - if (DO_SAFETY_CHECKS) - { - if (newData.size() != this.verticalDataCount) - throw new IllegalArgumentException("newData size not the same as this column's vertical size"); - if (posX < 0 || posX >= SECTION_SIZE) - throw new IllegalArgumentException("X position is out of bounds"); - if (posZ < 0 || posZ >= SECTION_SIZE) - throw new IllegalArgumentException("Z position is out of bounds"); - } - - int dataOffset = posX * SECTION_SIZE * this.verticalDataCount + posZ * this.verticalDataCount; - int compare = RenderDataPointUtil.compareDatapointPriority(newData.get(0), this.renderDataContainer[dataOffset]); - if (overwriteDataWithSameGenerationMode) - { - if (compare < 0) - { - return false; - } - } - else - { - if (compare <= 0) - { - return false; - } - } - - // copy the newData into this column's data - newData.copyTo(this.renderDataContainer, dataOffset, newData.size()); - return true; - } - - - public long getFirstDataPoint(int posX, int posZ) { return getDataPoint(posX, posZ, 0); } - public long getDataPoint(int posX, int posZ, int verticalIndex) { return this.renderDataContainer[posX * SECTION_SIZE * this.verticalDataCount + posZ * this.verticalDataCount + verticalIndex]; } - - public long[] getVerticalDataPointArray(int posX, int posZ) - { - long[] result = new long[this.verticalDataCount]; - int index = posX * SECTION_SIZE * this.verticalDataCount + posZ * this.verticalDataCount; - System.arraycopy(this.renderDataContainer, index, result, 0, this.verticalDataCount); - return result; - } + public long getDataPoint(int posX, int posZ, int verticalIndex) { return this.renderDataContainer.getLong(posX * SECTION_SIZE * this.verticalDataCount + posZ * this.verticalDataCount + verticalIndex); } public ColumnArrayView getVerticalDataPointView(int posX, int posZ) { @@ -209,201 +143,70 @@ public class ColumnRenderSource implements IDataSource public ColumnQuadView getFullQuadView() { return this.getQuadViewOverRange(0, 0, SECTION_SIZE, SECTION_SIZE); } public ColumnQuadView getQuadViewOverRange(int quadX, int quadZ, int quadXSize, int quadZSize) { return new ColumnQuadView(this.renderDataContainer, SECTION_SIZE, this.verticalDataCount, quadX, quadZ, quadXSize, quadZSize); } - public int getVerticalSize() { return this.verticalDataCount; } - - //========================// - // data update and output // - //========================// + //=============// + // data update // + //=============// @Override - public void writeToStream(DhDataOutputStream outputStream, IDhClientLevel level) throws IOException { this.writeToStream(outputStream); } - public void writeToStream(DhDataOutputStream outputStream) throws IOException + public boolean update(FullDataSourceV2 inputFullDataSource, IDhClientLevel level) { - outputStream.flush(); - - outputStream.writeByte(this.getDataDetailLevel()); - outputStream.writeInt(this.verticalDataCount); - - if (this.isEmpty) - { - // no data is present - outputStream.writeByte(NO_DATA_FLAG_BYTE); - } - else - { - // data is present - outputStream.writeByte(DATA_GUARD_BYTE); - outputStream.writeInt(this.yOffset); - - // write the data for each column - for (int xz = 0; xz < SECTION_SIZE * SECTION_SIZE; xz++) - { - for (int y = 0; y < this.verticalDataCount; y++) - { - long currentDatapoint = this.renderDataContainer[xz * this.verticalDataCount + y]; - outputStream.writeLong(Long.reverseBytes(currentDatapoint)); // the reverse bytes is necessary to ensure the data is read in correctly - } - } - } - - outputStream.writeByte(DATA_GUARD_BYTE); - outputStream.writeByte(this.worldGenStep.value); - - outputStream.flush(); - } - - /** Overrides any data that has not been written directly using write(). Skips empty source dataPoints. */ - public void updateFromRenderSource(ColumnRenderSource renderSource) - { - // validate we are writing for the same location - LodUtil.assertTrue(renderSource.sectionPos.equals(this.sectionPos)); - - // change the vertical size if necessary (this can happen if the vertical quality was changed in the config) - this.clearAndChangeVerticalSize(renderSource.verticalDataCount); - // validate both objects have the same number of dataPoints - LodUtil.assertTrue(renderSource.verticalDataCount == this.verticalDataCount); - - - if (renderSource.isEmpty) - { - // the source is empty, don't attempt to update anything - return; - } - // the source isn't empty, this object won't be empty after the method finishes - this.isEmpty = false; - - localVersion.incrementAndGet(); - } - /** - * If the newVerticalSize is different than the current verticalSize, - * this will delete any data currently in this object and re-size it.
- * Otherwise this method will do nothing. - */ - private void clearAndChangeVerticalSize(int newVerticalSize) - { - if (newVerticalSize != this.verticalDataCount) - { - this.verticalDataCount = newVerticalSize; - this.renderDataContainer = new long[SECTION_SIZE * SECTION_SIZE * this.verticalDataCount]; - this.localVersion.incrementAndGet(); - } - } - - @Override - public void update(ChunkSizedFullDataAccessor chunkDataView, IDhClientLevel level) - { - final String errorMessagePrefix = "Unable to complete fastWrite for RenderSource pos: [" + this.sectionPos + "] and chunk pos: [" + chunkDataView.chunkPos + "]. Error:"; - - final DhSectionPos renderSourcePos = this.getSectionPos(); - - final int sourceBlockX = renderSourcePos.getMinCornerLodPos().getCornerBlockPos().x; - final int sourceBlockZ = renderSourcePos.getMinCornerLodPos().getCornerBlockPos().z; - - // offset between the incoming chunk data and this render source - final int blockOffsetX = (chunkDataView.chunkPos.x * LodUtil.CHUNK_WIDTH) - sourceBlockX; - final int blockOffsetZ = (chunkDataView.chunkPos.z * LodUtil.CHUNK_WIDTH) - sourceBlockZ; - - final int sourceDataPointBlockWidth = BitShiftUtil.powerOfTwo(this.getDataDetailLevel()); + final String errorMessagePrefix = "Unable to complete update for RenderSource pos: [" + this.pos + "] and pos: [" + inputFullDataSource.getPos() + "]. Error:"; boolean dataChanged = false; - - if (chunkDataView.detailLevel == this.getDataDetailLevel()) + if (inputFullDataSource.getPos().getDetailLevel() == this.pos.getDetailLevel()) { - this.markNotEmpty(); - // confirm the render source contains this chunk - if (blockOffsetX < 0 - || blockOffsetX + LodUtil.CHUNK_WIDTH > this.getWidthInDataPoints() - || blockOffsetZ < 0 - || blockOffsetZ + LodUtil.CHUNK_WIDTH > this.getWidthInDataPoints()) + try { - LOGGER.warn(errorMessagePrefix+"Data offset is out of bounds."); - return; - } - - - if (Thread.interrupted()) - { - LOGGER.warn(errorMessagePrefix+"write interrupted."); - return; - } - - - for (int x = 0; x < LodUtil.CHUNK_WIDTH; x++) - { - for (int z = 0; z < LodUtil.CHUNK_WIDTH; z++) + if (Thread.interrupted()) { - ColumnArrayView columnArrayView = this.getVerticalDataPointView(blockOffsetX + x, blockOffsetZ + z); - int hash = columnArrayView.getDataHash(); - SingleColumnFullDataAccessor fullArrayView = chunkDataView.get(x, z); - FullDataToRenderDataTransformer.convertColumnData(level, - sourceBlockX + sourceDataPointBlockWidth * (blockOffsetX + x), - sourceBlockZ + sourceDataPointBlockWidth * (blockOffsetZ + z), - columnArrayView, fullArrayView); - dataChanged |= hash != columnArrayView.getDataHash(); + LOGGER.warn(errorMessagePrefix + "write interrupted."); + return false; + } + + + + DhBlockPos2D centerBlockPos = inputFullDataSource.getPos().getCenterBlockPos(); + int halfBlockWidth = inputFullDataSource.getPos().getBlockWidth() / 2; + DhBlockPos2D minBlockPos = new DhBlockPos2D(centerBlockPos.x - halfBlockWidth, centerBlockPos.z - halfBlockWidth); + + for (int x = 0; x < FullDataSourceV2.WIDTH; x++) + { + for (int z = 0; z < FullDataSourceV2.WIDTH; z++) + { + ColumnArrayView columnArrayView = this.getVerticalDataPointView(x, z); + int columnHash = columnArrayView.getDataHash(); + + LongArrayList dataColumn = inputFullDataSource.get(x, z); + EDhApiWorldGenerationStep worldGenStep = inputFullDataSource.getWorldGenStepAtRelativePos(x, z); + if (dataColumn != null && worldGenStep != EDhApiWorldGenerationStep.EMPTY) + { + FullDataToRenderDataTransformer.convertColumnData( + level, inputFullDataSource.mapping, + minBlockPos.x + x, + minBlockPos.z + z, + columnArrayView, dataColumn); + dataChanged |= columnHash != columnArrayView.getDataHash(); + + this.fillDebugFlag(x, z, 1, 1, ColumnRenderSource.DebugSourceFlag.DIRECT); + } + } } } - this.fillDebugFlag(blockOffsetX, blockOffsetZ, LodUtil.CHUNK_WIDTH, LodUtil.CHUNK_WIDTH, ColumnRenderSource.DebugSourceFlag.DIRECT); - } - else if (chunkDataView.detailLevel < this.getDataDetailLevel() && this.getDataDetailLevel() <= chunkDataView.getSectionPos().getDetailLevel()) - { - this.markNotEmpty(); - // multiple chunk data points converting to 1 column data point - DhLodPos dataCornerPos = chunkDataView.getSectionPos().getMinCornerLodPos(chunkDataView.detailLevel); - DhLodPos sourceCornerPos = renderSourcePos.getMinCornerLodPos(this.getDataDetailLevel()); - DhLodPos sourceStartingChangePos = dataCornerPos.convertToDetailLevel(this.getDataDetailLevel()); - int relStartX = Math.floorMod(sourceStartingChangePos.x, this.getWidthInDataPoints()); - int relStartZ = Math.floorMod(sourceStartingChangePos.z, this.getWidthInDataPoints()); - int dataToSourceScale = sourceCornerPos.getWidthAtDetail(chunkDataView.detailLevel); - int columnsInChunk = chunkDataView.getSectionPos().getWidthCountForLowerDetailedSection(this.getDataDetailLevel()); - - for (int xOffset = 0; xOffset < columnsInChunk; xOffset++) + catch (Exception e) { - for (int zOffset = 0; zOffset < columnsInChunk; zOffset++) - { - int relSourceX = relStartX + xOffset; - int relSourceZ = relStartZ + zOffset; - ColumnArrayView columnArrayView = this.getVerticalDataPointView(relSourceX, relSourceZ); - int hash = columnArrayView.getDataHash(); - SingleColumnFullDataAccessor fullArrayView = chunkDataView.get(xOffset * dataToSourceScale, zOffset * dataToSourceScale); - FullDataToRenderDataTransformer.convertColumnData(level, - sourceBlockX + sourceDataPointBlockWidth * relSourceX, - sourceBlockZ + sourceDataPointBlockWidth * relSourceZ, - columnArrayView, fullArrayView); - dataChanged |= hash != columnArrayView.getDataHash(); - } + LOGGER.error(errorMessagePrefix + e.getMessage(), e); } - this.fillDebugFlag(relStartX, relStartZ, columnsInChunk, columnsInChunk, ColumnRenderSource.DebugSourceFlag.DIRECT); } - else if (chunkDataView.getSectionPos().getDetailLevel() < this.getDataDetailLevel()) - { - // The entire chunk is being converted to a single column data point, possibly. - DhLodPos dataCornerPos = chunkDataView.getSectionPos().getMinCornerLodPos(chunkDataView.detailLevel); - DhLodPos sourceCornerPos = renderSourcePos.getMinCornerLodPos(this.getDataDetailLevel()); - DhLodPos sourceStartingChangePos = dataCornerPos.convertToDetailLevel(this.getDataDetailLevel()); - int chunksPerColumn = sourceStartingChangePos.getWidthAtDetail(chunkDataView.getSectionPos().getDetailLevel()); - if (chunkDataView.getSectionPos().getX() % chunksPerColumn != 0 || chunkDataView.getSectionPos().getZ() % chunksPerColumn != 0) - { - return; // not a multiple of the column size, so no change - } - int relStartX = Math.floorMod(sourceStartingChangePos.x, this.getWidthInDataPoints()); - int relStartZ = Math.floorMod(sourceStartingChangePos.z, this.getWidthInDataPoints()); - ColumnArrayView columnArrayView = this.getVerticalDataPointView(relStartX, relStartZ); - int hash = columnArrayView.getDataHash(); - SingleColumnFullDataAccessor fullArrayView = chunkDataView.get(0, 0); - FullDataToRenderDataTransformer.convertColumnData(level, dataCornerPos.x * sourceDataPointBlockWidth, - dataCornerPos.z * sourceDataPointBlockWidth, - columnArrayView, fullArrayView); - dataChanged = hash != columnArrayView.getDataHash(); - this.fillDebugFlag(relStartX, relStartZ, 1, 1, ColumnRenderSource.DebugSourceFlag.DIRECT); - } - if (dataChanged) { this.localVersion.incrementAndGet(); + this.markNotEmpty(); } + + return dataChanged; } @@ -412,39 +215,11 @@ public class ColumnRenderSource implements IDataSource // data helper methods // //=====================// - public boolean doesDataPointExist(int posX, int posZ) { return RenderDataPointUtil.doesDataPointExist(this.getFirstDataPoint(posX, posZ)); } - - public void generateData(ColumnRenderSource lowerDataContainer, int posX, int posZ) - { - ColumnArrayView outputView = this.getVerticalDataPointView(posX, posZ); - ColumnQuadView quadView = lowerDataContainer.getQuadViewOverRange(posX * 2, posZ * 2, 2, 2); - outputView.mergeMultiDataFrom(quadView); - } - - public int getMaxLodCount() { return SECTION_SIZE * SECTION_SIZE * this.getVerticalSize(); } - - public long getRoughRamUsageInBytes() { return (long) this.renderDataContainer.length * Long.BYTES; } - - public DhSectionPos getSectionPos() { return this.sectionPos; } + public DhSectionPos getPos() { return this.pos; } @Override - public String getPrimaryKeyString() { return this.sectionPos.serialize(); } + public DhSectionPos getKey() { return this.pos; } - public byte getDataDetailLevel() { return (byte) (this.sectionPos.getDetailLevel() - SECTION_SIZE_OFFSET); } - - @Override - public EDhApiWorldGenerationStep getWorldGenStep() { return EDhApiWorldGenerationStep.EMPTY; } - - /** @return how many data points wide this {@link ColumnRenderSource} is. */ - public int getWidthInDataPoints() { return BitShiftUtil.powerOfTwo(this.getDetailOffset()); } - public byte getDetailOffset() { return SECTION_SIZE_OFFSET; } - - public byte getRenderDataFormatVersion() { return DATA_FORMAT_VERSION; } - - /** - * Whether this object is still valid. If not, a new one should be created. - * TODO this will be necessary for dedicated multiplayer support, if the server has newer data this section should no longer be valid - */ - public boolean isValid() { return true; } + public byte getDataDetailLevel() { return (byte) (this.pos.getDetailLevel() - SECTION_SIZE_OFFSET); } public boolean isEmpty() { return this.isEmpty; } public void markNotEmpty() { this.isEmpty = false; } @@ -493,7 +268,6 @@ public class ColumnRenderSource implements IDataSource this.debugSourceFlags[x * SECTION_SIZE + z] = flag; } } - localVersion.incrementAndGet(); } public DebugSourceFlag debugGetFlag(int ox, int oz) { return this.debugSourceFlags[ox * SECTION_SIZE + oz]; } @@ -512,7 +286,7 @@ public class ColumnRenderSource implements IDataSource String SUBDATA_DELIMITER = ","; StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.append(this.sectionPos); + stringBuilder.append(this.pos); stringBuilder.append(LINE_DELIMITER); int size = 1; @@ -538,6 +312,12 @@ public class ColumnRenderSource implements IDataSource return stringBuilder.toString(); } + @Override + public void close() throws Exception + { + DATA_SOURCE_POOL.returnPooledDataSource(this); + } + //==============// @@ -548,7 +328,6 @@ public class ColumnRenderSource implements IDataSource { FULL(ColorUtil.BLUE), DIRECT(ColorUtil.WHITE), - SPARSE(ColorUtil.YELLOW), FILE(ColorUtil.BROWN); public final int color; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/ColumnRenderSourceLoader.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/ColumnRenderSourceLoader.java deleted file mode 100644 index 538dac67d..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/ColumnRenderSourceLoader.java +++ /dev/null @@ -1,171 +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 . - */ - -package com.seibel.distanthorizons.core.dataObjects.render; - -import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; -import com.seibel.distanthorizons.core.level.IDhLevel; -import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; -import com.seibel.distanthorizons.core.sql.DataSourceDto; -import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream; -import org.apache.logging.log4j.Logger; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -/** - * Handles loading and parsing {@link DataSourceDto}s to create {@link ColumnRenderSource}s.

- * - * Please see the {@link ColumnRenderSourceLoader#loadRenderSource} method to see what - * file versions this class can handle. - */ -public class ColumnRenderSourceLoader -{ - public static ColumnRenderSourceLoader INSTANCE = new ColumnRenderSourceLoader(); - - private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - - - - private ColumnRenderSourceLoader() { } - - - - public ColumnRenderSource loadRenderSource(DataSourceDto dto, DhDataInputStream inputStream, IDhLevel level) throws IOException - { - int dataFileVersion = dto.binaryDataFormatVersion; - - switch (dataFileVersion) - { - case 1: - ParsedColumnData parsedColumnData = readDataV1(inputStream, level.getMinY()); - return new ColumnRenderSource(dto.pos, parsedColumnData, level); - default: - throw new IOException("Invalid Data: The data version [" + dataFileVersion + "] is not supported"); - } - } - - - - //========================// - // versioned file parsing // - //========================// - - /** - * @param inputStream Expected format: 1st byte: detail level, 2nd byte: vertical size, 3rd byte on: column data - * @throws IOException if there was an issue reading the stream - */ - private static ParsedColumnData readDataV1(DhDataInputStream inputStream, int expectedYOffset) throws IOException - { - // TODO move into ColumnRenderSource - - byte detailLevel = inputStream.readByte(); - - int verticalDataCount = inputStream.readInt(); - if (verticalDataCount <= 0) - { - throw new IOException("Invalid data: vertical size must be 0 or greater"); - } - - int maxNumberOfDataPoints = ColumnRenderSource.SECTION_SIZE * ColumnRenderSource.SECTION_SIZE * verticalDataCount; - - - byte dataPresentFlag = inputStream.readByte(); - if (dataPresentFlag != ColumnRenderSource.NO_DATA_FLAG_BYTE && dataPresentFlag != ColumnRenderSource.DATA_GUARD_BYTE) - { - throw new IOException("Incorrect render file format. Expected either: NO_DATA_FLAG_BYTE [" + ColumnRenderSource.NO_DATA_FLAG_BYTE + "] or DATA_GUARD_BYTE [" + ColumnRenderSource.DATA_GUARD_BYTE + "], Found: [" + dataPresentFlag + "]"); - } - else if (dataPresentFlag == ColumnRenderSource.NO_DATA_FLAG_BYTE) - { - // no data is present - return new ParsedColumnData(detailLevel, verticalDataCount, EDhApiWorldGenerationStep.EMPTY, new long[maxNumberOfDataPoints], true); - } - else - { - // data is present - - int fileYOffset = inputStream.readInt(); - if (fileYOffset != expectedYOffset) - { - throw new IOException("Invalid data: yOffset is incorrect. Expected: [" + expectedYOffset + "], found: [" + fileYOffset + "]."); - } - - - // read the column data - byte[] rawByteData = new byte[maxNumberOfDataPoints * Long.BYTES]; - ByteBuffer columnDataByteBuffer = ByteBuffer.wrap(rawByteData).order(ByteOrder.LITTLE_ENDIAN); - inputStream.readFully(rawByteData); - - - // parse the column data - long[] dataPoints = new long[maxNumberOfDataPoints]; - columnDataByteBuffer.asLongBuffer().get(dataPoints); - - boolean isEmpty = true; - for (long dataPoint : dataPoints) - { - if (dataPoint != 0) - { - isEmpty = false; - break; - } - } - - - - byte guardByteFlag = inputStream.readByte(); - if (guardByteFlag != ColumnRenderSource.DATA_GUARD_BYTE) - { - throw new IOException("invalid world gen step end guard"); - } - EDhApiWorldGenerationStep worldGenStep = EDhApiWorldGenerationStep.fromValue(inputStream.readByte()); - if (worldGenStep == null) - { - LOGGER.warn("Missing WorldGenStep, defaulting to: " + EDhApiWorldGenerationStep.SURFACE.name()); - worldGenStep = EDhApiWorldGenerationStep.SURFACE; - } - - - - return new ParsedColumnData(detailLevel, verticalDataCount, worldGenStep, dataPoints, isEmpty); - } - } - - public static class ParsedColumnData - { - public final byte detailLevel; - public final int verticalSize; - public final EDhApiWorldGenerationStep worldGenStep; - public final long[] dataContainer; - public final boolean isEmpty; - - public ParsedColumnData(byte detailLevel, int verticalSize, EDhApiWorldGenerationStep worldGenStep, long[] dataContainer, boolean isEmpty) - { - this.detailLevel = detailLevel; - this.verticalSize = verticalSize; - this.worldGenStep = worldGenStep; - this.dataContainer = dataContainer; - this.isEmpty = isEmpty; - } - - } - - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/BufferQuad.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/BufferQuad.java index d9e83458f..8392c25c3 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/BufferQuad.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/BufferQuad.java @@ -151,6 +151,7 @@ public final class BufferQuad { if (quad.hasError || this.hasError) return false; + // only merge quads that are in the same direction if (this.direction != quad.direction) return false; @@ -284,10 +285,17 @@ public final class BufferQuad if (thisPerpendicularCompareStartPos < otherPerpendicularCompareStartPos + otherPerpendicularCompareWidth) { // these quads are overlapping, they can't be merged - //EVENT_LOGGER.warn("Overlapping quads detected!"); - quad.hasError = true; - this.hasError = true; + + // Overlapping quads appear to render correctly, why are we marking them as errored? + // Is it possible the wrong quad will be extended thus the wrong color is rendered? + // Or is that the height/depth might be wrong? + if (Config.Client.Advanced.Debugging.showOverlappingQuadErrors.get()) + { + quad.hasError = true; + this.hasError = true; + } } + return false; } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnBox.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnBox.java index d7535a524..edf2a4c67 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnBox.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnBox.java @@ -22,7 +22,9 @@ package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.enums.EDhDirection; 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.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView; import com.seibel.distanthorizons.core.render.renderer.LodRenderer; @@ -63,13 +65,12 @@ public class ColumnBox // 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) + && 0 == skyLight + && builder.skyLightCullingBelow > maxY + && ( + (RenderDataPointUtil.getAlpha(topData) < 255 && RenderDataPointUtil.getYMax(topData) >= builder.skyLightCullingBelow) + || (RenderDataPointUtil.getYMin(topData) >= builder.skyLightCullingBelow) + || !RenderDataPointUtil.doesDataPointExist(topData) ) ) { @@ -124,7 +125,7 @@ public class ColumnBox // add an adjacent face if this is opaque face or transparent over the void if (!isTransparent || overVoid) { - builder.addQuadAdj(EDhDirection.NORTH, x, minY, z, xSize, ySize, color, irisBlockMaterialId, (byte) 15, blockLight); + builder.addQuadAdj(EDhDirection.NORTH, x, minY, z, xSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); } } else if (adjDataNorth.length == 1) @@ -151,7 +152,7 @@ public class ColumnBox if (adjDataSouth == null) { if (!isTransparent || overVoid) - builder.addQuadAdj(EDhDirection.SOUTH, x, minY, maxZ, xSize, ySize, color, irisBlockMaterialId, (byte) 15, blockLight); + builder.addQuadAdj(EDhDirection.SOUTH, x, minY, maxZ, xSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); } else if (adjDataSouth.length == 1) { @@ -178,7 +179,7 @@ public class ColumnBox if (adjDataWest == null) { if (!isTransparent || overVoid) - builder.addQuadAdj(EDhDirection.WEST, x, minY, z, zSize, ySize, color, irisBlockMaterialId, (byte) 15, blockLight); + builder.addQuadAdj(EDhDirection.WEST, x, minY, z, zSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); } else if (adjDataWest.length == 1) { @@ -204,7 +205,7 @@ public class ColumnBox if (adjData[EDhDirection.EAST.ordinal() - 2] == null) { if (!isTransparent || overVoid) - builder.addQuadAdj(EDhDirection.EAST, maxX, minY, z, zSize, ySize, color, irisBlockMaterialId, (byte) 15, blockLight); + builder.addQuadAdj(EDhDirection.EAST, maxX, minY, z, zSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); } else if (adjDataEast.length == 1) { @@ -236,7 +237,7 @@ public class ColumnBox if (adjColumnView == null || 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, (byte) 15, blockLight); + builder.addQuadAdj(direction, x, yMin, z, horizontalWidth, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); return; } @@ -393,7 +394,7 @@ public class ColumnBox // 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, (byte) 15, (byte) 15); + builder.addQuadAdj(direction, x, yMin, z, horizontalWidth, ySize, debugOverlapColor, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, LodUtil.MAX_MC_LIGHT); } } else @@ -402,7 +403,7 @@ public class ColumnBox if (adjYMax > yMin && debugOverlapColor != 0) { - builder.addQuadAdj(direction, x, yMin, z, horizontalWidth, (short) (adjYMax - yMin), debugOverlapColor, irisBlockMaterialId, (byte) 15, (byte) 15); + 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, @@ -448,7 +449,7 @@ public class ColumnBox // 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, (byte) 15, (byte) 15); + 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 @@ -459,7 +460,7 @@ public class ColumnBox // _______&&: y < depth ______ < yMax if (debugOverlapColor != 0) { - builder.addQuadAdj(direction, x, adjYMin, z, horizontalWidth, (short) (adjYMax - adjYMin), debugOverlapColor, irisBlockMaterialId, (byte) 15, (byte) 15); + builder.addQuadAdj(direction, x, adjYMin, z, horizontalWidth, (short) (adjYMax - adjYMin), debugOverlapColor, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, LodUtil.MAX_MC_LIGHT); } if (firstFace) @@ -474,6 +475,12 @@ public class ColumnBox throw new RuntimeException("Loop error"); if (previousAdjDepth > adjYMax) { + if (irisBlockMaterialId == IBlockStateWrapper.IrisBlockMaterial.GRASS) + { + // this LOD is underneath another, grass will never show here + irisBlockMaterialId = IBlockStateWrapper.IrisBlockMaterial.DIRT; + } + builder.addQuadAdj(direction, x, adjYMax, z, horizontalWidth, (short) (previousAdjDepth - adjYMax), color, irisBlockMaterialId, RenderDataPointUtil.getLightSky(adjPoint), blockLight); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnRenderBuffer.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnRenderBuffer.java index e041efb21..05eb3b340 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnRenderBuffer.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnRenderBuffer.java @@ -25,14 +25,12 @@ import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.enums.EGLProxyContext; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.pos.DhBlockPos; -import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.render.AbstractRenderBuffer; 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.util.LodUtil; import com.seibel.distanthorizons.core.util.objects.StatsMap; -import com.seibel.distanthorizons.api.enums.config.EGpuUploadMethod; +import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod; import com.seibel.distanthorizons.core.util.*; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import org.apache.logging.log4j.Logger; @@ -46,13 +44,19 @@ import java.util.concurrent.*; * * @see ColumnRenderBufferBuilder */ -public class ColumnRenderBuffer extends AbstractRenderBuffer +public class ColumnRenderBuffer implements AutoCloseable { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final IMinecraftClientWrapper minecraftClient = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final long MAX_BUFFER_UPLOAD_TIMEOUT_NANOSECONDS = 1_000_000; + public static final int QUADS_BYTE_SIZE = LodUtil.LOD_VERTEX_FORMAT.getByteSize() * 4; // TODO what does the 4 represent + public static final int MAX_QUADS_PER_BUFFER = (1024 * 1024 * 1) / QUADS_BYTE_SIZE; // TODO what do these multiples represent? + public static final int FULL_SIZED_BUFFER = MAX_QUADS_PER_BUFFER * QUADS_BYTE_SIZE; + + + public final DhBlockPos pos; @@ -61,17 +65,15 @@ public class ColumnRenderBuffer extends AbstractRenderBuffer private GLVertexBuffer[] vbos; private GLVertexBuffer[] vbosTransparent; - private final DhSectionPos debugPos; //==============// // constructors // //==============// - public ColumnRenderBuffer(DhBlockPos pos, DhSectionPos debugPos) + public ColumnRenderBuffer(DhBlockPos pos) { this.pos = pos; - this.debugPos = debugPos; this.vbos = new GLVertexBuffer[0]; this.vbosTransparent = new GLVertexBuffer[0]; } @@ -85,7 +87,7 @@ public class ColumnRenderBuffer extends AbstractRenderBuffer //==================// /** Should be run on a DH thread. */ - public void uploadBuffer(LodQuadBuilder builder, EGpuUploadMethod gpuUploadMethod) throws InterruptedException + public void uploadBuffer(LodQuadBuilder builder, EDhApiGpuUploadMethod gpuUploadMethod) throws InterruptedException { 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."); @@ -144,7 +146,7 @@ public class ColumnRenderBuffer extends AbstractRenderBuffer } } - private void uploadBuffersUsingUploadMethod(LodQuadBuilder builder, EGpuUploadMethod gpuUploadMethod) throws InterruptedException + private void uploadBuffersUsingUploadMethod(LodQuadBuilder builder, EDhApiGpuUploadMethod gpuUploadMethod) throws InterruptedException { if (gpuUploadMethod.useEarlyMapping) { @@ -160,7 +162,7 @@ public class ColumnRenderBuffer extends AbstractRenderBuffer - private void uploadBuffersMapped(LodQuadBuilder builder, EGpuUploadMethod method) + private void uploadBuffersMapped(LodQuadBuilder builder, EDhApiGpuUploadMethod method) { // opaque vbos // @@ -196,7 +198,7 @@ public class ColumnRenderBuffer extends AbstractRenderBuffer } } - private void uploadBuffersDirect(LodQuadBuilder builder, EGpuUploadMethod method) throws InterruptedException + private void uploadBuffersDirect(LodQuadBuilder builder, EDhApiGpuUploadMethod method) throws InterruptedException { this.vbos = ColumnRenderBufferBuilder.resizeBuffer(this.vbos, builder.getCurrentNeededOpaqueVertexBufferCount()); uploadBuffersDirect(this.vbos, builder.makeOpaqueVertexBuffers(), method); @@ -204,7 +206,7 @@ public class ColumnRenderBuffer extends AbstractRenderBuffer this.vbosTransparent = ColumnRenderBufferBuilder.resizeBuffer(this.vbosTransparent, builder.getCurrentNeededTransparentVertexBufferCount()); uploadBuffersDirect(this.vbosTransparent, builder.makeTransparentVertexBuffers(), method); } - private static void uploadBuffersDirect(GLVertexBuffer[] vbos, Iterator iter, EGpuUploadMethod method) throws InterruptedException + private static void uploadBuffersDirect(GLVertexBuffer[] vbos, Iterator iter, EDhApiGpuUploadMethod method) throws InterruptedException { long remainingMS = 0; long MBPerMS = Config.Client.Advanced.GpuBuffers.gpuUploadPerMegabyteInMilliseconds.get(); @@ -275,7 +277,7 @@ public class ColumnRenderBuffer extends AbstractRenderBuffer // render // //========// - @Override + /** @return true if something was rendered, false otherwise */ public boolean renderOpaque(LodRenderer renderContext, DhApiRenderParam renderEventParam) { boolean hasRendered = false; @@ -299,7 +301,7 @@ public class ColumnRenderBuffer extends AbstractRenderBuffer return hasRendered; } - @Override + /** @return true if something was rendered, false otherwise */ public boolean renderTransparent(LodRenderer renderContext, DhApiRenderParam renderEventParam) { boolean hasRendered = false; @@ -354,7 +356,6 @@ public class ColumnRenderBuffer extends AbstractRenderBuffer return false; } - @Override public void debugDumpStats(StatsMap statsMap) { statsMap.incStat("RenderBuffers"); @@ -378,6 +379,12 @@ public class ColumnRenderBuffer extends AbstractRenderBuffer } } + /** + * This method is called when object is no longer in use. + * Called either after uploadBuffers() returned false (On buffer Upload + * thread), or by others when the object is not being used. (not in build, + * upload, or render state). + */ @Override public void close() { diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnRenderBufferBuilder.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnRenderBufferBuilder.java index 6b75be291..aad029d2b 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnRenderBufferBuilder.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/ColumnRenderBufferBuilder.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding; -import com.seibel.distanthorizons.api.enums.rendering.EDebugRendering; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering; import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource; @@ -27,18 +27,19 @@ import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.pos.DhBlockPos; +import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.render.glObject.GLProxy; import com.seibel.distanthorizons.core.render.glObject.buffer.GLVertexBuffer; import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.RenderDataPointUtil; -import com.seibel.distanthorizons.core.util.objects.Reference; import com.seibel.distanthorizons.core.util.objects.UncheckedInterruptedException; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView; -import com.seibel.distanthorizons.core.util.threading.ThreadPools; +import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; /** @@ -63,12 +64,12 @@ public class ColumnRenderBufferBuilder // vbo building // //==============// - public static CompletableFuture buildBuffersAsync( - IDhClientLevel clientLevel, Reference renderBufferRef, + public static CompletableFuture buildAndUploadBuffersAsync( + IDhClientLevel clientLevel, ColumnRenderSource renderSource, ColumnRenderSource[] adjData) { - ThreadPoolExecutor bufferBuilderExecutor = ThreadPools.getBufferBuilderExecutor(); - ThreadPoolExecutor bufferUploaderExecutor = ThreadPools.getBufferUploaderExecutor(); + ThreadPoolExecutor bufferBuilderExecutor = ThreadPoolUtil.getBufferBuilderExecutor(); + ThreadPoolExecutor bufferUploaderExecutor = ThreadPoolUtil.getBufferUploaderExecutor(); if ((bufferBuilderExecutor == null || bufferBuilderExecutor.isTerminated()) || (bufferUploaderExecutor == null || bufferUploaderExecutor.isTerminated())) { @@ -78,15 +79,27 @@ public class ColumnRenderBufferBuilder return future; } - //LOGGER.info("RenderRegion startBuild @ "+renderSource.sectionPos); - return CompletableFuture.supplyAsync(() -> + try + { + return CompletableFuture.supplyAsync(() -> { try { boolean enableTransparency = Config.Client.Advanced.Graphics.Quality.transparency.get().transparencyEnabled; - EVENT_LOGGER.trace("RenderRegion start QuadBuild @ " + renderSource.sectionPos); - boolean enableSkyLightCulling = !clientLevel.getLevelWrapper().hasCeiling() && Config.Client.Advanced.Graphics.AdvancedGraphics.enableCaveCulling.get(); + //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 + && renderSource.pos.getDetailLevel() == DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL + ); int skyLightCullingBelow = Config.Client.Advanced.Graphics.AdvancedGraphics.caveCullingHeight.get(); // FIXME: Clamp also to the max world height. @@ -95,12 +108,12 @@ public class ColumnRenderBufferBuilder long builderStartTime = System.currentTimeMillis(); - LodQuadBuilder builder = new LodQuadBuilder(enableSkyLightCulling, (short) (skyLightCullingBelow - clientLevel.getMinY()), enableTransparency); + 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.sectionPos + " took: " + buildMs); + LOGGER.debug("RenderRegion end QuadBuild @ " + renderSource.pos + " took: " + buildMs); return builder; } @@ -118,19 +131,11 @@ public class ColumnRenderBufferBuilder { try { - EVENT_LOGGER.trace("RenderRegion start Upload @ " + renderSource.sectionPos); - - ColumnRenderBuffer buffer = renderBufferRef.swap(null); - if (buffer == null) - { - buffer = new ColumnRenderBuffer(new DhBlockPos(renderSource.sectionPos.getMinCornerLodPos().getCornerBlockPos(), clientLevel.getMinY()), renderSource.sectionPos); - } - + ColumnRenderBuffer buffer = new ColumnRenderBuffer(new DhBlockPos(renderSource.pos.getMinCornerLodPos().getCornerBlockPos(), clientLevel.getMinY())); try { buffer.uploadBuffer(quadBuilder, GLProxy.getInstance().getGpuUploadMethod()); LodUtil.assertTrue(buffer.buffersUploaded); - EVENT_LOGGER.trace("RenderRegion end Upload @ " + renderSource.sectionPos); return buffer; } catch (Exception e) @@ -145,49 +150,33 @@ public class ColumnRenderBufferBuilder } catch (Throwable e3) { - LOGGER.error("\"LodNodeBufferBuilder\" was unable to upload buffer: ", e3); + LOGGER.error("LodNodeBufferBuilder was unable to upload buffer: " + e3.getMessage(), e3); throw e3; } - }, bufferUploaderExecutor) - .handle((columnRenderBuffer, ex) -> - { - //LOGGER.info("RenderRegion endBuild @ {}", renderSource.sectionPos); - if (ex != null) - { - LOGGER.warn("Buffer building failed: " + ex.getMessage(), ex); - - if (!renderBufferRef.isEmpty()) - { - ColumnRenderBuffer buffer = renderBufferRef.swap(null); - buffer.close(); - } - - return null; - } - else - { - if (columnRenderBuffer != null) - { - LodUtil.assertTrue(columnRenderBuffer.buffersUploaded); - } - - return columnRenderBuffer; - } - }); + }, bufferUploaderExecutor); + } + 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 future = new CompletableFuture<>(); + future.cancel(true); + return future; + } } private static void makeLodRenderData(LodQuadBuilder quadBuilder, ColumnRenderSource renderSource, ColumnRenderSource[] adjRegions) { // Variable initialization - EDebugRendering debugMode = Config.Client.Advanced.Debugging.debugRendering.get(); + EDhApiDebugRendering debugMode = Config.Client.Advanced.Debugging.debugRendering.get(); // can be used to limit which section positions are build and thus, rendered // useful when debugging a specific section boolean enableColumnBufferLimit = Config.Client.Advanced.Debugging.columnBuilderDebugEnable.get(); if (enableColumnBufferLimit) { - if (renderSource.sectionPos.getDetailLevel() == Config.Client.Advanced.Debugging.columnBuilderDebugDetailLevel.get() - && renderSource.sectionPos.getX() == Config.Client.Advanced.Debugging.columnBuilderDebugXPos.get() - && renderSource.sectionPos.getZ() == Config.Client.Advanced.Debugging.columnBuilderDebugZPos.get()) + if (renderSource.pos.getDetailLevel() == Config.Client.Advanced.Debugging.columnBuilderDebugDetailLevel.get() + && renderSource.pos.getX() == Config.Client.Advanced.Debugging.columnBuilderDebugXPos.get() + && renderSource.pos.getZ() == Config.Client.Advanced.Debugging.columnBuilderDebugZPos.get()) { int test = 0; } @@ -312,8 +301,7 @@ public class ColumnRenderBufferBuilder } catch (RuntimeException e) { - EVENT_LOGGER.warn("Failed to get adj data for [" + detailLevel + ":" + x + "," + z + "] at [" + lodDirection + "]"); - EVENT_LOGGER.warn("Detail exception: ", e); + EVENT_LOGGER.warn("Failed to get adj data for [" + detailLevel + ":" + x + "," + z + "] at [" + lodDirection + "], Error: "+e.getMessage(), e); } } // for adjacent directions diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/CubicLodTemplate.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/CubicLodTemplate.java index dbfebb5d5..87b2e7720 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/CubicLodTemplate.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/CubicLodTemplate.java @@ -25,7 +25,7 @@ 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.EDebugRendering; +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; @@ -42,7 +42,7 @@ public class CubicLodTemplate public static void addLodToBuffer( long data, long topData, long bottomData, ColumnArrayView[][] adjColumnViews, byte detailLevel, int offsetPosX, int offsetOosZ, LodQuadBuilder quadBuilder, - EDebugRendering debugging, ColumnRenderSource.DebugSourceFlag debugSource) + EDhApiDebugRendering debugging, ColumnRenderSource.DebugSourceFlag debugSource) { DhLodPos blockOffsetPos = new DhLodPos(detailLevel, offsetPosX, offsetOosZ).convertToDetailLevel(LodUtil.BLOCK_DETAIL_LEVEL); @@ -88,7 +88,6 @@ public class CubicLodTemplate break; } case SHOW_DETAIL: - case SHOW_GENMODE: { color = LodUtil.DEBUG_DETAIL_LEVEL_COLORS[detailLevel]; fullBright = true; @@ -104,7 +103,7 @@ public class CubicLodTemplate break; case IBlockStateWrapper.IrisBlockMaterial.LEAVES: - color = ColorUtil.GREEN; + color = ColorUtil.DARK_GREEN; break; case IBlockStateWrapper.IrisBlockMaterial.STONE: color = ColorUtil.GRAY; @@ -139,6 +138,9 @@ public class CubicLodTemplate case IBlockStateWrapper.IrisBlockMaterial.WATER: color = ColorUtil.BLUE; break; + case IBlockStateWrapper.IrisBlockMaterial.GRASS: + color = ColorUtil.GREEN; + break; case IBlockStateWrapper.IrisBlockMaterial.ILLUMINATED: color = ColorUtil.YELLOW; break; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/LodQuadBuilder.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/LodQuadBuilder.java index 03ff71bf5..07744dcb9 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/LodQuadBuilder.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/LodQuadBuilder.java @@ -23,12 +23,18 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.*; +import com.seibel.distanthorizons.api.enums.config.EDhApiGrassSideRendering; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering; +import com.seibel.distanthorizons.core.config.Config; +import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; -import com.seibel.distanthorizons.core.render.AbstractRenderBuffer; import com.seibel.distanthorizons.core.render.glObject.buffer.GLVertexBuffer; import com.seibel.distanthorizons.core.util.ColorUtil; -import com.seibel.distanthorizons.api.enums.config.EGpuUploadMethod; +import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod; +import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.coreapi.util.MathUtil; import org.apache.logging.log4j.Logger; @@ -42,6 +48,7 @@ import org.apache.logging.log4j.Logger; public class LodQuadBuilder { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); public final boolean skipQuadsWithZeroSkylight; public final short skyLightCullingBelow; @@ -52,7 +59,10 @@ public class LodQuadBuilder private final ArrayList[] transparentQuads = (ArrayList[]) new ArrayList[6]; private final boolean doTransparency; + private final IClientLevelWrapper clientLevelWrapper; + private final EDhApiDebugRendering debugRenderingMode; + private final EDhApiGrassSideRendering grassSideRenderingMode; public static final int[][][] DIRECTION_VERTEX_IBO_QUAD = new int[][][] @@ -112,7 +122,7 @@ public class LodQuadBuilder // constructor // //=============// - public LodQuadBuilder(boolean enableSkylightCulling, short skyLightCullingBelow, boolean doTransparency) + public LodQuadBuilder(boolean enableSkylightCulling, short skyLightCullingBelow, boolean doTransparency, IClientLevelWrapper clientLevelWrapper) { this.doTransparency = doTransparency; for (int i = 0; i < 6; i++) @@ -123,6 +133,10 @@ public class LodQuadBuilder this.skipQuadsWithZeroSkylight = enableSkylightCulling; this.skyLightCullingBelow = skyLightCullingBelow; + this.clientLevelWrapper = clientLevelWrapper; + + this.debugRenderingMode = Config.Client.Advanced.Debugging.debugRendering.get(); + this.grassSideRenderingMode = Config.Client.Advanced.Graphics.AdvancedGraphics.grassSideRendering.get(); } @@ -254,8 +268,41 @@ public class LodQuadBuilder default: throw new IllegalArgumentException("Invalid Axis enum: " + axis); } - putVertex(bb, (short) (quad.x + dx), (short) (quad.y + dy), (short) (quad.z + dz), - quad.hasError ? ColorUtil.RED : quad.color, // TODO add debug config that allows toggling this + + + int color = quad.color; + + // use custom side color logic for grass blocks + if (quad.irisBlockMaterialId == IBlockStateWrapper.IrisBlockMaterial.GRASS) + { + // only use dirt colors if debug rendering is disabled + if (this.debugRenderingMode == EDhApiDebugRendering.OFF) + { + // determine if any custom coloring logic should be used + if (this.grassSideRenderingMode != EDhApiGrassSideRendering.AS_GRASS) + { + // only change the vertex color if it's on the side or bottom + if (quad.direction.getAxis().isHorizontal() || quad.direction == EDhDirection.DOWN) + { + if (this.grassSideRenderingMode == EDhApiGrassSideRendering.AS_DIRT + // 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) + // always render the bottom as dirt + || quad.direction == EDhDirection.DOWN) + { + // for horizontal and bottom faces of grass blocks, use the dirt color to + // prevent green cliff walls + color = this.clientLevelWrapper.getDirtBlockColor(); + color = ColorUtil.applyShade(color, MC.getShade(quad.direction)); + } + } + } + } + } + + + this.putVertex(bb, (short) (quad.x + dx), (short) (quad.y + dy), (short) (quad.z + dz), + quad.hasError ? ColorUtil.RED : color, quad.hasError ? 0 : normalIndex, quad.hasError ? 0 : quad.irisBlockMaterialId, quad.hasError ? 15 : quad.skyLight, @@ -386,7 +433,7 @@ public class LodQuadBuilder { return new Iterator() { - final ByteBuffer bb = ByteBuffer.allocateDirect(AbstractRenderBuffer.FULL_SIZED_BUFFER) + final ByteBuffer bb = ByteBuffer.allocateDirect(ColumnRenderBuffer.FULL_SIZED_BUFFER) .order(ByteOrder.nativeOrder()); int dir = skipEmpty(0); int quad = 0; @@ -414,7 +461,7 @@ public class LodQuadBuilder return null; } bb.clear(); - bb.limit(AbstractRenderBuffer.FULL_SIZED_BUFFER); + bb.limit(ColumnRenderBuffer.FULL_SIZED_BUFFER); while (bb.hasRemaining() && dir < 6) { writeData(); @@ -454,7 +501,7 @@ public class LodQuadBuilder { return new Iterator() { - final ByteBuffer bb = ByteBuffer.allocateDirect(AbstractRenderBuffer.FULL_SIZED_BUFFER) + final ByteBuffer bb = ByteBuffer.allocateDirect(ColumnRenderBuffer.FULL_SIZED_BUFFER) .order(ByteOrder.nativeOrder()); int directionIndex = this.skipEmptyDirectionIndices(0); int quad = 0; @@ -483,7 +530,7 @@ public class LodQuadBuilder } this.bb.clear(); - this.bb.limit(AbstractRenderBuffer.FULL_SIZED_BUFFER); + this.bb.limit(ColumnRenderBuffer.FULL_SIZED_BUFFER); while (this.bb.hasRemaining() && this.directionIndex < 6) { this.writeData(); @@ -525,7 +572,7 @@ public class LodQuadBuilder } - public BufferFiller makeOpaqueBufferFiller(EGpuUploadMethod method) + public BufferFiller makeOpaqueBufferFiller(EDhApiGpuUploadMethod method) { return new BufferFiller() { @@ -541,19 +588,19 @@ public class LodQuadBuilder } int numOfQuads = _countRemainingQuads(); - if (numOfQuads > AbstractRenderBuffer.MAX_QUADS_PER_BUFFER) - numOfQuads = AbstractRenderBuffer.MAX_QUADS_PER_BUFFER; + 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 * AbstractRenderBuffer.QUADS_BYTE_SIZE, method, - AbstractRenderBuffer.FULL_SIZED_BUFFER); + 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 * AbstractRenderBuffer.QUADS_BYTE_SIZE); + bb.limit(numOfQuads * ColumnRenderBuffer.QUADS_BYTE_SIZE); while (bb.hasRemaining() && dir < 6) { writeData(bb); @@ -605,7 +652,7 @@ public class LodQuadBuilder }; } - public BufferFiller makeTransparentBufferFiller(EGpuUploadMethod method) + public BufferFiller makeTransparentBufferFiller(EDhApiGpuUploadMethod method) { return new BufferFiller() { @@ -621,19 +668,19 @@ public class LodQuadBuilder } int numOfQuads = _countRemainingQuads(); - if (numOfQuads > AbstractRenderBuffer.MAX_QUADS_PER_BUFFER) - numOfQuads = AbstractRenderBuffer.MAX_QUADS_PER_BUFFER; + 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 * AbstractRenderBuffer.QUADS_BYTE_SIZE, method, - AbstractRenderBuffer.FULL_SIZED_BUFFER); + 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 * AbstractRenderBuffer.QUADS_BYTE_SIZE); + bb.limit(numOfQuads * ColumnRenderBuffer.QUADS_BYTE_SIZE); while (bb.hasRemaining() && dir < 6) { writeData(bb); @@ -718,7 +765,7 @@ public class LodQuadBuilder } /** Returns how many GpuBuffers will be needed to render opaque quads in this builder. */ - public int getCurrentNeededOpaqueVertexBufferCount() { return MathUtil.ceilDiv(this.getCurrentOpaqueQuadsCount(), AbstractRenderBuffer.MAX_QUADS_PER_BUFFER); } + public int getCurrentNeededOpaqueVertexBufferCount() { return MathUtil.ceilDiv(this.getCurrentOpaqueQuadsCount(), ColumnRenderBuffer.MAX_QUADS_PER_BUFFER); } /** Returns how many GpuBuffers will be needed to render transparent quads in this builder. */ public int getCurrentNeededTransparentVertexBufferCount() { @@ -727,7 +774,7 @@ public class LodQuadBuilder return 0; } - return MathUtil.ceilDiv(this.getCurrentTransparentQuadsCount(), AbstractRenderBuffer.MAX_QUADS_PER_BUFFER); + return MathUtil.ceilDiv(this.getCurrentTransparentQuadsCount(), ColumnRenderBuffer.MAX_QUADS_PER_BUFFER); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/columnViews/ColumnArrayView.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/columnViews/ColumnArrayView.java index dad170788..88bd337b3 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/columnViews/ColumnArrayView.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/columnViews/ColumnArrayView.java @@ -21,19 +21,21 @@ package com.seibel.distanthorizons.core.dataObjects.render.columnViews; import com.seibel.distanthorizons.core.util.RenderDataPointUtil; +import it.unimi.dsi.fastutil.longs.LongArrayList; import java.util.Arrays; public final class ColumnArrayView implements IColumnDataView { - public final long[] data; + public final LongArrayList data; public final int size; public final int offset; // offset in longs + /** can be 0 if this column was created for an empty data source */ public final int vertSize; // vertical size in longs - public ColumnArrayView(long[] data, int size, int offset, int vertSize) + public ColumnArrayView(LongArrayList data, int size, int offset, int vertSize) { this.data = data; this.size = size; @@ -44,9 +46,9 @@ public final class ColumnArrayView implements IColumnDataView @Override - public long get(int index) { return data[index + offset]; } + public long get(int index) { return data.getLong(index + offset); } - public void set(int index, long value) { data[index + offset] = value; } + public void set(int index, long value) { data.set(index + offset, value); } @Override public int size() { return size; } @@ -55,7 +57,7 @@ public final class ColumnArrayView implements IColumnDataView public int verticalSize() { return vertSize; } @Override - public int dataCount() { return size / vertSize; } + public int dataCount() { return (this.vertSize != 0) ? (this.size / this.vertSize) : 0; } @Override public ColumnArrayView subView(int dataIndexStart, int dataCount) @@ -63,7 +65,7 @@ public final class ColumnArrayView implements IColumnDataView return new ColumnArrayView(data, dataCount * vertSize, offset + dataIndexStart * vertSize, vertSize); } - public void fill(long value) { Arrays.fill(data, offset, offset + size, value); } + public void fill(long value) { Arrays.fill(data.elements(), offset, offset + size, value); } public void copyFrom(IColumnDataView source) { copyFrom(source, 0); } public void copyFrom(IColumnDataView source, int outputDataIndexOffset) @@ -81,19 +83,19 @@ public final class ColumnArrayView implements IColumnDataView for (int i = 0; i < source.dataCount(); i++) { int outputOffset = offset + outputDataIndexOffset * vertSize + i * vertSize; - source.subView(i, 1).copyTo(data, outputOffset, source.verticalSize()); - Arrays.fill(data, outputOffset + source.verticalSize(), + source.subView(i, 1).copyTo(data.elements(), outputOffset, source.verticalSize()); + Arrays.fill(data.elements(), outputOffset + source.verticalSize(), outputOffset + vertSize, 0); } } else { - source.copyTo(data, offset + outputDataIndexOffset * vertSize, source.size()); + source.copyTo(data.elements(), offset + outputDataIndexOffset * vertSize, source.size()); } } @Override - public void copyTo(long[] target, int offset, int size) { System.arraycopy(data, this.offset, target, offset, size); } + public void copyTo(long[] target, int offset, int size) { System.arraycopy(data.elements(), this.offset, target, offset, size); } public boolean mergeWith(ColumnArrayView source, boolean override) { @@ -130,17 +132,18 @@ public final class ColumnArrayView implements IColumnDataView public void changeVerticalSizeFrom(IColumnDataView source) { - if (dataCount() != source.dataCount()) + if (this.dataCount() != source.dataCount()) { throw new IllegalArgumentException("Cannot copy and resize to views with different dataCounts"); } - if (vertSize >= source.verticalSize()) + + if (this.vertSize >= source.verticalSize()) { - copyFrom(source); + this.copyFrom(source); } else { - for (int i = 0; i < dataCount(); i++) + for (int i = 0; i < this.dataCount(); i++) { RenderDataPointUtil.mergeMultiData(source.subView(i, 1), subView(i, 1)); } @@ -168,7 +171,7 @@ public final class ColumnArrayView implements IColumnDataView sb.append(" ["); for (int i = 0; i < size; i++) { - sb.append(RenderDataPointUtil.toString(data[offset + i])); + sb.append(RenderDataPointUtil.toString(data.getLong(offset + i))); if (i < size - 1) { sb.append(",\n"); @@ -184,15 +187,18 @@ public final class ColumnArrayView implements IColumnDataView return arrayHash(data, offset, size); } - private static int arrayHash(long[] a, int offset, int length) + private static int arrayHash(LongArrayList a, int offset, int length) { if (a == null) + { return 0; + } + int result = 1; int end = offset + length; for (int i = offset; i < end; i++) { - long element = a[i]; + long element = a.getLong(i); int elementHash = (int) (element ^ (element >>> 32)); result = 31 * result + elementHash; } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/columnViews/ColumnQuadView.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/columnViews/ColumnQuadView.java index 7c798dcb0..68d380735 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/columnViews/ColumnQuadView.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/columnViews/ColumnQuadView.java @@ -19,19 +19,24 @@ package com.seibel.distanthorizons.core.dataObjects.render.columnViews; +import it.unimi.dsi.fastutil.longs.LongArrayList; + public class ColumnQuadView implements IColumnDataView { - private final long[] data; + private final LongArrayList data; private final int perColumnOffset; // per column (of columns of data) offset in longs private final int xSize; // x size in datapoints private final int zSize; // x size in datapoints private final int offset; // offset in longs private final int vertSize; // vertical size in longs - public ColumnQuadView(long[] data, int dataZWidth, int dataVertSize, int viewXOffset, int viewZOffset, int xSize, int zSize) + public ColumnQuadView(LongArrayList data, int dataZWidth, int dataVertSize, int viewXOffset, int viewZOffset, int xSize, int zSize) { - if (viewXOffset + xSize > (data.length / (dataZWidth * dataVertSize)) || viewZOffset + zSize > dataZWidth) + if (viewXOffset + xSize > (data.size() / (dataZWidth * dataVertSize)) || viewZOffset + zSize > dataZWidth) + { throw new IllegalArgumentException("View is out of bounds"); + } + this.data = data; this.xSize = xSize; this.zSize = zSize; @@ -39,7 +44,7 @@ public class ColumnQuadView implements IColumnDataView this.perColumnOffset = dataZWidth * dataVertSize; this.offset = viewXOffset * perColumnOffset + viewZOffset * dataVertSize; } - private ColumnQuadView(long[] data, int perColumnOffset, int offset, int vertSize, int xSize, int zSize) + private ColumnQuadView(LongArrayList data, int perColumnOffset, int offset, int vertSize, int xSize, int zSize) { this.data = data; this.perColumnOffset = perColumnOffset; @@ -60,12 +65,12 @@ public class ColumnQuadView implements IColumnDataView public long get(int x, int z, int v) { - return data[offset + x * perColumnOffset + z * vertSize + v]; + return data.getLong(offset + x * perColumnOffset + z * vertSize + v); } public long set(int x, int z, int v, long value) { - return data[offset + x * perColumnOffset + z * vertSize + v] = value; + return data.set(offset + x * perColumnOffset + z * vertSize + v, value); } public ColumnArrayView get(int x, int z) @@ -82,7 +87,7 @@ public class ColumnQuadView implements IColumnDataView { if (singleColumn.verticalSize() != vertSize) throw new IllegalArgumentException("Vertical size of singleColumn must be equal to vertSize"); if (singleColumn.dataCount() != 1) throw new IllegalArgumentException("SingleColumn must contain exactly one data point"); - singleColumn.copyTo(data, offset + x * perColumnOffset + z * vertSize, singleColumn.size()); + singleColumn.copyTo(data.elements(), offset + x * perColumnOffset + z * vertSize, singleColumn.size()); } @Override diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/ChunkToLodBuilder.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/ChunkToLodBuilder.java index 3fd433a62..7336b42bd 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/ChunkToLodBuilder.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/ChunkToLodBuilder.java @@ -23,11 +23,11 @@ import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import com.seibel.distanthorizons.core.config.Config; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; +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.ThreadPools; +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; @@ -57,7 +57,7 @@ public class ChunkToLodBuilder implements AutoCloseable // data generation // //=================// - public CompletableFuture tryGenerateData(IChunkWrapper chunkWrapper) + public CompletableFuture tryGenerateData(IChunkWrapper chunkWrapper) { if (chunkWrapper == null) { @@ -74,7 +74,7 @@ public class ChunkToLodBuilder implements AutoCloseable } // Otherwise, it means we're the first to do so. Let's submit our task to this entry. - CompletableFuture future = new CompletableFuture<>(); + CompletableFuture future = new CompletableFuture<>(); this.concurrentTaskToBuildList.addLast(new Task(chunkWrapper.getChunkPos(), future)); return future; } @@ -82,7 +82,7 @@ public class ChunkToLodBuilder implements AutoCloseable // TODO why on tick? public void tick() { - int threadCount = ThreadPools.getWorkerThreadCount(); + int threadCount = ThreadPoolUtil.getWorkerThreadCount(); if (this.runningCount.get() >= threadCount) { return; @@ -102,7 +102,7 @@ public class ChunkToLodBuilder implements AutoCloseable return; } - ThreadPoolExecutor lodBuilderExecutor = ThreadPools.getChunkToLodBuilderExecutor(); + ThreadPoolExecutor lodBuilderExecutor = ThreadPoolUtil.getChunkToLodBuilderExecutor(); if (lodBuilderExecutor == null) { return; @@ -112,17 +112,21 @@ public class ChunkToLodBuilder implements AutoCloseable for (int i = 0; i < threadCount; i++) { this.runningCount.incrementAndGet(); - CompletableFuture.runAsync(() -> + try { - try + CompletableFuture.runAsync(() -> { - this.tickThreadTask(); - } - finally - { - this.runningCount.decrementAndGet(); - } - }, lodBuilderExecutor); + 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() @@ -158,10 +162,10 @@ public class ChunkToLodBuilder implements AutoCloseable { if (LodDataBuilder.canGenerateLodFromChunk(latestChunk)) { - ChunkSizedFullDataAccessor data = LodDataBuilder.createChunkData(latestChunk); - if (data != null) + FullDataSourceV2 dataSource = LodDataBuilder.createGeneratedDataSource(latestChunk); + if (dataSource != null) { - task.future.complete(data); + task.future.complete(dataSource); continue; } } @@ -233,11 +237,11 @@ public class ChunkToLodBuilder implements AutoCloseable private static class Task { public final DhChunkPos chunkPos; - public final CompletableFuture future; + public final CompletableFuture 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 future) + Task(DhChunkPos chunkPos, CompletableFuture future) { this.chunkPos = chunkPos; this.future = future; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/FullDataToRenderDataTransformer.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/FullDataToRenderDataTransformer.java index a3b3d54d1..2aabb2e71 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/FullDataToRenderDataTransformer.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/FullDataToRenderDataTransformer.java @@ -19,14 +19,10 @@ package com.seibel.distanthorizons.core.dataObjects.transformers; -import com.seibel.distanthorizons.api.enums.config.EBlocksToAvoid; +import com.seibel.distanthorizons.api.enums.config.EDhApiBlocksToAvoid; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.SingleColumnFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.CompleteFullDataSource; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IIncompleteFullDataSource; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; @@ -34,20 +30,20 @@ import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.pos.DhBlockPos; import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.util.ColorUtil; import com.seibel.distanthorizons.core.util.FullDataPointUtil; -import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.RenderDataPointUtil; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; +import it.unimi.dsi.fastutil.longs.LongArrayList; import org.apache.logging.log4j.Logger; import java.util.HashSet; /** - * Handles converting {@link ChunkSizedFullDataAccessor}, {@link IIncompleteFullDataSource}, - * and {@link IFullDataSource}'s to {@link ColumnRenderSource}. + * Handles converting {@link FullDataSourceV2}'s to {@link ColumnRenderSource}. */ public class FullDataToRenderDataTransformer { @@ -62,13 +58,13 @@ public class FullDataToRenderDataTransformer // public transformer interface // //==============================// - public static ColumnRenderSource transformFullDataToRenderSource(IFullDataSource fullDataSource, IDhClientLevel level) + public static ColumnRenderSource transformFullDataToRenderSource(FullDataSourceV2 fullDataSource, IDhClientLevel level) { if (fullDataSource == null) { return null; } - else if (MC.getWrappedClientLevel() == null) + else if (level == null) { // if the client is no longer loaded in the world, render sources cannot be created return null; @@ -77,17 +73,7 @@ public class FullDataToRenderDataTransformer try { - if (fullDataSource instanceof CompleteFullDataSource) - { - return transformCompleteFullDataToColumnData(level, (CompleteFullDataSource) fullDataSource); - } - else if (fullDataSource instanceof IIncompleteFullDataSource) - { - return transformIncompleteFullDataToColumnData(level, (IIncompleteFullDataSource) fullDataSource); - } - - LodUtil.assertNotReach("Unimplemented Full Data transformer for "+IFullDataSource.class.getSimpleName()+" of type ["+fullDataSource.getClass().getSimpleName()+"]."); - return null; + return transformCompleteFullDataToColumnData(level, fullDataSource); } catch (InterruptedException e) { @@ -108,13 +94,13 @@ public class FullDataToRenderDataTransformer * @throws InterruptedException Can be caused by interrupting the thread upstream. * Generally thrown if the method is running after the client leaves the current world. */ - private static ColumnRenderSource transformCompleteFullDataToColumnData(IDhClientLevel level, CompleteFullDataSource fullDataSource) throws InterruptedException + private static ColumnRenderSource transformCompleteFullDataToColumnData(IDhClientLevel level, FullDataSourceV2 fullDataSource) throws InterruptedException { - final DhSectionPos pos = fullDataSource.getSectionPos(); + final DhSectionPos pos = fullDataSource.getPos(); final byte dataDetail = fullDataSource.getDataDetailLevel(); final int vertSize = Config.Client.Advanced.Graphics.Quality.verticalQuality.get().calculateMaxVerticalData(fullDataSource.getDataDetailLevel()); - final ColumnRenderSource columnSource = new ColumnRenderSource(pos, vertSize, level.getMinY()); - if (fullDataSource.isEmpty()) + final ColumnRenderSource columnSource = ColumnRenderSource.getPooledRenderSource(pos, vertSize, level.getMinY(), true); + if (fullDataSource.isEmpty) { return columnSource; } @@ -133,8 +119,8 @@ public class FullDataToRenderDataTransformer throwIfThreadInterrupted(); ColumnArrayView columnArrayView = columnSource.getVerticalDataPointView(x, z); - SingleColumnFullDataAccessor fullArrayView = fullDataSource.get(x, z); - convertColumnData(level, baseX + x, baseZ + z, columnArrayView, fullArrayView); + LongArrayList dataColumn = fullDataSource.get(x, z); + convertColumnData(level, fullDataSource.mapping, baseX + x, baseZ + z, columnArrayView, dataColumn); } } @@ -149,56 +135,6 @@ public class FullDataToRenderDataTransformer return columnSource; } - /** - * @throws InterruptedException Can be caused by interrupting the thread upstream. - * Generally thrown if the method is running after the client leaves the current world. - */ - private static ColumnRenderSource transformIncompleteFullDataToColumnData(IDhClientLevel level, IIncompleteFullDataSource data) throws InterruptedException - { - final DhSectionPos pos = data.getSectionPos(); - final byte dataDetail = data.getDataDetailLevel(); - final int vertSize = Config.Client.Advanced.Graphics.Quality.verticalQuality.get().calculateMaxVerticalData(data.getDataDetailLevel()); - final ColumnRenderSource columnSource = new ColumnRenderSource(pos, vertSize, level.getMinY()); - if (data.isEmpty()) - { - return columnSource; - } - - columnSource.markNotEmpty(); - - if (dataDetail == columnSource.getDataDetailLevel()) - { - int baseX = pos.getMinCornerLodPos().getCornerBlockPos().x; - int baseZ = pos.getMinCornerLodPos().getCornerBlockPos().z; - - int width = pos.getWidthCountForLowerDetailedSection(dataDetail); - for (int x = 0; x < width; x++) - { - for (int z = 0; z < width; z++) - { - throwIfThreadInterrupted(); - - SingleColumnFullDataAccessor fullArrayView = data.tryGet(x, z); - if (fullArrayView == null) - { - continue; - } - - ColumnArrayView columnArrayView = columnSource.getVerticalDataPointView(x, z); - convertColumnData(level, baseX + x, baseZ + z, columnArrayView, fullArrayView); - - columnSource.fillDebugFlag(x, z, 1, 1, ColumnRenderSource.DebugSourceFlag.SPARSE); - } - } - } - else - { - throw new UnsupportedOperationException("To be implemented"); - //FIXME: Implement different size creation of renderData - } - return columnSource; - } - //================// @@ -222,26 +158,33 @@ public class FullDataToRenderDataTransformer // TODO what does this mean? - private static void iterateAndConvert(IDhClientLevel level, int blockX, int blockZ, ColumnArrayView column, SingleColumnFullDataAccessor data) + 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() == EBlocksToAvoid.NON_COLLIDING); + boolean avoidSolidBlocks = (Config.Client.Advanced.Graphics.Quality.blocksToIgnore.get() == EDhApiBlocksToAvoid.NON_COLLIDING); boolean colorBelowWithAvoidedBlocks = Config.Client.Advanced.Graphics.Quality.tintWithAvoidedBlocks.get(); - FullDataPointIdMap fullDataMapping = data.getMapping(); HashSet 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 < data.getSingleLength(); i++) + for (int i = 0; i < fullColumnData.size(); i++) { - long fullData = data.getSingle(i); + long fullData = fullColumnData.getLong(i); int bottomY = FullDataPointUtil.getBottomY(fullData); int blockHeight = FullDataPointUtil.getHeight(fullData); int id = FullDataPointUtil.getId(fullData); - int light = FullDataPointUtil.getLight(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? @@ -296,7 +239,10 @@ public class FullDataToRenderDataTransformer { if (colorBelowWithAvoidedBlocks) { - colorToApplyToNextBlock = level.computeBaseColor(new DhBlockPos(blockX, bottomY + level.getMinY(), blockZ), biome, block); + //mare sure to not trnasfer alpha if for some reason grass is transparent + colorToApplyToNextBlock = ColorUtil.setAlpha(level.computeBaseColor(new DhBlockPos(blockX, bottomY + level.getMinY(), blockZ), biome, block),255); + skylightToApplyToNextBlock = skyLight; + blocklightToApplyToNextBlock = blockLight; } // don't add this block @@ -315,46 +261,56 @@ public class FullDataToRenderDataTransformer // use the previous block's color color = colorToApplyToNextBlock; colorToApplyToNextBlock = -1; + skyLight = skylightToApplyToNextBlock; + blockLight = blocklightToApplyToNextBlock; } - - - // add the block - isVoid = false; - long columnData = RenderDataPointUtil.createDataPoint(bottomY + blockHeight, bottomY, color, light, block.getIrisBlockMaterialId()); - column.set(columnOffset, columnData); - columnOffset++; + + //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.getIrisBlockMaterialId()); + renderColumnData.set(columnOffset, columnData); + columnOffset++; + } + lastBottom = bottomY; + lastColor = color; + } if (isVoid) { - column.set(0, RenderDataPointUtil.createVoidDataPoint()); + renderColumnData.set(0, RenderDataPointUtil.createVoidDataPoint()); } } // TODO what does this mean? - public static void convertColumnData(IDhClientLevel level, int blockX, int blockZ, ColumnArrayView columnArrayView, SingleColumnFullDataAccessor fullArrayView) + public static void convertColumnData(IDhClientLevel level, FullDataPointIdMap fullDataMapping, int blockX, int blockZ, ColumnArrayView columnArrayView, LongArrayList fullDataColumn) { - if (!fullArrayView.doesColumnExist()) - { - return; - } - - int dataTotalLength = fullArrayView.getSingleLength(); - if (dataTotalLength == 0) + if (fullDataColumn == null || fullDataColumn.size() == 0) { return; } + int dataTotalLength = fullDataColumn.size(); if (dataTotalLength > columnArrayView.verticalSize()) { - ColumnArrayView totalColumnData = new ColumnArrayView(new long[dataTotalLength], dataTotalLength, 0, dataTotalLength); - iterateAndConvert(level, blockX, blockZ, totalColumnData, fullArrayView); + 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, blockX, blockZ, columnArrayView, fullArrayView); //Directly use the arrayView since it fits. + iterateAndConvert(level, fullDataMapping, blockX, blockZ, columnArrayView, fullDataColumn); //Directly use the arrayView since it fits. } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java index 8ac100f70..9437ec217 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java @@ -21,12 +21,18 @@ package com.seibel.distanthorizons.core.dataObjects.transformers; import java.util.List; +import com.seibel.distanthorizons.api.enums.config.EDhApiWorldCompressionMode; +import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; import com.seibel.distanthorizons.api.objects.data.DhApiChunk; import com.seibel.distanthorizons.api.objects.data.DhApiTerrainDataPoint; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; +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.enums.EDhDirection; 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.DhSectionPos; import com.seibel.distanthorizons.core.util.FullDataPointUtil; import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; @@ -40,6 +46,8 @@ public class LodDataBuilder { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); 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; @@ -49,7 +57,7 @@ public class LodDataBuilder // converters // //============// - public static ChunkSizedFullDataAccessor createChunkData(IChunkWrapper chunkWrapper) + public static FullDataSourceV2 createGeneratedDataSource(IChunkWrapper chunkWrapper) { if (!canGenerateLodFromChunk(chunkWrapper)) { @@ -57,26 +65,106 @@ public class LodDataBuilder } - ChunkSizedFullDataAccessor chunkData = new ChunkSizedFullDataAccessor(chunkWrapper.getChunkPos()); - int minBuildHeight = chunkWrapper.getMinNonEmptyHeight(); - for (int x = 0; x < LodUtil.CHUNK_WIDTH; x++) + // get the section position + 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); + DhSectionPos pos = new DhSectionPos(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ); + + FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos); + dataSource.isEmpty = false; + + + + // compute the chunk dataSource offset + // this offset is used to determine where in the dataSource this chunk's data should go + int chunkOffsetX = chunkWrapper.getChunkPos().x; + if (chunkWrapper.getChunkPos().x < 0) { - for (int z = 0; z < LodUtil.CHUNK_WIDTH; z++) + // expected offset positions: + // chunkPos -> offset + // 5 -> 1 + // 4 -> 0 --- + // 3 -> 3 + // 2 -> 2 + // 1 -> 1 + // 0 -> 0 === + // -1 -> 3 + // -2 -> 2 + // -3 -> 1 + // -4 -> 0 --- + // -5 -> 3 + chunkOffsetX = ((chunkOffsetX) % NUMB_OF_CHUNKS_WIDE); + if (chunkOffsetX != 0) + { + chunkOffsetX += NUMB_OF_CHUNKS_WIDE; + } + } + else + { + chunkOffsetX %= NUMB_OF_CHUNKS_WIDE; + } + chunkOffsetX *= LodUtil.CHUNK_WIDTH; + + int chunkOffsetZ = chunkWrapper.getChunkPos().z; + if (chunkWrapper.getChunkPos().z < 0) + { + chunkOffsetZ = ((chunkOffsetZ) % NUMB_OF_CHUNKS_WIDE); + if (chunkOffsetZ != 0) + { + chunkOffsetZ += NUMB_OF_CHUNKS_WIDE; + } + } + else + { + chunkOffsetZ %= NUMB_OF_CHUNKS_WIDE; + } + chunkOffsetZ *= LodUtil.CHUNK_WIDTH; + + + + //==========================// + // populate the data source // + //==========================// + + EDhApiWorldCompressionMode worldCompressionMode = Config.Client.Advanced.LodBuilding.worldCompression.get(); + boolean ignoreHiddenBlocks = (worldCompressionMode != EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS); + + int minBuildHeight = chunkWrapper.getMinNonEmptyHeight(); + for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++) + { + for (int relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++) { LongArrayList longs = new LongArrayList(chunkWrapper.getHeight() / 4); int lastY = chunkWrapper.getMaxBuildHeight(); - IBiomeWrapper biome = chunkWrapper.getBiome(x, lastY, z); + IBiomeWrapper biome = chunkWrapper.getBiome(relBlockX, lastY, relBlockZ); IBlockStateWrapper blockState = AIR; - int mappedId = chunkData.getMapping().addIfNotPresentAndGetId(biome, blockState); - // FIXME: The +1 offset to reproduce the old behavior. Remove this when we get per-face lighting - byte light = (byte) ((chunkWrapper.getBlockLight(x, lastY + 1, z) << 4) + chunkWrapper.getSkyLight(x, lastY + 1, z)); + int mappedId = dataSource.mapping.addIfNotPresentAndGetId(biome, blockState); + + + byte blockLight; + byte skyLight; + if (lastY < chunkWrapper.getMaxBuildHeight()) + { + // FIXME: The lastY +1 offset is to reproduce the old behavior. Remove this when we get per-face lighting + blockLight = (byte) chunkWrapper.getBlockLight(relBlockX, lastY + 1, relBlockZ); + skyLight = (byte) chunkWrapper.getSkyLight(relBlockX, lastY + 1, relBlockZ); + } + else + { + //we are at the height limit. There are no torches here, and sky is not obscured. + blockLight = 0; + skyLight = 15; + } // determine the starting Y Pos - int y = chunkWrapper.getLightBlockingHeightMapValue(x,z); + int y = chunkWrapper.getLightBlockingHeightMapValue(relBlockX,relBlockZ); // go up until we reach open air or the world limit - IBlockStateWrapper topBlockState = chunkWrapper.getBlockState(x, y, z); + IBlockStateWrapper topBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ); while (!topBlockState.isAir() && y < chunkWrapper.getMaxBuildHeight()) { try @@ -84,13 +172,13 @@ public class LodDataBuilder // This is necessary in some edge cases with snow layers and some other blocks that may not appear in the height map but do block light. // Interestingly this doesn't appear to be the case in the DhLightingEngine, if this same logic is added there the lighting breaks for the affected blocks. y++; - topBlockState = chunkWrapper.getBlockState(x, y, z); + topBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ); } catch (Exception e) { if (!getTopErrorLogged) { - LOGGER.warn("Unexpected issue in LodDataBuilder, future errors won't be logged. Chunk [" + chunkWrapper.getChunkPos() + "] with max height: [" + chunkWrapper.getMaxBuildHeight() + "] had issue getting block at pos [" + x + "," + y + "," + z + "] error: " + e.getMessage(), e); + LOGGER.warn("Unexpected issue in LodDataBuilder, future errors won't be logged. Chunk [" + chunkWrapper.getChunkPos() + "] with max height: [" + chunkWrapper.getMaxBuildHeight() + "] had issue getting block at pos [" + relBlockX + "," + y + "," + relBlockZ + "] error: " + e.getMessage(), e); getTopErrorLogged = true; } @@ -102,39 +190,111 @@ public class LodDataBuilder for (; y >= minBuildHeight; y--) { - IBiomeWrapper newBiome = chunkWrapper.getBiome(x, y, z); - IBlockStateWrapper newBlockState = chunkWrapper.getBlockState(x, y, z); - byte newLight = (byte) ((chunkWrapper.getBlockLight(x, y + 1, z) << 4) + chunkWrapper.getSkyLight(x, y + 1, z)); + IBiomeWrapper newBiome = chunkWrapper.getBiome(relBlockX, y, relBlockZ); + IBlockStateWrapper newBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ); + byte newBlockLight = (byte) chunkWrapper.getBlockLight(relBlockX, y + 1, relBlockZ); + byte newSkyLight = (byte) chunkWrapper.getSkyLight(relBlockX, y + 1, relBlockZ); + // save the biome/block change if (!newBiome.equals(biome) || !newBlockState.equals(blockState)) { - longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - chunkWrapper.getMinBuildHeight(), light)); - biome = newBiome; - blockState = newBlockState; - mappedId = chunkData.getMapping().addIfNotPresentAndGetId(biome, blockState); - light = newLight; - lastY = y; + // if we ignore hidden blocks, don't save this biome/block change + // wait until the block is visible and then save the new datapoint + if (!ignoreHiddenBlocks + // if the last block is air, this block will always be visible + || blockState.isAir() + // check if this block is visible from any direction + || blockVisible(chunkWrapper, relBlockX, y, relBlockZ)) + { + longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - chunkWrapper.getMinBuildHeight(), blockLight, skyLight)); + biome = newBiome; + blockState = newBlockState; + mappedId = dataSource.mapping.addIfNotPresentAndGetId(biome, blockState); + blockLight = newBlockLight; + skyLight = newSkyLight; + lastY = y; + } } -// else if (newLight != light) { -// longs.add(FullFormat.encode(mappedId, lastY-y, y+1 - chunk.getMinBuildHeight(), light)); -// light = newLight; -// lastY = y; -// } } - longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - chunkWrapper.getMinBuildHeight(), light)); + longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - chunkWrapper.getMinBuildHeight(), blockLight, skyLight)); - chunkData.setSingleColumn(longs.toLongArray(), x, z); + dataSource.setSingleColumn(longs, + relBlockX + chunkOffsetX, + relBlockZ + chunkOffsetZ, + EDhApiWorldGenerationStep.LIGHT, + worldCompressionMode); } } - if (!canGenerateLodFromChunk(chunkWrapper)) return null; - LodUtil.assertTrue(chunkData.emptyCount() == 0); - return chunkData; + + LodUtil.assertTrue(!dataSource.isEmpty); + return dataSource; + } + private static boolean blockVisible(IChunkWrapper chunkWrapper, int relBlockX, int blockY, int relBlockZ) + { + DhBlockPos originalBlockPos = new DhBlockPos(relBlockX,blockY,relBlockZ); + DhBlockPos testBlockPos = new DhBlockPos(relBlockX,blockY,relBlockZ); + + // up/down + if (blockInDirectionVisible(chunkWrapper, EDhDirection.UP, originalBlockPos, testBlockPos)) + { + return true; + } + if (blockInDirectionVisible(chunkWrapper, EDhDirection.DOWN, originalBlockPos, testBlockPos)) + { + return true; + } + + // north/south + if (blockInDirectionVisible(chunkWrapper, EDhDirection.NORTH, originalBlockPos, testBlockPos)) + { + return true; + } + if (blockInDirectionVisible(chunkWrapper, EDhDirection.SOUTH, originalBlockPos, testBlockPos)) + { + return true; + } + + // east/west + if (blockInDirectionVisible(chunkWrapper, EDhDirection.EAST, originalBlockPos, testBlockPos)) + { + return true; + } + if (blockInDirectionVisible(chunkWrapper, EDhDirection.WEST, originalBlockPos, testBlockPos)) + { + return true; + } + + + return false; + } + private static boolean blockInDirectionVisible(IChunkWrapper chunkWrapper, EDhDirection direction, DhBlockPos originalBlockPos, DhBlockPos testBlockPos) + { + originalBlockPos.mutateOffset(direction, testBlockPos); + + // if the block is next to the border of a chunk, assume it's visible + if (testBlockPos.x < 0 || testBlockPos.x >= LodUtil.CHUNK_WIDTH) + { + return true; + } + if (testBlockPos.z < 0 || testBlockPos.z >= LodUtil.CHUNK_WIDTH) + { + return true; + } + if (testBlockPos.y < chunkWrapper.getMinBuildHeight() || testBlockPos.y > chunkWrapper.getMaxBuildHeight()) + { + return true; + } + + // this block isn't on a chunk boundary, check if it is next to a transparent/air block + IBlockStateWrapper blockState = chunkWrapper.getBlockState(testBlockPos); + return blockState.isAir() || blockState.getOpacity() != IBlockStateWrapper.FULLY_OPAQUE; } + /** @throws ClassCastException if an API user returns the wrong object type(s) */ - public static ChunkSizedFullDataAccessor createApiChunkData(DhApiChunk dataPoints) throws ClassCastException + public static FullDataSourceV2 createFromApiChunkData(DhApiChunk dataPoints) throws ClassCastException { - ChunkSizedFullDataAccessor accessor = new ChunkSizedFullDataAccessor(new DhChunkPos(dataPoints.chunkPosX, dataPoints.chunkPosZ)); + FullDataSourceV2 accessor = FullDataSourceV2.createEmpty(new DhSectionPos(new DhChunkPos(dataPoints.chunkPosX, dataPoints.chunkPosZ))); for (int relZ = 0; relZ < LodUtil.CHUNK_WIDTH; relZ++) { for (int relX = 0; relX < LodUtil.CHUNK_WIDTH; relX++) @@ -148,31 +308,34 @@ public class LodDataBuilder // AND the below loop won't run. int size = (columnDataPoints != null) ? columnDataPoints.size() : 0; - long[] packedDataPoints = new long[size]; + LongArrayList packedDataPoints = new LongArrayList(new long[size]); for (int index = 0; index < size; index++) { DhApiTerrainDataPoint dataPoint = columnDataPoints.get(index); - int id = accessor.getMapping().addIfNotPresentAndGetId( + int id = accessor.mapping.addIfNotPresentAndGetId( (IBiomeWrapper) (dataPoint.biomeWrapper), (IBlockStateWrapper) (dataPoint.blockStateWrapper) - ); + ); - packedDataPoints[index] = FullDataPointUtil.encode( + packedDataPoints.set(index, FullDataPointUtil.encode( id, dataPoint.topYBlockPos - dataPoint.bottomYBlockPos, dataPoint.bottomYBlockPos - dataPoints.topYBlockPos, - (byte) (dataPoint.lightLevel) - ); + (byte) (dataPoint.blockLightLevel), + (byte) (dataPoint.skyLightLevel) + )); } - accessor.setSingleColumn(packedDataPoints, relX, relZ); + // TODO add the ability for API users to define a different compression mode + // or add a "unkown" compression mode + accessor.setSingleColumn(packedDataPoints, relX, relZ, EDhApiWorldGenerationStep.LIGHT, EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS); } } return accessor; } - + //================// diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/AbstractDataSourceHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/file/AbstractDataSourceHandler.java index 8f2ec3440..b913e16c7 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/AbstractDataSourceHandler.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/AbstractDataSourceHandler.java @@ -1,61 +1,70 @@ package com.seibel.distanthorizons.core.file; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.sql.*; +import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo; +import com.seibel.distanthorizons.core.sql.dto.IBaseDTO; import com.seibel.distanthorizons.core.util.LodUtil; -import com.seibel.distanthorizons.core.util.TimerUtil; -import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataOutputStream; -import com.seibel.distanthorizons.core.util.threading.ThreadPools; +import com.seibel.distanthorizons.core.util.threading.PositionalLockProvider; +import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.nio.channels.ClosedChannelException; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ThreadPoolExecutor; +import java.util.*; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; -import java.util.zip.Adler32; -import java.util.zip.CheckedOutputStream; -public abstract class AbstractDataSourceHandler, TDhLevel extends IDhLevel> implements ISourceProvider +// TODO is there a reason this is separate from FullDataSourceProviderV2? +// We shouldn't need multiple data source handlers +public abstract class AbstractDataSourceHandler + , + TDTO extends IBaseDTO, + TRepo extends AbstractDhRepo, + TDhLevel extends IDhLevel> + implements AutoCloseable { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - private static final Timer DELAYED_SAVE_TIMER = TimerUtil.CreateTimer("DataSourceSaveTimer"); - /** How long a data source must remain un-modified before being written to disk. */ - private static final int SAVE_DELAY_IN_MS = 4_000; /** - * The highest numerical detail level known about. + * The highest numerical detail level possible. * Used when determining which positions to update. + * + * @see AbstractDataSourceHandler#MIN_SECTION_DETAIL_LEVEL */ - protected final AtomicInteger topSectionDetailLevelRef; - protected final int minDetailLevel = DhSectionPos.SECTION_MINIMUM_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. + * + * @see AbstractDataSourceHandler#TOP_SECTION_DETAIL_LEVEL + * */ + public static final byte MIN_SECTION_DETAIL_LEVEL = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; + + + protected final PositionalLockProvider updateLockProvider = new PositionalLockProvider(); + /** + * generally just used for debugging, + * keeps track of which positions are currently locked. + */ + public final Set lockedPosSet = ConcurrentHashMap.newKeySet(); + public final ConcurrentHashMap queuedUpdateCountsByPos = new ConcurrentHashMap<>(); - protected final ConcurrentHashMap unsavedDataSourceBySectionPos = new ConcurrentHashMap<>(); - protected final ConcurrentHashMap saveTimerTasksBySectionPos = new ConcurrentHashMap<>(); - protected final ReentrantLock[] updateLockArray; - protected final ReentrantLock[] queueSaveLockArray; protected final ReentrantLock closeLock = new ReentrantLock(); protected volatile boolean isShutdown = false; protected final TDhLevel level; protected final File saveDir; - public final AbstractDataSourceRepo repo; + public final TRepo repo; + + public final ArrayList> dateSourceUpdateListeners = new ArrayList<>(); @@ -73,22 +82,7 @@ public abstract class AbstractDataSourceHandler - * The returned data source may be null if there was a problem.

+ * The returned data source may be null if repo is in the process of shutting down.

* * This call is concurrent. I.e. it supports being called by multiple threads at the same time. */ - @Override public CompletableFuture getAsync(DhSectionPos pos) { - ThreadPoolExecutor executor = ThreadPools.getFileHandlerExecutor(); + ThreadPoolExecutor executor = ThreadPoolUtil.getFileHandlerExecutor(); if (executor == null || executor.isTerminated()) { return CompletableFuture.completedFuture(null); } - return CompletableFuture.supplyAsync(() -> this.get(pos), executor); + + try + { + return CompletableFuture.supplyAsync(() -> this.get(pos), executor); + } + 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 + return CompletableFuture.completedFuture(null); + } } /** * Should only be used in internal file handler methods where we are already running on a file handler thread. - * Can return null if there was a problem. + * Can return null if the repo is in the process of being shut down * @see AbstractDataSourceHandler#getAsync(DhSectionPos) */ @Nullable public TDataSource get(DhSectionPos pos) { - // used the unsaved data source if present - if (this.unsavedDataSourceBySectionPos.containsKey(pos)) - { - return this.unsavedDataSourceBySectionPos.get(pos); - } - // an unsaved data source isn't present - // check the database - - - // increase the top detail level if necessary - this.topSectionDetailLevelRef.updateAndGet(oldDetailLevel -> Math.max(oldDetailLevel, pos.getDetailLevel())); - - TDataSource dataSource = null; try { - DataSourceDto dto = this.repo.getByPrimaryKey(pos.serialize()); + TDTO dto = this.repo.getByKey(pos); if (dto != null) { - // load from file + // load from database dataSource = this.createDataSourceFromDto(dto); } else { - // attempt to create from any existing files - dataSource = this.createNewDataSourceFromExistingDtos(pos); + // TODO does this need any special logic to be populated from the existing DTOs? + // and/or is that necessary? + // Everything already appears to be populating correctly. + dataSource = this.makeEmptyDataSource(pos); } } catch (InterruptedException ignore) { } @@ -185,10 +171,9 @@ public abstract class AbstractDataSourceHandler updateDataSourcesWithChunkDataAsync(ChunkSizedFullDataAccessor chunkDataView) + public CompletableFuture updateDataSourceAsync(@NotNull FullDataSourceV2 inputDataSource) { - ThreadPoolExecutor executor = ThreadPools.getFileHandlerExecutor(); + ThreadPoolExecutor executor = ThreadPoolUtil.getUpdatePropagatorExecutor(); if (executor == null || executor.isTerminated()) { return CompletableFuture.completedFuture(null); @@ -198,165 +183,87 @@ public abstract class AbstractDataSourceHandler { - DhSectionPos bottomPos = chunkDataView.getSectionPos().convertNewToDetailLevel(DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); - - bottomPos.forEachPosUpToDetailLevel( - this.topSectionDetailLevelRef.byteValue(), - (pos) -> this.updateDataSourceAtPos(pos, chunkDataView) ); - + try + { + this.updateDataSourceAtPos(inputDataSource.getPos(), inputDataSource, true); + } + catch (Exception e) + { + LOGGER.error("Unexpected error in async data source update, error: "+e.getMessage(), e); + } + finally + { + this.markUpdateEnd(inputDataSource.getPos()); + } }, executor); } catch (RejectedExecutionException ignore) { // can happen if the executor was shutdown while this task was queued + this.markUpdateEnd(inputDataSource.getPos()); return CompletableFuture.completedFuture(null); } } - protected void updateDataSourceAtPos(DhSectionPos pos, ChunkSizedFullDataAccessor chunkData) + + /** + * After this method returns the inputData will be written to file. + * @param updatePos the position to update + */ + protected void updateDataSourceAtPos(DhSectionPos updatePos, @NotNull FullDataSourceV2 inputData, boolean lockOnUpdatePos) { + boolean methodLocked = false; // a lock is necessary to prevent two threads from writing to the same position at once, // if that happens only the second update will apply and the LOD will end up with hole(s) - ReentrantLock updateLock = this.getUpdateLockForPos(pos); + ReentrantLock updateLock = this.updateLockProvider.getLock(updatePos); try { - updateLock.lock(); + if (lockOnUpdatePos) + { + methodLocked = true; + updateLock.lock(); + this.lockedPosSet.add(updatePos); + } + // get or create the data source - TDataSource dataSource = this.get(pos); - if (dataSource == null) + try (TDataSource recipientDataSource = this.get(updatePos)) { - dataSource = this.makeEmptyDataSource(pos); + if (recipientDataSource != null) + { + boolean dataModified = recipientDataSource.update(inputData, this.level); + if (dataModified) + { + // save the updated data to the database + TDTO dto = this.createDtoFromDataSource(recipientDataSource); + this.repo.save(dto); + + + for (IDataSourceUpdateFunc listener : this.dateSourceUpdateListeners) + { + if (listener != null) + { + listener.OnDataSourceUpdated(recipientDataSource); + } + } + } + } } - dataSource.update(chunkData, this.level); - - this.queueDelayedSave(dataSource); } catch (Exception e) { - LOGGER.error("Error updating pos ["+pos+"], error: "+e.getMessage(), e); + LOGGER.error("Error updating pos ["+updatePos+"], error: "+e.getMessage(), e); } finally { - updateLock.unlock(); - } - } - /** - * Queues the given data source to save after {@link AbstractDataSourceHandler#SAVE_DELAY_IN_MS} - * milliseconds have passed without any additional modifications.

- * - * This prevents repeatedly reading/writing the same data source to/from disk if said - * source is currently being updated via world gen or chunk modifications. - * This drastically reduces disk usage and improves performance. - */ - protected void queueDelayedSave(TDataSource dataSource) - { - // a lock is necessary to prevent two threads from queuing a save at the same time, - // which can cause the timer to queue canceled tasks - DhSectionPos pos = dataSource.getSectionPos(); - ReentrantLock saveQueueLock = this.getSaveQueueLockForPos(pos); - - - // done to prevent queueing saves while the current queue is being cleared - if (this.isShutdown) - { - return; - } - - - try - { - saveQueueLock.lock(); - - // put the data source in memory until it can be flushed to disk - this.unsavedDataSourceBySectionPos.put(pos, dataSource); - - TimerTask task = new TimerTask() + if (methodLocked) { - @Override - public void run() - { - - // remove this task from the queue - AbstractDataSourceHandler.this.saveTimerTasksBySectionPos.remove(pos); - - try - { - final TDataSource finalDataSource = AbstractDataSourceHandler.this.unsavedDataSourceBySectionPos.remove(pos); - - // this can rarely happen due to imperfect concurrency handling, - // if the data source is null that just means it has already been saved so nothing needs to be done - if (finalDataSource != null) - { - AbstractDataSourceHandler.this.writeDataSourceToFile(finalDataSource); - } - } - catch (Exception e) // this can throw errors (not exceptions) when installed in Iris' dev environment for some reason due to an issue with LZ4's compression library - { - LOGGER.error("Failed to save updated data for section ["+pos+"], error: ["+e.getMessage()+"]", e); - } - } - }; - try - { - DELAYED_SAVE_TIMER.schedule(task, SAVE_DELAY_IN_MS); + updateLock.unlock(); + this.lockedPosSet.remove(updatePos); } - catch (IllegalStateException ignore) - { - // James isn't sure why this is possible since this logic is inside a lock, - // maybe the timer is just async enough that there can be problems? - LOGGER.warn("Attempted to queue an already canceled task. Pos: ["+pos+"], task already queued for pos: ["+this.saveTimerTasksBySectionPos.containsKey(pos)+"]"); - } - - - // cancel the old save timer if present - // (this is equivalent to restarting the timer) - TimerTask oldTask = this.saveTimerTasksBySectionPos.put(pos, task); - if (oldTask != null) - { - oldTask.cancel(); - } - } - finally - { - saveQueueLock.unlock(); - } - } - protected void writeDataSourceToFile(TDataSource dataSource) throws IOException - { - LodUtil.assertTrue(dataSource != null); - - try - { - // write the outputs to a stream to prep for writing to the database - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - - // the order of these streams is important, otherwise the checksum won't be calculated - CheckedOutputStream checkedOut = new CheckedOutputStream(byteArrayOutputStream, new Adler32()); - // normally a DhStream should be the topmost stream to prevent closing the stream accidentally, - // but since this stream will be closed immediately after writing anyway, it won't be an issue - DhDataOutputStream compressedOut = new DhDataOutputStream(checkedOut); - - dataSource.writeToStream(compressedOut, AbstractDataSourceHandler.this.level); - - compressedOut.flush(); - int checksum = (int) checkedOut.getChecksum().getValue(); - byteArrayOutputStream.close(); - - - // save the DTO - DataSourceDto newDto = new DataSourceDto( - dataSource.getSectionPos(), checksum, - dataSource.getDataDetailLevel(), dataSource.getWorldGenStep(), dataSource.getDataTypeName(), - dataSource.getDataFormatVersion(), - byteArrayOutputStream.toByteArray()); - this.repo.save(newDto); - } - catch (ClosedChannelException e) // includes ClosedByInterruptException - { - // expected if the file handler is shut down, the exception can be ignored } } @@ -366,9 +273,31 @@ public abstract class AbstractDataSourceHandler + { + if (atomicCount == null) + { + atomicCount = new AtomicInteger(0); + } + atomicCount.incrementAndGet(); + return atomicCount; + }); + } + /** used for debugging to track which positions are queued for updating */ + private void markUpdateEnd(DhSectionPos dataSourcePos) + { + this.queuedUpdateCountsByPos.compute(dataSourcePos, (pos, atomicCount) -> + { + if (atomicCount != null && atomicCount.decrementAndGet() <= 0) + { + atomicCount = null; + } + return atomicCount; + }); + } @@ -388,22 +317,8 @@ public abstract class AbstractDataSourceHandler list = this.saveTimerTasksBySectionPos.keys(); - while (list.hasMoreElements()) - { - DhSectionPos pos = list.nextElement(); - TimerTask saveTask = this.saveTimerTasksBySectionPos.remove(pos); - if (saveTask != null) - { - saveTask.run(); - // canceling the task doesn't need to be done since the it has internal logic to prevent running more than once - } - } - - LOGGER.info("[" + this.getClass().getSimpleName() + "] saving complete, closing repo."); this.repo.close(); } catch (InterruptedException ignore) { } @@ -413,4 +328,16 @@ public abstract class AbstractDataSourceHandler + { + void OnDataSourceUpdated(TDataSource updatedFullDataSource); + } + } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/DataSourcePool.java b/core/src/main/java/com/seibel/distanthorizons/core/file/DataSourcePool.java new file mode 100644 index 000000000..fa6b80fdf --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/DataSourcePool.java @@ -0,0 +1,139 @@ +package com.seibel.distanthorizons.core.file; + +import com.seibel.distanthorizons.core.level.IDhLevel; +import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.util.ThreadUtil; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; + +/** + * Data sources are often very large objects and aren't used for very long. + * This means their frequent construction and garbage collection can result in quite a bit of GC pressure. + * By pooling said data sources and reusing them we can drastically reduce this GC pressure and improve + * performance significantly. + */ +public class DataSourcePool, TDhLevel extends IDhLevel> +{ + /** + * James tested with a static 25 on a 8 core 16 processor machine and didn't have any issues. + * In most cases the number of pooled sources won't probably even get close to the number of processors, + * but just in case the user has a overkill CPU (or config) this should hopefully prevent thrashing. + */ + private static final int MAX_POOLED_SOURCES = Runtime.getRuntime().availableProcessors() * 2; + + private final ArrayList pooledDataSources = new ArrayList<>(); + private final ReentrantLock poolLock = new ReentrantLock(); + + private final Function createEmptyDatasourceFunc; + @Nullable + private final IPrepPooledDataSourceFunc prepDatasourceFunc; + + + + //=============// + // constructor // + //=============// + + public DataSourcePool(Function createEmptyDatasourceFunc, @Nullable IPrepPooledDataSourceFunc prepDatasourceFunc) + { + this.createEmptyDatasourceFunc = createEmptyDatasourceFunc; + this.prepDatasourceFunc = prepDatasourceFunc; + } + + + + //===============// + // pool handlers // + //===============// + + /** + * Returns a cleared data source. + * @see DataSourcePool#getPooledSource(DhSectionPos, boolean) + */ + public TDataSource getPooledSource(DhSectionPos pos) { return this.getPooledSource(pos, true);} + + /** @return an empty data source if non are cached */ + public TDataSource getPooledSource(DhSectionPos pos, boolean clearData) + { + try + { + this.poolLock.lock(); + + int index = this.pooledDataSources.size() - 1; + if (index == -1) + { + // no pooled sources exist + return this.createEmptyDatasourceFunc.apply(pos); + } + else + { + TDataSource dataSource = this.pooledDataSources.remove(index); + + // some data sources may want to handle prep themselves + // (due to needing additional inputs than what this pool keeps track of) + if (this.prepDatasourceFunc != null) + { + this.prepDatasourceFunc.prepDataSource(pos, clearData, dataSource); + } + + return dataSource; + } + } + finally + { + this.poolLock.unlock(); + } + } + + /** + * Doesn't have to be called, if a data source isn't returned, nothing will be leaked. + * It just means a new source must be constructed next time {@link DataSourcePool#getPooledSource} is called. + */ + public void returnPooledDataSource(TDataSource dataSource) + { + if (dataSource == null) + { + return; + } + else if (this.pooledDataSources.size() > MAX_POOLED_SOURCES) + { + return; + } + + try + { + this.poolLock.lock(); + this.pooledDataSources.add(dataSource); + } + finally + { + this.poolLock.unlock(); + } + } + + + + //===============// + // debug methods // + //===============// + + /** Returns how many data sources are in the pool */ + public int size() { return this.pooledDataSources.size(); } + + + + //================// + // helper classes // + //================// + + @FunctionalInterface + public interface IPrepPooledDataSourceFunc, TDhLevel extends IDhLevel> + { + /** @param clearData will be false if the data will be immediately overwritten anyway */ + void prepDataSource(DhSectionPos pos, boolean clearData, TDataSource dataSource); + } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/IDataSource.java b/core/src/main/java/com/seibel/distanthorizons/core/file/IDataSource.java index a4c19974e..09d4d4ee8 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/IDataSource.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/IDataSource.java @@ -1,37 +1,24 @@ package com.seibel.distanthorizons.core.file; -import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.loader.AbstractFullDataSourceLoader; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; +import com.seibel.distanthorizons.api.enums.EDhApiDetailLevel; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.sql.IBaseDTO; -import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataOutputStream; - -import java.io.IOException; +import com.seibel.distanthorizons.core.sql.dto.IBaseDTO; /** - * Base for all data sources. + * Base for all data sources.

* - * @param what type of level this data source can be created from + * AutoCloseable Can be implemented to allow for disposing of pooled data sources.

+ * + * @param there are times when we need specifically a client level vs a more generic level */ -public interface IDataSource extends IBaseDTO +public interface IDataSource extends IBaseDTO, AutoCloseable { + DhSectionPos getPos(); - DhSectionPos getSectionPos(); - @Override - default String getPrimaryKeyString() { return this.getSectionPos().serialize(); } - - - - //===============// - // file handling // - //===============// - - void update(ChunkSizedFullDataAccessor chunkData, TDhLevel level); - - void writeToStream(DhDataOutputStream outputStream, TDhLevel level) throws IOException; + /** @return true if the data was changed */ + boolean update(FullDataSourceV2 chunkData, TDhLevel level); @@ -39,16 +26,12 @@ public interface IDataSource extends IBaseDTO // meta data // //===========// - /** Returns the detail level of the data contained by this {@link IFullDataSource}. */ - byte getDataDetailLevel(); - EDhApiWorldGenerationStep getWorldGenStep(); - /** - * Returns the name of this data source.
- * Primarily by {@link AbstractFullDataSourceLoader#getLoader(String, byte)} to determine how to parse - * the binary data when read from file. + /** + * Returns the detail level of the data contained by this data source. + * IE: 0 for block, 1 for 2x2 blocks, etc. + * + * @see EDhApiDetailLevel */ - String getDataTypeName(); - /** Defines how the binary data is formatted and which {@link AbstractFullDataSourceLoader} should be used when loading from file. */ - byte getDataFormatVersion(); + byte getDataDetailLevel(); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/ISourceProvider.java b/core/src/main/java/com/seibel/distanthorizons/core/file/ISourceProvider.java deleted file mode 100644 index 1d2b15ef4..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/ISourceProvider.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.seibel.distanthorizons.core.file; - -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; -import com.seibel.distanthorizons.core.level.IDhLevel; -import com.seibel.distanthorizons.core.pos.DhSectionPos; - -import java.util.concurrent.CompletableFuture; - -public interface ISourceProvider, TDhLevel extends IDhLevel> extends AutoCloseable -{ - CompletableFuture getAsync(DhSectionPos pos); - - CompletableFuture updateDataSourcesWithChunkDataAsync(ChunkSizedFullDataAccessor chunkData); - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/DelayedFullDataSourceSaveCache.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/DelayedFullDataSourceSaveCache.java new file mode 100644 index 000000000..aa87662f8 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/DelayedFullDataSourceSaveCache.java @@ -0,0 +1,121 @@ +package com.seibel.distanthorizons.core.file.fullDatafile; + +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.util.TimerUtil; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Used to batch together multiple data source updates that all + * affect the same position. + */ +public class DelayedFullDataSourceSaveCache +{ + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + + private static final Timer DELAY_UPDATE_TIMER = TimerUtil.CreateTimer("Delayed Full Datasource Save Timer"); + + + public final ConcurrentHashMap dataSourceByPosition = new ConcurrentHashMap<>(); + private final ConcurrentHashMap saveTimerTasksBySectionPos = new ConcurrentHashMap<>(); + + private final ISaveDataSourceFunc onSaveTimeoutFunc; + private final int saveDelayInMs; + + + + //=============// + // constructor // + //=============// + + public DelayedFullDataSourceSaveCache(@NotNull ISaveDataSourceFunc onSaveTimeoutFunc, int saveDelayInMs) + { + this.onSaveTimeoutFunc = onSaveTimeoutFunc; + this.saveDelayInMs = saveDelayInMs; + } + + + + //==============// + // update queue // + //==============// + + public void queueDataSourceForUpdateAndSave(FullDataSourceV2 inputDataSource) + { + DhSectionPos dataSourcePos = inputDataSource.getPos(); + this.dataSourceByPosition.compute(dataSourcePos, (inputPos, temporaryDataSource) -> + { + if (temporaryDataSource == null) + { + temporaryDataSource = FullDataSourceV2.createEmpty(inputPos); + } + temporaryDataSource.update(inputDataSource); + + + TimerTask timerTask = new TimerTask() + { + @Override + public void run() + { + DelayedFullDataSourceSaveCache.this.saveTimerTasksBySectionPos.remove(dataSourcePos); + + try + { + FullDataSourceV2 dataSourceToSave = DelayedFullDataSourceSaveCache.this.dataSourceByPosition.remove(dataSourcePos); + if (dataSourceToSave != null) + { + DelayedFullDataSourceSaveCache.this.onSaveTimeoutFunc.save(dataSourceToSave); + } + } + catch (Exception e) // this can throw errors (not exceptions) when installed in Iris' dev environment for some reason due to an issue with LZ4's compression library + { + LOGGER.error("Failed to save updated data for section ["+dataSourcePos+"], error: ["+e.getMessage()+"]", e); + } + } + }; + try + { + DELAY_UPDATE_TIMER.schedule(timerTask, this.saveDelayInMs); + } + catch (IllegalStateException ignore) + { + // James isn't sure why this is possible since this logic is inside a lock, + // maybe the timer is just async enough that there can be problems? + LOGGER.warn("Attempted to queue an already canceled task. Pos: ["+dataSourcePos+"], task already queued for pos: ["+this.saveTimerTasksBySectionPos.containsKey(dataSourcePos)+"]"); + } + + + // cancel the old save timer if present + // (this is equivalent to restarting the timer) + TimerTask oldTask = this.saveTimerTasksBySectionPos.put(dataSourcePos, timerTask); + if (oldTask != null) + { + oldTask.cancel(); + } + + return temporaryDataSource; + }); + } + + public int getUnsavedCount() { return this.dataSourceByPosition.size(); } + + + + //================// + // helper classes // + //================// + + @FunctionalInterface + public interface ISaveDataSourceFunc + { + /** called after the timeout expires */ + void save(FullDataSourceV2 inputDataSource); + } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataFileHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataFileHandler.java deleted file mode 100644 index 77610a252..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataFileHandler.java +++ /dev/null @@ -1,233 +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 . - */ - -package com.seibel.distanthorizons.core.file.fullDatafile; - -import com.seibel.distanthorizons.core.config.Config; -import com.seibel.distanthorizons.core.dataObjects.fullData.loader.AbstractFullDataSourceLoader; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.HighDetailIncompleteFullDataSource; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.LowDetailIncompleteFullDataSource; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IIncompleteFullDataSource; -import com.seibel.distanthorizons.core.file.AbstractDataSourceHandler; -import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; -import com.seibel.distanthorizons.core.level.IDhLevel; -import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; -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.AbstractDataSourceRepo; -import com.seibel.distanthorizons.core.sql.FullDataRepo; -import com.seibel.distanthorizons.core.sql.DataSourceDto; -import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.Nullable; - -import java.awt.*; -import java.io.File; -import java.io.IOException; -import java.sql.SQLException; -import java.util.*; - -public class FullDataFileHandler - extends AbstractDataSourceHandler - implements IFullDataSourceProvider, IDebugRenderable -{ - private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - - - - //=============// - // constructor // - //=============// - - public FullDataFileHandler(IDhLevel level, AbstractSaveStructure saveStructure) { this(level, saveStructure, null); } - public FullDataFileHandler(IDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride) - { - super(level, saveStructure, saveDirOverride); - - DebugRenderer.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showWorldGenQueue); - } - - - - //====================// - // Abstract overrides // - //====================// - - @Override - protected AbstractDataSourceRepo createRepo() - { - try - { - return new FullDataRepo("jdbc:sqlite", this.saveDir.getPath() + "/" + AbstractSaveStructure.DATABASE_NAME); - } - catch (SQLException e) - { - // should only happen if there is an issue with the database (it's locked or can't be created if missing) - // or the database update failed - throw new RuntimeException(e); - } - } - - @Override - protected IFullDataSource createDataSourceFromDto(DataSourceDto dto) throws InterruptedException, IOException - { - AbstractFullDataSourceLoader loader = AbstractFullDataSourceLoader.getLoader(dto.dataType, dto.binaryDataFormatVersion); - IFullDataSource dataSource = loader.loadDataSource(dto, this.level); - return dataSource; - } - /** Creates a new data source using any DTOs already present in the database. */ - @Override - protected IFullDataSource createNewDataSourceFromExistingDtos(DhSectionPos pos) - { - IIncompleteFullDataSource newFullDataSource = this.makeEmptyDataSource(pos); - return this.updateFromDataSourceFromExistingDtos(newFullDataSource); - } - protected IFullDataSource updateFromDataSourceFromExistingDtos(IIncompleteFullDataSource newFullDataSource) - { - DhSectionPos pos = newFullDataSource.getSectionPos(); - - boolean showFullDataFileSampling = Config.Client.Advanced.Debugging.DebugWireframe.showFullDataFileStatus.get(); - if (showFullDataFileSampling) - { - DebugRenderer.makeParticle(new DebugRenderer.BoxParticle( - new DebugRenderer.Box(newFullDataSource.getSectionPos(), 64f, 72f, 0.03f, Color.MAGENTA), - 0.2, 32f)); - } - - - // get all non-empty sections to sample from - ArrayList samplePosList = new ArrayList<>(); - ArrayList possibleChildList = new ArrayList<>(); - pos.forEachChild((childPos) -> - { - if (childPos.getDetailLevel() >= this.minDetailLevel) - { - possibleChildList.add(childPos); - } - }); - while (possibleChildList.size() != 0) - { - DhSectionPos possiblePos = possibleChildList.remove(possibleChildList.size()-1); - if (this.repo.existsWithPrimaryKey(possiblePos.serialize())) - { - samplePosList.add(possiblePos); - } - else - { - possiblePos.forEachChild((childPos) -> - { - if (childPos.getDetailLevel() >= this.minDetailLevel) - { - possibleChildList.add(childPos); - } - }); - } - } - - - // read in the existing data - for (int i = 0; i < samplePosList.size(); i++) - { - DhSectionPos samplePos = samplePosList.get(i); - IFullDataSource sampleDataSource = this.get(samplePos); - if (sampleDataSource == null) - { - // no file was found, this is unexpected, but can be ignored - continue; - } - - if (showFullDataFileSampling) - { - DebugRenderer.makeParticle(new DebugRenderer.BoxParticle( - new DebugRenderer.Box(newFullDataSource.getSectionPos(), 64f, 72f, 0.03f, Color.MAGENTA.darker()), - 0.2, 32f)); - } - - try - { - newFullDataSource.sampleFrom(sampleDataSource); - } - catch (Exception e) - { - LOGGER.warn("Unable to sample "+sampleDataSource.getSectionPos()+" into "+newFullDataSource.getSectionPos(), e); - } - } - - - // promotion may happen if all children are fully populated - return newFullDataSource.tryPromotingToCompleteDataSource(); - } - - - - @Override - protected IIncompleteFullDataSource makeEmptyDataSource(DhSectionPos pos) - { - return pos.getDetailLevel() <= HighDetailIncompleteFullDataSource.MAX_SECTION_DETAIL ? - HighDetailIncompleteFullDataSource.createEmpty(pos) : - LowDetailIncompleteFullDataSource.createEmpty(pos); - } - - - - //===================// - // extension methods // - //===================// - - @Override - public void writeDataSourceToFile(IFullDataSource fullDataSource) throws IOException - { - // doing this here guarantees that all changes are caught and promoted - if (fullDataSource instanceof IIncompleteFullDataSource) - { - fullDataSource = ((IIncompleteFullDataSource) fullDataSource).tryPromotingToCompleteDataSource(); - } - - super.writeDataSourceToFile(fullDataSource); - - // save has completed - boolean showFullDataFileStatus = Config.Client.Advanced.Debugging.DebugWireframe.showFullDataFileStatus.get(); - if (showFullDataFileStatus) - { - DebugRenderer.makeParticle(new DebugRenderer.BoxParticle( - new DebugRenderer.Box(fullDataSource.getSectionPos(), 64f, 70f, 0.02f, Color.YELLOW), - 0.2, 16f)); - } - } - - @Override - public int getUnsavedDataSourceCount() { return this.unsavedDataSourceBySectionPos.size(); } - - - - //===========// - // overrides // - //===========// - - @Override - public void debugRender(DebugRenderer renderer) - { - this.saveTimerTasksBySectionPos.keySet() - .forEach((pos) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 128f, 0.15f, Color.cyan)); }); - - } - - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV1.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV1.java new file mode 100644 index 000000000..fde42a6a7 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV1.java @@ -0,0 +1,190 @@ +package com.seibel.distanthorizons.core.file.fullDatafile; + +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV1; +import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; +import com.seibel.distanthorizons.core.level.IDhLevel; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV1DTO; +import com.seibel.distanthorizons.core.sql.repo.FullDataSourceV1Repo; +import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.locks.ReentrantLock; + +public class FullDataSourceProviderV1 + implements AutoCloseable +{ + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + + protected final ReentrantLock closeLock = new ReentrantLock(); + protected volatile boolean isShutdown = false; + + protected final TDhLevel level; + protected final File saveDir; + + public final FullDataSourceV1Repo repo; + + + + //=============// + // constructor // + //=============// + + public FullDataSourceProviderV1(TDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride) + { + this.level = level; + this.saveDir = (saveDirOverride == null) ? saveStructure.getFullDataFolder(level.getLevelWrapper()) : saveDirOverride; + if (!this.saveDir.exists() && !this.saveDir.mkdirs()) + { + LOGGER.warn("Unable to create full data folder, file saving may fail."); + } + + this.repo = this.createRepo(); + } + + + + //==================// + // abstract methods // + //==================// + + /** When this is called the parent folders should be created */ + protected FullDataSourceV1Repo createRepo() + { + try + { + return new FullDataSourceV1Repo("jdbc:sqlite", this.saveDir.getPath() + "/" + AbstractSaveStructure.DATABASE_NAME); + } + catch (SQLException e) + { + // should only happen if there is an issue with the database (it's locked or can't be created if missing) + // or the database update failed + throw new RuntimeException(e); + } + } + + protected FullDataSourceV1 createDataSourceFromDto(FullDataSourceV1DTO dto) throws InterruptedException, IOException + { + FullDataSourceV1 dataSource = FullDataSourceV1.createEmpty(dto.pos); + dataSource.populateFromStream(dto, dto.getInputStream(), this.level); + return dataSource; + } + + + + //==============// + // data reading // + //==============// + + /** + * Returns the {@link FullDataSourceV1} for the given section position.
+ * The returned data source may be null if there was a problem.

+ * + * This call is concurrent. I.e. it supports being called by multiple threads at the same time. + */ + public CompletableFuture getAsync(DhSectionPos pos) + { + ThreadPoolExecutor executor = ThreadPoolUtil.getFileHandlerExecutor(); + if (executor == null || executor.isTerminated()) + { + return CompletableFuture.completedFuture(null); + } + + try + { + return CompletableFuture.supplyAsync(() -> this.get(pos), executor); + } + 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 + return CompletableFuture.completedFuture(null); + } + } + /** + * Should only be used in internal file handler methods where we are already running on a file handler thread. + * Can return null. + * @see FullDataSourceProviderV1#getAsync(DhSectionPos) + */ + @Nullable + public FullDataSourceV1 get(DhSectionPos pos) + { + FullDataSourceV1 dataSource = null; + try + { + FullDataSourceV1DTO dto = this.repo.getByKey(pos); + if (dto != null) + { + // load from file + dataSource = this.createDataSourceFromDto(dto); + } + } + catch (InterruptedException ignore) { } + catch (IOException e) + { + LOGGER.warn("File read Error for pos ["+pos+"], error: "+e.getMessage(), e); + } + + return dataSource; + } + + + + //===========// + // migration // + //===========// + + public long getDataSourceMigrationCount() { return this.repo.getMigrationCount(); } + + public ArrayList getDataSourcesToMigrate(int limit) + { + ArrayList dataSourceList = new ArrayList<>(); + + ArrayList migrationPosList = this.repo.getPositionsToMigrate(limit); + for (int i = 0; i < migrationPosList.size(); i++) + { + DhSectionPos pos = migrationPosList.get(i); + FullDataSourceV1 dataSource = this.get(pos); + if (dataSource != null) + { + dataSourceList.add(dataSource); + } + } + + return dataSourceList; + } + + public void markMigrationFailed(DhSectionPos pos) { ((FullDataSourceV1Repo) this.repo).markMigrationFailed(pos); } + + + + //=========// + // cleanup // + //=========// + + @Override + public void close() + { + try + { + this.closeLock.lock(); + this.isShutdown = true; + + LOGGER.info("Closing [" + this.getClass().getSimpleName() + "] for level: [" + this.level + "]."); + this.repo.close(); + } + finally + { + this.closeLock.unlock(); + } + } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV2.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV2.java new file mode 100644 index 000000000..795ffb15c --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV2.java @@ -0,0 +1,573 @@ +/* + * 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 . + */ + +package com.seibel.distanthorizons.core.file.fullDatafile; + +import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode; +import com.seibel.distanthorizons.core.api.internal.ClientApi; +import com.seibel.distanthorizons.core.config.Config; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV1; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; +import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; +import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; +import com.seibel.distanthorizons.core.file.AbstractDataSourceHandler; +import com.seibel.distanthorizons.core.level.IDhLevel; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +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.sql.repo.FullDataSourceV2Repo; +import com.seibel.distanthorizons.core.util.ThreadUtil; +import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; +import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.io.File; +import java.io.IOException; +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; + +/** + * Handles reading/writing {@link FullDataSourceV2} + * to and from the database. + */ +public class FullDataSourceProviderV2 + extends AbstractDataSourceHandler + implements IDebugRenderable +{ + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); + + protected static final int NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD = 50; + /** how many parent update tasks can be in the queue at once */ + protected static final int MAX_UPDATE_TASK_COUNT = NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD * Config.Client.Advanced.MultiThreading.numberOfFileHandlerThreads.get(); + + /** indicates how long the update queue thread should wait between queuing ticks */ + protected static final int UPDATE_QUEUE_THREAD_DELAY_IN_MS = 250; + + /** 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 String MIGRATION_THREAD_NAME_PREFIX = "Full Data Migration Thread: "; + /** + * 5 minutes
+ * This should be much longer than any update should take. This is just + * to make sure the thread doesn't get stuck. + */ + private static final int MIGRATION_MAX_UPDATE_TIMEOUT_IN_MS = 5 * 60 * 1_000; + + + protected final ThreadPoolExecutor migrationThreadPool; + /** + * Interrupting the migration thread pool doesn't work well and may corrupt the database + * vs gracefully shutting down the thread ourselves. + */ + protected final AtomicBoolean migrationThreadRunning = new AtomicBoolean(true); + protected final FullDataSourceProviderV1 legacyFileHandler; + + protected long legacyDeletionCount = -1; + protected long migrationCount = -1; + + /** + * Tracks which positions are currently being updated + * to prevent duplicate concurrent updates. + */ + public final Set parentUpdatingPosSet = ConcurrentHashMap.newKeySet(); + + // TODO only run thread if modifications happened recently + /** + * This isn't in {@link AbstractDataSourceHandler} since we don't need parent updating logic + * for render data, only full data. + */ + private final ThreadPoolExecutor updateQueueProcessor; + + + + //=============// + // constructor // + //=============// + + public FullDataSourceProviderV2(IDhLevel level, AbstractSaveStructure saveStructure) { this(level, saveStructure, null); } + public FullDataSourceProviderV2(IDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride) + { + super(level, saveStructure, saveDirOverride); + this.legacyFileHandler = new FullDataSourceProviderV1<>(level, saveStructure, saveDirOverride); + + DebugRenderer.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showFullDataUpdateStatus); + + String dimensionName = level.getLevelWrapper().getDimensionType().getDimensionName(); + + // 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.execute(() -> this.convertLegacyDataSources()); + + this.updateQueueProcessor = ThreadUtil.makeSingleThreadPool("Parent Update Queue ["+dimensionName+"]"); + this.updateQueueProcessor.execute(() -> this.runUpdateQueue()); + } + + + + //====================// + // Abstract overrides // + //====================// + + @Override + protected FullDataSourceV2Repo createRepo() + { + try + { + return new FullDataSourceV2Repo("jdbc:sqlite", this.saveDir.getPath() + "/" + AbstractSaveStructure.DATABASE_NAME); + } + catch (SQLException e) + { + // should only happen if there is an issue with the database (it's locked or the folder path is missing) + // or the database update failed + throw new RuntimeException(e); + } + } + + @Override + protected FullDataSourceV2DTO createDtoFromDataSource(FullDataSourceV2 dataSource) + { + try + { + // when creating new data use the compressor currently selected in the config + EDhApiDataCompressionMode compressionModeEnum = Config.Client.Advanced.LodBuilding.dataCompression.get(); + return FullDataSourceV2DTO.CreateFromDataSource(dataSource, compressionModeEnum); + } + catch (IOException e) + { + LOGGER.warn("Unable to create DTO, error: "+e.getMessage(), e); + return null; + } + } + + @Override + protected FullDataSourceV2 createDataSourceFromDto(FullDataSourceV2DTO dto) throws InterruptedException, IOException + { return dto.createPooledDataSource(this.level.getLevelWrapper()); } + + @Override + protected FullDataSourceV2 makeEmptyDataSource(DhSectionPos pos) { return FullDataSourceV2.DATA_SOURCE_POOL.getPooledSource(pos, true); } + + + + //================// + // parent updates // + //================// + + private void runUpdateQueue() + { + while (!Thread.interrupted()) + { + try + { + Thread.sleep(UPDATE_QUEUE_THREAD_DELAY_IN_MS); + + ThreadPoolExecutor executor = ThreadPoolUtil.getUpdatePropagatorExecutor(); + if (executor == null || executor.isTerminated()) + { + continue; + } + + // TODO it might be worth skipping this logic if no parent updates happened + + // queue parent updates + if (executor.getQueue().size() < MAX_UPDATE_TASK_COUNT + && this.parentUpdatingPosSet.size() < MAX_UPDATE_TASK_COUNT) + { + // get the positions that need to be applied to their parents + ArrayList parentUpdatePosList = this.repo.getPositionsToUpdate(MAX_UPDATE_TASK_COUNT); + + // combine updates together based on their parent + HashMap> updatePosByParentPos = new HashMap<>(); + for (DhSectionPos pos : parentUpdatePosList) + { + updatePosByParentPos.compute(pos.getParentPos(), (parentPos, updatePosSet) -> + { + if (updatePosSet == null) + { + updatePosSet = new HashSet<>(); + } + updatePosSet.add(pos); + return updatePosSet; + }); + } + + // queue the updates + for (DhSectionPos parentUpdatePos : updatePosByParentPos.keySet()) + { + // stop if there are already a bunch of updates queued + if (this.parentUpdatingPosSet.size() > MAX_UPDATE_TASK_COUNT + || !this.parentUpdatingPosSet.add(parentUpdatePos)) + { + break; + } + + try + { + executor.execute(() -> + { + ReentrantLock parentWriteLock = this.updateLockProvider.getLock(parentUpdatePos); + boolean parentLocked = false; + try + { + //LOGGER.info("updating parent: "+parentUpdatePos); + + // Locking the parent before the children should prevent deadlocks. + // TryLock is used instead of lock so this thread can handle a different update. + if (parentWriteLock.tryLock()) + { + parentLocked = true; + this.lockedPosSet.add(parentUpdatePos); + + // apply each child pos to the parent + for (DhSectionPos childPos : updatePosByParentPos.get(parentUpdatePos)) + { + ReentrantLock childReadLock = this.updateLockProvider.getLock(childPos); + try + { + childReadLock.lock(); + this.lockedPosSet.add(childPos); + + try (FullDataSourceV2 dataSource = this.get(childPos)) + { + // can return null when the file handler is being shut down + if (dataSource != null) + { + this.updateDataSourceAtPos(parentUpdatePos, dataSource, false); + this.repo.setApplyToParent(childPos, false); + } + } + } + catch (Exception e) + { + LOGGER.error("issue in update for parent pos: " + parentUpdatePos); + } + finally + { + childReadLock.unlock(); + this.lockedPosSet.remove(childPos); + } + } + } + } + finally + { + if (parentLocked) + { + parentWriteLock.unlock(); + this.lockedPosSet.remove(parentUpdatePos); + } + + this.parentUpdatingPosSet.remove(parentUpdatePos); + } + }); + } + catch (RejectedExecutionException ignore) { /* the executor was shut down, it should be back up shortly and able to accept new jobs */ } + catch (Exception e) + { + this.parentUpdatingPosSet.remove(parentUpdatePos); + throw e; + } + } + } + + } + catch (InterruptedException ignored) { Thread.currentThread().interrupt(); } + catch (Exception e) + { + LOGGER.error("Unexpected error in the parent update queue thread. Error: " + e.getMessage(), e); + } + } + + LOGGER.info("Update thread ["+Thread.currentThread().getName()+"] terminated."); + } + + + + //=======================// + // data source migration // + //=======================// + + private void convertLegacyDataSources() + { + String dimensionName = this.level.getLevelWrapper().getDimensionType().getDimensionName(); + LOGGER.info("Attempting to migrate data sources for: ["+dimensionName+"]-["+this.saveDir+"]..."); + + + + //============================// + // delete unused data sources // + //============================// + + // this could be done all at once via SQL, + // but doing it in chunks prevents locking the database for long periods of time + long unusedCount = 0; + long totalDeleteCount = this.legacyFileHandler.repo.getUnusedDataSourceCount(); + if (totalDeleteCount != 0) + { + // this should only be shown once per session but should be shown during + // either when the deletion or migration phases start + ClientApi.INSTANCE.showMigrationMessageOnNextFrame(); + + + LOGGER.info("deleting [" + dimensionName + "] - ["+totalDeleteCount+"] unused data sources..."); + this.legacyDeletionCount = totalDeleteCount; + + ArrayList unusedDataPosList = this.legacyFileHandler.repo.getUnusedDataSourcePositionStringList(50); + while (unusedDataPosList.size() != 0) + { + unusedCount += unusedDataPosList.size(); + this.legacyDeletionCount -= unusedDataPosList.size(); + + + long startTime = System.currentTimeMillis(); + + // delete batch and get next batch + this.legacyFileHandler.repo.deleteUnusedLegacyData(unusedDataPosList); + unusedDataPosList = this.legacyFileHandler.repo.getUnusedDataSourcePositionStringList(50); + + long endStart = System.currentTimeMillis(); + long deleteTime = endStart - startTime; + 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 + // (that shouldn't be the case since we're using WAL journaling, but just in case) + try + { + // use the delete time so we don't make powerful computers wait super long + // and weak computers wait no time at all + Thread.sleep(deleteTime / 2); + } + catch (InterruptedException ignore){} + } + + LOGGER.info("Done deleting [" + dimensionName + "] - ["+totalDeleteCount+"] unused data sources."); + } + + + + //===========// + // migration // + //===========// + + long totalMigrationCount = this.legacyFileHandler.getDataSourceMigrationCount(); + this.migrationCount = totalMigrationCount; + LOGGER.info("Found ["+totalMigrationCount+"] data sources that need migration."); + + ArrayList legacyDataSourceList = this.legacyFileHandler.getDataSourcesToMigrate(MIGRATION_BATCH_COUNT); + if (!legacyDataSourceList.isEmpty()) + { + ClientApi.INSTANCE.showMigrationMessageOnNextFrame(); + + + // keep going until every data source has been migrated + int progressCount = 0; + while (!legacyDataSourceList.isEmpty() && this.migrationThreadRunning.get()) + { + LOGGER.info("Migrating [" + dimensionName + "] - [" + progressCount + "/" + totalMigrationCount + "]..."); + + ArrayList> updateFutureList = new ArrayList<>(); + for (int i = 0; i < legacyDataSourceList.size() && this.migrationThreadRunning.get(); i++) + { + FullDataSourceV1 legacyDataSource = legacyDataSourceList.get(i); + + try + { + // convert the legacy data source to the new format, + // this is a relatively cheap operation + FullDataSourceV2 newDataSource = FullDataSourceV2.createFromLegacyDataSourceV1(legacyDataSource); + newDataSource.applyToParent = true; + + // the actual update process can be moderately expensive due to having to update + // the render data along with the full data, so running it async on the update threads gains us a good bit of speed + CompletableFuture future = this.updateDataSourceAsync(newDataSource); + updateFutureList.add(future); + future.thenRun(() -> + { + // after the update finishes the legacy data source can be safely deleted + this.legacyFileHandler.repo.deleteWithKey(legacyDataSource.getPos()); + + try + { + newDataSource.close(); + } + catch (Exception ignore){ } + }); + } + catch (Exception e) + { + DhSectionPos migrationPos = legacyDataSource.getPos(); + LOGGER.warn("Unexpected issue migrating data source at pos " + migrationPos + ". Error: " + e.getMessage(), e); + this.legacyFileHandler.markMigrationFailed(migrationPos); + } + } + + + try + { + // wait for each thread to finish updating + CompletableFuture 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(); + } + + + if (this.migrationThreadRunning.get()) + { + LOGGER.info("migration complete for: ["+dimensionName+"]-["+this.saveDir+"]."); + this.migrationCount = 0; + } + else + { + LOGGER.info("migration stopped for: ["+dimensionName+"]-["+this.saveDir+"]."); + } + } + else + { + LOGGER.info("No migration necessary."); + } + + this.migrationThreadRunning.set(false); + } + + public long getLegacyDeletionCount() { return this.legacyDeletionCount; } + public long getTotalMigrationCount() { return this.migrationCount; } + + + + //=======================// + // retrieval (world gen) // + //=======================// + + /** + * Returns true if this provider can generate or retrieve + * {@link FullDataSourceV2}'s that aren't currently in the database. + */ + public boolean canRetrieveMissingDataSources() + { + // the base handler just handles basic reading/writing + // to the database and as such can't retrieve anything else. + return false; + } + + /** + * Returns false if this provider isn't accepting new requests, + * this can be due to having a full queue or some other + * limiting factor.

+ * + * Note: when overriding make sure to add:
+ * + * if (!super.canQueueRetrieval())
+ * {
+ * return false;
+ * }
+ *
+ * to the beginning of your override. + * Otherwise, parent retrieval limits will be ignored. + */ + public boolean canQueueRetrieval() + { + // Retrieval shouldn't happen while an unknown number of + // legacy data sources are present. + // If retrieval was allowed we might run into concurrency issues. + return !this.migrationThreadRunning.get(); + } + + /** + * @return null if this provider can't generate any positions and + * an empty array if all positions were generated + */ + @Nullable + public ArrayList getPositionsToRetrieve(DhSectionPos pos) { return null; } + /** + * 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. + */ + public int getMaxPossibleRetrievalPositionCountForPos(DhSectionPos pos) { return -1; } + + /** @return true if the position was queued, false if not */ + public boolean queuePositionForRetrieval(DhSectionPos genPos) { return false; } + + /** does nothing if the given position isn't present in the queue */ + public void removeRetrievalRequestIf(Function removeIf) { } + + public void clearRetrievalQueue() { } + + /** Can be used to display how many total retrieval requests might be available. */ + public void setTotalRetrievalPositionCount(int newCount) { } + + /** + * Returns how many data sources are currently in memory and haven't + * been saved to the database. + * Returns -1 if this provider never stores data sources to memory. + */ + public int getUnsavedDataSourceCount() { return -1; } + + + + //===========// + // overrides // + //===========// + + @Override + public void debugRender(DebugRenderer renderer) + { + this.lockedPosSet + .forEach((pos) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 74f, 0.15f, Color.PINK)); }); + + this.queuedUpdateCountsByPos + .forEach((pos, updateCountRef) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 80f + (updateCountRef.get() * 16f), 0.20f, Color.WHITE)); }); + this.parentUpdatingPosSet + .forEach((pos) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 80f, 0.20f, Color.MAGENTA)); }); + } + + @Override + public void close() + { + super.close(); + this.updateQueueProcessor.shutdownNow(); + + this.legacyFileHandler.close(); + + this.migrationThreadRunning.set(false); + this.migrationThreadPool.shutdown(); + } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataFileHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataFileHandler.java deleted file mode 100644 index d9bc2923a..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataFileHandler.java +++ /dev/null @@ -1,263 +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 . - */ - -package com.seibel.distanthorizons.core.file.fullDatafile; - -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; -import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; -import com.seibel.distanthorizons.core.generation.IWorldGenerationQueue; -import com.seibel.distanthorizons.core.generation.MissingWorldGenPositionFinder; -import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; -import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; -import com.seibel.distanthorizons.core.level.IDhLevel; -import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; -import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.util.LodUtil; -import org.apache.logging.log4j.Logger; - -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.function.Function; - -public class GeneratedFullDataFileHandler extends FullDataFileHandler -{ - private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - - private final AtomicReference worldGenQueueRef = new AtomicReference<>(null); - - private final ArrayList onWorldGenTaskCompleteListeners = new ArrayList<>(); - - /** Used to prevent world gen tasks from being queued multiple times. */ - private final Set generatingDataPos = Collections.newSetFromMap(new ConcurrentHashMap<>()); - - - - //=============// - // constructor // - //=============// - - public GeneratedFullDataFileHandler(IDhLevel level, AbstractSaveStructure saveStructure) { super(level, saveStructure); } - - - - //===========// - // overrides // - //===========// - - @Override - public IFullDataSource get(DhSectionPos pos) { return this.get(pos, true); } - public IFullDataSource get(DhSectionPos pos, boolean runWorldGenCheck) - { - IFullDataSource dataSource = super.get(pos); - - if (runWorldGenCheck) - { - // add world gen tasks for missing columns in the data source - // if this position hasn't already been queued for generation - IWorldGenerationQueue worldGenQueue = this.worldGenQueueRef.get(); - if (worldGenQueue != null && !this.generatingDataPos.contains(pos)) - { - this.queueWorldGenForMissingColumnsInDataSource(worldGenQueue, pos, dataSource); - } - } - - return dataSource; - } - - - - //==================// - // generation queue // - //==================// - - /** - * Assigns the queue for handling world gen and does first time setup as well.
- * Assumes there isn't a pre-existing queue. - */ - public void setWorldGenerationQueue(IWorldGenerationQueue newWorldGenQueue) - { - boolean oldQueueExists = this.worldGenQueueRef.compareAndSet(null, newWorldGenQueue); - LodUtil.assertTrue(oldQueueExists, "previous world gen queue is still here!"); - LOGGER.info("Set world gen queue for level ["+this.level+"]."); - } - - public void clearGenerationQueue() - { - this.worldGenQueueRef.set(null); - this.generatingDataPos.clear(); // clear the incomplete data sources - } - - /** Can be used to remove positions that are outside the player's render distance. */ - public void removeGenRequestIf(Function removeIf) - { - this.generatingDataPos.forEach((pos) -> - { - if (removeIf.apply(pos)) - { - this.generatingDataPos.remove(pos); - } - }); - } - - - - //=================// - // event listeners // - //=================// - - public void addWorldGenCompleteListener(IOnWorldGenCompleteListener listener) { this.onWorldGenTaskCompleteListeners.add(listener); } - public void removeWorldGenCompleteListener(IOnWorldGenCompleteListener listener) { this.onWorldGenTaskCompleteListeners.remove(listener); } - - - - //========// - // events // - //========// - - private void onWorldGenTaskComplete(WorldGenResult genTaskResult, Throwable exception) - { - if (exception != null) - { - // don't log shutdown exceptions - if (!(exception instanceof CancellationException || exception.getCause() instanceof CancellationException)) - { - LOGGER.error("Uncaught Gen Task Exception at [" + genTaskResult.pos + "], error: ["+ exception.getMessage() + "].", exception); - } - } - else if (genTaskResult.success) - { - this.fireOnGenPosSuccessListeners(genTaskResult.pos); - return; - } - else - { - // generation didn't complete - LOGGER.debug("Gen Task Failed at " + genTaskResult.pos); - } - - - // if the generation task was split up into smaller positions, add the on-complete event to them - for (CompletableFuture siblingFuture : genTaskResult.childFutures) - { - siblingFuture.whenComplete((siblingGenTaskResult, siblingEx) -> this.onWorldGenTaskComplete(siblingGenTaskResult, siblingEx)); - } - } - - private void fireOnGenPosSuccessListeners(DhSectionPos pos) - { - // fire the event listeners - for (IOnWorldGenCompleteListener listener : this.onWorldGenTaskCompleteListeners) - { - listener.onWorldGenTaskComplete(pos); - } - } - - - - //================// - // helper methods // - //================// - - private void queueWorldGenForMissingColumnsInDataSource(IWorldGenerationQueue worldGenQueue, DhSectionPos pos, IFullDataSource dataSource) - { - // get the un-generated pos list - byte minGeneratorSectionDetailLevel = (byte) (worldGenQueue.highestDataDetail() + DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); - ArrayList genPosList = MissingWorldGenPositionFinder.getUngeneratedPosList(dataSource, minGeneratorSectionDetailLevel, true); - - // start each pos generating - ArrayList> taskFutureList = new ArrayList<>(); - for (DhSectionPos genPos : genPosList) - { - // try not to re-queue already generating tasks - if (this.generatingDataPos.contains(genPos)) - { - continue; - } - - if (this.repo.existsWithPrimaryKey(genPos.serialize())) - { - continue; - } - - - // queue each new gen task - GenTask genTask = new GenTask(dataSource.getSectionPos()); - CompletableFuture worldGenFuture = worldGenQueue.submitGenTask(genPos, dataSource.getDataDetailLevel(), genTask); - worldGenFuture.whenComplete((genTaskResult, ex) -> this.onWorldGenTaskComplete(genTaskResult, ex)); - - taskFutureList.add(worldGenFuture); - } - - - // mark the data source as generating if necessary - if (taskFutureList.size() != 0) - { - this.generatingDataPos.add(pos); - CompletableFuture.allOf(taskFutureList.toArray(new CompletableFuture[0])) - .whenComplete((voidObj, ex) -> - { - this.generatingDataPos.remove(pos); - }); - } - } - - - - //================// - // helper classes // - //================// - - // TODO may not be needed - private class GenTask implements IWorldGenTaskTracker - { - private final DhSectionPos pos; - - public GenTask(DhSectionPos pos) - { - this.pos = pos; - } - - - - @Override - public boolean isMemoryAddressValid() { return true; } - - @Override - public Consumer getChunkDataConsumer() - { - return (chunkSizedFullDataSource) -> - { - GeneratedFullDataFileHandler.this.level.updateDataSourcesWithChunkData(chunkSizedFullDataSource); - }; - } - } - - /** used by external event listeners */ - @FunctionalInterface - public interface IOnWorldGenCompleteListener - { - /** Fired whenever a section has completed generating */ - void onWorldGenTaskComplete(DhSectionPos pos); - - } - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java new file mode 100644 index 000000000..dc2abe9cf --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java @@ -0,0 +1,396 @@ +/* + * 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 . + */ + +package com.seibel.distanthorizons.core.file.fullDatafile; + +import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; +import com.seibel.distanthorizons.core.config.Config; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; +import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; +import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue; +import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; +import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; +import com.seibel.distanthorizons.core.level.IDhLevel; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +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.util.LodUtil; +import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; +import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; +import org.apache.logging.log4j.Logger; + +import java.awt.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; + +public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 implements IDebugRenderable +{ + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + + public static final int MAX_WORLD_GEN_REQUESTS_PER_THREAD = 20; + + + private final AtomicReference worldGenQueueRef = new AtomicReference<>(null); + private final ArrayList onWorldGenTaskCompleteListeners = new ArrayList<>(); + + protected final DelayedFullDataSourceSaveCache delayedFullDataSourceSaveCache = new DelayedFullDataSourceSaveCache(this::onDataSourceSave, 5_000); + + + + //=============// + // constructor // + //=============// + + public GeneratedFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure) { super(level, saveStructure); } + + + + //=================// + // event listeners // + //=================// + + public void addWorldGenCompleteListener(IOnWorldGenCompleteListener listener) { this.onWorldGenTaskCompleteListeners.add(listener); } + public void removeWorldGenCompleteListener(IOnWorldGenCompleteListener listener) { this.onWorldGenTaskCompleteListeners.remove(listener); } + + + + //========// + // events // + //========// + + private void onWorldGenTaskComplete(WorldGenResult genTaskResult, Throwable exception) + { + if (exception != null) + { + // don't log shutdown exceptions + if (!(exception instanceof CancellationException || exception.getCause() instanceof CancellationException)) + { + LOGGER.error("Uncaught Gen Task Exception at [" + genTaskResult.pos + "], error: ["+ exception.getMessage() + "].", exception); + } + } + else if (genTaskResult.success) + { + this.fireOnGenPosSuccessListeners(genTaskResult.pos); + return; + } + else + { + // generation didn't complete + LOGGER.debug("Gen Task Failed at " + genTaskResult.pos); + } + + + // if the generation task was split up into smaller positions, add the on-complete event to them + for (CompletableFuture siblingFuture : genTaskResult.childFutures) + { + siblingFuture.whenComplete((siblingGenTaskResult, siblingEx) -> this.onWorldGenTaskComplete(siblingGenTaskResult, siblingEx)); + } + } + + // TODO only fire after the section has finished generated or once every X seconds + private void fireOnGenPosSuccessListeners(DhSectionPos pos) + { + // fire the event listeners + for (IOnWorldGenCompleteListener listener : this.onWorldGenTaskCompleteListeners) + { + listener.onWorldGenTaskComplete(pos); + } + } + + + + //===================================// + // world gen (data source retrieval) // + //===================================// + + /** + * Assigns the queue for handling world gen and does first time setup as well.
+ * Assumes there isn't a pre-existing queue. + */ + public void setWorldGenerationQueue(IFullDataSourceRetrievalQueue newWorldGenQueue) + { + boolean oldQueueExists = this.worldGenQueueRef.compareAndSet(null, newWorldGenQueue); + LodUtil.assertTrue(oldQueueExists, "previous world gen queue is still here!"); + LOGGER.info("Set world gen queue for level ["+this.level+"]."); + } + + @Override + public boolean canRetrieveMissingDataSources() { return true; } + + @Override + public void setTotalRetrievalPositionCount(int newCount) + { + IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get(); + if (worldGenQueue != null) + { + worldGenQueue.setEstimatedTotalTaskCount(newCount); + } + } + + @Override + public boolean canQueueRetrieval() + { + if (!super.canQueueRetrieval()) + { + return false; + } + + + IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get(); + if (worldGenQueue == null) + { + // we can't queue anything if the world generator isn't set up yet + return false; + } + + + ThreadPoolExecutor updateExecutor = ThreadPoolUtil.getUpdatePropagatorExecutor(); + if (updateExecutor == null || updateExecutor.getQueue().size() >= MAX_UPDATE_TASK_COUNT / 2) + { + // don't queue additional world gen requests if the updater is behind + return false; + } + + + int maxQueueCount = MAX_WORLD_GEN_REQUESTS_PER_THREAD * Config.Client.Advanced.MultiThreading.numberOfWorldGenerationThreads.get(); + + if (this.delayedFullDataSourceSaveCache.getUnsavedCount() >= maxQueueCount) + { + // don't queue additional world gen requests if there are + // a lot of data sources in memory + // (this is done to prevent infinite memory growth) + return false; + } + + + // don't queue additional world gen requests beyond the max allotted count + return worldGenQueue.getWaitingTaskCount() < maxQueueCount; + } + + @Override + public boolean queuePositionForRetrieval(DhSectionPos genPos) + { + IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get(); + if (worldGenQueue == null) + { + return false; + } + + GenTask genTask = new GenTask(genPos); + CompletableFuture worldGenFuture = worldGenQueue.submitGenTask(genPos, (byte) (genPos.getDetailLevel() - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL), genTask); + worldGenFuture.whenComplete((genTaskResult, ex) -> this.onWorldGenTaskComplete(genTaskResult, ex)); + + return true; + } + + @Override + public void removeRetrievalRequestIf(Function removeIf) + { + IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get(); + if (worldGenQueue != null) + { + worldGenQueue.removeRetrievalRequestIf(removeIf); + } + } + + @Override + public void clearRetrievalQueue() { this.worldGenQueueRef.set(null); } + + @Override + public int getUnsavedDataSourceCount() { return this.delayedFullDataSourceSaveCache.getUnsavedCount(); } + + + @Override + public ArrayList getPositionsToRetrieve(DhSectionPos pos) + { + IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get(); + if (worldGenQueue == null) + { + return null; + } + + + // don't check any child positions if this position is already fully generated + if (this.repo.existsWithKey(pos)) + { + byte[] columnGenerationSteps = this.repo.getColumnGenerationStepForPos(pos); + // shouldn't happen, but just in case + if (columnGenerationSteps != null) + { + boolean positionFullyGenerated = true; + + // check if any positions are ungenerated + for (int i = 0; i < columnGenerationSteps.length; i++) + { + if (columnGenerationSteps[i] == EDhApiWorldGenerationStep.EMPTY.value) + { + positionFullyGenerated = false; + break; + } + } + + if (positionFullyGenerated) + { + return new ArrayList<>(); + } + } + } + + + + // this section is missing one or more columns, queue the missing ones for generation. + // TODO speed up this logic by only checking ungenerated columns + ArrayList generationList = new ArrayList<>(); + byte minGeneratorSectionDetailLevel = (byte) (worldGenQueue.highestDataDetail() + DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); + pos.forEachChildAtDetailLevel(minGeneratorSectionDetailLevel, (genPos) -> + { + if (!this.repo.existsWithKey(genPos)) + { + // nothing exists for this position, it needs generation + generationList.add(genPos); + } + else + { + byte[] columnGenerationSteps = this.repo.getColumnGenerationStepForPos(genPos); + if (columnGenerationSteps == null) + { + // shouldn't happen, but just in case + return; + } + + + EDhApiWorldGenerationStep currentMinWorldGenStep = EDhApiWorldGenerationStep.LIGHT; + checkWorldGenLoop: + for (int x = 0; x < FullDataSourceV2.WIDTH; x++) + { + for (int z = 0; z < FullDataSourceV2.WIDTH; z++) + { + int index = FullDataSourceV2.relativePosToIndex(x, z); + byte genStepValue = columnGenerationSteps[index]; + + if (genStepValue < currentMinWorldGenStep.value) + { + EDhApiWorldGenerationStep newWorldGenStep = EDhApiWorldGenerationStep.fromValue(genStepValue); + if (newWorldGenStep != null && newWorldGenStep.value < currentMinWorldGenStep.value) + { + currentMinWorldGenStep = newWorldGenStep; + } + } + + if (currentMinWorldGenStep == EDhApiWorldGenerationStep.EMPTY) + { + // queue the task + break checkWorldGenLoop; + } + } + } + + if (currentMinWorldGenStep != EDhApiWorldGenerationStep.EMPTY) + { + // no world gen needed for this position + return; + } + + generationList.add(genPos); + } + }); + + return generationList; + } + + @Override + public int getMaxPossibleRetrievalPositionCountForPos(DhSectionPos pos) + { + IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get(); + if (worldGenQueue == null) + { + return -1; + } + + int minGeneratorSectionDetailLevel = worldGenQueue.highestDataDetail() + DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; + int detailLevelDiff = pos.getDetailLevel() - minGeneratorSectionDetailLevel; + + return BitShiftUtil.powerOfTwo(detailLevelDiff); + } + + + + //=======// + // debug // + //=======// + + @Override + public void debugRender(DebugRenderer renderer) + { + super.debugRender(renderer); + + this.delayedFullDataSourceSaveCache.dataSourceByPosition + .forEach((pos, dataSource) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 80f, 0.20f, Color.green.darker())); }); + } + + + + + //================// + // helper classes // + //================// + + // TODO may not be needed + private class GenTask implements IWorldGenTaskTracker + { + private final DhSectionPos pos; + + public GenTask(DhSectionPos pos) + { + this.pos = pos; + } + + + + @Override + public boolean isMemoryAddressValid() { return true; } + + @Override + public Consumer getChunkDataConsumer() + { + return (chunkSizedFullDataSource) -> + { + GeneratedFullDataSourceProvider.this.delayedFullDataSourceSaveCache.queueDataSourceForUpdateAndSave(chunkSizedFullDataSource); + }; + } + } + private void onDataSourceSave(FullDataSourceV2 fullDataSource) + { GeneratedFullDataSourceProvider.this.updateDataSourceAsync(fullDataSource); } + + + + /** used by external event listeners */ + @FunctionalInterface + public interface IOnWorldGenCompleteListener + { + /** Fired whenever a section has completed generating */ + void onWorldGenTaskComplete(DhSectionPos pos); + + } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/IFullDataSourceProvider.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/IFullDataSourceProvider.java deleted file mode 100644 index b487b6a20..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/IFullDataSourceProvider.java +++ /dev/null @@ -1,46 +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 . - */ - -package com.seibel.distanthorizons.core.file.fullDatafile; - -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; -import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource; -import com.seibel.distanthorizons.core.file.ISourceProvider; -import com.seibel.distanthorizons.core.level.IDhClientLevel; -import com.seibel.distanthorizons.core.level.IDhLevel; -import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.sql.FullDataRepo; - -import java.util.concurrent.CompletableFuture; - -/** - * Handles reading, writing, and updating {@link IFullDataSource}'s.
- * Should be backed by a database handled by a {@link FullDataRepo}. - */ -public interface IFullDataSourceProvider extends ISourceProvider, AutoCloseable -{ - CompletableFuture getAsync(DhSectionPos pos); - IFullDataSource get(DhSectionPos pos); - - CompletableFuture updateDataSourcesWithChunkDataAsync(ChunkSizedFullDataAccessor chunkData); - - int getUnsavedDataSourceCount(); - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/RemoteFullDataFileHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/RemoteFullDataSourceProvider.java similarity index 73% rename from core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/RemoteFullDataFileHandler.java rename to core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/RemoteFullDataSourceProvider.java index f3ef059cb..efdf28122 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/RemoteFullDataFileHandler.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/RemoteFullDataSourceProvider.java @@ -25,9 +25,9 @@ import org.jetbrains.annotations.Nullable; import java.io.File; -public class RemoteFullDataFileHandler extends FullDataFileHandler +public class RemoteFullDataSourceProvider extends FullDataSourceProviderV2 { - public RemoteFullDataFileHandler(IDhLevel level, AbstractSaveStructure saveStructure) { super(level, saveStructure); } - public RemoteFullDataFileHandler(IDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride) { super(level, saveStructure, saveDirOverride); } + public RemoteFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure) { super(level, saveStructure); } + public RemoteFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride) { super(level, saveStructure, saveDirOverride); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/IRenderSourceProvider.java b/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/IRenderSourceProvider.java deleted file mode 100644 index 2e31e1794..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/IRenderSourceProvider.java +++ /dev/null @@ -1,46 +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 . - */ - -package com.seibel.distanthorizons.core.file.renderfile; - -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; -import com.seibel.distanthorizons.core.file.ISourceProvider; -import com.seibel.distanthorizons.core.level.IDhClientLevel; -import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource; -import com.seibel.distanthorizons.core.sql.FullDataRepo; -import com.seibel.distanthorizons.core.sql.RenderDataRepo; - -import java.util.concurrent.CompletableFuture; - -/** - * Handles reading, writing, and updating {@link ColumnRenderSource}'s.
- * Should be backed by a database handled by a {@link RenderDataRepo}. - */ -public interface IRenderSourceProvider extends ISourceProvider -{ - CompletableFuture getAsync(DhSectionPos pos); - - CompletableFuture updateDataSourcesWithChunkDataAsync(ChunkSizedFullDataAccessor chunkData); - - /** Deletes any data stored in the render cache so it can be re-created */ - void deleteRenderCache(); - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/RenderSourceFileHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/RenderSourceFileHandler.java deleted file mode 100644 index 8b85153e4..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/RenderSourceFileHandler.java +++ /dev/null @@ -1,175 +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 . - */ - -package com.seibel.distanthorizons.core.file.renderfile; - -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; -import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSourceLoader; -import com.seibel.distanthorizons.core.dataObjects.transformers.FullDataToRenderDataTransformer; -import com.seibel.distanthorizons.core.file.AbstractDataSourceHandler; -import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; -import com.seibel.distanthorizons.core.level.IDhLevel; -import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; -import com.seibel.distanthorizons.core.logging.f3.F3Screen; -import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource; -import com.seibel.distanthorizons.core.file.fullDatafile.IFullDataSourceProvider; -import com.seibel.distanthorizons.core.level.IDhClientLevel; -import com.seibel.distanthorizons.core.sql.AbstractDataSourceRepo; -import com.seibel.distanthorizons.core.sql.DataSourceDto; -import com.seibel.distanthorizons.core.sql.FullDataRepo; -import com.seibel.distanthorizons.core.sql.RenderDataRepo; -import com.seibel.distanthorizons.core.util.threading.ThreadPools; -import org.apache.logging.log4j.Logger; - -import java.io.File; -import java.io.IOException; -import java.sql.SQLException; -import java.util.*; -import java.util.concurrent.*; - -public class RenderSourceFileHandler extends AbstractDataSourceHandler implements IRenderSourceProvider -{ - private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - - private final F3Screen.NestedMessage threadPoolMsg; - - private final IFullDataSourceProvider fullDataSourceProvider; - - - - //=============// - // constructor // - //=============// - - public RenderSourceFileHandler(IFullDataSourceProvider sourceProvider, IDhClientLevel clientLevel, AbstractSaveStructure saveStructure) - { - super(clientLevel, saveStructure); - - this.fullDataSourceProvider = sourceProvider; - this.threadPoolMsg = new F3Screen.NestedMessage(this::f3Log); - } - - - - //===========// - // overrides // - //===========// - - @Override - public ColumnRenderSource get(DhSectionPos pos) - { - // call the full data provider to make sure the full data is up to date - // and any necessary world generation has been queued/completed - this.fullDataSourceProvider.get(pos); - - return super.get(pos); - } - - - //====================// - // Abstract overrides // - //====================// - - @Override - protected AbstractDataSourceRepo createRepo() - { - try - { - return new RenderDataRepo("jdbc:sqlite", this.saveDir.getPath() + "/" + AbstractSaveStructure.DATABASE_NAME); - } - catch (SQLException e) - { - // should only happen if there is an issue with the database (it's locked or can't be created if missing) - // or the database update failed - throw new RuntimeException(e); - } - } - - @Override - protected ColumnRenderSource createDataSourceFromDto(DataSourceDto dto) throws InterruptedException, IOException - { return ColumnRenderSourceLoader.INSTANCE.loadRenderSource(dto, dto.getInputStream(), this.level); } - @Override - protected ColumnRenderSource createNewDataSourceFromExistingDtos(DhSectionPos pos) - { - ColumnRenderSource renderDataSource; - - IFullDataSource fullDataSource = this.fullDataSourceProvider.get(pos); - if (fullDataSource != null) - { - renderDataSource = FullDataToRenderDataTransformer.transformFullDataToRenderSource(fullDataSource, this.level); - } - else - { - renderDataSource = this.makeEmptyDataSource(pos); - } - return renderDataSource; - } - - @Override - protected ColumnRenderSource makeEmptyDataSource(DhSectionPos pos) - { return ColumnRenderSource.createEmptyRenderSource(pos); } - - - - - //=========// - // F3 menu // - //=========// - - /** Returns what should be displayed in Minecraft's F3 debug menu */ - private String[] f3Log() - { - ThreadPoolExecutor executor = ThreadPools.getFileHandlerExecutor(); - String queueSize = (executor != null) ? executor.getQueue().size()+"" : "-"; - String completedTaskSize = (executor != null) ? executor.getCompletedTaskCount()+"" : "-"; - - ArrayList lines = new ArrayList<>(); - lines.add("File Handler [" + this.level.getLevelWrapper().getDimensionType().getDimensionName() + "]"); - lines.add(" Thread pool tasks: " + queueSize + " (completed: " + completedTaskSize + ")"); - lines.add(" Unsaved render sources: " + this.unsavedDataSourceBySectionPos.size()); - lines.add(" Unsaved data sources: " + this.fullDataSourceProvider.getUnsavedDataSourceCount()); - - return lines.toArray(new String[0]); - } - - @Override - public CompletableFuture updateDataSourcesWithChunkDataAsync(ChunkSizedFullDataAccessor chunkDataView) - { - return CompletableFuture.allOf( - super.updateDataSourcesWithChunkDataAsync(chunkDataView), - this.fullDataSourceProvider.updateDataSourcesWithChunkDataAsync(chunkDataView) - ); - } - - - //=====================// - // shutdown / clearing // - //=====================// - - public void close() - { - super.close(); - this.threadPoolMsg.close(); - } - - public void deleteRenderCache() { this.repo.deleteAll(); } - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/structure/ClientOnlySaveStructure.java b/core/src/main/java/com/seibel/distanthorizons/core/file/structure/ClientOnlySaveStructure.java index 1060076fd..6e9809934 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/structure/ClientOnlySaveStructure.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/structure/ClientOnlySaveStructure.java @@ -23,7 +23,7 @@ import com.google.common.net.PercentEscaper; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.file.subDimMatching.SubDimensionLevelMatcher; import com.seibel.distanthorizons.core.config.Config; -import com.seibel.distanthorizons.api.enums.config.EServerFolderNameMode; +import com.seibel.distanthorizons.api.enums.config.EDhApiServerFolderNameMode; import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel; import com.seibel.distanthorizons.core.util.objects.ParsedIp; import com.seibel.distanthorizons.core.util.LodUtil; @@ -264,7 +264,7 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure // determine the auto folder name format - EServerFolderNameMode folderNameMode = Config.Client.Advanced.Multiplayer.serverFolderNameMode.get(); + EDhApiServerFolderNameMode folderNameMode = Config.Client.Advanced.Multiplayer.serverFolderNameMode.get(); String serverName = MC_CLIENT.getCurrentServerName().replaceAll(INVALID_FILE_CHARACTERS_REGEX, ""); String serverMcVersion = MC_CLIENT.getCurrentServerVersion().replaceAll(INVALID_FILE_CHARACTERS_REGEX, ""); @@ -277,6 +277,9 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure case NAME_ONLY: folderName = serverName; break; + case IP_ONLY: + folderName = serverIpCleaned; + break; case NAME_IP: folderName = serverName + ", IP " + serverIpCleaned; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/subDimMatching/SubDimensionLevelMatcher.java b/core/src/main/java/com/seibel/distanthorizons/core/file/subDimMatching/SubDimensionLevelMatcher.java index 85fdb0c58..a07c3176a 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/subDimMatching/SubDimensionLevelMatcher.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/subDimMatching/SubDimensionLevelMatcher.java @@ -21,10 +21,7 @@ package com.seibel.distanthorizons.core.file.subDimMatching; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.SingleColumnFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.CompleteFullDataSource; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure; import com.seibel.distanthorizons.core.generation.DhLightingEngine; @@ -43,6 +40,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftCli import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; +import it.unimi.dsi.fastutil.longs.LongArrayList; import org.apache.logging.log4j.LogManager; import java.io.File; @@ -190,14 +188,9 @@ public class SubDimensionLevelMatcher implements AutoCloseable LOGGER.warn("unable to build lod for chunk:"+newlyLoadedChunk.getChunkPos()); return null; } - ChunkSizedFullDataAccessor newChunkSizedFullDataView = LodDataBuilder.createChunkData(newlyLoadedChunk); - if (newChunkSizedFullDataView == null) - { - LOGGER.warn("Unexpected LOD build error for chunk:"+newlyLoadedChunk.getChunkPos()); - return null; - } + FullDataSourceV2 newChunkSizedFullDataView = FullDataSourceV2.createFromChunk(newlyLoadedChunk); // convert to a data source for easier comparing - CompleteFullDataSource newDataSource = CompleteFullDataSource.createEmpty(new DhSectionPos(this.playerData.playerBlockPos)); + FullDataSourceV2 newDataSource = FullDataSourceV2.createEmpty(new DhSectionPos(this.playerData.playerBlockPos)); newDataSource.update(newChunkSizedFullDataView); @@ -219,7 +212,7 @@ public class SubDimensionLevelMatcher implements AutoCloseable { // get the data source to compare against IDhLevel tempLevel = new DhClientLevel(new ClientOnlySaveStructure(), this.currentClientLevel, testLevelFolder, false); - IFullDataSource testFullDataSource = tempLevel.getFileHandler().getAsync(new DhSectionPos(this.playerData.playerBlockPos)).join(); + FullDataSourceV2 testFullDataSource = tempLevel.getFullDataProvider().getAsync(new DhSectionPos(this.playerData.playerBlockPos)).join(); if (testFullDataSource == null) { continue; @@ -227,8 +220,8 @@ public class SubDimensionLevelMatcher implements AutoCloseable // confirm both data sources have the same section pos - DhSectionPos newSectionChunkPos = newDataSource.getSectionPos().convertNewToDetailLevel(DhSectionPos.SECTION_CHUNK_DETAIL_LEVEL); - DhSectionPos testSectionChunkPos = testFullDataSource.getSectionPos().convertNewToDetailLevel(DhSectionPos.SECTION_CHUNK_DETAIL_LEVEL); + DhSectionPos newSectionChunkPos = newDataSource.getPos().convertNewToDetailLevel(DhSectionPos.SECTION_CHUNK_DETAIL_LEVEL); + DhSectionPos testSectionChunkPos = testFullDataSource.getPos().convertNewToDetailLevel(DhSectionPos.SECTION_CHUNK_DETAIL_LEVEL); LodUtil.assertTrue(newSectionChunkPos.equals(testSectionChunkPos), "data source positions don't match"); @@ -236,26 +229,26 @@ public class SubDimensionLevelMatcher implements AutoCloseable // compare the data sources int equalDataPoints = 0; int totalDataPointCount = 0; - for (int x = 0; x < CompleteFullDataSource.WIDTH; x++) + for (int x = 0; x < FullDataSourceV2.WIDTH; x++) { - for (int z = 0; z < CompleteFullDataSource.WIDTH; z++) + for (int z = 0; z < FullDataSourceV2.WIDTH; z++) { - SingleColumnFullDataAccessor newColumn = newDataSource.tryGet(x, z); - SingleColumnFullDataAccessor testColumn = testFullDataSource.tryGet(x, z); + LongArrayList newColumn = newDataSource.get(x, z); + LongArrayList testColumn = testFullDataSource.get(x, z); if (newColumn != null && testColumn != null) { // compare each data point in the column - FullDataPointIdMap newDataMap = newDataSource.getMapping(); - FullDataPointIdMap testDataMap = testFullDataSource.getMapping(); + FullDataPointIdMap newDataMap = newDataSource.mapping; + FullDataPointIdMap testDataMap = testFullDataSource.mapping; // use min to prevent going out of bounds - int minColumnIndex = Math.min(newColumn.getSingleLength(), testColumn.getSingleLength()); + int minColumnIndex = Math.min(newColumn.size(), testColumn.size()); for (int i = 0; i < minColumnIndex; i++) { - long newDataPoint = newColumn.getSingle(i); - long testDataPoint = testColumn.getSingle(i); + long newDataPoint = newColumn.getLong(i); + long testDataPoint = testColumn.getLong(i); int newId = FullDataPointUtil.getId(newDataPoint); int testId = FullDataPointUtil.getId(testDataPoint); @@ -304,7 +297,7 @@ public class SubDimensionLevelMatcher implements AutoCloseable else if (newColumn != null) { // missing test column - totalDataPointCount += newColumn.getSingleLength(); + totalDataPointCount += newColumn.size(); } else { diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/DhLightingEngine.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/DhLightingEngine.java index c319c3b07..0e648ef33 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/DhLightingEngine.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/DhLightingEngine.java @@ -207,7 +207,7 @@ public class DhLightingEngine long endTimeNs = System.nanoTime(); float totalTimeMs = (endTimeNs - startTimeNs) / 1_000_000.0f; - LOGGER.trace("Finished generating lighting for chunk: [" + centerChunkPos + "] in ["+totalTimeMs+"] milliseconds"); + //LOGGER.trace("Finished generating lighting for chunk: [" + centerChunkPos + "] in ["+totalTimeMs+"] milliseconds"); } /** Applies each {@link LightPos} from the queue to the given set of {@link IChunkWrapper}'s. */ diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/IFullDataSourceRetrievalQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/IFullDataSourceRetrievalQueue.java new file mode 100644 index 000000000..ade0d524e --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/IFullDataSourceRetrievalQueue.java @@ -0,0 +1,110 @@ +/* + * 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 . + */ + +package com.seibel.distanthorizons.core.generation; + +import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; +import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; +import com.seibel.distanthorizons.core.pos.DhBlockPos2D; +import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.render.LodQuadTree; + +import java.io.Closeable; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +/** + * Used to track what full data sources the system currently + * wants but doesn't have.
+ * IE, what sections should be generated via the world generator.

+ * + * Note:
+ * This won't contain every position that needs to be retrieved + * (due to causing issues at extreme render distances). + * TODO does that mean this object isn't necessary or + * should just be renamed since it isn't the full queue

+ * + * Use by both world gen and server networking. + * + * @see LodQuadTree + */ +public interface IFullDataSourceRetrievalQueue extends Closeable +{ + //=========// + // getters // + //=========// + + /** the largest numerical detail level */ + byte lowestDataDetail(); + /** the smallest numerical detail level */ + byte highestDataDetail(); + + + + //=======// + // setup // + //=======// + + /** + * Starts the retrieval process if not already running, + * and if running updates the target position. + * + * @param targetPos the position that retrieval should be centered around, + * generally this will be the player's position. + * */ + void startAndSetTargetPos(DhBlockPos2D targetPos); + + + + //===============// + // task handling // + //===============// + + /** + * Generally the retrieval queue should be fairly small, so its faster to iterate over the existing list + * and check if each one is valid vs dumbly attempting to remove every position that just went out of range. + */ + void removeRetrievalRequestIf(Function removeIf); + + CompletableFuture submitGenTask(DhSectionPos pos, byte requiredDataDetail, IWorldGenTaskTracker tracker); + + + + //==========// + // shutdown // + //==========// + + CompletableFuture startClosing(boolean cancelCurrentGeneration, boolean alsoInterruptRunning); + void close(); + + + + //===============// + // debug display // + //===============// + + int getWaitingTaskCount(); + int getInProgressTaskCount(); + + /** used for rendering to the F3 menu */ + int getEstimatedTotalTaskCount(); + void setEstimatedTotalTaskCount(int newEstimate); + + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/IWorldGenerationQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/IWorldGenerationQueue.java deleted file mode 100644 index af9c28755..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/IWorldGenerationQueue.java +++ /dev/null @@ -1,48 +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 . - */ - -package com.seibel.distanthorizons.core.generation; - -import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; -import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; -import com.seibel.distanthorizons.core.pos.DhBlockPos2D; -import com.seibel.distanthorizons.core.pos.DhSectionPos; - -import java.io.Closeable; -import java.util.concurrent.CompletableFuture; - -public interface IWorldGenerationQueue extends Closeable -{ - /** the largest numerical detail level */ - byte lowestDataDetail(); - /** the smallest numerical detail level */ - byte highestDataDetail(); - - CompletableFuture submitGenTask(DhSectionPos pos, byte requiredDataDetail, IWorldGenTaskTracker tracker); - - /** @param targetPos the position that world generation should be centered around, generally this will be the player's position. */ - void startGenerationQueueAndSetTargetPos(DhBlockPos2D targetPos); - - int getWaitingTaskCount(); - int getInProgressTaskCount(); - - CompletableFuture startClosing(boolean cancelCurrentGeneration, boolean alsoInterruptRunning); - void close(); - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/MissingWorldGenPositionFinder.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/MissingWorldGenPositionFinder.java deleted file mode 100644 index f6e554dd8..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/MissingWorldGenPositionFinder.java +++ /dev/null @@ -1,213 +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 . - */ - -package com.seibel.distanthorizons.core.generation; - -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.SingleColumnFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; -import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; -import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; -import org.apache.logging.log4j.Logger; - -import java.util.ArrayList; -import java.util.LinkedList; - -/** - * Handles finding any positions in a given {@link IFullDataSource} that - * aren't generated. - */ -public class MissingWorldGenPositionFinder -{ - private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - - - /** - * @param generatorDetailLevel the detail level that the un-generated positions should be split into. - * @param onlyReturnPositionsTheGeneratorCanAccept - * If true this will only return positions with the same detail level as generatorDetailLevel.
- * If false this will return the lowest detail level possible for totally empty sections.
- * As of 2023-9-28 both have been tested and confirmed working with the Batch world generator, the only difference is that - * the task list will be deceptively small if this value is false. - * - * @return the list of {@link DhSectionPos} that aren't generated in this data source. - */ - public static ArrayList getUngeneratedPosList(IFullDataSource dataSource, byte generatorDetailLevel, boolean onlyReturnPositionsTheGeneratorCanAccept) - { - ArrayList posArray = getUngeneratedPosListForQuadrant(dataSource, dataSource.getSectionPos(), generatorDetailLevel); - - if (onlyReturnPositionsTheGeneratorCanAccept) - { - LinkedList posList = new LinkedList<>(posArray); - - // subdivide positions until they match the generatorDetailLevel - ArrayList cleanedPosArray = new ArrayList<>(); - while (posList.size() > 0) - { - DhSectionPos pos = posList.remove(); - if (pos.getDetailLevel() > generatorDetailLevel) - { - pos.forEachChild((childPos) -> posList.push(childPos)); - } - else - { - cleanedPosArray.add(pos); - } - } - - return cleanedPosArray; - } - else - { - return posArray; - } - } - private static ArrayList getUngeneratedPosListForQuadrant(IFullDataSource dataSource, DhSectionPos quadrantPos, byte generatorDetailLevel) - { - ArrayList ungeneratedPosList = new ArrayList<>(); - - int sourceRelWidthInDataPoints = dataSource.getWidthInDataPoints(); - - - if (quadrantPos.getDetailLevel() == generatorDetailLevel) - { - // we are at the highest detail level the world generator can accept, - // we either need to generate this whole section, or not at all - - ESectionPopulationState populationState = getPopulationStateForPos(dataSource, quadrantPos, sourceRelWidthInDataPoints); - if (populationState != ESectionPopulationState.COMPLETE) - { - // at least 1 data point is missing, - // this whole section must be generated - ungeneratedPosList.add(quadrantPos); - } - } - else if (quadrantPos.getDetailLevel() > generatorDetailLevel) - { - // detail level too low for world generator, check child positions - for (int i = 0; i < 4; i++) - { - DhSectionPos inputPos = quadrantPos.getChildByIndex(i); - - ESectionPopulationState populationState = getPopulationStateForPos(dataSource, inputPos, sourceRelWidthInDataPoints); - if (populationState == ESectionPopulationState.COMPLETE) - { - // no generation necessary - continue; - } - else if (populationState == ESectionPopulationState.EMPTY) - { - // nothing exists for this sub quadrant, add this sub-quadrant's position - ungeneratedPosList.add(inputPos); - } - else if (populationState == ESectionPopulationState.PARTIAL) - { - // some data exists in this quadrant, but not all that we need - // recurse down to determine which sub-quadrant positions will need generation - ungeneratedPosList.addAll(getUngeneratedPosListForQuadrant(dataSource, inputPos, generatorDetailLevel)); - } - } - } - else - { - throw new IllegalArgumentException("detail level lower than world generator can accept."); - } - - return ungeneratedPosList; - } - private static ESectionPopulationState getPopulationStateForPos(IFullDataSource dataSource, DhSectionPos inputPos, int sourceRelWidthInDataPoints) - { - // TODO comment - - byte childDetailLevel = inputPos.getDetailLevel(); - - int quadrantDetailLevelDiff = dataSource.getSectionPos().getDetailLevel() - childDetailLevel; - int widthInSecPos = BitShiftUtil.powerOfTwo(quadrantDetailLevelDiff); - int relWidthForSecPos = sourceRelWidthInDataPoints / widthInSecPos; - - DhSectionPos minSecPos = dataSource.getSectionPos().convertNewToDetailLevel(childDetailLevel); - - - - int minRelX = inputPos.getX() - minSecPos.getX(); - int maxRelX = minRelX + 1; - - int minRelZ = inputPos.getZ() - minSecPos.getZ(); - int maxRelZ = minRelZ + 1; - - minRelX = minRelX * relWidthForSecPos; - maxRelX = maxRelX * relWidthForSecPos; - - minRelZ = minRelZ * relWidthForSecPos; - maxRelZ = maxRelZ * relWidthForSecPos; - - - - boolean quadrantFullyGenerated = true; - boolean quadrantEmpty = true; - for (int relX = minRelX; relX < maxRelX; relX++) - { - for (int relZ = minRelZ; relZ < maxRelZ; relZ++) - { - SingleColumnFullDataAccessor column = dataSource.tryGet(relX, relZ); - if (column == null || !column.doesColumnExist()) - { - // no data for this relative position - quadrantFullyGenerated = false; - } - else - { - // data exists for this pos - quadrantEmpty = false; - } - } - } - - - if (quadrantFullyGenerated) - { - // no generation necessary - return ESectionPopulationState.COMPLETE; - } - else if (quadrantEmpty) - { - // nothing exists for this sub quadrant, add this sub-quadrant's position - return ESectionPopulationState.EMPTY; - } - else - { - // some data exists in this quadrant, but not all that we need - // recurse down to determine which sub-quadrant positions will need generation - return ESectionPopulationState.PARTIAL; - } - } - - - - //================// - // helper classes // - //================// - - private enum ESectionPopulationState - { - COMPLETE, - EMPTY, - PARTIAL - } -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java index 7c3b68744..04c485bf2 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java @@ -23,7 +23,7 @@ import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGenerat import com.seibel.distanthorizons.api.interfaces.override.worldGenerator.IDhApiWorldGenerator; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGeneratorReturnType; import com.seibel.distanthorizons.api.objects.data.DhApiChunk; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.generation.tasks.*; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; @@ -38,7 +38,7 @@ import com.seibel.distanthorizons.core.util.LodUtil.AssertFailureException; import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.objects.UncheckedInterruptedException; import com.seibel.distanthorizons.core.util.LodUtil; -import com.seibel.distanthorizons.core.util.threading.ThreadPools; +import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import org.apache.logging.log4j.Logger; @@ -47,8 +47,9 @@ import java.awt.*; import java.util.*; import java.util.concurrent.*; import java.util.function.Consumer; +import java.util.function.Function; -public class WorldGenerationQueue implements IWorldGenerationQueue, IDebugRenderable +public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDebugRenderable { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class); @@ -66,13 +67,8 @@ public class WorldGenerationQueue implements IWorldGenerationQueue, IDebugRender /** largest numerical detail level allowed */ public final byte lowestDataDetail; - @Override - public byte lowestDataDetail() { return this.lowestDataDetail; } - /** smallest numerical detail level allowed */ public final byte highestDataDetail; - @Override - public byte highestDataDetail() { return this.highestDataDetail; } /** If not null this generator is in the process of shutting down */ @@ -95,6 +91,9 @@ public class WorldGenerationQueue implements IWorldGenerationQueue, IDebugRender private final HashMap alreadyGeneratedPosHashSet = new HashMap<>(MAX_ALREADY_GENERATED_COUNT); private final Queue alreadyGeneratedPosQueue = new LinkedList<>(); + /** just used for rendering to the F3 menu */ + private int estimatedTotalTaskCount = 0; + //==============// @@ -159,13 +158,27 @@ public class WorldGenerationQueue implements IWorldGenerationQueue, IDebugRender return future; } + @Override + public void removeRetrievalRequestIf(Function removeIf) + { + this.waitingTasks.forEachKey(100, (genPos) -> + { + if (removeIf.apply(genPos)) + { + this.waitingTasks.remove(genPos); + } + }); + } + + //===============// // running tasks // //===============// - public void startGenerationQueueAndSetTargetPos(DhBlockPos2D targetPos) + @Override + public void startAndSetTargetPos(DhBlockPos2D targetPos) { // update the target pos this.generationTargetPos = targetPos; @@ -269,7 +282,7 @@ public class WorldGenerationQueue implements IWorldGenerationQueue, IDebugRender boolean taskStarted = this.tryStartingWorldGenTaskGroup(newTaskGroup); if (!taskStarted) { - LOGGER.trace("Unable to start task: "+closestTask.pos+", skipping. Task position may have already been generated."); + //LOGGER.trace("Unable to start task: "+closestTask.pos+", skipping. Task position may have already been generated."); } } else @@ -279,7 +292,7 @@ public class WorldGenerationQueue implements IWorldGenerationQueue, IDebugRender // the newly selected task, we cannot use it, // as some chunks may have already been written into. - LOGGER.trace("A task already exists for this position, todo: "+closestTask.pos); + //LOGGER.trace("A task already exists for this position, todo: "+closestTask.pos); } // a task has been started @@ -326,7 +339,7 @@ public class WorldGenerationQueue implements IWorldGenerationQueue, IDebugRender if (this.alreadyGeneratedPosHashSet.containsKey(newTaskGroup.group.pos)) { // temporary solution to prevent generating the same section multiple times - LOGGER.trace("Duplicate generation section " + taskPos + " with granularity [" + granularity + "] at " + chunkPosMin + ". Skipping..."); + //LOGGER.trace("Duplicate generation section " + taskPos + " with granularity [" + granularity + "] at " + chunkPosMin + ". Skipping..."); // sending a success result is necessary to make sure the render sections are reloaded correctly newTaskGroup.group.worldGenTasks.forEach(worldGenTask -> worldGenTask.future.complete(WorldGenResult.CreateSuccess(new DhSectionPos(granularity, taskPos.getX(), taskPos.getZ())))); @@ -407,7 +420,7 @@ public class WorldGenerationQueue implements IWorldGenerationQueue, IDebugRender DhChunkPos chunkPosMin, byte granularity, byte targetDataDetail, - Consumer chunkDataConsumer + Consumer chunkDataConsumer ) { EDhApiDistantGeneratorMode generatorMode = Config.Client.Advanced.WorldGenerator.distantGeneratorMode.get(); @@ -422,15 +435,15 @@ public class WorldGenerationQueue implements IWorldGenerationQueue, IDebugRender granularity, targetDataDetail, generatorMode, - ThreadPools.getWorldGenExecutor(), + ThreadPoolUtil.getWorldGenExecutor(), (Object[] generatedObjectArray) -> { try { IChunkWrapper chunk = WRAPPER_FACTORY.createChunkWrapper(generatedObjectArray); - ChunkSizedFullDataAccessor chunkDataAccessor = LodDataBuilder.createChunkData(chunk); - LodUtil.assertTrue(chunkDataAccessor != null); - chunkDataConsumer.accept(chunkDataAccessor); + FullDataSourceV2 dataSource = LodDataBuilder.createGeneratedDataSource(chunk); + LodUtil.assertTrue(dataSource != null); + chunkDataConsumer.accept(dataSource); } catch (ClassCastException e) { @@ -448,13 +461,13 @@ public class WorldGenerationQueue implements IWorldGenerationQueue, IDebugRender granularity, targetDataDetail, generatorMode, - ThreadPools.getWorldGenExecutor(), + ThreadPoolUtil.getWorldGenExecutor(), (DhApiChunk dataPoints) -> { try { - ChunkSizedFullDataAccessor chunkDataAccessor = LodDataBuilder.createApiChunkData(dataPoints); - chunkDataConsumer.accept(chunkDataAccessor); + FullDataSourceV2 dataSource = LodDataBuilder.createFromApiChunkData(dataPoints); + chunkDataConsumer.accept(dataSource); } catch (ClassCastException e) { @@ -474,13 +487,24 @@ public class WorldGenerationQueue implements IWorldGenerationQueue, IDebugRender - //=========// - // getters // - //=========// + //===================// + // getters / setters // + //===================// public int getWaitingTaskCount() { return this.waitingTasks.size(); } public int getInProgressTaskCount() { return this.inProgressGenTasksByLodPos.size(); } + @Override + public byte lowestDataDetail() { return this.lowestDataDetail; } + @Override + public byte highestDataDetail() { return this.highestDataDetail; } + + @Override + public int getEstimatedTotalTaskCount() { return this.estimatedTotalTaskCount; } + @Override + public void setEstimatedTotalTaskCount(int newEstimate) { this.estimatedTotalTaskCount = newEstimate; } + + //==========// // shutdown // @@ -542,13 +566,11 @@ public class WorldGenerationQueue implements IWorldGenerationQueue, IDebugRender LodUtil.assertTrue(this.generatorClosingFuture != null); - - LOGGER.info("Awaiting world generator thread pool termination..."); try { int waitTimeInSeconds = 3; - ThreadPoolExecutor executor = ThreadPools.getWorldGenExecutor(); + ThreadPoolExecutor executor = ThreadPoolUtil.getWorldGenExecutor(); if (executor != null && !executor.awaitTermination(waitTimeInSeconds, TimeUnit.SECONDS)) { LOGGER.warn("World generator thread pool shutdown didn't complete after [" + waitTimeInSeconds + "] seconds. Some world generator requests may still be running."); @@ -560,9 +582,8 @@ public class WorldGenerationQueue implements IWorldGenerationQueue, IDebugRender } - this.generator.close(); - + DebugRenderer.unregister(this, Config.Client.Advanced.Debugging.DebugWireframe.showWorldGenQueue); try @@ -573,8 +594,9 @@ public class WorldGenerationQueue implements IWorldGenerationQueue, IDebugRender { LOGGER.warn("Failed to close generation queue: ", e); } + + LOGGER.info("Finished closing " + WorldGenerationQueue.class.getSimpleName()); - DebugRenderer.unregister(this, Config.Client.Advanced.Debugging.DebugWireframe.showWorldGenQueue); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/tasks/IWorldGenTaskTracker.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/tasks/IWorldGenTaskTracker.java index d30653121..d0eecf2ea 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/tasks/IWorldGenTaskTracker.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/tasks/IWorldGenTaskTracker.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.core.generation.tasks; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import java.util.function.Consumer; @@ -32,6 +32,6 @@ public interface IWorldGenTaskTracker /** Returns true if the task hasn't been garbage collected. */ boolean isMemoryAddressValid(); - Consumer getChunkDataConsumer(); + Consumer getChunkDataConsumer(); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/tasks/WorldGenTaskGroup.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/tasks/WorldGenTaskGroup.java index fdc188d55..ea7b5b91e 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/tasks/WorldGenTaskGroup.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/tasks/WorldGenTaskGroup.java @@ -19,8 +19,7 @@ package com.seibel.distanthorizons.core.generation.tasks; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; -import com.seibel.distanthorizons.core.pos.DhLodPos; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.pos.DhSectionPos; import java.util.Iterator; @@ -31,6 +30,7 @@ import java.util.function.Consumer; * @author Leetom * @version 2022-11-25 */ +@Deprecated // TODO look into how these are used and if they should continue to be used public final class WorldGenTaskGroup { public final DhSectionPos pos; @@ -46,13 +46,13 @@ public final class WorldGenTaskGroup this.dataDetail = dataDetail; } - public void consumeChunkData(ChunkSizedFullDataAccessor chunkSizedFullDataView) + public void consumeChunkData(FullDataSourceV2 chunkSizedFullDataView) { Iterator tasks = this.worldGenTasks.iterator(); while (tasks.hasNext()) { WorldGenTask task = tasks.next(); - Consumer chunkDataConsumer = task.taskTracker.getChunkDataConsumer(); + Consumer chunkDataConsumer = task.taskTracker.getChunkDataConsumer(); if (chunkDataConsumer == null) { tasks.remove(); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/jar/updater/SelfUpdater.java b/core/src/main/java/com/seibel/distanthorizons/core/jar/updater/SelfUpdater.java index 1bd11960a..16df88e2b 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/jar/updater/SelfUpdater.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/jar/updater/SelfUpdater.java @@ -33,6 +33,7 @@ import com.seibel.distanthorizons.coreapi.util.jar.DeleteOnUnlock; import org.apache.logging.log4j.Logger; import javax.swing.*; +import java.awt.*; import java.io.*; import java.net.URLEncoder; import java.nio.file.Files; @@ -222,9 +223,17 @@ public class SelfUpdater deleteOldJarOnJvmShutdown = true; LOGGER.info(ModInfo.READABLE_NAME + " successfully updated. It will apply on game's relaunch"); - new Thread(() -> { - System.setProperty("java.awt.headless", "false"); // Required to make it work - JOptionPane.showMessageDialog(null, ModInfo.READABLE_NAME + " updated, this will be applied on game restart.", ModInfo.READABLE_NAME, JOptionPane.INFORMATION_MESSAGE); + new Thread(() -> + { + String message = ModInfo.READABLE_NAME + " updated, this will be applied on game restart."; + if (!GraphicsEnvironment.isHeadless()) + { + JOptionPane.showMessageDialog(null, message, ModInfo.READABLE_NAME, JOptionPane.INFORMATION_MESSAGE); + } + else + { + LOGGER.info(message); + } }).start(); return true; } @@ -270,9 +279,17 @@ public class SelfUpdater deleteOldJarOnJvmShutdown = true; LOGGER.info(ModInfo.READABLE_NAME + " successfully updated. It will apply on game's relaunch"); - new Thread(() -> { - System.setProperty("java.awt.headless", "false"); // Required to make it work - JOptionPane.showMessageDialog(null, ModInfo.READABLE_NAME + " updated, this will be applied on game restart.", ModInfo.READABLE_NAME, JOptionPane.INFORMATION_MESSAGE); + new Thread(() -> + { + String message = ModInfo.READABLE_NAME + " updated, this will be applied on game restart."; + if (!GraphicsEnvironment.isHeadless()) + { + JOptionPane.showMessageDialog(null, message, ModInfo.READABLE_NAME, JOptionPane.INFORMATION_MESSAGE); + } + else + { + LOGGER.info(message); + } }).start(); zis.close(); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhLevel.java index 72e3324c7..ccaba1934 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhLevel.java @@ -20,17 +20,25 @@ package com.seibel.distanthorizons.core.level; import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiChunkModifiedEvent; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; +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.pos.DhChunkPos; +import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; -import java.util.concurrent.CompletableFuture; +import java.util.HashSet; +import java.util.concurrent.ConcurrentHashMap; public abstract class AbstractDhLevel implements IDhLevel { public final ChunkToLodBuilder chunkToLodBuilder; + 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} */ + protected final ConcurrentHashMap> updatedChunkPosSetBySectionPos = new ConcurrentHashMap<>(); + //=============// @@ -46,27 +54,51 @@ public abstract class AbstractDhLevel implements IDhLevel //=================// @Override - public void updateChunkAsync(IChunkWrapper chunk) + public int getUnsavedDataSourceCount() { return this.delayedFullDataSourceSaveCache.getUnsavedCount(); } + + @Override + public void updateChunkAsync(IChunkWrapper chunkWrapper) { - CompletableFuture future = this.chunkToLodBuilder.tryGenerateData(chunk); - if (future != null) + FullDataSourceV2 dataSource = FullDataSourceV2.createFromChunk(chunkWrapper); + if (dataSource == null) { - future.thenAccept((chunkSizedFullDataAccessor) -> - { - if (chunkSizedFullDataAccessor == null) - { - // This can happen if, among other reasons, a chunk save is superceded by a later event - return; - } - - this.updateDataSourcesWithChunkData(chunkSizedFullDataAccessor); - ApiEventInjector.INSTANCE.fireAllEvents( - DhApiChunkModifiedEvent.class, - new DhApiChunkModifiedEvent.EventParam(this.getLevelWrapper(), chunk.getChunkPos().x, chunk.getChunkPos().z)); - }); + // This can happen if, among other reasons, a chunk save is superseded by a later event + return; } + + + this.updatedChunkPosSetBySectionPos.compute(dataSource.getPos(), (dataSourcePos, chunkPosSet) -> + { + if (chunkPosSet == null) + { + chunkPosSet = new HashSet<>(); + } + chunkPosSet.add(chunkWrapper.getChunkPos()); + return chunkPosSet; + }); + + // batch updates to reduce overhead when flying around or breaking/placing a lot of blocks in an area + this.delayedFullDataSourceSaveCache.queueDataSourceForUpdateAndSave(dataSource); } + private void onDataSourceSave(FullDataSourceV2 fullDataSource) + { + this.updateDataSourcesAsync(fullDataSource).thenRun(() -> + { + HashSet updatedChunkPosSet = this.updatedChunkPosSetBySectionPos.remove(fullDataSource.getPos()); + if (updatedChunkPosSet != null) + { + for (DhChunkPos chunkPos : updatedChunkPosSet) + { + ApiEventInjector.INSTANCE.fireAllEvents( + DhApiChunkModifiedEvent.class, + new DhApiChunkModifiedEvent.EventParam(this.getLevelWrapper(), chunkPos.x, chunkPos.z)); + } + } + }); + } + + @Override public void close() { this.chunkToLodBuilder.close(); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/ClientLevelModule.java b/core/src/main/java/com/seibel/distanthorizons/core/level/ClientLevelModule.java index e83aa706f..c24341d62 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/ClientLevelModule.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/ClientLevelModule.java @@ -19,14 +19,13 @@ package com.seibel.distanthorizons.core.level; -import com.seibel.distanthorizons.api.enums.rendering.EDebugRendering; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; import com.seibel.distanthorizons.core.config.Config; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; -import com.seibel.distanthorizons.core.file.fullDatafile.IFullDataSourceProvider; -import com.seibel.distanthorizons.core.file.renderfile.RenderSourceFileHandler; -import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; +import com.seibel.distanthorizons.core.file.AbstractDataSourceHandler; +import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.f3.F3Screen; import com.seibel.distanthorizons.core.pos.DhBlockPos2D; @@ -35,33 +34,54 @@ import com.seibel.distanthorizons.core.render.LodQuadTree; import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.renderer.LodRenderer; import com.seibel.distanthorizons.core.util.LodUtil; +import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; -import com.seibel.distanthorizons.coreapi.util.math.Mat4f; import org.apache.logging.log4j.Logger; +import javax.annotation.WillNotClose; import java.io.Closeable; +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.atomic.AtomicReference; -public class ClientLevelModule implements Closeable +public class ClientLevelModule implements Closeable, AbstractDataSourceHandler.IDataSourceUpdateFunc { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); - private final IDhClientLevel parentClientLevel; + + private final IDhClientLevel clientLevel; + + @WillNotClose + public final FullDataSourceProviderV2 fullDataSourceProvider; public final AtomicReference ClientRenderStateRef = new AtomicReference<>(); + public final F3Screen.NestedMessage f3Message; - public ClientLevelModule(IDhClientLevel parentClientLevel) + + + + //=============// + // constructor // + //=============// + + public ClientLevelModule(IDhClientLevel clientLevel) { - this.parentClientLevel = parentClientLevel; + this.clientLevel = clientLevel; this.f3Message = new F3Screen.NestedMessage(this::f3Log); + + this.fullDataSourceProvider = this.clientLevel.getFullDataProvider(); + this.fullDataSourceProvider.dateSourceUpdateListeners.add(this); } + + //==============// // tick methods // //==============// - private EDebugRendering lastDebugRendering = EDebugRendering.OFF; + private EDhApiDebugRendering lastDebugRendering = EDhApiDebugRendering.OFF; public void clientTick() { @@ -85,14 +105,14 @@ public class ClientLevelModule implements Closeable return; } - IClientLevelWrapper clientLevelWrapper = this.parentClientLevel.getClientLevelWrapper(); + IClientLevelWrapper clientLevelWrapper = this.clientLevel.getClientLevelWrapper(); if (clientLevelWrapper == null) { return; } clientRenderState.close(); - clientRenderState = new ClientRenderState(this.parentClientLevel, clientLevelWrapper, this.parentClientLevel.getFileHandler(), this.parentClientLevel.getSaveStructure()); + clientRenderState = new ClientRenderState(this.clientLevel, clientLevelWrapper, this.clientLevel.getFullDataProvider()); if (!this.ClientRenderStateRef.compareAndSet(null, clientRenderState)) { //FIXME: How to handle this? @@ -104,7 +124,7 @@ public class ClientLevelModule implements Closeable clientRenderState.quadtree.tick(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos())); boolean isBuffersDirty = false; - EDebugRendering newDebugRendering = Config.Client.Advanced.Debugging.debugRendering.get(); + EDhApiDebugRendering newDebugRendering = Config.Client.Advanced.Debugging.debugRendering.get(); if (newDebugRendering != lastDebugRendering) { lastDebugRendering = newDebugRendering; @@ -124,7 +144,8 @@ public class ClientLevelModule implements Closeable /** @return if the {@link ClientRenderState} was successfully swapped */ public boolean startRenderer(IClientLevelWrapper clientLevelWrapper) { - ClientRenderState ClientRenderState = new ClientRenderState(parentClientLevel, clientLevelWrapper, parentClientLevel.getFileHandler(), parentClientLevel.getSaveStructure()); + // TODO why are we passing in a level wrapper? Our client level is already defined. + ClientRenderState ClientRenderState = new ClientRenderState(this.clientLevel, clientLevelWrapper, this.clientLevel.getFullDataProvider()); if (!this.ClientRenderStateRef.compareAndSet(null, ClientRenderState)) { LOGGER.warn("Failed to start renderer due to concurrency"); @@ -185,22 +206,21 @@ public class ClientLevelModule implements Closeable ClientRenderState.close(); } + + //===============// // data handling // //===============// - public void updateDataSourcesWithChunkData(ChunkSizedFullDataAccessor data) + + public CompletableFuture updateDataSourcesAsync(FullDataSourceV2 data) { return this.clientLevel.getFullDataProvider().updateDataSourceAsync(data); } + @Override + public void OnDataSourceUpdated(FullDataSourceV2 updatedFullDataSource) { + // if rendering, also update the render sources ClientRenderState ClientRenderState = this.ClientRenderStateRef.get(); if (ClientRenderState != null) { - ClientRenderState.renderSourceFileHandler - .updateDataSourcesWithChunkDataAsync(data) - // wait for the update to finish before triggering a reload to prevent holes in the world - .thenRun(() -> ClientRenderState.quadtree.reloadPos(data.sectionPos)); - } - else - { - this.parentClientLevel.getFileHandler().updateDataSourcesWithChunkDataAsync(data); + ClientRenderState.quadtree.reloadPos(updatedFullDataSource.getPos()); } } @@ -226,34 +246,62 @@ public class ClientLevelModule implements Closeable } } + this.fullDataSourceProvider.dateSourceUpdateListeners.remove(this); + this.f3Message.close(); } - //=======================// // misc helper functions // //=======================// - public void dumpRamUsage() + private String[] f3Log() { - //TODO - } - - /** Returns what should be displayed in Minecraft's F3 debug menu */ - protected String[] f3Log() - { - String dimName = parentClientLevel.getClientLevelWrapper().getDimensionType().getDimensionName(); - ClientRenderState renderState = this.ClientRenderStateRef.get(); - if (renderState == null) + String dimName = this.clientLevel.getLevelWrapper().getDimensionType().getDimensionName(); + boolean rendererActive = this.ClientRenderStateRef.get() != null; + + ThreadPoolExecutor fileExecutor = ThreadPoolUtil.getFileHandlerExecutor(); + String fileQueueSize = (fileExecutor != null) ? fileExecutor.getQueue().size()+"" : "-"; + String fileCompletedTaskSize = (fileExecutor != null) ? fileExecutor.getCompletedTaskCount()+"" : "-"; + + ThreadPoolExecutor updateExecutor = ThreadPoolUtil.getUpdatePropagatorExecutor(); + String updateQueueSize = (updateExecutor != null) ? updateExecutor.getQueue().size()+"" : "-"; + String updateCompletedTaskSize = (updateExecutor != null) ? updateExecutor.getCompletedTaskCount()+"" : "-"; + + int unsavedDataSourceCount = this.fullDataSourceProvider.getUnsavedDataSourceCount(); + long legacyDeletionCount = this.fullDataSourceProvider.getLegacyDeletionCount(); + long migrationCount = this.fullDataSourceProvider.getTotalMigrationCount(); + + + + ArrayList lines = new ArrayList<>(); + lines.add(""); + lines.add("level [" + dimName + "] rendering: " + (rendererActive ? "Active" : "Inactive")); + // TODO a lot of these items only need to be rendered once, but we don't currently have a way of doing that, so only add them for the rendered level + if (rendererActive) { - return new String[]{"level @ " + dimName + ": Inactive"}; - } - else - { - return new String[]{"level @ " + dimName + ": Active"}; + lines.add("File Handler [" + dimName + "]"); + lines.add(" File thread pool tasks: " + fileQueueSize + " (completed: " + fileCompletedTaskSize + ")"); + if (legacyDeletionCount > 0) + { + lines.add(" Legacy Deletion #: " + legacyDeletionCount); + } + if (migrationCount > 0) + { + lines.add(" Legacy Migration #: " + migrationCount); + } + lines.add(" Update thread pool tasks: " + updateQueueSize + " (completed: " + updateCompletedTaskSize + ")"); + lines.add(" Level Unsaved #: " + this.clientLevel.getUnsavedDataSourceCount()); + if (unsavedDataSourceCount != -1) + { + lines.add(" File Handler Unsaved #: " + unsavedDataSourceCount); + } + lines.add(" Parent Update #: " + this.fullDataSourceProvider.parentUpdatingPosSet.size()); } + + return lines.toArray(new String[0]); } public void clearRenderCache() @@ -274,26 +322,28 @@ public class ClientLevelModule implements Closeable } } + + + //================// + // helper classes // + //================// + public static class ClientRenderState { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); public final IClientLevelWrapper clientLevelWrapper; public final LodQuadTree quadtree; - public final RenderSourceFileHandler renderSourceFileHandler; public final LodRenderer renderer; - public ClientRenderState( - IDhClientLevel dhClientLevel, IClientLevelWrapper clientLevelWrapper, IFullDataSourceProvider fullDataSourceProvider, - AbstractSaveStructure saveStructure) + public ClientRenderState(IDhClientLevel dhClientLevel, IClientLevelWrapper clientLevelWrapper, FullDataSourceProviderV2 fullDataSourceProvider) { this.clientLevelWrapper = clientLevelWrapper; - this.renderSourceFileHandler = new RenderSourceFileHandler(fullDataSourceProvider, dhClientLevel, saveStructure); this.quadtree = new LodQuadTree(dhClientLevel, Config.Client.Advanced.Graphics.Quality.lodChunkRenderDistanceRadius.get() * LodUtil.CHUNK_WIDTH * 2, // initial position is (0,0) just in case the player hasn't loaded in yet, the tree will be moved once the level starts ticking 0, 0, - this.renderSourceFileHandler); + fullDataSourceProvider); RenderBufferHandler renderBufferHandler = new RenderBufferHandler(this.quadtree); this.renderer = new LodRenderer(renderBufferHandler); @@ -307,7 +357,6 @@ public class ClientLevelModule implements Closeable this.renderer.close(); this.quadtree.close(); - this.renderSourceFileHandler.close(); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java index 1c69e5c83..184bf20af 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java @@ -20,9 +20,9 @@ package com.seibel.distanthorizons.core.level; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; -import com.seibel.distanthorizons.core.file.fullDatafile.IFullDataSourceProvider; -import com.seibel.distanthorizons.core.file.fullDatafile.RemoteFullDataFileHandler; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; +import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; +import com.seibel.distanthorizons.core.file.fullDatafile.RemoteFullDataSourceProvider; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.pos.DhBlockPos; @@ -35,6 +35,7 @@ import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; import java.io.File; +import java.util.concurrent.CompletableFuture; /** The level used when connected to a server */ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel @@ -44,7 +45,7 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel public final ClientLevelModule clientside; public final IClientLevelWrapper levelWrapper; public final AbstractSaveStructure saveStructure; - public final RemoteFullDataFileHandler dataFileHandler; + public final RemoteFullDataSourceProvider dataFileHandler; @@ -57,7 +58,7 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel { this.levelWrapper = clientLevelWrapper; this.saveStructure = saveStructure; - this.dataFileHandler = new RemoteFullDataFileHandler(this, saveStructure, fullDataSaveDirOverride); + this.dataFileHandler = new RemoteFullDataSourceProvider(this, saveStructure, fullDataSaveDirOverride); this.clientside = new ClientLevelModule(this); if (enableRendering) @@ -102,50 +103,37 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel //================// @Override - public int computeBaseColor(DhBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper block) { return levelWrapper.computeBaseColor(pos, biome, block); } + public int computeBaseColor(DhBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper block) { return this.levelWrapper.computeBaseColor(pos, biome, block); } @Override - public IClientLevelWrapper getClientLevelWrapper() { return levelWrapper; } + public IClientLevelWrapper getClientLevelWrapper() { return this.levelWrapper; } @Override - public void clearRenderCache() - { - clientside.clearRenderCache(); - } + public void clearRenderCache() { this.clientside.clearRenderCache(); } @Override - public ILevelWrapper getLevelWrapper() { return levelWrapper; } + public ILevelWrapper getLevelWrapper() { return this.levelWrapper; } @Override - public void updateDataSourcesWithChunkData(ChunkSizedFullDataAccessor data) { this.clientside.updateDataSourcesWithChunkData(data); } + public CompletableFuture updateDataSourcesAsync(FullDataSourceV2 data) { return this.clientside.updateDataSourcesAsync(data); } @Override - public int getMinY() { return levelWrapper.getMinHeight(); } + public int getMinY() { return this.levelWrapper.getMinHeight(); } @Override public void close() { - clientside.close(); + this.clientside.close(); super.close(); - dataFileHandler.close(); + this.dataFileHandler.close(); LOGGER.info("Closed " + DhClientLevel.class.getSimpleName() + " for " + levelWrapper); } - //=======================// - // misc helper functions // - //=======================// + @Override + public FullDataSourceProviderV2 getFullDataProvider() { return this.dataFileHandler; } @Override - public IFullDataSourceProvider getFileHandler() - { - return dataFileHandler; - } - - @Override - public AbstractSaveStructure getSaveStructure() - { - return saveStructure; - } + public AbstractSaveStructure getSaveStructure() { return this.saveStructure; } @Override public boolean hasSkyLight() { return this.levelWrapper.hasSkyLight(); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientServerLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientServerLevel.java index be70e9f95..0527f61c9 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientServerLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientServerLevel.java @@ -20,17 +20,15 @@ package com.seibel.distanthorizons.core.level; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; -import com.seibel.distanthorizons.core.file.fullDatafile.IFullDataSourceProvider; -import com.seibel.distanthorizons.core.render.LodRenderSection; +import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.pos.DhBlockPos; import com.seibel.distanthorizons.core.pos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.DhSectionPos; -import com.seibel.distanthorizons.core.util.objects.quadTree.QuadNode; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; @@ -41,7 +39,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapp import org.apache.logging.log4j.Logger; import java.awt.*; -import java.util.Iterator; +import java.util.concurrent.CompletableFuture; /** The level used on a singleplayer world */ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLevel, IDhServerLevel @@ -102,32 +100,27 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev // start world gen // create a new queue - this.serverside.worldGenModule.startWorldGen(this.serverside.dataFileHandler, new ServerLevelModule.WorldGenState(this)); + this.serverside.worldGenModule.startWorldGen(this.serverside.fullDataFileHandler, new ServerLevelModule.WorldGenState(this)); + // TODO I think this used to queue the world gen + // is it still needed? // populate the queue based on the current rendering tree - ClientLevelModule.ClientRenderState renderState = this.clientside.ClientRenderStateRef.get(); - Iterator> iterator = renderState.quadtree.leafNodeIterator(); - while (iterator.hasNext()) - { - QuadNode node = iterator.next(); - this.serverside.dataFileHandler.getAsync(node.sectionPos); - } + //ClientLevelModule.ClientRenderState renderState = this.clientside.ClientRenderStateRef.get(); + //Iterator> iterator = renderState.quadtree.leafNodeIterator(); + //while (iterator.hasNext()) + //{ + // QuadNode node = iterator.next(); + // //this.serverside.dataFileHandler.getAsync(node.sectionPos); + //} } else if (!shouldDoWorldGen && isWorldGenRunning) { // stop world gen - this.serverside.worldGenModule.stopWorldGen(this.serverside.dataFileHandler); + this.serverside.worldGenModule.stopWorldGen(this.serverside.fullDataFileHandler); } if (isWorldGenRunning) { - ClientLevelModule.ClientRenderState renderState = this.clientside.ClientRenderStateRef.get(); - if (renderState != null && renderState.quadtree != null) - { - // remove any generator sections that are out of bounds - this.serverside.dataFileHandler.removeGenRequestIf(pos -> !renderState.quadtree.isSectionPosInBounds(pos)); - } - this.serverside.worldGenModule.worldGenTick(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos())); } } @@ -173,10 +166,7 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev public ILevelWrapper getLevelWrapper() { return getServerLevelWrapper(); } @Override - public IFullDataSourceProvider getFileHandler() - { - return serverside.dataFileHandler; - } + public FullDataSourceProviderV2 getFullDataProvider() { return this.serverside.fullDataFileHandler; } @Override public AbstractSaveStructure getSaveStructure() @@ -188,7 +178,7 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev public boolean hasSkyLight() { return this.serverLevelWrapper.hasSkyLight(); } @Override - public void updateDataSourcesWithChunkData(ChunkSizedFullDataAccessor data) { this.clientside.updateDataSourcesWithChunkData(data); } + public CompletableFuture updateDataSourcesAsync(FullDataSourceV2 data) { return this.clientside.updateDataSourcesAsync(data); } @Override public int getMinY() { return getLevelWrapper().getMinHeight(); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/DhServerLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/DhServerLevel.java index 9b12a3627..8b591258a 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/DhServerLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/DhServerLevel.java @@ -19,9 +19,8 @@ package com.seibel.distanthorizons.core.level; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.CompleteFullDataSource; -import com.seibel.distanthorizons.core.file.fullDatafile.IFullDataSourceProvider; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; +import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.pos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.DhSectionPos; @@ -30,6 +29,8 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import org.apache.logging.log4j.Logger; +import java.util.concurrent.CompletableFuture; + public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); @@ -50,12 +51,7 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel public void serverTick() { this.chunkToLodBuilder.tick(); } @Override - public void updateDataSourcesWithChunkData(ChunkSizedFullDataAccessor data) - { - DhSectionPos pos = data.getSectionPos(); - pos = pos.convertNewToDetailLevel(CompleteFullDataSource.SECTION_SIZE_OFFSET); - this.getFileHandler().updateDataSourcesWithChunkDataAsync(data); - } + public CompletableFuture updateDataSourcesAsync(FullDataSourceV2 data) { return this.getFullDataProvider().updateDataSourceAsync(data); } @Override public int getMinY() { return getLevelWrapper().getMinHeight(); } @@ -76,12 +72,12 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel if (shouldDoWorldGen && !isWorldGenRunning) { // start world gen - serverside.worldGenModule.startWorldGen(serverside.dataFileHandler, new ServerLevelModule.WorldGenState(this)); + serverside.worldGenModule.startWorldGen(serverside.fullDataFileHandler, new ServerLevelModule.WorldGenState(this)); } else if (!shouldDoWorldGen && isWorldGenRunning) { // stop world gen - serverside.worldGenModule.stopWorldGen(serverside.dataFileHandler); + serverside.worldGenModule.stopWorldGen(serverside.fullDataFileHandler); } if (serverside.worldGenModule.isWorldGenRunning()) @@ -97,7 +93,7 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel public ILevelWrapper getLevelWrapper() { return getServerLevelWrapper(); } @Override - public IFullDataSourceProvider getFileHandler() { return serverside.dataFileHandler; } + public FullDataSourceProviderV2 getFullDataProvider() { return this.serverside.fullDataFileHandler; } @Override public AbstractSaveStructure getSaveStructure() diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/IDhLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/IDhLevel.java index f614dad86..0d717bf08 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/IDhLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/IDhLevel.java @@ -19,8 +19,8 @@ package com.seibel.distanthorizons.core.level; -import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; -import com.seibel.distanthorizons.core.file.fullDatafile.IFullDataSourceProvider; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; +import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; @@ -39,12 +39,18 @@ public interface IDhLevel extends AutoCloseable void updateChunkAsync(IChunkWrapper chunk); - IFullDataSourceProvider getFileHandler(); + FullDataSourceProviderV2 getFullDataProvider(); AbstractSaveStructure getSaveStructure(); boolean hasSkyLight(); - void updateDataSourcesWithChunkData(ChunkSizedFullDataAccessor data); + CompletableFuture updateDataSourcesAsync(FullDataSourceV2 data); + + /** + * this number is generally related to how many data sources have been updated + * due to chunk modifications or loads. + */ + int getUnsavedDataSourceCount(); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/IDhWorldGenLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/IDhWorldGenLevel.java index 94108b380..287d6ad62 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/IDhWorldGenLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/IDhWorldGenLevel.java @@ -19,9 +19,9 @@ package com.seibel.distanthorizons.core.level; -import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataFileHandler; +import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider; -public interface IDhWorldGenLevel extends IDhLevel, GeneratedFullDataFileHandler.IOnWorldGenCompleteListener +public interface IDhWorldGenLevel extends IDhLevel, GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener { void doWorldGen(); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/ServerLevelModule.java b/core/src/main/java/com/seibel/distanthorizons/core/level/ServerLevelModule.java index e73deca38..57a7fedf4 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/ServerLevelModule.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/ServerLevelModule.java @@ -22,22 +22,21 @@ package com.seibel.distanthorizons.core.level; import com.seibel.distanthorizons.api.interfaces.override.worldGenerator.IDhApiWorldGenerator; import com.seibel.distanthorizons.core.config.AppliedConfigState; import com.seibel.distanthorizons.core.config.Config; -import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataFileHandler; +import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.generation.BatchGenerator; import com.seibel.distanthorizons.core.generation.WorldGenerationQueue; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; -import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.coreapi.DependencyInjection.WorldGeneratorInjector; import org.apache.logging.log4j.Logger; -public class ServerLevelModule +public class ServerLevelModule implements AutoCloseable { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); public final IDhServerLevel parentServerLevel; public final AbstractSaveStructure saveStructure; - public final GeneratedFullDataFileHandler dataFileHandler; + public final GeneratedFullDataSourceProvider fullDataFileHandler; public final AppliedConfigState worldGeneratorEnabledConfig; public final WorldGenModule worldGenModule; @@ -48,18 +47,19 @@ public class ServerLevelModule { this.parentServerLevel = parentServerLevel; this.saveStructure = saveStructure; - this.dataFileHandler = new GeneratedFullDataFileHandler(parentServerLevel, saveStructure); + this.fullDataFileHandler = new GeneratedFullDataSourceProvider(parentServerLevel, saveStructure); this.worldGeneratorEnabledConfig = new AppliedConfigState<>(Config.Client.Advanced.WorldGenerator.enableDistantGeneration); - this.worldGenModule = new WorldGenModule(this.dataFileHandler, this.parentServerLevel); + this.worldGenModule = new WorldGenModule(this.parentServerLevel); } + @Override public void close() { // shutdown the world-gen this.worldGenModule.close(); - this.dataFileHandler.close(); + this.fullDataFileHandler.close(); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java b/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java index 5b274857f..07c9dd3b7 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java @@ -19,8 +19,8 @@ package com.seibel.distanthorizons.core.level; -import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataFileHandler; -import com.seibel.distanthorizons.core.generation.IWorldGenerationQueue; +import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider; +import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.f3.F3Screen; import com.seibel.distanthorizons.core.pos.DhBlockPos2D; @@ -34,17 +34,15 @@ public class WorldGenModule implements Closeable { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - private final GeneratedFullDataFileHandler dataFileHandler; - private final GeneratedFullDataFileHandler.IOnWorldGenCompleteListener onWorldGenCompleteListener; + private final GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener onWorldGenCompleteListener; private final AtomicReference worldGenStateRef = new AtomicReference<>(); private final F3Screen.DynamicMessage worldGenF3Message; - public WorldGenModule(GeneratedFullDataFileHandler dataFileHandler, GeneratedFullDataFileHandler.IOnWorldGenCompleteListener onWorldGenCompleteListener) + public WorldGenModule(GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener onWorldGenCompleteListener) { - this.dataFileHandler = dataFileHandler; this.onWorldGenCompleteListener = onWorldGenCompleteListener; this.worldGenF3Message = new F3Screen.DynamicMessage(() -> { @@ -53,8 +51,9 @@ public class WorldGenModule implements Closeable { int waitingCount = worldGenState.worldGenerationQueue.getWaitingTaskCount(); int inProgressCount = worldGenState.worldGenerationQueue.getInProgressTaskCount(); + int totalCountEstimate = worldGenState.worldGenerationQueue.getEstimatedTotalTaskCount(); - return "World Gen Tasks: "+waitingCount+", (in progress: "+inProgressCount+")"; + return "World Gen Tasks: "+waitingCount+"/"+totalCountEstimate+", (in progress: "+inProgressCount+")"; } else { @@ -70,7 +69,7 @@ public class WorldGenModule implements Closeable // world gen control // //===================// - public void startWorldGen(GeneratedFullDataFileHandler dataFileHandler, AbstractWorldGenState newWgs) + public void startWorldGen(GeneratedFullDataSourceProvider dataFileHandler, AbstractWorldGenState newWgs) { // create the new world generator if (!this.worldGenStateRef.compareAndSet(null, newWgs)) @@ -82,7 +81,7 @@ public class WorldGenModule implements Closeable dataFileHandler.setWorldGenerationQueue(newWgs.worldGenerationQueue); } - public void stopWorldGen(GeneratedFullDataFileHandler dataFileHandler) + public void stopWorldGen(GeneratedFullDataSourceProvider dataFileHandler) { AbstractWorldGenState worldGenState = this.worldGenStateRef.get(); if (worldGenState == null) @@ -100,7 +99,7 @@ public class WorldGenModule implements Closeable return; } } - dataFileHandler.clearGenerationQueue(); + dataFileHandler.clearRetrievalQueue(); worldGenState.closeAsync(true).join(); //TODO: Make it async. dataFileHandler.removeWorldGenCompleteListener(this.onWorldGenCompleteListener); } @@ -137,7 +136,6 @@ public class WorldGenModule implements Closeable } } - this.dataFileHandler.close(); this.worldGenF3Message.close(); } @@ -155,10 +153,10 @@ public class WorldGenModule implements Closeable // helper classes // //================// - /** Handles the {@link IWorldGenerationQueue} and any other necessary world gen information. */ + /** Handles the {@link IFullDataSourceRetrievalQueue} and any other necessary world gen information. */ public static abstract class AbstractWorldGenState { - public IWorldGenerationQueue worldGenerationQueue; + public IFullDataSourceRetrievalQueue worldGenerationQueue; CompletableFuture closeAsync(boolean doInterrupt) { @@ -177,7 +175,8 @@ public class WorldGenModule implements Closeable } /** @param targetPosForGeneration the position that world generation should be centered around */ - public void startGenerationQueueAndSetTargetPos(DhBlockPos2D targetPosForGeneration) { this.worldGenerationQueue.startGenerationQueueAndSetTargetPos(targetPosForGeneration); } + public void startGenerationQueueAndSetTargetPos(DhBlockPos2D targetPosForGeneration) + { this.worldGenerationQueue.startAndSetTargetPos(targetPosForGeneration); } } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/logging/ConfigBasedLogger.java b/core/src/main/java/com/seibel/distanthorizons/core/logging/ConfigBasedLogger.java index 9471d77af..798080895 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/logging/ConfigBasedLogger.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/logging/ConfigBasedLogger.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.core.logging; -import com.seibel.distanthorizons.api.enums.config.ELoggerMode; +import com.seibel.distanthorizons.api.enums.config.EDhApiLoggerMode; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import org.apache.logging.log4j.Level; @@ -50,11 +50,11 @@ public class ConfigBasedLogger }); } - private ELoggerMode mode; - private final Supplier getter; + private EDhApiLoggerMode mode; + private final Supplier getter; private final Logger logger; - public ConfigBasedLogger(Logger logger, Supplier configQuery) + public ConfigBasedLogger(Logger logger, Supplier configQuery) { getter = configQuery; mode = getter.get(); @@ -85,7 +85,7 @@ public class ConfigBasedLogger public boolean canMaybeLog() { - return mode != ELoggerMode.DISABLED; + return mode != EDhApiLoggerMode.DISABLED; } public void log(Level level, String str, Object... param) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/logging/ConfigBasedSpamLogger.java b/core/src/main/java/com/seibel/distanthorizons/core/logging/ConfigBasedSpamLogger.java index 4c2ba312c..82f9258dc 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/logging/ConfigBasedSpamLogger.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/logging/ConfigBasedSpamLogger.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.core.logging; -import com.seibel.distanthorizons.api.enums.config.ELoggerMode; +import com.seibel.distanthorizons.api.enums.config.EDhApiLoggerMode; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import org.apache.logging.log4j.Level; @@ -52,13 +52,13 @@ public class ConfigBasedSpamLogger }); } - private ELoggerMode mode; - private final Supplier getter; + private EDhApiLoggerMode mode; + private final Supplier getter; private final int maxLogCount; private final AtomicInteger logTries = new AtomicInteger(0); private final Logger logger; - public ConfigBasedSpamLogger(Logger logger, Supplier configQuery, int maxLogPerSec) + public ConfigBasedSpamLogger(Logger logger, Supplier configQuery, int maxLogPerSec) { getter = configQuery; mode = getter.get(); @@ -74,7 +74,7 @@ public class ConfigBasedSpamLogger public boolean canMaybeLog() { - return mode != ELoggerMode.DISABLED && logTries.get() < maxLogCount; + return mode != EDhApiLoggerMode.DISABLED && logTries.get() < maxLogCount; } public void update() diff --git a/core/src/main/java/com/seibel/distanthorizons/core/pos/DhBlockPos.java b/core/src/main/java/com/seibel/distanthorizons/core/pos/DhBlockPos.java index 7fe2bad0a..ecb4e3010 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/pos/DhBlockPos.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/pos/DhBlockPos.java @@ -45,10 +45,16 @@ public class DhBlockPos public static final int PACKED_Z_OFFSET = PACKED_Y_LENGTH; public static final int PACKED_X_OFFSET = PACKED_Y_LENGTH + PACKED_Z_LENGTH; + /** Useful for methods that need a position passed in but won't actually be used */ + public static final DhBlockPos ZERO = new DhBlockPos(0, 0, 0); + + public int x; public int y; public int z; + + public DhBlockPos(int x, int y, int z) { this.x = x; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/pos/DhLodPos.java b/core/src/main/java/com/seibel/distanthorizons/core/pos/DhLodPos.java index d6d038b74..2709fc6de 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/pos/DhLodPos.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/pos/DhLodPos.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.core.pos; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; import com.seibel.distanthorizons.core.util.LodUtil; import org.jetbrains.annotations.NotNull; @@ -31,7 +31,13 @@ import java.util.Objects; * * @author Leetom * @version 2022-11-6 + * + * @deprecated TODO replace entirely with DhSectionPos, + * we don't need to have a full fledged position object for + * positions inside a LOD, we only need this position object + * to get to/from the LOD section. */ +@Deprecated public class DhLodPos implements Comparable { public final byte detailLevel; @@ -66,7 +72,11 @@ public class DhLodPos implements Comparable // Get the width of this pos, measured in the target detail level. public int getWidthAtDetail(byte targetLevel) { - LodUtil.assertTrue(targetLevel <= this.detailLevel); + if (targetLevel > this.detailLevel) + { + LodUtil.assertNotReach("getWidthAtDetail for pos "+this+", given target detail level of bounds: ["+targetLevel+"], this: ["+this.detailLevel+"]"); + } + return BitShiftUtil.powerOfTwo(this.detailLevel - targetLevel); } @@ -116,7 +126,7 @@ public class DhLodPos implements Comparable public DhLodPos getDhSectionRelativePositionForDetailLevel() throws IllegalArgumentException { return this.getDhSectionRelativePositionForDetailLevel(this.detailLevel); } /** * Returns a DhLodPos with the given detail level and an X/Z position somewhere between (0,0) and (63,63). - * This is done to access specific sections from a {@link IFullDataSource} where LOD columns are stored + * This is done to access specific sections from a {@link FullDataSourceV2} where LOD columns are stored * in 64 x 64 blocks. * * @throws IllegalArgumentException if this position's detail level is lower than the output detail level diff --git a/core/src/main/java/com/seibel/distanthorizons/core/pos/DhSectionPos.java b/core/src/main/java/com/seibel/distanthorizons/core/pos/DhSectionPos.java index 819e2caa8..8be858116 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/pos/DhSectionPos.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/pos/DhSectionPos.java @@ -154,9 +154,17 @@ public class DhSectionPos // getters // //=========// - /** @return the corner with the smallest X and Z coordinate */ + /** + * @deprecated use DhSectionPos instead + * @return the corner with the smallest X and Z coordinate + */ + @Deprecated public DhLodPos getMinCornerLodPos() { return this.getMinCornerLodPos((byte) (this.detailLevel - 1)); } - /** @return the corner with the smallest X and Z coordinate */ + /** + * @deprecated use DhSectionPos instead + * @return the corner with the smallest X and Z coordinate + */ + @Deprecated public DhLodPos getMinCornerLodPos(byte returnDetailLevel) { LodUtil.assertTrue(returnDetailLevel <= this.detailLevel, "returnDetailLevel must be less than sectionDetail"); @@ -167,6 +175,16 @@ public class DhSectionPos this.z * BitShiftUtil.powerOfTwo(offset)); } + public DhSectionPos getMinCornerPos(byte returnDetailLevel) + { + LodUtil.assertTrue(returnDetailLevel <= this.detailLevel, "returnDetailLevel must be less than sectionDetail"); + + byte offset = (byte) (this.detailLevel - returnDetailLevel); + return new DhSectionPos(returnDetailLevel, + this.x * BitShiftUtil.powerOfTwo(offset), + this.z * BitShiftUtil.powerOfTwo(offset)); + } + /** * A detail level of X lower than this section's detail level will return:
* 0 -> 1
@@ -212,6 +230,12 @@ public class DhSectionPos return (centerBlockPos * BitShiftUtil.powerOfTwo(this.detailLevel)) + positionOffset; } + public int getManhattanBlockDistance(DhBlockPos2D blockPos) + { + return Math.abs(this.getCenterBlockPosX() - blockPos.x) + + Math.abs(this.getCenterBlockPosZ() - blockPos.z); + } + //==================// @@ -331,7 +355,7 @@ public class DhSectionPos } /** Applies the given consumer to all children of the position at the given section detail level. */ - public void forEachChildAtLevel(byte sectionDetailLevel, Consumer callback) + public void forEachChildAtDetailLevel(byte sectionDetailLevel, Consumer callback) { if (sectionDetailLevel == this.detailLevel) { @@ -341,7 +365,7 @@ public class DhSectionPos for (int i = 0; i < 4; i++) { - this.getChildByIndex(i).forEachChildAtLevel(sectionDetailLevel, callback); + this.getChildByIndex(i).forEachChildAtDetailLevel(sectionDetailLevel, callback); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/AbstractRenderBuffer.java b/core/src/main/java/com/seibel/distanthorizons/core/render/AbstractRenderBuffer.java deleted file mode 100644 index 29e303fbb..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/AbstractRenderBuffer.java +++ /dev/null @@ -1,61 +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 . - */ - -package com.seibel.distanthorizons.core.render; - -import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; -import com.seibel.distanthorizons.core.util.LodUtil; -import com.seibel.distanthorizons.core.util.objects.StatsMap; -import com.seibel.distanthorizons.core.render.renderer.LodRenderer; - -public abstract class AbstractRenderBuffer implements AutoCloseable -{ - // ====================================================================== - // ====================== Methods for implementations =================== - // ====================================================================== - - // ========== Called by render thread ========== - /** @return true if something was rendered, false otherwise */ - public abstract boolean renderOpaque(LodRenderer renderContext, DhApiRenderParam renderEventParam); - /** @return true if something was rendered, false otherwise */ - public abstract boolean renderTransparent(LodRenderer renderContext, DhApiRenderParam renderEventParam); - - // ========== Called by any thread. (thread safe) ========== - - /* Called by anyone. This method is allowed to throw exceptions, but - * are never allowed to modify any values. This should behave the same - * to other methods as if the method have never been called. - * Note: This method is PURELY for debug or stats logging ONLY! */ - public abstract void debugDumpStats(StatsMap statsMap); - - // ========= Called only when 1 thread is using it ======= - /* This method is called when object is no longer in use. - * Called either after uploadBuffers() returned false (On buffer Upload - * thread), or by others when the object is not being used. (not in build, - * upload, or render state). */ - public abstract void close(); - - - - public static final int QUADS_BYTE_SIZE = LodUtil.LOD_VERTEX_FORMAT.getByteSize() * 4; // TODO what does the 4 represent - public static final int MAX_QUADS_PER_BUFFER = (1024 * 1024 * 1) / QUADS_BYTE_SIZE; // TODO what do these multiples represent? - public static final int FULL_SIZED_BUFFER = MAX_QUADS_PER_BUFFER * QUADS_BYTE_SIZE; - - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/LodQuadTree.java b/core/src/main/java/com/seibel/distanthorizons/core/render/LodQuadTree.java index 2f9a9bda9..391d834ee 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/LodQuadTree.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/LodQuadTree.java @@ -19,23 +19,32 @@ package com.seibel.distanthorizons.core.render; -import com.seibel.distanthorizons.api.enums.config.EHorizontalQuality; +import com.seibel.distanthorizons.api.enums.config.EDhApiHorizontalQuality; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource; -import com.seibel.distanthorizons.core.file.renderfile.IRenderSourceProvider; +import com.seibel.distanthorizons.core.enums.EDhDirection; +import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.pos.DhBlockPos2D; 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.ThreadUtil; import com.seibel.distanthorizons.core.util.objects.quadTree.QuadNode; import com.seibel.distanthorizons.core.util.objects.quadTree.QuadTree; import com.seibel.distanthorizons.coreapi.util.MathUtil; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import javax.annotation.WillNotClose; +import java.awt.*; +import java.util.ArrayList; import java.util.Iterator; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; /** @@ -47,9 +56,14 @@ public class LodQuadTree extends QuadTree implements AutoClose public static final byte TREE_LOWEST_DETAIL_LEVEL = ColumnRenderSource.SECTION_SIZE_OFFSET; private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + /** there should only ever be one {@link LodQuadTree} so having the thread static should be fine */ + private static final ThreadPoolExecutor FULL_DATA_RETRIEVAL_QUEUE_THREAD = ThreadUtil.makeSingleThreadPool("QuadTree Full Data Retrieval Queue Populator"); + private static final int WORLD_GEN_QUEUE_UPDATE_DELAY_IN_MS = 1_000; + public final int blockRenderDistanceDiameter; - private final IRenderSourceProvider renderSourceProvider; + @WillNotClose + private final FullDataSourceProviderV2 fullDataSourceProvider; /** * This holds every {@link DhSectionPos} that should be reloaded next tick.
@@ -57,8 +71,9 @@ public class LodQuadTree extends QuadTree implements AutoClose */ private final ConcurrentLinkedQueue sectionsToReload = new ConcurrentLinkedQueue<>(); private final IDhClientLevel level; //FIXME: Proper hierarchy to remove this reference! - private final ConfigChangeListener horizontalScaleChangeListener; + private final ConfigChangeListener horizontalScaleChangeListener; private final ReentrantLock treeReadWriteLock = new ReentrantLock(); + private final AtomicBoolean fullDataRetrievalQueueRunning = new AtomicBoolean(false); /** the smallest numerical detail level number that can be rendered */ private byte maxRenderDetailLevel; @@ -79,12 +94,12 @@ public class LodQuadTree extends QuadTree implements AutoClose public LodQuadTree( IDhClientLevel level, int viewDiameterInBlocks, int initialPlayerBlockX, int initialPlayerBlockZ, - IRenderSourceProvider provider) + FullDataSourceProviderV2 fullDataSourceProvider) { super(viewDiameterInBlocks, new DhBlockPos2D(initialPlayerBlockX, initialPlayerBlockZ), TREE_LOWEST_DETAIL_LEVEL); this.level = level; - this.renderSourceProvider = provider; + this.fullDataSourceProvider = fullDataSourceProvider; this.blockRenderDistanceDiameter = viewDiameterInBlocks; this.horizontalScaleChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Graphics.Quality.horizontalQuality, (newHorizontalScale) -> this.onHorizontalQualityChange()); @@ -121,7 +136,7 @@ public class LodQuadTree extends QuadTree implements AutoClose try { // recenter if necessary, removing out of bounds sections - this.setCenterBlockPos(playerPos, LodRenderSection::dispose); + this.setCenterBlockPos(playerPos, LodRenderSection::close); this.updateAllRenderSections(playerPos); } @@ -148,9 +163,9 @@ public class LodQuadTree extends QuadTree implements AutoClose try { LodRenderSection renderSection = this.getValue(pos); - if (renderSection != null) + if (renderSection != null && renderSection.renderingEnabled) { - renderSection.reload(this.renderSourceProvider); + renderSection.uploadRenderDataToGpuAsync(); } } catch (IndexOutOfBoundsException e) @@ -162,6 +177,8 @@ public class LodQuadTree extends QuadTree implements AutoClose // walk through each root node + ArrayList nodesNeedingRetrieval = new ArrayList<>(); + ArrayList nodesNeedingLoading = new ArrayList<>(); Iterator rootPosIterator = this.rootNodePosIterator(); while (rootPosIterator.hasNext()) { @@ -169,18 +186,46 @@ public class LodQuadTree extends QuadTree implements AutoClose DhSectionPos rootPos = rootPosIterator.next(); if (this.getNode(rootPos) == null) { - this.setValue(rootPos, new LodRenderSection(this, rootPos)); + this.setValue(rootPos, new LodRenderSection(rootPos, this, this.level, this.fullDataSourceProvider)); } QuadNode rootNode = this.getNode(rootPos); - this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, rootNode, rootNode.sectionPos, false); + this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, rootNode, rootNode.sectionPos, false, nodesNeedingRetrieval, nodesNeedingLoading); } + + + // full data retrieval (world gen) + if (!this.fullDataRetrievalQueueRunning.get()) + { + this.fullDataRetrievalQueueRunning.set(true); + FULL_DATA_RETRIEVAL_QUEUE_THREAD.execute(() -> this.queueFullDataRetrievalTasks(playerPos, nodesNeedingRetrieval)); + } + + + nodesNeedingLoading.sort((a, b) -> + { + int aDist = a.pos.getManhattanBlockDistance(playerPos); + int bDist = b.pos.getManhattanBlockDistance(playerPos); + return Integer.compare(aDist, bDist); + }); + + for (int i = 0; i < nodesNeedingLoading.size(); i++) + { + LodRenderSection renderSection = nodesNeedingLoading.get(i); + if (!renderSection.gpuUploadInProgress() && renderSection.renderBuffer == null) + { + renderSection.uploadRenderDataToGpuAsync(); + } + } + } /** @return whether the current position is able to render (note: not if it IS rendering, just if it is ABLE to.) */ private boolean recursivelyUpdateRenderSectionNode( DhBlockPos2D playerPos, QuadNode rootNode, QuadNode quadNode, DhSectionPos sectionPos, - boolean parentRenderSectionIsEnabled) + boolean parentSectionIsRendering, + ArrayList nodesNeedingRetrieval, + ArrayList nodesNeedingLoading) { //===============================// // node and render section setup // @@ -189,7 +234,7 @@ public class LodQuadTree extends QuadTree implements AutoClose // make sure the node is created if (quadNode == null && this.isSectionPosInBounds(sectionPos)) // the position bounds should only fail when at the edge of the user's render distance { - rootNode.setValue(sectionPos, new LodRenderSection(this, sectionPos)); + rootNode.setValue(sectionPos, new LodRenderSection(sectionPos, this, this.level, this.fullDataSourceProvider)); quadNode = rootNode.getNode(sectionPos); } if (quadNode == null) @@ -203,15 +248,14 @@ public class LodQuadTree extends QuadTree implements AutoClose // create a new render section if missing if (renderSection == null) { - LodRenderSection newRenderSection = new LodRenderSection(this, sectionPos); + LodRenderSection newRenderSection = new LodRenderSection(sectionPos, this, this.level, this.fullDataSourceProvider); rootNode.setValue(sectionPos, newRenderSection); - renderSection = newRenderSection; + renderSection = newRenderSection; // TODO this never seemed to be called, is it necessary? } - //===============================// // handle enabling, loading, // // and disabling render sections // @@ -226,7 +270,7 @@ public class LodQuadTree extends QuadTree implements AutoClose if (sectionPos.getDetailLevel() > expectedDetailLevel) { // section detail level too high // - boolean canThisPosRender = renderSection.isRenderingEnabled(); + boolean thisPosIsRendering = renderSection.renderingEnabled; boolean allChildrenSectionsAreLoaded = true; // recursively update all child render sections @@ -236,20 +280,31 @@ public class LodQuadTree extends QuadTree implements AutoClose DhSectionPos childPos = childPosIterator.next(); QuadNode childNode = rootNode.getNode(childPos); - boolean childSectionLoaded = this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, childNode, childPos, canThisPosRender || parentRenderSectionIsEnabled); + boolean childSectionLoaded = this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, childNode, childPos, thisPosIsRendering || parentSectionIsRendering, nodesNeedingRetrieval, nodesNeedingLoading); allChildrenSectionsAreLoaded = childSectionLoaded && allChildrenSectionsAreLoaded; } if (!allChildrenSectionsAreLoaded) { // not all child positions are loaded yet, or this section is out of render range - return canThisPosRender; + return thisPosIsRendering; } else { + if (renderSection.renderingEnabled + && Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionStatus.get()) + { + // show that this position has just been disabled + DebugRenderer.makeParticle( + new DebugRenderer.BoxParticle( + new DebugRenderer.Box(renderSection.pos, 128f, 156f, 0.09f, Color.CYAN.darker()), + 0.2, 32f + ) + ); + } + // all child positions are loaded, disable this section and enable its children. - renderSection.disableRendering(); - renderSection.disposeRenderData(); + renderSection.renderingEnabled = false; // walk back down the tree and enable the child sections //TODO there are probably more efficient ways of doing this, but this will work for now childPosIterator = quadNode.getChildPosIterator(); @@ -258,7 +313,7 @@ public class LodQuadTree extends QuadTree implements AutoClose DhSectionPos childPos = childPosIterator.next(); QuadNode childNode = rootNode.getNode(childPos); - boolean childSectionLoaded = this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, childNode, childPos, parentRenderSectionIsEnabled); + boolean childSectionLoaded = this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, childNode, childPos, parentSectionIsRendering, nodesNeedingRetrieval, nodesNeedingLoading); allChildrenSectionsAreLoaded = childSectionLoaded && allChildrenSectionsAreLoaded; } if (!allChildrenSectionsAreLoaded) @@ -269,12 +324,15 @@ public class LodQuadTree extends QuadTree implements AutoClose } // this section is now being rendered via its children - return true; + return allChildrenSectionsAreLoaded; } } // TODO this should only equal the expected detail level, the (expectedDetailLevel-1) is a temporary fix to prevent corners from being cut out else if (sectionPos.getDetailLevel() == expectedDetailLevel || sectionPos.getDetailLevel() == expectedDetailLevel - 1) { + // this is the detail level we want to render // + + /* Can be uncommented to easily debug a single render section. */ /* Don't forget the disableRendering() at the bottom though. */ //if (sectionPos.getDetailLevel() == 10 @@ -284,36 +342,55 @@ public class LodQuadTree extends QuadTree implements AutoClose // sectionPos.getZ() == -4 // )) { - // this is the detail level we want to render // // prepare this section for rendering - renderSection.loadRenderSource(this.renderSourceProvider, this.level); // TODO this should fire for the lowest detail level first, wait for it to finish then fire the next highest to prevent waiting forever for 2 million chunk section to finish sampling everything + // TODO this should fire for the lowest detail level first to improve loading speed + if (!renderSection.gpuUploadInProgress() && renderSection.renderBuffer == null) + { + nodesNeedingLoading.add(renderSection); + } // wait for the parent to disable before enabling this section, so we don't overdraw/overlap render sections - if (!parentRenderSectionIsEnabled && renderSection.canRenderNow()) + if (!parentSectionIsRendering && renderSection.canRender()) { // if rendering is already enabled we don't have to re-enable it - if (!renderSection.isRenderingEnabled()) + if (!renderSection.renderingEnabled) { - renderSection.enableRendering(); + renderSection.renderingEnabled = true; // delete/disable children, all of them will be a lower detail level than requested quadNode.deleteAllChildren((childRenderSection) -> { if (childRenderSection != null) { - childRenderSection.disableRendering(); - childRenderSection.disposeRenderData(); + if (childRenderSection.renderingEnabled) + { + // show that this position's rendering has been disabled due to a parent rendering + DebugRenderer.makeParticle( + new DebugRenderer.BoxParticle( + new DebugRenderer.Box(childRenderSection.pos, 128f, 156f, 0.09f, Color.MAGENTA.darker()), + 0.2, 32f + ) + ); + } + + childRenderSection.renderingEnabled = false; + childRenderSection.close(); } }); } } + + if (!renderSection.isFullyGenerated()) + { + nodesNeedingRetrieval.add(renderSection); + } } //else //{ // renderSection.disableRendering(); //} - return renderSection.canRenderNow(); + return renderSection.canRender(); } else { @@ -338,13 +415,6 @@ public class LodQuadTree extends QuadTree implements AutoClose public byte calculateExpectedDetailLevel(DhBlockPos2D playerPos, DhSectionPos sectionPos) { return this.getDetailLevelFromDistance(playerPos.dist(sectionPos.getCenterBlockPosX(), sectionPos.getCenterBlockPosZ())); } private byte getDetailLevelFromDistance(double distance) { - // special case, never drop the quality - if (Config.Client.Advanced.Graphics.Quality.horizontalQuality.get() == EHorizontalQuality.UNLIMITED) - { - return this.maxRenderDetailLevel; - } - - double maxDetailDistance = this.getDrawDistanceFromDetail(Byte.MAX_VALUE - 1); if (distance > maxDetailDistance) { @@ -399,11 +469,11 @@ public class LodQuadTree extends QuadTree implements AutoClose */ public void clearRenderDataCache() { - if (this.treeReadWriteLock.tryLock()) + if (this.treeReadWriteLock.tryLock()) // TODO make async, can lock render thread { try { - LOGGER.info("Clearing render cache..."); + LOGGER.info("Disposing render data..."); // clear the tree Iterator> nodeIterator = this.nodeIterator(); @@ -412,13 +482,12 @@ public class LodQuadTree extends QuadTree implements AutoClose QuadNode quadNode = nodeIterator.next(); if (quadNode.value != null) { - quadNode.value.disposeRenderData(); + quadNode.value.close(); quadNode.value = null; } } - this.renderSourceProvider.deleteRenderCache(); - LOGGER.info("Render cache invalidated, please wait a moment for everything to reload..."); + LOGGER.info("Render data cleared, please wait a moment for everything to reload..."); } catch (Exception e) { @@ -435,7 +504,7 @@ public class LodQuadTree extends QuadTree implements AutoClose * Can be called whenever a render section's data needs to be refreshed.
* This should be called whenever a world generation task is completed or if the connected server has new data to show. */ - public void reloadPos(DhSectionPos pos) + public void reloadPos(@NotNull DhSectionPos pos) { if (pos == null) { @@ -444,20 +513,84 @@ public class LodQuadTree extends QuadTree implements AutoClose return; } - //LOGGER.info("LodQuadTree reloadPos ["+pos+"]."); + this.sectionsToReload.add(pos); + + // the adjacent locations also need to be updated to make sure lighting + // and water updates correctly, otherwise oceans may have walls + // and lights may not show up over LOD borders + for (EDhDirection direction : EDhDirection.ADJ_DIRECTIONS) + { + this.sectionsToReload.add(pos.getAdjacentPos(direction)); + } } + //=================================// + // full data retrieval (world gen) // + //=================================// + + private void queueFullDataRetrievalTasks(DhBlockPos2D playerPos, ArrayList nodesNeedingRetrieval) + { + try + { + // add a slight delay since we don't need to check the world gen queue every tick + Thread.sleep(WORLD_GEN_QUEUE_UPDATE_DELAY_IN_MS); + + // sort the nodes from nearest to farthest + nodesNeedingRetrieval.sort((a, b) -> + { + int aDist = a.pos.getManhattanBlockDistance(playerPos); + int bDist = b.pos.getManhattanBlockDistance(playerPos); + return Integer.compare(aDist, bDist); + }); + + // add retrieval tasks to the queue + for (int i = 0; i < nodesNeedingRetrieval.size(); i++) + { + LodRenderSection renderSection = nodesNeedingRetrieval.get(i); + if (!this.fullDataSourceProvider.canQueueRetrieval()) + { + break; + } + + renderSection.tryQueuingMissingLodRetrieval(); + } + + // calculate an estimate for the max number of tasks for the queue + int totalWorldGenCount = 0; + for (int i = 0; i < nodesNeedingRetrieval.size(); i++) + { + LodRenderSection renderSection = nodesNeedingRetrieval.get(i); + if (!renderSection.missingPositionsCalculated()) + { + // may be higher than the actual amount + totalWorldGenCount += this.fullDataSourceProvider.getMaxPossibleRetrievalPositionCountForPos(renderSection.pos); + } + else + { + totalWorldGenCount += renderSection.ungeneratedPositionCount(); + } + } + this.fullDataSourceProvider.setTotalRetrievalPositionCount(totalWorldGenCount); + } + catch (Exception e) + { + LOGGER.error("Unexpected error: "+e.getMessage(), e); + } + finally + { + this.fullDataRetrievalQueueRunning.set(false); + } + } + + //==================// // config listeners // //==================// - private void onHorizontalQualityChange() - { - this.clearRenderDataCache(); - } + private void onHorizontalQualityChange() { this.clearRenderDataCache(); } @@ -478,7 +611,7 @@ public class LodQuadTree extends QuadTree implements AutoClose QuadNode quadNode = nodeIterator.next(); if (quadNode.value != null) { - quadNode.value.dispose(); + quadNode.value.close(); quadNode.value = null; } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java b/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java index 629fffab0..d4c25cc74 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java @@ -20,69 +20,72 @@ package com.seibel.distanthorizons.core.render; import com.seibel.distanthorizons.core.config.Config; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource; import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.ColumnRenderBufferBuilder; +import com.seibel.distanthorizons.core.dataObjects.transformers.FullDataToRenderDataTransformer; import com.seibel.distanthorizons.core.enums.EDhDirection; -import com.seibel.distanthorizons.core.file.renderfile.IRenderSourceProvider; +import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.render.glObject.GLProxy; import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; -import com.seibel.distanthorizons.core.util.LodUtil; -import com.seibel.distanthorizons.core.util.objects.Reference; -import com.seibel.distanthorizons.core.util.objects.quadTree.QuadTree; import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.ColumnRenderBuffer; import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; +import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; +import javax.annotation.WillNotClose; import java.awt.*; -import java.util.Objects; -import java.util.concurrent.CancellationException; +import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; /** * A render section represents an area that could be rendered. * For more information see {@link LodQuadTree}. */ -public class LodRenderSection implements IDebugRenderable +public class LodRenderSection implements IDebugRenderable, AutoCloseable { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + public final DhSectionPos pos; - private boolean isRenderingEnabled = false; - /** - * If this is true, then {@link LodRenderSection#reload(IRenderSourceProvider)} was called while - * a {@link IRenderSourceProvider} was already being loaded. + private final IDhClientLevel level; + @WillNotClose + private final FullDataSourceProviderV2 fullDataSourceProvider; + private final LodQuadTree quadTree; + + + public boolean renderingEnabled = false; + private boolean canRender = false; + + /** this reference is necessary so we can determine what VBO to render */ + public ColumnRenderBuffer renderBuffer; + + + /** + * Encapsulates everything between pulling data from the database (including neighbors) + * up to the point when geometry data is uploaded to the GPU. */ - private boolean reloadRenderSourceOnceLoaded = false; + private CompletableFuture uploadRenderDataToGpuFuture = null; - private IRenderSourceProvider renderSourceProvider = null; - private CompletableFuture renderSourceLoadFuture; - private ColumnRenderSource renderSource; + private final ReentrantLock getRenderSourceLock = new ReentrantLock(); + /** Stored as a class variable so we can reuse it's result across multiple LOD loads if necessary */ + private ReferencedFutureWrapper renderSourceLoadingRefFuture = null; + /** Stored as a class variable so we can decrement reference counts as each {@link LodRenderSection} finishes using them. */ + private ReferencedFutureWrapper[] adjacentLoadRefFutures; - private IDhClientLevel level = null; - - //FIXME: Temp Hack to prevent swapping buffers too quickly - private long lastNs = -1; - private long lastSwapLocalVersion = -1; - private boolean neighborUpdated = false; - /** 2 sec */ - private static final long SWAP_TIMEOUT_IN_NS = 2_000000000L; - /** 1 sec */ - private static final long SWAP_BUSY_COLLISION_TIMEOUT_IN_NS = 1_000000000L; - - private CompletableFuture buildRenderBufferFuture = null; - private final Reference inactiveRenderBufferRef = new Reference<>(); - - /** a reference is used so the render buffer can be swapped to and from the buffer builder */ - public final AtomicReference activeRenderBufferRef = new AtomicReference<>(); - private volatile boolean disposeActiveBuffer = false; - - private final QuadTree parentQuadTree; + private boolean missingPositionsCalculated = false; + /** should be an empty array if no positions need to be generated */ + private ArrayList missingGenerationPos = null; @@ -90,108 +93,214 @@ public class LodRenderSection implements IDebugRenderable // constructor // //=============// - public LodRenderSection(QuadTree parentQuadTree, DhSectionPos pos) + public LodRenderSection(DhSectionPos pos, LodQuadTree quadTree, IDhClientLevel level, FullDataSourceProviderV2 fullDataSourceProvider) { this.pos = pos; - this.parentQuadTree = parentQuadTree; + this.quadTree = quadTree; + this.level = level; + this.fullDataSourceProvider = fullDataSourceProvider; DebugRenderer.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionStatus); } - //===========// - // rendering // - //===========// + //===============================// + // render data loading/uploading // + //===============================// - public void enableRendering() { this.isRenderingEnabled = true; } - public void disableRendering() { this.isRenderingEnabled = false; } - - - - //=============// - // render data // - //=============// - - /** does nothing if a render source is already loaded or in the process of loading */ - public void loadRenderSource(IRenderSourceProvider renderDataProvider, IDhClientLevel level) + public synchronized void uploadRenderDataToGpuAsync() { - this.renderSourceProvider = renderDataProvider; - this.level = level; - if (this.renderSourceProvider == null) + if (!GLProxy.hasInstance()) { - LOGGER.warn("LodRenderSection [" + this.pos + "] called loadRenderSource with a empty source provider"); + // it's possible to try uploading buffers before the GLProxy has been initialized + // which would cause the system to crash return; } - // don't re-load or double load the render source - if (this.renderSource != null || this.renderSourceLoadFuture != null) + + if (this.uploadRenderDataToGpuFuture != null) { - // since the render source has been loaded, make sure the render buffers are populated - // FIXME this is a duck tape solution, since the renderBufferRef should be populated elsewhere, but this does fix empty LODs when moving around the world - if (this.activeRenderBufferRef.get() == null) + // don't accidentally queue multiple uploads at the same time + return; + } + + + + ThreadPoolExecutor executor = ThreadPoolUtil.getFileHandlerExecutor(); + if (executor == null || executor.isTerminated()) + { + return; + } + + this.uploadRenderDataToGpuFuture = CompletableFuture.runAsync(() -> + { + //==================// + // load render data // + //==================// + + this.tryDecrementingLoadFutureArray(this.adjacentLoadRefFutures); + + ReferencedFutureWrapper thisLoadFuture = this.getRenderSourceAsync(); + ReferencedFutureWrapper[] adjLoadRefFutures = this.getNeighborRenderSourcesAsync(); + + + // wait for all futures to complete together, + // merging the futures makes loading significantly faster than loading this position then loading its neighbors + ArrayList> futureList = new ArrayList<>(); + futureList.add(thisLoadFuture.future); + for (ReferencedFutureWrapper refFuture : adjLoadRefFutures) { - this.markBufferDirty(); // empty LOD fix #3, all solutions revolve around markBufferDirty() + futureList.add(refFuture.future); } - return; - } - - this.startLoadRenderSourceAsync(); - } - - public void reload(IRenderSourceProvider renderDataProvider) - { - // debug rendering - boolean showRenderSectionStatus = Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionStatus.get(); - if (showRenderSectionStatus && this.pos.getDetailLevel() == DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL) - { - DebugRenderer.makeParticle( - new DebugRenderer.BoxParticle( - new DebugRenderer.Box(this.pos, 0, 256f, 0.03f, Color.cyan), - 0.5, 512f - ) - ); - } - - - this.renderSourceProvider = renderDataProvider; - if (this.renderSourceProvider == null) - { - LOGGER.warn("LodRenderSection [" + this.pos + "] called reload with a empty source provider"); - return; - } - - // don't accidentally enable rendering for a disabled section - if (!this.isRenderingEnabled) - { - return; - } - // wait for the current load future to finish before re-loading - if (this.renderSourceLoadFuture != null) - { - this.reloadRenderSourceOnceLoaded = true; - return; - } - - this.startLoadRenderSourceAsync(); - } - - private void startLoadRenderSourceAsync() - { - this.renderSourceLoadFuture = this.renderSourceProvider.getAsync(this.pos); - this.renderSourceLoadFuture.whenComplete((renderSource, ex) -> - { - this.renderSource = renderSource; - this.lastNs = -1; - this.markBufferDirty(); - if (this.reloadRenderSourceOnceLoaded) + CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).thenAccept((voidObj) -> { - this.reloadRenderSourceOnceLoaded = false; - this.reload(this.renderSourceProvider); + try + { + ColumnRenderSource renderSource = thisLoadFuture.future.get(); + if (renderSource == null || renderSource.isEmpty()) + { + thisLoadFuture.decrementRefCount(); + for (ReferencedFutureWrapper futureWrapper : adjLoadRefFutures) + { + futureWrapper.decrementRefCount(); + } + + // nothing needs to be rendered + this.canRender = false; + return; + } + + + + //==============================// + // build/upload new render data // + //==============================// + + try + { + ColumnRenderBuffer previousBuffer = this.renderBuffer; + + ColumnRenderSource[] adjacentRenderSections = new ColumnRenderSource[EDhDirection.ADJ_DIRECTIONS.length]; + for (int i = 0; i < EDhDirection.ADJ_DIRECTIONS.length; i++) + { + adjacentRenderSections[i] = adjLoadRefFutures[i].future.getNow(null); + } + ColumnRenderBufferBuilder.buildAndUploadBuffersAsync(this.level, renderSource, adjacentRenderSections).thenAccept((buffer) -> + { + // upload complete, clean up the old data if + this.renderBuffer = buffer; + this.canRender = true; + this.uploadRenderDataToGpuFuture = null; + + + if (previousBuffer != null) + { + previousBuffer.close(); + } + + thisLoadFuture.decrementRefCount(); + this.tryDecrementingLoadFutureArray(adjLoadRefFutures); + this.adjacentLoadRefFutures = null; + }); + } + catch (Exception e) + { + thisLoadFuture.decrementRefCount(); + this.tryDecrementingLoadFutureArray(adjLoadRefFutures); + this.adjacentLoadRefFutures = null; + + LOGGER.error("Unexpected error in LodRenderSection loading, Error: "+e.getMessage(), e); + this.uploadRenderDataToGpuFuture = null; + } + } + catch (Exception e) + { + thisLoadFuture.decrementRefCount(); + this.tryDecrementingLoadFutureArray(adjLoadRefFutures); + this.adjacentLoadRefFutures = null; + + LOGGER.error("Unexpected error in LodRenderSection loading, Error: "+e.getMessage(), e); + this.uploadRenderDataToGpuFuture = null; + } + }); + }, executor); + } + /** Should be called on the {@link ThreadPoolUtil#getFileHandlerExecutor()} */ + private ReferencedFutureWrapper[] getNeighborRenderSourcesAsync() + { + ReferencedFutureWrapper[] futureArray = new ReferencedFutureWrapper[EDhDirection.ADJ_DIRECTIONS.length]; + for (int i = 0; i < EDhDirection.ADJ_DIRECTIONS.length; i++) + { + EDhDirection direction = EDhDirection.ADJ_DIRECTIONS[i]; + int arrayIndex = direction.ordinal() - 2; + + DhSectionPos adjPos = this.pos.getAdjacentPos(direction); + try + { + LodRenderSection adjRenderSection = this.quadTree.getValue(adjPos); + if (adjRenderSection != null) + { + futureArray[arrayIndex] = adjRenderSection.getRenderSourceAsync(); + } + } + catch (IndexOutOfBoundsException ignore) {} + + if (futureArray[arrayIndex] == null) + { + futureArray[arrayIndex] = new ReferencedFutureWrapper(CompletableFuture.completedFuture(null)); + } + } + + this.adjacentLoadRefFutures = futureArray; + return futureArray; + } + /** Will try to return the same {@link CompletableFuture} if multiple requests are made for the same position */ + private ReferencedFutureWrapper getRenderSourceAsync() + { + try + { + this.getRenderSourceLock.lock(); + + + // if a load is already in progress, use that existing one + // (this reduces the number of duplicate loads that may happen when initially loading the world) + if (this.renderSourceLoadingRefFuture != null) + { + // increment the number of objects needing this future + this.renderSourceLoadingRefFuture.incrementRefCount(); + return this.renderSourceLoadingRefFuture; } - this.renderSourceLoadFuture = null; - }); + + + ThreadPoolExecutor executor = ThreadPoolUtil.getFileHandlerExecutor(); + if (executor == null || executor.isTerminated()) + { + return new ReferencedFutureWrapper(CompletableFuture.completedFuture(null)); + } + + this.renderSourceLoadingRefFuture = new ReferencedFutureWrapper(CompletableFuture.supplyAsync(() -> + { + try (FullDataSourceV2 fullDataSource = this.fullDataSourceProvider.get(this.pos)) + { + ColumnRenderSource renderSource = FullDataToRenderDataTransformer.transformFullDataToRenderSource(fullDataSource, this.level); + this.renderSourceLoadingRefFuture = null; + return renderSource; + } + catch (Exception e) + { + LOGGER.warn("Unable to get render source " + this.pos + ", error: " + e.getMessage(), e); + this.renderSourceLoadingRefFuture = null; + return null; + } + }, executor)); + return this.renderSourceLoadingRefFuture; + } + finally + { + this.getRenderSourceLock.unlock(); + } } @@ -200,209 +309,78 @@ public class LodRenderSection implements IDebugRenderable // getters and properties // //========================// - /** This can return true before the render data is loaded */ - public boolean isRenderingEnabled() { return this.isRenderingEnabled; } + public boolean canRender() { return this.canRender; } - public ColumnRenderSource getRenderSource() { return this.renderSource; } + public boolean gpuUploadInProgress() { return this.uploadRenderDataToGpuFuture != null; } - public boolean canRenderNow() + + + //=================================// + // full data retrieval (world gen) // + //=================================// + + public boolean isFullyGenerated() { return this.missingPositionsCalculated && this.missingGenerationPos.size() == 0; } + public boolean missingPositionsCalculated() { return this.missingPositionsCalculated; } + public int ungeneratedPositionCount() { return (this.missingGenerationPos != null) ? this.missingGenerationPos.size() : 0; } + + public void tryQueuingMissingLodRetrieval() { - if (this.renderSourceLoadFuture != null || this.buildRenderBufferFuture != null) + if (this.fullDataSourceProvider.canRetrieveMissingDataSources() && this.fullDataSourceProvider.canQueueRetrieval()) { - // wait for loading to finish - return false; - } - - return this.renderSource != null - && - ( - ( - // if true; either this section represents empty chunks or un-generated chunks. - // Either way, there isn't any data to render, but this should be considered "loaded" - this.renderSource.isEmpty() - ) - || - ( - // check if the buffers have been loaded - this.activeRenderBufferRef.get() != null // in the case of missing sections, this is probably null - && this.lastSwapLocalVersion != -1 - ) - ); - } - - public void markBufferDirty() { this.lastSwapLocalVersion = -1; } - - - - //=================// - // buffer building // - //=================// - - private LodRenderSection[] getNeighbors() - { - LodRenderSection[] adjacentRenderSections = new LodRenderSection[EDhDirection.ADJ_DIRECTIONS.length]; - for (EDhDirection direction : EDhDirection.ADJ_DIRECTIONS) - { - try + // calculate the missing positions if not already done + if (!this.missingPositionsCalculated) { - DhSectionPos adjPos = this.pos.getAdjacentPos(direction); - LodRenderSection adjRenderSection = this.parentQuadTree.getValue(adjPos); - // adjacent render sources might be null - adjacentRenderSections[direction.ordinal() - 2] = adjRenderSection; - } - catch (IndexOutOfBoundsException e) - { - // adjacent positions can be out of bounds, in that case a null render source will be used - } - } - - return adjacentRenderSections; - } - - private void tellNeighborsUpdated() - { - LodRenderSection[] adjacentRenderSections = this.getNeighbors(); - for (LodRenderSection adj : adjacentRenderSections) - { - if (adj != null) - { - adj.neighborUpdated = true; - } - } - } - - /** @return true if this section is loaded and set to render */ - public boolean canBuildBuffer() { return this.renderSourceLoadFuture == null && this.renderSource != null && this.buildRenderBufferFuture == null && !this.renderSource.isEmpty() && this.isBufferOutdated(); } - private boolean isBufferOutdated() { return this.neighborUpdated || this.renderSource.localVersion.get() != this.lastSwapLocalVersion; } - - /** @return true if this section is loaded and set to render */ - public boolean canSwapBuffer() { return this.buildRenderBufferFuture != null && this.buildRenderBufferFuture.isDone(); } - - - public synchronized void disposeRenderData() // synchronized is a band-aid solution to prevent a rare bug where the future isn't canceled in the right order - { - if (this.buildRenderBufferFuture != null) - { - //LOGGER.info("Cancelling build of render buffer for {}", sectionPos); - this.buildRenderBufferFuture.cancel(true); - this.buildRenderBufferFuture = null; - } - this.disposeActiveBuffer = true; - - this.renderSource = null; - if (this.renderSourceLoadFuture != null) - { - this.renderSourceLoadFuture.cancel(true); - this.renderSourceLoadFuture = null; - } - } - - // probably used by Iris - public void disposeBufferForRecreate() { this.disposeActiveBuffer = true; } - - - /** - * Try and swap in new render buffer for this section. Note that before this call, there should be no other - * places storing or referencing the render buffer. - * - * @return True if the swap was successful. False if swap is not needed or if it is in progress. - */ - public boolean tryBuildAndSwapBuffer() - { - // delete the existing buffer if it should be disposed - if (this.disposeActiveBuffer && this.activeRenderBufferRef.get() != null) - { - this.disposeActiveBuffer = false; - this.activeRenderBufferRef.getAndSet(null).close(); - return false; - } - - - // attempt to build the buffer - boolean didSwapped = false; - if (this.canBuildBuffer()) - { - // debug - boolean showRenderSectionStatus = Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionStatus.get(); - if (showRenderSectionStatus && this.pos.getDetailLevel() == DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL) - { - DebugRenderer.makeParticle( - new DebugRenderer.BoxParticle( - new DebugRenderer.Box(this.pos, 32f, 64f, 0.2f, Color.yellow), - 0.5, 16f - ) - ); - } - - - this.neighborUpdated = false; - long newVersion = this.renderSource.localVersion.get(); - if (this.lastSwapLocalVersion != newVersion) - { - this.lastSwapLocalVersion = newVersion; - this.tellNeighborsUpdated(); - } - - - LodRenderSection[] adjacentRenderSections = this.getNeighbors(); - ColumnRenderSource[] adjacentSources = new ColumnRenderSource[EDhDirection.ADJ_DIRECTIONS.length]; - for (int i = 0; i < EDhDirection.ADJ_DIRECTIONS.length; i++) - { - LodRenderSection adj = adjacentRenderSections[i]; - if (adj != null) + this.missingGenerationPos = this.fullDataSourceProvider.getPositionsToRetrieve(this.pos); + if (this.missingGenerationPos != null) { - adjacentSources[i] = adj.getRenderSource(); + this.missingPositionsCalculated = true; } } - this.buildRenderBufferFuture = ColumnRenderBufferBuilder.buildBuffersAsync(this.level, this.inactiveRenderBufferRef, this.renderSource, adjacentSources); + // if the missing positions were found, queue them + if (this.missingGenerationPos != null) + { + // queue from last to first to prevent shifting the array unnecessarily + for (int i = this.missingGenerationPos.size() - 1; i >= 0; i--) + { + if (!this.fullDataSourceProvider.canQueueRetrieval()) + { + // the data source provider isn't accepting any more jobs + break; + } + + DhSectionPos pos = this.missingGenerationPos.remove(i); + boolean positionQueued = this.fullDataSourceProvider.queuePositionForRetrieval(pos); + if (!positionQueued) + { + // shouldn't normally happen, but just in case + this.missingGenerationPos.add(pos); + break; + } + } + } } - - - // attempt to swap in the buffer - if (this.canSwapBuffer()) + } + + + + //=========// + // cleanup // + //=========// + + /** does nothing if the passed in value is null. */ + private void tryDecrementingLoadFutureArray(@Nullable ReferencedFutureWrapper[] refFutures) + { + if (refFutures != null) { - this.lastNs = System.nanoTime(); - ColumnRenderBuffer newBuffer; - try + for (ReferencedFutureWrapper futureWrapper : refFutures) { - newBuffer = this.buildRenderBufferFuture.getNow(null); - if (newBuffer == null) + if (futureWrapper != null) { - // failed. - this.markBufferDirty(); - return false; + futureWrapper.decrementRefCount(); } - - LodUtil.assertTrue(newBuffer.buffersUploaded, "The buffer future for " + this.pos + " returned an un-built buffer."); - ColumnRenderBuffer oldBuffer = this.activeRenderBufferRef.getAndSet(newBuffer); - if (oldBuffer != null) - { - // the old buffer is now considered unloaded, it will need to be freshly re-loaded - oldBuffer.buffersUploaded = false; - oldBuffer.close(); - } - ColumnRenderBuffer swapped = this.inactiveRenderBufferRef.swap(oldBuffer); - didSwapped = true; - LodUtil.assertTrue(swapped == null); - } - catch (CancellationException e1) - { - // ignore. - this.buildRenderBufferFuture = null; - } - catch (CompletionException e) - { - LOGGER.error("Unable to get render buffer for " + pos + ".", e); - this.buildRenderBufferFuture = null; - } - finally - { - this.buildRenderBufferFuture = null; } } - - return didSwapped; } @@ -411,27 +389,63 @@ public class LodRenderSection implements IDebugRenderable // base methods // //==============// + @Override public String toString() { return "LodRenderSection{" + "pos=" + this.pos + - ", lodRenderSource=" + this.renderSource + - ", loadFuture=" + this.renderSourceLoadFuture + - ", isRenderEnabled=" + this.isRenderingEnabled + '}'; } - public void dispose() + @Override + public void close() { - this.disposeRenderData(); DebugRenderer.unregister(this, Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionStatus); - if (this.activeRenderBufferRef.get() != null) + + if (Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionStatus.get()) { - this.activeRenderBufferRef.get().close(); + // show a particle for the closed section + DebugRenderer.makeParticle( + new DebugRenderer.BoxParticle( + new DebugRenderer.Box(this.pos, 128f, 156f, 0.09f, Color.RED.darker()), + 0.5, 32f + ) + ); } - if (this.inactiveRenderBufferRef.value != null) + + + + if (this.renderBuffer != null) { - this.inactiveRenderBufferRef.value.close(); + this.renderBuffer.close(); + } + + if (this.uploadRenderDataToGpuFuture != null) + { + this.uploadRenderDataToGpuFuture.cancel(true); + } + + // this render section won't be rendering, we don't need to load any data for it + this.tryDecrementingLoadFutureArray(this.adjacentLoadRefFutures); + if (this.renderSourceLoadingRefFuture != null) + { + this.renderSourceLoadingRefFuture.decrementRefCount(); + } + + + // remove any active world gen requests that may be for this position + ThreadPoolExecutor executor = ThreadPoolUtil.getCleanupExecutor(); + if (executor != null && !executor.isTerminated()) + { + // while this should generally be a fast operation + // this is run on a separate thread to prevent lag on the render thread + + try + { + executor.execute(() -> this.fullDataSourceProvider.removeRetrievalRequestIf((genPos) -> this.pos.contains(genPos))); + } + catch (RejectedExecutionException ignore) + { /* If this happens that means everything is already shut down and no additional cleanup will be necessary */ } } } @@ -439,32 +453,66 @@ public class LodRenderSection implements IDebugRenderable public void debugRender(DebugRenderer debugRenderer) { Color color = Color.red; - if (this.renderSourceProvider == null) + if (this.renderingEnabled) { - color = Color.black; + color = Color.green; } - else if (this.renderSourceLoadFuture != null) + else if (this.uploadRenderDataToGpuFuture != null) { color = Color.yellow; } - else if (this.renderSource != null) + else if (this.canRender) { - color = Color.blue; - if (this.buildRenderBufferFuture != null) - { - color = Color.magenta; - } - else if (this.canRenderNow()) - { - color = Color.cyan; - } - else if (this.canRenderNow() && this.isRenderingEnabled) - { - color = Color.green; - } + color = Color.cyan; } debugRenderer.renderBox(new DebugRenderer.Box(this.pos, 400, 8f, Objects.hashCode(this), 0.1f, color)); } + + + //================// + // helper classes // + //================// + + /** + * Used to keep track of whether a {@link ColumnRenderSource} {@link CompletableFuture} + * is in use or not, and if not in use cancels the future.

+ * + * This helps speed up LOD loading by canceling loads that are no longer needed, + * IE out of range or in an unloaded dimension. + */ + private static class ReferencedFutureWrapper + { + public final CompletableFuture future; + // starts at 1 since the constructing method is referencing this future + private final AtomicInteger refCount = new AtomicInteger(1); + + + + public ReferencedFutureWrapper(CompletableFuture future) { this.future = future; } + + public void incrementRefCount() { this.refCount.incrementAndGet(); } + public void decrementRefCount() + { + // automatically clean up this future if no one else is referencing it + if (this.refCount.decrementAndGet() <= 0) + { + if (this.future != null) + { + if (!this.future.isDone()) + { + this.future.cancel(true); + } + } + } + } + + + + @Override + public String toString() { return this.future.toString() + " - " + this.refCount.get(); } + + } + } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/RenderBufferHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/render/RenderBufferHandler.java index a66d3c028..6c8efc3ed 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/RenderBufferHandler.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/RenderBufferHandler.java @@ -24,6 +24,7 @@ import com.seibel.distanthorizons.api.interfaces.override.rendering.IDhApiCullin import com.seibel.distanthorizons.api.interfaces.override.rendering.IDhApiShadowCullingFrustum; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; import com.seibel.distanthorizons.core.config.Config; +import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.ColumnRenderBuffer; import com.seibel.distanthorizons.core.dependencyInjection.ModAccessorInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.enums.EDhDirection; @@ -338,19 +339,8 @@ public class RenderBufferHandler implements AutoCloseable } } - if (rebuildAllBuffers) - { - renderSection.markBufferDirty(); - } - - renderSection.tryBuildAndSwapBuffer(); - if (!renderSection.isRenderingEnabled()) - { - continue; - } - - AbstractRenderBuffer buffer = renderSection.activeRenderBufferRef.get(); - if (buffer == null) + ColumnRenderBuffer buffer = renderSection.renderBuffer; + if (buffer == null || !renderSection.renderingEnabled) { continue; } @@ -361,7 +351,6 @@ public class RenderBufferHandler implements AutoCloseable catch (Exception e) { LOGGER.error("Error updating QuadTree render source at " + renderSection.pos + ".", e); - renderSection.markBufferDirty(); } } @@ -420,7 +409,7 @@ public class RenderBufferHandler implements AutoCloseable LodRenderSection renderSection = nodeIterator.next().value; if (renderSection != null) { - renderSection.dispose(); + renderSection.close(); } } @@ -435,10 +424,10 @@ public class RenderBufferHandler implements AutoCloseable private static class LoadedRenderBuffer { - public final AbstractRenderBuffer buffer; + public final ColumnRenderBuffer buffer; public final DhSectionPos pos; - LoadedRenderBuffer(AbstractRenderBuffer buffer, DhSectionPos pos) + LoadedRenderBuffer(ColumnRenderBuffer buffer, DhSectionPos pos) { this.buffer = buffer; this.pos = pos; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/fog/FogSettings.java b/core/src/main/java/com/seibel/distanthorizons/core/render/fog/FogSettings.java index 98fa739ca..12db4070d 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/fog/FogSettings.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/fog/FogSettings.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.core.render.fog; -import com.seibel.distanthorizons.api.enums.rendering.EFogFalloff; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiFogFalloff; import java.util.Objects; @@ -31,7 +31,7 @@ import java.util.Objects; public class FogSettings { /** a FogSetting object with 0 for every value */ - public static final FogSettings EMPTY = new FogSettings(0, 0, 0, 0, 0, EFogFalloff.LINEAR); + public static final FogSettings EMPTY = new FogSettings(0, 0, 0, 0, 0, EDhApiFogFalloff.LINEAR); public final double start; @@ -39,9 +39,9 @@ public class FogSettings public final double min; public final double max; public final double density; - public final EFogFalloff fogType; + public final EDhApiFogFalloff fogType; - public FogSettings(double start, double end, double min, double max, double density, EFogFalloff fogType) + public FogSettings(double start, double end, double min, double max, double density, EDhApiFogFalloff fogType) { this.start = start; this.end = end; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/fog/LodFogConfig.java b/core/src/main/java/com/seibel/distanthorizons/core/render/fog/LodFogConfig.java index a67e992fe..be4adf73e 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/fog/LodFogConfig.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/fog/LodFogConfig.java @@ -48,12 +48,10 @@ public class LodFogConfig public final FogSettings farFogSetting; public final FogSettings heightFogSetting; - public final EHeightFogMixMode heightFogMixMode; - public final EHeightFogMode heightFogMode; + public final EDhApiHeightFogMixMode heightFogMixMode; + public final EDhApiHeightFogMode heightFogMode; public final float heightFogHeight; - final boolean drawNearFog; - // TODO: Move these out of here public final int earthCurveRatio; @@ -66,8 +64,8 @@ public class LodFogConfig public static LodFogConfig generateFogConfig() { - EFogDrawMode fogMode = Config.Client.Advanced.Graphics.Fog.drawMode.get(); - if (fogMode == EFogDrawMode.USE_OPTIFINE_SETTING && OPTIFINE != null) + EDhApiFogDrawMode fogMode = Config.Client.Advanced.Graphics.Fog.drawMode.get(); + if (fogMode == EDhApiFogDrawMode.USE_OPTIFINE_SETTING && OPTIFINE != null) { fogMode = OPTIFINE.getFogDrawMode(); } @@ -75,7 +73,7 @@ public class LodFogConfig } /** sets all fog options from the config */ - private LodFogConfig(EFogDrawMode fogDrawMode) + private LodFogConfig(EDhApiFogDrawMode fogDrawMode) { // TODO: Move these out of here earthCurveRatio = Config.Client.Advanced.Graphics.AdvancedGraphics.earthCurveRatio.get(); @@ -86,74 +84,57 @@ public class LodFogConfig noiseDropoff = Config.Client.Advanced.Graphics.NoiseTextureSettings.noiseDropoff.get(); - if (fogDrawMode != EFogDrawMode.FOG_DISABLED) + if (fogDrawMode != EDhApiFogDrawMode.FOG_DISABLED) { - EFogDistance fogDistance = Config.Client.Advanced.Graphics.Fog.distance.get(); - drawNearFog = (fogDistance == EFogDistance.NEAR || fogDistance == EFogDistance.NEAR_AND_FAR); + // fog should be drawn - if (fogDistance == EFogDistance.FAR || fogDistance == EFogDistance.NEAR_AND_FAR) + farFogSetting = new FogSettings( + Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogStart.get(), + Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogEnd.get(), + Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogMin.get(), + Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogMax.get(), + Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogDensity.get(), + Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogFalloff.get() + ); + + heightFogMixMode = Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMixMode.get(); + if (heightFogMixMode == EDhApiHeightFogMixMode.IGNORE_HEIGHT || heightFogMixMode == EDhApiHeightFogMixMode.BASIC) { - // far fog should be drawn + // basic fog mixing - farFogSetting = new FogSettings( - Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogStart.get(), - Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogEnd.get(), - Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogMin.get(), - Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogMax.get(), - Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogDensity.get(), - Config.Client.Advanced.Graphics.Fog.AdvancedFog.farFogFalloff.get() + heightFogSetting = null; + heightFogMode = null; + heightFogHeight = 0.f; + } + else + { + // advanced fog mixing + + heightFogSetting = new FogSettings( + Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogDensity.get(), + Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogEnd.get(), + Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMin.get(), + Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMax.get(), + Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogDensity.get(), + Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogFalloff.get() ); - heightFogMixMode = Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMixMode.get(); - if (heightFogMixMode == EHeightFogMixMode.IGNORE_HEIGHT || heightFogMixMode == EHeightFogMixMode.BASIC) + heightFogMode = Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMode.get(); + + if (heightFogMode.basedOnCamera) { - // basic fog mixing - - heightFogSetting = null; - heightFogMode = null; heightFogHeight = 0.f; } else { - // advanced fog mixing - - heightFogSetting = new FogSettings( - Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogDensity.get(), - Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogEnd.get(), - Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMin.get(), - Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMax.get(), - Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogDensity.get(), - Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogFalloff.get() - ); - - heightFogMode = Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogMode.get(); - - if (heightFogMode.basedOnCamera) - { - heightFogHeight = 0.f; - } - else - { - heightFogHeight = Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogBaseHeight.get().floatValue(); - } + heightFogHeight = Config.Client.Advanced.Graphics.Fog.AdvancedFog.HeightFog.heightFogBaseHeight.get().floatValue(); } } - else - { - // far fog should not be drawn - - farFogSetting = null; - heightFogSetting = null; - heightFogMode = null; - heightFogMixMode = null; - heightFogHeight = 0.f; - } } else { // fog disabled - drawNearFog = false; farFogSetting = null; heightFogMixMode = null; heightFogMode = null; @@ -205,7 +186,7 @@ public class LodFogConfig "float calculateHeightFogDepth(float vertical, float realY) { return 0.0; } \n" + "float mixFogThickness(float near, float far, float height) \n" + "{ \n" + - (drawNearFog ? "return 1.0-near;" : "return 0.0;") + + " return 0.0; \n" + "} \n\n"); } else @@ -238,7 +219,7 @@ public class LodFogConfig str.append("" + "float calculateFarFogDepth(float horizontal, float dist, float nearFogStart) \n" + "{ \n" + - " return " + (heightFogMixMode == EHeightFogMixMode.BASIC ? + " return " + (heightFogMixMode == EDhApiHeightFogMixMode.BASIC ? "(dist - nearFogStart)/(1.0 - nearFogStart);" : "(horizontal - nearFogStart)/(1.0 - nearFogStart);") + "} \n"); @@ -247,7 +228,7 @@ public class LodFogConfig str.append("" + "float mixFogThickness(float near, float far, float height) \n" + "{ \n" + - getMixFogLine(heightFogMixMode, drawNearFog) + "\n" + + getMixFogLine(heightFogMixMode) + "\n" + "} \n"); } } @@ -286,7 +267,7 @@ public class LodFogConfig return str; } - private static String getFarFogMethod(EFogFalloff fogType) + private static String getFarFogMethod(EDhApiFogFalloff fogType) { switch (fogType) { @@ -302,7 +283,7 @@ public class LodFogConfig } } - private static String getHeightDepthMethod(EHeightFogMode heightMode, float heightFogHeight) + private static String getHeightDepthMethod(EDhApiHeightFogMode heightMode, float heightFogHeight) { String str = ""; if (!heightMode.basedOnCamera) @@ -334,7 +315,7 @@ public class LodFogConfig * Example:
* " return linearFog(dist, heightFogStart, heightFogLength, heightFogMin, heightFogRange);" */ - private static String getHeightFogMethod(EFogFalloff fogType) + private static String getHeightFogMethod(EDhApiFogFalloff fogType) { switch (fogType) { @@ -354,7 +335,7 @@ public class LodFogConfig * creates a line in the format
* " return max(1.0-near, far);" */ - private static String getMixFogLine(EHeightFogMixMode heightFogMode, boolean drawNearFog) + private static String getMixFogLine(EDhApiHeightFogMixMode heightFogMode) { String str = " return "; @@ -362,65 +343,38 @@ public class LodFogConfig { case BASIC: case IGNORE_HEIGHT: - if (drawNearFog) - str += "max(1.0-near, far);\n"; - else str += "near * far;\n"; break; case ADDITION: - if (drawNearFog) - str += "max(1.0-near, far + height);\n"; - else str += "near * (far + height);\n"; break; case MAX: - if (drawNearFog) - str += "max(1.0-near, max(far, height));\n"; - else str += "near * max(far, height);\n"; break; case INVERSE_MULTIPLY: - if (drawNearFog) - str += "max(1.0-near, 1.0 - (1.0-far)*(1.0-height));\n"; - else str += "near * (1.0 - (1.0-far)*(1.0-height));\n"; break; case MULTIPLY: - if (drawNearFog) - str += "max(1.0-near, far*height);\n"; - else str += "near * far * height;\n"; break; case LIMITED_ADDITION: - if (drawNearFog) - str += "max(1.0-near, far + max(far, height));\n"; - else str += "near * (far + max(far, height));\n"; break; case MULTIPLY_ADDITION: - if (drawNearFog) - str += "max(1.0-near, far + far*height);\n"; - else str += "near * (far + far*height);\n"; break; case INVERSE_MULTIPLY_ADDITION: - if (drawNearFog) - str += "max(1.0-near, far + 1.0 - (1.0-far)*(1.0-height));\n"; - else str += "near * (far + 1.0 - (1.0-far)*(1.0-height));\n"; break; case AVERAGE: - if (drawNearFog) - str += "max(1.0-near, far*0.5 + height*0.5);\n"; - else str += "near * (far*0.5 + height*0.5);\n"; break; @@ -449,7 +403,7 @@ public class LodFogConfig return false; LodFogConfig that = (LodFogConfig) o; return Float.compare(that.heightFogHeight, heightFogHeight) == 0 && - drawNearFog == that.drawNearFog && Objects.equals(farFogSetting, that.farFogSetting) && + Objects.equals(farFogSetting, that.farFogSetting) && Objects.equals(heightFogSetting, that.heightFogSetting) && heightFogMixMode == that.heightFogMixMode && heightFogMode == that.heightFogMode // TODO: Move these out of here @@ -460,7 +414,7 @@ public class LodFogConfig @Override public int hashCode() { - return Objects.hash(farFogSetting, heightFogSetting, heightFogMixMode, heightFogMode, heightFogHeight, drawNearFog, earthCurveRatio, noiseEnable, noiseSteps, noiseIntensity, noiseDropoff); + return Objects.hash(farFogSetting, heightFogSetting, heightFogMixMode, heightFogMode, heightFogHeight, earthCurveRatio, noiseEnable, noiseSteps, noiseIntensity, noiseDropoff); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/GLProxy.java b/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/GLProxy.java index b53f782a5..c49f74ce9 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/GLProxy.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/GLProxy.java @@ -20,9 +20,9 @@ package com.seibel.distanthorizons.core.render.glObject; import com.google.common.util.concurrent.ThreadFactoryBuilder; -import com.seibel.distanthorizons.api.enums.config.EGLErrorHandlingMode; -import com.seibel.distanthorizons.api.enums.config.EGlProfileMode; -import com.seibel.distanthorizons.api.enums.config.EGpuUploadMethod; +import com.seibel.distanthorizons.api.enums.config.EDhApiGLErrorHandlingMode; +import com.seibel.distanthorizons.api.enums.config.EDhApiGlProfileMode; +import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.enums.EGLProxyContext; @@ -107,7 +107,7 @@ public class GLProxy public boolean bufferStorageSupported = false; // ~OpenGL 4.4 public boolean VertexAttributeBufferBindingSupported = false; // ~OpenGL 4.3 - private final EGpuUploadMethod preferredUploadMethod; + private final EDhApiGpuUploadMethod preferredUploadMethod; public final GLMessage.Builder vanillaDebugMessageBuilder = GLMessage.Builder.DEFAULT_MESSAGE_BUILDER; public final GLMessage.Builder lodBuilderDebugMessageBuilder = GLMessage.Builder.DEFAULT_MESSAGE_BUILDER; @@ -209,7 +209,7 @@ public class GLProxy GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, forwardCompatEnabled ? GLFW.GLFW_TRUE : GLFW.GLFW_FALSE); int profileModeInt; - EGlProfileMode profileModeEnum = Config.Client.Advanced.Debugging.OpenGl.glProfileMode.get(); + EDhApiGlProfileMode profileModeEnum = Config.Client.Advanced.Debugging.OpenGl.glProfileMode.get(); switch (profileModeEnum) { case CORE: @@ -322,12 +322,12 @@ public class GLProxy if (vendor.contains("NVIDIA") || vendor.contains("GEFORCE")) { // NVIDIA card - this.preferredUploadMethod = this.bufferStorageSupported ? EGpuUploadMethod.BUFFER_STORAGE : EGpuUploadMethod.SUB_DATA; + this.preferredUploadMethod = this.bufferStorageSupported ? EDhApiGpuUploadMethod.BUFFER_STORAGE : EDhApiGpuUploadMethod.SUB_DATA; } else { // AMD or Intel card - this.preferredUploadMethod = this.bufferStorageSupported ? EGpuUploadMethod.BUFFER_STORAGE : EGpuUploadMethod.DATA; + this.preferredUploadMethod = this.bufferStorageSupported ? EDhApiGpuUploadMethod.BUFFER_STORAGE : EDhApiGpuUploadMethod.DATA; } GL_LOGGER.info("GPU Vendor [" + vendor + "], Preferred upload method is [" + this.preferredUploadMethod + "]."); @@ -445,16 +445,16 @@ public class GLProxy return instance; } - public EGpuUploadMethod getGpuUploadMethod() + public EDhApiGpuUploadMethod getGpuUploadMethod() { - EGpuUploadMethod method = Config.Client.Advanced.GpuBuffers.gpuUploadMethod.get(); - if (!this.bufferStorageSupported && method == EGpuUploadMethod.BUFFER_STORAGE) + EDhApiGpuUploadMethod method = Config.Client.Advanced.GpuBuffers.gpuUploadMethod.get(); + if (!this.bufferStorageSupported && method == EDhApiGpuUploadMethod.BUFFER_STORAGE) { // if buffer storage isn't supported // default to DATA since that is the most compatible - method = EGpuUploadMethod.DATA; + method = EDhApiGpuUploadMethod.DATA; } - return method == EGpuUploadMethod.AUTO ? this.preferredUploadMethod : method; + return method == EDhApiGpuUploadMethod.AUTO ? this.preferredUploadMethod : method; } @@ -567,8 +567,8 @@ public class GLProxy private static void logMessage(GLMessage msg) { - EGLErrorHandlingMode errorHandlingMode = Config.Client.Advanced.Debugging.OpenGl.glErrorHandlingMode.get(); - if (errorHandlingMode == EGLErrorHandlingMode.IGNORE) + EDhApiGLErrorHandlingMode errorHandlingMode = Config.Client.Advanced.Debugging.OpenGl.glErrorHandlingMode.get(); + if (errorHandlingMode == EDhApiGLErrorHandlingMode.IGNORE) { return; } @@ -581,7 +581,7 @@ public class GLProxy GL_LOGGER.error("GL ERROR " + msg.id + " from " + msg.source + ": " + msg.message); - if (errorHandlingMode == EGLErrorHandlingMode.LOG_THROW) + if (errorHandlingMode == EDhApiGLErrorHandlingMode.LOG_THROW) { throw new RuntimeException("GL ERROR: " + msg); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/buffer/GLBuffer.java b/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/buffer/GLBuffer.java index e4d8b8d52..112a13cd0 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/buffer/GLBuffer.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/buffer/GLBuffer.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.core.render.glObject.buffer; -import com.seibel.distanthorizons.api.enums.config.EGpuUploadMethod; +import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod; import com.seibel.distanthorizons.core.enums.EGLProxyContext; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.render.glObject.GLProxy; @@ -94,8 +94,10 @@ public class GLBuffer implements AutoCloseable protected void create(boolean asBufferStorage) { - LodUtil.assertTrue(GLProxy.getInstance().getGlContext() != EGLProxyContext.NONE, - "Thread ["+Thread.currentThread()+"] tried to create a GLBuffer outside a OpenGL context."); + if (GLProxy.getInstance().getGlContext() == EGLProxyContext.NONE) + { + LodUtil.assertNotReach("Thread ["+Thread.currentThread()+"] tried to create a GLBuffer outside a OpenGL context."); + } this.id = GL32.glGenBuffers(); this.bufferStorage = asBufferStorage; @@ -161,11 +163,14 @@ public class GLBuffer implements AutoCloseable * Assumes the GL Context is already bound.
* Will create the VBO if one exist. */ - public void uploadBuffer(ByteBuffer bb, EGpuUploadMethod uploadMethod, int maxExpansionSize, int bufferHint) + public void uploadBuffer(ByteBuffer bb, EDhApiGpuUploadMethod uploadMethod, int maxExpansionSize, int bufferHint) { LodUtil.assertTrue(!uploadMethod.useEarlyMapping, "UploadMethod signal that this should use Mapping instead of uploadBuffer!"); int bbSize = bb.limit() - bb.position(); - LodUtil.assertTrue(bbSize <= maxExpansionSize, "maxExpansionSize is ["+maxExpansionSize+"] but buffer size is ["+bbSize+"]!"); + if (bbSize > maxExpansionSize) + { + LodUtil.assertNotReach("maxExpansionSize is [" + maxExpansionSize + "] but buffer size is [" + bbSize + "]!"); + } GLProxy.GL_LOGGER.debug("Uploading buffer with ["+new UnitBytes(bbSize)+"]."); // Don't upload an empty buffer @@ -219,6 +224,7 @@ public class GLBuffer implements AutoCloseable protected void uploadSubData(ByteBuffer bb, int maxExpansionSize, int bufferDataHint) { LodUtil.assertTrue(!this.bufferStorage, "Buffer is bufferStorage but its trying to use subData upload method!"); + int bbSize = bb.limit() - bb.position(); if (this.size < bbSize || this.size > bbSize * BUFFER_SHRINK_TRIGGER) { @@ -236,7 +242,7 @@ public class GLBuffer implements AutoCloseable // buffer mapping // //================// - public ByteBuffer mapBuffer(int targetSize, EGpuUploadMethod uploadMethod, int maxExpensionSize, int bufferHint, int mapFlags) + public ByteBuffer mapBuffer(int targetSize, EDhApiGpuUploadMethod uploadMethod, int maxExpensionSize, int bufferHint, int mapFlags) { LodUtil.assertTrue(targetSize != 0, "MapBuffer targetSize is 0"); LodUtil.assertTrue(uploadMethod.useEarlyMapping, "Upload method must be one that use early mappings in order to call mapBuffer"); @@ -306,7 +312,7 @@ public class GLBuffer implements AutoCloseable * Makes sure the buffer exists and is of the correct format * before uploading. */ - private void createOrChangeBufferTypeForUpload(EGpuUploadMethod uploadMethod) + private void createOrChangeBufferTypeForUpload(EDhApiGpuUploadMethod uploadMethod) { // create/change the buffer type if necessary if (uploadMethod.useBufferStorage != this.bufferStorage) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/buffer/GLVertexBuffer.java b/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/buffer/GLVertexBuffer.java index 23bf21b0f..05fbe1287 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/buffer/GLVertexBuffer.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/buffer/GLVertexBuffer.java @@ -21,11 +21,9 @@ package com.seibel.distanthorizons.core.render.glObject.buffer; import java.nio.ByteBuffer; -import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; -import org.apache.logging.log4j.Logger; import org.lwjgl.opengl.GL32; -import com.seibel.distanthorizons.api.enums.config.EGpuUploadMethod; +import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod; /** * This is a container for a OpenGL @@ -63,7 +61,7 @@ public class GLVertexBuffer extends GLBuffer @Override public int getBufferBindingTarget() { return GL32.GL_ARRAY_BUFFER; } - public void uploadBuffer(ByteBuffer byteBuffer, int vertCount, EGpuUploadMethod uploadMethod, int maxExpensionSize) + public void uploadBuffer(ByteBuffer byteBuffer, int vertCount, EDhApiGpuUploadMethod uploadMethod, int maxExpensionSize) { if (vertCount < 0) { @@ -79,7 +77,7 @@ public class GLVertexBuffer extends GLBuffer this.vertexCount = vertCount; } - public ByteBuffer mapBuffer(int targetSize, EGpuUploadMethod uploadMethod, int maxExpensionSize) + public ByteBuffer mapBuffer(int targetSize, EDhApiGpuUploadMethod uploadMethod, int maxExpensionSize) { return super.mapBuffer(targetSize, uploadMethod, maxExpensionSize, uploadMethod.useBufferStorage ? GL32.GL_MAP_WRITE_BIT : diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/buffer/QuadElementBuffer.java b/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/buffer/QuadElementBuffer.java index 900bf4f90..915d70c3d 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/buffer/QuadElementBuffer.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/buffer/QuadElementBuffer.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.core.render.glObject.buffer; -import com.seibel.distanthorizons.api.enums.config.EGpuUploadMethod; +import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.render.glObject.GLEnums; import com.seibel.distanthorizons.core.render.glObject.GLProxy; @@ -160,13 +160,13 @@ public class QuadElementBuffer extends GLElementBuffer { bind(); - super.uploadBuffer(buffer, EGpuUploadMethod.DATA, + super.uploadBuffer(buffer, EDhApiGpuUploadMethod.DATA, indicesCount * GLEnums.getTypeSize(type), GL32.GL_STATIC_DRAW); } else { bind(); - super.uploadBuffer(buffer, EGpuUploadMethod.BUFFER_STORAGE, + super.uploadBuffer(buffer, EDhApiGpuUploadMethod.BUFFER_STORAGE, indicesCount * GLEnums.getTypeSize(type), 0); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/DebugRenderer.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/DebugRenderer.java index 7f53323f0..c6fa77bbc 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/DebugRenderer.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/DebugRenderer.java @@ -19,8 +19,8 @@ package com.seibel.distanthorizons.core.render.renderer; -import com.seibel.distanthorizons.api.enums.config.EGpuUploadMethod; -import com.seibel.distanthorizons.api.enums.config.ELoggerMode; +import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod; +import com.seibel.distanthorizons.api.enums.config.EDhApiLoggerMode; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.types.ConfigEntry; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; @@ -56,10 +56,9 @@ public class DebugRenderer { public static DebugRenderer INSTANCE = new DebugRenderer(); - public static final ConfigBasedLogger logger = new ConfigBasedLogger( - LogManager.getLogger(TestRenderer.class), () -> ELoggerMode.LOG_ALL_TO_CHAT); - public static final ConfigBasedSpamLogger spamLogger = new ConfigBasedSpamLogger( - LogManager.getLogger(TestRenderer.class), () -> ELoggerMode.LOG_ALL_TO_CHAT, 1); + public static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(TestRenderer.class), () -> EDhApiLoggerMode.LOG_ALL_TO_CHAT); + public static final ConfigBasedSpamLogger SPAM_LOGGER = new ConfigBasedSpamLogger(LogManager.getLogger(TestRenderer.class), () -> EDhApiLoggerMode.LOG_ALL_TO_CHAT, 1); + private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); @@ -175,7 +174,7 @@ public class DebugRenderer this.boxBuffer = new GLVertexBuffer(false); this.boxBuffer.bind(); - this.boxBuffer.uploadBuffer(buffer, 8, EGpuUploadMethod.DATA, box_vertices.length * Float.BYTES); + this.boxBuffer.uploadBuffer(buffer, 8, EDhApiGpuUploadMethod.DATA, box_vertices.length * Float.BYTES); buffer = ByteBuffer.allocateDirect(box_outline_indices.length * Integer.BYTES); buffer.order(ByteOrder.nativeOrder()); @@ -183,7 +182,7 @@ public class DebugRenderer buffer.rewind(); this.boxOutlineBuffer = new GLElementBuffer(false); - this.boxOutlineBuffer.uploadBuffer(buffer, EGpuUploadMethod.DATA, box_outline_indices.length * Integer.BYTES, GL32.GL_STATIC_DRAW); + this.boxOutlineBuffer.uploadBuffer(buffer, EDhApiGpuUploadMethod.DATA, box_outline_indices.length * Integer.BYTES, GL32.GL_STATIC_DRAW); } public void render(Mat4f transform) @@ -480,18 +479,25 @@ public class DebugRenderer { synchronized (this) { - Iterator> iterator = rendererList.iterator(); - while (iterator.hasNext()) + try { - WeakReference ref = iterator.next(); - IDebugRenderable renderable = ref.get(); - if (renderable == null) + Iterator> iterator = rendererList.iterator(); + while (iterator.hasNext()) { - iterator.remove(); - continue; + WeakReference ref = iterator.next(); + IDebugRenderable renderable = ref.get(); + if (renderable == null) + { + iterator.remove(); + continue; + } + + renderable.debugRender(debugRenderer); } - - renderable.debugRender(debugRenderer); + } + catch (Exception e) + { + SPAM_LOGGER.error("Unexpected Debug renderer error, Error: "+e.getMessage(), e); } } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/LodRenderProgram.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/LodRenderProgram.java index d044b149d..96d106c38 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/LodRenderProgram.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/LodRenderProgram.java @@ -191,7 +191,13 @@ public class LodRenderProgram extends ShaderProgram implements IDhApiShaderProgr // this is to try and allow the fragment culling to go farther than the near clip plane. // Currently this only works for certain FOV/screen ratio combos. dhNearClipDistance *= 2.0f; - setUniform(clipDistanceUniform, dhNearClipDistance); + // if the player is very high up and the near clip plane has been modified, disable the distance clipping + // we're high enough that nothing will render on top of the player and this can cause issues otherwise + if (RenderUtil.getHeightBasedNearClipOverride() != -1) + { + dhNearClipDistance = 1.0f; + } + this.setUniform(this.clipDistanceUniform, dhNearClipDistance); } @Override diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/LodRenderer.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/LodRenderer.java index 0d188446a..584e275f1 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/LodRenderer.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/LodRenderer.java @@ -19,8 +19,7 @@ package com.seibel.distanthorizons.core.render.renderer; -import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass; -import com.seibel.distanthorizons.api.enums.rendering.EFogDrawMode; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiFogDrawMode; import com.seibel.distanthorizons.api.interfaces.override.rendering.IDhApiFramebuffer; import com.seibel.distanthorizons.api.interfaces.override.rendering.IDhApiShaderProgram; import com.seibel.distanthorizons.api.methods.events.abstractEvents.*; @@ -32,7 +31,6 @@ import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.ConfigBasedSpamLogger; import com.seibel.distanthorizons.core.pos.DhBlockPos; -import com.seibel.distanthorizons.core.render.AbstractRenderBuffer; import com.seibel.distanthorizons.core.render.DhApiRenderProxy; import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.glObject.GLProxy; @@ -41,12 +39,11 @@ import com.seibel.distanthorizons.core.render.glObject.buffer.GLVertexBuffer; import com.seibel.distanthorizons.core.render.glObject.buffer.QuadElementBuffer; import com.seibel.distanthorizons.core.render.glObject.texture.*; import com.seibel.distanthorizons.core.render.renderer.shaders.*; -import com.seibel.distanthorizons.core.util.RenderUtil; 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.misc.ILightMapWrapper; -import com.seibel.distanthorizons.api.enums.rendering.EFogColorMode; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiFogColorMode; import com.seibel.distanthorizons.core.render.fog.LodFogConfig; import com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor.AbstractOptifineAccessor; import com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor.IIrisAccessor; @@ -57,8 +54,6 @@ import com.seibel.distanthorizons.coreapi.util.math.Mat4f; import com.seibel.distanthorizons.coreapi.util.math.Vec3d; import com.seibel.distanthorizons.coreapi.util.math.Vec3f; import org.apache.logging.log4j.LogManager; -import org.joml.Matrix4f; -import org.joml.Matrix4fc; import org.lwjgl.opengl.GL32; import java.awt.*; @@ -389,7 +384,7 @@ public class LodRenderer } - if (Config.Client.Advanced.Graphics.Fog.drawMode.get() != EFogDrawMode.FOG_DISABLED) + if (Config.Client.Advanced.Graphics.Fog.drawMode.get() != EDhApiFogDrawMode.FOG_DISABLED) { profiler.popPush("LOD Fog"); @@ -520,13 +515,12 @@ public class LodRenderer GL32.glEnable(GL32.GL_BLEND); GL32.glBlendEquation(GL32.GL_FUNC_ADD); GL32.glBlendFuncSeparate(GL32.GL_SRC_ALPHA, GL32.GL_ONE_MINUS_SRC_ALPHA, GL32.GL_ONE, GL32.GL_ONE_MINUS_SRC_ALPHA); - //GL32.glBlendFunc(GL32.GL_ONE, GL32.GL_ONE_MINUS_SRC_ALPHA); ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeRenderPassEvent.class, renderEventParam); this.bufferHandler.renderTransparent(this, renderEventParam); GL32.glDepthMask(true); // Apparently the depth mask state is stored in the FBO, so glState fails to restore it... - if (Config.Client.Advanced.Graphics.Fog.drawMode.get() != EFogDrawMode.FOG_DISABLED) + if (Config.Client.Advanced.Graphics.Fog.drawMode.get() != EDhApiFogDrawMode.FOG_DISABLED) { profiler.popPush("LOD Fog"); FogShader.INSTANCE.render(partialTicks); @@ -705,7 +699,7 @@ public class LodRenderer if (ENABLE_IBO) { this.quadIBO = new QuadElementBuffer(); - this.quadIBO.reserve(AbstractRenderBuffer.MAX_QUADS_PER_BUFFER); + this.quadIBO.reserve(ColumnRenderBuffer.MAX_QUADS_PER_BUFFER); } @@ -750,8 +744,8 @@ public class LodRenderer this.cachedHeight = MC_RENDER.getTargetFrameBufferViewportHeight(); - ApiEventInjector.INSTANCE.fireAllEvents(DhApiScreenResizeEvent.class, - new DhApiScreenResizeEvent.EventParam( + ApiEventInjector.INSTANCE.fireAllEvents(DhApiColorDepthTextureCreatedEvent.class, + new DhApiColorDepthTextureCreatedEvent.EventParam( oldWidth, oldHeight, this.cachedWidth, this.cachedHeight )); @@ -794,7 +788,7 @@ public class LodRenderer { Color fogColor; - if (Config.Client.Advanced.Graphics.Fog.colorMode.get() == EFogColorMode.USE_SKY_COLOR) + if (Config.Client.Advanced.Graphics.Fog.colorMode.get() == EDhApiFogColorMode.USE_SKY_COLOR) { fogColor = MC_RENDER.getSkyColor(); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/ScreenQuad.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/ScreenQuad.java index c250d69d3..2f9abd294 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/ScreenQuad.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/ScreenQuad.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.core.render.renderer; -import com.seibel.distanthorizons.api.enums.config.EGpuUploadMethod; +import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod; import com.seibel.distanthorizons.core.render.glObject.buffer.GLVertexBuffer; import com.seibel.distanthorizons.core.render.glObject.vertexAttribute.AbstractVertexAttribute; import com.seibel.distanthorizons.core.render.glObject.vertexAttribute.VertexPointer; @@ -93,7 +93,7 @@ public class ScreenQuad this.boxBuffer = new GLVertexBuffer(false); this.boxBuffer.bind(); - this.boxBuffer.uploadBuffer(buffer, box_vertices.length, EGpuUploadMethod.DATA, box_vertices.length * Float.BYTES); + this.boxBuffer.uploadBuffer(buffer, box_vertices.length, EDhApiGpuUploadMethod.DATA, box_vertices.length * Float.BYTES); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/TestRenderer.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/TestRenderer.java index 2c871f892..5d13b95d5 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/TestRenderer.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/TestRenderer.java @@ -19,8 +19,8 @@ package com.seibel.distanthorizons.core.render.renderer; -import com.seibel.distanthorizons.api.enums.config.EGpuUploadMethod; -import com.seibel.distanthorizons.api.enums.config.ELoggerMode; +import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod; +import com.seibel.distanthorizons.api.enums.config.EDhApiLoggerMode; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.ConfigBasedSpamLogger; @@ -45,9 +45,9 @@ public class TestRenderer public TestRenderer() { } public static final ConfigBasedLogger logger = new ConfigBasedLogger( - LogManager.getLogger(TestRenderer.class), () -> ELoggerMode.LOG_ALL_TO_CHAT); + LogManager.getLogger(TestRenderer.class), () -> EDhApiLoggerMode.LOG_ALL_TO_CHAT); public static final ConfigBasedSpamLogger spamLogger = new ConfigBasedSpamLogger( - LogManager.getLogger(TestRenderer.class), () -> ELoggerMode.LOG_ALL_TO_CHAT, 1); + LogManager.getLogger(TestRenderer.class), () -> EDhApiLoggerMode.LOG_ALL_TO_CHAT, 1); private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); ShaderProgram basicShader; @@ -91,7 +91,7 @@ public class TestRenderer buffer.rewind(); GLVertexBuffer vbo = new GLVertexBuffer(false); vbo.bind(); - vbo.uploadBuffer(buffer, 4, EGpuUploadMethod.DATA, vertices.length * Float.BYTES); + vbo.uploadBuffer(buffer, 4, EDhApiGpuUploadMethod.DATA, vertices.length * Float.BYTES); return vbo; } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogShader.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogShader.java index 2d5a3491c..e5a1fc255 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogShader.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogShader.java @@ -19,7 +19,7 @@ package com.seibel.distanthorizons.core.render.renderer.shaders; -import com.seibel.distanthorizons.api.enums.rendering.EFogColorMode; +import com.seibel.distanthorizons.api.enums.rendering.EDhApiFogColorMode; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.render.fog.LodFogConfig; @@ -29,7 +29,6 @@ import com.seibel.distanthorizons.core.render.glObject.shader.ShaderProgram; import com.seibel.distanthorizons.core.render.renderer.LodRenderer; import com.seibel.distanthorizons.core.render.renderer.ScreenQuad; import com.seibel.distanthorizons.core.util.LodUtil; -import com.seibel.distanthorizons.core.util.RenderUtil; import com.seibel.distanthorizons.core.wrapperInterfaces.IVersionConstants; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.coreapi.util.math.Mat4f; @@ -124,7 +123,7 @@ public class FogShader extends AbstractShaderRenderer { Color fogColor; - if (Config.Client.Advanced.Graphics.Fog.colorMode.get() == EFogColorMode.USE_SKY_COLOR) + if (Config.Client.Advanced.Graphics.Fog.colorMode.get() == EDhApiFogColorMode.USE_SKY_COLOR) { fogColor = MC_RENDER.getSkyColor(); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/sql/AbstractDataSourceRepo.java b/core/src/main/java/com/seibel/distanthorizons/core/sql/AbstractDataSourceRepo.java deleted file mode 100644 index 479da8595..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/sql/AbstractDataSourceRepo.java +++ /dev/null @@ -1,168 +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 . - */ - -package com.seibel.distanthorizons.core.sql; - -import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; -import com.seibel.distanthorizons.core.pos.DhSectionPos; - -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.Map; - -public abstract class AbstractDataSourceRepo extends AbstractDhRepo -{ - public AbstractDataSourceRepo(String databaseType, String databaseLocation) throws SQLException - { - super(databaseType, databaseLocation, DataSourceDto.class); - } - - - - @Override - public String getPrimaryKeyName() { return "DhSectionPos"; } - - - //=======================// - // repo required methods // - //=======================// - - @Override - public DataSourceDto convertDictionaryToDto(Map objectMap) throws ClassCastException - { - String posString = (String) objectMap.get("DhSectionPos"); - DhSectionPos pos = DhSectionPos.deserialize(posString); - - // meta data - int checksum = (Integer) objectMap.get("Checksum"); - long dataVersion = (Long) objectMap.get("DataVersion"); - byte dataDetailLevel = (Byte) objectMap.get("DataDetailLevel"); - String worldGenStepString = (String) objectMap.get("WorldGenStep"); - EDhApiWorldGenerationStep worldGenStep = EDhApiWorldGenerationStep.fromName(worldGenStepString); - - String dataType = (String) objectMap.get("DataType"); - byte binaryDataFormatVersion = (Byte) objectMap.get("BinaryDataFormatVersion"); - - // binary data - byte[] dataByteArray = (byte[]) objectMap.get("Data"); - - DataSourceDto dto = new DataSourceDto( - pos, - checksum, dataDetailLevel, worldGenStep, - dataType, binaryDataFormatVersion, - dataByteArray); - return dto; - } - - @Override - public String createSelectPrimaryKeySql(String primaryKey) { return "SELECT * FROM "+this.getTableName()+" WHERE DhSectionPos = '"+primaryKey+"'"; } - - @Override - public PreparedStatement createInsertStatement(DataSourceDto dto) throws SQLException - { - String sql = - "INSERT INTO "+this.getTableName() + "\n" + - " (DhSectionPos, \n" + - "Checksum, DataVersion, DataDetailLevel, WorldGenStep, DataType, BinaryDataFormatVersion, \n" + - "Data) \n" + - " VALUES( \n" + - " ? \n" + - " ,? ,? ,? ,? ,? ,? \n" + - " ,? \n" + - // created/lastModified are automatically set by Sqlite - ");"; - PreparedStatement statement = this.createPreparedStatement(sql); - - int i = 1; - statement.setObject(i++, dto.getPrimaryKeyString()); - - statement.setObject(i++, dto.checksum); - statement.setObject(i++, dto.dataVersion); - statement.setObject(i++, dto.dataDetailLevel); - statement.setObject(i++, dto.worldGenStep); - statement.setObject(i++, dto.dataType); - statement.setObject(i++, dto.binaryDataFormatVersion); - - statement.setObject(i++, dto.dataArray); - - return statement; - } - - @Override - public PreparedStatement createUpdateStatement(DataSourceDto dto) throws SQLException - { - String sql = - "UPDATE "+this.getTableName()+" \n" + - "SET \n" + - " Checksum = ? \n" + - " ,DataVersion = ? \n" + - " ,DataDetailLevel = ? \n" + - " ,WorldGenStep = ? \n" + - " ,DataType = ? \n" + - " ,BinaryDataFormatVersion = ? \n" + - - " ,Data = ? \n" + - - " ,LastModifiedDateTime = CURRENT_TIMESTAMP \n" + - "WHERE DhSectionPos = ?"; - PreparedStatement statement = this.createPreparedStatement(sql); - - int i = 1; - statement.setObject(i++, dto.checksum); - statement.setObject(i++, dto.dataVersion); - statement.setObject(i++, dto.dataDetailLevel); - statement.setObject(i++, dto.worldGenStep); - statement.setObject(i++, dto.dataType); - statement.setObject(i++, dto.binaryDataFormatVersion); - - statement.setObject(i++, dto.dataArray); - - statement.setObject(i++, dto.getPrimaryKeyString()); - - return statement; - } - - - - //=====================// - // data source methods // - //=====================// - - /** - * Returns the highest numerical detail level in this table.
- * Returns {@link DhSectionPos#SECTION_MINIMUM_DETAIL_LEVEL} if no data is present. - */ - public int getMaxSectionDetailLevel() - { - Map resultMap = this.queryDictionaryFirst("select MAX(DataDetailLevel) as maxDetailLevel from DhFullData;"); - int maxDetailLevel; - if (resultMap == null || resultMap.get("maxDetailLevel") == null) - { - maxDetailLevel = 0; - } - else - { - maxDetailLevel = (int)resultMap.get("maxDetailLevel"); - } - - return maxDetailLevel + DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; - } - - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/sql/DatabaseUpdater.java b/core/src/main/java/com/seibel/distanthorizons/core/sql/DatabaseUpdater.java index d41c3cdc1..6d5c70b61 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/sql/DatabaseUpdater.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/sql/DatabaseUpdater.java @@ -20,6 +20,8 @@ package com.seibel.distanthorizons.core.sql; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.sql.dto.IBaseDTO; +import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo; import org.apache.logging.log4j.Logger; import java.io.IOException; @@ -39,6 +41,11 @@ public class DatabaseUpdater public static final String SCHEMA_TABLE_NAME = "Schema"; /** Since java can only run one sql query at a time this string is used to split up our scripts into individual queries. */ public static final String UPDATE_SCRIPT_BATCH_SEPARATOR = "--batch--"; + /** + * If this comment is present anywhere in the auto update script, then transactions won't be used.
+ * This is necessary for some commands that will auto-commit after running, IE: "PRAGMA journal_mode = TRUNCATE" + */ + public static final String UPDATE_SCRIPT_NO_TRANSACTION_FLAG = "--No Transactions--"; private static final String SQL_SCRIPT_RESOURCE_FOLDER = "sqlScripts/"; /** @@ -53,7 +60,7 @@ public class DatabaseUpdater // script running // //================// - public static void runAutoUpdateScripts(AbstractDhRepo repo) throws SQLException + public static > void runAutoUpdateScripts(AbstractDhRepo repo) throws SQLException { // get the resource scripts ArrayList scriptList; @@ -99,48 +106,43 @@ public class DatabaseUpdater // split up each individual statement so Java can handle the script as a whole String[] fileUpdateSqlArray = resource.queryString.split(UPDATE_SCRIPT_BATCH_SEPARATOR); + boolean transactScript = !resource.queryString.contains(UPDATE_SCRIPT_NO_TRANSACTION_FLAG); + + + Connection connection = repo.getConnection(); + connection.setAutoCommit(!transactScript); + try (Statement statement = connection.createStatement()) { + statement.setQueryTimeout(AbstractDhRepo.TIMEOUT_SECONDS); + // adding the scripts to a batched statement allows them to execute together and rollback together if there are any issues for (String updateSql : fileUpdateSqlArray) { - statement.addBatch(updateSql); + statement.execute(updateSql); } - - statement.setQueryTimeout(AbstractDhRepo.TIMEOUT_SECONDS); - int[] numberOfRowsModifiedArray = statement.executeBatch(); - - - // confirm the scripts ran successfully - for (;sqlIndex < numberOfRowsModifiedArray.length; sqlIndex++) + if (transactScript) { - int numberOfRowsModified = numberOfRowsModifiedArray[sqlIndex]; - if (numberOfRowsModified >= 0) - { - // the statement completed successfully - continue; - } - else if (numberOfRowsModified == Statement.EXECUTE_FAILED) - { - LOGGER.error("Execute failed for auto update script: [" + resource.name + "], query: [" + fileUpdateSqlArray[sqlIndex] + "]. Changes have been rolled back.", new SQLException()); - } - else if (numberOfRowsModified == Statement.SUCCESS_NO_INFO) - { - LOGGER.error("Execute failed for auto update script: [" + resource.name + "], query: [" + fileUpdateSqlArray[sqlIndex] + "]. Changes may not have been rolled back.", new SQLException()); - } - else - { - LOGGER.error("Unexpected error state [" + numberOfRowsModified + "] returned for auto update script: [" + resource.name + "], query: [" + fileUpdateSqlArray[sqlIndex] + "].", new SQLException()); - } + connection.commit(); } } catch (SQLException e) { - LOGGER.error("Unexpected SQL Error: ["+e.getMessage()+"] returned for auto update script: [" + resource.name + "], query: [" + fileUpdateSqlArray[sqlIndex] + "].", new SQLException()); + connection.rollback(); + LOGGER.error( + "Unexpected SQL Error: ["+e.getMessage()+"] " + + "returned for auto update script: [" + resource.name + "], " + + "query: [" + fileUpdateSqlArray[sqlIndex] + "]. " + + "Changes should have been rolled back.", new SQLException()); throw e; } + + if (transactScript) + { + connection.setAutoCommit(true); + } } catch (RuntimeException e) { diff --git a/core/src/main/java/com/seibel/distanthorizons/core/sql/RenderDataRepo.java b/core/src/main/java/com/seibel/distanthorizons/core/sql/RenderDataRepo.java deleted file mode 100644 index 8cbec7dfe..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/sql/RenderDataRepo.java +++ /dev/null @@ -1,38 +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 . - */ - -package com.seibel.distanthorizons.core.sql; - -import java.sql.SQLException; - -public class RenderDataRepo extends AbstractDataSourceRepo -{ - public static final String TABLE_NAME = "DhRenderData"; - - - public RenderDataRepo(String databaseType, String databaseLocation) throws SQLException - { - super(databaseType, databaseLocation); - } - - - @Override - public String getTableName() { return TABLE_NAME; } - -} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/sql/DataSourceDto.java b/core/src/main/java/com/seibel/distanthorizons/core/sql/dto/FullDataSourceV1DTO.java similarity index 73% rename from core/src/main/java/com/seibel/distanthorizons/core/sql/DataSourceDto.java rename to core/src/main/java/com/seibel/distanthorizons/core/sql/dto/FullDataSourceV1DTO.java index 8ad1ef558..6c5d1d7ce 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/sql/DataSourceDto.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/sql/dto/FullDataSourceV1DTO.java @@ -17,10 +17,11 @@ * along with this program. If not, see . */ -package com.seibel.distanthorizons.core.sql; +package com.seibel.distanthorizons.core.sql.dto; +import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.interfaces.IFullDataSource; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV1; import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource; import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream; @@ -30,14 +31,13 @@ import java.io.IOException; import java.io.InputStream; import java.util.concurrent.atomic.AtomicLong; -/** handles storing both {@link IFullDataSource}'s and {@link ColumnRenderSource}'s in the database. */ -public class DataSourceDto implements IBaseDTO +/** + * Handles storing{@link FullDataSourceV1}'s in the database. + */ +public class FullDataSourceV1DTO implements IBaseDTO { public DhSectionPos pos; public int checksum; - /** @deprecated the database now has a last modified date time that should be used instead */ - @Deprecated - public AtomicLong dataVersion = new AtomicLong(Long.MAX_VALUE); public byte dataDetailLevel; public EDhApiWorldGenerationStep worldGenStep; @@ -49,7 +49,12 @@ public class DataSourceDto implements IBaseDTO public final byte[] dataArray; - public DataSourceDto(DhSectionPos pos, int checksum, byte dataDetailLevel, EDhApiWorldGenerationStep worldGenStep, String dataType, byte binaryDataFormatVersion, byte[] dataArray) + + //=============// + // constructor // + //=============// + + public FullDataSourceV1DTO(DhSectionPos pos, int checksum, byte dataDetailLevel, EDhApiWorldGenerationStep worldGenStep, String dataType, byte binaryDataFormatVersion, byte[] dataArray) { this.pos = pos; this.checksum = checksum; @@ -63,15 +68,22 @@ public class DataSourceDto implements IBaseDTO } - @Override - public String getPrimaryKeyString() { return this.pos.serialize(); } - /** @return a stream for the data contained in this DTO. */ public DhDataInputStream getInputStream() throws IOException { InputStream inputStream = new ByteArrayInputStream(this.dataArray); - DhDataInputStream compressedStream = new DhDataInputStream(inputStream); + DhDataInputStream compressedStream = new DhDataInputStream(inputStream, EDhApiDataCompressionMode.LZ4); // LZ4 was used by DH before 2.1.0 and as such must be used until the render data format is changed to record the compressor return compressedStream; } + + + //===========// + // overrides // + //===========// + + @Override + public DhSectionPos getKey() { return this.pos; } + + } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/sql/dto/FullDataSourceV2DTO.java b/core/src/main/java/com/seibel/distanthorizons/core/sql/dto/FullDataSourceV2DTO.java new file mode 100644 index 000000000..e59007129 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/sql/dto/FullDataSourceV2DTO.java @@ -0,0 +1,341 @@ +/* + * 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 . + */ + +package com.seibel.distanthorizons.core.sql.dto; + +import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode; +import com.seibel.distanthorizons.api.enums.config.EDhApiWorldCompressionMode; +import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; +import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; +import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream; +import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataOutputStream; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.Adler32; +import java.util.zip.CheckedOutputStream; + +/** handles storing {@link FullDataSourceV2}'s in the database. */ +public class FullDataSourceV2DTO implements IBaseDTO +{ + public DhSectionPos pos; + + public int levelMinY; + + /** only for the data array */ + public int dataChecksum; + + public byte[] compressedDataByteArray; + + /** @see EDhApiWorldGenerationStep */ + public byte[] compressedColumnGenStepByteArray; + /** @see EDhApiWorldCompressionMode */ + public byte[] compressedWorldCompressionModeByteArray; + + public byte[] compressedMappingByteArray; + + public byte dataFormatVersion; + public EDhApiDataCompressionMode compressionModeEnum; + + public boolean applyToParent; + + public long lastModifiedUnixDateTime; + public long createdUnixDateTime; + + + + //=============// + // constructor // + //=============// + + public static FullDataSourceV2DTO CreateFromDataSource(FullDataSourceV2 dataSource, EDhApiDataCompressionMode compressionModeEnum) throws IOException + { + CheckedByteArray checkedDataPointArray = writeDataSourceDataArrayToBlob(dataSource.dataPoints, compressionModeEnum); + byte[] compressedWorldGenStepByteArray = writeGenerationStepsToBlob(dataSource.columnGenerationSteps, compressionModeEnum); + byte[] compressedWorldCompressionModeByteArray = writeWorldCompressionModeToBlob(dataSource.columnWorldCompressionMode, compressionModeEnum); + byte[] mappingByteArray = writeDataMappingToBlob(dataSource.mapping, compressionModeEnum); + + return new FullDataSourceV2DTO( + dataSource.getPos(), + checkedDataPointArray.checksum, compressedWorldGenStepByteArray, compressedWorldCompressionModeByteArray, FullDataSourceV2.DATA_FORMAT_VERSION, compressionModeEnum, checkedDataPointArray.byteArray, + dataSource.lastModifiedUnixDateTime, dataSource.createdUnixDateTime, + mappingByteArray, dataSource.applyToParent, + dataSource.levelMinY + ); + } + + public FullDataSourceV2DTO( + DhSectionPos pos, + int dataChecksum, byte[] compressedColumnGenStepByteArray, byte[] compressedWorldCompressionModeByteArray, byte dataFormatVersion, EDhApiDataCompressionMode compressionModeEnum, byte[] compressedDataByteArray, + long lastModifiedUnixDateTime, long createdUnixDateTime, + byte[] compressedMappingByteArray, boolean applyToParent, + int levelMinY) + { + this.pos = pos; + this.dataChecksum = dataChecksum; + this.compressedColumnGenStepByteArray = compressedColumnGenStepByteArray; + this.compressedWorldCompressionModeByteArray = compressedWorldCompressionModeByteArray; + + this.dataFormatVersion = dataFormatVersion; + this.compressionModeEnum = compressionModeEnum; + + this.compressedDataByteArray = compressedDataByteArray; + this.compressedMappingByteArray = compressedMappingByteArray; + + this.applyToParent = applyToParent; + + this.lastModifiedUnixDateTime = lastModifiedUnixDateTime; + this.createdUnixDateTime = createdUnixDateTime; + + this.levelMinY = levelMinY; + } + + + + //========================// + // data source population // + //========================// + + public FullDataSourceV2 createPooledDataSource(@NotNull ILevelWrapper levelWrapper) throws IOException, InterruptedException + { + FullDataSourceV2 dataSource = FullDataSourceV2.DATA_SOURCE_POOL.getPooledSource(this.pos, false); + return this.populateDataSource(dataSource, levelWrapper); + } + + public FullDataSourceV2 populateDataSource(FullDataSourceV2 dataSource, @NotNull ILevelWrapper levelWrapper) throws IOException, InterruptedException + { return this.internalPopulateDataSource(dataSource, levelWrapper, false); } + + /** + * May be missing one or more data fields.
+ * Designed to be used without access to Minecraft or any supporting objects. + */ + public FullDataSourceV2 createUnitTestDataSource() throws IOException, InterruptedException + { return this.internalPopulateDataSource(FullDataSourceV2.createEmpty(this.pos), null, true); } + + private FullDataSourceV2 internalPopulateDataSource(FullDataSourceV2 dataSource, ILevelWrapper levelWrapper, boolean unitTest) throws IOException, InterruptedException + { + if (FullDataSourceV2.DATA_FORMAT_VERSION != this.dataFormatVersion) + { + throw new IllegalStateException("There should only be one data format ["+FullDataSourceV2.DATA_FORMAT_VERSION+"]."); + } + + dataSource.columnGenerationSteps = readBlobToGenerationSteps(this.compressedColumnGenStepByteArray, this.compressionModeEnum); + dataSource.columnWorldCompressionMode = readBlobToGenerationSteps(this.compressedWorldCompressionModeByteArray, this.compressionModeEnum); + dataSource.dataPoints = readBlobToDataSourceDataArray(this.compressedDataByteArray, this.compressionModeEnum); + + dataSource.mapping.clear(dataSource.getPos()); + // should only be null when used in a unit test + if (!unitTest) + { + if (levelWrapper == null) + { + throw new NullPointerException("No level wrapper present, unable to deserialize data map. This should only be used for unit tests."); + } + + dataSource.mapping.mergeAndReturnRemappedEntityIds(readBlobToDataMapping(this.compressedMappingByteArray, dataSource.getPos(), levelWrapper, this.compressionModeEnum)); + } + + dataSource.lastModifiedUnixDateTime = this.lastModifiedUnixDateTime; + dataSource.createdUnixDateTime = this.createdUnixDateTime; + + dataSource.levelMinY = this.levelMinY; + + dataSource.isEmpty = false; + + return dataSource; + } + + + + //=================// + // (de)serializing // + //=================// + + private static CheckedByteArray writeDataSourceDataArrayToBlob(LongArrayList[] dataArray, EDhApiDataCompressionMode compressionModeEnum) throws IOException + { + // write the outputs to a stream to prep for writing to the database + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + // the order of these streams is important, otherwise the checksum won't be calculated + CheckedOutputStream checkedOut = new CheckedOutputStream(byteArrayOutputStream, new Adler32()); + // normally a DhStream should be the topmost stream to prevent closing the stream accidentally, + // but since this stream will be closed immediately after writing anyway, it won't be an issue + DhDataOutputStream compressedOut = new DhDataOutputStream(checkedOut, compressionModeEnum); + + + // write the data + int dataArrayLength = FullDataSourceV2.WIDTH * FullDataSourceV2.WIDTH; + for (int xz = 0; xz < dataArrayLength; xz++) + { + LongArrayList dataColumn = dataArray[xz]; + + // write column length + short columnLength = (dataColumn != null) ? (short) dataColumn.size() : 0; + // a short is used instead of an int because at most we store 4096 vertical slices and a + // short fits that with less wasted spaces vs an int (short has max value of 32,767 vs int's max of 2 billion) + compressedOut.writeShort(columnLength); + + // write column data (will be skipped if no data was present) + for (int y = 0; y < columnLength; y++) + { + compressedOut.writeLong(dataColumn.getLong(y)); + } + } + + + // generate the checksum + compressedOut.flush(); + int checksum = (int) checkedOut.getChecksum().getValue(); + byteArrayOutputStream.close(); + + return new CheckedByteArray(checksum, byteArrayOutputStream.toByteArray()); + } + private static LongArrayList[] readBlobToDataSourceDataArray(byte[] compressedDataByteArray, EDhApiDataCompressionMode compressionModeEnum) throws IOException + { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(compressedDataByteArray); + DhDataInputStream compressedIn = new DhDataInputStream(byteArrayInputStream, compressionModeEnum); + + + // read the data + int dataArrayLength = FullDataSourceV2.WIDTH * FullDataSourceV2.WIDTH; + LongArrayList[] dataArray = new LongArrayList[dataArrayLength]; + for (int xz = 0; xz < dataArray.length; xz++) + { + // read the column length + short dataColumnLength = compressedIn.readShort(); // separate variables are used for debugging and in case validation wants to be added later + LongArrayList dataColumn = new LongArrayList(new long[dataColumnLength]); + + // read column data (will be skipped if no data was present) + for (int y = 0; y < dataColumnLength; y++) + { + long dataPoint = compressedIn.readLong(); + dataColumn.set(y, dataPoint); + } + + dataArray[xz] = dataColumn; + } + + + return dataArray; + } + + + private static byte[] writeGenerationStepsToBlob(byte[] columnGenStepByteArray, EDhApiDataCompressionMode compressionModeEnum) throws IOException + { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + DhDataOutputStream compressedOut = new DhDataOutputStream(byteArrayOutputStream, compressionModeEnum); + + compressedOut.write(columnGenStepByteArray); + + compressedOut.flush(); + byteArrayOutputStream.close(); + + return byteArrayOutputStream.toByteArray(); + } + private static byte[] readBlobToGenerationSteps(byte[] dataByteArray, EDhApiDataCompressionMode compressionModeEnum) throws IOException, InterruptedException + { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(dataByteArray); + DhDataInputStream compressedIn = new DhDataInputStream(byteArrayInputStream, compressionModeEnum); + + byte[] columnGenStepByteArray = new byte[FullDataSourceV2.WIDTH * FullDataSourceV2.WIDTH]; + compressedIn.readFully(columnGenStepByteArray); + + return columnGenStepByteArray; + } + + + private static byte[] writeWorldCompressionModeToBlob(byte[] worldCompressionModeByteArray, EDhApiDataCompressionMode compressionModeEnum) throws IOException + { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + DhDataOutputStream compressedOut = new DhDataOutputStream(byteArrayOutputStream, compressionModeEnum); + + compressedOut.write(worldCompressionModeByteArray); + + compressedOut.flush(); + byteArrayOutputStream.close(); + + return byteArrayOutputStream.toByteArray(); + } + private static byte[] readBlobToWorldCompressionMode(byte[] dataByteArray, EDhApiDataCompressionMode compressionModeEnum) throws IOException, InterruptedException + { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(dataByteArray); + DhDataInputStream compressedIn = new DhDataInputStream(byteArrayInputStream, compressionModeEnum); + + byte[] worldCompressionModeByteArray = new byte[FullDataSourceV2.WIDTH * FullDataSourceV2.WIDTH]; + compressedIn.readFully(worldCompressionModeByteArray); + + return worldCompressionModeByteArray; + } + + + private static byte[] writeDataMappingToBlob(FullDataPointIdMap mapping, EDhApiDataCompressionMode compressionModeEnum) throws IOException + { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + DhDataOutputStream compressedOut = new DhDataOutputStream(byteArrayOutputStream, compressionModeEnum); + + mapping.serialize(compressedOut); + + compressedOut.flush(); + byteArrayOutputStream.close(); + + return byteArrayOutputStream.toByteArray(); + } + private static FullDataPointIdMap readBlobToDataMapping(byte[] compressedMappingByteArray, DhSectionPos pos, @NotNull ILevelWrapper levelWrapper, EDhApiDataCompressionMode compressionModeEnum) throws IOException, InterruptedException + { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(compressedMappingByteArray); + DhDataInputStream compressedIn = new DhDataInputStream(byteArrayInputStream, compressionModeEnum); + + FullDataPointIdMap mapping = FullDataPointIdMap.deserialize(compressedIn, pos, levelWrapper); + return mapping; + } + + + + //===========// + // overrides // + //===========// + + @Override + public DhSectionPos getKey() { return this.pos; } + + + + //================// + // helper classes // + //================// + + private static class CheckedByteArray + { + public final int checksum; + public final byte[] byteArray; + + public CheckedByteArray(int checksum, byte[] byteArray) + { + this.checksum = checksum; + this.byteArray = byteArray; + } + } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/sql/IBaseDTO.java b/core/src/main/java/com/seibel/distanthorizons/core/sql/dto/IBaseDTO.java similarity index 89% rename from core/src/main/java/com/seibel/distanthorizons/core/sql/IBaseDTO.java rename to core/src/main/java/com/seibel/distanthorizons/core/sql/dto/IBaseDTO.java index e7c1f99af..36405f26b 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/sql/IBaseDTO.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/sql/dto/IBaseDTO.java @@ -17,14 +17,15 @@ * along with this program. If not, see . */ -package com.seibel.distanthorizons.core.sql; +package com.seibel.distanthorizons.core.sql.dto; /** * DTO = DataTable Object
* Any object that's stored in the database should extend this object. */ -public interface IBaseDTO +public interface IBaseDTO { - String getPrimaryKeyString(); + TKey getKey(); + } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/sql/AbstractDhRepo.java b/core/src/main/java/com/seibel/distanthorizons/core/sql/repo/AbstractDhRepo.java similarity index 79% rename from core/src/main/java/com/seibel/distanthorizons/core/sql/AbstractDhRepo.java rename to core/src/main/java/com/seibel/distanthorizons/core/sql/repo/AbstractDhRepo.java index c0a8611b7..cdd059b59 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/sql/AbstractDhRepo.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/sql/repo/AbstractDhRepo.java @@ -17,28 +17,33 @@ * along with this program. If not, see . */ -package com.seibel.distanthorizons.core.sql; +package com.seibel.distanthorizons.core.sql.repo; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.sql.DatabaseUpdater; +import com.seibel.distanthorizons.core.sql.DbConnectionClosedException; +import com.seibel.distanthorizons.core.sql.dto.IBaseDTO; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; import java.sql.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; /** * Handles interfacing with SQL databases. * - * @param DTO stands for "Data Table Object" + * @param DTO stands for "Data Transfer Object" */ -public abstract class AbstractDhRepo +public abstract class AbstractDhRepo> implements AutoCloseable { public static final int TIMEOUT_SECONDS = 30; private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final ConcurrentHashMap CONNECTIONS_BY_CONNECTION_STRING = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap, String> ACTIVE_CONNECTION_STRINGS_BY_REPO = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap, String> ACTIVE_CONNECTION_STRINGS_BY_REPO = new ConcurrentHashMap<>(); private final String connectionString; private final Connection connection; @@ -48,6 +53,11 @@ public abstract class AbstractDhRepo public final Class dtoClass; + protected final ReentrantLock[] saveLockArray; + /** Based on the stack overflow post: https://stackoverflow.com/a/45909920 */ + protected ReentrantLock getSaveLockForKey(TKey key) { return this.saveLockArray[Math.abs(key.hashCode()) % this.saveLockArray.length]; } + + //=============// @@ -62,6 +72,16 @@ public abstract class AbstractDhRepo this.dtoClass = dtoClass; + // the lock array's length is 2x the number of CPU cores so the number of collisions + // should be relatively low without having too many extra locks + int lockCount = Runtime.getRuntime().availableProcessors() * 2; + this.saveLockArray = new ReentrantLock[lockCount]; + for (int i = 0; i < lockCount; i++) + { + this.saveLockArray[i] = new ReentrantLock(); + } + + try { // needed by Forge to load the Java database connection @@ -106,10 +126,9 @@ public abstract class AbstractDhRepo // high level DB // //===============// - public TDTO get(TDTO dto) { return this.getByPrimaryKey(dto.getPrimaryKeyString()); } - public TDTO getByPrimaryKey(String primaryKey) + public TDTO getByKey(TKey primaryKey) { - Map objectMap = this.queryDictionaryFirst(this.createSelectPrimaryKeySql(primaryKey)); + Map objectMap = this.queryDictionaryFirst(this.createSelectByKeySql(primaryKey)); if (objectMap != null && !objectMap.isEmpty()) { return this.convertDictionaryToDto(objectMap); @@ -123,13 +142,27 @@ public abstract class AbstractDhRepo public void save(TDTO dto) { - if (this.getByPrimaryKey(dto.getPrimaryKeyString()) != null) + // a lock is necessary to prevent concurrent modification between + // existsWithKey and insert/update, + // otherwise another thread might cause the insert/update to fail. + ReentrantLock saveLock = this.getSaveLockForKey(dto.getKey()); + + try { - this.update(dto); + saveLock.lock(); + + if (this.existsWithKey(dto.getKey())) + { + this.update(dto); + } + else + { + this.insert(dto); + } } - else + finally { - this.insert(dto); + saveLock.unlock(); } } private void insert(TDTO dto) @@ -140,7 +173,7 @@ public abstract class AbstractDhRepo } catch (DbConnectionClosedException ignored) { - LOGGER.warn("Attempted to insert ["+this.dtoClass.getSimpleName()+"] with primary key ["+(dto != null ? dto.getPrimaryKeyString() : "NULL")+"] on closed repo ["+this.connectionString+"]."); + LOGGER.warn("Attempted to insert ["+this.dtoClass.getSimpleName()+"] with primary key ["+(dto != null ? dto.getKey() : "NULL")+"] on closed repo ["+this.connectionString+"]."); } catch (SQLException e) { @@ -157,7 +190,7 @@ public abstract class AbstractDhRepo } catch (DbConnectionClosedException e) { - LOGGER.warn("Attempted to update ["+this.dtoClass.getSimpleName()+"] with primary key ["+(dto != null ? dto.getPrimaryKeyString() : "NULL")+"] on closed repo ["+this.connectionString+"]."); + LOGGER.warn("Attempted to update ["+this.dtoClass.getSimpleName()+"] with primary key ["+(dto != null ? dto.getKey() : "NULL")+"] on closed repo ["+this.connectionString+"]."); } catch (SQLException e) { @@ -168,10 +201,10 @@ public abstract class AbstractDhRepo } - public void delete(TDTO dto) { this.deleteByPrimaryKey(dto.getPrimaryKeyString()); } - public void deleteByPrimaryKey(String primaryKey) + public void delete(TDTO dto) { this.deleteWithKey(dto.getKey()); } + public void deleteWithKey(TKey key) { - String whereEqualStatement = this.createWherePrimaryKeySql(primaryKey); + String whereEqualStatement = this.createWhereStatement(key); this.queryDictionaryFirst("DELETE FROM "+this.getTableName()+" WHERE "+whereEqualStatement); } @@ -179,10 +212,10 @@ public abstract class AbstractDhRepo public void deleteAll() { this.queryDictionaryFirst("DELETE FROM "+this.getTableName()); } - public boolean exists(TDTO dto) { return this.existsWithPrimaryKey(dto.getPrimaryKeyString()); } - public boolean existsWithPrimaryKey(String primaryKey) + public boolean exists(TDTO dto) { return this.existsWithKey(dto.getKey()); } + public boolean existsWithKey(TKey key) { - String whereEqualStatement = this.createWherePrimaryKeySql(primaryKey); + String whereEqualStatement = this.createWhereStatement(key); Map result = this.queryDictionaryFirst("SELECT EXISTS(SELECT 1 FROM "+this.getTableName()+" WHERE "+whereEqualStatement+") as 'existingCount';"); return result != null && (int)result.get("existingCount") != 0; } @@ -346,6 +379,7 @@ public abstract class AbstractDhRepo } } + @Override public void close() { try @@ -358,9 +392,17 @@ public abstract class AbstractDhRepo { if(this.connection != null) { - LOGGER.info("Closing database connection ["+this.connectionString+"]"); CONNECTIONS_BY_CONNECTION_STRING.remove(this.connectionString); - this.connection.close(); + + if (!this.connection.isClosed()) + { + LOGGER.info("Closing database connection: [" + this.connectionString + "]"); + this.connection.close(); + } + else + { + LOGGER.warn("Attempting to close already closed database connection: [" + this.connectionString + "]"); + } } ACTIVE_CONNECTION_STRINGS_BY_REPO.remove(this); } @@ -378,10 +420,7 @@ public abstract class AbstractDhRepo // helper methods // //================// - /** Example: Id = '0' */ - public String createWherePrimaryKeySql(TDTO dto) { return this.createWherePrimaryKeySql(dto.getPrimaryKeyString()); } - /** Example: Id = '0' */ - public String createWherePrimaryKeySql(String primaryKeyValue) { return this.getPrimaryKeyName()+" = '"+primaryKeyValue+"'"; } + public String createWhereStatement(TDTO dto) { return this.createWhereStatement(dto.getKey()); } public static List> convertResultSetToDictionaryList(ResultSet resultSet) throws SQLException { @@ -439,12 +478,17 @@ public abstract class AbstractDhRepo //==================// public abstract String getTableName(); - public abstract String getPrimaryKeyName(); @Nullable public abstract TDTO convertDictionaryToDto(Map objectMap) throws ClassCastException; - public abstract String createSelectPrimaryKeySql(String primaryKey); + public String createSelectByKeySql(TKey key) { return "SELECT * FROM "+this.getTableName()+" WHERE "+this.createWhereStatement(key); } + /** + * Example: + * Id = '0' + * ColOne = '0' AND ColTwo = '2' + */ + public abstract String createWhereStatement(TKey key); public abstract PreparedStatement createInsertStatement(TDTO dto) throws SQLException; public abstract PreparedStatement createUpdateStatement(TDTO dto) throws SQLException; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/sql/repo/FullDataSourceV1Repo.java b/core/src/main/java/com/seibel/distanthorizons/core/sql/repo/FullDataSourceV1Repo.java new file mode 100644 index 000000000..03a9f232c --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/sql/repo/FullDataSourceV1Repo.java @@ -0,0 +1,286 @@ +/* + * 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 . + */ + +package com.seibel.distanthorizons.core.sql.repo; + +import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; +import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV1DTO; +import com.seibel.distanthorizons.coreapi.util.StringUtil; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class FullDataSourceV1Repo extends AbstractDhRepo +{ + public static final String TABLE_NAME = "Legacy_FullData_V1"; + + + + //=============// + // constructor // + //=============// + + public FullDataSourceV1Repo(String databaseType, String databaseLocation) throws SQLException + { + super(databaseType, databaseLocation, FullDataSourceV1DTO.class); + } + + + + //===========// + // overrides // + //===========// + + @Override + public String getTableName() { return TABLE_NAME; } + + @Override + public String createWhereStatement(DhSectionPos pos) { return "DhSectionPos = '"+pos.serialize()+"'"; } + + + + //=======================// + // repo required methods // + //=======================// + + @Override + public FullDataSourceV1DTO convertDictionaryToDto(Map objectMap) throws ClassCastException + { + String posString = (String) objectMap.get("DhSectionPos"); + DhSectionPos pos = DhSectionPos.deserialize(posString); + + // meta data + int checksum = (Integer) objectMap.get("Checksum"); + byte dataDetailLevel = (Byte) objectMap.get("DataDetailLevel"); + String worldGenStepString = (String) objectMap.get("WorldGenStep"); + EDhApiWorldGenerationStep worldGenStep = EDhApiWorldGenerationStep.fromName(worldGenStepString); + + String dataType = (String) objectMap.get("DataType"); + byte binaryDataFormatVersion = (Byte) objectMap.get("BinaryDataFormatVersion"); + + // binary data + byte[] dataByteArray = (byte[]) objectMap.get("Data"); + + FullDataSourceV1DTO dto = new FullDataSourceV1DTO( + pos, + checksum, dataDetailLevel, worldGenStep, + dataType, binaryDataFormatVersion, + dataByteArray); + return dto; + } + + @Override + public PreparedStatement createInsertStatement(FullDataSourceV1DTO dto) throws SQLException + { + String sql = + "INSERT INTO "+this.getTableName() + "\n" + + " (DhSectionPos, \n" + + "Checksum, DataVersion, DataDetailLevel, WorldGenStep, DataType, BinaryDataFormatVersion, \n" + + "Data) \n" + + " VALUES( \n" + + " ? \n" + + " ,? ,? ,? ,? ,? ,? \n" + + " ,? \n" + + // created/lastModified are automatically set by Sqlite + ");"; + PreparedStatement statement = this.createPreparedStatement(sql); + + int i = 1; + statement.setObject(i++, dto.pos.serialize()); + + statement.setObject(i++, dto.checksum); + statement.setObject(i++, 0 /*dto.dataVersion*/); + statement.setObject(i++, dto.dataDetailLevel); + statement.setObject(i++, dto.worldGenStep); + statement.setObject(i++, dto.dataType); + statement.setObject(i++, dto.binaryDataFormatVersion); + + statement.setObject(i++, dto.dataArray); + + return statement; + } + + @Override + public PreparedStatement createUpdateStatement(FullDataSourceV1DTO dto) throws SQLException + { + String sql = + "UPDATE "+this.getTableName()+" \n" + + "SET \n" + + " Checksum = ? \n" + + " ,DataVersion = ? \n" + + " ,DataDetailLevel = ? \n" + + " ,WorldGenStep = ? \n" + + " ,DataType = ? \n" + + " ,BinaryDataFormatVersion = ? \n" + + + " ,Data = ? \n" + + + " ,LastModifiedDateTime = CURRENT_TIMESTAMP \n" + + "WHERE DhSectionPos = ?"; + PreparedStatement statement = this.createPreparedStatement(sql); + + int i = 1; + statement.setObject(i++, dto.checksum); + statement.setObject(i++, 0 /*dto.dataVersion*/); + statement.setObject(i++, dto.dataDetailLevel); + statement.setObject(i++, dto.worldGenStep); + statement.setObject(i++, dto.dataType); + statement.setObject(i++, dto.binaryDataFormatVersion); + + statement.setObject(i++, dto.dataArray); + + statement.setObject(i++, dto.pos.serialize()); + + return statement; + } + + + + //=====================// + // data source methods // + //=====================// + + /** + * Returns the highest numerical detail level in this table.
+ * Returns {@link DhSectionPos#SECTION_MINIMUM_DETAIL_LEVEL} if no data is present. + */ + public int getMaxSectionDetailLevel() + { + Map resultMap = this.queryDictionaryFirst("select MAX(DataDetailLevel) as maxDetailLevel from "+this.getTableName()+";"); + int maxDetailLevel; + if (resultMap == null || resultMap.get("maxDetailLevel") == null) + { + maxDetailLevel = 0; + } + else + { + maxDetailLevel = (int)resultMap.get("maxDetailLevel"); + } + + return maxDetailLevel + DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; + } + + + + //===========// + // migration // + //===========// + + /** Returns how many positions need to be migrated over to the new version */ + public long getMigrationCount() + { + Map resultMap = this.queryDictionaryFirst( + "select COUNT(*) as itemCount from "+this.getTableName()+" where MigrationFailed <> 1"); + + if (resultMap == null) + { + return 0; + } + else + { + Number resultNumber = (Number) resultMap.get("itemCount"); + long count = resultNumber.longValue(); + return count; + } + } + + /** Returns the new "returnCount" positions that need to be migrated */ + public ArrayList getPositionsToMigrate(int returnCount) + { + ArrayList list = new ArrayList<>(); + + List> resultMapList = this.queryDictionary( + "select DhSectionPos " + + "from "+this.getTableName()+" " + + "WHERE MigrationFailed <> 1 " + + "LIMIT "+returnCount+";"); + + for (Map resultMap : resultMapList) + { + // returned in the format [sectionDetailLevel,x,z] IE [6,0,0] + DhSectionPos sectionPos = DhSectionPos.deserialize((String) resultMap.get("DhSectionPos")); + list.add(sectionPos); + } + + return list; + } + + public void markMigrationFailed(DhSectionPos pos) + { + String sql = + "UPDATE "+this.getTableName()+" \n" + + "SET MigrationFailed = 1 \n" + + "WHERE DhSectionPos = '"+pos.serialize()+"'"; + + this.queryDictionaryFirst(sql); + } + + + + //======================// + // migration - deletion // + //======================// + + /** returns the number of data sources that should be deleted */ + public long getUnusedDataSourceCount() + { + Map resultMap = this.queryDictionaryFirst( + "select Count(*) as unusedCount from "+this.getTableName()+" where DataDetailLevel <> 0 or DataType <> 'CompleteFullDataSource'"); + + if (resultMap != null) + { + // Number cast is necessary because the returned number can be an int or long + Number resultNumber = (Number) resultMap.get("unusedCount"); + long count = resultNumber.longValue(); + return count; + } + else + { + return 0; + } + } + + /** Returns single quote surrounded {@link DhSectionPos} serailzed values */ + public ArrayList getUnusedDataSourcePositionStringList(int deleteCount) + { + List> deletePosResultMapList = this.queryDictionary( + "select DhSectionPos from "+this.getTableName()+" where DataDetailLevel <> 0 or DataType <> 'CompleteFullDataSource' limit "+deleteCount); + + ArrayList deletePosList = new ArrayList<>(); + for (Map deletePosMap : deletePosResultMapList) + { + String posString = (String) deletePosMap.get("DhSectionPos"); + deletePosList.add("'"+posString+"'"); + } + + return deletePosList; + } + + /** Expects positions to already be surrounded in single quotes */ + public void deleteUnusedLegacyData(ArrayList deletePosList) + { + String sectionPosCsv = StringUtil.join(",", deletePosList); + this.queryDictionaryFirst("delete from " + this.getTableName() + " where DhSectionPos in (" + sectionPosCsv + ")"); + } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/sql/repo/FullDataSourceV2Repo.java b/core/src/main/java/com/seibel/distanthorizons/core/sql/repo/FullDataSourceV2Repo.java new file mode 100644 index 000000000..619581a14 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/sql/repo/FullDataSourceV2Repo.java @@ -0,0 +1,357 @@ +/* + * 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 . + */ + +package com.seibel.distanthorizons.core.sql.repo; + +import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO; +import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream; +import org.apache.logging.log4j.Logger; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class FullDataSourceV2Repo extends AbstractDhRepo +{ + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + + + + //=============// + // constructor // + //=============// + + public FullDataSourceV2Repo(String databaseType, String databaseLocation) throws SQLException + { + super(databaseType, databaseLocation, FullDataSourceV2DTO.class); + } + + + + //===========// + // overrides // + //===========// + + @Override + public String getTableName() { return "FullData"; } + + @Override + public String createWhereStatement(DhSectionPos pos) + { + int detailLevel = pos.getDetailLevel() - DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL; + return "DetailLevel = '"+detailLevel+"' AND PosX = '"+pos.getX()+"' AND PosZ = '"+pos.getZ()+"'"; + } + + + + //=======================// + // repo required methods // + //=======================// + + @Override + public FullDataSourceV2DTO convertDictionaryToDto(Map objectMap) throws ClassCastException + { + byte detailLevel = (Byte) objectMap.get("DetailLevel"); + byte sectionDetailLevel = (byte) (detailLevel + DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); + int posX = (Integer) objectMap.get("PosX"); + int posZ = (Integer) objectMap.get("PosZ"); + DhSectionPos pos = new DhSectionPos(sectionDetailLevel, posX, posZ); + + int minY = (Integer) objectMap.get("MinY"); + int dataChecksum = (Integer) objectMap.get("DataChecksum"); + + byte[] dataByteArray = (byte[]) objectMap.get("Data"); + byte[] columnGenStepByteArray = (byte[]) objectMap.get("ColumnGenerationStep"); + byte[] columnWorldCompressionByteArray = (byte[]) objectMap.get("ColumnWorldCompressionMode"); + byte[] mappingByteArray = (byte[]) objectMap.get("Mapping"); + + + byte dataFormatVersion = (Byte) objectMap.get("DataFormatVersion"); + byte compressionMode = (Byte) objectMap.get("CompressionMode"); + EDhApiDataCompressionMode compressionModeEnum = EDhApiDataCompressionMode.getFromValue(compressionMode); + + boolean applyToParent = ((int) objectMap.get("ApplyToParent")) == 1; + + long lastModifiedUnixDateTime = (Long) objectMap.get("LastModifiedUnixDateTime"); + long createdUnixDateTime = (Long) objectMap.get("CreatedUnixDateTime"); + + FullDataSourceV2DTO dto = new FullDataSourceV2DTO( + pos, + dataChecksum, columnGenStepByteArray, columnWorldCompressionByteArray, dataFormatVersion, compressionModeEnum, dataByteArray, + lastModifiedUnixDateTime, createdUnixDateTime, + mappingByteArray, applyToParent, + minY); + return dto; + } + + @Override + public PreparedStatement createInsertStatement(FullDataSourceV2DTO dto) throws SQLException + { + String sql = + "INSERT INTO "+this.getTableName() + " (\n" + + " DetailLevel, PosX, PosZ, \n" + + " MinY, DataChecksum, \n" + + " Data, ColumnGenerationStep, ColumnWorldCompressionMode, Mapping, \n" + + " DataFormatVersion, CompressionMode, ApplyToParent, \n" + + " LastModifiedUnixDateTime, CreatedUnixDateTime) \n" + + "VALUES( \n" + + " ?, ?, ?, \n" + + " ?, ?, \n" + + " ?, ?, ?, ?, \n" + + " ?, ?, ?, \n" + + " ?, ? \n" + + ");"; + PreparedStatement statement = this.createPreparedStatement(sql); + + int i = 1; + statement.setObject(i++, dto.pos.getDetailLevel() - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); + statement.setObject(i++, dto.pos.getX()); + statement.setObject(i++, dto.pos.getZ()); + + statement.setObject(i++, dto.levelMinY); + statement.setObject(i++, dto.dataChecksum); + + statement.setObject(i++, dto.compressedDataByteArray); + statement.setObject(i++, dto.compressedColumnGenStepByteArray); + statement.setObject(i++, dto.compressedWorldCompressionModeByteArray); + statement.setObject(i++, dto.compressedMappingByteArray); + + statement.setObject(i++, dto.dataFormatVersion); + statement.setObject(i++, dto.compressionModeEnum.value); + statement.setObject(i++, dto.applyToParent); + + statement.setObject(i++, System.currentTimeMillis()); // last modified unix time + statement.setObject(i++, System.currentTimeMillis()); // created unix time + + return statement; + } + + @Override + public PreparedStatement createUpdateStatement(FullDataSourceV2DTO dto) throws SQLException + { + String sql = + "UPDATE "+this.getTableName()+" \n" + + "SET \n" + + " MinY = ? \n" + + " ,DataChecksum = ? \n" + + + " ,Data = ? \n" + + " ,ColumnGenerationStep = ? \n" + + " ,ColumnWorldCompressionMode = ? \n" + + " ,Mapping = ? \n" + + + " ,DataFormatVersion = ? \n" + + " ,CompressionMode = ? \n" + + " ,ApplyToParent = ? \n" + + + " ,LastModifiedUnixDateTime = ? \n" + + " ,CreatedUnixDateTime = ? \n" + + + "WHERE DetailLevel = ? AND PosX = ? AND PosZ = ?"; + PreparedStatement statement = this.createPreparedStatement(sql); + + int i = 1; + statement.setObject(i++, dto.levelMinY); + statement.setObject(i++, dto.dataChecksum); + + statement.setObject(i++, dto.compressedDataByteArray); + statement.setObject(i++, dto.compressedColumnGenStepByteArray); + statement.setObject(i++, dto.compressedWorldCompressionModeByteArray); + statement.setObject(i++, dto.compressedMappingByteArray); + + statement.setObject(i++, dto.dataFormatVersion); + statement.setObject(i++, dto.compressionModeEnum.value); + statement.setObject(i++, dto.applyToParent); + + statement.setObject(i++, System.currentTimeMillis()); // last modified unix time + statement.setObject(i++, dto.createdUnixDateTime); + + statement.setObject(i++, dto.pos.getDetailLevel() - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); + statement.setObject(i++, dto.pos.getX()); + statement.setObject(i++, dto.pos.getZ()); + + return statement; + } + + + + // updates // + + public void setApplyToParent(DhSectionPos pos, boolean applyToParent) throws SQLException + { + int detailLevel = pos.getDetailLevel() - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; + + String sql = + "UPDATE "+this.getTableName()+" \n" + + "SET ApplyToParent = "+applyToParent+" \n" + + "WHERE DetailLevel = "+detailLevel+" AND PosX = "+pos.getX()+" AND PosZ = "+pos.getZ(); + + this.queryDictionaryFirst(sql); + } + + public ArrayList getPositionsToUpdate(int returnCount) + { + ArrayList list = new ArrayList<>(); + + List> resultMapList = this.queryDictionary( + "select DetailLevel, PosX, PosZ " + + "from "+this.getTableName()+" " + + "where ApplyToParent = 1 " + + "order by DetailLevel asc LIMIT "+returnCount+";"); + + for (Map resultMap : resultMapList) + { + byte detailLevel = (Byte) resultMap.get("DetailLevel"); + byte sectionDetailLevel = (byte) (detailLevel + DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); + int posX = (Integer) resultMap.get("PosX"); + int posZ = (Integer) resultMap.get("PosZ"); + + DhSectionPos pos = new DhSectionPos(sectionDetailLevel, posX, posZ); + list.add(pos); + } + + return list; + } + + /** @return null if nothing exists for this position */ + public byte[] getColumnGenerationStepForPos(DhSectionPos pos) + { + int detailLevel = pos.getDetailLevel() - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; + + Map resultMap = this.queryDictionaryFirst( + "select ColumnGenerationStep, CompressionMode " + + "from "+this.getTableName()+" " + + "WHERE DetailLevel = "+detailLevel+" AND PosX = "+pos.getX()+" AND PosZ = "+pos.getZ()); + + if (resultMap != null) + { + byte[] compressedByteArray = (byte[]) resultMap.get("ColumnGenerationStep"); + + byte compressionModeEnumValue = (byte) resultMap.get("CompressionMode"); + EDhApiDataCompressionMode compressionModeEnum = EDhApiDataCompressionMode.getFromValue(compressionModeEnumValue); + + try + { + // decompress the data + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(compressedByteArray); + DhDataInputStream compressedIn = new DhDataInputStream(byteArrayInputStream, compressionModeEnum); + + byte[] columnGenStepByteArray = new byte[FullDataSourceV2.WIDTH * FullDataSourceV2.WIDTH]; + compressedIn.readFully(columnGenStepByteArray); + + return columnGenStepByteArray; + } + catch (IOException e) + { + LOGGER.warn("Decompression issue when getting column gen steps for pos: "+pos, e); + return null; + } + } + else + { + return null; + } + } + + + + //===================// + // compression tests // + //===================// + + /** @return every position in this database */ + public ArrayList getAllPositions() + { + ArrayList list = new ArrayList<>(); + + List> resultMapList = this.queryDictionary( + "select DetailLevel, PosX, PosZ " + + "from "+this.getTableName()+"; "); + + for (Map resultMap : resultMapList) + { + byte detailLevel = (Byte) resultMap.get("DetailLevel"); + byte sectionDetailLevel = (byte) (detailLevel + DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); + int posX = (Integer) resultMap.get("PosX"); + int posZ = (Integer) resultMap.get("PosZ"); + + DhSectionPos pos = new DhSectionPos(sectionDetailLevel, posX, posZ); + list.add(pos); + } + + return list; + } + + /** + * @return the size of the full data at the given position + * (doesn't include the size of the mapping or any other column) + */ + public long getDataSizeInBytes(DhSectionPos pos) + { + int detailLevel = pos.getDetailLevel() - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; + + Map resultMap = this.queryDictionaryFirst( + "select LENGTH(Data) as dataSize " + + "from "+this.getTableName()+" " + + "WHERE DetailLevel = "+detailLevel+" AND PosX = "+pos.getX()+" AND PosZ = "+pos.getZ()); + + if (resultMap != null && resultMap.get("dataSize") != null) + { + // Number cast is necessary because the returned number can be an int or long + Number resultNumber = (Number) resultMap.get("dataSize"); + long dataLength = resultNumber.longValue(); + return dataLength; + + } + else + { + return 0; + } + } + + /** @return the total size in bytes of the full data for this entire database */ + public long getTotalDataSizeInBytes() + { + Map resultMap = this.queryDictionaryFirst( + "select SUM(LENGTH(Data)) as dataSize " + + "from "+this.getTableName()+"; "); + + if (resultMap != null && resultMap.get("dataSize") != null) + { + Number resultNumber = (Number) resultMap.get("dataSize"); + long dataLength = resultNumber.longValue(); + return dataLength; + + } + else + { + return 0; + } + } + + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/ColorUtil.java b/core/src/main/java/com/seibel/distanthorizons/core/util/ColorUtil.java index 89ec90fa9..222fcbb9b 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/ColorUtil.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/ColorUtil.java @@ -41,6 +41,7 @@ public class ColorUtil public static final int RED = rgbToInt(255, 0, 0); public static final int DARK_RED = rgbToInt(100, 0, 0); public static final int GREEN = rgbToInt(0, 255, 0); + public static final int DARK_GREEN = rgbToInt(80, 140, 80); public static final int BLUE = rgbToInt(0, 0, 255); public static final int YELLOW = rgbToInt(255, 255, 0); public static final int CYAN = rgbToInt(0, 255, 255); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/FullDataPointUtil.java b/core/src/main/java/com/seibel/distanthorizons/core/util/FullDataPointUtil.java index c02f567af..b63c6f0e7 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/FullDataPointUtil.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/FullDataPointUtil.java @@ -1,111 +1,159 @@ -/* - * 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 . - */ - package com.seibel.distanthorizons.core.util; -// - +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV1; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; +import com.seibel.distanthorizons.coreapi.ModInfo; import org.jetbrains.annotations.Contract; /** * A helper class that is used to access the data from a long * formatted as a full data point.
* A full data point contains the most information and is the - * base truth used when creating render data.

+ * source of truth used when creating render data.

* - * To access data from a long formatted as a render data point see: {@link RenderDataPointUtil}

+ * Specifically used by the data sources:
+ * - {@link FullDataSourceV2}
+ * - {@link FullDataSourceV1} aka CompleteFullDataSource
+ * - (Deleted) HighDetailIncompleteFullDataSource aka SparseDataSource
+ * - (Deleted) LowDetailIncompleteFullDataSource aka SpottyDataSource

* * DataPoint Format:
* * ID: blockState id
- * Y: Height(signed)
- * DP: Depth (Depth means the length of the block!)
+ * MY: Min Y Height (unsigned, relative to the minimum level height)
+ * HI: Height (how tall this data point is in blocks)
* BL: Block light
* SL: Sky light

* * =======Bit layout=======
- * BL BL BL BL SL SL SL SL <-- Top bits
- * YY YY YY YY YY YY YY YY
- * YY YY YY YY DP DP DP DP
- * DP DP DP DP DP DP DP DP
- * ID ID ID ID ID ID IO ID
- * ID ID ID ID ID ID IO ID
- * ID ID ID ID ID ID IO ID
- * ID ID ID ID ID ID IO ID <-- Bottom bits
+ * SL SL SL SL BL BL BL BL <-- Top bits
+ * MY MY MY MY MY MY MY MY
+ * MY MY MY MY HI HI HI HI
+ * HI HI HI HI HI HI HI HI
+ * ID ID ID ID ID ID ID ID
+ * ID ID ID ID ID ID ID ID
+ * ID ID ID ID ID ID ID ID
+ * ID ID ID ID ID ID ID ID <-- Bottom bits
*
* - * @see RenderDataPointUtil + * @see FullDataSourceV1 + * @see FullDataSourceV2 */ public class FullDataPointUtil { + public static final boolean RUN_VALIDATION = ModInfo.IS_DEV_BUILD; + /** Represents the data held by an empty data point */ public static final int EMPTY_DATA_POINT = 0; public static final int ID_WIDTH = 32; - public static final int DP_WIDTH = 12; - public static final int Y_WIDTH = 12; - public static final int LIGHT_WIDTH = 8; + public static final int HEIGHT_WIDTH = 12; + public static final int MIN_Y_WIDTH = 12; + public static final int SKY_LIGHT_WIDTH = 4; + public static final int BLOCK_LIGHT_WIDTH = 4; + public static final int ID_OFFSET = 0; - public static final int DP_OFFSET = ID_OFFSET + ID_WIDTH; + public static final int HEIGHT_OFFSET = ID_OFFSET + ID_WIDTH; /** indicates the Y position where the LOD starts relative to the level's minimum height */ - public static final int Y_OFFSET = DP_OFFSET + DP_WIDTH; - public static final int LIGHT_OFFSET = Y_OFFSET + Y_WIDTH; + public static final int MIN_Y_OFFSET = HEIGHT_OFFSET + HEIGHT_WIDTH; + public static final int SKY_LIGHT_OFFSET = MIN_Y_OFFSET + MIN_Y_WIDTH; + public static final int BLOCK_LIGHT_OFFSET = SKY_LIGHT_OFFSET + SKY_LIGHT_WIDTH; public static final long ID_MASK = Integer.MAX_VALUE; public static final long INVERSE_ID_MASK = ~ID_MASK; - public static final int DP_MASK = (int) Math.pow(2, DP_WIDTH) - 1; - public static final int Y_MASK = (int) Math.pow(2, Y_WIDTH) - 1; - public static final int LIGHT_MASK = (int) Math.pow(2, LIGHT_WIDTH) - 1; + public static final int HEIGHT_MASK = (int) Math.pow(2, HEIGHT_WIDTH) - 1; + public static final int MIN_Y_MASK = (int) Math.pow(2, MIN_Y_WIDTH) - 1; + public static final int SKY_LIGHT_MASK = (int) Math.pow(2, SKY_LIGHT_WIDTH) - 1; + public static final int BLOCK_LIGHT_MASK = (int) Math.pow(2, BLOCK_LIGHT_WIDTH) - 1; - /** creates a new datapoint with the given values */ - public static long encode(int id, int depth, int y, byte lightPair) + /** + * creates a new datapoint with the given values + * @param relMinY relative to the minimum level Y value + */ + public static long encode(int id, int height, int relMinY, byte blockLight, byte skyLight) { - LodUtil.assertTrue(y >= 0 && y < RenderDataPointUtil.MAX_WORLD_Y_SIZE, "Trying to create datapoint with y[{}] out of range!", y); - LodUtil.assertTrue(depth > 0 && depth < RenderDataPointUtil.MAX_WORLD_Y_SIZE, "Trying to create datapoint with depth[{}] out of range!", depth); - LodUtil.assertTrue(y + depth <= RenderDataPointUtil.MAX_WORLD_Y_SIZE, "Trying to create datapoint with y+depth[{}] out of range!", y + depth); + if (RUN_VALIDATION) + { + // assertions are inside if-blocks to prevent unnecessary string concatenations + if (relMinY < 0 || relMinY >= RenderDataPointUtil.MAX_WORLD_Y_SIZE) + { + LodUtil.assertNotReach("Trying to create datapoint with y[" + relMinY + "] out of range!"); + } + if (height <= 0 || height >= RenderDataPointUtil.MAX_WORLD_Y_SIZE) + { + LodUtil.assertNotReach("Trying to create datapoint with height[" + height + "] out of range!"); + } + if (relMinY + height > RenderDataPointUtil.MAX_WORLD_Y_SIZE) + { + LodUtil.assertNotReach("Trying to create datapoint with y+depth[" + (relMinY + height) + "] out of range!"); + } + } + long data = 0; data |= id & ID_MASK; - data |= (long) (depth & DP_MASK) << DP_OFFSET; - data |= (long) (y & Y_MASK) << Y_OFFSET; - data |= (long) lightPair << LIGHT_OFFSET; - LodUtil.assertTrue(getId(data) == id && getHeight(data) == depth && getBottomY(data) == y && getLight(data) == Byte.toUnsignedInt(lightPair), - "Trying to create datapoint with id[{}], depth[{}], y[{}], lightPair[{}] but got id[{}], depth[{}], y[{}], lightPair[{}]!", - id, depth, y, Byte.toUnsignedInt(lightPair), getId(data), getHeight(data), getBottomY(data), getLight(data)); + data |= (long) (height & HEIGHT_MASK) << HEIGHT_OFFSET; + data |= (long) (relMinY & MIN_Y_MASK) << MIN_Y_OFFSET; + data |= (long) blockLight << BLOCK_LIGHT_OFFSET; + data |= (long) skyLight << SKY_LIGHT_OFFSET; + + + if (RUN_VALIDATION) + { + if (getId(data) != id || getHeight(data) != height || getBottomY(data) != relMinY + || getBlockLight(data) != Byte.toUnsignedInt(blockLight) || getSkyLight(data) != Byte.toUnsignedInt(skyLight)) + { + LodUtil.assertNotReach( + "Trying to create datapoint with " + + "id[" + id + "], height[" + height + "], minY[" + relMinY + "], blockLight[" + blockLight + "], skyLight[" + skyLight + "] " + + "but got " + + "id[" + getId(data) + "], height[" + getHeight(data) + "], minY[" + getBottomY(data) + "], blockLight[" + getBlockLight(data) + "], skyLight[" + getSkyLight(data) + "]!"); + } + } return data; } + /** Returns the BlockState/Biome pair ID used to identify this LOD's color */ public static int getId(long data) { return (int) (data & ID_MASK); } /** Returns how many blocks tall this LOD is. */ - public static int getHeight(long data) { return (int) ((data >> DP_OFFSET) & DP_MASK); } - /** Returns the block position of the bottom vertices for this LOD relative to the level's minimum height. */ - public static int getBottomY(long data) { return (int) ((data >> Y_OFFSET) & Y_MASK); } - public static int getLight(long data) { return (int) ((data >> LIGHT_OFFSET) & LIGHT_MASK); } + public static int getHeight(long data) { return (int) ((data >> HEIGHT_OFFSET) & HEIGHT_MASK); } + /** + * Returns the unsigned block position of the bottom vertices for this LOD relative to the level's minimum height. + * Should be between 0 and {@link RenderDataPointUtil#MAX_WORLD_Y_SIZE} + */ + public static int getBottomY(long data) { return (int) ((data >> MIN_Y_OFFSET) & MIN_Y_MASK); } + public static int getBlockLight(long data) { return (int) ((data >> BLOCK_LIGHT_OFFSET) & BLOCK_LIGHT_MASK); } + public static int getSkyLight(long data) { return (int) ((data >> SKY_LIGHT_OFFSET) & SKY_LIGHT_MASK); } + + public static long setBlockLight(long data, byte blockLight) { return (data & ~((long) BLOCK_LIGHT_MASK << BLOCK_LIGHT_OFFSET) | (long) blockLight << BLOCK_LIGHT_OFFSET); } + + public static String toString(long data) { return "[ID:" + getId(data) + ",Y:" + getBottomY(data) + ",Height:" + getHeight(data) + ",BlockLight:" + getBlockLight(data) + ",SkyLight:" + getSkyLight(data) + "]"; } + - public static String toString(long data) { return "[ID:" + getId(data) + ",Y:" + getBottomY(data) + ",Height:" + getHeight(data) + ",Light:" + getLight(data) + "]"; } /** Remaps the biome/blockState ID of the given datapoint */ @Contract(pure = true) - public static long remap(int[] newIdMapping, long data) { return (data & INVERSE_ID_MASK) | newIdMapping[(int) data]; } + public static long remap(int[] newIdMapping, long data) throws IndexOutOfBoundsException + { + int currentId = getId(data); + try + { + int newId = newIdMapping[currentId]; + return (data & INVERSE_ID_MASK) | newId; + } + catch (IndexOutOfBoundsException e) + { + // this try-catch is present to fix an issue where the stack trace is missing + // and to allow for easily attaching a debugger + + // if this was thrown that probably means the datasource has been + // re-mapped multiple times, causing the ID's to go out of their expected bounds. + throw new RuntimeException(e); + } + } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/LodUtil.java b/core/src/main/java/com/seibel/distanthorizons/core/util/LodUtil.java index edfdc8cf1..7b43cf6a8 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/LodUtil.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/LodUtil.java @@ -24,7 +24,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.CompletionException; import java.util.concurrent.RejectedExecutionException; -import com.seibel.distanthorizons.api.enums.config.EVanillaOverdraw; +import com.seibel.distanthorizons.api.enums.config.EDhApiVanillaOverdraw; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.pos.DhChunkPos; @@ -118,6 +118,13 @@ public class LodUtil public static final int REGION_WIDTH_IN_CHUNKS = REGION_WIDTH / CHUNK_WIDTH; + /** maximum possible light level handled by Minecraft */ + public static final byte MAX_MC_LIGHT = 15; + /** lowest possible light level handled by Minecraft */ + public static final byte MIN_MC_LIGHT = 0; + + + /** * This regex finds any characters that are invalid for use in a windows * (and by extension mac and linux) file path @@ -172,10 +179,11 @@ public class LodUtil public static int computeOverdrawOffset() { int chunkRenderDist = MC_RENDER.getRenderDistance() + 1; - EVanillaOverdraw overdraw = EVanillaOverdraw.ALWAYS; //Config.Client.Advanced.Graphics.AdvancedGraphics.vanillaOverdraw.get(); - if (overdraw == EVanillaOverdraw.ALWAYS) return Integer.MAX_VALUE; + EDhApiVanillaOverdraw overdraw = EDhApiVanillaOverdraw.ALWAYS; //Config.Client.Advanced.Graphics.AdvancedGraphics.vanillaOverdraw.get(); + if (overdraw == EDhApiVanillaOverdraw.ALWAYS) return Integer.MAX_VALUE; + int offset; - if (overdraw == EVanillaOverdraw.NEVER) + if (overdraw == EDhApiVanillaOverdraw.NEVER) { offset = 0; //Config.Client.Advanced.Graphics.AdvancedGraphics.overdrawOffset.get(); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointUtil.java b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointUtil.java index 1a110fc2a..72bb1864f 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointUtil.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointUtil.java @@ -23,6 +23,7 @@ import com.seibel.distanthorizons.core.level.AbstractDhLevel; import com.seibel.distanthorizons.core.logging.SpamReducedLogger; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.IColumnDataView; +import com.seibel.distanthorizons.coreapi.ModInfo; /** @@ -63,7 +64,7 @@ public class RenderDataPointUtil // When converting to or from an int a 128 should be added or removed. // If there is a bug with color then it's probably caused by this. - private static final SpamReducedLogger warnLogger = new SpamReducedLogger(1); + public static final boolean RUN_VALIDATION = ModInfo.IS_DEV_BUILD; public final static int EMPTY_DATA = 0; @@ -121,36 +122,58 @@ public class RenderDataPointUtil height, depth, lightSky, lightBlock, irisBlockMaterialId); } - public static long createDataPoint(int height, int depth, int color, int light, int irisBlockMaterialId) - { - LodUtil.assertTrue(light >= 0 && light < 256, "Raw Light value must be between 0 and 255!"); - - return createDataPoint( - ColorUtil.getAlpha(color), - ColorUtil.getRed(color), - ColorUtil.getGreen(color), - ColorUtil.getBlue(color), - height, depth, - light % 16, light / 16, - irisBlockMaterialId); - } - public static long createDataPoint(int alpha, int red, int green, int blue, int height, int depth, int lightSky, int lightBlock, int irisBlockMaterialId) { - LodUtil.assertTrue(height >= 0 && height < MAX_WORLD_Y_SIZE, "Trying to create datapoint with height[" + height + "] out of range!"); - LodUtil.assertTrue(depth >= 0 && depth < MAX_WORLD_Y_SIZE, "Trying to create datapoint with depth[" + depth + "] out of range!"); + if (RUN_VALIDATION) + { + // assertions are inside if-blocks to prevent unnecessary string concatenations + if (height < 0 || height >= MAX_WORLD_Y_SIZE) + { + LodUtil.assertNotReach("Trying to create datapoint with height[" + height + "] out of range!"); + } + if (depth < 0 || depth >= MAX_WORLD_Y_SIZE) + { + LodUtil.assertNotReach("Trying to create datapoint with depth[" + depth + "] out of range!"); + } + + if (lightSky < 0 || lightSky >= 16) + { + LodUtil.assertNotReach("Trying to create datapoint with lightSky[" + lightSky + "] out of range!"); + } + if (lightBlock < 0 || lightBlock >= 16) + { + LodUtil.assertNotReach("Trying to create datapoint with lightBlock[" + lightBlock + "] out of range!"); + } + + if (irisBlockMaterialId < 0 || irisBlockMaterialId >= 256) + { + LodUtil.assertNotReach("Trying to create datapoint with irisBlockMaterialId[" + irisBlockMaterialId + "] out of range!"); + } + + if (alpha < 0 || alpha >= 256) + { + LodUtil.assertNotReach("Trying to create datapoint with alpha[" + alpha + "] out of range!"); + } + if (red < 0 || red >= 256) + { + LodUtil.assertNotReach("Trying to create datapoint with red[" + red + "] out of range!"); + } + if (green < 0 || green >= 256) + { + LodUtil.assertNotReach("Trying to create datapoint with green[" + green + "] out of range!"); + } + if (blue < 0 || blue >= 256) + { + LodUtil.assertNotReach("Trying to create datapoint with blue[" + blue + "] out of range!"); + } + + + if (depth > height) + { + LodUtil.assertNotReach("Trying to create datapoint with depth[" + depth + "] greater than height[" + height + "]!"); + } + } - LodUtil.assertTrue(lightSky >= 0 && lightSky < 16, "Trying to create datapoint with lightSky[" + lightSky + "] out of range!"); - LodUtil.assertTrue(lightBlock >= 0 && lightBlock < 16, "Trying to create datapoint with lightBlock[" + lightBlock + "] out of range!"); - - LodUtil.assertTrue(irisBlockMaterialId >= 0 && irisBlockMaterialId < 256, "Trying to create datapoint with irisBlockMaterialId[" + irisBlockMaterialId + "] out of range!"); - - LodUtil.assertTrue(alpha >= 0 && alpha < 256, "Trying to create datapoint with alpha[" + alpha + "] out of range!"); - LodUtil.assertTrue(red >= 0 && red < 256, "Trying to create datapoint with red[" + red + "] out of range!"); - LodUtil.assertTrue(green >= 0 && green < 256, "Trying to create datapoint with green[" + green + "] out of range!"); - LodUtil.assertTrue(blue >= 0 && blue < 256, "Trying to create datapoint with blue[" + blue + "] out of range!"); - - LodUtil.assertTrue(depth <= height, "Trying to create datapoint with depth[" + depth + "] greater than height[" + height + "]!"); long out = (long) (alpha >>> ALPHA_DOWNSIZE_SHIFT) << ALPHA_SHIFT | (red & RED_MASK) << RED_SHIFT @@ -178,6 +201,7 @@ public class RenderDataPointUtil public static short getYMax(long dataPoint) { return (short) ((dataPoint >>> HEIGHT_SHIFT) & HEIGHT_MASK); } /** AKA the starting/bottom/lowest Y value above {@link AbstractDhLevel#getMinY()} */ public static short getYMin(long dataPoint) { return (short) ((dataPoint >>> DEPTH_SHIFT) & DEPTH_MASK); } + public static long setYMin(long dataPoint, int depth) { return (long) ((dataPoint & ~(DEPTH_MASK << DEPTH_SHIFT)) | (depth & DEPTH_MASK) << DEPTH_SHIFT); } public static short getAlpha(long dataPoint) { return (short) ((((dataPoint >>> ALPHA_SHIFT) & ALPHA_MASK) << ALPHA_DOWNSIZE_SHIFT) | 0b1111); } public static short getRed(long dataPoint) { return (short) ((dataPoint >>> RED_SHIFT) & RED_MASK); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderUtil.java b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderUtil.java index 0ea8414ca..05fb69b54 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderUtil.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderUtil.java @@ -28,6 +28,7 @@ import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.world.IDhClientWorld; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.coreapi.util.MathUtil; import com.seibel.distanthorizons.coreapi.util.math.Mat4f; import com.seibel.distanthorizons.coreapi.util.math.Vec3f; @@ -148,11 +149,6 @@ public class RenderUtil // in James' testing a near clip plane distance of 2 blocks is enough to allow the fragment // culling to take effect instead of seeing the near clip plane. float nearClipDist = RenderUtil.getNearClipPlaneDistanceInBlocks(partialTicks); - if (Config.Client.Advanced.Debugging.lodOnlyMode.get()) - { - nearClipDist = 0.1f; - } - float farClipDist = (float) RenderUtil.getFarClipPlaneDistanceInBlocks(); // Create a copy of the current matrix, so it won't be modified. @@ -210,6 +206,15 @@ public class RenderUtil } } + + // TODO move into method and use to override discard value in shader program + float heightOverride = getHeightBasedNearClipOverride(); + if (heightOverride != -1.0f) + { + nearClipPlane = heightOverride; + } + + // modify based on the player's FOV double fov = MC_RENDER.getFov(partialTicks); double aspectRatio = (double) MC_RENDER.getScreenWidth() / MC_RENDER.getScreenHeight(); @@ -227,6 +232,28 @@ public class RenderUtil return (int)((lodBlockDist + LodUtil.REGION_WIDTH) * Math.sqrt(2)); } + /** @return -1 if no override is necessary */ + public static float getHeightBasedNearClipOverride() + { + // TODO always using the client level like this might cause issues with immersive portals and the like, + // but for now it should work well enough + IClientLevelWrapper level = MC.getWrappedClientLevel(); + // a level should always be loaded, but just in case + if (level != null) + { + // if the player is a significant distance above the work, increase the + // near clip plane to fix Z imprecision issues + int playerHeight = MC.getPlayerBlockPos().y; + int levelMaxHeight = level.getHeight(); + if (playerHeight > levelMaxHeight + 1_000) + { + return playerHeight - (levelMaxHeight + 1000); + } + } + + return -1.0f; + } + /** @return false if LODs shouldn't be rendered for any reason */ public static boolean shouldLodsRender(ILevelWrapper levelWrapper) { diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/ThreadUtil.java b/core/src/main/java/com/seibel/distanthorizons/core/util/ThreadUtil.java index 1ac4567d0..7c5339d7c 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/ThreadUtil.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/ThreadUtil.java @@ -23,7 +23,7 @@ import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; import com.seibel.distanthorizons.core.config.types.ConfigEntry; import com.seibel.distanthorizons.core.util.threading.DhThreadFactory; import com.seibel.distanthorizons.core.util.threading.RateLimitedThreadPoolExecutor; -import com.seibel.distanthorizons.core.util.threading.ThreadPools; +import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.coreapi.ModInfo; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -33,7 +33,7 @@ import java.util.concurrent.*; /** * Handles thread pool creation. * - * @see ThreadPools + * @see ThreadPoolUtil * @see TimerUtil */ public class ThreadUtil @@ -80,9 +80,9 @@ public class ThreadUtil /** should only be used if there isn't a config controlling the run time ratio of this thread pool */ - public static RateLimitedThreadPoolExecutor makeRateLimitedThreadPool(int poolSize, String name, Double runTimeRatio, int relativePriority, Semaphore activeThreadCountSemaphore) + public static RateLimitedThreadPoolExecutor makeRateLimitedThreadPool(int poolSize, String name, Double runTimeRatio, int threadPriority, Semaphore activeThreadCountSemaphore) { - return new RateLimitedThreadPoolExecutor(poolSize, runTimeRatio, new DhThreadFactory(name, Thread.NORM_PRIORITY + relativePriority), activeThreadCountSemaphore); + return new RateLimitedThreadPoolExecutor(poolSize, runTimeRatio, new DhThreadFactory(name, threadPriority), activeThreadCountSemaphore); } public static RateLimitedThreadPoolExecutor makeRateLimitedThreadPool(int poolSize, Double runTimeRatio, DhThreadFactory threadFactory, Semaphore activeThreadCountSemaphore) { diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/dataStreams/DhDataInputStream.java b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/dataStreams/DhDataInputStream.java index 81ae64895..1c0fb0114 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/dataStreams/DhDataInputStream.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/dataStreams/DhDataInputStream.java @@ -19,12 +19,13 @@ package com.seibel.distanthorizons.core.util.objects.dataStreams; +import com.github.luben.zstd.RecyclingBufferPool; +import com.github.luben.zstd.ZstdInputStream; +import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode; import net.jpountz.lz4.LZ4FrameInputStream; +import org.tukaani.xz.XZInputStream; -import java.io.BufferedInputStream; -import java.io.DataInputStream; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; /** * Combines multiple different streams together for ease of use @@ -38,9 +39,27 @@ import java.io.InputStream; */ public class DhDataInputStream extends DataInputStream { - public DhDataInputStream(InputStream stream) throws IOException + public DhDataInputStream(InputStream stream, EDhApiDataCompressionMode compressionMode) throws IOException + { + super(warpStream(new BufferedInputStream(stream), compressionMode)); + } + private static InputStream warpStream(InputStream stream, EDhApiDataCompressionMode compressionMode) throws IOException { - super(new LZ4FrameInputStream(new BufferedInputStream(stream))); + switch (compressionMode) + { + case UNCOMPRESSED: + return stream; + case LZ4: + return new LZ4FrameInputStream(stream); + case Z_STD: + return new ZstdInputStream(stream, RecyclingBufferPool.INSTANCE); + case LZMA2: + // Note: all LZMA/XZ compressors can be decompressed using this same InputStream + return new XZInputStream(stream); + + default: + throw new IllegalArgumentException("No compressor defined for ["+compressionMode+"]"); + } } @Override diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/dataStreams/DhDataOutputStream.java b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/dataStreams/DhDataOutputStream.java index 650aab083..3337cb26f 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/dataStreams/DhDataOutputStream.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/dataStreams/DhDataOutputStream.java @@ -19,7 +19,15 @@ package com.seibel.distanthorizons.core.util.objects.dataStreams; +import com.github.luben.zstd.RecyclingBufferPool; +import com.github.luben.zstd.ZstdOutputStream; +import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import net.jpountz.lz4.LZ4Factory; import net.jpountz.lz4.LZ4FrameOutputStream; +import net.jpountz.xxhash.XXHashFactory; +import org.apache.logging.log4j.Logger; +import org.tukaani.xz.*; import java.io.*; @@ -30,9 +38,41 @@ import java.io.*; */ public class DhDataOutputStream extends DataOutputStream { - public DhDataOutputStream(OutputStream stream) throws IOException + private static final ThreadLocal LZMA_RESETTABLE_ARRAY_CACHE_GETTER = ThreadLocal.withInitial(() -> new ResettableArrayCache(new LzmaArrayCache())); + + + + public DhDataOutputStream(OutputStream stream, EDhApiDataCompressionMode compressionMode) throws IOException + { + super(warpStream(new BufferedOutputStream(stream), compressionMode)); + } + private static OutputStream warpStream(OutputStream stream, EDhApiDataCompressionMode compressionMode) throws IOException { - super(new LZ4FrameOutputStream(new BufferedOutputStream(stream))); + switch (compressionMode) + { + case UNCOMPRESSED: + return stream; + case LZ4: + return new LZ4FrameOutputStream(stream, + LZ4FrameOutputStream.BLOCKSIZE.SIZE_64KB, -1L, + // using native instances reduce GC pressure + LZ4Factory.nativeInstance().fastCompressor(), + XXHashFactory.nativeInstance().hash32(), + LZ4FrameOutputStream.FLG.Bits.BLOCK_INDEPENDENCE); + case Z_STD: + return new ZstdOutputStream(stream, RecyclingBufferPool.INSTANCE); + case LZMA2: + // using an array cache significantly reduces GC pressure + ResettableArrayCache arrayCache = LZMA_RESETTABLE_ARRAY_CACHE_GETTER.get(); + arrayCache.reset(); + // Note: if the LZMA2Options are changed the array cache may need to be re-tested. + // the array cache was specifically tested and tuned for LZMA preset 4 + return new XZOutputStream(stream, new LZMA2Options(4), + XZ.CHECK_CRC64, arrayCache); + + default: + throw new IllegalArgumentException("No compressor defined for ["+compressionMode+"]"); + } } @Override diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/dataStreams/LzmaArrayCache.java b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/dataStreams/LzmaArrayCache.java new file mode 100644 index 000000000..e932e9936 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/dataStreams/LzmaArrayCache.java @@ -0,0 +1,135 @@ +package com.seibel.distanthorizons.core.util.objects.dataStreams; + +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap; +import it.unimi.dsi.fastutil.ints.IntUnaryOperator; +import org.apache.logging.log4j.Logger; +import org.tukaani.xz.ArrayCache; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * LZMA requires a custom object to cache it's backend arrays. + * + * TODO there's a lot of duplicate code in this class since it has logic for both + * int[]'s and byte[]'s. + */ +public class LzmaArrayCache extends ArrayCache +{ + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + + /** + * In James' testing the byte and int caches only ever had to store 2 and 4 arrays respectively. + * With the in mind we could take a few shortcuts, but if that changes then we need to be + * notified as it might cause issues with the current logic. + */ + public static final int WARN_CACHE_LENGTH_EXCEEDED = 10; + + public static final AtomicInteger MAX_INT_CACHE_LENGTH_REF = new AtomicInteger(WARN_CACHE_LENGTH_EXCEEDED); + public static final AtomicInteger MAX_BYTE_CACHE_LENGTH_REF = new AtomicInteger(WARN_CACHE_LENGTH_EXCEEDED); + + + public final IntUnaryOperator maxByteCacheSizeUnaryOperator = (x) -> Math.max(this.byteCache.size(), x); + public final IntUnaryOperator maxIntCacheSizeUnaryOperator = (x) -> Math.max(this.intCache.size(), x); + + /** + * generally only 2 items long
+ * {@link Int2ReferenceArrayMap} can be used since the cache should only be a few items long. + * If the array ends up being longer then this design will need to be changed. + */ + public final Int2ReferenceArrayMap> byteCache = new Int2ReferenceArrayMap<>(); + /** generally only 4 items long */ + public final Int2ReferenceArrayMap> intCache = new Int2ReferenceArrayMap<>(); + + + + //=============// + // byte arrays // + //=============// + + @Override + public byte[] getByteArray(int size, boolean fillWithZeros) + { + ArrayList cacheList = this.byteCache.computeIfAbsent(size, (newSize) -> new ArrayList<>(4)); + if (cacheList.size() == 0) + { + return new byte[size]; + } + + byte[] array = cacheList.remove(cacheList.size()-1); + if (array == null) + { + return new byte[size]; + } + // the array needs to be cleared to prevent accidentally sending dirty data + Arrays.fill(array, (byte)0); + return array; + } + + @Override + public void putArray(byte[] array) + { + int size = array.length; + this.byteCache.computeIfAbsent(size, (newSize) -> new ArrayList<>()); + this.byteCache.get(size).add(array); + + + if (this.byteCache.size() > WARN_CACHE_LENGTH_EXCEEDED) + { + int previousMax = MAX_BYTE_CACHE_LENGTH_REF.getAndUpdate(this.maxByteCacheSizeUnaryOperator); + int newMax = MAX_BYTE_CACHE_LENGTH_REF.get(); + if (newMax > previousMax) + { + LOGGER.warn("LZMA byte array cache expected size exceeded. Expected max length ["+WARN_CACHE_LENGTH_EXCEEDED+"], actual length ["+this.byteCache.size()+"]."); + } + } + } + + + + //============// + // int arrays // + //============// + + @Override + public int[] getIntArray(int size, boolean fillWithZeros) + { + ArrayList cacheList = this.intCache.computeIfAbsent(size, (newSize) -> new ArrayList<>(4)); + if (cacheList.size() == 0) + { + return new int[size]; + } + + int[] array = cacheList.remove(cacheList.size()-1); + if (array == null) + { + return new int[size]; + } + // the array needs to be cleared to prevent accidentally sending dirty data + Arrays.fill(array, (byte)0); + return array; + } + + @Override + public void putArray(int[] array) + { + int size = array.length; + this.intCache.computeIfAbsent(size, (newSize) -> new ArrayList<>()); + this.intCache.get(size).add(array); + + + if (this.intCache.size() > WARN_CACHE_LENGTH_EXCEEDED) + { + int previousMax = MAX_INT_CACHE_LENGTH_REF.getAndUpdate(this.maxIntCacheSizeUnaryOperator); + int newMax = MAX_INT_CACHE_LENGTH_REF.get(); + if (newMax > previousMax) + { + LOGGER.warn("LZMA int array cache expected size exceeded. Expected max length ["+WARN_CACHE_LENGTH_EXCEEDED+"], actual length ["+this.intCache.size()+"]."); + } + } + } + + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadTree.java b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadTree.java index 1b4dc969b..1679bea9e 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadTree.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadTree.java @@ -143,7 +143,10 @@ public class QuadTree topQuadNode = new QuadNode(rootPos, this.treeMaxDetailLevel); boolean successfullyAdded = this.topRingList.set(ringListPosX, ringListPosZ, topQuadNode); - LodUtil.assertTrue(successfullyAdded, "Failed to add top quadTree node at position: " + rootPos); + if (!successfullyAdded) + { + LodUtil.assertNotReach("Failed to add top quadTree node at position: " + rootPos); + } } if (!topQuadNode.sectionPos.contains(pos)) @@ -261,6 +264,8 @@ public class QuadTree { if (quadNode != null && removedItemConsumer != null) { + quadNode.deleteAllChildren(removedItemConsumer); + removedItemConsumer.accept(quadNode.value); } }); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/threading/DhThreadFactory.java b/core/src/main/java/com/seibel/distanthorizons/core/util/threading/DhThreadFactory.java index 8eb41b5bc..129e628f2 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/threading/DhThreadFactory.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/threading/DhThreadFactory.java @@ -32,12 +32,10 @@ import org.jetbrains.annotations.NotNull; /** * Just a simple ThreadFactory to name ExecutorService * threads, which is helpful when debugging. - * - * @author James Seibel */ public class DhThreadFactory implements ThreadFactory { - private static final Logger LOGGER = DhLoggerBuilder.getLogger(MethodHandles.lookup().lookupClass().getSimpleName()); + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); public final String threadName; public final int priority; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/threading/PositionalLockProvider.java b/core/src/main/java/com/seibel/distanthorizons/core/util/threading/PositionalLockProvider.java new file mode 100644 index 000000000..515c8c04a --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/threading/PositionalLockProvider.java @@ -0,0 +1,187 @@ +package com.seibel.distanthorizons.core.util.threading; + +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.util.ThreadUtil; +import org.apache.logging.log4j.Logger; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Handles creating and destroying {@link ReentrantLock}'s for + * a given {@link DhSectionPos}. + * This is necessary since we need an unlimited number of locks + * when handling data updating, but we don't want to infinitely create locks. + * This provider will create/destroy locks as necessary given the current requirements by the file handlers. + */ +public class PositionalLockProvider +{ + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + + private static final ThreadPoolExecutor LOCK_CLEANUP_THREAD = ThreadUtil.makeSingleThreadPool("Positional Lock Cleanup"); + private static final int CLEANUP_THREAD_MAX_FREQUENCY_IN_MS = 1000; + + /** How long a lock can be unused before it is eligible for deletion */ + private static final long UNUSED_LOCK_TIMEOUT_IN_MS = 10_000; // 10 seconds + private static final int MAX_NUMBER_OF_LOCKS = 100; + + + private final ConcurrentHashMap lockByPos = new ConcurrentHashMap<>(); + + private final AtomicBoolean lockRemovalThreadRunning = new AtomicBoolean(false); + + + + //=============// + // constructor // + //=============// + + public PositionalLockProvider() {} + + + + //========// + // getter // + //========// + + public ReentrantLock getLock(DhSectionPos pos) + { + return this.lockByPos.compute(pos, (ignorePos, lock) -> + { + if (lock == null) + { + lock = new ExpiringLock(); + } + + if (this.lockByPos.size() > MAX_NUMBER_OF_LOCKS + && this.lockRemovalThreadRunning.getAndSet(true)) + { + LOCK_CLEANUP_THREAD.execute(this::removeExpiredLocks); + } + + return lock; + }); + } + private void removeExpiredLocks() + { + try + { + // we don't need to trigger this every time the lock count is over the limit + Thread.sleep(CLEANUP_THREAD_MAX_FREQUENCY_IN_MS); + + // walk over every lock and check which ones need to be removed + Iterator keySet = this.lockByPos.keySet().iterator(); + while (keySet.hasNext()) + { + try + { + long currentTime = System.currentTimeMillis(); + + DhSectionPos pos = keySet.next(); + ExpiringLock lock = this.lockByPos.get(pos); + + // don't try removing a lock that's currently in use + if (lock.tryLockWithoutUpdatingExpirationTime()) + { + if (currentTime > lock.expirationTimeInMs) + { + this.lockByPos.remove(pos); + //LOGGER.info("removed lock: "+pos); + } + lock.unlock(); + } + + } + catch (NoSuchElementException ignore) { } + } + } + catch (Exception e) + { + LOGGER.error("PositionLockProvider unexpected error when removing expired locks. Error: "+e.getMessage(), e); + } + finally + { + this.lockRemovalThreadRunning.set(false); + //LOGGER.info("update lock count: "+this.lockByPos.size()); + } + } + + + + //================// + // helper classes // + //================// + + /** a lock that tracks when it was last used */ + private static class ExpiringLock extends ReentrantLock + { + /** + * Indicates the system time in Milliseconds when this lock has expired.
+ * Should update whenever the lock is used. + */ + public long expirationTimeInMs; + + + + //=============// + // constructor // + //=============// + + public ExpiringLock() { this.resetExpirationTime(); } + + + + //===========// + // overrides // + //===========// + + @Override + public void lock() + { + this.resetExpirationTime(); + super.lock(); + } + @Override + public boolean tryLock() + { + this.resetExpirationTime(); + return super.tryLock(); + } + @Override + public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException + { + this.resetExpirationTime(); + return super.tryLock(timeout, unit); + } + + @Override + public void unlock() + { + this.resetExpirationTime(); + super.unlock(); + } + + + + //================// + // helper methods // + //================// + + /** should be called whenever this lock is needed */ + private void resetExpirationTime() + { + this.expirationTimeInMs = System.currentTimeMillis() + UNUSED_LOCK_TIMEOUT_IN_MS; + } + + public boolean tryLockWithoutUpdatingExpirationTime() { return super.tryLock(); } + + + } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/threading/RateLimitedThreadPoolExecutor.java b/core/src/main/java/com/seibel/distanthorizons/core/util/threading/RateLimitedThreadPoolExecutor.java index 5b27bb6fb..4b49ade2c 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/threading/RateLimitedThreadPoolExecutor.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/threading/RateLimitedThreadPoolExecutor.java @@ -62,7 +62,7 @@ public class RateLimitedThreadPoolExecutor extends ThreadPoolExecutor { super(corePoolSize, corePoolSize, 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue<>(), + new LinkedBlockingQueue<>(), // TODO using a PriorityBlockingQueue would be nice to allow for prioritizing tasks, but then all tasks must be Comparable threadFactory); this.runTimeRatio = runTimeRatio; @@ -75,6 +75,7 @@ public class RateLimitedThreadPoolExecutor extends ThreadPoolExecutor // overrides // //===========// + @Override protected void beforeExecute(Thread thread, Runnable runnable) { super.beforeExecute(thread, runnable); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/threading/ThreadPools.java b/core/src/main/java/com/seibel/distanthorizons/core/util/threading/ThreadPoolUtil.java similarity index 83% rename from core/src/main/java/com/seibel/distanthorizons/core/util/threading/ThreadPools.java rename to core/src/main/java/com/seibel/distanthorizons/core/util/threading/ThreadPoolUtil.java index 279486bf6..e4f786a84 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/threading/ThreadPools.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/threading/ThreadPoolUtil.java @@ -32,7 +32,7 @@ import java.util.concurrent.ThreadPoolExecutor; * * @see ThreadUtil */ -public class ThreadPools +public class ThreadPoolUtil { //=========================// // standalone thread pools // @@ -46,15 +46,26 @@ public class ThreadPools @Nullable public static ThreadPoolExecutor getFileHandlerExecutor() { return fileHandlerThreadPool.executor; } + public static final DhThreadFactory UPDATE_PROPAGATOR_THREAD_FACTORY = new DhThreadFactory("LOD Update Propagator", Thread.MIN_PRIORITY); + private static ConfigThreadPool updatePropagatorThreadPool; + @Nullable + public static ThreadPoolExecutor getUpdatePropagatorExecutor() { return updatePropagatorThreadPool.executor; } + public static final DhThreadFactory WORLD_GEN_THREAD_FACTORY = new DhThreadFactory("World Gen", Thread.MIN_PRIORITY); private static ConfigThreadPool worldGenThreadPool; @Nullable public static ThreadPoolExecutor getWorldGenExecutor() { return worldGenThreadPool.executor; } + public static final String BUFFER_UPLOADER_THREAD_NAME = "Buffer Uploader"; private static ThreadPoolExecutor bufferUploaderThreadPool; @Nullable public static ThreadPoolExecutor getBufferUploaderExecutor() { return bufferUploaderThreadPool; } + public static final String CLEANUP_THREAD_NAME = "Cleanup"; + private static ThreadPoolExecutor cleanupThreadPool; + @Nullable + public static ThreadPoolExecutor getCleanupExecutor() { return cleanupThreadPool; } + //======================// @@ -99,8 +110,10 @@ public class ThreadPools // standalone threads // fileHandlerThreadPool = new ConfigThreadPool(FILE_HANDLER_THREAD_FACTORY, Config.Client.Advanced.MultiThreading.numberOfFileHandlerThreads, Config.Client.Advanced.MultiThreading.runTimeRatioForFileHandlerThreads, null); + updatePropagatorThreadPool = new ConfigThreadPool(UPDATE_PROPAGATOR_THREAD_FACTORY, Config.Client.Advanced.MultiThreading.numberOfUpdatePropagatorThreads, Config.Client.Advanced.MultiThreading.runTimeRatioForUpdatePropagatorThreads, null); worldGenThreadPool = new ConfigThreadPool(WORLD_GEN_THREAD_FACTORY, Config.Client.Advanced.MultiThreading.numberOfWorldGenerationThreads, Config.Client.Advanced.MultiThreading.runTimeRatioForWorldGenerationThreads, null); - bufferUploaderThreadPool = ThreadUtil.makeSingleThreadPool("Buffer Uploader"); + bufferUploaderThreadPool = ThreadUtil.makeSingleThreadPool(BUFFER_UPLOADER_THREAD_NAME); + cleanupThreadPool = ThreadUtil.makeSingleThreadPool(CLEANUP_THREAD_NAME); @@ -138,14 +151,16 @@ public class ThreadPools { // standalone threads fileHandlerThreadPool.shutdownExecutorService(); + updatePropagatorThreadPool.shutdownExecutorService(); worldGenThreadPool.shutdownExecutorService(); bufferUploaderThreadPool.shutdown(); + cleanupThreadPool.shutdown(); // worker threads - ThreadPools.lightPopulatorThreadPool.shutdownExecutorService(); - ThreadPools.chunkToLodBuilderThreadPool.shutdownExecutorService(); - ThreadPools.bufferBuilderThreadPool.shutdownExecutorService(); + ThreadPoolUtil.lightPopulatorThreadPool.shutdownExecutorService(); + ThreadPoolUtil.chunkToLodBuilderThreadPool.shutdownExecutorService(); + ThreadPoolUtil.bufferBuilderThreadPool.shutdownExecutorService(); workerThreadSemaphore = null; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientServerWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientServerWorld.java index 275d606ea..2f31238ac 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientServerWorld.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientServerWorld.java @@ -90,7 +90,10 @@ public class DhClientServerWorld extends AbstractDhWorld implements IDhClientWor IClientLevelWrapper clientLevelWrapper = (IClientLevelWrapper) levelWrapper; IServerLevelWrapper serverLevelWrapper = clientLevelWrapper.tryGetServerSideWrapper(); LodUtil.assertTrue(serverLevelWrapper != null); - LodUtil.assertTrue(clientLevelWrapper.getDimensionType().equals(serverLevelWrapper.getDimensionType()), "tryGetServerSideWrapper returned a level for a different dimension. ClientLevelWrapper dim: " + clientLevelWrapper.getDimensionType().getDimensionName() + " ServerLevelWrapper dim: " + serverLevelWrapper.getDimensionType().getDimensionName()); + if (!clientLevelWrapper.getDimensionType().equals(serverLevelWrapper.getDimensionType())) + { + LodUtil.assertNotReach("tryGetServerSideWrapper returned a level for a different dimension. ClientLevelWrapper dim: " + clientLevelWrapper.getDimensionType().getDimensionName() + " ServerLevelWrapper dim: " + serverLevelWrapper.getDimensionType().getDimensionName()); + } DhClientServerLevel level = this.levelWrapperByDhLevel.get(serverLevelWrapper); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/block/IBlockStateWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/block/IBlockStateWrapper.java index 85bcd10cd..b26481a87 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/block/IBlockStateWrapper.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/block/IBlockStateWrapper.java @@ -47,7 +47,7 @@ public interface IBlockStateWrapper extends IDhApiBlockStateWrapper public static final byte TERRACOTTA = 10; public static final byte NETHER_STONE = 11; public static final byte WATER = 12; - // unlisted numbers are unused + public static final byte GRASS = 13; /** shouldn't normally be needed, but just in case */ public static final byte AIR = 14; @@ -67,6 +67,9 @@ public interface IBlockStateWrapper extends IDhApiBlockStateWrapper /** * Returning a value of 0 means the block is completely transparent. getNormalRenderedChunks(); /** Get what type of fog optifine is currently set to render. */ - EFogDrawMode getFogDrawMode(); + EDhApiFogDrawMode getFogDrawMode(); /** * Returns the percentage multiplier of the screen's current resolution.
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/IClientLevelWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/IClientLevelWrapper.java index 8ef1e3a01..60d74d4bf 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/IClientLevelWrapper.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/IClientLevelWrapper.java @@ -34,4 +34,7 @@ public interface IClientLevelWrapper extends ILevelWrapper int computeBaseColor(DhBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper blockState); + /** @return -1 if there was a problem getting the color */ + int getDirtBlockColor(); + } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/IDimensionTypeWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/IDimensionTypeWrapper.java index 0422e0ba9..bf9097608 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/IDimensionTypeWrapper.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/IDimensionTypeWrapper.java @@ -22,10 +22,6 @@ package com.seibel.distanthorizons.core.wrapperInterfaces.world; import com.seibel.distanthorizons.api.interfaces.world.IDhApiDimensionTypeWrapper; import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable; -/** - * @author James Seibel - * @version 2023-6-17 - */ public interface IDimensionTypeWrapper extends IDhApiDimensionTypeWrapper, IBindable { @Override @@ -37,4 +33,7 @@ public interface IDimensionTypeWrapper extends IDhApiDimensionTypeWrapper, IBind @Override boolean hasSkyLight(); + // there's definitely a better way of doing this, but it should work well enough for now + default boolean isTheEnd() { return this.getDimensionName().equalsIgnoreCase("the_end"); } + } diff --git a/core/src/main/resources/assets/distanthorizons/lang/en_us.json b/core/src/main/resources/assets/distanthorizons/lang/en_us.json index b6a02475e..d3694ab96 100644 --- a/core/src/main/resources/assets/distanthorizons/lang/en_us.json +++ b/core/src/main/resources/assets/distanthorizons/lang/en_us.json @@ -101,7 +101,7 @@ "distanthorizons.config.client.advanced.graphics.quality.horizontalScale.@tooltip": "How quickly LODs drop off in quality.\n\nLarger numbers will improve how distant terrain looks\nbut will increase memory and GPU usage.", "distanthorizons.config.client.advanced.graphics.quality.horizontalQuality": - "Horizontal Quality", + "LOD Dropoff Distance", "distanthorizons.config.client.advanced.graphics.quality.horizontalQuality.@tooltip": "How far apart drops in quality are.\n\nHigher settings will increase the distance between drops\nbut will increase memory and GPU usage.", "distanthorizons.config.client.advanced.graphics.quality.transparency": @@ -269,10 +269,6 @@ "Overdraw Prevention", "distanthorizons.config.client.advanced.graphics.advancedGraphics.overdrawPrevention.@tooltip": "Determines how far from the camera Distant Horizons will start rendering. \nMeasured as a percentage of the vanilla render distance.\n\nHigher values will prevent LODs from rendering behind vanilla blocks at a higher distance,\nbut may cause holes to appear in the LODs. \nHoles are most likely to appear when flying through unloaded terrain. \n\nIncreasing the vanilla render distance increases the effectiveness of this setting.", - "distanthorizons.config.client.advanced.graphics.advancedGraphics.seamlessOverdraw": - "(Experimental) Seamless Overdraw", - "distanthorizons.config.client.advanced.graphics.advancedGraphics.seamlessOverdraw.@tooltip": - "Buggy experimental option that will attempt to match up \nDistant Horizons' and Minecraft's near/far clip planes, \nreducing overdraw. \n\nNote: only works with Fabric.", "distanthorizons.config.client.advanced.graphics.advancedGraphics.brightnessMultiplier": "Brightness Multiplier", "distanthorizons.config.client.advanced.graphics.advancedGraphics.brightnessMultiplier.@tooltip": @@ -309,6 +305,10 @@ "Disable Shadow Pass Frustum Culling", "distanthorizons.config.client.advanced.graphics.advancedGraphics.disableShadowPassFrustumCulling.@tooltip": "Identical to the other frustum culling option except that it is \nonly used when a shader mod is present using the DH API \nand the shadow pass is being rendered. \n\nDisable this if shadows render incorrectly.", + "distanthorizons.config.client.advanced.graphics.advancedGraphics.grassSideRendering": + "Grass Side Rendering", + "distanthorizons.config.client.advanced.graphics.advancedGraphics.grassSideRendering.@tooltip": + "How should the sides and bottom of grass block LODs render?", "distanthorizons.config.client.advanced.worldGenerator": @@ -341,13 +341,24 @@ "distanthorizons.config.client.advanced.lodBuilding.minTimeBetweenChunkUpdatesInSeconds": "Minimum Time Between Chunk Updates In Seconds", + "distanthorizons.config.client.advanced.lodBuilding.minTimeBetweenChunkUpdatesInSeconds.@tooltip": "Determines how long must pass between LOD chunk updates before another. \nupdate can occur\n\nIncreasing this value will reduce CPU load but may may cause \nLODs to become outdated more frequently or for longer.", "distanthorizons.config.client.advanced.lodBuilding.onlyUseDhLightingEngine": "Only Use DH Lighting Engine", "distanthorizons.config.client.advanced.lodBuilding.onlyUseDhLightingEngine.@tooltip": "If false LODs will be lit by Minecraft's lighting engine when possible \nand fall back to the DH lighting engine only when necessary. \n\nIf true LODs will only be lit using Distant Horizons' lighting engine. \n\nGenerally it is best to leave this disabled and should only be enabled \nif there are lighting issues or for debugging.", - + "distanthorizons.config.client.advanced.lodBuilding.dataCompression": + "Data Compression", + "distanthorizons.config.client.advanced.lodBuilding.dataCompression.@tooltip": + "What algorithm should be used to compress new LOD data? \nThis setting will only affect new or updated LOD data, \nany data already generated when this setting is changed will be \nunaffected until it needs to be re-written to the database. \n\nFastest: LZ4 \nHighest Compression: LZMA2", + "distanthorizons.config.client.advanced.lodBuilding.worldCompression": + "Lossy World Compression", + "distanthorizons.config.client.advanced.lodBuilding.worldCompression.@tooltip": + "How should block data be compressed when creating LOD data? \nThis setting will only affect new or updated LOD data, \nany data already generated when this setting is changed will be \nunaffected until it is modified or re-loaded. \n\nMost Accurate: Merge Same Blocks \nHighest Compression: Visually Equal", + "distanthorizons.config.client.advanced.lodBuilding.showMigrationChatWarning": + "Log Migration In Chat", + "distanthorizons.config.client.advanced.multiplayer": "Multiplayer", @@ -395,6 +406,13 @@ "distanthorizons.config.client.advanced.multiThreading.runTimeRatioForFileHandlerThreads": "Runtime % for file handler threads", + "distanthorizons.config.client.advanced.multiThreading.numberOfUpdatePropagatorThreads": + "NO. of update propagator threads", + "distanthorizons.config.client.advanced.multiThreading.numberOfUpdatePropagatorThreads.@tooltip": + "How many threads should be used when applying LOD updates? \nAn LOD update is the operation of down-sampling a high detail LOD \ninto a lower detail one. \n\nThis config can have a much higher number of threads \nassigned and much lower run time ratio vs other thread pools \nbecause the amount of time any particular thread may run is relatively low.", + "distanthorizons.config.client.advanced.multiThreading.runTimeRatioForUpdatePropagatorThreads": + "Runtime % for update propagator threads", + "distanthorizons.config.client.advanced.multiThreading.numberOfLodBuilderThreads": "NO. of LOD builder threads", "distanthorizons.config.client.advanced.multiThreading.numberOfLodBuilderThreads.@tooltip": @@ -432,6 +450,10 @@ "Enable white world", "distanthorizons.config.client.advanced.debugging.enableWhiteWorld.@tooltip": "If enabled, vertex color will not be passed.\nUseful for debugging shaders.", + "distanthorizons.config.client.advanced.debugging.showOverlappingQuadErrors": + "Show overlapping quad errors", + "distanthorizons.config.client.advanced.debugging.showOverlappingQuadErrors.@tooltip": + "f true overlapping quads will be rendered as bright red for easy identification. \nIf false the quads will be rendered normally.", "distanthorizons.config.client.advanced.debugging.allowUnsafeValues": "Allow Unsafe UI Values", "distanthorizons.config.client.advanced.debugging.allowUnsafeValues.@tooltip": @@ -551,12 +573,8 @@ "Show World Gen Queue", "distanthorizons.config.client.advanced.debugging.debugWireframe.showRenderSectionStatus": "Show Render Section Status", - "distanthorizons.config.client.advanced.debugging.debugWireframe.showFullDataFileStatus": - "Show Full Data file Status", - "distanthorizons.config.client.advanced.debugging.debugWireframe.showFullDataFileSampling": - "Show Full Data file Sampling", - "distanthorizons.config.client.advanced.debugging.debugWireframe.showRenderDataFileStatus": - "Show Render Data file Status", + "distanthorizons.config.client.advanced.debugging.debugWireframe.showFullDataUpdateStatus": + "Show Full Data Update Status", "distanthorizons.config.client.advanced.debugging.openGl": @@ -604,137 +622,142 @@ - "distanthorizons.config.enum.EQualityPreset.CUSTOM": + "distanthorizons.config.enum.EDhApiQualityPreset.CUSTOM": "Custom", - "distanthorizons.config.enum.EQualityPreset.MINIMUM": + "distanthorizons.config.enum.EDhApiQualityPreset.MINIMUM": "Minimum", - "distanthorizons.config.enum.EQualityPreset.LOW": + "distanthorizons.config.enum.EDhApiQualityPreset.LOW": "Low", - "distanthorizons.config.enum.EQualityPreset.MEDIUM": + "distanthorizons.config.enum.EDhApiQualityPreset.MEDIUM": "Medium", - "distanthorizons.config.enum.EQualityPreset.HIGH": + "distanthorizons.config.enum.EDhApiQualityPreset.HIGH": "High", - "distanthorizons.config.enum.EQualityPreset.EXTREME": + "distanthorizons.config.enum.EDhApiQualityPreset.EXTREME": "Extreme", - "distanthorizons.config.enum.EThreadPreset.CUSTOM": + "distanthorizons.config.enum.EDhApiThreadPreset.CUSTOM": "Custom", - "distanthorizons.config.enum.EThreadPreset.MINIMAL_IMPACT": + "distanthorizons.config.enum.EDhApiThreadPreset.MINIMAL_IMPACT": "Minimal Impact", - "distanthorizons.config.enum.EThreadPreset.LOW_IMPACT": + "distanthorizons.config.enum.EDhApiThreadPreset.LOW_IMPACT": "Low Impact", - "distanthorizons.config.enum.EThreadPreset.BALANCED": + "distanthorizons.config.enum.EDhApiThreadPreset.BALANCED": "Balanced", - "distanthorizons.config.enum.EThreadPreset.AGGRESSIVE": + "distanthorizons.config.enum.EDhApiThreadPreset.AGGRESSIVE": "Aggressive", - "distanthorizons.config.enum.EThreadPreset.I_PAID_FOR_THE_WHOLE_CPU": + "distanthorizons.config.enum.EDhApiThreadPreset.I_PAID_FOR_THE_WHOLE_CPU": "I Paid For The Whole CPU", - "distanthorizons.config.enum.EMaxHorizontalResolution.BLOCK": + "distanthorizons.config.enum.EDhApiMaxHorizontalResolution.BLOCK": "Block", - "distanthorizons.config.enum.EMaxHorizontalResolution.TWO_BLOCKS": + "distanthorizons.config.enum.EDhApiMaxHorizontalResolution.TWO_BLOCKS": "2 blocks", - "distanthorizons.config.enum.EMaxHorizontalResolution.FOUR_BLOCKS": + "distanthorizons.config.enum.EDhApiMaxHorizontalResolution.FOUR_BLOCKS": "4 blocks", - "distanthorizons.config.enum.EMaxHorizontalResolution.HALF_CHUNK": + "distanthorizons.config.enum.EDhApiMaxHorizontalResolution.HALF_CHUNK": "Half a chunk", - "distanthorizons.config.enum.EMaxHorizontalResolution.CHUNK": + "distanthorizons.config.enum.EDhApiMaxHorizontalResolution.CHUNK": "Chunk", - "distanthorizons.config.enum.EVerticalQuality.HEIGHT_MAP": - "Height Map", - "distanthorizons.config.enum.EVerticalQuality.LOW": - "Low", - "distanthorizons.config.enum.EVerticalQuality.MEDIUM": - "Medium", - "distanthorizons.config.enum.EVerticalQuality.HIGH": - "High", - "distanthorizons.config.enum.EVerticalQuality.EXTREME": - "Extreme", - "distanthorizons.config.enum.EHorizontalQuality.LOWEST": + "distanthorizons.config.enum.EDhApiVerticalQuality.HEIGHT_MAP": + "1. Height Map", + "distanthorizons.config.enum.EDhApiVerticalQuality.LOW": + "2. Low", + "distanthorizons.config.enum.EDhApiVerticalQuality.MEDIUM": + "3. Medium", + "distanthorizons.config.enum.EDhApiVerticalQuality.HIGH": + "4. High", + "distanthorizons.config.enum.EDhApiVerticalQuality.VERY_HIGH": + "5. Very High", + "distanthorizons.config.enum.EDhApiVerticalQuality.EXTREME": + "6. Extreme", + "distanthorizons.config.enum.EDhApiVerticalQuality.PIXEL_ART": + "7. Pixel Art", + + "distanthorizons.config.enum.EDhApiHorizontalQuality.LOWEST": "Lowest", - "distanthorizons.config.enum.EHorizontalQuality.LOW": + "distanthorizons.config.enum.EDhApiHorizontalQuality.LOW": "Low", - "distanthorizons.config.enum.EHorizontalQuality.MEDIUM": + "distanthorizons.config.enum.EDhApiHorizontalQuality.MEDIUM": "Medium", - "distanthorizons.config.enum.EHorizontalQuality.HIGH": + "distanthorizons.config.enum.EDhApiHorizontalQuality.HIGH": "High", - "distanthorizons.config.enum.EHorizontalQuality.EXTREME": + "distanthorizons.config.enum.EDhApiHorizontalQuality.EXTREME": "Extreme", - "distanthorizons.config.enum.EHorizontalQuality.UNLIMITED": + "distanthorizons.config.enum.EDhApiHorizontalQuality.UNLIMITED": "Unlimited", - "distanthorizons.config.enum.ETransparency.DISABLED": + "distanthorizons.config.enum.EDhApiTransparency.DISABLED": "Disabled", - "distanthorizons.config.enum.ETransparency.FAKE": + "distanthorizons.config.enum.EDhApiTransparency.FAKE": "Fake", - "distanthorizons.config.enum.ETransparency.COMPLETE": + "distanthorizons.config.enum.EDhApiTransparency.COMPLETE": "Complete", - "distanthorizons.config.enum.EFogDistance.NEAR": + "distanthorizons.config.enum.EDhApiFogDistance.NEAR": "Near", - "distanthorizons.config.enum.EFogDistance.FAR": + "distanthorizons.config.enum.EDhApiFogDistance.FAR": "Far", - "distanthorizons.config.enum.EFogDistance.NEAR_AND_FAR": + "distanthorizons.config.enum.EDhApiFogDistance.NEAR_AND_FAR": "Near and far", - "distanthorizons.config.enum.EFogDrawMode.USE_OPTIFINE_SETTING": + "distanthorizons.config.enum.EDhApiFogDrawMode.USE_OPTIFINE_SETTING": "Use modded settings", - "distanthorizons.config.enum.EFogDrawMode.FOG_ENABLED": + "distanthorizons.config.enum.EDhApiFogDrawMode.FOG_ENABLED": "Enabled", - "distanthorizons.config.enum.EFogDrawMode.FOG_DISABLED": + "distanthorizons.config.enum.EDhApiFogDrawMode.FOG_DISABLED": "Disabled", - "distanthorizons.config.enum.EFogColorMode.USE_WORLD_FOG_COLOR": + "distanthorizons.config.enum.EDhApiFogColorMode.USE_WORLD_FOG_COLOR": "Use world fog", - "distanthorizons.config.enum.EFogColorMode.USE_SKY_COLOR": + "distanthorizons.config.enum.EDhApiFogColorMode.USE_SKY_COLOR": "Use sky color", - "distanthorizons.config.enum.EFogFalloff.LINEAR": + "distanthorizons.config.enum.EDhApiFogFalloff.LINEAR": "Linear", - "distanthorizons.config.enum.EFogFalloff.EXPONENTIAL": + "distanthorizons.config.enum.EDhApiFogFalloff.EXPONENTIAL": "Exponential", - "distanthorizons.config.enum.EFogFalloff.EXPONENTIAL_SQUARED": + "distanthorizons.config.enum.EDhApiFogFalloff.EXPONENTIAL_SQUARED": "Exponential squared", - "distanthorizons.config.enum.EHeightFogMixMode.BASIC": + "distanthorizons.config.enum.EDhApiHeightFogMixMode.BASIC": "Basic", - "distanthorizons.config.enum.EHeightFogMixMode.IGNORE_HEIGHT": + "distanthorizons.config.enum.EDhApiHeightFogMixMode.IGNORE_HEIGHT": "Ignore Height", - "distanthorizons.config.enum.EHeightFogMixMode.ADDITION": + "distanthorizons.config.enum.EDhApiHeightFogMixMode.ADDITION": "Addition", - "distanthorizons.config.enum.EHeightFogMixMode.MAX": + "distanthorizons.config.enum.EDhApiHeightFogMixMode.MAX": "Max", - "distanthorizons.config.enum.EHeightFogMixMode.MULTIPLY": + "distanthorizons.config.enum.EDhApiHeightFogMixMode.MULTIPLY": "Multiply", - "distanthorizons.config.enum.EHeightFogMixMode.INVERSE_MULTIPLY": + "distanthorizons.config.enum.EDhApiHeightFogMixMode.INVERSE_MULTIPLY": "Inverse Multiply", - "distanthorizons.config.enum.EHeightFogMixMode.LIMITED_ADDITION": + "distanthorizons.config.enum.EDhApiHeightFogMixMode.LIMITED_ADDITION": "Limited Addition", - "distanthorizons.config.enum.EHeightFogMixMode.MULTIPLY_ADDITION": + "distanthorizons.config.enum.EDhApiHeightFogMixMode.MULTIPLY_ADDITION": "Multiply Addition", - "distanthorizons.config.enum.EHeightFogMixMode.INVERSE_MULTIPLY_ADDITION": + "distanthorizons.config.enum.EDhApiHeightFogMixMode.INVERSE_MULTIPLY_ADDITION": "Inverse Multiply Addition", - "distanthorizons.config.enum.EHeightFogMixMode.AVERAGE": + "distanthorizons.config.enum.EDhApiHeightFogMixMode.AVERAGE": "Average", - "distanthorizons.config.enum.EHeightFogMode.ABOVE_CAMERA": + "distanthorizons.config.enum.EDhApiHeightFogMode.ABOVE_CAMERA": "Above Camera", - "distanthorizons.config.enum.EHeightFogMode.BELOW_CAMERA": + "distanthorizons.config.enum.EDhApiHeightFogMode.BELOW_CAMERA": "Below Camera", - "distanthorizons.config.enum.EHeightFogMode.ABOVE_AND_BELOW_CAMERA": + "distanthorizons.config.enum.EDhApiHeightFogMode.ABOVE_AND_BELOW_CAMERA": "Above And Below Camera", - "distanthorizons.config.enum.EHeightFogMode.ABOVE_SET_HEIGHT": + "distanthorizons.config.enum.EDhApiHeightFogMode.ABOVE_SET_HEIGHT": "Above Set Height", - "distanthorizons.config.enum.EHeightFogMode.BELOW_SET_HEIGHT": + "distanthorizons.config.enum.EDhApiHeightFogMode.BELOW_SET_HEIGHT": "Below Set Height", - "distanthorizons.config.enum.EHeightFogMode.ABOVE_AND_BELOW_SET_HEIGHT": + "distanthorizons.config.enum.EDhApiHeightFogMode.ABOVE_AND_BELOW_SET_HEIGHT": "Above And Below Set Height", - "distanthorizons.config.enum.EVanillaOverdraw.NEVER": + "distanthorizons.config.enum.EDhApiVanillaOverdraw.NEVER": "Never", - "distanthorizons.config.enum.EVanillaOverdraw.DYNAMIC": + "distanthorizons.config.enum.EDhApiVanillaOverdraw.DYNAMIC": "Dynamic", - "distanthorizons.config.enum.EVanillaOverdraw.ALWAYS": + "distanthorizons.config.enum.EDhApiVanillaOverdraw.ALWAYS": "Always", "distanthorizons.config.enum.EDhApiDistantGeneratorMode.NONE": @@ -752,133 +775,157 @@ "distanthorizons.config.enum.EDhApiDistantGeneratorMode.FULL": "Full", - "distanthorizons.config.enum.ELightGenerationMode.DISTANT_HORIZONS": + "distanthorizons.config.enum.EDhApiDataCompressionMode.UNCOMPRESSED": + "1. Uncompressed", + "distanthorizons.config.enum.EDhApiDataCompressionMode.LZ4": + "2. LZ4", + "distanthorizons.config.enum.EDhApiDataCompressionMode.Z_STD": + "3. Zstd", + "distanthorizons.config.enum.EDhApiDataCompressionMode.LZMA2": + "4. LZMA2", + + "distanthorizons.config.enum.EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS": + "1. Merge Same Blocks", + "distanthorizons.config.enum.EDhApiWorldCompressionMode.VISUALLY_EQUAL": + "2. Visually Equal", + + "distanthorizons.config.enum.EDhApiLightGenerationMode.DISTANT_HORIZONS": "Distant Horizons", - "distanthorizons.config.enum.ELightGenerationMode.MINECRAFT": + "distanthorizons.config.enum.EDhApiLightGenerationMode.MINECRAFT": "Minecraft", - "distanthorizons.config.enum.EGenerationPriority.AUTO": + "distanthorizons.config.enum.EDhApiGenerationPriority.AUTO": "Auto", - "distanthorizons.config.enum.EGenerationPriority.NEAR_FIRST": + "distanthorizons.config.enum.EDhApiGenerationPriority.NEAR_FIRST": "Near first", - "distanthorizons.config.enum.EGenerationPriority.BALANCED": + "distanthorizons.config.enum.EDhApiGenerationPriority.BALANCED": "Balanced", - "distanthorizons.config.enum.EGenerationPriority.FAR_FIRST": + "distanthorizons.config.enum.EDhApiGenerationPriority.FAR_FIRST": "Far first", - "distanthorizons.config.enum.EBlocksToAvoid.NONE": + "distanthorizons.config.enum.EDhApiBlocksToAvoid.NONE": "None", - "distanthorizons.config.enum.EBlocksToAvoid.NON_COLLIDING": + "distanthorizons.config.enum.EDhApiBlocksToAvoid.NON_COLLIDING": "Non-Colliding", - "distanthorizons.config.enum.EOverdrawPrevention.NONE": + "distanthorizons.config.enum.EDhApiOverdrawPrevention.NONE": "None", - "distanthorizons.config.enum.EOverdrawPrevention.LIGHT": + "distanthorizons.config.enum.EDhApiOverdrawPrevention.LIGHT": "Light", - "distanthorizons.config.enum.EOverdrawPrevention.MEDIUM": + "distanthorizons.config.enum.EDhApiOverdrawPrevention.MEDIUM": "Medium", - "distanthorizons.config.enum.EOverdrawPrevention.HEAVY": + "distanthorizons.config.enum.EDhApiOverdrawPrevention.HEAVY": "Heavy", - "distanthorizons.config.enum.EServerFolderNameMode.NAME_ONLY": + "distanthorizons.config.enum.EDhApiServerFolderNameMode.NAME_ONLY": "Name Only", - "distanthorizons.config.enum.EServerFolderNameMode.NAME_IP": + "distanthorizons.config.enum.EDhApiServerFolderNameMode.IP_ONLY": + "IP Only", + "distanthorizons.config.enum.EDhApiServerFolderNameMode.NAME_IP": "Name and IP", - "distanthorizons.config.enum.EServerFolderNameMode.NAME_IP_PORT": + "distanthorizons.config.enum.EDhApiServerFolderNameMode.NAME_IP_PORT": "Name, IP, Port", - "distanthorizons.config.enum.EServerFolderNameMode.NAME_IP_PORT_MC_VERSION": + "distanthorizons.config.enum.EDhApiServerFolderNameMode.NAME_IP_PORT_MC_VERSION": "Name, IP, Port, MC version", - "distanthorizons.config.enum.ERendererMode.DEFAULT": + "distanthorizons.config.enum.EDhApiRendererMode.DEFAULT": "Default", - "distanthorizons.config.enum.ERendererMode.DEBUG": + "distanthorizons.config.enum.EDhApiRendererMode.DEBUG": "Debug", - "distanthorizons.config.enum.ERendererMode.DISABLED": + "distanthorizons.config.enum.EDhApiRendererMode.DISABLED": "Disabled", - "distanthorizons.config.enum.EDebugRendering.OFF": + "distanthorizons.config.enum.EDhApiDebugRendering.OFF": "Off", - "distanthorizons.config.enum.EDebugRendering.SHOW_DETAIL": + "distanthorizons.config.enum.EDhApiDebugRendering.SHOW_DETAIL": "Show detail", - "distanthorizons.config.enum.EDebugRendering.SHOW_GENMODE": + "distanthorizons.config.enum.EDhApiDebugRendering.SHOW_GENMODE": "Show generation mode", - "distanthorizons.config.enum.EDebugRendering.SHOW_BLOCK_MATERIAL": + "distanthorizons.config.enum.EDhApiDebugRendering.SHOW_BLOCK_MATERIAL": "Show Material", - "distanthorizons.config.enum.EDebugRendering.SHOW_OVERLAPPING_QUADS": + "distanthorizons.config.enum.EDhApiDebugRendering.SHOW_OVERLAPPING_QUADS": "Show overlapping quads", - "distanthorizons.config.enum.EDebugRendering.SHOW_RENDER_SOURCE_FLAG": + "distanthorizons.config.enum.EDhApiDebugRendering.SHOW_RENDER_SOURCE_FLAG": "Show render source flag", - "distanthorizons.config.enum.EGLErrorHandlingMode.IGNORE": + "distanthorizons.config.enum.EDhApiGLErrorHandlingMode.IGNORE": "Ignore", - "distanthorizons.config.enum.EGLErrorHandlingMode.LOG": + "distanthorizons.config.enum.EDhApiGLErrorHandlingMode.LOG": "Log", - "distanthorizons.config.enum.EGLErrorHandlingMode.LOG_THROW": + "distanthorizons.config.enum.EDhApiGLErrorHandlingMode.LOG_THROW": "Log-Throw", - "distanthorizons.config.enum.EGlProfileMode.CORE": + "distanthorizons.config.enum.EDhApiGlProfileMode.CORE": "Core", - "distanthorizons.config.enum.EGlProfileMode.COMPAT": + "distanthorizons.config.enum.EDhApiGlProfileMode.COMPAT": "Compat", - "distanthorizons.config.enum.EGlProfileMode.ANY": + "distanthorizons.config.enum.EDhApiGlProfileMode.ANY": "Any", - "distanthorizons.config.enum.ELoggerMode.DISABLED": + "distanthorizons.config.enum.EDhApiLoggerMode.DISABLED": "Disabled", - "distanthorizons.config.enum.ELoggerMode.LOG_ALL_TO_FILE": + "distanthorizons.config.enum.EDhApiLoggerMode.LOG_ALL_TO_FILE": "File: All, Chat: Off", - "distanthorizons.config.enum.ELoggerMode.LOG_ERROR_TO_CHAT": + "distanthorizons.config.enum.EDhApiLoggerMode.LOG_ERROR_TO_CHAT": "File: All, Chat: Error", - "distanthorizons.config.enum.ELoggerMode.LOG_WARNING_TO_CHAT": + "distanthorizons.config.enum.EDhApiLoggerMode.LOG_WARNING_TO_CHAT": "File: All, Chat: Warning", - "distanthorizons.config.enum.ELoggerMode.LOG_INFO_TO_CHAT": + "distanthorizons.config.enum.EDhApiLoggerMode.LOG_INFO_TO_CHAT": "File: All, Chat: Info", - "distanthorizons.config.enum.ELoggerMode.LOG_DEBUG_TO_CHAT": + "distanthorizons.config.enum.EDhApiLoggerMode.LOG_DEBUG_TO_CHAT": "File: All, Chat: Debug", - "distanthorizons.config.enum.ELoggerMode.LOG_ALL_TO_CHAT": + "distanthorizons.config.enum.EDhApiLoggerMode.LOG_ALL_TO_CHAT": "File: All, Chat: All", - "distanthorizons.config.enum.ELoggerMode.LOG_ERROR_TO_CHAT_AND_FILE": + "distanthorizons.config.enum.EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_FILE": "File: Error, Chat: Error", - "distanthorizons.config.enum.ELoggerMode.LOG_WARNING_TO_CHAT_AND_FILE": + "distanthorizons.config.enum.EDhApiLoggerMode.LOG_WARNING_TO_CHAT_AND_FILE": "File: Warning, Chat: Warning", - "distanthorizons.config.enum.ELoggerMode.LOG_INFO_TO_CHAT_AND_FILE": + "distanthorizons.config.enum.EDhApiLoggerMode.LOG_INFO_TO_CHAT_AND_FILE": "File: Info, Chat: Info", - "distanthorizons.config.enum.ELoggerMode.LOG_DEBUG_TO_CHAT_AND_FILE": + "distanthorizons.config.enum.EDhApiLoggerMode.LOG_DEBUG_TO_CHAT_AND_FILE": "File: Debug, Chat: Debug", - "distanthorizons.config.enum.ELoggerMode.LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE": + "distanthorizons.config.enum.EDhApiLoggerMode.LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE": "File: Info, Chat: Warning", - "distanthorizons.config.enum.ELoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE": + "distanthorizons.config.enum.EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE": "File: Info, Chat: Error", - "distanthorizons.config.enum.EGpuUploadMethod.AUTO": + "distanthorizons.config.enum.EDhApiGpuUploadMethod.AUTO": "Auto", - "distanthorizons.config.enum.EGpuUploadMethod.BUFFER_STORAGE": + "distanthorizons.config.enum.EDhApiGpuUploadMethod.BUFFER_STORAGE": "Buffer storage", - "distanthorizons.config.enum.EGpuUploadMethod.SUB_DATA": + "distanthorizons.config.enum.EDhApiGpuUploadMethod.SUB_DATA": "Sub data", - "distanthorizons.config.enum.EGpuUploadMethod.BUFFER_MAPPING": + "distanthorizons.config.enum.EDhApiGpuUploadMethod.BUFFER_MAPPING": "Buffer mapping", - "distanthorizons.config.enum.EGpuUploadMethod.DATA": + "distanthorizons.config.enum.EDhApiGpuUploadMethod.DATA": "Data", - "distanthorizons.config.enum.EBufferRebuildTimes.CONSTANT": + "distanthorizons.config.enum.EDhApiBufferRebuildTimes.CONSTANT": "Constant", - "distanthorizons.config.enum.EBufferRebuildTimes.FREQUENT": + "distanthorizons.config.enum.EDhApiBufferRebuildTimes.FREQUENT": "Frequent", - "distanthorizons.config.enum.EBufferRebuildTimes.NORMAL": + "distanthorizons.config.enum.EDhApiBufferRebuildTimes.NORMAL": "Normal", - "distanthorizons.config.enum.EBufferRebuildTimes.RARE": + "distanthorizons.config.enum.EDhApiBufferRebuildTimes.RARE": "Rare", - "distanthorizons.config.enum.ELodShading.MINECRAFT": - "Minecraft", - "distanthorizons.config.enum.ELodShading.OLD_LIGHTING": - "Old Lighting", - "distanthorizons.config.enum.ELodShading.NONE": - "None", + "distanthorizons.config.enum.EDhApiLodShading.AUTO": + "Auto", + "distanthorizons.config.enum.EDhApiLodShading.ENABLED": + "Enabled", + "distanthorizons.config.enum.EDhApiLodShading.DISABLED": + "Disabled", - "distanthorizons.config.enum.EUpdateBranch.STABLE": + "distanthorizons.config.enum.EDhApiUpdateBranch.STABLE": "Stable", - "distanthorizons.config.enum.EUpdateBranch.NIGHTLY": - "Nightly" + "distanthorizons.config.enum.EDhApiUpdateBranch.NIGHTLY": + "Nightly", + + "distanthorizons.config.enum.EDhApiGrassSideRendering.AS_GRASS": + "As Grass", + "distanthorizons.config.enum.EDhApiGrassSideRendering.FADE_TO_DIRT": + "Fade To Dirt", + "distanthorizons.config.enum.EDhApiGrassSideRendering.AS_DIRT": + "As Dirt" + } diff --git a/core/src/main/resources/sqlScripts/0020-sqlite-createFullDataSourceV2Tables.sql b/core/src/main/resources/sqlScripts/0020-sqlite-createFullDataSourceV2Tables.sql new file mode 100644 index 000000000..150b788cf --- /dev/null +++ b/core/src/main/resources/sqlScripts/0020-sqlite-createFullDataSourceV2Tables.sql @@ -0,0 +1,33 @@ + +ALTER TABLE DhFullData RENAME TO Legacy_FullData_V1; + +--batch-- + +ALTER TABLE Legacy_FullData_V1 ADD COLUMN MigrationFailed BIT NOT NULL DEFAULT 0; + +--batch-- + +CREATE TABLE FullData ( + -- compound primary key + DetailLevel TINYINT NOT NULL -- LOD detail level, not section detail level IE 0, 1, 2 not 6, 7, 8 + ,PosX INT NOT NULL + ,PosZ INT NOT NULL + + ,MinY INT NOT NULL + ,DataChecksum INT NOT NULL + + ,Data BLOB NULL + ,ColumnGenerationStep BLOB NULL + ,ColumnWorldCompressionMode BLOB NULL + ,Mapping BLOB NULL + + ,DataFormatVersion TINYINT NULL + ,CompressionMode TINYINT NULL + + ,ApplyToParent BIT NULL + + ,LastModifiedUnixDateTime BIGINT NOT NULL -- in GMT 0 + ,CreatedUnixDateTime BIGINT NOT NULL -- in GMT 0 + + ,PRIMARY KEY (DetailLevel, PosX, PosZ) +); diff --git a/core/src/main/resources/sqlScripts/0030-sqlite-changeTableJournaling.sql b/core/src/main/resources/sqlScripts/0030-sqlite-changeTableJournaling.sql new file mode 100644 index 000000000..6dd0b6898 --- /dev/null +++ b/core/src/main/resources/sqlScripts/0030-sqlite-changeTableJournaling.sql @@ -0,0 +1,9 @@ + +-- this PRAGMA will automatically commit, so we have to disable +-- DH's automatic transactions, otherwise the connection will throw an error + +--No Transactions-- + +-- James ran into some issues where Windows had trouble deleting the Journal file, +-- using TRUNCATE should fix that issue +PRAGMA journal_mode = TRUNCATE; diff --git a/core/src/main/resources/sqlScripts/0031-sqlite-useSqliteWalJournaling.sql b/core/src/main/resources/sqlScripts/0031-sqlite-useSqliteWalJournaling.sql new file mode 100644 index 000000000..1c48df1c7 --- /dev/null +++ b/core/src/main/resources/sqlScripts/0031-sqlite-useSqliteWalJournaling.sql @@ -0,0 +1,8 @@ + +-- these PRAGMA's will automatically commit, so we have to disable +-- DH's automatic transactions, otherwise the connection will throw an error + +--No Transactions-- + +pragma journal_mode = WAL; +pragma synchronous = NORMAL; diff --git a/core/src/main/resources/sqlScripts/0040-sqlite-removeRenderCache.sql b/core/src/main/resources/sqlScripts/0040-sqlite-removeRenderCache.sql new file mode 100644 index 000000000..8e7aef49a --- /dev/null +++ b/core/src/main/resources/sqlScripts/0040-sqlite-removeRenderCache.sql @@ -0,0 +1,4 @@ + +-- The render cache was discovered to not speed up LOD loading, +-- so to reduce DB file size it was removed. +drop table DhRenderData; diff --git a/core/src/main/resources/sqlScripts/0050-sqlite-addApplyToParentIndex.sql b/core/src/main/resources/sqlScripts/0050-sqlite-addApplyToParentIndex.sql new file mode 100644 index 000000000..3f8fb5e93 --- /dev/null +++ b/core/src/main/resources/sqlScripts/0050-sqlite-addApplyToParentIndex.sql @@ -0,0 +1,3 @@ + +-- significantly speeds up parent update handling +create index FullDataUpdatedIndex on FullData (ApplyToParent) where ApplyToParent = 1 diff --git a/core/src/main/resources/sqlScripts/readme.md b/core/src/main/resources/sqlScripts/readme.md index 18fdb2456..683e39c0f 100644 --- a/core/src/main/resources/sqlScripts/readme.md +++ b/core/src/main/resources/sqlScripts/readme.md @@ -38,5 +38,14 @@ CREATE TABLE TableTwo( ); ``` +### PRAGMA Auto Commits +Certain queries will auto commit after running, specifically certain `PRAGMA` commands. In that case we have to disable DH's automatic transactions by putting `--No Transactions--` somewhere in the file. Otherwise, when the system attempts to commit, it will fail due to the PRAGMA having already committed itself. +Due to how these commands work it's best to only have a single command in the file to prevent confusion and potential database corruption. + +```roomsql +--No Transactions-- + +PRAGMA journal_mode = TRUNCATE; +``` diff --git a/core/src/main/resources/sqlScripts/scriptList.txt b/core/src/main/resources/sqlScripts/scriptList.txt index 47c749378..d2104f42c 100644 --- a/core/src/main/resources/sqlScripts/scriptList.txt +++ b/core/src/main/resources/sqlScripts/scriptList.txt @@ -1,2 +1,7 @@ 0010-sqlite-createInitialDataTables.sql +0020-sqlite-createFullDataSourceV2Tables.sql +0030-sqlite-changeTableJournaling.sql +0031-sqlite-useSqliteWalJournaling.sql +0040-sqlite-removeRenderCache.sql +0050-sqlite-addApplyToParentIndex.sql diff --git a/core/src/main/java/com/seibel/distanthorizons/core/sql/FullDataRepo.java b/core/src/test/java/testItems/sql/TestCompoundKeyDto.java similarity index 51% rename from core/src/main/java/com/seibel/distanthorizons/core/sql/FullDataRepo.java rename to core/src/test/java/testItems/sql/TestCompoundKeyDto.java index 7d6e3f633..745014dec 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/sql/FullDataRepo.java +++ b/core/src/test/java/testItems/sql/TestCompoundKeyDto.java @@ -17,22 +17,48 @@ * along with this program. If not, see . */ -package com.seibel.distanthorizons.core.sql; +package testItems.sql; -import java.sql.SQLException; +import com.seibel.distanthorizons.core.pos.DhChunkPos; +import com.seibel.distanthorizons.core.sql.dto.IBaseDTO; -public class FullDataRepo extends AbstractDataSourceRepo +public class TestCompoundKeyDto implements IBaseDTO { - public static final String TABLE_NAME = "DhFullData"; + public DhChunkPos id; + public String value; - public FullDataRepo(String databaseType, String databaseLocation) throws SQLException - { - super(databaseType, databaseLocation); + + public TestCompoundKeyDto(DhChunkPos id, String value) + { + this.id = id; + this.value = value; } + @Override + public DhChunkPos getKey() { return this.id; } + @Override - public String getTableName() { return TABLE_NAME; } + public boolean equals(Object other) + { + if (other.getClass() != this.getClass()) + { + return false; + } + else + { + TestCompoundKeyDto otherDto = (TestCompoundKeyDto) other; + + return otherDto.id.equals(this.id) + && otherDto.value.equals(this.value); + } + } + + @Override + public String toString() + { + return this.id + ", " + this.value; + } } diff --git a/core/src/test/java/testItems/sql/TestCompoundKeyRepo.java b/core/src/test/java/testItems/sql/TestCompoundKeyRepo.java new file mode 100644 index 000000000..3b7ed530e --- /dev/null +++ b/core/src/test/java/testItems/sql/TestCompoundKeyRepo.java @@ -0,0 +1,104 @@ +/* + * 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 . + */ + +package testItems.sql; + +import com.seibel.distanthorizons.core.pos.DhChunkPos; +import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Map; + +public class TestCompoundKeyRepo extends AbstractDhRepo +{ + + public TestCompoundKeyRepo(String databaseType, String databaseLocation) throws SQLException + { + super(databaseType, databaseLocation, TestCompoundKeyDto.class); + + // note: this should only ever be done with the test repo. + // All long term tables should be created using a sql Script. + String createTableSql = + "CREATE TABLE IF NOT EXISTS "+this.getTableName()+"(\n" + + "XPos INT NOT NULL\n" + + ",ZPos INT NOT NULL\n" + + "\n" + + ",Value TEXT NULL\n" + + "\n" + + ",PRIMARY KEY (XPos, ZPos)" + + ");"; + this.queryDictionaryFirst(createTableSql); + } + + + @Override + public String getTableName() { return "TestCompound"; } + @Override + public String createWhereStatement(DhChunkPos key) { return "XPos = '"+key.x+"' AND ZPos = '"+key.z+"'"; } + + + @Override + public TestCompoundKeyDto convertDictionaryToDto(Map objectMap) throws ClassCastException + { + int xPos = (int) objectMap.get("XPos"); + int zPos = (int) objectMap.get("ZPos"); + String value = (String) objectMap.get("Value"); + + return new TestCompoundKeyDto(new DhChunkPos(xPos, zPos), value); + } + + @Override + public PreparedStatement createInsertStatement(TestCompoundKeyDto dto) throws SQLException + { + String sql = + "INSERT INTO "+this.getTableName()+" \n" + + "(XPos, ZPos, Value) \n" + + "VALUES(?,?,?);"; + PreparedStatement statement = this.createPreparedStatement(sql); + + int i = 1; // post-increment for the win! + statement.setObject(i++, dto.id.x); + statement.setObject(i++, dto.id.z); + + statement.setObject(i++, dto.value); + + return statement; + } + + @Override + public PreparedStatement createUpdateStatement(TestCompoundKeyDto dto) throws SQLException + { + String sql = + "UPDATE "+this.getTableName()+" \n" + + "SET \n" + + " Value = ? \n" + + "WHERE XPos = ? AND ZPos = ?"; + PreparedStatement statement = this.createPreparedStatement(sql); + + int i = 1; + statement.setObject(i++, dto.value); + + statement.setObject(i++, dto.id.x); + statement.setObject(i++, dto.id.z); + + return statement; + } + +} diff --git a/core/src/test/java/testItems/sql/TestDataRepo.java b/core/src/test/java/testItems/sql/TestPrimaryKeyRepo.java similarity index 76% rename from core/src/test/java/testItems/sql/TestDataRepo.java rename to core/src/test/java/testItems/sql/TestPrimaryKeyRepo.java index 38bb3c909..347e6d686 100644 --- a/core/src/test/java/testItems/sql/TestDataRepo.java +++ b/core/src/test/java/testItems/sql/TestPrimaryKeyRepo.java @@ -19,18 +19,18 @@ package testItems.sql; -import com.seibel.distanthorizons.core.sql.AbstractDhRepo; +import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Map; -public class TestDataRepo extends AbstractDhRepo +public class TestPrimaryKeyRepo extends AbstractDhRepo { - public TestDataRepo(String databaseType, String databaseLocation) throws SQLException + public TestPrimaryKeyRepo(String databaseType, String databaseLocation) throws SQLException { - super(databaseType, databaseLocation, TestDto.class); + super(databaseType, databaseLocation, TestSingleKeyDto.class); // note: this should only ever be done with the test repo. // All long term tables should be created using a sql Script. @@ -49,26 +49,23 @@ public class TestDataRepo extends AbstractDhRepo @Override public String getTableName() { return "Test"; } - @Override - public String getPrimaryKeyName() { return "Id"; } + @Override + public String createWhereStatement(Integer keyString) { return "Id = '"+keyString+"'"; } @Override - public TestDto convertDictionaryToDto(Map objectMap) throws ClassCastException + public TestSingleKeyDto convertDictionaryToDto(Map objectMap) throws ClassCastException { int id = (int) objectMap.get("Id"); String value = (String) objectMap.get("Value"); long longValue = (Long) objectMap.get("LongValue"); byte byteValue = (Byte) objectMap.get("ByteValue"); - return new TestDto(id, value, longValue, byteValue); + return new TestSingleKeyDto(id, value, longValue, byteValue); } - @Override - public String createSelectPrimaryKeySql(String primaryKey) { return "SELECT * FROM "+this.getTableName()+" WHERE Id = '"+primaryKey+"'"; } - @Override - public PreparedStatement createInsertStatement(TestDto dto) throws SQLException + public PreparedStatement createInsertStatement(TestSingleKeyDto dto) throws SQLException { String sql = "INSERT INTO "+this.getTableName()+" \n" + @@ -87,7 +84,7 @@ public class TestDataRepo extends AbstractDhRepo } @Override - public PreparedStatement createUpdateStatement(TestDto dto) throws SQLException + public PreparedStatement createUpdateStatement(TestSingleKeyDto dto) throws SQLException { String sql = "UPDATE "+this.getTableName()+" \n" + diff --git a/core/src/test/java/testItems/sql/TestDto.java b/core/src/test/java/testItems/sql/TestSingleKeyDto.java similarity index 82% rename from core/src/test/java/testItems/sql/TestDto.java rename to core/src/test/java/testItems/sql/TestSingleKeyDto.java index 488184eb5..90709a162 100644 --- a/core/src/test/java/testItems/sql/TestDto.java +++ b/core/src/test/java/testItems/sql/TestSingleKeyDto.java @@ -19,17 +19,19 @@ package testItems.sql; -import com.seibel.distanthorizons.core.sql.IBaseDTO; -import org.junit.Assert; +import com.seibel.distanthorizons.core.sql.dto.IBaseDTO; -public class TestDto implements IBaseDTO +public class TestSingleKeyDto implements IBaseDTO { public int id; public String value; public long longValue; public byte byteValue; - public TestDto(int id, String value, long longValue, byte byteValue) + + + + public TestSingleKeyDto(int id, String value, long longValue, byte byteValue) { this.id = id; this.value = value; @@ -38,7 +40,7 @@ public class TestDto implements IBaseDTO } @Override - public String getPrimaryKeyString() { return this.id+""; } + public Integer getKey() { return this.id; } @Override @@ -50,7 +52,7 @@ public class TestDto implements IBaseDTO } else { - TestDto otherDto = (TestDto) other; + TestSingleKeyDto otherDto = (TestSingleKeyDto) other; return otherDto.id == this.id && otherDto.value.equals(this.value) diff --git a/core/src/test/java/tests/CompressionTest.java b/core/src/test/java/tests/CompressionTest.java index 8eb54eda2..68319522c 100644 --- a/core/src/test/java/tests/CompressionTest.java +++ b/core/src/test/java/tests/CompressionTest.java @@ -19,152 +19,212 @@ package tests; -import net.jpountz.lz4.*; +import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; +import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO; +import com.seibel.distanthorizons.core.sql.repo.FullDataSourceV2Repo; import org.junit.Assert; -import org.junit.Test; import java.io.*; -import java.nio.file.Files; import java.text.CharacterIterator; import java.text.StringCharacterIterator; import java.util.ArrayList; -//import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; -//import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream; -//import com.github.luben.zstd.ZstdInputStream; -//import com.github.luben.zstd.ZstdOutputStream; - /** - * Results (2023-5-20):
- * 200 files

- * - * uncompressed

- * - * render data - ratio 1.0 (shocker :P)
- * read time in - 784 ms, avg 3 ms/file
- * write time in - 803 ms, avg 4 ms/file

- * - * full data - ratio 1.0
- * read time in - 2,213 ms, avg 11 ms/file
- * write time in - 1,753 ms, avg 8 ms/file


- * - * - * XZ

- * - * render data - ratio 0.1044
- * read time in - 2,413 ms, avg 12 ms/file
- * write time in - 28,441 ms, avg 142 ms/file

- * - * full data - ratio 0.1123
- * read time in - 5,888 ms, avg 29 ms/file
- * write time in - 79,675 ms, avg 398 ms/file


- * - * - * LZ4

- * - * render data - ratio 0.2933
- * read time in - 846 ms, avg 4 ms/file
- * write time in - 1,040 ms, avg 5 ms/file

- * - * full data - ratio 0.3275
- * read time in - 1,964 ms, avg 9 ms/file
- * write time in - 1,584 ms, avg 7 ms/file


- * - * - * Z Standard

- * - * render data - ratio 0.1791
- * read time in - 5,170 ms, avg 25 ms/file
- * write time in - 5,294 ms, avg 26 ms/file

- * - * full data - ratio 0.2060
- * read time in - 14,754 ms, avg 73 ms/file
- * write time in - 14,057 ms, avg 70 ms/file


- * - * - * * Note: * In order to test the compressors that aren't currently in use:
- * 1. Generate DH data and point the {@link CompressionTest#TEST_DIR} variable to the "Distant_Horizons" folder. - * 2. Add the following to build.gradle's dependencies block:
+ * 1. Generate DH data (64 DH render distance is suggest) + * 2. Point the {@link CompressionTest#TEST_DIR} variable to the world's "data" folder. + * 3. (Optional) Add the following to build.gradle's dependencies block:
* - * shadowMe("org.tukaani:xz:1.9") - * shadowMe("org.apache.commons:commons-compress:1.21") - * shadowMe("com.github.luben:zstd-jni:1.5.5-3") + * forgeShadowMe('org.apache.commons:commons-compress:1.26.1') + * forgeShadowMe('org.itadaki:bzip2:0.9.1') + * forgeShadowMe('lzma:lzma:0.0.1') *
- * 3. Uncomment the tests in this file
- * 4. Run the tests like normal + * 4. (Optional) Uncomment the tests in this file
+ * 5. Run tests like normal */ public class CompressionTest { - public static String TEST_DIR = "C:\\DistantHorizonsWorkspace\\distantHorizons\\fabric\\run\\saves\\Arcapelago\\data\\Distant_Horizons"; - public static String RENDER_DATA_PATH = TEST_DIR + "\\renderCache"; - public static String FULL_DATA_PATH = TEST_DIR + "\\data"; + public static String TEST_DIR = "C:\\DistantHorizonsWorkspace\\distant-horizons\\run\\saves\\Arcapelago\\data"; + public static String DB_FILE_NAME_PREFIX = "DistantHorizons"; + public static String UNCOMPRESSED_DB_FILE_NAME = "DistantHorizons.sqlite"; - /** limits the number of files tested so I don't have to wait 10 minutes for the slower compressors */ - public static int MAX_NUMBER_OF_FILES_TO_TEST = 200; + /** -1 will test all of them */ + public static int MAX_DTO_TEST_COUNT = -1; - // @Test + //@Test public void NoCompression() { String compressorName = "Uncompressed"; - - CreateInputStreamFunc createInputStreamFunc = (inputStream) -> inputStream; - CreateOutputStreamFunc createOutputStreamFunc = (outputStream) -> outputStream; - - - System.out.println(compressorName + " testing render data"); - this.testCompressor(compressorName, RENDER_DATA_PATH, createInputStreamFunc, createOutputStreamFunc); - System.out.println(compressorName + " testing full data"); - this.testCompressor(compressorName, FULL_DATA_PATH, createInputStreamFunc, createOutputStreamFunc); + this.testCompressor(compressorName, EDhApiDataCompressionMode.UNCOMPRESSED); } - // @Test - public void Lz4() + // collapse the following commented out code when looking at tests + + //@Test + //public void GZIP() // DNF + //{ + // String compressorName = "GZIP"; + // + // DhDataInputStream.CreateInputStreamFunc createInputStreamFunc = (inputStream) -> new GZIPInputStream(inputStream); + // DhDataOutputStream.CreateOutputStreamFunc createOutputStreamFunc = (outputStream) -> new GZIPOutputStream(outputStream); + // + // this.testCompressor(compressorName, createInputStreamFunc, createOutputStreamFunc); + //} + + //@Test + //public void BZip2() // DNF + //{ + // String compressorName = "bzip2"; + // + // DhDataInputStream.CreateInputStreamFunc createInputStreamFunc = (inputStream) -> new BZip2InputStream(inputStream, true); + // DhDataOutputStream.CreateOutputStreamFunc createOutputStreamFunc = (outputStream) -> new BZip2OutputStream(outputStream); + // + // this.testCompressor(compressorName, createInputStreamFunc, createOutputStreamFunc); + //} + + //@Test + //public void blockLz4() // DNF + //{ + // String compressorName = "Block LZ4"; + // + // DhDataInputStream.CreateInputStreamFunc createInputStreamFunc = (inputStream) -> new BlockLZ4CompressorInputStream(inputStream); + // DhDataOutputStream.CreateOutputStreamFunc createOutputStreamFunc = (outputStream) -> new BlockLZ4CompressorOutputStream(outputStream); + // + // this.testCompressor(compressorName, createInputStreamFunc, createOutputStreamFunc); + //} + + //@Test + //public void lzma() // DNF, doesn't support flushing + //{ + // String compressorName = "lzma"; + // + // DhDataInputStream.CreateInputStreamFunc createInputStreamFunc = (inputStream) -> new LZMA2InputStream(inputStream, LZMA2Options.DICT_SIZE_MIN); + // DhDataOutputStream.CreateOutputStreamFunc createOutputStreamFunc = (outputStream) -> new LZMAOutputStream(outputStream, new LZMA2Options(), -1); + // + // this.testCompressor(compressorName, createInputStreamFunc, createOutputStreamFunc); + //} + + //@Test + //public void deflate() // DNF + //{ + // String compressorName = "deflate"; + // + // DhDataInputStream.CreateInputStreamFunc createInputStreamFunc = (inputStream) -> new DeflateCompressorInputStream(inputStream); + // DhDataOutputStream.CreateOutputStreamFunc createOutputStreamFunc = (outputStream) -> new DeflateCompressorOutputStream(outputStream); + // + // this.testCompressor(compressorName, createInputStreamFunc, createOutputStreamFunc); + //} + + //@Test + //public void snappy() // DNF + //{ + // String compressorName = "snappy"; + // + // DhDataInputStream.CreateInputStreamFunc createInputStreamFunc = (inputStream) -> new SnappyCompressorInputStream(inputStream); + // DhDataOutputStream.CreateOutputStreamFunc createOutputStreamFunc = (outputStream) -> new SnappyCompressorOutputStream(outputStream, Long.MAX_VALUE); + // + // this.testCompressor(compressorName, createInputStreamFunc, createOutputStreamFunc); + //} + + + //@Test + //public void Zstd() + //{ + // String compressorName = "Zstd"; + // + // DhDataInputStream.CreateInputStreamFunc createInputStreamFunc = (inputStream) -> new ZstdInputStream(inputStream); + // DhDataOutputStream.CreateOutputStreamFunc createOutputStreamFunc = (outputStream) -> new ZstdOutputStream(outputStream); + // + // this.testCompressor(compressorName, createInputStreamFunc, createOutputStreamFunc); + //} + + ////@Test + //public void ZstdDictionary() throws SQLException // isn't any better than normal Zstd + //{ + // String compressorName = "ZstdDictionary"; + // + // BufferPool pool = RecyclingBufferPool.INSTANCE; + // + // + // // create the dictionary + // byte[] dictionary; + // { + // String uncompressedDatabaseFilePath = TEST_DIR + "/" + UNCOMPRESSED_DB_FILE_NAME; + // FullDataSourceV2Repo uncompressedRepo = new FullDataSourceV2Repo("jdbc:sqlite", uncompressedDatabaseFilePath); + // ArrayList positionList = uncompressedRepo.getAllPositions(); + // + // // sample size of 10 MB or less + // // dictionary size of 64 KB (1 MB and 10 MB's both seemed to perform worse) + // ZstdDictTrainer dictTrainer = new ZstdDictTrainer(10 * 1024 * 1024, 64 * 1024); + // + // for (int i = 0; i < positionList.size(); i++) + // { + // DhSectionPos pos = positionList.get(i); + // FullDataSourceV2DTO uncompressedDto = uncompressedRepo.getByKey(pos); + // + // dictTrainer.addSample(uncompressedDto.dataByteArray); + // } + // + // dictionary = dictTrainer.trainSamples(); + // } + // + // + // + // + // DhDataInputStream.CreateInputStreamFunc createInputStreamFunc = (inputStream) -> + // { + // ZstdInputStream stream = new ZstdInputStream(inputStream, pool); + // stream.setDict(dictionary); + // return stream; + // }; + // DhDataOutputStream.CreateOutputStreamFunc createOutputStreamFunc = (outputStream) -> + // { + // ZstdOutputStream stream = new ZstdOutputStream(outputStream, pool); + // stream.setDict(dictionary); + // return stream; + // }; + // + // this.testCompressor(compressorName, createInputStreamFunc, createOutputStreamFunc); + //} + + //@Test + //public void Lz4FastCompression() // DNF + //{ + // String compressorName = "LZ4FastCompression"; + // + // LZ4FastDecompressor fastCompressor = LZ4Factory.fastestInstance().fastDecompressor(); + // + // DhDataInputStream.CreateInputStreamFunc createInputStreamFunc = (inputStream) -> new LZ4BlockInputStream(inputStream, fastCompressor); + // DhDataOutputStream.CreateOutputStreamFunc createOutputStreamFunc = (outputStream) -> new LZ4BlockOutputStream(outputStream); + // + // this.testCompressor(compressorName, createInputStreamFunc, createOutputStreamFunc); + //} + + //@Test + public void Lz4() // fast, poor compression { String compressorName = "LZ4"; - - CreateInputStreamFunc createInputStreamFunc = (inputStream) -> new LZ4FrameInputStream(inputStream); - CreateOutputStreamFunc createOutputStreamFunc = (outputStream) -> new LZ4FrameOutputStream(outputStream); - - - System.out.println(compressorName + " testing render data"); - this.testCompressor(compressorName, RENDER_DATA_PATH, createInputStreamFunc, createOutputStreamFunc); - System.out.println(compressorName + " testing full data"); - this.testCompressor(compressorName, FULL_DATA_PATH, createInputStreamFunc, createOutputStreamFunc); + this.testCompressor(compressorName, EDhApiDataCompressionMode.LZ4); + } + + //@Test + public void Zstd() // middle of the road + { + String compressorName = "Zstd"; + this.testCompressor(compressorName, EDhApiDataCompressionMode.Z_STD); + } + + //@Test + public void LZMA2() // very slow, very good compression though + { + String compressorName = "LZMA"; + this.testCompressor(compressorName, EDhApiDataCompressionMode.LZMA2); } - -// @Test -// public void Zstandard() -// { -// String compressorName = "Z_std"; -// -// CreateInputStreamFunc createInputStreamFunc = (inputStream) -> new ZstdInputStream(inputStream); -// CreateOutputStreamFunc createOutputStreamFunc = (outputStream) -> new ZstdOutputStream(outputStream); -// -// -// System.out.println(compressorName+" testing render data"); -// this.testCompressor(compressorName, RENDER_DATA_PATH, createInputStreamFunc, createOutputStreamFunc); -// System.out.println(compressorName+" testing full data"); -// this.testCompressor(compressorName, FULL_DATA_PATH, createInputStreamFunc, createOutputStreamFunc); -// } - -// @Test -// public void Xz() -// { -// String compressorName = "XZ"; -// -// CreateInputStreamFunc createInputStreamFunc = (inputStream) -> new XZCompressorInputStream(inputStream); -// CreateOutputStreamFunc createOutputStreamFunc = (outputStream) -> new XZCompressorOutputStream(outputStream); -// -// -// System.out.println(compressorName+" testing render data"); -// this.testCompressor(compressorName, RENDER_DATA_PATH, createInputStreamFunc, createOutputStreamFunc); -// System.out.println(compressorName+" testing full data"); -// this.testCompressor(compressorName, FULL_DATA_PATH, createInputStreamFunc, createOutputStreamFunc); -// } @@ -172,149 +232,139 @@ public class CompressionTest // testing methods // //=================// - @FunctionalInterface - public interface CreateInputStreamFunc + private void testCompressor(String compressorName, EDhApiDataCompressionMode compressionMode) { - InputStream apply(InputStream inputStream) throws Exception; + System.out.println("\n"); + System.out.println("Testing " + compressorName); - } - - @FunctionalInterface - public interface CreateOutputStreamFunc - { - OutputStream apply(OutputStream outputStream) throws Exception; - } - - private void testCompressor( - String compressorName, String inputFolderPath, - CreateInputStreamFunc createInputStreamFunc, - CreateOutputStreamFunc createOutputStreamFunc) - { - long totalUncompressedFileSizeInBytes = 0; - long totalCompressedFileSizeInBytes = 0; + long minUncompressedDtoSizeInBytes = Long.MAX_VALUE; + long maxUncompressedDtoSizeInBytes = 0; + long avgUncompressedDtoSizeInBytes = 0; + + long minCompressedDtoSizeInBytes = Long.MAX_VALUE; + long maxCompressedDtoSizeInBytes = 0; + long avgCompressedDtoSizeInBytes = 0; + + long totalReadTimeInNano = 0; + long totalWriteTimeInNano = 0; + + + long totalUncompressedFileSizeInBytes; + long totalCompressedFileSizeInBytes; - long totalReadTimeInMs = 0; - long totalWriteTimeInMs = 0; try { - File inputFolder = new File(inputFolderPath); - File[] inputFileArray = inputFolder.listFiles(); - Assert.assertNotNull(inputFileArray); + String uncompressedDatabaseFilePath = TEST_DIR + "/" + UNCOMPRESSED_DB_FILE_NAME; + File uncompressedDatabaseFile = new File(uncompressedDatabaseFilePath); + Assert.assertTrue(uncompressedDatabaseFile.exists()); - File compressedFolder = new File(inputFolderPath + "\\" + compressorName); - compressedFolder.delete(); - compressedFolder.mkdirs(); + FullDataSourceV2Repo uncompressedRepo = new FullDataSourceV2Repo("jdbc:sqlite", uncompressedDatabaseFilePath); - int processedFileCount = 0; - for (File inputFile : inputFileArray) + String compressedDatabaseFilePath = TEST_DIR + "/output/" + DB_FILE_NAME_PREFIX + "_" + compressorName + ".sqlite"; + File compressedDatabaseFile = new File(compressedDatabaseFilePath); + compressedDatabaseFile.mkdirs(); + compressedDatabaseFile.delete(); + Assert.assertTrue(!compressedDatabaseFile.exists()); + FullDataSourceV2Repo compressedRepo = new FullDataSourceV2Repo("jdbc:sqlite", compressedDatabaseFilePath); + + + + ArrayList positionList = uncompressedRepo.getAllPositions(); + totalUncompressedFileSizeInBytes = uncompressedRepo.getTotalDataSizeInBytes(); + System.out.println("Found [" + positionList.size() + "] DTOs."); + + long processedDtoCount = 0; + int maxTestPosition = (MAX_DTO_TEST_COUNT == -1) ? positionList.size() : MAX_DTO_TEST_COUNT; + for (int i = 0; i < maxTestPosition; i++) { - if (inputFile.isDirectory()) + try { - continue; - } - - // can be used to speed up the tests - if (processedFileCount >= MAX_NUMBER_OF_FILES_TO_TEST) - { - break; - } - - - - // uncompressed file input // - ArrayList originalFileByteArray = new ArrayList<>(); - totalUncompressedFileSizeInBytes += Files.size(inputFile.toPath()); - - try (FileInputStream fileStream = new FileInputStream(inputFile); - BufferedInputStream bufferedStream = new BufferedInputStream(fileStream); - DataInputStream dataStream = new DataInputStream(bufferedStream)) - { - try + DhSectionPos pos = positionList.get(i); + if (i % 20 == 0) { - while (true) - { - byte nextByte = dataStream.readByte(); - originalFileByteArray.add(nextByte); - } + System.out.println(i + "/" + maxTestPosition); } - catch (EOFException e) - { /* end of file reached */ } + + + + // uncompressed input // + + FullDataSourceV2DTO uncompressedDto = uncompressedRepo.getByKey(pos); + Assert.assertEquals(uncompressedDto.compressionModeEnum, EDhApiDataCompressionMode.UNCOMPRESSED); + FullDataSourceV2 uncompressedDataSource = uncompressedDto.createUnitTestDataSource(); + + long uncompressedDtoSize = uncompressedRepo.getDataSizeInBytes(pos); + minUncompressedDtoSizeInBytes = Math.min(uncompressedDtoSize, minUncompressedDtoSizeInBytes); + maxUncompressedDtoSizeInBytes = Math.max(uncompressedDtoSize, maxUncompressedDtoSizeInBytes); + avgUncompressedDtoSizeInBytes += uncompressedDtoSize; + + + + // compress file // + + long startWriteNanoTime = System.nanoTime(); + + FullDataSourceV2DTO compressedDto = FullDataSourceV2DTO.CreateFromDataSource(uncompressedDataSource, compressionMode); + compressedRepo.save(compressedDto); + + long endWriteNanoTime = System.nanoTime(); + totalWriteTimeInNano += (endWriteNanoTime - startWriteNanoTime); + + + long compressedDtoSize = compressedRepo.getDataSizeInBytes(pos); + minCompressedDtoSizeInBytes = Math.min(compressedDtoSize, minCompressedDtoSizeInBytes); + maxCompressedDtoSizeInBytes = Math.max(compressedDtoSize, maxCompressedDtoSizeInBytes); + avgCompressedDtoSizeInBytes += compressedDtoSize; + + + + // read compressed file // + + long startReadNanoTime = System.nanoTime(); + + compressedDto = compressedRepo.getByKey(pos); + FullDataSourceV2 compressedDataSource = compressedDto.createUnitTestDataSource(); + + long endReadMsTime = System.nanoTime(); + totalReadTimeInNano += (endReadMsTime - startReadNanoTime); + + + processedDtoCount++; } - - - - // compress file // - long startWriteMsTime = System.currentTimeMillis(); - - File compressedFile = new File(inputFolderPath + "\\" + compressorName + "\\" + inputFile.getName()); - compressedFile.delete(); - compressedFile.createNewFile(); - - try (FileOutputStream fileStream = new FileOutputStream(compressedFile); - BufferedOutputStream bufferedStream = new BufferedOutputStream(fileStream); - OutputStream compressorStream = createOutputStreamFunc.apply(bufferedStream); - DataOutputStream dataStream = new DataOutputStream(compressorStream)) + catch (Exception | Error e) { - for (byte nextByte : originalFileByteArray) - { - dataStream.writeByte(nextByte); - } + e.printStackTrace(); + Assert.fail(e.getMessage()); } - - long endWriteMsTime = System.currentTimeMillis(); - totalWriteTimeInMs += (endWriteMsTime - startWriteMsTime); - - totalCompressedFileSizeInBytes += Files.size(compressedFile.toPath()); - - - - // read compressed file // - long startReadMsTime = System.currentTimeMillis(); - ArrayList compressedFileByteArray = new ArrayList<>(); - - try (FileInputStream fileStream = new FileInputStream(compressedFile); - BufferedInputStream bufferedStream = new BufferedInputStream(fileStream); - InputStream compressorStream = createInputStreamFunc.apply(bufferedStream); - DataInputStream dataStream = new DataInputStream(compressorStream)) - { - try - { - while (true) - { - byte nextByte = dataStream.readByte(); - compressedFileByteArray.add(nextByte); - } - } - catch (EOFException e) - { /* end of file reached */ } - } - - long endReadMsTime = System.currentTimeMillis(); - totalReadTimeInMs += (endReadMsTime - startReadMsTime); - - - // confirm the file contents are the same - Assert.assertEquals("byte array size mismatch", compressedFileByteArray.size(), originalFileByteArray.size()); - for (int i = 0; i < compressedFileByteArray.size(); i++) - { - Assert.assertEquals("array content mismatch at index [" + i + "]", compressedFileByteArray.get(i), originalFileByteArray.get(i)); - } - - - processedFileCount++; } + + totalCompressedFileSizeInBytes = compressedRepo.getTotalDataSizeInBytes(); + + avgCompressedDtoSizeInBytes /= processedDtoCount; + avgUncompressedDtoSizeInBytes /= processedDtoCount; + + double compressionRatio = (totalCompressedFileSizeInBytes / (double) totalUncompressedFileSizeInBytes); String compressionRatioString = compressionRatio + ""; compressionRatioString = compressionRatioString.substring(0, Math.min(6, compressionRatioString.length())); - System.out.println("Uncompressed file size: [" + humanReadableByteCountSI(totalUncompressedFileSizeInBytes) + "] Compressed file size: [" + humanReadableByteCountSI(totalCompressedFileSizeInBytes) + "]. Compression ratio: [" + compressionRatioString + "]."); - System.out.println("Total read time in MS: [" + totalReadTimeInMs + "] Average read time per file: [" + (totalReadTimeInMs / processedFileCount) + "]"); - System.out.println("Total write time in MS: [" + totalWriteTimeInMs + "] Average write time per file: [" + (totalWriteTimeInMs / processedFileCount) + "]"); + + System.out.println("\n"); + System.out.println("Results: " + compressorName); + System.out.println(); + System.out.println("Total uncompressed data: [" + humanReadableByteCountSI(totalUncompressedFileSizeInBytes) + "] Total compressed data: [" + humanReadableByteCountSI(totalCompressedFileSizeInBytes) + "]. Compression ratio: [" + compressionRatioString + "]."); + System.out.println("Min uncompressed data: [" + humanReadableByteCountSI(minUncompressedDtoSizeInBytes) + "] Min compressed data: [" + humanReadableByteCountSI(minCompressedDtoSizeInBytes) + "]."); + System.out.println("Max uncompressed data: [" + humanReadableByteCountSI(maxUncompressedDtoSizeInBytes) + "] Max compressed data: [" + humanReadableByteCountSI(maxCompressedDtoSizeInBytes) + "]."); + System.out.println("Avg uncompressed data: [" + humanReadableByteCountSI(avgUncompressedDtoSizeInBytes) + "] Avg compressed data: [" + humanReadableByteCountSI(avgCompressedDtoSizeInBytes) + "]."); + System.out.println(); + System.out.println("Total read time in MS: [" + totalReadTimeInNano / 1_000_000.0 + "] Average read time per dto: [" + (totalReadTimeInNano / processedDtoCount) / 1_000_000.0 + "]"); + System.out.println("Total write time in MS: [" + totalWriteTimeInNano / 1_000_000.0 + "] Average write time per dto: [" + (totalWriteTimeInNano / processedDtoCount) / 1_000_000.0 + "]"); + System.out.println(); } catch (Exception e) { diff --git a/core/src/test/java/tests/DhRepoSqliteTest.java b/core/src/test/java/tests/DhRepoSqliteTest.java index 0a2030a8a..066106110 100644 --- a/core/src/test/java/tests/DhRepoSqliteTest.java +++ b/core/src/test/java/tests/DhRepoSqliteTest.java @@ -19,44 +19,50 @@ package tests; +import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.sql.DatabaseUpdater; +import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; -import testItems.sql.TestDataRepo; -import testItems.sql.TestDto; +import testItems.sql.TestCompoundKeyRepo; +import testItems.sql.TestCompoundKeyDto; +import testItems.sql.TestPrimaryKeyRepo; +import testItems.sql.TestSingleKeyDto; import java.io.File; import java.sql.SQLException; import java.util.Map; /** - * Validates {@link com.seibel.distanthorizons.core.sql.AbstractDhRepo} is set up correctly. + * Validates {@link AbstractDhRepo} is set up correctly. */ public class DhRepoSqliteTest { public static String DATABASE_TYPE = "jdbc:sqlite"; + public static String DB_FILE_NAME = "test.sqlite"; - @Test - public void testFileSqlite() + + @BeforeClass + public static void testSetup() { - String dbFileName = "test.sqlite"; - - - File dbFile = new File(dbFileName); + File dbFile = new File(DB_FILE_NAME); if (dbFile.exists()) { Assert.assertTrue("unable to delete old test DB File.", dbFile.delete()); } - - - TestDataRepo testDataRepo = null; + } + + + + @Test + public void testPrimaryKeyRepo() + { + TestPrimaryKeyRepo primaryKeyRepo = null; try { - testDataRepo = new TestDataRepo(DATABASE_TYPE, dbFileName); - - dbFile = new File(dbFileName); - Assert.assertTrue("dbFile not created", dbFile.exists()); + primaryKeyRepo = new TestPrimaryKeyRepo(DATABASE_TYPE, DB_FILE_NAME); @@ -65,15 +71,15 @@ public class DhRepoSqliteTest //==========================// // check that the schema table is created - Map autoUpdateTablePresentResult = testDataRepo.queryDictionaryFirst("SELECT name FROM sqlite_master WHERE type='table' AND name='"+DatabaseUpdater.SCHEMA_TABLE_NAME+"';"); + Map autoUpdateTablePresentResult = primaryKeyRepo.queryDictionaryFirst("SELECT name FROM sqlite_master WHERE type='table' AND name='"+DatabaseUpdater.SCHEMA_TABLE_NAME+"';"); if (autoUpdateTablePresentResult == null || autoUpdateTablePresentResult.get("name") == null) { Assert.fail("Auto DB update table missing."); } // check that the update scripts aren't run multiple times - TestDataRepo altDataRepoOne = new TestDataRepo(DATABASE_TYPE, dbFileName); - TestDataRepo altDataRepoTwo = new TestDataRepo(DATABASE_TYPE, dbFileName); + TestPrimaryKeyRepo altDataRepoOne = new TestPrimaryKeyRepo(DATABASE_TYPE, DB_FILE_NAME); + TestPrimaryKeyRepo altDataRepoTwo = new TestPrimaryKeyRepo(DATABASE_TYPE, DB_FILE_NAME); @@ -82,39 +88,39 @@ public class DhRepoSqliteTest //===========// // insert - TestDto insertDto = new TestDto(0, "a", 0L, (byte) 0); - testDataRepo.save(insertDto); + TestSingleKeyDto insertDto = new TestSingleKeyDto(0, "a", 0L, (byte) 0); + primaryKeyRepo.save(insertDto); // get - TestDto getDto = testDataRepo.getByPrimaryKey("0"); + TestSingleKeyDto getDto = primaryKeyRepo.getByKey(0); Assert.assertNotNull("get failed, null returned", getDto); Assert.assertEquals("get/insert failed, not equal", insertDto, getDto); // exists - DTO present - Assert.assertTrue("DTO exists failed", testDataRepo.exists(insertDto)); - Assert.assertTrue("DTO exists failed", testDataRepo.existsWithPrimaryKey(insertDto.getPrimaryKeyString())); + Assert.assertTrue("DTO exists failed", primaryKeyRepo.exists(insertDto)); + Assert.assertTrue("DTO exists failed", primaryKeyRepo.existsWithKey(insertDto.getKey())); // update - TestDto updateMetaFile = new TestDto(0, "b", Long.MAX_VALUE, Byte.MAX_VALUE); - testDataRepo.save(updateMetaFile); + TestSingleKeyDto updateMetaFile = new TestSingleKeyDto(0, "b", Long.MAX_VALUE, Byte.MAX_VALUE); + primaryKeyRepo.save(updateMetaFile); // get - getDto = testDataRepo.getByPrimaryKey("0"); + getDto = primaryKeyRepo.getByKey(0); Assert.assertNotNull("get failed, null returned", getDto); Assert.assertEquals("get/insert failed, not equal", updateMetaFile, getDto); // delete - testDataRepo.delete(updateMetaFile); + primaryKeyRepo.delete(updateMetaFile); // get - getDto = testDataRepo.getByPrimaryKey("0"); + getDto = primaryKeyRepo.getByKey(0); Assert.assertNull("delete failed, not null returned", getDto); // exists - DTO absent - Assert.assertFalse("DTO exists failed", testDataRepo.exists(insertDto)); - Assert.assertFalse("DTO exists failed", testDataRepo.existsWithPrimaryKey(insertDto.getPrimaryKeyString())); + Assert.assertFalse("DTO exists failed", primaryKeyRepo.exists(insertDto)); + Assert.assertFalse("DTO exists failed", primaryKeyRepo.existsWithKey(insertDto.getKey())); } catch (SQLException e) @@ -123,11 +129,75 @@ public class DhRepoSqliteTest } finally { - if (testDataRepo != null) + if (primaryKeyRepo != null) { - testDataRepo.close(); + primaryKeyRepo.close(); } } } + @Test + public void testCompoundKeyRepo() + { + TestCompoundKeyRepo compoundKeyRepo = null; + try + { + compoundKeyRepo = new TestCompoundKeyRepo(DATABASE_TYPE, DB_FILE_NAME); + + + + //===========// + // DTO tests // + //===========// + + // insert + TestCompoundKeyDto insertDto = new TestCompoundKeyDto(new DhChunkPos(1, 2), "a"); + compoundKeyRepo.save(insertDto); + + // get + TestCompoundKeyDto getDto = compoundKeyRepo.getByKey(new DhChunkPos(1, 2)); + Assert.assertNotNull("get failed, null returned", getDto); + Assert.assertEquals("get/insert failed, not equal", insertDto, getDto); + + // exists - DTO present + Assert.assertTrue("DTO exists failed", compoundKeyRepo.exists(insertDto)); + Assert.assertTrue("DTO exists failed", compoundKeyRepo.existsWithKey(insertDto.getKey())); + + + // update + TestCompoundKeyDto updateMetaFile = new TestCompoundKeyDto(new DhChunkPos(1, 2), "b"); + compoundKeyRepo.save(updateMetaFile); + + // get + getDto = compoundKeyRepo.getByKey(new DhChunkPos(1, 2)); + Assert.assertNotNull("get failed, null returned", getDto); + Assert.assertEquals("get/insert failed, not equal", updateMetaFile, getDto); + + + // delete + compoundKeyRepo.delete(updateMetaFile); + + // get + getDto = compoundKeyRepo.getByKey(new DhChunkPos(1, 2)); + Assert.assertNull("delete failed, not null returned", getDto); + + // exists - DTO absent + Assert.assertFalse("DTO exists failed", compoundKeyRepo.exists(insertDto)); + Assert.assertFalse("DTO exists failed", compoundKeyRepo.existsWithKey(insertDto.getKey())); + + } + catch (SQLException e) + { + Assert.fail(e.getMessage()); + } + finally + { + if (compoundKeyRepo != null) + { + compoundKeyRepo.close(); + } + } + } + + } diff --git a/api/src/test/java/tests/ExampleTest.java b/core/src/test/java/tests/ExampleCoreTest.java similarity index 97% rename from api/src/test/java/tests/ExampleTest.java rename to core/src/test/java/tests/ExampleCoreTest.java index 63e1ecf89..8e51284d0 100644 --- a/api/src/test/java/tests/ExampleTest.java +++ b/core/src/test/java/tests/ExampleCoreTest.java @@ -28,7 +28,7 @@ import org.junit.Test; * @author James Seibel * @version 2022-9-5 */ -public class ExampleTest +public class ExampleCoreTest { @Test diff --git a/core/src/test/java/tests/SqliteSetupTest.java b/core/src/test/java/tests/SqliteSetupTest.java index 350e8edbd..3e19bd973 100644 --- a/core/src/test/java/tests/SqliteSetupTest.java +++ b/core/src/test/java/tests/SqliteSetupTest.java @@ -43,12 +43,12 @@ public class SqliteSetupTest Assert.assertTrue("Unable to delete test database.", dbFile.delete()); } - @Test - public void testInMemorySqlite() - { - String databaseLocation = ":memory:"; - testSqliteDatabase(DATABASE_TYPE, databaseLocation); - } + //@Test + //public void testInMemorySqlite() + //{ + // String databaseLocation = ":memory:"; + // testSqliteDatabase(DATABASE_TYPE, databaseLocation); + //}