Compare commits

...

248 Commits

Author SHA1 Message Date
James Seibel 26d4220967 Add logging/messaging for corrupted DB files 2025-12-09 07:12:33 -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 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 b82a59ecbc Speed up shutdown and reduce logging 2025-11-14 07:46:02 -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
James Seibel 6fe0df7d0f Don't duplicate adjacent data 2025-11-13 07:18:09 -06:00
James Seibel b9746381eb Add varint encoding for full data
Closes Merge !93
Thanks Ryan Hitchman!
2025-11-12 07:21:54 -06:00
s809 91dffa3c3e Prevent auto-pause while pregen is running 2025-11-11 23:48:13 +05:00
James Seibel 6eb24ecde1 re-add GPU upload config including "none" 2025-11-10 07:33:03 -06:00
James Seibel 767753c004 add logging to infinite repo unit test 2025-11-10 06:56:24 -06:00
James Seibel 97442f8833 Fix config min/max validation default setup 2025-11-08 19:11:56 -06:00
James Seibel 62359e3dde remove LOD load pref logging 2025-11-08 19:08:30 -06:00
James Seibel b5199cfa87 Optimize ColumnBox building 2025-11-08 18:08:02 -06:00
James Seibel f0acc73c56 Add compass Index to Edirection 2025-11-08 17:48:30 -06:00
James Seibel f9dfc38bf1 Separate BlockBiomeWrapperPair from FullDataPointIdMap 2025-11-08 17:47:50 -06:00
James Seibel 5c5d39738e minor reformating 2025-11-08 17:44:08 -06:00
James Seibel 27fb629c22 default unsafe UI values to config option 2025-11-08 17:41:07 -06:00
James Seibel c374bf7ca8 test 2025-11-08 08:14:03 -06:00
James Seibel 7e04b12e37 Optimize PrefRecorder slightly 2025-11-07 07:41:59 -06:00
James Seibel 67637dbf10 detail level renaming 2025-11-06 21:50:43 -06:00
James Seibel 6456651d27 Handle non-adjacent data conversion 2025-11-06 21:28:25 -06:00
James Seibel 9343854b4a Clean up data source getters 2025-11-06 07:42:58 -06:00
James Seibel 5fd8ed840f Add adjacent data to FullDataDTO for faster loading 2025-11-06 07:35:23 -06:00
James Seibel 4d4d8fd8e9 Split up full data source provider into multiple classes 2025-11-04 07:46:06 -06:00
James Seibel bf05965015 remove IDataSource 2025-11-02 07:20:07 -06:00
James Seibel 47569f2b3c minor dataSourceHandler refactor 2025-11-01 16:33:07 -04:00
James Seibel 0567195f73 minor datasource renaming 2025-11-01 16:27:54 -04:00
James Seibel e355366ffc Clean up EDhDirection 2025-11-01 09:06:53 -04:00
James Seibel 3681d50eb2 minor comment cleanup 2025-11-01 08:42:25 -04:00
James Seibel e0c143881f Fix compression mode javadoc 2025-11-01 08:34:02 -04:00
James Seibel 2a49fdee7f Add experimental loading option and perfRecorder 2025-10-28 07:46:53 -05:00
James Seibel f39e06b6dc remove unused interrupt check 2025-10-28 07:36:28 -05:00
James Seibel 0d5c454dd4 remove unused ColumnQuadView methods 2025-10-28 07:24:24 -05:00
James Seibel 1b447fdc98 Fix logger builder doubling DH name 2025-10-28 07:23:58 -05:00
James Seibel d84ba05380 minor style reformatting 2025-10-27 06:52:36 -05:00
James Seibel 3e7f160fcd Merge Fade apply shaders 2025-10-25 11:54:32 -05:00
James Seibel dcaf334828 use same fade apply frag shader 2025-10-25 11:39:27 -05:00
James Seibel 789306ccff Add far clip fading 2025-10-25 11:06:19 -05:00
James Seibel e33fa3cb5e Rename fade renderer -> Vanilla Fade renderer 2025-10-25 09:36:11 -05:00
James Seibel 8f99117066 Fix iris not setting face culling in the MC state manager 2025-10-25 08:38:29 -05:00
James Seibel 2136c0fe83 framebuffer name consistency fix 2025-10-25 08:37:11 -05:00
James Seibel 7a6cffe19d Move getKeyedLevelDimensionName() to implementation 2025-10-23 07:17:46 -05:00
James Seibel 06bef93c82 run occlusion culling whenever saving a LOD
Also run culling for every column in an LOD, which improves compression by about 20%
- Thanks Scaevolus
2025-10-22 07:25:04 -05:00
James Seibel 939e45ce62 minor RenderBufferHandler optimization and bugfix 2025-10-19 16:40:57 -05:00
James Seibel 7f958269e4 Fix not reloading LODs on horizontal quality change 2025-10-19 16:16:35 -05:00
James Seibel 07e3091d13 Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2025-10-19 16:06:24 -05:00
James Seibel f7ece2b02e Clean up LodRendering logic 2025-10-19 16:06:00 -05:00
s809 bd796c2ce0 Fix handling of empty server keys 2025-10-19 22:58:07 +05:00
s809 4e6be35da9 Merge branch 'feature/server-keys' 2025-10-19 22:57:50 +05:00
James Seibel 0e0e1e1b0f Make LodRenderer a singleton 2025-10-18 11:42:36 -05:00
James Seibel f4ab101403 Dh and level wrapper refactoring and commenting 2025-10-17 07:21:16 -05:00
James Seibel 0902d3f0f5 merge loggers and add logger builder 2025-10-15 17:37:08 -05:00
James Seibel 75c2758fd5 up version number 2.3.6 -> 2.3.7 2025-10-13 18:03:19 -05:00
James Seibel 9ddd917f3b remove dev from version number 2025-10-13 16:32:29 -05:00
James Seibel c5945b1254 remove unused EConfigEntryPerformance
It is a cool idea, but one that unfortunately never got implemented
2025-10-13 07:46:53 -05:00
James Seibel 9060579615 Fix world gen progress ui button 2025-10-13 07:42:46 -05:00
James Seibel 8f0217185f Improve config gui object casting 2025-10-13 07:33:27 -05:00
James Seibel 656971b0b9 typo fixing for major config refactoring 2025-10-12 21:10:18 -05:00
James Seibel 5fa3a11024 major config backend refactoring 2025-10-12 20:56:15 -05:00
James Seibel ed3d00bfce up version number 2.3.5 -> 2.3.6 2025-10-11 20:55:36 -05:00
James Seibel ba2681d7b2 remove dev from version number 2025-10-11 20:54:00 -05:00
James Seibel 168570f21f minor lodRenderer refactor 2025-10-11 18:40:32 -05:00
James Seibel b3928d3b1f rename renderFade -> renderFadeTransparent 2025-10-11 11:14:03 -05:00
James Seibel 57aec6092c comment out delayed save cache test to improve build speed 2025-10-10 07:00:32 -05:00
James Seibel 278f4b1642 move more logic into a global RenderState 2025-10-10 06:58:47 -05:00
James Seibel 26d0b5c571 disable world gen progress display by default 2025-10-09 20:12:31 -05:00
James Seibel 3cb8bbeaa7 Fix some tasks being dropped 2025-10-09 20:12:22 -05:00
James Seibel 009cfdce93 Fix VANILLA_CHUNKS API world gen 2025-10-08 17:27:04 -05:00
James Seibel 463565384b Re-add biome blending 2025-10-05 16:23:09 -05:00
James Seibel aed5bb4163 Separate DH pool threads and new executor "Render Loader"
Having separate threads for each task behind the scenes allows for easier performance monitoring vs having a single threadpool that handles everything.
2025-10-04 20:10:10 -05:00
James Seibel bd517e54cf remove duplicate "thread" name in ticker threads 2025-10-04 19:54:19 -05:00
James Seibel b323b7e52d rename uniforms in SSAO shader 2025-10-04 13:45:18 -05:00
James Seibel 32b3eac589 add nullable attributes to world getters 2025-10-04 10:48:34 -05:00
James Seibel 569a5442a9 fix a potential null pointer on world shutdown 2025-10-04 10:26:53 -05:00
James Seibel 25213cae39 Fix noise texture only applying changes on level change 2025-10-04 10:26:34 -05:00
James Seibel 82bb5ef64e fix typo in far falloff 2025-10-03 06:58:04 -05:00
James Seibel a8748471df Handle null pointer on server shutdown 2025-10-02 20:29:42 -05:00
James Seibel 721124b886 Write custom timeout logic for DelayedDataSourceCache
This should make the code a bit more transparent vs using the CacheBuilder, plus hopefully resolve a concurrent writing issue that causes monoliths
2025-10-02 20:29:26 -05:00
James Seibel 85e52301d6 typo in ApiEventInjector 2025-10-02 18:08:47 -05:00
James Seibel 08ede3351d Add DhApiChunkProcessingEvent 2025-10-02 18:03:27 -05:00
James Seibel 9690c898b0 handle null pointer on server shutdown 2025-10-02 07:33:05 -05:00
James Seibel 328336bd29 Allow unbinding Dependencies
TODO replacing may be a better way to handle it
2025-10-02 07:32:58 -05:00
James Seibel 75f0061d97 remove unused ServerPlayerWrapper methods 2025-10-02 07:07:31 -05:00
James Seibel be87c79b1b Handle a few rendering setup edge cases 2025-10-02 07:07:22 -05:00
James Seibel 12a885aa6e Manually close compression streams to try reducing GC reliance 2025-09-29 17:21:01 -05:00
James Seibel d33be490a7 cull LOD rendering on the quad tree 2025-09-29 07:28:03 -05:00
James Seibel cb654f2429 replace IConfigEntry apiValuePresent -> apiIsOverriding 2025-09-28 16:16:31 -05:00
James Seibel 2705cb679e minor config handler refactoring 2025-09-28 16:14:22 -05:00
James Seibel 372fcedc7c add IConfigEntry.apiValuePresent 2025-09-27 20:58:15 -05:00
James Seibel 25e909203d prep for Config UI refactoring 2025-09-27 20:55:37 -05:00
s809 b312582ce4 Add global bandwidth limit setting 2025-09-26 21:45:10 +05:00
James Seibel 73324c71ec Force Mac upload method to DATA
Maybe will help with crashing/memory corruption?
Data is the most basic upload method in GL so Mac should be able to support it a lot better than BUFFER_STORAGE.
2025-09-24 07:23:14 -05:00
James Seibel 0cdb5cf0ec Remove Mac state validation option 2025-09-24 07:13:51 -05:00
James Seibel cbfb1625bc add extra logic to proof-of-concept java swing UI 2025-09-21 21:28:56 -05:00
James Seibel 25e69d03ba Make config lang test return empty string if up to date 2025-09-21 21:28:36 -05:00
James Seibel 9564f02283 maybe fix freebsd OS crashing 2025-09-20 22:40:53 -05:00
James Seibel 9e7378be63 Merge branch 'merge-bedrock' 2025-09-20 16:16:36 -05:00
James Seibel 2495c38dc2 Merge branch 'merge-bedrock' 2025-09-20 15:23:34 -05:00
James Seibel 17fcdb428c finish glproxy comment 2025-09-20 15:14:28 -05:00
James Seibel 944e4f9cb4 Add experimental option to maybe help with Mac crashing 2025-09-20 15:10:54 -05:00
James Seibel 7c0b746220 re-add notnull anotation to ClientPluginChannelApi 2025-09-20 14:21:29 -05:00
Fabian Maurer b4cb390333 Use correct Supplier interface (1.7.10)
It works on modern since
com.google.common.base.Supplier implements
java.util.function.Supplier
but that is not guaranteed
2025-09-19 14:16:36 +02:00
Fabian Maurer 15cda35434 Remove dependency on org.checkerframework (1.7.10) 2025-09-19 14:16:36 +02:00
Fabian Maurer 361d251aa2 Replace isLessSpecificThan with helper function (for 1.7.10) 2025-09-19 14:16:36 +02:00
Fabian Maurer a565e7d906 User older netty functions (1.7.10) 2025-09-19 14:16:36 +02:00
James Seibel 57bbb12b39 Fix "CUSTOM" quality preset when Iris is present 2025-09-16 07:44:18 -05:00
James Seibel df17c1cc1b include world gen chunk/sec rate in progress log 2025-09-14 08:18:37 -05:00
James Seibel a4f7aad306 change world gen progress message to reduce confusion 2025-09-14 08:18:14 -05:00
James Seibel 1b2c1a59f9 Improve world gen task queue speed slightly 2025-09-13 17:59:39 -05:00
James Seibel f0bcf88b35 cache a few repo sql strings 2025-09-13 17:06:33 -05:00
James Seibel 5dbda75c0b add a unit test for SQL update performance testing 2025-09-13 17:01:40 -05:00
James Seibel 5caa945925 remove sea level from level wrapper 2025-09-11 07:07:21 -05:00
James Seibel 6bdfee3636 remove unexplored terrain rendering 2025-09-11 07:06:15 -05:00
James Seibel 1ec536b7df Add unexplored ocean for overworld 2025-09-10 07:46:21 -05:00
James Seibel 9ffda4d43e ColumnRenderSource doesn't need to be a IDataSource 2025-09-07 16:15:26 -05:00
James Seibel 670ec28b6f improve lod load time slightly
done by caching the ClientLevelWrapper used to determine block colors
2025-09-07 16:15:05 -05:00
James Seibel 771814af98 Fix typo in config 2025-09-06 22:10:24 -05:00
James Seibel 90f1d38233 make unexplored fog slightly lighter 2025-09-06 11:59:44 -05:00
James Seibel 54a4f380bd change world gen wireframe height to match unexplored fog 2025-09-06 11:59:44 -05:00
James Seibel bab421c381 add a config for unexplored fog 2025-09-06 11:59:44 -05:00
James Seibel 9c285c17a9 lower unexplored fog slightly 2025-09-06 11:59:44 -05:00
James Seibel 470a9ce8f1 Close #1036 (LODs reloading twice on config change)
Also clean up config event handling
2025-09-06 11:59:44 -05:00
James Seibel d6b79f8b06 fix concurrency issue during unexplored fog setup 2025-09-06 11:59:44 -05:00
James Seibel 71f1dce956 Add unexplored fog 2025-09-06 11:59:44 -05:00
James Seibel 9857eb337f Add remove(obj) and remove(index) to RenderableBoxGroup 2025-09-06 11:59:44 -05:00
James Seibel bced9938f3 Add unexplored fog proof of concept 2025-09-06 11:59:25 -05:00
James Seibel 7f46257e1a add TODO to testRenderer 2025-09-06 09:33:49 -05:00
James Seibel 5f8b566486 improve generic obj render perf logging 2025-09-06 09:33:29 -05:00
James Seibel 9fe2a3fa7b minor dontMergeColoredColumns reformat and comment 2025-09-06 08:53:09 -05:00
James Seibel eb6750bb8d Merge branch 'dontMergeColoredColumns' 2025-09-06 08:38:58 -05:00
James Seibel e86487ab9d Fix LOD-only rendering mode 2025-09-06 08:38:34 -05:00
James Seibel 5423b49f3d Merge !83 (Improve Chunk Update Queue) 2025-09-05 22:23:25 -05:00
James Seibel a2c6f906fa update compression unit test file path 2025-09-05 07:10:49 -05:00
Fabian Maurer d51474a64a Don't merge blocks that get colored by blocks above into columns 2025-09-04 17:56:54 +02:00
James Seibel 5b41c7d48a add (native) ZStd compression as default compressor 2025-09-03 07:39:58 -05:00
s809 034ec7d656 Bump protocol version 2025-08-16 21:01:45 +05:00
s809 fb5e15a2f1 Add a server keys feature 2025-08-16 20:59:28 +05:00
s809 674fc30e77 Replace pooled buffers with unpooled 2025-08-07 17:55:22 +05:00
s809 a05bd307f9 Reduce network logging by default 2025-07-27 23:21:13 +05:00
James Seibel d78a50ce49 up version number 2.3.4 -> 2.3.5 2025-07-19 14:59:14 -05:00
James Seibel 013eab9268 add space to self updater warning log 2025-07-19 14:57:22 -05:00
James Seibel 435cbde238 remove dev from version number 2025-07-19 14:56:53 -05:00
s809 d7040bad13 Load level on player add if missing 2025-07-19 19:05:52 +05:00
James Seibel a588070ce1 up version number 2.3.3 -> 2.3.4 2025-07-12 09:35:05 -05:00
James Seibel d156772438 remove dev from the version number 2025-07-12 09:34:18 -05:00
James Seibel de7ae41769 Fix API config renderingEnabled() changing the user value
Fixes #1083
2025-07-12 08:16:35 -05:00
James Seibel 618ad1938b full data DTO close data source if corrupted 2025-07-10 22:24:33 -05:00
James Seibel 5b10263f82 minor format cleanup 2025-07-10 07:27:23 -05:00
James Seibel 34f914c52f Mark a unit test as deprecated
Done to suppress warnings in compiler log
2025-07-10 07:26:41 -05:00
James Seibel 67b766c674 Fix monoliths when connected to a server 2025-07-10 07:26:14 -05:00
James Seibel a3e7469203 Fixes !1078 (lag due to beacon updating on server) 2025-07-09 07:28:25 -05:00
James Seibel 4ecaa6a9a1 Potentially fix an issue with AMD GPU shader compiling
Fix from Cortex and the Canvas mod
2025-07-08 07:22:36 -05:00
James Seibel 24f9dadc58 Disable vanilla fading when shaders are active 2025-07-07 07:49:36 -05:00
s809 b3ebaffa85 Disable enableAdaptiveTransferSpeed bby default 2025-07-01 22:03:12 +05:00
James Seibel b7ac1909d6 Fix config UI changes not always saving 2025-07-01 07:45:26 -05:00
James Seibel 32c3118afa comment out Z_STD compression 2025-06-30 06:54:27 -05:00
James Seibel 3a525f53f0 Rename world gen mode "Internal Server" -> "Full - Save Chunks" 2025-06-28 13:57:58 -05:00
James Seibel f3947312c1 Re-Add Z_STD compression for testing 2025-06-28 11:37:06 -05:00
James Seibel 839ea1e778 increase ram amount for unit tests
needed for compression tests
2025-06-28 11:36:17 -05:00
James Seibel d2becd2c03 Fix rare fade error when restarting the LodRenderer 2025-06-28 10:45:36 -05:00
James Seibel 7d87347199 Fix multiplayer null pointer 2025-06-28 09:22:23 -05:00
James Seibel f4117751c9 Fix world-gen progress not showing in release builds 2025-06-27 07:29:31 -05:00
James Seibel a8a085f296 Move RenderState to core 2025-06-26 07:50:53 -05:00
James Seibel 317319593e rename renderDeferredLods -> renderDeferredLodsForShaders 2025-06-25 07:47:08 -05:00
James Seibel 4633f90a03 Add null handling to ServerPlayerStateManager.handlePluginMessage() 2025-06-25 07:45:43 -05:00
James Seibel 5802bbb3f3 keep cave culling for medium quality preset
This may be changed back at some point in the future, but depending on the usecase (IE vanilla survival) cave culling is generally better than not having it, and if people see weirdness they can probably guess that increasing the quality preset may fix it.
2025-06-23 07:23:26 -05:00
James Seibel e93d5b90f1 Disable cave culling for medium quality and higher 2025-06-17 07:15:28 -05:00
James Seibel 9be56607a5 Reduce stuttering with fast world gen 2025-06-14 16:17:28 -05:00
James Seibel 91743bf742 Add Api Before/After Text Create events
Deprecate DhApiColorDepthTextureCreatedEvent since it is less obvious when it fires
2025-06-09 07:50:21 -05:00
James Seibel d40d293f54 Fix hash collisions in FullDataPointIdMap 2025-06-06 07:43:38 -05:00
James Seibel a075e60e3e Fix GLMC.glDeleteTextures() calls 2025-06-04 07:07:39 -05:00
s809 d72c7c3695 Check LOD timestamps in file handler threads 2025-06-03 23:41:47 +05:00
Ran 309fa07664 Merge branch 'fix_max_y' into 'main'
Fix max Y validation

See merge request distant-horizons-team/distant-horizons-core!85
2025-05-18 00:32:51 +00:00
Stewart Borle 0a017567c4 Fix max Y validation 2025-05-18 00:32:51 +00:00
James Seibel e01261da5c Remove line ending from editorconfig
Done to fix some issues with some devs on linux
2025-05-17 11:47:00 -05:00
James Seibel a0879d07c5 json indent 2 -> 4
for consistency
2025-05-17 11:25:18 -05:00
Ran bbb15263f2 Fix gradle versioning 2025-05-03 11:21:05 +10:00
s809 5ca3563c66 Bump protocol version 2025-05-03 00:08:03 +05:00
s809 30256a2779 Send scaled generation bounds coordinates 2025-05-03 00:08:03 +05:00
Ran 4b4f10f5e6 Fix gradle versioning for core application 2025-05-02 12:44:25 +10:00
s809 ad995544f7 Use bytesReceived instead of decreasing multiplicatively 2025-04-20 23:59:34 +05:00
s809 d521e931f4 Change data send tick rate 4 -> 20 2025-04-20 18:26:07 +05:00
s809 dd30a8274a Add a config entry and refactor 2025-04-20 18:25:27 +05:00
s809 3ca5efadc9 Adaptive data transfer speed 2025-04-20 03:02:18 +05:00
Ran 09174c2d2a Improve LodDataBuilder.java
- Use bitwise modulo
- Don't compute certain things 256 times when they can be computed once.
- Removed expressions that are always false
- Improved comments
2025-04-11 11:24:16 +10:00
James Seibel e079b28e77 maybe break n-sized rendering but fix LOD loading getting stuck 2025-04-07 06:56:53 -05:00
James Seibel 136124a703 up version number 2.3.2 -> 2.3.3 2025-04-05 09:11:19 -05:00
James Seibel 3ed50e5134 remove dev from version number 2025-04-05 09:10:01 -05:00
James Seibel b5e3e6867c Improve DH world gen progress message 2025-04-02 07:25:14 -05:00
James Seibel 3e04342148 Add FIXME comments to Lod and Fade renderers 2025-04-02 07:24:38 -05:00
James Seibel 6699b568df Fix memory leaks due to un-closed thread pools and worlds
How did it take this long to realize the DhWorld objects were never being closed?
2025-03-30 17:30:57 -05:00
James Seibel 53bee4ad42 Remove unused code in LodRenderer 2025-03-30 16:55:01 -05:00
James Seibel 5d5e462221 Fix the sun/moon and stars not rendering 2025-03-30 16:49:58 -05:00
James Seibel d9b924cfed Fix beacon beams now going through some blocks 2025-03-30 15:23:19 -05:00
James Seibel 8bd70d593c Fix flashing on MC 1.21.5 in non-overworld dimensions 2025-03-30 14:36:51 -05:00
James Seibel 5597044604 don't log InterruptedException during threadPool shutdown 2025-03-29 20:11:31 -05:00
James Seibel 5d7c043d06 Fix fog for MC 1.16.5 2025-03-29 19:22:51 -05:00
James Seibel 4aac61b37f hide repo double close warnings in release 2025-03-29 15:39:45 -05:00
James Seibel 22460fa1f5 Fix duplicate world gen due to short memoization time
Reverts 276f2adf00
2025-03-29 15:30:28 -05:00
James Seibel 2d127c7d98 Fix an infinite loop in the lighting engine
Not sure how I didn't catch this until MC 1.21.5
2025-03-29 15:29:34 -05:00
James Seibel 91e17c420a Fix SSAO applying to sky 2025-03-29 10:31:48 -05:00
James Seibel 93f5a85cb5 Fix MC 1.21.5 rendering and bright glass on sky 2025-03-29 10:31:34 -05:00
James Seibel b275971486 re-add stencil to GL state
shouldn't be needed, but just in case
2025-03-29 09:52:41 -05:00
James Seibel 1234ff4d28 up version number 2.3.1 -> 2.3.2 2025-03-25 07:17:27 -05:00
286 changed files with 13208 additions and 9495 deletions
+2 -3
View File
@@ -4,14 +4,13 @@
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = crlf
indent_size = 4 indent_size = 4
indent_style = space indent_style = space
insert_final_newline = false 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
@@ -537,7 +536,7 @@ ij_groovy_wrap_chain_calls_after_dot = false
ij_groovy_wrap_long_lines = false ij_groovy_wrap_long_lines = false
[{*.har,*.json,*.png.mcmeta,mcmod.info,pack.mcmeta}] [{*.har,*.json,*.png.mcmeta,mcmod.info,pack.mcmeta}]
indent_size = 2 indent_size = 4
ij_json_array_wrapping = split_into_lines ij_json_array_wrapping = split_into_lines
ij_json_keep_blank_lines_in_code = 0 ij_json_keep_blank_lines_in_code = 0
ij_json_keep_indents_on_empty_lines = false ij_json_keep_indents_on_empty_lines = false
@@ -22,7 +22,8 @@ package com.seibel.distanthorizons.api.enums.config;
/** /**
* UNCOMPRESSED <br> * UNCOMPRESSED <br>
* LZ4 <br> * LZ4 <br>
* XZ <br><br> * Z_STD <br>
* LZMA2 <br><br>
* *
* Note: speed and compression ratios are examples * Note: speed and compression ratios are examples
* and should only be used for estimated comparisons. * and should only be used for estimated comparisons.
@@ -39,8 +40,8 @@ public enum EDhApiDataCompressionMode
/** /**
* Should only be used internally and for unit testing. <br><br> * Should only be used internally and for unit testing. <br><br>
* *
* Read Speed: 1.64 MS / DTO <br> * Read Speed: 6.09 MS / DTO <br>
* Write Speed: 12.44 MS / DTO <br> * Write Speed: 6.01 MS / DTO <br>
* Compression ratio: 1.0 <br> * Compression ratio: 1.0 <br>
*/ */
@DisallowSelectingViaConfigGui @DisallowSelectingViaConfigGui
@@ -49,28 +50,38 @@ public enum EDhApiDataCompressionMode
/** /**
* Extremely fast (often faster than uncompressed), but generally poor compression. <br><br> * Extremely fast (often faster than uncompressed), but generally poor compression. <br><br>
* *
* Read Speed: 1.85 MS / DTO <br> * Read Speed: 3.25 MS / DTO <br>
* Write Speed: 9.46 MS / DTO <br> * Write Speed: 5.99 MS / DTO <br>
* Compression ratio: 0.3638 <br> * Compression ratio: 0.4513 <br>
*/ */
LZ4(1), LZ4(1),
/* /**
* Decent speed and good compression. <br><br> * Decent speed and good compression. <br><br>
* *
* Read Speed: 11.78 MS / DTO <br> * Read Speed: 9.31 MS / DTO <br>
* Write Speed: 16.76 MS / DTO <br> * Write Speed: 15.13 MS / DTO <br>
* Compression ratio: 0.2199 <br> * Compression ratio: 0.2606 <br>
*/ */
//@Deprecated Z_STD(4),
//Z_STD(2),
/**
* Similar to {@link EDhApiDataCompressionMode#Z_STD}
* except slower.
* <br>
* This option is only provided for legacy support when processing old databases.
*/
@Deprecated
@DisallowSelectingViaConfigGui
Z_STD_STREAM(2),
/** /**
* Extremely slow, but very good compression. <br><br> * Extremely slow, but very good compression. <br><br>
* *
* Read Speed: 12.25 MS / DTO <br> * Read Speed: 13.29 MS / DTO <br>
* Write Speed: 490.07 MS / DTO <br> * Write Speed: 70.95 MS / DTO <br>
* Compression ratio: 0.1242 <br> * Compression ratio: 0.2068 <br>
*/ */
LZMA2(3); LZMA2(3);
@@ -44,6 +44,10 @@ 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 */
@Deprecated // TODO remove before release
NONE(false, false),
/** /**
* May end up storing buffers in System memory. <br> * May end up storing buffers in System memory. <br>
* Fast rending if in GPU memory, slow if in system memory, <br> * Fast rending if in GPU memory, slow if in system memory, <br>
@@ -17,34 +17,37 @@
* 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.api.enums.config;
import org.apache.logging.log4j.Level;
/** /**
* MULTI_THREADED, <br> * ALL
* SINGLE_THREADED, <br> * DEBUG
* SERVER_THREAD, <br> * INFO
* WARN
* ERROR
* DISABLED
* *
* @author James Seibel * @since API 5.0.0
* @version 7-25-2022 * @version 2024-4-6
*/ */
public enum EWorldGenThreadMode public enum EDhApiLoggerLevel
{ {
/** // ordered from most to least broad
* This world generator can be run on an unlimited number ALL(Level.ALL),
* of concurrent threads. DEBUG(Level.DEBUG),
*/ INFO(Level.INFO),
MULTI_THREADED, WARN(Level.WARN),
ERROR(Level.ERROR),
DISABLED(Level.OFF),
;
public final Level level;
EDhApiLoggerLevel(Level level)
{ this.level = level; }
/**
* This world generator can only be run on one thread at
* a time, however that thread can run concurrently
* to Minecraft's server thread.
*/
SINGLE_THREADED,
/**
* This world generator can only be run on Minecraft's
* server thread.
*/
SERVER_THREAD,
} }
@@ -1,54 +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.api.enums.config;
import org.apache.logging.log4j.Level;
/**
* @since API 2.0.0
* @version 2024-4-6
*/
public enum EDhApiLoggerMode
{
DISABLED(Level.OFF, Level.OFF),
LOG_ALL_TO_FILE(Level.ALL, Level.OFF),
LOG_ERROR_TO_CHAT(Level.ALL, Level.ERROR),
LOG_WARNING_TO_CHAT(Level.ALL, Level.WARN),
LOG_INFO_TO_CHAT(Level.ALL, Level.INFO),
LOG_DEBUG_TO_CHAT(Level.ALL, Level.DEBUG),
LOG_ALL_TO_CHAT(Level.ALL, Level.ALL),
LOG_ERROR_TO_CHAT_AND_FILE(Level.ERROR, Level.ERROR),
LOG_WARNING_TO_CHAT_AND_FILE(Level.WARN, Level.WARN),
LOG_INFO_TO_CHAT_AND_FILE(Level.INFO, Level.INFO),
LOG_DEBUG_TO_CHAT_AND_FILE(Level.DEBUG, Level.DEBUG),
LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE(Level.INFO, Level.WARN),
LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE(Level.INFO, Level.ERROR),
;
public final Level levelForFile;
public final Level levelForChat;
EDhApiLoggerMode(Level levelForFile, Level levelForChat)
{
this.levelForFile = levelForFile;
this.levelForChat = levelForChat;
}
}
@@ -64,10 +64,7 @@ public enum EDhApiMaxHorizontalResolution
/** How wide each LOD DataPoint is */ /** How wide each LOD DataPoint is */
public final int dataPointWidth; public final int dataPointWidth;
/** /** This is the same as detailLevel in LodQuadTreeNode */
* This is the same as detailLevel in LodQuadTreeNode,
* lowest is 0 highest is 9
*/
public final byte detailLevel; public final byte detailLevel;
/* Start/End X/Z give the block positions /* Start/End X/Z give the block positions
@@ -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();
@@ -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;
@@ -0,0 +1,48 @@
/*
* 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.api.methods.events.abstractEvents;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEvent;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiEventParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiTextureCreatedParam;
/**
* Called after Distant Horizons (re)creates
* the color and depth textures it renders to. <br>
*
* @author James Seibel
* @version 2025-6-9
* @since API 4.1.0
*/
public abstract class DhApiAfterColorDepthTextureCreatedEvent implements IDhApiEvent<DhApiTextureCreatedParam>
{
/** Fired before Distant Horizons creates. */
public abstract void onResize(DhApiEventParam<DhApiTextureCreatedParam> event);
//=========================//
// internal DH API methods //
//=========================//
@Override
public final void fireEvent(DhApiEventParam<DhApiTextureCreatedParam> event) { this.onResize(event); }
}
@@ -0,0 +1,49 @@
/*
* 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.api.methods.events.abstractEvents;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEvent;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiEventParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiTextureCreatedParam;
/**
* Called before Distant Horizons (re)creates
* the color and depth textures it renders to. <br>
*
* @author James Seibel
* @version 2025-6-9
* @since API 4.1.0
*/
public abstract class DhApiBeforeColorDepthTextureCreatedEvent implements IDhApiEvent<DhApiTextureCreatedParam>
{
/** Fired before Distant Horizons creates. */
public abstract void onResize(DhApiEventParam<DhApiTextureCreatedParam> event);
//=========================//
// internal DH API methods //
//=========================//
@Override
public final void fireEvent(DhApiEventParam<DhApiTextureCreatedParam> event) { this.onResize(event); }
}
@@ -0,0 +1,197 @@
/*
* 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.api.methods.events.abstractEvents;
import com.seibel.distanthorizons.api.interfaces.block.IDhApiBiomeWrapper;
import com.seibel.distanthorizons.api.interfaces.block.IDhApiBlockStateWrapper;
import com.seibel.distanthorizons.api.interfaces.factories.IDhApiWrapperFactory;
import com.seibel.distanthorizons.api.interfaces.world.IDhApiLevelWrapper;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEvent;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiEventParam;
/**
* Used to override which blocks may be stored in a given chunk.
* This can be used for X-ray prevention or to replace problematic mod blocks
* that don't fit into the {@link IDhApiBlockStateWrapper} format DH requires
* (IE modded blocks that use NBT data
* to determine their model and/or texture). <br/><br/>
*
* This event is fired for each block or biome change when DH is processing a chunk.
* A change happens when DH finds a different block or biome while walking through a chunk.
* For example with the block sequence:<br/>
* <code> stone -> stone -> air -> stone </code> <br/>
* This event would be fired for the first, third, and forth blocks in the sequence
* (IE the first stone, first air, and last stone respectively). <br/> <br/>
*
* The order DH will process blocks is undefined so a specific ordering shouldn't be relied upon for your logic to function. <br/> <br/>
*
* <b>Threading note:</b> this event may be called concurrently across multiple threads. <br/>
* <b>Performance note:</b> this event will be called very frequently, avoid expensive lookups or other slow operations if possible. <br/>
*
* @see DhApiLevelLoadEvent
* @see IDhApiWrapperFactory
*
* @author James Seibel
* @version 2025-09-29
* @since API 4.1.0
*/
public abstract class DhApiChunkProcessingEvent implements IDhApiEvent<DhApiChunkProcessingEvent.EventParam>
{
public abstract void blockOrBiomeChangedDuringChunkProcessing(DhApiEventParam<EventParam> event);
//=========================//
// internal DH API methods //
//=========================//
@Override
public final void fireEvent(DhApiEventParam<EventParam> event) { this.blockOrBiomeChangedDuringChunkProcessing(event); }
//==================//
// parameter object //
//==================//
public static class EventParam implements IDhApiEventParam
{
/** The saved level. */
public final IDhApiLevelWrapper levelWrapper;
/** the processed chunk's X pos in chunk coordinates */
public final int chunkX;
/** the processed chunk's Z pos in chunk coordinates */
public final int chunkZ;
public int relativeBlockPosX;
public int blockPosY;
public int relativeBlockPosZ;
public IDhApiBlockStateWrapper currentBlock;
public IDhApiBiomeWrapper currentBiome;
private IDhApiBlockStateWrapper newBlock;
private IDhApiBiomeWrapper newBiome;
//=============//
// constructor //
//=============//
public EventParam(IDhApiLevelWrapper newLevelWrapper, int chunkX, int chunkZ)
{
this.levelWrapper = newLevelWrapper;
this.chunkX = chunkX;
this.chunkZ = chunkZ;
}
/**
* Internal method use by Distant Horizons
* to set up this event.
*/
public void updateForPosition(
int relativeBlockPosX, int blockPosY, int relativeBlockPosZ,
IDhApiBlockStateWrapper currentBlock,
IDhApiBiomeWrapper currentBiome)
{
this.relativeBlockPosX = relativeBlockPosX;
this.blockPosY = blockPosY;
this.relativeBlockPosZ = relativeBlockPosZ;
this.newBlock = null;
this.newBiome = null;
this.currentBlock = currentBlock;
this.currentBiome = currentBiome;
}
//=================//
// getters/setters //
//=================//
/**
* Sets the {@link IDhApiBlockStateWrapper} that should be used at this event's current position in the chunk.
* If you don't want to modify the block at this event's current position,
* either don't call this method or pass in null. <br>
* Passing in null will remove the override, meaning the original block will be used. <br><br>
*
* A {@link IDhApiWrapperFactory} should be used to get the {@link IDhApiBlockStateWrapper} that's returned.
* Attempting to create your own {@link IDhApiBlockStateWrapper} will cause a {@link ClassCastException}. <br/> <br/>
*
* If multiple API users are listening to this event the override may already have been set.
* With that in mind it is recommended to check if an override has already been set via
* {@link EventParam#getBlockOverride()} to handle that occurrence. <br>
* Note that the order of API events firing is undefined so a specific order shouldn't be relied upon. <br><br>
*
* @see IDhApiWrapperFactory
*/
public void setBlockOverride(IDhApiBlockStateWrapper block) { this.newBlock = block; }
/**
* Returns the currently overriding block for this position.
* This will be null if no other API event has set the override.
*/
public IDhApiBlockStateWrapper getBlockOverride() { return this.newBlock; }
/**
* Sets the {@link IDhApiBiomeWrapper} that should be used at this event's current position in the chunk.
* If you don't want to modify the biome at this event's current position,
* either don't call this method or pass in null. <br>
* Passing in null will remove the override, meaning the original biome will be used. <br><br>
*
* A {@link IDhApiWrapperFactory} should be used to get the {@link IDhApiBiomeWrapper} that's returned.
* Attempting to create your own {@link IDhApiBiomeWrapper} will cause a {@link ClassCastException}. <br/> <br/>
*
* If multiple API users are listening to this event the override may already have been set.
* With that in mind it is recommended to check if an override has already been set via
* {@link EventParam#getBiomeOverride()} ()} to handle that occurrence. <br>
* Note that the order of API events firing is undefined so a specific order shouldn't be relied upon. <br><br>
*
* @see IDhApiWrapperFactory
*/
public void setBiomeOverride(IDhApiBiomeWrapper biome) { this.newBiome = biome; }
/**
* Returns the currently overriding biome for this position.
* This will be null if no other API event has set the override.
*/
public IDhApiBiomeWrapper getBiomeOverride() { return this.newBiome; }
/**
* Returns the same instance of this event.
* Copying this event isn't recommended due to
* how often it would be called per chunk, creating
* unnecessary garbage collector pressure.
*/
@Override
public EventParam copy() { return this; }
@Override
public boolean getCopyBeforeFire() { return false; }
}
}
@@ -22,15 +22,18 @@ package com.seibel.distanthorizons.api.methods.events.abstractEvents;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEvent; import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEvent;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam; import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiEventParam; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiEventParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiTextureCreatedParam;
/** /**
* Called whenever Distant Horizons (re)creates * Called before Distant Horizons (re)creates
* the color and depth textures it renders to. <br> * the color and depth textures it renders to. <br>
* *
* @author James Seibel * @author James Seibel
* @version 2024-3-2 * @version 2024-3-2
* @since API 2.0.0 * @since API 2.0.0
* @deprecated Replaced by {@link DhApiBeforeColorDepthTextureCreatedEvent} since this event's name isn't obvious when it fires.
*/ */
@Deprecated // internal notes: this method must be kept around due to Iris using it and we don't want to break old Iris support
public abstract class DhApiColorDepthTextureCreatedEvent implements IDhApiEvent<DhApiColorDepthTextureCreatedEvent.EventParam> public abstract class DhApiColorDepthTextureCreatedEvent implements IDhApiEvent<DhApiColorDepthTextureCreatedEvent.EventParam>
{ {
/** Fired before Distant Horizons creates. */ /** Fired before Distant Horizons creates. */
@@ -73,6 +76,15 @@ public abstract class DhApiColorDepthTextureCreatedEvent implements IDhApiEvent<
this.newHeight = newHeight; this.newHeight = newHeight;
} }
public EventParam(DhApiTextureCreatedParam textureCreatedParam)
{
this.previousWidth = textureCreatedParam.previousWidth;
this.previousHeight = textureCreatedParam.previousHeight;
this.newWidth = textureCreatedParam.newWidth;
this.newHeight = textureCreatedParam.newHeight;
}
@Override @Override
@@ -9,5 +9,23 @@ import com.seibel.distanthorizons.api.interfaces.util.IDhApiCopyable;
*/ */
public interface IDhApiEventParam extends IDhApiCopyable public interface IDhApiEventParam extends IDhApiCopyable
{ {
/**
* Internal DH use. <br> <br>
*
* Most API events will clone their parameters
* before firing to prevent API implementors
* from modifying the properties causing
* any subsequent listeners to see the wrong data. <br><br>
*
* However, this can be overridden for API events that shouldn't
* be cloned before firing.
* Generally that would be done for performance reasons
* where an event may fire hundreds or thousands of times
* in quick succession or where the event parameter is needed
* internally by DH after firing.
*
* @since API 4.1.0
*/
default boolean getCopyBeforeFire() { return true; }
} }
@@ -67,7 +67,6 @@ public class DhApiRenderParam implements IDhApiEventParam
// constructors // // constructors //
//==============// //==============//
public DhApiRenderParam(DhApiRenderParam parent) public DhApiRenderParam(DhApiRenderParam parent)
{ {
this( this(
@@ -112,9 +111,8 @@ public class DhApiRenderParam implements IDhApiEventParam
//================// //================//
@Override @Override
public DhApiRenderParam copy() public DhApiRenderParam copy() { return new DhApiRenderParam(this); }
{
return new DhApiRenderParam(this);
}
} }
@@ -0,0 +1,68 @@
/*
* 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.api.methods.events.sharedParameterObjects;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam;
import com.seibel.distanthorizons.api.objects.math.DhApiMat4f;
/**
* Contains information relevant to when Distant Horizons (re)creates
* depth/color textures for rendering.
*
* @author James Seibel
* @version 2025-6-9
* @since API 4.1.0
*/
public class DhApiTextureCreatedParam implements IDhApiEventParam
{
/** Measured in pixels */
public final int previousWidth;
/** Measured in pixels */
public final int previousHeight;
/** Measured in pixels */
public final int newWidth;
/** Measured in pixels */
public final int newHeight;
public DhApiTextureCreatedParam(
int previousWidth, int previousHeight,
int newWidth, int newHeight)
{
this.previousWidth = previousWidth;
this.previousHeight = previousHeight;
this.newWidth = newWidth;
this.newHeight = newHeight;
}
@Override
public DhApiTextureCreatedParam copy()
{
return new DhApiTextureCreatedParam(
this.previousWidth, this.previousHeight,
this.newWidth, this.newHeight
);
}
}
@@ -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 +
"]";
}
} }
@@ -143,19 +143,23 @@ public class ApiEventInjector extends DependencyInjector<IDhApiEvent> implements
// attempt to clone the event input if possible // attempt to clone the event input if possible
// this is done to reduce the likely hood that one event listener // this is done to reduce the likelihood that one event listener
// will make change the event parameter for other listeners // will change the event parameter for other listeners
T input = eventInput; T input = eventInput;
if (eventInput instanceof IDhApiEventParam) if (eventInput instanceof IDhApiEventParam)
{
IDhApiEventParam dhApiEventParam = (IDhApiEventParam) eventInput;
if (dhApiEventParam.getCopyBeforeFire())
{ {
try try
{ {
//noinspection unchecked //noinspection unchecked
input = (T) ((IDhApiEventParam) eventInput).copy(); input = (T) dhApiEventParam.copy();
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.error("Unable to clone event parameter ["+eventInput.getClass().getSimpleName()+"], error: ["+e.getMessage()+"].", e); LOGGER.error("Unable to clone event parameter [" + eventInput.getClass().getSimpleName() + "], error: [" + e.getMessage() + "].", e);
}
} }
} }
@@ -43,11 +43,9 @@ public class DependencyInjector<BindableType extends IBindable> implements IDepe
protected final boolean allowDuplicateBindings; protected final boolean allowDuplicateBindings;
public DependencyInjector(Class<BindableType> newBindableInterface) //==============//
{ // constructors //
this.bindableInterface = newBindableInterface; //==============//
this.allowDuplicateBindings = false;
}
public DependencyInjector(Class<BindableType> newBindableInterface, boolean newAllowDuplicateBindings) public DependencyInjector(Class<BindableType> newBindableInterface, boolean newAllowDuplicateBindings)
{ {
@@ -57,12 +55,16 @@ public class DependencyInjector<BindableType extends IBindable> implements IDepe
//=========//
// binding //
//=========//
@Override @Override
public void bind(Class<? extends BindableType> dependencyInterface, BindableType dependencyImplementation) throws IllegalStateException, IllegalArgumentException public void bind(Class<? extends BindableType> dependencyInterface, BindableType dependencyImplementation) throws IllegalStateException, IllegalArgumentException
{ {
// duplicate check if requested // duplicate check if requested
if (this.dependencies.containsKey(dependencyInterface) && !this.allowDuplicateBindings) if (this.dependencies.containsKey(dependencyInterface)
&& !this.allowDuplicateBindings)
{ {
throw new IllegalStateException("The dependency [" + dependencyInterface.getSimpleName() + "] has already been bound."); throw new IllegalStateException("The dependency [" + dependencyInterface.getSimpleName() + "] has already been bound.");
} }
@@ -130,6 +132,54 @@ public class DependencyInjector<BindableType extends IBindable> implements IDepe
public boolean checkIfClassExtends(Class<?> classToTest, Class<?> extensionToLookFor) { return extensionToLookFor.isAssignableFrom(classToTest); } public boolean checkIfClassExtends(Class<?> classToTest, Class<?> extensionToLookFor) { return extensionToLookFor.isAssignableFrom(classToTest); }
//===========//
// unbinding //
//===========//
// TODO having a bindOrReplace method would probably be better since it wouldn't have the possiblity of having nothing bound
public void unbind(Class<? extends BindableType> dependencyInterface, BindableType dependencyImplementation) throws IllegalStateException, IllegalArgumentException
{
// check if this object is bound
if (!this.dependencies.containsKey(dependencyInterface))
{
return;
}
// make sure the given dependency implements the necessary interfaces
boolean implementsInterface = this.checkIfClassImplements(dependencyImplementation.getClass(), dependencyInterface)
|| this.checkIfClassExtends(dependencyImplementation.getClass(), dependencyInterface);
boolean implementsBindable = this.checkIfClassImplements(dependencyImplementation.getClass(), this.bindableInterface);
// display any errors
if (!implementsInterface)
{
throw new IllegalArgumentException("The dependency [" + dependencyImplementation.getClass().getSimpleName() + "] doesn't implement or extend: [" + dependencyInterface.getSimpleName() + "].");
}
if (!implementsBindable)
{
throw new IllegalArgumentException("The dependency [" + dependencyImplementation.getClass().getSimpleName() + "] doesn't implement the interface: [" + IBindable.class.getSimpleName() + "].");
}
// make sure the hashSet has an array to hold the dependency
if (!this.dependencies.containsKey(dependencyInterface))
{
this.dependencies.put(dependencyInterface, new ArrayList<BindableType>());
}
// remove the dependency if present
this.dependencies.get(dependencyInterface).remove(dependencyImplementation);
this.dependencies.remove(dependencyInterface);
}
//=========//
// getters //
//=========//
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public <T extends BindableType> T get(Class<T> interfaceClass) throws ClassCastException public <T extends BindableType> T get(Class<T> interfaceClass) throws ClassCastException
@@ -31,19 +31,19 @@ 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 = 10; 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.1-b"; public static final String VERSION = "2.3.7-b-dev";
/** Returns true if the current build is an unstable developer build, false otherwise. */ /** Returns true if the current build is an unstable developer build, false otherwise. */
public static final boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev"); public static final boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev");
/** 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 = 4; 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 = 0;
/** This version should be updated whenever non-breaking fixes are added to the DH API */ /** This version should be updated whenever non-breaking fixes are added to the DH API */
@@ -1,83 +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.coreapi.interfaces.config;
import java.util.function.Consumer;
/**
* Use for making the config variables
*
* @author coolGi
* @version 2022-5-26
*/
public interface IConfigEntry<T>
{
/** Gets the default value of the option */
T getDefaultValue();
void setApiValue(T newApiValue);
T getApiValue();
/** Returns true if this config can be set via the API. */
boolean getAllowApiOverride();
void set(T newValue);
T get();
T getTrueValue();
/** Sets the value without saving */
void setWithoutSaving(T newValue);
/** Gets the min value */
T getMin();
/** Sets the min value */
void setMin(T newMin);
/** Gets the max value */
T getMax();
/** Sets the max value */
void setMax(T newMax);
/** Sets the min and max in 1 setter */
void setMinMax(T newMin, T newMax);
/** Gets the comment */
String getComment();
/** Sets the comment */
void setComment(String newComment);
/**
* Checks if the option is valid
*
* 0 == valid
* 2 == invalid
* 1 == number too high
* -1 == number too low
*/
byte isValid(); // TODO replace with an enum
/** Checks if a value is valid */
byte isValid(T value);
/** Is the value of this equal to another */
boolean equals(IConfigEntry<?> obj);
void addValueChangeListener(Consumer<T> onValueChangeFunc);
}
+6
View File
@@ -61,3 +61,9 @@ shadowJar {
// relocate "it.unimi.dsi.fastutil", "${librariesLocation}.unimi.dsi.fastutil" // relocate "it.unimi.dsi.fastutil", "${librariesLocation}.unimi.dsi.fastutil"
mergeServiceFiles() mergeServiceFiles()
} }
test {
// this is necessary specifically for the Compression tests since those
// need more than the default 512 MB of RAM
jvmArgs '-Xmx4096m'
}
@@ -19,7 +19,9 @@
package com.seibel.distanthorizons.core; package com.seibel.distanthorizons.core;
import com.github.luben.zstd.ZstdOutputStream;
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.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;
@@ -32,7 +34,7 @@ import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.core.render.DhApiRenderProxy; import com.seibel.distanthorizons.core.render.DhApiRenderProxy;
import net.jpountz.lz4.LZ4FrameOutputStream; import net.jpountz.lz4.LZ4FrameOutputStream;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.sqlite.SQLiteJDBCLoader; import org.sqlite.SQLiteJDBCLoader;
import org.sqlite.util.OSInfo; import org.sqlite.util.OSInfo;
import org.tukaani.xz.XZOutputStream; import org.tukaani.xz.XZOutputStream;
@@ -43,8 +45,7 @@ import java.io.File;
/** Handles first time Core setup. */ /** Handles first time Core setup. */
public class Initializer public class Initializer
{ {
private static final Logger LOGGER = LogManager.getLogger(ModInfo.NAME + "-" + Initializer.class.getSimpleName()); 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()
@@ -54,8 +55,9 @@ public class Initializer
{ {
// if any library isn't present in the jar its class // if any library isn't present in the jar its class
// will throw an error (not an exception) // will throw an error (not an exception)
Class<?> fastCompressor = LZ4FrameOutputStream.class; Class<?> lz4Compressor = LZ4FrameOutputStream.class;
Class<?> smallCompressor = XZOutputStream.class; Class<?> zstdCompressor = ZstdOutputStream.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;
Class<?> oldFastUtil = it.unimi.dsi.fastutil.longs.LongArrayList.class; // available in 8.2.1 Class<?> oldFastUtil = it.unimi.dsi.fastutil.longs.LongArrayList.class; // available in 8.2.1
@@ -63,30 +65,6 @@ public class Initializer
Class<?> sqliteJava = org.sqlite.SQLiteConnection.class; Class<?> sqliteJava = org.sqlite.SQLiteConnection.class;
Class<?> sqliteNative = org.sqlite.core.NativeDB.class; Class<?> sqliteNative = org.sqlite.core.NativeDB.class;
//// maybe these lines are needed to shade SQLite, James isn't sure.
//// Although they never seemed to fail, which is a bit odd.
//try
//{
// // needed by Forge to load the Java database connection
// Class.forName("org.sqlite.JDBC");
// LOGGER.info("loaded normal SQLITE");
//}
//catch (ClassNotFoundException e)
//{
// LOGGER.warn("normal: " + e.getMessage(), e);
//}
//
//try
//{
// // needed by Forge to load the Java database connection
// Class.forName("DistantHorizons.libraries.sqlite.JDBC");
// LOGGER.info("loaded shaded SQLITE");
//}
//catch (ClassNotFoundException e)
//{
// LOGGER.warn("shaded: " + e.getMessage(), e);
//}
boolean sqliteLoaded = SQLiteJDBCLoader.initialize(); boolean sqliteLoaded = SQLiteJDBCLoader.initialize();
if (!sqliteLoaded) if (!sqliteLoaded)
{ {
@@ -21,7 +21,7 @@ package com.seibel.distanthorizons.core.api.external.methods.config.client;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiAmbientOcclusionConfig; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiAmbientOcclusionConfig;
import com.seibel.distanthorizons.api.objects.config.DhApiConfigValue; import com.seibel.distanthorizons.core.config.api.DhApiConfigValue;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
public class DhApiAmbientOcclusionConfig implements IDhApiAmbientOcclusionConfig public class DhApiAmbientOcclusionConfig implements IDhApiAmbientOcclusionConfig
@@ -21,7 +21,7 @@ package com.seibel.distanthorizons.core.api.external.methods.config.client;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiDebuggingConfig; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiDebuggingConfig;
import com.seibel.distanthorizons.api.objects.config.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.api.enums.rendering.EDhApiDebugRendering; import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering;
@@ -22,7 +22,7 @@ package com.seibel.distanthorizons.core.api.external.methods.config.client;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiFogFalloff; import com.seibel.distanthorizons.api.enums.rendering.EDhApiFogFalloff;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiFarFogConfig; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiFarFogConfig;
import com.seibel.distanthorizons.api.objects.config.DhApiConfigValue; import com.seibel.distanthorizons.core.config.api.DhApiConfigValue;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
public class DhApiFarFogConfig implements IDhApiFarFogConfig public class DhApiFarFogConfig implements IDhApiFarFogConfig
@@ -25,9 +25,9 @@ import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiFarFogConfig; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiFarFogConfig;
import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiFogConfig; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiFogConfig;
import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiHeightFogConfig; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiHeightFogConfig;
import com.seibel.distanthorizons.api.objects.config.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.coreapi.util.converters.ApiFogDrawModeConverter; import com.seibel.distanthorizons.core.config.api.converters.ApiFogDrawModeConverter;
public class DhApiFogConfig implements IDhApiFogConfig public class DhApiFogConfig implements IDhApiFogConfig
{ {
@@ -21,7 +21,7 @@ package com.seibel.distanthorizons.core.api.external.methods.config.client;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiGenericRenderingConfig; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiGenericRenderingConfig;
import com.seibel.distanthorizons.api.objects.config.DhApiConfigValue; import com.seibel.distanthorizons.core.config.api.DhApiConfigValue;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
public class DhApiGenericRenderingConfig implements IDhApiGenericRenderingConfig public class DhApiGenericRenderingConfig implements IDhApiGenericRenderingConfig
@@ -23,9 +23,10 @@ import com.seibel.distanthorizons.api.enums.config.*;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiTransparency; import com.seibel.distanthorizons.api.enums.rendering.EDhApiTransparency;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
import com.seibel.distanthorizons.api.interfaces.config.client.*; import com.seibel.distanthorizons.api.interfaces.config.client.*;
import com.seibel.distanthorizons.api.objects.config.DhApiConfigValue; import com.seibel.distanthorizons.core.config.api.DhApiConfigValue;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.api.converters.RenderModeEnabledConverter;
public class DhApiGraphicsConfig implements IDhApiGraphicsConfig public class DhApiGraphicsConfig implements IDhApiGraphicsConfig
{ {
@@ -60,7 +61,7 @@ public class DhApiGraphicsConfig implements IDhApiGraphicsConfig
@Override @Override
public IDhApiConfigValue<Boolean> renderingEnabled() public IDhApiConfigValue<Boolean> renderingEnabled()
{ return new DhApiConfigValue<Boolean, Boolean>(Config.Client.quickEnableRendering); } { return new DhApiConfigValue<EDhApiRendererMode, Boolean>(Config.Client.Advanced.Debugging.rendererMode, new RenderModeEnabledConverter()); }
@Override @Override
public IDhApiConfigValue<EDhApiRendererMode> renderingMode() public IDhApiConfigValue<EDhApiRendererMode> renderingMode()
@@ -96,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); }
@@ -24,7 +24,7 @@ import com.seibel.distanthorizons.api.enums.rendering.EDhApiHeightFogMixMode;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiHeightFogDirection; import com.seibel.distanthorizons.api.enums.rendering.EDhApiHeightFogDirection;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiHeightFogConfig; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiHeightFogConfig;
import com.seibel.distanthorizons.api.objects.config.DhApiConfigValue; import com.seibel.distanthorizons.core.config.api.DhApiConfigValue;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
public class DhApiHeightFogConfig implements IDhApiHeightFogConfig public class DhApiHeightFogConfig implements IDhApiHeightFogConfig
@@ -21,7 +21,7 @@ package com.seibel.distanthorizons.core.api.external.methods.config.client;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiMultiThreadingConfig; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiMultiThreadingConfig;
import com.seibel.distanthorizons.api.objects.config.DhApiConfigValue; import com.seibel.distanthorizons.core.config.api.DhApiConfigValue;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
public class DhApiMultiThreadingConfig implements IDhApiMultiThreadingConfig public class DhApiMultiThreadingConfig implements IDhApiMultiThreadingConfig
@@ -21,7 +21,7 @@ package com.seibel.distanthorizons.core.api.external.methods.config.client;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiMultiplayerConfig; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiMultiplayerConfig;
import com.seibel.distanthorizons.api.objects.config.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.api.enums.config.EDhApiServerFolderNameMode; import com.seibel.distanthorizons.api.enums.config.EDhApiServerFolderNameMode;
@@ -21,7 +21,7 @@ package com.seibel.distanthorizons.core.api.external.methods.config.client;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiNoiseTextureConfig; import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiNoiseTextureConfig;
import com.seibel.distanthorizons.api.objects.config.DhApiConfigValue; import com.seibel.distanthorizons.core.config.api.DhApiConfigValue;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
public class DhApiNoiseTextureConfig implements IDhApiNoiseTextureConfig public class DhApiNoiseTextureConfig implements IDhApiNoiseTextureConfig
@@ -21,7 +21,7 @@ package com.seibel.distanthorizons.core.api.external.methods.config.common;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
import com.seibel.distanthorizons.api.interfaces.config.both.IDhApiWorldGenerationConfig; import com.seibel.distanthorizons.api.interfaces.config.both.IDhApiWorldGenerationConfig;
import com.seibel.distanthorizons.api.objects.config.DhApiConfigValue; import com.seibel.distanthorizons.core.config.api.DhApiConfigValue;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorMode; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorMode;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
@@ -2,10 +2,10 @@ package com.seibel.distanthorizons.core.api.external.methods.data;
import com.seibel.distanthorizons.api.interfaces.data.IDhApiTerrainDataCache; import com.seibel.distanthorizons.api.interfaces.data.IDhApiTerrainDataCache;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongSet; import it.unimi.dsi.fastutil.longs.LongSet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.lang.ref.SoftReference; import java.lang.ref.SoftReference;
@@ -15,7 +15,7 @@ public class DhApiTerrainDataCache implements IDhApiTerrainDataCache
private final Object modificationLock = new Object(); private final Object modificationLock = new Object();
private Long2ReferenceOpenHashMap<SoftReference<FullDataSourceV2>> posToFullDataRef = new Long2ReferenceOpenHashMap<>(); private Long2ReferenceOpenHashMap<SoftReference<FullDataSourceV2>> posToFullDataRef = new Long2ReferenceOpenHashMap<>();
private static final Logger LOGGER = LogManager.getLogger(DhApiTerrainDataCache.class.getSimpleName()); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
@@ -31,6 +31,7 @@ import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap;
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.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhLodPos; import com.seibel.distanthorizons.core.pos.DhLodPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
@@ -48,8 +49,7 @@ import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import com.seibel.distanthorizons.core.util.math.Vec3d; import com.seibel.distanthorizons.core.util.math.Vec3d;
import com.seibel.distanthorizons.core.util.math.Vec3i; import com.seibel.distanthorizons.core.util.math.Vec3i;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.LogManager; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.awt.*; import java.awt.*;
@@ -64,7 +64,7 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
{ {
public static DhApiTerrainDataRepo INSTANCE = new DhApiTerrainDataRepo(); public static DhApiTerrainDataRepo INSTANCE = new DhApiTerrainDataRepo();
private static final Logger LOGGER = LogManager.getLogger(DhApiTerrainDataRepo.class.getSimpleName()); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
// debugging values // debugging values
private static volatile boolean debugThreadRunning = false; private static volatile boolean debugThreadRunning = false;
@@ -258,7 +258,7 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
//===============================// //===============================//
FullDataPointIdMap mapping = dataSource.mapping; FullDataPointIdMap mapping = dataSource.mapping;
LongArrayList dataColumn = dataSource.get(relativePos.x, relativePos.z); LongArrayList dataColumn = dataSource.getColumnAtRelPos(relativePos.x, relativePos.z);
if (dataColumn != null) if (dataColumn != null)
{ {
int dataColumnIndexCount = dataColumn.size(); int dataColumnIndexCount = dataColumn.size();
@@ -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)
{ {
@@ -503,7 +508,7 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
// this will throw a cast exception if the chunk object array isn't correct // this will throw a cast exception if the chunk object array isn't correct
IChunkWrapper chunk = SingletonInjector.INSTANCE.get(IWrapperFactory.class).createChunkWrapper(chunkObjectArray); IChunkWrapper chunk = SingletonInjector.INSTANCE.get(IWrapperFactory.class).createChunkWrapper(chunkObjectArray);
SharedApi.INSTANCE.applyChunkUpdate(chunk, dhLevel.getLevelWrapper(), true); SharedApi.INSTANCE.applyChunkUpdate(chunk, dhLevel.getLevelWrapper(), true, true);
return DhApiResult.createSuccess(); return DhApiResult.createSuccess();
@@ -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
) )
); );
@@ -23,17 +23,20 @@ import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.api.enums.config.EDhApiMcRenderingFadeMode; import com.seibel.distanthorizons.api.enums.config.EDhApiMcRenderingFadeMode;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.*; import com.seibel.distanthorizons.api.methods.events.abstractEvents.*;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; import com.seibel.distanthorizons.core.api.external.methods.data.DhApiTerrainDataRepo;
import com.seibel.distanthorizons.core.api.internal.rendering.DhRenderState;
import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure; import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure;
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.FadeRenderer; import com.seibel.distanthorizons.core.render.renderer.VanillaFadeRenderer;
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;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.network.session.NetworkSession; import com.seibel.distanthorizons.core.network.session.NetworkSession;
@@ -42,13 +45,8 @@ import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel; import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.logging.ConfigBasedSpamLogger;
import com.seibel.distanthorizons.core.logging.SpamReducedLogger;
import com.seibel.distanthorizons.core.util.math.Mat4f;
import com.seibel.distanthorizons.core.render.glObject.GLProxy; import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.render.renderer.TestRenderer; import com.seibel.distanthorizons.core.render.renderer.TestRenderer;
import com.seibel.distanthorizons.core.util.RenderUtil;
import com.seibel.distanthorizons.core.world.AbstractDhWorld; import com.seibel.distanthorizons.core.world.AbstractDhWorld;
import com.seibel.distanthorizons.core.world.DhClientServerWorld; import com.seibel.distanthorizons.core.world.DhClientServerWorld;
import com.seibel.distanthorizons.core.world.DhClientWorld; import com.seibel.distanthorizons.core.world.DhClientWorld;
@@ -57,15 +55,14 @@ 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;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import org.apache.logging.log4j.LogManager; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import java.io.File; import java.io.File;
import java.util.*; import java.util.*;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/** /**
* This holds the methods that should be called * This holds the methods that should be called
@@ -74,7 +71,7 @@ import java.util.concurrent.TimeUnit;
*/ */
public class ClientApi public class ClientApi
{ {
private static final Logger LOGGER = LogManager.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public static boolean prefLoggerEnabled = false; public static boolean prefLoggerEnabled = false;
@@ -84,18 +81,24 @@ public class ClientApi
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
public static final long SPAM_LOGGER_FLUSH_NS = TimeUnit.NANOSECONDS.convert(1, TimeUnit.SECONDS);
/** this includes the is dev build message and low allocated memory warning */ /** this includes the is dev build message and low allocated memory warning */
private static final int MS_BETWEEN_STATIC_STARTUP_MESSAGES = 4_000; private static final int MS_BETWEEN_STATIC_STARTUP_MESSAGES = 4_000;
/**
* This isn't the cleanest way of storing variables before passing them to the LOD renderer,
* but due to how mixins work and the inconsistency between MC versions,
* having a static object that stores a single frame's data
* is often the easiest solution. <br><br>
*
* Only downside is making sure each variable is populated before rendering.
*/
public static final DhRenderState RENDER_STATE = new DhRenderState();
private boolean isDevBuildMessagePrinted = false; private boolean isDevBuildMessagePrinted = false;
private boolean lowMemoryWarningPrinted = false; private boolean lowMemoryWarningPrinted = false;
private boolean highVanillaRenderDistanceWarningPrinted = false; private boolean highVanillaRenderDistanceWarningPrinted = false;
/** when the last static */
private long lastStaticWarningMessageSentMsTime = 0L; private long lastStaticWarningMessageSentMsTime = 0L;
private final Queue<String> chatMessageQueueForNextFrame = new LinkedBlockingQueue<>(); private final Queue<String> chatMessageQueueForNextFrame = new LinkedBlockingQueue<>();
@@ -103,8 +106,6 @@ public class ClientApi
public boolean rendererDisabledBecauseOfExceptions = false; public boolean rendererDisabledBecauseOfExceptions = false;
private long lastFlushNanoTime = 0;
private final ClientPluginChannelApi pluginChannelApi = new ClientPluginChannelApi(this::clientLevelLoadEvent, this::clientLevelUnloadEvent); private final ClientPluginChannelApi pluginChannelApi = new ClientPluginChannelApi(this::clientLevelLoadEvent, this::clientLevelUnloadEvent);
/** Delay loading the first level to give the server some time to respond with level to actually load */ /** Delay loading the first level to give the server some time to respond with level to actually load */
@@ -116,8 +117,8 @@ public class ClientApi
/** Holds any chunks that were loaded before the {@link ClientApi#clientLevelLoadEvent(IClientLevelWrapper)} was fired. */ /** Holds any chunks that were loaded before the {@link ClientApi#clientLevelLoadEvent(IClientLevelWrapper)} was fired. */
public final HashMap<Pair<IClientLevelWrapper, DhChunkPos>, IChunkWrapper> waitingChunkByClientLevelAndPos = new HashMap<>(); public final HashMap<Pair<IClientLevelWrapper, DhChunkPos>, IChunkWrapper> waitingChunkByClientLevelAndPos = new HashMap<>();
/** re-set every frame during the opaque rendering stage */ @Nullable
private boolean renderingCancelledForThisFrame; public String lastRenderParamValidationMessage = null;
@@ -322,10 +323,11 @@ public class ClientApi
//===============// //============//
// render events // // clint tick //
//===============// //============//
@Deprecated
public void clientTickEvent() public void clientTickEvent()
{ {
IProfilerWrapper profiler = MC_CLIENT.getProfiler(); IProfilerWrapper profiler = MC_CLIENT.getProfiler();
@@ -333,25 +335,10 @@ public class ClientApi
try try
{ {
boolean doFlush = System.nanoTime() - this.lastFlushNanoTime >= SPAM_LOGGER_FLUSH_NS; IDhClientWorld clientWorld = SharedApi.tryGetDhClientWorld();
if (doFlush)
{
this.lastFlushNanoTime = System.nanoTime();
SpamReducedLogger.flushAll();
}
ConfigBasedLogger.updateAll();
ConfigBasedSpamLogger.updateAll(doFlush);
IDhClientWorld clientWorld = SharedApi.getIDhClientWorld();
if (clientWorld != null) if (clientWorld != null)
{ {
clientWorld.clientTick(); clientWorld.clientTick();
// Ignore local world gen, as it's managed by server ticking
if (!(clientWorld instanceof DhClientServerWorld))
{
SharedApi.worldGenTick(clientWorld::worldGenTick);
}
} }
} }
catch (Exception e) catch (Exception e)
@@ -385,26 +372,24 @@ public class ClientApi
//===========// //===============//
// rendering // // LOD rendering //
//===========// //===============//
/** Should be called before {@link ClientApi#renderDeferredLods} */ /** Should be called before {@link ClientApi#renderDeferredLodsForShaders} */
public void renderLods(IClientLevelWrapper levelWrapper, Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks) public void renderLods() { this.renderLodLayer(false); }
{ this.renderLodLayer(levelWrapper, mcModelViewMatrix, mcProjectionMatrix, partialTicks, false); }
/** /**
* Only necessary when Shaders are in use. * Only necessary when Shaders are in use.
* Should be called after {@link ClientApi#renderLods} * Should be called after {@link ClientApi#renderLods}
*/ */
public void renderDeferredLods(IClientLevelWrapper levelWrapper, Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks) public void renderDeferredLodsForShaders() { this.renderLodLayer(true); }
{ this.renderLodLayer(levelWrapper, mcModelViewMatrix, mcProjectionMatrix, partialTicks, true); }
private void renderLodLayer( private void renderLodLayer(boolean renderingDeferredLayer)
IClientLevelWrapper levelWrapper, Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks,
boolean renderingDeferredLayer)
{ {
//=========//
// logging // // logging //
//=========//
this.sendQueuedChatMessages(); this.sendQueuedChatMessages();
@@ -414,7 +399,33 @@ public class ClientApi
// render parameter setup // //=====================//
// render thread tasks //
//=====================//
// only run these tasks once per frame
if (!renderingDeferredLayer)
{
profiler.push("DH render thread tasks");
try
{
// these tasks always need to be called, regardless of whether the renderer is enabled or not to prevent memory leaks
GLProxy.getInstance().runRenderThreadTasks();
}
catch (Exception e)
{
LOGGER.error("Unexpected issue running render thread tasks, error: [" + e.getMessage() + "].", e);
}
profiler.pop();
}
//=================//
// parameter setup //
//=================//
EDhApiRenderPass renderPass; EDhApiRenderPass renderPass;
if (DhApiRenderProxy.INSTANCE.getDeferTransparentRendering()) if (DhApiRenderProxy.INSTANCE.getDeferTransparentRendering())
@@ -433,42 +444,36 @@ public class ClientApi
renderPass = EDhApiRenderPass.OPAQUE_AND_TRANSPARENT; renderPass = EDhApiRenderPass.OPAQUE_AND_TRANSPARENT;
} }
DhApiRenderParam renderEventParam = // A global render state variable is used since MC has split up their
new DhApiRenderParam( // render prep and actual rendering into different threads/methods
// this is annoying since it's possible to start a render with only
// partially complete info, but there isn't a better option at the moment
RenderParams renderParams =
new RenderParams(
renderPass, renderPass,
partialTicks, RENDER_STATE.frameTime,
RenderUtil.getNearClipPlaneDistanceInBlocks(partialTicks), RenderUtil.getFarClipPlaneDistanceInBlocks(), RENDER_STATE.mcProjectionMatrix, RENDER_STATE.mcModelViewMatrix,
mcProjectionMatrix, mcModelViewMatrix, RENDER_STATE.clientLevelWrapper
RenderUtil.createLodProjectionMatrix(mcProjectionMatrix, partialTicks), RenderUtil.createLodModelViewMatrix(mcModelViewMatrix),
levelWrapper.getMinHeight()
); );
// render validation // //============//
// validation //
//============//
try
{
// TODO write this message to the F3 menu so people can see when a different mod screws with the lightmap // TODO write this message to the F3 menu so people can see when a different mod screws with the lightmap
String reasonLodsCannotRender = RenderUtil.shouldLodsRender(levelWrapper, renderEventParam); String validationMessage = renderParams.getValidationErrorMessage();
if (reasonLodsCannotRender != null) if (validationMessage != null)
{ {
this.lastRenderParamValidationMessage = validationMessage;
return; return;
} }
else
IDhClientWorld dhClientWorld = SharedApi.getIDhClientWorld();
if (dhClientWorld == null)
{ {
return; this.lastRenderParamValidationMessage = null;
} }
IDhClientLevel level = (IDhClientLevel) dhClientWorld.getLevel(levelWrapper);
if (level == null)
{
return;
}
if (this.rendererDisabledBecauseOfExceptions) if (this.rendererDisabledBecauseOfExceptions)
{ {
// re-enable rendering if the user toggles DH rendering // re-enable rendering if the user toggles DH rendering
@@ -484,16 +489,22 @@ public class ClientApi
//===========//
// rendering //
//===========//
try
{
// render pass // // render pass //
if (!renderingDeferredLayer) if (!renderingDeferredLayer)
{ {
if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT) if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT)
{ {
this.renderingCancelledForThisFrame = ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeRenderEvent.class, renderEventParam); boolean renderingCancelledForThisFrame = ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeRenderEvent.class, renderParams);
if (!this.renderingCancelledForThisFrame) if (!renderingCancelledForThisFrame)
{ {
level.render(renderEventParam, profiler); LodRenderer.INSTANCE.render(renderParams, profiler);
} }
if (!DhApi.Delayed.renderProxy.getDeferTransparentRendering()) if (!DhApi.Delayed.renderProxy.getDeferTransparentRendering())
@@ -510,10 +521,10 @@ public class ClientApi
} }
else else
{ {
boolean renderingCancelled = ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeDeferredRenderEvent.class, renderEventParam); boolean renderingCancelled = ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeDeferredRenderEvent.class, renderParams);
if (!renderingCancelled) if (!renderingCancelled)
{ {
level.renderDeferred(renderEventParam, profiler); LodRenderer.INSTANCE.renderDeferred(renderParams, profiler);
} }
@@ -533,50 +544,65 @@ public class ClientApi
MC_CLIENT.sendChatMessage("\u00A74Toggle DH rendering via the config UI to re-activate DH rendering."); MC_CLIENT.sendChatMessage("\u00A74Toggle DH rendering via the config UI to re-activate DH rendering.");
MC_CLIENT.sendChatMessage("\u00A74Error: " + e); MC_CLIENT.sendChatMessage("\u00A74Error: " + e);
} }
finally
{
try
{
// these tasks always need to be called, regardless of whether the renderer is enabled or not to prevent memory leaks
GLProxy.getInstance().runRenderThreadTasks();
}
catch (Exception e)
{
LOGGER.error("Unexpected issue running render thread tasks.", e);
}
profiler.pop(); // end LOD profiler.pop(); // end LOD
profiler.push("terrain"); // go back into "terrain" profiler.push("terrain"); // go back into "terrain"
} }
}
/** should be called after DH and MC finish rendering so we can smooth the transition between the two */
public void renderFadeOpaque(Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks, IClientLevelWrapper level)
//================//
// fade rendering //
//================//
/**
* The first fade pass.
* Called after MC finishes rendering the opaque passes.
*/
public void renderFadeOpaque()
{
// only fade when DH is rendering
if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT
&&
(
// only fade when requested
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
&& !DhApiRenderProxy.INSTANCE.getDeferTransparentRendering())
{
VanillaFadeRenderer.INSTANCE.render(RENDER_STATE.mcModelViewMatrix, RENDER_STATE.mcProjectionMatrix, RENDER_STATE.frameTime, RENDER_STATE.clientLevelWrapper);
}
}
/**
* The second fade pass.
* Called after MC finishes rendering both opaque
* and transparent passes.
*/
public void renderFadeTransparent()
{ {
// 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)
{ {
if (Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.get() == EDhApiMcRenderingFadeMode.DOUBLE_PASS) boolean renderFade =
(
// only fade when requested
Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.get() != EDhApiMcRenderingFadeMode.NONE
// 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
&& !DhApiRenderProxy.INSTANCE.getDeferTransparentRendering();
if (renderFade)
{ {
FadeRenderer.INSTANCE.render(mcModelViewMatrix, mcProjectionMatrix, partialTicks, level); VanillaFadeRenderer.INSTANCE.render(RENDER_STATE.mcModelViewMatrix, RENDER_STATE.mcProjectionMatrix, RENDER_STATE.frameTime, RENDER_STATE.clientLevelWrapper);
} }
} }
} }
/** should be called after DH and MC finish rendering so we can smooth the transition between the two */
public void renderFade(Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks, IClientLevelWrapper level)
{
// only fade when DH is rendering
if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT)
{
// fade if any level fading is active
if (Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.get() != EDhApiMcRenderingFadeMode.NONE)
{
FadeRenderer.INSTANCE.render(mcModelViewMatrix, mcProjectionMatrix, partialTicks, level);
}
}
}
@@ -652,7 +678,8 @@ public class ClientApi
private void detectAndSendBootTimeWarnings() private void detectAndSendBootTimeWarnings()
{ {
// dev build // dev build
if (ModInfo.IS_DEV_BUILD && !this.isDevBuildMessagePrinted && MC_CLIENT.playerExists()) if (ModInfo.IS_DEV_BUILD
&& !this.isDevBuildMessagePrinted && MC_CLIENT.playerExists())
{ {
this.isDevBuildMessagePrinted = true; this.isDevBuildMessagePrinted = true;
this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis(); this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis();
@@ -669,7 +696,8 @@ public class ClientApi
// memory // memory
if (this.staticStartupMessageSentRecently()) return; if (this.staticStartupMessageSentRecently()) return;
if (!this.lowMemoryWarningPrinted && Config.Common.Logging.Warning.showLowMemoryWarningOnStartup.get()) if (!this.lowMemoryWarningPrinted
&& Config.Common.Logging.Warning.showLowMemoryWarningOnStartup.get())
{ {
this.lowMemoryWarningPrinted = true; this.lowMemoryWarningPrinted = true;
this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis(); this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis();
@@ -694,7 +722,8 @@ public class ClientApi
// high vanilla render distance // high vanilla render distance
if (this.staticStartupMessageSentRecently()) return; if (this.staticStartupMessageSentRecently()) return;
if (!this.highVanillaRenderDistanceWarningPrinted && Config.Common.Logging.Warning.showHighVanillaRenderDistanceWarning.get()) if (!this.highVanillaRenderDistanceWarningPrinted
&& Config.Common.Logging.Warning.showHighVanillaRenderDistanceWarning.get())
{ {
// 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)
@@ -721,7 +750,8 @@ public class ClientApi
{ {
if (this.lastStaticWarningMessageSentMsTime == 0) if (this.lastStaticWarningMessageSentMsTime == 0)
{ {
return true; // no static message has ever been sent
return false;
} }
long timeSinceLastMessage = System.currentTimeMillis() - this.lastStaticWarningMessageSentMsTime; long timeSinceLastMessage = System.currentTimeMillis() - this.lastStaticWarningMessageSentMsTime;
@@ -740,4 +770,6 @@ public class ClientApi
*/ */
public void showOverlayMessageNextFrame(String message) { this.overlayMessageQueueForNextFrame.add(message); } public void showOverlayMessageNextFrame(String message) { this.overlayMessageQueueForNextFrame.add(message); }
} }
@@ -4,14 +4,14 @@ import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager; import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager;
import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel; import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
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.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.apache.logging.log4j.LogManager; import org.jetbrains.annotations.NotNull;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Objects; import java.util.Objects;
@@ -22,8 +22,10 @@ import java.util.function.Consumer;
*/ */
public class ClientPluginChannelApi public class ClientPluginChannelApi
{ {
private static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(), private static final DhLogger LOGGER = new DhLoggerBuilder()
() -> Config.Common.Logging.logNetworkEvent.get()); .fileLevelConfig(Config.Common.Logging.logNetworkEventToFile)
.build();
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IKeyedClientLevelManager KEYED_CLIENT_LEVEL_MANAGER = SingletonInjector.INSTANCE.get(IKeyedClientLevelManager.class); private static final IKeyedClientLevelManager KEYED_CLIENT_LEVEL_MANAGER = SingletonInjector.INSTANCE.get(IKeyedClientLevelManager.class);
@@ -65,7 +67,7 @@ public class ClientPluginChannelApi
//================// //================//
/** fired when this client connects to a server with DH support */ /** fired when this client connects to a server with DH support */
public void onJoinServer(@NonNull NetworkSession networkSession) public void onJoinServer(@NotNull NetworkSession networkSession)
{ {
Objects.requireNonNull(networkSession); Objects.requireNonNull(networkSession);
this.networkSession = networkSession; this.networkSession = networkSession;
@@ -75,7 +77,12 @@ public class ClientPluginChannelApi
private void onLevelInitMessage(LevelInitMessage msg) private void onLevelInitMessage(LevelInitMessage msg)
{ {
if (!msg.levelKey.matches(LevelInitMessage.VALIDATION_REGEX)) if (!msg.serverKey.isEmpty() && !msg.serverKey.matches(LevelInitMessage.SERVER_KEY_REGEX))
{
throw new IllegalArgumentException("Server sent invalid server key.");
}
if (!msg.levelKey.matches(LevelInitMessage.LEVEL_KEY_REGEX))
{ {
throw new IllegalArgumentException("Server sent invalid level key."); throw new IllegalArgumentException("Server sent invalid level key.");
} }
@@ -105,10 +112,12 @@ public class ClientPluginChannelApi
this.levelUnloadHandler.accept(clientLevel); this.levelUnloadHandler.accept(clientLevel);
} }
if (existingKeyedClientLevel == null || !existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey)) if (existingKeyedClientLevel == null
|| !existingKeyedClientLevel.getServerKey().equals(msg.serverKey)
|| !existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey))
{ {
LOGGER.info("Loading level with key: [" + msg.levelKey + "]."); LOGGER.info("Loading level with key: [" + msg.levelKey + "].");
IServerKeyedClientLevel keyedLevel = KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel, msg.levelKey); IServerKeyedClientLevel keyedLevel = KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel, msg.serverKey, msg.levelKey);
this.levelLoadHandler.accept(keyedLevel); this.levelLoadHandler.accept(keyedLevel);
} }
}); });
@@ -30,7 +30,7 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/** /**
@@ -41,7 +41,7 @@ public class ServerApi
{ {
public static final ServerApi INSTANCE = new ServerApi(); public static final ServerApi INSTANCE = new ServerApi();
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
@@ -53,30 +53,6 @@ public class ServerApi
//=============//
// tick events //
//=============//
public void serverTickEvent()
{
try
{
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld();
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 //
//===============// //===============//
@@ -136,8 +112,8 @@ public class ServerApi
// chunk modified events // // chunk modified events //
//=======================// //=======================//
public void serverChunkLoadEvent(IChunkWrapper chunkWrapper, ILevelWrapper level) { SharedApi.INSTANCE.applyChunkUpdate(chunkWrapper, level, false); } public void serverChunkLoadEvent(IChunkWrapper chunkWrapper, ILevelWrapper level) { SharedApi.INSTANCE.applyChunkUpdate(chunkWrapper, level, false, false); }
public void serverChunkSaveEvent(IChunkWrapper chunkWrapper, ILevelWrapper level) { SharedApi.INSTANCE.applyChunkUpdate(chunkWrapper, level, true); } public void serverChunkSaveEvent(IChunkWrapper chunkWrapper, ILevelWrapper level) { SharedApi.INSTANCE.applyChunkUpdate(chunkWrapper, level, true, false); }
@@ -152,7 +128,7 @@ public class ServerApi
return; return;
} }
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); IDhServerWorld serverWorld = SharedApi.tryGetDhServerWorld();
LOGGER.info("Player ["+player.getName()+"] joined."); LOGGER.info("Player ["+player.getName()+"] joined.");
if (serverWorld != null) if (serverWorld != null)
{ {
@@ -166,7 +142,7 @@ public class ServerApi
return; return;
} }
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); IDhServerWorld serverWorld = SharedApi.tryGetDhServerWorld();
LOGGER.info("Player ["+player.getName()+"] disconnected."); LOGGER.info("Player ["+player.getName()+"] disconnected.");
if (serverWorld != null) if (serverWorld != null)
{ {
@@ -180,7 +156,7 @@ public class ServerApi
return; return;
} }
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); IDhServerWorld serverWorld = SharedApi.tryGetDhServerWorld();
LOGGER.info("Player ["+player.getName()+"] changed level: ["+originLevel.getKeyedLevelDimensionName()+"] -> ["+destinationLevel.getKeyedLevelDimensionName()+"]."); LOGGER.info("Player ["+player.getName()+"] changed level: ["+originLevel.getKeyedLevelDimensionName()+"] -> ["+destinationLevel.getKeyedLevelDimensionName()+"].");
if (serverWorld != null) if (serverWorld != null)
{ {
@@ -200,7 +176,7 @@ public class ServerApi
return; return;
} }
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); IDhServerWorld serverWorld = SharedApi.tryGetDhServerWorld();
if (serverWorld != null) if (serverWorld != null)
{ {
serverWorld.getServerPlayerStateManager().handlePluginMessage(player, message); serverWorld.getServerPlayerStateManager().handlePluginMessage(player, message);
@@ -22,6 +22,8 @@ package com.seibel.distanthorizons.core.api.internal;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiWorldLoadEvent; import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiWorldLoadEvent;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiWorldUnloadEvent; import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiWorldUnloadEvent;
import com.seibel.distanthorizons.core.Initializer; import com.seibel.distanthorizons.core.Initializer;
import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateData;
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.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.generation.DhLightingEngine; import com.seibel.distanthorizons.core.generation.DhLightingEngine;
@@ -45,7 +47,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSha
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.*;
@@ -56,7 +58,7 @@ public class SharedApi
{ {
public static final SharedApi INSTANCE = new SharedApi(); public static final SharedApi INSTANCE = new SharedApi();
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
/** will be null on the server-side */ /** will be null on the server-side */
@Nullable @Nullable
private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
@@ -65,20 +67,20 @@ public class SharedApi
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class); private static final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
private static final UpdateChunkPosManager UPDATE_POS_MANAGER = new UpdateChunkPosManager(); public static final ChunkUpdateQueueManager CHUNK_UPDATE_QUEUE_MANAGER = new ChunkUpdateQueueManager();
/** /**
* how many chunks can be queued for updating per thread + player (in multiplayer), * how many chunks can be queued for updating per thread + player (in multiplayer),
* used to prevent updates from infinitely pilling up if the user flies around extremely fast * used to prevent updates from infinitely pilling up if the user flies around extremely fast
*/ */
private static final int MAX_UPDATING_CHUNK_COUNT_PER_THREAD_AND_PLAYER = 1_000; public static final int MAX_UPDATING_CHUNK_COUNT_PER_THREAD_AND_PLAYER = 1_000;
/** how many milliseconds must pass before an overloaded message can be sent in chat or the log */ /** how many milliseconds must pass before an overloaded message can be sent in chat or the log */
private static final int MIN_MS_BETWEEN_OVERLOADED_LOG_MESSAGE = 30_000; public static final int MIN_MS_BETWEEN_OVERLOADED_LOG_MESSAGE = 30_000;
@Nullable
private static AbstractDhWorld currentWorld; private static AbstractDhWorld currentWorld;
private static int lastWorldGenTickDelta = 0; private static int lastWorldGenTickDelta = 0;
private static long lastOverloadedLogMessageMsTime = 0;
@@ -99,6 +101,11 @@ public class SharedApi
public static void setDhWorld(AbstractDhWorld newWorld) public static void setDhWorld(AbstractDhWorld newWorld)
{ {
AbstractDhWorld oldWorld = currentWorld;
if (oldWorld != null)
{
oldWorld.close();
}
currentWorld = newWorld; currentWorld = newWorld;
// starting and stopping the DataRenderTransformer is necessary to prevent attempting to // starting and stopping the DataRenderTransformer is necessary to prevent attempting to
@@ -122,7 +129,7 @@ public class SharedApi
// shouldn't be necessary, but if we missed closing one of the connections this should make sure they're all closed // shouldn't be necessary, but if we missed closing one of the connections this should make sure they're all closed
AbstractDhRepo.closeAllConnections(); AbstractDhRepo.closeAllConnections();
// needs to be closed on world shutdown to clear out un-processed chunks // needs to be closed on world shutdown to clear out un-processed chunks
UPDATE_POS_MANAGER.clear(); CHUNK_UPDATE_QUEUE_MANAGER.clear();
// recommend that the garbage collector cleans up any objects from the old world and thread pools // recommend that the garbage collector cleans up any objects from the old world and thread pools
System.gc(); System.gc();
@@ -134,23 +141,16 @@ public class SharedApi
} }
} }
public static void worldGenTick(Runnable worldGenRunnable) @Nullable
{
lastWorldGenTickDelta--;
if (lastWorldGenTickDelta <= 0)
{
worldGenRunnable.run();
lastWorldGenTickDelta = 20;
}
}
public static AbstractDhWorld getAbstractDhWorld() { return currentWorld; } public static AbstractDhWorld getAbstractDhWorld() { return currentWorld; }
/** returns null if the {@link SharedApi#currentWorld} isn't a {@link DhClientServerWorld} */
public static DhClientServerWorld getDhClientServerWorld() { return (currentWorld instanceof DhClientServerWorld) ? (DhClientServerWorld) currentWorld : null; }
/** returns null if the {@link SharedApi#currentWorld} isn't a {@link DhClientWorld} or {@link DhClientServerWorld} */ /** returns null if the {@link SharedApi#currentWorld} isn't a {@link DhClientWorld} or {@link DhClientServerWorld} */
public static IDhClientWorld getIDhClientWorld() { return (currentWorld instanceof IDhClientWorld) ? (IDhClientWorld) currentWorld : null; } @Nullable
public static IDhClientWorld tryGetDhClientWorld() { return (currentWorld instanceof IDhClientWorld) ? (IDhClientWorld) currentWorld : null; }
/** returns null if the {@link SharedApi#currentWorld} isn't a {@link DhServerWorld} or {@link DhClientServerWorld} */ /** returns null if the {@link SharedApi#currentWorld} isn't a {@link DhServerWorld} or {@link DhClientServerWorld} */
public static IDhServerWorld getIDhServerWorld() { return (currentWorld instanceof IDhServerWorld) ? (IDhServerWorld) currentWorld : null; } @Nullable
public static IDhServerWorld tryGetDhServerWorld() { return (currentWorld instanceof IDhServerWorld) ? (IDhServerWorld) currentWorld : null; }
@@ -163,10 +163,10 @@ public class SharedApi
* This is important since asking MC for a chunk is slow and may block the render thread. * This is important since asking MC for a chunk is slow and may block the render thread.
*/ */
public static boolean isChunkAtBlockPosAlreadyUpdating(int blockPosX, int blockPosZ) public static boolean isChunkAtBlockPosAlreadyUpdating(int blockPosX, int blockPosZ)
{ return UPDATE_POS_MANAGER.contains(new DhChunkPos(new DhBlockPos2D(blockPosX, blockPosZ))); } { return CHUNK_UPDATE_QUEUE_MANAGER.contains(new DhChunkPos(new DhBlockPos2D(blockPosX, blockPosZ))); }
public static boolean isChunkAtChunkPosAlreadyUpdating(int chunkPosX, int chunkPosZ) public static boolean isChunkAtChunkPosAlreadyUpdating(int chunkPosX, int chunkPosZ)
{ return UPDATE_POS_MANAGER.contains(new DhChunkPos(chunkPosX, chunkPosZ)); } { return CHUNK_UPDATE_QUEUE_MANAGER.contains(new DhChunkPos(chunkPosX, chunkPosZ)); }
/** /**
* This is often fired when unloading a level. * This is often fired when unloading a level.
@@ -174,17 +174,18 @@ public class SharedApi
* rapidly changing dimensions. * rapidly changing dimensions.
* (IE prevent DH from infinitely allocating memory * (IE prevent DH from infinitely allocating memory
*/ */
public void clearQueuedChunkUpdates() { UPDATE_POS_MANAGER.clear(); } public void clearQueuedChunkUpdates() { CHUNK_UPDATE_QUEUE_MANAGER.clear(); }
public int getQueuedChunkUpdateCount() { return UPDATE_POS_MANAGER.closestQueue.size(); } public int getQueuedChunkUpdateCount() { return CHUNK_UPDATE_QUEUE_MANAGER.getQueuedCount(); }
/** handles both block place and break events */ /** handles both block place and break events */
public void chunkBlockChangedEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true); } public void chunkBlockChangedEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true, false); }
public void chunkLoadEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, false); } public void chunkLoadEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true, true); }
public void applyChunkUpdate(IChunkWrapper chunkWrapper, ILevelWrapper level, boolean updateNeighborChunks) //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)
{ {
//========================// //========================//
// world and level checks // // world and level checks //
@@ -240,7 +241,7 @@ public class SharedApi
} }
// shoudln't normally happen, but just in case // shoudln't normally happen, but just in case
if (UPDATE_POS_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
return; return;
@@ -252,23 +253,53 @@ public class SharedApi
// update the necessary chunk(s) // // update the necessary chunk(s) //
//===============================// //===============================//
if (!updateNeighborChunks) if (!canGetNeighboringChunks)
{ {
// only update the center chunk // only update the center chunk
queueChunkUpdate(chunkWrapper, null, dhLevel, false);
return;
}
queueChunkUpdate(chunkWrapper, null, 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 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. // update the center with any existing neighbour chunks.
// this is done so lighting changes are propagated correctly // this is done so lighting changes are propagated correctly
queueChunkUpdate(chunkWrapper, getNeighbourChunkListForChunk(chunkWrapper,dhLevel), dhLevel); queueChunkUpdate(chunkWrapper, neighboringChunkList, dhLevel, true);
} }
} }
private static ArrayList<IChunkWrapper> getNeighbourChunkListForChunk(IChunkWrapper chunkWrapper, IDhLevel dhLevel) private static ArrayList<IChunkWrapper> getNeighborChunkListForChunk(IChunkWrapper chunkWrapper, IDhLevel dhLevel)
{ {
// get the neighboring chunk list // get the neighboring chunk list
ArrayList<IChunkWrapper> neighbourChunkList = new ArrayList<>(9); ArrayList<IChunkWrapper> neighborChunkList = new ArrayList<>(9);
for (int xOffset = -1; xOffset <= 1; xOffset++) for (int xOffset = -1; xOffset <= 1; xOffset++)
{ {
for (int zOffset = -1; zOffset <= 1; zOffset++) for (int zOffset = -1; zOffset <= 1; zOffset++)
@@ -276,80 +307,36 @@ public class SharedApi
if (xOffset == 0 && zOffset == 0) if (xOffset == 0 && zOffset == 0)
{ {
// center chunk // center chunk
neighbourChunkList.add(chunkWrapper); neighborChunkList.add(chunkWrapper);
} }
else else
{ {
// neighboring chunk // neighboring chunk
DhChunkPos neighbourPos = new DhChunkPos(chunkWrapper.getChunkPos().getX() + xOffset, chunkWrapper.getChunkPos().getZ() + zOffset); DhChunkPos neighborPos = new DhChunkPos(chunkWrapper.getChunkPos().getX() + xOffset, chunkWrapper.getChunkPos().getZ() + zOffset);
IChunkWrapper neighbourChunk = dhLevel.getLevelWrapper().tryGetChunk(neighbourPos); IChunkWrapper neighborChunk = dhLevel.getLevelWrapper().tryGetChunk(neighborPos);
if (neighbourChunk != null) if (neighborChunk != null)
{ {
neighbourChunkList.add(neighbourChunk); neighborChunkList.add(neighborChunk);
} }
} }
} }
} }
return neighbourChunkList; return neighborChunkList;
} }
private static void queueChunkUpdate(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighbourChunkList, IDhLevel dhLevel) private static void queueChunkUpdate(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighborChunkList, IDhLevel dhLevel, boolean canGetNeighboringChunks)
{ queueChunkUpdate(chunkWrapper, neighbourChunkList, dhLevel,false); }
private static void queueChunkUpdate(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighbourChunkList, IDhLevel dhLevel, boolean lightUpdateOnly)
{ {
int maxUpdateSizeMultiplier;
if (MC_CLIENT != null && MC_CLIENT.playerExists()) // return if the chunk is already queued
if (CHUNK_UPDATE_QUEUE_MANAGER.contains(chunkWrapper.getChunkPos()))
{ {
// Local worlds & multiplayer return;
UPDATE_POS_MANAGER.setCenter(MC_CLIENT.getPlayerChunkPos());
maxUpdateSizeMultiplier = MC_CLIENT.clientConnectedToDedicatedServer() ? 1 : MC_SHARED.getPlayerCount();
}
else
{
// Dedicated servers
// Also includes spawn chunks since they're likely to be intentionally utilized with updates
maxUpdateSizeMultiplier = 1 + MC_SHARED.getPlayerCount();
} }
UPDATE_POS_MANAGER.maxSize = MAX_UPDATING_CHUNK_COUNT_PER_THREAD_AND_PLAYER
* Config.Common.MultiThreading.numberOfThreads.get()
* maxUpdateSizeMultiplier;
UpdateChunkData updateData = new UpdateChunkData(chunkWrapper, neighbourChunkList, dhLevel, lightUpdateOnly);
if(lightUpdateOnly)
{
UPDATE_POS_MANAGER.removeItem(chunkWrapper.getChunkPos());
}
int remainingCapacity = UPDATE_POS_MANAGER.addItem(chunkWrapper.getChunkPos(), updateData);
if (remainingCapacity <= 0)
{
// limit how often an overloaded message can be sent
long msBetweenLastLog = System.currentTimeMillis() - lastOverloadedLogMessageMsTime;
if (msBetweenLastLog >= MIN_MS_BETWEEN_OVERLOADED_LOG_MESSAGE)
{
lastOverloadedLogMessageMsTime = System.currentTimeMillis();
String message = "\u00A76" + "Distant Horizons overloaded, too many chunks queued for LOD processing. " + "\u00A7r" +
"\nThis may result in holes in your LODs. " +
"\nFix: move through the world slower, decrease your vanilla render distance, slow down your world pre-generator (IE Chunky), or increase the Distant Horizons' CPU thread counts. " +
"\nMax queue count ["+UPDATE_POS_MANAGER.maxSize+"] (["+ MAX_UPDATING_CHUNK_COUNT_PER_THREAD_AND_PLAYER +"] per thread+players).";
boolean showWarningInChat = Config.Common.Logging.Warning.showUpdateQueueOverloadedChatWarning.get();
if (showWarningInChat)
{
ClientApi.INSTANCE.showChatMessageNextFrame(message);
}
// Don't log warnings in singleplayer or in hosted LAN since it usually isn't a problem (and if it is it's easy to notice).
// Servers should always log since being overloaded is harder to notice.
EWorldEnvironment environment = SharedApi.getEnvironment();
if (showWarningInChat || environment == EWorldEnvironment.SERVER_ONLY)
{
LOGGER.warn(message);
}
}
}
// add chunk update data to preUpdate queue
ChunkUpdateData updateData = new ChunkUpdateData(chunkWrapper, neighborChunkList, dhLevel, canGetNeighboringChunks);
CHUNK_UPDATE_QUEUE_MANAGER.addItemToPreUpdateQueue(chunkWrapper.getChunkPos(), updateData);
// queue updates up to the number of CPU cores allocated for the job // queue updates up to the number of CPU cores allocated for the job
@@ -360,7 +347,7 @@ public class SharedApi
{ {
try try
{ {
executor.execute(SharedApi::processQueuedChunkUpdate); executor.execute(SharedApi::processQueue);
} }
catch (RejectedExecutionException ignore) catch (RejectedExecutionException ignore)
{ {
@@ -368,94 +355,159 @@ public class SharedApi
} }
} }
} }
private static void processQueue()
{
// update the center & max size of the queue manager
int maxUpdateSizeMultiplier;
if (MC_CLIENT != null && MC_CLIENT.playerExists())
{
// Local worlds & multiplayer
CHUNK_UPDATE_QUEUE_MANAGER.setCenter(MC_CLIENT.getPlayerChunkPos());
maxUpdateSizeMultiplier = MC_CLIENT.clientConnectedToDedicatedServer() ? 1 : MC_SHARED.getPlayerCount();
}
else
{
// Dedicated servers
// Also includes spawn chunks since they're likely to be intentionally utilized with updates
maxUpdateSizeMultiplier = 1 + MC_SHARED.getPlayerCount();
}
CHUNK_UPDATE_QUEUE_MANAGER.maxSize = MAX_UPDATING_CHUNK_COUNT_PER_THREAD_AND_PLAYER
* Config.Common.MultiThreading.numberOfThreads.get()
* maxUpdateSizeMultiplier;
//===============================//
// update the necessary chunk(s) //
//===============================//
// process preUpdate queue
processQueuedChunkPreUpdate();
// process update queue
processQueuedChunkUpdate();
// queue the next position if there are still positions to process
AbstractExecutorService executor = ThreadPoolUtil.getChunkToLodBuilderExecutor();
if (executor != null && !CHUNK_UPDATE_QUEUE_MANAGER.isEmpty())
{
try
{
executor.execute(SharedApi::processQueue);
}
catch (RejectedExecutionException ignore)
{
// the executor was shut down, it should be back up shortly and able to accept new jobs
}
}
}
private static void processQueuedChunkPreUpdate()
{
ChunkUpdateData preUpdateData = CHUNK_UPDATE_QUEUE_MANAGER.preUpdateQueue.popClosest();
if (preUpdateData == null)
{
return;
}
IDhLevel dhLevel = preUpdateData.dhLevel;
IChunkWrapper chunkWrapper = preUpdateData.chunkWrapper;
boolean canGetNeighboringChunks = preUpdateData.canGetNeighboringChunks;
ArrayList<IChunkWrapper> neighborChunkList = preUpdateData.neighborChunkList;
try
{
// check if this chunk has been converted into an LOD already
boolean checkChunkHash = !Config.Common.LodBuilding.disableUnchangedChunkCheck.get();
if (checkChunkHash)
{
int oldChunkHash = dhLevel.getChunkHash(chunkWrapper.getChunkPos()); // shouldn't happen on the render thread since it may take a few moments to run
int newChunkHash = chunkWrapper.getBlockBiomeHashCode();
boolean hasNewChunkHash = (oldChunkHash != newChunkHash);
if (!hasNewChunkHash)
{
// do not update the chunk if the hash is the same
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);
}
catch (Exception e)
{
LOGGER.error("Unexpected error when pre-updating chunk at pos: [" + chunkWrapper.getChunkPos() + "]", e);
}
}
private static void processQueuedChunkUpdate() private static void processQueuedChunkUpdate()
{ {
//LOGGER.trace(chunkWrapper.getChunkPos() + " " + executor.getActiveCount() + " / " + executor.getQueue().size() + " - " + executor.getCompletedTaskCount()); //LOGGER.trace(chunkWrapper.getChunkPos() + " " + executor.getActiveCount() + " / " + executor.getQueue().size() + " - " + executor.getCompletedTaskCount());
UpdateChunkData updateData = UPDATE_POS_MANAGER.popClosest(); ChunkUpdateData updateData = CHUNK_UPDATE_QUEUE_MANAGER.updateQueue.popClosest();
if (updateData == null) if (updateData == null)
{ {
return; return;
} }
IChunkWrapper chunkWrapper = updateData.chunkWrapper; IChunkWrapper chunkWrapper = updateData.chunkWrapper;
@Nullable ArrayList<IChunkWrapper> neighbourChunkList = updateData.neighbourChunkList;
IDhLevel dhLevel = updateData.dhLevel; IDhLevel dhLevel = updateData.dhLevel;
ILevelWrapper levelWrapper = dhLevel.getLevelWrapper();
try
{
boolean checkChunkHash = !Config.Common.LodBuilding.disableUnchangedChunkCheck.get();
// check if this chunk has been converted into an LOD already
int oldChunkHash = dhLevel.getChunkHash(chunkWrapper.getChunkPos()); // shouldn't happen on the render thread since it may take a few moments to run
int newChunkHash = chunkWrapper.getBlockBiomeHashCode();
if (checkChunkHash)
{
if (oldChunkHash == newChunkHash && !updateData.lightUpdateOnly)
{
// if the chunk hashes are the same then we don't need to bother with lighting the chunk
// or creating/updating the LODs
return;
}
}
// having a list of the nearby chunks is needed for lighting and beacon generation // having a list of the nearby chunks is needed for lighting and beacon generation
ArrayList<IChunkWrapper> nearbyChunkList; @Nullable ArrayList<IChunkWrapper> nearbyChunkList = updateData.neighborChunkList;
if (neighbourChunkList != null)
// a non-null list is needed for the lighting engine
if (nearbyChunkList == null)
{ {
nearbyChunkList = neighbourChunkList; nearbyChunkList = new ArrayList<IChunkWrapper>();
}
else
{
nearbyChunkList = new ArrayList<>(1);
nearbyChunkList.add(chunkWrapper); nearbyChunkList.add(chunkWrapper);
} }
// if this chunk will update its lighting try
// then queue adjacent chunks to update theirs as well
// adjacent chunk will have 'lightUpdateOnly' true
// so they won't schedule further chunk updates
if (!updateData.lightUpdateOnly)
{ {
for (IChunkWrapper adjacentChunk : nearbyChunkList)
{
// pulling a new chunkWrapper is necessary to prevent concurrent modification on the existing chunkWrappers
IChunkWrapper newCenterChunk = dhLevel.getLevelWrapper().tryGetChunk(adjacentChunk.getChunkPos());
if (newCenterChunk != null)
{
queueChunkUpdate(newCenterChunk, getNeighbourChunkListForChunk(newCenterChunk, dhLevel), dhLevel, true);
}
}
}
// sky lighting is populated later at the data source level // sky lighting is populated later at the data source level
DhLightingEngine.INSTANCE.bakeChunkBlockLighting(chunkWrapper, nearbyChunkList, dhLevel.hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT); DhLightingEngine.INSTANCE.bakeChunkBlockLighting(chunkWrapper, nearbyChunkList, levelWrapper.hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT);
dhLevel.updateBeaconBeamsForChunk(chunkWrapper, nearbyChunkList); dhLevel.updateBeaconBeamsForChunk(chunkWrapper, nearbyChunkList);
int newChunkHash = chunkWrapper.getBlockBiomeHashCode();
dhLevel.updateChunkAsync(chunkWrapper, newChunkHash); dhLevel.updateChunkAsync(chunkWrapper, newChunkHash);
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.error("Unexpected error when updating chunk at pos: [" + chunkWrapper.getChunkPos() + "]", e); LOGGER.error("Unexpected error when updating chunk at pos: [" + chunkWrapper.getChunkPos() + "]", e);
} }
finally
{
// queue the next position if there are still positions to process
AbstractExecutorService executor = ThreadPoolUtil.getChunkToLodBuilderExecutor();
if (executor != null && !UPDATE_POS_MANAGER.updateDataByChunkPos.isEmpty())
{
try
{
executor.execute(SharedApi::processQueuedChunkUpdate);
}
catch (RejectedExecutionException ignore)
{
// the executor was shut down, it should be back up shortly and able to accept new jobs
}
}
}
} }
@@ -466,161 +518,13 @@ public class SharedApi
public String getDebugMenuString() public String getDebugMenuString()
{ {
String updatingCountStr = F3Screen.NUMBER_FORMAT.format(UPDATE_POS_MANAGER.closestQueue.size()); String preUpdatingCountStr = F3Screen.NUMBER_FORMAT.format(CHUNK_UPDATE_QUEUE_MANAGER.preUpdateQueue.getQueuedCount());
String maxUpdateCountStr = F3Screen.NUMBER_FORMAT.format(UPDATE_POS_MANAGER.maxSize); String updatingCountStr = F3Screen.NUMBER_FORMAT.format(CHUNK_UPDATE_QUEUE_MANAGER.updateQueue.getQueuedCount());
return "Queued chunk updates: "+updatingCountStr+" / "+maxUpdateCountStr; String queuedCountStr = F3Screen.NUMBER_FORMAT.format(CHUNK_UPDATE_QUEUE_MANAGER.getQueuedCount());
}
String maxUpdateCountStr = F3Screen.NUMBER_FORMAT.format(CHUNK_UPDATE_QUEUE_MANAGER.maxSize);
return "Queued chunk updates: "+"( "+preUpdatingCountStr+" + "+updatingCountStr+" ) [ "+queuedCountStr+" / "+maxUpdateCountStr+" ]";
//================//
// helper classes //
//================//
/** contains the objects needed to update a chunk */
private static class UpdateChunkData
{
public IChunkWrapper chunkWrapper;
@Nullable
public ArrayList<IChunkWrapper> neighbourChunkList;
public IDhLevel dhLevel;
/** adjacent chunks will only update their light */
public boolean lightUpdateOnly;
public UpdateChunkData(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighbourChunkList, IDhLevel dhLevel, boolean lightUpdateOnly)
{
this.chunkWrapper = chunkWrapper;
this.neighbourChunkList = neighbourChunkList;
this.dhLevel = dhLevel;
this.lightUpdateOnly = lightUpdateOnly;
}
}
/** keeps track of which chunks need to be updated */
private static class UpdateChunkPosManager
{
private final PriorityBlockingQueue<DhChunkPos> closestQueue;
private final PriorityBlockingQueue<DhChunkPos> furthestQueue;
private final ConcurrentHashMap<DhChunkPos, UpdateChunkData> updateDataByChunkPos;
private DhChunkPos center;
private int maxSize = 500;
//=============//
// constructor //
//=============//
public UpdateChunkPosManager()
{
this.closestQueue = new PriorityBlockingQueue<>(500, Comparator.comparingDouble(pos -> pos.squaredDistance(this.center)));
this.furthestQueue = new PriorityBlockingQueue<>(500, Comparator.comparingDouble(pos -> ((DhChunkPos)pos).squaredDistance(this.center)).reversed());
this.updateDataByChunkPos = new ConcurrentHashMap<>();
// defaulting to 0,0 is fine since it'll be updated once we start adding items
this.center = new DhChunkPos(0, 0);
}
//==================//
// list/set methods //
//==================//
public boolean contains(DhChunkPos pos) { return this.updateDataByChunkPos.containsKey(pos); }
public void clear()
{
this.updateDataByChunkPos.clear();
this.closestQueue.clear();
this.furthestQueue.clear();
}
public void removeItem(DhChunkPos pos)
{
this.updateDataByChunkPos.remove(pos);
this.closestQueue.remove(pos);
this.furthestQueue.remove(pos);
}
/**
* Adds an item to the queue of chunks that need to be updated.
* If there are no more slots, replaces the item furthest from the center.
*
* @return The number of remaining slots available in the queue.
*/
public int addItem(DhChunkPos pos, UpdateChunkData updateData)
{
int remainingSlots = this.maxSize - this.updateDataByChunkPos.size();
if (this.updateDataByChunkPos.containsKey(pos))
{
// Chunk is already present in queue, no need to insert
return remainingSlots;
}
// If no slots are left, get one by removing the item furthest from the center
if (remainingSlots <= 0)
{
DhChunkPos furthest = this.furthestQueue.poll();
if (furthest != null)
{
this.closestQueue.remove(furthest);
this.updateDataByChunkPos.remove(furthest);
}
}
this.updateDataByChunkPos.put(pos, updateData);
this.closestQueue.add(pos);
this.furthestQueue.add(pos);
return remainingSlots;
}
//==================//
// position methods //
//==================//
public void setCenter(DhChunkPos newCenter)
{
// if the rebuild time takes too long
// (in James' testing a queue of 500 items only took around 0.1 milliseconds)
// this equation could be changed to only update after moving 2 or 4 chunks from the center
if (newCenter.equals(this.center))
{
return;
}
this.center = newCenter;
// rebuild the priority queues to match the new center
this.closestQueue.clear();
this.furthestQueue.clear();
for (DhChunkPos pos : this.updateDataByChunkPos.keySet())
{
this.closestQueue.add(pos);
this.furthestQueue.add(pos);
}
}
public UpdateChunkData popClosest()
{
if (this.closestQueue.isEmpty())
{
return null;
}
DhChunkPos closest = this.closestQueue.poll();
if (closest == null)
{
return null;
}
this.furthestQueue.remove(closest);
return this.updateDataByChunkPos.remove(closest);
}
} }
@@ -0,0 +1,125 @@
package com.seibel.distanthorizons.core.api.internal.chunkUpdating;
import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
import java.util.Comparator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.PriorityBlockingQueue;
public class ChunkPosQueue
{
private final PriorityBlockingQueue<DhChunkPos> closestQueue;
private final PriorityBlockingQueue<DhChunkPos> furthestQueue;
private final ConcurrentHashMap<DhChunkPos, ChunkUpdateData> updateDataByChunkPos;
private DhChunkPos center;
//=============//
// constructor //
//=============//
public ChunkPosQueue()
{
this.closestQueue = new PriorityBlockingQueue<>(500, Comparator.comparingDouble(pos -> pos.squaredDistance(this.center)));
this.furthestQueue = new PriorityBlockingQueue<>(500, Comparator.comparingDouble(pos -> ((DhChunkPos)pos).squaredDistance(this.center)).reversed());
this.updateDataByChunkPos = new ConcurrentHashMap<>();
// defaulting to 0,0 is fine since it'll be updated once we start adding items
this.center = new DhChunkPos(0, 0);
}
//==============//
// list methods //
//==============//
public boolean contains(DhChunkPos pos) { return this.updateDataByChunkPos.containsKey(pos); }
public void clear()
{
this.updateDataByChunkPos.clear();
this.closestQueue.clear();
this.furthestQueue.clear();
}
public void addItem(DhChunkPos pos, ChunkUpdateData updateData)
{
if (this.updateDataByChunkPos.containsKey(pos))
{
// Chunk is already present in queue, no need to insert
return;
}
this.updateDataByChunkPos.put(pos, updateData);
this.closestQueue.add(pos);
this.furthestQueue.add(pos);
}
public int getQueuedCount() { return this.updateDataByChunkPos.size(); }
public boolean isEmpty() { return this.updateDataByChunkPos.isEmpty(); }
//==================//
// position methods //
//==================//
public void setCenter(DhChunkPos newCenter)
{
// if the rebuild time takes too long
// (in James' testing a queue of 500 items only took around 0.1 milliseconds)
// this equation could be changed to only update after moving 2 or 4 chunks from the center
if (newCenter.equals(this.center))
{
return;
}
this.center = newCenter;
// rebuild the priority queues to match the new center
this.closestQueue.clear();
this.furthestQueue.clear();
for (DhChunkPos pos : this.updateDataByChunkPos.keySet())
{
this.closestQueue.add(pos);
this.furthestQueue.add(pos);
}
}
public ChunkUpdateData popClosest()
{
if (this.closestQueue.isEmpty())
{
return null;
}
DhChunkPos closest = this.closestQueue.poll();
if (closest == null)
{
return null;
}
this.furthestQueue.remove(closest);
return this.updateDataByChunkPos.remove(closest);
}
public ChunkUpdateData popFurthest()
{
if (this.furthestQueue.isEmpty())
{
return null;
}
DhChunkPos furthest = this.furthestQueue.poll();
if (furthest == null)
{
return null;
}
this.closestQueue.remove(furthest);
return this.updateDataByChunkPos.remove(furthest);
}
}
@@ -0,0 +1,26 @@
package com.seibel.distanthorizons.core.api.internal.chunkUpdating;
import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
public class ChunkUpdateData
{
public IChunkWrapper chunkWrapper;
@Nullable
public ArrayList<IChunkWrapper> neighborChunkList;
public IDhLevel dhLevel;
public boolean canGetNeighboringChunks;
public ChunkUpdateData(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighborChunkList, IDhLevel dhLevel, boolean canGetNeighborChunks)
{
this.chunkWrapper = chunkWrapper;
this.neighborChunkList = neighborChunkList;
this.dhLevel = dhLevel;
this.canGetNeighboringChunks = canGetNeighborChunks;
}
}
@@ -0,0 +1,165 @@
package com.seibel.distanthorizons.core.api.internal.chunkUpdating;
import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.world.EWorldEnvironment;
import com.seibel.distanthorizons.core.logging.DhLogger;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class ChunkUpdateQueueManager
{
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public final ChunkPosQueue updateQueue;
public final ChunkPosQueue preUpdateQueue;
public final Set<DhChunkPos> ignoredChunkPosSet = Collections.newSetFromMap(new ConcurrentHashMap<>());
public int maxSize = 500;
private static long lastOverloadedLogMessageMsTime = 0;
//=============//
// constructor //
//=============//
public ChunkUpdateQueueManager()
{
this.updateQueue = new ChunkPosQueue();
this.preUpdateQueue = new ChunkPosQueue();
}
//==================//
// list/set methods //
//==================//
public boolean contains(DhChunkPos pos)
{
return this.updateQueue.contains(pos)
|| this.ignoredChunkPosSet.contains(pos)
|| this.preUpdateQueue.contains(pos);
}
public void clear()
{
this.updateQueue.clear();
this.preUpdateQueue.clear();
this.ignoredChunkPosSet.clear();
}
public int getQueuedCount() { return this.updateQueue.getQueuedCount() + this.preUpdateQueue.getQueuedCount(); }
public boolean isEmpty()
{
return this.updateQueue.isEmpty()
&& this.preUpdateQueue.isEmpty();
}
/**
* Adds an item to the pre-update queue of chunks that might need to be updated.
* If there are no more slots, replaces the item furthest from the center in the update queue.
*/
public void addItemToPreUpdateQueue(DhChunkPos pos, ChunkUpdateData updateData)
{
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)
{
int remainingSlots = this.maxSize - this.getQueuedCount();
// If no slots are left, get one by removing the item furthest from the center
if (remainingSlots <= 0)
{
this.updateQueue.popFurthest();
}
this.updateQueue.addItem(pos,updateData);
remainingSlots = this.maxSize - this.getQueuedCount();
if (remainingSlots <= 0)
{
this.sendOverloadMessage();
}
}
private void sendOverloadMessage()
{
// limit how often an overloaded message can be sent
long msBetweenLastLog = System.currentTimeMillis() - lastOverloadedLogMessageMsTime;
if (msBetweenLastLog >= SharedApi.MIN_MS_BETWEEN_OVERLOADED_LOG_MESSAGE)
{
lastOverloadedLogMessageMsTime = System.currentTimeMillis();
String message = "\u00A76" + "Distant Horizons overloaded, too many chunks queued for LOD processing. " + "\u00A7r" +
"\nThis may result in holes in your LODs. " +
"\nFix: move through the world slower, decrease your vanilla render distance, slow down your world pre-generator (IE Chunky), or increase the Distant Horizons' CPU thread counts. " +
"\nMax queue count [" + SharedApi.CHUNK_UPDATE_QUEUE_MANAGER.maxSize + "] ([" + SharedApi.MAX_UPDATING_CHUNK_COUNT_PER_THREAD_AND_PLAYER + "] per thread+players).";
boolean showWarningInChat = Config.Common.Logging.Warning.showUpdateQueueOverloadedChatWarning.get();
if (showWarningInChat)
{
ClientApi.INSTANCE.showChatMessageNextFrame(message);
}
// Don't log warnings in singleplayer or in hosted LAN since it usually isn't a problem (and if it is it's easy to notice).
// Servers should always log since being overloaded is harder to notice.
EWorldEnvironment environment = SharedApi.getEnvironment();
if (showWarningInChat || environment == EWorldEnvironment.SERVER_ONLY)
{
LOGGER.warn(message);
}
}
}
//=========//
// ignores //
//=========//
public void addPosToIgnore(DhChunkPos chunkPos) { this.ignoredChunkPosSet.add(chunkPos); }
public void removePosToIgnore(DhChunkPos chunkPos) { this.ignoredChunkPosSet.remove(chunkPos); }
//==================//
// position methods //
//==================//
public void setCenter(DhChunkPos newCenter)
{
this.updateQueue.setCenter(newCenter);
this.preUpdateQueue.setCenter(newCenter);
}
}
@@ -0,0 +1,72 @@
package com.seibel.distanthorizons.core.api.internal.rendering;
import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.util.math.Mat4f;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
/**
* Used to track the rendering state for the current frame.
*
* @see ClientApi
*/
public class DhRenderState
{
public Mat4f mcModelViewMatrix = null;
public Mat4f mcProjectionMatrix = null;
public float frameTime = -1;
public IClientLevelWrapper clientLevelWrapper = null;
//========//
// checks //
//========//
public String unableToRenderBecause()
{
String errorReasons = "";
// the matrix may be the identity matrix or and old/incorrect matrix
// but we did set it at least once before this
if (this.mcModelViewMatrix == null)
{
errorReasons += "no MVM Matrix, ";
}
if (this.mcProjectionMatrix == null)
{
errorReasons += "no Projection Matrix, ";
}
if (this.frameTime == -1)
{
errorReasons += "no Frame Time, ";
}
if (this.clientLevelWrapper == null)
{
errorReasons += "no Level Wrapper, ";
}
return errorReasons;
}
public boolean canRender()
{
// separated variable to allow for easy checking with the debugger
String errorReasons = this.unableToRenderBecause();
return errorReasons.isEmpty();
}
public void canRenderOrThrow() throws IllegalStateException
{
String errorReasons = this.unableToRenderBecause();
if (!errorReasons.isEmpty())
{
throw new IllegalStateException(errorReasons);
}
}
}
@@ -36,7 +36,7 @@ import com.seibel.distanthorizons.core.pos.DhSectionPos;
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 org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.lwjgl.util.tinyfd.TinyFileDialogs; import org.lwjgl.util.tinyfd.TinyFileDialogs;
import java.awt.*; import java.awt.*;
@@ -45,19 +45,16 @@ import java.util.*;
import java.util.List; import java.util.List;
/** /**
* This handles any configuration the user has access to. <br><br> * This handles any configuration the user has access to.
*
* Note: <br>
* Only add simpler listeners here (IE listeners that only depend on 1 config entry).
* For listeners that depend on 2 or more config entries, add them before the config menu is opened.
* Otherwise, you will have issues where only some of the config entries will exist when your listener is created.
* *
* @author coolGi * @author coolGi
*
* @see ConfigHandler
*/ */
@SuppressWarnings("ConcatenationWithEmptyString") @SuppressWarnings("ConcatenationWithEmptyString")
public class Config public class Config
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public static ConfigCategory client = new ConfigCategory.Builder().set(Client.class).build(); public static ConfigCategory client = new ConfigCategory.Builder().set(Client.class).build();
@@ -106,10 +103,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(true)
.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);
@@ -131,6 +125,8 @@ public class Config
// 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 // TODO determine their destination programically instead of hard coding the value
public static ConfigUIComment advancedHeader = new ConfigUIComment.Builder().setParentConfigClass(Advanced.class).build();
public static ConfigCategory graphics = new ConfigCategory.Builder().set(Graphics.class).build(); public static ConfigCategory graphics = new ConfigCategory.Builder().set(Graphics.class).build();
public static ConfigCategory worldGenerator = new ConfigCategory.Builder().set(Common.WorldGenerator.class).setDestination("common.worldGenerator").build(); public static ConfigCategory worldGenerator = new ConfigCategory.Builder().set(Common.WorldGenerator.class).setDestination("common.worldGenerator").build();
public static ConfigCategory multiplayer = new ConfigCategory.Builder().set(Multiplayer.class).build(); public static ConfigCategory multiplayer = new ConfigCategory.Builder().set(Multiplayer.class).build();
@@ -146,23 +142,32 @@ public class Config
public static class Graphics public static class Graphics
{ {
public static ConfigUIComment advancedGraphicsHeader = new ConfigUIComment.Builder().setParentConfigClass(Graphics.class).build();
public static ConfigCategory quality = new ConfigCategory.Builder().set(Quality.class).build(); public static ConfigCategory quality = new ConfigCategory.Builder().set(Quality.class).build();
public static ConfigUISpacer qualitySpacer = new ConfigUISpacer.Builder().build();
public static ConfigUiLinkedEntry quickEnableSsao = new ConfigUiLinkedEntry(Ssao.enableSsao); public static ConfigUiLinkedEntry quickEnableSsao = new ConfigUiLinkedEntry(Ssao.enableSsao);
public static ConfigCategory ssao = new ConfigCategory.Builder().set(Ssao.class).build(); public static ConfigCategory ssao = new ConfigCategory.Builder().set(Ssao.class).build();
public static ConfigUISpacer ssaoSpacer = new ConfigUISpacer.Builder().build();
public static ConfigUiLinkedEntry quickEnableGenericRendering = new ConfigUiLinkedEntry(GenericRendering.enableGenericRendering); public static ConfigUiLinkedEntry quickEnableGenericRendering = new ConfigUiLinkedEntry(GenericRendering.enableGenericRendering);
public static ConfigCategory genericRendering = new ConfigCategory.Builder().set(GenericRendering.class).build(); public static ConfigCategory genericRendering = new ConfigCategory.Builder().set(GenericRendering.class).build();
public static ConfigUISpacer genericRenderingSpacer = new ConfigUISpacer.Builder().build();
public static ConfigUiLinkedEntry quickEnableDhFog = new ConfigUiLinkedEntry(Fog.enableDhFog); public static ConfigUiLinkedEntry quickEnableDhFog = new ConfigUiLinkedEntry(Fog.enableDhFog);
public static ConfigUiLinkedEntry quickEnableMcFog = new ConfigUiLinkedEntry(Fog.enableVanillaFog); public static ConfigUiLinkedEntry quickEnableMcFog = new ConfigUiLinkedEntry(Fog.enableVanillaFog);
public static ConfigCategory fog = new ConfigCategory.Builder().set(Fog.class).build(); public static ConfigCategory fog = new ConfigCategory.Builder().set(Fog.class).build();
public static ConfigUISpacer fogSpacer = new ConfigUISpacer.Builder().build();
public static ConfigUiLinkedEntry quickEnableNoiseTexture = new ConfigUiLinkedEntry(NoiseTexture.enableNoiseTexture); public static ConfigUiLinkedEntry quickEnableNoiseTexture = new ConfigUiLinkedEntry(NoiseTexture.enableNoiseTexture);
public static ConfigCategory noiseTexture = new ConfigCategory.Builder().set(NoiseTexture.class).build(); public static ConfigCategory noiseTexture = new ConfigCategory.Builder().set(NoiseTexture.class).build();
public static ConfigUISpacer noiseTextureSpacer = new ConfigUISpacer.Builder().build();
public static ConfigUiLinkedEntry quickEnableCaveCulling = new ConfigUiLinkedEntry(Culling.enableCaveCulling); public static ConfigUiLinkedEntry quickEnableCaveCulling = new ConfigUiLinkedEntry(Culling.enableCaveCulling);
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 ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build(); public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build();
@@ -170,12 +175,13 @@ public class Config
public static class Quality public static class Quality
{ {
public static ConfigUIComment qualityHeader = new ConfigUIComment.Builder().setParentConfigClass(Quality.class).build();
public static ConfigEntry<Integer> lodChunkRenderDistanceRadius = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> lodChunkRenderDistanceRadius = new ConfigEntry.Builder<Integer>()
.setMinDefaultMax(32, 256, 4096) .setMinDefaultMax(32, 256, 4096)
.comment("" + .comment("" +
"The radius of the mod's render distance. (measured in chunks)\n" + "The radius of the mod's render distance. (measured in chunks)\n" +
"") "")
.setPerformance(EConfigEntryPerformance.HIGH)
.build(); .build();
public static ConfigEntry<EDhApiHorizontalQuality> horizontalQuality = new ConfigEntry.Builder<EDhApiHorizontalQuality>() public static ConfigEntry<EDhApiHorizontalQuality> horizontalQuality = new ConfigEntry.Builder<EDhApiHorizontalQuality>()
@@ -184,7 +190,7 @@ public class Config
+ "This indicates how quickly LODs decrease in quality the further away they are. \n" + "This indicates how quickly LODs decrease in quality the further away they are. \n"
+ "Higher settings will render higher quality fake chunks farther away, \n" + "Higher settings will render higher quality fake chunks farther away, \n"
+ "but will increase memory and GPU usage.") + "but will increase memory and GPU usage.")
.setPerformance(EConfigEntryPerformance.MEDIUM) .addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build(); .build();
public static ConfigEntry<EDhApiMaxHorizontalResolution> maxHorizontalResolution = new ConfigEntry.Builder<EDhApiMaxHorizontalResolution>() public static ConfigEntry<EDhApiMaxHorizontalResolution> maxHorizontalResolution = new ConfigEntry.Builder<EDhApiMaxHorizontalResolution>()
@@ -201,7 +207,7 @@ public class Config
+ "\n" + "\n"
+ "Lowest Quality: " + EDhApiMaxHorizontalResolution.CHUNK + "\n" + "Lowest Quality: " + EDhApiMaxHorizontalResolution.CHUNK + "\n"
+ "Highest Quality: " + EDhApiMaxHorizontalResolution.BLOCK) + "Highest Quality: " + EDhApiMaxHorizontalResolution.BLOCK)
.setPerformance(EConfigEntryPerformance.MEDIUM) .addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build(); .build();
public static ConfigEntry<EDhApiVerticalQuality> verticalQuality = new ConfigEntry.Builder<EDhApiVerticalQuality>() public static ConfigEntry<EDhApiVerticalQuality> verticalQuality = new ConfigEntry.Builder<EDhApiVerticalQuality>()
@@ -214,8 +220,7 @@ public class Config
+ "\n" + "\n"
+ "Lowest Quality: " + EDhApiVerticalQuality.HEIGHT_MAP + "\n" + "Lowest Quality: " + EDhApiVerticalQuality.HEIGHT_MAP + "\n"
+ "Highest Quality: " + EDhApiVerticalQuality.EXTREME) + "Highest Quality: " + EDhApiVerticalQuality.EXTREME)
.setPerformance(EConfigEntryPerformance.VERY_HIGH) .addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<EDhApiTransparency> transparency = new ConfigEntry.Builder<EDhApiTransparency>() public static ConfigEntry<EDhApiTransparency> transparency = new ConfigEntry.Builder<EDhApiTransparency>()
@@ -227,8 +232,7 @@ public class Config
+ EDhApiTransparency.FAKE + ": LODs will be opaque, but shaded to match the blocks underneath. \n" + EDhApiTransparency.FAKE + ": LODs will be opaque, but shaded to match the blocks underneath. \n"
+ EDhApiTransparency.DISABLED + ": LODs will be opaque. \n" + EDhApiTransparency.DISABLED + ": LODs will be opaque. \n"
+ "") + "")
.setPerformance(EConfigEntryPerformance.MEDIUM) .addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<EDhApiBlocksToAvoid> blocksToIgnore = new ConfigEntry.Builder<EDhApiBlocksToAvoid>() public static ConfigEntry<EDhApiBlocksToAvoid> blocksToIgnore = new ConfigEntry.Builder<EDhApiBlocksToAvoid>()
@@ -239,8 +243,7 @@ public class Config
+ EDhApiBlocksToAvoid.NONE + ": Represent all blocks in the LODs \n" + EDhApiBlocksToAvoid.NONE + ": Represent all blocks in the LODs \n"
+ EDhApiBlocksToAvoid.NON_COLLIDING + ": Only represent solid blocks in the LODs (tall grass, torches, etc. won't count for a LOD's height) \n" + EDhApiBlocksToAvoid.NON_COLLIDING + ": Only represent solid blocks in the LODs (tall grass, torches, etc. won't count for a LOD's height) \n"
+ "") + "")
.setPerformance(EConfigEntryPerformance.NONE) .addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<Boolean> tintWithAvoidedBlocks = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> tintWithAvoidedBlocks = new ConfigEntry.Builder<Boolean>()
@@ -251,8 +254,7 @@ public class Config
+ "True: a red flower will tint the grass below it red. \n" + "True: a red flower will tint the grass below it red. \n"
+ "False: skipped blocks will not change color of surface below them. " + "False: skipped blocks will not change color of surface below them. "
+ "") + "")
.setPerformance(EConfigEntryPerformance.NONE) .addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<Double> lodBias = new ConfigEntry.Builder<Double>() public static ConfigEntry<Double> lodBias = new ConfigEntry.Builder<Double>()
@@ -272,8 +274,7 @@ public class Config
+ " Can be used to force LOD shading when using some shaders. \n" + " Can be used to force LOD shading when using some shaders. \n"
+ EDhApiLodShading.DISABLED + ": All LOD sides will be rendered with the same brightness. \n" + EDhApiLodShading.DISABLED + ": All LOD sides will be rendered with the same brightness. \n"
+ "") + "")
.setPerformance(EConfigEntryPerformance.NONE) .addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<EDhApiGrassSideRendering> grassSideRendering = new ConfigEntry.Builder<EDhApiGrassSideRendering>() public static ConfigEntry<EDhApiGrassSideRendering> grassSideRendering = new ConfigEntry.Builder<EDhApiGrassSideRendering>()
@@ -285,8 +286,7 @@ public class Config
+ EDhApiGrassSideRendering.FADE_TO_DIRT + ": sides fade from grass to dirt. \n" + EDhApiGrassSideRendering.FADE_TO_DIRT + ": sides fade from grass to dirt. \n"
+ EDhApiGrassSideRendering.AS_DIRT + ": sides render entirely as dirt. \n" + EDhApiGrassSideRendering.AS_DIRT + ": sides render entirely as dirt. \n"
+ "") + "")
.setPerformance(EConfigEntryPerformance.NONE) .addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<Boolean> ditherDhFade = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> ditherDhFade = new ConfigEntry.Builder<Boolean>()
@@ -296,7 +296,6 @@ public class Config
+ "If false LODs will cut off abruptly at a set distance from the camera. \n" + "If false LODs will cut off abruptly at a set distance from the camera. \n"
+ "This setting is affected by the vanilla overdraw prevention config. \n" + "This setting is affected by the vanilla overdraw prevention config. \n"
+ "") + "")
.setPerformance(EConfigEntryPerformance.LOW)
.build(); .build();
public static ConfigEntry<EDhApiMcRenderingFadeMode> vanillaFadeMode = new ConfigEntry.Builder<EDhApiMcRenderingFadeMode>() public static ConfigEntry<EDhApiMcRenderingFadeMode> vanillaFadeMode = new ConfigEntry.Builder<EDhApiMcRenderingFadeMode>()
@@ -308,7 +307,14 @@ public class Config
+ EDhApiMcRenderingFadeMode.SINGLE_PASS + ": Fades after MC's transparent pass, opaque blocks underwater won't be faded. \n" + EDhApiMcRenderingFadeMode.SINGLE_PASS + ": Fades after MC's transparent pass, opaque blocks underwater won't be faded. \n"
+ EDhApiMcRenderingFadeMode.DOUBLE_PASS + ": Slowest, fades after both MC's opaque and transparent passes, provides the smoothest transition. \n" + EDhApiMcRenderingFadeMode.DOUBLE_PASS + ": Slowest, fades after both MC's opaque and transparent passes, provides the smoothest transition. \n"
+ "") + "")
.setPerformance(EConfigEntryPerformance.LOW) .build();
public static ConfigEntry<Boolean> dhFadeFarClipPlane = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "Should DH fade out before reaching the far clip plane? \n"
+ "This is helpful to prevent DH clouds from cutting off in the distance. \n"
+ "")
.build(); .build();
public static ConfigEntry<Double> brightnessMultiplier = new ConfigEntry.Builder<Double>() // TODO: Make this a float (the ClassicConfigGUI doesnt support floats) public static ConfigEntry<Double> brightnessMultiplier = new ConfigEntry.Builder<Double>() // TODO: Make this a float (the ClassicConfigGUI doesnt support floats)
@@ -319,7 +325,7 @@ public class Config
+ "0 = black \n" + "0 = black \n"
+ "1 = normal \n" + "1 = normal \n"
+ "2 = near white") + "2 = near white")
.addListener(ReloadLodsConfigEventHandler.INSTANCE) .addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build(); .build();
public static ConfigEntry<Double> saturationMultiplier = new ConfigEntry.Builder<Double>() // TODO: Make this a float (the ClassicConfigGUI doesnt support floats) public static ConfigEntry<Double> saturationMultiplier = new ConfigEntry.Builder<Double>() // TODO: Make this a float (the ClassicConfigGUI doesnt support floats)
@@ -330,30 +336,29 @@ public class Config
+ "0 = black and white \n" + "0 = black and white \n"
+ "1 = normal \n" + "1 = normal \n"
+ "2 = very saturated") + "2 = very saturated")
.addListener(ReloadLodsConfigEventHandler.INSTANCE) .addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build(); .build();
// TODO fixme public static ConfigEntry<Integer> lodBiomeBlending = new ConfigEntry.Builder<Integer>()
//public static ConfigEntry<Integer> lodBiomeBlending = new ConfigEntry.Builder<Integer>() .setMinDefaultMax(0,3,3) // going higher than 3 causes banding issues for blending across LOD borders and an exponential increase in load times
// .setMinDefaultMax(0,1,7) .comment(""
// .comment("" + "This is the same as vanilla Biome Blending settings for Lod area. \n"
// + "This is the same as vanilla Biome Blending settings for Lod area. \n" + " Note that anything other than '0' will greatly effect Lod building time. \n"
// + " Note that anything other than '0' will greatly effect Lod building time \n" + "\n"
// + " and increase triangle count. The cost on chunk generation speed is also \n" + " '0' equals to Vanilla Biome Blending of '1x1' or 'OFF', \n"
// + " quite large if set too high.\n" + " '1' equals to Vanilla Biome Blending of '3x3', \n"
// + "\n" + " '2' equals to Vanilla Biome Blending of '5x5'...")
// + " '0' equals to Vanilla Biome Blending of '1x1' or 'OFF', \n" .addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
// + " '1' equals to Vanilla Biome Blending of '3x3', \n" .build();
// + " '2' equals to Vanilla Biome Blending of '5x5'...")
// .build();
} }
public static class Ssao public static class Ssao
{ {
public static ConfigUIComment ssaoHeader = new ConfigUIComment.Builder().setParentConfigClass(Ssao.class).build();
public static ConfigEntry<Boolean> enableSsao = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableSsao = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.comment("Enable Screen Space Ambient Occlusion") .comment("Enable Screen Space Ambient Occlusion")
.setPerformance(EConfigEntryPerformance.MEDIUM)
.build(); .build();
public static ConfigEntry<Integer> sampleCount = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> sampleCount = new ConfigEntry.Builder<Integer>()
@@ -362,7 +367,6 @@ public class Config
"Determines how many points in space are sampled for the occlusion test. \n" + "Determines how many points in space are sampled for the occlusion test. \n" +
"Higher numbers will improve quality and reduce banding, but will increase GPU load." + "Higher numbers will improve quality and reduce banding, but will increase GPU load." +
"") "")
.setPerformance(EConfigEntryPerformance.MEDIUM)
.build(); .build();
public static ConfigEntry<Double> radius = new ConfigEntry.Builder<Double>() public static ConfigEntry<Double> radius = new ConfigEntry.Builder<Double>()
@@ -370,7 +374,6 @@ public class Config
.comment("" + .comment("" +
"Determines the radius Screen Space Ambient Occlusion is applied, measured in blocks." + "Determines the radius Screen Space Ambient Occlusion is applied, measured in blocks." +
"") "")
.setPerformance(EConfigEntryPerformance.NONE)
.build(); .build();
public static ConfigEntry<Double> strength = new ConfigEntry.Builder<Double>() public static ConfigEntry<Double> strength = new ConfigEntry.Builder<Double>()
@@ -378,7 +381,6 @@ public class Config
.comment("" + .comment("" +
"Determines how dark the Screen Space Ambient Occlusion effect will be." + "Determines how dark the Screen Space Ambient Occlusion effect will be." +
"") "")
.setPerformance(EConfigEntryPerformance.NONE)
.build(); .build();
public static ConfigEntry<Double> bias = new ConfigEntry.Builder<Double>() public static ConfigEntry<Double> bias = new ConfigEntry.Builder<Double>()
@@ -386,7 +388,6 @@ public class Config
.comment("" + .comment("" +
"Increasing the value can reduce banding at the cost of reducing the strength of the effect." + "Increasing the value can reduce banding at the cost of reducing the strength of the effect." +
"") "")
.setPerformance(EConfigEntryPerformance.NONE)
.build(); .build();
public static ConfigEntry<Double> minLight = new ConfigEntry.Builder<Double>() public static ConfigEntry<Double> minLight = new ConfigEntry.Builder<Double>()
@@ -396,7 +397,6 @@ public class Config
"0 = totally black at the corners \n" + "0 = totally black at the corners \n" +
"1 = no shadow" + "1 = no shadow" +
"") "")
.setPerformance(EConfigEntryPerformance.NONE)
.build(); .build();
public static ConfigEntry<Integer> blurRadius = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> blurRadius = new ConfigEntry.Builder<Integer>()
@@ -405,13 +405,14 @@ public class Config
"The radius, measured in pixels, that blurring is calculated for the SSAO. \n" + "The radius, measured in pixels, that blurring is calculated for the SSAO. \n" +
"Higher numbers will reduce banding at the cost of GPU performance." + "Higher numbers will reduce banding at the cost of GPU performance." +
"") "")
.setPerformance(EConfigEntryPerformance.HIGH)
.build(); .build();
} }
public static class GenericRendering public static class GenericRendering
{ {
public static ConfigUIComment genericRendererHeader = new ConfigUIComment.Builder().setParentConfigClass(GenericRendering.class).build();
public static ConfigEntry<Boolean> enableGenericRendering = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableGenericRendering = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.comment("" .comment(""
@@ -459,12 +460,13 @@ public class Config
public static ConfigUIComment fogHeader = new ConfigUIComment.Builder().setParentConfigClass(Fog.class).build();
public static ConfigEntry<Boolean> enableDhFog = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableDhFog = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.comment("" .comment(""
+ "Determines if fog is drawn on DH LODs. \n" + "Determines if fog is drawn on DH LODs. \n"
+ "") + "")
.setPerformance(EConfigEntryPerformance.MEDIUM)
.build(); .build();
public static ConfigEntry<EDhApiFogColorMode> colorMode = new ConfigEntry.Builder<EDhApiFogColorMode>() public static ConfigEntry<EDhApiFogColorMode> colorMode = new ConfigEntry.Builder<EDhApiFogColorMode>()
@@ -474,7 +476,6 @@ public class Config
+ "\n" + "\n"
+ EDhApiFogColorMode.USE_WORLD_FOG_COLOR + ": Use the world's fog color. \n" + EDhApiFogColorMode.USE_WORLD_FOG_COLOR + ": Use the world's fog color. \n"
+ EDhApiFogColorMode.USE_SKY_COLOR + ": Use the sky's color.") + EDhApiFogColorMode.USE_SKY_COLOR + ": Use the sky's color.")
.setPerformance(EConfigEntryPerformance.NONE)
.build(); .build();
public static ConfigEntry<Boolean> enableVanillaFog = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableVanillaFog = new ConfigEntry.Builder<Boolean>()
@@ -560,6 +561,8 @@ public class Config
public static class HeightFog public static class HeightFog
{ {
public static ConfigUIComment heightFogHeader = new ConfigUIComment.Builder().setParentConfigClass(HeightFog.class).build();
public static ConfigEntry<EDhApiHeightFogMixMode> heightFogMixMode = new ConfigEntry.Builder<EDhApiHeightFogMixMode>() public static ConfigEntry<EDhApiHeightFogMixMode> heightFogMixMode = new ConfigEntry.Builder<EDhApiHeightFogMixMode>()
.set(EDhApiHeightFogMixMode.SPHERICAL) .set(EDhApiHeightFogMixMode.SPHERICAL)
.comment("" .comment(""
@@ -655,6 +658,8 @@ public class Config
public static class NoiseTexture public static class NoiseTexture
{ {
public static ConfigUIComment noiseTextureHeader = new ConfigUIComment.Builder().setParentConfigClass(NoiseTexture.class).build();
public static ConfigEntry<Boolean> enableNoiseTexture = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableNoiseTexture = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.comment("" .comment(""
@@ -689,8 +694,10 @@ public class Config
public static class Culling public static class Culling
{ {
public static ConfigUIComment cullingHeader = new ConfigUIComment.Builder().setParentConfigClass(Culling.class).build();
public static ConfigEntry<Double> overdrawPrevention = new ConfigEntry.Builder<Double>() public static ConfigEntry<Double> overdrawPrevention = new ConfigEntry.Builder<Double>()
.setMinDefaultMax(0.0, 0.0, 1.0) .setMinDefaultMax(0.0, 0.0, 1.0) // TODO change -1 to auto
.comment("" .comment(""
+ "Determines how far from the camera Distant Horizons will start rendering. \n" + "Determines how far from the camera Distant Horizons will start rendering. \n"
+ "Measured as a percentage of the vanilla render distance.\n" + "Measured as a percentage of the vanilla render distance.\n"
@@ -703,12 +710,10 @@ public class Config
+ "\n" + "\n"
+ "Increasing the vanilla render distance increases the effectiveness of this setting." + "Increasing the vanilla render distance increases the effectiveness of this setting."
+ "") + "")
.setPerformance(EConfigEntryPerformance.NONE)
.build(); .build();
public static ConfigEntry<Boolean> enableCaveCulling = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableCaveCulling = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.setPerformance(EConfigEntryPerformance.HIGH)
.comment("" .comment(""
+ "If enabled caves won't be rendered. \n" + "If enabled caves won't be rendered. \n"
+ "\n" + "\n"
@@ -717,7 +722,7 @@ public class Config
+ " Tweaking the caveCullingHeight, can resolve some \n" + " Tweaking the caveCullingHeight, can resolve some \n"
+ " of those issues. \n" + " of those issues. \n"
+ "") + "")
.addListener(ReloadLodsConfigEventHandler.INSTANCE) .addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build(); .build();
public static ConfigEntry<Integer> caveCullingHeight = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> caveCullingHeight = new ConfigEntry.Builder<Integer>()
@@ -725,7 +730,7 @@ public class Config
.comment("" .comment(""
+ "At what Y value should cave culling start? \n" + "At what Y value should cave culling start? \n"
+ "Lower this value if you get walls for areas with 0 light.") + "Lower this value if you get walls for areas with 0 light.")
.addListener(ReloadLodsConfigEventHandler.INSTANCE) .addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build(); .build();
public static ConfigEntry<Boolean> disableBeaconDistanceCulling = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> disableBeaconDistanceCulling = new ConfigEntry.Builder<Boolean>()
@@ -813,6 +818,8 @@ public class Config
public static class Experimental public static class Experimental
{ {
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(0, 0, 5000)
.comment("" .comment(""
@@ -829,12 +836,27 @@ public class Config
+ "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
@Deprecated
public static ConfigEntry<Boolean> onlyLoadCenterLods = new ConfigEntry.Builder<Boolean>()
.set(false)
.comment(""
+ "For internal testing:\n"
+ "Skips loading adjacent LODs to significantly reduce load times (~5x)\n"
+ "but causes lighting on LOD borders to appear as full-bright\n"
+ "and other graphical bugs.\n"
+ "")
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build();
} }
} }
public static class AutoUpdater public static class AutoUpdater
{ {
public static ConfigUIComment autoUpdaterHeader = new ConfigUIComment.Builder().setParentConfigClass(AutoUpdater.class).build();
public static ConfigEntry<Boolean> enableAutoUpdater = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableAutoUpdater = new ConfigEntry.Builder<Boolean>()
.set(!isRunningInDevEnvironment()) .set(!isRunningInDevEnvironment())
.comment("" .comment(""
@@ -862,6 +884,8 @@ public class Config
public static class Multiplayer public static class Multiplayer
{ {
public static ConfigUIComment multiplayerHeader = new ConfigUIComment.Builder().setParentConfigClass(Multiplayer.class).build();
public static ConfigEntry<EDhApiServerFolderNameMode> serverFolderNameMode = new ConfigEntry.Builder<EDhApiServerFolderNameMode>() public static ConfigEntry<EDhApiServerFolderNameMode> serverFolderNameMode = new ConfigEntry.Builder<EDhApiServerFolderNameMode>()
.set(EDhApiServerFolderNameMode.NAME_ONLY) .set(EDhApiServerFolderNameMode.NAME_ONLY)
.comment("" .comment(""
@@ -878,6 +902,8 @@ public class Config
public static class Debugging public static class Debugging
{ {
public static ConfigUIComment debuggingHeader = new ConfigUIComment.Builder().setParentConfigClass(Debugging.class).build();
public static ConfigEntry<EDhApiRendererMode> rendererMode = new ConfigEntry.Builder<EDhApiRendererMode>() public static ConfigEntry<EDhApiRendererMode> rendererMode = new ConfigEntry.Builder<EDhApiRendererMode>()
.set(EDhApiRendererMode.DEFAULT) .set(EDhApiRendererMode.DEFAULT)
.comment("" .comment(""
@@ -898,6 +924,7 @@ public class Config
+ EDhApiDebugRendering.SHOW_BLOCK_MATERIAL + ": LODs' color will be based on their material. \n" + EDhApiDebugRendering.SHOW_BLOCK_MATERIAL + ": LODs' color will be based on their material. \n"
+ EDhApiDebugRendering.SHOW_OVERLAPPING_QUADS + ": LODs will be drawn with total white, but overlapping quads will be drawn with red. \n" + EDhApiDebugRendering.SHOW_OVERLAPPING_QUADS + ": LODs will be drawn with total white, but overlapping quads will be drawn with red. \n"
+ "") + "")
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build(); .build();
public static ConfigEntry<Boolean> lodOnlyMode = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> lodOnlyMode = new ConfigEntry.Builder<Boolean>()
@@ -965,6 +992,8 @@ public class Config
public static class DebugWireframe public static class DebugWireframe
{ {
public static ConfigUIComment debugWireframeHeader = new ConfigUIComment.Builder().setParentConfigClass(DebugWireframe.class).build();
public static ConfigEntry<Boolean> enableRendering = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableRendering = new ConfigEntry.Builder<Boolean>()
.set(false) .set(false)
.comment("" .comment(""
@@ -1014,6 +1043,8 @@ public class Config
public static class OpenGl public static class OpenGl
{ {
public static ConfigUIComment openGlHeader = new ConfigUIComment.Builder().setParentConfigClass(OpenGl.class).build();
public static ConfigEntry<Boolean> overrideVanillaGLLogger = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> overrideVanillaGLLogger = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.comment("" .comment(""
@@ -1049,11 +1080,10 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Boolean> validateBufferIdsBeforeRendering = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<EDhApiGpuUploadMethod> glUploadMode = new ConfigEntry.Builder<EDhApiGpuUploadMethod>()
.set(false) .set(EDhApiGpuUploadMethod.AUTO)
.comment("" .comment(""
+ "Massively reduces FPS. \n" + "\n"
+ "Should only be used if mysterious EXCEPTION_ACCESS_VIOLATION crashes are happening in DH's rendering code for troubleshooting. \n"
+ "") + "")
.build(); .build();
@@ -1061,50 +1091,54 @@ public class Config
public static class ColumnBuilderDebugging public static class ColumnBuilderDebugging
{ {
public static ConfigUIComment columnBuilderDebuggingHeader = new ConfigUIComment.Builder().setParentConfigClass(ColumnBuilderDebugging.class).build();
public static ConfigEntry<Boolean> columnBuilderDebugEnable = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> columnBuilderDebugEnable = new ConfigEntry.Builder<Boolean>()
.set(false) .set(false)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI) .setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE) .addListener(ReloadLodsConfigEventHandler.INSTANT_INSTANCE)
.build(); .build();
public static ConfigEntry<Integer> columnBuilderDebugDetailLevel = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> columnBuilderDebugDetailLevel = new ConfigEntry.Builder<Integer>()
.set((int) DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL) .set((int) DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI) .setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE) .addListener(ReloadLodsConfigEventHandler.INSTANT_INSTANCE)
.build(); .build();
public static ConfigEntry<Integer> columnBuilderDebugXPos = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> columnBuilderDebugXPos = new ConfigEntry.Builder<Integer>()
.set(0) .set(0)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI) .setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE) .addListener(ReloadLodsConfigEventHandler.INSTANT_INSTANCE)
.build(); .build();
public static ConfigEntry<Integer> columnBuilderDebugZPos = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> columnBuilderDebugZPos = new ConfigEntry.Builder<Integer>()
.set(0) .set(0)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI) .setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE) .addListener(ReloadLodsConfigEventHandler.INSTANT_INSTANCE)
.build(); .build();
public static ConfigEntry<Integer> columnBuilderDebugXRow = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> columnBuilderDebugXRow = new ConfigEntry.Builder<Integer>()
.set(-1) .set(-1)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI) .setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE) .addListener(ReloadLodsConfigEventHandler.INSTANT_INSTANCE)
.build(); .build();
public static ConfigEntry<Integer> columnBuilderDebugZRow = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> columnBuilderDebugZRow = new ConfigEntry.Builder<Integer>()
.set(-1) .set(-1)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI) .setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE) .addListener(ReloadLodsConfigEventHandler.INSTANT_INSTANCE)
.build(); .build();
public static ConfigEntry<Integer> columnBuilderDebugColumnIndex = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> columnBuilderDebugColumnIndex = new ConfigEntry.Builder<Integer>()
.set(-1) .set(-1)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI) .setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE) .addListener(ReloadLodsConfigEventHandler.INSTANT_INSTANCE)
.build(); .build();
} }
public static class F3Screen public static class F3Screen
{ {
public static ConfigUIComment f3ScreenHeader = new ConfigUIComment.Builder().setParentConfigClass(F3Screen.class).build();
public static ConfigEntry<Boolean> showPlayerPos = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> showPlayerPos = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.comment("Shows info about each thread pool.") .comment("Shows the player's LOD position.")
.build(); .build();
public static ConfigEntry<Integer> playerPosSectionDetailLevel = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> playerPosSectionDetailLevel = new ConfigEntry.Builder<Integer>()
.setMinDefaultMax(6, 6, 16) .setMinDefaultMax(6, 6, 16)
@@ -1145,8 +1179,10 @@ public class Config
// This will throw a warning when opened in the default ui to tell you about it not showing // This will throw a warning when opened in the default ui to tell you about it not showing
public static class ExampleConfigScreen public static class ExampleConfigScreen
{ {
public static ConfigUIComment exampleConfigHeader = new ConfigUIComment.Builder().setParentConfigClass(ExampleConfigScreen.class).build();
// Defined in the lang, just a note about this screen // Defined in the lang, just a note about this screen
public static ConfigUIComment debugConfigScreenNote = new ConfigUIComment(); public static ConfigUIComment debugConfigScreenNote = new ConfigUIComment.Builder().setTextPosition(EConfigCommentTextPosition.CENTER_OF_SCREEN).build();
public static ConfigEntry<Boolean> boolTest = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> boolTest = new ConfigEntry.Builder<Boolean>()
.set(false) .set(false)
@@ -1154,6 +1190,7 @@ public class Config
public static ConfigEntry<Byte> byteTest = new ConfigEntry.Builder<Byte>() public static ConfigEntry<Byte> byteTest = new ConfigEntry.Builder<Byte>()
.set((byte) 8) .set((byte) 8)
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // no GUI renderer set up currently
.build(); .build();
public static ConfigEntry<Integer> intTest = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> intTest = new ConfigEntry.Builder<Integer>()
@@ -1166,14 +1203,17 @@ public class Config
public static ConfigEntry<Short> shortTest = new ConfigEntry.Builder<Short>() public static ConfigEntry<Short> shortTest = new ConfigEntry.Builder<Short>()
.set((short) 69) .set((short) 69)
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // no GUI renderer set up currently
.build(); .build();
public static ConfigEntry<Long> longTest = new ConfigEntry.Builder<Long>() public static ConfigEntry<Long> longTest = new ConfigEntry.Builder<Long>()
.set(42069L) .set(42069L)
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // no GUI renderer set up currently
.build(); .build();
public static ConfigEntry<Float> floatTest = new ConfigEntry.Builder<Float>() public static ConfigEntry<Float> floatTest = new ConfigEntry.Builder<Float>()
.set(0.42069f) .set(0.42069f)
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // no GUI renderer set up currently
.build(); .build();
public static ConfigEntry<String> stringTest = new ConfigEntry.Builder<String>() public static ConfigEntry<String> stringTest = new ConfigEntry.Builder<String>()
@@ -1182,10 +1222,12 @@ public class Config
public static ConfigEntry<List<String>> listTest = new ConfigEntry.Builder<List<String>>() public static ConfigEntry<List<String>> listTest = new ConfigEntry.Builder<List<String>>()
.set(new ArrayList<String>(Arrays.asList("option 1", "option 2", "option 3"))) .set(new ArrayList<String>(Arrays.asList("option 1", "option 2", "option 3")))
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // no GUI renderer set up currently
.build(); .build();
public static ConfigEntry<Map<String, String>> mapTest = new ConfigEntry.Builder<Map<String, String>>() public static ConfigEntry<Map<String, String>> mapTest = new ConfigEntry.Builder<Map<String, String>>()
.set(new HashMap<String, String>()) .set(new HashMap<String, String>())
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // no GUI renderer set up currently
.build(); .build();
public static ConfigUIButton uiButtonTest = new ConfigUIButton(() -> public static ConfigUIButton uiButtonTest = new ConfigUIButton(() ->
@@ -1229,6 +1271,8 @@ public class Config
{ {
public static class WorldGenerator public static class WorldGenerator
{ {
public static ConfigUIComment worldGeneratorHeader = new ConfigUIComment.Builder().setParentConfigClass(WorldGenerator.class).build();
public static ConfigEntry<Boolean> enableDistantGeneration = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableDistantGeneration = new ConfigEntry.Builder<Boolean>()
.setChatCommandName("generation.enable") .setChatCommandName("generation.enable")
.set(true) .set(true)
@@ -1282,7 +1326,7 @@ public class Config
.build(); .build();
public static ConfigEntry<EDhApiDistantGeneratorProgressDisplayLocation> showGenerationProgress = new ConfigEntry.Builder<EDhApiDistantGeneratorProgressDisplayLocation>() public static ConfigEntry<EDhApiDistantGeneratorProgressDisplayLocation> showGenerationProgress = new ConfigEntry.Builder<EDhApiDistantGeneratorProgressDisplayLocation>()
.set(EDhApiDistantGeneratorProgressDisplayLocation.OVERLAY) .set(EDhApiDistantGeneratorProgressDisplayLocation.DISABLED)
.comment("" .comment(""
+ "How should distant generator progress be displayed? \n" + "How should distant generator progress be displayed? \n"
+ "\n" + "\n"
@@ -1309,10 +1353,21 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Boolean> generationProgressIncludeChunksPerSecond = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "When logging generation progress also include the rate at which chunks \n"
+ "are being generated. \n"
+ "This can be useful for troubleshooting performance. \n"
+ "")
.build();
} }
public static class LodBuilding public static class LodBuilding
{ {
public static ConfigUIComment lodBuildingHeader = new ConfigUIComment.Builder().setParentConfigClass(LodBuilding.class).build();
public static ConfigEntry<Boolean> disableUnchangedChunkCheck = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> disableUnchangedChunkCheck = new ConfigEntry.Builder<Boolean>()
.set(false) .set(false)
// enabling this can be quite detrimental to performance, // enabling this can be quite detrimental to performance,
@@ -1332,31 +1387,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.LZMA2) .set(EDhApiDataCompressionMode.Z_STD)
.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: 1.64 milliseconds\n"
+ "Estimated average DTO write speed: 12.44 milliseconds\n"
+ "\n"
+ EDhApiDataCompressionMode.LZ4 + " \n"
+ "A good option if you're CPU limited and have plenty of hard drive space.\n"
+ "Expected Compression Ratio: 0.36\n"
+ "Estimated average DTO read speed: 1.85 ms\n"
+ "Estimated average DTO write speed: 9.46 ms\n"
+ "\n"
+ EDhApiDataCompressionMode.LZMA2 + " \n"
+ "Slow but very good compression.\n"
+ "Expected Compression Ratio: 0.14\n"
+ "Estimated average DTO read speed: 11.89 ms\n"
+ "Estimated average DTO write speed: 192.01 ms\n"
+ "")
.build(); .build();
public static ConfigEntry<EDhApiWorldCompressionMode> worldCompression = new ConfigEntry.Builder<EDhApiWorldCompressionMode>() public static ConfigEntry<EDhApiWorldCompressionMode> worldCompression = new ConfigEntry.Builder<EDhApiWorldCompressionMode>()
@@ -1379,55 +1412,14 @@ 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();
public static class Experimental public static class Experimental
{ {
public static ConfigUIComment experimentalHeader = new ConfigUIComment.Builder().setParentConfigClass(Experimental.class).build();
public static ConfigEntry<Boolean> upsampleLowerDetailLodsToFillHoles = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> upsampleLowerDetailLodsToFillHoles = new ConfigEntry.Builder<Boolean>()
.set(false) .set(false)
.comment("" .comment(""
@@ -1450,6 +1442,8 @@ public class Config
public static class MultiThreading public static class MultiThreading
{ {
public static ConfigUIComment multiThreadingHeader = new ConfigUIComment.Builder().setParentConfigClass(MultiThreading.class).build();
public static final ConfigEntry<Integer> numberOfThreads = new ConfigEntry.Builder<Integer>() public static final ConfigEntry<Integer> numberOfThreads = new ConfigEntry.Builder<Integer>()
.setChatCommandName("threading.numberOfThreads") .setChatCommandName("threading.numberOfThreads")
.setMinDefaultMax(1, .setMinDefaultMax(1,
@@ -1459,6 +1453,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)
@@ -1472,55 +1467,87 @@ 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();
} }
public static class Logging public static class Logging
{ {
// TODO add change all option public static ConfigUIComment loggingHeader = new ConfigUIComment.Builder().setParentConfigClass(Logging.class).build();
// TODO default to error chat and info file
public static ConfigEntry<EDhApiLoggerMode> logWorldGenEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
public static ConfigEntry<EDhApiLoggerLevel> globalFileMaxLevel = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.setChatCommandName("logging.globalFileMaxLevel")
.set(EDhApiLoggerLevel.INFO)
.comment(""
+ ""
+ "")
.build();
public static ConfigEntry<EDhApiLoggerLevel> globalChatMaxLevel = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.setChatCommandName("logging.globalChatMaxLevel")
.set(EDhApiLoggerLevel.ERROR)
.comment(""
+ ""
+ "")
.build();
public static ConfigUISpacer globalLoggingSpacer = new ConfigUISpacer.Builder().build();
public static ConfigEntry<EDhApiLoggerLevel> logWorldGenEventToFile = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.setChatCommandName("logging.logWorldGenEvent") .setChatCommandName("logging.logWorldGenEvent")
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerLevel.INFO)
.comment("" .comment(""
+ "If enabled, the mod will log information about the world generation process. \n" + "If enabled, the mod will log information about the world generation process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logWorldGenPerformance = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerLevel> logWorldGenChunkLoadEventToFile = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.setChatCommandName("logging.logWorldGenPerformance")
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE)
.comment(""
+ "If enabled, the mod will log performance about the world generation process. \n"
+ "This can be useful for debugging.")
.build();
public static ConfigEntry<EDhApiLoggerMode> logWorldGenLoadEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setChatCommandName("logging.logWorldGenLoadEvent") .setChatCommandName("logging.logWorldGenLoadEvent")
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerLevel.INFO)
.comment("" .comment(""
+ "If enabled, the mod will log information about the world generation process. \n" + "If enabled, the mod will log information about the world generation process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logRendererBufferEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerLevel> logRendererEventToFile = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerLevel.INFO)
.comment("" .comment(""
+ "If enabled, the mod will log information about the renderer buffer process. \n" + "If enabled, the mod will log information about the renderer setup, cleanup, and any issues it may encounter. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logRendererGLEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerLevel> logRendererGLEventToFile = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerLevel.INFO)
.comment("" .comment(""
+ "If enabled, the mod will log information about the renderer OpenGL process. \n" + "If enabled, the mod will log information about the renderer OpenGL process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logNetworkEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerLevel> logRendererGLEventToChat = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.set(EDhApiLoggerLevel.ERROR)
.comment(""
+ "If enabled, the mod will log information about the renderer OpenGL process. \n"
+ "This can be useful for debugging.")
.build();
public static ConfigEntry<EDhApiLoggerLevel> logNetworkEventToFile = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.setChatCommandName("logging.logNetworkEvent") .setChatCommandName("logging.logNetworkEvent")
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerLevel.INFO)
.comment("" .comment(""
+ "If enabled, the mod will log information about network operations. \n" + "If enabled, the mod will log information about network operations. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
@@ -1532,6 +1559,7 @@ public class Config
public static class Warning public static class Warning
{ {
public static ConfigUIComment warningHeader = new ConfigUIComment.Builder().setParentConfigClass(Warning.class).build();
public static ConfigEntry<Boolean> showLowMemoryWarningOnStartup = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> showLowMemoryWarningOnStartup = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
@@ -1573,6 +1601,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(""
@@ -1599,6 +1635,28 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Integer> serverId = new ConfigEntry.Builder<Integer>()
.set(new Random().nextInt())
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.comment(""
+ "DO NOT CHANGE UNLESS YOU KNOW WHAT YOU'RE DOING.\n"
+ "Autogenerated ID used to prevent multiple independent servers from accidentally\n"
+ "writing over each other's LODs when the same serverKey is set on both.\n"
+ "")
.build();
public static ConfigEntry<String> serverKey = new ConfigEntry.Builder<String>()
.setChatCommandName("levelKeys.serverKey")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.set("")
.comment(""
+ "Custom server key used which can be used to always reuse the same LOD data folder,\n"
+ "for cases when the server doesn't have a static IP for some reason.\n"
+ "If this value is empty, the client itself decides which folder name to use.\n"
+ "Requires rejoining the server to apply after changing.\n"
+ "")
.build();
public static ConfigEntry<String> levelKeyPrefix = new ConfigEntry.Builder<String>() public static ConfigEntry<String> levelKeyPrefix = new ConfigEntry.Builder<String>()
.setChatCommandName("levelKeys.prefix") .setChatCommandName("levelKeys.prefix")
.set("") .set("")
@@ -1626,7 +1684,6 @@ public class Config
.comment("" + .comment("" +
"Defines the distance allowed to generate around the player." + "Defines the distance allowed to generate around the player." +
"") "")
.setPerformance(EConfigEntryPerformance.HIGH)
.build(); .build();
public static ConfigEntry<Integer> generationBoundsX = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> generationBoundsX = new ConfigEntry.Builder<Integer>()
@@ -1671,7 +1728,6 @@ public class Config
.comment("" + .comment("" +
"Defines the distance the player will receive updates around." + "Defines the distance the player will receive updates around." +
"") "")
.setPerformance(EConfigEntryPerformance.HIGH)
.build(); .build();
@@ -1700,19 +1756,35 @@ public class Config
"Defines the distance allowed to be synchronized around the player. \n" + "Defines the distance allowed to be synchronized around the player. \n" +
"Should be the same or larger than maxGenerationRequestDistance in most cases." + "Should be the same or larger than maxGenerationRequestDistance in most cases." +
"") "")
.setPerformance(EConfigEntryPerformance.HIGH)
.build(); .build();
// Common // Common
public static ConfigEntry<Integer> maxDataTransferSpeed = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> playerBandwidthLimit = new ConfigEntry.Builder<Integer>()
.setChatCommandName("common.maxDataTransferSpeed") .setChatCommandName("common.playerBandwidthLimit")
.setMinDefaultMax(0, 500, 1000000 /* 1 GB/s */) .setMinDefaultMax(0, 500, 1000000 /* 1 GB/s */)
.comment("" .comment(""
+ "Maximum speed for uploading LODs to the clients, in KB/s.\n" + "Maximum per-player speed for uploading LODs to the clients, in KB/s.\n"
+ "Value of 0 disables the limit." + "Value of 0 disables the limit."
+ "") + "")
.build(); .build();
public static ConfigEntry<Integer> globalBandwidthLimit = new ConfigEntry.Builder<Integer>()
.setChatCommandName("common.globalBandwidthLimit")
.setMinDefaultMax(0, 0, 10000000 /* 10 GB/s */)
.comment(""
+ "Maximum global speed for uploading LODs to the clients, in KB/s.\n"
+ "Value of 0 disables the limit."
+ "")
.build();
public static ConfigEntry<Boolean> enableAdaptiveTransferSpeed = new ConfigEntry.Builder<Boolean>()
.set(false)
.comment(""
+ "Enables adaptive transfer speed based on client performance.\n"
+ "If true, DH will automatically adjust transfer rate to minimize connection lag.\n"
+ "If false, transfer speed will remain fixed.\n"
+ "")
.build();
public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build(); public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build();
@@ -1757,12 +1829,9 @@ public class Config
try try
{ {
// TODO automatically get all instances of AbstractPresetConfigEventHandler and fire "setUiOnlyConfigValues"
ThreadPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues(); ThreadPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
RenderQualityPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues(); RenderQualityPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
QuickRenderToggleConfigEventHandler.INSTANCE.setUiOnlyConfigValues(); QuickRenderToggleConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
QuickShowWorldGenProgressConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
RenderCacheConfigEventHandler.getInstance();
} }
catch (Exception e) catch (Exception e)
{ {
@@ -1,275 +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;
import com.seibel.distanthorizons.core.config.file.ConfigFileHandling;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.config.types.AbstractConfigType;
import com.seibel.distanthorizons.core.config.types.ConfigCategory;
import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.config.types.ConfigUiLinkedEntry;
import com.seibel.distanthorizons.core.wrapperInterfaces.config.ILangWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.util.*;
/**
* Indexes and sets everything up for the file handling and gui
*
* @author coolGi
* @author Ran
* @version 2023-8-26
*/
// Init the config after singletons have been blinded
public class ConfigBase
{
/** Our own config instance, don't modify unless you are the DH mod */
public static ConfigBase INSTANCE;
public ConfigFileHandling configFileINSTANCE;
private final Logger logger;
public final String modID;
public final String modName;
public final int configVersion;
public boolean isLoaded = false;
/**
* What the config works with
* <br>
* <br> {@link Enum}
* <br> {@link Boolean}
* <br> {@link Byte}
* <br> {@link Integer}
* <br> {@link Double}
* <br> {@link Short}
* <br> {@link Long}
* <br> {@link Float}
* <br> {@link String}
* <br>
* <br> // Below, "T" should be a value from above
* <br> // Note: This is not checked, so we trust that you are doing the right thing (TODO: Check it)
* <br> List<T>
* <br> ArrayList<T>
* <br> Map<String, T>
* <br> HashMap<String, T>
*/
public static final List<Class<?>> acceptableInputs = new ArrayList<Class<?>>()
{{
add(Boolean.class);
add(Byte.class);
add(Integer.class);
add(Double.class);
add(Short.class);
add(Long.class);
add(Float.class);
add(String.class);
// TODO[CONFIG]: Check the type of these is valid
add(List.class);
add(ArrayList.class);
add(Map.class);
add(HashMap.class);
}};
/** Disables the minimum and maximum of any variable */
public boolean disableMinMax = false; // Very fun to use, but should always be disabled by default
public final List<AbstractConfigType<?, ?>> entries = new ArrayList<>();
public ConfigBase(String modID, String modName, Class<?> configClass)
{
this(modID, modName, configClass, getConfigPath(modName), -1);
}
public ConfigBase(String modID, String modName, Class<?> configClass, Path configPath)
{
this(modID, modName, configClass, configPath, -1);
}
public ConfigBase(String modID, String modName, Class<?> configClass, int configVersion)
{
this(modID, modName, configClass, getConfigPath(modName), configVersion);
}
public ConfigBase(String modID, String modName, Class<?> configClass, Path configPath, int configVersion)
{
this.logger = LogManager.getLogger(this.getClass().getSimpleName() + ", " + modID);
this.logger.info("Initialising config for " + modName);
this.modID = modID;
this.modName = modName;
this.configVersion = configVersion;
this.initNestedClass(configClass, ""); // Init root category
// File handling (load from file)
this.configFileINSTANCE = new ConfigFileHandling(this, configPath);
this.configFileINSTANCE.loadFromFile();
this.isLoaded = true;
this.logger.info("Config for " + modName + " initialised");
}
private void initNestedClass(Class<?> configClass, String category)
{
// Put all the entries in entries
for (Field field : configClass.getFields())
{
if (AbstractConfigType.class.isAssignableFrom(field.getType()))
{
try
{
this.entries.add((AbstractConfigType<?, ?>) field.get(field.getType()));
}
catch (IllegalAccessException exception)
{
this.logger.warn(exception);
}
AbstractConfigType<?, ?> entry = this.entries.get(this.entries.size() - 1);
entry.category = category;
entry.name = field.getName();
entry.configBase = this;
if (ConfigEntry.class.isAssignableFrom(field.getType()))
{ // If item is type ConfigEntry
if (!isAcceptableType(entry.getType()))
{
this.logger.error("Invalid variable type at [" + (category.isEmpty() ? "" : category + ".") + field.getName() + "].");
this.logger.error("Type [" + entry.getType() + "] is not one of these types [" + acceptableInputs.toString() + "]");
this.entries.remove(this.entries.size() - 1); // Delete the entry if it is invalid so the game can still run
}
}
if (ConfigCategory.class.isAssignableFrom(field.getType()))
{ // If it's a category then init the stuff inside it and put it in the category list
assert entry instanceof ConfigCategory;
if (((ConfigCategory) entry).getDestination() == null)
((ConfigCategory) entry).destination = entry.getNameWCategory();
if (entry.get() != null)
{
this.initNestedClass(((ConfigCategory) entry).get(), ((ConfigCategory) entry).getDestination());
}
}
}
}
}
private static boolean isAcceptableType(Class<?> Clazz)
{
if (Clazz.isEnum())
return true;
return acceptableInputs.contains(Clazz);
}
/** Gets the default config path given a mod name */
public static Path getConfigPath(String modName)
{
return SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class)
.getInstallationDirectory().toPath().resolve("config").resolve(modName + ".toml");
}
/**
* Used for checking that all the lang files for the config exist
*
* @param onlyShowNew If disabled then it would basically remake the config lang
* @param checkEnums Checks if all the lang for the enum's exist
*/
// This is just to re-format the lang or check if there is something in the lang that is missing
@SuppressWarnings("unchecked")
public String generateLang(boolean onlyShowNew, boolean checkEnums)
{
ILangWrapper langWrapper = SingletonInjector.INSTANCE.get(ILangWrapper.class);
List<Class<? extends Enum<?>>> enumList = new ArrayList<>();
String generatedLang = "";
String starter = " \"";
String separator = "\":\n \"";
String ending = "\",\n";
for (AbstractConfigType<?, ?> entry : this.entries)
{
String entryPrefix = "lod.config." + entry.getNameWCategory();
if (checkEnums && entry.getType().isEnum() && !enumList.contains(entry.getType()))
{ // Put it in an enum list to work with at the end
enumList.add((Class<? extends Enum<?>>) entry.getType());
}
if (!onlyShowNew || langWrapper.langExists(entryPrefix))
{
if (!ConfigUiLinkedEntry.class.isAssignableFrom(entry.getClass()))
{ // If it is a linked item, dont generate the base lang
generatedLang += starter
+ entryPrefix
+ separator
+ langWrapper.getLang(entryPrefix)
+ ending
;
}
// Adds tooltips
if (langWrapper.langExists(entryPrefix + ".@tooltip"))
{
generatedLang += starter
+ entryPrefix + ".@tooltip"
+ separator
+ langWrapper.getLang(entryPrefix + ".@tooltip")
.replaceAll("\n", "\\\\n")
.replaceAll("\"", "\\\\\"")
+ ending
;
}
}
}
if (!enumList.isEmpty())
{
generatedLang += "\n"; // Separate the main lang with the enum's
for (Class<? extends Enum> anEnum : enumList)
{
for (Object enumStr : new ArrayList<>(EnumSet.allOf(anEnum)))
{
String enumPrefix = "lod.config.enum." + anEnum.getSimpleName() + "." + enumStr.toString();
if (!onlyShowNew || langWrapper.langExists(enumPrefix))
{
generatedLang += starter
+ enumPrefix
+ separator
+ langWrapper.getLang(enumPrefix)
+ ending
;
}
}
}
}
return generatedLang;
}
}
@@ -0,0 +1,331 @@
/*
* 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;
import com.seibel.distanthorizons.core.config.file.ConfigFileHandler;
import com.seibel.distanthorizons.core.config.types.*;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.wrapperInterfaces.config.ILangWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.core.logging.DhLogger;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.util.*;
/**
* Sets up everything in {@link Config} for the file/GUI and keeps track of all
* entries therein. <br>
* This should be run after the singletons have been bound.
*
* @author coolGi
* @author Ran
*
* @see Config
*/
public class ConfigHandler
{
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
/**
* What the config works with
* <br>
* <br> {@link Enum}
* <br> {@link Boolean}
* <br> {@link Byte}
* <br> {@link Integer}
* <br> {@link Double}
* <br> {@link Short}
* <br> {@link Long}
* <br> {@link Float}
* <br> {@link String}
* <br>
* <br> // Below, "T" should be a value from above
* <br> // Note: This is not checked, so we trust that you are doing the right thing (TODO: Check it)
* <br> List<T>
* <br> ArrayList<T>
* <br> Map<String, T>
* <br> HashMap<String, T>
*/
private static final List<Class<?>> ACCEPTABLE_INPUTS = new ArrayList<Class<?>>()
{{
this.add(Boolean.class);
this.add(Byte.class);
this.add(Integer.class);
this.add(Double.class);
this.add(Short.class);
this.add(Long.class);
this.add(Float.class);
this.add(String.class);
// partially implemented but not entirely
this.add(List.class);
this.add(ArrayList.class);
this.add(Map.class);
this.add(HashMap.class);
}};
public static final ConfigHandler INSTANCE = new ConfigHandler();
public final ConfigFileHandler configFileHandler = new ConfigFileHandler(getConfigPath());
public final List<AbstractConfigBase<?>> configBaseList = new ArrayList<>();
public boolean isLoaded = false;
/**
* Disables the minimum and maximum validation. <Br>
* Fun to use, but should be disabled by default.
*/
public boolean runMinMaxValidation = true;
//=============//
// constructor //
//=============//
public static void tryRunFirstTimeSetup()
{
if (INSTANCE.isLoaded)
{
LOGGER.debug("ConfigHandler setup already run, ignoring.");
return;
}
INSTANCE.runFirstTimeSetup();
}
private void runFirstTimeSetup()
{
LOGGER.info("Initialising config for [" + ModInfo.NAME + "]");
this.initNestedClass(Config.class, ""); // Init root category
this.configFileHandler.loadFromFile();
this.runMinMaxValidation = !Config.Client.Advanced.Debugging.allowUnsafeValues.get();
this.isLoaded = true;
LOGGER.info("[" + ModInfo.NAME + "] Config initialised");
}
/** Gets the default config path given a mod name */
private static Path getConfigPath()
{
return MC_SHARED
.getInstallationDirectory().toPath()
.resolve("config")
.resolve(ModInfo.NAME + ".toml");
}
/** Put all the config entries into configEntryList */
private void initNestedClass(Class<?> configClass, String category)
{
Field[] fields = configClass.getFields();
for (Field field : fields)
{
// ignore any non-config variables
if (!AbstractConfigBase.class.isAssignableFrom(field.getType()))
{
continue;
}
// add this config to the master list
try
{
this.configBaseList.add((AbstractConfigBase<?>) field.get(field.getType()));
}
catch (IllegalAccessException e)
{
LOGGER.warn("Unable to add config ["+field.getType().getName()+"], error: ["+e.getMessage()+"].", e);
continue;
}
// set any necessary variables in this config
AbstractConfigBase<?> configBase = this.configBaseList.get(this.configBaseList.size() - 1);
configBase.category = category;
configBase.name = field.getName();
// validate the config's input type
if (ConfigEntry.class.isAssignableFrom(field.getType()))
{
if (!isAcceptableType(configBase.getType()))
{
LOGGER.error("Invalid variable type at [" + (category.isEmpty() ? "" : category + ".") + field.getName() + "].");
LOGGER.error("Type [" + configBase.getType() + "] is not one of these types [" + ACCEPTABLE_INPUTS.toString() + "]");
this.configBaseList.remove(this.configBaseList.size() - 1); // Delete the entry if it is invalid so the game can still run
}
}
// recursively add deeper categories if present
if (ConfigCategory.class.isAssignableFrom(field.getType()))
{
ConfigCategory configCategory = (ConfigCategory) configBase;
if (configCategory.getDestination() == null)
{
configCategory.destination = configBase.getNameAndCategory();
}
// shouldn't happen, but just in case
if (configBase.get() != null)
{
this.initNestedClass(configCategory.get(), configCategory.getDestination());
}
}
}
}
private static boolean isAcceptableType(Class<?> inputClass)
{
if (inputClass.isEnum())
{
return true;
}
return ACCEPTABLE_INPUTS.contains(inputClass);
}
//===============//
// lang handling //
//===============//
/**
* Used for checking that all the lang files for the config exist.
* This is just to re-format the lang or check if there is something in the lang that is missing
*
* @param onlyShowMissing If false then this will remake the entire config lang
* @param checkEnums Checks if all the lang for the enum's exist
*/
@SuppressWarnings("unchecked")
public String generateLang(boolean onlyShowMissing, boolean checkEnums)
{
ILangWrapper langWrapper = SingletonInjector.INSTANCE.get(ILangWrapper.class);
List<Class<? extends Enum<?>>> enumList = new ArrayList<>();
String generatedLang = "";
String starter = " \"";
String separator = "\":\n \"";
String ending = "\",\n";
// config entries
for (AbstractConfigBase<?> entry : this.configBaseList)
{
String entryPrefix = "distanthorizons.config." + entry.getNameAndCategory();
if (checkEnums
&& entry.getType().isEnum()
&& !enumList.contains(entry.getType()))
{
// Put it in an enum list to work with at the end
enumList.add((Class<? extends Enum<?>>) entry.getType());
}
// config file items don't need lang entries
if (!entry.getAppearance().showInGui)
{
continue;
}
// some entries don't need localization
if (ConfigUiLinkedEntry.class.isAssignableFrom(entry.getClass())
|| ConfigUISpacer.class.isAssignableFrom(entry.getClass()))
{
continue;
}
if (ConfigUIComment.class.isAssignableFrom(entry.getClass())
&& ((ConfigUIComment)entry).parentConfigPath != null)
{
// TODO this could potentially add the same item multiple times
entryPrefix = "distanthorizons.config." + ((ConfigUIComment)entry).parentConfigPath;
}
if (langWrapper.langExists(entryPrefix)
&& onlyShowMissing)
{
continue;
}
generatedLang += starter
+ entryPrefix
+ separator
+ langWrapper.getLang(entryPrefix)
+ ending
;
// only add tooltips for entries that are also missing
// their primary lang
// this is done since not all menu items need a tooltip
if (!langWrapper.langExists(entryPrefix + ".@tooltip")
|| !onlyShowMissing)
{
generatedLang += starter
+ entryPrefix + ".@tooltip"
+ separator
+ langWrapper.getLang(entryPrefix + ".@tooltip")
.replaceAll("\n", "\\\\n")
.replaceAll("\"", "\\\\\"")
+ ending
;
}
}
// enums
if (!enumList.isEmpty())
{
generatedLang += "\n"; // Separate the main lang with the enum's
for (Class<? extends Enum> anEnum : enumList)
{
for (Object enumStr : new ArrayList<Object>(EnumSet.allOf(anEnum)))
{
String enumPrefix = "distanthorizons.config.enum." + anEnum.getSimpleName() + "." + enumStr.toString();
if (!langWrapper.langExists(enumPrefix)
|| !onlyShowMissing)
{
generatedLang += starter
+ enumPrefix
+ separator
+ langWrapper.getLang(enumPrefix)
+ ending
;
}
}
}
}
return generatedLang;
}
}
@@ -24,7 +24,7 @@ import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
public class ConfigEntryWithPresetOptions<TQuickEnum, TConfig> public class ConfigPresetOptions<TQuickEnum, TConfig>
{ {
public final ConfigEntry<TConfig> configEntry; public final ConfigEntry<TConfig> configEntry;
@@ -32,7 +32,11 @@ public class ConfigEntryWithPresetOptions<TQuickEnum, TConfig>
public ConfigEntryWithPresetOptions(ConfigEntry<TConfig> configEntry, HashMap<TQuickEnum, TConfig> configOptionByQualityOption) //=============//
// constructor //
//=============//
public ConfigPresetOptions(ConfigEntry<TConfig> configEntry, HashMap<TQuickEnum, TConfig> configOptionByQualityOption)
{ {
this.configEntry = configEntry; this.configEntry = configEntry;
this.configOptionByQualityOption = configOptionByQualityOption; this.configOptionByQualityOption = configOptionByQualityOption;
@@ -40,6 +44,10 @@ public class ConfigEntryWithPresetOptions<TQuickEnum, TConfig>
//=========//
// methods //
//=========//
public void updateConfigEntry(TQuickEnum quickQuality) public void updateConfigEntry(TQuickEnum quickQuality)
{ {
TConfig newValue = this.configOptionByQualityOption.get(quickQuality); TConfig newValue = this.configOptionByQualityOption.get(quickQuality);
@@ -48,7 +56,9 @@ public class ConfigEntryWithPresetOptions<TQuickEnum, TConfig>
public HashSet<TQuickEnum> getPossibleQualitiesFromCurrentOptionValue() public HashSet<TQuickEnum> getPossibleQualitiesFromCurrentOptionValue()
{ {
TConfig inputOptionValue = this.configEntry.get(); // get true value so we can ignore API overrides,
// users find this confusing if their preset is set to "CUSTOM"
TConfig inputOptionValue = this.configEntry.getTrueValue();
HashSet<TQuickEnum> possibleQualities = new HashSet<>(); HashSet<TQuickEnum> possibleQualities = new HashSet<>();
for (TQuickEnum key : this.configOptionByQualityOption.keySet()) for (TQuickEnum key : this.configOptionByQualityOption.keySet())
@@ -17,12 +17,12 @@
* 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.api.objects.config; package com.seibel.distanthorizons.core.config.api;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry; import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConverter; import com.seibel.distanthorizons.coreapi.interfaces.config.IConverter;
import com.seibel.distanthorizons.coreapi.util.converters.DefaultConverter; import com.seibel.distanthorizons.core.config.api.converters.DefaultConverter;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -41,7 +41,7 @@ import java.util.function.Consumer;
*/ */
public class DhApiConfigValue<coreType, apiType> implements IDhApiConfigValue<apiType> public class DhApiConfigValue<coreType, apiType> implements IDhApiConfigValue<apiType>
{ {
private final IConfigEntry<coreType> configEntry; private final ConfigEntry<coreType> configBase;
private final IConverter<coreType, apiType> configConverter; private final IConverter<coreType, apiType> configConverter;
@@ -53,9 +53,9 @@ public class DhApiConfigValue<coreType, apiType> implements IDhApiConfigValue<ap
* Uses the default object converter, this requires coreType and apiType to be the same. * Uses the default object converter, this requires coreType and apiType to be the same.
*/ */
@SuppressWarnings("unchecked") // DefaultConverter's cast is safe @SuppressWarnings("unchecked") // DefaultConverter's cast is safe
public DhApiConfigValue(IConfigEntry<coreType> newConfigEntry) public DhApiConfigValue(ConfigEntry<coreType> configBase)
{ {
this.configEntry = newConfigEntry; this.configBase = configBase;
this.configConverter = (IConverter<coreType, apiType>) new DefaultConverter<coreType>(); this.configConverter = (IConverter<coreType, apiType>) new DefaultConverter<coreType>();
} }
@@ -63,22 +63,22 @@ public class DhApiConfigValue<coreType, apiType> implements IDhApiConfigValue<ap
* This constructor should only be called internally. <br> * This constructor should only be called internally. <br>
* There is no reason for API users to create this object. <br><br> * There is no reason for API users to create this object. <br><br>
*/ */
public DhApiConfigValue(IConfigEntry<coreType> newConfigEntry, IConverter<coreType, apiType> newConverter) public DhApiConfigValue(ConfigEntry<coreType> configBase, IConverter<coreType, apiType> newConverter)
{ {
this.configEntry = newConfigEntry; this.configBase = configBase;
this.configConverter = newConverter; this.configConverter = newConverter;
} }
public apiType getValue() { return this.configConverter.convertToApiType(this.configEntry.get()); } public apiType getValue() { return this.configConverter.convertToApiType(this.configBase.get()); }
public apiType getTrueValue() { return this.configConverter.convertToApiType(this.configEntry.getTrueValue()); } public apiType getTrueValue() { return this.configConverter.convertToApiType(this.configBase.getTrueValue()); }
public apiType getApiValue() { return this.configConverter.convertToApiType(this.configEntry.getApiValue()); } public apiType getApiValue() { return this.configConverter.convertToApiType(this.configBase.getApiValue()); }
public boolean setValue(apiType newValue) public boolean setValue(apiType newValue)
{ {
if (this.configEntry.getAllowApiOverride()) if (this.configBase.getAllowApiOverride())
{ {
this.configEntry.setApiValue(this.configConverter.convertToCoreType(newValue)); this.configBase.setApiValue(this.configConverter.convertToCoreType(newValue));
return true; return true;
} }
else else
@@ -89,11 +89,11 @@ public class DhApiConfigValue<coreType, apiType> implements IDhApiConfigValue<ap
public boolean clearValue() public boolean clearValue()
{ {
if (this.configEntry.getAllowApiOverride()) if (this.configBase.getAllowApiOverride())
{ {
// no converter should be used here since null objects may need to be handled differently // no converter should be used here since null objects may need to be handled differently
// TODO the API should just have a bool to keep track of whether the API value is in use instead of using NULL // TODO the API should just have a bool to keep track of whether the API value is in use instead of using NULL
this.configEntry.setApiValue(null); this.configBase.setApiValue(null);
return true; return true;
} }
else else
@@ -102,16 +102,16 @@ public class DhApiConfigValue<coreType, apiType> implements IDhApiConfigValue<ap
} }
} }
public boolean getCanBeOverrodeByApi() { return this.configEntry.getAllowApiOverride(); } public boolean getCanBeOverrodeByApi() { return this.configBase.getAllowApiOverride(); }
public apiType getDefaultValue() { return this.configConverter.convertToApiType(this.configEntry.getDefaultValue()); } public apiType getDefaultValue() { return this.configConverter.convertToApiType(this.configBase.getDefaultValue()); }
public apiType getMaxValue() { return this.configConverter.convertToApiType(this.configEntry.getMax()); } public apiType getMaxValue() { return this.configConverter.convertToApiType(this.configBase.getMax()); }
public apiType getMinValue() { return this.configConverter.convertToApiType(this.configEntry.getMin()); } public apiType getMinValue() { return this.configConverter.convertToApiType(this.configBase.getMin()); }
public void addChangeListener(Consumer<apiType> onValueChangeFunc) public void addChangeListener(Consumer<apiType> onValueChangeFunc)
{ {
this.configEntry.addValueChangeListener((coreValue) -> this.configBase.addValueChangeListener((coreValue) ->
{ {
apiType apiValue = this.configConverter.convertToApiType(coreValue); apiType apiValue = this.configConverter.convertToApiType(coreValue);
onValueChangeFunc.accept(apiValue); onValueChangeFunc.accept(apiValue);
@@ -17,7 +17,7 @@
* 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.coreapi.util.converters; package com.seibel.distanthorizons.core.config.api.converters;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiFogDrawMode; import com.seibel.distanthorizons.api.enums.rendering.EDhApiFogDrawMode;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConverter; import com.seibel.distanthorizons.coreapi.interfaces.config.IConverter;
@@ -17,7 +17,7 @@
* 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.coreapi.util.converters; package com.seibel.distanthorizons.core.config.api.converters;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConverter; import com.seibel.distanthorizons.coreapi.interfaces.config.IConverter;
@@ -17,7 +17,7 @@
* 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.coreapi.util.converters; package com.seibel.distanthorizons.core.config.api.converters;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConverter; import com.seibel.distanthorizons.coreapi.interfaces.config.IConverter;
@@ -31,14 +31,12 @@ import com.seibel.distanthorizons.coreapi.interfaces.config.IConverter;
public class RenderModeEnabledConverter implements IConverter<EDhApiRendererMode, Boolean> public class RenderModeEnabledConverter implements IConverter<EDhApiRendererMode, Boolean>
{ {
@Override public EDhApiRendererMode convertToCoreType(Boolean renderingEnabled) @Override
{ public EDhApiRendererMode convertToCoreType(Boolean renderingEnabled)
return renderingEnabled ? EDhApiRendererMode.DEFAULT : EDhApiRendererMode.DISABLED; { return renderingEnabled ? EDhApiRendererMode.DEFAULT : EDhApiRendererMode.DISABLED; }
}
@Override public Boolean convertToApiType(EDhApiRendererMode renderingMode) @Override
{ public Boolean convertToApiType(EDhApiRendererMode renderingMode)
return renderingMode == EDhApiRendererMode.DEFAULT; { return renderingMode == EDhApiRendererMode.DEFAULT; }
}
} }
@@ -22,13 +22,77 @@ package com.seibel.distanthorizons.core.config.eventHandlers;
import com.seibel.distanthorizons.api.DhApi; import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.api.interfaces.render.IDhApiRenderProxy; import com.seibel.distanthorizons.api.interfaces.render.IDhApiRenderProxy;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener; import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
import com.seibel.distanthorizons.core.util.TimerUtil;
import java.util.Timer;
import java.util.TimerTask;
public class ReloadLodsConfigEventHandler implements IConfigListener public class ReloadLodsConfigEventHandler implements IConfigListener
{ {
public static ReloadLodsConfigEventHandler INSTANCE = new ReloadLodsConfigEventHandler(); /**
* should be used for user facing UI options
* this allows the user a second to click through options before they're applied
*/
public static ReloadLodsConfigEventHandler DELAYED_INSTANCE = new ReloadLodsConfigEventHandler(2_000L);
/** should be used for debug options so their change can be seen instantly */
public static ReloadLodsConfigEventHandler INSTANT_INSTANCE = new ReloadLodsConfigEventHandler(0);
/** how long to wait in milliseconds before applying the config changes */
private final long timeoutInMs;
private Timer cacheClearingTimer;
//=============//
// constructor //
//=============//
public ReloadLodsConfigEventHandler(long timeoutInMs)
{
this.timeoutInMs = timeoutInMs;
}
//========//
// events //
//========//
@Override @Override
public void onConfigValueSet() public void onConfigValueSet()
{
if (this.timeoutInMs > 0)
{
this.refreshRenderDataAfterTimeout();
}
else
{
clearRenderDataCache();
}
}
/** Calling this method multiple times will reset the timer */
private synchronized void refreshRenderDataAfterTimeout() // synchronized to prevent potential threading issues when adding/removing the timer
{
// stop the previous timer if one exists
if (this.cacheClearingTimer != null)
{
this.cacheClearingTimer.cancel();
}
// create a new timer task
TimerTask timerTask = new TimerTask()
{
public void run()
{
clearRenderDataCache();
}
};
this.cacheClearingTimer = TimerUtil.CreateTimer("RenderCacheClearConfigTimer");
this.cacheClearingTimer.schedule(timerTask, this.timeoutInMs);
}
private static void clearRenderDataCache()
{ {
IDhApiRenderProxy renderProxy = DhApi.Delayed.renderProxy; IDhApiRenderProxy renderProxy = DhApi.Delayed.renderProxy;
if (renderProxy != null) if (renderProxy != null)
@@ -37,4 +101,5 @@ public class ReloadLodsConfigEventHandler implements IConfigListener
} }
} }
} }
@@ -1,114 +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;
import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.api.enums.config.*;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiTransparency;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.util.TimerUtil;
import java.util.Timer;
import java.util.TimerTask;
/**
* Listens to the config and will automatically
* clear the current render cache if certain settings are changed. <br> <br>
*
* Note: if additional settings should clear the render cache, add those to this listener, don't create a new listener
*/
public class RenderCacheConfigEventHandler
{
private static RenderCacheConfigEventHandler INSTANCE;
// previous values used to check if a watched setting was actually modified
private final ConfigChangeListener<EDhApiMaxHorizontalResolution> horizontalResolutionChangeListener;
private final ConfigChangeListener<EDhApiVerticalQuality> verticalQualityChangeListener;
private final ConfigChangeListener<EDhApiTransparency> transparencyChangeListener;
private final ConfigChangeListener<EDhApiBlocksToAvoid> blocksToIgnoreChangeListener;
private final ConfigChangeListener<Boolean> tintWithAvoidedBlocksChangeListener;
private final ConfigChangeListener<Double> brightnessMultiplierChangeListener;
private final ConfigChangeListener<Double> saturationMultiplierChangeListener;
private final ConfigChangeListener<EDhApiLodShading> lodShadingChangeListener;
private final ConfigChangeListener<EDhApiGrassSideRendering> grassSideChangeListener;
private final ConfigChangeListener<EDhApiDebugRendering> debugRenderingChangeListener;
/** how long to wait in milliseconds before applying the config changes */
private static final long TIMEOUT_IN_MS = 4_000L;
private Timer cacheClearingTimer;
public static RenderCacheConfigEventHandler getInstance()
{
if (INSTANCE == null)
{
INSTANCE = new RenderCacheConfigEventHandler();
}
return INSTANCE;
}
/** private since we only ever need one handler at a time */
private RenderCacheConfigEventHandler()
{
this.horizontalResolutionChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Graphics.Quality.maxHorizontalResolution, (newValue) -> this.refreshRenderDataAfterTimeout());
this.verticalQualityChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Graphics.Quality.verticalQuality, (newValue) -> this.refreshRenderDataAfterTimeout());
this.transparencyChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Graphics.Quality.transparency, (newValue) -> this.refreshRenderDataAfterTimeout());
this.blocksToIgnoreChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Graphics.Quality.blocksToIgnore, (newValue) -> this.refreshRenderDataAfterTimeout());
this.tintWithAvoidedBlocksChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Graphics.Quality.tintWithAvoidedBlocks, (newValue) -> this.refreshRenderDataAfterTimeout());
this.brightnessMultiplierChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Graphics.Quality.brightnessMultiplier, (newValue) -> this.refreshRenderDataAfterTimeout());
this.saturationMultiplierChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Graphics.Quality.saturationMultiplier, (newValue) -> this.refreshRenderDataAfterTimeout());
this.lodShadingChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Graphics.Quality.lodShading, (newValue) -> this.refreshRenderDataAfterTimeout());
this.grassSideChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Graphics.Quality.grassSideRendering, (newValue) -> this.refreshRenderDataAfterTimeout());
this.debugRenderingChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Debugging.debugRendering, (newValue) -> this.refreshRenderDataAfterTimeout());
}
/** Calling this method multiple times will reset the timer */
private void refreshRenderDataAfterTimeout()
{
// stop the previous timer if one exists
if (this.cacheClearingTimer != null)
{
this.cacheClearingTimer.cancel();
}
// create a new timer task
TimerTask timerTask = new TimerTask()
{
public void run()
{
DhApi.Delayed.renderProxy.clearRenderDataCache();
}
};
this.cacheClearingTimer = TimerUtil.CreateTimer("RenderCacheClearConfigTimer");
this.cacheClearingTimer.schedule(timerTask, TIMEOUT_IN_MS);
}
}
@@ -20,23 +20,22 @@
package com.seibel.distanthorizons.core.config.eventHandlers; package com.seibel.distanthorizons.core.config.eventHandlers;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.ConfigHandler;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener; import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
/**
* handles enabling/disabling config validation when the
* {@link Config.Client.Advanced.Debugging#allowUnsafeValues} option
* is changed.
*/
public class UnsafeValuesConfigListener implements IConfigListener public class UnsafeValuesConfigListener implements IConfigListener
{ {
public static UnsafeValuesConfigListener INSTANCE = new UnsafeValuesConfigListener(); public static UnsafeValuesConfigListener INSTANCE = new UnsafeValuesConfigListener();
@Override @Override
public void onConfigValueSet() public void onConfigValueSet()
{ { ConfigHandler.INSTANCE.runMinMaxValidation = !Config.Client.Advanced.Debugging.allowUnsafeValues.get(); }
Config.Client.Advanced.Debugging.allowUnsafeValues.configBase.disableMinMax =
Config.Client.Advanced.Debugging.allowUnsafeValues.get();
}
@Override
public void onUiModify()
{
}
} }
@@ -19,30 +19,30 @@
package com.seibel.distanthorizons.core.config.eventHandlers.presets; package com.seibel.distanthorizons.core.config.eventHandlers.presets;
import com.seibel.distanthorizons.core.config.ConfigBase; import com.seibel.distanthorizons.core.config.ConfigHandler;
import com.seibel.distanthorizons.core.config.ConfigEntryWithPresetOptions; import com.seibel.distanthorizons.core.config.ConfigPresetOptions;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener; import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
import com.seibel.distanthorizons.core.config.types.AbstractConfigBase;
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.util.TimerUtil; import com.seibel.distanthorizons.core.util.TimerUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.config.IConfigGui; import com.seibel.distanthorizons.core.wrapperInterfaces.config.IConfigGui;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry;
import com.seibel.distanthorizons.coreapi.util.StringUtil; import com.seibel.distanthorizons.coreapi.util.StringUtil;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.*;
public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<?>> implements IConfigListener public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<?>> implements IConfigListener
{ {
private static final Logger LOGGER = LogManager.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final long MS_DELAY_BEFORE_APPLYING_PRESET = 3_000; private static final long MS_DELAY_BEFORE_APPLYING_PRESET = 3_000;
@Nullable @Nullable
private static IConfigGui configGui = SingletonInjector.INSTANCE.get(IConfigGui.class); private static final IConfigGui CONFIG_GUI = SingletonInjector.INSTANCE.get(IConfigGui.class);
private static boolean guiListenersAdded = false;
protected final ArrayList<ConfigEntryWithPresetOptions<TPresetEnum, ?>> configList = new ArrayList<>(); protected final ArrayList<ConfigPresetOptions<TPresetEnum, ?>> configList = new ArrayList<>();
/** this timer is used so each preset isn't applied while a user is clicking through the config options */ /** this timer is used so each preset isn't applied while a user is clicking through the config options */
protected Timer applyPresetTimer = null; protected Timer applyPresetTimer = null;
/** the enum to apply after the timer expires or the UI screen changes. */ /** the enum to apply after the timer expires or the UI screen changes. */
@@ -59,9 +59,9 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
public AbstractPresetConfigEventHandler() public AbstractPresetConfigEventHandler()
{ {
// don't update the UI when running on a server // don't update the UI when running on a server
if (configGui != null) if (CONFIG_GUI != null)
{ {
configGui.addOnScreenChangeListener(this::onConfigUiClosed); CONFIG_GUI.addOnScreenChangeListener(this::onConfigUiClosed);
} }
} }
@@ -90,7 +90,7 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
public void onConfigValueSet() public void onConfigValueSet()
{ {
// don't try modifying the config before it's been loaded from file // don't try modifying the config before it's been loaded from file
if (!ConfigBase.INSTANCE.isLoaded) if (!ConfigHandler.INSTANCE.isLoaded)
{ {
return; return;
} }
@@ -140,11 +140,11 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
LOGGER.info("changing preset to: " + newPresetEnum); LOGGER.debug("changing preset to: [" + newPresetEnum + "].");
this.changingPreset = true; this.changingPreset = true;
// update the controlled config values // update the controlled config values
for (ConfigEntryWithPresetOptions<TPresetEnum, ?> configEntry : this.configList) for (ConfigPresetOptions<TPresetEnum, ?> configEntry : this.configList)
{ {
configEntry.updateConfigEntry(newPresetEnum); configEntry.updateConfigEntry(newPresetEnum);
} }
@@ -152,7 +152,7 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
this.setUiOnlyConfigValues(); this.setUiOnlyConfigValues();
this.changingPreset = false; this.changingPreset = false;
LOGGER.info("preset active: " + newPresetEnum); LOGGER.debug("preset active: [" + newPresetEnum + "].");
} }
/** /**
@@ -200,7 +200,7 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
// remove any quick options that aren't possible with the currently selected options // remove any quick options that aren't possible with the currently selected options
for (ConfigEntryWithPresetOptions<TPresetEnum, ?> configEntry : this.configList) for (ConfigPresetOptions<TPresetEnum, ?> configEntry : this.configList)
{ {
HashSet<TPresetEnum> optionPresetSet = configEntry.getPossibleQualitiesFromCurrentOptionValue(); HashSet<TPresetEnum> optionPresetSet = configEntry.getPossibleQualitiesFromCurrentOptionValue();
possiblePresetSet.retainAll(optionPresetSet); possiblePresetSet.retainAll(optionPresetSet);
@@ -230,7 +230,7 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
// abstract methods // // abstract methods //
//==================// //==================//
protected abstract IConfigEntry<TPresetEnum> getPresetConfigEntry(); protected abstract AbstractConfigBase<TPresetEnum> getPresetConfigEntry();
protected abstract List<TPresetEnum> getPresetEnumList(); protected abstract List<TPresetEnum> getPresetEnumList();
protected abstract TPresetEnum getCustomPresetEnum(); protected abstract TPresetEnum getCustomPresetEnum();
@@ -1,63 +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) ->
{
Config.Common.WorldGenerator.showGenerationProgress.set(Config.Client.quickShowWorldGenProgress.get()
? Config.Common.WorldGenerator.showGenerationProgress.getDefaultValue()
: EDhApiDistantGeneratorProgressDisplayLocation.DISABLED);
});
this.fullChangeListener = new ConfigChangeListener<>(Config.Common.WorldGenerator.showGenerationProgress,
(val) ->
{
Config.Client.quickShowWorldGenProgress.set(Config.Common.WorldGenerator.showGenerationProgress.get() != EDhApiDistantGeneratorProgressDisplayLocation.DISABLED);
});
}
/**
* 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);
}
}
@@ -26,11 +26,11 @@ import com.seibel.distanthorizons.api.enums.config.EDhApiVerticalQuality;
import com.seibel.distanthorizons.api.enums.config.quickOptions.EDhApiQualityPreset; import com.seibel.distanthorizons.api.enums.config.quickOptions.EDhApiQualityPreset;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiTransparency; import com.seibel.distanthorizons.api.enums.rendering.EDhApiTransparency;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.ConfigEntryWithPresetOptions; import com.seibel.distanthorizons.core.config.ConfigPresetOptions;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry; import com.seibel.distanthorizons.core.config.types.AbstractConfigBase;
import org.apache.logging.log4j.LogManager; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import java.util.*; import java.util.*;
@@ -39,10 +39,10 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
{ {
public static final RenderQualityPresetConfigEventHandler INSTANCE = new RenderQualityPresetConfigEventHandler(); public static final RenderQualityPresetConfigEventHandler INSTANCE = new RenderQualityPresetConfigEventHandler();
private static final Logger LOGGER = LogManager.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private final ConfigEntryWithPresetOptions<EDhApiQualityPreset, EDhApiMaxHorizontalResolution> drawResolution = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Quality.maxHorizontalResolution, private final ConfigPresetOptions<EDhApiQualityPreset, EDhApiMaxHorizontalResolution> drawResolution = new ConfigPresetOptions<>(Config.Client.Advanced.Graphics.Quality.maxHorizontalResolution,
new HashMap<EDhApiQualityPreset, EDhApiMaxHorizontalResolution>() new HashMap<EDhApiQualityPreset, EDhApiMaxHorizontalResolution>()
{{ {{
this.put(EDhApiQualityPreset.MINIMUM, EDhApiMaxHorizontalResolution.TWO_BLOCKS); this.put(EDhApiQualityPreset.MINIMUM, EDhApiMaxHorizontalResolution.TWO_BLOCKS);
@@ -51,7 +51,7 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.put(EDhApiQualityPreset.HIGH, EDhApiMaxHorizontalResolution.BLOCK); this.put(EDhApiQualityPreset.HIGH, EDhApiMaxHorizontalResolution.BLOCK);
this.put(EDhApiQualityPreset.EXTREME, EDhApiMaxHorizontalResolution.BLOCK); this.put(EDhApiQualityPreset.EXTREME, EDhApiMaxHorizontalResolution.BLOCK);
}}); }});
private final ConfigEntryWithPresetOptions<EDhApiQualityPreset, EDhApiVerticalQuality> verticalQuality = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Quality.verticalQuality, private final ConfigPresetOptions<EDhApiQualityPreset, EDhApiVerticalQuality> verticalQuality = new ConfigPresetOptions<>(Config.Client.Advanced.Graphics.Quality.verticalQuality,
new HashMap<EDhApiQualityPreset, EDhApiVerticalQuality>() new HashMap<EDhApiQualityPreset, EDhApiVerticalQuality>()
{{ {{
this.put(EDhApiQualityPreset.MINIMUM, EDhApiVerticalQuality.HEIGHT_MAP); this.put(EDhApiQualityPreset.MINIMUM, EDhApiVerticalQuality.HEIGHT_MAP);
@@ -60,7 +60,7 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.put(EDhApiQualityPreset.HIGH, EDhApiVerticalQuality.HIGH); this.put(EDhApiQualityPreset.HIGH, EDhApiVerticalQuality.HIGH);
this.put(EDhApiQualityPreset.EXTREME, EDhApiVerticalQuality.EXTREME); this.put(EDhApiQualityPreset.EXTREME, EDhApiVerticalQuality.EXTREME);
}}); }});
private final ConfigEntryWithPresetOptions<EDhApiQualityPreset, EDhApiHorizontalQuality> horizontalQuality = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Quality.horizontalQuality, private final ConfigPresetOptions<EDhApiQualityPreset, EDhApiHorizontalQuality> horizontalQuality = new ConfigPresetOptions<>(Config.Client.Advanced.Graphics.Quality.horizontalQuality,
new HashMap<EDhApiQualityPreset, EDhApiHorizontalQuality>() new HashMap<EDhApiQualityPreset, EDhApiHorizontalQuality>()
{{ {{
this.put(EDhApiQualityPreset.MINIMUM, EDhApiHorizontalQuality.LOWEST); this.put(EDhApiQualityPreset.MINIMUM, EDhApiHorizontalQuality.LOWEST);
@@ -69,7 +69,7 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.put(EDhApiQualityPreset.HIGH, EDhApiHorizontalQuality.HIGH); this.put(EDhApiQualityPreset.HIGH, EDhApiHorizontalQuality.HIGH);
this.put(EDhApiQualityPreset.EXTREME, EDhApiHorizontalQuality.EXTREME); this.put(EDhApiQualityPreset.EXTREME, EDhApiHorizontalQuality.EXTREME);
}}); }});
private final ConfigEntryWithPresetOptions<EDhApiQualityPreset, EDhApiTransparency> transparency = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Quality.transparency, private final ConfigPresetOptions<EDhApiQualityPreset, EDhApiTransparency> transparency = new ConfigPresetOptions<>(Config.Client.Advanced.Graphics.Quality.transparency,
new HashMap<EDhApiQualityPreset, EDhApiTransparency>() new HashMap<EDhApiQualityPreset, EDhApiTransparency>()
{{ {{
this.put(EDhApiQualityPreset.MINIMUM, EDhApiTransparency.DISABLED); this.put(EDhApiQualityPreset.MINIMUM, EDhApiTransparency.DISABLED);
@@ -78,7 +78,7 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.put(EDhApiQualityPreset.HIGH, EDhApiTransparency.COMPLETE); this.put(EDhApiQualityPreset.HIGH, EDhApiTransparency.COMPLETE);
this.put(EDhApiQualityPreset.EXTREME, EDhApiTransparency.COMPLETE); this.put(EDhApiQualityPreset.EXTREME, EDhApiTransparency.COMPLETE);
}}); }});
private final ConfigEntryWithPresetOptions<EDhApiQualityPreset, Boolean> ssaoEnabled = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Ssao.enableSsao, private final ConfigPresetOptions<EDhApiQualityPreset, Boolean> ssaoEnabled = new ConfigPresetOptions<>(Config.Client.Advanced.Graphics.Ssao.enableSsao,
new HashMap<EDhApiQualityPreset, Boolean>() new HashMap<EDhApiQualityPreset, Boolean>()
{{ {{
this.put(EDhApiQualityPreset.MINIMUM, false); this.put(EDhApiQualityPreset.MINIMUM, false);
@@ -87,7 +87,7 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.put(EDhApiQualityPreset.HIGH, true); this.put(EDhApiQualityPreset.HIGH, true);
this.put(EDhApiQualityPreset.EXTREME, true); this.put(EDhApiQualityPreset.EXTREME, true);
}}); }});
private final ConfigEntryWithPresetOptions<EDhApiQualityPreset, EDhApiMcRenderingFadeMode> vanillaFade = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Quality.vanillaFadeMode, private final ConfigPresetOptions<EDhApiQualityPreset, EDhApiMcRenderingFadeMode> vanillaFade = new ConfigPresetOptions<>(Config.Client.Advanced.Graphics.Quality.vanillaFadeMode,
new HashMap<EDhApiQualityPreset, EDhApiMcRenderingFadeMode>() new HashMap<EDhApiQualityPreset, EDhApiMcRenderingFadeMode>()
{{ {{
this.put(EDhApiQualityPreset.MINIMUM, EDhApiMcRenderingFadeMode.NONE); this.put(EDhApiQualityPreset.MINIMUM, EDhApiMcRenderingFadeMode.NONE);
@@ -96,7 +96,16 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.put(EDhApiQualityPreset.HIGH, EDhApiMcRenderingFadeMode.DOUBLE_PASS); this.put(EDhApiQualityPreset.HIGH, EDhApiMcRenderingFadeMode.DOUBLE_PASS);
this.put(EDhApiQualityPreset.EXTREME, EDhApiMcRenderingFadeMode.DOUBLE_PASS); this.put(EDhApiQualityPreset.EXTREME, EDhApiMcRenderingFadeMode.DOUBLE_PASS);
}}); }});
private final ConfigEntryWithPresetOptions<EDhApiQualityPreset, Boolean> dhDither = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Quality.ditherDhFade, private final ConfigPresetOptions<EDhApiQualityPreset, Boolean> dhFadeFarClipPlane = new ConfigPresetOptions<>(Config.Client.Advanced.Graphics.Quality.dhFadeFarClipPlane,
new HashMap<EDhApiQualityPreset, Boolean>()
{{
this.put(EDhApiQualityPreset.MINIMUM, false);
this.put(EDhApiQualityPreset.LOW, false);
this.put(EDhApiQualityPreset.MEDIUM, true);
this.put(EDhApiQualityPreset.HIGH, true);
this.put(EDhApiQualityPreset.EXTREME, true);
}});
private final ConfigPresetOptions<EDhApiQualityPreset, Boolean> dhDither = new ConfigPresetOptions<>(Config.Client.Advanced.Graphics.Quality.ditherDhFade,
new HashMap<EDhApiQualityPreset, Boolean>() new HashMap<EDhApiQualityPreset, Boolean>()
{{ {{
this.put(EDhApiQualityPreset.MINIMUM, false); this.put(EDhApiQualityPreset.MINIMUM, false);
@@ -105,6 +114,24 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.put(EDhApiQualityPreset.HIGH, true); this.put(EDhApiQualityPreset.HIGH, true);
this.put(EDhApiQualityPreset.EXTREME, true); this.put(EDhApiQualityPreset.EXTREME, true);
}}); }});
private final ConfigPresetOptions<EDhApiQualityPreset, Boolean> caveCulling = new ConfigPresetOptions<>(Config.Client.Advanced.Graphics.Culling.enableCaveCulling,
new HashMap<EDhApiQualityPreset, Boolean>()
{{
this.put(EDhApiQualityPreset.MINIMUM, true);
this.put(EDhApiQualityPreset.LOW, true);
this.put(EDhApiQualityPreset.MEDIUM, true);
this.put(EDhApiQualityPreset.HIGH, false);
this.put(EDhApiQualityPreset.EXTREME, false);
}});
private final ConfigPresetOptions<EDhApiQualityPreset, Integer> biomeBlending = new ConfigPresetOptions<>(Config.Client.Advanced.Graphics.Quality.lodBiomeBlending,
new HashMap<EDhApiQualityPreset, Integer>()
{{
this.put(EDhApiQualityPreset.MINIMUM, 0);
this.put(EDhApiQualityPreset.LOW, 1);
this.put(EDhApiQualityPreset.MEDIUM, 3);
this.put(EDhApiQualityPreset.HIGH, 3);
this.put(EDhApiQualityPreset.EXTREME, 3);
}});
@@ -121,11 +148,14 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.configList.add(this.horizontalQuality); this.configList.add(this.horizontalQuality);
this.configList.add(this.transparency); this.configList.add(this.transparency);
this.configList.add(this.ssaoEnabled); this.configList.add(this.ssaoEnabled);
this.configList.add(this.dhFadeFarClipPlane);
this.configList.add(this.vanillaFade); this.configList.add(this.vanillaFade);
this.configList.add(this.dhDither); this.configList.add(this.dhDither);
this.configList.add(this.caveCulling);
this.configList.add(this.biomeBlending);
for (ConfigEntryWithPresetOptions<EDhApiQualityPreset, ?> config : this.configList) for (ConfigPresetOptions<EDhApiQualityPreset, ?> config : this.configList)
{ {
// ignore try-using, the listener should only ever be added once and should never be removed // ignore try-using, the listener should only ever be added once and should never be removed
new ConfigChangeListener<>(config.configEntry, (val) -> { this.onConfigValueChanged(); }); new ConfigChangeListener<>(config.configEntry, (val) -> { this.onConfigValueChanged(); });
@@ -139,7 +169,7 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
//==============// //==============//
@Override @Override
protected IConfigEntry<EDhApiQualityPreset> getPresetConfigEntry() { return Config.Client.qualityPresetSetting; } protected AbstractConfigBase<EDhApiQualityPreset> getPresetConfigEntry() { return Config.Client.qualityPresetSetting; }
@Override @Override
protected List<EDhApiQualityPreset> getPresetEnumList() { return Arrays.asList(EDhApiQualityPreset.values()); } protected List<EDhApiQualityPreset> getPresetEnumList() { return Arrays.asList(EDhApiQualityPreset.values()); }
@@ -22,11 +22,12 @@ package com.seibel.distanthorizons.core.config.eventHandlers.presets;
import com.seibel.distanthorizons.api.enums.config.quickOptions.EDhApiThreadPreset; import com.seibel.distanthorizons.api.enums.config.quickOptions.EDhApiThreadPreset;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.ConfigEntryWithPresetOptions; import com.seibel.distanthorizons.core.config.ConfigPresetOptions;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry; import com.seibel.distanthorizons.core.config.types.AbstractConfigBase;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.coreapi.util.MathUtil; import com.seibel.distanthorizons.coreapi.util.MathUtil;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
@@ -37,11 +38,11 @@ public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHan
{ {
public static final ThreadPresetConfigEventHandler INSTANCE = new ThreadPresetConfigEventHandler(); public static final ThreadPresetConfigEventHandler INSTANCE = new ThreadPresetConfigEventHandler();
private static final Logger LOGGER = LogManager.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public static int getDefaultThreadCount() { return getThreadCountByPercent(0.5); } public static int getDefaultThreadCount() { return getThreadCountByPercent(0.5); }
private final ConfigEntryWithPresetOptions<EDhApiThreadPreset, Integer> threadCount = new ConfigEntryWithPresetOptions<>(Config.Common.MultiThreading.numberOfThreads, private final ConfigPresetOptions<EDhApiThreadPreset, Integer> threadCount = new ConfigPresetOptions<>(Config.Common.MultiThreading.numberOfThreads,
new HashMap<EDhApiThreadPreset, Integer>() new HashMap<EDhApiThreadPreset, Integer>()
{{ {{
this.put(EDhApiThreadPreset.MINIMAL_IMPACT, getThreadCountByPercent(0.1)); this.put(EDhApiThreadPreset.MINIMAL_IMPACT, getThreadCountByPercent(0.1));
@@ -51,7 +52,7 @@ public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHan
this.put(EDhApiThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, getThreadCountByPercent(1.0)); this.put(EDhApiThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, getThreadCountByPercent(1.0));
}}); }});
public static double getDefaultRunTimeRatio() { return 1.0; } public static double getDefaultRunTimeRatio() { return 1.0; }
private final ConfigEntryWithPresetOptions<EDhApiThreadPreset, Double> threadRunTime = new ConfigEntryWithPresetOptions<>(Config.Common.MultiThreading.threadRunTimeRatio, private final ConfigPresetOptions<EDhApiThreadPreset, Double> threadRunTime = new ConfigPresetOptions<>(Config.Common.MultiThreading.threadRunTimeRatio,
new HashMap<EDhApiThreadPreset, Double>() new HashMap<EDhApiThreadPreset, Double>()
{{ {{
this.put(EDhApiThreadPreset.MINIMAL_IMPACT, 0.5); this.put(EDhApiThreadPreset.MINIMAL_IMPACT, 0.5);
@@ -74,7 +75,7 @@ public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHan
this.configList.add(this.threadCount); this.configList.add(this.threadCount);
this.configList.add(this.threadRunTime); this.configList.add(this.threadRunTime);
for (ConfigEntryWithPresetOptions<EDhApiThreadPreset, ?> config : this.configList) for (ConfigPresetOptions<EDhApiThreadPreset, ?> config : this.configList)
{ {
// ignore try-using, the listeners should only ever be added once and should never be removed // ignore try-using, the listeners should only ever be added once and should never be removed
new ConfigChangeListener<>(config.configEntry, (val) -> { this.onConfigValueChanged(); }); new ConfigChangeListener<>(config.configEntry, (val) -> { this.onConfigValueChanged(); });
@@ -119,7 +120,7 @@ public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHan
//==============// //==============//
@Override @Override
protected IConfigEntry<EDhApiThreadPreset> getPresetConfigEntry() { return Config.Client.threadPresetSetting; } protected AbstractConfigBase<EDhApiThreadPreset> getPresetConfigEntry() { return Config.Client.threadPresetSetting; }
@Override @Override
protected List<EDhApiThreadPreset> getPresetEnumList() { return Arrays.asList(EDhApiThreadPreset.values()); } protected List<EDhApiThreadPreset> getPresetEnumList() { return Arrays.asList(EDhApiThreadPreset.values()); }
@@ -20,15 +20,15 @@
package com.seibel.distanthorizons.core.config.file; package com.seibel.distanthorizons.core.config.file;
import com.electronwill.nightconfig.core.file.CommentedFileConfig; import com.electronwill.nightconfig.core.file.CommentedFileConfig;
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.ConfigBase; import com.seibel.distanthorizons.core.config.types.AbstractConfigBase;
import com.seibel.distanthorizons.core.config.types.AbstractConfigType;
import com.seibel.distanthorizons.core.config.types.ConfigEntry; import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.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.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper; import com.seibel.distanthorizons.coreapi.ModInfo;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@@ -41,17 +41,13 @@ import java.util.concurrent.locks.ReentrantLock;
* @author coolGi * @author coolGi
* @version 2023-8-26 * @version 2023-8-26
*/ */
public class ConfigFileHandling public class ConfigFileHandler
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
public final ConfigBase configBase;
public final Path configPath; public final Path configPath;
private final Logger logger;
/** This is the object for night-config */ /** This is the object for night-config */
private final CommentedFileConfig nightConfig; private final CommentedFileConfig nightConfig;
@@ -64,17 +60,23 @@ public class ConfigFileHandling
// constructor // // constructor //
//=============// //=============//
public ConfigFileHandling(ConfigBase configBase, Path configPath) public ConfigFileHandler(Path configPath)
{ {
this.logger = LogManager.getLogger(this.getClass().getSimpleName() + ", " + configBase.modID);
this.configBase = configBase;
this.configPath = configPath; this.configPath = configPath;
this.nightConfig = CommentedFileConfig.builder(this.configPath.toFile()).build(); this.nightConfig = CommentedFileConfig
.builder(this.configPath.toFile())
// sync is needed so file reading/writing only happens during locked sections,
// otherwise some GUI changes may be lost when changing screens
.sync()
.build();
} }
//====================//
// entire config file //
//====================//
/** Saves the entire config to the file */ /** Saves the entire config to the file */
public void saveToFile() { this.saveToFile(this.nightConfig); } public void saveToFile() { this.saveToFile(this.nightConfig); }
@@ -96,7 +98,7 @@ public class ConfigFileHandling
this.loadNightConfig(nightConfig); this.loadNightConfig(nightConfig);
for (AbstractConfigType<?, ?> entry : this.configBase.entries) for (AbstractConfigBase<?> entry : ConfigHandler.INSTANCE.configBaseList)
{ {
if (ConfigEntry.class.isAssignableFrom(entry.getClass())) if (ConfigEntry.class.isAssignableFrom(entry.getClass()))
{ {
@@ -134,7 +136,7 @@ public class ConfigFileHandling
{ {
this.readWriteLock.lock(); this.readWriteLock.lock();
int currentCfgVersion = this.configBase.configVersion; int currentCfgVersion = ModInfo.CONFIG_FILE_VERSION;
try try
{ {
// Dont load the real `this.nightConfig`, instead create a tempoary one // Dont load the real `this.nightConfig`, instead create a tempoary one
@@ -146,29 +148,29 @@ public class ConfigFileHandling
} }
catch (Exception ignored) { } catch (Exception ignored) { }
if (currentCfgVersion == this.configBase.configVersion) if (currentCfgVersion == ModInfo.CONFIG_FILE_VERSION)
{ {
// handle normally // handle normally
} }
else if (currentCfgVersion > this.configBase.configVersion) else if (currentCfgVersion > ModInfo.CONFIG_FILE_VERSION)
{ {
this.logger.warn("Found config version [" + currentCfgVersion + "] which is newer than current mods config version of [" + this.configBase.configVersion + "]. You may have downgraded the mod and items may have been moved, you have been warned"); LOGGER.warn("Found config version [" + currentCfgVersion + "] which is newer than current mods config version of [" + ModInfo.CONFIG_FILE_VERSION + "]. You may have downgraded the mod and items may have been moved, you have been warned");
} }
else // if (currentCfgVersion < configBase.configVersion) else // if (currentCfgVersion < configBase.configVersion)
{ {
this.logger.warn(this.configBase.modName + " config is of an older version, currently there is no config updater... so resetting config"); LOGGER.warn(ModInfo.NAME + " config is of an older version, currently there is no config updater... so resetting config");
try try
{ {
Files.delete(this.configPath); Files.delete(this.configPath);
} }
catch (Exception e) catch (Exception e)
{ {
this.logger.error(e); LOGGER.error("Unable to delete outdated config file at: ["+this.configPath+"], error: ["+e.getMessage()+"].", e);
} }
} }
this.loadFromFile(this.nightConfig); this.loadFromFile(this.nightConfig);
this.nightConfig.set("_version", this.configBase.configVersion); this.nightConfig.set("_version", ModInfo.CONFIG_FILE_VERSION);
} }
finally finally
{ {
@@ -194,7 +196,7 @@ public class ConfigFileHandling
// Load all the entries // Load all the entries
for (AbstractConfigType<?, ?> entry : this.configBase.entries) for (AbstractConfigBase<?> entry : ConfigHandler.INSTANCE.configBaseList)
{ {
if (ConfigEntry.class.isAssignableFrom(entry.getClass()) if (ConfigEntry.class.isAssignableFrom(entry.getClass())
&& entry.getAppearance().showInFile) && entry.getAppearance().showInFile)
@@ -218,6 +220,9 @@ public class ConfigFileHandling
//=======================//
// single config entries //
//=======================//
// Save an entry when only given the entry // Save an entry when only given the entry
public void saveEntry(ConfigEntry<?> entry) public void saveEntry(ConfigEntry<?> entry)
@@ -235,10 +240,10 @@ public class ConfigFileHandling
else if (entry.getTrueValue() == null) else if (entry.getTrueValue() == null)
{ {
// TODO when can this happen? // TODO when can this happen?
throw new IllegalArgumentException("Entry [" + entry.getNameWCategory() + "] is null, this may be a problem with [" + this.configBase.modName + "]. Please contact the authors."); throw new IllegalArgumentException("BlockBiomeWrapperPair [" + entry.getNameAndCategory() + "] is null, this may be a problem with [" + ModInfo.NAME + "]. Please contact the authors.");
} }
workConfig.set(entry.getNameWCategory(), ConfigTypeConverters.attemptToConvertToString(entry.getType(), entry.getTrueValue())); workConfig.set(entry.getNameAndCategory(), ConfigTypeConverters.attemptToConvertToString(entry.getType(), entry.getTrueValue()));
} }
/** Loads an entry when only given the entry */ /** Loads an entry when only given the entry */
@@ -248,9 +253,11 @@ public class ConfigFileHandling
public <T> void loadEntry(ConfigEntry<T> entry, CommentedFileConfig nightConfig) public <T> void loadEntry(ConfigEntry<T> entry, CommentedFileConfig nightConfig)
{ {
if (!entry.getAppearance().showInFile) if (!entry.getAppearance().showInFile)
{
return; return;
}
if (!nightConfig.contains(entry.getNameWCategory())) if (!nightConfig.contains(entry.getNameAndCategory()))
{ {
this.saveEntry(entry, nightConfig); this.saveEntry(entry, nightConfig);
return; return;
@@ -261,34 +268,33 @@ public class ConfigFileHandling
{ {
if (entry.getType().isEnum()) if (entry.getType().isEnum())
{ {
entry.pureSet((T) (nightConfig.getEnum(entry.getNameWCategory(), (Class<? extends Enum>) entry.getType()))); entry.setWithoutFiringEvents((T) (nightConfig.getEnum(entry.getNameAndCategory(), (Class<? extends Enum>) entry.getType())));
return; return;
} }
// try converting the value if necessary // try converting the value if necessary
Class<?> expectedValueClass = entry.getType(); Class<?> expectedValueClass = entry.getType();
Object value = nightConfig.get(entry.getNameWCategory()); Object value = nightConfig.get(entry.getNameAndCategory());
Object convertedValue = ConfigTypeConverters.attemptToConvertFromString(expectedValueClass, value); Object convertedValue = ConfigTypeConverters.attemptToConvertFromString(expectedValueClass, value);
if (!convertedValue.getClass().equals(expectedValueClass)) if (!convertedValue.getClass().equals(expectedValueClass))
{ {
this.logger.error("Unable to convert config value ["+value+"] from ["+(value != null ? value.getClass() : "NULL")+"] to ["+expectedValueClass+"] for config ["+entry.name+"], " + LOGGER.error("Unable to convert config value ["+value+"] from ["+(value != null ? value.getClass() : "NULL")+"] to ["+expectedValueClass+"] for config ["+entry.name+"], " +
"the default config value will be used instead ["+entry.getDefaultValue()+"]. " + "the default config value will be used instead ["+entry.getDefaultValue()+"]. " +
"Make sure a converter is defined in ["+ConfigTypeConverters.class.getSimpleName()+"]."); "Make sure a converter is defined in ["+ConfigTypeConverters.class.getSimpleName()+"].");
convertedValue = entry.getDefaultValue(); convertedValue = entry.getDefaultValue();
} }
entry.pureSet((T) convertedValue); entry.setWithoutFiringEvents((T) convertedValue);
if (entry.getTrueValue() == null) if (entry.getTrueValue() == null)
{ {
this.logger.warn("Entry [" + entry.getNameWCategory() + "] returned as null from the config. Using default value."); LOGGER.warn("BlockBiomeWrapperPair [" + entry.getNameAndCategory() + "] returned as null from the config. Using default value.");
entry.pureSet(entry.getDefaultValue()); entry.setWithoutFiringEvents(entry.getDefaultValue());
} }
} }
catch (Exception e) catch (Exception e)
{ {
// e.printStackTrace(); LOGGER.warn("BlockBiomeWrapperPair [" + entry.getNameAndCategory() + "] had an invalid value when loading the config. Using default value.");
this.logger.warn("Entry [" + entry.getNameWCategory() + "] had an invalid value when loading the config. Using default value."); entry.setWithoutFiringEvents(entry.getDefaultValue());
entry.pureSet(entry.getDefaultValue());
} }
} }
@@ -309,22 +315,21 @@ public class ConfigFileHandling
// the new line makes it easier to read and separate configs // the new line makes it easier to read and separate configs
// the space makes sure the first word of a comment isn't directly in line with the "#" // the space makes sure the first word of a comment isn't directly in line with the "#"
comment = "\n " + comment; comment = "\n " + comment;
nightConfig.setComment(entry.getNameWCategory(), comment); nightConfig.setComment(entry.getNameAndCategory(), comment);
} }
//=============//
// nightconfig //
//=============//
/** /**
* Uses {@link ConfigFileHandling#nightConfig} to do {@link CommentedFileConfig#load()} but with error checking * Uses {@link ConfigFileHandler#nightConfig} to do {@link CommentedFileConfig#load()} but with error checking
* *
* @apiNote This overwrites any value currently stored in the config * @apiNote This overwrites any value currently stored in the config
*/ */
public void loadNightConfig() public void loadNightConfig() { this.loadNightConfig(this.nightConfig); }
{
loadNightConfig(this.nightConfig);
}
/** /**
* Does {@link CommentedFileConfig#load()} but with error checking * Does {@link CommentedFileConfig#load()} but with error checking
* *
@@ -342,23 +347,29 @@ public class ConfigFileHandling
} }
catch (Exception e) catch (Exception e)
{ {
this.logger.warn("Loading file failed because of this expectation:\n" + e); LOGGER.warn("Loading file failed because of this expectation:\n" + e);
reCreateFile(this.configPath); reCreateFile(this.configPath);
nightConfig.load(); nightConfig.load();
} }
} }
catch (Exception ex) catch (Exception e)
{ {
System.out.println("Creating file failed"); LOGGER.error("File creation failed at ["+this.configPath+"], error: ["+e.getMessage()+"].", e);
this.logger.error(ex);
SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class).crashMinecraft("Loading file and resetting config file failed at path [" + configPath + "]. Please check the file is ok and you have the permissions", ex); // TODO is there a reason this is lazily gotten?
IMinecraftClientWrapper mc = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
mc.crashMinecraft("Loading file and resetting config file failed at path [" + this.configPath + "]. Please check the file is ok and you have the permissions", e);
} }
} }
//===============//
// file handling //
//===============//
public static void reCreateFile(Path path) public static void reCreateFile(Path path)
{ {
try try
@@ -23,7 +23,7 @@ import com.electronwill.nightconfig.core.Config;
import com.electronwill.nightconfig.core.io.ParsingMode; import com.electronwill.nightconfig.core.io.ParsingMode;
import com.electronwill.nightconfig.json.JsonFormat; import com.electronwill.nightconfig.json.JsonFormat;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -36,7 +36,7 @@ import java.util.Map;
*/ */
public class ConfigTypeConverters public class ConfigTypeConverters
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
// Once you've made a converter add it to here where the first value is the type you want to convert and the 2nd value is the converter // Once you've made a converter add it to here where the first value is the type you want to convert and the 2nd value is the converter
public static final Map<Class<?>, ConverterBase> convertObjects = new HashMap<Class<?>, ConverterBase>() public static final Map<Class<?>, ConverterBase> convertObjects = new HashMap<Class<?>, ConverterBase>()
@@ -0,0 +1,10 @@
package com.seibel.distanthorizons.core.config.gui;
/**
* Points to a Common object that holds the GUI state.
* Having this interface allows for cleaner casting.
*/
public interface IConfigGuiInfo
{
}
@@ -23,6 +23,7 @@ import org.jetbrains.annotations.NotNull;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.awt.event.KeyListener; import java.awt.event.KeyListener;
@@ -35,6 +36,8 @@ public class JavaScreenHandlerScreen extends AbstractScreen
public static boolean firstRun = true; public static boolean firstRun = true;
public final Component jComponent; public final Component jComponent;
static static
{ {
// Note: this code can cause Mac // Note: this code can cause Mac
@@ -54,20 +57,26 @@ public class JavaScreenHandlerScreen extends AbstractScreen
public void init() public void init()
{ {
if (firstRun) if (firstRun)
{
frame = EmbeddedFrameUtil.embeddedFrameCreate(this.minecraftWindow); // Don't call this multiple times frame = EmbeddedFrameUtil.embeddedFrameCreate(this.minecraftWindow); // Don't call this multiple times
}
frame.add(jComponent); frame.add(this.jComponent);
frame.setBackground(new Color(0, 125, 155));
JavaScreenHandlerScreen thiss = this; JavaScreenHandlerScreen thiss = this;
frame.addKeyListener(new KeyListener() { frame.addKeyListener(new KeyListener()
{
@Override @Override
public void keyPressed(KeyEvent keyEvent) public void keyPressed(KeyEvent keyEvent)
{ {
System.out.println("Key pressed code=" + keyEvent.getKeyCode() + ", char=" + keyEvent.getKeyChar()); System.out.println("Key pressed code=" + keyEvent.getKeyCode() + ", char=" + keyEvent.getKeyChar());
if (keyEvent.getKeyCode() == KeyEvent.VK_ESCAPE) if (keyEvent.getKeyCode() == KeyEvent.VK_ESCAPE)
{
thiss.close = true; thiss.close = true;
} }
}
@Override @Override
public void keyTyped(KeyEvent keyEvent) { } public void keyTyped(KeyEvent keyEvent) { }
@@ -80,7 +89,7 @@ public class JavaScreenHandlerScreen extends AbstractScreen
EmbeddedFrameUtil.embeddedFrameSetBounds(frame, 0, 0, this.width, this.height); EmbeddedFrameUtil.embeddedFrameSetBounds(frame, 0, 0, this.width, this.height);
firstRun = false; firstRun = false;
} }
else
EmbeddedFrameUtil.showFrame(frame); EmbeddedFrameUtil.showFrame(frame);
} }
@@ -89,13 +98,43 @@ public class JavaScreenHandlerScreen extends AbstractScreen
{ {
public ExampleScreen() public ExampleScreen()
{ {
setLayout(new GridBagLayout()); this.setLayout(new GridBagLayout());
GridBagConstraints constraints = new GridBagConstraints(); this.setBackground(new Color(255, 0, 0)); // doesn't appear to be used
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.weightx = 0.5; GridBagConstraints helloWorldConstraints = new GridBagConstraints();
constraints.gridx = 0; helloWorldConstraints.weightx = 0.5;
constraints.gridy = 0; helloWorldConstraints.gridx = 0;
add(new JLabel("Hello World!"), constraints); helloWorldConstraints.gridy = 0;
//helloWorldConstraints.fill = GridBagConstraints.BOTH;
this.add(new JLabel("Hello World!"), helloWorldConstraints);
GridBagConstraints buttonConstraints = new GridBagConstraints();
buttonConstraints.weightx = 0.5;
buttonConstraints.gridx = 0;
buttonConstraints.gridy = 1;
//buttonConstraints.fill = GridBagConstraints.BOTH;
JButton button = new JButton();
button.setBackground(Color.GREEN);
button.setFocusable(false); // otherwise we can't use escape to leave
button.setAction(new ExampleButtonEventHandler("Button text"));
this.add(button, buttonConstraints);
}
private class ExampleButtonEventHandler extends AbstractAction
{
public ExampleButtonEventHandler(String text)
{
super(text);
//this.putValue(SHORT_DESCRIPTION, text);
//this.putValue(MNEMONIC_KEY, text);
}
@Override
public void actionPerformed(ActionEvent e)
{
System.out.println("button pressed");
}
} }
} }
@@ -116,8 +155,10 @@ public class JavaScreenHandlerScreen extends AbstractScreen
@Override @Override
public void onClose() public void onClose()
{ {
frame.remove(jComponent); frame.remove(this.jComponent);
EmbeddedFrameUtil.hideFrame(frame); EmbeddedFrameUtil.hideFrame(frame);
} }
} }
@@ -1,126 +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.gui;
import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.render.glObject.buffer.GLVertexBuffer;
import com.seibel.distanthorizons.core.render.glObject.shader.ShaderProgram;
import com.seibel.distanthorizons.core.render.glObject.vertexAttribute.AbstractVertexAttribute;
import com.seibel.distanthorizons.core.render.glObject.vertexAttribute.VertexPointer;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftGLWrapper;
import org.lwjgl.opengl.GL32;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* @author coolGi
*/
public class OpenGLConfigScreen extends AbstractScreen
{
private static final IMinecraftGLWrapper GLMC = SingletonInjector.INSTANCE.get(IMinecraftGLWrapper.class);
private ShaderProgram basicShader;
private GLVertexBuffer sameContextBuffer;
private GLVertexBuffer sharedContextBuffer;
private AbstractVertexAttribute va;
@Override
public void init()
{
System.out.println("init");
this.va = AbstractVertexAttribute.create();
this.va.bind();
// Pos
this.va.setVertexAttribute(0, 0, VertexPointer.addVec2Pointer(false));
// Color
this.va.setVertexAttribute(0, 1, VertexPointer.addVec4Pointer(false));
this.va.completeAndCheck(Float.BYTES * 6);
this.basicShader = new ShaderProgram("shaders/test/vert.vert", "shaders/test/frag.frag",
"fragColor", new String[]{"vPosition", "color"});
this.createBuffer();
}
// Render a square with uv color
private static final float[] vertices = {
// PosX,Y, ColorR,G,B,A
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f,
0.4f, -0.4f, 1.0f, 0.0f, 0.0f, 1.0f,
0.3f, 0.3f, 1.0f, 1.0f, 0.0f, 0.0f,
-0.2f, 0.2f, 0.0f, 1.0f, 1.0f, 1.0f
};
private static GLVertexBuffer createTextingBuffer()
{
ByteBuffer buffer = ByteBuffer.allocateDirect(vertices.length * Float.BYTES);
// Fill buffer with the vertices.
buffer = buffer.order(ByteOrder.nativeOrder());
buffer.asFloatBuffer().put(vertices);
buffer.rewind();
GLVertexBuffer vbo = new GLVertexBuffer(false);
vbo.bind();
vbo.uploadBuffer(buffer, 4, EDhApiGpuUploadMethod.DATA, vertices.length * Float.BYTES);
return vbo;
}
private void createBuffer()
{
this.sharedContextBuffer = createTextingBuffer();
this.sameContextBuffer = createTextingBuffer();
}
@Override
public void render(float delta)
{
System.out.println("Updated config screen with the delta of " + delta);
GL32.glViewport(0, 0, this.width, this.height);
GL32.glPolygonMode(GL32.GL_FRONT_AND_BACK, GL32.GL_FILL);
GLMC.disableFaceCulling();
GLMC.disableDepthTest();
GLMC.disableBlend();
this.basicShader.bind();
this.va.bind();
// Switch between the two buffers per second
if (System.currentTimeMillis() % 2000 < 1000)
{
this.sameContextBuffer.bind();
this.va.bindBufferToAllBindingPoints(this.sameContextBuffer.getId());
}
else
{
this.sameContextBuffer.bind();
this.va.bindBufferToAllBindingPoints(this.sharedContextBuffer.getId());
}
// Render the square
GL32.glDrawArrays(GL32.GL_TRIANGLE_FAN, 0, 4);
GL32.glClear(GL32.GL_DEPTH_BUFFER_BIT);
}
@Override
public void tick() { System.out.println("Ticked"); }
}
@@ -19,25 +19,27 @@
package com.seibel.distanthorizons.core.config.types; package com.seibel.distanthorizons.core.config.types;
import com.seibel.distanthorizons.core.config.ConfigBase; import com.seibel.distanthorizons.core.config.gui.IConfigGuiInfo;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance; import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance;
/** /**
* The class where all config options should extend * The class all config options should extend
* *
* @author coolGi * @author coolGi
*/ */
// Note for devs: The "S" is the class that is extending this public abstract class AbstractConfigBase<T>
public abstract class AbstractConfigType<T, S>
{ {
public String category = ""; // This should only be set once in the init public String category = ""; // This should only be set once in the init
public String name; // This should only be set once in the init public String name; // This should only be set once in the init
protected final T defaultValue; protected final T defaultValue;
protected final boolean isFloatingPointNumber; protected final boolean isFloatingPointNumber;
protected T value; protected T value;
public ConfigBase configBase;
public Object guiValue; // This is a storage variable something like the gui can use /**
* This stores information related to the GUI state.
* This is set during config UI setup.
*/
public IConfigGuiInfo guiValue;
protected EConfigEntryAppearance appearance; protected EConfigEntryAppearance appearance;
@@ -47,7 +49,7 @@ public abstract class AbstractConfigType<T, S>
// constructor // // constructor //
//=============// //=============//
protected AbstractConfigType(EConfigEntryAppearance appearance, T defaultValue) protected AbstractConfigBase(EConfigEntryAppearance appearance, T defaultValue)
{ {
this.defaultValue = defaultValue; this.defaultValue = defaultValue;
this.value = defaultValue; this.value = defaultValue;
@@ -74,7 +76,7 @@ public abstract class AbstractConfigType<T, S>
public String getCategory() { return this.category; } public String getCategory() { return this.category; }
public String getName() { return this.name; } public String getName() { return this.name; }
public String getNameWCategory() { return (this.category.isEmpty() ? "" : this.category + ".") + this.name; } public String getNameAndCategory() { return (this.category.isEmpty() ? "" : this.category + ".") + this.name; }
/** Gets the class of T */ /** Gets the class of T */
@@ -27,10 +27,19 @@ import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance
* *
* @author coolGi * @author coolGi
*/ */
public class ConfigCategory extends AbstractConfigType<Class<?>, ConfigCategory> public class ConfigCategory extends AbstractConfigBase<Class<?>>
{ {
/** This should not be set by anything other than the config system itself */ /**
public String destination; // Where the category goes to * Defines where this category points to. <br>
* May be defined during config setup.
*/
public String destination;
//=============//
// constructor //
//=============//
private ConfigCategory(EConfigEntryAppearance appearance, Class<?> value, String destination) private ConfigCategory(EConfigEntryAppearance appearance, Class<?> value, String destination)
{ {
@@ -38,20 +47,26 @@ public class ConfigCategory extends AbstractConfigType<Class<?>, ConfigCategory>
this.destination = destination; this.destination = destination;
} }
public String getDestination()
{
return this.destination; //==================//
} // property getters //
//==================//
public String getDestination() { return this.destination; }
/** Use get() instead for category */ /** Use get() instead for category */
@Override @Override
@Deprecated @Deprecated
public Class<?> getType() public Class<?> getType() { return this.value; }
{
return value;
}
public static class Builder extends AbstractConfigType.Builder<Class<?>, Builder>
//=========//
// builder //
//=========//
public static class Builder extends AbstractConfigBase.Builder<Class<?>, Builder>
{ {
private String tmpDestination = null; private String tmpDestination = null;
@@ -20,12 +20,13 @@
package com.seibel.distanthorizons.core.config.types; package com.seibel.distanthorizons.core.config.types;
import com.seibel.distanthorizons.core.config.NumberUtil; import com.seibel.distanthorizons.core.config.ConfigHandler;
import com.seibel.distanthorizons.core.util.NumberUtil;
import com.seibel.distanthorizons.core.config.file.ConfigFileHandler;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener; import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance; import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryPerformance; import com.seibel.distanthorizons.core.config.types.enums.EConfigValidity;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
@@ -33,40 +34,38 @@ import java.util.Arrays;
import java.util.function.Consumer; import java.util.function.Consumer;
/** /**
* Use for making the config variables * This config type allows for entering text, number, or enum values.
* for types that are not supported by it look in ConfigBase
* *
* @author coolGi * @author coolGi
* @version 2023-7-16
*/ */
public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implements IConfigEntry<T> public class ConfigEntry<T> extends AbstractConfigBase<T>
{ {
private String comment; private final String comment;
private T min; private T min;
private T max; private T max;
private final ArrayList<IConfigListener> listenerList; private final ArrayList<IConfigListener> listenerList;
private final String chatCommandName; private final String chatCommandName;
private final EConfigEntryPerformance performance;
// API control //
/** /**
* If true this config can be controlled by the API <br> * If true this config can be controlled by the API <br>
* and any get() method calls will return the apiValue if it is set. * and any get() method calls will return the apiValue if it is set.
*/ */
public final boolean allowApiOverride; private final boolean allowApiOverride;
/** Will be null if un-set */ /** Will be null if un-set */
@Nullable @Nullable
private T apiValue; private T apiValue;
/** Creates the entry */ //=============//
// constructor //
//=============//
private ConfigEntry( private ConfigEntry(
EConfigEntryAppearance appearance, EConfigEntryAppearance appearance,
T value, String comment, T min, T max, String comment, String chatCommandName,
String chatCommandName, boolean allowApiOverride, T value, T min, T max,
EConfigEntryPerformance performance, boolean allowApiOverride,
ArrayList<IConfigListener> listenerList) ArrayList<IConfigListener> listenerList)
{ {
super(appearance, value); super(appearance, value);
@@ -76,38 +75,57 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
this.max = max; this.max = max;
this.chatCommandName = chatCommandName; this.chatCommandName = chatCommandName;
this.allowApiOverride = allowApiOverride; this.allowApiOverride = allowApiOverride;
this.performance = performance;
this.listenerList = listenerList; this.listenerList = listenerList;
} }
/** Gets the default value of the option */ //==========================//
@Override // property getters/setters //
public T getDefaultValue() { return super.defaultValue; } //==========================//
/** the string used when entering the config into the command line or chat */
public String getChatCommandName() { return this.chatCommandName; }
public String getComment() { return this.comment; }
/**
* If true this config can be controlled by the API <br>
* and any get() method calls will return the apiValue if it is set.
*/
public boolean getAllowApiOverride() { return this.allowApiOverride; }
public T getMin() { return this.min; }
public void setMin(T newMin) { this.min = newMin; }
public T getMax() { return this.max; }
public void setMax(T newMax) { this.max = newMax; }
//===============//
// value setters //
//===============//
@Override
public void setApiValue(T newApiValue) public void setApiValue(T newApiValue)
{ {
this.apiValue = newApiValue; this.apiValue = newApiValue;
this.listenerList.forEach(IConfigListener::onConfigValueSet); this.listenerList.forEach(IConfigListener::onConfigValueSet);
} }
@Override
public T getApiValue() { return this.apiValue; }
@Override
public boolean getAllowApiOverride() { return this.allowApiOverride; }
/** public boolean apiIsOverriding()
* DONT USE THIS IN YOUR CODE <br> {
* Sets the value without informing the rest of the code (ie, doesnt call listeners, or saves the value). <br> return this.allowApiOverride
* Should only be used when loading the config from the file (in places like the {@link com.seibel.distanthorizons.core.config.file.ConfigFileHandling} or {@link com.seibel.distanthorizons.core.config.ConfigBase}) && this.apiValue != null;
*/
public void pureSet(T newValue) {
super.set(newValue);
} }
/**
* Should only be used when loading the config from file. <Br>
* Sets the value without informing the rest of the code (ie, it doesn't call listeners, or saving the value to file).
* @see ConfigFileHandler
*/
public void setWithoutFiringEvents(T newValue) { super.set(newValue); }
/** Sets the value without saving */ /** Sets the value without saving */
@Override
public void setWithoutSaving(T newValue) public void setWithoutSaving(T newValue)
{ {
super.set(newValue); super.set(newValue);
@@ -132,74 +150,35 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
} }
//===============//
// value getters //
//===============//
@Override @Override
public T get() public T get()
{ {
if (this.allowApiOverride && this.apiValue != null) if (this.allowApiOverride
&& this.apiValue != null)
{ {
return this.apiValue; return this.apiValue;
} }
return super.get(); return super.get();
} }
@Override /** Ignores the API value if set. */
public T getTrueValue() public T getTrueValue() { return super.get(); }
{
return super.get(); public T getDefaultValue() { return super.defaultValue; }
}
@Nullable
public T getApiValue() { return this.apiValue; }
/** Gets the min value */
@Override
public T getMin() { return this.min; }
/** Sets the min value */
@Override
public void setMin(T newMin) { this.min = newMin; }
/** Gets the max value */
@Override
public T getMax() { return this.max; }
/** Sets the max value */
@Override
public void setMax(T newMax) { this.max = newMax; }
/** Sets the min and max within a single setter */
@Override
public void setMinMax(T newMin, T newMax)
{
this.setMin(newMin);
this.setMax(newMax);
}
/** //===========//
* Clamps the value within the set range // listeners //
* //===========//
* @apiNote This does not save the value
*/
public void clampWithinRange() { this.clampWithinRange(this.min, this.max); }
/**
* Clamps the value within a set range
*
* @param min The minimum that the value can be
* @param max The maximum that the value can be
* @apiNote This does not save the value
*/
@SuppressWarnings("unchecked") // Suppress due to its always safe
public void clampWithinRange(T min, T max)
{
byte validness = this.isValid(min, max);
if (validness == -1) this.value = (T) NumberUtil.getMinimum(this.value.getClass());
if (validness == 1) this.value = (T) NumberUtil.getMaximum(this.value.getClass());
}
// TODO is this for command line use?
public String getChatCommandName() { return this.chatCommandName; }
@Override
public String getComment() { return this.comment; }
@Override
public void setComment(String newComment) { this.comment = newComment; }
/** Gets the performance impact of an option */
public EConfigEntryPerformance getPerformance() { return this.performance; }
/** Fired whenever the config value changes to a new value. */ /** Fired whenever the config value changes to a new value. */
public void addValueChangeListener(Consumer<T> onValueChangeFunc) public void addValueChangeListener(Consumer<T> onValueChangeFunc)
@@ -224,116 +203,114 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
public void setListeners(IConfigListener... newListeners) { this.listenerList.addAll(Arrays.asList(newListeners)); } public void setListeners(IConfigListener... newListeners) { this.listenerList.addAll(Arrays.asList(newListeners)); }
/**
* Checks if the option is valid //====================//
* // min/max validation //
* @return 0 == valid //====================//
* <p> 2 == invalid
* <p> 1 == number too high /** Checks if this config's current value is valid */
* <p> -1 == number too low public EConfigValidity getValidity() { return this.getValidity(this.value, this.min, this.max); }
*/ /** Checks if the given value is valid */
@Override public EConfigValidity getValidity(@Nullable T value) { return this.getValidity(value, this.min, this.max); }
public byte isValid() { return isValid(this.value, this.min, this.max); } /** Checks if the given value is valid */
/** public EConfigValidity getValidity(@Nullable T value, @Nullable T min, @Nullable T max)
* Checks if a new value is valid
*
* @param value Value that is being checked whether valid
* @return 0 == valid
* <p> 2 == invalid
* <p> 1 == number too high
* <p> -1 == number too low
*/
@Override
public byte isValid(T value) { return this.isValid(value, this.min, this.max); }
/**
* Checks if a new value is valid
*
* @param min The minimum that the value can be
* @param max The maximum that the value can be
* @return 0 == valid
* <p> 2 == invalid
* <p> 1 == number too high
* <p> -1 == number too low
*/
public byte isValid(T min, T max) { return this.isValid(this.value, min, max); }
/**
* Checks if a new value is valid
*
* @param value Value that is being checked whether valid
* @param min The minimum that the value can be
* @param max The maximum that the value can be
* @return 0 == valid
* <p> 2 == invalid
* <p> 1 == number too high
* <p> -1 == number too low
*/
public byte isValid(T value, T min, T max)
{ {
if (this.configBase.disableMinMax) if (!ConfigHandler.INSTANCE.runMinMaxValidation)
{ {
return 0; return EConfigValidity.VALID;
} }
else if (min == null && max == null) else if (min == null
&& max == null)
{ {
// no validation is needed for this field // no validation is needed for this field
return 0; return EConfigValidity.VALID;
} }
else if (value == null || this.value == null else if (value == null
|| this.value == null
|| value.getClass() != this.value.getClass()) || value.getClass() != this.value.getClass())
{ {
// If the 2 variables aren't the same type then it will be invalid // If the 2 variables aren't the same type
return 2; // or the input is missing
// then it will be invalid
return EConfigValidity.INVALID;
} }
else if (Number.class.isAssignableFrom(value.getClass())) else if (value instanceof Number)
{ {
// Only check min max if it is a number // Only check min/max if this config's type is a number
if (max != null && NumberUtil.greaterThan((Number) value, (Number) max)) if (max != null
&& NumberUtil.greaterThan((Number) value, (Number) max))
{ {
return 1; return EConfigValidity.NUMBER_TOO_HIGH;
}
if (min != null && NumberUtil.lessThan((Number) value, (Number) min))
{
return -1;
} }
return 0; if (min != null
&& NumberUtil.lessThan((Number) value, (Number) min))
{
return EConfigValidity.NUMBER_TOO_LOW;
}
return EConfigValidity.VALID;
} }
else else
{ {
return 0; return EConfigValidity.VALID;
} }
} }
//===============//
// file handling //
//===============//
/** This should normally not be called since set() automatically calls this */ /** This should normally not be called since set() automatically calls this */
public void save() { configBase.configFileINSTANCE.saveEntry(this); } public void save() { ConfigHandler.INSTANCE.configFileHandler.saveEntry(this); }
/** This should normally not be called except for special circumstances */ /** This should normally not be called except for special circumstances */
public void load() { configBase.configFileINSTANCE.loadEntry(this); } public void load() { ConfigHandler.INSTANCE.configFileHandler.loadEntry(this); }
@Override
public boolean equals(IConfigEntry<?> obj) { return obj.getClass() == ConfigEntry.class && equals((ConfigEntry<?>) obj); } //================//
// base overrides //
//================//
public boolean equals(AbstractConfigBase<?> obj)
{
return obj.getClass() == ConfigEntry.class
&& this.equals((ConfigEntry<?>) obj);
}
/** Is the value of this equal to another */ /** Is the value of this equal to another */
public boolean equals(ConfigEntry<?> obj) public boolean equals(ConfigEntry<?> obj)
{ {
// Can all of this just be "return this.value.equals(obj.value)"? // Can all of this just be "return this.value.equals(obj.value)"?
if (Number.class.isAssignableFrom(this.value.getClass())) if (Number.class.isAssignableFrom(this.value.getClass()))
{
return this.value == obj.value; return this.value == obj.value;
}
else else
{
return this.value.equals(obj.value); return this.value.equals(obj.value);
} }
}
public static class Builder<T> extends AbstractConfigType.Builder<T, Builder<T>>
//=========//
// builder //
//=========//
public static class Builder<T> extends AbstractConfigBase.Builder<T, Builder<T>>
{ {
private String tmpComment = null; private String tmpComment = null;
private T tmpMin = null; private T tmpMin = null;
private T tmpMax = null; private T tmpMax = null;
protected String tmpChatCommandName = null; protected String tmpChatCommandName = null;
private boolean tmpUseApiOverwrite = true; private boolean tmpUseApiOverwrite = true;
private EConfigEntryPerformance tmpPerformance = EConfigEntryPerformance.DONT_SHOW;
protected ArrayList<IConfigListener> tmpIConfigListener = new ArrayList<>(); protected ArrayList<IConfigListener> tmpIConfigListener = new ArrayList<>();
public Builder<T> comment(String newComment) public Builder<T> comment(String newComment)
{ {
this.tmpComment = newComment; this.tmpComment = newComment;
@@ -379,12 +356,6 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
return this; return this;
} }
public Builder<T> setPerformance(EConfigEntryPerformance newPerformance)
{
this.tmpPerformance = newPerformance;
return this;
}
public Builder<T> replaceListeners(ArrayList<IConfigListener> newConfigListener) public Builder<T> replaceListeners(ArrayList<IConfigListener> newConfigListener)
@@ -413,13 +384,15 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
// build //
public ConfigEntry<T> build() public ConfigEntry<T> build()
{ {
return new ConfigEntry<>( return new ConfigEntry<>(
this.tmpAppearance, this.tmpAppearance,
this.tmpValue, this.tmpComment, this.tmpMin, this.tmpMax, this.tmpComment, this.tmpChatCommandName, this.tmpValue, this.tmpMin, this.tmpMax,
this.tmpChatCommandName, this.tmpUseApiOverwrite, this.tmpUseApiOverwrite,
this.tmpPerformance, this.tmpIConfigListener); this.tmpIConfigListener);
} }
} }
@@ -21,29 +21,42 @@ package com.seibel.distanthorizons.core.config.types;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance; import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance;
public class ConfigUIButton extends AbstractConfigType<Runnable, ConfigUIButton> public class ConfigUIButton extends AbstractConfigBase<Runnable>
{ {
public ConfigUIButton(Runnable runnable) //=============//
{ // constructor //
super(EConfigEntryAppearance.ONLY_IN_GUI, runnable); //=============//
}
/** Runs the action of the button. NOTE: Will run on the main thread (so can halt the main process if not offloaded to a different thread) */ public ConfigUIButton(Runnable runnable)
{ super(EConfigEntryAppearance.ONLY_IN_GUI, runnable); }
//=========//
// actions //
//=========//
/**
* Runs the action of the button.
* NOTE: This will run on the render thread
* (so it can halt the main process if it takes too long and isn't offloaded to another thread)
*/
public void runAction() { this.value.run(); } public void runAction() { this.value.run(); }
public static class Builder extends AbstractConfigType.Builder<Runnable, Builder>
//=========//
// builder //
//=========//
public static class Builder extends AbstractConfigBase.Builder<Runnable, Builder>
{ {
/** Appearance shouldn't be changed */ /** Appearance shouldn't be changed */
@Override @Override
public Builder setAppearance(EConfigEntryAppearance newAppearance) public Builder setAppearance(EConfigEntryAppearance newAppearance) { return this; }
{
return this;
}
public ConfigUIButton build() public ConfigUIButton build()
{ { return new ConfigUIButton(this.tmpValue); }
return new ConfigUIButton(this.tmpValue);
}
} }
@@ -19,20 +19,47 @@
package com.seibel.distanthorizons.core.config.types; package com.seibel.distanthorizons.core.config.types;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.types.enums.EConfigCommentTextPosition;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance; import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/** /**
* Adds something like a ConfigEntry but without a button to change the input * Adds something like a ConfigEntry but without a button to change the input
* *
* @author coolGi * @author coolGi
*/ */
public class ConfigUIComment extends AbstractConfigType<String, ConfigUIComment> public class ConfigUIComment extends AbstractConfigBase<String>
{ {
public ConfigUIComment() private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public String parentConfigPath = null;
@Nullable
public EConfigCommentTextPosition textPosition = null;
//=============//
// constructor //
//=============//
public ConfigUIComment(String parentConfigPath, @Nullable EConfigCommentTextPosition textPosition)
{ {
super(EConfigEntryAppearance.ONLY_IN_GUI, ""); super(EConfigEntryAppearance.ONLY_IN_GUI, "");
this.parentConfigPath = parentConfigPath;
this.textPosition = textPosition;
} }
//=========//
// setters //
//=========//
/** Appearance shouldn't be changed */ /** Appearance shouldn't be changed */
@Override @Override
public void setAppearance(EConfigEntryAppearance newAppearance) { } public void setAppearance(EConfigEntryAppearance newAppearance) { }
@@ -41,27 +68,112 @@ public class ConfigUIComment extends AbstractConfigType<String, ConfigUIComment>
@Override @Override
public void set(String newValue) { } public void set(String newValue) { }
public static class Builder extends AbstractConfigType.Builder<String, Builder>
//=========//
// builder //
//=========//
public static class Builder extends AbstractConfigBase.Builder<String, Builder>
{ {
public String tempParentConfigPath = null;
@Nullable
public EConfigCommentTextPosition tempTextPosition = null;
/** Appearance shouldn't be changed */ /** Appearance shouldn't be changed */
@Deprecated
@Override @Override
public Builder setAppearance(EConfigEntryAppearance newAppearance) public Builder setAppearance(EConfigEntryAppearance newAppearance) { return this; }
{
return this;
}
/** Pointless to set the value */ /** Pointless to set the value */
@Deprecated
@Override @Override
public Builder set(String newValue) public Builder set(String newValue)
{ return this; }
public Builder setParentConfigClass(@NotNull Class<?> parentConfigClass)
{ {
// expected format: "Config.Client.Advanced"
String packageName = parentConfigClass.getPackage().getName(); // com.seibel.distanthorizons.core.config
String fullName = parentConfigClass.getName(); // com.seibel.distanthorizons.core.config.Config$Common$MultiThreading
try
{
String configPath = fullName.substring(
packageName.length() + // "com.seibel.distanthorizons.core.config"
1 + // "." before "Config"
Config.class.getSimpleName().length() + // "Config"
1); // "$" before the inner class name
// configPath after substring:
// Config$Common$MultiThreading
this.tempParentConfigPath = convertPackageNameToLangPath(configPath); // client.advanced.graphics.Quality
}
catch (Exception e)
{
this.tempParentConfigPath = parentConfigClass.getSimpleName();
LOGGER.warn("Failed to parse config class: ["+fullName+"], error: ["+e.getMessage()+"], defaulting to: ["+this.tempParentConfigPath+"].", e);
}
return this;
}
/**
* example:
* input: "Client$Advanced$multiThreading"
* output: "client.advanced.multiThreading"
*/
public static String convertPackageNameToLangPath(String input)
{
StringBuilder result = new StringBuilder(input.length());
for (int i = 0; i < input.length(); i++)
{
char ch = input.charAt(i);
if (i == 0)
{
result.append(Character.toLowerCase(ch));
continue;
}
// replace '$' -> '.' to match lang path naming
if (ch == '$')
{
result.append('.');
continue;
}
char lastCh = input.charAt(i-1);
if (lastCh == '$')
{
result.append(Character.toLowerCase(ch));
continue;
}
result.append(ch);
}
return result.toString();
}
public Builder setTextPosition(EConfigCommentTextPosition textPosition)
{
this.tempTextPosition = textPosition;
return this; return this;
} }
// build //
public ConfigUIComment build() public ConfigUIComment build()
{ { return new ConfigUIComment(this.tempParentConfigPath, this.tempTextPosition); }
return new ConfigUIComment();
}
} }
} }
@@ -0,0 +1,71 @@
/*
* 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.types;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance;
/**
* Adds empty space the height of a button.
* Useful for separating different categories.
*/
public class ConfigUISpacer extends AbstractConfigBase<String>
{
//=============//
// constructor //
//=============//
public ConfigUISpacer()
{ super(EConfigEntryAppearance.ONLY_IN_GUI, ""); }
//=========//
// setters //
//=========//
/** Appearance shouldn't be changed */
@Override
public void setAppearance(EConfigEntryAppearance newAppearance) { }
/** Pointless to set the value */
@Override
public void set(String newValue) { }
//=========//
// builder //
//=========//
public static class Builder extends AbstractConfigBase.Builder<String, Builder>
{
/** Appearance shouldn't be changed */
@Override
public Builder setAppearance(EConfigEntryAppearance newAppearance) { return this; }
/** Pointless to set the value */
@Override
public Builder set(String newValue) { return this; }
public ConfigUISpacer build() { return new ConfigUISpacer(); }
}
}
@@ -23,17 +23,24 @@ import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance
/** /**
* Creates a UI element that copies everything from another element. * Creates a UI element that copies everything from another element.
* This only effects the UI * This element is only visible in the GUI.
* *
* @author coolGi * @author coolGi
*/ */
@Deprecated // FIXME doesn't work with localization public class ConfigUiLinkedEntry extends AbstractConfigBase<AbstractConfigBase<?>>
public class ConfigUiLinkedEntry extends AbstractConfigType<AbstractConfigType<?, ?>, ConfigUiLinkedEntry>
{ {
public ConfigUiLinkedEntry(AbstractConfigType<?, ?> value) //=============//
{ // constructor //
super(EConfigEntryAppearance.ONLY_IN_GUI, value); //=============//
}
public ConfigUiLinkedEntry(AbstractConfigBase<?> value)
{ super(EConfigEntryAppearance.ONLY_IN_GUI, value); }
//=========//
// setters //
//=========//
/** Appearance shouldn't be changed */ /** Appearance shouldn't be changed */
@Override @Override
@@ -41,10 +48,15 @@ public class ConfigUiLinkedEntry extends AbstractConfigType<AbstractConfigType<?
/** Value shouldn't be changed after creation */ /** Value shouldn't be changed after creation */
@Override @Override
public void set(AbstractConfigType<?, ?> newValue) { } public void set(AbstractConfigBase<?> newValue) { }
public static class Builder extends AbstractConfigType.Builder<AbstractConfigType<?, ?>, Builder>
//=========//
// builder //
//=========//
public static class Builder extends AbstractConfigBase.Builder<AbstractConfigBase<?>, Builder>
{ {
/** Appearance shouldn't be changed */ /** Appearance shouldn't be changed */
@Override @Override
@@ -60,4 +72,6 @@ public class ConfigUiLinkedEntry extends AbstractConfigType<AbstractConfigType<?
} }
} }
@@ -20,19 +20,13 @@
package com.seibel.distanthorizons.core.config.types.enums; package com.seibel.distanthorizons.core.config.types.enums;
/** /**
* What is the performance impact of an entry * RIGHT_OVER_BUTTONS <br/>
* (default is DONT_SHOW) * CENTER_OF_SCREEN <br/>
* * CENTERED_OVER_BUTTONS <br/>
* @author coolGi
*/ */
public enum EConfigEntryPerformance public enum EConfigCommentTextPosition
{ {
NONE, RIGHT_JUSTIFIED,
VERY_LOW, CENTER_OF_SCREEN,
LOW, CENTERED_OVER_BUTTONS,
MEDIUM,
HIGH,
VERY_HIGH,
DONT_SHOW
} }
@@ -0,0 +1,15 @@
package com.seibel.distanthorizons.core.config.types.enums;
/**
* VALID
* INVALID
* NUMBER_TOO_HIGH
* NUMBER_TOO_LOW
*/
public enum EConfigValidity
{
VALID,
INVALID,
NUMBER_TOO_HIGH,
NUMBER_TOO_LOW,
}
@@ -0,0 +1,151 @@
package com.seibel.distanthorizons.core.dataObjects;
import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import java.util.concurrent.ConcurrentHashMap;
/**
* A pooled compound key between the biome and blockState. <br>
* These objects are pooled since we will need this compound key
* many times.
*
* @see FullDataPointIdMap
* @see IBlockStateWrapper
* @see IBiomeWrapper
*/
public class BlockBiomeWrapperPair
{
private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
/** two levels are present so we don't need to use a key object */
private static final ConcurrentHashMap<IBlockStateWrapper, ConcurrentHashMap<IBiomeWrapper, BlockBiomeWrapperPair>> CACHED_PAIR_BY_BIOME_BY_BLOCK = new ConcurrentHashMap<>();
public final IBiomeWrapper biome;
public final IBlockStateWrapper blockState;
private int hashCode = 0;
private boolean hashGenerated = false;
private String serialString = null;
//=============//
// constructor //
//=============//
public static BlockBiomeWrapperPair get(IBlockStateWrapper blockState, IBiomeWrapper biome)
{
// check for existing entry
ConcurrentHashMap<IBiomeWrapper, BlockBiomeWrapperPair> pairByBiomeWrapper = CACHED_PAIR_BY_BIOME_BY_BLOCK.get(blockState);
if (pairByBiomeWrapper != null)
{
BlockBiomeWrapperPair pair = pairByBiomeWrapper.get(biome);
if (pair != null)
{
return pair;
}
}
// Lazily create the inner map and new BlockBiomeWrapperPair
return CACHED_PAIR_BY_BIOME_BY_BLOCK
.computeIfAbsent(blockState, newBlockState -> new ConcurrentHashMap<>())
.computeIfAbsent(biome, newBiome -> new BlockBiomeWrapperPair(biome, blockState));
}
private BlockBiomeWrapperPair(IBiomeWrapper biome, IBlockStateWrapper blockState)
{
this.biome = biome;
this.blockState = blockState;
}
//===========//
// overrides //
//===========//
/**
* Reminder: this hash code won't always be unique, collisions can occur;
* because of that this hash shouldn't be the only unique identifier for this object.
*/
@Override
public int hashCode()
{
// cache the hash code to improve speed
if (!this.hashGenerated)
{
this.hashCode = generateHashCode(this);
this.hashGenerated = true;
}
return this.hashCode;
}
private static int generateHashCode(BlockBiomeWrapperPair pair) { return generateHashCode(pair.biome, pair.blockState); }
private static int generateHashCode(IBiomeWrapper biome, IBlockStateWrapper blockState)
{
final int prime = 31;
int result = 1;
// the biome and blockstate hashcode should be already calculated by the time
// we get here, so this operation should be very fast
result = prime * result + (biome == null ? 0 : biome.hashCode());
result = prime * result + (blockState == null ? 0 : blockState.hashCode());
return result;
}
@Override
public boolean equals(Object otherObj)
{
if (otherObj == this)
{
return true;
}
if (!(otherObj instanceof BlockBiomeWrapperPair))
{
return false;
}
BlockBiomeWrapperPair other = (BlockBiomeWrapperPair) otherObj;
return other.biome.getSerialString().equals(this.biome.getSerialString())
&& other.blockState.getSerialString().equals(this.blockState.getSerialString());
}
@Override
public String toString() { return this.serialize(); }
//=================//
// (de)serializing //
//=================//
public String serialize()
{
if (this.serialString == null)
{
this.serialString = this.biome.getSerialString() + FullDataPointIdMap.BLOCK_STATE_SEPARATOR_STRING + this.blockState.getSerialString();
}
return this.serialString;
}
public static BlockBiomeWrapperPair deserialize(String str, ILevelWrapper levelWrapper) throws DataCorruptedException
{
int separatorIndex = str.indexOf(FullDataPointIdMap.BLOCK_STATE_SEPARATOR_STRING);
if (separatorIndex == -1)
{
throw new DataCorruptedException("Failed to deserialize BiomeBlockStateEntry ["+str+"], unable to find separator.");
}
IBiomeWrapper biome = WRAPPER_FACTORY.deserializeBiomeWrapperOrGetDefault(str.substring(0, separatorIndex), levelWrapper);
IBlockStateWrapper blockState = WRAPPER_FACTORY.deserializeBlockStateWrapperOrGetDefault(str.substring(separatorIndex+FullDataPointIdMap.BLOCK_STATE_SEPARATOR_STRING.length()), levelWrapper);
return BlockBiomeWrapperPair.get(blockState, biome);
}
}
@@ -19,7 +19,8 @@
package com.seibel.distanthorizons.core.dataObjects.fullData; package com.seibel.distanthorizons.core.dataObjects.fullData;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dataObjects.BlockBiomeWrapperPair;
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.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
@@ -27,16 +28,12 @@ import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStrea
import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataOutputStream; import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataOutputStream;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.*; import java.io.*;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/** /**
* WARNING: This is not THREAD-SAFE! <br><br> * WARNING: This is not THREAD-SAFE! <br><br>
@@ -53,7 +50,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
*/ */
public class FullDataPointIdMap public class FullDataPointIdMap
{ {
private static final Logger LOGGER = LogManager.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
/** /**
* Should only be enabled when debugging. * Should only be enabled when debugging.
* Has the system check if any duplicate Entries were read/written * Has the system check if any duplicate Entries were read/written
@@ -61,15 +58,15 @@ public class FullDataPointIdMap
*/ */
private static final boolean RUN_SERIALIZATION_DUPLICATE_VALIDATION = false; private static final boolean RUN_SERIALIZATION_DUPLICATE_VALIDATION = false;
/** Distant Horizons - Block State Wrapper */ /** Distant Horizons - Block State Wrapper */
private static final String BLOCK_STATE_SEPARATOR_STRING = "_DH-BSW_"; public static final String BLOCK_STATE_SEPARATOR_STRING = "_DH-BSW_";
/** should only be used for debugging */ /** should only be used for debugging */
private long pos; private long pos;
/** The index should be the same as the Entry's ID */ /** The index should be the same as the BlockBiomeWrapperPair's ID */
private final ArrayList<Entry> entryList = new ArrayList<>(); private final ArrayList<BlockBiomeWrapperPair> blockBiomePairList = new ArrayList<>();
private final ConcurrentHashMap<Entry, Integer> idMap = new ConcurrentHashMap<>(); private final ConcurrentHashMap<BlockBiomeWrapperPair, Integer> idMap = new ConcurrentHashMap<>();
private int cachedHashCode = 0; private int cachedHashCode = 0;
@@ -91,28 +88,28 @@ public class FullDataPointIdMap
public IBiomeWrapper getBiomeWrapper(int id) throws IndexOutOfBoundsException { return this.getEntry(id).biome; } public IBiomeWrapper getBiomeWrapper(int id) throws IndexOutOfBoundsException { return this.getEntry(id).biome; }
/** @see FullDataPointIdMap#getEntry(int) */ /** @see FullDataPointIdMap#getEntry(int) */
public IBlockStateWrapper getBlockStateWrapper(int id) throws IndexOutOfBoundsException { return this.getEntry(id).blockState; } public IBlockStateWrapper getBlockStateWrapper(int id) throws IndexOutOfBoundsException { return this.getEntry(id).blockState; }
/** @throws IndexOutOfBoundsException if the given ID isn't in the {@link FullDataPointIdMap#entryList} */ /** @throws IndexOutOfBoundsException if the given ID isn't in the {@link FullDataPointIdMap#blockBiomePairList} */
private Entry getEntry(int id) throws IndexOutOfBoundsException private BlockBiomeWrapperPair getEntry(int id) throws IndexOutOfBoundsException
{ {
Entry entry; BlockBiomeWrapperPair pair;
try try
{ {
entry = this.entryList.get(id); pair = this.blockBiomePairList.get(id);
} }
catch (IndexOutOfBoundsException e) catch (IndexOutOfBoundsException e)
{ {
throw new IndexOutOfBoundsException("FullData ID Map out of sync for pos: "+this.pos+". ID: ["+id+"] greater than the number of known ID's: ["+this.entryList.size()+"]."); throw new IndexOutOfBoundsException("FullData ID Map out of sync for pos: "+DhSectionPos.toString(this.pos)+". ID: ["+id+"] greater than the number of known ID's: ["+this.blockBiomePairList.size()+"].");
} }
return entry; return pair;
} }
/** @return -1 if the list is empty */ /** @return -1 if the list is empty */
public int getMaxValidId() { return this.entryList.size() - 1; } public int getMaxValidId() { return this.blockBiomePairList.size() - 1; }
public int size() { return this.entryList.size(); } public int size() { return this.blockBiomePairList.size(); }
public boolean isEmpty() { return this.entryList.isEmpty(); } public boolean isEmpty() { return this.blockBiomePairList.isEmpty(); }
public long getPos() { return this.pos; } public long getPos() { return this.pos; }
@@ -126,11 +123,11 @@ public class FullDataPointIdMap
* If an entry with the given values already exists nothing will * If an entry with the given values already exists nothing will
* be added but the existing item's ID will still be returned. * be added but the existing item's ID will still be returned.
*/ */
public int addIfNotPresentAndGetId(IBiomeWrapper biome, IBlockStateWrapper blockState) { return this.addIfNotPresentAndGetId(Entry.getEntry(biome, blockState)); } public int addIfNotPresentAndGetId(IBiomeWrapper biome, IBlockStateWrapper blockState) { return this.addIfNotPresentAndGetId(BlockBiomeWrapperPair.get(blockState, biome)); }
private int addIfNotPresentAndGetId(Entry biomeBlockStateEntry) private int addIfNotPresentAndGetId(BlockBiomeWrapperPair pair)
{ {
// try getting the existing ID // try getting the existing ID
Integer nullableId = this.idMap.get(biomeBlockStateEntry); Integer nullableId = this.idMap.get(pair);
if (nullableId != null) if (nullableId != null)
{ {
return nullableId; return nullableId;
@@ -138,7 +135,7 @@ public class FullDataPointIdMap
// create the new ID // create the new ID
return this.idMap.compute(biomeBlockStateEntry, (Entry newBiomeBlockStateEntry, Integer currentId) -> return this.idMap.compute(pair, (BlockBiomeWrapperPair newPair, Integer currentId) ->
{ {
if (currentId != null) if (currentId != null)
{ {
@@ -147,8 +144,8 @@ public class FullDataPointIdMap
// Add the new ID // Add the new ID
currentId = this.entryList.size(); currentId = this.blockBiomePairList.size();
this.entryList.add(biomeBlockStateEntry); this.blockBiomePairList.add(newPair);
// invalidate the cached hash code // invalidate the cached hash code
this.cachedHashCode = 0; this.cachedHashCode = 0;
@@ -158,7 +155,7 @@ public class FullDataPointIdMap
} }
/** /**
* Adds every {@link Entry} from inputMap into this map. <br> * Adds every {@link BlockBiomeWrapperPair} from inputMap into this map. <br>
* Allows duplicate entries. <br><br> * Allows duplicate entries. <br><br>
* *
* Allowing duplicate entries should be done if a datasource is just being read in and * Allowing duplicate entries should be done if a datasource is just being read in and
@@ -168,19 +165,19 @@ public class FullDataPointIdMap
*/ */
public void addAll(FullDataPointIdMap inputMap) public void addAll(FullDataPointIdMap inputMap)
{ {
ArrayList<Entry> entriesToMerge = inputMap.entryList; ArrayList<BlockBiomeWrapperPair> pairsToMerge = inputMap.blockBiomePairList;
for (int i = 0; i < entriesToMerge.size(); i++) for (int i = 0; i < pairsToMerge.size(); i++)
{ {
Entry entity = entriesToMerge.get(i); BlockBiomeWrapperPair pair = pairsToMerge.get(i);
this.add(entity); this.add(pair);
} }
} }
/** allows for adding duplicate {@link Entry} */ /** allows for adding duplicate {@link BlockBiomeWrapperPair} */
private void add(Entry biomeBlockStateEntry) private void add(BlockBiomeWrapperPair pair)
{ {
int id = this.entryList.size(); int id = this.blockBiomePairList.size();
this.entryList.add(biomeBlockStateEntry); this.blockBiomePairList.add(pair);
this.idMap.put(biomeBlockStateEntry, id); this.idMap.put(pair, id);
// invalidate the cached hash code // invalidate the cached hash code
this.cachedHashCode = 0; this.cachedHashCode = 0;
@@ -197,23 +194,23 @@ public class FullDataPointIdMap
*/ */
public int[] mergeAndReturnRemappedEntityIds(FullDataPointIdMap inputMap) public int[] mergeAndReturnRemappedEntityIds(FullDataPointIdMap inputMap)
{ {
ArrayList<Entry> entriesToMerge = inputMap.entryList; ArrayList<BlockBiomeWrapperPair> entriesToMerge = inputMap.blockBiomePairList;
int[] remappedEntryIds = new int[entriesToMerge.size()]; int[] remappedPairIds = new int[entriesToMerge.size()];
for (int i = 0; i < entriesToMerge.size(); i++) for (int i = 0; i < entriesToMerge.size(); i++)
{ {
Entry entity = entriesToMerge.get(i); BlockBiomeWrapperPair entity = entriesToMerge.get(i);
int id = this.addIfNotPresentAndGetId(entity); int id = this.addIfNotPresentAndGetId(entity);
remappedEntryIds[i] = id; remappedPairIds[i] = id;
} }
return remappedEntryIds; return remappedPairIds;
} }
/** Should only be used if this map is going to be reused, otherwise bad things will happen. */ /** Should only be used if this map is going to be reused, otherwise bad things will happen. */
public void clear(long pos) public void clear(long pos)
{ {
this.pos = pos; this.pos = pos;
this.entryList.clear(); this.blockBiomePairList.clear();
this.idMap.clear(); this.idMap.clear();
this.cachedHashCode = 0; this.cachedHashCode = 0;
} }
@@ -227,27 +224,27 @@ public class FullDataPointIdMap
/** Serializes all contained entries into the given stream, formatted in UTF */ /** Serializes all contained entries into the given stream, formatted in UTF */
public void serialize(DhDataOutputStream outputStream) throws IOException public void serialize(DhDataOutputStream outputStream) throws IOException
{ {
outputStream.writeInt(this.entryList.size()); outputStream.writeInt(this.blockBiomePairList.size());
// only used when debugging // only used when debugging
HashMap<String, FullDataPointIdMap.Entry> dataPointEntryBySerialization = new HashMap<>(); HashMap<String, BlockBiomeWrapperPair> dataPointEntryBySerialization = new HashMap<>();
for (Entry entry : this.entryList) for (BlockBiomeWrapperPair pair : this.blockBiomePairList)
{ {
String entryString = entry.serialize(); String entryString = pair.serialize();
outputStream.writeUTF(entryString); outputStream.writeUTF(entryString);
if (RUN_SERIALIZATION_DUPLICATE_VALIDATION) if (RUN_SERIALIZATION_DUPLICATE_VALIDATION)
{ {
if (dataPointEntryBySerialization.containsKey(entryString)) if (dataPointEntryBySerialization.containsKey(entryString))
{ {
LOGGER.error("Duplicate serialized entry found with serial: " + entryString); LOGGER.error("Duplicate serialized pair found with serial: " + entryString);
} }
if (dataPointEntryBySerialization.containsValue(entry)) if (dataPointEntryBySerialization.containsValue(pair))
{ {
LOGGER.error("Duplicate serialized entry found with value: " + entry.serialize()); LOGGER.error("Duplicate serialized pair found with value: " + pair.serialize());
} }
dataPointEntryBySerialization.put(entryString, entry); dataPointEntryBySerialization.put(entryString, pair);
} }
} }
} }
@@ -263,7 +260,7 @@ public class FullDataPointIdMap
// only used when debugging // only used when debugging
HashMap<String, FullDataPointIdMap.Entry> dataPointEntryBySerialization = new HashMap<>(); HashMap<String, BlockBiomeWrapperPair> dataPointEntryBySerialization = new HashMap<>();
FullDataPointIdMap newMap = new FullDataPointIdMap(pos); FullDataPointIdMap newMap = new FullDataPointIdMap(pos);
for (int i = 0; i < entityCount; i++) for (int i = 0; i < entityCount; i++)
@@ -271,13 +268,13 @@ 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.");
} }
String entryString = inputStream.readUTF(); String entryString = inputStream.readUTF();
Entry newEntry = Entry.deserialize(entryString, levelWrapper); BlockBiomeWrapperPair newPair = BlockBiomeWrapperPair.deserialize(entryString, levelWrapper);
newMap.entryList.add(newEntry); newMap.blockBiomePairList.add(newPair);
if (RUN_SERIALIZATION_DUPLICATE_VALIDATION) if (RUN_SERIALIZATION_DUPLICATE_VALIDATION)
{ {
@@ -285,11 +282,11 @@ public class FullDataPointIdMap
{ {
LOGGER.error("Duplicate deserialized entry found with serial: " + entryString); LOGGER.error("Duplicate deserialized entry found with serial: " + entryString);
} }
if (dataPointEntryBySerialization.containsValue(newEntry)) if (dataPointEntryBySerialization.containsValue(newPair))
{ {
LOGGER.error("Duplicate deserialized entry found with value: " + newEntry.serialize()); LOGGER.error("Duplicate deserialized entry found with value: " + newPair.serialize());
} }
dataPointEntryBySerialization.put(entryString, newEntry); dataPointEntryBySerialization.put(entryString, newPair);
} }
} }
@@ -335,144 +332,13 @@ public class FullDataPointIdMap
private void generateHashCode() private void generateHashCode()
{ {
int result = DhSectionPos.hashCode(this.pos); int result = DhSectionPos.hashCode(this.pos);
for (int i = 0; i < this.entryList.size(); i++) for (int i = 0; i < this.blockBiomePairList.size(); i++)
{ {
result = 31 * result + this.entryList.hashCode(); result = 31 * result + this.blockBiomePairList.hashCode();
} }
this.cachedHashCode = result; this.cachedHashCode = result;
} }
//==============//
// helper class //
//==============//
private static final class Entry
{
private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
private static final ConcurrentHashMap<Integer, Entry> ENTRY_BY_HASH = new ConcurrentHashMap<>();
/** lock is necessary since {@link Int2ReferenceOpenHashMap} isn't concurrent and concurrent threads can cause infinite loops */
private static final ReentrantReadWriteLock ENTRY_POOL_LOCK = new ReentrantReadWriteLock();
public final IBiomeWrapper biome;
public final IBlockStateWrapper blockState;
private Integer hashCode = null;
private String serialString = null;
//=============//
// constructor //
//=============//
public static Entry getEntry(IBiomeWrapper biome, IBlockStateWrapper blockState)
{
int entryHash = generateHashCode(biome, blockState);
// try getting the existing Entry
Entry entry = ENTRY_BY_HASH.get(entryHash);
if (entry != null)
{
return entry;
}
// create the missing entry
return ENTRY_BY_HASH.compute(entryHash, (Integer newHash, Entry currentEntry) ->
{
if (currentEntry != null)
{
return currentEntry;
}
return new Entry(biome, blockState);
});
}
private Entry(IBiomeWrapper biome, IBlockStateWrapper blockState)
{
this.biome = biome;
this.blockState = blockState;
}
//===========//
// overrides //
//===========//
@Override
public int hashCode()
{
// cache the hash code to improve speed
if (this.hashCode == null)
{
this.hashCode = generateHashCode(this);
}
return this.hashCode;
}
private static int generateHashCode(Entry entry) { return generateHashCode(entry.biome, entry.blockState); }
private static int generateHashCode(IBiomeWrapper biome, IBlockStateWrapper blockState)
{
final int prime = 31;
int result = 1;
// the biome and blockstate hashcode should be already calculated by the time
// we get here, so this operation should be very fast
result = prime * result + (biome == null ? 0 : biome.hashCode());
result = prime * result + (blockState == null ? 0 : blockState.hashCode());
return result;
}
@Override
public boolean equals(Object otherObj)
{
if (otherObj == this)
return true;
if (!(otherObj instanceof Entry))
return false;
Entry other = (Entry) otherObj;
return other.biome.getSerialString().equals(this.biome.getSerialString())
&& other.blockState.getSerialString().equals(this.blockState.getSerialString());
}
@Override
public String toString() { return this.serialize(); }
//=================//
// (de)serializing //
//=================//
public String serialize()
{
if (this.serialString == null)
{
this.serialString = this.biome.getSerialString() + BLOCK_STATE_SEPARATOR_STRING + this.blockState.getSerialString();
}
return this.serialString;
}
public static Entry deserialize(String str, ILevelWrapper levelWrapper) throws DataCorruptedException
{
int separatorIndex = str.indexOf(BLOCK_STATE_SEPARATOR_STRING);
if (separatorIndex == -1)
{
throw new DataCorruptedException("Failed to deserialize BiomeBlockStateEntry ["+str+"], unable to find separator.");
}
IBiomeWrapper biome = WRAPPER_FACTORY.deserializeBiomeWrapperOrGetDefault(str.substring(0, separatorIndex), levelWrapper);
IBlockStateWrapper blockState = WRAPPER_FACTORY.deserializeBlockStateWrapperOrGetDefault(str.substring(separatorIndex+BLOCK_STATE_SEPARATOR_STRING.length()), levelWrapper);
return Entry.getEntry(biome, blockState);
}
}
} }
@@ -20,8 +20,8 @@
package com.seibel.distanthorizons.core.dataObjects.fullData.sources; package com.seibel.distanthorizons.core.dataObjects.fullData.sources;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep;
import com.seibel.distanthorizons.core.file.IDataSource;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
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.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV1DTO; import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV1DTO;
@@ -33,7 +33,6 @@ import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataOutputStre
import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import org.apache.logging.log4j.Logger;
import java.io.*; import java.io.*;
import java.util.Arrays; import java.util.Arrays;
@@ -47,9 +46,9 @@ import java.util.Arrays;
* @see FullDataPointUtil * @see FullDataPointUtil
* @see FullDataSourceV2 * @see FullDataSourceV2
*/ */
public class FullDataSourceV1 implements IDataSource<IDhLevel> public class FullDataSourceV1
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public static final byte SECTION_SIZE_OFFSET = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; public static final byte SECTION_SIZE_OFFSET = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL;
/** measured in dataPoints */ /** measured in dataPoints */
@@ -94,29 +93,14 @@ public class FullDataSourceV1 implements IDataSource<IDhLevel>
} }
//======//
// data //
//======//
@Deprecated
@Override
public boolean update(FullDataSourceV2 dataSource, IDhLevel level) { throw new UnsupportedOperationException("Deprecated"); }
//=====================// //=====================//
// setters and getters // // setters and getters //
//=====================// //=====================//
@Override
public Long getKey() { return this.pos; } public Long getKey() { return this.pos; }
@Override
public String getKeyDisplayString() { return DhSectionPos.toString(this.pos); } public String getKeyDisplayString() { return DhSectionPos.toString(this.pos); }
@Override public long getPos() { return this.pos; }
public Long getPos() { return this.pos; }
public void resizeDataStructuresForRepopulation(long pos) public void resizeDataStructuresForRepopulation(long pos)
{ {
@@ -124,7 +108,6 @@ public class FullDataSourceV1 implements IDataSource<IDhLevel>
this.pos = pos; this.pos = pos;
} }
@Override
public byte getDataDetailLevel() { return (byte) (DhSectionPos.getDetailLevel(this.pos) - SECTION_SIZE_OFFSET); } public byte getDataDetailLevel() { return (byte) (DhSectionPos.getDetailLevel(this.pos) - SECTION_SIZE_OFFSET); }
public boolean isEmpty() { return this.isEmpty; } public boolean isEmpty() { return this.isEmpty; }
@@ -197,7 +180,7 @@ public class FullDataSourceV1 implements IDataSource<IDhLevel>
{ {
outputStream.writeInt(this.getDataDetailLevel()); outputStream.writeInt(this.getDataDetailLevel());
outputStream.writeInt(WIDTH); outputStream.writeInt(WIDTH);
outputStream.writeInt(level.getMinY()); outputStream.writeInt(level.getLevelWrapper().getMinHeight());
outputStream.writeByte(this.worldGenStep.value); outputStream.writeByte(this.worldGenStep.value);
} }
@@ -206,19 +189,19 @@ public class FullDataSourceV1 implements IDataSource<IDhLevel>
int dataDetail = inputStream.readInt(); int dataDetail = inputStream.readInt();
if (dataDetail != dto.dataDetailLevel) if (dataDetail != dto.dataDetailLevel)
{ {
throw new IOException(LodUtil.formatLog("Data level mismatch. Expected: ["+dto.dataDetailLevel+"], found ["+dataDetail+"].")); throw new IOException("Data level mismatch. Expected: ["+dto.dataDetailLevel+"], found ["+dataDetail+"].");
} }
int width = inputStream.readInt(); int width = inputStream.readInt();
if (width != WIDTH) if (width != WIDTH)
{ {
throw new IOException(LodUtil.formatLog("Section width mismatch: " + width + " != " + WIDTH + " (Currently only 1 section width is supported)")); throw new IOException("Section width mismatch: [" + width + "] != [" + WIDTH + "] (Currently only 1 section width is supported)");
} }
int minY = inputStream.readInt(); int minY = inputStream.readInt();
if (minY != level.getMinY()) if (minY != level.getLevelWrapper().getMinHeight())
{ {
LOGGER.warn("Data minY mismatch: " + minY + " != " + level.getMinY() + ". Will ignore data's y level"); LOGGER.warn("Data minY mismatch: [" + minY + "] != [" + level.getLevelWrapper().getMinHeight() + "]. Will ignore data's y level");
} }
byte worldGenByte = inputStream.readByte(); byte worldGenByte = inputStream.readByte();
@@ -377,15 +360,6 @@ public class FullDataSourceV1 implements IDataSource<IDhLevel>
public void setIdMapping(FullDataPointIdMap mappings) { this.mapping.mergeAndReturnRemappedEntityIds(mappings); } public void setIdMapping(FullDataPointIdMap mappings) { this.mapping.mergeAndReturnRemappedEntityIds(mappings); }
//==================//
// override methods //
//==================//
@Override
public void close()
{ /* not currently needed */ }
//================// //================//
// helper classes // // helper classes //
@@ -23,25 +23,32 @@ import com.seibel.distanthorizons.api.enums.config.EDhApiWorldCompressionMode;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep;
import com.seibel.distanthorizons.api.objects.data.DhApiTerrainDataPoint; import com.seibel.distanthorizons.api.objects.data.DhApiTerrainDataPoint;
import com.seibel.distanthorizons.api.objects.data.IDhApiFullDataSource; import com.seibel.distanthorizons.api.objects.data.IDhApiFullDataSource;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap;
import com.seibel.distanthorizons.core.dataObjects.transformers.FullDataOcclusionCuller;
import com.seibel.distanthorizons.core.dataObjects.transformers.LodDataBuilder; import com.seibel.distanthorizons.core.dataObjects.transformers.LodDataBuilder;
import com.seibel.distanthorizons.core.file.AbstractDataSourceHandler; import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.file.IDataSource; import com.seibel.distanthorizons.core.file.fullDatafile.V2.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.level.IDhLevel; 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.pooling.PhantomArrayListParent; import com.seibel.distanthorizons.core.pooling.AbstractPhantomArrayList;
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.DhLodPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.sql.dto.util.FullDataMinMaxPosUtil;
import com.seibel.distanthorizons.core.util.*; import com.seibel.distanthorizons.core.util.*;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.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.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@@ -53,10 +60,10 @@ import java.util.List;
* @see FullDataSourceV1 * @see FullDataSourceV1
*/ */
public class FullDataSourceV2 public class FullDataSourceV2
extends PhantomArrayListParent extends AbstractPhantomArrayList
implements IDataSource<IDhLevel>, IDhApiFullDataSource implements IDhApiFullDataSource
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
/** useful for debugging, but can slow down update operations quite a bit due to being called so often. */ /** useful for debugging, but can slow down update operations quite a bit due to being called so often. */
private static final boolean RUN_UPDATE_DEV_VALIDATION = false; private static final boolean RUN_UPDATE_DEV_VALIDATION = false;
/** /**
@@ -71,8 +78,6 @@ public class FullDataSourceV2
/** how many chunks wide this datasource is at detail level 0. */ /** how many chunks wide this datasource is at detail level 0. */
public static final int NUMB_OF_CHUNKS_WIDE = WIDTH / LodUtil.CHUNK_WIDTH; public static final int NUMB_OF_CHUNKS_WIDE = WIDTH / LodUtil.CHUNK_WIDTH;
public static final byte DATA_FORMAT_VERSION = 1;
public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("FullDataV2"); public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("FullDataV2");
@@ -80,10 +85,6 @@ public class FullDataSourceV2
private int cachedHashCode = 0; private int cachedHashCode = 0;
private final long pos; private final long pos;
@Override
public Long getKey() { return this.pos; }
@Override
public String getKeyDisplayString() { return DhSectionPos.toString(this.pos); }
public final FullDataPointIdMap mapping; public final FullDataPointIdMap mapping;
@@ -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}
* *
@@ -109,9 +108,7 @@ public class FullDataSourceV2
/** /**
* stored x/z, y <br> * stored x/z, y <br>
* The y data should be sorted from top to bottom <br> * The y data should be sorted from top to bottom
* TODO that ordering feels weird, it'd be nice to reverse that order, unfortunately
* there's something in the render data logic that expects this order so we can't change it right now
*/ */
public final LongArrayList[] dataPoints; public final LongArrayList[] dataPoints;
@@ -132,7 +129,7 @@ public class FullDataSourceV2
// constructors // // constructors //
//==============// //==============//
public static FullDataSourceV2 createFromChunk(IChunkWrapper chunkWrapper) { return LodDataBuilder.createFromChunk(chunkWrapper); } public static FullDataSourceV2 createFromChunk(ILevelWrapper levelWrapper, IChunkWrapper chunkWrapper) { return LodDataBuilder.createFromChunk(levelWrapper, chunkWrapper); }
public static FullDataSourceV2 createFromLegacyDataSourceV1(FullDataSourceV1 legacyData) public static FullDataSourceV2 createFromLegacyDataSourceV1(FullDataSourceV1 legacyData)
{ {
@@ -221,7 +218,7 @@ public class FullDataSourceV2
private FullDataSourceV2( private FullDataSourceV2(
long pos, long pos,
FullDataPointIdMap mapping, @Nullable LongArrayList[] data, FullDataPointIdMap mapping, @Nullable LongArrayList[] data,
@Nullable byte[] columnGenerationSteps, @Nullable byte[] columnWorldCompressionMode, byte @Nullable [] columnGenerationSteps, byte @Nullable [] columnWorldCompressionMode,
boolean empty) boolean empty)
{ {
super(ARRAY_LIST_POOL, 2, 0, WIDTH * WIDTH); super(ARRAY_LIST_POOL, 2, 0, WIDTH * WIDTH);
@@ -278,15 +275,98 @@ public class FullDataSourceV2
//======// //=========//
// data // // getters //
//======// //=========//
public LongArrayList get(int relX, int relZ) throws IndexOutOfBoundsException { return this.dataPoints[relativePosToIndex(relX, relZ)]; } public LongArrayList getColumnAtRelPos(int relX, int relZ) throws IndexOutOfBoundsException
{ return this.dataPoints[relativePosToIndex(relX, relZ)]; }
@Override @Nullable
public boolean update(@NotNull FullDataSourceV2 inputDataSource, @Nullable IDhLevel level) { return this.update(inputDataSource); } public LongArrayList tryGetColumnAtRelPos(int relX, int relZ)
public boolean update(@NotNull FullDataSourceV2 inputDataSource) {
int index = tryGetRelativePosToIndex(relX, relZ);
if (index == -1)
{
return null;
}
return this.dataPoints[index];
}
/**
* returns {@link FullDataPointUtil#EMPTY_DATA_POINT} if the given {@link DhBlockPos}
* is outside this data source's boundaries.
*/
public long getDataPointAtBlockPos(int blockPosX, int blockPosY, int blockPosZ, int levelMinY)
{
DhLodPos requestedPos = new DhLodPos(LodUtil.BLOCK_DETAIL_LEVEL, blockPosX, blockPosZ);
// stop if the requested blockPos is outside this datasource
{
// get the detail levels for this request
byte requestedDetailLevel = requestedPos.detailLevel;
byte requestedSectionDetailLevel = (byte) (requestedDetailLevel + DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
// get the positions for this request
long sectionPos = requestedPos.getSectionPosWithSectionDetailLevel(requestedSectionDetailLevel);
if (!DhSectionPos.contains(this.pos, sectionPos))
{
return FullDataPointUtil.EMPTY_DATA_POINT;
}
}
// get the relative data source position
byte requestDetailLevel = (byte) (DhSectionPos.getDetailLevel(this.pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
DhLodPos relativePos = requestedPos.getDhSectionRelativePositionForDetailLevel(requestDetailLevel);
// get the data column
LongArrayList dataColumn = this.getColumnAtRelPos(relativePos.x, relativePos.z);
if (dataColumn == null)
{
return FullDataPointUtil.EMPTY_DATA_POINT;
}
// search for a datapoint that contains the given block y position
int relBlockPosY = blockPosY - levelMinY;
long dataPoint;
for (int i = 0; i < dataColumn.size(); i++)
{
dataPoint = dataColumn.getLong(i);
// we are looking for a specific datapoint,
// don't look at null ones
if (dataPoint == FullDataPointUtil.EMPTY_DATA_POINT)
{
continue;
}
int bottomY = FullDataPointUtil.getBottomY(dataPoint);
int height = FullDataPointUtil.getHeight(dataPoint);
int topY = bottomY + height;
// does this datapoint contain the requested Y position?
if (bottomY <= relBlockPosY
&& relBlockPosY < topY) // blockPositions start from the bottom of the block, thus "<=" for bottomY, just "<" for topY
{
return dataPoint;
}
}
return FullDataPointUtil.EMPTY_DATA_POINT;
}
//==========//
// updating //
//==========//
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())
@@ -314,7 +394,7 @@ public class FullDataSourceV2
// copy over application flag if either are set to continue propagating // copy over application flag if either are set to continue propagating
(BoolUtil.falseIfNull(this.applyToParent) || BoolUtil.falseIfNull(inputDataSource.applyToParent)) (BoolUtil.falseIfNull(this.applyToParent) || BoolUtil.falseIfNull(inputDataSource.applyToParent))
// don't propagate past the top of the tree // don't propagate past the top of the tree
&& (DhSectionPos.getDetailLevel(this.pos) < AbstractDataSourceHandler.TOP_SECTION_DETAIL_LEVEL); && (DhSectionPos.getDetailLevel(this.pos) < FullDataSourceProviderV2.ROOT_SECTION_DETAIL_LEVEL);
} }
// null check to prevent setting a flag we don't want to save in the DB // null check to prevent setting a flag we don't want to save in the DB
@@ -323,7 +403,7 @@ public class FullDataSourceV2
this.applyToChildren = this.applyToChildren =
(BoolUtil.falseIfNull(this.applyToChildren) || BoolUtil.falseIfNull(inputDataSource.applyToChildren)) (BoolUtil.falseIfNull(this.applyToChildren) || BoolUtil.falseIfNull(inputDataSource.applyToChildren))
// don't propagate past the bottom of the tree // don't propagate past the bottom of the tree
&& (DhSectionPos.getDetailLevel(this.pos) > AbstractDataSourceHandler.MIN_SECTION_DETAIL_LEVEL); && (DhSectionPos.getDetailLevel(this.pos) > FullDataSourceProviderV2.LEAF_SECTION_DETAIL_LEVEL);
} }
} }
else if (inputDetailLevel + 1 == thisDetailLevel) else if (inputDetailLevel + 1 == thisDetailLevel)
@@ -334,7 +414,7 @@ public class FullDataSourceV2
this.applyToParent = this.applyToParent =
dataChanged dataChanged
&& (BoolUtil.falseIfNull(this.applyToParent) || BoolUtil.falseIfNull(inputDataSource.applyToParent)) && (BoolUtil.falseIfNull(this.applyToParent) || BoolUtil.falseIfNull(inputDataSource.applyToParent))
&& (DhSectionPos.getDetailLevel(this.pos) < AbstractDataSourceHandler.TOP_SECTION_DETAIL_LEVEL); && (DhSectionPos.getDetailLevel(this.pos) < FullDataSourceProviderV2.ROOT_SECTION_DETAIL_LEVEL);
} }
else if (inputDetailLevel - 1 == thisDetailLevel) else if (inputDetailLevel - 1 == thisDetailLevel)
@@ -346,7 +426,7 @@ public class FullDataSourceV2
this.applyToChildren = this.applyToChildren =
dataChanged dataChanged
&& (BoolUtil.falseIfNull(this.applyToChildren) || BoolUtil.falseIfNull(inputDataSource.applyToChildren)) && (BoolUtil.falseIfNull(this.applyToChildren) || BoolUtil.falseIfNull(inputDataSource.applyToChildren))
&& (DhSectionPos.getDetailLevel(this.pos) > AbstractDataSourceHandler.MIN_SECTION_DETAIL_LEVEL); && (DhSectionPos.getDetailLevel(this.pos) > FullDataSourceProviderV2.LEAF_SECTION_DETAIL_LEVEL);
} }
else else
{ {
@@ -357,8 +437,31 @@ public class FullDataSourceV2
throw new UnsupportedOperationException("Unsupported data source update. Expected input detail level of ["+(thisDetailLevel-1)+"], ["+thisDetailLevel+"], or ["+(thisDetailLevel+1)+"], received detail level ["+inputDetailLevel+"]."); throw new UnsupportedOperationException("Unsupported data source update. Expected input detail level of ["+(thisDetailLevel-1)+"], ["+thisDetailLevel+"], or ["+(thisDetailLevel+1)+"], received detail level ["+inputDetailLevel+"].");
} }
if (dataChanged) if (dataChanged)
{ {
EDhApiWorldCompressionMode worldCompressionMode = Config.Common.LodBuilding.worldCompression.get();
boolean cullHiddenBlocks = (worldCompressionMode != EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS);
if (cullHiddenBlocks)
{
for (int x = 0; x < WIDTH; x++)
{
for (int z = 0; z < WIDTH; z++)
{
LongArrayList dataColumn = this.getColumnAtRelPos(x, z);
if (dataColumn != null
&& dataColumn.size() > 1)
{
FullDataOcclusionCuller.cullHiddenDatapointsInColumn(this, x, z);
}
}
}
}
// update the hash code // update the hash code
this.generateHashCode(); this.generateHashCode();
} }
@@ -366,7 +469,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))
@@ -382,10 +485,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);
@@ -411,9 +518,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)
{ {
@@ -466,12 +577,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))
{ {
@@ -607,176 +717,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 "+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 "+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;
@@ -794,23 +949,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)
{ {
@@ -854,7 +994,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
@@ -989,6 +1129,39 @@ public class FullDataSourceV2
} }
//===================//
// adjacent clearing //
//===================//
/** Removes any non-adjacent data from the given direction. */
public void clearAllNonAdjData(EDhDirection direction)
{
long encodedMinMaxPos = FullDataMinMaxPosUtil.getEncodedMinMaxPos(direction);
int minX = FullDataMinMaxPosUtil.getAdjMinX(encodedMinMaxPos);
int maxX = FullDataMinMaxPosUtil.getAdjMaxX(encodedMinMaxPos);
int minZ = FullDataMinMaxPosUtil.getAdjMinZ(encodedMinMaxPos);
int maxZ = FullDataMinMaxPosUtil.getAdjMaxZ(encodedMinMaxPos);
for (int relX = 0; relX < FullDataSourceV2.WIDTH; relX++)
{
for (int relZ = 0; relZ < FullDataSourceV2.WIDTH; relZ++)
{
// skip non-adjacent data
if (relX >= minX && relX < maxX
&& relZ >= minZ && relZ < maxZ)
{
continue;
}
LongArrayList dataColumn = this.getColumnAtRelPos(relX, relZ);
dataColumn.clear();
dataColumn.add(FullDataPointUtil.EMPTY_DATA_POINT);
}
}
}
//================// //================//
// helper methods // // helper methods //
//================// //================//
@@ -996,18 +1169,35 @@ public class FullDataSourceV2
/** /**
* Usually this should just be used internally, but there may be instances * Usually this should just be used internally, but there may be instances
* where the raw data arrays are available without the data source object. * where the raw data arrays are available without the data source object.
*
* @return -1 if given an out-of-bounds relative position
*/ */
public static int relativePosToIndex(int relX, int relZ) throws IndexOutOfBoundsException public static int tryGetRelativePosToIndex(int relX, int relZ)
{ {
if (relX < 0 || relZ < 0 || if (relX < 0 || relZ < 0
relX > WIDTH || relZ > WIDTH) || relX >= WIDTH || relZ >= WIDTH)
{ {
throw new IndexOutOfBoundsException("Relative data source positions must be between [0] and ["+WIDTH+"] (inclusive) the relative pos: ["+relX+","+relZ+"] is outside of those boundaries."); return -1;
} }
return (relX * WIDTH) + relZ; return (relX * WIDTH) + relZ;
} }
/**
* Usually this should just be used internally, but there may be instances
* where the raw data arrays are available without the data source object.
*/
public static int relativePosToIndex(int relX, int relZ) throws IndexOutOfBoundsException
{
int index = tryGetRelativePosToIndex(relX, relZ);
if (index < 0)
{
throw new IndexOutOfBoundsException("Relative data source positions must be between [0] (inclusive) and ["+WIDTH+"] (exclusive) the relative pos: ["+relX+","+relZ+"] is outside those boundaries.");
}
return index;
}
/** /**
* Throws an exception if the given * Throws an exception if the given
* full data column array is in the wrong order * full data column array is in the wrong order
@@ -1072,18 +1262,10 @@ public class FullDataSourceV2
// setters and getters // // setters and getters //
//=====================// //=====================//
@Override public long getPos() { return this.pos; }
public Long getPos() { return this.pos; }
@Override
public byte getDataDetailLevel() { return (byte) (DhSectionPos.getDetailLevel(this.pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); } public byte getDataDetailLevel() { return (byte) (DhSectionPos.getDetailLevel(this.pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); }
public EDhApiWorldGenerationStep getWorldGenStepAtRelativePos(int relX, int relZ)
{
int index = relativePosToIndex(relX, relZ);
return EDhApiWorldGenerationStep.fromValue(this.columnGenerationSteps.getByte(index));
}
public void setSingleColumn(LongArrayList longArray, int relX, int relZ, EDhApiWorldGenerationStep worldGenStep, EDhApiWorldCompressionMode worldCompressionMode) public void setSingleColumn(LongArrayList longArray, int relX, int relZ, EDhApiWorldGenerationStep worldGenStep, EDhApiWorldCompressionMode worldCompressionMode)
{ {
int index = relativePosToIndex(relX, relZ); int index = relativePosToIndex(relX, relZ);
@@ -1125,13 +1307,13 @@ public class FullDataSourceV2
{ {
try try
{ {
LodDataBuilder.correctDataColumnOrder(columnDataPoints); LodDataBuilder.putListInTopDownOrder(columnDataPoints);
if (this.runApiChunkValidation) if (this.runApiChunkValidation)
{ {
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);
@@ -1147,14 +1329,14 @@ public class FullDataSourceV2
@Override @Override
public List<DhApiTerrainDataPoint> getApiDataPointColumn(int relX, int relZ) throws IndexOutOfBoundsException public List<DhApiTerrainDataPoint> getApiDataPointColumn(int relX, int relZ) throws IndexOutOfBoundsException
{ {
LongArrayList dataColumn = this.get(relX, relZ); LongArrayList dataColumn = this.getColumnAtRelPos(relX, relZ);
ArrayList<DhApiTerrainDataPoint> apiList = new ArrayList<>(); ArrayList<DhApiTerrainDataPoint> apiList = new ArrayList<>();
for (int i = 0; i < dataColumn.size(); i++) for (int i = 0; i < dataColumn.size(); i++)
{ {
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);
} }
@@ -1163,6 +1345,15 @@ public class FullDataSourceV2
//============//
// unit tests //
//============//
public PhantomArrayListCheckout getPhantomArrayCheckoutForUnitTesting()
{ return this.pooledArraysCheckout; }
//================// //================//
// base overrides // // base overrides //
//================// //================//
@@ -1,101 +0,0 @@
package com.seibel.distanthorizons.core.dataObjects.render;
import com.google.common.cache.Cache;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dataObjects.transformers.FullDataToRenderDataTransformer;
import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
/**
* Wrapper for {@link ColumnRenderSource} that handles reference counting
* and cache tracking.
*/
public class CachedColumnRenderSource implements AutoCloseable
{
/** an externally handled future that will complete once the {@link CachedColumnRenderSource#columnRenderSource} has finished loading */
public final CompletableFuture<CachedColumnRenderSource> loadFuture;
/** will be null initially, should be non-null once the corresponding load future is done */
@Nullable
public ColumnRenderSource columnRenderSource = null;
private final AtomicInteger referenceCount;
private final Cache<Long, CachedColumnRenderSource> cachedRenderSourceByPos;
private final ReentrantLock getterLock;
//=============//
// constructor //
//=============//
public CachedColumnRenderSource(
@NotNull CompletableFuture<CachedColumnRenderSource> loadFuture,
@NotNull ReentrantLock getterLock,
@NotNull Cache<Long, CachedColumnRenderSource> cachedRenderSourceByPos)
{
this.loadFuture = loadFuture;
this.getterLock = getterLock;
this.referenceCount = new AtomicInteger(1);
this.cachedRenderSourceByPos = cachedRenderSourceByPos;
}
//====================//
// reference counting //
//====================//
public void markInUse() { this.referenceCount.getAndIncrement(); }
//================//
// base overrides //
//================//
/**
* Will be called multiple times,
* however it will only close the underlying data once
* all references have closed.
*/
@Override
public void close() throws IllegalStateException
{
try
{
// lock to prevent other threads for accessing the cache if we invalidate it
this.getterLock.lock();
// should only happen if something goes wrong up-stream
if (this.columnRenderSource == null)
{
return;
}
// only close once everyone is done with this datasource
int refCount = this.referenceCount.decrementAndGet();
if (refCount == 0)
{
this.cachedRenderSourceByPos.invalidate(this.columnRenderSource.pos);
this.columnRenderSource.close();
}
else if (refCount < 0)
{
throw new IllegalStateException("Render source ["+this.columnRenderSource.pos+"] reference count incorrect. Object already closed.");
}
}
finally
{
this.getterLock.unlock();
}
}
}
@@ -19,43 +19,28 @@
package com.seibel.distanthorizons.core.dataObjects.render; package com.seibel.distanthorizons.core.dataObjects.render;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dataObjects.transformers.FullDataToRenderDataTransformer;
import com.seibel.distanthorizons.core.file.IDataSource;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListParent; import com.seibel.distanthorizons.core.pooling.AbstractPhantomArrayList;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListPool; import com.seibel.distanthorizons.core.pooling.PhantomArrayListPool;
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.util.ListUtil;
import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView;
import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnQuadView; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnQuadView;
import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import com.seibel.distanthorizons.core.util.ColorUtil; import com.seibel.distanthorizons.core.util.ColorUtil;
import com.seibel.distanthorizons.core.util.RenderDataPointUtil; import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import java.util.concurrent.atomic.AtomicLong;
/** /**
* Stores the render data used to generate OpenGL buffers. * Stores the render data used to generate OpenGL buffers.
* *
* @see RenderDataPointUtil * @see RenderDataPointUtil
*/ */
public class ColumnRenderSource public class ColumnRenderSource extends AbstractPhantomArrayList
extends PhantomArrayListParent
implements IDataSource<IDhClientLevel>
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public static final boolean DO_SAFETY_CHECKS = ModInfo.IS_DEV_BUILD; /** measured in data columns */
public static final byte SECTION_SIZE_OFFSET = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; public static final int WIDTH = 64;
/** width of this data in columns */
public static final int SECTION_SIZE = BitShiftUtil.powerOfTwo(SECTION_SIZE_OFFSET); // 64
public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Render Source"); public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Render Source");
@@ -72,8 +57,6 @@ public class ColumnRenderSource
private boolean isEmpty = true; private boolean isEmpty = true;
public AtomicLong localVersion = new AtomicLong(0); // used to track changes to the data source, so that buffers can be updated when necessary
//==============// //==============//
@@ -97,9 +80,9 @@ public class ColumnRenderSource
this.verticalDataCount = maxVerticalSize; this.verticalDataCount = maxVerticalSize;
this.renderDataContainer = this.pooledArraysCheckout.getLongArray(0, SECTION_SIZE * SECTION_SIZE * this.verticalDataCount); this.renderDataContainer = this.pooledArraysCheckout.getLongArray(0, WIDTH * WIDTH * this.verticalDataCount);
this.debugSourceFlags = new DebugSourceFlag[SECTION_SIZE * SECTION_SIZE]; this.debugSourceFlags = new DebugSourceFlag[WIDTH * WIDTH];
} }
@@ -108,19 +91,19 @@ public class ColumnRenderSource
// datapoint manipulation // // datapoint manipulation //
//========================// //========================//
public long getDataPoint(int posX, int posZ, int verticalIndex) { return this.renderDataContainer.getLong(posX * SECTION_SIZE * this.verticalDataCount + posZ * this.verticalDataCount + verticalIndex); } public long getDataPoint(int posX, int posZ, int verticalIndex) { return this.renderDataContainer.getLong(posX * WIDTH * this.verticalDataCount + posZ * this.verticalDataCount + verticalIndex); }
public ColumnArrayView getVerticalDataPointView(int posX, int posZ) public ColumnArrayView getVerticalDataPointView(int posX, int posZ)
{ {
int offset = posX * SECTION_SIZE * this.verticalDataCount + posZ * this.verticalDataCount; int offset = posX * WIDTH * this.verticalDataCount + posZ * this.verticalDataCount;
// don't allow returning views that are outside this render source's bounds // don't allow returning views that are outside this render source's bounds
if (offset >= this.renderDataContainer.size()) if (offset >= this.renderDataContainer.size())
{ {
return null; return null;
} }
else if (posX < 0 || posX >= SECTION_SIZE else if (posX < 0 || posX >= WIDTH
|| posZ < 0 || posZ >= SECTION_SIZE) || posZ < 0 || posZ >= WIDTH)
{ {
return null; return null;
} }
@@ -129,74 +112,8 @@ public class ColumnRenderSource
offset, this.verticalDataCount); offset, this.verticalDataCount);
} }
public ColumnQuadView getFullQuadView() { return this.getQuadViewOverRange(0, 0, SECTION_SIZE, SECTION_SIZE); } public ColumnQuadView getFullQuadView() { return this.getQuadViewOverRange(0, 0, WIDTH, WIDTH); }
public ColumnQuadView getQuadViewOverRange(int quadX, int quadZ, int quadXSize, int quadZSize) { return new ColumnQuadView(this.renderDataContainer, SECTION_SIZE, this.verticalDataCount, quadX, quadZ, quadXSize, quadZSize); } public ColumnQuadView getQuadViewOverRange(int quadX, int quadZ, int quadXSize, int quadZSize) { return new ColumnQuadView(this.renderDataContainer, WIDTH, this.verticalDataCount, quadX, quadZ, quadXSize, quadZSize); }
//=============//
// data update //
//=============//
@Override
public boolean update(FullDataSourceV2 inputFullDataSource, IDhClientLevel level)
{
final String errorMessagePrefix = "Unable to complete update for RenderSource pos: [" + this.pos + "] and pos: [" + inputFullDataSource.getPos() + "]. Error:";
boolean dataChanged = false;
if (DhSectionPos.getDetailLevel(inputFullDataSource.getPos()) == DhSectionPos.getDetailLevel(this.pos))
{
try
{
if (Thread.interrupted())
{
LOGGER.warn(errorMessagePrefix + "write interrupted.");
return false;
}
DhBlockPos2D centerBlockPos = DhSectionPos.getCenterBlockPos(inputFullDataSource.getPos());
int halfBlockWidth = DhSectionPos.getBlockWidth(inputFullDataSource.getPos()) / 2;
DhBlockPos2D minBlockPos = new DhBlockPos2D(centerBlockPos.x - halfBlockWidth, centerBlockPos.z - halfBlockWidth);
for (int x = 0; x < FullDataSourceV2.WIDTH; x++)
{
for (int z = 0; z < FullDataSourceV2.WIDTH; z++)
{
ColumnArrayView columnArrayView = this.getVerticalDataPointView(x, z);
int columnHash = columnArrayView.getDataHash();
LongArrayList dataColumn = inputFullDataSource.get(x, z);
EDhApiWorldGenerationStep worldGenStep = inputFullDataSource.getWorldGenStepAtRelativePos(x, z);
if (dataColumn != null && worldGenStep != EDhApiWorldGenerationStep.EMPTY)
{
FullDataToRenderDataTransformer.updateOrReplaceRenderDataViewColumnWithFullDataColumn(
level, inputFullDataSource.mapping,
minBlockPos.x + x,
minBlockPos.z + z,
columnArrayView, dataColumn);
dataChanged |= columnHash != columnArrayView.getDataHash();
this.fillDebugFlag(x, z, 1, 1, ColumnRenderSource.DebugSourceFlag.DIRECT);
}
}
}
}
catch (Exception e)
{
LOGGER.error(errorMessagePrefix + e.getMessage(), e);
}
}
if (dataChanged)
{
this.localVersion.incrementAndGet();
this.markNotEmpty();
}
return dataChanged;
}
@@ -205,12 +122,9 @@ public class ColumnRenderSource
//=====================// //=====================//
public Long getPos() { return this.pos; } public Long getPos() { return this.pos; }
@Override
public Long getKey() { return this.pos; } public Long getKey() { return this.pos; }
@Override
public String getKeyDisplayString() { return DhSectionPos.toString(this.pos); }
public byte getDataDetailLevel() { return (byte) (DhSectionPos.getDetailLevel(this.pos) - SECTION_SIZE_OFFSET); } public byte getDataDetailLevel() { return (byte) (DhSectionPos.getDetailLevel(this.pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); }
public boolean isEmpty() { return this.isEmpty; } public boolean isEmpty() { return this.isEmpty; }
public void markNotEmpty() { this.isEmpty = false; } public void markNotEmpty() { this.isEmpty = false; }
@@ -224,15 +138,15 @@ public class ColumnRenderSource
} }
for (int x = 0; x < SECTION_SIZE; x++) for (int x = 0; x < WIDTH; x++)
{ {
for (int z = 0; z < SECTION_SIZE; z++) for (int z = 0; z < WIDTH; z++)
{ {
ColumnArrayView columnArrayView = this.getVerticalDataPointView(x,z); ColumnArrayView columnArrayView = this.getVerticalDataPointView(x,z);
for (int i = 0; i < columnArrayView.size; i++) for (int i = 0; i < columnArrayView.size; i++)
{ {
long dataPoint = columnArrayView.get(i); long dataPoint = columnArrayView.get(i);
if (!RenderDataPointUtil.isVoid(dataPoint)) if (!RenderDataPointUtil.hasZeroHeight(dataPoint))
{ {
return true; return true;
} }
@@ -256,12 +170,12 @@ public class ColumnRenderSource
{ {
for (int z = zStart; z < zStart + zWidth; z++) for (int z = zStart; z < zStart + zWidth; z++)
{ {
this.debugSourceFlags[x * SECTION_SIZE + z] = flag; this.debugSourceFlags[x * WIDTH + z] = flag;
} }
} }
} }
public DebugSourceFlag debugGetFlag(int ox, int oz) { return this.debugSourceFlags[ox * SECTION_SIZE + oz]; } public DebugSourceFlag debugGetFlag(int ox, int oz) { return this.debugSourceFlags[ox * WIDTH + oz]; }
@@ -35,7 +35,7 @@ public final class BufferQuad
public static final int NORMAL_MAX_QUAD_WIDTH = 2048; public static final int NORMAL_MAX_QUAD_WIDTH = 2048;
/** /**
* The maximum number of blocks wide a quad can be * The maximum number of blocks wide a quad can be
* when {@link Config.Client.Advanced.Graphics.AdvancedGraphics#earthCurveRatio earthCurveRatio} * when {@link Config.Client.Advanced.Graphics.Experimental#earthCurveRatio earthCurveRatio}
* is enabled. * is enabled.
*/ */
public static final int MAX_QUAD_WIDTH_FOR_EARTH_CURVATURE = LodUtil.CHUNK_WIDTH; public static final int MAX_QUAD_WIDTH_FOR_EARTH_CURVATURE = LodUtil.CHUNK_WIDTH;
@@ -99,7 +99,7 @@ public final class BufferQuad
if (compareDirection == BufferMergeDirectionEnum.EastWest) if (compareDirection == BufferMergeDirectionEnum.EastWest)
{ {
switch (this.direction.getAxis()) switch (this.direction.axis)
{ {
case X: case X:
return threeDimensionalCompare(this.x, this.y, this.z, quad.x, quad.y, quad.z); return threeDimensionalCompare(this.x, this.y, this.z, quad.x, quad.y, quad.z);
@@ -109,12 +109,12 @@ public final class BufferQuad
return threeDimensionalCompare(this.z, this.y, this.x, quad.z, quad.y, quad.x); return threeDimensionalCompare(this.z, this.y, this.x, quad.z, quad.y, quad.x);
default: default:
throw new IllegalArgumentException("Invalid Axis enum: " + this.direction.getAxis()); throw new IllegalArgumentException("Invalid Axis enum: [" + this.direction.axis + "].");
} }
} }
else else
{ {
switch (this.direction.getAxis()) switch (this.direction.axis)
{ {
case X: case X:
return threeDimensionalCompare(this.x, this.z, this.y, quad.x, quad.z, quad.y); return threeDimensionalCompare(this.x, this.z, this.y, quad.x, quad.z, quad.y);
@@ -124,7 +124,7 @@ public final class BufferQuad
return threeDimensionalCompare(this.z, this.x, this.y, quad.z, quad.x, quad.y); return threeDimensionalCompare(this.z, this.x, this.y, quad.z, quad.x, quad.y);
default: default:
throw new IllegalArgumentException("Invalid Axis enum: " + this.direction.getAxis()); throw new IllegalArgumentException("Invalid Axis enum: [" + this.direction.axis + "].");
} }
} }
} }
@@ -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;
} }
@@ -169,7 +169,7 @@ public final class BufferQuad
short thisParallelCompareStartPos; // edge parallel to the merge direction short thisParallelCompareStartPos; // edge parallel to the merge direction
short otherPerpendicularCompareStartPos; short otherPerpendicularCompareStartPos;
short otherParallelCompareStartPos; short otherParallelCompareStartPos;
switch (this.direction.getAxis()) switch (this.direction.axis)
{ {
default: // shouldn't normally happen, just here to make the compiler happy default: // shouldn't normally happen, just here to make the compiler happy
case X: case X:
@@ -23,38 +23,25 @@ import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListCheckout;
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.render.renderer.LodRenderer;
import com.seibel.distanthorizons.coreapi.util.MathUtil; import com.seibel.distanthorizons.coreapi.util.MathUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
public class ColumnBox public class ColumnBox
{ {
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
/** /**
* if the skylight has this value that means * if the skylight has this value that means
* no data is expected * that block position is covered/occluded by an adjacent block/column.
*/ */
private static final byte SKYLIGHT_EMPTY = -1; private static final byte SKYLIGHT_COVERED = -1;
/**
* if the skylight has this value that means
* that block position is covered/occuled by an adjacent block/column.
*/
private static final byte SKYLIGHT_COVERED = -2;
private static final ThreadLocal<byte[]> THREAD_LOCAL_SKY_LIGHT_ARRAY = ThreadLocal.withInitial(() ->
{
byte[] array = new byte[RenderDataPointUtil.MAX_WORLD_Y_SIZE];
Arrays.fill(array, SKYLIGHT_EMPTY);
return array;
});
@@ -63,8 +50,8 @@ public class ColumnBox
//=========// //=========//
public static void addBoxQuadsToBuilder( public static void addBoxQuadsToBuilder(
LodQuadBuilder builder, IDhClientLevel clientLevel, LodQuadBuilder builder, PhantomArrayListCheckout phantomArrayCheckout, IDhClientLevel clientLevel,
short xSize, short ySize, short zSize, short width, short yHeight,
short minX, short minY, short minZ, short minX, short minY, short minZ,
int color, byte irisBlockMaterialId, byte skyLight, byte blockLight, int color, byte irisBlockMaterialId, byte skyLight, byte blockLight,
long topData, long bottomData, ColumnArrayView[] adjData, boolean[] isAdjDataSameDetailLevel) long topData, long bottomData, ColumnArrayView[] adjData, boolean[] isAdjDataSameDetailLevel)
@@ -73,23 +60,26 @@ public class ColumnBox
// variable setup // // variable setup //
//================// //================//
short maxX = (short) (minX + xSize); short maxX = (short) (minX + width);
short maxY = (short) (minY + ySize); short maxY = (short) (minY + yHeight);
short maxZ = (short) (minZ + zSize); short maxZ = (short) (minZ + width);
byte skyLightTop = skyLight; byte skyLightTop = skyLight;
byte skyLightBot = RenderDataPointUtil.doesDataPointExist(bottomData) ? RenderDataPointUtil.getLightSky(bottomData) : 0; byte skyLightBot = RenderDataPointUtil.doesDataPointExist(bottomData) ? RenderDataPointUtil.getLightSky(bottomData) : 0;
boolean isTransparent = ColorUtil.getAlpha(color) < 255 && LodRenderer.transparencyEnabled; boolean transparencyEnabled = Config.Client.Advanced.Graphics.Quality.transparency.get().transparencyEnabled;
boolean fakeOceanFloor = Config.Client.Advanced.Graphics.Quality.transparency.get().fakeTransparencyEnabled;
boolean isTransparent = ColorUtil.getAlpha(color) < 255 && transparencyEnabled;
boolean overVoid = !RenderDataPointUtil.doesDataPointExist(bottomData); boolean overVoid = !RenderDataPointUtil.doesDataPointExist(bottomData);
boolean isTopTransparent = RenderDataPointUtil.getAlpha(topData) < 255 && LodRenderer.transparencyEnabled; boolean isTopTransparent = RenderDataPointUtil.getAlpha(topData) < 255 && transparencyEnabled;
boolean isBottomTransparent = RenderDataPointUtil.getAlpha(bottomData) < 255 && LodRenderer.transparencyEnabled; boolean isBottomTransparent = RenderDataPointUtil.getAlpha(bottomData) < 255 && transparencyEnabled;
// defaulting to a value far below what we can normally render means we // defaulting to a value far below what we can normally render means we
// don't need to have an additional "is cave culling enabled" check // don't need to have an additional "is cave culling enabled" check
int caveCullingMaxY = Integer.MIN_VALUE; int caveCullingMaxY = Integer.MIN_VALUE;
if (Config.Client.Advanced.Graphics.Culling.enableCaveCulling.get()) if (Config.Client.Advanced.Graphics.Culling.enableCaveCulling.get())
{ {
caveCullingMaxY = Config.Client.Advanced.Graphics.Culling.caveCullingHeight.get() - clientLevel.getMinY(); caveCullingMaxY = Config.Client.Advanced.Graphics.Culling.caveCullingHeight.get() - clientLevel.getLevelWrapper().getMinHeight();
} }
@@ -103,20 +93,20 @@ public class ColumnBox
// fake ocean transparency // fake ocean transparency
if (LodRenderer.transparencyEnabled && LodRenderer.fakeOceanFloor) if (transparencyEnabled && fakeOceanFloor)
{ {
if (!isTransparent && isTopTransparent && RenderDataPointUtil.doesDataPointExist(topData)) if (!isTransparent && isTopTransparent && RenderDataPointUtil.doesDataPointExist(topData))
{ {
skyLightTop = (byte) MathUtil.clamp(0, 15 - (RenderDataPointUtil.getYMax(topData) - minY), 15); skyLightTop = (byte) MathUtil.clamp(0, 15 - (RenderDataPointUtil.getYMax(topData) - minY), 15);
ySize = (short) (RenderDataPointUtil.getYMax(topData) - minY - 1); yHeight = (short) (RenderDataPointUtil.getYMax(topData) - minY - 1);
} }
else if (isTransparent && !isBottomTransparent && RenderDataPointUtil.doesDataPointExist(bottomData)) else if (isTransparent && !isBottomTransparent && RenderDataPointUtil.doesDataPointExist(bottomData))
{ {
minY = (short) (minY + ySize - 1); minY = (short) (minY + yHeight - 1);
ySize = 1; yHeight = 1;
} }
maxY = (short) (minY + ySize); maxY = (short) (minY + yHeight);
} }
@@ -125,16 +115,26 @@ public class ColumnBox
// add top and bottom faces // // add top and bottom faces //
//==========================// //==========================//
boolean skipTop = RenderDataPointUtil.doesDataPointExist(topData) && (RenderDataPointUtil.getYMin(topData) == maxY) && !isTopTransparent; // top face
{
boolean skipTop = RenderDataPointUtil.doesDataPointExist(topData)
&& (RenderDataPointUtil.getYMin(topData) == maxY)
&& !isTopTransparent;
if (!skipTop) if (!skipTop)
{ {
builder.addQuadUp(minX, maxY, minZ, xSize, zSize, ColorUtil.applyShade(color, MC.getShade(EDhDirection.UP)), irisBlockMaterialId, skyLightTop, blockLight); builder.addQuadUp(minX, maxY, minZ, width, width, ColorUtil.applyShade(color, MC.getShade(EDhDirection.UP)), irisBlockMaterialId, skyLightTop, blockLight);
}
} }
boolean skipBottom = RenderDataPointUtil.doesDataPointExist(bottomData) && (RenderDataPointUtil.getYMax(bottomData) == minY) && !isBottomTransparent; // bottom face
{
boolean skipBottom = RenderDataPointUtil.doesDataPointExist(bottomData)
&& (RenderDataPointUtil.getYMax(bottomData) == minY)
&& !isBottomTransparent;
if (!skipBottom) if (!skipBottom)
{ {
builder.addQuadDown(minX, minY, minZ, xSize, zSize, ColorUtil.applyShade(color, MC.getShade(EDhDirection.DOWN)), irisBlockMaterialId, skyLightBot, blockLight); builder.addQuadDown(minX, minY, minZ, width, width, ColorUtil.applyShade(color, MC.getShade(EDhDirection.DOWN)), irisBlockMaterialId, skyLightBot, blockLight);
}
} }
@@ -145,84 +145,119 @@ public class ColumnBox
// NORTH face // NORTH face
{ {
ColumnArrayView adjCol = adjData[EDhDirection.NORTH.ordinal() - 2]; // TODO can we use something other than ordinal-2? ColumnArrayView adjCol = adjData[EDhDirection.NORTH.compassIndex];
boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.NORTH.ordinal() - 2]; boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.NORTH.compassIndex];
// if the adjacent column is null that generally means the adjacent area hasn't been generated yet // if the adjacent column is null that generally means the adjacent area hasn't been generated yet
if (adjCol == null) if (adjCol == null)
{ {
// Add an adjacent face if this is opaque face or transparent over the void. // Add an adjacent face if this is opaque face or transparent over the void.
if (!isTransparent || overVoid) if (!isTransparent || overVoid)
{ {
builder.addQuadAdj(EDhDirection.NORTH, minX, minY, minZ, xSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); builder.addQuadAdj(
EDhDirection.NORTH,
minX, minY, minZ,
width, yHeight,
color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight);
} }
} }
else else
{ {
makeAdjVerticalQuad(builder, adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.NORTH, minX, minY, minZ, xSize, ySize, makeAdjVerticalQuad(
builder, phantomArrayCheckout,
adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.NORTH,
minX, minY, minZ, width, yHeight,
color, irisBlockMaterialId, blockLight); color, irisBlockMaterialId, blockLight);
} }
} }
// SOUTH face // SOUTH face
{ {
ColumnArrayView adjCol = adjData[EDhDirection.SOUTH.ordinal() - 2]; ColumnArrayView adjCol = adjData[EDhDirection.SOUTH.compassIndex];
boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.SOUTH.ordinal() - 2]; boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.SOUTH.compassIndex];
if (adjCol == null) if (adjCol == null)
{ {
if (!isTransparent || overVoid) if (!isTransparent || overVoid)
{ {
builder.addQuadAdj(EDhDirection.SOUTH, minX, minY, maxZ, xSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); builder.addQuadAdj(
EDhDirection.SOUTH,
minX, minY, maxZ,
width, yHeight,
color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight);
} }
} }
else else
{ {
makeAdjVerticalQuad(builder, adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.SOUTH, minX, minY, maxZ, xSize, ySize, makeAdjVerticalQuad(
builder, phantomArrayCheckout,
adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.SOUTH,
minX, minY, maxZ, width, yHeight,
color, irisBlockMaterialId, blockLight); color, irisBlockMaterialId, blockLight);
} }
} }
// WEST face // WEST face
{ {
ColumnArrayView adjCol = adjData[EDhDirection.WEST.ordinal() - 2]; ColumnArrayView adjCol = adjData[EDhDirection.WEST.compassIndex];
boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.WEST.ordinal() - 2]; boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.WEST.compassIndex];
if (adjCol == null) if (adjCol == null)
{ {
if (!isTransparent || overVoid) if (!isTransparent || overVoid)
{ {
builder.addQuadAdj(EDhDirection.WEST, minX, minY, minZ, zSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); builder.addQuadAdj(
EDhDirection.WEST,
minX, minY, minZ,
width, yHeight,
color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight);
} }
} }
else else
{ {
makeAdjVerticalQuad(builder, adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.WEST, minX, minY, minZ, zSize, ySize, makeAdjVerticalQuad(
builder, phantomArrayCheckout,
adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.WEST,
minX, minY, minZ, width, yHeight,
color, irisBlockMaterialId, blockLight); color, irisBlockMaterialId, blockLight);
} }
} }
// EAST face // EAST face
{ {
ColumnArrayView adjCol = adjData[EDhDirection.EAST.ordinal() - 2]; ColumnArrayView adjCol = adjData[EDhDirection.EAST.compassIndex];
boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.EAST.ordinal() - 2]; boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.EAST.compassIndex];
if (adjCol == null) if (adjCol == null)
{ {
if (!isTransparent || overVoid) if (!isTransparent || overVoid)
{ {
builder.addQuadAdj(EDhDirection.EAST, maxX, minY, minZ, zSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); builder.addQuadAdj(
EDhDirection.EAST,
maxX, minY, minZ,
width, yHeight,
color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight);
} }
} }
else else
{ {
makeAdjVerticalQuad(builder, adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.EAST, maxX, minY, minZ, zSize, ySize, makeAdjVerticalQuad(
builder, phantomArrayCheckout,
adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.EAST,
maxX, minY, minZ, width, yHeight,
color, irisBlockMaterialId, blockLight); color, irisBlockMaterialId, blockLight);
} }
} }
} }
private static void makeAdjVerticalQuad( private static void makeAdjVerticalQuad(
LodQuadBuilder builder, @NotNull ColumnArrayView adjColumnView, boolean adjacentIsSameDetailLevel, int caveCullingMaxY, EDhDirection direction, LodQuadBuilder builder, PhantomArrayListCheckout phantomArrayCheckout,
@NotNull ColumnArrayView adjColumnView, boolean adjacentIsSameDetailLevel, int caveCullingMaxY, EDhDirection direction,
short x, short yMin, short z, short horizontalWidth, short ySize, short x, short yMin, short z, short horizontalWidth, short ySize,
int color, byte irisBlockMaterialId, byte blockLight) int color, byte irisBlockMaterialId, byte blockLight)
{ {
// pooled arrays
LongArrayList segments = phantomArrayCheckout.getLongArray(0, 0);
LongArrayList newSegments = phantomArrayCheckout.getLongArray(1, 0);
//==================// //==================//
// create face with // // create face with //
// no adjacent data // // no adjacent data //
@@ -230,48 +265,41 @@ public class ColumnBox
color = ColorUtil.applyShade(color, MC.getShade(direction)); color = ColorUtil.applyShade(color, MC.getShade(direction));
// if there isn't any data adjacent to this LOD, if (adjColumnView.size == 0
// just add the full vertical quad || RenderDataPointUtil.hasZeroHeight(adjColumnView.get(0)))
if (adjColumnView.size == 0 || RenderDataPointUtil.isVoid(adjColumnView.get(0)))
{ {
builder.addQuadAdj(direction, x, yMin, z, horizontalWidth, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); builder.addQuadAdj(direction, x, yMin, z, horizontalWidth, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight);
return; return;
} }
//===========================// //=================================//
// Determine face visibility // // determine face visibility/light //
// based on it's neighbors // //=================================//
//===========================//
short yMax = (short) (yMin + ySize); // min is inclusive, max is exclusive boolean transparencyEnabled = Config.Client.Advanced.Graphics.Quality.transparency.get().transparencyEnabled;
byte[] skyLightAtInputPos = THREAD_LOCAL_SKY_LIGHT_ARRAY.get(); boolean inputTransparent = ColorUtil.getAlpha(color) < 255 && transparencyEnabled;
short yMax = (short) (yMin + ySize);
try
{
// set the initial sky-lights for this face,
// if nothing overlaps or overhangs the face should have max sky light
Arrays.fill(skyLightAtInputPos, yMin, yMax, LodUtil.MAX_MC_LIGHT);
// iterate top down
int adjCount = adjColumnView.size(); int adjCount = adjColumnView.size();
// Start with the entire range at max light
segments.add(YSegmentUtil.encode(yMin, yMax, LodUtil.MAX_MC_LIGHT));
// Process each adjacent datapoint and split segments as needed
for (int adjIndex = 0; adjIndex < adjCount; adjIndex++) for (int adjIndex = 0; adjIndex < adjCount; adjIndex++)
{ {
long adjPoint = adjColumnView.get(adjIndex); long adjPoint = adjColumnView.get(adjIndex);
short adjMinY = RenderDataPointUtil.getYMin(adjPoint); short adjMinY = RenderDataPointUtil.getYMin(adjPoint);
short adjMaxY = RenderDataPointUtil.getYMax(adjPoint); short adjMaxY = RenderDataPointUtil.getYMax(adjPoint);
// skip empty adjacent datapoints // skip empty adjacent points
// or points below this one
if (!RenderDataPointUtil.doesDataPointExist(adjPoint) if (!RenderDataPointUtil.doesDataPointExist(adjPoint)
|| RenderDataPointUtil.isVoid(adjPoint)) || RenderDataPointUtil.hasZeroHeight(adjPoint)
{ || yMax <= adjMinY)
continue;
}
// skip this adjacent datapoint if it's above the input datapoint (since it can't affect the input data point)
if (yMax <= adjMinY)
{ {
continue; continue;
} }
@@ -280,123 +308,122 @@ public class ColumnBox
long adjAbovePoint = (adjIndex != 0) ? adjColumnView.get(adjIndex - 1) : RenderDataPointUtil.EMPTY_DATA; long adjAbovePoint = (adjIndex != 0) ? adjColumnView.get(adjIndex - 1) : RenderDataPointUtil.EMPTY_DATA;
long adjBelowPoint = (adjIndex + 1 < adjCount) ? adjColumnView.get(adjIndex + 1) : RenderDataPointUtil.EMPTY_DATA; long adjBelowPoint = (adjIndex + 1 < adjCount) ? adjColumnView.get(adjIndex + 1) : RenderDataPointUtil.EMPTY_DATA;
// if the adjacent data point is over the void
// don't consider it as transparent
boolean adjOverVoid = !RenderDataPointUtil.doesDataPointExist(adjBelowPoint); boolean adjOverVoid = !RenderDataPointUtil.doesDataPointExist(adjBelowPoint);
boolean adjTransparent = !adjOverVoid && RenderDataPointUtil.getAlpha(adjPoint) < 255 && LodRenderer.transparencyEnabled; boolean adjTransparent =
!adjOverVoid
&& RenderDataPointUtil.getAlpha(adjPoint) < 255
&& transparencyEnabled;
byte adjSkyLight = RenderDataPointUtil.getLightSky(adjPoint);
byte lightToApply;
//=================================//
// set sky light based on adjacent //
//=================================//
// set light based on overlapping adjacent
if (!adjTransparent) if (!adjTransparent)
{ {
// adj opaque // Adjacent is opaque
// mark positions adjacent is covering
byte adjSkyLight = RenderDataPointUtil.getLightSky(adjPoint);
for (int i = adjMinY; i < adjMaxY; i++)
{
byte skyLightAtPos = skyLightAtInputPos[i];
// if the adjacent is a different detail level, we want to render adjacent opaque
// faces to try and reduce the chance of holes on detail level borders
boolean adjacentCoversThis = boolean adjacentCoversThis =
// if the adjacent is the same detail level, no special handling is necessary
!adjacentIsSameDetailLevel !adjacentIsSameDetailLevel
// if the adjacent face is underground we probably don't need it
&& RenderDataPointUtil.getYMax(adjPoint) >= caveCullingMaxY && RenderDataPointUtil.getYMax(adjPoint) >= caveCullingMaxY
// check if this face is on a border
&& &&
( (
(x == 0 && direction == EDhDirection.WEST) (x == 0 && direction == EDhDirection.WEST)
|| (z == 0 && direction == EDhDirection.NORTH) || (z == 0 && direction == EDhDirection.NORTH)
// TODO why does 256 represent a border? aren't LODs only 64 datapoints wide?
|| (x == 256 && direction == EDhDirection.EAST) || (x == 256 && direction == EDhDirection.EAST)
|| (z == 256 && direction == EDhDirection.SOUTH) || (z == 256 && direction == EDhDirection.SOUTH)
); );
byte newSkyLightAtPos = adjacentCoversThis ? adjSkyLight : SKYLIGHT_COVERED; lightToApply = adjacentCoversThis ? adjSkyLight : SKYLIGHT_COVERED;
skyLightAtInputPos[i] = (byte) Math.min(newSkyLightAtPos, skyLightAtPos);
}
} }
else else
{ {
// adjacent is transparent, // Adjacent is transparent, use below light
// use datapoint below adjacent for lighting lightToApply = RenderDataPointUtil.getLightSky(adjBelowPoint);
byte belowSkyLight = RenderDataPointUtil.getLightSky(adjBelowPoint);
for (int i = adjMinY; i < adjMaxY; i++)
{
byte skyLightAtPos = skyLightAtInputPos[i];
skyLightAtInputPos[i] = (byte) Math.min(belowSkyLight, skyLightAtPos);
}
} }
// fill in sky light up to the next DP, // Apply light to the range [adjMinY, adjMaxY)
// this is done to handle overhangs applyLightToRange(segments, newSegments, adjMinY, adjMaxY, lightToApply);
byte adjSkyLight = RenderDataPointUtil.getLightSky(adjPoint);
int adjAboveMinY = RenderDataPointUtil.getYMin(adjAbovePoint); // Fill overhang area [adjMaxY, adjAboveMinY) with adjSkyLight
for (int i = adjMaxY; i < adjAboveMinY; i++) short adjAboveMinY = RenderDataPointUtil.getYMin(adjAbovePoint);
if (adjMaxY < adjAboveMinY)
{ {
byte skyLightAtPos = skyLightAtInputPos[i]; applyLightToRange(segments, newSegments, adjMaxY, adjAboveMinY, adjSkyLight);
skyLightAtInputPos[i] = (byte) Math.min(adjSkyLight, skyLightAtPos);
} }
} }
//=======================// //=======================//
// create vertical faces // // Create vertical faces //
// from segments //
//=======================// //=======================//
boolean inputTransparent = ColorUtil.getAlpha(color) < 255 && LodRenderer.transparencyEnabled; for (int i = 0; i < segments.size(); i++)
byte lastSkyLight = skyLightAtInputPos[yMin];
int quadBottomY = yMin;
int quadTopY = -1;
// walk up the sky lights and create a new face
// whenever the light changes to different valid value
for (int i = yMin; i < yMax; i++)
{ {
byte skyLight = skyLightAtInputPos[i]; long segment = segments.getLong(i);
if (skyLight != lastSkyLight)
{
// the sky light changed, create the in-progress face
tryAddVerticalFaceWithSkyLightToBuilder( tryAddVerticalFaceWithSkyLightToBuilder(
builder, direction, builder, direction,
x, z, horizontalWidth, x, z, horizontalWidth,
color, irisBlockMaterialId, blockLight, color, irisBlockMaterialId, blockLight,
lastSkyLight, inputTransparent, quadTopY, quadBottomY YSegmentUtil.getSkyLight(segment), inputTransparent, YSegmentUtil.getEndY(segment), YSegmentUtil.getStartY(segment)
);
lastSkyLight = skyLight;
quadBottomY = i;
}
quadTopY = (i + 1);
}
// add the in-progress face if present
if (quadTopY != -1)
{
tryAddVerticalFaceWithSkyLightToBuilder(
builder, direction,
x, z, horizontalWidth,
color, irisBlockMaterialId, blockLight,
lastSkyLight, inputTransparent, quadTopY, quadBottomY
); );
} }
} }
finally
/**
* Apply the new light value over the given y range,
* splitting segments as needed
* <p>
* source: claude.ai
*/
private static void applyLightToRange(
LongArrayList segments, LongArrayList newSegments,
short rangeStart, short rangeEnd,
byte newLight)
{ {
// clean up the array before the next thread uses it // clear the pooled array that the new segments will go into
// (may be unnecessary since we only work between the yMin-yMax anyway, but is helpful for debugging) newSegments.clear();
Arrays.fill(skyLightAtInputPos, yMin, yMax, SKYLIGHT_EMPTY);
for (int i = 0; i < segments.size(); i++)
{
long seg = segments.getLong(i);
short endY = YSegmentUtil.getEndY(seg);
short startY = YSegmentUtil.getStartY(seg);
byte skyLight = YSegmentUtil.getSkyLight(seg);
// No overlap
if (endY <= rangeStart
|| startY >= rangeEnd)
{
newSegments.add(seg);
continue;
}
// Partial or complete overlap - need to split
// Part before the range
if (startY < rangeStart)
{
newSegments.add(YSegmentUtil.encode(startY, rangeStart, skyLight));
}
// Overlapping part - take minimum light
short overlapStart = (short)Math.max(startY, rangeStart);
short overlapEnd = (short)Math.min(endY, rangeEnd);
byte minLight = (byte) Math.min(newLight, skyLight);
newSegments.add(YSegmentUtil.encode(overlapStart, overlapEnd, minLight));
// Part after the range
if (endY > rangeEnd)
{
newSegments.add(YSegmentUtil.encode(rangeEnd, endY, skyLight));
} }
} }
segments.clear();
segments.addAll(newSegments);
}
private static void tryAddVerticalFaceWithSkyLightToBuilder( private static void tryAddVerticalFaceWithSkyLightToBuilder(
LodQuadBuilder builder, EDhDirection direction, LodQuadBuilder builder, EDhDirection direction,
short x, short z, short horizontalWidth, short x, short z, short horizontalWidth,
@@ -405,22 +432,72 @@ public class ColumnBox
) )
{ {
// invalid positions will have a negative skylight // invalid positions will have a negative skylight
if (lastSkyLight >= 0) if (lastSkyLight < 0)
{ {
return;
}
// Don't add transparent vertical faces // Don't add transparent vertical faces
// unless the adjacent position is empty. // unless the adjacent position is empty.
// This is done to prevent walls between water blocks in the ocean. // This is done to prevent walls between water blocks in the ocean.
if (!inputTransparent if (inputTransparent
|| (lastSkyLight == LodUtil.MAX_MC_LIGHT)) && (lastSkyLight != LodUtil.MAX_MC_LIGHT))
{ {
return;
}
// don't add negative/empty height faces // don't add negative/empty height faces
short height = (short) (quadTopY - quadBottomY); short height = (short) (quadTopY - quadBottomY);
if (height > 0) if (height <= 0)
{ {
builder.addQuadAdj(direction, x, (short) quadBottomY, z, horizontalWidth, height, color, irisBlockMaterialId, lastSkyLight, blockLight); return;
} }
builder.addQuadAdj(
direction,
x, (short) quadBottomY, z,
horizontalWidth, height,
color, irisBlockMaterialId, lastSkyLight, blockLight);
} }
//================//
// helper classes //
//================//
/**
* encodes height/light data into a long
* to reduce object allocations.
*/
private static class YSegmentUtil
{
private static final int HEIGHT_WIDTH = Short.SIZE;
private static final int SKY_LIGHT_WIDTH = Byte.SIZE;
private static final int START_Y_MASK = (int) Math.pow(2, HEIGHT_WIDTH) - 1;
private static final int END_Y_MASK = (int) Math.pow(2, HEIGHT_WIDTH) - 1;
private static final int SKY_LIGHT_MASK = (int) Math.pow(2, SKY_LIGHT_WIDTH) - 1;
private static final int START_Y_OFFSET = 0;
private static final int END_Y_OFFSET = START_Y_OFFSET + HEIGHT_WIDTH;
private static final int SKY_LIGHT_OFFSET = END_Y_OFFSET + HEIGHT_WIDTH;
public static long encode(short startY, short endY, byte skyLight)
{
long data = 0L;
data |= (long) (startY & START_Y_MASK) << START_Y_OFFSET;
data |= (long) (endY & END_Y_MASK) << END_Y_OFFSET;
data |= (long) (skyLight & SKY_LIGHT_MASK) << SKY_LIGHT_OFFSET;
return data;
} }
public static short getStartY(long data) { return (short) ((data >> START_Y_OFFSET) & START_Y_MASK); }
public static short getEndY(long data) { return (short) ((data >> END_Y_OFFSET) & END_Y_MASK); }
public static byte getSkyLight(long data) { return (byte) ((data >> SKY_LIGHT_OFFSET) & SKY_LIGHT_MASK); }
} }
@@ -25,20 +25,17 @@ import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource; import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource;
import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListCheckout;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListPool;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.glObject.GLProxy; import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.util.ColorUtil; import com.seibel.distanthorizons.core.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.util.objects.UncheckedInterruptedException;
import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -49,9 +46,9 @@ import java.util.concurrent.CompletableFuture;
*/ */
public class ColumnRenderBufferBuilder public class ColumnRenderBufferBuilder
{ {
public static final ConfigBasedLogger EVENT_LOGGER = new ConfigBasedLogger(LogManager.getLogger(), private static final DhLogger LOGGER = new DhLoggerBuilder().build();
() -> Config.Common.Logging.logRendererBufferEvent.get());
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Column Buffer Builder");
@@ -60,14 +57,15 @@ public class ColumnRenderBufferBuilder
//==============// //==============//
/** @link adjData should be null for adjacent sections that cross detail level boundaries */ /** @link adjData should be null for adjacent sections that cross detail level boundaries */
public static CompletableFuture<ColumnRenderBuffer> uploadBuffersAsync( public static CompletableFuture<LodBufferContainer> uploadBuffersAsync(
IDhClientLevel clientLevel, IDhClientLevel clientLevel,
long pos, long pos,
LodQuadBuilder quadBuilder LodQuadBuilder quadBuilder
) )
{ {
ColumnRenderBuffer buffer = new ColumnRenderBuffer(new DhBlockPos(DhSectionPos.getMinCornerBlockX(pos), clientLevel.getMinY(), DhSectionPos.getMinCornerBlockZ(pos))); DhBlockPos minBlockPos = new DhBlockPos(DhSectionPos.getMinCornerBlockX(pos), clientLevel.getLevelWrapper().getMinHeight(), DhSectionPos.getMinCornerBlockZ(pos));
CompletableFuture<ColumnRenderBuffer> uploadFuture = buffer.makeAndUploadBuffersAsync(quadBuilder, GLProxy.getInstance().getGpuUploadMethod()); LodBufferContainer bufferContainer = new LodBufferContainer(pos, minBlockPos);
CompletableFuture<LodBufferContainer> uploadFuture = bufferContainer.makeAndUploadBuffersAsync(quadBuilder);
uploadFuture.whenComplete((uploadedBuffer, exception) -> uploadFuture.whenComplete((uploadedBuffer, exception) ->
{ {
// clean up if not uploaded // clean up if not uploaded
@@ -109,19 +107,19 @@ public class ColumnRenderBufferBuilder
// build each column // // build each column //
//===================// //===================//
// pooled arrays for ColumnBox use
try (PhantomArrayListCheckout phantomArrayCheckout = ARRAY_LIST_POOL.checkoutArrays(0, 0, 2))
{
byte thisDetailLevel = renderSource.getDataDetailLevel(); byte thisDetailLevel = renderSource.getDataDetailLevel();
for (int relX = 0; relX < ColumnRenderSource.SECTION_SIZE; relX++) for (int relX = 0; relX < ColumnRenderSource.WIDTH; relX++)
{ {
for (int relZ = 0; relZ < ColumnRenderSource.SECTION_SIZE; relZ++) for (int relZ = 0; relZ < ColumnRenderSource.WIDTH; relZ++)
{ {
// stop the builder if requested
UncheckedInterruptedException.throwIfInterrupted();
// ignore empty/null columns // ignore empty/null columns
ColumnArrayView columnRenderData = renderSource.getVerticalDataPointView(relX, relZ); ColumnArrayView columnRenderData = renderSource.getVerticalDataPointView(relX, relZ);
if (columnRenderData.size() == 0 if (columnRenderData.size() == 0
|| !RenderDataPointUtil.doesDataPointExist(columnRenderData.get(0)) || !RenderDataPointUtil.doesDataPointExist(columnRenderData.get(0))
|| RenderDataPointUtil.isVoid(columnRenderData.get(0))) || RenderDataPointUtil.hasZeroHeight(columnRenderData.get(0)))
{ {
continue; continue;
} }
@@ -154,16 +152,16 @@ public class ColumnRenderBufferBuilder
// get adjacent render data columns // // get adjacent render data columns //
//==================================// //==================================//
ColumnArrayView[] adjColumnViews = new ColumnArrayView[EDhDirection.ADJ_DIRECTIONS.length]; ColumnArrayView[] adjColumnViews = new ColumnArrayView[EDhDirection.CARDINAL_COMPASS.length];
for (EDhDirection lodDirection : EDhDirection.ADJ_DIRECTIONS) for (EDhDirection direction : EDhDirection.CARDINAL_COMPASS)
{ {
try try
{ {
int xAdj = relX + lodDirection.getNormal().x; int xAdj = relX + direction.normal.x;
int zAdj = relZ + lodDirection.getNormal().z; int zAdj = relZ + direction.normal.z;
boolean isCrossRenderSourceBoundary = boolean isCrossRenderSourceBoundary =
(xAdj < 0 || xAdj >= ColumnRenderSource.SECTION_SIZE) || (xAdj < 0 || xAdj >= ColumnRenderSource.WIDTH) ||
(zAdj < 0 || zAdj >= ColumnRenderSource.SECTION_SIZE); (zAdj < 0 || zAdj >= ColumnRenderSource.WIDTH);
ColumnRenderSource adjRenderSource; ColumnRenderSource adjRenderSource;
byte adjDetailLevel; byte adjDetailLevel;
@@ -186,7 +184,7 @@ public class ColumnRenderBufferBuilder
// the adjacent position is outside this render source // the adjacent position is outside this render source
// skip empty sections // skip empty sections
adjRenderSource = adjRegions[lodDirection.ordinal() - 2]; adjRenderSource = adjRegions[direction.compassIndex];
if (adjRenderSource == null) if (adjRenderSource == null)
{ {
continue; continue;
@@ -200,20 +198,20 @@ public class ColumnRenderBufferBuilder
if (xAdj < 0) if (xAdj < 0)
{ {
xAdj += ColumnRenderSource.SECTION_SIZE; xAdj += ColumnRenderSource.WIDTH;
} }
if (xAdj >= ColumnRenderSource.SECTION_SIZE) if (xAdj >= ColumnRenderSource.WIDTH)
{ {
xAdj -= ColumnRenderSource.SECTION_SIZE; xAdj -= ColumnRenderSource.WIDTH;
} }
if (zAdj < 0) if (zAdj < 0)
{ {
zAdj += ColumnRenderSource.SECTION_SIZE; zAdj += ColumnRenderSource.WIDTH;
} }
if (zAdj >= ColumnRenderSource.SECTION_SIZE) if (zAdj >= ColumnRenderSource.WIDTH)
{ {
zAdj -= ColumnRenderSource.SECTION_SIZE; zAdj -= ColumnRenderSource.WIDTH;
} }
} }
} }
@@ -232,11 +230,11 @@ public class ColumnRenderBufferBuilder
LodUtil.assertNotReach("Mismatch between adjacent detail level ["+adjDetailLevel+"] and this render source's detail level ["+thisDetailLevel+"]. Detail levels should be adj >= this."); LodUtil.assertNotReach("Mismatch between adjacent detail level ["+adjDetailLevel+"] and this render source's detail level ["+thisDetailLevel+"]. Detail levels should be adj >= this.");
} }
adjColumnViews[lodDirection.ordinal() - 2] = adjRenderSource.getVerticalDataPointView(xAdj, zAdj); adjColumnViews[direction.compassIndex] = adjRenderSource.getVerticalDataPointView(xAdj, zAdj);
} }
catch (RuntimeException e) catch (RuntimeException e)
{ {
EVENT_LOGGER.warn("Failed to get adj data for relative pos: [" + thisDetailLevel + ":" + relX + "," + relZ + "] at [" + lodDirection + "], Error: "+e.getMessage(), e); LOGGER.warn("Failed to get adj data for relative pos: [" + thisDetailLevel + ":" + relX + "," + relZ + "] at [" + direction + "], Error: [" + e.getMessage() + "].", e);
} }
} // for adjacent directions } // for adjacent directions
@@ -248,15 +246,14 @@ public class ColumnRenderBufferBuilder
ColumnRenderSource.DebugSourceFlag debugSourceFlag = renderSource.debugGetFlag(relX, relZ); ColumnRenderSource.DebugSourceFlag debugSourceFlag = renderSource.debugGetFlag(relX, relZ);
// We render every vertical lod present in this position
// We only stop when we find a block that is void or non-existing block
for (int i = 0; i < columnRenderData.size(); i++) for (int i = 0; i < columnRenderData.size(); i++)
{ {
// can be uncommented to limit which vertical LOD is generated // can be uncommented to limit which vertical LOD is generated
if (Config.Client.Advanced.Debugging.ColumnBuilderDebugging.columnBuilderDebugEnable.get()) if (Config.Client.Advanced.Debugging.ColumnBuilderDebugging.columnBuilderDebugEnable.get())
{ {
int wantedColumnIndex = Config.Client.Advanced.Debugging.ColumnBuilderDebugging.columnBuilderDebugColumnIndex.get(); int wantedColumnIndex = Config.Client.Advanced.Debugging.ColumnBuilderDebugging.columnBuilderDebugColumnIndex.get();
if (wantedColumnIndex >= 0 && i != wantedColumnIndex) if (wantedColumnIndex >= 0
&& i != wantedColumnIndex)
{ {
continue; continue;
} }
@@ -265,7 +262,8 @@ public class ColumnRenderBufferBuilder
long data = columnRenderData.get(i); long data = columnRenderData.get(i);
// If the data is not render-able (Void or non-existing) we stop since there is // If the data is not render-able (Void or non-existing) we stop since there is
// no data left in this position // no data left in this position
if (RenderDataPointUtil.isVoid(data) || !RenderDataPointUtil.doesDataPointExist(data)) if (RenderDataPointUtil.hasZeroHeight(data)
|| !RenderDataPointUtil.doesDataPointExist(data))
{ {
break; break;
} }
@@ -273,8 +271,8 @@ public class ColumnRenderBufferBuilder
long topDataPoint = (i - 1) >= 0 ? columnRenderData.get(i - 1) : RenderDataPointUtil.EMPTY_DATA; long topDataPoint = (i - 1) >= 0 ? columnRenderData.get(i - 1) : RenderDataPointUtil.EMPTY_DATA;
long bottomDataPoint = (i + 1) < columnRenderData.size() ? columnRenderData.get(i + 1) : RenderDataPointUtil.EMPTY_DATA; long bottomDataPoint = (i + 1) < columnRenderData.size() ? columnRenderData.get(i + 1) : RenderDataPointUtil.EMPTY_DATA;
addLodToBuffer( addRenderDataPointToBuilder(
clientLevel, clientLevel, phantomArrayCheckout,
data, topDataPoint, bottomDataPoint, data, topDataPoint, bottomDataPoint,
adjColumnViews, isSameDetailLevel, adjColumnViews, isSameDetailLevel,
thisDetailLevel, relX, relZ, thisDetailLevel, relX, relZ,
@@ -283,34 +281,35 @@ public class ColumnRenderBufferBuilder
}// for z }// for z
}// for x }// for x
}// phantom checkout
quadBuilder.finalizeData(); quadBuilder.mergeQuads();
} }
private static void addLodToBuffer( private static void addRenderDataPointToBuilder(
IDhClientLevel clientLevel, IDhClientLevel clientLevel, PhantomArrayListCheckout phantomArrayCheckout,
long data, long topData, long bottomData, long renderData, long topRenderData, long bottomRenderData,
ColumnArrayView[] adjColumnViews, boolean[] isSameDetailLevel, ColumnArrayView[] adjColumnViews, boolean[] isSameDetailLevel,
byte detailLevel, int renderSourceOffsetPosX, int renderSourceOffsetPosZ, byte detailLevel, int renderSourceOffsetPosX, int renderSourceOffsetPosZ,
LodQuadBuilder quadBuilder, ColumnRenderSource.DebugSourceFlag debugSource) LodQuadBuilder quadBuilder, ColumnRenderSource.DebugSourceFlag debugSource)
{ {
long sectionPos = DhSectionPos.encode(detailLevel, renderSourceOffsetPosX, renderSourceOffsetPosZ); long sectionPos = DhSectionPos.encode(detailLevel, renderSourceOffsetPosX, renderSourceOffsetPosZ);
short width = (short) BitShiftUtil.powerOfTwo(detailLevel); short blockWidth = (short) DhSectionPos.getDetailLevelWidthInBlocks(detailLevel);
short xMin = (short) DhSectionPos.getMinCornerBlockX(sectionPos); short blockMinX = (short) DhSectionPos.getMinCornerBlockX(sectionPos);
short yMin = RenderDataPointUtil.getYMin(data); short blockMinY = RenderDataPointUtil.getYMin(renderData);
short zMin = (short) DhSectionPos.getMinCornerBlockZ(sectionPos); short blockMinZ = (short) DhSectionPos.getMinCornerBlockZ(sectionPos);
short ySize = (short) (RenderDataPointUtil.getYMax(data) - yMin); short blockMaxY = (short) (RenderDataPointUtil.getYMax(renderData) - blockMinY);
if (ySize == 0) if (blockMaxY == 0)
{ {
return; return;
} }
else if (ySize < 0) else if (blockMaxY < 0)
{ {
throw new IllegalArgumentException("Negative y size for the data! Data: [" + RenderDataPointUtil.toString(data) + "]."); throw new IllegalArgumentException("Negative y size for the renderDataPoint! Data: [" + RenderDataPointUtil.toString(renderData) + "].");
} }
byte blockMaterialId = RenderDataPointUtil.getBlockMaterialId(data); byte blockMaterialId = RenderDataPointUtil.getBlockMaterialId(renderData);
@@ -325,11 +324,11 @@ public class ColumnRenderBufferBuilder
float brightnessMultiplier = Config.Client.Advanced.Graphics.Quality.brightnessMultiplier.get().floatValue(); float brightnessMultiplier = Config.Client.Advanced.Graphics.Quality.brightnessMultiplier.get().floatValue();
if (saturationMultiplier == 1.0 && brightnessMultiplier == 1.0) if (saturationMultiplier == 1.0 && brightnessMultiplier == 1.0)
{ {
color = RenderDataPointUtil.getColor(data); color = RenderDataPointUtil.getColor(renderData);
} }
else else
{ {
float[] ahsv = ColorUtil.argbToAhsv(RenderDataPointUtil.getColor(data)); float[] ahsv = ColorUtil.argbToAhsv(RenderDataPointUtil.getColor(renderData));
color = ColorUtil.ahsvToArgb(ahsv[0], ahsv[1], ahsv[2] * saturationMultiplier, ahsv[3] * brightnessMultiplier); color = ColorUtil.ahsvToArgb(ahsv[0], ahsv[1], ahsv[2] * saturationMultiplier, ahsv[3] * brightnessMultiplier);
} }
break; break;
@@ -419,14 +418,14 @@ public class ColumnRenderBufferBuilder
} }
ColumnBox.addBoxQuadsToBuilder( ColumnBox.addBoxQuadsToBuilder(
quadBuilder, clientLevel, quadBuilder, phantomArrayCheckout, clientLevel,
width, ySize, width, blockWidth, blockMaxY,
xMin, yMin, zMin, blockMinX, blockMinY, blockMinZ,
color, color,
blockMaterialId, blockMaterialId,
RenderDataPointUtil.getLightSky(data), RenderDataPointUtil.getLightSky(renderData),
fullBright ? 15 : RenderDataPointUtil.getLightBlock(data), fullBright ? LodUtil.MAX_MC_LIGHT : RenderDataPointUtil.getLightBlock(renderData),
topData, bottomData, adjColumnViews, isSameDetailLevel); topRenderData, bottomRenderData, adjColumnViews, isSameDetailLevel);
} }
} }
@@ -19,16 +19,14 @@
package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding; package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; 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.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.render.glObject.GLProxy; import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.render.glObject.buffer.GLVertexBuffer; import com.seibel.distanthorizons.core.render.glObject.buffer.GLVertexBuffer;
import com.seibel.distanthorizons.core.render.renderer.LodRenderer;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.objects.StatsMap; import com.seibel.distanthorizons.core.util.objects.StatsMap;
import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod; import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod;
import org.apache.logging.log4j.Logger;
import org.lwjgl.system.MemoryUtil; import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@@ -40,9 +38,9 @@ import java.util.concurrent.*;
* *
* @see ColumnRenderBufferBuilder * @see ColumnRenderBufferBuilder
*/ */
public class ColumnRenderBuffer implements AutoCloseable public class LodBufferContainer implements AutoCloseable
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
/** number of bytes a single quad takes */ /** number of bytes a single quad takes */
public static final int QUADS_BYTE_SIZE = LodUtil.LOD_VERTEX_FORMAT.getByteSize() * 4; public static final int QUADS_BYTE_SIZE = LodUtil.LOD_VERTEX_FORMAT.getByteSize() * 4;
@@ -52,15 +50,16 @@ public class ColumnRenderBuffer implements AutoCloseable
public static final int FULL_SIZED_BUFFER = MAX_QUADS_PER_BUFFER * QUADS_BYTE_SIZE; public static final int FULL_SIZED_BUFFER = MAX_QUADS_PER_BUFFER * QUADS_BYTE_SIZE;
/** the position closest to minimum X/Z infinity and the level's lowest Y */
public final DhBlockPos blockPos; public final DhBlockPos minCornerBlockPos;
public final long pos;
public boolean buffersUploaded = false; public boolean buffersUploaded = false;
private GLVertexBuffer[] vbos; public GLVertexBuffer[] vbos;
private GLVertexBuffer[] vbosTransparent; public GLVertexBuffer[] vbosTransparent;
private CompletableFuture<ColumnRenderBuffer> uploadFuture = null; private CompletableFuture<LodBufferContainer> uploadFuture = null;
@@ -68,9 +67,10 @@ public class ColumnRenderBuffer implements AutoCloseable
// constructors // // constructors //
//==============// //==============//
public ColumnRenderBuffer(DhBlockPos blockPos) public LodBufferContainer(long pos, DhBlockPos minCornerBlockPos)
{ {
this.blockPos = blockPos; this.pos = pos;
this.minCornerBlockPos = minCornerBlockPos;
this.vbos = new GLVertexBuffer[0]; this.vbos = new GLVertexBuffer[0];
this.vbosTransparent = new GLVertexBuffer[0]; this.vbosTransparent = new GLVertexBuffer[0];
} }
@@ -82,10 +82,10 @@ public class ColumnRenderBuffer implements AutoCloseable
//==================// //==================//
/** Should be run on a DH thread. */ /** Should be run on a DH thread. */
public synchronized CompletableFuture<ColumnRenderBuffer> makeAndUploadBuffersAsync(LodQuadBuilder builder, EDhApiGpuUploadMethod gpuUploadMethod) public synchronized CompletableFuture<LodBufferContainer> makeAndUploadBuffersAsync(LodQuadBuilder builder)
{ {
// separate variable to prevent race condition when checking null // separate variable to prevent race condition when checking null
CompletableFuture<ColumnRenderBuffer> future = this.uploadFuture; CompletableFuture<LodBufferContainer> future = this.uploadFuture;
if (future != null) if (future != null)
{ {
// upload already in process // upload already in process
@@ -112,11 +112,14 @@ public class ColumnRenderBuffer implements AutoCloseable
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();
} }
EDhApiGpuUploadMethod gpuUploadMethod = GLProxy.getInstance().getGpuUploadMethod();
// upload on the render thread // upload on the render thread
uploadBuffersDirect(this.vbos, opaqueBuffers, gpuUploadMethod); uploadBuffersDirect(this.vbos, opaqueBuffers, gpuUploadMethod);
uploadBuffersDirect(this.vbosTransparent, transparentBuffers, gpuUploadMethod); uploadBuffersDirect(this.vbosTransparent, transparentBuffers, gpuUploadMethod);
@@ -133,7 +136,7 @@ public class ColumnRenderBuffer implements AutoCloseable
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.error("Unexpected issue uploading buffer ["+this.blockPos +"], error: ["+e.getMessage()+"].", e); LOGGER.error("Unexpected issue uploading buffer ["+this.minCornerBlockPos +"], error: ["+e.getMessage()+"].", e);
this.uploadFuture.completeExceptionally(e); this.uploadFuture.completeExceptionally(e);
this.uploadFuture = null; this.uploadFuture = null;
@@ -177,7 +180,9 @@ public class ColumnRenderBuffer implements AutoCloseable
} }
return newVbos; return newVbos;
} }
private static void uploadBuffersDirect(GLVertexBuffer[] vbos, ArrayList<ByteBuffer> byteBuffers, EDhApiGpuUploadMethod method) throws InterruptedException private static void uploadBuffersDirect(
GLVertexBuffer[] vbos, ArrayList<ByteBuffer> byteBuffers,
EDhApiGpuUploadMethod uploadMethod) throws InterruptedException
{ {
int vboIndex = 0; int vboIndex = 0;
for (int i = 0; i < byteBuffers.size(); i++) for (int i = 0; i < byteBuffers.size(); i++)
@@ -191,7 +196,7 @@ public class ColumnRenderBuffer implements AutoCloseable
// get or create the VBO // get or create the VBO
if (vbos[vboIndex] == null) if (vbos[vboIndex] == null)
{ {
vbos[vboIndex] = new GLVertexBuffer(method.useBufferStorage); vbos[vboIndex] = new GLVertexBuffer(uploadMethod.useBufferStorage);
} }
GLVertexBuffer vbo = vbos[vboIndex]; GLVertexBuffer vbo = vbos[vboIndex];
@@ -202,13 +207,13 @@ public class ColumnRenderBuffer implements AutoCloseable
try try
{ {
vbo.bind(); vbo.bind();
vbo.uploadBuffer(buffer, size / LodUtil.LOD_VERTEX_FORMAT.getByteSize(), method, FULL_SIZED_BUFFER); vbo.uploadBuffer(buffer, size / LodUtil.LOD_VERTEX_FORMAT.getByteSize(), uploadMethod, FULL_SIZED_BUFFER);
} }
catch (Exception e) catch (Exception e)
{ {
vbos[vboIndex] = null; vbos[vboIndex] = null;
vbo.close(); vbo.close();
LOGGER.error("Failed to upload buffer: ", e); LOGGER.error("Failed to upload buffer. Error: ["+e.getMessage()+"].", e);
} }
vboIndex++; vboIndex++;
@@ -222,69 +227,6 @@ public class ColumnRenderBuffer implements AutoCloseable
//========//
// render //
//========//
/** @return true if something was rendered, false otherwise */
public boolean renderOpaque(LodRenderer renderContext, DhApiRenderParam renderEventParam)
{
boolean hasRendered = false;
renderContext.setModelViewMatrixOffset(this.blockPos, renderEventParam);
for (GLVertexBuffer vbo : this.vbos)
{
if (vbo == null)
{
continue;
}
if (vbo.getVertexCount() == 0)
{
continue;
}
hasRendered = true;
renderContext.drawVbo(vbo, this);
}
return hasRendered;
}
/** @return true if something was rendered, false otherwise */
public boolean renderTransparent(LodRenderer renderContext, DhApiRenderParam renderEventParam)
{
boolean hasRendered = false;
try
{
// can throw an IllegalStateException if the GL program was freed before it should've been
renderContext.setModelViewMatrixOffset(this.blockPos, renderEventParam);
for (GLVertexBuffer vbo : this.vbosTransparent)
{
if (vbo == null)
{
continue;
}
if (vbo.getVertexCount() == 0)
{
continue;
}
hasRendered = true;
renderContext.drawVbo(vbo, this);
}
}
catch (IllegalStateException e)
{
LOGGER.error("renderContext program doesn't exist for pos: "+this.blockPos, e);
}
return hasRendered;
}
//================// //================//
// helper methods // // helper methods //
//================// //================//
@@ -328,7 +270,7 @@ public class ColumnRenderBuffer implements AutoCloseable
if (vertexBuffer.getSize() == 0) if (vertexBuffer.getSize() == 0)
{ {
GLProxy.GL_LOGGER.warn("VBO with size 0"); GLProxy.LOGGER.warn("VBO with size 0");
} }
statsMap.incBytesStat("TotalUsage", vertexBuffer.getSize()); statsMap.incBytesStat("TotalUsage", vertexBuffer.getSize());
} }
@@ -20,7 +20,6 @@
package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding; package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*; import java.util.*;
import com.seibel.distanthorizons.api.enums.config.EDhApiGrassSideRendering; import com.seibel.distanthorizons.api.enums.config.EDhApiGrassSideRendering;
@@ -29,15 +28,12 @@ import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering;
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.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.enums.EDhDirection;
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.render.glObject.buffer.GLVertexBuffer;
import com.seibel.distanthorizons.core.util.ColorUtil; import com.seibel.distanthorizons.core.util.ColorUtil;
import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.coreapi.util.MathUtil; import com.seibel.distanthorizons.coreapi.util.MathUtil;
import org.apache.logging.log4j.Logger;
import org.lwjgl.system.MemoryUtil; import org.lwjgl.system.MemoryUtil;
/** /**
@@ -47,7 +43,7 @@ import org.lwjgl.system.MemoryUtil;
*/ */
public class LodQuadBuilder public class LodQuadBuilder
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -142,7 +138,8 @@ public class LodQuadBuilder
//===========// //===========//
public void addQuadAdj( public void addQuadAdj(
EDhDirection dir, short x, short y, short z, EDhDirection dir,
short x, short y, short z,
short widthEastWest, short widthNorthSouthOrUpDown, short widthEastWest, short widthNorthSouthOrUpDown,
int color, byte irisBlockMaterialId, byte skyLight, byte blockLight) int color, byte irisBlockMaterialId, byte skyLight, byte blockLight)
{ {
@@ -151,10 +148,20 @@ 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)
|| quadList.get(quadList.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.NorthSouthOrUpDown)) || quadList.get(quadList.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.NorthSouthOrUpDown))
) )
@@ -169,39 +176,23 @@ public class LodQuadBuilder
// XZ // XZ
public void addQuadUp(short minX, short maxY, short minZ, short widthEastWest, short widthNorthSouthOrUpDown, int color, byte irisBlockMaterialId, byte skylight, byte blocklight) // TODO argument names are wrong public void addQuadUp(short minX, short maxY, short minZ, short widthEastWest, short widthNorthSouthOrUpDown, int color, byte irisBlockMaterialId, byte skylight, byte blocklight) // TODO argument names are wrong
{ {
BufferQuad quad = new BufferQuad(minX, maxY, minZ, widthEastWest, widthNorthSouthOrUpDown, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.UP);
boolean isTransparent = (this.doTransparency && ColorUtil.getAlpha(color) < 255); boolean isTransparent = (this.doTransparency && ColorUtil.getAlpha(color) < 255);
ArrayList<BufferQuad> quadList = isTransparent ? this.transparentQuads[EDhDirection.UP.ordinal()] : this.opaqueQuads[EDhDirection.UP.ordinal()]; ArrayList<BufferQuad> quadList = isTransparent
? this.transparentQuads[EDhDirection.UP.ordinal()]
: this.opaqueQuads[EDhDirection.UP.ordinal()];
// attempt to merge this quad with adjacent ones
if (!quadList.isEmpty() &&
(
quadList.get(quadList.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.EastWest)
|| quadList.get(quadList.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.NorthSouthOrUpDown))
)
{
this.premergeCount++;
return;
}
BufferQuad quad = new BufferQuad(minX, maxY, minZ, widthEastWest, widthNorthSouthOrUpDown, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.UP);
quadList.add(quad); quadList.add(quad);
} }
public void addQuadDown(short x, short y, short z, short width, short wz, int color, byte irisBlockMaterialId, byte skylight, byte blocklight) public void addQuadDown(short x, short y, short z, short width, short wz, int color, byte irisBlockMaterialId, byte skylight, byte blocklight)
{ {
ArrayList<BufferQuad> quadArray = (this.doTransparency && ColorUtil.getAlpha(color) < 255)
? this.transparentQuads[EDhDirection.DOWN.ordinal()]
: this.opaqueQuads[EDhDirection.DOWN.ordinal()];
BufferQuad quad = new BufferQuad(x, y, z, width, wz, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.DOWN); BufferQuad quad = new BufferQuad(x, y, z, width, wz, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.DOWN);
ArrayList<BufferQuad> qs = (doTransparency && ColorUtil.getAlpha(color) < 255) quadArray.add(quad);
? transparentQuads[EDhDirection.DOWN.ordinal()] : opaqueQuads[EDhDirection.DOWN.ordinal()];
if (!qs.isEmpty()
&& (qs.get(qs.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.EastWest)
|| qs.get(qs.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.NorthSouthOrUpDown))
)
{
premergeCount++;
return;
}
qs.add(quad);
} }
@@ -210,9 +201,6 @@ public class LodQuadBuilder
// data finalizing // // data finalizing //
//=================// //=================//
/** runs any final data cleanup, merging, etc. */
public void finalizeData() { this.mergeQuads(); }
/** Uses Greedy meshing to merge this builder's Quads. */ /** Uses Greedy meshing to merge this builder's Quads. */
public void mergeQuads() public void mergeQuads()
{ {
@@ -251,7 +239,9 @@ public class LodQuadBuilder
private static long mergeQuadsInternal(ArrayList<BufferQuad>[] list, int directionIndex, BufferMergeDirectionEnum mergeDirection) private static long mergeQuadsInternal(ArrayList<BufferQuad>[] list, int directionIndex, BufferMergeDirectionEnum mergeDirection)
{ {
if (list[directionIndex].size() <= 1) if (list[directionIndex].size() <= 1)
{
return 0; return 0;
}
list[directionIndex].sort((objOne, objTwo) -> objOne.compare(objTwo, mergeDirection)); list[directionIndex].sort((objOne, objTwo) -> objOne.compare(objTwo, mergeDirection));
@@ -306,7 +296,7 @@ public class LodQuadBuilder
// create a new buffer // create a new buffer
if (buffer == null || !buffer.hasRemaining()) if (buffer == null || !buffer.hasRemaining())
{ {
buffer = MemoryUtil.memAlloc(ColumnRenderBuffer.FULL_SIZED_BUFFER); buffer = MemoryUtil.memAlloc(LodBufferContainer.FULL_SIZED_BUFFER);
byteBufferList.add(buffer); byteBufferList.add(buffer);
} }
@@ -330,7 +320,7 @@ public class LodQuadBuilder
short widthEastWest = quad.widthEastWest; short widthEastWest = quad.widthEastWest;
short widthNorthSouth = quad.widthNorthSouthOrUpDown; short widthNorthSouth = quad.widthNorthSouthOrUpDown;
byte normalIndex = (byte) quad.direction.ordinal(); byte normalIndex = (byte) quad.direction.ordinal();
EDhDirection.Axis axis = quad.direction.getAxis(); EDhDirection.Axis axis = quad.direction.axis;
for (int i = 0; i < quadBase.length; i++) for (int i = 0; i < quadBase.length; i++)
{ {
short dx, dy, dz; short dx, dy, dz;
@@ -378,7 +368,7 @@ public class LodQuadBuilder
if (this.grassSideRenderingMode != EDhApiGrassSideRendering.AS_GRASS) if (this.grassSideRenderingMode != EDhApiGrassSideRendering.AS_GRASS)
{ {
// only change the vertex color if it's on the side or bottom // only change the vertex color if it's on the side or bottom
if (quad.direction.getAxis().isHorizontal() || quad.direction == EDhDirection.DOWN) if (quad.direction.axis.isHorizontal() || quad.direction == EDhDirection.DOWN)
{ {
if (this.grassSideRenderingMode == EDhApiGrassSideRendering.AS_DIRT if (this.grassSideRenderingMode == EDhApiGrassSideRendering.AS_DIRT
// if we want the color to fade, only apply the dirt color to the bottom vertices // if we want the color to fade, only apply the dirt color to the bottom vertices
@@ -477,7 +467,7 @@ public class LodQuadBuilder
} }
/** Returns how many GpuBuffers will be needed to render opaque quads in this builder. */ /** Returns how many GpuBuffers will be needed to render opaque quads in this builder. */
public int getCurrentNeededOpaqueVertexBufferCount() { return MathUtil.ceilDiv(this.getCurrentOpaqueQuadsCount(), ColumnRenderBuffer.MAX_QUADS_PER_BUFFER); } public int getCurrentNeededOpaqueVertexBufferCount() { return MathUtil.ceilDiv(this.getCurrentOpaqueQuadsCount(), LodBufferContainer.MAX_QUADS_PER_BUFFER); }
/** Returns how many GpuBuffers will be needed to render transparent quads in this builder. */ /** Returns how many GpuBuffers will be needed to render transparent quads in this builder. */
public int getCurrentNeededTransparentVertexBufferCount() public int getCurrentNeededTransparentVertexBufferCount()
{ {
@@ -486,7 +476,7 @@ public class LodQuadBuilder
return 0; return 0;
} }
return MathUtil.ceilDiv(this.getCurrentTransparentQuadsCount(), ColumnRenderBuffer.MAX_QUADS_PER_BUFFER); return MathUtil.ceilDiv(this.getCurrentTransparentQuadsCount(), LodBufferContainer.MAX_QUADS_PER_BUFFER);
} }
} }
@@ -101,9 +101,7 @@ public final class ColumnArrayView implements IColumnDataView
@Override @Override
public ColumnArrayView subView(int dataIndexStart, int dataCount) public ColumnArrayView subView(int dataIndexStart, int dataCount)
{ { return new ColumnArrayView(data, dataCount * verticalSize, offset + dataIndexStart * verticalSize, verticalSize); }
return new ColumnArrayView(data, dataCount * verticalSize, offset + dataIndexStart * verticalSize, verticalSize);
}
public void fill(long value) { Arrays.fill(data.elements(), offset, offset + size, value); } public void fill(long value) { Arrays.fill(data.elements(), offset, offset + size, value); }
@@ -185,21 +183,11 @@ public final class ColumnArrayView implements IColumnDataView
{ {
for (int i = 0; i < this.dataCount(); i++) for (int i = 0; i < this.dataCount(); i++)
{ {
RenderDataPointUtil.mergeMultiData(source.subView(i, 1), subView(i, 1)); RenderDataPointUtil.mergeMultiData(source.subView(i, 1), this.subView(i, 1));
} }
} }
} }
public void mergeMultiDataFrom(IColumnDataView source)
{
if (dataCount() != 1)
{
throw new IllegalArgumentException("output dataCount must be 1");
}
RenderDataPointUtil.mergeMultiData(source, this);
}
//================// //================//
@@ -210,15 +198,15 @@ public final class ColumnArrayView implements IColumnDataView
public String toString() public String toString()
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("S:").append(size); sb.append("S:").append(this.size);
sb.append(" V:").append(verticalSize); sb.append(" V:").append(this.verticalSize);
sb.append(" O:").append(offset); sb.append(" O:").append(this.offset);
sb.append(" ["); sb.append(" [");
for (int i = 0; i < size; i++) for (int i = 0; i < this.size; i++)
{ {
sb.append(RenderDataPointUtil.toString(data.getLong(offset + i))); sb.append(RenderDataPointUtil.toString(this.data.getLong(this.offset + i)));
if (i < size - 1) if (i < this.size - 1)
{ {
sb.append(",\n"); sb.append(",\n");
} }
@@ -229,11 +217,7 @@ public final class ColumnArrayView implements IColumnDataView
} }
public int getDataHash() public int getDataHash() { return arrayHash(this.data, this.offset, this.size); }
{
return arrayHash(data, offset, size);
}
private static int arrayHash(LongArrayList a, int offset, int length) private static int arrayHash(LongArrayList a, int offset, int length)
{ {
if (a == null) if (a == null)
@@ -252,4 +236,6 @@ public final class ColumnArrayView implements IColumnDataView
return result; return result;
} }
} }
@@ -140,40 +140,4 @@ public class ColumnQuadView implements IColumnDataView
} }
} }
public void copyTo(ColumnQuadView target)
{
if (target.xSize != xSize || target.zSize != zSize)
throw new IllegalArgumentException("Target view must have same size as this view");
for (int x = 0; x < xSize; x++)
{
target.getRow(x).changeVerticalSizeFrom(getRow(x));
}
}
public void mergeMultiColumnFrom(ColumnQuadView source)
{
if (source.xSize == xSize && source.zSize == zSize)
{
source.copyTo(this);
return;
}
if (source.xSize < xSize || source.zSize < zSize)
throw new IllegalArgumentException("Source view must have same or larger size as this view");
int srcXPerTrgX = source.xSize / xSize;
int srcZPerTrgZ = source.zSize / zSize;
if (source.xSize % xSize != 0 || source.zSize % zSize != 0)
throw new IllegalArgumentException("Source view's size must be a multiple of this view's size");
for (int x = 0; x < xSize; x++)
{
for (int z = 0; z < zSize; z++)
{
ColumnQuadView srcBlock = source.subView(x * srcXPerTrgX, z * srcZPerTrgZ, srcXPerTrgX, srcZPerTrgZ);
get(x, z).mergeMultiDataFrom(srcBlock);
}
}
}
} }
@@ -0,0 +1,199 @@
package com.seibel.distanthorizons.core.dataObjects.transformers;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.util.FullDataPointUtil;
import com.seibel.distanthorizons.core.util.LodUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.jetbrains.annotations.NotNull;
public class FullDataOcclusionCuller
{
/**
* Mutates the given datasource so blocks that aren't visible
* (IE completely surrounded by other opaque blocks)
* are removed from the data column.
*
* @param dataSource
* @param relX relative X position in the datasource
* @param relZ relative Z position in the datasource
*/
public static void cullHiddenDatapointsInColumn(
FullDataSourceV2 dataSource,
int relX, int relZ
)
{
LongArrayList centerColumn = dataSource.getColumnAtRelPos(relX, relZ);
LongArrayList posXColumn = dataSource.tryGetColumnAtRelPos(relX + 1, relZ);
LongArrayList negXColumn = dataSource.tryGetColumnAtRelPos(relX - 1, relZ);
LongArrayList posZColumn = dataSource.tryGetColumnAtRelPos(relX, relZ + 1);
LongArrayList negZColumn = dataSource.tryGetColumnAtRelPos(relX, relZ - 1);
if (posXColumn == null || posXColumn.size() == 0
|| negXColumn == null || negXColumn.size() == 0
|| posZColumn == null || posZColumn.size() == 0
|| negZColumn == null || negZColumn.size() == 0)
{
// if any adjacent columns are empty then we can't
// cull this column, since at least one side will be open
// to air/void
return;
}
int centerIndex = centerColumn.size() - 1;
int posXIndex = (posXColumn.size() - 1);
int negXIndex = (negXColumn.size() - 1);
int posZIndex = (posZColumn.size() - 1);
int negZIndex = (negZColumn.size() - 1);
for (; centerIndex >= 0; centerIndex--)
{
long currentPoint = centerColumn.getLong(centerIndex);
// Translucent data points are not eligible to be culled.
if (isTranslucent(dataSource, currentPoint))
{
continue;
}
// the top segment should never be culled.
if (centerIndex == 0
|| isTranslucent(dataSource, centerColumn.getLong(centerIndex - 1)))
{
continue;
}
// the bottom segment can sometimes be culled.
// assume it will not be seen from below,
// because this would imply the player is in the void.
if (centerIndex + 1 < centerColumn.size()
&& isTranslucent(dataSource, centerColumn.getLong(centerIndex + 1)))
{
continue;
}
// the lowest/bedrock segment should not be culled
if (centerIndex + 1 == centerColumn.size())
{
continue;
}
posXIndex = checkOcclusion(dataSource, currentPoint, posXColumn, posXIndex);
if (posXIndex < 0)
{
posXIndex = ~posXIndex;
continue;
}
negXIndex = checkOcclusion(dataSource, currentPoint, negXColumn, negXIndex);
if (negXIndex < 0)
{
negXIndex = ~negXIndex;
continue;
}
posZIndex = checkOcclusion(dataSource, currentPoint, posZColumn, posZIndex);
if (posZIndex < 0)
{
posZIndex = ~posZIndex;
continue;
}
negZIndex = checkOcclusion(dataSource, currentPoint, negZColumn, negZIndex);
if (negZIndex < 0)
{
negZIndex = ~negZIndex;
continue;
}
// Current point is fully surrounded. remove it.
centerColumn.removeLong(centerIndex);
// Make the above data point cover the area that the current point used to occupy.
// The element that was at `centerIndex - 1` is still at that position even after removal of centerIndex.
long above = centerColumn.getLong(centerIndex - 1);
above = FullDataPointUtil.setBottomY(above, FullDataPointUtil.getBottomY(currentPoint));
above = FullDataPointUtil.setHeight(above, FullDataPointUtil.getHeight(currentPoint) + FullDataPointUtil.getHeight(above));
centerColumn.set(centerIndex - 1, above);
}
}
/**
checks if centerPoint is "covered" by opaque data points in adjacentColumn.
centerPoint counts as covered if, and only if, for all Y levels in its height range,
there exists an opaque data point in adjacentColumn which overlaps with that Y level.
@param source used to lookup blocks (and their opacities) based on their IDs.
@param centerPoint the point being checked to see if it's fully covered.
@param adjacentColumn the data points which might cover centerPoint.
@param adjacentIndex the starting index in adjacentColumn to start scanning at.
indices greater than adjacentIndex have already been checked and confirmed to
not overlap or only overlap partially with centerPoint's Y range.
@return if centerPoint is covered, returns the index of the segment which finishes covering it.
the start of the covering may be a smaller index. in this case, the returned index may be used
as the adjacentIndex provided to this method on the next iteration which yields a new centerPoint.
if centerPoint is NOT covered, returns the bitwise negation of the index of the
segment which did not cover it. this guarantees that the returned value is negative.
the caller should check for negative return values and manually un-negate them to proceed with the loop.
in other words, this function returns the index of the next adjacent data
point to use in the loop, AND a boolean indicating whether or not the
centerPoint is covered; both are packed into the same int, and returned.
*/
private static int checkOcclusion(@NotNull FullDataSourceV2 source, long centerPoint, @NotNull LongArrayList adjacentColumn, int adjacentIndex)
{
// check if this point is adjacent to an empty column
// if so it will always be shown
if (adjacentColumn.isEmpty())
{
return ~adjacentIndex;
}
else if (adjacentColumn.size() == 1
&& adjacentColumn.getLong(0) == FullDataPointUtil.EMPTY_DATA_POINT)
{
return ~adjacentIndex;
}
int bottomOfCenter = FullDataPointUtil.getBottomY(centerPoint);
int topOfCenter = bottomOfCenter + FullDataPointUtil.getHeight(centerPoint);
for (; adjacentIndex >= 0; adjacentIndex--)
{
long adjacentPoint = adjacentColumn.getLong(adjacentIndex);
int topOfAdjacent = FullDataPointUtil.getBottomY(adjacentPoint) + FullDataPointUtil.getHeight(adjacentPoint);
if (topOfAdjacent <= bottomOfCenter)
{
// the adjacent point is below the center point,
// check the next one
continue;
}
else if (isTranslucent(source, adjacentPoint))
{
// this point is adjacent to a transparent LOD and should be shown
return ~adjacentIndex;
}
else if (topOfAdjacent >= topOfCenter)
{
// the adjacent point covers the center point
return adjacentIndex;
}
}
// the Adjacent column ends before center column does,
// this point should be visible
return ~adjacentIndex;
}
private static boolean isTranslucent(FullDataSourceV2 source, long point)
{
int id = FullDataPointUtil.getId(point);
int opacity = source.mapping.getBlockStateWrapper(id).getOpacity();
return opacity < LodUtil.BLOCK_FULLY_OPAQUE;
}
}
@@ -26,7 +26,6 @@ import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSour
import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource; import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource;
import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
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.pooling.PhantomArrayListPool;
@@ -35,12 +34,12 @@ import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPosMutable;
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.coreapi.util.BitShiftUtil; import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.HashSet; import java.util.HashSet;
@@ -50,14 +49,14 @@ import java.util.HashSet;
*/ */
public class FullDataToRenderDataTransformer public class FullDataToRenderDataTransformer
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); 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;
@@ -66,13 +65,14 @@ public class FullDataToRenderDataTransformer
//==============================// //==============================//
@Nullable @Nullable
public static ColumnRenderSource transformFullDataToRenderSource(@Nullable FullDataSourceV2 fullDataSource, @Nullable IDhClientLevel level) public static ColumnRenderSource transformFullDataToRenderSource(
@Nullable FullDataSourceV2 fullDataSource, @Nullable IClientLevelWrapper levelWrapper)
{ {
if (fullDataSource == null) if (fullDataSource == null)
{ {
return null; return null;
} }
else if (level == null) else if (levelWrapper == null)
{ {
// if the client is no longer loaded in the world, render sources cannot be created // if the client is no longer loaded in the world, render sources cannot be created
return null; return null;
@@ -81,7 +81,7 @@ public class FullDataToRenderDataTransformer
try try
{ {
return transformCompleteFullDataToColumnData(level, fullDataSource); return transformCompleteFullDataToColumnData(levelWrapper, fullDataSource);
} }
catch (InterruptedException e) catch (InterruptedException e)
{ {
@@ -102,7 +102,8 @@ public class FullDataToRenderDataTransformer
* @throws InterruptedException Can be caused by interrupting the thread upstream. * @throws InterruptedException Can be caused by interrupting the thread upstream.
* Generally thrown if the method is running after the client leaves the current world. * Generally thrown if the method is running after the client leaves the current world.
*/ */
private static ColumnRenderSource transformCompleteFullDataToColumnData(IDhClientLevel level, FullDataSourceV2 fullDataSource) throws InterruptedException private static ColumnRenderSource transformCompleteFullDataToColumnData(
IClientLevelWrapper levelWrapper, FullDataSourceV2 fullDataSource) throws InterruptedException
{ {
final long pos = fullDataSource.getPos(); final long pos = fullDataSource.getPos();
final byte dataDetail = fullDataSource.getDataDetailLevel(); final byte dataDetail = fullDataSource.getDataDetailLevel();
@@ -111,7 +112,7 @@ public class FullDataToRenderDataTransformer
final ColumnRenderSource columnSource = ColumnRenderSource.createEmpty(pos, vertSize, level.getMinY()); final ColumnRenderSource columnSource = ColumnRenderSource.createEmpty(pos, vertSize, levelWrapper.getMinHeight());
if (fullDataSource.isEmpty) if (fullDataSource.isEmpty)
{ {
return columnSource; return columnSource;
@@ -121,37 +122,36 @@ public class FullDataToRenderDataTransformer
int baseX = DhSectionPos.getMinCornerBlockX(pos); int baseX = DhSectionPos.getMinCornerBlockX(pos);
int baseZ = DhSectionPos.getMinCornerBlockZ(pos); int baseZ = DhSectionPos.getMinCornerBlockZ(pos);
for (int x = 0; x < DhSectionPos.getWidthCountForLowerDetailedSection(pos, dataDetail); x++) for (int x = 0; x < FullDataSourceV2.WIDTH; x++)
{ {
for (int z = 0; z < DhSectionPos.getWidthCountForLowerDetailedSection(pos, dataDetail); z++) for (int z = 0; z < FullDataSourceV2.WIDTH; z++)
{ {
throwIfThreadInterrupted();
ColumnArrayView columnArrayView = columnSource.getVerticalDataPointView(x, z); ColumnArrayView columnArrayView = columnSource.getVerticalDataPointView(x, z);
LongArrayList dataColumn = fullDataSource.get(x, z); LongArrayList dataColumn = fullDataSource.getColumnAtRelPos(x, z);
updateOrReplaceRenderDataViewColumnWithFullDataColumn( updateOrReplaceRenderDataViewColumnWithFullDataColumn(
level, fullDataSource.mapping, levelWrapper, fullDataSource,
// bitshift is to account for LODs with a detail level greater than 0 so the block pos is correct // bitshift is to account for LODs with a detail level greater than 0 so the block pos is correct
baseX + BitShiftUtil.pow(x,dataDetail), baseZ + BitShiftUtil.pow(z,dataDetail), baseX + BitShiftUtil.pow(x,dataDetail), baseZ + BitShiftUtil.pow(z,dataDetail),
columnArrayView, dataColumn); columnArrayView, dataColumn);
} }
} }
columnSource.fillDebugFlag(0, 0, ColumnRenderSource.SECTION_SIZE, ColumnRenderSource.SECTION_SIZE, ColumnRenderSource.DebugSourceFlag.FULL); columnSource.fillDebugFlag(0, 0, ColumnRenderSource.WIDTH, ColumnRenderSource.WIDTH, ColumnRenderSource.DebugSourceFlag.FULL);
return columnSource; return columnSource;
} }
/** Updates the given {@link ColumnArrayView} to match the incoming Full data {@link LongArrayList} */ /** Updates the given {@link ColumnArrayView} to match the incoming Full data {@link LongArrayList} */
public static void updateOrReplaceRenderDataViewColumnWithFullDataColumn( public static void updateOrReplaceRenderDataViewColumnWithFullDataColumn(
IDhClientLevel level, IClientLevelWrapper levelWrapper,
FullDataPointIdMap fullDataMapping, int blockX, int blockZ, FullDataSourceV2 fullDataSource, int blockX, int blockZ,
ColumnArrayView columnArrayView, ColumnArrayView columnArrayView,
LongArrayList fullDataColumn) LongArrayList fullDataColumn)
{ {
// we can't do anything if the full data is missing or empty // we can't do anything if the full data is missing or empty
if (fullDataColumn == null || fullDataColumn.size() == 0) if (fullDataColumn == null
|| fullDataColumn.size() == 0)
{ {
return; return;
} }
@@ -160,7 +160,7 @@ public class FullDataToRenderDataTransformer
if (fullDataLength <= columnArrayView.verticalSize()) if (fullDataLength <= columnArrayView.verticalSize())
{ {
// Directly use the arrayView since it fits. // Directly use the arrayView since it fits.
setRenderColumnView(level, fullDataMapping, blockX, blockZ, columnArrayView, fullDataColumn); setRenderColumnView(levelWrapper, fullDataSource, blockX, blockZ, columnArrayView, fullDataColumn);
} }
else else
{ {
@@ -171,7 +171,8 @@ public class FullDataToRenderDataTransformer
{ {
// expand the ColumnArrayView to fit the new larger max vertical size // expand the ColumnArrayView to fit the new larger max vertical size
ColumnArrayView newColumnArrayView = new ColumnArrayView(dataArrayList, fullDataLength, 0, fullDataLength); ColumnArrayView newColumnArrayView = new ColumnArrayView(dataArrayList, fullDataLength, 0, fullDataLength);
setRenderColumnView(level, fullDataMapping, blockX, blockZ, newColumnArrayView, fullDataColumn); setRenderColumnView(levelWrapper, fullDataSource, blockX, blockZ, newColumnArrayView, fullDataColumn);
columnArrayView.changeVerticalSizeFrom(newColumnArrayView); columnArrayView.changeVerticalSizeFrom(newColumnArrayView);
} }
finally finally
@@ -181,7 +182,7 @@ public class FullDataToRenderDataTransformer
} }
} }
private static void setRenderColumnView( private static void setRenderColumnView(
IDhClientLevel level, FullDataPointIdMap fullDataMapping, IClientLevelWrapper levelWrapper, FullDataSourceV2 fullDataSource,
int blockX, int blockZ, int blockX, int blockZ,
ColumnArrayView renderColumnData, LongArrayList fullColumnData) ColumnArrayView renderColumnData, LongArrayList fullColumnData)
{ {
@@ -192,18 +193,28 @@ public class FullDataToRenderDataTransformer
boolean ignoreNonCollidingBlocks = (Config.Client.Advanced.Graphics.Quality.blocksToIgnore.get() == EDhApiBlocksToAvoid.NON_COLLIDING); boolean ignoreNonCollidingBlocks = (Config.Client.Advanced.Graphics.Quality.blocksToIgnore.get() == EDhApiBlocksToAvoid.NON_COLLIDING);
boolean colorBelowWithAvoidedBlocks = Config.Client.Advanced.Graphics.Quality.tintWithAvoidedBlocks.get(); boolean colorBelowWithAvoidedBlocks = Config.Client.Advanced.Graphics.Quality.tintWithAvoidedBlocks.get();
HashSet<IBlockStateWrapper> blockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredBlocks(level.getLevelWrapper()); HashSet<IBlockStateWrapper> blockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredBlocks(levelWrapper);
HashSet<IBlockStateWrapper> caveBlockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredCaveBlocks(level.getLevelWrapper()); HashSet<IBlockStateWrapper> caveBlockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredCaveBlocks(levelWrapper);
int caveCullingMaxY = Config.Client.Advanced.Graphics.Culling.caveCullingHeight.get() - level.getMinY(); // 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();
boolean caveCullingEnabled = boolean caveCullingEnabled =
Config.Client.Advanced.Graphics.Culling.enableCaveCulling.get() Config.Client.Advanced.Graphics.Culling.enableCaveCulling.get()
&& ( && (
// dimensions with a ceiling will be all caves so we don't want cave culling // dimensions with a ceiling will be all caves so we don't want cave culling
!level.getLevelWrapper().hasCeiling() !levelWrapper.hasCeiling()
// the end has a lot of overhangs with 0 lighting above the void, which look broken with // the end has a lot of overhangs with 0 lighting above the void, which look broken with
// the current cave culling logic (this could probably be improved, but just skipping it works best for now) // the current cave culling logic (this could probably be improved, but just skipping it works best for now)
&& !level.getLevelWrapper().getDimensionType().isTheEnd() && !levelWrapper.getDimensionType().isTheEnd()
); );
boolean isColumnVoid = true; boolean isColumnVoid = true;
@@ -222,6 +233,8 @@ public class FullDataToRenderDataTransformer
// convert full data to render data // // convert full data to render data //
//==================================// //==================================//
FullDataPointIdMap fullDataMapping = fullDataSource.mapping;
DhBlockPosMutable mutableBlockPos = new DhBlockPosMutable(blockX, 0, blockZ); DhBlockPosMutable mutableBlockPos = new DhBlockPosMutable(blockX, 0, blockZ);
// goes from the top down // goes from the top down
@@ -236,7 +249,7 @@ public class FullDataToRenderDataTransformer
int blockLight = FullDataPointUtil.getBlockLight(fullData); int blockLight = FullDataPointUtil.getBlockLight(fullData);
int skyLight = FullDataPointUtil.getSkyLight(fullData); int skyLight = FullDataPointUtil.getSkyLight(fullData);
mutableBlockPos.setY(bottomY + level.getMinY()); mutableBlockPos.setY(bottomY + levelWrapper.getMinHeight());
IBiomeWrapper biome; IBiomeWrapper biome;
IBlockStateWrapper block; IBlockStateWrapper block;
@@ -247,10 +260,10 @@ 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 = level.getLevelWrapper().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()+"]) " +
"for pos ["+fullDataMapping.getPos()+"] in level ["+levelId+"]. " + "for pos ["+fullDataMapping.getPos()+"] in level ["+levelId+"]. " +
@@ -281,11 +294,11 @@ public class FullDataToRenderDataTransformer
// cave culling shouldn't happen when at the top of the world // cave culling shouldn't happen when at the top of the world
&& renderDataIndex != 0 && fullDataIndex != 0 && renderDataIndex != 0 && fullDataIndex != 0
// cave culling can't happen when at the bottom of the world // cave culling can't happen when at the bottom of the world
&& (fullDataIndex+1) < fullColumnData.size()) && (fullDataIndex + 1) < fullColumnData.size())
{ {
// we need to get the next sky/block lights because // we need to get the next sky/block lights because
// the air block here will always have a light of 0/0 due to only the top of the LOD's light being saved. // the air block here will always have a light of 0/0 due to only the top of the LOD's light being saved.
long nextFullData = fullColumnData.getLong(fullDataIndex+1); long nextFullData = fullColumnData.getLong(fullDataIndex + 1);
int nextSkyLight = FullDataPointUtil.getSkyLight(nextFullData); int nextSkyLight = FullDataPointUtil.getSkyLight(nextFullData);
if (nextSkyLight == LodUtil.MIN_MC_LIGHT if (nextSkyLight == LodUtil.MIN_MC_LIGHT
@@ -319,17 +332,36 @@ public class FullDataToRenderDataTransformer
// non-solid block check // // non-solid block check //
//=======================// //=======================//
if (ignoreNonCollidingBlocks boolean ignoreNonSolidBlock =
&& !block.isSolid() && !block.isLiquid() && block.getOpacity() != LodUtil.BLOCK_FULLY_OPAQUE) ignoreNonCollidingBlocks
&& !block.isSolid()
&& !block.isLiquid()
&& 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)
{ {
int tempColor = level.computeBaseColor(mutableBlockPos, biome, block); int tempColor = levelWrapper.getBlockColor(mutableBlockPos, biome, fullDataSource, block);
// don't transfer the color when alpha is 0 // don't transfer the color when alpha is 0
// this prevents issues if grass is transparent // this prevents issues if grass is transparent
if (ColorUtil.getAlpha(tempColor) != 0) if (ColorUtil.getAlpha(tempColor) != 0)
{ {
colorToApplyToNextBlock = ColorUtil.setAlpha(tempColor,255); colorToApplyToNextBlock = ColorUtil.setAlpha(tempColor, 255);
skylightToApplyToNextBlock = skyLight; skylightToApplyToNextBlock = skyLight;
blocklightToApplyToNextBlock = blockLight; blocklightToApplyToNextBlock = blockLight;
} }
@@ -344,7 +376,7 @@ public class FullDataToRenderDataTransformer
if (colorToApplyToNextBlock == -1) if (colorToApplyToNextBlock == -1)
{ {
// use this block's color // use this block's color
color = level.computeBaseColor(mutableBlockPos, biome, block); color = levelWrapper.getBlockColor(mutableBlockPos, biome, fullDataSource, block);
} }
else else
{ {
@@ -362,7 +394,9 @@ public class FullDataToRenderDataTransformer
//=============================// //=============================//
// check if they share a top-bottom face and if they have same color // check if they share a top-bottom face and if they have same color
if (color == lastColor && bottomY + blockHeight == lastBottom && renderDataIndex > 0) if (color == lastColor
&& bottomY + blockHeight == lastBottom
&& renderDataIndex > 0)
{ {
//replace the previous block with new bottom //replace the previous block with new bottom
long columnData = renderColumnData.get(renderDataIndex - 1); long columnData = renderColumnData.get(renderDataIndex - 1);
@@ -390,21 +424,4 @@ public class FullDataToRenderDataTransformer
//================//
// helper methods //
//================//
/**
* Called in loops that may run for an extended period of time. <br>
* This is necessary to allow canceling these transformers since running
* them after the client has left a given world will throw exceptions.
*/
private static void throwIfThreadInterrupted() throws InterruptedException
{
if (Thread.interrupted())
{
throw new InterruptedException(FullDataToRenderDataTransformer.class.getSimpleName() + " task interrupted.");
}
}
} }
@@ -24,15 +24,13 @@ import java.util.List;
import com.seibel.distanthorizons.api.enums.config.EDhApiWorldCompressionMode; import com.seibel.distanthorizons.api.enums.config.EDhApiWorldCompressionMode;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiChunkProcessingEvent;
import com.seibel.distanthorizons.api.objects.data.DhApiChunk; import com.seibel.distanthorizons.api.objects.data.DhApiChunk;
import com.seibel.distanthorizons.api.objects.data.DhApiTerrainDataPoint; import com.seibel.distanthorizons.api.objects.data.DhApiTerrainDataPoint;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPosMutable;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.util.FullDataPointUtil; import com.seibel.distanthorizons.core.util.FullDataPointUtil;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
@@ -43,14 +41,17 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IMutableBlockPosWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IMutableBlockPosWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
public class LodDataBuilder public class LodDataBuilder
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IBlockStateWrapper AIR = SingletonInjector.INSTANCE.get(IWrapperFactory.class).getAirBlockStateWrapper(); private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
private static final IBlockStateWrapper AIR = WRAPPER_FACTORY.getAirBlockStateWrapper();
private static boolean getTopErrorLogged = false; private static boolean getTopErrorLogged = false;
@@ -60,19 +61,20 @@ public class LodDataBuilder
// converters // // converters //
//============// //============//
public static FullDataSourceV2 createFromChunk(IChunkWrapper chunkWrapper) public static FullDataSourceV2 createFromChunk(ILevelWrapper levelWrapper, IChunkWrapper chunkWrapper)
{ {
// only block lighting is needed here, sky lighting is populated at the data source stage // only block lighting is needed here, sky lighting is populated at the data source stage
LodUtil.assertTrue(chunkWrapper.isDhBlockLightingCorrect()); LodUtil.assertTrue(chunkWrapper.isDhBlockLightingCorrect(), "Provided chunk's DH Block lighting hasn't been baked.");
int chunkPosX = chunkWrapper.getChunkPos().getX();
int chunkPosZ = chunkWrapper.getChunkPos().getZ();
int sectionPosX = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getX()); int sectionPosX = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getX());
int sectionPosZ = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getZ()); int sectionPosZ = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getZ());
long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ); long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ);
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;
@@ -80,9 +82,7 @@ public class LodDataBuilder
// compute the chunk dataSource offset // compute the chunk dataSource offset
// this offset is used to determine where in the dataSource this chunk's data should go // this offset is used to determine where in the dataSource this chunk's data should go
int chunkOffsetX = chunkWrapper.getChunkPos().getX();
if (chunkWrapper.getChunkPos().getX() < 0)
{
// expected offset positions: // expected offset positions:
// chunkPos -> offset // chunkPos -> offset
// 5 -> 1 // 5 -> 1
@@ -96,31 +96,17 @@ public class LodDataBuilder
// -3 -> 1 // -3 -> 1
// -4 -> 0 --- // -4 -> 0 ---
// -5 -> 3 // -5 -> 3
chunkOffsetX = ((chunkOffsetX) % FullDataSourceV2.NUMB_OF_CHUNKS_WIDE);
if (chunkOffsetX != 0)
{
chunkOffsetX += FullDataSourceV2.NUMB_OF_CHUNKS_WIDE;
}
}
else
{
chunkOffsetX %= FullDataSourceV2.NUMB_OF_CHUNKS_WIDE;
}
chunkOffsetX *= LodUtil.CHUNK_WIDTH;
int chunkOffsetZ = chunkWrapper.getChunkPos().getZ(); // Fast modulo calculation using bitwise AND since NUMB_OF_CHUNKS_WIDE is a power of 2 (4)
if (chunkWrapper.getChunkPos().getZ() < 0) // For any number n: n & (2^k - 1) is equivalent to Math.floorMod(n, 2^k)
{ // Original: Math.floorMod(x, 4) - Handles negative numbers, gives non-negative result in range [0,3]
chunkOffsetZ = ((chunkOffsetZ) % FullDataSourceV2.NUMB_OF_CHUNKS_WIDE); // Bitwise: x & (4-1) - Also gives non-negative result in range [0,3]
if (chunkOffsetZ != 0) // Example: -5 & 3 = 3, which equals Math.floorMod(-5, 4) = 3
{ int chunkOffsetX = chunkWrapper.getChunkPos().getX() & (FullDataSourceV2.NUMB_OF_CHUNKS_WIDE - 1);
chunkOffsetZ += FullDataSourceV2.NUMB_OF_CHUNKS_WIDE; int chunkOffsetZ = chunkWrapper.getChunkPos().getZ() & (FullDataSourceV2.NUMB_OF_CHUNKS_WIDE - 1);
}
} // Convert from chunk coordinates to block coordinates
else chunkOffsetX *= LodUtil.CHUNK_WIDTH;
{
chunkOffsetZ %= FullDataSourceV2.NUMB_OF_CHUNKS_WIDE;
}
chunkOffsetZ *= LodUtil.CHUNK_WIDTH; chunkOffsetZ *= LodUtil.CHUNK_WIDTH;
@@ -130,62 +116,60 @@ public class LodDataBuilder
//==========================// //==========================//
EDhApiWorldCompressionMode worldCompressionMode = Config.Common.LodBuilding.worldCompression.get(); EDhApiWorldCompressionMode worldCompressionMode = Config.Common.LodBuilding.worldCompression.get();
boolean ignoreHiddenBlocks = (worldCompressionMode != EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS);
try try
{ {
IMutableBlockPosWrapper mcBlockPos = chunkWrapper.getMutableBlockPosWrapper(); IMutableBlockPosWrapper mcBlockPos = chunkWrapper.getMutableBlockPosWrapper();
IBlockStateWrapper previousBlockState = null; IBlockStateWrapper previousBlockState = null;
DhApiChunkProcessingEvent.EventParam mutableChunkProcessedEventParam
= new DhApiChunkProcessingEvent.EventParam(levelWrapper, chunkPosX, chunkPosZ);
int minBuildHeight = chunkWrapper.getMinNonEmptyHeight(); int minBuildHeight = chunkWrapper.getMinNonEmptyHeight();
int exclusiveMaxBuildHeight = chunkWrapper.getExclusiveMaxBuildHeight();
int inclusiveMinBuildHeight = chunkWrapper.getInclusiveMinBuildHeight();
int dataCapacity = chunkWrapper.getHeight() / 4;
for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++) for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++)
{ {
for (int relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++) for (int relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++)
{ {
LongArrayList longs = dataSource.get( // Calculate column position
relBlockX + chunkOffsetX, int columnX = relBlockX + chunkOffsetX;
relBlockZ + chunkOffsetZ); int columnZ = relBlockZ + chunkOffsetZ;
// Get column data
LongArrayList longs = dataSource.getColumnAtRelPos(columnX, columnZ);
if (longs == null) if (longs == null)
{ {
longs = new LongArrayList(chunkWrapper.getHeight() / 4); longs = new LongArrayList(dataCapacity);
} }
else else
{ {
longs.clear(); longs.clear();
} }
int lastY = chunkWrapper.getExclusiveMaxBuildHeight(); int lastY = exclusiveMaxBuildHeight;
IBiomeWrapper biome = chunkWrapper.getBiome(relBlockX, lastY, relBlockZ); IBiomeWrapper currentBiome = chunkWrapper.getBiome(relBlockX, lastY, relBlockZ);
IBlockStateWrapper blockState = AIR; IBlockStateWrapper currentBlockState = AIR;
int mappedId = dataSource.mapping.addIfNotPresentAndGetId(biome, blockState); int mappedId = dataSource.mapping.addIfNotPresentAndGetId(currentBiome, currentBlockState);
// Determine lighting (we are at the height limit. There are no torches here, and sky is not obscured.) // TODO: Per face lighting someday?
byte blockLight = LodUtil.MIN_MC_LIGHT;
byte skyLight = LodUtil.MAX_MC_LIGHT;
byte blockLight; // Get the maximum height from both heightmaps
byte skyLight;
if (lastY < chunkWrapper.getExclusiveMaxBuildHeight())
{
// FIXME: The lastY +1 offset is to reproduce the old behavior. Remove this when we get per-face lighting
blockLight = (byte) chunkWrapper.getDhBlockLight(relBlockX, lastY + 1, relBlockZ);
skyLight = (byte) chunkWrapper.getDhSkyLight(relBlockX, lastY + 1, relBlockZ);
}
else
{
//we are at the height limit. There are no torches here, and sky is not obscured.
blockLight = LodUtil.MIN_MC_LIGHT;
skyLight = LodUtil.MAX_MC_LIGHT;
}
// determine the starting Y Pos
int y = Math.max( int y = Math.max(
// max between both heightmaps to account for solid invisible blocks (glass) // max between both heightmaps to account for solid invisible blocks (glass)
// and non-solid opaque blocks (at one point this was stairs, not sure what would fit this now) // and non-solid opaque blocks (at one point this was stairs, not sure what would fit this now)
chunkWrapper.getLightBlockingHeightMapValue(relBlockX, relBlockZ), chunkWrapper.getLightBlockingHeightMapValue(relBlockX, relBlockZ),
chunkWrapper.getSolidHeightMapValue(relBlockX, relBlockZ) chunkWrapper.getSolidHeightMapValue(relBlockX, relBlockZ)
); );
// go up until we reach open air or the world limit
// Go up until we reach open air or the world limit
IBlockStateWrapper topBlockState = previousBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ, mcBlockPos, previousBlockState); IBlockStateWrapper topBlockState = previousBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ, mcBlockPos, previousBlockState);
while (!topBlockState.isAir() && y < chunkWrapper.getExclusiveMaxBuildHeight()) while (!topBlockState.isAir()
&& y < exclusiveMaxBuildHeight)
{ {
try try
{ {
@@ -198,7 +182,7 @@ public class LodDataBuilder
{ {
if (!getTopErrorLogged) if (!getTopErrorLogged)
{ {
LOGGER.warn("Unexpected issue in LodDataBuilder, future errors won't be logged. Chunk [" + chunkWrapper.getChunkPos() + "] with max height: [" + chunkWrapper.getExclusiveMaxBuildHeight() + "] had issue getting block at pos [" + relBlockX + "," + y + "," + relBlockZ + "] error: " + e.getMessage(), e); LOGGER.warn("Unexpected issue in LodDataBuilder, future errors won't be logged. Chunk [" + chunkWrapper.getChunkPos() + "] with max height: [" + exclusiveMaxBuildHeight + "] had issue getting block at pos [" + relBlockX + "," + y + "," + relBlockZ + "] error: " + e.getMessage(), e);
getTopErrorLogged = true; getTopErrorLogged = true;
} }
@@ -207,7 +191,8 @@ public class LodDataBuilder
} }
} }
// Process blocks from top to bottom
boolean forceSingleBlock = false;
for (; y >= minBuildHeight; y--) for (; y >= minBuildHeight; y--)
{ {
IBiomeWrapper newBiome = chunkWrapper.getBiome(relBlockX, y, relBlockZ); IBiomeWrapper newBiome = chunkWrapper.getBiome(relBlockX, y, relBlockZ);
@@ -215,32 +200,68 @@ public class LodDataBuilder
byte newBlockLight = (byte) chunkWrapper.getDhBlockLight(relBlockX, y + 1, relBlockZ); byte newBlockLight = (byte) chunkWrapper.getDhBlockLight(relBlockX, y + 1, relBlockZ);
byte newSkyLight = (byte) chunkWrapper.getDhSkyLight(relBlockX, y + 1, relBlockZ); byte newSkyLight = (byte) chunkWrapper.getDhSkyLight(relBlockX, y + 1, relBlockZ);
// save the biome/block change // Save the biome/block change if different from previous
if (!newBiome.equals(biome) || !newBlockState.equals(blockState)) if (!newBiome.equals(currentBiome)
|| !newBlockState.equals(currentBlockState)
|| forceSingleBlock)
{ {
longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - chunkWrapper.getInclusiveMinBuildHeight(), blockLight, skyLight)); // if the previous block potentially colors this block
biome = newBiome; // make this block a single entry, aka add the next block even if it is the same
blockState = newBlockState; // this is done to allow fire, snow, flowers, etc. to properly color the top of columns vs the whole column
forceSingleBlock =
!currentBlockState.isAir()
&& !currentBlockState.isSolid()
&& !currentBlockState.isLiquid()
&& currentBlockState.getOpacity() != LodUtil.BLOCK_FULLY_OPAQUE;
mappedId = dataSource.mapping.addIfNotPresentAndGetId(biome, blockState);
// check for API overrides
{
mutableChunkProcessedEventParam.updateForPosition(relBlockX, y, relBlockZ, newBlockState, newBiome);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiChunkProcessingEvent.class, mutableChunkProcessedEventParam);
// did the API user override this block?
if (mutableChunkProcessedEventParam.getBlockOverride() != null)
{
// API users shouldn't be creating their own IBlockStateWrapper objects
newBlockState = (IBlockStateWrapper)mutableChunkProcessedEventParam.getBlockOverride();
}
// did the API user override this biome?
if (mutableChunkProcessedEventParam.getBiomeOverride() != null)
{
// API users shouldn't be creating their own IBlockStateWrapper objects
newBiome = (IBiomeWrapper) mutableChunkProcessedEventParam.getBiomeOverride();
}
}
longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - inclusiveMinBuildHeight, blockLight, skyLight));
currentBiome = newBiome;
currentBlockState = newBlockState;
mappedId = dataSource.mapping.addIfNotPresentAndGetId(currentBiome, currentBlockState);
blockLight = newBlockLight; blockLight = newBlockLight;
skyLight = newSkyLight; skyLight = newSkyLight;
lastY = y; lastY = y;
}
}
longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - chunkWrapper.getInclusiveMinBuildHeight(), blockLight, skyLight));
dataSource.setSingleColumn(longs,
relBlockX + chunkOffsetX,
relBlockZ + chunkOffsetZ,
EDhApiWorldGenerationStep.LIGHT,
worldCompressionMode);
}
}
if (ignoreHiddenBlocks) // mark the data source as non-empty if we find any non-air blocks
if (dataSource.isEmpty
&& newBlockState != null
&& !newBlockState.isAir())
{ {
cullHiddenBlocks(dataSource, chunkOffsetX, chunkOffsetZ); dataSource.isEmpty = false;
}
}
}
// Add the final data point
longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - inclusiveMinBuildHeight, blockLight, skyLight));
// Set the column in the data source
dataSource.setSingleColumn(longs, columnX, columnZ, EDhApiWorldGenerationStep.LIGHT, worldCompressionMode);
}
} }
} }
catch (DataCorruptedException e) catch (DataCorruptedException e)
@@ -249,149 +270,9 @@ public class LodDataBuilder
return null; return null;
} }
LodUtil.assertTrue(!dataSource.isEmpty);
return dataSource; return dataSource;
} }
private static void cullHiddenBlocks(FullDataSourceV2 dataSource, int chunkOffsetX, int chunkOffsetZ)
{
for (int relZ = 1; relZ < LodUtil.CHUNK_WIDTH - 1; relZ++)
{
for (int relX = 1; relX < LodUtil.CHUNK_WIDTH - 1; relX++)
{
LongArrayList
centerColumn = dataSource.get(relX + chunkOffsetX, relZ + chunkOffsetZ),
posXColumn = dataSource.get(relX + chunkOffsetX + 1, relZ + chunkOffsetZ),
negXColumn = dataSource.get(relX + chunkOffsetX - 1, relZ + chunkOffsetZ),
posZColumn = dataSource.get(relX + chunkOffsetX, relZ + chunkOffsetZ + 1),
negZColumn = dataSource.get(relX + chunkOffsetX, relZ + chunkOffsetZ - 1);
int
centerIndex = centerColumn.size() - 1,
posXIndex = posXColumn.size() - 1,
negXIndex = negXColumn.size() - 1,
posZIndex = posZColumn.size() - 1,
negZIndex = negZColumn.size() - 1;
for (; centerIndex >= 0; centerIndex--)
{
long currentPoint = centerColumn.getLong(centerIndex);
// translucent data points are not eligible to be culled.
if (isTranslucent(dataSource, currentPoint))
{
continue;
}
// the top segment should never be culled.
if (centerIndex == 0
|| isTranslucent(dataSource, centerColumn.getLong(centerIndex - 1))
)
{
continue;
}
// the bottom segment can sometimes be culled.
// assume it will not be seen from below,
// because this would imply the player is in the void.
if (centerIndex + 1 < centerColumn.size()
&& isTranslucent(dataSource, centerColumn.getLong(centerIndex + 1))
)
{
continue;
}
posXIndex = checkOcclusion(dataSource, currentPoint, posXColumn, posXIndex);
if (posXIndex < 0)
{
posXIndex = ~posXIndex;
continue;
}
negXIndex = checkOcclusion(dataSource, currentPoint, negXColumn, negXIndex);
if (negXIndex < 0)
{
negXIndex = ~negXIndex;
continue;
}
posZIndex = checkOcclusion(dataSource, currentPoint, posZColumn, posZIndex);
if (posZIndex < 0)
{
posZIndex = ~posZIndex;
continue;
}
negZIndex = checkOcclusion(dataSource, currentPoint, negZColumn, negZIndex);
if (negZIndex < 0)
{
negZIndex = ~negZIndex;
continue;
}
// current point is fully surrounded. remove it.
centerColumn.removeLong(centerIndex);
// make the above data point cover the area that the current point used to occupy.
long above = centerColumn.getLong(centerIndex - 1);
above = FullDataPointUtil.setBottomY(above, FullDataPointUtil.getBottomY(currentPoint));
above = FullDataPointUtil.setHeight(above, FullDataPointUtil.getHeight(currentPoint) + FullDataPointUtil.getHeight(above));
centerColumn.set(centerIndex - 1, above);
}
}
}
}
/**
checks if centerPoint is "covered" by opaque data points in adjacentColumn.
centerPoint counts as covered if, and only if, for all Y levels in its height range,
there exists an opaque data point in adjacentColumn which overlaps with that Y level.
@param source used to lookup blocks (and their opacities) based on their IDs.
@param centerPoint the point being checked to see if it's fully covered.
@param adjacentColumn the data points which might cover centerPoint.
@param adjacentIndex the starting index in adjacentColumn to start scanning at.
indices greater than adjacentIndex have already been checked and confirmed to
not overlap or only overlap partially with centerPoint's Y range.
@return if centerPoint is covered, returns the index of the segment which finishes covering it.
the start of the covering may be a smaller index. in this case, the returned index may be used
as the adjacentIndex provided to this method on the next iteration which yields a new centerPoint.
if centerPoint is NOT covered, returns the bitwise negation of the index of the
segment which did not cover it. this guarantees that the returned value is negative.
the caller should check for negative return values and manually un-negate them to proceed with the loop.
in other words, this function returns the index of the next adjacent data
point to use in the loop, AND a boolean indicating whether or not the
centerPoint is covered; both are packed into the same int, and returned.
*/
private static int checkOcclusion(FullDataSourceV2 source, long centerPoint, LongArrayList adjacentColumn, int adjacentIndex)
{
int bottomOfCenter = FullDataPointUtil.getBottomY(centerPoint);
int topOfCenter = bottomOfCenter + FullDataPointUtil.getHeight(centerPoint);
for (; adjacentIndex >= 0; adjacentIndex--)
{
long adjacentPoint = adjacentColumn.getLong(adjacentIndex);
int topOfAdjacent = FullDataPointUtil.getBottomY(adjacentPoint) + FullDataPointUtil.getHeight(adjacentPoint);
if (topOfAdjacent <= bottomOfCenter)
{
continue;
}
else if (isTranslucent(source, adjacentPoint))
{
return ~adjacentIndex;
}
else if (topOfAdjacent >= topOfCenter)
{
return adjacentIndex;
}
}
throw new LodUtil.AssertFailureException("Adjacent column ends before center column does.");
}
private static boolean isTranslucent(FullDataSourceV2 source, long point) {
return source.mapping.getBlockStateWrapper(FullDataPointUtil.getId(point)).getOpacity() < LodUtil.BLOCK_FULLY_OPAQUE;
}
/** @throws ClassCastException if an API user returns the wrong object type(s) */ /** @throws ClassCastException if an API user returns the wrong object type(s) */
@@ -402,9 +283,13 @@ public class LodDataBuilder
int sectionPosZ = getXOrZSectionPosFromChunkPos(apiChunk.chunkPosZ); int sectionPosZ = getXOrZSectionPosFromChunkPos(apiChunk.chunkPosZ);
long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ); long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ);
// chunk relative block position in the data source // Fast modulo calculation using bitwise AND since NUMB_OF_CHUNKS_WIDE is a power of 2 (4)
int relSourceBlockX = Math.floorMod(apiChunk.chunkPosX, 4) * LodUtil.CHUNK_WIDTH; // For any number n: n & (2^k - 1) is equivalent to Math.floorMod(n, 2^k)
int relSourceBlockZ = Math.floorMod(apiChunk.chunkPosZ, 4) * LodUtil.CHUNK_WIDTH; // Original: Math.floorMod(x, 4) - Handles negative numbers, gives non-negative result in range [0,3]
// Bitwise: x & (4-1) - Also gives non-negative result in range [0,3]
// Example: -5 & 3 = 3, which equals Math.floorMod(-5, 4) = 3
int relSourceBlockX = (apiChunk.chunkPosX & (FullDataSourceV2.NUMB_OF_CHUNKS_WIDE - 1)) * LodUtil.CHUNK_WIDTH;
int relSourceBlockZ = (apiChunk.chunkPosZ & (FullDataSourceV2.NUMB_OF_CHUNKS_WIDE - 1)) * LodUtil.CHUNK_WIDTH;
FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos); FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos);
for (int relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++) for (int relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++)
@@ -412,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
@@ -426,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;
@@ -440,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)
@@ -546,11 +502,11 @@ public class LodDataBuilder
//================// //==================//
// helper methods // // internal helpers //
//================// //==================//
public static int getXOrZSectionPosFromChunkPos(int chunkXOrZPos) private static int getXOrZSectionPosFromChunkPos(int chunkXOrZPos)
{ {
// get the section position // get the section position
int sectionPos = chunkXOrZPos; int sectionPos = chunkXOrZPos;
@@ -559,4 +515,6 @@ public class LodDataBuilder
return sectionPos; return sectionPos;
} }
} }
@@ -23,7 +23,7 @@ import com.seibel.distanthorizons.coreapi.DependencyInjection.DependencyInjector
import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable; import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor.IModAccessor; import com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor.IModAccessor;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles;
@@ -40,7 +40,7 @@ import java.lang.invoke.MethodHandles;
*/ */
public class ModAccessorInjector extends DependencyInjector<IModAccessor> public class ModAccessorInjector extends DependencyInjector<IModAccessor>
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(MethodHandles.lookup().lookupClass().getSimpleName()); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public static final ModAccessorInjector INSTANCE = new ModAccessorInjector(IModAccessor.class); public static final ModAccessorInjector INSTANCE = new ModAccessorInjector(IModAccessor.class);
@@ -19,496 +19,165 @@
package com.seibel.distanthorizons.core.enums; package com.seibel.distanthorizons.core.enums;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import com.seibel.distanthorizons.core.util.math.Vec3i; import com.seibel.distanthorizons.core.util.math.Vec3i;
/** /**
* An (almost) exact copy of Minecraft's
* Direction enum. <Br><Br>
*
* Up <Br> * Up <Br>
* Down <Br> * Down <Br>
* North <Br> * North <Br>
* South <Br> * South <Br>
* East <Br> * East <Br>
* West <Br> * West <Br>
*
* @author James Seibel
* @version 2021-11-13
*/ */
public enum EDhDirection public enum EDhDirection
{ {
/** negative Y */ /** negative Y */
DOWN(0, 1, -1, "down", EDhDirection.AxisDirection.NEGATIVE, EDhDirection.Axis.Y, new Vec3i(0, -1, 0)), DOWN("down", EDhDirection.AxisDirection.NEGATIVE, EDhDirection.Axis.Y, new Vec3i(0, -1, 0), -1),
/** positive Y */ /** positive Y */
UP(1, 0, -1, "up", EDhDirection.AxisDirection.POSITIVE, EDhDirection.Axis.Y, new Vec3i(0, 1, 0)), UP("up", EDhDirection.AxisDirection.POSITIVE, EDhDirection.Axis.Y, new Vec3i(0, 1, 0), -1),
/** negative Z */ /** negative Z */
NORTH(2, 3, 2, "north", EDhDirection.AxisDirection.NEGATIVE, EDhDirection.Axis.Z, new Vec3i(0, 0, -1)), NORTH("north", EDhDirection.AxisDirection.NEGATIVE, EDhDirection.Axis.Z, new Vec3i(0, 0, -1), 0),
/** positive Z */ /** positive Z */
SOUTH(3, 2, 0, "south", EDhDirection.AxisDirection.POSITIVE, EDhDirection.Axis.Z, new Vec3i(0, 0, 1)), SOUTH("south", EDhDirection.AxisDirection.POSITIVE, EDhDirection.Axis.Z, new Vec3i(0, 0, 1), 1),
/** negative X */ /** negative X */
WEST(4, 5, 1, "west", EDhDirection.AxisDirection.NEGATIVE, EDhDirection.Axis.X, new Vec3i(-1, 0, 0)), WEST("west", EDhDirection.AxisDirection.NEGATIVE, EDhDirection.Axis.X, new Vec3i(-1, 0, 0), 2),
/** positive X */ /** positive X */
EAST(5, 4, 3, "east", EDhDirection.AxisDirection.POSITIVE, EDhDirection.Axis.X, new Vec3i(1, 0, 0)); EAST("east", EDhDirection.AxisDirection.POSITIVE, EDhDirection.Axis.X, new Vec3i(1, 0, 0), 3);
/**
* Up, Down, West, East, North, South <br> /** Up, Down, West, East, North, South */
* Similar to {@link EDhDirection#OPPOSITE_DIRECTIONS}, just with a different order public static final EDhDirection[] ALL = new EDhDirection[] {
*/
public static final EDhDirection[] CARDINAL_DIRECTIONS = new EDhDirection[]{
EDhDirection.UP, EDhDirection.UP,
EDhDirection.DOWN, EDhDirection.DOWN,
EDhDirection.WEST, EDhDirection.WEST,
EDhDirection.EAST, EDhDirection.EAST,
EDhDirection.NORTH, EDhDirection.NORTH,
EDhDirection.SOUTH}; EDhDirection.SOUTH
/**
* Up, Down, South, North, East, West <br>
* Similar to {@link EDhDirection#CARDINAL_DIRECTIONS}, just with a different order
*/
public static final EDhDirection[] OPPOSITE_DIRECTIONS = new EDhDirection[]{
EDhDirection.UP,
EDhDirection.DOWN,
EDhDirection.SOUTH,
EDhDirection.NORTH,
EDhDirection.EAST,
EDhDirection.WEST};
/** North, South, East, West */ // TODO rename to state this is just X/Z or flat directions
public static final EDhDirection[] ADJ_DIRECTIONS = new EDhDirection[]{
EDhDirection.EAST,
EDhDirection.WEST,
EDhDirection.SOUTH,
EDhDirection.NORTH};
// private final int data3d;
// private final int oppositeIndex;
// private final int data2d;
private final String name;
private final EDhDirection.Axis axis;
private final EDhDirection.AxisDirection axisDirection;
private final Vec3i normal;
private static final EDhDirection[] VALUES = values();
private static final Map<String, EDhDirection> BY_NAME = Arrays.stream(VALUES).collect(Collectors.toMap(EDhDirection::getName, (p_199787_0_) ->
{
return p_199787_0_;
}));
// private static final LodDirection[] BY_3D_DATA = Arrays.stream(VALUES).sorted(Comparator.comparingInt((p_199790_0_) ->
// {
// return p_199790_0_.data3d;
// })).toArray((p_199788_0_) ->
// {
// return new LodDirection[p_199788_0_];
// });
//
// private static final LodDirection[] BY_2D_DATA = Arrays.stream(VALUES).filter((p_199786_0_) ->
// {
// return p_199786_0_.getAxis().isHorizontal();
// }).sorted(Comparator.comparingInt((p_199789_0_) ->
// {
// return p_199789_0_.data2d;
// })).toArray((p_199791_0_) ->
// {
// return new LodDirection[p_199791_0_];
// });
// private static final Long2ObjectMap<LodDirection> BY_NORMAL = Arrays.stream(VALUES).collect(Collectors.toMap((p_218385_0_) ->
// {
// return (new BlockPos(p_218385_0_.getNormal())).asLong();
// }, (p_218384_0_) ->
// {
// return p_218384_0_;
// }, (p_218386_0_, p_218386_1_) ->
// {
// throw new IllegalArgumentException("Duplicate keys");
// }, Long2ObjectOpenHashMap::new));
EDhDirection(int p_i46016_3_, int p_i46016_4_, int p_i46016_5_, String p_i46016_6_, EDhDirection.AxisDirection p_i46016_7_, EDhDirection.Axis p_i46016_8_, Vec3i p_i46016_9_)
{
// this.data3d = p_i46016_3_;
// this.data2d = p_i46016_5_;
// this.oppositeIndex = p_i46016_4_;
this.name = p_i46016_6_;
this.axis = p_i46016_8_;
this.axisDirection = p_i46016_7_;
this.normal = p_i46016_9_;
}
// public static LodDirection[] orderedByNearest(Entity p_196054_0_)
// {
// float f = p_196054_0_.getViewXRot(1.0F) * ((float) Math.PI / 180F);
// float f1 = -p_196054_0_.getViewYRot(1.0F) * ((float) Math.PI / 180F);
// float f2 = MathHelper.sin(f);
// float f3 = MathHelper.cos(f);
// float f4 = MathHelper.sin(f1);
// float f5 = MathHelper.cos(f1);
// boolean flag = f4 > 0.0F;
// boolean flag1 = f2 < 0.0F;
// boolean flag2 = f5 > 0.0F;
// float f6 = flag ? f4 : -f4;
// float f7 = flag1 ? -f2 : f2;
// float f8 = flag2 ? f5 : -f5;
// float f9 = f6 * f3;
// float f10 = f8 * f3;
// LodDirection lodDirection = flag ? EAST : WEST;
// LodDirection direction1 = flag1 ? UP : DOWN;
// LodDirection direction2 = flag2 ? SOUTH : NORTH;
// if (f6 > f8)
// {
// if (f7 > f9)
// {
// return makeDirectionArray(direction1, lodDirection, direction2);
// }
// else
// {
// return f10 > f7 ? makeDirectionArray(lodDirection, direction2, direction1) : makeDirectionArray(lodDirection, direction1, direction2);
// }
// }
// else if (f7 > f10)
// {
// return makeDirectionArray(direction1, direction2, lodDirection);
// }
// else
// {
// return f9 > f7 ? makeDirectionArray(direction2, lodDirection, direction1) : makeDirectionArray(direction2, direction1, lodDirection);
// }
// }
// private static LodDirection[] makeDirectionArray(LodDirection p_196053_0_, LodDirection p_196053_1_, LodDirection p_196053_2_)
// {
// return new LodDirection[] { p_196053_0_, p_196053_1_, p_196053_2_, p_196053_2_.getOpposite(), p_196053_1_.getOpposite(), p_196053_0_.getOpposite() };
// }
// public static LodDirection rotate(Mat4f p_229385_0_, LodDirection p_229385_1_)
// {
// Vec3i Vec3i = p_229385_1_.getNormal();
// Vector4f vector4f = new Vector4f(Vec3i.getX(), Vec3i.getY(), Vec3i.getZ(), 0.0F);
// vector4f.transform(p_229385_0_);
// return getNearest(vector4f.x(), vector4f.y(), vector4f.z());
// }
// public Quaternion getRotation()
// {
// Quaternion quaternion = Vector3f.XP.rotationDegrees(90.0F);
// switch (this)
// {
// case DOWN:
// return Vector3f.XP.rotationDegrees(180.0F);
// case UP:
// return Quaternion.ONE.copy();
// case NORTH:
// quaternion.mul(Vector3f.ZP.rotationDegrees(180.0F));
// return quaternion;
// case SOUTH:
// return quaternion;
// case WEST:
// quaternion.mul(Vector3f.ZP.rotationDegrees(90.0F));
// return quaternion;
// case EAST:
// default:
// quaternion.mul(Vector3f.ZP.rotationDegrees(-90.0F));
// return quaternion;
// }
// }
// public int get3DDataValue()
// {
// return this.data3d;
// }
//
// public int get2DDataValue()
// {
// return this.data2d;
// }
public EDhDirection.AxisDirection getAxisDirection()
{
return this.axisDirection;
}
// public LodDirection getOpposite()
// {
// return from3DDataValue(this.oppositeIndex);
// }
public EDhDirection getClockWise()
{
switch (this)
{
case NORTH:
return EAST;
case SOUTH:
return WEST;
case WEST:
return NORTH;
case EAST:
return SOUTH;
default:
throw new IllegalStateException("Unable to get Y-rotated facing of " + this);
}
}
public EDhDirection getCounterClockWise()
{
switch (this)
{
case NORTH:
return WEST;
case SOUTH:
return EAST;
case WEST:
return SOUTH;
case EAST:
return NORTH;
default:
throw new IllegalStateException("Unable to get CCW facing of " + this);
}
}
public String getName()
{
return this.name;
}
public EDhDirection.Axis getAxis()
{
return this.axis;
}
public static EDhDirection byName(String name)
{
return name == null ? null : BY_NAME.get(name.toLowerCase(Locale.ROOT));
}
// public static LodDirection from3DDataValue(int p_82600_0_)
// {
// return BY_3D_DATA[MathHelper.abs(p_82600_0_ % BY_3D_DATA.length)];
// }
//
// public static LodDirection from2DDataValue(int p_176731_0_)
// {
// return BY_2D_DATA[MathHelper.abs(p_176731_0_ % BY_2D_DATA.length)];
// }
// @Nullable
// public static LodDirection fromNormal(int p_218383_0_, int p_218383_1_, int p_218383_2_)
// {
// return BY_NORMAL.get(BlockPos.asLong(p_218383_0_, p_218383_1_, p_218383_2_));
// }
// public static LodDirection fromYRot(double p_176733_0_)
// {
// return from2DDataValue(MathHelper.floor(p_176733_0_ / 90.0D + 0.5D) & 3);
// }
public static EDhDirection fromAxisAndDirection(EDhDirection.Axis p_211699_0_, EDhDirection.AxisDirection p_211699_1_)
{
switch (p_211699_0_)
{
case X:
return p_211699_1_ == EDhDirection.AxisDirection.POSITIVE ? EAST : WEST;
case Y:
return p_211699_1_ == EDhDirection.AxisDirection.POSITIVE ? UP : DOWN;
case Z:
default:
return p_211699_1_ == EDhDirection.AxisDirection.POSITIVE ? SOUTH : NORTH;
}
}
// public float toYRot()
// {
// return (this.data2d & 3) * 90;
// }
// public static LodDirection getRandom(Random p_239631_0_)
// {
// return Util.getRandom(VALUES, p_239631_0_);
// }
// public static LodDirection getNearest(double p_210769_0_, double p_210769_2_, double p_210769_4_)
// {
// return getNearest((float) p_210769_0_, (float) p_210769_2_, (float) p_210769_4_);
// }
// public static LodDirection getNearest(float p_176737_0_, float p_176737_1_, float p_176737_2_)
// {
// LodDirection lodDirection = NORTH;
// float f = Float.MIN_VALUE;
//
// for (LodDirection direction1 : VALUES)
// {
// float f1 = p_176737_0_ * direction1.normal.x + p_176737_1_ * direction1.normal.y + p_176737_2_ * direction1.normal.z;
// if (f1 > f)
// {
// f = f1;
// lodDirection = direction1;
// }
// }
//
// return lodDirection;
// }
public static EDhDirection get(EDhDirection.AxisDirection p_181076_0_, EDhDirection.Axis p_181076_1_)
{
for (EDhDirection lodDirection : VALUES)
{
if (lodDirection.getAxisDirection() == p_181076_0_ && lodDirection.getAxis() == p_181076_1_)
{
return lodDirection;
}
}
throw new IllegalArgumentException("No such direction: " + p_181076_0_ + " " + p_181076_1_);
}
public Vec3i getNormal()
{
return this.normal;
}
// public boolean isFacingAngle(float p_243532_1_)
// {
// float f = p_243532_1_ * ((float) Math.PI / 180F);
// float f1 = -MathHelper.sin(f);
// float f2 = MathHelper.cos(f);
// return this.normal.getX() * f1 + this.normal.getZ() * f2 > 0.0F;
// }
public enum Axis implements Predicate<EDhDirection>
{
X("x")
{
@Override
public int choose(int x, int y, int z)
{
return x;
}
@Override
public double choose(double x, double y, double z)
{
return x;
}
},
Y("y")
{
@Override
public int choose(int x, int y, int z)
{
return y;
}
@Override
public double choose(double x, double y, double z)
{
return y;
}
},
Z("z")
{
@Override
public int choose(int x, int y, int z)
{
return z;
}
@Override
public double choose(double x, double y, double z)
{
return z;
}
}; };
private static final EDhDirection.Axis[] VALUES = values(); /** North, South, East, West */
public static final EDhDirection[] CARDINAL_COMPASS = new EDhDirection[] {
EDhDirection.EAST,
EDhDirection.WEST,
EDhDirection.SOUTH,
EDhDirection.NORTH
};
private static final Map<String, EDhDirection.Axis> BY_NAME = Arrays.stream(VALUES).collect(Collectors.toMap(EDhDirection.Axis::getName, (p_199785_0_) ->
{
return p_199785_0_;
}));
private final String name;
Axis(String name)
public final String name;
public final EDhDirection.Axis axis;
public final EDhDirection.AxisDirection axisDirection;
public final Vec3i normal;
/** -1 if not a {@link EDhDirection#CARDINAL_COMPASS} direction */
public final int compassIndex;
//=============//
// constructor //
//=============//
EDhDirection(String name, EDhDirection.AxisDirection axisDirection, EDhDirection.Axis axis, Vec3i normal, int compassIndex)
{ {
this.name = name; this.name = name;
this.axis = axis;
this.axisDirection = axisDirection;
this.normal = normal;
this.compassIndex = compassIndex;
} }
public static EDhDirection.Axis byName(String name)
//=========//
// methods //
//=========//
public EDhDirection opposite()
{ {
return BY_NAME.get(name.toLowerCase(Locale.ROOT)); switch(this)
{
case UP:
return EDhDirection.DOWN;
case DOWN:
return EDhDirection.UP;
case NORTH:
return EDhDirection.SOUTH;
case SOUTH:
return EDhDirection.NORTH;
case EAST:
return EDhDirection.WEST;
case WEST:
return EDhDirection.EAST;
default:
throw new IllegalArgumentException();
}
} }
public String getName()
{
return this.name;
}
public boolean isVertical()
{
return this == Y;
}
public boolean isHorizontal()
{
return this == X || this == Z;
}
@Override @Override
public String toString() public String toString() { return this.name; }
{
return this.name;
}
// public static LodDirection.Axis getRandom(Random p_239634_0_)
// {
// return Util.getRandom(VALUES, p_239634_0_); //================//
// } // helper classes //
//================//
/**
* X <br>
* Y <br>
* Z <br>
*/
public enum Axis
{
X("x"),
Y("y"),
Z("z");
public final String name;
//=============//
// constructor //
//=============//
Axis(String name) { this.name = name; }
//=========//
// methods //
//=========//
public boolean isVertical() { return this == Y; }
public boolean isHorizontal() { return this == X || this == Z; }
@Override @Override
public boolean test(EDhDirection p_test_1_) public String toString() { return this.name; }
{
return p_test_1_ != null && p_test_1_.getAxis() == this;
}
// public LodDirection.Plane getPlane()
// {
// switch (this)
// {
// case X:
// case Z:
// return LodDirection.Plane.HORIZONTAL;
// case Y:
// return LodDirection.Plane.VERTICAL;
// default:
// throw new Error("Someone's been tampering with the universe!");
// }
// }
public abstract int choose(int p_196052_1_, int p_196052_2_, int p_196052_3_);
public abstract double choose(double p_196051_1_, double p_196051_3_, double p_196051_5_);
} }
/**
* POSITIVE <br>
* NEGATIVE <br>
*/
public enum AxisDirection public enum AxisDirection
{ {
POSITIVE(1, "Towards positive"), POSITIVE(1, "Towards positive"),
NEGATIVE(-1, "Towards negative"); NEGATIVE(-1, "Towards negative");
private final int step; public final int step;
private final String name; public final String name;
//=============//
// constructor //
//=============//
AxisDirection(int newStep, String newName) AxisDirection(int newStep, String newName)
{ {
@@ -516,77 +185,20 @@ public enum EDhDirection
this.name = newName; this.name = newName;
} }
public int getStep()
{
return this.step;
}
@Override
public String toString() //=========//
{ // methods //
return this.name; //=========//
}
public EDhDirection.AxisDirection opposite() public EDhDirection.AxisDirection opposite()
{ { return (this == POSITIVE) ? NEGATIVE : POSITIVE; }
return this == POSITIVE ? NEGATIVE : POSITIVE;
}
}
// public static enum Plane implements Iterable<LodDirection>, Predicate<LodDirection>
// {
// HORIZONTAL(new LodDirection[] { LodDirection.NORTH, LodDirection.EAST, LodDirection.SOUTH, LodDirection.WEST }, new LodDirection.Axis[] { LodDirection.Axis.X, LodDirection.Axis.Z }),
// VERTICAL(new LodDirection[] { LodDirection.UP, LodDirection.DOWN }, new LodDirection.Axis[] { LodDirection.Axis.Y });
//
// private final LodDirection[] faces;
// private final LodDirection.Axis[] axis;
//
// private Plane(LodDirection[] p_i49393_3_, LodDirection.Axis[] p_i49393_4_)
// {
// this.faces = p_i49393_3_;
// this.axis = p_i49393_4_;
// }
//
// public LodDirection getRandomDirection(Random p_179518_1_)
// {
// return Util.getRandom(this.faces, p_179518_1_);
// }
//
// public LodDirection.Axis getRandomAxis(Random p_244803_1_)
// {
// return Util.getRandom(this.axis, p_244803_1_);
// }
//
// @Override
// public boolean test(@Nullable LodDirection p_test_1_)
// {
// return p_test_1_ != null && p_test_1_.getAxis().getPlane() == this;
// }
//
// @Override
// public Iterator<LodDirection> iterator()
// {
// return Iterators.forArray(this.faces);
// }
//
// public Stream<LodDirection> stream()
// {
// return Arrays.stream(this.faces);
// }
// }
public String getSerializedName()
{
return this.name;
}
@Override @Override
public String toString() public String toString() { return this.name; }
{
return this.name;
} }
} }
@@ -1,369 +0,0 @@
package com.seibel.distanthorizons.core.file;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.structure.ISaveStructure;
import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo;
import com.seibel.distanthorizons.core.sql.dto.IBaseDTO;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.core.util.threading.PositionalLockProvider;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
// TODO is there a reason this is separate from FullDataSourceProviderV2?
// We shouldn't need multiple data source handlers
public abstract class AbstractDataSourceHandler
<TDataSource extends IDataSource<TDhLevel>,
TDTO extends IBaseDTO<Long>,
TRepo extends AbstractDhRepo<Long, TDTO>,
TDhLevel extends IDhLevel>
implements AutoCloseable
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final Set<String> CORRUPT_DATA_ERRORS_LOGGED = Collections.newSetFromMap(new ConcurrentHashMap<>());
/**
* The highest numerical detail level possible.
* Used when determining which positions to update.
*
* @see AbstractDataSourceHandler#MIN_SECTION_DETAIL_LEVEL
*/
public static final byte TOP_SECTION_DETAIL_LEVEL = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL + LodUtil.REGION_DETAIL_LEVEL;
/**
* The lowest numerical detail level possible.
*
* @see AbstractDataSourceHandler#TOP_SECTION_DETAIL_LEVEL
*/
public static final byte MIN_SECTION_DETAIL_LEVEL = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL;
protected final PositionalLockProvider updateLockProvider = new PositionalLockProvider();
/**
* generally just used for debugging,
* keeps track of which positions are currently locked.
*/
public final Set<Long> lockedPosSet = ConcurrentHashMap.newKeySet();
public final ConcurrentHashMap<Long, AtomicInteger> queuedUpdateCountsByPos = new ConcurrentHashMap<>();
protected final ReentrantLock closeLock = new ReentrantLock();
protected volatile boolean isShutdown = false;
protected final TDhLevel level;
protected final File saveDir;
public final TRepo repo;
public final ArrayList<IDataSourceUpdateFunc<TDataSource>> dateSourceUpdateListeners = new ArrayList<>();
//=============//
// constructor //
//=============//
public AbstractDataSourceHandler(TDhLevel level, ISaveStructure saveStructure) { this(level, saveStructure, null); }
public AbstractDataSourceHandler(TDhLevel level, ISaveStructure saveStructure, @Nullable File saveDirOverride)
{
this.level = level;
this.saveDir = (saveDirOverride == null) ? saveStructure.getSaveFolder(level.getLevelWrapper()) : saveDirOverride;
this.repo = this.createRepo();
}
//==================//
// abstract methods //
//==================//
/** When this is called the parent folders should be created */
protected abstract TRepo createRepo();
protected abstract TDataSource createDataSourceFromDto(TDTO dto) throws InterruptedException, IOException, DataCorruptedException;
protected abstract TDTO createDtoFromDataSource(TDataSource dataSource);
protected abstract TDataSource makeEmptyDataSource(long pos);
//==============//
// data reading //
//==============//
/**
* Returns the {@link TDataSource} for the given section position. <Br>
* The returned data source may be null if repo is in the process of shutting down. <Br> <Br>
*
* This call is concurrent. I.e. it supports being called by multiple threads at the same time.
*/
public CompletableFuture<TDataSource> getAsync(long pos)
{
AbstractExecutorService executor = ThreadPoolUtil.getFileHandlerExecutor();
if (executor == null || executor.isTerminated())
{
return CompletableFuture.completedFuture(null);
}
try
{
return CompletableFuture.supplyAsync(() -> this.get(pos), executor);
}
catch (RejectedExecutionException ignore)
{
// the thread pool was probably shut down because it's size is being changed, just wait a sec and it should be back
return CompletableFuture.completedFuture(null);
}
}
/**
* Should only be used in internal file handler methods where we are already running on a file handler thread.
* Can return null if the repo is in the process of being shut down
* @see AbstractDataSourceHandler#getAsync(long)
*/
@Nullable
public TDataSource get(long pos)
{
TDataSource dataSource = null;
try(TDTO dto = this.repo.getByKey(pos))
{
if (dto != null)
{
try
{
// load from database
dataSource = this.createDataSourceFromDto(dto);
}
catch (DataCorruptedException e)
{
// there's a rare issue where the exception doesn't
// have a message, which can cause problems
String message = (e.getMessage() == null) ? e.getMessage() : "No Error message for exception ["+e.getClass().getSimpleName()+"]";
// Only log each message type once.
// This is done to prevent logging "No compression mode with the value [2]" 10,000 times
// if the user is migrating from a nightly build and used ZStd.
if (CORRUPT_DATA_ERRORS_LOGGED.add(message))
{
LOGGER.warn("Corrupted data found at pos [" + DhSectionPos.toString(pos) + "]. Data at position will be deleted so it can be re-generated to prevent issues. Future errors with this same message won't be logged. Error: [" + message + "].", e);
}
this.repo.deleteWithKey(pos);
}
}
else
{
dataSource = this.makeEmptyDataSource(pos);
}
}
catch (InterruptedException ignore) { }
catch (IOException e)
{
LOGGER.warn("File read Error for pos ["+ DhSectionPos.toString(pos)+"], error: "+e.getMessage(), e);
}
return dataSource;
}
//===============//
// data updating //
//===============//
/**
* Can be used if the same thread is already handling IO and/or LOD generation.
* Otherwise the async version {@link AbstractDataSourceHandler#updateDataSourceAsync(FullDataSourceV2)} may be a better choice.
*/
public void updateDataSource(@NotNull FullDataSourceV2 inputDataSource)
{ this.updateDataSourceAtPos(inputDataSource.getPos(), inputDataSource, true); }
/**
* Can be used if you don't want to lock the current thread
* Otherwise the sync version {@link AbstractDataSourceHandler#updateDataSource(FullDataSourceV2)} may be a better choice.
*/
public CompletableFuture<Void> updateDataSourceAsync(@NotNull FullDataSourceV2 inputDataSource)
{
AbstractExecutorService executor = ThreadPoolUtil.getChunkToLodBuilderExecutor();
if (executor == null || executor.isTerminated())
{
return CompletableFuture.completedFuture(null);
}
try
{
this.markUpdateStart(inputDataSource.getPos());
return CompletableFuture.runAsync(() ->
{
try
{
this.updateDataSourceAtPos(inputDataSource.getPos(), inputDataSource, true);
}
catch (Exception e)
{
LOGGER.error("Unexpected error in async data source update at pos: ["+DhSectionPos.toString(inputDataSource.getPos())+"], error: ["+e.getMessage()+"].", e);
}
finally
{
this.markUpdateEnd(inputDataSource.getPos());
}
}, executor);
}
catch (RejectedExecutionException ignore)
{
// can happen if the executor was shutdown while this task was queued
this.markUpdateEnd(inputDataSource.getPos());
return CompletableFuture.completedFuture(null);
}
}
/**
* After this method returns the inputData will be written to file.
*
* @param updatePos the position to update
*/
protected void updateDataSourceAtPos(long updatePos, @NotNull FullDataSourceV2 inputData, boolean lockOnUpdatePos)
{
boolean methodLocked = false;
// a lock is necessary to prevent two threads from writing to the same position at once,
// if that happens only the second update will apply and the LOD will end up with hole(s)
ReentrantLock updateLock = this.updateLockProvider.getLock(updatePos);
try
{
if (lockOnUpdatePos)
{
methodLocked = true;
updateLock.lock();
this.lockedPosSet.add(updatePos);
}
// get or create the data source
try (TDataSource recipientDataSource = this.get(updatePos))
{
if (recipientDataSource != null)
{
boolean dataModified = recipientDataSource.update(inputData, this.level);
if (dataModified)
{
// save the updated data to the database
try (TDTO dto = this.createDtoFromDataSource(recipientDataSource))
{
this.repo.save(dto);
}
for (IDataSourceUpdateFunc<TDataSource> listener : this.dateSourceUpdateListeners)
{
if (listener != null)
{
listener.OnDataSourceUpdated(recipientDataSource);
}
}
}
}
}
}
catch (Exception e)
{
LOGGER.error("Error updating pos ["+DhSectionPos.toString(updatePos)+"], error: "+e.getMessage(), e);
}
finally
{
if (methodLocked)
{
updateLock.unlock();
this.lockedPosSet.remove(updatePos);
}
}
}
//================//
// helper methods //
//================//
/** used for debugging to track which positions are queued for updating */
private void markUpdateStart(long dataSourcePos)
{
this.queuedUpdateCountsByPos.compute(dataSourcePos, (pos, atomicCount) ->
{
if (atomicCount == null)
{
atomicCount = new AtomicInteger(0);
}
atomicCount.incrementAndGet();
return atomicCount;
});
}
/** used for debugging to track which positions are queued for updating */
private void markUpdateEnd(long dataSourcePos)
{
this.queuedUpdateCountsByPos.compute(dataSourcePos, (pos, atomicCount) ->
{
if (atomicCount != null && atomicCount.decrementAndGet() <= 0)
{
atomicCount = null;
}
return atomicCount;
});
}
//=========//
// cleanup //
//=========//
@Override
public void close()
{
try
{
this.closeLock.lock();
this.isShutdown = true;
// wait a moment so any queued saves can finish queuing,
// otherwise we might not see everything that needs saving and attempt to use a closed repo
Thread.sleep(200);
LOGGER.info("Closing [" + this.getClass().getSimpleName() + "] for level: [" + this.level + "].");
this.repo.close();
}
catch (InterruptedException ignore) { }
finally
{
this.closeLock.unlock();
}
}
//================//
// helper classes //
//================//
@FunctionalInterface
public interface IDataSourceUpdateFunc<TDataSource>
{
void OnDataSourceUpdated(TDataSource updatedFullDataSource);
}
}
@@ -1,36 +0,0 @@
package com.seibel.distanthorizons.core.file;
import com.seibel.distanthorizons.api.enums.EDhApiDetailLevel;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.sql.dto.IBaseDTO;
/**
* Base for all data sources. <br><br>
*
* AutoCloseable Can be implemented to allow for disposing of pooled data sources. <br><br>
*
* @param <TDhLevel> there are times when we need specifically a client level vs a more generic level
*/
public interface IDataSource<TDhLevel extends IDhLevel> extends IBaseDTO<Long>, AutoCloseable
{
Long getPos();
/** @return true if the data was changed */
boolean update(FullDataSourceV2 chunkData, TDhLevel level);
//===========//
// meta data //
//===========//
/**
* Returns the detail level of the data contained by this data source.
* IE: 0 for block, 1 for 2x2 blocks, etc.
*
* @see EDhApiDetailLevel
*/
byte getDataDetailLevel();
}
@@ -1,19 +1,16 @@
package com.seibel.distanthorizons.core.file.fullDatafile; package com.seibel.distanthorizons.core.file.fullDatafile;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalCause;
import com.google.common.cache.RemovalNotification;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
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.KeyedLockContainer; import com.seibel.distanthorizons.core.util.KeyedLockContainer;
import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.ThreadUtil;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.Collections; import java.util.Collections;
import java.util.Enumeration;
import java.util.Set; import java.util.Set;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@@ -24,7 +21,7 @@ import java.util.concurrent.locks.ReentrantLock;
*/ */
public class DelayedFullDataSourceSaveCache implements AutoCloseable public class DelayedFullDataSourceSaveCache implements AutoCloseable
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
/** /**
* a cache won't automatically clean itself unless we trigger it's clean method * a cache won't automatically clean itself unless we trigger it's clean method
@@ -38,7 +35,7 @@ public class DelayedFullDataSourceSaveCache implements AutoCloseable
private final Cache<Long, FullDataSourceV2> dataSourceByPosition; private final ConcurrentHashMap<Long, DataSourceSavedTimePair> dataSourceByPosition = new ConcurrentHashMap<Long, DataSourceSavedTimePair>();
/* don't let two threads load the same position at the same time */ /* don't let two threads load the same position at the same time */
protected final KeyedLockContainer<Long> saveLockContainer = new KeyedLockContainer<>(); protected final KeyedLockContainer<Long> saveLockContainer = new KeyedLockContainer<>();
@@ -60,16 +57,15 @@ public class DelayedFullDataSourceSaveCache implements AutoCloseable
public DelayedFullDataSourceSaveCache(@NotNull ISaveDataSourceFunc onSaveTimeoutAsyncFunc, int saveDelayInMs) public DelayedFullDataSourceSaveCache(@NotNull ISaveDataSourceFunc onSaveTimeoutAsyncFunc, int saveDelayInMs)
{ {
this.onSaveTimeoutAsyncFunc = onSaveTimeoutAsyncFunc; this.onSaveTimeoutAsyncFunc = onSaveTimeoutAsyncFunc;
// we can't clean items faster than the cleanup timer fires
if (saveDelayInMs < CLEANUP_CHECK_TIME_IN_MS)
{
LOGGER.warn("The save delay ["+saveDelayInMs+"] shouldn't be less than the cleanup check timer interval ["+CLEANUP_CHECK_TIME_IN_MS+"].");
}
this.saveDelayInMs = saveDelayInMs; this.saveDelayInMs = saveDelayInMs;
this.dataSourceByPosition =
CacheBuilder.newBuilder()
.expireAfterAccess(this.saveDelayInMs, TimeUnit.MILLISECONDS)
.expireAfterWrite(this.saveDelayInMs, TimeUnit.MILLISECONDS)
.removalListener(this::handleDataSourceRemoval)
.<Long, FullDataSourceV2>build();
SAVE_CACHE_SET.add(new WeakReference<>(this)); SAVE_CACHE_SET.add(new WeakReference<>(this));
} }
@@ -83,62 +79,62 @@ public class DelayedFullDataSourceSaveCache implements AutoCloseable
* Writing into memory is done synchronously so inputDataSource can * Writing into memory is done synchronously so inputDataSource can
* be closed after this method finishes. * be closed after this method finishes.
*/ */
public void writeDataSourceToMemoryAndQueueSave(FullDataSourceV2 inputDataSource) public void writeDataSourceToMemoryAndQueueSave(@NotNull FullDataSourceV2 inputDataSource)
{ {
long inputPos = inputDataSource.getPos(); long inputPos = inputDataSource.getPos();
ReentrantLock lock = this.saveLockContainer.getLockForPos(inputPos); ReentrantLock lockForPos = this.saveLockContainer.getLockForPos(inputPos);
try try
{ {
lock.lock(); lockForPos.lock();
FullDataSourceV2 memoryDataSource = this.dataSourceByPosition.getIfPresent(inputPos); FullDataSourceV2 memoryDataSource;
if (memoryDataSource == null)
DataSourceSavedTimePair pair = this.dataSourceByPosition.getOrDefault(inputPos, null);
if (pair == null)
{ {
// no data currently in the memory cache for this position
memoryDataSource = FullDataSourceV2.createEmpty(inputPos); memoryDataSource = FullDataSourceV2.createEmpty(inputPos);
pair = new DataSourceSavedTimePair(memoryDataSource);
this.dataSourceByPosition.put(inputPos, pair);
} }
memoryDataSource.update(inputDataSource); else
this.dataSourceByPosition.put(inputPos, memoryDataSource); {
memoryDataSource = pair.dataSource;
}
// write the new data into memory
memoryDataSource.updateFromDataSource(inputDataSource);
// keep track of when the last time we saved something was
pair.updateLastWrittenTimestamp();
} }
finally finally
{ {
lock.unlock(); lockForPos.unlock();
} }
} }
public void handleDataSourceRemoval(RemovalNotification<Long, FullDataSourceV2> removalNotification) /** when this method is called the datasource should no longer be in the memory cache */
public void handleDataSourceRemoval(@NotNull FullDataSourceV2 removedDataSource)
{ {
RemovalCause cause = removalNotification.getCause(); this.onSaveTimeoutAsyncFunc.saveAsync(removedDataSource)
if (cause == RemovalCause.EXPIRED
|| cause == RemovalCause.COLLECTED
|| cause == RemovalCause.EXPLICIT
|| cause == RemovalCause.SIZE)
{
// close the data source after it has expired from the cache
FullDataSourceV2 dataSource = removalNotification.getValue();
if (dataSource != null)
{
this.onSaveTimeoutAsyncFunc.saveAsync(dataSource)
.handle((voidObj, throwable) -> .handle((voidObj, throwable) ->
{ {
try try
{ {
dataSource.close(); // if this close method is fired multiple times
// monoliths can appear due to concurrent writing to the
// backend arrays
removedDataSource.close();
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.error("Unable to close datasource ["+ DhSectionPos.toString(dataSource.getPos()) +"], removal cause: ["+cause+"], error: ["+e.getMessage()+"].", e); LOGGER.error("Unable to close datasource ["+ DhSectionPos.toString(removedDataSource.getPos()) +"], error: ["+e.getMessage()+"].", e);
} }
return null; return null;
}); });
} }
else
{
LOGGER.error("Unable to close null cached data source.");
}
}
}
@@ -146,26 +142,36 @@ public class DelayedFullDataSourceSaveCache implements AutoCloseable
// List methods // // List methods //
//==============// //==============//
public int getUnsavedCount() { return (int)this.dataSourceByPosition.size(); } public int getUnsavedCount() { return this.dataSourceByPosition.size(); }
public void flush() { this.cleanUp(true); }
/** Removes everything from the memory cache and fires the {@link DelayedFullDataSourceSaveCache#onSaveTimeoutAsyncFunc} for each. */ /** Removes everything from the memory cache and fires the {@link DelayedFullDataSourceSaveCache#onSaveTimeoutAsyncFunc} for each. */
public void flush() public void cleanUp(boolean flushAll)
{ {
Set<Long> keySet = this.dataSourceByPosition.asMap().keySet(); Enumeration<Long> keyIterator = this.dataSourceByPosition.keys();
for (Long pos : keySet) while (keyIterator.hasMoreElements())
{ {
ReentrantLock lock = this.saveLockContainer.getLockForPos(pos); Long pos = keyIterator.nextElement();
ReentrantLock posLock = this.saveLockContainer.getLockForPos(pos);
try try
{ {
lock.lock(); posLock.lock();
this.dataSourceByPosition.invalidate(pos); DataSourceSavedTimePair savedPair = this.dataSourceByPosition.getOrDefault(pos, null);
if (savedPair != null)
{
if (flushAll
|| savedPair.dataSourceHasTimedOut(this.saveDelayInMs))
{
this.dataSourceByPosition.remove(pos);
this.handleDataSourceRemoval(savedPair.dataSource);
}
}
} }
finally finally
{ {
lock.unlock(); posLock.unlock();
} }
} }
} }
@@ -197,7 +203,7 @@ public class DelayedFullDataSourceSaveCache implements AutoCloseable
} }
else else
{ {
cache.dataSourceByPosition.cleanUp(); cache.cleanUp(false);
} }
}); });
} }
@@ -240,4 +246,36 @@ public class DelayedFullDataSourceSaveCache implements AutoCloseable
CompletableFuture<Void> saveAsync(FullDataSourceV2 inputDataSource); CompletableFuture<Void> saveAsync(FullDataSourceV2 inputDataSource);
} }
/**
* used to keep track of when data sources
* were written to so we can flush them once
* enough time has passed.
*/
private static class DataSourceSavedTimePair
{
@NotNull
public final FullDataSourceV2 dataSource;
/** the last unix millisecond time this data source was written to */
public long lastWrittenDateTimeMs;
public DataSourceSavedTimePair(@NotNull FullDataSourceV2 dataSource)
{
this.dataSource = dataSource;
this.lastWrittenDateTimeMs = System.currentTimeMillis();
}
public void updateLastWrittenTimestamp()
{ this.lastWrittenDateTimeMs = System.currentTimeMillis(); }
public boolean dataSourceHasTimedOut(long msTillTimeout)
{
long currentTime = System.currentTimeMillis();
long timeSinceUpdate = currentTime - this.lastWrittenDateTimeMs;
return (timeSinceUpdate > msTillTimeout);
}
}
} }
@@ -1,813 +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.file.fullDatafile;
import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode;
import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV1;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.file.structure.ISaveStructure;
import com.seibel.distanthorizons.core.file.AbstractDataSourceHandler;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult;
import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo;
import com.seibel.distanthorizons.core.sql.repo.FullDataSourceV2Repo;
import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
import java.text.NumberFormat;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
/**
* Handles reading/writing {@link FullDataSourceV2}
* to and from the database.
*/
public class FullDataSourceProviderV2
extends AbstractDataSourceHandler<FullDataSourceV2, FullDataSourceV2DTO, FullDataSourceV2Repo, IDhLevel>
implements IDebugRenderable
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
protected static final int NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD = 5;
/** how many parent update tasks can be in the queue at once */
protected static int getMaxUpdateTaskCount() { return NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD* Config.Common.MultiThreading.numberOfThreads.get(); }
/** indicates how long the update queue thread should wait between queuing ticks */
protected static final int UPDATE_QUEUE_THREAD_DELAY_IN_MS = 250;
/** how many data sources should be pulled down for migration at once */
private static final int MIGRATION_BATCH_COUNT = NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD;
/**
* 5 minutes <br>
* This should be much longer than any update should take. This is just
* to make sure the thread doesn't get stuck.
*/
private static final int MIGRATION_MAX_UPDATE_TIMEOUT_IN_MS = 5 * 60 * 1_000;
/**
* Interrupting the migration thread pool doesn't work well and may corrupt the database
* vs gracefully shutting down the thread ourselves.
*/
protected final AtomicBoolean migrationThreadRunning = new AtomicBoolean(true);
protected final FullDataSourceProviderV1<IDhLevel> legacyFileHandler;
protected boolean migrationStartMessageQueued = false;
protected long legacyDeletionCount = -1;
protected long migrationCount = -1;
protected boolean migrationStoppedWithError = false;
/**
* Tracks which positions are currently being updated
* to prevent duplicate concurrent updates.
*/
public final Set<Long> updatingPosSet = ConcurrentHashMap.newKeySet();
// TODO only run thread if modifications happened recently
/**
* This isn't in {@link AbstractDataSourceHandler} since we only want to update
* the newest version of the full data, so if we have providers for either
* render data or old full data, we don't want to update them. <br><br>
*
* Will be null on the dedicated server since updates don't need to be propagated,
* only the highest detail level is needed.
*/
@Nullable
private final ThreadPoolExecutor updateQueueProcessor;
//=============//
// constructor //
//=============//
public FullDataSourceProviderV2(IDhLevel level, ISaveStructure saveStructure) { this(level, saveStructure, null); }
public FullDataSourceProviderV2(IDhLevel level, ISaveStructure saveStructure, @Nullable File saveDirOverride)
{
super(level, saveStructure, saveDirOverride);
this.legacyFileHandler = new FullDataSourceProviderV1<>(level, saveStructure, saveDirOverride);
DebugRenderer.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showFullDataUpdateStatus);
String levelId = level.getLevelWrapper().getDhIdentifier();
// start migrating any legacy data sources present in the background
ThreadPoolExecutor executor = ThreadPoolUtil.getFullDataMigrationExecutor();
if (executor != null)
{
executor.execute(this::convertLegacyDataSources);
}
else
{
// shouldn't happen, but just in case
LOGGER.error("Unable to start migration for level: ["+levelId+"] due to missing executor.");
}
// update propagation doesn't need to be run on the server since only the highest detail level is needed
this.updateQueueProcessor = ThreadUtil.makeSingleThreadPool("Parent Update Queue [" + levelId + "]");
this.updateQueueProcessor.execute(this::runUpdateQueue);
}
//====================//
// Abstract overrides //
//====================//
@Override
protected 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);
}
}
@Override
protected FullDataSourceV2DTO createDtoFromDataSource(FullDataSourceV2 dataSource)
{
try
{
// when creating new data use the compressor currently selected in the config
EDhApiDataCompressionMode compressionModeEnum = Config.Common.LodBuilding.dataCompression.get();
return FullDataSourceV2DTO.CreateFromDataSource(dataSource, compressionModeEnum);
}
catch (IOException e)
{
LOGGER.warn("Unable to create DTO, error: "+e.getMessage(), e);
return null;
}
}
@Override
protected FullDataSourceV2 createDataSourceFromDto(FullDataSourceV2DTO dto) throws InterruptedException, IOException, DataCorruptedException
{ return dto.createDataSource(this.level.getLevelWrapper()); }
@Override
protected FullDataSourceV2 makeEmptyDataSource(long pos)
{ return FullDataSourceV2.createEmpty(pos); }
//================//
// parent updates //
//================//
private void runUpdateQueue()
{
while (!Thread.interrupted())
{
try
{
Thread.sleep(UPDATE_QUEUE_THREAD_DELAY_IN_MS);
PriorityTaskPicker.Executor executor = ThreadPoolUtil.getUpdatePropagatorExecutor();
if (executor == null || executor.isTerminated())
{
continue;
}
// TODO it might be worth skipping this logic if no parent updates happened
// update positions closest to the player (if not on a server)
// to make world gen appear faster
DhBlockPos targetBlockPos = DhBlockPos.ZERO;
if (MC_CLIENT != null && MC_CLIENT.playerExists())
{
targetBlockPos = MC_CLIENT.getPlayerBlockPos();
}
this.runParentUpdates(executor, targetBlockPos);
if (Config.Common.LodBuilding.Experimental.upsampleLowerDetailLodsToFillHoles.get())
{
this.runChildUpdates(executor, targetBlockPos);
}
}
catch (InterruptedException ignored)
{
Thread.currentThread().interrupt();
}
catch (Exception e)
{
LOGGER.error("Unexpected error in the parent update queue thread. Error: " + e.getMessage(), e);
}
}
LOGGER.info("Update thread ["+Thread.currentThread().getName()+"] terminated.");
}
/** will always apply updates */
private void runParentUpdates(PriorityTaskPicker.Executor executor, DhBlockPos targetBlockPos)
{
int maxUpdateTaskCount = getMaxUpdateTaskCount();
// queue parent updates
if (executor.getQueueSize() < maxUpdateTaskCount
&& this.updatingPosSet.size() < maxUpdateTaskCount)
{
// get the positions that need to be applied to their parents
LongArrayList parentUpdatePosList = this.repo.getPositionsToUpdate(targetBlockPos.getX(), targetBlockPos.getZ(), maxUpdateTaskCount);
// combine updates together based on their parent
HashMap<Long, HashSet<Long>> updatePosByParentPos = new HashMap<>();
for (Long pos : parentUpdatePosList)
{
updatePosByParentPos.compute(DhSectionPos.getParentPos(pos), (parentPos, updatePosSet) ->
{
if (updatePosSet == null)
{
updatePosSet = new HashSet<>();
}
updatePosSet.add(pos);
return updatePosSet;
});
}
// queue the updates
for (Long parentUpdatePos : updatePosByParentPos.keySet())
{
// stop if there are already a bunch of updates queued
if (this.updatingPosSet.size() > maxUpdateTaskCount
|| executor.getQueueSize() > maxUpdateTaskCount
|| !this.updatingPosSet.add(parentUpdatePos))
{
break;
}
try
{
executor.execute(() ->
{
ReentrantLock parentWriteLock = this.updateLockProvider.getLock(parentUpdatePos);
boolean parentLocked = false;
try
{
//LOGGER.info("updating parent: "+parentUpdatePos);
// Locking the parent before the children should prevent deadlocks.
// TryLock is used instead of lock so this thread can handle a different update.
if (parentWriteLock.tryLock())
{
parentLocked = true;
this.lockedPosSet.add(parentUpdatePos);
try (FullDataSourceV2 parentDataSource = this.get(parentUpdatePos))
{
// will return null if the file handler is shutting down
if (parentDataSource != null)
{
// apply each child pos to the parent
for (Long childPos : updatePosByParentPos.get(parentUpdatePos))
{
ReentrantLock childReadLock = this.updateLockProvider.getLock(childPos);
try
{
childReadLock.lock();
this.lockedPosSet.add(childPos);
try (FullDataSourceV2 childDataSource = this.get(childPos))
{
// can return null when the file handler is being shut down
if (childDataSource != null)
{
parentDataSource.update(childDataSource);
}
}
}
catch (Exception e)
{
LOGGER.error("Unexpected in parent update propagation for parent pos: ["+DhSectionPos.toString(parentUpdatePos)+"], child pos: [" + DhSectionPos.toString(parentUpdatePos) + "], Error: [" + e.getMessage() + "].", e);
}
finally
{
childReadLock.unlock();
this.lockedPosSet.remove(childPos);
}
}
if (DhSectionPos.getDetailLevel(parentUpdatePos) < TOP_SECTION_DETAIL_LEVEL)
{
parentDataSource.applyToParent = true;
}
this.updateDataSourceAtPos(parentUpdatePos, parentDataSource, false);
for (Long childPos : updatePosByParentPos.get(parentUpdatePos))
{
this.repo.setApplyToParent(childPos, false);
}
}
}
}
}
finally
{
if (parentLocked)
{
parentWriteLock.unlock();
this.lockedPosSet.remove(parentUpdatePos);
}
this.updatingPosSet.remove(parentUpdatePos);
}
});
}
catch (RejectedExecutionException ignore)
{ /* the executor was shut down, it should be back up shortly and able to accept new jobs */ }
catch (Exception e)
{
this.updatingPosSet.remove(parentUpdatePos);
throw e;
}
}
}
}
/** stops if it finds any LOD data */
private void runChildUpdates(PriorityTaskPicker.Executor executor, DhBlockPos targetBlockPos)
{
int maxUpdateTaskCount = getMaxUpdateTaskCount();
// queue child updates
if (executor.getQueueSize() < maxUpdateTaskCount
&& this.updatingPosSet.size() < maxUpdateTaskCount)
{
// get the positions that need to be applied to their children
LongArrayList childUpdatePosList = this.repo.getChildPositionsToUpdate(targetBlockPos.getX(), targetBlockPos.getZ(), maxUpdateTaskCount);
// queue the updates
for (long parentUpdatePos : childUpdatePosList)
{
// stop if there are already a bunch of updates queued
if (this.updatingPosSet.size() > maxUpdateTaskCount
|| executor.getQueueSize() > maxUpdateTaskCount)
{
break;
}
// skip already updating positions
if (!this.updatingPosSet.add(parentUpdatePos))
{
continue;
}
try
{
executor.execute(() ->
{
ReentrantLock parentReadLock = this.updateLockProvider.getLock(parentUpdatePos);
boolean parentLocked = false;
try
{
//LOGGER.info("updating parent: "+parentUpdatePos);
// Locking the parent before the children should prevent deadlocks.
// TryLock is used instead of lock so this thread can handle a different update.
if (parentReadLock.tryLock())
{
parentLocked = true;
this.lockedPosSet.add(parentUpdatePos);
try (FullDataSourceV2 parentDataSource = this.get(parentUpdatePos))
{
// will return null if the file handler is shutting down
if (parentDataSource != null)
{
// apply parent to each child
for (int i = 0; i < 4; i++)
{
long childPos = DhSectionPos.getChildByIndex(parentUpdatePos, i);
ReentrantLock childWriteLock = this.updateLockProvider.getLock(childPos);
try
{
childWriteLock.lock();
this.lockedPosSet.add(childPos);
try (FullDataSourceV2 childDataSource = this.get(childPos))
{
// will return null if the file handler is shutting down
if (childDataSource != null)
{
childDataSource.update(parentDataSource);
// don't propagate child updates past the bottom of the tree
if (DhSectionPos.getDetailLevel(childPos) != DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL)
{
childDataSource.applyToChildren = true;
}
this.updateDataSourceAtPos(childPos, childDataSource, false);
}
}
}
catch (Exception e)
{
LOGGER.error("Unexpected in child update propagation for parent pos: ["+DhSectionPos.toString(parentUpdatePos)+"], child pos: [" + DhSectionPos.toString(parentUpdatePos) + "], Error: [" + e.getMessage() + "].", e);
}
finally
{
childWriteLock.unlock();
this.lockedPosSet.remove(childPos);
}
}
this.repo.setApplyToChild(parentUpdatePos, false);
}
}
}
}
finally
{
if (parentLocked)
{
parentReadLock.unlock();
this.lockedPosSet.remove(parentUpdatePos);
}
this.updatingPosSet.remove(parentUpdatePos);
}
});
}
catch (RejectedExecutionException ignore)
{ /* the executor was shut down, it should be back up shortly and able to accept new jobs */ }
catch (Exception e)
{
this.updatingPosSet.remove(parentUpdatePos);
throw e;
}
}
}
}
//=======================//
// data source migration //
//=======================//
private void convertLegacyDataSources()
{
try
{
String levelId = this.level.getLevelWrapper().getDhIdentifier();
LOGGER.info("Attempting to migrate data sources for: [" + levelId + "]-[" + this.saveDir + "]...");
this.migrationThreadRunning.set(true);
//============================//
// delete unused data sources //
//============================//
// this could be done all at once via SQL,
// but doing it in chunks prevents locking the database for long periods of time
long unusedCount = 0;
long totalDeleteCount = this.legacyFileHandler.repo.getUnusedDataSourceCount();
if (totalDeleteCount != 0)
{
// this should only be shown once per session but should be shown during
// either when the deletion or migration phases start
this.showMigrationStartMessage();
LOGGER.info("deleting [" + levelId + "] - [" + totalDeleteCount + "] unused data sources...");
this.legacyDeletionCount = totalDeleteCount;
ArrayList<String> unusedDataPosList = this.legacyFileHandler.repo.getUnusedDataSourcePositionStringList(50);
while (unusedDataPosList.size() != 0)
{
unusedCount += unusedDataPosList.size();
this.legacyDeletionCount -= unusedDataPosList.size();
long startTime = System.currentTimeMillis();
// delete batch and get next batch
this.legacyFileHandler.repo.deleteUnusedLegacyData(unusedDataPosList);
unusedDataPosList = this.legacyFileHandler.repo.getUnusedDataSourcePositionStringList(50);
long endStart = System.currentTimeMillis();
long deleteTime = endStart - startTime;
LOGGER.info("Deleting [" + levelId + "] - [" + unusedCount + "/" + totalDeleteCount + "] in [" + deleteTime + "]ms ...");
// a slight delay is added to prevent accidentally locking the database when deleting a lot of rows
// (that shouldn't be the case since we're using WAL journaling, but just in case)
try
{
// use the delete time so we don't make powerful computers wait super long
// and weak computers wait no time at all
Thread.sleep(deleteTime / 2);
}
catch (InterruptedException ignore)
{
}
}
LOGGER.info("Done deleting [" + levelId + "] - [" + totalDeleteCount + "] unused data sources.");
}
//===========//
// migration //
//===========//
long totalMigrationCount = this.legacyFileHandler.getDataSourceMigrationCount();
this.migrationCount = totalMigrationCount;
LOGGER.info("Found [" + totalMigrationCount + "] data sources that need migration.");
ArrayList<FullDataSourceV1> legacyDataSourceList = this.legacyFileHandler.getDataSourcesToMigrate(MIGRATION_BATCH_COUNT);
if (!legacyDataSourceList.isEmpty())
{
this.showMigrationStartMessage();
try
{
// keep going until every data source has been migrated
int progressCount = 0;
while (!legacyDataSourceList.isEmpty() && this.migrationThreadRunning.get())
{
NumberFormat numFormat = F3Screen.NUMBER_FORMAT;
LOGGER.info("Migrating [" + levelId + "] - [" + numFormat.format(progressCount) + "/" + numFormat.format(totalMigrationCount) + "]...");
ArrayList<CompletableFuture<Void>> updateFutureList = new ArrayList<>();
for (int i = 0; i < legacyDataSourceList.size() && this.migrationThreadRunning.get(); i++)
{
FullDataSourceV1 legacyDataSource = legacyDataSourceList.get(i);
try
{
// convert the legacy data source to the new format,
// this is a relatively cheap operation
FullDataSourceV2 newDataSource = FullDataSourceV2.createFromLegacyDataSourceV1(legacyDataSource);
newDataSource.applyToParent = true;
// the actual update process can be moderately expensive due to having to update
// the render data along with the full data, so running it async on the update threads gains us a good bit of speed
CompletableFuture<Void> future = this.updateDataSourceAsync(newDataSource);
updateFutureList.add(future);
future.thenRun(() ->
{
// after the update finishes the legacy data source can be safely deleted
this.legacyFileHandler.repo.deleteWithKey(legacyDataSource.getPos());
newDataSource.close();
});
}
catch (Exception e)
{
Long migrationPos = legacyDataSource.getPos();
LOGGER.warn("Unexpected issue migrating data source at pos [" + DhSectionPos.toString(migrationPos) + "]. Error: " + e.getMessage(), e);
this.legacyFileHandler.markMigrationFailed(migrationPos);
}
}
try
{
// wait for each thread to finish updating
CompletableFuture<Void> combinedFutures = CompletableFuture.allOf(updateFutureList.toArray(new CompletableFuture[0]));
combinedFutures.get(MIGRATION_MAX_UPDATE_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS);
}
catch (InterruptedException | TimeoutException e)
{
LOGGER.warn("Migration update timed out after [" + MIGRATION_MAX_UPDATE_TIMEOUT_IN_MS + "] milliseconds. Migration will re-try the same positions again in a moment.", e);
}
catch (ExecutionException e)
{
LOGGER.warn("Migration update failed. Migration will re-try the same positions again. Error:" + e.getMessage(), e);
}
legacyDataSourceList = this.legacyFileHandler.getDataSourcesToMigrate(MIGRATION_BATCH_COUNT);
progressCount += legacyDataSourceList.size();
this.migrationCount -= legacyDataSourceList.size();
}
}
catch (Exception e)
{
LOGGER.info("migration stopped due to error for: [" + levelId + "]-[" + this.saveDir + "], error: [" + e.getMessage() + "].", e);
this.showMigrationEndMessage(false);
this.migrationStoppedWithError = true;
}
finally
{
if (this.migrationThreadRunning.get())
{
LOGGER.info("migration complete for: [" + levelId + "]-[" + this.saveDir + "].");
this.showMigrationEndMessage(true);
this.migrationCount = 0;
}
else
{
LOGGER.info("migration stopped for: [" + levelId + "]-[" + this.saveDir + "].");
this.showMigrationEndMessage(false);
this.migrationStoppedWithError = true;
}
}
}
else
{
LOGGER.info("No migration necessary.");
}
}
finally
{
this.migrationThreadRunning.set(false);
}
}
public long getLegacyDeletionCount() { return this.legacyDeletionCount; }
public long getTotalMigrationCount() { return this.migrationCount; }
public boolean getMigrationStoppedWithError() { return this.migrationStoppedWithError; }
private void showMigrationStartMessage()
{
if (this.migrationStartMessageQueued)
{
return;
}
this.migrationStartMessageQueued = true;
String levelId = this.level.getLevelWrapper().getDhIdentifier();
ClientApi.INSTANCE.showChatMessageNextFrame(
"Old Distant Horizons data is being migrated for ["+levelId+"]. \n" +
"While migrating LODs may load slowly \n" +
"and DH world gen will be disabled. \n" +
"You can see migration progress in the F3 menu."
);
}
private void showMigrationEndMessage(boolean success)
{
String levelId = this.level.getLevelWrapper().getDhIdentifier();
if (success)
{
ClientApi.INSTANCE.showChatMessageNextFrame("Distant Horizons data migration for ["+levelId+"] completed.");
}
else
{
ClientApi.INSTANCE.showChatMessageNextFrame(
"Distant Horizons data migration for ["+levelId+"] stopped. \n" +
"Some data may not have been migrated."
);
}
}
//=======================//
// retrieval (world gen) //
//=======================//
/**
* Returns true if this provider can generate or retrieve
* {@link FullDataSourceV2}'s that aren't currently in the database.
*/
public boolean canRetrieveMissingDataSources()
{
// the base handler just handles basic reading/writing
// to the database and as such can't retrieve anything else.
return false;
}
/**
* Returns false if this provider isn't accepting new requests,
* this can be due to having a full queue or some other
* limiting factor. <br><br>
*
* Note: when overriding make sure to add: <br>
* <code>
* if (!super.canQueueRetrieval()) <br>
* { <br>
* return false; <br>
* } <br>
* </code>
* to the beginning of your override.
* Otherwise, parent retrieval limits will be ignored.
*/
public boolean canQueueRetrieval()
{
// Retrieval shouldn't happen while an unknown number of
// legacy data sources are present.
// If retrieval was allowed we might run into concurrency issues.
return !this.migrationThreadRunning.get();
}
/**
* @return null if this provider can't generate any positions and
* an empty array if all positions were generated
*/
@Nullable
public LongArrayList getPositionsToRetrieve(Long pos) { return null; }
/** @return true if the position was queued, false if not */
@Nullable
public CompletableFuture<WorldGenResult> queuePositionForRetrieval(Long genPos) { return null; }
/** does nothing if the given position isn't present in the queue */
public void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf) { }
public void clearRetrievalQueue() { }
/** Can be used to display how many total retrieval requests might be available. */
public void setTotalRetrievalPositionCount(int newCount) { }
/** Can be used to display how many total chunk retrieval requests should be available. */
public void setEstimatedRemainingRetrievalChunkCount(int newCount) { }
public boolean fileExists(long pos) { return this.repo.getDataSizeInBytes(pos) > 0; }
//========================//
// multiplayer networking //
//========================//
@Nullable
public Long getTimestampForPos(long pos)
{ return this.repo.getTimestampForPos(pos); }
//===========//
// overrides //
//===========//
@Override
public void debugRender(DebugRenderer renderer)
{
this.lockedPosSet
.forEach((pos) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 74f, 0.15f, Color.PINK)); });
this.queuedUpdateCountsByPos
.forEach((pos, updateCountRef) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 80f + (updateCountRef.get() * 16f), 0.20f, Color.WHITE)); });
this.updatingPosSet
.forEach((pos) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 80f, 0.20f, Color.MAGENTA)); });
}
@Override
public void close()
{
super.close();
if (this.updateQueueProcessor != null)
{
this.updateQueueProcessor.shutdownNow();
}
this.legacyFileHandler.close();
this.migrationThreadRunning.set(false);
}
}
@@ -23,12 +23,15 @@ import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGeneratio
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.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.fullDatafile.V2.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.file.fullDatafile.V2.FullDataUpdatePropagatorV2;
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.IWorldGenTaskTracker;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult;
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.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
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.pooling.PhantomArrayListPool;
@@ -40,11 +43,12 @@ 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.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull; 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;
@@ -54,7 +58,7 @@ import java.util.stream.IntStream;
public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 implements IDebugRenderable public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 implements IDebugRenderable
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();;
/** /**
* Having this number too high causes the system to become overwhelmed by * Having this number too high causes the system to become overwhelmed by
@@ -78,8 +82,17 @@ 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 { super(level, saveStructure); }
public GeneratedFullDataSourceProvider(IDhLevel level, ISaveStructure saveStructure, @Nullable File saveDirOverride) { super(level, saveStructure, saveDirOverride); } public GeneratedFullDataSourceProvider(IDhLevel level, ISaveStructure saveStructure, @Nullable File saveDirOverride) throws SQLException, IOException
{
super(level, saveStructure, saveDirOverride);
this.addDataSourceUpdateListener((@NotNull FullDataSourceV2 updatedData) ->
{
this.onWorldGenTaskComplete(WorldGenResult.CreateSuccess(updatedData.getPos()), null);
});
}
@@ -176,7 +189,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
{ {
boolean oldQueueExists = this.worldGenQueueRef.compareAndSet(null, newWorldGenQueue); boolean oldQueueExists = this.worldGenQueueRef.compareAndSet(null, newWorldGenQueue);
LodUtil.assertTrue(oldQueueExists, "previous world gen queue is still here!"); LodUtil.assertTrue(oldQueueExists, "previous world gen queue is still here!");
LOGGER.info("Set world gen queue for level [" + this.level.getLevelWrapper().getDhIdentifier() + "]."); LOGGER.info("Set world gen queue for level [" + this.levelId + "].");
} }
@Override @Override
@@ -210,8 +223,18 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
} }
PriorityTaskPicker.Executor fileExecutor = ThreadPoolUtil.getFileHandlerExecutor(); PriorityTaskPicker.Executor renderLoadExecutor = ThreadPoolUtil.getRenderLoadingExecutor();
if (fileExecutor == null || fileExecutor.getQueueSize() >= getMaxUpdateTaskCount() / 2) if (renderLoadExecutor == null
|| renderLoadExecutor.getQueueSize() >= FullDataUpdatePropagatorV2.getMaxPropagateTaskCount() / 2)
{
// don't queue additional world gen requests if the render loader handler is overwhelmed,
// otherwise LODs may not load in properly
return false;
}
PriorityTaskPicker.Executor fileHandlerExecutor = ThreadPoolUtil.getFileHandlerExecutor();
if (fileHandlerExecutor == null
|| fileHandlerExecutor.getQueueSize() >= FullDataUpdatePropagatorV2.getMaxPropagateTaskCount() / 2)
{ {
// don't queue additional world gen requests if the file handler is overwhelmed, // don't queue additional world gen requests if the file handler is overwhelmed,
// otherwise LODs may not load in properly // otherwise LODs may not load in properly
@@ -244,6 +267,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
return false; return false;
} }
int availableTaskSlots = maxWorldGenQueueCount - worldGenQueue.getWaitingTaskCount(); int availableTaskSlots = maxWorldGenQueueCount - worldGenQueue.getWaitingTaskCount();
if (availableTaskSlots <= 0) if (availableTaskSlots <= 0)
{ {
@@ -282,17 +306,6 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
return worldGenFuture; return worldGenFuture;
} }
@Override
protected void updateDataSourceAtPos(long updatePos, @NotNull FullDataSourceV2 inputData, boolean lockOnUpdatePos)
{
super.updateDataSourceAtPos(updatePos, inputData, lockOnUpdatePos);
//if (SharedApi.getEnvironment() != EWorldEnvironment.CLIENT_ONLY)
// LOGGER.info("updated ["+DhSectionPos.toString(updatePos)+"]");
this.onWorldGenTaskComplete(WorldGenResult.CreateSuccess(updatePos), null);
}
@Override @Override
public void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf) public void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf)
{ {
@@ -0,0 +1,7 @@
package com.seibel.distanthorizons.core.file.fullDatafile;
@FunctionalInterface
public interface IDataSourceUpdateListenerFunc<TDataSource>
{
void OnDataSourceUpdated(TDataSource updatedFullDataSource);
}
@@ -24,24 +24,26 @@ import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSour
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.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.multiplayer.client.SyncOnLoadRequestQueue; import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoadRequestQueue;
import org.apache.logging.log4j.Logger; 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
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
@Nullable @Nullable
private final SyncOnLoadRequestQueue syncOnLoadRequestQueue; private final SyncOnLoadRequestQueue syncOnLoadRequestQueue;
@@ -58,7 +60,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;
@@ -1,4 +1,4 @@
package com.seibel.distanthorizons.core.file.fullDatafile; package com.seibel.distanthorizons.core.file.fullDatafile.V1;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV1; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV1;
import com.seibel.distanthorizons.core.file.structure.ISaveStructure; import com.seibel.distanthorizons.core.file.structure.ISaveStructure;
@@ -9,9 +9,10 @@ import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV1DTO;
import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo; import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo;
import com.seibel.distanthorizons.core.sql.repo.FullDataSourceV1Repo; import com.seibel.distanthorizons.core.sql.repo.FullDataSourceV1Repo;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.Logger; 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;
@@ -21,13 +22,12 @@ import java.util.ArrayList;
import java.util.concurrent.AbstractExecutorService; import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
public class FullDataSourceProviderV1<TDhLevel extends IDhLevel> public class FullDataSourceProviderV1<TDhLevel extends IDhLevel>
implements AutoCloseable implements AutoCloseable
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
protected final ReentrantLock closeLock = new ReentrantLock(); protected final ReentrantLock closeLock = new ReentrantLock();
protected volatile boolean isShutdown = false; protected volatile boolean isShutdown = false;
@@ -43,16 +43,16 @@ public class FullDataSourceProviderV1<TDhLevel extends IDhLevel>
// constructor // // constructor //
//=============// //=============//
public FullDataSourceProviderV1(TDhLevel level, ISaveStructure saveStructure, @Nullable File saveDirOverride) public FullDataSourceProviderV1(TDhLevel level, File saveDir) throws SQLException, IOException
{ {
this.level = level; this.level = level;
this.saveDir = (saveDirOverride == null) ? saveStructure.getSaveFolder(level.getLevelWrapper()) : saveDirOverride; this.saveDir = saveDir;
if (!this.saveDir.exists() && !this.saveDir.mkdirs()) if (!this.saveDir.exists() && !this.saveDir.mkdirs())
{ {
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,25 +61,13 @@ 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);
dataSource.populateFromStream(dto, dto.getInputStream(), this.level); try (DhDataInputStream inputStream = dto.getInputStream())
{
dataSource.populateFromStream(dto, inputStream, this.level);
}
return dataSource; return dataSource;
} }
@@ -0,0 +1,349 @@
package com.seibel.distanthorizons.core.file.fullDatafile.V2;
import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV1;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.fullDatafile.V1.FullDataSourceProviderV1;
import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
public class DataMigratorV1 implements IDebugRenderable, AutoCloseable
{
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
/** how many data sources should be pulled down for migration at once */
private static final int MIGRATION_BATCH_COUNT = FullDataUpdatePropagatorV2.NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD;
/**
* 5 minutes <br>
* This should be much longer than any update should take. This is just
* to make sure the thread doesn't get stuck.
*/
private static final int MIGRATION_MAX_UPDATE_TIMEOUT_IN_MS = 5 * 60 * 1_000;
private final FullDataUpdaterV2 dataUpdater;
private boolean migrationStartMessageQueued = false;
private long legacyDeletionCount = -1;
private long migrationCount = -1;
private boolean migrationStoppedWithError = false;
/**
* Interrupting the migration thread pool doesn't work well and may corrupt the database
* vs gracefully shutting down the thread ourselves.
*/
public final AtomicBoolean migrationThreadRunning = new AtomicBoolean(true);
private final FullDataSourceProviderV1<IDhLevel> v1DataSourceProvider;
private final String levelId;
private final File saveDir;
//=============//
// constructor //
//=============//
public DataMigratorV1(
FullDataUpdaterV2 dataUpdater,
IDhLevel level, String levelId, File saveDir
) throws SQLException, IOException
{
this.dataUpdater = dataUpdater;
this.saveDir = saveDir;
this.v1DataSourceProvider = new FullDataSourceProviderV1<>(level, saveDir);
this.levelId = levelId;
// start migrating any legacy data sources present in the background
ThreadPoolExecutor executor = ThreadPoolUtil.getFullDataMigrationExecutor();
if (executor != null)
{
executor.execute(this::convertLegacyDataSources);
}
else
{
// shouldn't happen, but just in case
LOGGER.error("Unable to start migration for level: ["+this.levelId+"] due to missing executor.");
}
}
//=======================//
// data source migration //
//=======================//
private void convertLegacyDataSources()
{
try
{
LOGGER.debug("Attempting to migrate data sources for: [" + this.levelId + "]-[" + this.saveDir + "]...");
this.migrationThreadRunning.set(true);
//============================//
// delete unused data sources //
//============================//
// this could be done all at once via SQL,
// but doing it in chunks prevents locking the database for long periods of time
long unusedCount = 0;
long totalDeleteCount = this.v1DataSourceProvider.repo.getUnusedDataSourceCount();
if (totalDeleteCount != 0)
{
// this should only be shown once per session but should be shown during
// either when the deletion or migration phases start
this.showMigrationStartMessage();
LOGGER.info("deleting [" + this.levelId + "] - [" + totalDeleteCount + "] unused data sources...");
this.legacyDeletionCount = totalDeleteCount;
ArrayList<String> unusedDataPosList = this.v1DataSourceProvider.repo.getUnusedDataSourcePositionStringList(50);
while (unusedDataPosList.size() != 0)
{
unusedCount += unusedDataPosList.size();
this.legacyDeletionCount -= unusedDataPosList.size();
long startTime = System.currentTimeMillis();
// delete batch and get next batch
this.v1DataSourceProvider.repo.deleteUnusedLegacyData(unusedDataPosList);
unusedDataPosList = this.v1DataSourceProvider.repo.getUnusedDataSourcePositionStringList(50);
long endStart = System.currentTimeMillis();
long deleteTime = endStart - startTime;
LOGGER.info("Deleting [" + this.levelId + "] - [" + unusedCount + "/" + totalDeleteCount + "] in [" + deleteTime + "]ms ...");
// a slight delay is added to prevent accidentally locking the database when deleting a lot of rows
// (that shouldn't be the case since we're using WAL journaling, but just in case)
try
{
// use the delete time so we don't make powerful computers wait super long
// and weak computers wait no time at all
Thread.sleep(deleteTime / 2);
}
catch (InterruptedException ignore)
{
}
}
LOGGER.info("Done deleting [" + this.levelId + "] - [" + totalDeleteCount + "] unused data sources.");
}
//===========//
// migration //
//===========//
long totalMigrationCount = this.v1DataSourceProvider.getDataSourceMigrationCount();
this.migrationCount = totalMigrationCount;
LOGGER.debug("Found [" + totalMigrationCount + "] data sources that need migration.");
ArrayList<FullDataSourceV1> legacyDataSourceList = this.v1DataSourceProvider.getDataSourcesToMigrate(MIGRATION_BATCH_COUNT);
if (!legacyDataSourceList.isEmpty())
{
this.showMigrationStartMessage();
try
{
// keep going until every data source has been migrated
int progressCount = 0;
while (!legacyDataSourceList.isEmpty() && this.migrationThreadRunning.get())
{
NumberFormat numFormat = F3Screen.NUMBER_FORMAT;
LOGGER.info("Migrating [" + this.levelId + "] - [" + numFormat.format(progressCount) + "/" + numFormat.format(totalMigrationCount) + "]...");
ArrayList<CompletableFuture<Void>> updateFutureList = new ArrayList<>();
for (int i = 0; i < legacyDataSourceList.size() && this.migrationThreadRunning.get(); i++)
{
FullDataSourceV1 legacyDataSource = legacyDataSourceList.get(i);
try
{
// convert the legacy data source to the new format,
// this is a relatively cheap operation
FullDataSourceV2 newDataSource = FullDataSourceV2.createFromLegacyDataSourceV1(legacyDataSource);
newDataSource.applyToParent = true;
// the actual update process can be moderately expensive due to having to update
// the render data along with the full data, so running it async on the update threads gains us a good bit of speed
CompletableFuture<Void> future = this.dataUpdater.updateDataSourceAsync(newDataSource);
updateFutureList.add(future);
future.thenRun(() ->
{
// after the update finishes the legacy data source can be safely deleted
this.v1DataSourceProvider.repo.deleteWithKey(legacyDataSource.getPos());
newDataSource.close();
});
}
catch (Exception e)
{
long migrationPos = legacyDataSource.getPos();
LOGGER.warn("Unexpected issue migrating data source at pos [" + DhSectionPos.toString(migrationPos) + "]. Error: " + e.getMessage(), e);
this.v1DataSourceProvider.markMigrationFailed(migrationPos);
}
}
try
{
// wait for each thread to finish updating
CompletableFuture<Void> combinedFutures = CompletableFuture.allOf(updateFutureList.toArray(new CompletableFuture[0]));
combinedFutures.get(MIGRATION_MAX_UPDATE_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS);
}
catch (InterruptedException | TimeoutException e)
{
LOGGER.warn("Migration update timed out after [" + MIGRATION_MAX_UPDATE_TIMEOUT_IN_MS + "] milliseconds. Migration will re-try the same positions again in a moment.", e);
}
catch (ExecutionException e)
{
LOGGER.warn("Migration update failed. Migration will re-try the same positions again. Error:" + e.getMessage(), e);
}
legacyDataSourceList = this.v1DataSourceProvider.getDataSourcesToMigrate(MIGRATION_BATCH_COUNT);
progressCount += legacyDataSourceList.size();
this.migrationCount -= legacyDataSourceList.size();
}
}
catch (Exception e)
{
LOGGER.info("migration stopped due to error for: [" + this.levelId + "]-[" + this.saveDir + "], error: [" + e.getMessage() + "].", e);
this.showMigrationEndMessage(false);
this.migrationStoppedWithError = true;
}
finally
{
if (this.migrationThreadRunning.get())
{
LOGGER.info("migration complete for: [" + this.levelId + "]-[" + this.saveDir + "].");
this.showMigrationEndMessage(true);
this.migrationCount = 0;
}
else
{
LOGGER.info("migration stopped for: [" + this.levelId + "]-[" + this.saveDir + "].");
this.showMigrationEndMessage(false);
this.migrationStoppedWithError = true;
}
}
}
else
{
LOGGER.info("No migration necessary.");
}
}
finally
{
this.migrationThreadRunning.set(false);
}
}
private void showMigrationStartMessage()
{
if (this.migrationStartMessageQueued)
{
return;
}
this.migrationStartMessageQueued = true;
ClientApi.INSTANCE.showChatMessageNextFrame(
"Old Distant Horizons data is being migrated for ["+this.levelId+"]. \n" +
"While migrating LODs may load slowly \n" +
"and DH world gen will be disabled. \n" +
"You can see migration progress in the F3 menu."
);
}
private void showMigrationEndMessage(boolean success)
{
if (success)
{
ClientApi.INSTANCE.showChatMessageNextFrame("Distant Horizons data migration for ["+this.levelId+"] completed.");
}
else
{
ClientApi.INSTANCE.showChatMessageNextFrame(
"Distant Horizons data migration for ["+this.levelId+"] stopped. \n" +
"Some data may not have been migrated."
);
}
}
//===========//
// debugging //
//===========//
public void addDebugMenuStringsToList(List<String> messageList)
{
// migration
boolean migrationErrored = this.migrationStoppedWithError;
if (!migrationErrored)
{
long legacyDeletionCount = this.legacyDeletionCount;
if (legacyDeletionCount > 0)
{
messageList.add(" Migrating - Deleting #: " + F3Screen.NUMBER_FORMAT.format(legacyDeletionCount));
}
long migrationCount = this.migrationCount;
if (migrationCount > 0)
{
messageList.add(" Migrating - Conversion #: " + F3Screen.NUMBER_FORMAT.format(migrationCount));
}
}
else
{
messageList.add(" Migration Failed");
}
}
//===========//
// overrides //
//===========//
@Override
public void debugRender(DebugRenderer renderer)
{
// nothing currently needed
}
@Override
public void close()
{
//LOGGER.info("Closing [" + this.getClass().getSimpleName() + "] for level: [" + this.levelId + "].");
}
}

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