Compare commits

...

137 Commits

Author SHA1 Message Date
James Seibel 7c4ac2bd7e remove dev from version number 2025-12-23 22:55:40 -06:00
James Seibel 8c13c2cf47 Fix toggling world gen not recreating queue 2025-12-23 22:55:40 -06:00
James Seibel 802019ff72 up DH api version 5.0.0 -> 5.1.0 2025-12-23 20:01:06 -06:00
James Seibel 141890556c Revert "remove deprecated getHeight() from DhApiLevelWrapper"
This reverts commit 50bdb73a52.
2025-12-23 19:56:28 -06:00
James Seibel 353838db41 add experimental option to ignore rendering dimensions by name 2025-12-23 12:22:00 -06:00
James Seibel f1547477c9 add clientLevelWrapper to DhApiRenderParam 2025-12-23 12:20:42 -06:00
James Seibel 535a645a84 minor internal API cleanup 2025-12-23 12:19:14 -06:00
James Seibel 2dc7f02b32 Remove experimental option onlyLoadCenterLods
option is now merged into main
2025-12-23 12:18:28 -06:00
James Seibel 50bdb73a52 remove deprecated getHeight() from DhApiLevelWrapper
use getMaxHeight() instead
2025-12-23 12:06:15 -06:00
James Seibel 53e6c95432 commenting DhTerrainShaderProgram 2025-12-23 08:57:51 -06:00
James Seibel 36f0029e45 Fix earth curvature shader compiling 2025-12-23 08:47:44 -06:00
s809 5067e970a2 Use another method to create a buffer 2025-12-23 12:50:02 +05:00
James Seibel 167ca94e69 Remove deprecated disableVanillaFog config 2025-12-22 20:31:24 -06:00
James Seibel 8d94b86bfd Hide network config changes by default 2025-12-22 14:51:29 -06:00
James Seibel a29567430e Net only log changed config values 2025-12-22 14:37:34 -06:00
James Seibel fb2dae48e2 re-enable remote timestamp getting 2025-12-22 14:21:12 -06:00
James Seibel 948b4bfd9c comment out debug log 2025-12-22 14:17:57 -06:00
James Seibel ca44256ca9 disable full data debug phantom array stacks 2025-12-22 14:17:47 -06:00
James Seibel a29b6a5aab remove unnecessary config appearance check 2025-12-22 14:17:13 -06:00
James Seibel 868254ccc8 try fixing rare leak in delayed data source cache
Didn't fix the problem, but shouldn't hurt
2025-12-22 14:16:34 -06:00
James Seibel 195fde8d73 quad tree spilt request cleanup 2025-12-22 13:58:26 -06:00
James Seibel ce7b9b94b6 fix/improve world gen/retrieval error handling 2025-12-22 13:58:26 -06:00
James Seibel 1f0c2e286a fix network splitting requests 2025-12-22 13:58:26 -06:00
James Seibel f79fd5e06f error handling in AbstactDhLevel chunk update 2025-12-22 13:58:26 -06:00
James Seibel 47c1d3955f failed attmpt to fix leaks
Breaks split world gen requests
2025-12-22 13:57:49 -06:00
James Seibel 2c5f5a3d4c minor refactors 2025-12-22 09:46:21 -06:00
James Seibel 81c533051e close errored data sources in full data provider 2025-12-22 08:35:15 -06:00
James Seibel 5cbe5ecfd8 Fix dis/re-enabling world gen queuing 2025-12-21 19:44:48 -06:00
James Seibel d4b4d28c9f Fix null error log in Data source provider 2025-12-21 08:53:27 -06:00
James Seibel b8e653b5f7 Fix phantom checkout not updating stack trace 2025-12-21 08:52:44 -06:00
James Seibel 80fea09598 Fix concurrency error in LodQuadTree 2025-12-21 08:52:25 -06:00
James Seibel 1d4f914a9f Merge branch 'worldGenRefactor' 2025-12-20 10:53:39 -06:00
James Seibel bf92dea2eb reduce stuttering at the cost of lighting quality 2025-12-20 10:52:51 -06:00
s809 2dd675b8da Handle generated LOD updates outside the render thread 2025-12-20 15:22:26 +05:00
s809 ff3145336d Revert "Run plugin messages on a DH thread"
This reverts commit 280181c91e.
2025-12-20 14:32:39 +05:00
James Seibel 280181c91e Run plugin messages on a DH thread 2025-12-19 16:54:29 -06:00
James Seibel 60232e713b refactor world gen queue 2025-12-19 16:54:07 -06:00
James Seibel 55d9030954 Remove extra particle for world gen 2025-12-18 10:20:01 -06:00
James Seibel 452bd75f5d remove chunkWrapper.isStillValid() 2025-12-18 10:18:07 -06:00
James Seibel 72be1e2602 Remove LodRenderSection.isFullyGenerated() 2025-12-18 10:17:36 -06:00
James Seibel 1c30213aca up version number 2.4.3 -> 2.4.4-dev 2025-12-18 10:04:41 -06:00
James Seibel e9a044308f remove dev from version number 2025-12-18 09:35:07 -06:00
James Seibel 1aabc0c792 remove chunkWrapper.isStillValid() 2025-12-18 09:35:02 -06:00
James Seibel 4a1513ed65 fix compiling 2025-12-17 22:41:22 -06:00
James Seibel 6d98c9cb84 start world gen refactoring 2025-12-17 22:39:23 -06:00
James Seibel b1b0642fbe LodRenderSection commenting/regions 2025-12-17 09:32:12 -06:00
James Seibel eecb28d11f Fix GLProxy error in multiplayer
Make some GLProxy methods static to prevent setup order issues
2025-12-17 09:02:07 -06:00
James Seibel 90564f2537 fix javadoc in LevelWrapper 2025-12-16 16:39:03 -06:00
James Seibel ded0b979cf Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2025-12-16 14:45:57 -06:00
James Seibel ed9cc5485c Add SSAO fade out distance 2025-12-16 14:45:53 -06:00
s809 cbd5974657 Fix packet handle errors not showing on F3 screen 2025-12-17 00:15:55 +05:00
James Seibel 0e5fba58ab minor shader program refactor 2025-12-16 09:13:22 -06:00
James Seibel 2943e63382 slight light engine optimization 2025-12-15 14:37:15 -06:00
James Seibel 30564aade7 up version number 2.4.2 -> 2.4.3-dev 2025-12-15 10:17:28 -06:00
James Seibel aabb90ada6 remove dev from version number 2025-12-15 09:00:15 -06:00
James Seibel 963a8dc53f comment LevelWrapper getDimensionName() 2025-12-15 08:55:40 -06:00
James Seibel aa6d69385b Move GC warning into the log 2025-12-15 08:44:06 -06:00
James Seibel f42c9cf8fb Improve initial library check error handling 2025-12-14 22:29:08 -06:00
James Seibel 92e0011c8d Fix auto update success dialog 2025-12-14 21:50:56 -06:00
James Seibel c20d95a7c7 improve spacing for self updater version log 2025-12-14 21:21:45 -06:00
James Seibel 353aa1ed2c maybe improve ZStd version check 2025-12-14 21:20:42 -06:00
James Seibel 5aa43ebcc8 hide LODs when underwater 2025-12-14 17:22:35 -06:00
James Seibel b6145461b6 add note to ignored block CSV 2025-12-14 17:02:53 -06:00
James Seibel 478e431076 up version number 2.4.1 -> 2.4.2-dev 2025-12-14 17:00:34 -06:00
James Seibel 6feb7f1b42 remove dev from version number 2025-12-14 13:46:04 -06:00
James Seibel 016fc66293 Print a warning if G1GC is used
G1GC is known to cause stuttering
2025-12-13 16:46:59 -06:00
James Seibel 6d3e30d425 add Zstd decompress lib check in initalizer 2025-12-13 15:48:05 -06:00
James Seibel 5be5c5a5bc replace client ticks with a timer
Prevents DH loading issues when MC ticks are paused
2025-12-13 11:19:33 -06:00
James Seibel ed5aeb8951 minor texture setup reformatting 2025-12-13 10:43:01 -06:00
James Seibel 7f0ddadf26 up version number 2.4.0 -> 2.4.1-dev 2025-12-13 10:20:44 -06:00
James Seibel a2c61ed278 up version number 2.3.7 -> 2.4.0 2025-12-13 10:19:50 -06:00
James Seibel 99eb4ac8a1 Fix infinite loop in DhSectionPos 2025-12-13 09:10:12 -06:00
James Seibel c75902d9d6 debug particle cleanup 2025-12-13 08:50:15 -06:00
James Seibel 1743949ba5 fix GeneratedFullDataSourceProvider not adding update listener 2025-12-13 08:49:45 -06:00
James Seibel a74a37a0e8 world gen queue refactoring 2025-12-13 08:49:31 -06:00
James Seibel 4ed7941288 fix missing localization 2025-12-12 07:45:12 -06:00
James Seibel ec59a5f754 comment cleanup and enum renaming for API use 2025-12-11 07:35:37 -06:00
James Seibel 895e04b7cc Remove unused wrapper functions and refactor 2025-12-10 18:50:35 -06:00
James Seibel 8f0930fa02 Allow world gen limits on singleplayer 2025-12-10 07:09:29 -06:00
James Seibel c1c4328fa5 rename API getSoftCache -> createSoftCahe 2025-12-09 20:57:27 -06:00
James Seibel 91240e4f7a disable mip-mapping on textures
necessary to fix MC 1.21.11 rendering
2025-12-09 20:57:09 -06:00
James Seibel 17c61a97cc revert long windows filepath char 2025-12-09 07:21:40 -06:00
James Seibel b78b852ffb Merge branch 'batchGenRefactor' 2025-12-09 07:16:18 -06:00
James Seibel 26d4220967 Add logging/messaging for corrupted DB files 2025-12-09 07:12:33 -06:00
James Seibel 5edc73cc03 enable long file paths for the config file 2025-12-06 12:28:22 -06:00
James Seibel 6fcfc9379e Fix repo unit tests 2025-12-06 12:27:53 -06:00
James Seibel 149fbccfa5 Merge branch 'batchGenRefactor' 2025-12-06 12:19:17 -06:00
James Seibel 5ca754d2ac Fix world gen progress config resetting on reboot 2025-12-06 09:18:34 -06:00
James Seibel f13744e858 Add thread pool priority setting
Setting this to 1 higher than C2ME can reduce issues with Chunky overwhelming DH.
2025-12-05 07:35:16 -06:00
James Seibel 64ac218003 Improve empty LOD debugging slightly 2025-12-05 07:28:57 -06:00
James Seibel 385bd326cf minor world gen related refactoring 2025-12-04 07:39:09 -06:00
James Seibel 4e9559f230 enable long file paths on windows for the DB 2025-12-02 07:07:17 -06:00
James Seibel 6ea864ef6b TEST 2025-11-29 09:59:33 -06:00
James Seibel 4e96728c25 maybe fix concurrency error during world gen shutdown 2025-11-28 16:29:47 -06:00
James Seibel 1c44ef7f0c minor reformatting 2025-11-28 16:23:36 -06:00
James Seibel 227d0d09ba fix getDataPointAtBlockPos() relative Y 2025-11-28 15:53:47 -06:00
James Seibel d7ba3fa724 fix LOD only mode when transparency is disabled 2025-11-28 15:53:38 -06:00
James Seibel 7e46adf469 add the ability to ignore update chunk pos 2025-11-28 10:48:42 -06:00
James Seibel f43e2fa441 don't render thick snow layers 2025-11-28 09:39:03 -06:00
James Seibel f9819d3d46 fix vanilla fading for MC versions before 1.21.5 2025-11-28 08:42:20 -06:00
James Seibel 19b23bea5f add slow world gen warning config 2025-11-27 09:59:16 -06:00
James Seibel d1c0f7ebb4 Update .editorconfig 2025-11-26 13:55:33 -06:00
James Seibel 5a4ddafbbb Z_std_stream localization 2025-11-26 13:52:17 -06:00
James Seibel 7c40d96f2e DhApiTerrainDataPoint to string 2025-11-26 13:52:07 -06:00
James Seibel b535be16c0 auto merge API world gen data
done to reduce memory use with broken API world generators
2025-11-26 13:51:58 -06:00
James Seibel 22f5608f9a hide the compressor config option 2025-11-24 14:31:42 -06:00
James Seibel a498422843 stream cleanup 3 2025-11-24 14:30:17 -06:00
James Seibel bfd6efb4a4 handle ZStd streams 2025-11-24 14:28:06 -06:00
James Seibel c8c9df3a34 data stream cleanup 2025-11-24 14:15:23 -06:00
James Seibel 3349e5b898 clean up DhDataInputStream 2025-11-24 13:51:48 -06:00
James Seibel ed7511ff6a proof-of-concept block Zstd compression 2025-11-24 12:40:49 -06:00
James Seibel 8516e8f9ab re-enable varint unit tests 2025-11-24 12:38:34 -06:00
James Seibel 47a4d1535f minor variable refactoring 2025-11-22 11:01:53 -06:00
James Seibel 33a55dc7cd Delete EventTimer.java 2025-11-22 09:30:00 -06:00
James Seibel 1b4f9e8942 minor throw/this cleanup 2025-11-22 09:24:31 -06:00
James Seibel 2537c4a259 Rename IBatchGeneratorEvnWrapper 2025-11-22 08:16:30 -06:00
James Seibel b74b6e8068 minor RollingAverage refactor 2025-11-22 08:16:11 -06:00
James Seibel 25979d6a76 Move some exception logic into ExceptionUtil 2025-11-21 06:59:03 -06:00
James Seibel 3f287388d5 re-add biome blending to API config options 2025-11-18 07:42:43 -06:00
James Seibel 72d2ba6aae comment out phantom buffer cleanup log 2025-11-18 07:32:58 -06:00
James Seibel 611ed4e24a add mod note in memory low message 2025-11-18 07:32:48 -06:00
James Seibel eac7a38e73 hopefully reduce the chance of downsampling holes 2025-11-18 07:32:18 -06:00
James Seibel afd7da7763 Optimize full data update processing 2025-11-18 07:16:50 -06:00
James Seibel ff7abb6a18 Fix rendering when Iris isn't installed 2025-11-16 16:11:40 -06:00
James Seibel ca3f5da5de Add unit test for data source merging speed 2025-11-16 15:30:16 -06:00
James Seibel 69012ab7e6 rename and cleanup data source update methods 2025-11-16 15:29:13 -06:00
James Seibel e5e502b4f8 Remove unused/broken FullData LevelMinY 2025-11-15 19:09:16 -06:00
James Seibel 42dc0903de Fix shaders when far clip fading is active 2025-11-15 18:20:47 -06:00
James Seibel 4b20637e47 Fix WorldGen after restarting generation 2025-11-15 12:07:53 -06:00
James Seibel 3257ae8480 replace server tick/world gen tick with a timer 2025-11-15 09:47:15 -06:00
James Seibel a6ddc561a0 up protocol version 12 -> 13 2025-11-15 09:42:38 -06:00
James Seibel 7c82c9eb7b add adj data to DTO en/decoding 2025-11-15 09:42:20 -06:00
James Seibel 3c62e18502 Fix gitlab getter Long/Int cast 2025-11-15 07:55:56 -06:00
James Seibel eea5198fb6 Merge branch 'adjData' 2025-11-14 07:46:37 -06:00
James Seibel 6bfcf36687 Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2025-11-13 07:19:19 -06:00
s809 91dffa3c3e Prevent auto-pause while pregen is running 2025-11-11 23:48:13 +05:00
James Seibel e0c143881f Fix compression mode javadoc 2025-11-01 08:34:02 -04:00
172 changed files with 4088 additions and 3480 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ insert_final_newline = false
max_line_length = 1000 max_line_length = 1000
tab_width = 4 tab_width = 4
trim_trailing_whitespace = false trim_trailing_whitespace = false
ij_continuation_indent_size = 8 ij_continuation_indent_size = 4
ij_formatter_off_tag = @formatter:off ij_formatter_off_tag = @formatter:off
ij_formatter_on_tag = @formatter:on ij_formatter_on_tag = @formatter:on
ij_formatter_tags_enabled = true ij_formatter_tags_enabled = true
@@ -39,10 +39,6 @@ package com.seibel.distanthorizons.api.enums;
*/ */
public enum EDhApiDetailLevel public enum EDhApiDetailLevel
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** /**
* detail level: 0 <Br> * detail level: 0 <Br>
* width in Blocks: 1 * width in Blocks: 1
@@ -28,10 +28,6 @@ package com.seibel.distanthorizons.api.enums.config;
*/ */
public enum EDhApiBlocksToAvoid public enum EDhApiBlocksToAvoid
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
NONE(false), NONE(false),
NON_COLLIDING(true); NON_COLLIDING(true);
@@ -23,6 +23,7 @@ package com.seibel.distanthorizons.api.enums.config;
* UNCOMPRESSED <br> * UNCOMPRESSED <br>
* LZ4 <br> * LZ4 <br>
* Z_STD <br> * Z_STD <br>
* Z_STD_STREAM <br>
* LZMA2 <br><br> * LZMA2 <br><br>
* *
* Note: speed and compression ratios are examples * Note: speed and compression ratios are examples
@@ -33,10 +34,6 @@ package com.seibel.distanthorizons.api.enums.config;
*/ */
public enum EDhApiDataCompressionMode 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. <br><br> * Should only be used internally and for unit testing. <br><br>
* *
@@ -57,17 +54,31 @@ public enum EDhApiDataCompressionMode
LZ4(1), LZ4(1),
/** /**
* Decent speed and good compression. <br><br> * Great speed and good compression. <br><br>
*
* Read Speed: 2.1 MS / DTO <br>
* Write Speed: 4.9 MS / DTO <br>
* Compression ratio: 0.2606 <br>
*/
Z_STD_BLOCK(4),
/**
* Similar to {@link EDhApiDataCompressionMode#Z_STD_BLOCK}
* except slower. <br><br>
*
* This option is only provided for legacy support when processing old databases. <br><br>
* *
* Read Speed: 9.31 MS / DTO <br> * Read Speed: 9.31 MS / DTO <br>
* Write Speed: 15.13 MS / DTO <br> * Write Speed: 15.13 MS / DTO <br>
* Compression ratio: 0.2606 <br> * Compression ratio: 0.2606 <br>
*/ */
Z_STD(2), @Deprecated
@DisallowSelectingViaConfigGui
Z_STD_STREAM(2),
/** /**
* Extremely slow, but very good compression. <br><br> * Extremely slow, but very good compression. <br>
* Often causes whole computer stuttering due to memory bandwidth saturation. <br><br>
* *
* Read Speed: 13.29 MS / DTO <br> * Read Speed: 13.29 MS / DTO <br>
* Write Speed: 70.95 MS / DTO <br> * Write Speed: 70.95 MS / DTO <br>
@@ -44,9 +44,9 @@ public enum EDhApiGpuUploadMethod
/** Fast rendering but may stutter when uploading. */ /** Fast rendering but may stutter when uploading. */
SUB_DATA(false, false), SUB_DATA(false, false),
/** Don't upload, only should be used for debugging */ ///** Don't upload, only should be used for debugging */
@Deprecated // TODO remove before release //@Deprecated
NONE(false, false), //NONE(false, false),
/** /**
* May end up storing buffers in System memory. <br> * May end up storing buffers in System memory. <br>
@@ -29,10 +29,6 @@ package com.seibel.distanthorizons.api.enums.config;
*/ */
public enum EDhApiGrassSideRendering public enum EDhApiGrassSideRendering
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
AS_GRASS, AS_GRASS,
FADE_TO_DIRT, FADE_TO_DIRT,
AS_DIRT; AS_DIRT;
@@ -31,11 +31,6 @@ package com.seibel.distanthorizons.api.enums.config;
*/ */
public enum EDhApiHorizontalQuality public enum EDhApiHorizontalQuality
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
// Note: any quadraticBase less than 2.0f has issues with DetailDistanceUtil, and will always return the lowest detail level. // Note: any quadraticBase less than 2.0f has issues with DetailDistanceUtil, and will always return the lowest detail level.
// So for now we are limiting the lowest value to 2.0 // So for now we are limiting the lowest value to 2.0
// LOWEST was originally 1.0f and LOW was 1.5f // LOWEST was originally 1.0f and LOW was 1.5f
@@ -29,10 +29,6 @@ package com.seibel.distanthorizons.api.enums.config;
*/ */
public enum EDhApiLodShading 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. <Br> * Uses Minecraft's shading for LODs. <Br>
* This means if Minecraft's shading is disabled DH's shading will be as well. * This means if Minecraft's shading is disabled DH's shading will be as well.
@@ -33,10 +33,6 @@ package com.seibel.distanthorizons.api.enums.config;
*/ */
public enum EDhApiMcRenderingFadeMode public enum EDhApiMcRenderingFadeMode
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** /**
* No fading is done, there will be a pronounced border between * No fading is done, there will be a pronounced border between
* Minecraft and Distant Horizons. <br> * Minecraft and Distant Horizons. <br>
@@ -35,11 +35,6 @@ package com.seibel.distanthorizons.api.enums.config;
*/ */
public enum EDhApiServerFolderNameMode public enum EDhApiServerFolderNameMode
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** Only use the server name */ /** Only use the server name */
NAME_ONLY, NAME_ONLY,
@@ -34,11 +34,6 @@ package com.seibel.distanthorizons.api.enums.config;
@Deprecated // not currently in use, if the config this enum represents is re-implemented, the deprecated flag can be removed @Deprecated // not currently in use, if the config this enum represents is re-implemented, the deprecated flag can be removed
public enum EDhApiVanillaOverdraw public enum EDhApiVanillaOverdraw
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** /**
* Don't draw LODs where a minecraft chunk could be. * Don't draw LODs where a minecraft chunk could be.
* Use Overdraw Offset to tweak the border thickness. * Use Overdraw Offset to tweak the border thickness.
@@ -28,10 +28,6 @@ package com.seibel.distanthorizons.api.enums.config;
*/ */
public enum EDhApiWorldCompressionMode 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. <br> * Every block/biome change is recorded in the database. <br>
* This is what DH 2.0 and 2.0.1 all used by default and will store a lot of data. * This is what DH 2.0 and 2.0.1 all used by default and will store a lot of data.
@@ -35,10 +35,6 @@ import com.seibel.distanthorizons.api.enums.config.DisallowSelectingViaConfigGui
*/ */
public enum EDhApiQualityPreset public enum EDhApiQualityPreset
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
@DisallowSelectingViaConfigGui @DisallowSelectingViaConfigGui
CUSTOM, CUSTOM,
@@ -34,10 +34,6 @@ import com.seibel.distanthorizons.api.enums.config.DisallowSelectingViaConfigGui
*/ */
public enum EDhApiThreadPreset public enum EDhApiThreadPreset
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
@DisallowSelectingViaConfigGui @DisallowSelectingViaConfigGui
CUSTOM, CUSTOM,
@@ -33,11 +33,6 @@ package com.seibel.distanthorizons.api.enums.rendering;
*/ */
public enum EDhApiDebugRendering public enum EDhApiDebugRendering
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** LODs are rendered normally */ /** LODs are rendered normally */
OFF, OFF,
@@ -33,10 +33,6 @@ package com.seibel.distanthorizons.api.enums.rendering;
@Deprecated @Deprecated
public enum EDhApiFogDrawMode public enum EDhApiFogDrawMode
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** /**
* Use whatever Fog setting Optifine is using. * Use whatever Fog setting Optifine is using.
* If Optifine isn't installed this defaults to {@link EDhApiFogDrawMode#FOG_ENABLED}. * If Optifine isn't installed this defaults to {@link EDhApiFogDrawMode#FOG_ENABLED}.
@@ -30,11 +30,6 @@ package com.seibel.distanthorizons.api.enums.rendering;
*/ */
public enum EDhApiFogFalloff public enum EDhApiFogFalloff
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
LINEAR(0), LINEAR(0),
EXPONENTIAL(1), EXPONENTIAL(1),
EXPONENTIAL_SQUARED(2); EXPONENTIAL_SQUARED(2);
@@ -33,11 +33,6 @@ package com.seibel.distanthorizons.api.enums.rendering;
*/ */
public enum EDhApiHeightFogDirection public enum EDhApiHeightFogDirection
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
ABOVE_CAMERA (true, true, false), ABOVE_CAMERA (true, true, false),
BELOW_CAMERA (true, false, true), BELOW_CAMERA (true, false, true),
ABOVE_AND_BELOW_CAMERA (true, true, true), ABOVE_AND_BELOW_CAMERA (true, true, true),
@@ -29,11 +29,6 @@ package com.seibel.distanthorizons.api.enums.rendering;
*/ */
public enum EDhApiRendererMode public enum EDhApiRendererMode
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
DEFAULT, DEFAULT,
DEBUG, DEBUG,
DISABLED; DISABLED;
@@ -29,11 +29,6 @@ package com.seibel.distanthorizons.api.enums.rendering;
*/ */
public enum EDhApiTransparency public enum EDhApiTransparency
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
DISABLED(false, false), DISABLED(false, false),
FAKE(true, true), FAKE(true, true),
COMPLETE(true, false); COMPLETE(true, false);
@@ -34,11 +34,6 @@ package com.seibel.distanthorizons.api.enums.worldGeneration;
*/ */
public enum EDhApiDistantGeneratorMode public enum EDhApiDistantGeneratorMode
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** Don't generate any new terrain, just generate LODs for already generated chunks. */ /** Don't generate any new terrain, just generate LODs for already generated chunks. */
PRE_EXISTING_ONLY((byte) 1), PRE_EXISTING_ONLY((byte) 1),
@@ -31,11 +31,6 @@ package com.seibel.distanthorizons.api.enums.worldGeneration;
*/ */
public enum EDhApiDistantGeneratorProgressDisplayLocation public enum EDhApiDistantGeneratorProgressDisplayLocation
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
OVERLAY, OVERLAY,
CHAT, CHAT,
LOG, LOG,
@@ -104,7 +104,7 @@ public interface IDhApiGraphicsConfig extends IDhApiConfigGroup
* 2 = blending of 5x5 <br> * 2 = blending of 5x5 <br>
* ... <br> * ... <br>
*/ */
// IDhApiConfigValue<Integer> getBiomeBlending(); IDhApiConfigValue<Integer> getBiomeBlending();
@@ -161,9 +161,16 @@ public interface IDhApiTerrainDataRepo
//=========// //=========//
/** /**
* Creates a new cache you manage that can be used to speed up repeat
* read operations. <br>
* Without a cache each operation must: hit the backing database file,
* decompress it, and parse it; which is a fairly slow process. <br>
*
* @return a {@link IDhApiTerrainDataCache} backed by {@link java.lang.ref.SoftReference}'s. * @return a {@link IDhApiTerrainDataCache} backed by {@link java.lang.ref.SoftReference}'s.
* @since API 3.0.0 * @since API 5.0.0
*/ */
IDhApiTerrainDataCache getSoftCache(); IDhApiTerrainDataCache createSoftCache();
} }
@@ -41,6 +41,7 @@ public interface IDhApiEventInjector extends IDependencyInjector<IDhApiEvent>
* @throws IllegalArgumentException if the implementation object doesn't implement the interface * @throws IllegalArgumentException if the implementation object doesn't implement the interface
*/ */
// Note to self: Don't try adding a generic type to IDhApiEvent, the constructor won't accept it // Note to self: Don't try adding a generic type to IDhApiEvent, the constructor won't accept it
// TODO why are we removing the class instead of an instance?
boolean unbind(Class<? extends IDhApiEvent> dependencyInterface, Class<? extends IDhApiEvent> dependencyClassToRemove) throws IllegalArgumentException; boolean unbind(Class<? extends IDhApiEvent> dependencyInterface, Class<? extends IDhApiEvent> dependencyClassToRemove) throws IllegalArgumentException;
@@ -169,7 +169,7 @@ public interface IDhApiWorldGenerator extends Closeable, IDhApiOverrideable
* *
* After the {@link IDhApiWorldGenerator} has been generated, it should be passed into the * After the {@link IDhApiWorldGenerator} has been generated, it should be passed into the
* resultConsumer's {@link Consumer#accept(Object)} method. * resultConsumer's {@link Consumer#accept(Object)} method.
* Note: if air blocks aren't included in the with the {@link DhApiChunk} with proper lighting, lower detail levels will appear as black/unlit. * Note: if air blocks aren't included in the {@link IDhApiFullDataSource} with proper lighting, lower detail levels will appear as black/unlit.
* *
* @implNote the default implementation of this method throws an {@link UnsupportedOperationException}, * @implNote the default implementation of this method throws an {@link UnsupportedOperationException},
* and must be overridden when {@link #getReturnType()} returns {@link EDhApiWorldGeneratorReturnType#API_CHUNKS}. * and must be overridden when {@link #getReturnType()} returns {@link EDhApiWorldGeneratorReturnType#API_CHUNKS}.
@@ -20,6 +20,8 @@
package com.seibel.distanthorizons.api.methods.events.sharedParameterObjects; package com.seibel.distanthorizons.api.methods.events.sharedParameterObjects;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass;
import com.seibel.distanthorizons.api.interfaces.world.IDhApiLevelWrapper;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiWorldLoadEvent;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam; import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam;
import com.seibel.distanthorizons.api.objects.math.DhApiMat4f; import com.seibel.distanthorizons.api.objects.math.DhApiMat4f;
@@ -27,7 +29,7 @@ import com.seibel.distanthorizons.api.objects.math.DhApiMat4f;
* Contains information relevant to Distant Horizons and Minecraft rendering. * Contains information relevant to Distant Horizons and Minecraft rendering.
* *
* @author James Seibel * @author James Seibel
* @version 2024-1-31 * @version 2025-12-23
* @since API 1.0.0 * @since API 1.0.0
*/ */
public class DhApiRenderParam implements IDhApiEventParam public class DhApiRenderParam implements IDhApiEventParam
@@ -61,6 +63,13 @@ public class DhApiRenderParam implements IDhApiEventParam
public final int worldYOffset; public final int worldYOffset;
/**
* The level currently being rendered.
*
* @since API 5.1.0
*/
public final IDhApiLevelWrapper clientLevelWrapper;
//==============// //==============//
@@ -75,7 +84,8 @@ public class DhApiRenderParam implements IDhApiEventParam
parent.nearClipPlane, parent.farClipPlane, parent.nearClipPlane, parent.farClipPlane,
parent.mcProjectionMatrix.copy(), parent.mcModelViewMatrix.copy(), parent.mcProjectionMatrix.copy(), parent.mcModelViewMatrix.copy(),
parent.dhProjectionMatrix.copy(), parent.dhModelViewMatrix.copy(), parent.dhProjectionMatrix.copy(), parent.dhModelViewMatrix.copy(),
parent.worldYOffset parent.worldYOffset,
parent.clientLevelWrapper
); );
} }
public DhApiRenderParam( public DhApiRenderParam(
@@ -84,7 +94,8 @@ public class DhApiRenderParam implements IDhApiEventParam
float nearClipPlane, float farClipPlane, float nearClipPlane, float farClipPlane,
DhApiMat4f newMcProjectionMatrix, DhApiMat4f newMcModelViewMatrix, DhApiMat4f newMcProjectionMatrix, DhApiMat4f newMcModelViewMatrix,
DhApiMat4f newDhProjectionMatrix, DhApiMat4f newDhModelViewMatrix, DhApiMat4f newDhProjectionMatrix, DhApiMat4f newDhModelViewMatrix,
int worldYOffset int worldYOffset,
IDhApiLevelWrapper clientLevelWrapper
) )
{ {
this.renderPass = renderPass; this.renderPass = renderPass;
@@ -101,6 +112,7 @@ public class DhApiRenderParam implements IDhApiEventParam
this.dhModelViewMatrix = newDhModelViewMatrix; this.dhModelViewMatrix = newDhModelViewMatrix;
this.worldYOffset = worldYOffset; this.worldYOffset = worldYOffset;
this.clientLevelWrapper = clientLevelWrapper;
} }
@@ -31,7 +31,7 @@ import java.util.List;
* Contains a list of {@link DhApiTerrainDataPoint} representing the blocks in a Minecraft chunk. * Contains a list of {@link DhApiTerrainDataPoint} representing the blocks in a Minecraft chunk.
* *
* @author Builderb0y, James Seibel * @author Builderb0y, James Seibel
* @version 2024-7-21 * @version 2025-12-11
* @since API 2.0.0 * @since API 2.0.0
* *
* @see IDhApiWrapperFactory * @see IDhApiWrapperFactory
@@ -54,27 +54,12 @@ public class DhApiChunk
// constructors // // constructors //
//==============// //==============//
/** /** @since API 3.0.0 */
* Deprecated due to the topYBlockPos and bottomYBlockPos variables being put in the wrong order.
* They should have been in bottom -> top order.
*
* @see DhApiChunk#create(int, int, int, int)
*/
@Deprecated
public DhApiChunk(int chunkPosX, int chunkPosZ, int topYBlockPos, int bottomYBlockPos)
{ this(chunkPosX, chunkPosZ, bottomYBlockPos, topYBlockPos, false); }
/**
* @since API 3.0.0
*/
public static DhApiChunk create(int chunkPosX, int chunkPosZ, int bottomYBlockPos, int topYBlockPos) public static DhApiChunk create(int chunkPosX, int chunkPosZ, int bottomYBlockPos, int topYBlockPos)
{ return new DhApiChunk(chunkPosX, chunkPosZ, bottomYBlockPos, topYBlockPos, false); } { return new DhApiChunk(chunkPosX, chunkPosZ, bottomYBlockPos, topYBlockPos); }
/** /** Only visible to internal DH methods */
* Only visible to internal DH methods private DhApiChunk(int chunkPosX, int chunkPosZ, int bottomYBlockPos, int topYBlockPos)
* @param ignoredParameter is only present to differentiate the two constructors and isn't actually used
*/
private DhApiChunk(int chunkPosX, int chunkPosZ, int bottomYBlockPos, int topYBlockPos, boolean ignoredParameter)
{ {
this.chunkPosX = chunkPosX; this.chunkPosX = chunkPosX;
this.chunkPosZ = chunkPosZ; this.chunkPosZ = chunkPosZ;
@@ -29,7 +29,7 @@ import java.util.ArrayList;
* Holds a single datapoint of terrain data. * Holds a single datapoint of terrain data.
* *
* @author James Seibel * @author James Seibel
* @version 2024-7-20 * @version 2025-11-15
* @since API 1.0.0 * @since API 1.0.0
*/ */
public class DhApiTerrainDataPoint public class DhApiTerrainDataPoint
@@ -47,6 +47,10 @@ public class DhApiTerrainDataPoint
public final int blockLightLevel; public final int blockLightLevel;
public final int skyLightLevel; public final int skyLightLevel;
/**
* An unsigned block position of the bottom vertex for this LOD relative to the level's minimum height.
* Should be greater than or equal to 0.
*/
public final int bottomYBlockPos; public final int bottomYBlockPos;
public final int topYBlockPos; public final int topYBlockPos;
@@ -59,28 +63,7 @@ public class DhApiTerrainDataPoint
// constructors // // constructors //
//==============// //==============//
/** /** @since API 3.0.0 */
* Deprecated due to the topYBlockPos and bottomYBlockPos variables being put in the wrong order.
* They should have been in bottom -> top order.
*
* @see DhApiTerrainDataPoint#create(byte, int, int, int, int, IDhApiBlockStateWrapper, IDhApiBiomeWrapper)
*/
@Deprecated
public DhApiTerrainDataPoint(
byte detailLevel,
int blockLightLevel, int skyLightLevel,
int topYBlockPos, int bottomYBlockPos,
IDhApiBlockStateWrapper blockStateWrapper, IDhApiBiomeWrapper biomeWrapper)
{
this(detailLevel, blockLightLevel, skyLightLevel,
bottomYBlockPos, topYBlockPos,
blockStateWrapper, biomeWrapper,
false);
}
/**
* @since API 3.0.0
*/
public static DhApiTerrainDataPoint create( public static DhApiTerrainDataPoint create(
byte detailLevel, byte detailLevel,
int blockLightLevel, int skyLightLevel, int blockLightLevel, int skyLightLevel,
@@ -91,20 +74,15 @@ public class DhApiTerrainDataPoint
return new DhApiTerrainDataPoint( return new DhApiTerrainDataPoint(
detailLevel, blockLightLevel, skyLightLevel, detailLevel, blockLightLevel, skyLightLevel,
bottomYBlockPos, topYBlockPos, bottomYBlockPos, topYBlockPos,
blockStateWrapper, biomeWrapper, blockStateWrapper, biomeWrapper);
false);
} }
/** /** Only visible to internal DH methods */
* Only visible to internal DH methods
* @param ignoredParameter is only present to differentiate the two constructors and isn't actually used
*/
private DhApiTerrainDataPoint( private DhApiTerrainDataPoint(
byte detailLevel, byte detailLevel,
int blockLightLevel, int skyLightLevel, int blockLightLevel, int skyLightLevel,
int bottomYBlockPos, int topYBlockPos, int bottomYBlockPos, int topYBlockPos,
IDhApiBlockStateWrapper blockStateWrapper, IDhApiBiomeWrapper biomeWrapper, IDhApiBlockStateWrapper blockStateWrapper, IDhApiBiomeWrapper biomeWrapper
boolean ignoredParameter
) )
{ {
this.detailLevel = detailLevel; this.detailLevel = detailLevel;
@@ -118,4 +96,24 @@ public class DhApiTerrainDataPoint
this.biomeWrapper = biomeWrapper; this.biomeWrapper = biomeWrapper;
} }
//================//
// base overrides //
//================//
@Override
public String toString()
{
return "[Block:" + this.blockStateWrapper.getSerialString() +
",Biome:" + this.biomeWrapper.getName() +
",TopY:" + this.topYBlockPos +
",BottomY:" + this.bottomYBlockPos +
",BlockLight:" + this.blockLightLevel +
",SkyLight:" + this.skyLightLevel +
"]";
}
} }
@@ -31,21 +31,21 @@ public final class ModInfo
public static final String DEDICATED_SERVER_INITIAL_PATH = "dedicated_server_initial"; public static final String DEDICATED_SERVER_INITIAL_PATH = "dedicated_server_initial";
/** Incremented every time any packets are added, changed or removed, with a few exceptions. */ /** Incremented every time any packets are added, changed or removed, with a few exceptions. */
public static final int PROTOCOL_VERSION = 12; public static final int PROTOCOL_VERSION = 13;
public static final String WRAPPER_PACKET_PATH = "message"; public static final String WRAPPER_PACKET_PATH = "message";
/** The internal mod name */ /** The internal mod name */
public static final String NAME = "DistantHorizons"; public static final String NAME = "DistantHorizons";
/** Human-readable version of NAME */ /** Human-readable version of NAME */
public static final String READABLE_NAME = "Distant Horizons"; public static final String READABLE_NAME = "Distant Horizons";
public static final String VERSION = "2.3.7-b-dev"; public static final String VERSION = "2.4.4-b";
/** Returns true if the current build is an unstable developer build, false otherwise. */ /** Returns true if the current build is an unstable developer build, false otherwise. */
public static final boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev"); public static final boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev");
/** This version should only be updated when breaking changes are introduced to the DH API */ /** This version should only be updated when breaking changes are introduced to the DH API */
public static final int API_MAJOR_VERSION = 5; public static final int API_MAJOR_VERSION = 5;
/** This version should be updated whenever new methods are added to the DH API */ /** This version should be updated whenever new methods are added to the DH API */
public static final int API_MINOR_VERSION = 0; public static final int API_MINOR_VERSION = 1;
/** This version should be updated whenever non-breaking fixes are added to the DH API */ /** This version should be updated whenever non-breaking fixes are added to the DH API */
public static final int API_PATCH_VERSION = 0; public static final int API_PATCH_VERSION = 0;
@@ -20,36 +20,43 @@
package com.seibel.distanthorizons.core; package com.seibel.distanthorizons.core;
import com.github.luben.zstd.ZstdOutputStream; import com.github.luben.zstd.ZstdOutputStream;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiBeforeRenderEvent;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.eventHandlers.IgnoredDimensionCsvHandler;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericRenderObjectFactory; import com.seibel.distanthorizons.core.render.renderer.generic.GenericRenderObjectFactory;
import com.seibel.distanthorizons.core.sql.DatabaseUpdater; import com.seibel.distanthorizons.core.sql.DatabaseUpdater;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.core.world.DhApiWorldProxy; import com.seibel.distanthorizons.core.world.DhApiWorldProxy;
import com.seibel.distanthorizons.core.api.external.methods.config.DhApiConfig; import com.seibel.distanthorizons.core.api.external.methods.config.DhApiConfig;
import com.seibel.distanthorizons.core.api.external.methods.data.DhApiTerrainDataRepo; import com.seibel.distanthorizons.core.api.external.methods.data.DhApiTerrainDataRepo;
import com.seibel.distanthorizons.api.DhApi; import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.core.render.DhApiRenderProxy; import com.seibel.distanthorizons.core.render.DhApiRenderProxy;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.coreapi.util.StringUtil;
import net.jpountz.lz4.LZ4FrameOutputStream; import net.jpountz.lz4.LZ4FrameOutputStream;
import org.apache.logging.log4j.LogManager;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.sqlite.SQLiteJDBCLoader; import org.sqlite.SQLiteJDBCLoader;
import org.sqlite.util.OSInfo;
import org.tukaani.xz.XZOutputStream; import org.tukaani.xz.XZOutputStream;
import java.awt.*; import java.lang.management.GarbageCollectorMXBean;
import java.io.File; import java.lang.management.ManagementFactory;
import java.util.List;
/** Handles first time Core setup. */ /** Handles first time Core setup. */
public class Initializer public class Initializer
{ {
private static final DhLogger LOGGER = new DhLoggerBuilder().build(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
public static void init() public static void init()
{ {
LOGGER.info("Running library validation...");
// confirm that all referenced libraries are available to use // confirm that all referenced libraries are available to use
try try
{ {
@@ -57,6 +64,17 @@ public class Initializer
// will throw an error (not an exception) // will throw an error (not an exception)
Class<?> lz4Compressor = LZ4FrameOutputStream.class; Class<?> lz4Compressor = LZ4FrameOutputStream.class;
Class<?> zstdCompressor = ZstdOutputStream.class; Class<?> zstdCompressor = ZstdOutputStream.class;
{
byte[] testCompressByteArray = new byte[1024];
for (int i = 0; i < testCompressByteArray.length; i++)
{
testCompressByteArray[i] = (byte) (i % 126);
}
byte[] compressedBytes = com.github.luben.zstd.Zstd.compress(testCompressByteArray);
com.github.luben.zstd.Zstd.decompress(compressedBytes);
}
Class<?> lzmaCompressor = XZOutputStream.class; Class<?> lzmaCompressor = XZOutputStream.class;
//Class<?> networking = ByteBuf.class; //Class<?> networking = ByteBuf.class;
Class<?> config = com.electronwill.nightconfig.core.Config.class; Class<?> config = com.electronwill.nightconfig.core.Config.class;
@@ -73,9 +91,7 @@ public class Initializer
} }
catch (Throwable e) catch (Throwable e)
{ {
LOGGER.fatal("Critical programmer error: One or more libraries aren't present. Error: [" + e.getMessage() + "].", e); MC_CLIENT.crashMinecraft("Distant Horizons critical setup error: One or more libraries are either in-accessible, corrupted, or overwritten by another mod. Error: [" + e.getMessage() + "].", e);
// throwing here should crash the game, notifying the developer that something is wrong
throw new RuntimeException(e);
} }
// confirm the resource directory is present // confirm the resource directory is present
@@ -89,8 +105,7 @@ public class Initializer
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.fatal("Critical programmer error: Can't read SQL Scripts resource folder is either missing or malformed. Error: [" + e.getMessage() + "]."); MC_CLIENT.crashMinecraft("Critical programmer error: Can't read SQL Scripts resource folder is either missing or malformed. Error: [" + e.getMessage() + "].", e);
throw new RuntimeException(e);
} }
// This code has been disabled since it can cause Mac // This code has been disabled since it can cause Mac
@@ -121,6 +136,45 @@ public class Initializer
LOGGER.error("Programmer Error: No ["+IWrapperFactory.class.getSimpleName()+"] assigned to the DhApi."); LOGGER.error("Programmer Error: No ["+IWrapperFactory.class.getSimpleName()+"] assigned to the DhApi.");
} }
// log a warning if G1GC is being used
// (this garbage collector is known to cause stuttering)
{
boolean g1GcInUse = false;
StringBuilder garbageCollectorNames = new StringBuilder();
List<GarbageCollectorMXBean> gcMxBeans = ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean gcMxBean : gcMxBeans)
{
if (!garbageCollectorNames.toString().isEmpty())
{
garbageCollectorNames.append(", ");
}
garbageCollectorNames.append(gcMxBean.getName());
// "G1 Young Generation" // "G1 Concurrent GC" // "G1 Old Generation"
if (gcMxBean.getName().toLowerCase().contains("g1 "))
{
g1GcInUse = true;
}
}
LOGGER.info("Garbage collectors: ["+garbageCollectorNames+"]");
if (g1GcInUse
&& Config.Common.Logging.Warning.logGarbageCollectorWarning.get())
{
LOGGER.warn(
"Distant Horizons: G1 Garbage collector detected. \n" +
"This garbage collector can cause FPS stuttering. \n" +
"It's recommended to use a concurrent garbage collector \n" +
"like ZGC (Java 21+) or Shenandoah (Java 8 through 17) for a smoother experience. \n" +
"");
}
}
DhApi.events.bind(DhApiBeforeRenderEvent.class, IgnoredDimensionCsvHandler.INSTANCE);
} }
} }
@@ -28,6 +28,7 @@ import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiHeightFogCo
import com.seibel.distanthorizons.core.config.api.DhApiConfigValue; import com.seibel.distanthorizons.core.config.api.DhApiConfigValue;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.api.converters.ApiFogDrawModeConverter; import com.seibel.distanthorizons.core.config.api.converters.ApiFogDrawModeConverter;
import com.seibel.distanthorizons.core.config.api.converters.InvertedBoolConverter;
public class DhApiFogConfig implements IDhApiFogConfig public class DhApiFogConfig implements IDhApiFogConfig
{ {
@@ -67,7 +68,7 @@ public class DhApiFogConfig implements IDhApiFogConfig
@Override @Override
@Deprecated @Deprecated
public IDhApiConfigValue<Boolean> disableVanillaFog() public IDhApiConfigValue<Boolean> disableVanillaFog()
{ return new DhApiConfigValue<>(Config.Client.Advanced.Graphics.Fog.disableVanillaFog); } { return new DhApiConfigValue<>(Config.Client.Advanced.Graphics.Fog.enableVanillaFog, new InvertedBoolConverter()); }
@Override @Override
public IDhApiConfigValue<Boolean> enableVanillaFog() public IDhApiConfigValue<Boolean> enableVanillaFog()
{ return new DhApiConfigValue<>(Config.Client.Advanced.Graphics.Fog.enableVanillaFog); } { return new DhApiConfigValue<>(Config.Client.Advanced.Graphics.Fog.enableVanillaFog); }
@@ -97,10 +97,9 @@ public class DhApiGraphicsConfig implements IDhApiGraphicsConfig
public IDhApiConfigValue<Boolean> tintWithAvoidedBlocks() public IDhApiConfigValue<Boolean> tintWithAvoidedBlocks()
{ return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Graphics.Quality.tintWithAvoidedBlocks); } { return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Graphics.Quality.tintWithAvoidedBlocks); }
// TODO re-implement @Override
// @Override public IDhApiConfigValue<Integer> getBiomeBlending()
// public IDhApiConfigValue<Integer> getBiomeBlending() { return new DhApiConfigValue<Integer, Integer>(Config.Client.Advanced.Graphics.Quality.lodBiomeBlending); }
// { return new DhApiConfigValue<Integer, Integer>(Quality.lodBiomeBlending); }
@@ -277,7 +277,7 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
if (!getSpecificYCoordinate) if (!getSpecificYCoordinate)
{ {
// if we aren't look for a specific datapoint, add each datapoint to the return array // if we aren't look for a specific datapoint, add each datapoint to the return array
returnArray[i] = DhApiTerrainDataPointUtil.createApiDatapoint(levelWrapper, mapping, requestedDetailLevel, dataPoint); returnArray[i] = DhApiTerrainDataPointUtil.createApiDatapoint(levelWrapper.getMinHeight(), mapping, requestedDetailLevel, dataPoint);
} }
else else
{ {
@@ -294,7 +294,7 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
if (bottomY <= requestedY && requestedY < topY) // blockPositions start from the bottom of the block, thus "<=" for bottomY, just "<" for topY if (bottomY <= requestedY && requestedY < topY) // blockPositions start from the bottom of the block, thus "<=" for bottomY, just "<" for topY
{ {
// this datapoint contains the requested block position, return it // this datapoint contains the requested block position, return it
DhApiTerrainDataPoint apiTerrainData = DhApiTerrainDataPointUtil.createApiDatapoint(levelWrapper, mapping, requestedDetailLevel, dataPoint); DhApiTerrainDataPoint apiTerrainData = DhApiTerrainDataPointUtil.createApiDatapoint(levelWrapper.getMinHeight(), mapping, requestedDetailLevel, dataPoint);
return DhApiResult.createSuccess(new DhApiTerrainDataPoint[]{apiTerrainData}); return DhApiResult.createSuccess(new DhApiTerrainDataPoint[]{apiTerrainData});
} }
} }
@@ -345,7 +345,10 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
@Nullable @Nullable
IDhApiTerrainDataCache dataCache) IDhApiTerrainDataCache dataCache)
{ {
return this.raycastLodData(levelWrapper, new Vec3d(rayOriginX, rayOriginY, rayOriginZ), new Vec3f(rayDirectionX, rayDirectionY, rayDirectionZ), maxRayBlockLength, dataCache); return this.raycastLodData(levelWrapper,
new Vec3d(rayOriginX, rayOriginY, rayOriginZ),
new Vec3f(rayDirectionX, rayDirectionY, rayDirectionZ),
maxRayBlockLength, dataCache);
} }
/** /**
@@ -363,8 +366,8 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
{ {
rayDirection.normalize(); rayDirection.normalize();
int minBlockHeight = levelWrapper.getMinHeight(); int minLevelBlockHeight = levelWrapper.getMinHeight();
int maxBlockHeight = levelWrapper.getMaxHeight(); int maxLevelBlockHeight = levelWrapper.getMaxHeight();
@@ -380,7 +383,8 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
DhApiRaycastResult closetFoundDataPoint = null; DhApiRaycastResult closetFoundDataPoint = null;
while (blockPos.y >= minBlockHeight && blockPos.y < maxBlockHeight while (blockPos.y >= minLevelBlockHeight
&& blockPos.y < maxLevelBlockHeight
&& currentLength <= maxRayBlockLength) && currentLength <= maxRayBlockLength)
{ {
// get the LOD columns around this position // get the LOD columns around this position
@@ -403,7 +407,8 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
{ {
// does this LOD contain the given Y position? // does this LOD contain the given Y position?
Vec3i dataPointPos = new Vec3i(columnPos.x, dataPoint.bottomYBlockPos, columnPos.z); Vec3i dataPointPos = new Vec3i(columnPos.x, dataPoint.bottomYBlockPos, columnPos.z);
if (exactPos.y >= dataPoint.bottomYBlockPos && exactPos.y <= dataPoint.topYBlockPos) if (exactPos.y >= dataPoint.bottomYBlockPos
&& exactPos.y <= dataPoint.topYBlockPos)
{ {
if (closetFoundDataPoint == null) if (closetFoundDataPoint == null)
{ {
@@ -516,7 +521,7 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
//=============// //=============//
@Override @Override
public IDhApiTerrainDataCache getSoftCache() { return new DhApiTerrainDataCache(); } public IDhApiTerrainDataCache createSoftCache() { return new DhApiTerrainDataCache(); }
@@ -572,12 +577,16 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
} }
// draw raycast position // draw raycast position
if (rayCast.success && rayCast.payload != null) if (rayCast.success
&& rayCast.payload != null)
{ {
DebugRenderer.makeParticle( DebugRenderer.makeParticle(
new DebugRenderer.BoxParticle( new DebugRenderer.BoxParticle(
new DebugRenderer.Box( new DebugRenderer.Box(
DhSectionPos.encode((byte) 0, rayCast.payload.pos.x, rayCast.payload.pos.z), rayCast.payload.dataPoint.bottomYBlockPos, rayCast.payload.dataPoint.topYBlockPos, -0.1f, Color.RED), DhSectionPos.encode((byte) 0, rayCast.payload.pos.x, rayCast.payload.pos.z),
rayCast.payload.dataPoint.bottomYBlockPos,
rayCast.payload.dataPoint.topYBlockPos,
-0.1f, Color.RED),
1.0, 0f 1.0, 0f
) )
); );
@@ -29,9 +29,7 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.network.messages.MessageRegistry; import com.seibel.distanthorizons.core.network.messages.MessageRegistry;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.render.DhApiRenderProxy; import com.seibel.distanthorizons.core.render.DhApiRenderProxy;
import com.seibel.distanthorizons.core.render.renderer.VanillaFadeRenderer; import com.seibel.distanthorizons.core.render.renderer.*;
import com.seibel.distanthorizons.core.render.renderer.LodRenderer;
import com.seibel.distanthorizons.core.render.renderer.RenderParams;
import com.seibel.distanthorizons.core.util.TimerUtil; import com.seibel.distanthorizons.core.util.TimerUtil;
import com.seibel.distanthorizons.core.util.objects.Pair; import com.seibel.distanthorizons.core.util.objects.Pair;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
@@ -45,11 +43,8 @@ import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel; import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel;
import com.seibel.distanthorizons.core.render.glObject.GLProxy; import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.render.renderer.TestRenderer;
import com.seibel.distanthorizons.core.world.AbstractDhWorld; import com.seibel.distanthorizons.core.world.AbstractDhWorld;
import com.seibel.distanthorizons.core.world.DhClientServerWorld;
import com.seibel.distanthorizons.core.world.DhClientWorld; import com.seibel.distanthorizons.core.world.DhClientWorld;
import com.seibel.distanthorizons.core.world.IDhClientWorld;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper;
@@ -246,7 +241,7 @@ public class ClientApi
} }
} }
public void clientLevelLoadEvent(IClientLevelWrapper levelWrapper) public void clientLevelLoadEvent(@Nullable IClientLevelWrapper levelWrapper)
{ {
// wait a moment before loading the level to give the server a chance to handle the client's login request // wait a moment before loading the level to give the server a chance to handle the client's login request
if (MC_CLIENT.clientConnectedToDedicatedServer()) if (MC_CLIENT.clientConnectedToDedicatedServer())
@@ -322,41 +317,6 @@ public class ClientApi
//============//
// clint tick //
//============//
@Deprecated
public void clientTickEvent()
{
IProfilerWrapper profiler = MC_CLIENT.getProfiler();
profiler.push("DH-ClientTick");
try
{
IDhClientWorld clientWorld = SharedApi.tryGetDhClientWorld();
if (clientWorld != null)
{
clientWorld.clientTick();
// Ignore local world gen, as it's managed by server ticking
if (!(clientWorld instanceof DhClientServerWorld))
{
SharedApi.worldGenTick(clientWorld::worldGenTick);
}
}
}
catch (Exception e)
{
// handle errors here to prevent blowing up a mixin or API up stream
LOGGER.error("Unexpected error in ClientApi.clientTickEvent(), error: "+e.getMessage(), e);
}
profiler.pop();
}
//============// //============//
// networking // // networking //
//============// //============//
@@ -415,8 +375,11 @@ public class ClientApi
try try
{ {
// make sure the GLProxy is created for future use
GLProxy.getInstance();
// these tasks always need to be called, regardless of whether the renderer is enabled or not to prevent memory leaks // these tasks always need to be called, regardless of whether the renderer is enabled or not to prevent memory leaks
GLProxy.getInstance().runRenderThreadTasks(); GLProxy.runRenderThreadTasks();
} }
catch (Exception e) catch (Exception e)
{ {
@@ -570,8 +533,13 @@ public class ClientApi
{ {
// only fade when DH is rendering // only fade when DH is rendering
if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT
&&
(
// only fade when requested // only fade when requested
&& Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.get() == EDhApiMcRenderingFadeMode.DOUBLE_PASS Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.get() == EDhApiMcRenderingFadeMode.DOUBLE_PASS
// or if LOD-only mode is enabled (fading is used to remove the MC render pass)
|| Config.Client.Advanced.Debugging.lodOnlyMode.get()
)
// don't fade when Iris shaders are active, otherwise the rendering can get weird // don't fade when Iris shaders are active, otherwise the rendering can get weird
&& !DhApiRenderProxy.INSTANCE.getDeferTransparentRendering()) && !DhApiRenderProxy.INSTANCE.getDeferTransparentRendering())
{ {
@@ -679,7 +647,8 @@ public class ClientApi
{ {
// dev build // dev build
if (ModInfo.IS_DEV_BUILD if (ModInfo.IS_DEV_BUILD
&& !this.isDevBuildMessagePrinted && MC_CLIENT.playerExists()) && !this.isDevBuildMessagePrinted
&& MC_CLIENT.playerExists())
{ {
this.isDevBuildMessagePrinted = true; this.isDevBuildMessagePrinted = true;
this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis(); this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis();
@@ -725,10 +694,11 @@ public class ClientApi
if (!this.highVanillaRenderDistanceWarningPrinted if (!this.highVanillaRenderDistanceWarningPrinted
&& Config.Common.Logging.Warning.showHighVanillaRenderDistanceWarning.get()) && Config.Common.Logging.Warning.showHighVanillaRenderDistanceWarning.get())
{ {
this.highVanillaRenderDistanceWarningPrinted = true;
// DH generally doesn't need a vanilla render distance above 12 // DH generally doesn't need a vanilla render distance above 12
if (MC_RENDER.getRenderDistance() > 12) if (MC_RENDER.getRenderDistance() > 12)
{ {
this.highVanillaRenderDistanceWarningPrinted = true;
this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis(); this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis();
String message = String message =
@@ -9,6 +9,7 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent; import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent;
import com.seibel.distanthorizons.core.network.messages.base.LevelInitMessage; import com.seibel.distanthorizons.core.network.messages.base.LevelInitMessage;
import com.seibel.distanthorizons.core.network.session.NetworkSession; import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -89,7 +90,7 @@ public class ClientPluginChannelApi
LOGGER.info("Server level key received: [" + msg.levelKey + "]."); LOGGER.info("Server level key received: [" + msg.levelKey + "].");
MC.executeOnRenderThread(() -> GLProxy.queueRunningOnRenderThread(() ->
{ {
IClientLevelWrapper clientLevel = MC.getWrappedClientLevel(true); IClientLevelWrapper clientLevel = MC.getWrappedClientLevel(true);
IServerKeyedClientLevel existingKeyedClientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel(); IServerKeyedClientLevel existingKeyedClientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel();
@@ -53,30 +53,6 @@ public class ServerApi
//=============//
// tick events //
//=============//
public void serverTickEvent()
{
try
{
IDhServerWorld serverWorld = SharedApi.tryGetDhServerWorld();
if (serverWorld != null)
{
serverWorld.serverTick();
SharedApi.worldGenTick(serverWorld::worldGenTick);
}
}
catch (Exception e)
{
// try catch is necessary to prevent crashing the internal server when an exception is thrown
LOGGER.error("ServerTickEvent error: " + e.getMessage(), e);
}
}
//===============// //===============//
// server events // // server events //
//===============// //===============//
@@ -106,15 +82,15 @@ public class ServerApi
// level events // // level events //
//==============// //==============//
public void serverLevelLoadEvent(IServerLevelWrapper level) public void serverLevelLoadEvent(IServerLevelWrapper levelWrapper)
{ {
LOGGER.debug("Server Level " + level + " loading"); LOGGER.debug("Server Level " + levelWrapper + " loading");
AbstractDhWorld serverWorld = SharedApi.getAbstractDhWorld(); AbstractDhWorld serverWorld = SharedApi.getAbstractDhWorld();
if (serverWorld != null) if (serverWorld != null)
{ {
serverWorld.getOrLoadLevel(level); serverWorld.getOrLoadLevel(levelWrapper);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level)); ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(levelWrapper));
} }
} }
public void serverLevelUnloadEvent(IServerLevelWrapper level) public void serverLevelUnloadEvent(IServerLevelWrapper level)
@@ -25,6 +25,7 @@ import com.seibel.distanthorizons.core.Initializer;
import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateData; import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateData;
import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateQueueManager; import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateQueueManager;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.eventHandlers.IgnoredDimensionCsvHandler;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.generation.DhLightingEngine; import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.level.DhClientLevel; import com.seibel.distanthorizons.core.level.DhClientLevel;
@@ -141,16 +142,6 @@ public class SharedApi
} }
} }
public static void worldGenTick(Runnable worldGenRunnable)
{
lastWorldGenTickDelta--;
if (lastWorldGenTickDelta <= 0)
{
worldGenRunnable.run();
lastWorldGenTickDelta = 20;
}
}
@Nullable @Nullable
public static AbstractDhWorld getAbstractDhWorld() { return currentWorld; } public static AbstractDhWorld getAbstractDhWorld() { return currentWorld; }
@@ -194,12 +185,11 @@ public class SharedApi
public void chunkBlockChangedEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true, false); } public void chunkBlockChangedEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true, false); }
public void chunkLoadEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true, true); } public void chunkLoadEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true, true); }
//public void applyChunkUpdate(IChunkWrapper chunkWrapper, ILevelWrapper level, boolean canGetNeighboringChunks) { this.applyChunkUpdate(chunkWrapper, level, canGetNeighboringChunks, false); }
public void applyChunkUpdate(IChunkWrapper chunkWrapper, ILevelWrapper level, boolean canGetNeighboringChunks, boolean newlyLoaded) public void applyChunkUpdate(IChunkWrapper chunkWrapper, ILevelWrapper level, boolean canGetNeighboringChunks, boolean newlyLoaded)
{ {
//========================// //===================//
// world and level checks // // validation checks //
//========================// //===================//
if (chunkWrapper == null) if (chunkWrapper == null)
{ {
@@ -227,7 +217,6 @@ public class SharedApi
return; return;
} }
// only continue if the level is loaded // only continue if the level is loaded
IDhLevel dhLevel = dhWorld.getLevel(level); IDhLevel dhLevel = dhWorld.getLevel(level);
if (dhLevel == null) if (dhLevel == null)
@@ -242,6 +231,7 @@ public class SharedApi
return; return;
} }
// ignore chunk updates if the network should handle them
if (dhLevel instanceof DhClientLevel) if (dhLevel instanceof DhClientLevel)
{ {
if (!((DhClientLevel) dhLevel).shouldProcessChunkUpdate(chunkWrapper.getChunkPos())) if (!((DhClientLevel) dhLevel).shouldProcessChunkUpdate(chunkWrapper.getChunkPos()))
@@ -250,7 +240,14 @@ public class SharedApi
} }
} }
// shoudln't normally happen, but just in case // ignore chunk updates for non-rendered levels
String dimName = dhLevel.getLevelWrapper().getDimensionName();
if (IgnoredDimensionCsvHandler.INSTANCE.dimensionNameShouldBeIgnored(dimName))
{
return;
}
// shouldn't normally happen, but just in case
if (CHUNK_UPDATE_QUEUE_MANAGER.contains(chunkWrapper.getChunkPos())) if (CHUNK_UPDATE_QUEUE_MANAGER.contains(chunkWrapper.getChunkPos()))
{ {
// TODO this will prevent some LODs from updating across dimensions if multiple levels are loaded // TODO this will prevent some LODs from updating across dimensions if multiple levels are loaded
@@ -258,85 +255,11 @@ public class SharedApi
} }
queueChunkUpdate(chunkWrapper, dhLevel);
//===============================//
// update the necessary chunk(s) //
//===============================//
if (!canGetNeighboringChunks)
{
// only update the center chunk
queueChunkUpdate(chunkWrapper, null, dhLevel, false);
return;
} }
private static void queueChunkUpdate(IChunkWrapper chunkWrapper, IDhLevel dhLevel)
ArrayList<IChunkWrapper> neighboringChunkList = getNeighborChunkListForChunk(chunkWrapper, dhLevel);
if (newlyLoaded)
{ {
// this means this chunkWrapper is a newly loaded chunk
// which may be missing some neighboring chunk data
// because it is bordering the render distance
// thus, only the chunks neighboring this chunkWrapper will get updated
// because those are more likely to have their full neighboring chunk data
//TODO this does not prevent those neighboring chunks from updating
// this newly loaded chunk that were just skipped
// leading to occasional lighting issues
for (IChunkWrapper neighboringChunk : neighboringChunkList)
{
if (neighboringChunk == chunkWrapper)
{
continue;
}
this.applyChunkUpdate(neighboringChunk, level, true, false);
}
}
else
{
// if not all neighboring chunk data is available, do not try to update
if (neighboringChunkList.size() < 9)
{
return;
}
// update the center with any existing neighbour chunks.
// this is done so lighting changes are propagated correctly
queueChunkUpdate(chunkWrapper, neighboringChunkList, dhLevel, true);
}
}
private static ArrayList<IChunkWrapper> getNeighborChunkListForChunk(IChunkWrapper chunkWrapper, IDhLevel dhLevel)
{
// get the neighboring chunk list
ArrayList<IChunkWrapper> neighborChunkList = new ArrayList<>(9);
for (int xOffset = -1; xOffset <= 1; xOffset++)
{
for (int zOffset = -1; zOffset <= 1; zOffset++)
{
if (xOffset == 0 && zOffset == 0)
{
// center chunk
neighborChunkList.add(chunkWrapper);
}
else
{
// neighboring chunk
DhChunkPos neighborPos = new DhChunkPos(chunkWrapper.getChunkPos().getX() + xOffset, chunkWrapper.getChunkPos().getZ() + zOffset);
IChunkWrapper neighborChunk = dhLevel.getLevelWrapper().tryGetChunk(neighborPos);
if (neighborChunk != null)
{
neighborChunkList.add(neighborChunk);
}
}
}
}
return neighborChunkList;
}
private static void queueChunkUpdate(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighborChunkList, IDhLevel dhLevel, boolean canGetNeighboringChunks)
{
// return if the chunk is already queued // return if the chunk is already queued
if (CHUNK_UPDATE_QUEUE_MANAGER.contains(chunkWrapper.getChunkPos())) if (CHUNK_UPDATE_QUEUE_MANAGER.contains(chunkWrapper.getChunkPos()))
{ {
@@ -345,7 +268,7 @@ public class SharedApi
// add chunk update data to preUpdate queue // add chunk update data to preUpdate queue
ChunkUpdateData updateData = new ChunkUpdateData(chunkWrapper, neighborChunkList, dhLevel, canGetNeighboringChunks); ChunkUpdateData updateData = new ChunkUpdateData(chunkWrapper, dhLevel);
CHUNK_UPDATE_QUEUE_MANAGER.addItemToPreUpdateQueue(chunkWrapper.getChunkPos(), updateData); CHUNK_UPDATE_QUEUE_MANAGER.addItemToPreUpdateQueue(chunkWrapper.getChunkPos(), updateData);
@@ -353,7 +276,8 @@ public class SharedApi
// (this prevents doing extra work queuing tasks that may not be necessary) // (this prevents doing extra work queuing tasks that may not be necessary)
// and makes sure the chunks closest to the player are updated first // and makes sure the chunks closest to the player are updated first
PriorityTaskPicker.Executor executor = ThreadPoolUtil.getChunkToLodBuilderExecutor(); PriorityTaskPicker.Executor executor = ThreadPoolUtil.getChunkToLodBuilderExecutor();
if (executor != null && executor.getQueueSize() < executor.getPoolSize()) if (executor != null
&& executor.getQueueSize() < executor.getPoolSize())
{ {
try try
{ {
@@ -393,10 +317,7 @@ public class SharedApi
// update the necessary chunk(s) // // update the necessary chunk(s) //
//===============================// //===============================//
// process preUpdate queue
processQueuedChunkPreUpdate(); processQueuedChunkPreUpdate();
// process update queue
processQueuedChunkUpdate(); processQueuedChunkUpdate();
// queue the next position if there are still positions to process // queue the next position if there are still positions to process
@@ -425,8 +346,7 @@ public class SharedApi
IDhLevel dhLevel = preUpdateData.dhLevel; IDhLevel dhLevel = preUpdateData.dhLevel;
IChunkWrapper chunkWrapper = preUpdateData.chunkWrapper; IChunkWrapper chunkWrapper = preUpdateData.chunkWrapper;
boolean canGetNeighboringChunks = preUpdateData.canGetNeighboringChunks; chunkWrapper.createDhHeightMaps();
ArrayList<IChunkWrapper> neighborChunkList = preUpdateData.neighborChunkList;
try try
{ {
@@ -443,34 +363,6 @@ public class SharedApi
// do not update the chunk if the hash is the same // do not update the chunk if the hash is the same
return; return;
} }
// if this chunk will update and can get neighbors
// then queue neighboring chunks to update as well
// neighboring chunk will get added directly to the update queue
// so they won't queue further chunk updates
if (neighborChunkList != null
&& !neighborChunkList.isEmpty())
{
for (IChunkWrapper adjacentChunk : neighborChunkList)
{
// pulling a new chunkWrapper is necessary to prevent concurrent modification on the existing chunkWrappers
IChunkWrapper newCenterChunk = dhLevel.getLevelWrapper().tryGetChunk(adjacentChunk.getChunkPos());
if (newCenterChunk != null)
{
ChunkUpdateData newUpdateData;
if (canGetNeighboringChunks)
{
newUpdateData = new ChunkUpdateData(newCenterChunk, getNeighborChunkListForChunk(newCenterChunk, dhLevel), dhLevel, true);
}
else
{
newUpdateData = new ChunkUpdateData(newCenterChunk, null, dhLevel, false);
}
CHUNK_UPDATE_QUEUE_MANAGER.addItemToUpdateQueue(newCenterChunk.getChunkPos(), newUpdateData);
}
}
}
} }
CHUNK_UPDATE_QUEUE_MANAGER.addItemToUpdateQueue(chunkWrapper.getChunkPos(), preUpdateData); CHUNK_UPDATE_QUEUE_MANAGER.addItemToUpdateQueue(chunkWrapper.getChunkPos(), preUpdateData);
@@ -483,8 +375,6 @@ public class SharedApi
private static void processQueuedChunkUpdate() private static void processQueuedChunkUpdate()
{ {
//LOGGER.trace(chunkWrapper.getChunkPos() + " " + executor.getActiveCount() + " / " + executor.getQueue().size() + " - " + executor.getCompletedTaskCount());
ChunkUpdateData updateData = CHUNK_UPDATE_QUEUE_MANAGER.updateQueue.popClosest(); ChunkUpdateData updateData = CHUNK_UPDATE_QUEUE_MANAGER.updateQueue.popClosest();
if (updateData == null) if (updateData == null)
{ {
@@ -494,15 +384,11 @@ public class SharedApi
IChunkWrapper chunkWrapper = updateData.chunkWrapper; IChunkWrapper chunkWrapper = updateData.chunkWrapper;
IDhLevel dhLevel = updateData.dhLevel; IDhLevel dhLevel = updateData.dhLevel;
ILevelWrapper levelWrapper = dhLevel.getLevelWrapper(); ILevelWrapper levelWrapper = dhLevel.getLevelWrapper();
// having a list of the nearby chunks is needed for lighting and beacon generation
@Nullable ArrayList<IChunkWrapper> nearbyChunkList = updateData.neighborChunkList;
// a non-null list is needed for the lighting engine // having a list of the nearby chunks is needed for lighting and beacon generation
if (nearbyChunkList == null) ArrayList<IChunkWrapper> nearbyChunkList = tryGetNeighborChunkListForChunk(chunkWrapper);
{
nearbyChunkList = new ArrayList<IChunkWrapper>();
nearbyChunkList.add(chunkWrapper);
}
try try
{ {
@@ -518,6 +404,35 @@ public class SharedApi
{ {
LOGGER.error("Unexpected error when updating chunk at pos: [" + chunkWrapper.getChunkPos() + "]", e); LOGGER.error("Unexpected error when updating chunk at pos: [" + chunkWrapper.getChunkPos() + "]", e);
} }
CHUNK_UPDATE_QUEUE_MANAGER.queuedChunkWrapperByChunkPos.remove(updateData.chunkWrapper.getChunkPos());
}
private static ArrayList<IChunkWrapper> tryGetNeighborChunkListForChunk(IChunkWrapper chunkWrapper)
{
// get the neighboring chunk list
ArrayList<IChunkWrapper> neighborChunkList = new ArrayList<>(9);
for (int xOffset = -1; xOffset <= 1; xOffset++)
{
for (int zOffset = -1; zOffset <= 1; zOffset++)
{
if (xOffset == 0 && zOffset == 0)
{
// center chunk
neighborChunkList.add(chunkWrapper);
}
else
{
// neighboring chunk
DhChunkPos neighborPos = new DhChunkPos(chunkWrapper.getChunkPos().getX() + xOffset, chunkWrapper.getChunkPos().getZ() + zOffset);
IChunkWrapper neighborChunk = CHUNK_UPDATE_QUEUE_MANAGER.tryGetChunk(neighborPos);
if (neighborChunk != null)
{
neighborChunkList.add(neighborChunk);
}
}
}
}
return neighborChunkList;
} }
@@ -9,18 +9,13 @@ import java.util.ArrayList;
public class ChunkUpdateData public class ChunkUpdateData
{ {
public IChunkWrapper chunkWrapper; public IChunkWrapper chunkWrapper;
@Nullable
public ArrayList<IChunkWrapper> neighborChunkList;
public IDhLevel dhLevel; public IDhLevel dhLevel;
public boolean canGetNeighboringChunks;
public ChunkUpdateData(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighborChunkList, IDhLevel dhLevel, boolean canGetNeighborChunks) public ChunkUpdateData(IChunkWrapper chunkWrapper, IDhLevel dhLevel)
{ {
this.chunkWrapper = chunkWrapper; this.chunkWrapper = chunkWrapper;
this.neighborChunkList = neighborChunkList;
this.dhLevel = dhLevel; this.dhLevel = dhLevel;
this.canGetNeighboringChunks = canGetNeighborChunks;
} }
} }
@@ -1,12 +1,23 @@
package com.seibel.distanthorizons.core.api.internal.chunkUpdating; package com.seibel.distanthorizons.core.api.internal.chunkUpdating;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.seibel.distanthorizons.core.api.internal.ClientApi; import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.api.internal.SharedApi; import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoadRequestQueue;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.world.EWorldEnvironment; import com.seibel.distanthorizons.core.world.EWorldEnvironment;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
public class ChunkUpdateQueueManager public class ChunkUpdateQueueManager
{ {
@@ -15,7 +26,14 @@ public class ChunkUpdateQueueManager
public final ChunkPosQueue updateQueue; public final ChunkPosQueue updateQueue;
public final ChunkPosQueue preUpdateQueue; public final ChunkPosQueue preUpdateQueue;
public final Set<DhChunkPos> ignoredChunkPosSet = Collections.newSetFromMap(new ConcurrentHashMap<>());
public final ConcurrentMap<DhChunkPos, IChunkWrapper> queuedChunkWrapperByChunkPos = CacheBuilder.newBuilder()
.expireAfterWrite(20, TimeUnit.SECONDS)
.<DhChunkPos, IChunkWrapper>build()
.asMap();
/** dynamically changes based on the number of threads currently available */
public int maxSize = 500; public int maxSize = 500;
private static long lastOverloadedLogMessageMsTime = 0; private static long lastOverloadedLogMessageMsTime = 0;
@@ -38,12 +56,18 @@ public class ChunkUpdateQueueManager
// list/set methods // // list/set methods //
//==================// //==================//
public boolean contains(DhChunkPos pos) { return this.updateQueue.contains(pos) || this.preUpdateQueue.contains(pos); } public boolean contains(DhChunkPos pos)
{
return this.updateQueue.contains(pos)
|| this.ignoredChunkPosSet.contains(pos)
|| this.preUpdateQueue.contains(pos);
}
public void clear() public void clear()
{ {
this.updateQueue.clear(); this.updateQueue.clear();
this.preUpdateQueue.clear(); this.preUpdateQueue.clear();
this.ignoredChunkPosSet.clear();
} }
public int getQueuedCount() { return this.updateQueue.getQueuedCount() + this.preUpdateQueue.getQueuedCount(); } public int getQueuedCount() { return this.updateQueue.getQueuedCount() + this.preUpdateQueue.getQueuedCount(); }
public boolean isEmpty() public boolean isEmpty()
@@ -57,41 +81,24 @@ public class ChunkUpdateQueueManager
* If there are no more slots, replaces the item furthest from the center in the update queue. * If there are no more slots, replaces the item furthest from the center in the update queue.
*/ */
public void addItemToPreUpdateQueue(DhChunkPos pos, ChunkUpdateData updateData) public void addItemToPreUpdateQueue(DhChunkPos pos, ChunkUpdateData updateData)
{ { this.addItemToQueue(pos, updateData, this.preUpdateQueue); }
int remainingSlots = this.maxSize - this.getQueuedCount();
// If no slots are left, get one by removing the item furthest from the center
if (remainingSlots <= 0)
{
if (!this.updateQueue.isEmpty())
{
this.updateQueue.popFurthest();
}
else
{
this.preUpdateQueue.popFurthest();
}
}
this.preUpdateQueue.addItem(pos, updateData);
remainingSlots = this.maxSize - this.getQueuedCount();
if (remainingSlots <= 0)
{
this.sendOverloadMessage();
}
}
public void addItemToUpdateQueue(DhChunkPos pos, ChunkUpdateData updateData) public void addItemToUpdateQueue(DhChunkPos pos, ChunkUpdateData updateData)
{ this.addItemToQueue(pos, updateData, this.updateQueue); }
private void addItemToQueue(DhChunkPos pos, ChunkUpdateData updateData, ChunkPosQueue queue)
{ {
int remainingSlots = this.maxSize - this.getQueuedCount(); int remainingSlots = this.maxSize - this.getQueuedCount();
// If no slots are left, get one by removing the item furthest from the center // If no slots are left, get one by removing the item furthest from the center
if (remainingSlots <= 0) if (remainingSlots <= 0)
{ {
this.updateQueue.popFurthest(); ChunkUpdateData removedData = queue.popFurthest();
this.queuedChunkWrapperByChunkPos.remove(removedData.chunkWrapper.getChunkPos());
} }
this.updateQueue.addItem(pos,updateData); queue.addItem(pos,updateData);
this.queuedChunkWrapperByChunkPos.putIfAbsent(pos, updateData.chunkWrapper);
remainingSlots = this.maxSize - this.getQueuedCount(); remainingSlots = this.maxSize - this.getQueuedCount();
if (remainingSlots <= 0) if (remainingSlots <= 0)
@@ -100,6 +107,7 @@ public class ChunkUpdateQueueManager
} }
} }
private void sendOverloadMessage() private void sendOverloadMessage()
{ {
// limit how often an overloaded message can be sent // limit how often an overloaded message can be sent
@@ -129,6 +137,35 @@ public class ChunkUpdateQueueManager
} }
} }
/**
* Tries to return a cloned chunk wrapper from memory.
* Returns null if no chunk is available.
* <br><br>
* This is done instead of accessing the MC level since
* accessing the level often requires running on the render or server
* thread, which causes stuttering.
*/
@Nullable
public IChunkWrapper tryGetChunk(DhChunkPos pos)
{
IChunkWrapper existingWrapper = this.queuedChunkWrapperByChunkPos.get(pos);
if (existingWrapper == null)
{
return null;
}
return existingWrapper.copy();
}
//=========//
// ignores //
//=========//
public void addPosToIgnore(DhChunkPos chunkPos) { this.ignoredChunkPosSet.add(chunkPos); }
public void removePosToIgnore(DhChunkPos chunkPos) { this.ignoredChunkPosSet.remove(chunkPos); }
//==================// //==================//
@@ -33,13 +33,12 @@ import com.seibel.distanthorizons.core.config.types.enums.*;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.util.NativeDialogUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.lwjgl.util.tinyfd.TinyFileDialogs;
import java.awt.*;
import java.io.File; import java.io.File;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
@@ -103,10 +102,7 @@ public class Config
public static ConfigUiLinkedEntry quickEnableWorldGenerator = new ConfigUiLinkedEntry(Common.WorldGenerator.enableDistantGeneration); public static ConfigUiLinkedEntry quickEnableWorldGenerator = new ConfigUiLinkedEntry(Common.WorldGenerator.enableDistantGeneration);
public static ConfigEntry<Boolean> quickShowWorldGenProgress = new ConfigEntry.Builder<Boolean>() public static ConfigUiLinkedEntry quickShowWorldGenProgress = new ConfigUiLinkedEntry(Common.WorldGenerator.showGenerationProgress);
.set(false) // TODO should be set by the underlying world gen progress button, not a static default
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.build();
public static ConfigUiLinkedEntry quickLodCloudRendering = new ConfigUiLinkedEntry(Advanced.Graphics.GenericRendering.enableCloudRendering); public static ConfigUiLinkedEntry quickLodCloudRendering = new ConfigUiLinkedEntry(Advanced.Graphics.GenericRendering.enableCloudRendering);
@@ -126,7 +122,6 @@ public class Config
{ {
// common config links need to have their destination // common config links need to have their destination
// since they aren't part of "client" config class // since they aren't part of "client" config class
// TODO determine their destination programically instead of hard coding the value
public static ConfigUIComment advancedHeader = new ConfigUIComment.Builder().setParentConfigClass(Advanced.class).build(); public static ConfigUIComment advancedHeader = new ConfigUIComment.Builder().setParentConfigClass(Advanced.class).build();
@@ -172,6 +167,20 @@ public class Config
public static ConfigCategory culling = new ConfigCategory.Builder().set(Culling.class).build(); public static ConfigCategory culling = new ConfigCategory.Builder().set(Culling.class).build();
public static ConfigUISpacer cullingSpacer = new ConfigUISpacer.Builder().build(); public static ConfigUISpacer cullingSpacer = new ConfigUISpacer.Builder().build();
public static ConfigEntry<Boolean> overrideVanillaGraphicsSettings = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment("" +
"If true some vanilla graphics settings will be automatically changed \n" +
"during DH setup to provide a better experience. \n" +
" \n" +
"IE disabling vanilla clouds (which render on top of DH LODs), \n" +
" and chunk fading (DH already fades MC chunks) \n" +
"")
.build();
public static ConfigUISpacer overrideVanillaSpacer = new ConfigUISpacer.Builder().build();
public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build(); public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build();
@@ -263,7 +272,7 @@ public class Config
public static ConfigEntry<Double> lodBias = new ConfigEntry.Builder<Double>() public static ConfigEntry<Double> lodBias = new ConfigEntry.Builder<Double>()
.setMinDefaultMax(0d, 0d, null) .setMinDefaultMax(0d, 0d, null)
.comment("" .comment(""
+ "What the value should vanilla Minecraft's texture LodBias be? \n" + "What value should vanilla Minecraft's texture LodBias be? \n"
+ "If set to 0 the mod wont overwrite vanilla's default (which so happens to also be 0)") + "If set to 0 the mod wont overwrite vanilla's default (which so happens to also be 0)")
.build(); .build();
@@ -410,6 +419,14 @@ public class Config
"") "")
.build(); .build();
public static ConfigEntry<Integer> fadeDistanceInBlocks = new ConfigEntry.Builder<Integer>()
.setMinDefaultMax(0, 1_600, 30_000_000)
.comment("" +
"The distance in blocks from the camera where the SSAO will fade out to. \n"+
"This is done to prevent banding and noise at extreme distances. \n"+
"")
.build();
} }
public static class GenericRendering public static class GenericRendering
@@ -488,11 +505,6 @@ public class Config
+ "Note: Other mods may conflict with this setting. \n" + "Note: Other mods may conflict with this setting. \n"
+ "") + "")
.build(); .build();
@Deprecated
public static ConfigEntry<Boolean> disableVanillaFog = new ConfigEntry.Builder<Boolean>()
.set(!enableVanillaFog.get())
.setAppearance(EConfigEntryAppearance.ONLY_IN_API)
.build();
@@ -554,14 +566,6 @@ public class Config
static
{
disableVanillaFog.addListener(
new ConfigChangeListener<Boolean>(disableVanillaFog,
(disableVanillaFog) -> enableVanillaFog.setApiValue(disableVanillaFog))
);
}
public static class HeightFog public static class HeightFog
{ {
public static ConfigUIComment heightFogHeader = new ConfigUIComment.Builder().setParentConfigClass(HeightFog.class).build(); public static ConfigUIComment heightFogHeader = new ConfigUIComment.Builder().setParentConfigClass(HeightFog.class).build();
@@ -776,6 +780,11 @@ public class Config
+ "A comma separated list of block resource locations that won't be rendered by DH. \n" + "A comma separated list of block resource locations that won't be rendered by DH. \n"
+ "Air is always included in this list. \n" + "Air is always included in this list. \n"
+ "Requires a restart to change. \n" + "Requires a restart to change. \n"
+ "\n"
+ "Note:\n"
+ "If you see gaps, or holes you may have to change\n"
+ "worldCompression to ["+EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS+"] and re-generate the LODs.\n"
+ "Black spots may happen occur to block lighting being zero for covered blocks.\n"
+ "") + "")
.build(); .build();
@@ -824,7 +833,7 @@ public class Config
public static ConfigUIComment experimentalHeader = new ConfigUIComment.Builder().setParentConfigClass(Experimental.class).build(); public static ConfigUIComment experimentalHeader = new ConfigUIComment.Builder().setParentConfigClass(Experimental.class).build();
public static ConfigEntry<Integer> earthCurveRatio = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> earthCurveRatio = new ConfigEntry.Builder<Integer>()
.setMinDefaultMax(0, 0, 5000) .setMinDefaultMax(-5000, 0, 5000)
.comment("" .comment(""
+ "This is the earth size ratio when applying the curvature shader effect. \n" + "This is the earth size ratio when applying the curvature shader effect. \n"
+ "Note: Enabling this feature may cause rendering bugs. \n" + "Note: Enabling this feature may cause rendering bugs. \n"
@@ -834,24 +843,26 @@ public class Config
+ "100 = 1 to 100 (63,710 blocks) \n" + "100 = 1 to 100 (63,710 blocks) \n"
+ "10000 = 1 to 10000 (637.1 blocks) \n" + "10000 = 1 to 10000 (637.1 blocks) \n"
+ "\n" + "\n"
+ "Note: Due to current limitations, the min value is 50 \n" + "Note: Due to current limitations, the min value is ["+WorldCurvatureConfigEventHandler.MIN_VALID_CURVE_VALUE+"] \n"
+ "and the max value is 5000. Any values outside this range \n" + "and the max value is 5000. Any values outside this range \n"
+ "will be set to 0 (disabled).") + "will be set to 0 (disabled).")
.addListener(WorldCurvatureConfigEventHandler.INSTANCE) .addListener(WorldCurvatureConfigEventHandler.INSTANCE)
.build(); .build();
// TODO should be replaced with a better long-term solution public static ConfigEntry<String> ignoredDimensionCsv = new ConfigEntry.Builder<String>()
@Deprecated .set("")
public static ConfigEntry<Boolean> onlyLoadCenterLods = new ConfigEntry.Builder<Boolean>()
.set(false)
.comment("" .comment(""
+ "For internal testing:\n" + "A comma separated list of dimension resource locations where DH won't render. \n"
+ "Skips loading adjacent LODs to significantly reduce load times (~5x)\n" + "\n"
+ "but causes lighting on LOD borders to appear as full-bright\n" + "Example: \"minecraft:the_nether,minecraft:the_end\"\n"
+ "and other graphical bugs.\n" + "\n"
+ "Note:\n"
+ "Some DH settings will be disabled and/or changed to improve \n"
+ "visuals when DH rendering is disabled. \n"
+ "") + "")
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE) .addListener(IgnoredDimensionCsvHandler.INSTANCE)
.build(); .build();
} }
} }
@@ -1236,20 +1247,14 @@ public class Config
public static ConfigUIButton uiButtonTest = new ConfigUIButton(() -> public static ConfigUIButton uiButtonTest = new ConfigUIButton(() ->
{ {
// running on a separate thread is necessary to prevent locking // running on a separate thread is necessary to prevent locking
new Thread(() -> new Thread(() -> onButtonPressed()).start();
{ });
if (!GraphicsEnvironment.isHeadless()) public static void onButtonPressed()
{ {
LOGGER.info("Attempting to show tinyfd message box..."); LOGGER.info("Attempting to show tinyfd message box...");
boolean buttonPress = TinyFileDialogs.tinyfd_messageBox("Button pressed!", "UITester dialog", "ok", "info", false); boolean buttonPress = NativeDialogUtil.showDialog("Button pressed!", "UITester dialog", "ok", "info");
LOGGER.info("dialog returned with ["+(buttonPress ? "TRUE" : "FALSE")+"]"); LOGGER.info("dialog returned with ["+(buttonPress ? "TRUE" : "FALSE")+"]");
} }
else
{
LOGGER.info("button pressed!");
}
}).start();
});
public static ConfigCategory categoryTest = new ConfigCategory.Builder().set(CategoryTest.class).build(); public static ConfigCategory categoryTest = new ConfigCategory.Builder().set(CategoryTest.class).build();
@@ -1365,6 +1370,37 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Integer> generationCenterChunkX = new ConfigEntry.Builder<Integer>()
.setChatCommandName("generation.bounds.centerChunk.x")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.setMinDefaultMax(Integer.MIN_VALUE, 0, Integer.MAX_VALUE)
.comment("" +
"The center X chunk position that the world gen max radius is centered around. \n" +
"")
.build();
public static ConfigEntry<Integer> generationCenterChunkZ = new ConfigEntry.Builder<Integer>()
.setChatCommandName("generation.bounds.centerChunk.z")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.setMinDefaultMax(Integer.MIN_VALUE, 0, Integer.MAX_VALUE)
.comment("" +
"The center Z chunk position that the world gen max radius is centered around. \n" +
"")
.build();
public static ConfigEntry<Integer> generationMaxChunkRadius = new ConfigEntry.Builder<Integer>()
.setChatCommandName("generation.bounds.radiusInChunks")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.setMinDefaultMax(0, 0, Integer.MAX_VALUE)
.comment("" +
"The max radius in chunks around the central point where world generation is allowed. \n" +
"If this value is set to 0, generation bounds are disabled and the render distance will be used. \n" +
"\n" +
"This should only be set if you have a pre-generated world that has a very limited size. \n" +
"Setting this on a normal MC world will prevent the world generator from filling \n" +
"out your render distance. \n" +
"")
.build();
} }
public static class LodBuilding public static class LodBuilding
@@ -1375,7 +1411,7 @@ public class Config
.set(false) .set(false)
// enabling this can be quite detrimental to performance, // enabling this can be quite detrimental to performance,
// so hiding it in the config file should reduce people accidentally enabling it // so hiding it in the config file should reduce people accidentally enabling it
.setAppearance(isRunningInDevEnvironment() ? EConfigEntryAppearance.ALL : EConfigEntryAppearance.ONLY_IN_FILE) .setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.comment("" .comment(""
+ "Enabling this will drastically increase chunk processing time\n" + "Enabling this will drastically increase chunk processing time\n"
+ "and you may need to increase your CPU load to handle it.\n" + "and you may need to increase your CPU load to handle it.\n"
@@ -1390,37 +1426,9 @@ public class Config
.build(); .build();
public static ConfigEntry<EDhApiDataCompressionMode> dataCompression = new ConfigEntry.Builder<EDhApiDataCompressionMode>() public static ConfigEntry<EDhApiDataCompressionMode> dataCompression = new ConfigEntry.Builder<EDhApiDataCompressionMode>()
.set(EDhApiDataCompressionMode.Z_STD) .set(EDhApiDataCompressionMode.Z_STD_BLOCK)
.comment("" // only visible via the API since there is no reason to use any compressor except ZStandard as of 2025-11-24
+ "What algorithm should be used to compress new LOD data? \n" .setAppearance(EConfigEntryAppearance.ONLY_IN_API)
+ "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: 6.09 milliseconds\n"
+ "Estimated average DTO write speed: 6.01 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.4513\n"
+ "Estimated average DTO read speed: 3.25 ms\n"
+ "Estimated average DTO write speed: 5.99 ms\n"
+ "\n"
+ EDhApiDataCompressionMode.Z_STD + " \n"
+ "A good option if you're CPU limited and have plenty of hard drive space.\n"
+ "Expected Compression Ratio: 0.2606\n"
+ "Estimated average DTO read speed: 9.31 ms\n"
+ "Estimated average DTO write speed: 15.13 ms\n"
+ "\n"
+ EDhApiDataCompressionMode.LZMA2 + " \n"
+ "Slow but very good compression.\n"
+ "Expected Compression Ratio: 0.2\n"
+ "Estimated average DTO read speed: 13.29 ms\n"
+ "Estimated average DTO write speed: 70.95 ms\n"
+ "")
.build(); .build();
public static ConfigEntry<EDhApiWorldCompressionMode> worldCompression = new ConfigEntry.Builder<EDhApiWorldCompressionMode>() public static ConfigEntry<EDhApiWorldCompressionMode> worldCompression = new ConfigEntry.Builder<EDhApiWorldCompressionMode>()
@@ -1443,49 +1451,6 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Boolean> recalculateChunkHeightmaps = new ConfigEntry.Builder<Boolean>()
.set(false)
.comment(""
+ "True: Recalculate chunk height maps before chunks can be used by DH.\n"
+ " This can fix problems with worlds created by World Painter or \n"
+ " other external tools where the heightmap format may be incorrect. \n"
+ "False: Assume any height maps handled by Minecraft are correct. \n"
+ "\n"
+ "Fastest: False\n"
+ "Most Compatible: True\n"
+ "")
.build();
public static ConfigEntry<Boolean> pullLightingForPregeneratedChunks = new ConfigEntry.Builder<Boolean>()
.set(false)
.comment(""
+ "If true LOD generation for pre-existing chunks will attempt to pull the lighting data \n"
+ "saved in Minecraft's Region files. \n"
+ "If false DH will pull in chunks without lighting and re-light them. \n"
+ " \n"
+ "Setting this to true will result in faster LOD generation \n"
+ "for already generated worlds, but is broken by most lighting mods. \n"
+ " \n"
+ "Set this to false if LODs are black. \n"
+ "")
.build();
public static ConfigEntry<Boolean> assumePreExistingChunksAreFinished = new ConfigEntry.Builder<Boolean>()
.set(false)
.comment(""
+ "When DH pulls in pre-existing chunks it will attempt to \n"
+ "run any missing world generation steps; for example: \n"
+ "if a chunk has the status SURFACE, DH will skip BIOMES \n"
+ "and SURFACE, but will run FEATURES. \n"
+ " \n"
+ "However if for some reason the chunks are malformed \n"
+ "or there's some other issue that causes the status \n"
+ "to be incorrect that can either cause world gen \n"
+ "lock-ups and/or crashes. \n"
+ "If either of those happen try setting this to True. \n"
+ "")
.build();
public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build(); public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build();
@@ -1527,6 +1492,7 @@ public class Config
+ "How many threads should be used by Distant Horizons? \n" + "How many threads should be used by Distant Horizons? \n"
+ "") + "")
.build(); .build();
public static final ConfigEntry<Double> threadRunTimeRatio = new ConfigEntry.Builder<Double>() public static final ConfigEntry<Double> threadRunTimeRatio = new ConfigEntry.Builder<Double>()
.setChatCommandName("threading.threadRunTimeRatio") .setChatCommandName("threading.threadRunTimeRatio")
.setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getDefaultRunTimeRatio(), 1.0) .setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getDefaultRunTimeRatio(), 1.0)
@@ -1540,6 +1506,19 @@ public class Config
"") "")
.build(); .build();
public static final ConfigEntry<Integer> threadPriority = new ConfigEntry.Builder<Integer>()
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // only in file since this requires a MC reboot to change
.setMinDefaultMax(Thread.MIN_PRIORITY, // 1
Thread.NORM_PRIORITY, // 5 (1 higher than C2ME's default priority of 4 which can help reduce issues with Chunky)
Thread.MAX_PRIORITY) // 10
.comment(""
+ "What Java thread priority should DH's primary thread pools run with? \n"
+ "\n"
+ "You probably don't need to change this unless you are also \n"
+ "running C2ME and are seeing thread starvation in either C2ME or DH. \n"
+ "")
.build();
} }
@@ -1576,14 +1555,6 @@ public class Config
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerLevel> logWorldGenPerformanceToFile = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.setChatCommandName("logging.logWorldGenPerformance")
.set(EDhApiLoggerLevel.INFO)
.comment(""
+ "If enabled, the mod will log performance about the world generation process. \n"
+ "This can be useful for debugging.")
.build();
public static ConfigEntry<EDhApiLoggerLevel> logWorldGenChunkLoadEventToFile = new ConfigEntry.Builder<EDhApiLoggerLevel>() public static ConfigEntry<EDhApiLoggerLevel> logWorldGenChunkLoadEventToFile = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.setChatCommandName("logging.logWorldGenLoadEvent") .setChatCommandName("logging.logWorldGenLoadEvent")
.set(EDhApiLoggerLevel.INFO) .set(EDhApiLoggerLevel.INFO)
@@ -1621,6 +1592,14 @@ public class Config
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerLevel> logConnectionConfigChangesToFile = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.setChatCommandName("logging.logConnectionConfigChanges")
.set(EDhApiLoggerLevel.WARN)
.comment(""
+ "If enabled, config changes sent by the server will be logged. \n"
+ "")
.build();
public static ConfigCategory warning = new ConfigCategory.Builder().set(Warning.class).build(); public static ConfigCategory warning = new ConfigCategory.Builder().set(Warning.class).build();
@@ -1669,6 +1648,14 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Boolean> showSlowWorldGenSettingWarnings = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "If enabled, a chat message will be displayed when DH has too many chunks \n"
+ "queued for updating. \n"
+ "")
.build();
public static ConfigEntry<Boolean> showModCompatibilityWarningsOnStartup = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> showModCompatibilityWarningsOnStartup = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.comment("" .comment(""
@@ -1677,6 +1664,15 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Boolean> logGarbageCollectorWarning = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "If enabled, a message will be logged if the garbage \n"
+ "collector Java is currently using is known \n"
+ "to cause stutters and/or issues. \n"
+ "")
.build();
} }
} }
@@ -1746,32 +1742,6 @@ public class Config
"") "")
.build(); .build();
public static ConfigEntry<Integer> generationBoundsX = new ConfigEntry.Builder<Integer>()
.setChatCommandName("generation.bounds.x")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.setMinDefaultMax(Integer.MIN_VALUE, 0, Integer.MAX_VALUE)
.comment("" +
"Defines the X-coordinate of the central point for generation boundaries, in blocks. \n" +
"")
.build();
public static ConfigEntry<Integer> generationBoundsZ = new ConfigEntry.Builder<Integer>()
.setChatCommandName("generation.bounds.z")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.setMinDefaultMax(Integer.MIN_VALUE, 0, Integer.MAX_VALUE)
.comment("" +
"Defines the Z-coordinate of the central point for generation boundaries, in blocks. \n" +
"")
.build();
public static ConfigEntry<Integer> generationBoundsRadius = new ConfigEntry.Builder<Integer>()
.setChatCommandName("generation.bounds.radius")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.setMinDefaultMax(0, 0, Integer.MAX_VALUE)
.comment("" +
"Defines the radius around the central point within which generation is allowed, in blocks. \n" +
"If this value is set to 0, generation bounds are disabled." +
"")
.build();
// Real-time updates // Real-time updates
public static ConfigEntry<Boolean> enableRealTimeUpdates = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableRealTimeUpdates = new ConfigEntry.Builder<Boolean>()
@@ -1892,7 +1862,8 @@ public class Config
ThreadPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues(); ThreadPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
RenderQualityPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues(); RenderQualityPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
QuickRenderToggleConfigEventHandler.INSTANCE.setUiOnlyConfigValues(); QuickRenderToggleConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
QuickShowWorldGenProgressConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
IgnoredDimensionCsvHandler.INSTANCE.onConfigValueSet();
} }
catch (Exception e) catch (Exception e)
{ {
@@ -17,34 +17,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.seibel.distanthorizons.core.enums.worldGeneration; package com.seibel.distanthorizons.core.config.api.converters;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConverter;
/** /**
* MULTI_THREADED, <br> * Used to support deprecated config options that may be identical
* SINGLE_THREADED, <br> * in implementation but with the On/Off values flipped.
* SERVER_THREAD, <br>
* *
* @author James Seibel * @author James Seibel
* @version 7-25-2022 * @version 2025-12-22
*/ */
public enum EWorldGenThreadMode public class InvertedBoolConverter implements IConverter<Boolean, Boolean>
{ {
/**
* This world generator can be run on an unlimited number
* of concurrent threads.
*/
MULTI_THREADED,
/** @Override
* This world generator can only be run on one thread at public Boolean convertToCoreType(Boolean core)
* a time, however that thread can run concurrently { return !core; }
* to Minecraft's server thread.
*/ @Override
SINGLE_THREADED, public Boolean convertToApiType(Boolean api)
{ return !api; }
/**
* This world generator can only be run on Minecraft's
* server thread.
*/
SERVER_THREAD,
} }
@@ -0,0 +1,125 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.config.eventHandlers;
import com.seibel.distanthorizons.api.enums.config.EDhApiMcRenderingFadeMode;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiBeforeRenderEvent;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiCancelableEventParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.coreapi.util.StringUtil;
public class IgnoredDimensionCsvHandler extends DhApiBeforeRenderEvent implements IConfigListener
{
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public static IgnoredDimensionCsvHandler INSTANCE = new IgnoredDimensionCsvHandler();
private String[] dimensionNames = null;
//=============//
// constructor //
//=============//
/** private since we only ever need one handler at a time */
private IgnoredDimensionCsvHandler() { }
//=================//
// config handling //
//=================//
@Override
public void onConfigValueSet()
{
String ignoredDimensionCsvString = Config.Client.Advanced.Graphics.Experimental.ignoredDimensionCsv.get();
if (ignoredDimensionCsvString == null
|| ignoredDimensionCsvString.isEmpty())
{
LOGGER.info("Dimension ignoring disabled, DH will render all dimensions.");
this.dimensionNames = null;
}
else
{
try
{
this.dimensionNames = ignoredDimensionCsvString.split(",");
LOGGER.info("DH set to ignore dimensions: ["+ StringUtil.join(", ", this.dimensionNames)+"].");
}
catch (Exception e)
{
LOGGER.error("Failed to separate ignored dimensions from CSV string, error: ["+e.getMessage()+"].", e);
this.dimensionNames = null;
}
}
}
//===================//
// external handling //
//===================//
@Override
public void beforeRender(DhApiCancelableEventParam<DhApiRenderParam> event)
{
String dimName = event.value.clientLevelWrapper.getDimensionName();
if (IgnoredDimensionCsvHandler.INSTANCE.dimensionNameShouldBeIgnored(dimName))
{
event.cancelEvent();
Config.Client.Advanced.Graphics.Fog.enableVanillaFog.setApiValue(true);
Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.setApiValue(EDhApiMcRenderingFadeMode.NONE);
}
else
{
Config.Client.Advanced.Graphics.Fog.enableVanillaFog.setApiValue(null);
Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.setApiValue(null);
}
}
public boolean dimensionNameShouldBeIgnored(String dimName)
{
if (this.dimensionNames == null
|| this.dimensionNames.length == 0)
{
return false;
}
for (int i = 0; i < this.dimensionNames.length; i++)
{
String dimNameToIgnore = this.dimensionNames[i];
if (dimName.equalsIgnoreCase(dimNameToIgnore))
{
return true;
}
}
return false;
}
}
@@ -35,7 +35,7 @@ public class WorldCurvatureConfigEventHandler implements IConfigListener
{ {
public static WorldCurvatureConfigEventHandler INSTANCE = new WorldCurvatureConfigEventHandler(); public static WorldCurvatureConfigEventHandler INSTANCE = new WorldCurvatureConfigEventHandler();
private static final int MIN_VALID_CURVE_VALUE = 50; public static final int MIN_VALID_CURVE_VALUE = 50;
/** private since we only ever need one handler at a time */ /** private since we only ever need one handler at a time */
@@ -52,6 +52,11 @@ public class WorldCurvatureConfigEventHandler implements IConfigListener
// shouldn't update the UI, otherwise we may end up fighting the user // shouldn't update the UI, otherwise we may end up fighting the user
Config.Client.Advanced.Graphics.Experimental.earthCurveRatio.set(MIN_VALID_CURVE_VALUE); Config.Client.Advanced.Graphics.Experimental.earthCurveRatio.set(MIN_VALID_CURVE_VALUE);
} }
else if (curveRatio < 0 && curveRatio > -MIN_VALID_CURVE_VALUE)
{
// same as above, but in the negative direction
Config.Client.Advanced.Graphics.Experimental.earthCurveRatio.set(-MIN_VALID_CURVE_VALUE);
}
} }
@@ -1,66 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.config.eventHandlers.presets;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorProgressDisplayLocation;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
public class QuickShowWorldGenProgressConfigEventHandler
{
public static QuickShowWorldGenProgressConfigEventHandler INSTANCE = new QuickShowWorldGenProgressConfigEventHandler();
private final ConfigChangeListener<Boolean> quickChangeListener;
private final ConfigChangeListener<EDhApiDistantGeneratorProgressDisplayLocation> fullChangeListener;
/** private since we only ever need one handler at a time */
private QuickShowWorldGenProgressConfigEventHandler()
{
this.quickChangeListener = new ConfigChangeListener<>(Config.Client.quickShowWorldGenProgress,
(val) ->
{
boolean quickShowProgress = Config.Client.quickShowWorldGenProgress.get();
Config.Common.WorldGenerator.showGenerationProgress.set(
quickShowProgress
? EDhApiDistantGeneratorProgressDisplayLocation.OVERLAY
: EDhApiDistantGeneratorProgressDisplayLocation.DISABLED);
});
this.fullChangeListener = new ConfigChangeListener<>(Config.Common.WorldGenerator.showGenerationProgress,
(val) ->
{
boolean showProgress = Config.Common.WorldGenerator.showGenerationProgress.get() != EDhApiDistantGeneratorProgressDisplayLocation.DISABLED;
Config.Client.quickShowWorldGenProgress.setWithoutFiringEvents(showProgress);
});
}
/**
* Set the UI only config based on what is set in the file. <br>
* This should only be called once.
*/
public void setUiOnlyConfigValues()
{
boolean showProgress = Config.Common.WorldGenerator.showGenerationProgress.get() != EDhApiDistantGeneratorProgressDisplayLocation.DISABLED;
Config.Client.quickShowWorldGenProgress.set(showProgress);
}
}
@@ -24,12 +24,14 @@ import com.seibel.distanthorizons.core.config.ConfigHandler;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.config.types.AbstractConfigBase; import com.seibel.distanthorizons.core.config.types.AbstractConfigBase;
import com.seibel.distanthorizons.core.config.types.ConfigEntry; import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.jar.EPlatform;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -268,7 +268,7 @@ public class FullDataPointIdMap
// necessary to prevent issues with deserializing objects after the level has been closed // necessary to prevent issues with deserializing objects after the level has been closed
if (Thread.interrupted()) if (Thread.interrupted())
{ {
throw new InterruptedException(FullDataPointIdMap.class.getSimpleName() + " task interrupted."); throw new InterruptedException("[" + FullDataPointIdMap.class.getSimpleName() + "] deserializing interrupted.");
} }
@@ -44,6 +44,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import it.unimi.dsi.fastutil.bytes.ByteArrayList; import it.unimi.dsi.fastutil.bytes.ByteArrayList;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -92,8 +93,6 @@ public class FullDataSourceV2
public long lastModifiedUnixDateTime; public long lastModifiedUnixDateTime;
public long createdUnixDateTime; public long createdUnixDateTime;
public int levelMinY;
/** /**
* stores how far each column has been generated should start with {@link EDhApiWorldGenerationStep#EMPTY} * stores how far each column has been generated should start with {@link EDhApiWorldGenerationStep#EMPTY}
* *
@@ -122,7 +121,7 @@ public class FullDataSourceV2
public Boolean applyToChildren = null; public Boolean applyToChildren = null;
/** should only be used by methods exposed via the DH API */ /** should only be used by methods exposed via the DH API */
private boolean runApiChunkValidation = false; private boolean runApiSetterValidation = false;
@@ -203,8 +202,9 @@ public class FullDataSourceV2
public static FullDataSourceV2 createEmpty(long pos) public static FullDataSourceV2 createEmpty(long pos)
{ {
FullDataPointIdMap map = new FullDataPointIdMap(pos);
return new FullDataSourceV2( return new FullDataSourceV2(
pos, new FullDataPointIdMap(pos), pos, map,
// data points, genSteps, and columnCompression are all null since // data points, genSteps, and columnCompression are all null since
// nothing has been generated yet. // nothing has been generated yet.
// Using the default value of all 0's is adequate // Using the default value of all 0's is adequate
@@ -299,9 +299,9 @@ public class FullDataSourceV2
* returns {@link FullDataPointUtil#EMPTY_DATA_POINT} if the given {@link DhBlockPos} * returns {@link FullDataPointUtil#EMPTY_DATA_POINT} if the given {@link DhBlockPos}
* is outside this data source's boundaries. * is outside this data source's boundaries.
*/ */
public long getDataPointAtBlockPos(DhBlockPos blockPos) public long getDataPointAtBlockPos(int blockPosX, int blockPosY, int blockPosZ, int levelMinY)
{ {
DhLodPos requestedPos = new DhLodPos(LodUtil.BLOCK_DETAIL_LEVEL, blockPos.getX(), blockPos.getZ()); DhLodPos requestedPos = new DhLodPos(LodUtil.BLOCK_DETAIL_LEVEL, blockPosX, blockPosZ);
// stop if the requested blockPos is outside this datasource // stop if the requested blockPos is outside this datasource
{ {
@@ -331,6 +331,7 @@ public class FullDataSourceV2
// search for a datapoint that contains the given block y position // search for a datapoint that contains the given block y position
int relBlockPosY = blockPosY - levelMinY;
long dataPoint; long dataPoint;
for (int i = 0; i < dataColumn.size(); i++) for (int i = 0; i < dataColumn.size(); i++)
{ {
@@ -345,14 +346,13 @@ public class FullDataSourceV2
int requestedY = blockPos.getY(); int bottomY = FullDataPointUtil.getBottomY(dataPoint);
int bottomY = FullDataPointUtil.getBottomY(dataPoint) + this.levelMinY;
int height = FullDataPointUtil.getHeight(dataPoint); int height = FullDataPointUtil.getHeight(dataPoint);
int topY = bottomY + height; int topY = bottomY + height;
// does this datapoint contain the requested Y position? // does this datapoint contain the requested Y position?
if (bottomY <= requestedY if (bottomY <= relBlockPosY
&& requestedY < topY) // blockPositions start from the bottom of the block, thus "<=" for bottomY, just "<" for topY && relBlockPosY < topY) // blockPositions start from the bottom of the block, thus "<=" for bottomY, just "<" for topY
{ {
return dataPoint; return dataPoint;
} }
@@ -367,7 +367,7 @@ public class FullDataSourceV2
// updating // // updating //
//==========// //==========//
public boolean updateFromChunk(@NotNull FullDataSourceV2 inputDataSource) public boolean updateFromDataSource(@NotNull FullDataSourceV2 inputDataSource)
{ {
// don't try updating if the input is empty // don't try updating if the input is empty
if (inputDataSource.mapping.isEmpty()) if (inputDataSource.mapping.isEmpty())
@@ -470,7 +470,7 @@ public class FullDataSourceV2
return dataChanged; return dataChanged;
} }
public boolean updateFromSameDetailLevel(FullDataSourceV2 inputDataSource, int[] remappedIds) private boolean updateFromSameDetailLevel(FullDataSourceV2 inputDataSource, int[] remappedIds)
{ {
// both data sources should have the same detail level // both data sources should have the same detail level
if (DhSectionPos.getDetailLevel(inputDataSource.pos) != DhSectionPos.getDetailLevel(this.pos)) if (DhSectionPos.getDetailLevel(inputDataSource.pos) != DhSectionPos.getDetailLevel(this.pos))
@@ -486,10 +486,14 @@ public class FullDataSourceV2
for (int z = 0; z < WIDTH; z++) for (int z = 0; z < WIDTH; z++)
{ {
int index = relativePosToIndex(x, z); int index = relativePosToIndex(x, z);
LongArrayList inputDataArray = inputDataSource.dataPoints[index]; LongArrayList inputDataArray = inputDataSource.dataPoints[index];
if (inputDataArray != null) if (inputDataArray == null)
{ {
continue;
}
byte thisGenState = this.columnGenerationSteps.getByte(index); byte thisGenState = this.columnGenerationSteps.getByte(index);
byte inputGenState = inputDataSource.columnGenerationSteps.getByte(index); byte inputGenState = inputDataSource.columnGenerationSteps.getByte(index);
@@ -515,9 +519,13 @@ public class FullDataSourceV2
genStateAllowsUpdating = true; genStateAllowsUpdating = true;
} }
if (!genStateAllowsUpdating)
if (genStateAllowsUpdating)
{ {
continue;
}
// check if the data changed // check if the data changed
if (this.dataPoints[index] == null) if (this.dataPoints[index] == null)
{ {
@@ -570,12 +578,11 @@ public class FullDataSourceV2
this.isEmpty = false; this.isEmpty = false;
} }
} }
}
}
return dataChanged; return dataChanged;
} }
public boolean updateFromOneBelowDetailLevel(FullDataSourceV2 inputDataSource, int[] remappedIds)
private boolean updateFromOneBelowDetailLevel(FullDataSourceV2 inputDataSource, int[] remappedIds)
{ {
if (DhSectionPos.getDetailLevel(inputDataSource.pos) + 1 != DhSectionPos.getDetailLevel(this.pos)) if (DhSectionPos.getDetailLevel(inputDataSource.pos) + 1 != DhSectionPos.getDetailLevel(this.pos))
{ {
@@ -711,176 +718,221 @@ public class FullDataSourceV2
{ {
LongArrayList newColumnList = new LongArrayList(); 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; // get the 4 input columns //
byte lastSkyLight = 0; //=========================//
int height = 0;
int minY = 0;
LongArrayList[] inputColumns = new LongArrayList[4];
// 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; int colIndex = 0;
for (int inputX = x; inputX < x + 2; inputX++) for (int inputX = x; inputX < x + 2; inputX++)
{ {
for (int inputZ = z; inputZ < z + 2; inputZ++, colIndex++) 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... inputColumns[colIndex] = inputDataSource.dataPoints[relativePosToIndex(inputX, inputZ)];
LongArrayList inputDataArray = inputDataSource.dataPoints[relativePosToIndex(inputX, inputZ)]; if (inputColumns[colIndex] != null
if (inputDataArray == null || inputDataArray.size() == 0) && RUN_DATA_ORDER_VALIDATION)
{
throwIfDataColumnInWrongOrder(inputDataSource.pos, inputColumns[colIndex]);
}
}
}
//========================================//
// find all y levels where changes happen //
//========================================//
IntArrayList yTransitions = new IntArrayList();
for (int i = 0; i < 4; i++)
{
if (inputColumns[i] == null
|| inputColumns[i].isEmpty())
{ {
currentDatapointIndex[colIndex] = -1;
continue; continue;
} }
// determine the last index (the lowest data point) for each column for (int j = 0; j < inputColumns[i].size(); j++)
if (currentDatapointIndex[colIndex] == -2)
{ {
currentDatapointIndex[colIndex] = inputDataArray.size() - 1; long datapoint = inputColumns[i].getLong(j);
int minY = FullDataPointUtil.getBottomY(datapoint);
int maxY = minY + FullDataPointUtil.getHeight(datapoint);
if (RUN_DATA_ORDER_VALIDATION) if (!yTransitions.contains(minY))
{ {
throwIfDataColumnInWrongOrder(inputDataSource.pos, inputDataArray); yTransitions.add(minY);
}
} }
if (!yTransitions.contains(maxY))
int dataPointIndex = currentDatapointIndex[colIndex];
if (dataPointIndex == -1)
{ {
// went over the end yTransitions.add(maxY);
continue; }
}
} }
long datapoint = inputDataArray.getLong(dataPointIndex);
int datapointMinY = FullDataPointUtil.getBottomY(datapoint); // can happen if the columns are empty
int numbOfBlocksTall = FullDataPointUtil.getHeight(datapoint); if (yTransitions.isEmpty())
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 return newColumnList;
continue;
} }
else if (blockY >= datapointMaxY)
// sort the transitions from bottom to top // TODO
yTransitions.sort(null);
// create index trackers for each column,
// starting with the top-most datapoint
int[] currentIndices = new int[4];
for (int i = 0; i < 4; i++)
{ {
// this y-slice is above the current datapoint, if (inputColumns[i] != null
// try the next data point && !inputColumns[i].isEmpty())
int newDatapointIndex = currentDatapointIndex[colIndex] - 1;
if (newDatapointIndex < 0)
{ {
// went to far, no additional data present currentIndices[i] = inputColumns[i].size() - 1;
newDatapointIndex = -1;
} }
currentDatapointIndex[colIndex] = newDatapointIndex; else
{
currentIndices[i] = -1;
// try again with the next data point
inputZ--;
colIndex--;
continue;
}
datapointsForYSlice[colIndex] = datapoint;
} }
} }
//=======================//
// process each Y change //
//=======================//
int lastId = 0;
byte lastBlockLight = 0;
byte lastSkyLight = 0;
int currentMinY = yTransitions.getInt(0);
int accumulatedHeight = 0;
int[] mergeIds = new int[4];
int[] mergeBlockLights = new int[4];
int[] mergeSkyLights = new int[4];
for (int yIndex = 0; yIndex < yTransitions.size() - 1; yIndex++)
{
int sliceMinY = yTransitions.getInt(yIndex);
int sliceMaxY = yTransitions.getInt(yIndex + 1);
int sliceHeight = sliceMaxY - sliceMinY;
// Sample at the midpoint of this slice
int sampleY = sliceMinY + (sliceHeight / 2);
// Get data from each column at this Y level
Arrays.fill(mergeIds, 0); Arrays.fill(mergeIds, 0);
Arrays.fill(mergeBlockLights, 0); Arrays.fill(mergeBlockLights, 0);
Arrays.fill(mergeSkyLights, 0); Arrays.fill(mergeSkyLights, 0);
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
mergeIds[i] = FullDataPointUtil.getId(datapointsForYSlice[i]); // skip columns that are empty or where we have already reached the bottom
mergeBlockLights[i] = FullDataPointUtil.getBlockLight(datapointsForYSlice[i]); if (currentIndices[i] == -1)
mergeSkyLights[i] = FullDataPointUtil.getSkyLight(datapointsForYSlice[i]); {
continue;
} }
// determine the most common values for this slice LongArrayList column = inputColumns[i];
int id = determineMostValueInColumnSlice(mergeIds, inputDataSource.mapping); if (column == null)
{
continue;
}
// move the index down if we've passed the current datapoint
while (currentIndices[i] >= 0)
{
long datapoint = column.getLong(currentIndices[i]);
int inputMinY = FullDataPointUtil.getBottomY(datapoint);
int inputMaxY = inputMinY + FullDataPointUtil.getHeight(datapoint);
if (sampleY >= inputMaxY)
{
// Sample point is above this datapoint, move to next (lower) one
currentIndices[i]--;
}
else if (sampleY >= inputMinY
&& sampleY < inputMaxY)
{
// Sample point is within this datapoint
mergeIds[i] = FullDataPointUtil.getId(datapoint);
mergeBlockLights[i] = FullDataPointUtil.getBlockLight(datapoint);
mergeSkyLights[i] = FullDataPointUtil.getSkyLight(datapoint);
break;
}
else
{
// Sample point is below this datapoint
break;
}
}
}
// Determine merged values for this slice
int id = determineMostCommonValueInColumnSlice(mergeIds, inputDataSource.mapping);
byte blockLight = (byte) determineAverageValueInColumnSlice(mergeBlockLights); byte blockLight = (byte) determineAverageValueInColumnSlice(mergeBlockLights);
byte skyLight = (byte) determineAverageValueInColumnSlice(mergeSkyLights); byte skyLight = (byte) determineAverageValueInColumnSlice(mergeSkyLights);
// if this slice is different then the last one, create a new one // Check if we need to start a new datapoint
if (id != lastId if (accumulatedHeight == 0)
// block and sky light might not be necessary {
// first datapoint
lastId = id;
lastBlockLight = blockLight;
lastSkyLight = skyLight;
currentMinY = sliceMinY;
accumulatedHeight = sliceHeight;
}
else if (id != lastId
|| blockLight != lastBlockLight || blockLight != lastBlockLight
|| skyLight != lastSkyLight) || skyLight != lastSkyLight)
{ {
if (height != 0) // the data changed, create a new datapoint
{
try try
{ {
long datapoint = FullDataPointUtil.encode(lastId, height, minY, lastBlockLight, lastSkyLight); long datapoint = FullDataPointUtil.encode(lastId, accumulatedHeight, currentMinY, lastBlockLight, lastSkyLight);
newColumnList.add(datapoint); newColumnList.add(datapoint);
} }
catch (DataCorruptedException e) catch (DataCorruptedException e)
{ {
// shouldn't happen, (especially if validation is disabled) but just in case LOGGER.warn("Skipping corrupt datapoint for pos ["+DhSectionPos.toString(inputDataSource.pos)+"] at relative position ["+x+","+z+"] with data: ID["+lastId+"], Height["+accumulatedHeight+"], minY["+currentMinY+"], lastBlockLight["+lastBlockLight+"], lastSkyLight["+lastSkyLight+"].");
LOGGER.warn("Skipping corrupt datapoint for pos ["+DhSectionPos.toString(inputDataSource.pos)+"] at relative position ["+x+","+z+"] with data: ID["+lastId+"], Height["+height+"], minY["+minY+"], lastBlockLight["+lastBlockLight+"], lastSkyLight["+lastSkyLight+"].");
}
} }
// start the next datapoint
lastId = id; lastId = id;
lastBlockLight = blockLight; lastBlockLight = blockLight;
lastSkyLight = skyLight; lastSkyLight = skyLight;
height = 0; currentMinY = sliceMinY;
minY = blockY; accumulatedHeight = sliceHeight;
}
else
{
// this datapoint is the same as the last one,
// just extend it's height
accumulatedHeight += sliceHeight;
} }
} }
// add the last slice if present
if (height != 0) // add the final datapoint if needed
if (accumulatedHeight > 0)
{ {
try try
{ {
newColumnList.add(FullDataPointUtil.encode(lastId, height, minY, lastBlockLight, lastSkyLight)); newColumnList.add(FullDataPointUtil.encode(lastId, accumulatedHeight, currentMinY, lastBlockLight, lastSkyLight));
} }
catch (DataCorruptedException e) catch (DataCorruptedException e)
{ {
// shouldn't happen, (especially if validation is disabled) but just in case LOGGER.warn("Skipping corrupt datapoint for pos ["+DhSectionPos.toString(inputDataSource.pos)+"] at relative position ["+x+","+z+"] with data: ID["+lastId+"], Height["+accumulatedHeight+"], minY["+currentMinY+"], lastBlockLight["+lastBlockLight+"], lastSkyLight["+lastSkyLight+"].");
LOGGER.warn("Skipping corrupt datapoint for pos ["+DhSectionPos.toString(inputDataSource.pos)+"] at relative position ["+x+","+z+"] with data: ID["+lastId+"], Height["+height+"], minY["+minY+"], lastBlockLight["+lastBlockLight+"], lastSkyLight["+lastSkyLight+"].");
} }
} }
// flip the array if necessary // confirm the array is in the correct order
// 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); ensureDataColumnOrder(newColumnList);
return newColumnList; return newColumnList;
@@ -898,23 +950,8 @@ public class FullDataSourceV2
dataColumn.set(i, FullDataPointUtil.remap(remappedIds, dataColumn.getLong(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 */ /** @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) private static int determineMostCommonValueInColumnSlice(int[] sliceArray, @Nullable FullDataPointIdMap mapping)
{ {
if (RUN_UPDATE_DEV_VALIDATION) if (RUN_UPDATE_DEV_VALIDATION)
{ {
@@ -958,7 +995,7 @@ public class FullDataSourceV2
} }
} }
// return the most common occurance // return the most common occurrence
int maxCount = Math.max(count0, Math.max(count1, Math.max(count2, count3))); int maxCount = Math.max(count0, Math.max(count1, Math.max(count2, count3)));
if (maxCount == count0) if (maxCount == count0)
// if the max count is 1 then we'll just go with the first column // if the max count is 1 then we'll just go with the first column
@@ -1260,7 +1297,7 @@ public class FullDataSourceV2
// API methods // // API methods //
//=============// //=============//
public void setRunApiChunkValidation(boolean runValidation) { this.runApiChunkValidation = runValidation; } public void setRunApiSetterValidation(boolean runValidation) { this.runApiSetterValidation = runValidation; }
@Override @Override
public int getWidthInDataColumns() { return WIDTH; } public int getWidthInDataColumns() { return WIDTH; }
@@ -1271,13 +1308,13 @@ public class FullDataSourceV2
{ {
try try
{ {
LodDataBuilder.correctDataColumnOrder(columnDataPoints); LodDataBuilder.putListInTopDownOrder(columnDataPoints);
if (this.runApiChunkValidation) if (this.runApiSetterValidation)
{ {
LodDataBuilder.validateOrThrowApiDataColumn(columnDataPoints); LodDataBuilder.validateOrThrowApiDataColumn(columnDataPoints);
} }
LongArrayList packedDataPoints = LodDataBuilder.convertApiDataPointListToPackedLongArray(columnDataPoints, this, 0); LongArrayList packedDataPoints = LodDataBuilder.convertApiDataPointListToPackedLongArray(columnDataPoints, this, 0, true);
// TODO there should be an "unknown" compression and generation step, or be defined via the datapoints // TODO there should be an "unknown" compression and generation step, or be defined via the datapoints
this.setSingleColumn(packedDataPoints, relX, relZ, EDhApiWorldGenerationStep.SURFACE, EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS); this.setSingleColumn(packedDataPoints, relX, relZ, EDhApiWorldGenerationStep.SURFACE, EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS);
@@ -1300,7 +1337,7 @@ public class FullDataSourceV2
{ {
long datapoint = dataColumn.getLong(i); long datapoint = dataColumn.getLong(i);
DhApiTerrainDataPoint apiDataPoint = DhApiTerrainDataPointUtil.createApiDatapoint(this.levelMinY, this.mapping, DhSectionPos.getDetailLevel(this.pos), datapoint); DhApiTerrainDataPoint apiDataPoint = DhApiTerrainDataPointUtil.createApiDatapoint(0, this.mapping, DhSectionPos.getDetailLevel(this.pos), datapoint);
apiList.add(apiDataPoint); apiList.add(apiDataPoint);
} }
@@ -157,8 +157,8 @@ public final class BufferQuad
return false; return false;
// make sure these quads share the same perpendicular axis // make sure these quads share the same perpendicular axis
if ((mergeDirection == BufferMergeDirectionEnum.EastWest && this.y != quad.y) || if ((mergeDirection == BufferMergeDirectionEnum.EastWest && this.y != quad.y)
(mergeDirection == BufferMergeDirectionEnum.NorthSouthOrUpDown && this.x != quad.x)) || (mergeDirection == BufferMergeDirectionEnum.NorthSouthOrUpDown && this.x != quad.x))
{ {
return false; return false;
} }
@@ -24,19 +24,19 @@ import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListCheckout; import com.seibel.distanthorizons.core.pooling.PhantomArrayListCheckout;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListPool;
import com.seibel.distanthorizons.core.util.ColorUtil; import com.seibel.distanthorizons.core.util.ColorUtil;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.RenderDataPointUtil; import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import com.seibel.distanthorizons.coreapi.util.MathUtil; import com.seibel.distanthorizons.coreapi.util.MathUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class ColumnBox public class ColumnBox
{ {
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
/** /**
* if the skylight has this value that means * if the skylight has this value that means
@@ -46,7 +46,6 @@ public class ColumnBox
//=========// //=========//
// builder // // builder //
//=========// //=========//
@@ -124,7 +123,7 @@ public class ColumnBox
&& !isTopTransparent; && !isTopTransparent;
if (!skipTop) if (!skipTop)
{ {
builder.addQuadUp(minX, maxY, minZ, width, width, ColorUtil.applyShade(color, MC.getShade(EDhDirection.UP)), irisBlockMaterialId, skyLightTop, blockLight); builder.addQuadUp(minX, maxY, minZ, width, width, ColorUtil.applyShade(color, MC_RENDER.getShade(EDhDirection.UP)), irisBlockMaterialId, skyLightTop, blockLight);
} }
} }
@@ -135,7 +134,7 @@ public class ColumnBox
&& !isBottomTransparent; && !isBottomTransparent;
if (!skipBottom) if (!skipBottom)
{ {
builder.addQuadDown(minX, minY, minZ, width, width, ColorUtil.applyShade(color, MC.getShade(EDhDirection.DOWN)), irisBlockMaterialId, skyLightBot, blockLight); builder.addQuadDown(minX, minY, minZ, width, width, ColorUtil.applyShade(color, MC_RENDER.getShade(EDhDirection.DOWN)), irisBlockMaterialId, skyLightBot, blockLight);
} }
} }
@@ -265,7 +264,7 @@ public class ColumnBox
// no adjacent data // // no adjacent data //
//==================// //==================//
color = ColorUtil.applyShade(color, MC.getShade(direction)); color = ColorUtil.applyShade(color, MC_RENDER.getShade(direction));
if (adjColumnView.size == 0 if (adjColumnView.size == 0
|| RenderDataPointUtil.hasZeroHeight(adjColumnView.get(0))) || RenderDataPointUtil.hasZeroHeight(adjColumnView.get(0)))
@@ -311,7 +310,8 @@ public class ColumnBox
long adjBelowPoint = (adjIndex + 1 < adjCount) ? adjColumnView.get(adjIndex + 1) : RenderDataPointUtil.EMPTY_DATA; long adjBelowPoint = (adjIndex + 1 < adjCount) ? adjColumnView.get(adjIndex + 1) : RenderDataPointUtil.EMPTY_DATA;
boolean adjOverVoid = !RenderDataPointUtil.doesDataPointExist(adjBelowPoint); boolean adjOverVoid = !RenderDataPointUtil.doesDataPointExist(adjBelowPoint);
boolean adjTransparent = !adjOverVoid boolean adjTransparent =
!adjOverVoid
&& RenderDataPointUtil.getAlpha(adjPoint) < 255 && RenderDataPointUtil.getAlpha(adjPoint) < 255
&& transparencyEnabled; && transparencyEnabled;
@@ -107,12 +107,13 @@ public class LodBufferContainer implements AutoCloseable
// upload on MC's render thread // upload on MC's render thread
GLProxy.getInstance().queueRunningOnRenderThread(() -> GLProxy.queueRunningOnRenderThread(() ->
{ {
try try
{ {
// skip this event if requested // skip this event if requested
if (Thread.interrupted() || this.uploadFuture.isCancelled()) if (Thread.interrupted()
|| this.uploadFuture.isCancelled())
{ {
throw new InterruptedException(); throw new InterruptedException();
} }
@@ -294,7 +295,7 @@ public class LodBufferContainer implements AutoCloseable
{ {
this.buffersUploaded = false; this.buffersUploaded = false;
GLProxy.getInstance().queueRunningOnRenderThread(() -> GLProxy.queueRunningOnRenderThread(() ->
{ {
for (GLVertexBuffer buffer : this.vbos) for (GLVertexBuffer buffer : this.vbos)
{ {
@@ -32,6 +32,7 @@ import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.util.ColorUtil; import com.seibel.distanthorizons.core.util.ColorUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.coreapi.util.MathUtil; import com.seibel.distanthorizons.coreapi.util.MathUtil;
import org.lwjgl.system.MemoryUtil; import org.lwjgl.system.MemoryUtil;
@@ -44,7 +45,7 @@ import org.lwjgl.system.MemoryUtil;
public class LodQuadBuilder public class LodQuadBuilder
{ {
private static final DhLogger LOGGER = new DhLoggerBuilder().build(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private final ArrayList<BufferQuad>[] opaqueQuads = (ArrayList<BufferQuad>[]) new ArrayList[6]; private final ArrayList<BufferQuad>[] opaqueQuads = (ArrayList<BufferQuad>[]) new ArrayList[6];
@@ -148,8 +149,18 @@ public class LodQuadBuilder
throw new IllegalArgumentException("addQuadAdj() is only for adj direction! Not UP or Down!"); throw new IllegalArgumentException("addQuadAdj() is only for adj direction! Not UP or Down!");
} }
ArrayList<BufferQuad> quadList;
if (this.doTransparency && ColorUtil.getAlpha(color) < 255)
{
quadList = this.transparentQuads[dir.ordinal()];
}
else
{
quadList = this.opaqueQuads[dir.ordinal()];
}
BufferQuad quad = new BufferQuad(x, y, z, widthEastWest, widthNorthSouthOrUpDown, color, irisBlockMaterialId, skyLight, blockLight, dir); BufferQuad quad = new BufferQuad(x, y, z, widthEastWest, widthNorthSouthOrUpDown, color, irisBlockMaterialId, skyLight, blockLight, dir);
ArrayList<BufferQuad> quadList = (this.doTransparency && ColorUtil.getAlpha(color) < 255) ? this.transparentQuads[dir.ordinal()] : this.opaqueQuads[dir.ordinal()];
if (!quadList.isEmpty() if (!quadList.isEmpty()
&& ( && (
quadList.get(quadList.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.EastWest) quadList.get(quadList.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.EastWest)
@@ -369,7 +380,7 @@ public class LodQuadBuilder
// for horizontal and bottom faces of grass blocks, use the dirt color to // for horizontal and bottom faces of grass blocks, use the dirt color to
// prevent green cliff walls // prevent green cliff walls
color = this.clientLevelWrapper.getDirtBlockColor(); color = this.clientLevelWrapper.getDirtBlockColor();
color = ColorUtil.applyShade(color, MC.getShade(quad.direction)); color = ColorUtil.applyShade(color, MC_RENDER.getShade(quad.direction));
} }
} }
} }
@@ -388,31 +399,33 @@ public class LodQuadBuilder
} }
private void putVertex(ByteBuffer bb, short x, short y, short z, int color, byte normalIndex, byte irisBlockMaterialId, byte skylight, byte blocklight, int mx, int my, int mz) private void putVertex(ByteBuffer bb, short x, short y, short z, int color, byte normalIndex, byte irisBlockMaterialId, byte skylight, byte blocklight, int mx, int my, int mz)
{ {
skylight %= 16;
blocklight %= 16;
bb.putShort(x); bb.putShort(x);
bb.putShort(y); bb.putShort(y);
bb.putShort(z); bb.putShort(z);
short meta = 0; short meta = 0;
meta |= (skylight | (blocklight << 4)); {
byte mirco = 0; skylight %= 16;
blocklight %= 16;
meta |= (short) (skylight | (blocklight << 4));
byte mircoOffset = 0;
// mirco offset which is a xyz 2bit value // mirco offset which is a xyz 2bit value
// 0b00 = no offset // 0b00 = no offset
// 0b01 = positive offset // 0b01 = positive offset
// 0b11 = negative offset // 0b11 = negative offset
// format is: 0b00zzyyxx // format is: 0b00zzyyxx
if (mx != 0) mirco |= mx > 0 ? 0b01 : 0b11; if (mx != 0) { mircoOffset |= (byte) (mx > 0 ? 0b01 : 0b11); }
if (my != 0) mirco |= my > 0 ? 0b0100 : 0b1100; if (my != 0) { mircoOffset |= (byte) (my > 0 ? 0b0100 : 0b1100); }
if (mz != 0) mirco |= mz > 0 ? 0b010000 : 0b110000; if (mz != 0) { mircoOffset |= (byte) (mz > 0 ? 0b010000 : 0b110000); }
meta |= mirco << 8; meta |= (short) (mircoOffset << 8);
}
bb.putShort(meta); bb.putShort(meta);
byte r = (byte) ColorUtil.getRed(color); byte r = (byte) ColorUtil.getRed(color);
byte g = (byte) ColorUtil.getGreen(color); byte g = (byte) ColorUtil.getGreen(color);
byte b = (byte) ColorUtil.getBlue(color); byte b = (byte) ColorUtil.getBlue(color);
byte a = this.doTransparency ? (byte) ColorUtil.getAlpha(color) : (byte) 255; // TODO should this be called here or happen somewhere else? byte a = this.doTransparency ? (byte) ColorUtil.getAlpha(color) : (byte) 255;
bb.put(r); bb.put(r);
bb.put(g); bb.put(g);
bb.put(b); bb.put(b);
@@ -31,11 +31,9 @@ import com.seibel.distanthorizons.core.pooling.PhantomArrayListCheckout;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListPool; import com.seibel.distanthorizons.core.pooling.PhantomArrayListPool;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPosMutable; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPosMutable;
import com.seibel.distanthorizons.core.render.LodQuadTree;
import com.seibel.distanthorizons.core.util.*; import com.seibel.distanthorizons.core.util.*;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
@@ -54,11 +52,11 @@ public class FullDataToRenderDataTransformer
private static final DhLogger LOGGER = new DhLoggerBuilder().build(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class); private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final LongOpenHashSet brokenPos = new LongOpenHashSet(); private static final LongOpenHashSet BROKEN_POS_SET = new LongOpenHashSet();
private static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Data Transformer");
public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Data Transformer"); private static HashSet<IBlockStateWrapper> snowLayerBlockStates = null;
@@ -198,6 +196,16 @@ public class FullDataToRenderDataTransformer
HashSet<IBlockStateWrapper> blockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredBlocks(levelWrapper); HashSet<IBlockStateWrapper> blockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredBlocks(levelWrapper);
HashSet<IBlockStateWrapper> caveBlockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredCaveBlocks(levelWrapper); HashSet<IBlockStateWrapper> caveBlockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredCaveBlocks(levelWrapper);
// build snow block cache if needed
if (snowLayerBlockStates == null)
{
snowLayerBlockStates = new HashSet<>();
// ignore snow layers 1-3, everything above should be considered a full block
snowLayerBlockStates.add(WRAPPER_FACTORY.deserializeBlockStateWrapperOrGetDefault("minecraft:snow_STATE_{layers:1}", levelWrapper));
snowLayerBlockStates.add(WRAPPER_FACTORY.deserializeBlockStateWrapperOrGetDefault("minecraft:snow_STATE_{layers:2}", levelWrapper));
snowLayerBlockStates.add(WRAPPER_FACTORY.deserializeBlockStateWrapperOrGetDefault("minecraft:snow_STATE_{layers:3}", levelWrapper));
}
int caveCullingMaxY = Config.Client.Advanced.Graphics.Culling.caveCullingHeight.get() - levelWrapper.getMinHeight(); int caveCullingMaxY = Config.Client.Advanced.Graphics.Culling.caveCullingHeight.get() - levelWrapper.getMinHeight();
boolean caveCullingEnabled = boolean caveCullingEnabled =
Config.Client.Advanced.Graphics.Culling.enableCaveCulling.get() Config.Client.Advanced.Graphics.Culling.enableCaveCulling.get()
@@ -252,9 +260,9 @@ public class FullDataToRenderDataTransformer
} }
catch (IndexOutOfBoundsException e) catch (IndexOutOfBoundsException e)
{ {
if (!brokenPos.contains(fullDataMapping.getPos())) if (!BROKEN_POS_SET.contains(fullDataMapping.getPos()))
{ {
brokenPos.add(fullDataMapping.getPos()); BROKEN_POS_SET.add(fullDataMapping.getPos());
String levelId = levelWrapper.getDhIdentifier(); String levelId = levelWrapper.getDhIdentifier();
LOGGER.warn("Unable to get data point with id ["+id+"] " + LOGGER.warn("Unable to get data point with id ["+id+"] " +
"(Max possible ID: ["+fullDataMapping.getMaxValidId()+"]) " + "(Max possible ID: ["+fullDataMapping.getMaxValidId()+"]) " +
@@ -324,10 +332,26 @@ public class FullDataToRenderDataTransformer
// non-solid block check // // non-solid block check //
//=======================// //=======================//
if (ignoreNonCollidingBlocks boolean ignoreNonSolidBlock =
ignoreNonCollidingBlocks
&& !block.isSolid() && !block.isSolid()
&& !block.isLiquid() && !block.isLiquid()
&& block.getOpacity() != LodUtil.BLOCK_FULLY_OPAQUE) && block.getOpacity() != LodUtil.BLOCK_FULLY_OPAQUE;
// merge snow into the block below it
if (snowLayerBlockStates.contains(block))
{
// sometimes a snow datapoint will be multiple blocks tall,
// in that case we just want to drop the top by 1
blockHeight -= 1;
if (blockHeight == 0)
{
// this snow block was entirely removed, just color the block below it
ignoreNonSolidBlock = true;
}
}
if (ignoreNonSolidBlock)
{ {
if (colorBelowWithAvoidedBlocks) if (colorBelowWithAvoidedBlocks)
{ {
@@ -74,7 +74,7 @@ public class LodDataBuilder
long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ); long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ);
FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos); FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos);
dataSource.isEmpty = false; dataSource.isEmpty = false; // this will be set to "true" if any blocks are found
// chunk updates always propagate up // chunk updates always propagate up
dataSource.applyToParent = true; dataSource.applyToParent = true;
@@ -244,6 +244,15 @@ public class LodDataBuilder
blockLight = newBlockLight; blockLight = newBlockLight;
skyLight = newSkyLight; skyLight = newSkyLight;
lastY = y; lastY = y;
// mark the data source as non-empty if we find any non-air blocks
if (dataSource.isEmpty
&& newBlockState != null
&& !newBlockState.isAir())
{
dataSource.isEmpty = false;
}
} }
} }
@@ -261,7 +270,6 @@ public class LodDataBuilder
return null; return null;
} }
LodUtil.assertTrue(!dataSource.isEmpty);
return dataSource; return dataSource;
} }
@@ -289,13 +297,29 @@ public class LodDataBuilder
for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++) for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++)
{ {
List<DhApiTerrainDataPoint> columnDataPoints = apiChunk.getDataPoints(relBlockX, relBlockZ); List<DhApiTerrainDataPoint> columnDataPoints = apiChunk.getDataPoints(relBlockX, relBlockZ);
LodDataBuilder.correctDataColumnOrder(columnDataPoints);
// mark the data source as non-empty if we find any non-air blocks
if (dataSource.isEmpty)
{
for (int i = 0; i < columnDataPoints.size(); i++)
{
DhApiTerrainDataPoint dataPoint = columnDataPoints.get(i);
if (dataPoint.blockStateWrapper != null
&& !dataPoint.blockStateWrapper.isAir())
{
dataSource.isEmpty = false;
break;
}
}
}
LodDataBuilder.putListInTopDownOrder(columnDataPoints);
if (runAdditionalValidation) if (runAdditionalValidation)
{ {
validateOrThrowApiDataColumn(columnDataPoints); validateOrThrowApiDataColumn(columnDataPoints);
} }
LongArrayList packedDataPoints = convertApiDataPointListToPackedLongArray(columnDataPoints, dataSource, apiChunk.bottomYBlockPos); LongArrayList packedDataPoints = convertApiDataPointListToPackedLongArray(columnDataPoints, dataSource, apiChunk.bottomYBlockPos, runAdditionalValidation);
// TODO add the ability for API users to define a different compression mode // TODO add the ability for API users to define a different compression mode
// or add a "unkown" compression mode // or add a "unkown" compression mode
@@ -303,7 +327,6 @@ public class LodDataBuilder
packedDataPoints, packedDataPoints,
relBlockX + relSourceBlockX, relBlockZ + relSourceBlockZ, relBlockX + relSourceBlockX, relBlockZ + relSourceBlockZ,
EDhApiWorldGenerationStep.LIGHT, EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS); EDhApiWorldGenerationStep.LIGHT, EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS);
dataSource.isEmpty = false;
} }
} }
return dataSource; return dataSource;
@@ -317,41 +340,97 @@ public class LodDataBuilder
/** @see FullDataPointUtil */ /** @see FullDataPointUtil */
public static LongArrayList convertApiDataPointListToPackedLongArray( public static LongArrayList convertApiDataPointListToPackedLongArray(
@Nullable List<DhApiTerrainDataPoint> columnDataPoints, FullDataSourceV2 dataSource, @Nullable List<DhApiTerrainDataPoint> topDownColumnDataPoints, FullDataSourceV2 dataSource,
int bottomYBlockPos) throws DataCorruptedException int bottomYBlockPos, boolean runAdditionalValidation) throws DataCorruptedException
{ {
// this null check does 2 nice things at the same time: if (topDownColumnDataPoints == null
// if columnDataPoints is null, || topDownColumnDataPoints.size() == 0)
// then packedDataPoints will be of length 0
// AND the below loop won't run.
int size = (columnDataPoints != null) ? columnDataPoints.size() : 0;
// TODO make missing air LODs
// TODO merge duplicate datapoints
LongArrayList packedDataPoints = new LongArrayList(new long[size]);
for (int index = 0; index < size; index++)
{ {
DhApiTerrainDataPoint dataPoint = columnDataPoints.get(index); return new LongArrayList(0);
}
int id = dataSource.mapping.addIfNotPresentAndGetId(
(IBiomeWrapper) (dataPoint.biomeWrapper),
(IBlockStateWrapper) (dataPoint.blockStateWrapper) // array to store data
int size = topDownColumnDataPoints.size();
LongArrayList packedDataPoints = new LongArrayList(size);
packedDataPoints.clear();
if (runAdditionalValidation)
{
// check for missing data
int lastTopY = Integer.MAX_VALUE;
for (int i = 0; i < size; i++)
{
DhApiTerrainDataPoint apiDataPoint = topDownColumnDataPoints.get(i);
if (lastTopY != apiDataPoint.topYBlockPos
// the first index won't have a lastTopY value
&& i != 0)
{
throw new DataCorruptedException("LOD data has a gap between ["+lastTopY+"] and ["+apiDataPoint.bottomYBlockPos+"]. Empty areas should be filled with air datapoints so light propagates correctly.");
}
lastTopY = apiDataPoint.bottomYBlockPos;
}
}
// go through data from top down
long lastDataPoint = FullDataPointUtil.EMPTY_DATA_POINT;
for (int i = 0; i < size; i++)
{
DhApiTerrainDataPoint apiDataPoint = topDownColumnDataPoints.get(i);
int thisId = dataSource.mapping.addIfNotPresentAndGetId(
(IBiomeWrapper) (apiDataPoint.biomeWrapper),
(IBlockStateWrapper) (apiDataPoint.blockStateWrapper)
); );
int thisHeight = (apiDataPoint.topYBlockPos - apiDataPoint.bottomYBlockPos);
packedDataPoints.set(index, FullDataPointUtil.encode( int lastId = FullDataPointUtil.getId(lastDataPoint);
id, byte lastBlockLight = (byte)FullDataPointUtil.getBlockLight(lastDataPoint);
dataPoint.topYBlockPos - dataPoint.bottomYBlockPos, byte lastSkyLight = (byte)FullDataPointUtil.getSkyLight(lastDataPoint);
dataPoint.bottomYBlockPos - bottomYBlockPos,
(byte) (dataPoint.blockLightLevel),
(byte) (dataPoint.skyLightLevel) // if the ID and light are the same, merge the height
)); if (thisId == lastId
&& apiDataPoint.blockLightLevel == lastBlockLight
&& apiDataPoint.skyLightLevel == lastSkyLight
// the first index should always be added to the list
&& i != 0 )
{
// add adjacent height
int lastHeight = FullDataPointUtil.getHeight(lastDataPoint);
int newHeight = (lastHeight + thisHeight);
lastDataPoint = FullDataPointUtil.setHeight(lastDataPoint, newHeight);
// subtract bottom Y
int lastBottomY = FullDataPointUtil.getBottomY(lastDataPoint);
int newBottomY = lastBottomY - thisHeight;
lastDataPoint = FullDataPointUtil.setBottomY(lastDataPoint, newBottomY);
packedDataPoints.set(packedDataPoints.size()-1, lastDataPoint);
}
else
{
// data changed, create a new datapoint
long dataPoint = FullDataPointUtil.encode(
thisId,
thisHeight,
apiDataPoint.bottomYBlockPos - bottomYBlockPos,
(byte) (apiDataPoint.blockLightLevel),
(byte) (apiDataPoint.skyLightLevel)
);
lastDataPoint = dataPoint;
packedDataPoints.add(dataPoint);
}
} }
return packedDataPoints; return packedDataPoints;
} }
/** also corrects the order if it's backwards */ public static void putListInTopDownOrder(List<DhApiTerrainDataPoint> dataPoints)
public static void correctDataColumnOrder(List<DhApiTerrainDataPoint> dataPoints)
{ {
// order doesn't need to be checked if there is 0 or 1 items // order doesn't need to be checked if there is 0 or 1 items
if (dataPoints.size() > 1) if (dataPoints.size() > 1)
@@ -0,0 +1,12 @@
package com.seibel.distanthorizons.core.enums;
/**
* TODO
* might be deprecated in the future? in that case we'll probably want a wrapper
* function to handle colors for new MC versions
*
* source: https://minecraft.wiki/w/Formatting_codes
*/
public class EMinecraftColor
{
}
@@ -96,7 +96,12 @@ public class DelayedFullDataSourceSaveCache implements AutoCloseable
// no data currently in the memory cache for this position // no data currently in the memory cache for this position
memoryDataSource = FullDataSourceV2.createEmpty(inputPos); memoryDataSource = FullDataSourceV2.createEmpty(inputPos);
pair = new DataSourceSavedTimePair(memoryDataSource); pair = new DataSourceSavedTimePair(memoryDataSource);
this.dataSourceByPosition.put(inputPos, pair); DataSourceSavedTimePair oldPair = this.dataSourceByPosition.put(inputPos, pair);
if (oldPair != null)
{
// shouldn't happen, but just in case
this.handleDataSourceRemoval(oldPair.dataSource);
}
} }
else else
{ {
@@ -104,7 +109,7 @@ public class DelayedFullDataSourceSaveCache implements AutoCloseable
} }
// write the new data into memory // write the new data into memory
memoryDataSource.updateFromChunk(inputDataSource); memoryDataSource.updateFromDataSource(inputDataSource);
// keep track of when the last time we saved something was // keep track of when the last time we saved something was
pair.updateLastWrittenTimestamp(); pair.updateLastWrittenTimestamp();
} }
@@ -28,8 +28,8 @@ import com.seibel.distanthorizons.core.file.fullDatafile.V2.FullDataUpdatePropag
import com.seibel.distanthorizons.core.file.structure.ISaveStructure; import com.seibel.distanthorizons.core.file.structure.ISaveStructure;
import com.seibel.distanthorizons.core.generation.DhLightingEngine; import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue; import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue;
import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; import com.seibel.distanthorizons.core.generation.tasks.ERetrievalResultState;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
@@ -38,15 +38,17 @@ import com.seibel.distanthorizons.core.pooling.PhantomArrayListPool;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.util.ExceptionUtil;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker; import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import it.unimi.dsi.fastutil.bytes.ByteArrayList; import it.unimi.dsi.fastutil.bytes.ByteArrayList;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@@ -68,6 +70,8 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
*/ */
public static final int MAX_WORLD_GEN_REQUESTS_PER_THREAD = 20; public static final int MAX_WORLD_GEN_REQUESTS_PER_THREAD = 20;
public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Generated Provider");
private final AtomicReference<IFullDataSourceRetrievalQueue> worldGenQueueRef = new AtomicReference<>(null); private final AtomicReference<IFullDataSourceRetrievalQueue> worldGenQueueRef = new AtomicReference<>(null);
private final ArrayList<IOnWorldGenCompleteListener> onWorldGenTaskCompleteListeners = new ArrayList<>(); private final ArrayList<IOnWorldGenCompleteListener> onWorldGenTaskCompleteListeners = new ArrayList<>();
@@ -80,17 +84,10 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// constructor // // constructor //
//=============// //=============//
public GeneratedFullDataSourceProvider(IDhLevel level, ISaveStructure saveStructure) { super(level, saveStructure); } public GeneratedFullDataSourceProvider(IDhLevel level, ISaveStructure saveStructure) throws SQLException, IOException
public GeneratedFullDataSourceProvider(IDhLevel level, ISaveStructure saveStructure, @Nullable File saveDirOverride) { this(level, saveStructure, null); }
{ public GeneratedFullDataSourceProvider(IDhLevel level, ISaveStructure saveStructure, @Nullable File saveDirOverride) throws SQLException, IOException
super(level, saveStructure, saveDirOverride); { super(level, saveStructure, saveDirOverride); }
this.addDataSourceUpdateListener((@NotNull FullDataSourceV2 updatedData) ->
{
this.onWorldGenTaskComplete(WorldGenResult.CreateSuccess(updatedData.getPos()), null);
});
}
@@ -119,32 +116,47 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// events // // events //
//========// //========//
private void onWorldGenTaskComplete(WorldGenResult genTaskResult, Throwable exception) private void onWorldGenTaskComplete(DataSourceRetrievalResult genTaskResult, Throwable exception)
{
try
{ {
if (exception != null) 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; return;
} }
if (genTaskResult.state == ERetrievalResultState.FAIL)
{
LodUtil.assertTrue(genTaskResult.dataSource == null, "Errored retrieval object should not have a datasource.");
// don't log shutdown exceptions
if (!ExceptionUtil.isInterruptOrReject(exception))
{
LOGGER.error("Uncaught Gen Task Exception at [" + genTaskResult.pos + "], error: [" + exception.getMessage() + "].", exception);
}
}
else if (genTaskResult.state == ERetrievalResultState.SUCCESS)
{
LodUtil.assertTrue(genTaskResult.dataSource != null, "Successful retrieval object should have a datasource.");
this.dataUpdater.updateDataSource(genTaskResult.dataSource);
this.fireOnGenPosSuccessListeners(genTaskResult.pos);
genTaskResult.dataSource.close();
}
else if (genTaskResult.state == ERetrievalResultState.REQUIRES_SPLITTING)
{
// task was split
LodUtil.assertTrue(genTaskResult.dataSource == null, "Split retrieval object should not have a datasource.");
}
else else
{ {
// generation didn't complete // shouldn't happen, but just in case
LOGGER.debug("Gen Task Failed at " + genTaskResult.pos); LOGGER.warn("Unexpected gen Task state at: [" + DhSectionPos.toString(genTaskResult.pos) + "], state: [" + genTaskResult.state + "], datasource: NULL, exception: NULL.");
} }
}
catch (Exception e)
// if the generation task was split up into smaller positions, add the on-complete event to them
for (CompletableFuture<WorldGenResult> siblingFuture : genTaskResult.childFutures)
{ {
siblingFuture.whenComplete((siblingGenTaskResult, siblingEx) -> this.onWorldGenTaskComplete(siblingGenTaskResult, siblingEx)); LOGGER.error("Unexpected issue during onWorldGenTaskComplete, error: ["+e.getMessage()+"].", e);
} }
} }
@@ -204,10 +216,10 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
} }
@Override @Override
public boolean canQueueRetrieval() { return this.canQueueRetrieval(false); } public boolean canQueueRetrievalNow() { return this.canQueueRetrievalNow(false); }
public boolean canQueueRetrieval(boolean pruneWaitingTasksAboveLimit) public boolean canQueueRetrievalNow(boolean pruneWaitingTasksAboveLimit)
{ {
if (!super.canQueueRetrieval()) if (!super.canQueueRetrievalNow())
{ {
return false; return false;
} }
@@ -267,12 +279,16 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
int availableTaskSlots = maxWorldGenQueueCount - worldGenQueue.getWaitingTaskCount(); int availableTaskSlots = maxWorldGenQueueCount - worldGenQueue.getWaitingTaskCount();
if (availableTaskSlots <= 0) if (availableTaskSlots == 0)
{
return false;
}
else if (availableTaskSlots < 0)
{ {
if (pruneWaitingTasksAboveLimit) if (pruneWaitingTasksAboveLimit)
{ {
AtomicInteger tasksToCancel = new AtomicInteger(-availableTaskSlots + 1); AtomicInteger tasksToCancel = new AtomicInteger(availableTaskSlots * -1);
worldGenQueue.removeRetrievalRequestIf(x -> tasksToCancel.getAndDecrement() > 0); worldGenQueue.removeRetrievalRequestIf(taskPos -> tasksToCancel.getAndDecrement() > 0);
} }
else else
{ {
@@ -285,7 +301,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
} }
@Override @Override
public CompletableFuture<WorldGenResult> queuePositionForRetrieval(Long genPos) public CompletableFuture<DataSourceRetrievalResult> queuePositionForRetrieval(Long genPos)
{ {
IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get(); IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get();
if (worldGenQueue == null) if (worldGenQueue == null)
@@ -293,13 +309,8 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
return null; return null;
} }
WorldGenTaskTracker genTaskTracker = new WorldGenTaskTracker(genPos); CompletableFuture<DataSourceRetrievalResult> worldGenFuture = worldGenQueue.submitRetrievalTask(genPos, (byte) (DhSectionPos.getDetailLevel(genPos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL));
CompletableFuture<WorldGenResult> worldGenFuture = worldGenQueue.submitRetrievalTask(genPos, (byte) (DhSectionPos.getDetailLevel(genPos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL), genTaskTracker); worldGenFuture.whenComplete(this::onWorldGenTaskComplete);
worldGenFuture.whenComplete((genTaskResult, ex) ->
{
//LOGGER.info("gen task complete ["+DhSectionPos.toString(genPos)+"]");
//this.onWorldGenTaskComplete(genTaskResult, ex);
});
return worldGenFuture; return worldGenFuture;
} }
@@ -318,22 +329,20 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
public void clearRetrievalQueue() { this.worldGenQueueRef.set(null); } public void clearRetrievalQueue() { this.worldGenQueueRef.set(null); }
public boolean isFullyGenerated(ByteArrayList columnGenerationSteps) public boolean generationStepsAreFullyGenerated(ByteArrayList columnGenerationSteps)
{ {
return IntStream.range(0, columnGenerationSteps.size()) return IntStream.range(0, columnGenerationSteps.size())
.noneMatch(i -> .noneMatch((int intValue) ->
{ {
byte value = columnGenerationSteps.getByte(i); byte value = columnGenerationSteps.getByte(intValue);
return value == EDhApiWorldGenerationStep.EMPTY.value return value == EDhApiWorldGenerationStep.EMPTY.value
|| value == EDhApiWorldGenerationStep.DOWN_SAMPLED.value; || value == EDhApiWorldGenerationStep.DOWN_SAMPLED.value;
}); });
} }
public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Generated Provider");
@Override @Override
public LongArrayList getPositionsToRetrieve(Long pos) public LongArrayList getPositionsToRetrieve(long pos)
{ {
IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get(); IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get();
if (worldGenQueue == null) if (worldGenQueue == null)
@@ -349,7 +358,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
{ {
ByteArrayList columnGenStepArray = checkout.getByteArray(0, FullDataSourceV2.WIDTH*FullDataSourceV2.WIDTH); ByteArrayList columnGenStepArray = checkout.getByteArray(0, FullDataSourceV2.WIDTH*FullDataSourceV2.WIDTH);
this.repo.getColumnGenerationStepForPos(pos, columnGenStepArray); this.repo.getColumnGenerationStepForPos(pos, columnGenStepArray);
if (!columnGenStepArray.isEmpty()) if (columnGenStepArray.size() != 0)
{ {
boolean positionFullyGenerated = true; boolean positionFullyGenerated = true;
@@ -375,7 +384,6 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// this section is missing one or more columns, queue the missing ones for generation. // this section is missing one or more columns, queue the missing ones for generation.
// TODO speed up this logic by only checking ungenerated columns
LongArrayList generationList = new LongArrayList(); LongArrayList generationList = new LongArrayList();
byte lowestGeneratorDetailLevel = (byte) Math.min( byte lowestGeneratorDetailLevel = (byte) Math.min(
@@ -468,48 +476,13 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// helper classes // // helper classes //
//================// //================//
// TODO may not be needed
private class WorldGenTaskTracker implements IWorldGenTaskTracker
{
/** just used when debugging/troubleshooting */
private final long pos;
public WorldGenTaskTracker(long pos) { this.pos = pos; }
@Override
public Consumer<FullDataSourceV2> getDataSourceConsumer()
{
return (dataSource) ->
{
GeneratedFullDataSourceProvider.this.delayedFullDataSourceSaveCache.writeDataSourceToMemoryAndQueueSave(dataSource);
};
}
@Override
public CompletableFuture<Boolean> shouldGenerateSplitChild(long pos)
{
return GeneratedFullDataSourceProvider.this.getAsync(pos).thenApply(fullDataSource ->
{
//noinspection TryFinallyCanBeTryWithResources
try
{
return !GeneratedFullDataSourceProvider.this.isFullyGenerated(fullDataSource.columnGenerationSteps);
}
finally
{
fullDataSource.close();
}
});
}
}
private CompletableFuture<Void> onDataSourceSaveAsync(FullDataSourceV2 fullDataSource) private CompletableFuture<Void> onDataSourceSaveAsync(FullDataSourceV2 fullDataSource)
{ {
// block lights should have been populated at the chunkWrapper stage // block lights should have been populated at the chunkWrapper stage
// waiting to populate the data source's skylight at this stage prevents re-lighting and // waiting to populate the data source's skylight at this stage prevents re-lighting and
// allows us to reduce cross-chunk lighting issues by lighting the whole 4x4 LOD at once // allows us to reduce cross-chunk lighting issues by lighting the whole 4x4 LOD at once
DhLightingEngine.INSTANCE.bakeDataSourceSkyLight(fullDataSource, LodUtil.MAX_MC_LIGHT); int skyLight = this.level.getLevelWrapper().hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT;
DhLightingEngine.INSTANCE.bakeDataSourceSkyLight(fullDataSource, skyLight);
return this.updateDataSourceAsync(fullDataSource); return this.updateDataSourceAsync(fullDataSource);
} }
@@ -521,7 +494,6 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
{ {
boolean shouldDoWorldGen(); boolean shouldDoWorldGen();
@Nullable
DhBlockPos2D getTargetPosForGeneration(); DhBlockPos2D getTargetPosForGeneration();
/** Fired whenever a section has completed generating */ /** Fired whenever a section has completed generating */
@@ -23,21 +23,25 @@ import com.google.common.cache.CacheBuilder;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.structure.ISaveStructure; import com.seibel.distanthorizons.core.file.structure.ISaveStructure;
import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue; import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue;
import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.level.WorldGenModule; import com.seibel.distanthorizons.core.level.LodRequestModule;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.generation.tasks.ERetrievalResultState;
import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoadRequestQueue; import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoadRequestQueue;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* Only handles {@link SyncOnLoadRequestQueue} requests (IE updating existing LODs based on a timestamp). * Only handles {@link SyncOnLoadRequestQueue} requests (IE updating existing LODs based on a timestamp).
* Missing data is handled by {@link WorldGenModule} and {@link RemoteWorldRetrievalQueue}. * Missing data is handled by {@link LodRequestModule} and {@link RemoteWorldRetrievalQueue}.
*/ */
public class RemoteFullDataSourceProvider extends GeneratedFullDataSourceProvider public class RemoteFullDataSourceProvider extends GeneratedFullDataSourceProvider
{ {
@@ -58,7 +62,8 @@ public class RemoteFullDataSourceProvider extends GeneratedFullDataSourceProvide
public RemoteFullDataSourceProvider( public RemoteFullDataSourceProvider(
IDhLevel level, ISaveStructure saveStructure, @Nullable File saveDirOverride, IDhLevel level, ISaveStructure saveStructure, @Nullable File saveDirOverride,
@Nullable SyncOnLoadRequestQueue syncOnLoadRequestQueue) @Nullable SyncOnLoadRequestQueue syncOnLoadRequestQueue
) throws SQLException, IOException
{ {
super(level, saveStructure, saveDirOverride); super(level, saveStructure, saveDirOverride);
this.syncOnLoadRequestQueue = syncOnLoadRequestQueue; this.syncOnLoadRequestQueue = syncOnLoadRequestQueue;
@@ -71,7 +76,7 @@ public class RemoteFullDataSourceProvider extends GeneratedFullDataSourceProvide
//==================// //==================//
@Override @Override
public boolean canQueueRetrieval() { return this.canQueueRetrieval(true); } public boolean canQueueRetrievalNow() { return this.canQueueRetrievalNow(true); }
@Override @Override
@Nullable @Nullable
@@ -99,9 +104,19 @@ public class RemoteFullDataSourceProvider extends GeneratedFullDataSourceProvide
Long timestamp = this.getTimestampForPos(pos); Long timestamp = this.getTimestampForPos(pos);
if (timestamp != null) if (timestamp != null)
{ {
this.syncOnLoadRequestQueue.submitRequest(pos, timestamp, fullDataSource -> this.syncOnLoadRequestQueue.submitRequest(pos, timestamp)
.thenAccept((DataSourceRetrievalResult result) ->
{ {
this.updateDataSourceAsync(fullDataSource).whenComplete((result, throwable) -> fullDataSource.close()); if (result.state == ERetrievalResultState.SUCCESS
&& result.dataSource != null)
{
this.updateDataSourceAsync(result.dataSource)
.handle((voidObj, throwable) ->
{
result.dataSource.close();
return null;
});
}
}); });
} }
@@ -43,7 +43,7 @@ public class FullDataSourceProviderV1<TDhLevel extends IDhLevel>
// constructor // // constructor //
//=============// //=============//
public FullDataSourceProviderV1(TDhLevel level, File saveDir) public FullDataSourceProviderV1(TDhLevel level, File saveDir) throws SQLException, IOException
{ {
this.level = level; this.level = level;
this.saveDir = saveDir; this.saveDir = saveDir;
@@ -52,7 +52,7 @@ public class FullDataSourceProviderV1<TDhLevel extends IDhLevel>
LOGGER.warn("Unable to create full data folder, file saving may fail."); LOGGER.warn("Unable to create full data folder, file saving may fail.");
} }
this.repo = this.createRepo(); this.repo = new FullDataSourceV1Repo(AbstractDhRepo.DEFAULT_DATABASE_TYPE, new File(this.saveDir.getPath() + File.separator + ISaveStructure.DATABASE_NAME));
} }
@@ -61,21 +61,6 @@ public class FullDataSourceProviderV1<TDhLevel extends IDhLevel>
// abstract methods // // abstract methods //
//==================// //==================//
/** When this is called the parent folders should be created */
protected FullDataSourceV1Repo createRepo()
{
try
{
return new FullDataSourceV1Repo(AbstractDhRepo.DEFAULT_DATABASE_TYPE, new File(this.saveDir.getPath() + File.separator + ISaveStructure.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, DataCorruptedException protected FullDataSourceV1 createDataSourceFromDto(FullDataSourceV1DTO dto) throws InterruptedException, IOException, DataCorruptedException
{ {
FullDataSourceV1 dataSource = FullDataSourceV1.createEmpty(dto.pos); FullDataSourceV1 dataSource = FullDataSourceV1.createEmpty(dto.pos);
@@ -14,6 +14,8 @@ import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -62,7 +64,8 @@ public class DataMigratorV1 implements IDebugRenderable, AutoCloseable
public DataMigratorV1( public DataMigratorV1(
FullDataUpdaterV2 dataUpdater, FullDataUpdaterV2 dataUpdater,
IDhLevel level, String levelId, File saveDir) IDhLevel level, String levelId, File saveDir
) throws SQLException, IOException
{ {
this.dataUpdater = dataUpdater; this.dataUpdater = dataUpdater;
this.saveDir = saveDir; this.saveDir = saveDir;
@@ -25,7 +25,7 @@ import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSour
import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.file.fullDatafile.IDataSourceUpdateListenerFunc; import com.seibel.distanthorizons.core.file.fullDatafile.IDataSourceUpdateListenerFunc;
import com.seibel.distanthorizons.core.file.structure.ISaveStructure; import com.seibel.distanthorizons.core.file.structure.ISaveStructure;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
@@ -48,7 +48,6 @@ import java.sql.SQLException;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
/** /**
* Handles reading/writing {@link FullDataSourceV2} * Handles reading/writing {@link FullDataSourceV2}
@@ -86,9 +85,9 @@ public class FullDataSourceProviderV2 implements IDebugRenderable, AutoCloseable
protected final String levelId; protected final String levelId;
private final FullDataUpdaterV2 dataUpdater; protected final FullDataUpdaterV2 dataUpdater;
private final FullDataUpdatePropagatorV2 updatePropagator; protected final FullDataUpdatePropagatorV2 updatePropagator;
private final DataMigratorV1 dataMigratorV1; protected final DataMigratorV1 dataMigratorV1;
@@ -96,11 +95,11 @@ public class FullDataSourceProviderV2 implements IDebugRenderable, AutoCloseable
// constructor // // constructor //
//=============// //=============//
public FullDataSourceProviderV2(IDhLevel level, ISaveStructure saveStructure) { this(level, saveStructure, null); } public FullDataSourceProviderV2(IDhLevel level, ISaveStructure saveStructure) throws SQLException, IOException { this(level, saveStructure, null); }
public FullDataSourceProviderV2(IDhLevel level, ISaveStructure saveStructure, @Nullable File saveDirOverride) public FullDataSourceProviderV2(IDhLevel level, ISaveStructure saveStructure, @Nullable File saveDirOverride) throws SQLException, IOException
{ {
this.saveDir = (saveDirOverride == null) ? saveStructure.getSaveFolder(level.getLevelWrapper()) : saveDirOverride; this.saveDir = (saveDirOverride == null) ? saveStructure.getSaveFolder(level.getLevelWrapper()) : saveDirOverride;
this.repo = this.createRepo(); this.repo = new FullDataSourceV2Repo(AbstractDhRepo.DEFAULT_DATABASE_TYPE, new File(this.saveDir.getPath() + File.separator + ISaveStructure.DATABASE_NAME));
this.level = level; this.level = level;
this.levelId = this.level.getLevelWrapper().getDhIdentifier(); this.levelId = this.level.getLevelWrapper().getDhIdentifier();
@@ -112,19 +111,6 @@ public class FullDataSourceProviderV2 implements IDebugRenderable, AutoCloseable
DebugRenderer.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showFullDataUpdateStatus); DebugRenderer.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showFullDataUpdateStatus);
} }
private FullDataSourceV2Repo createRepo()
{
try
{
return new FullDataSourceV2Repo(AbstractDhRepo.DEFAULT_DATABASE_TYPE, new File(this.saveDir.getPath() + File.separator + ISaveStructure.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);
}
}
@@ -134,14 +120,14 @@ public class FullDataSourceProviderV2 implements IDebugRenderable, AutoCloseable
public void addDataSourceUpdateListener(IDataSourceUpdateListenerFunc<FullDataSourceV2> listener) public void addDataSourceUpdateListener(IDataSourceUpdateListenerFunc<FullDataSourceV2> listener)
{ {
synchronized (this.dataUpdater) synchronized (this.dataUpdater.dateSourceUpdateListeners)
{ {
this.dataUpdater.dateSourceUpdateListeners.add(listener); this.dataUpdater.dateSourceUpdateListeners.add(listener);
} }
} }
public void removeDataSourceUpdateListener(IDataSourceUpdateListenerFunc<FullDataSourceV2> listener) public void removeDataSourceUpdateListener(IDataSourceUpdateListenerFunc<FullDataSourceV2> listener)
{ {
synchronized (this.dataUpdater) synchronized (this.dataUpdater.dateSourceUpdateListeners)
{ {
this.dataUpdater.dateSourceUpdateListeners.add(listener); this.dataUpdater.dateSourceUpdateListeners.add(listener);
} }
@@ -214,9 +200,10 @@ public class FullDataSourceProviderV2 implements IDebugRenderable, AutoCloseable
return FullDataSourceV2.createEmpty(pos); return FullDataSourceV2.createEmpty(pos);
} }
FullDataSourceV2 dataSource = null;
try try
{ {
FullDataSourceV2 dataSource = this.createDataSourceFromDto(dto); dataSource = this.createDataSourceFromDto(dto);
// automatically create and save adjacent data if missing // automatically create and save adjacent data if missing
if (dto.dataFormatVersion == FullDataSourceV2DTO.DATA_FORMAT.V1_NO_ADJACENT_DATA) if (dto.dataFormatVersion == FullDataSourceV2DTO.DATA_FORMAT.V1_NO_ADJACENT_DATA)
@@ -235,11 +222,45 @@ public class FullDataSourceProviderV2 implements IDebugRenderable, AutoCloseable
this.tryLogCorruptedDataError(DhSectionPos.toString(pos), e); this.tryLogCorruptedDataError(DhSectionPos.toString(pos), e);
this.repo.deleteWithKey(pos); this.repo.deleteWithKey(pos);
} }
catch (Exception e)
{
if (dataSource != null)
{
dataSource.close();
}
throw e;
}
} }
catch (InterruptedException ignore) { } catch (InterruptedException ignore) { }
catch (IOException e) catch (IOException e)
{ {
LOGGER.warn("File read Error for pos ["+DhSectionPos.toString(pos)+"], error: "+e.getMessage(), e); String message = e.getMessage();
if (CORRUPT_DATA_ERRORS_LOGGED.add(message))
{
LOGGER.warn("File read Error for pos [" + DhSectionPos.toString(pos) + "], this error message will only be logged once, error: [" + message + "].", e);
}
}
catch (IllegalStateException e)
{
String message = e.getMessage();
if (CORRUPT_DATA_ERRORS_LOGGED.add(message))
{
LOGGER.warn("Incorrectly formatted data for: [" + DhSectionPos.toString(pos) + "], this error message will only be logged once, error: [" + message + "].", e);
}
}
catch (Exception e)
{
String message = e.getMessage();
if (message == null)
{
message = "NULL";
}
if (CORRUPT_DATA_ERRORS_LOGGED.add(message))
{
LOGGER.warn("Unexpected error getting: [" + DhSectionPos.toString(pos) + "], this error message will only be logged once, error: [" + message + "].", e);
}
} }
// an error occurred // an error occurred
@@ -353,7 +374,7 @@ public class FullDataSourceProviderV2 implements IDebugRenderable, AutoCloseable
* to the beginning of your override. * to the beginning of your override.
* Otherwise, parent retrieval limits will be ignored. * Otherwise, parent retrieval limits will be ignored.
*/ */
public boolean canQueueRetrieval() public boolean canQueueRetrievalNow()
{ {
// Retrieval shouldn't happen while an unknown number of // Retrieval shouldn't happen while an unknown number of
// legacy data sources are present. // legacy data sources are present.
@@ -362,15 +383,15 @@ public class FullDataSourceProviderV2 implements IDebugRenderable, AutoCloseable
} }
/** /**
* @return null if this provider can't generate any positions and * @return null if this provider can't generate any positions or
* an empty array if all positions were generated * an empty array if all positions were generated
*/ */
@Nullable @Nullable
public LongArrayList getPositionsToRetrieve(Long pos) { return null; } public LongArrayList getPositionsToRetrieve(long pos) { return null; }
/** @return true if the position was queued, false if not */ /** @return null if the position couldn't be queued */
@Nullable @Nullable
public CompletableFuture<WorldGenResult> queuePositionForRetrieval(Long genPos) { return null; } public CompletableFuture<DataSourceRetrievalResult> queuePositionForRetrieval(Long genPos) { return null; }
/** does nothing if the given position isn't present in the queue */ /** does nothing if the given position isn't present in the queue */
public void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf) { } public void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf) { }
@@ -82,6 +82,7 @@ public class FullDataUpdatePropagatorV2 implements IDebugRenderable, AutoCloseab
} }
//================// //================//
// parent updates // // parent updates //
//================// //================//
@@ -203,7 +204,7 @@ public class FullDataUpdatePropagatorV2 implements IDebugRenderable, AutoCloseab
// can return null when the file handler is being shut down // can return null when the file handler is being shut down
if (childDataSource != null) if (childDataSource != null)
{ {
parentDataSource.updateFromChunk(childDataSource); parentDataSource.updateFromDataSource(childDataSource);
} }
} }
} }
@@ -213,6 +214,8 @@ public class FullDataUpdatePropagatorV2 implements IDebugRenderable, AutoCloseab
} }
finally finally
{ {
this.provider.repo.setApplyToParent(childPos, false);
childReadLock.unlock(); childReadLock.unlock();
this.dataUpdater.lockedPosSet.remove(childPos); this.dataUpdater.lockedPosSet.remove(childPos);
} }
@@ -224,11 +227,7 @@ public class FullDataUpdatePropagatorV2 implements IDebugRenderable, AutoCloseab
parentDataSource.applyToParent = true; parentDataSource.applyToParent = true;
} }
this.dataUpdater.updateDataSource(parentDataSource, false); this.dataUpdater.updateDataSource(parentDataSource);
for (Long childPos : updatePosByParentPos.get(parentUpdatePos))
{
this.provider.repo.setApplyToParent(childPos, false);
}
} }
} }
} }
@@ -321,7 +320,7 @@ public class FullDataUpdatePropagatorV2 implements IDebugRenderable, AutoCloseab
// will return null if the file handler is shutting down // will return null if the file handler is shutting down
if (childDataSource != null) if (childDataSource != null)
{ {
childDataSource.updateFromChunk(parentDataSource); childDataSource.updateFromDataSource(parentDataSource);
// don't propagate child updates past the bottom of the tree // don't propagate child updates past the bottom of the tree
if (DhSectionPos.getDetailLevel(childPos) != DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL) if (DhSectionPos.getDetailLevel(childPos) != DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL)
@@ -329,7 +328,7 @@ public class FullDataUpdatePropagatorV2 implements IDebugRenderable, AutoCloseab
childDataSource.applyToChildren = true; childDataSource.applyToChildren = true;
} }
this.dataUpdater.updateDataSource(childDataSource, false); this.dataUpdater.updateDataSource(childDataSource);
} }
} }
} }
@@ -339,12 +338,12 @@ public class FullDataUpdatePropagatorV2 implements IDebugRenderable, AutoCloseab
} }
finally finally
{ {
this.provider.repo.setApplyToChild(parentUpdatePos, false);
childWriteLock.unlock(); childWriteLock.unlock();
this.dataUpdater.lockedPosSet.remove(childPos); this.dataUpdater.lockedPosSet.remove(childPos);
} }
} }
this.provider.repo.setApplyToChild(parentUpdatePos, false);
} }
} }
} }
@@ -63,7 +63,7 @@ public class FullDataUpdaterV2 implements IDebugRenderable, AutoCloseable
/** /**
* Can be used if you don't want to lock the current thread * Can be used if you don't want to lock the current thread
* Otherwise the sync version {@link FullDataUpdaterV2#updateDataSource(FullDataSourceV2, boolean)} may be a better choice. * Otherwise the sync version {@link FullDataUpdaterV2#updateDataSource(FullDataSourceV2)} may be a better choice.
*/ */
public CompletableFuture<Void> updateDataSourceAsync(@NotNull FullDataSourceV2 inputDataSource) public CompletableFuture<Void> updateDataSourceAsync(@NotNull FullDataSourceV2 inputDataSource)
{ {
@@ -86,7 +86,7 @@ public class FullDataUpdaterV2 implements IDebugRenderable, AutoCloseable
{ {
try try
{ {
this.updateDataSource(inputDataSource, true); this.updateDataSource(inputDataSource);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -107,7 +107,7 @@ public class FullDataUpdaterV2 implements IDebugRenderable, AutoCloseable
} }
/** After this method returns the inputData will be written to file. */ /** After this method returns the inputData will be written to file. */
public void updateDataSource(@NotNull FullDataSourceV2 inputData, boolean lockOnUpdatePos) public void updateDataSource(@NotNull FullDataSourceV2 inputData)
{ {
if (this.isShutdownRef.get()) if (this.isShutdownRef.get())
{ {
@@ -117,27 +117,22 @@ public class FullDataUpdaterV2 implements IDebugRenderable, AutoCloseable
long updatePos = inputData.getPos(); long updatePos = inputData.getPos();
boolean methodLocked = false;
// a lock is necessary to prevent two threads from writing to the same position at once, // 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) // if that happens only the second update will apply and the LOD will end up with hole(s)
ReentrantLock updateLock = this.updateLockProvider.getLock(updatePos); ReentrantLock updateLock = this.updateLockProvider.getLock(updatePos);
try try
{ {
if (lockOnUpdatePos)
{
methodLocked = true;
updateLock.lock(); updateLock.lock();
this.lockedPosSet.add(updatePos); this.lockedPosSet.add(updatePos);
}
// get or create the data source // get or create the data source
try (FullDataSourceV2 recipientDataSource = this.provider.get(updatePos)) try (FullDataSourceV2 recipientDataSource = this.provider.get(updatePos))
{ {
if (recipientDataSource != null) if (recipientDataSource != null) // will be null if the repo was shut down
{ {
boolean dataModified = recipientDataSource.updateFromChunk(inputData); boolean dataModified = recipientDataSource.updateFromDataSource(inputData);
if (dataModified) if (dataModified)
{ {
// save the updated data to the database // save the updated data to the database
@@ -150,6 +145,8 @@ public class FullDataUpdaterV2 implements IDebugRenderable, AutoCloseable
} }
synchronized (this.dateSourceUpdateListeners)
{
for (IDataSourceUpdateListenerFunc<FullDataSourceV2> listener : this.dateSourceUpdateListeners) for (IDataSourceUpdateListenerFunc<FullDataSourceV2> listener : this.dateSourceUpdateListeners)
{ {
if (listener != null) if (listener != null)
@@ -161,19 +158,17 @@ public class FullDataUpdaterV2 implements IDebugRenderable, AutoCloseable
} }
} }
} }
}
catch (Exception e) catch (Exception e)
{ {
LOGGER.error("Error updating pos ["+DhSectionPos.toString(updatePos)+"], error: "+e.getMessage(), e); LOGGER.error("Error updating pos ["+DhSectionPos.toString(updatePos)+"], error: "+e.getMessage(), e);
} }
finally finally
{
if (methodLocked)
{ {
updateLock.unlock(); updateLock.unlock();
this.lockedPosSet.remove(updatePos); this.lockedPosSet.remove(updatePos);
} }
} }
}
private FullDataSourceV2DTO createDtoFromDataSource(FullDataSourceV2 dataSource) private FullDataSourceV2DTO createDtoFromDataSource(FullDataSourceV2 dataSource)
{ {
@@ -24,12 +24,13 @@ import com.seibel.distanthorizons.api.interfaces.override.worldGenerator.IDhApiW
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.util.ExceptionUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.worldGeneration.IBatchGeneratorEnvironmentWrapper;
import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IOverrideInjector; import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IOverrideInjector;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorMode; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorMode;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.worldGeneration.AbstractBatchGenerationEnvironmentWrapper;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -42,10 +43,10 @@ import java.util.function.Consumer;
*/ */
public class BatchGenerator implements IDhApiWorldGenerator public class BatchGenerator implements IDhApiWorldGenerator
{ {
private static final IWrapperFactory FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class); private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
private static final DhLogger LOGGER = new DhLoggerBuilder().build(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public AbstractBatchGenerationEnvironmentWrapper generationEnvironment; public IBatchGeneratorEnvironmentWrapper generationEnvironment;
public IDhLevel targetDhLevel; public IDhLevel targetDhLevel;
@@ -57,7 +58,7 @@ public class BatchGenerator implements IDhApiWorldGenerator
public BatchGenerator(IDhLevel targetDhLevel) public BatchGenerator(IDhLevel targetDhLevel)
{ {
this.targetDhLevel = targetDhLevel; this.targetDhLevel = targetDhLevel;
this.generationEnvironment = FACTORY.createBatchGenerator(targetDhLevel); this.generationEnvironment = WRAPPER_FACTORY.createBatchGenerator(targetDhLevel);
LOGGER.info("Batch Chunk Generator initialized"); LOGGER.info("Batch Chunk Generator initialized");
} }
@@ -83,26 +84,26 @@ public class BatchGenerator implements IDhApiWorldGenerator
//===================// //===================//
// generator methods // // generator methods //
//===================// //===================//
@Override @Override
public CompletableFuture<Void> generateChunks( public CompletableFuture<Void> generateChunks(
int chunkPosMinX, int chunkPosMinZ, int generationRequestChunkWidthCount, byte targetDataDetail, EDhApiDistantGeneratorMode generatorMode, int chunkPosMinX,
ExecutorService worldGeneratorThreadPool, Consumer<Object[]> resultConsumer) int chunkPosMinZ,
int chunkWidthCount,
byte targetDataDetail,
EDhApiDistantGeneratorMode generatorMode,
ExecutorService worldGeneratorThreadPool,
Consumer<Object[]> resultConsumer)
{ {
EDhApiWorldGenerationStep targetStep = null; EDhApiWorldGenerationStep targetStep;
switch (generatorMode) switch (generatorMode)
{ {
case PRE_EXISTING_ONLY: // Only load in existing chunks. Note: this requires the biome generation step in order for biomes to be properly initialized. case PRE_EXISTING_ONLY: // Only load in existing chunks.
//case BIOME_ONLY: // No blocks. Require fake height in LodBuilder targetStep = EDhApiWorldGenerationStep.EMPTY; // special logic
targetStep = EDhApiWorldGenerationStep.BIOMES;
break; break;
//case BIOME_ONLY_SIMULATE_HEIGHT:
// targetStep = EDhApiWorldGenerationStep.NOISE; // Stone only. Requires a fake surface
// break;
case SURFACE: case SURFACE:
targetStep = EDhApiWorldGenerationStep.SURFACE; targetStep = EDhApiWorldGenerationStep.SURFACE;
break; break;
@@ -112,20 +113,27 @@ public class BatchGenerator implements IDhApiWorldGenerator
case INTERNAL_SERVER: case INTERNAL_SERVER:
targetStep = EDhApiWorldGenerationStep.LIGHT; targetStep = EDhApiWorldGenerationStep.LIGHT;
break; break;
default:
throw new IllegalArgumentException("no target step defined for generator mode: ["+generatorMode+"].");
} }
// the consumer needs to be wrapped like this because the API can't use DH core objects (and IChunkWrapper can't be easily put into the API project) // the consumer needs to be wrapped like this because the API can't use DH core objects (and IChunkWrapper can't be easily put into the API project)
Consumer<IChunkWrapper> consumerWrapper = (chunkWrapper) -> resultConsumer.accept(new Object[]{chunkWrapper}); Consumer<IChunkWrapper> consumerWrapper = (chunkWrapper) -> resultConsumer.accept(new Object[]{chunkWrapper});
try try
{ {
return this.generationEnvironment.generateChunks( return this.generationEnvironment.queueGenEvent(
chunkPosMinX, chunkPosMinZ, generationRequestChunkWidthCount, chunkPosMinX, chunkPosMinZ, chunkWidthCount,
generatorMode, targetStep, generatorMode, targetStep,
worldGeneratorThreadPool, consumerWrapper); worldGeneratorThreadPool, consumerWrapper);
} }
catch (Exception e) catch (Exception e)
{ {
if (!LodUtil.isInterruptOrReject(e)) LOGGER.error("Error starting future for chunk generation", e); if (!ExceptionUtil.isInterruptOrReject(e))
{
LOGGER.error("Error starting future for chunk generation, error: ["+e.getMessage()+"].", e);
}
CompletableFuture<Void> future = new CompletableFuture<>(); CompletableFuture<Void> future = new CompletableFuture<>();
future.completeExceptionally(e); future.completeExceptionally(e);
return future; return future;
@@ -144,9 +152,10 @@ public class BatchGenerator implements IDhApiWorldGenerator
@Override @Override
public void close() public void close()
{ {
LOGGER.info(BatchGenerator.class.getSimpleName() + " shutting down..."); LOGGER.info("["+BatchGenerator.class.getSimpleName()+"] shutting down...");
this.generationEnvironment.stop(); this.generationEnvironment.close();
} }
} }
@@ -118,14 +118,18 @@ public class DhLightingEngine
* @param centerChunk the chunk we want to apply lighting to * @param centerChunk the chunk we want to apply lighting to
* @param nearbyChunkList should also contain centerChunk * @param nearbyChunkList should also contain centerChunk
* @param maxSkyLight should be a value between 0 and 15 * @param maxSkyLight should be a value between 0 and 15
*
* @return the number of light positions iterated over, can be used for profiling.
*/ */
private void lightChunk( private int lightChunk(
@NotNull IChunkWrapper centerChunk, @NotNull ArrayList<IChunkWrapper> nearbyChunkList, @NotNull IChunkWrapper centerChunk, @NotNull ArrayList<IChunkWrapper> nearbyChunkList,
int maxSkyLight, boolean updateBlockLight, boolean updateSkyLight) int maxSkyLight, boolean updateBlockLight, boolean updateSkyLight)
{ {
DhChunkPos centerChunkPos = centerChunk.getChunkPos(); DhChunkPos centerChunkPos = centerChunk.getChunkPos();
AdjacentChunkHolder adjacentChunkHolder = new AdjacentChunkHolder(centerChunk); AdjacentChunkHolder adjacentChunkHolder = new AdjacentChunkHolder(centerChunk);
// how many positions we've walked over, can be used for profiling/debugging
int posIterations = 0;
// try-finally to handle the stableArray resources // try-finally to handle the stableArray resources
StableLightPosStack blockLightWorldPosQueue = null; StableLightPosStack blockLightWorldPosQueue = null;
@@ -154,14 +158,15 @@ public class DhLightingEngine
// and get any necessary info from them // and get any necessary info from them
for (int chunkIndex = 0; chunkIndex < nearbyChunkList.size(); chunkIndex++) // using iterators in high traffic areas can cause GC issues due to allocating a bunch of iterators, use an indexed for-loop instead for (int chunkIndex = 0; chunkIndex < nearbyChunkList.size(); chunkIndex++) // using iterators in high traffic areas can cause GC issues due to allocating a bunch of iterators, use an indexed for-loop instead
{ {
IChunkWrapper chunk = nearbyChunkList.get(chunkIndex); IChunkWrapper neighborChunk = nearbyChunkList.get(chunkIndex);
if (chunk != null && requestedAdjacentPositions.contains(chunk.getChunkPos())) if (neighborChunk != null
&& requestedAdjacentPositions.contains(neighborChunk.getChunkPos()))
{ {
// remove the newly found position // remove the newly found position
requestedAdjacentPositions.remove(chunk.getChunkPos()); requestedAdjacentPositions.remove(neighborChunk.getChunkPos());
// add the adjacent chunk // add the adjacent chunk
adjacentChunkHolder.add(chunk); adjacentChunkHolder.add(neighborChunk);
// get and set the adjacent chunk's initial block lights // get and set the adjacent chunk's initial block lights
final DhBlockPosMutable relLightBlockPos = PRIMARY_BLOCK_POS_REF.get(); final DhBlockPosMutable relLightBlockPos = PRIMARY_BLOCK_POS_REF.get();
@@ -174,19 +179,19 @@ public class DhLightingEngine
if (updateBlockLight) if (updateBlockLight)
{ {
ArrayList<DhBlockPos> blockLightPosList = chunk.getWorldBlockLightPosList(); ArrayList<DhBlockPos> blockLightPosList = neighborChunk.getWorldBlockLightPosList();
for (int blockLightIndex = 0; blockLightIndex < blockLightPosList.size(); blockLightIndex++) // using iterators in high traffic areas can cause GC issues due to allocating a bunch of iterators, use an indexed for-loop instead for (int blockLightIndex = 0; blockLightIndex < blockLightPosList.size(); blockLightIndex++) // using iterators in high traffic areas can cause GC issues due to allocating a bunch of iterators, use an indexed for-loop instead
{ {
DhBlockPos blockLightPos = blockLightPosList.get(blockLightIndex); DhBlockPos blockLightPos = blockLightPosList.get(blockLightIndex);
blockLightPos.mutateToChunkRelativePos(relLightBlockPos); blockLightPos.mutateToChunkRelativePos(relLightBlockPos);
// get the light // get the light
IBlockStateWrapper blockState = chunk.getBlockState(relLightBlockPos); IBlockStateWrapper blockState = neighborChunk.getBlockState(relLightBlockPos);
int lightValue = blockState.getLightEmission(); int lightValue = blockState.getLightEmission();
blockLightWorldPosQueue.push(blockLightPos.getX(), blockLightPos.getY(), blockLightPos.getZ(), lightValue); blockLightWorldPosQueue.push(blockLightPos.getX(), blockLightPos.getY(), blockLightPos.getZ(), lightValue);
// set the light // set the light
chunk.setDhBlockLight(relLightBlockPos.getX(), relLightBlockPos.getY(), relLightBlockPos.getZ(), lightValue); neighborChunk.setDhBlockLight(relLightBlockPos.getX(), relLightBlockPos.getY(), relLightBlockPos.getZ(), lightValue);
} }
} }
@@ -198,23 +203,24 @@ public class DhLightingEngine
// get and set the adjacent chunk's initial skylights, // get and set the adjacent chunk's initial skylights,
// if the dimension has skylights // if the dimension has skylights
if (updateSkyLight && maxSkyLight > 0) if (updateSkyLight
&& maxSkyLight > 0)
{ {
IMutableBlockPosWrapper mcBlockPos = chunk.getMutableBlockPosWrapper(); IMutableBlockPosWrapper mcBlockPos = neighborChunk.getMutableBlockPosWrapper();
IBlockStateWrapper previousBlockState = null; IBlockStateWrapper previousBlockState = null;
int maxY = chunk.getMaxNonEmptyHeight(); int maxY = neighborChunk.getMaxNonEmptyHeight();
int minY = chunk.getInclusiveMinBuildHeight(); int minY = neighborChunk.getInclusiveMinBuildHeight();
// get the adjacent chunk's sky lights // get the adjacent chunk's sky lights
for (int relX = 0; relX < LodUtil.CHUNK_WIDTH; relX++) // relative block pos for (int relX = 0; relX < LodUtil.CHUNK_WIDTH; relX++) // relative block pos
{ {
for (int relZ = 0; relZ < LodUtil.CHUNK_WIDTH; relZ++) for (int relZ = 0; relZ < LodUtil.CHUNK_WIDTH; relZ++)
{ {
// set each pos' sky light all the way down until an opaque block is hit // set each pos sky light all the way down until an opaque block is hit
for (int y = maxY; y >= minY; y--) for (int y = maxY; y >= minY; y--)
{ {
IBlockStateWrapper block = previousBlockState = chunk.getBlockState(relX, y, relZ, mcBlockPos, previousBlockState); IBlockStateWrapper block = previousBlockState = neighborChunk.getBlockState(relX, y, relZ, mcBlockPos, previousBlockState);
if (block != null && block.getOpacity() != LodUtil.BLOCK_FULLY_TRANSPARENT) if (block != null && block.getOpacity() != LodUtil.BLOCK_FULLY_TRANSPARENT)
{ {
// keep moving down until we find a non-transparent block // keep moving down until we find a non-transparent block
@@ -223,12 +229,12 @@ public class DhLightingEngine
// add sky light to the queue // add sky light to the queue
DhBlockPos skyLightPos = new DhBlockPos(chunk.getMinBlockX() + relX, y, chunk.getMinBlockZ() + relZ); DhBlockPos skyLightPos = new DhBlockPos(neighborChunk.getMinBlockX() + relX, y, neighborChunk.getMinBlockZ() + relZ);
skyLightWorldPosQueue.push(skyLightPos.getX(), skyLightPos.getY(), skyLightPos.getZ(), maxSkyLight); skyLightWorldPosQueue.push(skyLightPos.getX(), skyLightPos.getY(), skyLightPos.getZ(), maxSkyLight);
// set the chunk's sky light // set the chunk's sky light
skyLightPos.mutateToChunkRelativePos(relLightBlockPos); skyLightPos.mutateToChunkRelativePos(relLightBlockPos);
chunk.setDhSkyLight(relLightBlockPos.getX(), relLightBlockPos.getY(), relLightBlockPos.getZ(), maxSkyLight); neighborChunk.setDhSkyLight(relLightBlockPos.getX(), relLightBlockPos.getY(), relLightBlockPos.getZ(), maxSkyLight);
} }
} }
} }
@@ -243,14 +249,15 @@ public class DhLightingEngine
} }
} }
// block light // block light
if (updateBlockLight) if (updateBlockLight)
{ {
// done to prevent a rare issue where the light values are incorrectly set to -1 // done to prevent a rare issue where the light values are incorrectly set to -1
// TODO why could that happen?
centerChunk.clearDhBlockLighting(); centerChunk.clearDhBlockLighting();
this.propagateChunkLightPosList(blockLightWorldPosQueue, adjacentChunkHolder, posIterations += this.propagateChunkLightPosList(blockLightWorldPosQueue, adjacentChunkHolder,
(neighbourChunk, relBlockPos) -> neighbourChunk.getDhBlockLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ()), (neighbourChunk, relBlockPos) -> neighbourChunk.getDhBlockLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ()),
(neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhBlockLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ(), newLightValue), (neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhBlockLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ(), newLightValue),
true); true);
@@ -261,7 +268,7 @@ public class DhLightingEngine
{ {
centerChunk.clearDhSkyLighting(); centerChunk.clearDhSkyLighting();
this.propagateChunkLightPosList(skyLightWorldPosQueue, adjacentChunkHolder, posIterations += this.propagateChunkLightPosList(skyLightWorldPosQueue, adjacentChunkHolder,
(neighbourChunk, relBlockPos) -> neighbourChunk.getDhSkyLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ()), (neighbourChunk, relBlockPos) -> neighbourChunk.getDhSkyLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ()),
(neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhSkyLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ(), newLightValue), (neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhSkyLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ(), newLightValue),
false); false);
@@ -286,10 +293,12 @@ public class DhLightingEngine
{ {
centerChunk.setIsDhSkyLightCorrect(true); centerChunk.setIsDhSkyLightCorrect(true);
} }
return posIterations;
} }
/** Applies each {@link LightPos} from the queue to the given set of {@link IChunkWrapper}'s. */ /** Applies each {@link LightPos} from the queue to the given set of {@link IChunkWrapper}'s. */
private void propagateChunkLightPosList( private int propagateChunkLightPosList(
StableLightPosStack lightPosQueue, AdjacentChunkHolder adjacentChunkHolder, StableLightPosStack lightPosQueue, AdjacentChunkHolder adjacentChunkHolder,
IGetLightFunc getLightFunc, ISetLightFunc setLightFunc, IGetLightFunc getLightFunc, ISetLightFunc setLightFunc,
boolean propagatingBlockLights) boolean propagatingBlockLights)
@@ -300,15 +309,44 @@ public class DhLightingEngine
final DhBlockPosMutable neighbourBlockPos = PRIMARY_BLOCK_POS_REF.get(); final DhBlockPosMutable neighbourBlockPos = PRIMARY_BLOCK_POS_REF.get();
final DhBlockPosMutable relNeighbourBlockPos = SECONDARY_BLOCK_POS_REF.get(); final DhBlockPosMutable relNeighbourBlockPos = SECONDARY_BLOCK_POS_REF.get();
// it doesn't matter what chunk we get the mutable block pos from
IMutableBlockPosWrapper mcBlockPos = null; IMutableBlockPosWrapper mcBlockPos = null;
for (int i = 0; i < adjacentChunkHolder.chunkArray.length; i++)
{
IChunkWrapper chunkWrapper = adjacentChunkHolder.chunkArray[i];
if (chunkWrapper != null)
{
mcBlockPos = chunkWrapper.getMutableBlockPosWrapper();
break;
}
}
if (mcBlockPos == null)
{
LodUtil.assertNotReach("How did we try to light a chunk with no chunks?");
}
IBlockStateWrapper previousBlockState = null; IBlockStateWrapper previousBlockState = null;
int iterations = 0;
// update each light level
for (int currentLightLevel = LodUtil.MAX_MC_LIGHT; currentLightLevel >= LodUtil.MIN_MC_LIGHT; currentLightLevel--)
{
// Walking down from the top light level to the bottom can reduce iterating over
// the same positions multiple times.
// At best this seems to behave at roughly 2x the speed of just blindly putting light pos
// in a queue and at worse slightly faster than the blind queue.
lightPos.lightValue = currentLightLevel;
// update each light position // update each light position
while (!lightPosQueue.isEmpty()) while (!lightPosQueue.isLightLevelEmpty(currentLightLevel))
{ {
// since we don't care about the order the positions are processed, // since we don't care about the order the positions are processed,
// we can grab the last position instead of the first for a slight performance increase (this way the array doesn't need to be shifted over every loop) // we can grab the last position instead of the first for a slight performance increase (this way the array doesn't need to be shifted over every loop)
lightPosQueue.popMutate(lightPos); lightPosQueue.popMutate(lightPos, currentLightLevel);
iterations++;
int lightValue = lightPos.lightValue; int lightValue = lightPos.lightValue;
@@ -346,42 +384,49 @@ public class DhLightingEngine
} }
if (mcBlockPos == null) IBlockStateWrapper neighbourBlockState = neighbourChunk.getBlockState(relNeighbourBlockPos, mcBlockPos, previousBlockState);
{ previousBlockState = neighbourBlockState;
// it doesn't matter what chunk we get the position object from
// TODO move this getter logic out of ChunkWrapper
mcBlockPos = neighbourChunk.getMutableBlockPosWrapper();
}
IBlockStateWrapper neighbourBlockState = previousBlockState = neighbourChunk.getBlockState(relNeighbourBlockPos, mcBlockPos, previousBlockState);
// Math.max(1, ...) is used so that the propagated light level always drops by at least 1, preventing infinite cycles. // Math.max(1, ...) is used so that the propagated light level always drops by at least 1, preventing infinite cycles.
int targetLevel = lightValue - Math.max(1, neighbourBlockState.getOpacity()); int targetLightLevel = lightValue - Math.max(1, neighbourBlockState.getOpacity());
if (targetLevel > currentBlockLight) if (targetLightLevel > currentBlockLight)
{ {
// this position is darker than the new light value, update/set it // this position is darker than the new light value, update/set it
setLightFunc.setLight(neighbourChunk, relNeighbourBlockPos, targetLevel); setLightFunc.setLight(neighbourChunk, relNeighbourBlockPos, targetLightLevel);
// now that light has been propagated to this blockPos // now that light has been propagated to this blockPos
// we need to queue it up so its neighbours can be propagated as well // we need to queue it up so its neighbours can be propagated as well
lightPosQueue.push(neighbourBlockPos.getX(), neighbourBlockPos.getY(), neighbourBlockPos.getZ(), targetLevel); lightPosQueue.push(neighbourBlockPos.getX(), neighbourBlockPos.getY(), neighbourBlockPos.getZ(), targetLightLevel);
}
} }
} }
} }
for (int currentLightLevel = LodUtil.MAX_MC_LIGHT; currentLightLevel >= LodUtil.MIN_MC_LIGHT; currentLightLevel--)
{
if (!lightPosQueue.isLightLevelEmpty(currentLightLevel))
{
LodUtil.assertNotReach("Non empty light pos queue for light level ["+currentLightLevel+"] after light engine running");
}
}
// can be enable if troubleshooting lighting issues
if (RENDER_BLOCK_LIGHT_WIREFRAME && propagatingBlockLights)
// can be enabled if troubleshooting lighting issues
if (RENDER_BLOCK_LIGHT_WIREFRAME
&& propagatingBlockLights)
{ {
RenderDhLightValuesAsWireframe(adjacentChunkHolder, true); RenderDhLightValuesAsWireframe(adjacentChunkHolder, true);
} }
else if (RENDER_SKY_LIGHT_WIREFRAME && !propagatingBlockLights) else if (RENDER_SKY_LIGHT_WIREFRAME
&& !propagatingBlockLights)
{ {
RenderDhLightValuesAsWireframe(adjacentChunkHolder, false); RenderDhLightValuesAsWireframe(adjacentChunkHolder, false);
} }
// propagation complete // propagation complete
return iterations;
} }
@@ -462,7 +507,7 @@ public class DhLightingEngine
point = FullDataPointUtil.setSkyLight(point, skylight); point = FullDataPointUtil.setSkyLight(point, skylight);
dataPoints.set(index, point); dataPoints.set(index, point);
// now for the propagation. // now for the propagation.
recursivelyLightAdjacentDataPoints(dataSource, airIDs, x, z, point); this.recursivelyLightAdjacentDataPoints(dataSource, airIDs, x, z, point);
} }
} }
} }
@@ -596,7 +641,7 @@ public class DhLightingEngine
else if (!airIDs.get(FullDataPointUtil.getId(adjacentDataPoint))) else if (!airIDs.get(FullDataPointUtil.getId(adjacentDataPoint)))
{ {
// assume for now that we cannot propagate into non-transparent data points. // assume for now that we cannot propagate into non-transparent data points.
continue; // TODO how does this work with water? Do we care? continue;
} }
else else
{ {
@@ -610,7 +655,7 @@ public class DhLightingEngine
adjacentDataPoint = FullDataPointUtil.setSkyLight(adjacentDataPoint, lightLevel - 1); adjacentDataPoint = FullDataPointUtil.setSkyLight(adjacentDataPoint, lightLevel - 1);
adjacentDataPoints.set(adjacentIndex, adjacentDataPoint); adjacentDataPoints.set(adjacentIndex, adjacentDataPoint);
// if propagation succeeded, recursively propagate again starting at the adjacent data point. // if propagation succeeded, recursively propagate again starting at the adjacent data point.
recursivelyLightAdjacentDataPoints(chunk, airIDs, adjacentX, adjacentZ, adjacentDataPoint); this.recursivelyLightAdjacentDataPoints(chunk, airIDs, adjacentX, adjacentZ, adjacentDataPoint);
} }
} }
} }
@@ -735,16 +780,24 @@ public class DhLightingEngine
private static final Queue<StableLightPosStack> lightArrayCache = new ArrayDeque<>(); private static final Queue<StableLightPosStack> lightArrayCache = new ArrayDeque<>();
/** the index of the last item in the array, -1 if empty */ /** the index of the last item in the array, -1 if empty */
private int index = -1; private int[] indexByLightLevel = new int[LodUtil.MAX_MC_LIGHT + 1];
/** x, y, z, and lightValue. */ /** x, y, z */
public static final int INTS_PER_LIGHT_POS = 4; public static final int INTS_PER_LIGHT_POS = 3;
/** private final IntArrayList[] lightPositionsByLightLevel = new IntArrayList[LodUtil.MAX_MC_LIGHT + 1];
* When tested with a normal 1.20 world James saw a maximum of 36,709 block and 2,355 sky lights,
* so 40,000 should be a good starting point that can contain most lighting tasks.
*/ public StableLightPosStack()
private final IntArrayList lightPositions = new IntArrayList(40_000 * INTS_PER_LIGHT_POS); {
for (int i = 0; i < this.lightPositionsByLightLevel.length; i++)
{
// When tested with a normal 1.20 world James saw a maximum of 36,709 block and 2,355 sky lights,
// so 40,000 should be a good starting point that can contain most lighting tasks.
this.lightPositionsByLightLevel[i] = new IntArrayList(40_000 * INTS_PER_LIGHT_POS);
this.indexByLightLevel[i] = -1;
}
}
@@ -791,45 +844,56 @@ public class DhLightingEngine
// stack methods // // stack methods //
//===============// //===============//
public boolean isEmpty() { return this.index == -1; } public boolean isLightLevelEmpty(int lightLevel) { return this.indexByLightLevel[lightLevel] == -1; }
public int size() { return this.index+1; } //public int size() { return this.index+1; }
public void push(int blockX, int blockY, int blockZ, int lightValue) public void push(int blockX, int blockY, int blockZ, int lightLevel)
{ {
this.index++; IntArrayList lightPositions = this.lightPositionsByLightLevel[lightLevel];
int subIndex = this.index * INTS_PER_LIGHT_POS;
if (subIndex < this.lightPositions.size()) this.indexByLightLevel[lightLevel]++;
int subIndex = this.indexByLightLevel[lightLevel] * INTS_PER_LIGHT_POS;
if (subIndex < lightPositions.size())
{ {
this.lightPositions.set(subIndex, blockX); lightPositions.set(subIndex, blockX);
this.lightPositions.set(subIndex + 1, blockY); lightPositions.set(subIndex + 1, blockY);
this.lightPositions.set(subIndex + 2, blockZ); lightPositions.set(subIndex + 2, blockZ);
this.lightPositions.set(subIndex + 3, lightValue);
} }
else else
{ {
// add a new pos // add a new pos
this.lightPositions.add(blockX); lightPositions.add(blockX);
this.lightPositions.add(blockY); lightPositions.add(blockY);
this.lightPositions.add(blockZ); lightPositions.add(blockZ);
this.lightPositions.add(lightValue);
} }
} }
/** mutates the given {@link LightPos} to match the next {@link LightPos} in the queue. */ /** mutates the given {@link LightPos} to match the next {@link LightPos} in the queue. */
public void popMutate(LightPos pos) public void popMutate(LightPos pos, int lightLevel)
{ {
int subIndex = this.index * INTS_PER_LIGHT_POS; int subIndex = this.indexByLightLevel[lightLevel] * INTS_PER_LIGHT_POS;
IntArrayList lightPositions = this.lightPositionsByLightLevel[lightLevel];
pos.setX(this.lightPositions.getInt(subIndex)); pos.setX(lightPositions.getInt(subIndex));
pos.setY(this.lightPositions.getInt(subIndex + 1)); pos.setY(lightPositions.getInt(subIndex + 1));
pos.setZ(this.lightPositions.getInt(subIndex + 2)); pos.setZ(lightPositions.getInt(subIndex + 2));
pos.lightValue = this.lightPositions.getInt(subIndex + 3);
this.index--; this.indexByLightLevel[lightLevel]--;
} }
@Override @Override
public String toString() { return this.index + "/" + (this.lightPositions.size() / INTS_PER_LIGHT_POS); } public String toString()
{
StringBuilder builder = new StringBuilder();
for (int i = 0; i < this.indexByLightLevel.length; i++)
{
builder.append("light: ").append(i)
.append(" size: ").append(this.indexByLightLevel[i]).append("/").append(this.lightPositionsByLightLevel[i].size() / INTS_PER_LIGHT_POS).append("\n");
}
return builder.toString();
}
} }
@@ -19,13 +19,11 @@
package com.seibel.distanthorizons.core.generation; package com.seibel.distanthorizons.core.generation;
import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.LodQuadTree; import com.seibel.distanthorizons.core.render.LodQuadTree;
import com.seibel.distanthorizons.core.util.objects.RollingAverage; import com.seibel.distanthorizons.core.util.objects.RollingAverage;
import org.jetbrains.annotations.Nullable;
import java.io.Closeable; import java.io.Closeable;
import java.util.List; import java.util.List;
@@ -92,7 +90,7 @@ public interface IFullDataSourceRetrievalQueue extends Closeable
*/ */
void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf); void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf);
CompletableFuture<WorldGenResult> submitRetrievalTask(long pos, byte requiredDataDetail, IWorldGenTaskTracker tracker); CompletableFuture<DataSourceRetrievalResult> submitRetrievalTask(long pos, byte requiredDataDetail);
@@ -5,13 +5,17 @@ import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalCause; import com.google.common.cache.RemovalCause;
import com.seibel.distanthorizons.core.api.internal.SharedApi; import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider; import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider;
import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.generation.tasks.ERetrievalResultState;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.util.FormatUtil; import com.seibel.distanthorizons.core.util.FormatUtil;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.objects.RollingAverage; import com.seibel.distanthorizons.core.util.objects.RollingAverage;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -28,6 +32,8 @@ public class PregenManager
{ {
protected static final DhLogger LOGGER = new DhLoggerBuilder().build(); protected static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IMinecraftSharedWrapper MC_SERVER = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
private final AtomicReference<PregenState> pregenFuture = new AtomicReference<>(); private final AtomicReference<PregenState> pregenFuture = new AtomicReference<>();
@@ -51,6 +57,7 @@ public class PregenManager
pregenState.completeExceptionally(new IllegalStateException("Pregen is already running.")); pregenState.completeExceptionally(new IllegalStateException("Pregen is already running."));
return pregenState; return pregenState;
} }
pregenState.whenComplete((result, throwable) -> { pregenState.whenComplete((result, throwable) -> {
this.pregenFuture.set(null); this.pregenFuture.set(null);
}); });
@@ -104,7 +111,7 @@ public class PregenManager
} }
long timeSincePreviousTaskFinish = System.currentTimeMillis() - this.lastTaskFinishTime.getAndSet(System.currentTimeMillis()); long timeSincePreviousTaskFinish = System.currentTimeMillis() - this.lastTaskFinishTime.getAndSet(System.currentTimeMillis());
this.averageTaskCompletionIntervalMs.addValue(timeSincePreviousTaskFinish); this.averageTaskCompletionIntervalMs.add(timeSincePreviousTaskFinish);
PregenState.this.fillPendingQueue(); PregenState.this.fillPendingQueue();
}) })
@@ -143,14 +150,16 @@ public class PregenManager
this.fullDataSourceProvider.getAsync(nextSectionPos) this.fullDataSourceProvider.getAsync(nextSectionPos)
.thenAccept(fullDataSource -> .thenAccept(fullDataSource ->
{ {
if (this.fullDataSourceProvider.isFullyGenerated(fullDataSource.columnGenerationSteps)) if (this.fullDataSourceProvider.generationStepsAreFullyGenerated(fullDataSource.columnGenerationSteps))
{ {
this.pendingGenerations.invalidate(fullDataSource.getPos()); this.pendingGenerations.invalidate(fullDataSource.getPos());
} }
else else
{ {
this.fullDataSourceProvider.queuePositionForRetrieval(fullDataSource.getPos()).thenAccept(result -> { this.fullDataSourceProvider.queuePositionForRetrieval(fullDataSource.getPos())
if (!result.success) .thenAccept((DataSourceRetrievalResult result) ->
{
if (result.state == ERetrievalResultState.FAIL)
{ {
LOGGER.warn("Failed to generate section " + DhSectionPos.toString(result.pos)); LOGGER.warn("Failed to generate section " + DhSectionPos.toString(result.pos));
} }
@@ -1,8 +1,8 @@
package com.seibel.distanthorizons.core.generation; package com.seibel.distanthorizons.core.generation;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; import com.seibel.distanthorizons.core.generation.tasks.ERetrievalResultState;
import com.seibel.distanthorizons.core.level.DhClientLevel; import com.seibel.distanthorizons.core.level.DhClientLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.multiplayer.client.AbstractFullDataNetworkRequestQueue; import com.seibel.distanthorizons.core.multiplayer.client.AbstractFullDataNetworkRequestQueue;
@@ -11,12 +11,11 @@ import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.WorldGenUtil;
import com.seibel.distanthorizons.core.util.objects.RollingAverage; import com.seibel.distanthorizons.core.util.objects.RollingAverage;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.*; import java.util.concurrent.*;
public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQueue implements IFullDataSourceRetrievalQueue, IDebugRenderable public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQueue implements IFullDataSourceRetrievalQueue, IDebugRenderable
@@ -53,46 +52,28 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue
public byte highestDataDetail() { return LodUtil.BLOCK_DETAIL_LEVEL; } public byte highestDataDetail() { return LodUtil.BLOCK_DETAIL_LEVEL; }
@Override @Override
public CompletableFuture<WorldGenResult> submitRetrievalTask(long sectionPos, byte requiredDataDetail, IWorldGenTaskTracker tracker) public CompletableFuture<DataSourceRetrievalResult> submitRetrievalTask(long sectionPos, byte requiredDataDetail)
{ {
long generationStartMsTime = System.currentTimeMillis(); long generationStartMsTime = System.currentTimeMillis();
return super.submitRequest(sectionPos, fullDataSource -> { CompletableFuture<DataSourceRetrievalResult> future = super.submitRequest(sectionPos, /* client timestamp */null);
Objects.requireNonNull(tracker.getDataSourceConsumer()).accept(fullDataSource); future.thenAccept((DataSourceRetrievalResult result) ->
fullDataSource.close(); {
}) if (result.state == ERetrievalResultState.SUCCESS)
.thenApply(requestResult ->
{ {
long totalGenTimeInMs = System.currentTimeMillis() - generationStartMsTime; long totalGenTimeInMs = System.currentTimeMillis() - generationStartMsTime;
int chunkWidth = DhSectionPos.getChunkWidth(sectionPos); int chunkWidth = DhSectionPos.getChunkWidth(sectionPos);
int chunkCount = chunkWidth * chunkWidth; int chunkCount = chunkWidth * chunkWidth;
double timePerChunk = (double)totalGenTimeInMs / (double)chunkCount; double timePerChunk = (double) totalGenTimeInMs / (double) chunkCount;
this.rollingAverageChunkGenTimeInMs.addValue(timePerChunk);
switch (requestResult) // only add the time on successes
{ // it won't be a perfect estimate but fails will often come back faster, skewing the time faster
case SUCCEEDED: this.rollingAverageChunkGenTimeInMs.add(timePerChunk);
return WorldGenResult.CreateSuccess(sectionPos);
case FAILED:
return WorldGenResult.CreateFail();
case REQUIRES_SPLITTING:
List<CompletableFuture<WorldGenResult>> childFutures = new ArrayList<>(4);
DhSectionPos.forEachChild(sectionPos, childPos -> {
tracker.shouldGenerateSplitChild(childPos).thenAccept(shouldGenerate -> {
if (shouldGenerate)
{
childFutures.add(this.submitRetrievalTask(childPos, requiredDataDetail, tracker));
} }
}); });
}); return future;
return WorldGenResult.CreateSplit(childFutures);
}
LodUtil.assertNotReach("Unexpected and unhandled request response result: ["+requestResult+"]");
return WorldGenResult.CreateFail();
});
} }
@Override @Override
@@ -108,14 +89,16 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue
@Override @Override
protected int getRequestRateLimit() { return this.networkState.sessionConfig.getGenerationRequestRateLimit(); } protected int getRequestRateLimit() { return this.networkState.sessionConfig.getGenerationRequestRateLimit(); }
@Override @Override
protected boolean isSectionAllowedToGenerate(long sectionPos, DhBlockPos2D targetPos) protected boolean sectionInAllowedGenerationRadius(long sectionPos, DhBlockPos2D targetPos)
{ {
if (this.networkState.sessionConfig.getGenerationBoundsRadius() > 0) if (this.networkState.sessionConfig.getGenerationMaxChunkRadius() > 0)
{ {
if (DhSectionPos.getChebyshevSignedBlockDistance(sectionPos, new DhBlockPos2D( boolean posInRange = WorldGenUtil.isPosInWorldGenRange(
this.networkState.sessionConfig.getGenerationBoundsX(), sectionPos,
this.networkState.sessionConfig.getGenerationBoundsZ() this.networkState.sessionConfig.getGenerationCenterChunkX(), this.networkState.sessionConfig.getGenerationCenterChunkZ(),
)) > this.networkState.sessionConfig.getGenerationBoundsRadius()) this.networkState.sessionConfig.getGenerationMaxChunkRadius()
);
if (!posInRange)
{ {
return false; return false;
} }
@@ -124,12 +107,13 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue
return DhSectionPos.getChebyshevSignedBlockDistance(sectionPos, targetPos) <= this.networkState.sessionConfig.getMaxGenerationRequestDistance() * 16; return DhSectionPos.getChebyshevSignedBlockDistance(sectionPos, targetPos) <= this.networkState.sessionConfig.getMaxGenerationRequestDistance() * 16;
} }
@Override @Override
protected boolean onBeforeRequest(long sectionPos, CompletableFuture<ERequestResult> future) protected boolean onBeforeRequest(long sectionPos, CompletableFuture<DataSourceRetrievalResult> future)
{ {
if (DhSectionPos.getDetailLevel(sectionPos) > DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL // split up large requests if N-sized gen isn't enabled
&& !Config.Server.Experimental.enableNSizedGeneration.get()) if (!Config.Server.Experimental.enableNSizedGeneration.get()
&& DhSectionPos.getDetailLevel(sectionPos) > DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL)
{ {
future.complete(ERequestResult.REQUIRES_SPLITTING); future.complete(DataSourceRetrievalResult.CreateSplit());
return false; return false;
} }
@@ -26,11 +26,8 @@ import com.seibel.distanthorizons.api.objects.data.DhApiChunk;
import com.seibel.distanthorizons.api.objects.data.IDhApiFullDataSource; import com.seibel.distanthorizons.api.objects.data.IDhApiFullDataSource;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.generation.tasks.InProgressWorldGenTaskGroup; import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalTask;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenTask;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenTaskGroup;
import com.seibel.distanthorizons.core.level.IDhServerLevel; import com.seibel.distanthorizons.core.level.IDhServerLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
@@ -40,6 +37,7 @@ import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.transformers.LodDataBuilder; import com.seibel.distanthorizons.core.dataObjects.transformers.LodDataBuilder;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.util.ExceptionUtil;
import com.seibel.distanthorizons.core.util.LodUtil.AssertFailureException; import com.seibel.distanthorizons.core.util.LodUtil.AssertFailureException;
import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
@@ -51,37 +49,25 @@ import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.world.DhApiWorldProxy; import com.seibel.distanthorizons.core.world.DhApiWorldProxy;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import java.awt.*; import java.awt.*;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.function.Consumer;
public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDebugRenderable public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDebugRenderable
{ {
private static final DhLogger LOGGER = new DhLoggerBuilder().build(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class); private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
/**
* Defines how many tasks can be queued per thread. <br><br>
*
* TODO the multiplier here should change dynamically based on how fast the generator is vs the queuing thread,
* if this is too high it may cause issues when moving,
* but if it is too low the generator threads won't have enough tasks to work on
*/
private static final int MAX_QUEUED_TASKS_PER_THREAD = 3;
private final IDhApiWorldGenerator generator; private final IDhApiWorldGenerator generator;
private final IDhServerLevel level; private final IDhServerLevel level;
/** contains the positions that need to be generated */ /** contains the positions that need to be generated */
private final ConcurrentHashMap<Long, WorldGenTask> waitingTasks = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Long, DataSourceRetrievalTask> waitingTasks = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, DataSourceRetrievalTask> inProgressGenTasksByLodPos = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, InProgressWorldGenTaskGroup> inProgressGenTasksByLodPos = new ConcurrentHashMap<>();
/** largest numerical detail level allowed */ /** largest numerical detail level allowed */
public final byte lowestDataDetail; public final byte lowestDataDetail;
@@ -110,9 +96,10 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
//==============// //=============//
// constructors // // constructor //
//==============// //=============//
///region constructor
public WorldGenerationQueue(IDhApiWorldGenerator generator, IDhServerLevel level) public WorldGenerationQueue(IDhApiWorldGenerator generator, IDhServerLevel level)
{ {
@@ -126,20 +113,29 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
LOGGER.info("Created world gen queue"); LOGGER.info("Created world gen queue");
} }
///endregion constructor
//=================//
// world generator // //===============//
// task handling // // task handling //
//=================// //===============//
///region task handling
@Override @Override
public CompletableFuture<WorldGenResult> submitRetrievalTask(long pos, byte requiredDataDetail, IWorldGenTaskTracker tracker) public CompletableFuture<DataSourceRetrievalResult> submitRetrievalTask(long pos, byte requiredDataDetail)
{ {
// the generator is shutting down, don't add new tasks // the generator is shutting down, don't add new tasks
if (this.generatorClosingFuture != null) if (this.generatorClosingFuture != null)
{ {
return CompletableFuture.completedFuture(WorldGenResult.CreateFail()); return CompletableFuture.completedFuture(DataSourceRetrievalResult.CreateFail());
}
// use the existing task if present
DataSourceRetrievalTask existingGenTask = this.waitingTasks.get(pos);
if (existingGenTask != null)
{
return existingGenTask.future;
} }
@@ -153,13 +149,12 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
requiredDataDetail = this.lowestDataDetail; requiredDataDetail = this.lowestDataDetail;
} }
// Assert that the data at least can fill in 1 single ChunkSizedFullDataAccessor // the request should be at least chunk-sized
LodUtil.assertTrue(DhSectionPos.getDetailLevel(pos) > requiredDataDetail + LodUtil.CHUNK_DETAIL_LEVEL); LodUtil.assertTrue(DhSectionPos.getDetailLevel(pos) > requiredDataDetail + LodUtil.CHUNK_DETAIL_LEVEL);
DataSourceRetrievalTask genTask = new DataSourceRetrievalTask(pos, requiredDataDetail);
CompletableFuture<WorldGenResult> future = new CompletableFuture<>(); this.waitingTasks.put(pos, genTask);
this.waitingTasks.put(pos, new WorldGenTask(pos, requiredDataDetail, tracker, future)); return genTask.future;
return future;
} }
@Override @Override
@@ -169,11 +164,17 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
{ {
if (removeIf.accept(genPos)) if (removeIf.accept(genPos))
{ {
this.waitingTasks.remove(genPos); DataSourceRetrievalTask removedTask = this.waitingTasks.remove(genPos);
if (removedTask != null)
{
// cancel tasks so any waiting future steps can be triggered
removedTask.future.cancel(true);
}
} }
}); });
} }
///endregion task handling
@@ -218,7 +219,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
while (!this.isGeneratorBusy() while (!this.isGeneratorBusy()
&& taskStarted) && taskStarted)
{ {
taskStarted = this.startNextWorldGenTask(this.generationTargetPos); taskStarted = this.tryStartNextWorldGenTask(this.generationTargetPos);
} }
} }
catch (Exception e) catch (Exception e)
@@ -240,15 +241,15 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
return true; return true;
} }
// queue more tasks if any of the threads are available
int worldGenThreadCount = Math.max(Config.Common.MultiThreading.numberOfThreads.get(), 1); int worldGenThreadCount = Math.max(Config.Common.MultiThreading.numberOfThreads.get(), 1);
int maxWorldGenTaskCount = worldGenThreadCount * MAX_QUEUED_TASKS_PER_THREAD; return this.inProgressGenTasksByLodPos.size() > worldGenThreadCount;
return executor.getQueueSize() > maxWorldGenTaskCount;
} }
/** /**
* @param targetPos the position to center the generation around * @param targetPos the position to center the generation around
* @return false if no tasks were found to generate * @return false if no tasks were found to generate
*/ */
private boolean startNextWorldGenTask(DhBlockPos2D targetPos) private boolean tryStartNextWorldGenTask(DhBlockPos2D targetPos)
{ {
if (this.waitingTasks.isEmpty()) if (this.waitingTasks.isEmpty())
{ {
@@ -256,143 +257,139 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
} }
// find the closest task
Mapper closestTaskMap = this.waitingTasks.reduceEntries(1024, TaskDistancePair closestTaskPair = this.waitingTasks.reduceEntries(1024,
entry -> new Mapper(entry.getValue(), DhSectionPos.getSectionBBoxPos(entry.getValue().pos).getCenterBlockPos().toPos2D().chebyshevDist(targetPos.toPos2D())), // get the target distance for each task
(aMapper, bMapper) -> aMapper.dist < bMapper.dist ? aMapper : bMapper); (Map.Entry<Long, DataSourceRetrievalTask> entry) ->
if (closestTaskMap == null)
{ {
// FIXME concurrency issue DataSourceRetrievalTask task = entry.getValue();
int distance = DhSectionPos.getCenterBlockPos(task.pos).chebyshevDist(targetPos);
return new TaskDistancePair(entry.getValue(), distance);
},
// find the closest task
(TaskDistancePair aTaskPair, TaskDistancePair bTaskPair) ->
{
return (aTaskPair.dist < bTaskPair.dist) ? aTaskPair : bTaskPair;
});
if (closestTaskPair == null)
{
// the waitingTasks was modified while this check was running
return false; return false;
} }
DataSourceRetrievalTask closestTask = closestTaskPair.task;
WorldGenTask closestTask = closestTaskMap.task;
// remove the task we found, we are going to start it and don't want to run it multiple times // remove the task we found, we are going to start it and don't want to run it multiple times
this.waitingTasks.remove(closestTask.pos, closestTask); this.waitingTasks.remove(closestTask.pos, closestTask);
// do we need to modify this task to generate it? // do we need to modify this task to generate it?
if (this.canGeneratePos(closestTask.pos)) if (this.canGenerateDetailLevel(DhSectionPos.getDetailLevel(closestTask.pos)))
{ {
// detail level is correct for generation, start generation // detail level is correct for generation, start generation
WorldGenTaskGroup closestTaskGroup = new WorldGenTaskGroup(closestTask.pos, (byte)(closestTask.pos - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL)); DataSourceRetrievalTask existingTask = this.inProgressGenTasksByLodPos.get(closestTask.pos);
closestTaskGroup.worldGenTasks.add(closestTask); if (existingTask == null)
if (!this.inProgressGenTasksByLodPos.containsKey(closestTask.pos))
{ {
// no task exists for this position, start one // no task exists for this position, start one
InProgressWorldGenTaskGroup newTaskGroup = new InProgressWorldGenTaskGroup(closestTaskGroup); this.startWorldGenTaskGroup(closestTask);
boolean taskStarted = this.tryStartingWorldGenTaskGroup(newTaskGroup);
if (!taskStarted)
{
//LOGGER.trace("Unable to start task: "+closestTask.pos+", skipping. Task position may have already been generated.");
}
} }
else else
{ {
// TODO replace the previous inProgress task if one exists // shouldn't normally happen, but if
// Note: Due to concurrency reasons, even if the currently running task is compatible with // we somehow queued the same task twice:
// the newly selected task, we cannot use it, // merge the two futures so they both complete
// as some chunks may have already been written into.
//LOGGER.trace("A task already exists for this position, todo: "+closestTask.pos); existingTask.future.thenApply((DataSourceRetrievalResult result)->
{
closestTask.future.complete(result);
return closestTask.future; // return value ignored
});
existingTask.future.exceptionally((Throwable throwable)->
{
closestTask.future.completeExceptionally(throwable);
return null; // return value ignored
});
} }
// a task has been started
return true;
} }
else else
{ {
// detail level is too high (if the detail level was too low, the generator would've ignored the request), // detail level is too high (if the detail level was too low, the generator would've ignored the request),
// split up the task // split up the task
closestTask.future.complete(DataSourceRetrievalResult.CreateSplit());
}
// split up the task and add each one to the tree
LinkedList<CompletableFuture<WorldGenResult>> childFutures = new LinkedList<>();
long sectionPos = closestTask.pos;
WorldGenTask finalClosestTask = closestTask;
DhSectionPos.forEachChild(sectionPos, (childDhSectionPos) ->
{
CompletableFuture<WorldGenResult> newFuture = new CompletableFuture<>();
childFutures.add(newFuture);
WorldGenTask newGenTask = new WorldGenTask(childDhSectionPos, DhSectionPos.getDetailLevel(childDhSectionPos), finalClosestTask.taskTracker, newFuture); // a task has been started or queued,
this.waitingTasks.put(newGenTask.pos, newGenTask); // queue another task
});
// send the child futures to the future recipient, to notify them of the new tasks
closestTask.future.complete(WorldGenResult.CreateSplit(childFutures));
// return true so we attempt to generate again
return true; return true;
} }
} private boolean canGenerateDetailLevel(byte taskDetailLevel)
/** @return true if the task was started, false otherwise */
private boolean tryStartingWorldGenTaskGroup(InProgressWorldGenTaskGroup newTaskGroup)
{ {
byte taskDetailLevel = newTaskGroup.group.dataDetail; byte requestedDetailLevel = (byte) (taskDetailLevel - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
long taskPos = newTaskGroup.group.pos; return (this.highestDataDetail <= requestedDetailLevel && requestedDetailLevel <= this.lowestDataDetail);
LodUtil.assertTrue(taskDetailLevel >= this.highestDataDetail && taskDetailLevel <= this.lowestDataDetail); }
private void startWorldGenTaskGroup(DataSourceRetrievalTask worldGenTask)
int generationRequestChunkWidthCount = BitShiftUtil.powerOfTwo(DhSectionPos.getDetailLevel(taskPos) - taskDetailLevel - 4); // minus 4 is equal to dividing by 16 to convert to chunk scale {
long taskPos = worldGenTask.pos;
LodUtil.assertTrue(
worldGenTask.requestDetailLevel >= this.highestDataDetail
&& worldGenTask.requestDetailLevel <= this.lowestDataDetail,
"World gen task started that isn't within the range that the generator can create.");
long generationStartMsTime = System.currentTimeMillis(); long generationStartMsTime = System.currentTimeMillis();
CompletableFuture<Void> generationFuture = this.startGenerationEvent(taskPos, taskDetailLevel, generationRequestChunkWidthCount, newTaskGroup.group::consumeDataSource); CompletableFuture<FullDataSourceV2> generationFuture = this.startGenerationEvent(worldGenTask);
// calculate generation speed
generationFuture.thenRun(() -> generationFuture.thenRun(() ->
{ {
long totalGenTimeInMs = System.currentTimeMillis() - generationStartMsTime; long totalGenTimeInMs = System.currentTimeMillis() - generationStartMsTime;
int chunkCount = generationRequestChunkWidthCount * generationRequestChunkWidthCount; int chunkCount = worldGenTask.widthInChunks * worldGenTask.widthInChunks;
double timePerChunk = (double)totalGenTimeInMs / (double)chunkCount; double timePerChunk = (double)totalGenTimeInMs / (double)chunkCount;
this.rollingAverageChunkGenTimeInMs.addValue(timePerChunk); this.rollingAverageChunkGenTimeInMs.add(timePerChunk);
}); });
newTaskGroup.genFuture = generationFuture; generationFuture.handle((FullDataSourceV2 fullDataSource, Throwable exception) ->
LodUtil.assertTrue(newTaskGroup.genFuture != null);
newTaskGroup.genFuture.whenComplete((voidObj, exception) ->
{ {
try try
{ {
if (exception != null) if (exception != null)
{ {
// don't log the shutdown exceptions // don't log the shutdown exceptions
if (!LodUtil.isInterruptOrReject(exception)) if (!ExceptionUtil.isInterruptOrReject(exception))
{ {
LOGGER.error("Error generating data for pos: " + DhSectionPos.toString(taskPos), exception); LOGGER.error("Error generating data for pos: " + DhSectionPos.toString(taskPos), exception);
} }
newTaskGroup.group.worldGenTasks.forEach(worldGenTask -> worldGenTask.future.complete(WorldGenResult.CreateFail())); LodUtil.assertTrue(fullDataSource == null);
worldGenTask.future.complete(DataSourceRetrievalResult.CreateFail());
} }
else else
{ {
newTaskGroup.group.worldGenTasks.forEach(worldGenTask -> worldGenTask.future.complete(WorldGenResult.CreateSuccess(taskPos))); boolean taskRemoved = this.inProgressGenTasksByLodPos.remove(taskPos, worldGenTask);
LodUtil.assertTrue(taskRemoved, "Unable to find in progress generator task with position ["+DhSectionPos.toString(taskPos)+"]");
worldGenTask.future.complete(DataSourceRetrievalResult.CreateSuccess(taskPos, fullDataSource));
} }
boolean worked = this.inProgressGenTasksByLodPos.remove(taskPos, newTaskGroup);
LodUtil.assertTrue(worked, "Unable to find in progress generator task with position ["+DhSectionPos.toString(taskPos)+"]");
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.error("Unexpected error completing world gen task at pos: ["+DhSectionPos.toString(taskPos)+"].", e); LOGGER.error("Unexpected error completing world gen task at pos: ["+DhSectionPos.toString(taskPos)+"].", e);
worldGenTask.future.completeExceptionally(e);
} }
finally finally
{ {
this.tryQueueNewWorldGenRequestsAsync(); this.tryQueueNewWorldGenRequestsAsync();
} }
});
this.inProgressGenTasksByLodPos.put(taskPos, newTaskGroup); return null;
return true; });
} }
private CompletableFuture<Void> startGenerationEvent( private CompletableFuture<FullDataSourceV2> startGenerationEvent(DataSourceRetrievalTask task)
long requestPos,
byte targetDataDetail,
int generationRequestChunkWidthCount,
Consumer<FullDataSourceV2> dataSourceConsumer
)
{ {
DhChunkPos chunkPosMin = new DhChunkPos(DhSectionPos.getSectionBBoxPos(requestPos).getCornerBlockPos()); this.inProgressGenTasksByLodPos.put(task.pos, task);
DhChunkPos chunkPosMin = new DhChunkPos(new DhBlockPos2D(DhSectionPos.getMinCornerBlockX(task.pos), DhSectionPos.getMinCornerBlockZ(task.pos)));
EDhApiDistantGeneratorMode generatorMode = Config.Common.WorldGenerator.distantGeneratorMode.get(); EDhApiDistantGeneratorMode generatorMode = Config.Common.WorldGenerator.distantGeneratorMode.get();
EDhApiWorldGeneratorReturnType returnType = this.generator.getReturnType(); EDhApiWorldGeneratorReturnType returnType = this.generator.getReturnType();
@@ -400,10 +397,34 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
{ {
case VANILLA_CHUNKS: case VANILLA_CHUNKS:
{ {
return this.generator.generateChunks( return this.startVanillaChunkGenerationEvent(task, chunkPosMin, generatorMode);
}
case API_CHUNKS:
{
return this.startApiChunkGenerationEvent(task, chunkPosMin, generatorMode);
}
case API_DATA_SOURCES:
{
return this.startApiDataSourceGenerationEvent(task, chunkPosMin, generatorMode);
}
default:
{
Config.Common.WorldGenerator.enableDistantGeneration.set(false);
throw new AssertFailureException("Unknown return type: " + returnType);
}
}
}
private CompletableFuture<FullDataSourceV2> startVanillaChunkGenerationEvent(
DataSourceRetrievalTask task, DhChunkPos chunkPosMin, EDhApiDistantGeneratorMode generatorMode)
{
final CompletableFuture<FullDataSourceV2> returnFuture = new CompletableFuture<>();
ArrayList<IChunkWrapper> generatedChunks = new ArrayList<>(task.widthInChunks * task.widthInChunks);
CompletableFuture<Void> chunkGenFuture = this.generator.generateChunks(
chunkPosMin.getX(), chunkPosMin.getZ(), chunkPosMin.getX(), chunkPosMin.getZ(),
generationRequestChunkWidthCount, task.widthInChunks,
targetDataDetail, task.requestDetailLevel,
generatorMode, generatorMode,
ThreadPoolUtil.getWorldGenExecutor(), ThreadPoolUtil.getWorldGenExecutor(),
(Object[] generatedObjectArray) -> (Object[] generatedObjectArray) ->
@@ -411,18 +432,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
try try
{ {
IChunkWrapper chunkWrapper = WRAPPER_FACTORY.createChunkWrapper(generatedObjectArray); IChunkWrapper chunkWrapper = WRAPPER_FACTORY.createChunkWrapper(generatedObjectArray);
generatedChunks.add(chunkWrapper);
// TODO light data should be pulled (if possible) from the ChunkAccess object itself via ChunkFileReader.readLight
// but this should work for now
ArrayList<IChunkWrapper> nearbyChunkList = new ArrayList<IChunkWrapper>();
nearbyChunkList.add(chunkWrapper);
DhLightingEngine.INSTANCE.bakeChunkBlockLighting(chunkWrapper, nearbyChunkList, this.level.getLevelWrapper().hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT);
try (FullDataSourceV2 dataSource = LodDataBuilder.createFromChunk(this.level.getLevelWrapper(), chunkWrapper))
{
LodUtil.assertTrue(dataSource != null);
dataSourceConsumer.accept(dataSource);
}
} }
catch (ClassCastException e) catch (ClassCastException e)
{ {
@@ -436,87 +446,129 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
} }
} }
); );
}
case API_CHUNKS: chunkGenFuture.exceptionally((throwable) ->
{ {
return this.generator.generateApiChunks( returnFuture.completeExceptionally(throwable);
return null;
});
chunkGenFuture.thenRun(() ->
{
FullDataSourceV2 requestedDataSource = FullDataSourceV2.createEmpty(task.pos);
// process chunks //
for (int i = 0; i < generatedChunks.size(); i++)
{
IChunkWrapper chunkWrapper = generatedChunks.get(i);
// only light the chunk here if necessary,
// lighting before this point is preferred but for legacy API use this
// check should be done
if (!chunkWrapper.isDhBlockLightingCorrect())
{
ArrayList<IChunkWrapper> nearbyChunkList = new ArrayList<>();
nearbyChunkList.add(chunkWrapper);
byte maxSkyLight = this.level.getLevelWrapper().hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT;
DhLightingEngine.INSTANCE.bakeChunkBlockLighting(chunkWrapper, nearbyChunkList, maxSkyLight);
}
try (FullDataSourceV2 generatedDataSource = LodDataBuilder.createFromChunk(this.level.getLevelWrapper(), chunkWrapper))
{
LodUtil.assertTrue(generatedDataSource != null);
requestedDataSource.updateFromDataSource(generatedDataSource);
}
}
DhLightingEngine.INSTANCE.bakeDataSourceSkyLight(requestedDataSource, LodUtil.MAX_MC_LIGHT);
returnFuture.complete(requestedDataSource);
});
return returnFuture;
}
private CompletableFuture<FullDataSourceV2> startApiChunkGenerationEvent(
DataSourceRetrievalTask task, DhChunkPos chunkPosMin, EDhApiDistantGeneratorMode generatorMode)
{
final CompletableFuture<FullDataSourceV2> returnFuture = new CompletableFuture<>();
ArrayList<DhApiChunk> generatedChunks = new ArrayList<>(task.widthInChunks * task.widthInChunks);
CompletableFuture<Void> chunkGenFuture = this.generator.generateApiChunks(
chunkPosMin.getX(), chunkPosMin.getZ(), chunkPosMin.getX(), chunkPosMin.getZ(),
generationRequestChunkWidthCount, task.widthInChunks,
targetDataDetail, task.requestDetailLevel,
generatorMode, generatorMode,
ThreadPoolUtil.getWorldGenExecutor(), ThreadPoolUtil.getWorldGenExecutor(),
(DhApiChunk dataPoints) -> (DhApiChunk apiChunk) -> { generatedChunks.add(apiChunk); }
);
chunkGenFuture.exceptionally((throwable) ->
{ {
try(FullDataSourceV2 dataSource = LodDataBuilder.createFromApiChunkData(dataPoints, this.generator.runApiValidation())) returnFuture.completeExceptionally(throwable);
return null;
});
chunkGenFuture.thenRun(() ->
{ {
dataSourceConsumer.accept(dataSource); FullDataSourceV2 requestedDataSource = FullDataSourceV2.createEmpty(task.pos);
for (int i = 0; i < generatedChunks.size(); i++)
{
DhApiChunk apiChunk = generatedChunks.get(i);
try(FullDataSourceV2 generatedDataSource = LodDataBuilder.createFromApiChunkData(apiChunk, this.generator.runApiValidation()))
{
requestedDataSource.updateFromDataSource(generatedDataSource);
} }
catch (DataCorruptedException | IllegalArgumentException e) catch (DataCorruptedException | IllegalArgumentException e)
{ {
LOGGER.error("World generator returned a corrupt chunk. Error: [" + e.getMessage() + "]. World generator disabled.", e); LOGGER.error("World generator returned a corrupt API chunk. Error: [" + e.getMessage() + "]. World generator disabled.", e);
Config.Common.WorldGenerator.enableDistantGeneration.set(false);
}
catch (ClassCastException e)
{
LOGGER.error("World generator return type incorrect. Error: [" + e.getMessage() + "]. World generator disabled.", e);
Config.Common.WorldGenerator.enableDistantGeneration.set(false); Config.Common.WorldGenerator.enableDistantGeneration.set(false);
} }
} }
);
returnFuture.complete(requestedDataSource);
});
return returnFuture;
} }
case API_DATA_SOURCES: private CompletableFuture<FullDataSourceV2> startApiDataSourceGenerationEvent(
DataSourceRetrievalTask task, DhChunkPos chunkPosMin, EDhApiDistantGeneratorMode generatorMode)
{ {
final CompletableFuture<FullDataSourceV2> returnFuture = new CompletableFuture<>();
// done to reduce GC overhead // done to reduce GC overhead
FullDataSourceV2 pooledDataSource = FullDataSourceV2.createEmpty(requestPos); FullDataSourceV2 pooledDataSource = FullDataSourceV2.createEmpty(task.pos);
// set here so the API user doesn't have to pass in this value anywhere themselves // set here so the API user doesn't have to pass in this value anywhere themselves
pooledDataSource.setRunApiChunkValidation(this.generator.runApiValidation()); pooledDataSource.setRunApiSetterValidation(this.generator.runApiValidation());
// only apply to children if we aren't at the bottom of the tree // only apply to children if we aren't at the bottom of the tree
pooledDataSource.applyToChildren = DhSectionPos.getDetailLevel(pooledDataSource.getPos()) > DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL; pooledDataSource.applyToChildren = DhSectionPos.getDetailLevel(pooledDataSource.getPos()) > DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL;
pooledDataSource.applyToParent = DhSectionPos.getDetailLevel(pooledDataSource.getPos()) < DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL + 12; pooledDataSource.applyToParent = DhSectionPos.getDetailLevel(pooledDataSource.getPos()) < DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL + 12; // TODO what does this 12 reference?
CompletableFuture<Void> lodGenFuture = this.generator.generateLod(
return this.generator.generateLod(
chunkPosMin.getX(), chunkPosMin.getZ(), chunkPosMin.getX(), chunkPosMin.getZ(),
DhSectionPos.getX(requestPos), DhSectionPos.getZ(requestPos), DhSectionPos.getX(task.pos), DhSectionPos.getZ(task.pos),
(byte) (DhSectionPos.getDetailLevel(requestPos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL), (byte) (DhSectionPos.getDetailLevel(task.pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL),
pooledDataSource, pooledDataSource,
generatorMode, generatorMode,
ThreadPoolUtil.getWorldGenExecutor(), ThreadPoolUtil.getWorldGenExecutor(),
(IDhApiFullDataSource apiDataSource) -> (IDhApiFullDataSource apiDataSource) -> { }
{
try
{
FullDataSourceV2 fullDataSource = (FullDataSourceV2) apiDataSource;
try
{
dataSourceConsumer.accept(fullDataSource);
}
finally
{
fullDataSource.close();
}
}
catch (IllegalArgumentException e)
{
LOGGER.error("World generator returned a corrupt data source. Error: [" + e.getMessage() + "]. World generator disabled.", e);
Config.Common.WorldGenerator.enableDistantGeneration.set(false);
}
catch (ClassCastException e)
{
LOGGER.error("World generator return type incorrect. Error: [" + e.getMessage() + "]. World generator disabled.", e);
Config.Common.WorldGenerator.enableDistantGeneration.set(false);
}
}
); );
}
default:
lodGenFuture.exceptionally((throwable) ->
{ {
Config.Common.WorldGenerator.enableDistantGeneration.set(false); returnFuture.completeExceptionally(throwable);
throw new AssertFailureException("Unknown return type: " + returnType); pooledDataSource.close();
} return null;
} });
lodGenFuture.thenRun(() ->
{
returnFuture.complete(pooledDataSource);
});
return returnFuture;
} }
@@ -524,24 +576,19 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
//===================// //===================//
// getters / setters // // getters / setters //
//===================// //===================//
///region getters/setters
@Override public int getWaitingTaskCount() { return this.waitingTasks.size(); } @Override public int getWaitingTaskCount() { return this.waitingTasks.size(); }
@Override public int getInProgressTaskCount() { return this.inProgressGenTasksByLodPos.size(); } @Override public int getInProgressTaskCount() { return this.inProgressGenTasksByLodPos.size(); }
@Override @Override public byte lowestDataDetail() { return this.lowestDataDetail; }
public byte lowestDataDetail() { return this.lowestDataDetail; } @Override public byte highestDataDetail() { return this.highestDataDetail; }
@Override
public byte highestDataDetail() { return this.highestDataDetail; }
@Override @Override public int getEstimatedRemainingTaskCount() { return this.estimatedRemainingTaskCount; }
public int getEstimatedRemainingTaskCount() { return this.estimatedRemainingTaskCount; } @Override public void setEstimatedRemainingTaskCount(int newEstimate) { this.estimatedRemainingTaskCount = newEstimate; }
@Override
public void setEstimatedRemainingTaskCount(int newEstimate) { this.estimatedRemainingTaskCount = newEstimate; }
@Override @Override public int getRetrievalEstimatedRemainingChunkCount() { return this.estimatedRemainingChunkCount; }
public int getRetrievalEstimatedRemainingChunkCount() { return this.estimatedRemainingChunkCount; } @Override public void setRetrievalEstimatedRemainingChunkCount(int newEstimate) { this.estimatedRemainingChunkCount = newEstimate; }
@Override
public void setRetrievalEstimatedRemainingChunkCount(int newEstimate) { this.estimatedRemainingChunkCount = newEstimate; }
@Override @Override
public void addDebugMenuStringsToList(List<String> messageList) { } public void addDebugMenuStringsToList(List<String> messageList) { }
@@ -559,13 +606,55 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
return chunkCount; return chunkCount;
} }
///endregion getters/setters
//=======//
// debug //
//=======//
///region debug
@Override
public void debugRender(DebugRenderer renderer)
{
int levelMinY = this.level.getLevelWrapper().getMinHeight();
int levelMaxY = this.level.getLevelWrapper().getMaxHeight();
// show the wireframe a bit lower than world max height,
// since most worlds don't render all the way up to the max height
int levelHeightRange = (levelMaxY - levelMinY);
int maxY = levelMaxY - (levelHeightRange / 2);
// blue - queued
this.waitingTasks.keySet().forEach((Long pos) ->
{
renderer.renderBox(
new DebugRenderer.Box(pos, levelMinY, maxY, 0.05f, Color.blue)
);
});
// red - in progress
this.inProgressGenTasksByLodPos.forEach((Long pos, DataSourceRetrievalTask task) ->
{
renderer.renderBox(
new DebugRenderer.Box(pos, levelMinY, maxY, 0.05f, Color.red)
);
});
}
///endregion debug
//==========// //==========//
// shutdown // // shutdown //
//==========// //==========//
///region shutdown
@Override public CompletableFuture<Void> startClosingAsync(boolean cancelCurrentGeneration, boolean alsoInterruptRunning) @Override
public CompletableFuture<Void> startClosingAsync(boolean cancelCurrentGeneration, boolean alsoInterruptRunning)
{ {
LOGGER.info("Closing world gen queue"); LOGGER.info("Closing world gen queue");
this.queueingThread.shutdownNow(); this.queueingThread.shutdownNow();
@@ -573,32 +662,32 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
// stop and remove any in progress tasks // stop and remove any in progress tasks
ArrayList<CompletableFuture<Void>> inProgressTasksCancelingFutures = new ArrayList<>(this.inProgressGenTasksByLodPos.size()); ArrayList<CompletableFuture<Void>> inProgressTasksCancelingFutures = new ArrayList<>(this.inProgressGenTasksByLodPos.size());
this.inProgressGenTasksByLodPos.values().forEach(runningTaskGroup -> this.inProgressGenTasksByLodPos.values().forEach((DataSourceRetrievalTask genTask) ->
{ {
CompletableFuture<Void> genFuture = runningTaskGroup.genFuture; // Do this to prevent it getting swapped out CompletableFuture<DataSourceRetrievalResult> genFuture = genTask.future;
if (genFuture == null)
{
// genFuture's shouldn't be null, but sometimes they are...
LOGGER.info("Null gen future: "+runningTaskGroup.group.pos);
return;
}
if (cancelCurrentGeneration) if (cancelCurrentGeneration)
{ {
genFuture.cancel(alsoInterruptRunning); genFuture.cancel(alsoInterruptRunning);
} }
inProgressTasksCancelingFutures.add(genFuture.handle((voidObj, exception) -> inProgressTasksCancelingFutures.add(genFuture.handle((DataSourceRetrievalResult result, Throwable throwable) ->
{ {
if (exception instanceof CompletionException) if (throwable instanceof CompletionException)
{ {
exception = exception.getCause(); throwable = throwable.getCause();
} }
if (!UncheckedInterruptedException.isInterrupt(exception) && !(exception instanceof CancellationException)) if (!UncheckedInterruptedException.isInterrupt(throwable)
&& !(throwable instanceof CancellationException))
{ {
LOGGER.error("Error when terminating data generation for section " + runningTaskGroup.group.pos, exception); LOGGER.error("Error when terminating data generation for pos: ["+DhSectionPos.toString(genTask.pos)+"], error: ["+throwable.getMessage()+"].", throwable);
}
if (result != null
&& result.dataSource != null)
{
result.dataSource.close();
} }
return null; return null;
@@ -623,13 +712,17 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
LOGGER.info("Shutting down world generator thread pool..."); LOGGER.info("Shutting down world generator thread pool...");
AbstractExecutorService executor = ThreadPoolUtil.getWorldGenExecutor(); PriorityTaskPicker.Executor executor = ThreadPoolUtil.getWorldGenExecutor();
if (executor != null) if (executor != null)
{ {
List<Runnable> tasks = executor.shutdownNow(); int queueSize = executor.getQueueSize();
LOGGER.info("World generator thread pool shutdown with [" + tasks.size() + "] incomplete tasks."); executor.clearQueue();
LOGGER.info("World generator thread pool shutdown with [" + queueSize + "] incomplete tasks.");
} }
this.inProgressGenTasksByLodPos.values().forEach((inProgressWorldGenTaskGroup) -> inProgressWorldGenTaskGroup.future.cancel(true));
this.waitingTasks.values().forEach((worldGenTask) -> worldGenTask.future.cancel(true));
this.generator.close(); this.generator.close();
DebugRenderer.unregister(this, Config.Client.Advanced.Debugging.DebugWireframe.showWorldGenQueue); DebugRenderer.unregister(this, Config.Client.Advanced.Debugging.DebugWireframe.showWorldGenQueue);
@@ -648,62 +741,22 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
LOGGER.info("Finished closing " + WorldGenerationQueue.class.getSimpleName()); LOGGER.info("Finished closing " + WorldGenerationQueue.class.getSimpleName());
} }
///endregion shutdown
//=======//
// debug //
//=======//
@Override
public void debugRender(DebugRenderer renderer)
{
int levelMinY = this.level.getLevelWrapper().getMinHeight();
int levelMaxY = this.level.getLevelWrapper().getMaxHeight();
// show the wireframe a bit lower than world max height,
// since most worlds don't render all the way up to the max height
int levelHeightRange = (levelMaxY - levelMinY);
int maxY = levelMaxY - (levelHeightRange / 2);
// blue - queued
this.waitingTasks.keySet().forEach((pos) ->
{
renderer.renderBox(
new DebugRenderer.Box(pos, levelMinY, maxY, 0.05f, Color.blue));
});
// red - in progress
this.inProgressGenTasksByLodPos.forEach((pos, t) ->
{
renderer.renderBox(
new DebugRenderer.Box(pos, levelMinY, maxY, 0.05f, Color.red));
});
}
//================//
// helper methods //
//================//
private boolean canGeneratePos(long taskPos)
{
byte requestedDetailLevel = (byte) (DhSectionPos.getDetailLevel(taskPos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
return (this.highestDataDetail <= requestedDetailLevel && requestedDetailLevel <= this.lowestDataDetail);
}
//================// //================//
// helper classes // // helper classes //
//================// //================//
///region helper classes
private static class Mapper /** Used during task starting to determine the closest task */
private static class TaskDistancePair
{ {
public final WorldGenTask task; public final DataSourceRetrievalTask task;
public final int dist; public final int dist;
public Mapper(WorldGenTask task, int dist)
public TaskDistancePair(DataSourceRetrievalTask task, int dist)
{ {
this.task = task; this.task = task;
this.dist = dist; this.dist = dist;
@@ -711,4 +764,8 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
} }
///endregion helper classes
} }
@@ -0,0 +1,53 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.generation.tasks;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import org.jetbrains.annotations.Nullable;
/**
* @see DataSourceRetrievalTask
*/
public class DataSourceRetrievalResult
{
public final ERetrievalResultState state;
/** the position that was generated, will be null if nothing was generated */
public final long pos;
@Nullable
public final FullDataSourceV2 dataSource;
//==============//
// constructors //
//==============//
public static DataSourceRetrievalResult CreateSplit() { return new DataSourceRetrievalResult(ERetrievalResultState.REQUIRES_SPLITTING, 0, null); }
public static DataSourceRetrievalResult CreateFail() { return new DataSourceRetrievalResult(ERetrievalResultState.FAIL, 0, null); }
public static DataSourceRetrievalResult CreateSuccess(long pos, FullDataSourceV2 generatedDataSource) { return new DataSourceRetrievalResult(ERetrievalResultState.SUCCESS, pos, generatedDataSource); }
private DataSourceRetrievalResult(ERetrievalResultState state, long pos, @Nullable FullDataSourceV2 dataSource)
{
this.state = state;
this.pos = pos;
this.dataSource = dataSource;
}
}
@@ -19,29 +19,37 @@
package com.seibel.distanthorizons.core.generation.tasks; package com.seibel.distanthorizons.core.generation.tasks;
import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
/** /**
* @author Leetom * @see DataSourceRetrievalResult
* @version 2022-11-25
*/ */
public final class WorldGenTask public final class DataSourceRetrievalTask
{ {
public final long pos; public final long pos;
public final byte dataDetailLevel; /**
public final IWorldGenTaskTracker taskTracker; * Usually the same as {@link DataSourceRetrievalTask#pos}, but
public final CompletableFuture<WorldGenResult> future; * can differ if the task needs something different.
*/
public final byte requestDetailLevel;
public final int widthInChunks;
public final CompletableFuture<DataSourceRetrievalResult> future = new CompletableFuture<>();
public WorldGenTask(long pos, byte dataDetail, IWorldGenTaskTracker taskTracker, CompletableFuture<WorldGenResult> future) //=============//
// constructor //
//=============//
public DataSourceRetrievalTask(long pos, byte dataDetail)
{ {
this.dataDetailLevel = dataDetail;
this.pos = pos; this.pos = pos;
this.taskTracker = taskTracker; this.requestDetailLevel = dataDetail;
this.future = future; this.widthInChunks = BitShiftUtil.powerOfTwo(DhSectionPos.getDetailLevel(this.pos) - this.requestDetailLevel - 4); // minus 4 is equal to dividing by 16 to convert to chunk scale
} }
} }
@@ -0,0 +1,15 @@
package com.seibel.distanthorizons.core.generation.tasks;
/**
* SUCCESS <br>
* REQUIRES_SPLITTING <br>
* FAIL <br>
*
* @see DataSourceRetrievalResult
*/
public enum ERetrievalResultState
{
SUCCESS,
REQUIRES_SPLITTING,
FAIL,
}
@@ -1,39 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.generation.tasks;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import javax.annotation.Nullable;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
/**
* @author Leetom
* @version 2022-11-25
*/
public interface IWorldGenTaskTracker
{
@Nullable
Consumer<FullDataSourceV2> getDataSourceConsumer();
CompletableFuture<Boolean> shouldGenerateSplitChild(long pos);
}
@@ -1,51 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.generation.tasks;
import java.util.Collection;
import java.util.LinkedList;
import java.util.concurrent.CompletableFuture;
public class WorldGenResult
{
/** true if terrain was generated */
public final boolean success;
/** the position that was generated, will be null if nothing was generated */
public final long pos;
/** if a position is too high detail for world generator to handle it, these futures are for its 4 children positions after being split up. */
public final LinkedList<CompletableFuture<WorldGenResult>> childFutures = new LinkedList<>();
public static WorldGenResult CreateSplit(Collection<CompletableFuture<WorldGenResult>> siblingFutures) { return new WorldGenResult(false, 0, siblingFutures); }
public static WorldGenResult CreateFail() { return new WorldGenResult(false, 0, null); }
public static WorldGenResult CreateSuccess(long pos) { return new WorldGenResult(true, pos, null); }
private WorldGenResult(boolean success, long pos, Collection<CompletableFuture<WorldGenResult>> childFutures)
{
this.success = success;
this.pos = pos;
if (childFutures != null)
{
this.childFutures.addAll(childFutures);
}
}
}
@@ -1,67 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.generation.tasks;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import java.util.Iterator;
import java.util.LinkedList;
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 long pos;
public byte dataDetail;
/** Only accessed by the generator polling thread */
public final LinkedList<WorldGenTask> worldGenTasks = new LinkedList<>();
public WorldGenTaskGroup(long pos, byte dataDetail)
{
this.pos = pos;
this.dataDetail = dataDetail;
}
public void consumeDataSource(FullDataSourceV2 dataSource)
{
Iterator<WorldGenTask> tasks = this.worldGenTasks.iterator();
while (tasks.hasNext())
{
WorldGenTask task = tasks.next();
Consumer<FullDataSourceV2> dataSourceConsumer = task.taskTracker.getDataSourceConsumer();
if (dataSourceConsumer == null)
{
tasks.remove();
task.future.complete(WorldGenResult.CreateFail());
}
else
{
dataSourceConsumer.accept(dataSource);
}
}
}
}
@@ -23,6 +23,7 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO; import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.sql.repo.FullDataSourceV2Repo; import com.seibel.distanthorizons.core.sql.repo.FullDataSourceV2Repo;
import com.seibel.distanthorizons.core.util.NativeDialogUtil;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.core.jar.gui.BaseJFrame; import com.seibel.distanthorizons.core.jar.gui.BaseJFrame;
import com.seibel.distanthorizons.core.jar.gui.cusomJObject.JBox; import com.seibel.distanthorizons.core.jar.gui.cusomJObject.JBox;
@@ -31,7 +32,6 @@ import com.seibel.distanthorizons.core.jar.installer.WebDownloader;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.LoggerContext;
import org.lwjgl.util.tinyfd.TinyFileDialogs;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
@@ -216,7 +216,7 @@ public class JarMain
{ {
repo = new FullDataSourceV2Repo(FullDataSourceV2Repo.DEFAULT_DATABASE_TYPE, dbFile); repo = new FullDataSourceV2Repo(FullDataSourceV2Repo.DEFAULT_DATABASE_TYPE, dbFile);
} }
catch (SQLException e) catch (SQLException | IOException e)
{ {
LOGGER.error("Failed to initialize connection with database: ["+exportFile.getAbsolutePath()+"], error: ["+e.getMessage()+"].", e); LOGGER.error("Failed to initialize connection with database: ["+exportFile.getAbsolutePath()+"], error: ["+e.getMessage()+"].", e);
return; return;
@@ -441,7 +441,7 @@ public class JarMain
installMod.addActionListener(e -> { installMod.addActionListener(e -> {
if (minecraftDirPop.getSelectedFile() == null) if (minecraftDirPop.getSelectedFile() == null)
{ {
TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, "Please select your install directory", "ok", "warning", false); NativeDialogUtil.showDialog(ModInfo.READABLE_NAME, "Please select your install directory", "ok", "warning");
return; return;
} }
@@ -455,11 +455,11 @@ public class JarMain
ModInfo.NAME + "-" + ModrinthGetter.releaseNames.get(downloadID.get()) + ".jar" ModInfo.NAME + "-" + ModrinthGetter.releaseNames.get(downloadID.get()) + ".jar"
).toFile()); ).toFile());
TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, "Installation done. \nYou can now close the installer", "ok", "info", false); NativeDialogUtil.showDialog(ModInfo.READABLE_NAME, "Installation done. \nYou can now close the installer", "ok", "info");
} }
catch (Exception f) catch (Exception f)
{ {
TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, "Download failed. Check your internet connection \nStacktrace: " + f.getMessage(), "error", "info", false); NativeDialogUtil.showDialog(ModInfo.READABLE_NAME, "Download failed. Check your internet connection \nStacktrace: " + f.getMessage(), "error", "info");
} }
}); });
frame.add(installMod); frame.add(installMod);
@@ -49,7 +49,7 @@ public class GitlabGetter
/** Commit sha; Commit info */ /** Commit sha; Commit info */
private static final Map<String, Config> commitInfo = new HashMap<>(); private static final Map<String, Config> commitInfo = new HashMap<>();
/** Pipeline ID; Pipeline info */ /** Pipeline ID; Pipeline info */
private static final Map<Integer, ArrayList<Config>> pipelineInfo = new HashMap<>(); private static final Map<Number, ArrayList<Config>> pipelineInfo = new HashMap<>();
/** Uses our projectID to init this */ /** Uses our projectID to init this */
public GitlabGetter() public GitlabGetter()
@@ -88,7 +88,7 @@ public class GitlabGetter
return commitInfo.get(commit); return commitInfo.get(commit);
} }
public ArrayList<Config> getPipelineInfo(int pipeline) public ArrayList<Config> getPipelineInfo(Number pipeline)
{ {
if (!pipelineInfo.containsKey(pipeline)) if (!pipelineInfo.containsKey(pipeline))
{ {
@@ -111,9 +111,10 @@ public class GitlabGetter
/** /**
* Gets all the Minecraft download links to a pipeline ID * Gets all the Minecraft download links to a pipeline ID
* *
* @param pipelineID Uses {@link Number} instead of a specific value due to the possibility of receiving Integer or Long
* @return Minecraft version; Download URL * @return Minecraft version; Download URL
*/ */
public Map<String, URL> getDownloads(int pipelineID) public Map<String, URL> getDownloads(Number pipelineID)
{ {
Map<String, URL> downloads = new HashMap<>(); Map<String, URL> downloads = new HashMap<>();
ArrayList<Config> currentPipelineInfo = this.getPipelineInfo(pipelineID); ArrayList<Config> currentPipelineInfo = this.getPipelineInfo(pipelineID);
@@ -29,12 +29,12 @@ import com.seibel.distanthorizons.core.jar.installer.ModrinthGetter;
import com.seibel.distanthorizons.core.jar.installer.WebDownloader; import com.seibel.distanthorizons.core.jar.installer.WebDownloader;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen; import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.util.NativeDialogUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.IVersionConstants; import com.seibel.distanthorizons.core.wrapperInterfaces.IVersionConstants;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.coreapi.util.StringUtil; import com.seibel.distanthorizons.coreapi.util.StringUtil;
import com.seibel.distanthorizons.coreapi.util.jar.DeleteOnUnlock; import com.seibel.distanthorizons.coreapi.util.jar.DeleteOnUnlock;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.lwjgl.util.tinyfd.TinyFileDialogs;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
@@ -174,7 +174,7 @@ public class SelfUpdater
if (!GitlabGetter.INSTANCE.getDownloads(pipeline.get("id")).containsKey(mcVersion)) if (!GitlabGetter.INSTANCE.getDownloads(pipeline.get("id")).containsKey(mcVersion))
{ {
LOGGER.warn("Minecraft version ["+ mcVersion +"] is not findable on Gitlab, findable versions are ["+ StringUtil.join(",", GitlabGetter.INSTANCE.getDownloads(pipeline.get("id")).keySet().toArray()) +"]."); LOGGER.warn("Minecraft version ["+ mcVersion +"] is not findable on Gitlab, findable versions are ["+ StringUtil.join(", ", GitlabGetter.INSTANCE.getDownloads(pipeline.get("id")).keySet().toArray()) +"].");
return false; return false;
} }
@@ -258,14 +258,13 @@ public class SelfUpdater
deleteOldJarOnJvmShutdown = true; deleteOldJarOnJvmShutdown = true;
// TODO one of these messages contains something TinyFd doesn't like, find it and fix it String successMessage = "Distant Horizons successfully updated. It will apply on game`s relaunch";
String successMessage = "Distant Horizons successfully updated. It will apply on game's relaunch";
LOGGER.info(successMessage); LOGGER.info(successMessage);
new Thread(() -> new Thread(() ->
{ {
try try
{ {
TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, successMessage, "ok", "info", false); NativeDialogUtil.showDialog(ModInfo.READABLE_NAME, successMessage, "ok", "info");
} }
catch (Exception ignore) { } catch (Exception ignore) { }
}).start(); }).start();
@@ -288,7 +287,7 @@ public class SelfUpdater
LOGGER.error(failMessage, e); LOGGER.error(failMessage, e);
try try
{ {
TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, failMessage, "ok", "error", false); NativeDialogUtil.showDialog(ModInfo.READABLE_NAME, failMessage, "ok", "error");
} }
catch (Exception ignore) { } catch (Exception ignore) { }
@@ -386,7 +385,7 @@ public class SelfUpdater
{ {
try try
{ {
TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, successMessage, "ok", "info", false); NativeDialogUtil.showDialog(ModInfo.READABLE_NAME, successMessage, "ok", "info");
} }
catch (Exception ignore) { } catch (Exception ignore) { }
}).start(); }).start();
@@ -424,7 +423,7 @@ public class SelfUpdater
LOGGER.error(failMessage, e); LOGGER.error(failMessage, e);
try try
{ {
TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, failMessage, "ok", "error", false); NativeDialogUtil.showDialog(ModInfo.READABLE_NAME, failMessage, "ok", "error");
} }
catch (Exception ignore) { } catch (Exception ignore) { }
@@ -55,6 +55,7 @@ import org.jetbrains.annotations.Nullable;
import java.awt.*; import java.awt.*;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@@ -106,9 +107,9 @@ public abstract class AbstractDhLevel implements IDhLevel
{ {
newChunkHashRepo = new ChunkHashRepo(AbstractDhRepo.DEFAULT_DATABASE_TYPE, databaseFile); newChunkHashRepo = new ChunkHashRepo(AbstractDhRepo.DEFAULT_DATABASE_TYPE, databaseFile);
} }
catch (SQLException e) catch (SQLException | IOException e)
{ {
LOGGER.error("Unable to create [ChunkHashRepo], error: ["+e.getMessage()+"].", e); LOGGER.fatal("Unable to create ["+ChunkHashRepo.class.getSimpleName()+"], error: ["+e.getMessage()+"].", e);
} }
this.chunkHashRepo = newChunkHashRepo; this.chunkHashRepo = newChunkHashRepo;
@@ -119,9 +120,9 @@ public abstract class AbstractDhLevel implements IDhLevel
{ {
newBeaconBeamRepo = new BeaconBeamRepo(AbstractDhRepo.DEFAULT_DATABASE_TYPE, databaseFile); newBeaconBeamRepo = new BeaconBeamRepo(AbstractDhRepo.DEFAULT_DATABASE_TYPE, databaseFile);
} }
catch (SQLException e) catch (SQLException | IOException e)
{ {
LOGGER.error("Unable to create [BeaconBeamRepo], error: ["+e.getMessage()+"].", e); LOGGER.error("Unable to create ["+BeaconBeamRepo.class.getSimpleName()+"], error: ["+e.getMessage()+"].", e);
} }
this.beaconBeamRepo = newBeaconBeamRepo; this.beaconBeamRepo = newBeaconBeamRepo;
} }
@@ -190,6 +191,8 @@ public abstract class AbstractDhLevel implements IDhLevel
return this.updateDataSourcesAsync(fullDataSource) return this.updateDataSourcesAsync(fullDataSource)
.thenRun(() -> .thenRun(() ->
{
try
{ {
HashSet<DhChunkPos> updatedChunkPosSet = this.updatedChunkPosSetBySectionPos.remove(fullDataSource.getPos()); HashSet<DhChunkPos> updatedChunkPosSet = this.updatedChunkPosSetBySectionPos.remove(fullDataSource.getPos());
if (updatedChunkPosSet != null) if (updatedChunkPosSet != null)
@@ -208,6 +211,11 @@ public abstract class AbstractDhLevel implements IDhLevel
new DhApiChunkModifiedEvent.EventParam(this.getLevelWrapper(), chunkPos.getX(), chunkPos.getZ())); new DhApiChunkModifiedEvent.EventParam(this.getLevelWrapper(), chunkPos.getX(), chunkPos.getZ()));
} }
} }
}
catch (Exception e)
{
LOGGER.error("Unexpected issue after onDataSourceSaveAsync, error: ["+e.getMessage()+"].", e);
}
}); });
} }
@@ -21,6 +21,7 @@ import com.seibel.distanthorizons.core.network.messages.requests.CancelMessage;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.WorldGenUtil;
import com.seibel.distanthorizons.core.util.math.Vec3d; import com.seibel.distanthorizons.core.util.math.Vec3d;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
@@ -29,6 +30,8 @@ import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.IOException;
import java.sql.SQLException;
import java.util.List; import java.util.List;
import java.util.concurrent.*; import java.util.concurrent.*;
@@ -48,23 +51,27 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I
*/ */
protected final ConcurrentLinkedQueue<IServerPlayerWrapper> worldGenPlayerCenteringQueue = new ConcurrentLinkedQueue<>(); protected final ConcurrentLinkedQueue<IServerPlayerWrapper> worldGenPlayerCenteringQueue = new ConcurrentLinkedQueue<>();
private final FullDataSourceRequestHandler requestHandler = new FullDataSourceRequestHandler(this); private final FullDataSourceRequestHandler requestHandler;
//=============// //=============//
// constructor // // constructor //
//=============// //=============//
public AbstractDhServerLevel(ISaveStructure saveStructure, IServerLevelWrapper serverLevelWrapper, ServerPlayerStateManager serverPlayerStateManager) public AbstractDhServerLevel(
{ ISaveStructure saveStructure,
this(saveStructure, serverLevelWrapper, serverPlayerStateManager, true); IServerLevelWrapper serverLevelWrapper,
} ServerPlayerStateManager serverPlayerStateManager
) throws SQLException, IOException
{ this(saveStructure, serverLevelWrapper, serverPlayerStateManager, true); }
public AbstractDhServerLevel( public AbstractDhServerLevel(
ISaveStructure saveStructure, ISaveStructure saveStructure,
IServerLevelWrapper serverLevelWrapper, IServerLevelWrapper serverLevelWrapper,
ServerPlayerStateManager serverPlayerStateManager, ServerPlayerStateManager serverPlayerStateManager,
boolean runRepoReliantSetup boolean runRepoReliantSetup
) ) throws SQLException, IOException
{ {
if (saveStructure.getSaveFolder(serverLevelWrapper).mkdirs()) if (saveStructure.getSaveFolder(serverLevelWrapper).mkdirs())
{ {
@@ -81,6 +88,7 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I
LOGGER.info("Started "+this.getClass().getSimpleName()+" for ["+serverLevelWrapper+"] at ["+saveStructure+"]."); LOGGER.info("Started "+this.getClass().getSimpleName()+" for ["+serverLevelWrapper+"] at ["+saveStructure+"].");
this.serverPlayerStateManager = serverPlayerStateManager; this.serverPlayerStateManager = serverPlayerStateManager;
this.requestHandler = new FullDataSourceRequestHandler(this);
} }
@@ -89,24 +97,17 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I
// ticks // // ticks //
//=======// //=======//
@Override
public void serverTick()
{
this.requestHandler.tick();
}
@Override @Override
public boolean shouldDoWorldGen() public boolean shouldDoWorldGen()
{ return Config.Common.WorldGenerator.enableDistantGeneration.get() && !this.worldGenPlayerCenteringQueue.isEmpty(); } { return Config.Common.WorldGenerator.enableDistantGeneration.get() && !this.worldGenPlayerCenteringQueue.isEmpty(); }
@Override @Override
@Nullable
public DhBlockPos2D getTargetPosForGeneration() public DhBlockPos2D getTargetPosForGeneration()
{ {
IServerPlayerWrapper firstPlayer = this.worldGenPlayerCenteringQueue.peek(); IServerPlayerWrapper firstPlayer = this.worldGenPlayerCenteringQueue.peek();
if (firstPlayer == null) if (firstPlayer == null)
{ {
return null; return DhBlockPos2D.ZERO;
} }
// Put first player in back before removing from front, so it can be removed by other thread without blocking // Put first player in back before removing from front, so it can be removed by other thread without blocking
@@ -118,9 +119,6 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I
return new DhBlockPos2D((int) position.x, (int) position.z); return new DhBlockPos2D((int) position.x, (int) position.z);
} }
@Override
public void worldGenTick() { this.serverside.worldGenModule.worldGenTick(); }
//==================// //==================//
@@ -149,17 +147,16 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I
return; return;
} }
if (Config.Server.generationBoundsRadius.get() > 0) boolean posInRange = WorldGenUtil.isPosInWorldGenRange(
{ message.sectionPos,
if (DhSectionPos.getChebyshevSignedBlockDistance(message.sectionPos, new DhBlockPos2D( Config.Common.WorldGenerator.generationCenterChunkX.get(), Config.Common.WorldGenerator.generationCenterChunkZ.get(),
serverPlayerState.sessionConfig.getGenerationBoundsX(), Config.Common.WorldGenerator.generationMaxChunkRadius.get()
serverPlayerState.sessionConfig.getGenerationBoundsZ() );
)) > Config.Server.generationBoundsRadius.get()) if (!posInRange)
{ {
message.sendResponse(new RequestOutOfRangeException("Section out of allowed bounds")); message.sendResponse(new RequestOutOfRangeException("Section out of allowed bounds"));
return; return;
} }
}
if (!Config.Server.Experimental.enableNSizedGeneration.get() && DhSectionPos.getDetailLevel(message.sectionPos) != DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL) if (!Config.Server.Experimental.enableNSizedGeneration.get() && DhSectionPos.getDetailLevel(message.sectionPos) != DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL)
{ {
@@ -297,7 +294,7 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I
public void addDebugMenuStringsToList(List<String> messageList) public void addDebugMenuStringsToList(List<String> messageList)
{ {
this.serverside.fullDataFileHandler.addDebugMenuStringsToList(messageList); this.serverside.fullDataFileHandler.addDebugMenuStringsToList(messageList);
this.serverside.worldGenModule.addDebugMenuStringsToList(messageList); this.serverside.lodRequestModule.addDebugMenuStringsToList(messageList);
} }
@@ -330,7 +327,10 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I
{ {
super.close(); super.close();
this.serverside.close(); this.serverside.close();
this.requestHandler.close();
LOGGER.info("Closed DHLevel for [" + this.getLevelWrapper() + "]."); LOGGER.info("Closed DHLevel for [" + this.getLevelWrapper() + "].");
} }
} }
@@ -49,6 +49,8 @@ import org.jetbrains.annotations.Nullable;
import javax.annotation.CheckForNull; import javax.annotation.CheckForNull;
import java.awt.*; import java.awt.*;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -83,7 +85,7 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
.asMap() .asMap()
); );
public final WorldGenModule worldGenModule; public final LodRequestModule lodRequestModule;
@Nullable @Nullable
private final SyncOnLoadRequestQueue syncOnLoadRequestQueue; private final SyncOnLoadRequestQueue syncOnLoadRequestQueue;
@@ -94,9 +96,18 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
// constructor // // constructor //
//=============// //=============//
public DhClientLevel(ISaveStructure saveStructure, IClientLevelWrapper clientLevelWrapper, @Nullable ClientNetworkState networkState) public DhClientLevel(
ISaveStructure saveStructure,
IClientLevelWrapper clientLevelWrapper,
@Nullable ClientNetworkState networkState
) throws SQLException, IOException
{ this(saveStructure, clientLevelWrapper, null, networkState); } { this(saveStructure, clientLevelWrapper, null, networkState); }
public DhClientLevel(ISaveStructure saveStructure, IClientLevelWrapper clientLevelWrapper, @Nullable File fullDataSaveDirOverride, @Nullable ClientNetworkState networkState) public DhClientLevel(
ISaveStructure saveStructure,
IClientLevelWrapper clientLevelWrapper,
@Nullable File fullDataSaveDirOverride,
@Nullable ClientNetworkState networkState
) throws SQLException, IOException
{ {
File saveFolder = saveStructure.getSaveFolder(clientLevelWrapper); File saveFolder = saveStructure.getSaveFolder(clientLevelWrapper);
File pre23Folder = saveStructure.getPre23SaveFolder(clientLevelWrapper); File pre23Folder = saveStructure.getPre23SaveFolder(clientLevelWrapper);
@@ -131,7 +142,7 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
} }
this.remoteDataSourceProvider = new RemoteFullDataSourceProvider(this, saveStructure, fullDataSaveDirOverride, this.syncOnLoadRequestQueue); this.remoteDataSourceProvider = new RemoteFullDataSourceProvider(this, saveStructure, fullDataSaveDirOverride, this.syncOnLoadRequestQueue);
this.worldGenModule = new WorldGenModule(this, this.remoteDataSourceProvider, () -> new WorldGenState(this, networkState)); this.lodRequestModule = new LodRequestModule(this,this, this.remoteDataSourceProvider, () -> new LodRequestState(this, networkState));
this.clientside = new ClientLevelModule(this); this.clientside = new ClientLevelModule(this);
@@ -236,14 +247,8 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
} }
@Override @Override
@Nullable
public DhBlockPos2D getTargetPosForGeneration() { return new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos()); } public DhBlockPos2D getTargetPosForGeneration() { return new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos()); }
@Override
public void worldGenTick() { this.worldGenModule.worldGenTick(); }
public void startRenderer() { this.clientside.startRenderer(); }
//===========// //===========//
@@ -253,13 +258,6 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
@Override @Override
public void onWorldGenTaskComplete(long pos) public void onWorldGenTaskComplete(long pos)
{ {
DebugRenderer.makeParticle(
new DebugRenderer.BoxParticle(
new DebugRenderer.Box(pos, 128f, 156f, 0.09f, Color.red.darker()),
0.2, 32f
)
);
this.clientside.reloadPos(pos); this.clientside.reloadPos(pos);
} }
@@ -325,7 +323,7 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
// world gen // world gen
this.worldGenModule.addDebugMenuStringsToList(messageList); this.lodRequestModule.addDebugMenuStringsToList(messageList);
if (this.syncOnLoadRequestQueue != null) if (this.syncOnLoadRequestQueue != null)
{ {
assert this.networkState != null; assert this.networkState != null;
@@ -348,9 +346,9 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
@Override @Override
public void close() public void close()
{ {
if (this.worldGenModule != null) if (this.lodRequestModule != null)
{ {
this.worldGenModule.close(); this.lodRequestModule.close();
} }
if (this.networkEventSource != null) if (this.networkEventSource != null)
@@ -371,11 +369,11 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
// helper classes // // helper classes //
//================// //================//
private static class WorldGenState extends WorldGenModule.AbstractWorldGenState private static class LodRequestState extends LodRequestModule.AbstractLodRequestState
{ {
WorldGenState(DhClientLevel level, ClientNetworkState networkState) LodRequestState(DhClientLevel clientLevel, ClientNetworkState networkState)
{ {
this.worldGenerationQueue = new RemoteWorldRetrievalQueue(networkState, level); this.retrievalQueue = new RemoteWorldRetrievalQueue(networkState, clientLevel);
} }
} }
@@ -33,6 +33,8 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapp
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.awt.*; import java.awt.*;
import java.io.IOException;
import java.sql.SQLException;
import java.util.List; import java.util.List;
/** The level used for a singleplayer world */ /** The level used for a singleplayer world */
@@ -48,7 +50,11 @@ public class DhClientServerLevel extends AbstractDhServerLevel implements IDhCli
// constructor // // constructor //
//=============// //=============//
public DhClientServerLevel(ISaveStructure saveStructure, IServerLevelWrapper serverLevelWrapper, ServerPlayerStateManager serverPlayerStateManager) public DhClientServerLevel(
ISaveStructure saveStructure,
IServerLevelWrapper serverLevelWrapper,
ServerPlayerStateManager serverPlayerStateManager
) throws SQLException, IOException
{ {
super(saveStructure, serverLevelWrapper, serverPlayerStateManager, false); super(saveStructure, serverLevelWrapper, serverPlayerStateManager, false);
@@ -126,14 +132,6 @@ public class DhClientServerLevel extends AbstractDhServerLevel implements IDhCli
public void onWorldGenTaskComplete(long pos) public void onWorldGenTaskComplete(long pos)
{ {
super.onWorldGenTaskComplete(pos); super.onWorldGenTaskComplete(pos);
DebugRenderer.makeParticle(
new DebugRenderer.BoxParticle(
new DebugRenderer.Box(pos, 128f, 156f, 0.09f, Color.red.darker()),
0.2, 32f
)
);
this.clientside.reloadPos(pos); this.clientside.reloadPos(pos);
} }
@@ -27,6 +27,8 @@ import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRend
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.sql.SQLException;
import java.util.List; import java.util.List;
public class DhServerLevel extends AbstractDhServerLevel public class DhServerLevel extends AbstractDhServerLevel
@@ -35,10 +37,12 @@ public class DhServerLevel extends AbstractDhServerLevel
// constructor // // constructor //
//=============// //=============//
public DhServerLevel(ISaveStructure saveStructure, IServerLevelWrapper serverLevelWrapper, ServerPlayerStateManager serverPlayerStateManager) public DhServerLevel(
{ ISaveStructure saveStructure,
super(saveStructure, serverLevelWrapper, serverPlayerStateManager); IServerLevelWrapper serverLevelWrapper,
} ServerPlayerStateManager serverPlayerStateManager
) throws SQLException, IOException
{ super(saveStructure, serverLevelWrapper, serverPlayerStateManager); }
@@ -52,7 +56,7 @@ public class DhServerLevel extends AbstractDhServerLevel
return true; //todo; return true; //todo;
} }
@Override @Override
public @Nullable DhBlockPos2D getTargetPosForGeneration() public DhBlockPos2D getTargetPosForGeneration()
{ {
DhBlockPos2D targetPos = super.getTargetPosForGeneration(); DhBlockPos2D targetPos = super.getTargetPosForGeneration();
if (targetPos == null) if (targetPos == null)
@@ -56,9 +56,6 @@ import java.util.concurrent.CompletableFuture;
*/ */
public interface IDhLevel extends AutoCloseable, GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener public interface IDhLevel extends AutoCloseable, GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener
{ {
@Deprecated
void worldGenTick();
/** /**
* May return either a client or server level wrapper. <br> * May return either a client or server level wrapper. <br>
* Should not return null * Should not return null
@@ -23,8 +23,6 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapp
public interface IDhServerLevel extends IDhLevel public interface IDhServerLevel extends IDhLevel
{ {
void serverTick();
IServerLevelWrapper getServerLevelWrapper(); IServerLevelWrapper getServerLevelWrapper();
} }
@@ -45,18 +45,18 @@ import java.util.function.Supplier;
/** /**
* Handles both single-player/server-side world gen and client side LOD requests. * Handles both single-player/server-side world gen and client side LOD requests.
* TODO rename
*/ */
public class WorldGenModule implements Closeable public class LodRequestModule implements Closeable
{ {
private static final DhLogger LOGGER = new DhLoggerBuilder().build(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private final GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener onWorldGenCompleteListener; private final GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener onWorldGenCompleteListener;
private final ThreadPoolExecutor tickerThread;
private final GeneratedFullDataSourceProvider dataSourceProvider; private final GeneratedFullDataSourceProvider dataSourceProvider;
private final Supplier<? extends AbstractWorldGenState> worldGenStateSupplier; private final Supplier<? extends AbstractLodRequestState> worldGenStateSupplier;
private final AtomicReference<AbstractWorldGenState> worldGenStateRef = new AtomicReference<>(); private final AtomicReference<AbstractLodRequestState> lodRequestStateRef = new AtomicReference<>();
@@ -64,59 +64,41 @@ public class WorldGenModule implements Closeable
// constructor // // constructor //
//=============// //=============//
public WorldGenModule( public LodRequestModule(
IDhLevel level,
GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener onWorldGenCompleteListener, GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener onWorldGenCompleteListener,
GeneratedFullDataSourceProvider dataSourceProvider, GeneratedFullDataSourceProvider dataSourceProvider,
Supplier<? extends AbstractWorldGenState> worldGenStateSupplier Supplier<? extends AbstractLodRequestState> worldGenStateSupplier
) )
{ {
this.onWorldGenCompleteListener = onWorldGenCompleteListener; this.onWorldGenCompleteListener = onWorldGenCompleteListener;
this.dataSourceProvider = dataSourceProvider; this.dataSourceProvider = dataSourceProvider;
this.worldGenStateSupplier = worldGenStateSupplier; this.worldGenStateSupplier = worldGenStateSupplier;
String levelId = level.getLevelWrapper().getDhIdentifier();
this.tickerThread = ThreadUtil.makeSingleDaemonThreadPool("Request Module Ticker ["+levelId+"]");
this.tickerThread.execute(this::tickLoop);
} }
//===================// //=========//
// world gen control // // ticking //
//===================// //=========//
public void startWorldGen(GeneratedFullDataSourceProvider dataFileHandler, AbstractWorldGenState newWgs) private void tickLoop()
{ {
// create the new world generator try
if (!this.worldGenStateRef.compareAndSet(null, newWgs))
{ {
LOGGER.warn("Failed to start world gen due to concurrency"); while (!Thread.interrupted())
newWgs.closeAsync(false);
}
dataFileHandler.addWorldGenCompleteListener(this.onWorldGenCompleteListener);
dataFileHandler.setWorldGenerationQueue(newWgs.worldGenerationQueue);
}
public void stopWorldGen(GeneratedFullDataSourceProvider dataFileHandler)
{ {
AbstractWorldGenState worldGenState = this.worldGenStateRef.get(); Thread.sleep(20);
if (worldGenState == null) this.tick();
{
LOGGER.warn("Attempted to stop world gen when it was not running");
return;
}
// shut down the world generator
while (!this.worldGenStateRef.compareAndSet(worldGenState, null))
{
worldGenState = this.worldGenStateRef.get();
if (worldGenState == null)
{
return;
} }
} }
dataFileHandler.clearRetrievalQueue(); catch (InterruptedException ignore) { }
worldGenState.closeAsync(true).join(); //TODO: Make it async.
dataFileHandler.removeWorldGenCompleteListener(this.onWorldGenCompleteListener);
} }
private void tick()
public void worldGenTick()
{ {
boolean shouldDoWorldGen = this.onWorldGenCompleteListener.shouldDoWorldGen(); boolean shouldDoWorldGen = this.onWorldGenCompleteListener.shouldDoWorldGen();
// if the world is read only don't generate anything // if the world is read only don't generate anything
@@ -136,13 +118,13 @@ public class WorldGenModule implements Closeable
if (this.isWorldGenRunning()) if (this.isWorldGenRunning())
{ {
AbstractWorldGenState worldGenState = this.worldGenStateRef.get(); AbstractLodRequestState lodRequestState = this.lodRequestStateRef.get();
if (worldGenState != null) if (lodRequestState != null)
{ {
DhBlockPos2D targetPosForGeneration = this.onWorldGenCompleteListener.getTargetPosForGeneration(); DhBlockPos2D targetPosForGeneration = this.onWorldGenCompleteListener.getTargetPosForGeneration();
if (targetPosForGeneration != null) if (targetPosForGeneration != null)
{ {
worldGenState.startGenerationQueueAndSetTargetPos(targetPosForGeneration); lodRequestState.startRequestQueueAndSetTargetPos(targetPosForGeneration);
} }
} }
} }
@@ -150,6 +132,48 @@ public class WorldGenModule implements Closeable
//===================//
// world gen control //
//===================//
public void startWorldGen(GeneratedFullDataSourceProvider dataFileHandler, AbstractLodRequestState newWgs)
{
// create the new world generator
if (!this.lodRequestStateRef.compareAndSet(null, newWgs))
{
LOGGER.warn("Failed to start world gen due to concurrency");
newWgs.closeAsync(false);
}
dataFileHandler.addWorldGenCompleteListener(this.onWorldGenCompleteListener);
dataFileHandler.setWorldGenerationQueue(newWgs.retrievalQueue);
}
public void stopWorldGen(GeneratedFullDataSourceProvider dataFileHandler)
{
AbstractLodRequestState worldGenState = this.lodRequestStateRef.get();
if (worldGenState == null)
{
LOGGER.warn("Attempted to stop world gen when it was not running");
return;
}
// shut down the world generator
while (!this.lodRequestStateRef.compareAndSet(worldGenState, null))
{
worldGenState = this.lodRequestStateRef.get();
if (worldGenState == null)
{
return;
}
}
dataFileHandler.clearRetrievalQueue();
worldGenState.closeAsync(true).join(); //TODO: Make it async.
dataFileHandler.removeWorldGenCompleteListener(this.onWorldGenCompleteListener);
}
//=======================// //=======================//
// base method overrides // // base method overrides //
//=======================// //=======================//
@@ -157,13 +181,15 @@ public class WorldGenModule implements Closeable
@Override @Override
public void close() public void close()
{ {
this.tickerThread.shutdownNow();
// shutdown the world-gen // shutdown the world-gen
AbstractWorldGenState worldGenState = this.worldGenStateRef.get(); AbstractLodRequestState worldGenState = this.lodRequestStateRef.get();
if (worldGenState != null) if (worldGenState != null)
{ {
while (!this.worldGenStateRef.compareAndSet(worldGenState, null)) while (!this.lodRequestStateRef.compareAndSet(worldGenState, null))
{ {
worldGenState = this.worldGenStateRef.get(); worldGenState = this.lodRequestStateRef.get();
if (worldGenState == null) if (worldGenState == null)
{ {
break; break;
@@ -183,12 +209,12 @@ public class WorldGenModule implements Closeable
// getters // // getters //
//=========// //=========//
public boolean isWorldGenRunning() { return this.worldGenStateRef.get() != null; } public boolean isWorldGenRunning() { return this.lodRequestStateRef.get() != null; }
/** mutates a list so it can be added to an existing {@link IDhLevel}'s debug list */ /** mutates a list so it can be added to an existing {@link IDhLevel}'s debug list */
public void addDebugMenuStringsToList(List<String> messageList) public void addDebugMenuStringsToList(List<String> messageList)
{ {
AbstractWorldGenState worldGenState = this.worldGenStateRef.get(); AbstractLodRequestState worldGenState = this.lodRequestStateRef.get();
if (worldGenState == null) if (worldGenState == null)
{ {
return; return;
@@ -196,9 +222,9 @@ public class WorldGenModule implements Closeable
// estimated tasks // estimated tasks
String waitingCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getWaitingTaskCount()); String waitingCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.retrievalQueue.getWaitingTaskCount());
String inProgressCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getInProgressTaskCount()); String inProgressCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.retrievalQueue.getInProgressTaskCount());
String totalCountEstimateStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getRetrievalEstimatedRemainingChunkCount()); String totalCountEstimateStr = F3Screen.NUMBER_FORMAT.format(worldGenState.retrievalQueue.getRetrievalEstimatedRemainingChunkCount());
String message = "World Gen/Import Tasks: "+waitingCountStr+"/"+totalCountEstimateStr+" (in progress "+inProgressCountStr+")"; String message = "World Gen/Import Tasks: "+waitingCountStr+"/"+totalCountEstimateStr+" (in progress "+inProgressCountStr+")";
// estimated chunks/sec // estimated chunks/sec
@@ -210,7 +236,7 @@ public class WorldGenModule implements Closeable
messageList.add(message); messageList.add(message);
worldGenState.worldGenerationQueue.addDebugMenuStringsToList(messageList); worldGenState.retrievalQueue.addDebugMenuStringsToList(messageList);
} }
@@ -220,40 +246,22 @@ public class WorldGenModule implements Closeable
//================// //================//
/** Handles the {@link IFullDataSourceRetrievalQueue} and any other necessary world gen information. */ /** Handles the {@link IFullDataSourceRetrievalQueue} and any other necessary world gen information. */
public static abstract class AbstractWorldGenState public static abstract class AbstractLodRequestState
{ {
/** static so we only send the disable message once per session */ /** static so we only send the disable message once per session */
private static long firstProgressMessageSentMs = 0; private static long firstProgressMessageSentMs = 0;
public IFullDataSourceRetrievalQueue worldGenerationQueue; public IFullDataSourceRetrievalQueue retrievalQueue;
private static final ThreadPoolExecutor PROGRESS_UPDATER_THREAD = ThreadUtil.makeSingleDaemonThreadPool("World Gen Progress Updater"); private static final ThreadPoolExecutor PROGRESS_UPDATER_THREAD = ThreadUtil.makeSingleDaemonThreadPool("World Gen Progress Updater");
private boolean progressUpdateThreadRunning = false; private boolean progressUpdateThreadRunning = false;
CompletableFuture<Void> closeAsync(boolean doInterrupt)
{
// this should stop the updater thread
this.progressUpdateThreadRunning = false;
return this.worldGenerationQueue.startClosingAsync(true, doInterrupt) /** @param targetPosForRequest the position that world generation should be centered around */
.exceptionally(e -> public void startRequestQueueAndSetTargetPos(DhBlockPos2D targetPosForRequest)
{ {
LOGGER.error("Error during first stage of generation queue shutdown, Error: ["+e.getMessage()+"].", e); this.retrievalQueue.startAndSetTargetPos(targetPosForRequest);
return null;
}
).thenRun(this.worldGenerationQueue::close)
.exceptionally(e ->
{
LOGGER.error("Error during second stage of generation queue shutdown, Error: ["+e.getMessage()+"].", e);
return null;
});
}
/** @param targetPosForGeneration the position that world generation should be centered around */
public void startGenerationQueueAndSetTargetPos(DhBlockPos2D targetPosForGeneration)
{
this.worldGenerationQueue.startAndSetTargetPos(targetPosForGeneration);
this.startProgressUpdateThread(); this.startProgressUpdateThread();
} }
private void startProgressUpdateThread() private void startProgressUpdateThread()
@@ -286,8 +294,8 @@ public class WorldGenModule implements Closeable
private void sendRetrievalProgress() private void sendRetrievalProgress()
{ {
// format the remaining chunks // format the remaining chunks
int remainingChunkCount = this.worldGenerationQueue.getRetrievalEstimatedRemainingChunkCount(); int remainingChunkCount = this.retrievalQueue.getRetrievalEstimatedRemainingChunkCount();
remainingChunkCount += this.worldGenerationQueue.getQueuedChunkCount(); remainingChunkCount += this.retrievalQueue.getQueuedChunkCount();
String remainingChunkCountStr = F3Screen.NUMBER_FORMAT.format(remainingChunkCount); String remainingChunkCountStr = F3Screen.NUMBER_FORMAT.format(remainingChunkCount);
String message = "DH is generating chunks. " + remainingChunkCountStr + " left."; String message = "DH is generating chunks. " + remainingChunkCountStr + " left.";
@@ -350,7 +358,7 @@ public class WorldGenModule implements Closeable
/** @return -1 if this method isn't supported or available */ /** @return -1 if this method isn't supported or available */
public double getEstimatedChunksPerSecond() public double getEstimatedChunksPerSecond()
{ {
RollingAverage avg = this.worldGenerationQueue.getRollingAverageChunkGenTimeInMs(); RollingAverage avg = this.retrievalQueue.getRollingAverageChunkGenTimeInMs();
if (avg == null) if (avg == null)
{ {
return -1; return -1;
@@ -373,6 +381,27 @@ public class WorldGenModule implements Closeable
return chunksPerSecond; return chunksPerSecond;
} }
CompletableFuture<Void> closeAsync(boolean doInterrupt)
{
// this should stop the updater thread
this.progressUpdateThreadRunning = false;
return this.retrievalQueue.startClosingAsync(true, doInterrupt)
.exceptionally(e ->
{
LOGGER.error("Error during first stage of generation queue shutdown, Error: ["+e.getMessage()+"].", e);
return null;
}
).thenRun(this.retrievalQueue::close)
.exceptionally(e ->
{
LOGGER.error("Error during second stage of generation queue shutdown, Error: ["+e.getMessage()+"].", e);
return null;
});
}
} }
@@ -28,6 +28,9 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.coreapi.DependencyInjection.WorldGeneratorInjector; import com.seibel.distanthorizons.coreapi.DependencyInjection.WorldGeneratorInjector;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import java.io.IOException;
import java.sql.SQLException;
public class ServerLevelModule implements AutoCloseable public class ServerLevelModule implements AutoCloseable
{ {
private static final DhLogger LOGGER = new DhLoggerBuilder().build(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
@@ -36,7 +39,7 @@ public class ServerLevelModule implements AutoCloseable
public final ISaveStructure saveStructure; public final ISaveStructure saveStructure;
public final GeneratedFullDataSourceProvider fullDataFileHandler; public final GeneratedFullDataSourceProvider fullDataFileHandler;
public final WorldGenModule worldGenModule; public final LodRequestModule lodRequestModule;
@@ -44,12 +47,12 @@ public class ServerLevelModule implements AutoCloseable
// constructor // // constructor //
//=============// //=============//
public ServerLevelModule(IDhServerLevel parentServerLevel, ISaveStructure saveStructure) public ServerLevelModule(IDhServerLevel parentServerLevel, ISaveStructure saveStructure) throws SQLException, IOException
{ {
this.parentServerLevel = parentServerLevel; this.parentServerLevel = parentServerLevel;
this.saveStructure = saveStructure; this.saveStructure = saveStructure;
this.fullDataFileHandler = new GeneratedFullDataSourceProvider(parentServerLevel, saveStructure); this.fullDataFileHandler = new GeneratedFullDataSourceProvider(parentServerLevel, saveStructure);
this.worldGenModule = new WorldGenModule(this.parentServerLevel, this.fullDataFileHandler, () -> new ServerLevelModule.WorldGenState(this.parentServerLevel)); this.lodRequestModule = new LodRequestModule(this.parentServerLevel, this.parentServerLevel, this.fullDataFileHandler, () -> new LodRequestState(this.parentServerLevel));
} }
@@ -62,7 +65,7 @@ public class ServerLevelModule implements AutoCloseable
public void close() public void close()
{ {
// shutdown the world-gen // shutdown the world-gen
this.worldGenModule.close(); this.lodRequestModule.close();
this.fullDataFileHandler.close(); this.fullDataFileHandler.close();
} }
@@ -72,9 +75,9 @@ public class ServerLevelModule implements AutoCloseable
// helper classes // // helper classes //
//================// //================//
public static class WorldGenState extends WorldGenModule.AbstractWorldGenState public static class LodRequestState extends LodRequestModule.AbstractLodRequestState
{ {
WorldGenState(IDhServerLevel level) LodRequestState(IDhServerLevel level)
{ {
IDhApiWorldGenerator worldGenerator = WorldGeneratorInjector.INSTANCE.get(level.getLevelWrapper()); IDhApiWorldGenerator worldGenerator = WorldGeneratorInjector.INSTANCE.get(level.getLevelWrapper());
if (worldGenerator == null) if (worldGenerator == null)
@@ -85,7 +88,7 @@ public class ServerLevelModule implements AutoCloseable
// since core world generator's should have the lowest override priority // since core world generator's should have the lowest override priority
WorldGeneratorInjector.INSTANCE.bind(level.getLevelWrapper(), worldGenerator); WorldGeneratorInjector.INSTANCE.bind(level.getLevelWrapper(), worldGenerator);
} }
this.worldGenerationQueue = new WorldGenerationQueue(worldGenerator, level); this.retrievalQueue = new WorldGenerationQueue(worldGenerator, level);
} }
} }
@@ -276,7 +276,7 @@ public class DhLogger implements IConfigListener
if (mc_client != null) if (mc_client != null)
{ {
mc_client.logToChat(level, msgStr); this.logToChat(level, msgStr);
messageLogged = true; messageLogged = true;
} }
} }
@@ -296,6 +296,41 @@ public class DhLogger implements IConfigListener
} }
private static boolean loggingLevelIsLessSpecificThan(Level thisLoggingLevel, Level requestedLogLevel) private static boolean loggingLevelIsLessSpecificThan(Level thisLoggingLevel, Level requestedLogLevel)
{ return thisLoggingLevel.intLevel() >= requestedLogLevel.intLevel(); } { return thisLoggingLevel.intLevel() >= requestedLogLevel.intLevel(); }
/** Sends the given message to chat with a formatted prefix and color based on the log level. */
private void logToChat(Level logLevel, String message)
{
String prefix = "[" + ModInfo.READABLE_NAME + "] ";
if (logLevel == Level.ERROR)
{
prefix += "\u00A74";
}
else if (logLevel == Level.WARN)
{
prefix += "\u00A76";
}
else if (logLevel == Level.INFO)
{
prefix += "\u00A7f";
}
else if (logLevel == Level.DEBUG)
{
prefix += "\u00A77";
}
else if (logLevel == Level.TRACE)
{
prefix += "\u00A78";
}
else
{
prefix += "\u00A7f";
}
prefix += "\u00A7l\u00A7u";
prefix += logLevel.name();
prefix += ":\u00A7r ";
mc_client.sendChatMessage(prefix + message);
}
@@ -6,6 +6,7 @@ import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.types.ConfigEntry; import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.level.DhClientLevel; import com.seibel.distanthorizons.core.level.DhClientLevel;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
@@ -23,7 +24,6 @@ import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO; import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.ratelimiting.SupplierBasedRateLimiter; import com.seibel.distanthorizons.core.util.ratelimiting.SupplierBasedRateLimiter;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.world.DhApiWorldProxy; import com.seibel.distanthorizons.core.world.DhApiWorldProxy;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
@@ -33,9 +33,7 @@ import java.awt.*;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRenderable, AutoCloseable public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRenderable, AutoCloseable
{ {
@@ -58,7 +56,7 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
private volatile CompletableFuture<Void> closingFuture = null; private volatile CompletableFuture<Void> closingFuture = null;
protected final ConcurrentMap<Long, RequestQueueEntry> waitingTasksBySectionPos = new ConcurrentHashMap<>(); protected final ConcurrentMap<Long, NetRequestTask> waitingTasksBySectionPos = new ConcurrentHashMap<>();
/** /**
* This semaphore prevents a given thread from accidentally locking on the same group * This semaphore prevents a given thread from accidentally locking on the same group
* multiple times, as the semaphore is tied to the given thread. <br> * multiple times, as the semaphore is tied to the given thread. <br>
@@ -74,16 +72,6 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
private final SupplierBasedRateLimiter<Void> rateLimiter = new SupplierBasedRateLimiter<>(this::getRequestRateLimit); private final SupplierBasedRateLimiter<Void> rateLimiter = new SupplierBasedRateLimiter<>(this::getRequestRateLimit);
private final Set<Long> succeededPositions = Collections.newSetFromMap(CacheBuilder.newBuilder()
.expireAfterWrite(20, TimeUnit.MINUTES)
.<Long, Boolean>build()
.asMap());
private final Set<Long> requiresSplittingPositions = Collections.newSetFromMap(CacheBuilder.newBuilder()
.expireAfterWrite(20, TimeUnit.MINUTES)
.<Long, Boolean>build()
.asMap());
//=============// //=============//
@@ -108,8 +96,8 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
//==================// //==================//
protected abstract int getRequestRateLimit(); protected abstract int getRequestRateLimit();
protected abstract boolean isSectionAllowedToGenerate(long sectionPos, DhBlockPos2D targetPos); protected abstract boolean sectionInAllowedGenerationRadius(long sectionPos, DhBlockPos2D targetPos);
protected abstract boolean onBeforeRequest(long sectionPos, CompletableFuture<ERequestResult> future); protected abstract boolean onBeforeRequest(long sectionPos, CompletableFuture<DataSourceRetrievalResult> future);
protected abstract String getQueueName(); protected abstract String getQueueName();
@@ -119,74 +107,60 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
// request submitting // // request submitting //
//====================// //====================//
public CompletableFuture<ERequestResult> submitRequest(long sectionPos, Consumer<FullDataSourceV2> dataSourceConsumer) public CompletableFuture<DataSourceRetrievalResult> submitRequest(long sectionPos, @Nullable Long clientTimestamp)
{ return this.submitRequest(sectionPos, null, dataSourceConsumer); }
public CompletableFuture<ERequestResult> submitRequest(long sectionPos, @Nullable Long clientTimestamp, Consumer<FullDataSourceV2> dataSourceConsumer)
{ {
if (this.succeededPositions.contains(sectionPos)) NetRequestTask requestEntry = this.waitingTasksBySectionPos.compute(sectionPos, (Long pos, NetRequestTask existingNetTask) ->
{ {
return CompletableFuture.completedFuture(ERequestResult.FAILED); // ignore already queued tasks
if (existingNetTask != null)
{
return existingNetTask;
} }
if (this.requiresSplittingPositions.contains(sectionPos))
NetRequestTask newRequestEntry = new NetRequestTask(pos, clientTimestamp);
newRequestEntry.future.whenComplete((DataSourceRetrievalResult requestResult, Throwable throwable) ->
{ {
return CompletableFuture.completedFuture(ERequestResult.REQUIRES_SPLITTING); this.waitingTasksBySectionPos.remove(pos);
if (throwable != null)
{
if (!(throwable instanceof CancellationException))
{
this.failedRequests.incrementAndGet();
}
return;
} }
AtomicBoolean added = new AtomicBoolean(false); switch (requestResult.state)
RequestQueueEntry entry = this.waitingTasksBySectionPos.compute(sectionPos, (pos, existingQueueEntry) ->
{ {
if (existingQueueEntry != null) case SUCCESS:
{
return existingQueueEntry;
}
RequestQueueEntry newEntry = new RequestQueueEntry(dataSourceConsumer, clientTimestamp);
newEntry.future.whenComplete((requestResult, throwable) ->
{
this.waitingTasksBySectionPos.remove(sectionPos);
switch (requestResult)
{
case SUCCEEDED:
this.finishedRequests.incrementAndGet(); this.finishedRequests.incrementAndGet();
this.succeededPositions.add(pos); break;
return;
case REQUIRES_SPLITTING: case REQUIRES_SPLITTING:
this.requiresSplittingPositions.add(sectionPos); break;
return; case FAIL:
case FAILED:
this.failedRequests.incrementAndGet(); this.failedRequests.incrementAndGet();
return;
default:
if (throwable != null && !(throwable instanceof CancellationException))
{
this.failedRequests.incrementAndGet();
}
break; break;
} }
}); });
added.set(true); return newRequestEntry;
return newEntry;
}); });
if (!added.get()) return requestEntry.future;
{
return CompletableFuture.completedFuture(ERequestResult.FAILED);
}
return entry.future;
} }
public synchronized boolean tick(DhBlockPos2D targetPos) public synchronized boolean tick(DhBlockPos2D targetPos)
{ {
if (DhApiWorldProxy.INSTANCE.worldLoaded() && DhApiWorldProxy.INSTANCE.getReadOnly()) if (DhApiWorldProxy.INSTANCE.worldLoaded()
&& DhApiWorldProxy.INSTANCE.getReadOnly())
{ {
return false; return false;
} }
if (this.closingFuture != null || !this.networkState.isReady()) if (this.closingFuture != null
|| !this.networkState.isReady())
{ {
return false; return false;
} }
@@ -209,43 +183,50 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
} }
private void sendNextRequest(DhBlockPos2D targetPos) private void sendNextRequest(DhBlockPos2D targetPos)
{ {
Map.Entry<Long, RequestQueueEntry> mapEntry = this.waitingTasksBySectionPos.entrySet().stream() Map.Entry<Long, NetRequestTask> nearestMapEntry = this.waitingTasksBySectionPos
.entrySet().stream()
.filter(task -> task.getValue().networkDataSourceFuture == null) .filter(task -> task.getValue().networkDataSourceFuture == null)
.min(Comparator.comparingInt(x -> DhSectionPos.getChebyshevSignedBlockDistance(x.getKey(), targetPos))) .min(Comparator.comparingInt(mapEntry -> DhSectionPos.getChebyshevSignedBlockDistance(mapEntry.getKey(), targetPos)))
.orElse(null); .orElse(null);
if (mapEntry == null) if (nearestMapEntry == null)
{ {
this.pendingTasksSemaphore.release(); this.pendingTasksSemaphore.release();
return; return;
} }
long sectionPos = mapEntry.getKey(); long requestPos = nearestMapEntry.getKey();
RequestQueueEntry entry = mapEntry.getValue(); NetRequestTask requestTask = nearestMapEntry.getValue();
if (!this.isSectionAllowedToGenerate(sectionPos, targetPos)) if (!this.sectionInAllowedGenerationRadius(requestPos, targetPos))
{ {
entry.future.cancel(false); requestTask.future.cancel(false);
this.pendingTasksSemaphore.release(); this.pendingTasksSemaphore.release();
return; return;
} }
if (!this.onBeforeRequest(sectionPos, entry.future)) if (!this.onBeforeRequest(requestPos, requestTask.future))
{ {
this.pendingTasksSemaphore.release(); this.pendingTasksSemaphore.release();
return; return;
} }
Long offsetEntryTimestamp = entry.updateTimestamp != null Long offsetEntryTimestamp = requestTask.updateTimestamp != null
? entry.updateTimestamp + this.networkState.getServerTimeOffset() ? requestTask.updateTimestamp + this.networkState.getServerTimeOffset()
: null; : null;
CompletableFuture<FullDataSourceResponseMessage> dataSourceFuture = this.networkState.getSession().sendRequest( CompletableFuture<FullDataSourceResponseMessage> dataSourceNetworkFuture = this.networkState.getSession().sendRequest(
new FullDataSourceRequestMessage(this.level.getLevelWrapper(), sectionPos, offsetEntryTimestamp), new FullDataSourceRequestMessage(this.level.getLevelWrapper(), requestPos, offsetEntryTimestamp),
FullDataSourceResponseMessage.class FullDataSourceResponseMessage.class
); );
entry.networkDataSourceFuture = dataSourceFuture; requestTask.networkDataSourceFuture = dataSourceNetworkFuture;
dataSourceFuture.handle((response, throwable) -> dataSourceNetworkFuture.handle((FullDataSourceResponseMessage response, Throwable throwable) ->
{
this.handleNetResponse(requestTask, response, throwable);
return null;
});
}
private void handleNetResponse(NetRequestTask requestTask, FullDataSourceResponseMessage response, Throwable throwable)
{ {
this.pendingTasksSemaphore.release(); this.pendingTasksSemaphore.release();
@@ -256,123 +237,104 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
throw throwable; throw throwable;
} }
if (response.payload != null) if (response.payload == null)
{ {
FullDataSourceV2DTO dataSourceDto = this.networkState.fullDataPayloadReceiver.decodeDataSource(response.payload); LodUtil.assertTrue(this.changedOnly, "Received empty data source response for not changes-only request");
return;
}
try(FullDataSourceV2DTO dataSourceDto = this.networkState.fullDataPayloadReceiver.decodeDataSource(response.payload))
{
// set application flags based on the received detail level, // set application flags based on the received detail level,
// this is needed so the data sources propagate correctly // this is needed so the data sources propagate correctly
dataSourceDto.applyToChildren = DhSectionPos.getDetailLevel(dataSourceDto.pos) > DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL; dataSourceDto.applyToChildren = DhSectionPos.getDetailLevel(dataSourceDto.pos) > DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL;
dataSourceDto.applyToParent = DhSectionPos.getDetailLevel(dataSourceDto.pos) < DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL + 12; dataSourceDto.applyToParent = DhSectionPos.getDetailLevel(dataSourceDto.pos) < DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL + 12;
AbstractExecutorService executor = ThreadPoolUtil.getNetworkCompressionExecutor();
if (executor == null)
{
LOGGER.warn("Unable to handle FullDataPayload - getNetworkCompressionExecutor() is null");
dataSourceDto.close();
return null;
}
CompletableFuture.runAsync(() ->
{
try
{
this.level.updateBeaconBeamsForSectionPos(dataSourceDto.pos, response.payload.beaconBeams); this.level.updateBeaconBeamsForSectionPos(dataSourceDto.pos, response.payload.beaconBeams);
FullDataSourceV2 fullDataSource = dataSourceDto.createDataSource(this.level.getLevelWrapper(), null); FullDataSourceV2 fullDataSource = dataSourceDto.createDataSource(this.level.getLevelWrapper(), null);
entry.dataSourceConsumer.accept(fullDataSource); requestTask.future.complete(DataSourceRetrievalResult.CreateSuccess(dataSourceDto.pos, fullDataSource));
}
catch (Exception e)
{
throw new RuntimeException(e);
}
finally
{
dataSourceDto.close();
}
}, executor);
}
else
{
LodUtil.assertTrue(this.changedOnly, "Received empty data source response for not changes-only request");
} }
} }
catch (SectionRequiresSplittingException ignored) catch (SectionRequiresSplittingException ignored)
{ {
return entry.future.complete(ERequestResult.REQUIRES_SPLITTING); requestTask.future.complete(DataSourceRetrievalResult.CreateSplit());
} }
catch (SessionClosedException | CancellationException ignored) catch (SessionClosedException | CancellationException ignored)
{ {
return entry.future.cancel(false); requestTask.future.cancel(false);
} }
catch (RequestRejectedException e) catch (RequestRejectedException e)
{ {
LOGGER.info("Request rejected by the server: " + e.getMessage()); LOGGER.info("Request rejected by the server, message: [" + e.getMessage() + "].");
return entry.future.complete(ERequestResult.FAILED); requestTask.future.complete(DataSourceRetrievalResult.CreateFail());
} }
catch (RateLimitedException e) catch (RateLimitedException e)
{ {
LOGGER.info("Rate limited by server, re-queueing task [" + DhSectionPos.toString(sectionPos) + "]: " + e.getMessage()); LOGGER.info("Rate limited by server, re-queueing task [" + DhSectionPos.toString(requestTask.pos) + "], message: [" + e.getMessage() + "].");
// Skip all requests for 1 second // Skip all requests for 1 second
this.rateLimiter.acquireAll(); this.rateLimiter.acquireAll();
entry.networkDataSourceFuture = null; requestTask.networkDataSourceFuture = null;
return null;
} }
catch (RequestOutOfRangeException e) catch (RequestOutOfRangeException e)
{ {
LOGGER.debug("Out of range, re-queueing task [" + DhSectionPos.toString(sectionPos) + "]: " + e.getMessage()); LOGGER.debug("Out of range, re-queueing task [" + DhSectionPos.toString(requestTask.pos) + "], message: [" + e.getMessage() + "].");
entry.networkDataSourceFuture = null; requestTask.networkDataSourceFuture = null;
return null;
} }
catch (Throwable e) catch (Throwable e)
{ {
entry.retryAttempts--; requestTask.retryAttempts--;
LOGGER.error("Error while fetching full data source, attempts left: {} / {}", entry.retryAttempts, MAX_RETRY_ATTEMPTS, e); LOGGER.error("Unexpected error: ["+e.getMessage()+"] while fetching full data source, attempts left: ["+requestTask.retryAttempts+"] / ["+MAX_RETRY_ATTEMPTS+"]", e);
// Retry logic // Retry logic
if (entry.retryAttempts > 0) if (requestTask.retryAttempts > 0)
{ {
entry.networkDataSourceFuture = null; requestTask.networkDataSourceFuture = null;
return null;
} }
else else
{ {
return entry.future.complete(ERequestResult.FAILED); requestTask.future.complete(DataSourceRetrievalResult.CreateFail());
} }
} }
return entry.future.complete(ERequestResult.SUCCEEDED);
});
} }
//=========================================// //=========================================//
// IFullDataSourceRetrievalQueue overrides // // IFullDataSourceRetrievalQueue overrides //
//=========================================// //=========================================//
public void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf) public void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf)
{ {
for (Map.Entry<Long, RequestQueueEntry> mapEntry : (Iterable<? extends Map.Entry<Long, RequestQueueEntry>>) this.waitingTasksBySectionPos.entrySet().stream() // remove tasks furthest
.sorted(Comparator.comparingInt((Map.Entry<Long, RequestQueueEntry> entry) -> DhSectionPos.getChebyshevSignedBlockDistance(entry.getKey(), Objects.requireNonNull(this.level.getTargetPosForGeneration()))).reversed()) Iterator<Map.Entry<Long, NetRequestTask>> farestTaskIterator = this.waitingTasksBySectionPos
::iterator) .entrySet().stream()
.sorted(Comparator.comparingInt((Map.Entry<Long, NetRequestTask> entry) ->
{ {
Long pos = entry.getKey();
DhBlockPos2D targetPos = this.level.getTargetPosForGeneration();
return DhSectionPos.getChebyshevSignedBlockDistance(pos, targetPos);
}).reversed())
.iterator();
while (farestTaskIterator.hasNext())
{
Map.Entry<Long, NetRequestTask> mapEntry = farestTaskIterator.next();
long pos = mapEntry.getKey(); long pos = mapEntry.getKey();
RequestQueueEntry entry = mapEntry.getValue(); NetRequestTask entry = mapEntry.getValue();
if (removeIf.accept(pos)) if (removeIf.accept(pos))
{ {
LOGGER.debug("Removing request [" + mapEntry.getKey() + "]...");
entry.future.cancel(false);
if (entry.networkDataSourceFuture != null) if (entry.networkDataSourceFuture != null)
{ {
entry.networkDataSourceFuture.cancel(false); entry.networkDataSourceFuture.cancel(false);
} }
entry.future.cancel(false);
} }
} }
} }
@@ -400,7 +362,7 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
do do
{ {
for (RequestQueueEntry entry : this.waitingTasksBySectionPos.values()) for (NetRequestTask entry : this.waitingTasksBySectionPos.values())
{ {
entry.future.cancel(alsoInterruptRunning); entry.future.cancel(alsoInterruptRunning);
if (entry.networkDataSourceFuture != null && entry.networkDataSourceFuture.cancel(alsoInterruptRunning)) if (entry.networkDataSourceFuture != null && entry.networkDataSourceFuture.cancel(alsoInterruptRunning))
@@ -438,13 +400,31 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
return; return;
} }
for (Map.Entry<Long, RequestQueueEntry> mapEntry : this.waitingTasksBySectionPos.entrySet()) DhBlockPos2D targetPos = this.level.getTargetPosForGeneration();
for (Map.Entry<Long, NetRequestTask> mapEntry : this.waitingTasksBySectionPos.entrySet())
{ {
renderer.renderBox(new DebugRenderer.Box(mapEntry.getKey(), -32f, 64f, 0.05f, long pos = mapEntry.getKey();
mapEntry.getValue().networkDataSourceFuture != null ? Color.red NetRequestTask task = mapEntry.getValue();
: this.isSectionAllowedToGenerate(mapEntry.getKey(), Objects.requireNonNull(this.level.getTargetPosForGeneration())) ? Color.gray
: Color.darkGray Color color;
)); if (task.networkDataSourceFuture != null)
{
color = Color.RED;
}
else
{
boolean taskInAllowedGenRadius = this.sectionInAllowedGenerationRadius(pos, targetPos);
if (taskInAllowedGenRadius)
{
color = Color.GRAY;
}
else
{
color = Color.DARK_GRAY;
}
}
renderer.renderBox(new DebugRenderer.Box(pos, -32f, 64f, 0.05f, color));
} }
} }
@@ -454,11 +434,12 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
// helper classes // // helper classes //
//================// //================//
protected static class RequestQueueEntry protected static class NetRequestTask
{ {
public final long pos;
/** encapsulates the entire request, including client side queuing and the actual server request */ /** encapsulates the entire request, including client side queuing and the actual server request */
public final CompletableFuture<ERequestResult> future = new CompletableFuture<>(); public final CompletableFuture<DataSourceRetrievalResult> future = new CompletableFuture<>();
public final Consumer<FullDataSourceV2> dataSourceConsumer;
/** will be null if we want to retrieve the LOD regardless of when it was last updated */ /** will be null if we want to retrieve the LOD regardless of when it was last updated */
@Nullable @Nullable
public final Long updateTimestamp; public final Long updateTimestamp;
@@ -477,23 +458,14 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
// constructor // // constructor //
//=============// //=============//
public RequestQueueEntry( public NetRequestTask(long pos, @Nullable Long updateTimestamp)
Consumer<FullDataSourceV2> dataSourceConsumer,
@Nullable Long updateTimestamp)
{ {
this.dataSourceConsumer = dataSourceConsumer; this.pos = pos;
this.updateTimestamp = updateTimestamp; this.updateTimestamp = updateTimestamp;
} }
} }
public enum ERequestResult
{
SUCCEEDED,
REQUIRES_SPLITTING,
FAILED,
}
} }
@@ -18,6 +18,7 @@ import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartial
import com.seibel.distanthorizons.core.network.session.NetworkSession; import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.Closeable; import java.io.Closeable;
@@ -29,6 +30,10 @@ public class ClientNetworkState implements Closeable
.fileLevelConfig(Config.Common.Logging.logNetworkEventToFile) .fileLevelConfig(Config.Common.Logging.logNetworkEventToFile)
.build(); .build();
protected static final DhLogger CONFIG_CHANGE_LOGGER = new DhLoggerBuilder()
.fileLevelConfig(Config.Common.Logging.logConnectionConfigChangesToFile)
.build();
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
@@ -44,6 +49,7 @@ public class ClientNetworkState implements Closeable
*/ */
public NetworkSession getSession() { return this.networkSession; } public NetworkSession getSession() { return this.networkSession; }
@NotNull
public SessionConfig sessionConfig = new SessionConfig(); public SessionConfig sessionConfig = new SessionConfig();
private volatile boolean configReceived = false; private volatile boolean configReceived = false;
@@ -129,7 +135,9 @@ public class ClientNetworkState implements Closeable
{ {
this.serverSupportStatus = EServerSupportStatus.FULL; this.serverSupportStatus = EServerSupportStatus.FULL;
LOGGER.info("Connection config has been changed: [" + message.config + "]."); String configChanges = this.sessionConfig.getDifferencesAsString(message.config);
CONFIG_CHANGE_LOGGER.info("Connection config has been changed: [" + configChanges + "].");
this.sessionConfig = message.config; this.sessionConfig = message.config;
this.configReceived = true; this.configReceived = true;
}); });
@@ -2,6 +2,7 @@ package com.seibel.distanthorizons.core.multiplayer.client;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue; import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue;
import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.level.DhClientLevel; import com.seibel.distanthorizons.core.level.DhClientLevel;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
@@ -35,12 +36,12 @@ public class SyncOnLoadRequestQueue extends AbstractFullDataNetworkRequestQueue
@Override @Override
protected int getRequestRateLimit() { return this.networkState.sessionConfig.getSyncOnLoginRateLimit(); } protected int getRequestRateLimit() { return this.networkState.sessionConfig.getSyncOnLoginRateLimit(); }
@Override @Override
protected boolean isSectionAllowedToGenerate(long sectionPos, DhBlockPos2D targetPos) protected boolean sectionInAllowedGenerationRadius(long sectionPos, DhBlockPos2D targetPos)
{ {
return DhSectionPos.getChebyshevSignedBlockDistance(sectionPos, targetPos) <= this.networkState.sessionConfig.getMaxSyncOnLoadDistance() * 16; return DhSectionPos.getChebyshevSignedBlockDistance(sectionPos, targetPos) <= this.networkState.sessionConfig.getMaxSyncOnLoadDistance() * 16;
} }
@Override @Override
protected boolean onBeforeRequest(long sectionPos, CompletableFuture<ERequestResult> future) { return true; } protected boolean onBeforeRequest(long sectionPos, CompletableFuture<DataSourceRetrievalResult> future) { return true; }
@Override @Override
protected String getQueueName() { return "Sync On Login Queue"; } protected String getQueueName() { return "Sync On Login Queue"; }
@@ -33,9 +33,9 @@ public class SessionConfig implements INetworkObject
registerConfigEntry(Config.Common.WorldGenerator.enableDistantGeneration, Boolean::logicalAnd); registerConfigEntry(Config.Common.WorldGenerator.enableDistantGeneration, Boolean::logicalAnd);
registerConfigEntry(Config.Server.maxGenerationRequestDistance, Math::min); registerConfigEntry(Config.Server.maxGenerationRequestDistance, Math::min);
registerConfigEntry(Config.Server.generationBoundsX, (x, y) -> y); registerConfigEntry(Config.Common.WorldGenerator.generationCenterChunkX, (x, y) -> y);
registerConfigEntry(Config.Server.generationBoundsZ, (x, y) -> y); registerConfigEntry(Config.Common.WorldGenerator.generationCenterChunkZ, (x, y) -> y);
registerConfigEntry(Config.Server.generationBoundsRadius, (x, y) -> y); registerConfigEntry(Config.Common.WorldGenerator.generationMaxChunkRadius, (x, y) -> y);
registerConfigEntry(Config.Server.generationRequestRateLimit, Math::min); registerConfigEntry(Config.Server.generationRequestRateLimit, Math::min);
registerConfigEntry(Config.Server.enableRealTimeUpdates, Boolean::logicalAnd); registerConfigEntry(Config.Server.enableRealTimeUpdates, Boolean::logicalAnd);
@@ -68,9 +68,9 @@ public class SessionConfig implements INetworkObject
public boolean isDistantGenerationEnabled() { return this.getValue(Config.Common.WorldGenerator.enableDistantGeneration); } public boolean isDistantGenerationEnabled() { return this.getValue(Config.Common.WorldGenerator.enableDistantGeneration); }
public int getMaxGenerationRequestDistance() { return this.getValue(Config.Server.maxGenerationRequestDistance); } public int getMaxGenerationRequestDistance() { return this.getValue(Config.Server.maxGenerationRequestDistance); }
public Integer getGenerationBoundsX() { return this.getValue(Config.Server.generationBoundsX); } public Integer getGenerationCenterChunkX() { return this.getValue(Config.Common.WorldGenerator.generationCenterChunkX); }
public Integer getGenerationBoundsZ() { return this.getValue(Config.Server.generationBoundsZ); } public Integer getGenerationCenterChunkZ() { return this.getValue(Config.Common.WorldGenerator.generationCenterChunkZ); }
public Integer getGenerationBoundsRadius() { return this.getValue(Config.Server.generationBoundsRadius); } public Integer getGenerationMaxChunkRadius() { return this.getValue(Config.Common.WorldGenerator.generationMaxChunkRadius); }
public int getGenerationRequestRateLimit() { return this.getValue(Config.Server.generationRequestRateLimit); } public int getGenerationRequestRateLimit() { return this.getValue(Config.Server.generationRequestRateLimit); }
public boolean isRealTimeUpdatesEnabled() { return this.getValue(Config.Server.enableRealTimeUpdates); } public boolean isRealTimeUpdatesEnabled() { return this.getValue(Config.Server.enableRealTimeUpdates); }
@@ -162,6 +162,34 @@ public class SessionConfig implements INetworkObject
//=========//
// logging //
//=========//
/**
* example: "common.playerBandwidthLimit:[497], " <br>
* Useful to see what was changed when receiving a new config from the server.
*/
public String getDifferencesAsString(SessionConfig that)
{
StringBuilder stringBuilder = new StringBuilder();
for (String key : this.values.keySet())
{
String thisFieldString = this.values.get(key) + "";
String thatFieldString = that.values.get(key) + "";
if (!thisFieldString.equals(thatFieldString))
{
stringBuilder.append(key+":["+thisFieldString+"], ");
}
}
return stringBuilder.toString();
}
//================// //================//
// base overrides // // base overrides //
//================// //================//
@@ -9,7 +9,7 @@ import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMe
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO; import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.buffer.Unpooled;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -36,7 +36,7 @@ public class FullDataPayloadReceiver implements AutoCloseable
{ {
if (message.isFirst) if (message.isFirst)
{ {
composite = UnpooledByteBufAllocator.DEFAULT.compositeBuffer(); composite = Unpooled.compositeBuffer();
LOGGER.debug("Created new full data buffer [" + message.bufferId + "]: [" + composite + "]"); LOGGER.debug("Created new full data buffer [" + message.bufferId + "]: [" + composite + "]");
} }
else if (composite == null) else if (composite == null)
@@ -55,12 +55,11 @@ public class FullDataPayloadReceiver implements AutoCloseable
public FullDataSourceV2DTO decodeDataSource(FullDataPayload payload) public FullDataSourceV2DTO decodeDataSource(FullDataPayload payload)
{ {
CompositeByteBuf compositeByteBuffer = this.buffersById.get(payload.dtoBufferId); CompositeByteBuf compositeByteBuffer = this.buffersById.get(payload.dtoBufferId);
LodUtil.assertTrue(compositeByteBuffer != null); LodUtil.assertTrue(compositeByteBuffer != null, "decoded data source missing byte buffer");
try try
{ {
FullDataSourceV2DTO dataSourceDto = INetworkObject.decodeToInstance(FullDataSourceV2DTO.CreateEmptyDataSourceForDecoding(), compositeByteBuffer); FullDataSourceV2DTO dataSourceDto = INetworkObject.decodeToInstance(FullDataSourceV2DTO.CreateEmptyDataSourceForDecoding(), compositeByteBuffer);
LOGGER.debug("Buffer {} DTO: {}", payload.dtoBufferId, dataSourceDto);
return dataSourceDto; return dataSourceDto;
} }
finally finally
@@ -14,6 +14,7 @@ import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceR
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceResponseMessage; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceResponseMessage;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO; import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import java.util.List; import java.util.List;
@@ -21,7 +22,7 @@ import java.util.Map;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
public class FullDataSourceRequestHandler public class FullDataSourceRequestHandler implements AutoCloseable
{ {
private static final DhLogger LOGGER = new DhLoggerBuilder() private static final DhLogger LOGGER = new DhLoggerBuilder()
.fileLevelConfig(Config.Common.Logging.logNetworkEventToFile) .fileLevelConfig(Config.Common.Logging.logNetworkEventToFile)
@@ -29,6 +30,8 @@ public class FullDataSourceRequestHandler
private final AbstractDhServerLevel serverLevel; private final AbstractDhServerLevel serverLevel;
private final ThreadPoolExecutor tickerThread;
private String getLevelIdentifier() { return this.serverLevel.getLevelWrapper().getDhIdentifier(); } private String getLevelIdentifier() { return this.serverLevel.getLevelWrapper().getDhIdentifier(); }
private GeneratedFullDataSourceProvider fullDataSourceProvider() { return this.serverLevel.serverside.fullDataFileHandler; } private GeneratedFullDataSourceProvider fullDataSourceProvider() { return this.serverLevel.serverside.fullDataFileHandler; }
private List<BeaconBeamDTO> getAllBeamsForPos(long pos) { return this.serverLevel.beaconBeamRepo.getAllBeamsForPos(pos); } private List<BeaconBeamDTO> getAllBeamsForPos(long pos) { return this.serverLevel.beaconBeamRepo.getAllBeamsForPos(pos); }
@@ -37,12 +40,22 @@ public class FullDataSourceRequestHandler
private final ConcurrentMap<Long, DataSourceRequestGroup> requestGroupsByFutureId = new ConcurrentHashMap<>(); private final ConcurrentMap<Long, DataSourceRequestGroup> requestGroupsByFutureId = new ConcurrentHashMap<>();
//=============//
// constructor //
//=============//
public FullDataSourceRequestHandler(AbstractDhServerLevel serverLevel) public FullDataSourceRequestHandler(AbstractDhServerLevel serverLevel)
{ {
this.serverLevel = serverLevel; this.serverLevel = serverLevel;
String levelId = this.serverLevel.getServerLevelWrapper().getDhIdentifier();
this.tickerThread = ThreadUtil.makeSingleDaemonThreadPool("DataSource Request Ticker ["+levelId+"]");
this.tickerThread.execute(this::tickLoop);
} }
//==================// //==================//
// network handling // // network handling //
//==================// //==================//
@@ -161,12 +174,21 @@ public class FullDataSourceRequestHandler
DataSourceRequestGroup requestGroup = this.requestGroupsByPos.computeIfAbsent(requestData.sectionPos(), pos -> DataSourceRequestGroup requestGroup = this.requestGroupsByPos.computeIfAbsent(requestData.sectionPos(), pos ->
{ {
DataSourceRequestGroup newGroup = new DataSourceRequestGroup(pos); DataSourceRequestGroup newGroup = new DataSourceRequestGroup(pos);
try
{
newGroup.tryAddRequest(requestData); newGroup.tryAddRequest(requestData);
createdNewGroup.set(true); createdNewGroup.set(true);
this.tryFulfillDataSourceRequestGroup(newGroup, pos); this.tryFulfillDataSourceRequestGroup(newGroup, pos);
LOGGER.debug("[" + this.getLevelIdentifier() + "] Created request group for pos [" + DhSectionPos.toString(pos) + "]."); LOGGER.debug("[" + this.getLevelIdentifier() + "] Created request group for pos [" + DhSectionPos.toString(pos) + "].");
return newGroup;
}
catch (Exception e)
{
LOGGER.error("Unable to queue request for pos: ["+DhSectionPos.toString(requestData.sectionPos())+"], error: ["+e.getMessage()+"].", e);
}
return newGroup; return newGroup;
}); });
@@ -214,14 +236,90 @@ public class FullDataSourceRequestHandler
} }
} }
private void tryFulfillDataSourceRequestGroup(DataSourceRequestGroup requestGroup, long pos)
{
final GeneratedFullDataSourceProvider provider = this.fullDataSourceProvider();
public void tick() provider.getAsync(pos)
.thenAccept((FullDataSourceV2 fullDataSource) ->
{
if (provider.generationStepsAreFullyGenerated(fullDataSource.columnGenerationSteps))
{
//LOGGER.info("sending - complete [" + DhSectionPos.toString(pos) + "]");
requestGroup.fullDataSource = fullDataSource;
return;
}
fullDataSource.close();
if (DhSectionPos.getDetailLevel(pos) > (Config.Common.WorldGenerator.distantGeneratorMode.get() == EDhApiDistantGeneratorMode.INTERNAL_SERVER
? DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL
: this.serverLevel.serverside.fullDataFileHandler.lowestDataDetailLevel()))
{
// Make this group unavailable for adding into
this.requestGroupsByPos.remove(pos);
if (!requestGroup.tryClose())
{
//LOGGER.info("closing [" + DhSectionPos.toString(pos) + "]");
return;
}
for (DataSourceRequestGroup.RequestData requestData : requestGroup.requestMessages.values())
{
//LOGGER.info("sending [" + DhSectionPos.toString(pos) + "] - ["+DhSectionPos.toString(requestData.sectionPos())+"]");
this.requestGroupsByFutureId.remove(requestData.futureId());
requestData.rateLimiterSet.generationRequestRateLimiter.release();
requestData.message.sendResponse(new SectionRequiresSplittingException());
}
}
else if (requestGroup.isWorldGenTaskComplete())
{
//LOGGER.info("sending - retry [" + DhSectionPos.toString(pos) + "]");
this.tryFulfillDataSourceRequestGroup(requestGroup, pos);
}
else
{
//LOGGER.info("queueing incomplete world gen [" + DhSectionPos.toString(pos) + "]");
this.fullDataSourceProvider().queuePositionForRetrieval(pos);
}
});
}
public void onWorldGenTaskComplete(long pos)
{
DataSourceRequestGroup requestGroup = this.requestGroupsByPos.get(pos);
if (requestGroup != null)
{
requestGroup.markWorldGenTaskComplete();
this.tryFulfillDataSourceRequestGroup(requestGroup, pos);
}
}
//=========//
// ticking //
//=========//
private void tickLoop()
{
try
{
while (!Thread.interrupted())
{
Thread.sleep(20);
this.tick();
}
}
catch (InterruptedException ignore) { }
}
private void tick()
{ {
// Send finished data source requests // Send finished data source requests
for (Map.Entry<Long, DataSourceRequestGroup> entry : this.requestGroupsByPos.entrySet()) for (Map.Entry<Long, DataSourceRequestGroup> entry : this.requestGroupsByPos.entrySet())
{ {
DataSourceRequestGroup requestGroup = entry.getValue(); DataSourceRequestGroup requestGroup = entry.getValue();
if (requestGroup.fullDataSource == null) if (requestGroup.fullDataSource == null)
{ {
continue; continue;
@@ -260,57 +358,18 @@ public class FullDataSourceRequestHandler
} }
} }
private void tryFulfillDataSourceRequestGroup(DataSourceRequestGroup requestGroup, long pos)
//================//
// base overrides //
//================//
@Override
public void close()
{ {
this.fullDataSourceProvider().getAsync(pos).thenAccept(fullDataSource -> this.tickerThread.shutdownNow();
{
if (this.fullDataSourceProvider().isFullyGenerated(fullDataSource.columnGenerationSteps))
{
requestGroup.fullDataSource = fullDataSource;
return;
} }
fullDataSource.close();
if (DhSectionPos.getDetailLevel(pos) > (Config.Common.WorldGenerator.distantGeneratorMode.get() == EDhApiDistantGeneratorMode.INTERNAL_SERVER
? DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL
: this.serverLevel.serverside.fullDataFileHandler.lowestDataDetailLevel()))
{
// Make this group unavailable for adding into
this.requestGroupsByPos.remove(pos);
if (!requestGroup.tryClose())
{
return;
}
for (DataSourceRequestGroup.RequestData requestData : requestGroup.requestMessages.values())
{
this.requestGroupsByFutureId.remove(requestData.futureId());
requestData.rateLimiterSet.generationRequestRateLimiter.release();
requestData.message.sendResponse(new SectionRequiresSplittingException());
}
}
else if (requestGroup.isWorldGenTaskComplete())
{
//LOGGER.info("sending - retry [" + DhSectionPos.toString(pos) + "]");
this.tryFulfillDataSourceRequestGroup(requestGroup, pos);
}
else
{
//LOGGER.info("sending - queueing [" + DhSectionPos.toString(pos) + "]");
this.fullDataSourceProvider().queuePositionForRetrieval(pos);
}
});
}
public void onWorldGenTaskComplete(long pos)
{
DataSourceRequestGroup requestGroup = this.requestGroupsByPos.get(pos);
if (requestGroup != null)
{
requestGroup.markWorldGenTaskComplete();
this.tryFulfillDataSourceRequestGroup(requestGroup, pos);
}
}
} }
@@ -102,8 +102,8 @@ public class ServerPlayerState implements Closeable
private void sendConfigMessage() private void sendConfigMessage()
{ {
double coordinateScale = this.getServerPlayer().getLevel().getDimensionType().getCoordinateScale(); double coordinateScale = this.getServerPlayer().getLevel().getDimensionType().getCoordinateScale();
this.sessionConfig.constrainValue(Config.Server.generationBoundsX, (int) (Config.Server.generationBoundsX.get() / coordinateScale)); this.sessionConfig.constrainValue(Config.Common.WorldGenerator.generationCenterChunkX, (int) (Config.Common.WorldGenerator.generationCenterChunkX.get() / coordinateScale));
this.sessionConfig.constrainValue(Config.Server.generationBoundsZ, (int) (Config.Server.generationBoundsZ.get() / coordinateScale)); this.sessionConfig.constrainValue(Config.Common.WorldGenerator.generationCenterChunkZ, (int) (Config.Common.WorldGenerator.generationCenterChunkZ.get() / coordinateScale));
this.networkSession.sendMessage(new SessionConfigMessage(this.sessionConfig)); this.networkSession.sendMessage(new SessionConfigMessage(this.sessionConfig));
} }
@@ -92,7 +92,8 @@ public class NetworkSession extends AbstractNetworkEventSource
{ {
LOGGER.error("Failed to handle the message. New messages will be ignored.", e); LOGGER.error("Failed to handle the message. New messages will be ignored.", e);
LOGGER.error("Message: ["+message+"]"); LOGGER.error("Message: ["+message+"]");
this.close();
this.close(e);
} }
} }
@@ -40,7 +40,9 @@ public abstract class AbstractPhantomArrayList implements AutoCloseable
/** The Array counts can be 0 or greater. */ /** The Array counts can be 0 or greater. */
public AbstractPhantomArrayList(PhantomArrayListPool phantomArrayListPool, int byteArrayCount, int shortArrayCount, int longArrayCount) public AbstractPhantomArrayList(PhantomArrayListPool phantomArrayListPool, int byteArrayCount, int shortArrayCount, int longArrayCount)
{ {
if (byteArrayCount < 0 || shortArrayCount < 0 || longArrayCount < 0) if (byteArrayCount < 0
|| shortArrayCount < 0
|| longArrayCount < 0)
{ {
throw new IllegalArgumentException("Can't get a negative number of pooled arrays."); throw new IllegalArgumentException("Can't get a negative number of pooled arrays.");
} }
@@ -10,6 +10,7 @@ import org.jetbrains.annotations.Nullable;
import java.lang.ref.SoftReference; import java.lang.ref.SoftReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
/** /**
* This keeps track of all the poolable * This keeps track of all the poolable
@@ -33,7 +34,7 @@ public class PhantomArrayListCheckout implements AutoCloseable
/** Will be null if the parent pool doesn't want leak stack tracing */ /** Will be null if the parent pool doesn't want leak stack tracing */
@Nullable @Nullable
public final String allocationStackTrace; public String allocationStackTrace = null;
private final ArrayList<ByteArrayList> byteArrayLists = new ArrayList<>(); private final ArrayList<ByteArrayList> byteArrayLists = new ArrayList<>();
private final ArrayList<ShortArrayList> shortArrayLists = new ArrayList<>(); private final ArrayList<ShortArrayList> shortArrayLists = new ArrayList<>();
@@ -47,21 +48,20 @@ public class PhantomArrayListCheckout implements AutoCloseable
public PhantomArrayListCheckout(@NotNull PhantomArrayListPool owningPool) public PhantomArrayListCheckout(@NotNull PhantomArrayListPool owningPool)
{ {
if (owningPool.logGarbageCollectedStacks)
{
// TODO remove the top 4 or so lines since those will always be the same (relating to the phantom allocations)
// and aren't helpful when debugging
this.allocationStackTrace = StringUtil.join("\n", Thread.currentThread().getStackTrace());
}
else
{
this.allocationStackTrace = null;
}
this.owningPool = owningPool; this.owningPool = owningPool;
this.ownerSoftReference = new SoftReference<>(this); this.ownerSoftReference = new SoftReference<>(this);
} }
public void onCheckout()
{
if (this.owningPool.logGarbageCollectedStacks)
{
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
StackTraceElement[] trimmedElements = Arrays.copyOfRange(stackTraceElements, 4, stackTraceElements.length);
this.allocationStackTrace = StringUtil.join("\n", trimmedElements).intern();
}
}
//=========// //=========//
@@ -159,6 +159,7 @@ public class PhantomArrayListPool
{ {
// pool is empty, create new checkout // pool is empty, create new checkout
checkout = new PhantomArrayListCheckout(this); checkout = new PhantomArrayListCheckout(this);
checkout.onCheckout();
} }
else else
{ {
@@ -166,6 +167,7 @@ public class PhantomArrayListPool
if (checkout != null) if (checkout != null)
{ {
// use pooled checkout // use pooled checkout
checkout.onCheckout();
} }
else else
{ {
@@ -182,7 +184,8 @@ public class PhantomArrayListPool
"Potential causes: \n" + "Potential causes: \n" +
"1. your allocated memory isn't high enough \n" + "1. your allocated memory isn't high enough \n" +
"2. your DH CPU preset is too high \n" + "2. your DH CPU preset is too high \n" +
"3. your DH quality preset is too high"; "3. your DH quality preset is too high \n" +
"4. you have other memory hungry mod(s)";
LOGGER.warn(message); LOGGER.warn(message);
if (Config.Common.Logging.Warning.showPoolInsufficientMemoryWarning.get()) if (Config.Common.Logging.Warning.showPoolInsufficientMemoryWarning.get())
@@ -42,6 +42,8 @@ import java.util.function.LongConsumer;
* <strong>Too big</strong>, and the LOD dropoff will be very noticeable.<br> * <strong>Too big</strong>, and the LOD dropoff will be very noticeable.<br>
* With those thoughts in mind we decided on a smallest section size of 64 data points square (IE 4x4 chunks). * With those thoughts in mind we decided on a smallest section size of 64 data points square (IE 4x4 chunks).
* *
* TODO absolute vs section detail levels
*
* @author Leetom * @author Leetom
*/ */
public class DhSectionPos public class DhSectionPos
@@ -279,17 +281,24 @@ public class DhSectionPos
+ Math.abs(getCenterBlockPosZ(pos) - blockPos.z); + Math.abs(getCenterBlockPosZ(pos) - blockPos.z);
} }
/** see: {@link DhSectionPos#getChebyshevSignedBlockDistance(long, int, int)} */
public static int getChebyshevSignedBlockDistance(long pos, DhBlockPos blockPos)
{ return getChebyshevSignedBlockDistance(pos, blockPos.getX(), blockPos.getZ()); }
/** see: {@link DhSectionPos#getChebyshevSignedBlockDistance(long, int, int)} */
public static int getChebyshevSignedBlockDistance(long pos, DhBlockPos2D blockPos)
{ return getChebyshevSignedBlockDistance(pos, blockPos.x, blockPos.z); }
/** /**
* Returns the signed distance from a given block to a given section. <br> * Returns the signed distance from a given block to a given section. <br>
* Essentially acts like a distance from the block to the nearest edge of the section, * Essentially acts like a distance from the block to the nearest edge of the section,
* except inside the section it's negative. <br> * except inside the section it's negative. <br>
* Useful for detail level insensitive distance comparisons. * Useful for detail level insensitive distance comparisons.
*/ */
public static int getChebyshevSignedBlockDistance(long pos, DhBlockPos2D blockPos) public static int getChebyshevSignedBlockDistance(long pos, int blockPosX, int blockPosZ)
{ {
return Math.max( return Math.max(
Math.abs(getCenterBlockPosX(pos) - blockPos.x), Math.abs(getCenterBlockPosX(pos) - blockPosX),
Math.abs(getCenterBlockPosZ(pos) - blockPos.z) Math.abs(getCenterBlockPosZ(pos) - blockPosZ)
) - getBlockWidth(pos) / 2; ) - getBlockWidth(pos) / 2;
} }

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