Compare commits

...

259 Commits

Author SHA1 Message Date
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 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
James Seibel f9bd7e2daf remove dev from version number 2025-03-25 07:16:43 -05:00
James Seibel 8ec4e235eb Add config to only log GL errors once 2025-03-19 22:02:53 -05:00
James Seibel b8a59d0ef6 Attempt to fix Linux complaining about glIsFramebuffer() 2025-03-19 18:34:02 -05:00
James Seibel e500143781 Potentially fix GL errors when accessing the default FBO on Linux 2025-03-19 17:00:30 -05:00
James Seibel 406468b54c Fix restoring textures to the default FBO 2025-03-18 20:18:13 -05:00
James Seibel 6857300ae2 Add stack tracing to GL error logging 2025-03-18 18:10:00 -05:00
James Seibel 6775ee23c3 fix passing in the wrong flags to glBufferStorage()
Might Resolve #964 and #950
2025-03-18 07:43:20 -05:00
James Seibel 44645943e2 Fix terrain API repo failing if no cache was provided 2025-03-18 07:42:31 -05:00
James Seibel f385c4a56b Fix changing graphics settings on world load via API 2025-03-16 14:29:56 -05:00
James Seibel 0cf5e6d594 Fix GL error logging 2025-03-14 10:17:52 -05:00
James Seibel 7b5b8da0d2 decrease gen message timeout 5 -> 2 seconds
done to make it consistently appear
2025-03-13 21:17:01 -05:00
James Seibel 851f2ccd06 Add additional error checking/handling to Shader compiling 2025-03-13 21:12:29 -05:00
James Seibel 6c40389c07 up version number 2.3.0-b -> 2.3.1-b-dev 2025-03-08 08:11:01 -06:00
James Seibel fada9e4cf6 Fix repo leak unit test failing in release 2025-03-08 08:10:43 -06:00
James Seibel 06198fdbb8 Revert "temporarily disable sqlite tests for release"
This reverts commit ebc1114a51.
2025-03-06 07:43:14 -06:00
James Seibel 3158eed5a3 Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2025-03-06 07:42:32 -06:00
James Seibel e701c0e5ea remove dev from version number 2025-03-06 07:40:49 -06:00
James Seibel ebc1114a51 temporarily disable sqlite tests for release 2025-03-06 07:40:24 -06:00
s809 d2ff4a5806 Add some debugging info for DTOs 2025-03-02 20:08:45 +05:00
s809 eb8563482e Replace chunk counts with speed in pregen 2025-02-27 21:08:27 +05:00
s809 b53c33e454 Make generation info text a bit clearer 2025-02-27 21:08:02 +05:00
s809 2483671e5e Should be division instead of multiplication 2025-02-26 23:16:58 +05:00
s809 cc4733b052 Offset generation bounds by teleportation scale 2025-02-26 22:13:38 +05:00
James Seibel 34e5463718 Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2025-02-25 07:25:49 -06:00
James Seibel 53011a13be duct tape fix to reduce chance of LOD uploading requiring MC reboot 2025-02-25 07:25:46 -06:00
s809 1c579675a2 Remember split section responses temporarily 2025-02-22 20:55:10 +05:00
James Seibel 69a4e6b27e Add TODO about why LODs sometimes fail to load 2025-02-22 08:55:41 -06:00
s809 b05eb78f3a Fix foreground thread sometimes blocking server shutdown 2025-02-19 21:17:38 +05:00
s809 83fabe3ee8 Show section numbers in pregen 2025-02-19 20:37:26 +05:00
s809 fdfab2b3a8 Use another method for enforcing non nsized generation 2025-02-19 20:15:34 +05:00
James Seibel 45c67d057a Fix IDhApiConfigValue.clearValue() failing for some deprecated functions 2025-02-17 21:16:22 -06:00
James Seibel c296795280 Fix DB leaks in FullDataV2Repo 2025-02-16 20:07:00 -06:00
James Seibel 2deb24ec1e Add javadocs to ClientWrapper getPlayer Pos methods 2025-02-16 19:53:44 -06:00
James Seibel 5ab7a3030a Fix DB leaks in FullDataV2Repo 2025-02-16 19:52:48 -06:00
James Seibel 1af4d23c14 improve DB leak tracking exception handling 2025-02-16 19:52:37 -06:00
James Seibel 977204abf0 Add DB leak tracking 2025-02-16 19:34:13 -06:00
James Seibel 276f2adf00 Revert 10 minute memoization for world gen
I thought this was only an issue for N-sized generation, but in testing found it to still be an issue for max-detail retrieval as well.
This will have to be looked into more another time
2025-02-15 11:56:21 -06:00
James Seibel 1b3c9e1a89 Fix beacon culling with auto overdraw prevention 2025-02-15 11:12:46 -06:00
James Seibel 6fbe0a9e72 Add missing cave blocks for cave culling 2025-02-15 11:06:43 -06:00
James Seibel 11a2b8bf5b Add TODO to PriorityTaskPicker about VisualVM 2025-02-15 11:06:30 -06:00
James Seibel 99f2d2f844 Add TODO comment about Immersive Portals only rendering 1 level 2025-02-14 07:48:05 -06:00
s809 a5c029203c Invert generateOnlyInHighestDetail and rename to enableNSizedGeneration 2025-02-11 22:08:25 +05:00
James Seibel 84015e4a40 Put N-sized generation and upsampling behind experimental configs 2025-02-11 07:47:24 -06:00
James Seibel 08f63470a5 Fix auto updater failing for nightly builds 2025-02-10 07:46:43 -06:00
James Seibel f2404b6455 remove unneeded IVersionConstant methods 2025-02-08 11:39:18 -06:00
James Seibel f20231ccbc fix rare null pointer in sharedApi 2025-02-08 11:38:36 -06:00
s809 3a94bbe804 Reduce queue size back to improve responsiveness 2025-02-07 23:23:15 +05:00
s809 15f1754922 Improve ordering of reading positions to update a bit 2025-02-07 23:21:41 +05:00
s809 28448941e1 Keep update propagation queue filled 2025-02-07 23:20:24 +05:00
James Seibel 18c29b9810 Attempt to fix threadpool shutdown rejection exception 2025-02-07 07:25:57 -06:00
James Seibel fa66cefbe2 Add comments to LodRenderSection memoized gen positions
also increase timeout from 15 sec -> 10 minutes
 - done to test if memoization is actually needed
2025-02-07 07:14:50 -06:00
James Seibel f7dc46cb55 Increase full data update task count to reduce down time 2025-02-06 20:10:30 -06:00
James Seibel 5cebee3be4 Flush world gen memory cache when full 2025-02-06 20:08:28 -06:00
s809 532ac8fe01 Fix incorrect distance being used in update propagation SQL and reduce queue size 2025-02-07 01:05:14 +05:00
James Seibel 8385eeb62c Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2025-02-05 17:31:26 -06:00
James Seibel 95db6885e7 add error logging to FullDataRequestHandler 2025-02-05 17:31:13 -06:00
James Seibel 10a3840373 Fix empty data sources when moving in multiplayer or with N-sized world gen
Increases Protocol version 9 -> 10
2025-02-05 17:30:59 -06:00
James Seibel cedaaa8a2e replace a few implicit datasource V2 repo statement setters 2025-02-04 19:29:39 -06:00
James Seibel 2c7f11c722 Improve DhApiBeforeRenderEvent javadocs 2025-02-03 20:30:28 -06:00
James Seibel 4fbda8f02b Fix render enabled config getting set by world gen progress config 2025-02-02 19:52:31 -06:00
James Seibel b0bd536248 Fix compiling with missing "E" 2025-02-02 15:52:17 -06:00
s809 a3ed0012e3 Balance tasks in thread pool using elapsed time instead of priorities 2025-02-02 20:30:35 +05:00
s809 9952481d77 Do not request already fulfilled sections again until some time passes 2025-02-02 20:30:35 +05:00
s809 5e137ee10d Auto-move old save data to new location 2025-02-02 20:30:35 +05:00
James Seibel f02ea68b6f Add missing Enum prefix to RequestResult -> ERequestResult 2025-02-01 16:08:15 -06:00
s809 1041e0a4dd Remove generationProgressDisableMessageDisplayTimeInSeconds from server config command 2025-02-01 19:38:43 +05:00
s809 6fb862ecfe Add GUI description for generateOnlyInHighestDetail 2025-02-01 19:38:25 +05:00
s809 1f8013c1cf Use generateOnlyInHighestDetail client-side 2025-02-01 18:30:07 +05:00
s809 157d72d8dc Decrease delay between missing generation rechecks 2025-01-31 14:54:18 +05:00
James Seibel 2c077f5224 Fix a null pointer in the chunk update queue 2025-01-30 20:13:53 -06:00
s809 6e5bd02ae0 Fix beacon beams flickering 2025-01-30 22:30:02 +05:00
s809 a7578b2a72 Process chunks only once with real-time updates enabled 2025-01-30 21:47:31 +05:00
s809 041cf4e0d4 Fix nightly self-updater after moving jars into zip root 2025-01-30 18:11:34 +05:00
s809 bb1154b036 Revert "Improve chunk processing throughput"
This reverts commit dd3903f66e.
2025-01-28 20:05:59 +05:00
James Seibel 9c9c90e786 Improve world gen import hiding message 2025-01-26 17:47:28 -06:00
James Seibel 3dbd05a4ae minor beacon beam height merge cleanup 2025-01-26 17:41:46 -06:00
James Seibel 042a0b6853 Merge branch 'distant-horizons-core-main' 2025-01-26 17:40:15 -06:00
James Seibel 39c621b8d9 Remove locks from LodRenderSection uploading 2025-01-26 17:12:39 -06:00
James Seibel dd3903f66e Improve chunk processing throughput 2025-01-26 17:05:37 -06:00
James Seibel 2d1859c77d change low memory warning to 4GB or more 2025-01-26 16:53:11 -06:00
James Seibel d62a801c43 Reduce locking in SharedApi.UpdateChunkPosManager 2025-01-25 10:10:10 -06:00
James Seibel cb40336fda Increase rolling average window for world gen queue
This should reduce fluctuations a bit
2025-01-24 21:53:13 -06:00
James Seibel 766c831af0 fix recalculate heightmap breaking stairs, slabs, and glass 2025-01-24 07:22:11 -06:00
s809 736df9f848 Check if session is ready before ignoring local chunks 2025-01-24 11:20:37 +05:00
James Seibel a347caafed Fix holes when moving with N-sized world gen/server side support 2025-01-23 19:44:58 -06:00
James Seibel 2d5902df28 Fix data source leaks for custom world generators 2025-01-23 19:38:01 -06:00
James Seibel 29e496757a Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2025-01-23 19:16:33 -06:00
James Seibel 7cf05ed31d Fix rare concurrency error on world gen shutdown 2025-01-23 19:15:55 -06:00
James Seibel e7eb8e24ae Speed up PhantomArrayListPool for large checkouts 2025-01-23 19:15:42 -06:00
s809 cdca7723a7 Ignore local chunks if realtime updates are enabled 2025-01-23 23:21:13 +05:00
s809 e0a0ba5222 Fix full data source being released too early 2025-01-23 22:06:20 +05:00
James Seibel 0f88c7c231 Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2025-01-22 21:31:20 -06:00
James Seibel d9911f64b9 Simplify Full data hash logic to speed up saving 2025-01-22 21:31:07 -06:00
s809 8bddd6d503 Fix column order check breaking on tiny columns 2 2025-01-23 00:18:33 +05:00
s809 9b261f6472 Fix column order check breaking on tiny columns 2025-01-23 00:15:12 +05:00
James Seibel 00559b5d34 Remove unneeded locks and speed up FullDataId Entry retrieval 2025-01-22 07:16:04 -06:00
James Seibel 9cae54a079 Show instructions to disable world gen progress message for short time 2025-01-21 07:49:30 -06:00
James Seibel 363ec76450 fix isClosedException name 2025-01-21 07:07:33 -06:00
s809 ebd00df388 Fix task splitting causing generation of already generated sections 2025-01-21 17:26:54 +05:00
James Seibel 13882f44ce minor LodRenderSection rename 2025-01-20 21:51:04 -06:00
James Seibel fce1fa3f41 Fix cached RenderSource closing while in use 2025-01-20 21:50:33 -06:00
James Seibel fab8191ddd remove unneeded wal flush logic 2025-01-20 07:39:04 -06:00
James Seibel 582541d240 handle additional DB closed message on DTO get 2025-01-20 07:38:40 -06:00
s809 f609dcb468 Merge remote-tracking branch 'origin/main' 2025-01-20 11:00:13 +05:00
s809 a69936ca69 Merge branch 'feature/generation-bounds' 2025-01-20 10:59:11 +05:00
James Seibel 8c81c867b6 merge 2025-01-19 17:42:45 -06:00
James Seibel 995f80d553 Fix beacons disappearing and not updating correctly 2025-01-19 17:42:14 -06:00
s809 08f36b4371 Lower the log level of rate limit hits 2025-01-18 17:02:35 +05:00
Jan Trummer 9dcc7e1ad2 Replace set with setMinDefaultMax 2025-01-11 16:05:58 +01:00
Jan Trummer 72139f1f59 Add config to set max beacon render height 2025-01-11 13:38:44 +01:00
210 changed files with 8306 additions and 4617 deletions
+1 -2
View File
@@ -4,7 +4,6 @@
[*]
charset = utf-8
end_of_line = crlf
indent_size = 4
indent_style = space
insert_final_newline = false
@@ -537,7 +536,7 @@ ij_groovy_wrap_chain_calls_after_dot = false
ij_groovy_wrap_long_lines = false
[{*.har,*.json,*.png.mcmeta,mcmod.info,pack.mcmeta}]
indent_size = 2
indent_size = 4
ij_json_array_wrapping = split_into_lines
ij_json_keep_blank_lines_in_code = 0
ij_json_keep_indents_on_empty_lines = false
@@ -39,8 +39,8 @@ public enum EDhApiDataCompressionMode
/**
* Should only be used internally and for unit testing. <br><br>
*
* Read Speed: 1.64 MS / DTO <br>
* Write Speed: 12.44 MS / DTO <br>
* Read Speed: 6.09 MS / DTO <br>
* Write Speed: 6.01 MS / DTO <br>
* Compression ratio: 1.0 <br>
*/
@DisallowSelectingViaConfigGui
@@ -49,28 +49,28 @@ public enum EDhApiDataCompressionMode
/**
* Extremely fast (often faster than uncompressed), but generally poor compression. <br><br>
*
* Read Speed: 1.85 MS / DTO <br>
* Write Speed: 9.46 MS / DTO <br>
* Compression ratio: 0.3638 <br>
* Read Speed: 3.25 MS / DTO <br>
* Write Speed: 5.99 MS / DTO <br>
* Compression ratio: 0.4513 <br>
*/
LZ4(1),
/*
/**
* Decent speed and good compression. <br><br>
*
* Read Speed: 11.78 MS / DTO <br>
* Write Speed: 16.76 MS / DTO <br>
* Compression ratio: 0.2199 <br>
* Read Speed: 9.31 MS / DTO <br>
* Write Speed: 15.13 MS / DTO <br>
* Compression ratio: 0.2606 <br>
*/
//@Deprecated
//Z_STD(2),
Z_STD(2),
/**
* Extremely slow, but very good compression. <br><br>
*
* Read Speed: 12.25 MS / DTO <br>
* Write Speed: 490.07 MS / DTO <br>
* Compression ratio: 0.1242 <br>
* Read Speed: 13.29 MS / DTO <br>
* Write Speed: 70.95 MS / DTO <br>
* Compression ratio: 0.2068 <br>
*/
LZMA2(3);
@@ -40,6 +40,7 @@ public enum EDhApiLoggerMode
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),
LOG_ERROR_TO_CHAT_AND_WARNING_TO_FILE(Level.ERROR, Level.WARN),
;
public final Level levelForFile;
@@ -20,6 +20,8 @@
package com.seibel.distanthorizons.api.enums.worldGeneration;
/**
* DOWN_SAMPLED, <br>
*
* EMPTY, <br>
* STRUCTURE_START, <br>
* STRUCTURE_REFERENCE, <br>
@@ -37,6 +39,14 @@ package com.seibel.distanthorizons.api.enums.worldGeneration;
*/
public enum EDhApiWorldGenerationStep
{
/**
* Only used when using N-sized world generators or server-side retrieval.
* This denotes that the given datasource was created using lower quality LOD data from above it in the quad tree. <br>
*
* This isn't a valid option for queuing world generation.
*/
DOWN_SAMPLED(-1, "down_sampled"),
EMPTY(0, "empty"),
STRUCTURE_START(1, "structure_start"),
STRUCTURE_REFERENCE(2, "structure_reference"),
@@ -9,12 +9,15 @@ package com.seibel.distanthorizons.api.interfaces.data;
* @version 2024-7-14
* @since API 3.0.0
*/
public interface IDhApiTerrainDataCache
public interface IDhApiTerrainDataCache // TODO should this be AutoClosable?
{
/**
* Removes any data that's currently stored in this cache.
* This cane be done to free up memory or invalidate
* the cache so fresh data can be pulled in.
* <br><br>
* This should be called before de-referencing this object
* so DH can handle any necessary cleanup for internal objects.
*/
void clear();
@@ -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); }
}
@@ -25,11 +25,16 @@ import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhAp
/**
* Called before Distant Horizons starts rendering a frame. <br>
* Canceling the event will prevent DH from rendering that frame.
* Canceling the event will prevent DH from rendering that frame. <br> <br>
*
* This is called before DH starts modifying the GL state.
* If you want to inject into DH's rendering pass, use {@link DhApiBeforeRenderPassEvent} instead.
*
* @author James Seibel
* @version 2023-6-23
* @since API 1.0.0
*
* @see DhApiBeforeRenderPassEvent
*/
public abstract class DhApiBeforeRenderEvent implements IDhApiCancelableEvent<DhApiRenderParam>
{
@@ -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.IDhApiEventParam;
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>
*
* @author James Seibel
* @version 2024-3-2
* @since API 2.0.0
* @deprecated Replaced by {@link DhApiBeforeColorDepthTextureCreatedEvent} since this event's name isn't obvious when it fires.
*/
@Deprecated
public abstract class DhApiColorDepthTextureCreatedEvent implements IDhApiEvent<DhApiColorDepthTextureCreatedEvent.EventParam>
{
/** Fired before Distant Horizons creates. */
@@ -73,6 +76,15 @@ public abstract class DhApiColorDepthTextureCreatedEvent implements IDhApiEvent<
this.newHeight = newHeight;
}
public EventParam(DhApiTextureCreatedParam textureCreatedParam)
{
this.previousWidth = textureCreatedParam.previousWidth;
this.previousHeight = textureCreatedParam.previousHeight;
this.newWidth = textureCreatedParam.newWidth;
this.newHeight = textureCreatedParam.newHeight;
}
@Override
@@ -9,5 +9,23 @@ import com.seibel.distanthorizons.api.interfaces.util.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; }
}
@@ -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
);
}
}
@@ -143,19 +143,23 @@ public class ApiEventInjector extends DependencyInjector<IDhApiEvent> implements
// attempt to clone the event input if possible
// this is done to reduce the likely hood that one event listener
// will make change the event parameter for other listeners
// this is done to reduce the likelihood that one event listener
// will change the event parameter for other listeners
T input = eventInput;
if (eventInput instanceof IDhApiEventParam)
{
try
IDhApiEventParam dhApiEventParam = (IDhApiEventParam) eventInput;
if (dhApiEventParam.getCopyBeforeFire())
{
//noinspection unchecked
input = (T) ((IDhApiEventParam) eventInput).copy();
}
catch (Exception e)
{
LOGGER.error("Unable to clone event parameter ["+eventInput.getClass().getSimpleName()+"], error: ["+e.getMessage()+"].", e);
try
{
//noinspection unchecked
input = (T) dhApiEventParam.copy();
}
catch (Exception 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;
public DependencyInjector(Class<BindableType> newBindableInterface)
{
this.bindableInterface = newBindableInterface;
this.allowDuplicateBindings = false;
}
//==============//
// constructors //
//==============//
public DependencyInjector(Class<BindableType> newBindableInterface, boolean newAllowDuplicateBindings)
{
@@ -57,12 +55,16 @@ public class DependencyInjector<BindableType extends IBindable> implements IDepe
//=========//
// binding //
//=========//
@Override
public void bind(Class<? extends BindableType> dependencyInterface, BindableType dependencyImplementation) throws IllegalStateException, IllegalArgumentException
{
// 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.");
}
@@ -130,6 +132,54 @@ public class DependencyInjector<BindableType extends IBindable> implements IDepe
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")
@Override
public <T extends BindableType> T get(Class<T> interfaceClass) throws ClassCastException
@@ -31,21 +31,21 @@ public final class ModInfo
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. */
public static final int PROTOCOL_VERSION = 9;
public static final int PROTOCOL_VERSION = 11;
public static final String WRAPPER_PACKET_PATH = "message";
/** The internal mod name */
public static final String NAME = "DistantHorizons";
/** Human-readable version of NAME */
public static final String READABLE_NAME = "Distant Horizons";
public static final String VERSION = "2.3.0-b-dev";
public static final String VERSION = "2.3.6-b";
/** Returns true if the current build is an unstable developer build, false otherwise. */
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 */
public static final int API_MAJOR_VERSION = 4;
/** This version should be updated whenever new methods are added to the DH API */
public static final int API_MINOR_VERSION = 0;
public static final int API_MINOR_VERSION = 1;
/** This version should be updated whenever non-breaking fixes are added to the DH API */
public static final int API_PATCH_VERSION = 0;
@@ -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);
}
+7 -1
View File
@@ -60,4 +60,10 @@ shadowJar {
def librariesLocation = "DistantHorizons.libraries"
// relocate "it.unimi.dsi.fastutil", "${librariesLocation}.unimi.dsi.fastutil"
mergeServiceFiles()
}
}
test {
// this is necessary specifically for the Compression tests since those
// need more than the default 512 MB of RAM
jvmArgs '-Xmx4096m'
}
@@ -19,6 +19,7 @@
package com.seibel.distanthorizons.core;
import com.github.luben.zstd.ZstdOutputStream;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericRenderObjectFactory;
import com.seibel.distanthorizons.core.sql.DatabaseUpdater;
@@ -54,8 +55,9 @@ public class Initializer
{
// if any library isn't present in the jar its class
// will throw an error (not an exception)
Class<?> fastCompressor = LZ4FrameOutputStream.class;
Class<?> smallCompressor = XZOutputStream.class;
Class<?> lz4Compressor = LZ4FrameOutputStream.class;
Class<?> zstdCompressor = ZstdOutputStream.class;
Class<?> lzmaCompressor = XZOutputStream.class;
//Class<?> networking = ByteBuf.class;
Class<?> config = com.electronwill.nightconfig.core.Config.class;
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<?> 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();
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.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;
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.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.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.interfaces.config.IDhApiConfigValue;
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;
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.IDhApiFogConfig;
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.coreapi.util.converters.ApiFogDrawModeConverter;
import com.seibel.distanthorizons.core.config.api.converters.ApiFogDrawModeConverter;
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.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;
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.interfaces.config.IDhApiConfigValue;
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.core.config.Config;
import com.seibel.distanthorizons.core.config.api.converters.RenderModeEnabledConverter;
public class DhApiGraphicsConfig implements IDhApiGraphicsConfig
{
@@ -60,7 +61,7 @@ public class DhApiGraphicsConfig implements IDhApiGraphicsConfig
@Override
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
public IDhApiConfigValue<EDhApiRendererMode> renderingMode()
@@ -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.interfaces.config.IDhApiConfigValue;
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;
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.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;
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.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.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.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;
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.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.core.config.Config;
@@ -197,10 +197,10 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
ILevelWrapper coreLevelWrapper = (ILevelWrapper) levelWrapper;
if (!(apiDataCache instanceof DhApiTerrainDataCache))
// the data cache can be null, but must be our own implementation
if (apiDataCache != null
&& !(apiDataCache instanceof DhApiTerrainDataCache))
{
// custom level wrappers aren't supported,
// the API user must get a level wrapper from our code somewhere
return DhApiResult.createFail("Unsupported [" + IDhApiTerrainDataCache.class.getSimpleName() + "] implementation, only the core class [" + DhApiTerrainDataCache.class.getSimpleName() + "] is a valid parameter.");
}
DhApiTerrainDataCache dataCache = (DhApiTerrainDataCache) apiDataCache;
@@ -226,10 +226,9 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
// get the data source //
//=====================//
FullDataSourceV2 dataSource = null;
try
{
FullDataSourceV2 dataSource = null;
// try using the cached data if possible
if (dataCache != null)
{
@@ -244,7 +243,12 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
{
return DhApiResult.createFail("Unable to find/generate any data at the " + DhSectionPos.class.getSimpleName() + " [" + DhSectionPos.toString(sectionPos) + "].");
}
dataCache.add(sectionPos, dataSource);
// save to the cache if present
if (dataCache != null)
{
dataCache.add(sectionPos, dataSource);
}
}
@@ -316,6 +320,14 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
LOGGER.error("Unexpected exception in getTerrainDataColumnArray. Error: [" + e.getMessage() + "]", e);
return DhApiResult.createFail("Unexpected exception: [" + e.getMessage() + "].");
}
finally
{
if (dataCache == null
&& dataSource != null)
{
dataSource.close();
}
}
}
@@ -491,7 +503,7 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
// this will throw a cast exception if the chunk object array isn't correct
IChunkWrapper chunk = SingletonInjector.INSTANCE.get(IWrapperFactory.class).createChunkWrapper(chunkObjectArray);
SharedApi.INSTANCE.applyChunkUpdate(chunk, dhLevel.getLevelWrapper(), true);
SharedApi.INSTANCE.applyChunkUpdate(chunk, dhLevel.getLevelWrapper(), true, true);
return DhApiResult.createSuccess();
@@ -24,6 +24,7 @@ import com.seibel.distanthorizons.api.enums.config.EDhApiMcRenderingFadeMode;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.*;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.core.api.internal.rendering.RenderState;
import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure;
import com.seibel.distanthorizons.core.network.messages.MessageRegistry;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
@@ -89,6 +90,15 @@ public class ClientApi
/** this includes the is dev build message and low allocated memory warning */
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 RenderState RENDER_STATE = new RenderState();
private boolean isDevBuildMessagePrinted = false;
@@ -389,21 +399,28 @@ public class ClientApi
// rendering //
//===========//
/** Should be called before {@link ClientApi#renderDeferredLods} */
public void renderLods(IClientLevelWrapper levelWrapper, Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks)
{ this.renderLodLayer(levelWrapper, mcModelViewMatrix, mcProjectionMatrix, partialTicks, false); }
/** Should be called before {@link ClientApi#renderDeferredLodsForShaders} */
public void renderLods() { this.renderLodLayer(false); }
/**
* Only necessary when Shaders are in use.
* Should be called after {@link ClientApi#renderLods}
*/
public void renderDeferredLods(IClientLevelWrapper levelWrapper, Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks)
{ this.renderLodLayer(levelWrapper, mcModelViewMatrix, mcProjectionMatrix, partialTicks, true); }
public void renderDeferredLodsForShaders() { this.renderLodLayer(true); }
private void renderLodLayer(
IClientLevelWrapper levelWrapper, Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks,
boolean renderingDeferredLayer)
private void renderLodLayer(boolean renderingDeferredLayer)
{
// A global render state variable is used since MC has split up their
// 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
IClientLevelWrapper levelWrapper = RENDER_STATE.clientLevelWrapper;
Mat4f mcModelViewMatrix = RENDER_STATE.mcModelViewMatrix;
Mat4f mcProjectionMatrix = RENDER_STATE.mcProjectionMatrix;
float partialTicks = RENDER_STATE.frameTime;
// logging //
this.sendQueuedChatMessages();
@@ -445,6 +462,25 @@ public class ClientApi
//Mat4f mcCombined = mcModelViewMatrix.copy();
//mcCombined.multiply(mcProjectionMatrix);
//
//com.seibel.distanthorizons.api.objects.math.DhApiMat4f dhCombined = renderEventParam.dhModelViewMatrix.copy();
//dhCombined.multiply(renderEventParam.dhProjectionMatrix);
//
//LOGGER.info("\n\n" +
// "API\n" +
// "Mc MVM: \n" + mcModelViewMatrix.toString() + "\n" +
// "Mc Proj: \n" + mcProjectionMatrix + "\n" +
// "Mc Combined:\n" + mcCombined.toString() + "\n" +
// "\n" +
// "DH MVM: \n" + renderEventParam.dhModelViewMatrix.toString() + "\n" +
// "DH Proj: \n" + renderEventParam.dhProjectionMatrix + "\n" +
// "DH Combined:\n" + mcCombined.toString()
//);
// render validation //
try
@@ -551,28 +587,44 @@ public class ClientApi
}
}
/** 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)
/**
* 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)
if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT
// only fade when requested
&& Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.get() == EDhApiMcRenderingFadeMode.DOUBLE_PASS
// don't fade when Iris shaders are active, otherwise the rendering can get weird
&& !DhApiRenderProxy.INSTANCE.getDeferTransparentRendering())
{
if (Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.get() == EDhApiMcRenderingFadeMode.DOUBLE_PASS)
{
FadeRenderer.INSTANCE.render(mcModelViewMatrix, mcProjectionMatrix, partialTicks, level);
}
FadeRenderer.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)
/**
* The second fade pass.
* Called after MC finishes rendering both opaque
* and transparent passes.
*/
public void renderFadeTransparent()
{
// 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)
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);
FadeRenderer.INSTANCE.render(RENDER_STATE.mcModelViewMatrix, RENDER_STATE.mcProjectionMatrix, RENDER_STATE.frameTime, RENDER_STATE.clientLevelWrapper);
}
}
}
@@ -652,7 +704,8 @@ public class ClientApi
private void detectAndSendBootTimeWarnings()
{
// 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.lastStaticWarningMessageSentMsTime = System.currentTimeMillis();
@@ -669,7 +722,8 @@ public class ClientApi
// memory
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.lastStaticWarningMessageSentMsTime = System.currentTimeMillis();
@@ -685,7 +739,7 @@ public class ClientApi
// orange text
"\u00A76" + "Distant Horizons: Low memory detected." + "\u00A7r \n" +
"Stuttering or low FPS may occur. \n" +
"Please increase Minecraft's available memory to 4 gigabytes. \n" +
"Please increase Minecraft's available memory to 4 GB or more. \n" +
"This warning can be disabled in DH's config under Advanced -> Logging. \n";
MC_CLIENT.sendChatMessage(message);
}
@@ -694,7 +748,8 @@ public class ClientApi
// high vanilla render distance
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
if (MC_RENDER.getRenderDistance() > 12)
@@ -721,7 +776,8 @@ public class ClientApi
{
if (this.lastStaticWarningMessageSentMsTime == 0)
{
return true;
// no static message has ever been sent
return false;
}
long timeSinceLastMessage = System.currentTimeMillis() - this.lastStaticWarningMessageSentMsTime;
@@ -740,4 +796,6 @@ public class ClientApi
*/
public void showOverlayMessageNextFrame(String message) { this.overlayMessageQueueForNextFrame.add(message); }
}
@@ -11,7 +11,7 @@ import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import org.apache.logging.log4j.LogManager;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
@@ -65,7 +65,7 @@ public class ClientPluginChannelApi
//================//
/** 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);
this.networkSession = networkSession;
@@ -136,8 +136,8 @@ public class ServerApi
// chunk modified events //
//=======================//
public void serverChunkLoadEvent(IChunkWrapper chunkWrapper, ILevelWrapper level) { SharedApi.INSTANCE.applyChunkUpdate(chunkWrapper, level, false); }
public void serverChunkSaveEvent(IChunkWrapper chunkWrapper, ILevelWrapper level) { SharedApi.INSTANCE.applyChunkUpdate(chunkWrapper, level, true); }
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, false); }
@@ -22,9 +22,12 @@ package com.seibel.distanthorizons.core.api.internal;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiWorldLoadEvent;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiWorldUnloadEvent;
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.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.level.DhClientLevel;
import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen;
@@ -49,7 +52,6 @@ import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
/** Contains code and variables used by both {@link ClientApi} and {@link ServerApi} */
public class SharedApi
@@ -65,20 +67,20 @@ public class SharedApi
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 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),
* 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 */
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 int lastWorldGenTickDelta = 0;
private static long lastOverloadedLogMessageMsTime = 0;
@@ -99,6 +101,11 @@ public class SharedApi
public static void setDhWorld(AbstractDhWorld newWorld)
{
AbstractDhWorld oldWorld = currentWorld;
if (oldWorld != null)
{
oldWorld.close();
}
currentWorld = newWorld;
// 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
AbstractDhRepo.closeAllConnections();
// 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
System.gc();
@@ -144,6 +151,7 @@ public class SharedApi
}
}
@Nullable
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; }
@@ -163,10 +171,10 @@ public class SharedApi
* 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)
{ 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)
{ 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.
@@ -174,17 +182,18 @@ public class SharedApi
* rapidly changing dimensions.
* (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 */
public void chunkBlockChangedEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true); }
public void chunkLoadEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, false); }
public void chunkBlockChangedEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true, false); }
public void chunkLoadEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true, true); }
public void 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 //
@@ -231,8 +240,16 @@ public class SharedApi
return;
}
if (dhLevel instanceof DhClientLevel)
{
if (!((DhClientLevel) dhLevel).shouldProcessChunkUpdate(chunkWrapper.getChunkPos()))
{
return;
}
}
// 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
return;
@@ -244,23 +261,53 @@ public class SharedApi
// update the necessary chunk(s) //
//===============================//
if (!updateNeighborChunks)
if (!canGetNeighboringChunks)
{
// only update the center chunk
queueChunkUpdate(chunkWrapper, null, dhLevel);
queueChunkUpdate(chunkWrapper, null, dhLevel, false);
return;
}
ArrayList<IChunkWrapper> neighboringChunkList = getNeighborChunkListForChunk(chunkWrapper, dhLevel);
if (newlyLoaded)
{
// this means this chunkWrapper is a newly loaded chunk
// which may be missing some neighboring chunk data
// because it is bordering the render distance
// thus, only the chunks neighboring this chunkWrapper will get updated
// because those are more likely to have their full neighboring chunk data
//TODO this does not prevent those neighboring chunks from updating
// this newly loaded chunk that were just skipped
// leading to occasional lighting issues
for (IChunkWrapper neighboringChunk : neighboringChunkList)
{
if (neighboringChunk == chunkWrapper)
{
continue;
}
this.applyChunkUpdate(neighboringChunk, level, true, false);
}
}
else
{
// if not all neighboring chunk data is available, do not try to update
if (neighboringChunkList.size() < 9)
{
return;
}
// update the center with any existing neighbour chunks.
// this is done so lighting changes are propagated correctly
queueChunkUpdate(chunkWrapper, 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
ArrayList<IChunkWrapper> neighbourChunkList = new ArrayList<>(9);
ArrayList<IChunkWrapper> neighborChunkList = new ArrayList<>(9);
for (int xOffset = -1; xOffset <= 1; xOffset++)
{
for (int zOffset = -1; zOffset <= 1; zOffset++)
@@ -268,80 +315,36 @@ public class SharedApi
if (xOffset == 0 && zOffset == 0)
{
// center chunk
neighbourChunkList.add(chunkWrapper);
neighborChunkList.add(chunkWrapper);
}
else
{
// neighboring chunk
DhChunkPos neighbourPos = new DhChunkPos(chunkWrapper.getChunkPos().getX() + xOffset, chunkWrapper.getChunkPos().getZ() + zOffset);
IChunkWrapper neighbourChunk = dhLevel.getLevelWrapper().tryGetChunk(neighbourPos);
if (neighbourChunk != null)
// neighboring chunk
DhChunkPos neighborPos = new DhChunkPos(chunkWrapper.getChunkPos().getX() + xOffset, chunkWrapper.getChunkPos().getZ() + zOffset);
IChunkWrapper neighborChunk = dhLevel.getLevelWrapper().tryGetChunk(neighborPos);
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)
{ queueChunkUpdate(chunkWrapper, neighbourChunkList, dhLevel,false); }
private static void queueChunkUpdate(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighbourChunkList, IDhLevel dhLevel, boolean lightUpdateOnly)
private static void queueChunkUpdate(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighborChunkList, IDhLevel dhLevel, boolean canGetNeighboringChunks)
{
int maxUpdateSizeMultiplier;
if (MC_CLIENT != null && MC_CLIENT.playerExists())
{
// Local worlds & multiplayer
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);
}
}
// return if the chunk is already queued
if (CHUNK_UPDATE_QUEUE_MANAGER.contains(chunkWrapper.getChunkPos()))
{
return;
}
// 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
@@ -352,7 +355,7 @@ public class SharedApi
{
try
{
executor.execute(SharedApi::processQueuedChunkUpdate);
executor.execute(SharedApi::processQueue);
}
catch (RejectedExecutionException ignore)
{
@@ -360,94 +363,158 @@ 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()
{
//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)
{
return;
}
IChunkWrapper chunkWrapper = updateData.chunkWrapper;
@Nullable ArrayList<IChunkWrapper> neighbourChunkList = updateData.neighbourChunkList;
IDhLevel dhLevel = updateData.dhLevel;
// having a list of the nearby chunks is needed for lighting and beacon generation
@Nullable ArrayList<IChunkWrapper> nearbyChunkList = updateData.neighborChunkList;
// a non-null list is needed for the lighting engine
if (nearbyChunkList == null)
{
nearbyChunkList = new ArrayList<IChunkWrapper>();
nearbyChunkList.add(chunkWrapper);
}
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
ArrayList<IChunkWrapper> nearbyChunkList;
if (neighbourChunkList != null)
{
nearbyChunkList = neighbourChunkList;
}
else
{
nearbyChunkList = new ArrayList<>(1);
nearbyChunkList.add(chunkWrapper);
}
// if this chunk will update its lighting
// 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
DhLightingEngine.INSTANCE.bakeChunkBlockLighting(chunkWrapper, nearbyChunkList, dhLevel.hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT);
dhLevel.updateBeaconBeamsForChunk(chunkWrapper, nearbyChunkList);
int newChunkHash = chunkWrapper.getBlockBiomeHashCode();
dhLevel.updateChunkAsync(chunkWrapper, newChunkHash);
}
catch (Exception 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
}
}
}
}
@@ -458,212 +525,13 @@ public class SharedApi
public String getDebugMenuString()
{
String updatingCountStr = F3Screen.NUMBER_FORMAT.format(UPDATE_POS_MANAGER.closestQueue.size());
String maxUpdateCountStr = F3Screen.NUMBER_FORMAT.format(UPDATE_POS_MANAGER.maxSize);
return "Queued chunk updates: "+updatingCountStr+" / "+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;
String preUpdatingCountStr = F3Screen.NUMBER_FORMAT.format(CHUNK_UPDATE_QUEUE_MANAGER.preUpdateQueue.getQueuedCount());
String updatingCountStr = F3Screen.NUMBER_FORMAT.format(CHUNK_UPDATE_QUEUE_MANAGER.updateQueue.getQueuedCount());
String queuedCountStr = F3Screen.NUMBER_FORMAT.format(CHUNK_UPDATE_QUEUE_MANAGER.getQueuedCount());
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 PriorityQueue<DhChunkPos> closestQueue;
private final PriorityQueue<DhChunkPos> furthestQueue;
private final HashMap<DhChunkPos, UpdateChunkData> updateDataByChunkPos;
private final ReentrantLock lock = new ReentrantLock();
private DhChunkPos center;
private int maxSize = 500;
//=============//
// constructor //
//=============//
public UpdateChunkPosManager()
{
this.closestQueue = new PriorityQueue<>(Comparator.comparingDouble(pos -> pos.squaredDistance(this.center)));
this.furthestQueue = new PriorityQueue<>(Comparator.comparingDouble(pos -> ((DhChunkPos)pos).squaredDistance(this.center)).reversed());
this.updateDataByChunkPos = new HashMap<>();
// 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)
{
try
{
this.lock.lock();
return this.updateDataByChunkPos.containsKey(pos);
}
finally
{
this.lock.unlock();
}
}
public void clear()
{
try
{
this.lock.lock();
this.updateDataByChunkPos.clear();
this.closestQueue.clear();
this.furthestQueue.clear();
}
finally
{
this.lock.unlock();
}
}
public void removeItem(DhChunkPos pos)
{
try
{
this.lock.lock();
this.updateDataByChunkPos.remove(pos);
this.closestQueue.remove(pos);
this.furthestQueue.remove(pos);
}
finally
{
this.lock.unlock();
}
}
/**
* 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)
{
try
{
this.lock.lock();
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();
this.closestQueue.remove(furthest);
this.updateDataByChunkPos.remove(furthest);
}
this.updateDataByChunkPos.put(pos, updateData);
this.closestQueue.add(pos);
this.furthestQueue.add(pos);
return remainingSlots;
}
finally
{
this.lock.unlock();
}
}
//==================//
// 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;
}
try
{
this.lock.lock();
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);
}
}
finally
{
this.lock.unlock();
}
}
public UpdateChunkData popClosest()
{
try
{
this.lock.lock();
if (this.closestQueue.isEmpty())
{
return null;
}
DhChunkPos closest = this.closestQueue.poll();
this.furthestQueue.remove(closest);
return this.updateDataByChunkPos.remove(closest);
}
finally
{
this.lock.unlock();
}
}
String maxUpdateCountStr = F3Screen.NUMBER_FORMAT.format(CHUNK_UPDATE_QUEUE_MANAGER.maxSize);
return "Queued chunk updates: "+"( "+preUpdatingCountStr+" + "+updatingCountStr+" ) [ "+queuedCountStr+" / "+maxUpdateCountStr+" ]";
}
@@ -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,145 @@
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 org.apache.logging.log4j.Logger;
public class ChunkUpdateQueueManager
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public final ChunkPosQueue updateQueue;
public final ChunkPosQueue preUpdateQueue;
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.preUpdateQueue.contains(pos); }
public void clear()
{
this.updateQueue.clear();
this.preUpdateQueue.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);
}
}
}
//==================//
// 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 RenderState
{
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);
}
}
}
@@ -45,14 +45,11 @@ import java.util.*;
import java.util.List;
/**
* This handles any configuration the user has access to. <br><br>
*
* 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.
* This handles any configuration the user has access to.
*
* @author coolGi
*
* @see ConfigHandler
*/
@SuppressWarnings("ConcatenationWithEmptyString")
public class Config
@@ -107,7 +104,7 @@ public class Config
public static ConfigUiLinkedEntry quickEnableWorldGenerator = new ConfigUiLinkedEntry(Common.WorldGenerator.enableDistantGeneration);
public static ConfigEntry<Boolean> quickShowWorldGenProgress = new ConfigEntry.Builder<Boolean>()
.set(true)
.set(false) // TODO should be set by the underlying world gen progress button, not a static default
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.build();
@@ -131,6 +128,8 @@ public class Config
// since they aren't part of "client" config class
// TODO determine their destination programically instead of hard coding the value
public static ConfigUIComment advancedHeader = new ConfigUIComment.Builder().setParentConfigClass(Advanced.class).build();
public static 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 multiplayer = new ConfigCategory.Builder().set(Multiplayer.class).build();
@@ -146,23 +145,32 @@ public class Config
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 ConfigUISpacer qualitySpacer = new ConfigUISpacer.Builder().build();
public static ConfigUiLinkedEntry quickEnableSsao = new ConfigUiLinkedEntry(Ssao.enableSsao);
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 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 quickEnableMcFog = new ConfigUiLinkedEntry(Fog.enableVanillaFog);
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 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 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();
@@ -170,12 +178,13 @@ public class Config
public static class Quality
{
public static ConfigUIComment qualityHeader = new ConfigUIComment.Builder().setParentConfigClass(Quality.class).build();
public static ConfigEntry<Integer> lodChunkRenderDistanceRadius = new ConfigEntry.Builder<Integer>()
.setMinDefaultMax(32, 256, 4096)
.comment("" +
"The radius of the mod's render distance. (measured in chunks)\n" +
"")
.setPerformance(EConfigEntryPerformance.HIGH)
.build();
public static ConfigEntry<EDhApiHorizontalQuality> horizontalQuality = new ConfigEntry.Builder<EDhApiHorizontalQuality>()
@@ -184,7 +193,6 @@ public class Config
+ "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"
+ "but will increase memory and GPU usage.")
.setPerformance(EConfigEntryPerformance.MEDIUM)
.build();
public static ConfigEntry<EDhApiMaxHorizontalResolution> maxHorizontalResolution = new ConfigEntry.Builder<EDhApiMaxHorizontalResolution>()
@@ -201,7 +209,7 @@ public class Config
+ "\n"
+ "Lowest Quality: " + EDhApiMaxHorizontalResolution.CHUNK + "\n"
+ "Highest Quality: " + EDhApiMaxHorizontalResolution.BLOCK)
.setPerformance(EConfigEntryPerformance.MEDIUM)
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build();
public static ConfigEntry<EDhApiVerticalQuality> verticalQuality = new ConfigEntry.Builder<EDhApiVerticalQuality>()
@@ -214,8 +222,7 @@ public class Config
+ "\n"
+ "Lowest Quality: " + EDhApiVerticalQuality.HEIGHT_MAP + "\n"
+ "Highest Quality: " + EDhApiVerticalQuality.EXTREME)
.setPerformance(EConfigEntryPerformance.VERY_HIGH)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build();
public static ConfigEntry<EDhApiTransparency> transparency = new ConfigEntry.Builder<EDhApiTransparency>()
@@ -227,8 +234,7 @@ public class Config
+ EDhApiTransparency.FAKE + ": LODs will be opaque, but shaded to match the blocks underneath. \n"
+ EDhApiTransparency.DISABLED + ": LODs will be opaque. \n"
+ "")
.setPerformance(EConfigEntryPerformance.MEDIUM)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build();
public static ConfigEntry<EDhApiBlocksToAvoid> blocksToIgnore = new ConfigEntry.Builder<EDhApiBlocksToAvoid>()
@@ -239,8 +245,7 @@ public class Config
+ EDhApiBlocksToAvoid.NONE + ": Represent all blocks in the LODs \n"
+ EDhApiBlocksToAvoid.NON_COLLIDING + ": Only represent solid blocks in the LODs (tall grass, torches, etc. won't count for a LOD's height) \n"
+ "")
.setPerformance(EConfigEntryPerformance.NONE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build();
public static ConfigEntry<Boolean> tintWithAvoidedBlocks = new ConfigEntry.Builder<Boolean>()
@@ -251,8 +256,7 @@ public class Config
+ "True: a red flower will tint the grass below it red. \n"
+ "False: skipped blocks will not change color of surface below them. "
+ "")
.setPerformance(EConfigEntryPerformance.NONE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build();
public static ConfigEntry<Double> lodBias = new ConfigEntry.Builder<Double>()
@@ -272,8 +276,7 @@ public class Config
+ " 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"
+ "")
.setPerformance(EConfigEntryPerformance.NONE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build();
public static ConfigEntry<EDhApiGrassSideRendering> grassSideRendering = new ConfigEntry.Builder<EDhApiGrassSideRendering>()
@@ -285,8 +288,7 @@ public class Config
+ EDhApiGrassSideRendering.FADE_TO_DIRT + ": sides fade from grass to dirt. \n"
+ EDhApiGrassSideRendering.AS_DIRT + ": sides render entirely as dirt. \n"
+ "")
.setPerformance(EConfigEntryPerformance.NONE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build();
public static ConfigEntry<Boolean> ditherDhFade = new ConfigEntry.Builder<Boolean>()
@@ -296,7 +298,6 @@ public class Config
+ "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"
+ "")
.setPerformance(EConfigEntryPerformance.LOW)
.build();
public static ConfigEntry<EDhApiMcRenderingFadeMode> vanillaFadeMode = new ConfigEntry.Builder<EDhApiMcRenderingFadeMode>()
@@ -308,7 +309,6 @@ public class Config
+ 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"
+ "")
.setPerformance(EConfigEntryPerformance.LOW)
.build();
public static ConfigEntry<Double> brightnessMultiplier = new ConfigEntry.Builder<Double>() // TODO: Make this a float (the ClassicConfigGUI doesnt support floats)
@@ -319,7 +319,7 @@ public class Config
+ "0 = black \n"
+ "1 = normal \n"
+ "2 = near white")
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build();
public static ConfigEntry<Double> saturationMultiplier = new ConfigEntry.Builder<Double>() // TODO: Make this a float (the ClassicConfigGUI doesnt support floats)
@@ -330,30 +330,29 @@ public class Config
+ "0 = black and white \n"
+ "1 = normal \n"
+ "2 = very saturated")
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build();
// TODO fixme
//public static ConfigEntry<Integer> lodBiomeBlending = new ConfigEntry.Builder<Integer>()
// .setMinDefaultMax(0,1,7)
// .comment(""
// + "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"
// + " and increase triangle count. The cost on chunk generation speed is also \n"
// + " quite large if set too high.\n"
// + "\n"
// + " '0' equals to Vanilla Biome Blending of '1x1' or 'OFF', \n"
// + " '1' equals to Vanilla Biome Blending of '3x3', \n"
// + " '2' equals to Vanilla Biome Blending of '5x5'...")
// .build();
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
.comment(""
+ "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"
+ "\n"
+ " '0' equals to Vanilla Biome Blending of '1x1' or 'OFF', \n"
+ " '1' equals to Vanilla Biome Blending of '3x3', \n"
+ " '2' equals to Vanilla Biome Blending of '5x5'...")
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build();
}
public static class Ssao
{
public static ConfigUIComment ssaoHeader = new ConfigUIComment.Builder().setParentConfigClass(Ssao.class).build();
public static ConfigEntry<Boolean> enableSsao = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment("Enable Screen Space Ambient Occlusion")
.setPerformance(EConfigEntryPerformance.MEDIUM)
.build();
public static ConfigEntry<Integer> sampleCount = new ConfigEntry.Builder<Integer>()
@@ -362,7 +361,6 @@ public class Config
"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." +
"")
.setPerformance(EConfigEntryPerformance.MEDIUM)
.build();
public static ConfigEntry<Double> radius = new ConfigEntry.Builder<Double>()
@@ -370,7 +368,6 @@ public class Config
.comment("" +
"Determines the radius Screen Space Ambient Occlusion is applied, measured in blocks." +
"")
.setPerformance(EConfigEntryPerformance.NONE)
.build();
public static ConfigEntry<Double> strength = new ConfigEntry.Builder<Double>()
@@ -378,7 +375,6 @@ public class Config
.comment("" +
"Determines how dark the Screen Space Ambient Occlusion effect will be." +
"")
.setPerformance(EConfigEntryPerformance.NONE)
.build();
public static ConfigEntry<Double> bias = new ConfigEntry.Builder<Double>()
@@ -386,7 +382,6 @@ public class Config
.comment("" +
"Increasing the value can reduce banding at the cost of reducing the strength of the effect." +
"")
.setPerformance(EConfigEntryPerformance.NONE)
.build();
public static ConfigEntry<Double> minLight = new ConfigEntry.Builder<Double>()
@@ -396,7 +391,6 @@ public class Config
"0 = totally black at the corners \n" +
"1 = no shadow" +
"")
.setPerformance(EConfigEntryPerformance.NONE)
.build();
public static ConfigEntry<Integer> blurRadius = new ConfigEntry.Builder<Integer>()
@@ -405,13 +399,14 @@ public class Config
"The radius, measured in pixels, that blurring is calculated for the SSAO. \n" +
"Higher numbers will reduce banding at the cost of GPU performance." +
"")
.setPerformance(EConfigEntryPerformance.HIGH)
.build();
}
public static class GenericRendering
{
public static ConfigUIComment genericRendererHeader = new ConfigUIComment.Builder().setParentConfigClass(GenericRendering.class).build();
public static ConfigEntry<Boolean> enableGenericRendering = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
@@ -427,6 +422,15 @@ public class Config
+ "")
.build();
public static ConfigEntry<Integer> beaconRenderHeight = new ConfigEntry.Builder<Integer>()
.setMinDefaultMax(1, 6000, 6_000_000)
.comment(""
+ "Sets the maximum height at which beacons will render."
+ "This will only affect new beacons coming into LOD render distance."
+ "Beacons currently visible in LOD chunks will not be affected."
+ "")
.build();
public static ConfigEntry<Boolean> enableCloudRendering = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
@@ -450,12 +454,13 @@ public class Config
public static ConfigUIComment fogHeader = new ConfigUIComment.Builder().setParentConfigClass(Fog.class).build();
public static ConfigEntry<Boolean> enableDhFog = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "Determines if fog is drawn on DH LODs. \n"
+ "")
.setPerformance(EConfigEntryPerformance.MEDIUM)
.build();
public static ConfigEntry<EDhApiFogColorMode> colorMode = new ConfigEntry.Builder<EDhApiFogColorMode>()
@@ -465,7 +470,6 @@ public class Config
+ "\n"
+ EDhApiFogColorMode.USE_WORLD_FOG_COLOR + ": Use the world's fog color. \n"
+ EDhApiFogColorMode.USE_SKY_COLOR + ": Use the sky's color.")
.setPerformance(EConfigEntryPerformance.NONE)
.build();
public static ConfigEntry<Boolean> enableVanillaFog = new ConfigEntry.Builder<Boolean>()
@@ -551,6 +555,8 @@ public class Config
public static class HeightFog
{
public static ConfigUIComment heightFogHeader = new ConfigUIComment.Builder().setParentConfigClass(HeightFog.class).build();
public static ConfigEntry<EDhApiHeightFogMixMode> heightFogMixMode = new ConfigEntry.Builder<EDhApiHeightFogMixMode>()
.set(EDhApiHeightFogMixMode.SPHERICAL)
.comment(""
@@ -646,6 +652,8 @@ public class Config
public static class NoiseTexture
{
public static ConfigUIComment noiseTextureHeader = new ConfigUIComment.Builder().setParentConfigClass(NoiseTexture.class).build();
public static ConfigEntry<Boolean> enableNoiseTexture = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
@@ -680,8 +688,10 @@ public class Config
public static class Culling
{
public static ConfigUIComment cullingHeader = new ConfigUIComment.Builder().setParentConfigClass(Culling.class).build();
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(""
+ "Determines how far from the camera Distant Horizons will start rendering. \n"
+ "Measured as a percentage of the vanilla render distance.\n"
@@ -694,22 +704,19 @@ public class Config
+ "\n"
+ "Increasing the vanilla render distance increases the effectiveness of this setting."
+ "")
.setPerformance(EConfigEntryPerformance.NONE)
.build();
public static ConfigEntry<Boolean> enableCaveCulling = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "If enabled caves will be culled \n"
+ "If enabled caves won't be rendered. \n"
+ "\n"
+ "NOTE: This feature is under development and \n"
+ " it is VERY experimental! Please don't report \n"
+ " any issues related to this feature. \n"
+ "\n"
+ "Additional Info: Currently this cull all faces \n"
+ " with skylight value of 0 in dimensions that \n"
+ " does not have a ceiling.")
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
+ " Note: for some world types this can cause \n"
+ " overhangs or walls for floating objects. \n"
+ " Tweaking the caveCullingHeight, can resolve some \n"
+ " of those issues. \n"
+ "")
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build();
public static ConfigEntry<Integer> caveCullingHeight = new ConfigEntry.Builder<Integer>()
@@ -717,7 +724,7 @@ public class Config
.comment(""
+ "At what Y value should cave culling start? \n"
+ "Lower this value if you get walls for areas with 0 light.")
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build();
public static ConfigEntry<Boolean> disableBeaconDistanceCulling = new ConfigEntry.Builder<Boolean>()
@@ -753,20 +760,26 @@ public class Config
+ "Disable this if shadows render incorrectly.")
.build();
public static ConfigEntry<String> ignoredRenderBlockCsv = new ConfigEntry.Builder<String>()
public static ConfigEntry<String> ignoredRenderBlockCsv = new ConfigEntry.Builder<String>() // TODO accept wildcards
.set("minecraft:barrier,minecraft:structure_void,minecraft:light,minecraft:tripwire,minecraft:brown_mushroom")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // only shown in file since the UI has a character limit
.comment(""
+ "A comma separated list of block resource locations that won't be rendered by DH. \n"
+ "Note: air is always included in this list. \n"
+ "Air is always included in this list. \n"
+ "Requires a restart to change. \n"
+ "")
.build();
public static ConfigEntry<String> ignoredRenderCaveBlockCsv = new ConfigEntry.Builder<String>()
.set("minecraft:glow_lichen,minecraft:rail,minecraft:water,minecraft:lava,minecraft:bubble_column")
public static ConfigEntry<String> ignoredRenderCaveBlockCsv = new ConfigEntry.Builder<String>() // TODO accept wildcards
.set("minecraft:glow_lichen,minecraft:rail,minecraft:water,minecraft:lava,minecraft:bubble_column," +
"minecraft:cave_vines_plant,minecraft:vine,minecraft:cave_vines,minecraft:short_grass,minecraft:tall_grass," +
"minecraft:small_dripleaf,minecraft:big_dripleaf,minecraft:big_dripleaf_stem,minecraft:sculk_vein")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // only shown in file since the UI has a character limit
.comment(""
+ "A comma separated list of block resource locations that shouldn't be rendered \n"
+ "if they are in a 0 sky light underground area. \n"
+ "Note: air is always included in this list. \n"
+ "Air is always included in this list. \n"
+ "Requires a restart to change. \n"
+ "")
.build();
@@ -799,6 +812,8 @@ public class Config
public static class Experimental
{
public static ConfigUIComment experimentalHeader = new ConfigUIComment.Builder().setParentConfigClass(Experimental.class).build();
public static ConfigEntry<Integer> earthCurveRatio = new ConfigEntry.Builder<Integer>()
.setMinDefaultMax(0, 0, 5000)
.comment(""
@@ -821,6 +836,8 @@ public class Config
public static class AutoUpdater
{
public static ConfigUIComment autoUpdaterHeader = new ConfigUIComment.Builder().setParentConfigClass(AutoUpdater.class).build();
public static ConfigEntry<Boolean> enableAutoUpdater = new ConfigEntry.Builder<Boolean>()
.set(!isRunningInDevEnvironment())
.comment(""
@@ -848,6 +865,8 @@ public class Config
public static class Multiplayer
{
public static ConfigUIComment multiplayerHeader = new ConfigUIComment.Builder().setParentConfigClass(Multiplayer.class).build();
public static ConfigEntry<EDhApiServerFolderNameMode> serverFolderNameMode = new ConfigEntry.Builder<EDhApiServerFolderNameMode>()
.set(EDhApiServerFolderNameMode.NAME_ONLY)
.comment(""
@@ -864,6 +883,8 @@ public class Config
public static class Debugging
{
public static ConfigUIComment debuggingHeader = new ConfigUIComment.Builder().setParentConfigClass(Debugging.class).build();
public static ConfigEntry<EDhApiRendererMode> rendererMode = new ConfigEntry.Builder<EDhApiRendererMode>()
.set(EDhApiRendererMode.DEFAULT)
.comment(""
@@ -884,6 +905,7 @@ public class Config
+ 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"
+ "")
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build();
public static ConfigEntry<Boolean> lodOnlyMode = new ConfigEntry.Builder<Boolean>()
@@ -951,6 +973,8 @@ public class Config
public static class DebugWireframe
{
public static ConfigUIComment debugWireframeHeader = new ConfigUIComment.Builder().setParentConfigClass(DebugWireframe.class).build();
public static ConfigEntry<Boolean> enableRendering = new ConfigEntry.Builder<Boolean>()
.set(false)
.comment(""
@@ -1000,10 +1024,26 @@ public class Config
public static class OpenGl
{
public static ConfigUIComment openGlHeader = new ConfigUIComment.Builder().setParentConfigClass(OpenGl.class).build();
public static ConfigEntry<Boolean> overrideVanillaGLLogger = new ConfigEntry.Builder<Boolean>()
.set(ModInfo.IS_DEV_BUILD)
.set(true)
.comment(""
+ "Requires a reboot to change. \n"
+ "Defines how OpenGL errors are handled. \n "
+ "Requires rebooting Minecraft to change. \n"
+ "Will catch OpenGL errors thrown by other mods. \n"
+ "")
.build();
public static ConfigEntry<Boolean> onlyLogGlErrorsOnce = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "If true each Open GL error will only be logged once. \n"
+ "Enabling this may cause some error logs to be missed. \n"
+ "Does nothing if overrideVanillaGLLogger is set to false. \n"
+ " \n"
+ "Generally this can be kept as 'true' to prevent log spam. \n"
+ "However, Please set this to 'false' if a developer needs your log to debug a GL issue. \n"
+ "")
.build();
@@ -1033,50 +1073,54 @@ public class Config
public static class ColumnBuilderDebugging
{
public static ConfigUIComment columnBuilderDebuggingHeader = new ConfigUIComment.Builder().setParentConfigClass(ColumnBuilderDebugging.class).build();
public static ConfigEntry<Boolean> columnBuilderDebugEnable = new ConfigEntry.Builder<Boolean>()
.set(false)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.addListener(ReloadLodsConfigEventHandler.INSTANT_INSTANCE)
.build();
public static ConfigEntry<Integer> columnBuilderDebugDetailLevel = new ConfigEntry.Builder<Integer>()
.set((int) DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.addListener(ReloadLodsConfigEventHandler.INSTANT_INSTANCE)
.build();
public static ConfigEntry<Integer> columnBuilderDebugXPos = new ConfigEntry.Builder<Integer>()
.set(0)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.addListener(ReloadLodsConfigEventHandler.INSTANT_INSTANCE)
.build();
public static ConfigEntry<Integer> columnBuilderDebugZPos = new ConfigEntry.Builder<Integer>()
.set(0)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.addListener(ReloadLodsConfigEventHandler.INSTANT_INSTANCE)
.build();
public static ConfigEntry<Integer> columnBuilderDebugXRow = new ConfigEntry.Builder<Integer>()
.set(-1)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.addListener(ReloadLodsConfigEventHandler.INSTANT_INSTANCE)
.build();
public static ConfigEntry<Integer> columnBuilderDebugZRow = new ConfigEntry.Builder<Integer>()
.set(-1)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.addListener(ReloadLodsConfigEventHandler.INSTANT_INSTANCE)
.build();
public static ConfigEntry<Integer> columnBuilderDebugColumnIndex = new ConfigEntry.Builder<Integer>()
.set(-1)
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.addListener(ReloadLodsConfigEventHandler.INSTANT_INSTANCE)
.build();
}
public static class F3Screen
{
public static ConfigUIComment f3ScreenHeader = new ConfigUIComment.Builder().setParentConfigClass(F3Screen.class).build();
public static ConfigEntry<Boolean> showPlayerPos = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment("Shows info about each thread pool.")
.comment("Shows the player's LOD position.")
.build();
public static ConfigEntry<Integer> playerPosSectionDetailLevel = new ConfigEntry.Builder<Integer>()
.setMinDefaultMax(6, 6, 16)
@@ -1117,8 +1161,10 @@ public class Config
// This will throw a warning when opened in the default ui to tell you about it not showing
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
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>()
.set(false)
@@ -1126,6 +1172,7 @@ public class Config
public static ConfigEntry<Byte> byteTest = new ConfigEntry.Builder<Byte>()
.set((byte) 8)
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // no GUI renderer set up currently
.build();
public static ConfigEntry<Integer> intTest = new ConfigEntry.Builder<Integer>()
@@ -1138,14 +1185,17 @@ public class Config
public static ConfigEntry<Short> shortTest = new ConfigEntry.Builder<Short>()
.set((short) 69)
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // no GUI renderer set up currently
.build();
public static ConfigEntry<Long> longTest = new ConfigEntry.Builder<Long>()
.set(42069L)
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // no GUI renderer set up currently
.build();
public static ConfigEntry<Float> floatTest = new ConfigEntry.Builder<Float>()
.set(0.42069f)
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // no GUI renderer set up currently
.build();
public static ConfigEntry<String> stringTest = new ConfigEntry.Builder<String>()
@@ -1154,10 +1204,12 @@ public class Config
public static ConfigEntry<List<String>> listTest = new ConfigEntry.Builder<List<String>>()
.set(new ArrayList<String>(Arrays.asList("option 1", "option 2", "option 3")))
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // no GUI renderer set up currently
.build();
public static ConfigEntry<Map<String, String>> mapTest = new ConfigEntry.Builder<Map<String, String>>()
.set(new HashMap<String, String>())
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // no GUI renderer set up currently
.build();
public static ConfigUIButton uiButtonTest = new ConfigUIButton(() ->
@@ -1201,6 +1253,8 @@ public class Config
{
public static class WorldGenerator
{
public static ConfigUIComment worldGeneratorHeader = new ConfigUIComment.Builder().setParentConfigClass(WorldGenerator.class).build();
public static ConfigEntry<Boolean> enableDistantGeneration = new ConfigEntry.Builder<Boolean>()
.setChatCommandName("generation.enable")
.set(true)
@@ -1254,7 +1308,7 @@ public class Config
.build();
public static ConfigEntry<EDhApiDistantGeneratorProgressDisplayLocation> showGenerationProgress = new ConfigEntry.Builder<EDhApiDistantGeneratorProgressDisplayLocation>()
.set(EDhApiDistantGeneratorProgressDisplayLocation.OVERLAY)
.set(EDhApiDistantGeneratorProgressDisplayLocation.DISABLED)
.comment(""
+ "How should distant generator progress be displayed? \n"
+ "\n"
@@ -1267,16 +1321,35 @@ public class Config
public static ConfigEntry<Integer> generationProgressDisplayIntervalInSeconds = new ConfigEntry.Builder<Integer>()
.setChatCommandName("generation.logInterval")
.setMinDefaultMax(1, 5, 60 * 60 * 4) // max = 4 hours
.setMinDefaultMax(1, 2, 60 * 60 * 4) // max = 4 hours
.comment(""
+ "How often should the distant generator progress be displayed? \n"
+ "")
.build();
public static ConfigEntry<Integer> generationProgressDisableMessageDisplayTimeInSeconds = new ConfigEntry.Builder<Integer>()
.setMinDefaultMax(0, 20, 60 * 60) // max = 1 hour
.comment(""
+ "For how many seconds should instructions for disabling the distant generator progress be displayed? \n"
+ "Setting this to 0 hides the instructional message so the world gen progress is shown immediately when it starts. \n"
+ "")
.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 ConfigUIComment lodBuildingHeader = new ConfigUIComment.Builder().setParentConfigClass(LodBuilding.class).build();
public static ConfigEntry<Boolean> disableUnchangedChunkCheck = new ConfigEntry.Builder<Boolean>()
.set(false)
// enabling this can be quite detrimental to performance,
@@ -1296,7 +1369,7 @@ public class Config
.build();
public static ConfigEntry<EDhApiDataCompressionMode> dataCompression = new ConfigEntry.Builder<EDhApiDataCompressionMode>()
.set(EDhApiDataCompressionMode.LZMA2)
.set(EDhApiDataCompressionMode.Z_STD)
.comment(""
+ "What algorithm should be used to compress new LOD data? \n"
+ "This setting will only affect new or updated LOD data, \n"
@@ -1306,20 +1379,26 @@ public class Config
+ 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"
+ "Estimated average DTO read speed: 6.09 milliseconds\n"
+ "Estimated average DTO write speed: 6.01 milliseconds\n"
+ "\n"
+ EDhApiDataCompressionMode.LZ4 + " \n"
+ "A good option if you're CPU limited and have plenty of hard drive space.\n"
+ "Expected Compression Ratio: 0.36\n"
+ "Estimated average DTO read speed: 1.85 ms\n"
+ "Estimated average DTO write speed: 9.46 ms\n"
+ "Expected Compression Ratio: 0.4513\n"
+ "Estimated average DTO read speed: 3.25 ms\n"
+ "Estimated average DTO write speed: 5.99 ms\n"
+ "\n"
+ EDhApiDataCompressionMode.Z_STD + " \n"
+ "A good option if you're CPU limited and have plenty of hard drive space.\n"
+ "Expected Compression Ratio: 0.2606\n"
+ "Estimated average DTO read speed: 9.31 ms\n"
+ "Estimated average DTO write speed: 15.13 ms\n"
+ "\n"
+ EDhApiDataCompressionMode.LZMA2 + " \n"
+ "Slow but very good compression.\n"
+ "Expected Compression Ratio: 0.14\n"
+ "Estimated average DTO read speed: 11.89 ms\n"
+ "Estimated average DTO write speed: 192.01 ms\n"
+ "Expected Compression Ratio: 0.2\n"
+ "Estimated average DTO read speed: 13.29 ms\n"
+ "Estimated average DTO write speed: 70.95 ms\n"
+ "")
.build();
@@ -1386,10 +1465,38 @@ public class Config
+ "")
.build();
public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build();
public static class Experimental
{
public static ConfigUIComment experimentalHeader = new ConfigUIComment.Builder().setParentConfigClass(Experimental.class).build();
public static ConfigEntry<Boolean> upsampleLowerDetailLodsToFillHoles = new ConfigEntry.Builder<Boolean>()
.set(false)
.comment(""
+ "When active DH will attempt to fill missing LOD data \n"
+ "with any data that is present in the tree, preventing holes when moving \n"
+ "when a N-sized generator (or server) is active. \n"
+ "\n"
+ "This is only used when N-sized world generation is available \n"
+ "and/or when on a server where [generateOnlyInHighestDetail] is false. \n"
+ "\n"
+ "Experimental:\n"
+ "Enabling this option will increase CPU and harddrive use\n"
+ "and may cause rendering bugs.\n"
+ "\n"
+ "")
.build();
}
}
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>()
.setChatCommandName("threading.numberOfThreads")
.setMinDefaultMax(1,
@@ -1418,6 +1525,8 @@ public class Config
public static class Logging
{
public static ConfigUIComment loggingHeader = new ConfigUIComment.Builder().setParentConfigClass(Logging.class).build();
// TODO add change all option
// TODO default to error chat and info file
public static ConfigEntry<EDhApiLoggerMode> logWorldGenEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
@@ -1460,7 +1569,7 @@ public class Config
public static ConfigEntry<EDhApiLoggerMode> logNetworkEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setChatCommandName("logging.logNetworkEvent")
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE)
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_WARNING_TO_FILE)
.comment(""
+ "If enabled, the mod will log information about network operations. \n"
+ "This can be useful for debugging.")
@@ -1472,6 +1581,7 @@ public class Config
public static class Warning
{
public static ConfigUIComment warningHeader = new ConfigUIComment.Builder().setParentConfigClass(Warning.class).build();
public static ConfigEntry<Boolean> showLowMemoryWarningOnStartup = new ConfigEntry.Builder<Boolean>()
.set(true)
@@ -1566,15 +1676,6 @@ public class Config
.comment("" +
"Defines the distance allowed to generate around the player." +
"")
.setPerformance(EConfigEntryPerformance.HIGH)
.build();
public static ConfigEntry<Boolean> generateOnlyInHighestDetail = new ConfigEntry.Builder<Boolean>()
.setChatCommandName("generation.highestDetailOnly")
.set(false)
.comment(""
+ "Makes the server reject all generation requests for detail levels below the highest one.\n"
+ "")
.build();
public static ConfigEntry<Integer> generationBoundsX = new ConfigEntry.Builder<Integer>()
@@ -1619,7 +1720,6 @@ public class Config
.comment("" +
"Defines the distance the player will receive updates around." +
"")
.setPerformance(EConfigEntryPerformance.HIGH)
.build();
@@ -1648,19 +1748,52 @@ public class Config
"Defines the distance allowed to be synchronized around the player. \n" +
"Should be the same or larger than maxGenerationRequestDistance in most cases." +
"")
.setPerformance(EConfigEntryPerformance.HIGH)
.build();
// Common
public static ConfigEntry<Integer> maxDataTransferSpeed = new ConfigEntry.Builder<Integer>()
.setChatCommandName("common.maxDataTransferSpeed")
public static ConfigEntry<Integer> playerBandwidthLimit = new ConfigEntry.Builder<Integer>()
.setChatCommandName("common.playerBandwidthLimit")
.setMinDefaultMax(0, 500, 1000000 /* 1 GB/s */)
.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."
+ "")
.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 class Experimental
{
public static ConfigEntry<Boolean> enableNSizedGeneration = new ConfigEntry.Builder<Boolean>()
.setChatCommandName("generation.nSized")
.set(false)
.comment(""
+ "When enabled on the client, this allows loading lower detail levels as needed to speed up terrain generation.\n"
+ "This must also be enabled on the server; otherwise, it will have no effect.\n"
+ "For better performance when switching LOD detail levels, enabling [upsampleLowerDetailLodsToFillHoles] is recommended.\n"
+ "")
.build();
}
}
@@ -1688,12 +1821,10 @@ public class Config
try
{
// TODO automatically get all instances of AbstractPresetConfigEventHandler and fire "setUiOnlyConfigValues"
ThreadPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
RenderQualityPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
QuickRenderToggleConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
QuickShowWorldGenProgressConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
RenderCacheConfigEventHandler.getInstance();
}
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,330 @@
/*
* 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 org.apache.logging.log4j.Logger;
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 Logger LOGGER = DhLoggerBuilder.getLogger();
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.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.HashSet;
public class ConfigEntryWithPresetOptions<TQuickEnum, TConfig>
public class ConfigPresetOptions<TQuickEnum, TConfig>
{
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.configOptionByQualityOption = configOptionByQualityOption;
@@ -40,6 +44,10 @@ public class ConfigEntryWithPresetOptions<TQuickEnum, TConfig>
//=========//
// methods //
//=========//
public void updateConfigEntry(TQuickEnum quickQuality)
{
TConfig newValue = this.configOptionByQualityOption.get(quickQuality);
@@ -48,7 +56,9 @@ public class ConfigEntryWithPresetOptions<TQuickEnum, TConfig>
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<>();
for (TQuickEnum key : this.configOptionByQualityOption.keySet())
@@ -17,12 +17,12 @@
* 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.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.util.converters.DefaultConverter;
import com.seibel.distanthorizons.core.config.api.converters.DefaultConverter;
import java.util.function.Consumer;
@@ -41,7 +41,7 @@ import java.util.function.Consumer;
*/
public class DhApiConfigValue<coreType, apiType> implements IDhApiConfigValue<apiType>
{
private final IConfigEntry<coreType> configEntry;
private final ConfigEntry<coreType> configBase;
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.
*/
@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>();
}
@@ -63,22 +63,22 @@ public class DhApiConfigValue<coreType, apiType> implements IDhApiConfigValue<ap
* This constructor should only be called internally. <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;
}
public apiType getValue() { return this.configConverter.convertToApiType(this.configEntry.get()); }
public apiType getTrueValue() { return this.configConverter.convertToApiType(this.configEntry.getTrueValue()); }
public apiType getApiValue() { return this.configConverter.convertToApiType(this.configEntry.getApiValue()); }
public apiType getValue() { return this.configConverter.convertToApiType(this.configBase.get()); }
public apiType getTrueValue() { return this.configConverter.convertToApiType(this.configBase.getTrueValue()); }
public apiType getApiValue() { return this.configConverter.convertToApiType(this.configBase.getApiValue()); }
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;
}
else
@@ -87,18 +87,31 @@ public class DhApiConfigValue<coreType, apiType> implements IDhApiConfigValue<ap
}
}
public boolean clearValue() { return this.setValue(null); }
public boolean clearValue()
{
if (this.configBase.getAllowApiOverride())
{
// 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
this.configBase.setApiValue(null);
return true;
}
else
{
return false;
}
}
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 getMaxValue() { return this.configConverter.convertToApiType(this.configEntry.getMax()); }
public apiType getMinValue() { return this.configConverter.convertToApiType(this.configEntry.getMin()); }
public apiType getDefaultValue() { return this.configConverter.convertToApiType(this.configBase.getDefaultValue()); }
public apiType getMaxValue() { return this.configConverter.convertToApiType(this.configBase.getMax()); }
public apiType getMinValue() { return this.configConverter.convertToApiType(this.configBase.getMin()); }
public void addChangeListener(Consumer<apiType> onValueChangeFunc)
{
this.configEntry.addValueChangeListener((coreValue) ->
this.configBase.addValueChangeListener((coreValue) ->
{
apiType apiValue = this.configConverter.convertToApiType(coreValue);
onValueChangeFunc.accept(apiValue);
@@ -17,7 +17,7 @@
* 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.coreapi.interfaces.config.IConverter;
@@ -17,7 +17,7 @@
* 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;
@@ -17,7 +17,7 @@
* 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.coreapi.interfaces.config.IConverter;
@@ -31,14 +31,12 @@ import com.seibel.distanthorizons.coreapi.interfaces.config.IConverter;
public class RenderModeEnabledConverter implements IConverter<EDhApiRendererMode, Boolean>
{
@Override public EDhApiRendererMode convertToCoreType(Boolean renderingEnabled)
{
return renderingEnabled ? EDhApiRendererMode.DEFAULT : EDhApiRendererMode.DISABLED;
}
@Override
public EDhApiRendererMode convertToCoreType(Boolean renderingEnabled)
{ return renderingEnabled ? EDhApiRendererMode.DEFAULT : EDhApiRendererMode.DISABLED; }
@Override public Boolean convertToApiType(EDhApiRendererMode renderingMode)
{
return renderingMode == EDhApiRendererMode.DEFAULT;
}
@Override
public Boolean convertToApiType(EDhApiRendererMode renderingMode)
{ return renderingMode == EDhApiRendererMode.DEFAULT; }
}
@@ -35,8 +35,17 @@ public class QuickRenderToggleConfigEventHandler
/** private since we only ever need one handler at a time */
private QuickRenderToggleConfigEventHandler()
{
this.quickRenderChangeListener = new ConfigChangeListener<>(Config.Client.quickEnableRendering, (val) -> { Config.Client.Advanced.Debugging.rendererMode.set(Config.Client.quickEnableRendering.get() ? EDhApiRendererMode.DEFAULT : EDhApiRendererMode.DISABLED); });
this.rendererModeChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Debugging.rendererMode, (val) -> { Config.Client.quickEnableRendering.set(Config.Client.Advanced.Debugging.rendererMode.get() != EDhApiRendererMode.DISABLED); });
this.quickRenderChangeListener = new ConfigChangeListener<>(Config.Client.quickEnableRendering,
(val) -> {
Config.Client.Advanced.Debugging.rendererMode.set(Config.Client.quickEnableRendering.get()
? EDhApiRendererMode.DEFAULT
: EDhApiRendererMode.DISABLED);
});
this.rendererModeChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Debugging.rendererMode,
(val) -> {
Config.Client.quickEnableRendering.set(
Config.Client.Advanced.Debugging.rendererMode.get() != EDhApiRendererMode.DISABLED);
});
}
/**
@@ -45,7 +54,8 @@ public class QuickRenderToggleConfigEventHandler
*/
public void setUiOnlyConfigValues()
{
Config.Client.quickEnableRendering.set(Config.Client.Advanced.Debugging.rendererMode.get() != EDhApiRendererMode.DISABLED);
boolean enableRendering = Config.Client.Advanced.Debugging.rendererMode.get() != EDhApiRendererMode.DISABLED;
Config.Client.quickEnableRendering.set(enableRendering);
}
}
@@ -22,13 +22,77 @@ package com.seibel.distanthorizons.core.config.eventHandlers;
import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.api.interfaces.render.IDhApiRenderProxy;
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 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
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;
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;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.ConfigHandler;
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 static UnsafeValuesConfigListener INSTANCE = new UnsafeValuesConfigListener();
@Override
public void onConfigValueSet()
{
Config.Client.Advanced.Debugging.allowUnsafeValues.configBase.disableMinMax =
Config.Client.Advanced.Debugging.allowUnsafeValues.get();
}
{ ConfigHandler.INSTANCE.runMinMaxValidation = !Config.Client.Advanced.Debugging.allowUnsafeValues.get(); }
@Override
public void onUiModify()
{
}
}
@@ -19,13 +19,13 @@
package com.seibel.distanthorizons.core.config.eventHandlers.presets;
import com.seibel.distanthorizons.core.config.ConfigBase;
import com.seibel.distanthorizons.core.config.ConfigEntryWithPresetOptions;
import com.seibel.distanthorizons.core.config.ConfigHandler;
import com.seibel.distanthorizons.core.config.ConfigPresetOptions;
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.util.TimerUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.config.IConfigGui;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry;
import com.seibel.distanthorizons.coreapi.util.StringUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -39,10 +39,9 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
private static final long MS_DELAY_BEFORE_APPLYING_PRESET = 3_000;
@Nullable
private static IConfigGui configGui = SingletonInjector.INSTANCE.get(IConfigGui.class);
private static boolean guiListenersAdded = false;
private static final IConfigGui CONFIG_GUI = SingletonInjector.INSTANCE.get(IConfigGui.class);
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 */
protected Timer applyPresetTimer = null;
/** the enum to apply after the timer expires or the UI screen changes. */
@@ -59,9 +58,9 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
public AbstractPresetConfigEventHandler()
{
// 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 +89,7 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
public void onConfigValueSet()
{
// don't try modifying the config before it's been loaded from file
if (!ConfigBase.INSTANCE.isLoaded)
if (!ConfigHandler.INSTANCE.isLoaded)
{
return;
}
@@ -144,7 +143,7 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
this.changingPreset = true;
// update the controlled config values
for (ConfigEntryWithPresetOptions<TPresetEnum, ?> configEntry : this.configList)
for (ConfigPresetOptions<TPresetEnum, ?> configEntry : this.configList)
{
configEntry.updateConfigEntry(newPresetEnum);
}
@@ -200,7 +199,7 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
// 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();
possiblePresetSet.retainAll(optionPresetSet);
@@ -230,7 +229,7 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
// abstract methods //
//==================//
protected abstract IConfigEntry<TPresetEnum> getPresetConfigEntry();
protected abstract AbstractConfigBase<TPresetEnum> getPresetConfigEntry();
protected abstract List<TPresetEnum> getPresetEnumList();
protected abstract TPresetEnum getCustomPresetEnum();
@@ -39,14 +39,17 @@ public class QuickShowWorldGenProgressConfigEventHandler
this.quickChangeListener = new ConfigChangeListener<>(Config.Client.quickShowWorldGenProgress,
(val) ->
{
Config.Common.WorldGenerator.showGenerationProgress.set(Config.Client.quickShowWorldGenProgress.get()
? Config.Common.WorldGenerator.showGenerationProgress.getDefaultValue()
boolean quickShowProgress = Config.Client.quickShowWorldGenProgress.get();
Config.Common.WorldGenerator.showGenerationProgress.set(
quickShowProgress
? EDhApiDistantGeneratorProgressDisplayLocation.OVERLAY
: EDhApiDistantGeneratorProgressDisplayLocation.DISABLED);
});
this.fullChangeListener = new ConfigChangeListener<>(Config.Common.WorldGenerator.showGenerationProgress,
(val) ->
{
Config.Client.quickShowWorldGenProgress.set(Config.Common.WorldGenerator.showGenerationProgress.get() != EDhApiDistantGeneratorProgressDisplayLocation.DISABLED);
boolean showProgress = Config.Common.WorldGenerator.showGenerationProgress.get() != EDhApiDistantGeneratorProgressDisplayLocation.DISABLED;
Config.Client.quickShowWorldGenProgress.setWithoutFiringEvents(showProgress);
});
}
@@ -56,7 +59,8 @@ public class QuickShowWorldGenProgressConfigEventHandler
*/
public void setUiOnlyConfigValues()
{
Config.Client.quickEnableRendering.set(Config.Common.WorldGenerator.showGenerationProgress.get() != EDhApiDistantGeneratorProgressDisplayLocation.DISABLED);
boolean showProgress = Config.Common.WorldGenerator.showGenerationProgress.get() != EDhApiDistantGeneratorProgressDisplayLocation.DISABLED;
Config.Client.quickShowWorldGenProgress.set(showProgress);
}
}
@@ -26,9 +26,9 @@ import com.seibel.distanthorizons.api.enums.config.EDhApiVerticalQuality;
import com.seibel.distanthorizons.api.enums.config.quickOptions.EDhApiQualityPreset;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiTransparency;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.ConfigEntryWithPresetOptions;
import com.seibel.distanthorizons.core.config.ConfigPresetOptions;
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 org.apache.logging.log4j.Logger;
@@ -42,7 +42,7 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
private static final Logger LOGGER = LogManager.getLogger();
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>()
{{
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.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>()
{{
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.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>()
{{
this.put(EDhApiQualityPreset.MINIMUM, EDhApiHorizontalQuality.LOWEST);
@@ -69,7 +69,7 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.put(EDhApiQualityPreset.HIGH, EDhApiHorizontalQuality.HIGH);
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>()
{{
this.put(EDhApiQualityPreset.MINIMUM, EDhApiTransparency.DISABLED);
@@ -78,7 +78,7 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.put(EDhApiQualityPreset.HIGH, 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>()
{{
this.put(EDhApiQualityPreset.MINIMUM, false);
@@ -87,7 +87,7 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.put(EDhApiQualityPreset.HIGH, 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>()
{{
this.put(EDhApiQualityPreset.MINIMUM, EDhApiMcRenderingFadeMode.NONE);
@@ -96,7 +96,7 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.put(EDhApiQualityPreset.HIGH, 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> dhDither = new ConfigPresetOptions<>(Config.Client.Advanced.Graphics.Quality.ditherDhFade,
new HashMap<EDhApiQualityPreset, Boolean>()
{{
this.put(EDhApiQualityPreset.MINIMUM, false);
@@ -105,6 +105,24 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.put(EDhApiQualityPreset.HIGH, 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);
}});
@@ -123,9 +141,11 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.configList.add(this.ssaoEnabled);
this.configList.add(this.vanillaFade);
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
new ConfigChangeListener<>(config.configEntry, (val) -> { this.onConfigValueChanged(); });
@@ -139,7 +159,7 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
//==============//
@Override
protected IConfigEntry<EDhApiQualityPreset> getPresetConfigEntry() { return Config.Client.qualityPresetSetting; }
protected AbstractConfigBase<EDhApiQualityPreset> getPresetConfigEntry() { return Config.Client.qualityPresetSetting; }
@Override
protected List<EDhApiQualityPreset> getPresetEnumList() { return Arrays.asList(EDhApiQualityPreset.values()); }
@@ -22,8 +22,8 @@ package com.seibel.distanthorizons.core.config.eventHandlers.presets;
import com.seibel.distanthorizons.api.enums.config.quickOptions.EDhApiThreadPreset;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.ConfigEntryWithPresetOptions;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry;
import com.seibel.distanthorizons.core.config.ConfigPresetOptions;
import com.seibel.distanthorizons.core.config.types.AbstractConfigBase;
import com.seibel.distanthorizons.coreapi.util.MathUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -41,7 +41,7 @@ public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHan
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>()
{{
this.put(EDhApiThreadPreset.MINIMAL_IMPACT, getThreadCountByPercent(0.1));
@@ -51,7 +51,7 @@ public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHan
this.put(EDhApiThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, getThreadCountByPercent(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>()
{{
this.put(EDhApiThreadPreset.MINIMAL_IMPACT, 0.5);
@@ -74,7 +74,7 @@ public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHan
this.configList.add(this.threadCount);
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
new ConfigChangeListener<>(config.configEntry, (val) -> { this.onConfigValueChanged(); });
@@ -119,7 +119,7 @@ public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHan
//==============//
@Override
protected IConfigEntry<EDhApiThreadPreset> getPresetConfigEntry() { return Config.Client.threadPresetSetting; }
protected AbstractConfigBase<EDhApiThreadPreset> getPresetConfigEntry() { return Config.Client.threadPresetSetting; }
@Override
protected List<EDhApiThreadPreset> getPresetEnumList() { return Arrays.asList(EDhApiThreadPreset.values()); }
@@ -20,13 +20,13 @@
package com.seibel.distanthorizons.core.config.file;
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.config.ConfigBase;
import com.seibel.distanthorizons.core.config.types.AbstractConfigType;
import com.seibel.distanthorizons.core.config.types.AbstractConfigBase;
import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
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.Logger;
@@ -41,13 +41,11 @@ import java.util.concurrent.locks.ReentrantLock;
* @author coolGi
* @version 2023-8-26
*/
public class ConfigFileHandling
public class ConfigFileHandler
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
public final ConfigBase configBase;
public final Path configPath;
private final Logger logger;
@@ -64,17 +62,24 @@ public class ConfigFileHandling
// constructor //
//=============//
public ConfigFileHandling(ConfigBase configBase, Path configPath)
public ConfigFileHandler(Path configPath)
{
this.logger = LogManager.getLogger(this.getClass().getSimpleName() + ", " + configBase.modID);
this.configBase = configBase;
this.logger = LogManager.getLogger(this.getClass().getSimpleName() + ", " + ModInfo.ID);
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 */
public void saveToFile() { this.saveToFile(this.nightConfig); }
@@ -96,7 +101,7 @@ public class ConfigFileHandling
this.loadNightConfig(nightConfig);
for (AbstractConfigType<?, ?> entry : this.configBase.entries)
for (AbstractConfigBase<?> entry : ConfigHandler.INSTANCE.configBaseList)
{
if (ConfigEntry.class.isAssignableFrom(entry.getClass()))
{
@@ -134,7 +139,7 @@ public class ConfigFileHandling
{
this.readWriteLock.lock();
int currentCfgVersion = this.configBase.configVersion;
int currentCfgVersion = ModInfo.CONFIG_FILE_VERSION;
try
{
// Dont load the real `this.nightConfig`, instead create a tempoary one
@@ -146,17 +151,17 @@ public class ConfigFileHandling
}
catch (Exception ignored) { }
if (currentCfgVersion == this.configBase.configVersion)
if (currentCfgVersion == ModInfo.CONFIG_FILE_VERSION)
{
// 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");
this.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)
{
this.logger.warn(this.configBase.modName + " config is of an older version, currently there is no config updater... so resetting config");
this.logger.warn(ModInfo.NAME + " config is of an older version, currently there is no config updater... so resetting config");
try
{
Files.delete(this.configPath);
@@ -168,7 +173,7 @@ public class ConfigFileHandling
}
this.loadFromFile(this.nightConfig);
this.nightConfig.set("_version", this.configBase.configVersion);
this.nightConfig.set("_version", ModInfo.CONFIG_FILE_VERSION);
}
finally
{
@@ -194,7 +199,7 @@ public class ConfigFileHandling
// Load all the entries
for (AbstractConfigType<?, ?> entry : this.configBase.entries)
for (AbstractConfigBase<?> entry : ConfigHandler.INSTANCE.configBaseList)
{
if (ConfigEntry.class.isAssignableFrom(entry.getClass())
&& entry.getAppearance().showInFile)
@@ -218,6 +223,9 @@ public class ConfigFileHandling
//=======================//
// single config entries //
//=======================//
// Save an entry when only given the entry
public void saveEntry(ConfigEntry<?> entry)
@@ -235,10 +243,10 @@ public class ConfigFileHandling
else if (entry.getTrueValue() == null)
{
// 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("Entry [" + 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 */
@@ -248,9 +256,11 @@ public class ConfigFileHandling
public <T> void loadEntry(ConfigEntry<T> entry, CommentedFileConfig nightConfig)
{
if (!entry.getAppearance().showInFile)
{
return;
}
if (!nightConfig.contains(entry.getNameWCategory()))
if (!nightConfig.contains(entry.getNameAndCategory()))
{
this.saveEntry(entry, nightConfig);
return;
@@ -261,13 +271,13 @@ public class ConfigFileHandling
{
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;
}
// try converting the value if necessary
Class<?> expectedValueClass = entry.getType();
Object value = nightConfig.get(entry.getNameWCategory());
Object value = nightConfig.get(entry.getNameAndCategory());
Object convertedValue = ConfigTypeConverters.attemptToConvertFromString(expectedValueClass, value);
if (!convertedValue.getClass().equals(expectedValueClass))
{
@@ -276,19 +286,18 @@ public class ConfigFileHandling
"Make sure a converter is defined in ["+ConfigTypeConverters.class.getSimpleName()+"].");
convertedValue = entry.getDefaultValue();
}
entry.pureSet((T) convertedValue);
entry.setWithoutFiringEvents((T) convertedValue);
if (entry.getTrueValue() == null)
{
this.logger.warn("Entry [" + entry.getNameWCategory() + "] returned as null from the config. Using default value.");
entry.pureSet(entry.getDefaultValue());
this.logger.warn("Entry [" + entry.getNameAndCategory() + "] returned as null from the config. Using default value.");
entry.setWithoutFiringEvents(entry.getDefaultValue());
}
}
catch (Exception e)
{
// e.printStackTrace();
this.logger.warn("Entry [" + entry.getNameWCategory() + "] had an invalid value when loading the config. Using default value.");
entry.pureSet(entry.getDefaultValue());
this.logger.warn("Entry [" + entry.getNameAndCategory() + "] had an invalid value when loading the config. Using default value.");
entry.setWithoutFiringEvents(entry.getDefaultValue());
}
}
@@ -309,22 +318,21 @@ public class ConfigFileHandling
// 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 "#"
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
*/
public void loadNightConfig()
{
loadNightConfig(this.nightConfig);
}
public void loadNightConfig() { this.loadNightConfig(this.nightConfig); }
/**
* Does {@link CommentedFileConfig#load()} but with error checking
*
@@ -353,12 +361,16 @@ public class ConfigFileHandling
{
System.out.println("Creating file failed");
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);
SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class).crashMinecraft("Loading file and resetting config file failed at path [" + this.configPath + "]. Please check the file is ok and you have the permissions", ex);
}
}
//===============//
// file handling //
//===============//
public static void reCreateFile(Path path)
{
try
@@ -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 java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
@@ -35,6 +36,8 @@ public class JavaScreenHandlerScreen extends AbstractScreen
public static boolean firstRun = true;
public final Component jComponent;
static
{
// Note: this code can cause Mac
@@ -54,19 +57,25 @@ public class JavaScreenHandlerScreen extends AbstractScreen
public void init()
{
if (firstRun)
{
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;
frame.addKeyListener(new KeyListener() {
frame.addKeyListener(new KeyListener()
{
@Override
public void keyPressed(KeyEvent keyEvent)
{
System.out.println("Key pressed code=" + keyEvent.getKeyCode() + ", char=" + keyEvent.getKeyChar());
if (keyEvent.getKeyCode() == KeyEvent.VK_ESCAPE)
{
thiss.close = true;
}
}
@Override
@@ -80,8 +89,8 @@ public class JavaScreenHandlerScreen extends AbstractScreen
EmbeddedFrameUtil.embeddedFrameSetBounds(frame, 0, 0, this.width, this.height);
firstRun = false;
}
else
EmbeddedFrameUtil.showFrame(frame);
EmbeddedFrameUtil.showFrame(frame);
}
/** A testing/debug screen */
@@ -89,13 +98,43 @@ public class JavaScreenHandlerScreen extends AbstractScreen
{
public ExampleScreen()
{
setLayout(new GridBagLayout());
GridBagConstraints constraints = new GridBagConstraints();
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.weightx = 0.5;
constraints.gridx = 0;
constraints.gridy = 0;
add(new JLabel("Hello World!"), constraints);
this.setLayout(new GridBagLayout());
this.setBackground(new Color(255, 0, 0)); // doesn't appear to be used
GridBagConstraints helloWorldConstraints = new GridBagConstraints();
helloWorldConstraints.weightx = 0.5;
helloWorldConstraints.gridx = 0;
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
public void onClose()
{
frame.remove(jComponent);
frame.remove(this.jComponent);
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;
import com.seibel.distanthorizons.core.config.ConfigBase;
import com.seibel.distanthorizons.core.config.gui.IConfigGuiInfo;
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
*/
// Note for devs: The "S" is the class that is extending this
public abstract class AbstractConfigType<T, S>
public abstract class AbstractConfigBase<T>
{
public String category = ""; // 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 boolean isFloatingPointNumber;
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;
@@ -47,7 +49,7 @@ public abstract class AbstractConfigType<T, S>
// constructor //
//=============//
protected AbstractConfigType(EConfigEntryAppearance appearance, T defaultValue)
protected AbstractConfigBase(EConfigEntryAppearance appearance, T defaultValue)
{
this.defaultValue = defaultValue;
this.value = defaultValue;
@@ -74,7 +76,7 @@ public abstract class AbstractConfigType<T, S>
public String getCategory() { return this.category; }
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 */
@@ -27,10 +27,19 @@ import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance
*
* @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)
{
@@ -38,20 +47,26 @@ public class ConfigCategory extends AbstractConfigType<Class<?>, ConfigCategory>
this.destination = destination;
}
public String getDestination()
{
return this.destination;
}
//==================//
// property getters //
//==================//
public String getDestination() { return this.destination; }
/** Use get() instead for category */
@Override
@Deprecated
public Class<?> getType()
{
return value;
}
public Class<?> getType() { return this.value; }
public static class Builder extends AbstractConfigType.Builder<Class<?>, Builder>
//=========//
// builder //
//=========//
public static class Builder extends AbstractConfigBase.Builder<Class<?>, Builder>
{
private String tmpDestination = null;
@@ -20,50 +20,52 @@
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.IConfigListener;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryPerformance;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry;
import com.seibel.distanthorizons.core.config.types.enums.EConfigValidity;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.function.Consumer;
/**
* Use for making the config variables
* for types that are not supported by it look in ConfigBase
* This config type allows for entering text, number, or enum values.
*
* @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 max;
private final ArrayList<IConfigListener> listenerList;
private final String chatCommandName;
private final EConfigEntryPerformance performance;
// API control //
/**
* 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 final boolean allowApiOverride;
private final boolean allowApiOverride;
/** Will be null if un-set */
@Nullable
private T apiValue;
/** Creates the entry */
//=============//
// constructor //
//=============//
private ConfigEntry(
EConfigEntryAppearance appearance,
T value, String comment, T min, T max,
String chatCommandName, boolean allowApiOverride,
EConfigEntryPerformance performance,
String comment, String chatCommandName,
T value, T min, T max,
boolean allowApiOverride,
ArrayList<IConfigListener> listenerList)
{
super(appearance, value);
@@ -73,38 +75,57 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
this.max = max;
this.chatCommandName = chatCommandName;
this.allowApiOverride = allowApiOverride;
this.performance = performance;
this.listenerList = listenerList;
}
/** Gets the default value of the option */
@Override
public T getDefaultValue() { return super.defaultValue; }
//==========================//
// property getters/setters //
//==========================//
/** 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)
{
this.apiValue = newApiValue;
this.listenerList.forEach(IConfigListener::onConfigValueSet);
}
@Override
public T getApiValue() { return this.apiValue; }
@Override
public boolean getAllowApiOverride() { return this.allowApiOverride; }
/**
* 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>
* 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})
*/
public void pureSet(T newValue) {
super.set(newValue);
public boolean apiIsOverriding()
{
return this.allowApiOverride
&& this.apiValue != null;
}
/**
* 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 */
@Override
public void setWithoutSaving(T newValue)
{
super.set(newValue);
@@ -129,74 +150,35 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
}
//===============//
// value getters //
//===============//
@Override
public T get()
{
if (allowApiOverride && apiValue != null)
if (this.allowApiOverride
&& this.apiValue != null)
{
return apiValue;
return this.apiValue;
}
return super.get();
}
@Override
public T getTrueValue()
{
return super.get();
}
/** Ignores the API value if set. */
public T getTrueValue() { 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
*
* @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; }
//===========//
// listeners //
//===========//
/** Fired whenever the config value changes to a new value. */
public void addValueChangeListener(Consumer<T> onValueChangeFunc)
@@ -221,116 +203,114 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
public void setListeners(IConfigListener... newListeners) { this.listenerList.addAll(Arrays.asList(newListeners)); }
/**
* Checks if the option is valid
*
* @return 0 == valid
* <p> 2 == invalid
* <p> 1 == number too high
* <p> -1 == number too low
*/
@Override
public byte isValid() { return isValid(this.value, this.min, this.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)
//====================//
// min/max validation //
//====================//
/** Checks if this config's current value is valid */
public EConfigValidity getValidity() { return this.getValidity(this.value, this.min, this.max); }
/** Checks if the given value is valid */
public EConfigValidity getValidity(@Nullable T value) { return this.getValidity(value, this.min, this.max); }
/** Checks if the given value is valid */
public EConfigValidity getValidity(@Nullable T value, @Nullable T min, @Nullable 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
return 0;
return EConfigValidity.VALID;
}
else if (value == null || this.value == null
else if (value == null
|| this.value == null
|| value.getClass() != this.value.getClass())
{
// If the 2 variables aren't the same type then it will be invalid
return 2;
// If the 2 variables aren't the same type
// 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
if (max != null && NumberUtil.greaterThan((Number) value, (Number) max))
// Only check min/max if this config's type is a number
if (max != null
&& NumberUtil.greaterThan((Number) value, (Number) max))
{
return 1;
}
if (min != null && NumberUtil.lessThan((Number) value, (Number) min))
{
return -1;
return EConfigValidity.NUMBER_TOO_HIGH;
}
return 0;
if (min != null
&& NumberUtil.lessThan((Number) value, (Number) min))
{
return EConfigValidity.NUMBER_TOO_LOW;
}
return EConfigValidity.VALID;
}
else
{
return 0;
return EConfigValidity.VALID;
}
}
//===============//
// file handling //
//===============//
/** 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 */
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 */
public boolean equals(ConfigEntry<?> obj)
{
// Can all of this just be "return this.value.equals(obj.value)"?
if (Number.class.isAssignableFrom(this.value.getClass()))
{
return this.value == obj.value;
}
else
{
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 T tmpMin = null;
private T tmpMax = null;
protected String tmpChatCommandName = null;
private boolean tmpUseApiOverwrite = true;
private EConfigEntryPerformance tmpPerformance = EConfigEntryPerformance.DONT_SHOW;
protected ArrayList<IConfigListener> tmpIConfigListener = new ArrayList<>();
public Builder<T> comment(String newComment)
{
this.tmpComment = newComment;
@@ -376,12 +356,6 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
return this;
}
public Builder<T> setPerformance(EConfigEntryPerformance newPerformance)
{
this.tmpPerformance = newPerformance;
return this;
}
public Builder<T> replaceListeners(ArrayList<IConfigListener> newConfigListener)
@@ -410,13 +384,15 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
// build //
public ConfigEntry<T> build()
{
return new ConfigEntry<>(
this.tmpAppearance,
this.tmpValue, this.tmpComment, this.tmpMin, this.tmpMax,
this.tmpChatCommandName, this.tmpUseApiOverwrite,
this.tmpPerformance, this.tmpIConfigListener);
this.tmpAppearance,
this.tmpComment, this.tmpChatCommandName, this.tmpValue, this.tmpMin, this.tmpMax,
this.tmpUseApiOverwrite,
this.tmpIConfigListener);
}
}
@@ -21,29 +21,42 @@ package com.seibel.distanthorizons.core.config.types;
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)
{
super(EConfigEntryAppearance.ONLY_IN_GUI, runnable);
}
//=============//
// constructor //
//=============//
/** 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 static class Builder extends AbstractConfigType.Builder<Runnable, Builder>
//=========//
// builder //
//=========//
public static class Builder extends AbstractConfigBase.Builder<Runnable, Builder>
{
/** Appearance shouldn't be changed */
@Override
public Builder setAppearance(EConfigEntryAppearance newAppearance)
{
return this;
}
public Builder setAppearance(EConfigEntryAppearance newAppearance) { return this; }
public ConfigUIButton build()
{
return new ConfigUIButton(this.tmpValue);
}
{ return new ConfigUIButton(this.tmpValue); }
}
@@ -19,20 +19,47 @@
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.logging.DhLoggerBuilder;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Adds something like a ConfigEntry but without a button to change the input
*
* @author coolGi
*/
public class ConfigUIComment extends AbstractConfigType<String, ConfigUIComment>
public class ConfigUIComment extends AbstractConfigBase<String>
{
public ConfigUIComment()
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public String parentConfigPath = null;
@Nullable
public EConfigCommentTextPosition textPosition = null;
//=============//
// constructor //
//=============//
public ConfigUIComment(String parentConfigPath, @Nullable EConfigCommentTextPosition textPosition)
{
super(EConfigEntryAppearance.ONLY_IN_GUI, "");
this.parentConfigPath = parentConfigPath;
this.textPosition = textPosition;
}
//=========//
// setters //
//=========//
/** Appearance shouldn't be changed */
@Override
public void setAppearance(EConfigEntryAppearance newAppearance) { }
@@ -41,27 +68,112 @@ public class ConfigUIComment extends AbstractConfigType<String, ConfigUIComment>
@Override
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 */
@Deprecated
@Override
public Builder setAppearance(EConfigEntryAppearance newAppearance)
{
return this;
}
public Builder setAppearance(EConfigEntryAppearance newAppearance) { return this; }
/** Pointless to set the value */
@Deprecated
@Override
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;
}
// build //
public ConfigUIComment build()
{
return new ConfigUIComment();
}
{ return new ConfigUIComment(this.tempParentConfigPath, this.tempTextPosition); }
}
}
@@ -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.
* This only effects the UI
* This element is only visible in the GUI.
*
* @author coolGi
*/
@Deprecated // FIXME doesn't work with localization
public class ConfigUiLinkedEntry extends AbstractConfigType<AbstractConfigType<?, ?>, ConfigUiLinkedEntry>
public class ConfigUiLinkedEntry extends AbstractConfigBase<AbstractConfigBase<?>>
{
public ConfigUiLinkedEntry(AbstractConfigType<?, ?> value)
{
super(EConfigEntryAppearance.ONLY_IN_GUI, value);
}
//=============//
// constructor //
//=============//
public ConfigUiLinkedEntry(AbstractConfigBase<?> value)
{ super(EConfigEntryAppearance.ONLY_IN_GUI, value); }
//=========//
// setters //
//=========//
/** Appearance shouldn't be changed */
@Override
@@ -41,10 +48,15 @@ public class ConfigUiLinkedEntry extends AbstractConfigType<AbstractConfigType<?
/** Value shouldn't be changed after creation */
@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 */
@Override
@@ -60,4 +72,6 @@ public class ConfigUiLinkedEntry extends AbstractConfigType<AbstractConfigType<?
}
}
@@ -20,19 +20,13 @@
package com.seibel.distanthorizons.core.config.types.enums;
/**
* What is the performance impact of an entry
* (default is DONT_SHOW)
*
* @author coolGi
* RIGHT_OVER_BUTTONS <br/>
* CENTER_OF_SCREEN <br/>
* CENTERED_OVER_BUTTONS <br/>
*/
public enum EConfigEntryPerformance
public enum EConfigCommentTextPosition
{
NONE,
VERY_LOW,
LOW,
MEDIUM,
HIGH,
VERY_HIGH,
DONT_SHOW
RIGHT_JUSTIFIED,
CENTER_OF_SCREEN,
CENTERED_OVER_BUTTONS,
}
@@ -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,
}
@@ -29,13 +29,12 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrappe
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.*;
import java.util.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.ConcurrentHashMap;
/**
* WARNING: This is not THREAD-SAFE! <br><br>
@@ -46,7 +45,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
* since it stringifies every block and biome name, which is quite bulky.
* It might be worth while to have a biome and block ID that then both get mapped
* to the data point ID to reduce file size.
* And/or it would be good to dynamically remove IDs that aren't currently in use.
* And/or it would be good to dynamically remove IDs that aren't currently in use.
*
* @author Leetom
*/
@@ -63,15 +62,12 @@ public class FullDataPointIdMap
private static final String BLOCK_STATE_SEPARATOR_STRING = "_DH-BSW_";
/** used when the data point map is running normally */
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/** should only be used for debugging */
private long pos;
/** The index should be the same as the Entry's ID */
private final ArrayList<Entry> entryList = new ArrayList<>();
private final HashMap<Entry, Integer> idMap = new HashMap<>();
private final ConcurrentHashMap<Entry, Integer> idMap = new ConcurrentHashMap<>();
private int cachedHashCode = 0;
@@ -89,34 +85,25 @@ public class FullDataPointIdMap
// getters //
//=========//
/** @throws IndexOutOfBoundsException if the given ID isn't in the {@link FullDataPointIdMap#entryList} */
private Entry getEntry(int id) throws IndexOutOfBoundsException
{
try
{
this.readWriteLock.readLock().lock();
Entry entry;
try
{
entry = this.entryList.get(id);
}
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()+"].");
}
return entry;
}
finally
{
this.readWriteLock.readLock().unlock();
}
}
/** @see FullDataPointIdMap#getEntry(int) */
public IBiomeWrapper getBiomeWrapper(int id) throws IndexOutOfBoundsException { return this.getEntry(id).biome; }
/** @see FullDataPointIdMap#getEntry(int) */
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} */
private Entry getEntry(int id) throws IndexOutOfBoundsException
{
Entry entry;
try
{
entry = this.entryList.get(id);
}
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()+"].");
}
return entry;
}
/** @return -1 if the list is empty */
@@ -137,74 +124,37 @@ public class FullDataPointIdMap
* If an entry with the given values already exists nothing will
* be added but the existing item's ID will still be returned.
*/
public int addIfNotPresentAndGetId(IBiomeWrapper biome, IBlockStateWrapper blockState) { return this.addIfNotPresentAndGetId(Entry.getEntry(biome, blockState), true); }
/** @param useWriteLocks should only be false if this method is already in a write lock to prevent unlocking at the wrong time */
private int addIfNotPresentAndGetId(Entry biomeBlockStateEntry, boolean useWriteLocks)
public int addIfNotPresentAndGetId(IBiomeWrapper biome, IBlockStateWrapper blockState) { return this.addIfNotPresentAndGetId(Entry.getEntry(biome, blockState)); }
private int addIfNotPresentAndGetId(Entry biomeBlockStateEntry)
{
try
// try getting the existing ID
Integer nullableId = this.idMap.get(biomeBlockStateEntry);
if (nullableId != null)
{
if (useWriteLocks)
{
this.readWriteLock.writeLock().lock();
}
int id;
if (this.idMap.containsKey(biomeBlockStateEntry))
{
// use the existing ID
id = this.idMap.get(biomeBlockStateEntry);
}
else
{
// Add the new ID
id = this.entryList.size();
this.entryList.add(biomeBlockStateEntry);
this.idMap.put(biomeBlockStateEntry, id);
// invalidate the cached hash code
this.cachedHashCode = 0;
}
return id;
return nullableId;
}
finally
// create the new ID
return this.idMap.compute(biomeBlockStateEntry, (Entry newBiomeBlockStateEntry, Integer currentId) ->
{
if (useWriteLocks)
if (currentId != null)
{
this.readWriteLock.writeLock().unlock();
}
}
}
/** allows for adding duplicate {@link Entry} */
private void add(Entry biomeBlockStateEntry, boolean useWriteLocks)
{
try
{
if (useWriteLocks)
{
this.readWriteLock.writeLock().lock();
return currentId;
}
int id = this.entryList.size();
// Add the new ID
currentId = this.entryList.size();
this.entryList.add(biomeBlockStateEntry);
this.idMap.put(biomeBlockStateEntry, id);
// invalidate the cached hash code
this.cachedHashCode = 0;
}
finally
{
if (useWriteLocks)
{
this.readWriteLock.writeLock().unlock();
}
}
return currentId;
});
}
/**
* Adds every {@link Entry} from inputMap into this map. <br>
* Allows duplicate entries. <br><br>
@@ -216,28 +166,23 @@ public class FullDataPointIdMap
*/
public void addAll(FullDataPointIdMap inputMap)
{
try
ArrayList<Entry> entriesToMerge = inputMap.entryList;
for (int i = 0; i < entriesToMerge.size(); i++)
{
//LOGGER.trace("adding {" + this.pos + ", " + this.entryList.size() + "} and {" + inputMap.pos + ", " + inputMap.entryList.size() + "}");
inputMap.readWriteLock.readLock().lock();
this.readWriteLock.writeLock().lock();
ArrayList<Entry> entriesToMerge = inputMap.entryList;
for (int i = 0; i < entriesToMerge.size(); i++)
{
Entry entity = entriesToMerge.get(i);
this.add(entity, false);
}
}
finally
{
this.readWriteLock.writeLock().unlock();
inputMap.readWriteLock.readLock().unlock();
//LOGGER.trace("finished merging {" + this.pos + ", " + this.entryList.size() + "} and {" + inputMap.pos + ", " + inputMap.entryList.size() + "}");
Entry entity = entriesToMerge.get(i);
this.add(entity);
}
}
/** allows for adding duplicate {@link Entry} */
private void add(Entry biomeBlockStateEntry)
{
int id = this.entryList.size();
this.entryList.add(biomeBlockStateEntry);
this.idMap.put(biomeBlockStateEntry, id);
// invalidate the cached hash code
this.cachedHashCode = 0;
}
/**
* Adds each entry from the given map to this map. <br><br>
@@ -250,31 +195,16 @@ public class FullDataPointIdMap
*/
public int[] mergeAndReturnRemappedEntityIds(FullDataPointIdMap inputMap)
{
try
ArrayList<Entry> entriesToMerge = inputMap.entryList;
int[] remappedEntryIds = new int[entriesToMerge.size()];
for (int i = 0; i < entriesToMerge.size(); i++)
{
//LOGGER.trace("merging {" + this.pos + ", " + this.entryList.size() + "} and {" + inputMap.pos + ", " + inputMap.entryList.size() + "}");
inputMap.readWriteLock.readLock().lock();
this.readWriteLock.writeLock().lock();
ArrayList<Entry> entriesToMerge = inputMap.entryList;
int[] remappedEntryIds = new int[entriesToMerge.size()];
for (int i = 0; i < entriesToMerge.size(); i++)
{
Entry entity = entriesToMerge.get(i);
int id = this.addIfNotPresentAndGetId(entity, false);
remappedEntryIds[i] = id;
}
return remappedEntryIds;
}
finally
{
this.readWriteLock.writeLock().unlock();
inputMap.readWriteLock.readLock().unlock();
//LOGGER.trace("finished merging {" + this.pos + ", " + this.entryList.size() + "} and {" + inputMap.pos + ", " + inputMap.entryList.size() + "}");
Entry entity = entriesToMerge.get(i);
int id = this.addIfNotPresentAndGetId(entity);
remappedEntryIds[i] = id;
}
return remappedEntryIds;
}
/** Should only be used if this map is going to be reused, otherwise bad things will happen. */
@@ -295,38 +225,29 @@ public class FullDataPointIdMap
/** Serializes all contained entries into the given stream, formatted in UTF */
public void serialize(DhDataOutputStream outputStream) throws IOException
{
try
outputStream.writeInt(this.entryList.size());
// only used when debugging
HashMap<String, FullDataPointIdMap.Entry> dataPointEntryBySerialization = new HashMap<>();
for (Entry entry : this.entryList)
{
this.readWriteLock.readLock().lock();
outputStream.writeInt(this.entryList.size());
String entryString = entry.serialize();
outputStream.writeUTF(entryString);
// only used when debugging
HashMap<String, FullDataPointIdMap.Entry> dataPointEntryBySerialization = new HashMap<>();
for (Entry entry : this.entryList)
if (RUN_SERIALIZATION_DUPLICATE_VALIDATION)
{
String entryString = entry.serialize();
outputStream.writeUTF(entryString);
if (RUN_SERIALIZATION_DUPLICATE_VALIDATION)
if (dataPointEntryBySerialization.containsKey(entryString))
{
if (dataPointEntryBySerialization.containsKey(entryString))
{
LOGGER.error("Duplicate serialized entry found with serial: " + entryString);
}
if (dataPointEntryBySerialization.containsValue(entry))
{
LOGGER.error("Duplicate serialized entry found with value: " + entry.serialize());
}
dataPointEntryBySerialization.put(entryString, entry);
LOGGER.error("Duplicate serialized entry found with serial: " + entryString);
}
if (dataPointEntryBySerialization.containsValue(entry))
{
LOGGER.error("Duplicate serialized entry found with value: " + entry.serialize());
}
dataPointEntryBySerialization.put(entryString, entry);
}
}
finally
{
this.readWriteLock.readLock().unlock();
//LOGGER.trace("serialize " + this.pos + " " + this.entryList.size());
}
}
/** Creates a new IdBiomeBlockStateMap from the given UTF formatted stream */
@@ -429,14 +350,15 @@ public class FullDataPointIdMap
{
private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
private static final Int2ReferenceOpenHashMap<ArrayList<Entry>> ENTRY_POOL = new Int2ReferenceOpenHashMap<>();
/** lock is necessary since {@link Int2ReferenceOpenHashMap} isn't concurrent and concurrent threads can cause infinite loops */
private static final ReentrantReadWriteLock ENTRY_POOL_LOCK = new ReentrantReadWriteLock();
/** two levels are present so we don't need to use a key object */
private static final ConcurrentHashMap<IBiomeWrapper, ConcurrentHashMap<IBlockStateWrapper, Entry>> ENTRY_BY_BLOCKSTATE_BY_BIOMEWRAPPER = new ConcurrentHashMap<>();
public final IBiomeWrapper biome;
public final IBlockStateWrapper blockState;
private Integer hashCode = null;
private int hashCode = 0;
private boolean hashGenerated = false;
private String serialString = null;
@@ -446,62 +368,21 @@ public class FullDataPointIdMap
public static Entry getEntry(IBiomeWrapper biome, IBlockStateWrapper blockState)
{
int entryHash = getHashCode(biome, blockState);
// try getting the existing entry
try
// check for existing entry
ConcurrentHashMap<IBlockStateWrapper, Entry> entryByBlockState = ENTRY_BY_BLOCKSTATE_BY_BIOMEWRAPPER.get(biome);
if (entryByBlockState != null)
{
ENTRY_POOL_LOCK.readLock().lock();
// check if an entry already exists
ArrayList<Entry> entryList = ENTRY_POOL.get(entryHash);
if (entryList != null)
Entry entry = entryByBlockState.get(blockState);
if (entry != null)
{
// at least one entry exists with this hash code
for (int i = 0; i < entryList.size(); i++)
{
Entry entry = entryList.get(i);
if (entry.biome.equals(biome) && entry.blockState.equals(blockState))
{
return entry;
}
}
// if we got here, then there was a hash collision and this entry wasn't present in the array
return entry;
}
}
finally
{
ENTRY_POOL_LOCK.readLock().unlock();
}
// no entry exists,
// create a new one
try
{
ENTRY_POOL_LOCK.writeLock().lock();
ArrayList<Entry> entryList = ENTRY_POOL.get(entryHash);
if (entryList == null)
{
// no entries exist for this hash code
// we assume that hash collisions should basically never happen,
// so the array starts with an initial capacity of 1.
// However, since collisions will eventually happen, using an arrayList prevents unexpected bugs caused by collisions.
entryList = new ArrayList<>(1);
ENTRY_POOL.put(entryHash, entryList);
}
Entry newEntry = new Entry(biome, blockState);
entryList.add(newEntry);
return newEntry;
}
finally
{
ENTRY_POOL_LOCK.writeLock().unlock();
}
// Lazily create the inner map and new Entry
return ENTRY_BY_BLOCKSTATE_BY_BIOMEWRAPPER
.computeIfAbsent(biome, newBiome -> new ConcurrentHashMap<>())
.computeIfAbsent(blockState, newBlockState -> new Entry(biome, blockState));
}
private Entry(IBiomeWrapper biome, IBlockStateWrapper blockState)
{
@@ -515,8 +396,24 @@ public class FullDataPointIdMap
// overrides //
//===========//
public static int getHashCode(Entry entry) { return getHashCode(entry.biome, entry.blockState); }
public static int getHashCode(IBiomeWrapper biome, IBlockStateWrapper blockState)
/**
* 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(Entry entry) { return generateHashCode(entry.biome, entry.blockState); }
private static int generateHashCode(IBiomeWrapper biome, IBlockStateWrapper blockState)
{
final int prime = 31;
@@ -527,26 +424,19 @@ public class FullDataPointIdMap
result = prime * result + (blockState == null ? 0 : blockState.hashCode());
return result;
}
@Override
public int hashCode()
{
// cache the hash code to improve speed
if (this.hashCode == null)
{
this.hashCode = getHashCode(this);
}
return this.hashCode;
}
@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())
@@ -562,7 +452,15 @@ public class FullDataPointIdMap
// (de)serializing //
//=================//
public String serialize() { return this.biome.getSerialString() + BLOCK_STATE_SEPARATOR_STRING + this.blockState.getSerialString(); }
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
{
@@ -116,7 +116,7 @@ public class FullDataSourceV1 implements IDataSource<IDhLevel>
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)
{
@@ -29,12 +29,16 @@ import com.seibel.distanthorizons.core.file.AbstractDataSourceHandler;
import com.seibel.distanthorizons.core.file.IDataSource;
import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.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.pos.DhLodPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.util.*;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo;
import it.unimi.dsi.fastutil.bytes.ByteArrayList;
import it.unimi.dsi.fastutil.longs.LongArrayList;
@@ -53,7 +57,7 @@ import java.util.List;
* @see FullDataSourceV1
*/
public class FullDataSourceV2
extends PhantomArrayListParent
extends AbstractPhantomArrayList
implements IDataSource<IDhLevel>, IDhApiFullDataSource
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
@@ -73,7 +77,7 @@ public class FullDataSourceV2
public static final byte DATA_FORMAT_VERSION = 1;
public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("FullDataV2", false);
public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("FullDataV2");
@@ -116,7 +120,12 @@ public class FullDataSourceV2
public final LongArrayList[] dataPoints;
public boolean isEmpty;
public boolean applyToParent = false;
/** Will be null if we don't want to update this value in the DB */
@Nullable
public Boolean applyToParent = null;
/** Will be null if we don't want to update this value in the DB */
@Nullable
public Boolean applyToChildren = null;
/** should only be used by methods exposed via the DH API */
private boolean runApiChunkValidation = false;
@@ -127,7 +136,7 @@ public class FullDataSourceV2
// 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)
{
@@ -269,20 +278,88 @@ public class FullDataSourceV2
{
ListUtil.clearAndSetSize(this.columnWorldCompressionMode, WIDTH * WIDTH);
}
// the pooled arrays have all been set,
// the checkout object is no longer needed
this.pooledArraysCheckout = null;
}
//======//
// data //
// getters //
//======//
public LongArrayList get(int relX, int relZ) throws IndexOutOfBoundsException { return this.dataPoints[relativePosToIndex(relX, relZ)]; }
public LongArrayList get(int relX, int relZ) throws IndexOutOfBoundsException
{ return this.dataPoints[relativePosToIndex(relX, relZ)]; }
/**
* returns {@link FullDataPointUtil#EMPTY_DATA_POINT} if the given {@link DhBlockPos}
* is outside this data source's boundaries.
*/
public long getAtBlockPos(DhBlockPos blockPos)
{
DhLodPos requestedPos = new DhLodPos(LodUtil.BLOCK_DETAIL_LEVEL, blockPos.getX(), blockPos.getZ());
// 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.get(relativePos.x, relativePos.z);
if (dataColumn == null)
{
return FullDataPointUtil.EMPTY_DATA_POINT;
}
// search for a datapoint that contains the given block y position
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 requestedY = blockPos.getY();
int bottomY = FullDataPointUtil.getBottomY(dataPoint) + this.levelMinY;
int height = FullDataPointUtil.getHeight(dataPoint);
int topY = bottomY + height;
// does this datapoint contain the requested Y position?
if (bottomY <= requestedY
&& requestedY < topY) // blockPositions start from the bottom of the block, thus "<=" for bottomY, just "<" for topY
{
return dataPoint;
}
}
return FullDataPointUtil.EMPTY_DATA_POINT;
}
//==========//
// updating //
//==========//
@Override
public boolean update(@NotNull FullDataSourceV2 inputDataSource, @Nullable IDhLevel level) { return this.update(inputDataSource); }
@@ -306,10 +383,47 @@ public class FullDataSourceV2
if (inputDetailLevel == thisDetailLevel)
{
dataChanged = this.updateFromSameDetailLevel(inputDataSource, remappedIds);
// same detail level, propagate parent/children update flags from input
if (this.applyToParent != null || inputDataSource.applyToParent != null)
{
this.applyToParent =
// copy over application flag if either are set to continue propagating
(BoolUtil.falseIfNull(this.applyToParent) || BoolUtil.falseIfNull(inputDataSource.applyToParent))
// don't propagate past the top of the tree
&& (DhSectionPos.getDetailLevel(this.pos) < AbstractDataSourceHandler.TOP_SECTION_DETAIL_LEVEL);
}
// null check to prevent setting a flag we don't want to save in the DB
if (this.applyToChildren != null || inputDataSource.applyToChildren != null)
{
this.applyToChildren =
(BoolUtil.falseIfNull(this.applyToChildren) || BoolUtil.falseIfNull(inputDataSource.applyToChildren))
// don't propagate past the bottom of the tree
&& (DhSectionPos.getDetailLevel(this.pos) > AbstractDataSourceHandler.MIN_SECTION_DETAIL_LEVEL);
}
}
else if (inputDetailLevel + 1 == thisDetailLevel)
{
dataChanged = this.updateFromOneBelowDetailLevel(inputDataSource, remappedIds);
// propagating up, parent will need changes
this.applyToParent =
dataChanged
&& (BoolUtil.falseIfNull(this.applyToParent) || BoolUtil.falseIfNull(inputDataSource.applyToParent))
&& (DhSectionPos.getDetailLevel(this.pos) < AbstractDataSourceHandler.TOP_SECTION_DETAIL_LEVEL);
}
else if (inputDetailLevel - 1 == thisDetailLevel)
{
dataChanged = this.downsampleFromOneAboveDetailLevel(inputDataSource, remappedIds);
// propagating down, children will need changes
this.applyToChildren =
dataChanged
&& (BoolUtil.falseIfNull(this.applyToChildren) || BoolUtil.falseIfNull(inputDataSource.applyToChildren))
&& (DhSectionPos.getDetailLevel(this.pos) > AbstractDataSourceHandler.MIN_SECTION_DETAIL_LEVEL);
}
else
{
@@ -317,12 +431,9 @@ public class FullDataSourceV2
// and would lead to edge cases that don't necessarily need to be supported
// (IE what do you do when the input is smaller than a single datapoint in the receiving data source?)
// instead it's better to just percolate the updates up
throw new UnsupportedOperationException("Unsupported data source update. Expected input detail level of ["+thisDetailLevel+"] or ["+(thisDetailLevel+1)+"], received detail level ["+inputDetailLevel+"].");
throw new UnsupportedOperationException("Unsupported data source update. Expected input detail level of ["+(thisDetailLevel-1)+"], ["+thisDetailLevel+"], or ["+(thisDetailLevel+1)+"], received detail level ["+inputDetailLevel+"].");
}
// determine if this data source should be applied to its parent
this.applyToParent = (dataChanged && DhSectionPos.getDetailLevel(this.pos) < AbstractDataSourceHandler.TOP_SECTION_DETAIL_LEVEL);
if (dataChanged)
{
// update the hash code
@@ -331,6 +442,7 @@ public class FullDataSourceV2
return dataChanged;
}
public boolean updateFromSameDetailLevel(FullDataSourceV2 inputDataSource, int[] remappedIds)
{
// both data sources should have the same detail level
@@ -353,9 +465,31 @@ public class FullDataSourceV2
{
byte thisGenState = this.columnGenerationSteps.getByte(index);
byte inputGenState = inputDataSource.columnGenerationSteps.getByte(index);
if (inputGenState != EDhApiWorldGenerationStep.EMPTY.value
&& thisGenState <= inputGenState)
// determine if this column should be updated
boolean genStateAllowsUpdating = false;
// if the input is downsampled, we only want to replace empty or downsampled values
if (inputGenState == EDhApiWorldGenerationStep.DOWN_SAMPLED.value
&&
(
thisGenState == EDhApiWorldGenerationStep.EMPTY.value
|| thisGenState == EDhApiWorldGenerationStep.DOWN_SAMPLED.value
))
{
genStateAllowsUpdating = true;
}
// if the input is any other non-empty value,
// replace anything that is less-complete
else if (inputGenState != EDhApiWorldGenerationStep.EMPTY.value
&& thisGenState <= inputGenState)
{
// don't apply less-complete generation data
genStateAllowsUpdating = true;
}
if (genStateAllowsUpdating)
{
// check if the data changed
if (this.dataPoints[index] == null)
@@ -835,6 +969,102 @@ public class FullDataSourceV2
return value;
}
/**
* Only downsamples into a given column if this data source doesn't
* already contain data in that column.
* This is done to prevent accidentally downsampling onto already present higher-detail data.
*/
public boolean downsampleFromOneAboveDetailLevel(FullDataSourceV2 inputDataSource, int[] remappedIds)
{
if (DhSectionPos.getDetailLevel(inputDataSource.pos) - 1 != DhSectionPos.getDetailLevel(this.pos))
{
throw new IllegalArgumentException("Input data source must be exactly 1 detail level above this data source. Expected [" + (DhSectionPos.getDetailLevel(this.pos) - 1) + "], received [" + DhSectionPos.getDetailLevel(inputDataSource.pos) + "].");
}
// input is one detail level higher (lower detail)
// so 1x1 input data points will be converted into 2x2 recipient data point
// determine where in this data source should be read from
// since the input is one detail level above this will be one of input position's 4 children
int minParentXPos = DhSectionPos.getX(DhSectionPos.getChildByIndex(inputDataSource.pos, 0));
int inputOffsetX = (DhSectionPos.getX(this.pos) == minParentXPos) ? 0 : (WIDTH / 2);
int minParentZPos = DhSectionPos.getZ(DhSectionPos.getChildByIndex(inputDataSource.pos, 0));
int inputOffsetZ = (DhSectionPos.getZ(this.pos) == minParentZPos) ? 0 : (WIDTH / 2);
// merge the input's data points
// into this data source's
boolean dataChanged = false;
for (int x = 0; x < WIDTH; x++)
{
for (int z = 0; z < WIDTH; z++)
{
// recipient index is 1-to-1
int recipientIndex = relativePosToIndex(x, z);
int inputX = (x / 2) + inputOffsetX;
int inputZ = (z / 2) + inputOffsetZ;
int inputIndex = relativePosToIndex(inputX, inputZ);
// world gen //
// a separate generation step needs to be used so can replace
// this data with higher-quality data when it is available
byte inputGenStep = EDhApiWorldGenerationStep.DOWN_SAMPLED.value;
this.columnGenerationSteps.set(recipientIndex, inputGenStep);
// world compression //
byte worldCompressionMode = inputDataSource.columnWorldCompressionMode.getByte(recipientIndex);
this.columnWorldCompressionMode.set(recipientIndex, worldCompressionMode);
// data points //
// check if this column should be downsampled
boolean downSampleColumn;
if (this.dataPoints[recipientIndex] == null)
{
downSampleColumn = true;
}
else
{
downSampleColumn = true; // assume empty until we find non-empty data
for (long dataPoint : this.dataPoints[recipientIndex])
{
if (dataPoint != FullDataPointUtil.EMPTY_DATA_POINT)
{
downSampleColumn = false;
break;
}
}
}
if (downSampleColumn)
{
LongArrayList inputDataArray = inputDataSource.dataPoints[inputIndex];
this.dataPoints[recipientIndex] = inputDataArray;
this.remapDataColumn(recipientIndex, remappedIds);
if (RUN_DATA_ORDER_VALIDATION)
{
throwIfDataColumnInWrongOrder(inputDataSource.pos, this.dataPoints[recipientIndex]);
}
dataChanged = true;
}
this.isEmpty = false;
}
}
return dataChanged;
}
//================//
@@ -866,6 +1096,11 @@ public class FullDataSourceV2
*/
public static void throwIfDataColumnInWrongOrder(long pos, LongArrayList dataArray) throws IllegalStateException
{
if (dataArray.size() < 2)
{
return;
}
long firstDataPoint = dataArray.getLong(0);
int firstBottomY = FullDataPointUtil.getBottomY(firstDataPoint);
@@ -884,6 +1119,11 @@ public class FullDataSourceV2
*/
private static void ensureDataColumnOrder(LongArrayList dataColumn)
{
if (dataColumn.size() < 2)
{
return;
}
long firstDataPoint = dataColumn.getLong(0);
int firstBottomY = FullDataPointUtil.getBottomY(firstDataPoint);
@@ -911,7 +1151,7 @@ public class FullDataSourceV2
//=====================//
@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); }
@@ -972,7 +1212,7 @@ public class FullDataSourceV2
LongArrayList packedDataPoints = LodDataBuilder.convertApiDataPointListToPackedLongArray(columnDataPoints, this, 0);
// TODO there should be an "unknown" compression and generation step, or be defined via the datapoints
this.setSingleColumn(packedDataPoints, relX, relZ, EDhApiWorldGenerationStep.LIGHT, EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS);
this.setSingleColumn(packedDataPoints, relX, relZ, EDhApiWorldGenerationStep.SURFACE, EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS);
return columnDataPoints;
}
@@ -1001,6 +1241,15 @@ public class FullDataSourceV2
//============//
// unit tests //
//============//
public PhantomArrayListCheckout getPhantomArrayCheckoutForUnitTesting()
{ return this.pooledArraysCheckout; }
//================//
// base overrides //
//================//
@@ -0,0 +1,96 @@
package com.seibel.distanthorizons.core.dataObjects.render;
import com.google.common.cache.Cache;
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,20 +19,13 @@
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.pooling.PhantomArrayListParent;
import com.seibel.distanthorizons.core.pooling.AbstractPhantomArrayList;
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.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.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.RenderDataPointUtil;
@@ -46,9 +39,7 @@ import java.util.concurrent.atomic.AtomicLong;
*
* @see RenderDataPointUtil
*/
public class ColumnRenderSource
extends PhantomArrayListParent
implements IDataSource<IDhClientLevel>
public class ColumnRenderSource extends AbstractPhantomArrayList
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
@@ -134,80 +125,12 @@ public class ColumnRenderSource
//=============//
// 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;
}
//=====================//
// data helper methods //
//=====================//
public Long getPos() { return this.pos; }
@Override
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); }
@@ -41,8 +41,6 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
/**
* Used to populate the buffers in a {@link ColumnRenderSource} object.
@@ -286,7 +284,7 @@ public class ColumnRenderBufferBuilder
}// for z
}// for x
quadBuilder.finalizeData();
quadBuilder.mergeQuads();
}
private static void addLodToBuffer(
IDhClientLevel clientLevel,
@@ -22,6 +22,7 @@ package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import com.seibel.distanthorizons.api.enums.config.EDhApiGrassSideRendering;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiBlockMaterial;
@@ -172,19 +173,6 @@ public class LodQuadBuilder
BufferQuad quad = new BufferQuad(minX, maxY, minZ, widthEastWest, widthNorthSouthOrUpDown, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.UP);
boolean isTransparent = (this.doTransparency && ColorUtil.getAlpha(color) < 255);
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;
}
quadList.add(quad);
}
@@ -193,14 +181,6 @@ public class LodQuadBuilder
BufferQuad quad = new BufferQuad(x, y, z, width, wz, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.DOWN);
ArrayList<BufferQuad> qs = (doTransparency && ColorUtil.getAlpha(color) < 255)
? 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 +190,6 @@ public class LodQuadBuilder
// data finalizing //
//=================//
/** runs any final data cleanup, merging, etc. */
public void finalizeData() { this.mergeQuads(); }
/** Uses Greedy meshing to merge this builder's Quads. */
public void mergeQuads()
{
@@ -251,7 +228,9 @@ public class LodQuadBuilder
private static long mergeQuadsInternal(ArrayList<BufferQuad>[] list, int directionIndex, BufferMergeDirectionEnum mergeDirection)
{
if (list[directionIndex].size() <= 1)
{
return 0;
}
list[directionIndex].sort((objOne, objTwo) -> objOne.compare(objTwo, mergeDirection));
@@ -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.columnViews.ColumnArrayView;
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.pooling.PhantomArrayListCheckout;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListPool;
@@ -37,6 +36,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
@@ -66,13 +66,13 @@ public class FullDataToRenderDataTransformer
//==============================//
@Nullable
public static ColumnRenderSource transformFullDataToRenderSource(@Nullable FullDataSourceV2 fullDataSource, @Nullable IDhClientLevel level)
public static ColumnRenderSource transformFullDataToRenderSource(@Nullable FullDataSourceV2 fullDataSource, @Nullable IClientLevelWrapper levelWrapper)
{
if (fullDataSource == 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
return null;
@@ -81,7 +81,7 @@ public class FullDataToRenderDataTransformer
try
{
return transformCompleteFullDataToColumnData(level, fullDataSource);
return transformCompleteFullDataToColumnData(levelWrapper, fullDataSource);
}
catch (InterruptedException e)
{
@@ -102,7 +102,7 @@ public class FullDataToRenderDataTransformer
* @throws InterruptedException Can be caused by interrupting the thread upstream.
* Generally thrown if the method is running after the client leaves the current world.
*/
private static ColumnRenderSource transformCompleteFullDataToColumnData(IDhClientLevel level, FullDataSourceV2 fullDataSource) throws InterruptedException
private static ColumnRenderSource transformCompleteFullDataToColumnData(IClientLevelWrapper levelWrapper, FullDataSourceV2 fullDataSource) throws InterruptedException
{
final long pos = fullDataSource.getPos();
final byte dataDetail = fullDataSource.getDataDetailLevel();
@@ -111,7 +111,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)
{
return columnSource;
@@ -121,9 +121,9 @@ public class FullDataToRenderDataTransformer
int baseX = DhSectionPos.getMinCornerBlockX(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();
@@ -131,7 +131,7 @@ public class FullDataToRenderDataTransformer
LongArrayList dataColumn = fullDataSource.get(x, z);
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
baseX + BitShiftUtil.pow(x,dataDetail), baseZ + BitShiftUtil.pow(z,dataDetail),
columnArrayView, dataColumn);
@@ -139,19 +139,20 @@ public class FullDataToRenderDataTransformer
}
columnSource.fillDebugFlag(0, 0, ColumnRenderSource.SECTION_SIZE, ColumnRenderSource.SECTION_SIZE, ColumnRenderSource.DebugSourceFlag.FULL);
return columnSource;
}
/** Updates the given {@link ColumnArrayView} to match the incoming Full data {@link LongArrayList} */
public static void updateOrReplaceRenderDataViewColumnWithFullDataColumn(
IDhClientLevel level,
FullDataPointIdMap fullDataMapping, int blockX, int blockZ,
IClientLevelWrapper levelWrapper,
FullDataSourceV2 fullDataSource, int blockX, int blockZ,
ColumnArrayView columnArrayView,
LongArrayList fullDataColumn)
{
// we can't do anything if the full data is missing or empty
if (fullDataColumn == null || fullDataColumn.size() == 0)
if (fullDataColumn == null
|| fullDataColumn.size() == 0)
{
return;
}
@@ -160,7 +161,7 @@ public class FullDataToRenderDataTransformer
if (fullDataLength <= columnArrayView.verticalSize())
{
// Directly use the arrayView since it fits.
setRenderColumnView(level, fullDataMapping, blockX, blockZ, columnArrayView, fullDataColumn);
setRenderColumnView(levelWrapper, fullDataSource, blockX, blockZ, columnArrayView, fullDataColumn);
}
else
{
@@ -171,7 +172,7 @@ public class FullDataToRenderDataTransformer
{
// expand the ColumnArrayView to fit the new larger max vertical size
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);
}
finally
@@ -181,7 +182,7 @@ public class FullDataToRenderDataTransformer
}
}
private static void setRenderColumnView(
IDhClientLevel level, FullDataPointIdMap fullDataMapping,
IClientLevelWrapper levelWrapper, FullDataSourceV2 fullDataSource,
int blockX, int blockZ,
ColumnArrayView renderColumnData, LongArrayList fullColumnData)
{
@@ -192,18 +193,18 @@ public class FullDataToRenderDataTransformer
boolean ignoreNonCollidingBlocks = (Config.Client.Advanced.Graphics.Quality.blocksToIgnore.get() == EDhApiBlocksToAvoid.NON_COLLIDING);
boolean colorBelowWithAvoidedBlocks = Config.Client.Advanced.Graphics.Quality.tintWithAvoidedBlocks.get();
HashSet<IBlockStateWrapper> blockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredBlocks(level.getLevelWrapper());
HashSet<IBlockStateWrapper> caveBlockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredCaveBlocks(level.getLevelWrapper());
HashSet<IBlockStateWrapper> blockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredBlocks(levelWrapper);
HashSet<IBlockStateWrapper> caveBlockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredCaveBlocks(levelWrapper);
int caveCullingMaxY = Config.Client.Advanced.Graphics.Culling.caveCullingHeight.get() - level.getMinY();
int caveCullingMaxY = Config.Client.Advanced.Graphics.Culling.caveCullingHeight.get() - levelWrapper.getMinHeight();
boolean caveCullingEnabled =
Config.Client.Advanced.Graphics.Culling.enableCaveCulling.get()
&& (
// 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 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;
@@ -222,6 +223,8 @@ public class FullDataToRenderDataTransformer
// convert full data to render data //
//==================================//
FullDataPointIdMap fullDataMapping = fullDataSource.mapping;
DhBlockPosMutable mutableBlockPos = new DhBlockPosMutable(blockX, 0, blockZ);
// goes from the top down
@@ -236,7 +239,7 @@ public class FullDataToRenderDataTransformer
int blockLight = FullDataPointUtil.getBlockLight(fullData);
int skyLight = FullDataPointUtil.getSkyLight(fullData);
mutableBlockPos.setY(bottomY + level.getMinY());
mutableBlockPos.setY(bottomY + levelWrapper.getMinHeight());
IBiomeWrapper biome;
IBlockStateWrapper block;
@@ -250,7 +253,7 @@ public class FullDataToRenderDataTransformer
if (!brokenPos.contains(fullDataMapping.getPos()))
{
brokenPos.add(fullDataMapping.getPos());
String levelId = level.getLevelWrapper().getDhIdentifier();
String levelId = levelWrapper.getDhIdentifier();
LOGGER.warn("Unable to get data point with id ["+id+"] " +
"(Max possible ID: ["+fullDataMapping.getMaxValidId()+"]) " +
"for pos ["+fullDataMapping.getPos()+"] in level ["+levelId+"]. " +
@@ -270,7 +273,7 @@ public class FullDataToRenderDataTransformer
//====================//
boolean ignoreBlock = blockStatesToIgnore.contains(block);
boolean caveBlock = caveBlockStatesToIgnore.contains(block);
boolean caveBlock = caveBlockStatesToIgnore.contains(block); // TODO caves should also ignore transparent/non-solid blocks (IE grass and plants) wthout each being defined
if (caveBlock)
{
if (caveCullingEnabled
@@ -320,11 +323,14 @@ public class FullDataToRenderDataTransformer
//=======================//
if (ignoreNonCollidingBlocks
&& !block.isSolid() && !block.isLiquid() && block.getOpacity() != LodUtil.BLOCK_FULLY_OPAQUE)
&& !block.isSolid()
&& !block.isLiquid()
&& block.getOpacity() != LodUtil.BLOCK_FULLY_OPAQUE)
{
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
// this prevents issues if grass is transparent
if (ColorUtil.getAlpha(tempColor) != 0)
@@ -344,7 +350,7 @@ public class FullDataToRenderDataTransformer
if (colorToApplyToNextBlock == -1)
{
// use this block's color
color = level.computeBaseColor(mutableBlockPos, biome, block);
color = levelWrapper.getBlockColor(mutableBlockPos, biome, fullDataSource, block);
}
else
{
@@ -24,15 +24,15 @@ import java.util.List;
import com.seibel.distanthorizons.api.enums.config.EDhApiWorldCompressionMode;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep;
import com.seibel.distanthorizons.api.interfaces.block.IDhApiBiomeWrapper;
import com.seibel.distanthorizons.api.interfaces.block.IDhApiBlockStateWrapper;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiChunkProcessingEvent;
import com.seibel.distanthorizons.api.objects.data.DhApiChunk;
import com.seibel.distanthorizons.api.objects.data.DhApiTerrainDataPoint;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPosMutable;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.util.FullDataPointUtil;
import com.seibel.distanthorizons.core.util.LodUtil;
@@ -43,6 +43,8 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IMutableBlockPosWrapper;
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.coreapi.DependencyInjection.ApiEventInjector;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
@@ -50,7 +52,8 @@ import org.jetbrains.annotations.Nullable;
public class LodDataBuilder
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
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;
@@ -60,12 +63,13 @@ public class LodDataBuilder
// 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
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 sectionPosZ = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getZ());
@@ -73,52 +77,38 @@ public class LodDataBuilder
FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos);
dataSource.isEmpty = false;
// chunk updates always propagate up
dataSource.applyToParent = true;
// compute the chunk dataSource offset
// this offset is used to determine where in the dataSource this chunk's data should go
int chunkOffsetX = chunkWrapper.getChunkPos().getX();
if (chunkWrapper.getChunkPos().getX() < 0)
{
// expected offset positions:
// chunkPos -> offset
// 5 -> 1
// 4 -> 0 ---
// 3 -> 3
// 2 -> 2
// 1 -> 1
// 0 -> 0 ===
// -1 -> 3
// -2 -> 2
// -3 -> 1
// -4 -> 0 ---
// -5 -> 3
chunkOffsetX = ((chunkOffsetX) % FullDataSourceV2.NUMB_OF_CHUNKS_WIDE);
if (chunkOffsetX != 0)
{
chunkOffsetX += FullDataSourceV2.NUMB_OF_CHUNKS_WIDE;
}
}
else
{
chunkOffsetX %= FullDataSourceV2.NUMB_OF_CHUNKS_WIDE;
}
chunkOffsetX *= LodUtil.CHUNK_WIDTH;
int chunkOffsetZ = chunkWrapper.getChunkPos().getZ();
if (chunkWrapper.getChunkPos().getZ() < 0)
{
chunkOffsetZ = ((chunkOffsetZ) % FullDataSourceV2.NUMB_OF_CHUNKS_WIDE);
if (chunkOffsetZ != 0)
{
chunkOffsetZ += FullDataSourceV2.NUMB_OF_CHUNKS_WIDE;
}
}
else
{
chunkOffsetZ %= FullDataSourceV2.NUMB_OF_CHUNKS_WIDE;
}
// expected offset positions:
// chunkPos -> offset
// 5 -> 1
// 4 -> 0 ---
// 3 -> 3
// 2 -> 2
// 1 -> 1
// 0 -> 0 ===
// -1 -> 3
// -2 -> 2
// -3 -> 1
// -4 -> 0 ---
// -5 -> 3
// Fast modulo calculation using bitwise AND since NUMB_OF_CHUNKS_WIDE is a power of 2 (4)
// For any number n: n & (2^k - 1) is equivalent to Math.floorMod(n, 2^k)
// Original: Math.floorMod(x, 4) - Handles negative numbers, gives non-negative result in range [0,3]
// Bitwise: x & (4-1) - Also gives non-negative result in range [0,3]
// Example: -5 & 3 = 3, which equals Math.floorMod(-5, 4) = 3
int chunkOffsetX = chunkWrapper.getChunkPos().getX() & (FullDataSourceV2.NUMB_OF_CHUNKS_WIDE - 1);
int chunkOffsetZ = chunkWrapper.getChunkPos().getZ() & (FullDataSourceV2.NUMB_OF_CHUNKS_WIDE - 1);
// Convert from chunk coordinates to block coordinates
chunkOffsetX *= LodUtil.CHUNK_WIDTH;
chunkOffsetZ *= LodUtil.CHUNK_WIDTH;
@@ -135,50 +125,54 @@ public class LodDataBuilder
IMutableBlockPosWrapper mcBlockPos = chunkWrapper.getMutableBlockPosWrapper();
IBlockStateWrapper previousBlockState = null;
DhApiChunkProcessingEvent.EventParam mutableChunkProcessedEventParam
= new DhApiChunkProcessingEvent.EventParam(levelWrapper, chunkPosX, chunkPosZ);
int minBuildHeight = chunkWrapper.getMinNonEmptyHeight();
int exclusiveMaxBuildHeight = chunkWrapper.getExclusiveMaxBuildHeight();
int inclusiveMinBuildHeight = chunkWrapper.getInclusiveMinBuildHeight();
int dataCapacity = chunkWrapper.getHeight() / 4;
for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++)
{
for (int relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++)
{
LongArrayList longs = dataSource.get(
relBlockX + chunkOffsetX,
relBlockZ + chunkOffsetZ);
// Calculate column position
int columnX = relBlockX + chunkOffsetX;
int columnZ = relBlockZ + chunkOffsetZ;
// Get column data
LongArrayList longs = dataSource.get(columnX, columnZ);
if (longs == null)
{
longs = new LongArrayList(chunkWrapper.getHeight() / 4);
longs = new LongArrayList(dataCapacity);
}
else
{
longs.clear();
}
int lastY = chunkWrapper.getExclusiveMaxBuildHeight();
IBiomeWrapper biome = chunkWrapper.getBiome(relBlockX, lastY, relBlockZ);
IBlockStateWrapper blockState = AIR;
int mappedId = dataSource.mapping.addIfNotPresentAndGetId(biome, blockState);
int lastY = exclusiveMaxBuildHeight;
IBiomeWrapper currentBiome = chunkWrapper.getBiome(relBlockX, lastY, relBlockZ);
IBlockStateWrapper currentBlockState = AIR;
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;
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;
}
// Get the maximum height from both heightmaps
int y = Math.max(
// max between both heightmaps to account for solid invisible blocks (glass)
// and non-solid opaque blocks (at one point this was stairs, not sure what would fit this now)
chunkWrapper.getLightBlockingHeightMapValue(relBlockX, relBlockZ),
chunkWrapper.getSolidHeightMapValue(relBlockX, relBlockZ)
);
// determine the starting Y Pos
int y = chunkWrapper.getLightBlockingHeightMapValue(relBlockX, relBlockZ);
// go up until we reach open air or the world limit
// Go up until we reach open air or the world limit
IBlockStateWrapper topBlockState = previousBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ, mcBlockPos, previousBlockState);
while (!topBlockState.isAir() && y < chunkWrapper.getExclusiveMaxBuildHeight())
while (!topBlockState.isAir()
&& y < exclusiveMaxBuildHeight)
{
try
{
@@ -191,7 +185,7 @@ public class LodDataBuilder
{
if (!getTopErrorLogged)
{
LOGGER.warn("Unexpected issue in LodDataBuilder, future errors won't be logged. Chunk [" + chunkWrapper.getChunkPos() + "] with max height: [" + chunkWrapper.getExclusiveMaxBuildHeight() + "] had issue getting block at pos [" + relBlockX + "," + y + "," + relBlockZ + "] error: " + e.getMessage(), e);
LOGGER.warn("Unexpected issue in LodDataBuilder, future errors won't be logged. Chunk [" + chunkWrapper.getChunkPos() + "] with max height: [" + exclusiveMaxBuildHeight + "] had issue getting block at pos [" + relBlockX + "," + y + "," + relBlockZ + "] error: " + e.getMessage(), e);
getTopErrorLogged = true;
}
@@ -200,7 +194,8 @@ public class LodDataBuilder
}
}
// Process blocks from top to bottom
boolean forceSingleBlock = false;
for (; y >= minBuildHeight; y--)
{
IBiomeWrapper newBiome = chunkWrapper.getBiome(relBlockX, y, relBlockZ);
@@ -208,30 +203,62 @@ public class LodDataBuilder
byte newBlockLight = (byte) chunkWrapper.getDhBlockLight(relBlockX, y + 1, relBlockZ);
byte newSkyLight = (byte) chunkWrapper.getDhSkyLight(relBlockX, y + 1, relBlockZ);
// save the biome/block change
if (!newBiome.equals(biome) || !newBlockState.equals(blockState))
// Save the biome/block change if different from previous
if (!newBiome.equals(currentBiome)
|| !newBlockState.equals(currentBlockState)
|| forceSingleBlock)
{
longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - chunkWrapper.getInclusiveMinBuildHeight(), blockLight, skyLight));
biome = newBiome;
blockState = newBlockState;
// if the previous block potentially colors this block
// make this block a single entry, aka add the next block even if it is the same
// 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;
skyLight = newSkyLight;
lastY = y;
}
}
longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - chunkWrapper.getInclusiveMinBuildHeight(), blockLight, skyLight));
dataSource.setSingleColumn(longs,
relBlockX + chunkOffsetX,
relBlockZ + chunkOffsetZ,
EDhApiWorldGenerationStep.LIGHT,
worldCompressionMode);
// Add the final data point
longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - inclusiveMinBuildHeight, blockLight, skyLight));
// Set the column in the data source
dataSource.setSingleColumn(longs, columnX, columnZ, EDhApiWorldGenerationStep.LIGHT, worldCompressionMode);
}
}
if (ignoreHiddenBlocks)
if (ignoreHiddenBlocks)
{
cullHiddenBlocks(dataSource, chunkOffsetX, chunkOffsetZ);
}
@@ -268,16 +295,16 @@ public class LodDataBuilder
{
long currentPoint = centerColumn.getLong(centerIndex);
// translucent data points are not eligible to be culled.
// Translucent data points are not eligible to be culled.
if (isTranslucent(dataSource, currentPoint))
{
continue;
}
// the top segment should never be culled.
if (centerIndex == 0
|| isTranslucent(dataSource, centerColumn.getLong(centerIndex - 1))
)
if (centerIndex == 0
|| isTranslucent(dataSource, centerColumn.getLong(centerIndex - 1))
)
{
continue;
}
@@ -285,9 +312,15 @@ public class LodDataBuilder
// the bottom segment can sometimes be culled.
// assume it will not be seen from below,
// because this would imply the player is in the void.
if (centerIndex + 1 < centerColumn.size()
&& isTranslucent(dataSource, centerColumn.getLong(centerIndex + 1))
)
if (centerIndex + 1 < centerColumn.size()
&& isTranslucent(dataSource, centerColumn.getLong(centerIndex + 1))
)
{
continue;
}
// the lowest/bedrock segment should not be culled
if (centerIndex + 1 == centerColumn.size())
{
continue;
}
@@ -320,9 +353,11 @@ public class LodDataBuilder
continue;
}
// current point is fully surrounded. remove it.
// Current point is fully surrounded. remove it.
centerColumn.removeLong(centerIndex);
// make the above data point cover the area that the current point used to occupy.
// Make the above data point cover the area that the current point used to occupy.
// The element that was at `centerIndex - 1` is still at that position even after removal of centerIndex.
long above = centerColumn.getLong(centerIndex - 1);
above = FullDataPointUtil.setBottomY(above, FullDataPointUtil.getBottomY(currentPoint));
above = FullDataPointUtil.setHeight(above, FullDataPointUtil.getHeight(currentPoint) + FullDataPointUtil.getHeight(above));
@@ -331,31 +366,31 @@ public class LodDataBuilder
}
}
}
/**
checks if centerPoint is "covered" by opaque data points in adjacentColumn.
centerPoint counts as covered if, and only if, for all Y levels in its height range,
there exists an opaque data point in adjacentColumn which overlaps with that Y level.
@param source used to lookup blocks (and their opacities) based on their IDs.
@param centerPoint the point being checked to see if it's fully covered.
@param adjacentColumn the data points which might cover centerPoint.
@param adjacentIndex the starting index in adjacentColumn to start scanning at.
indices greater than adjacentIndex have already been checked and confirmed to
not overlap or only overlap partially with centerPoint's Y range.
@return if centerPoint is covered, returns the index of the segment which finishes covering it.
the start of the covering may be a smaller index. in this case, the returned index may be used
as the adjacentIndex provided to this method on the next iteration which yields a new centerPoint.
if centerPoint is NOT covered, returns the bitwise negation of the index of the
segment which did not cover it. this guarantees that the returned value is negative.
the caller should check for negative return values and manually un-negate them to proceed with the loop.
in other words, this function returns the index of the next adjacent data
point to use in the loop, AND a boolean indicating whether or not the
centerPoint is covered; both are packed into the same int, and returned.
*/
checks if centerPoint is "covered" by opaque data points in adjacentColumn.
centerPoint counts as covered if, and only if, for all Y levels in its height range,
there exists an opaque data point in adjacentColumn which overlaps with that Y level.
@param source used to lookup blocks (and their opacities) based on their IDs.
@param centerPoint the point being checked to see if it's fully covered.
@param adjacentColumn the data points which might cover centerPoint.
@param adjacentIndex the starting index in adjacentColumn to start scanning at.
indices greater than adjacentIndex have already been checked and confirmed to
not overlap or only overlap partially with centerPoint's Y range.
@return if centerPoint is covered, returns the index of the segment which finishes covering it.
the start of the covering may be a smaller index. in this case, the returned index may be used
as the adjacentIndex provided to this method on the next iteration which yields a new centerPoint.
if centerPoint is NOT covered, returns the bitwise negation of the index of the
segment which did not cover it. this guarantees that the returned value is negative.
the caller should check for negative return values and manually un-negate them to proceed with the loop.
in other words, this function returns the index of the next adjacent data
point to use in the loop, AND a boolean indicating whether or not the
centerPoint is covered; both are packed into the same int, and returned.
*/
private static int checkOcclusion(FullDataSourceV2 source, long centerPoint, LongArrayList adjacentColumn, int adjacentIndex)
{
int bottomOfCenter = FullDataPointUtil.getBottomY(centerPoint);
@@ -380,12 +415,12 @@ public class LodDataBuilder
throw new LodUtil.AssertFailureException("Adjacent column ends before center column does.");
}
private static boolean isTranslucent(FullDataSourceV2 source, long point) {
return source.mapping.getBlockStateWrapper(FullDataPointUtil.getId(point)).getOpacity() < LodUtil.BLOCK_FULLY_OPAQUE;
}
/** @throws ClassCastException if an API user returns the wrong object type(s) */
public static FullDataSourceV2 createFromApiChunkData(DhApiChunk apiChunk, boolean runAdditionalValidation) throws ClassCastException, DataCorruptedException, IllegalArgumentException
@@ -395,9 +430,13 @@ public class LodDataBuilder
int sectionPosZ = getXOrZSectionPosFromChunkPos(apiChunk.chunkPosZ);
long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ);
// chunk relative block position in the data source
int relSourceBlockX = Math.floorMod(apiChunk.chunkPosX, 4) * LodUtil.CHUNK_WIDTH;
int relSourceBlockZ = Math.floorMod(apiChunk.chunkPosZ, 4) * LodUtil.CHUNK_WIDTH;
// Fast modulo calculation using bitwise AND since NUMB_OF_CHUNKS_WIDE is a power of 2 (4)
// For any number n: n & (2^k - 1) is equivalent to Math.floorMod(n, 2^k)
// Original: Math.floorMod(x, 4) - Handles negative numbers, gives non-negative result in range [0,3]
// Bitwise: x & (4-1) - Also gives non-negative result in range [0,3]
// Example: -5 & 3 = 3, which equals Math.floorMod(-5, 4) = 3
int relSourceBlockX = (apiChunk.chunkPosX & (FullDataSourceV2.NUMB_OF_CHUNKS_WIDE - 1)) * LodUtil.CHUNK_WIDTH;
int relSourceBlockZ = (apiChunk.chunkPosZ & (FullDataSourceV2.NUMB_OF_CHUNKS_WIDE - 1)) * LodUtil.CHUNK_WIDTH;
FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos);
for (int relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++)
@@ -416,8 +455,8 @@ public class LodDataBuilder
// TODO add the ability for API users to define a different compression mode
// or add a "unkown" compression mode
dataSource.setSingleColumn(
packedDataPoints,
relBlockX + relSourceBlockX, relBlockZ + relSourceBlockZ,
packedDataPoints,
relBlockX + relSourceBlockX, relBlockZ + relSourceBlockZ,
EDhApiWorldGenerationStep.LIGHT, EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS);
dataSource.isEmpty = false;
}
@@ -433,7 +472,7 @@ public class LodDataBuilder
/** @see FullDataPointUtil */
public static LongArrayList convertApiDataPointListToPackedLongArray(
@Nullable List<DhApiTerrainDataPoint> columnDataPoints, FullDataSourceV2 dataSource,
@Nullable List<DhApiTerrainDataPoint> columnDataPoints, FullDataSourceV2 dataSource,
int bottomYBlockPos) throws DataCorruptedException
{
// this null check does 2 nice things at the same time:
@@ -526,24 +565,24 @@ public class LodDataBuilder
}
// is there a gap between the last datapoint?
if (topYPos != lastBottomYPos
&& lastBottomYPos != Integer.MIN_VALUE)
&& lastBottomYPos != Integer.MIN_VALUE)
{
throw new IllegalArgumentException("DhApiTerrainDataPoint ["+i+"] has a gap between it and index ["+(i-1)+"]. Empty spaces should be filled by air, otherwise DH's downsampling won't calculate lighting correctly.");
}
lastBottomYPos = bottomYPos;
lastBottomYPos = bottomYPos;
}
}
//================//
// helper methods //
//================//
//==================//
// internal helpers //
//==================//
public static int getXOrZSectionPosFromChunkPos(int chunkXOrZPos)
private static int getXOrZSectionPosFromChunkPos(int chunkXOrZPos)
{
// get the section position
int sectionPos = chunkXOrZPos;
@@ -552,4 +591,6 @@ public class LodDataBuilder
return sectionPos;
}
}
@@ -14,7 +14,7 @@ import com.seibel.distanthorizons.core.sql.dto.IBaseDTO;
*/
public interface IDataSource<TDhLevel extends IDhLevel> extends IBaseDTO<Long>, AutoCloseable
{
Long getPos();
long getPos();
/** @return true if the data was changed */
boolean update(FullDataSourceV2 chunkData, TDhLevel level);
@@ -1,177 +0,0 @@
package com.seibel.distanthorizons.core.file.beacon;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.render.renderer.generic.BeaconRenderHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo;
import com.seibel.distanthorizons.core.util.KeyedLockContainer;
import com.seibel.distanthorizons.core.util.LodUtil;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
public class BeaconBeamDataHandler
{
private final BeaconBeamRepo beaconBeamRepo;
@Nullable
private BeaconRenderHandler beaconRenderHandler;
private final KeyedLockContainer<Long> updateLockContainer = new KeyedLockContainer<>();
//=============//
// constructor //
//=============//
public BeaconBeamDataHandler(@NotNull BeaconBeamRepo beaconBeamRepo, @Nullable GenericObjectRenderer renderer)
{
this.beaconBeamRepo = beaconBeamRepo;
if (renderer != null)
{
this.beaconRenderHandler = new BeaconRenderHandler(renderer);
}
}
//==========//
// updating //
//==========//
public void setBeaconBeamsForChunk(DhChunkPos chunkPos, List<BeaconBeamDTO> activeBeamList)
{
long sectionPos = DhSectionPos.encode(LodUtil.CHUNK_DETAIL_LEVEL, chunkPos.getX(), chunkPos.getZ());
this.setBeaconBeamsForPos(sectionPos, activeBeamList);
}
public void setBeaconBeamsForPos(long sectionPos, List<BeaconBeamDTO> activeBeamList)
{
// locked to prevent two threads from updating the same section at the same time
ReentrantLock lock = this.updateLockContainer.getLockForPos(sectionPos);
try
{
lock.lock();
HashSet<DhBlockPos> allPosSet = new HashSet<>();
// sort new beams
HashMap<DhBlockPos, BeaconBeamDTO> activeBeamByPos = new HashMap<>(activeBeamList.size());
for (BeaconBeamDTO beam : activeBeamList)
{
activeBeamByPos.put(beam.blockPos, beam);
allPosSet.add(beam.blockPos);
}
// get existing beams
List<BeaconBeamDTO> existingBeamList = this.beaconBeamRepo.getAllBeamsForPos(sectionPos);
HashMap<DhBlockPos, BeaconBeamDTO> existingBeamByPos = new HashMap<>(existingBeamList.size());
for (BeaconBeamDTO beam : existingBeamList)
{
existingBeamByPos.put(beam.blockPos, beam);
allPosSet.add(beam.blockPos);
}
for (DhBlockPos beaconPos : allPosSet)
{
if (!DhSectionPos.contains(sectionPos, beaconPos))
{
// don't update beacons outside the updated chunk
continue;
}
BeaconBeamDTO existingBeam = existingBeamByPos.get(beaconPos);
BeaconBeamDTO activeBeam = activeBeamByPos.get(beaconPos);
if (activeBeam != null)
{
if (existingBeam == null)
{
// new beam found, add to DB
this.beaconBeamRepo.save(activeBeam);
if (this.beaconRenderHandler != null)
{
this.beaconRenderHandler.startRenderingBeacon(activeBeam);
}
}
else
{
// beam still exists in chunk
if (!existingBeam.color.equals(activeBeam.color))
{
// beam colors were changed
this.beaconBeamRepo.save(activeBeam);
if (this.beaconRenderHandler != null)
{
this.beaconRenderHandler.updateBeaconColor(activeBeam);
}
}
}
}
else if (existingBeam != null)
{
// beam no longer exists at position, remove from DB
this.beaconBeamRepo.deleteWithKey(beaconPos);
if (this.beaconRenderHandler != null)
{
this.beaconRenderHandler.stopRenderingBeaconAtPos(beaconPos);
}
}
}
}
finally
{
lock.unlock();
}
}
//===================//
// loading/unloading //
//===================//
public void loadBeaconBeamsInPos(long pos)
{
if (this.beaconRenderHandler == null)
{
return;
}
// get all beams in pos
List<BeaconBeamDTO> existingBeamList = this.beaconBeamRepo.getAllBeamsForPos(pos);
for (BeaconBeamDTO newBeam : existingBeamList)
{
this.beaconRenderHandler.startRenderingBeacon(newBeam);
}
}
public void unloadBeaconBeamsInPos(long pos)
{
if (this.beaconRenderHandler == null)
{
return;
}
// get all beams in pos
List<BeaconBeamDTO> existingBeamList = this.beaconBeamRepo.getAllBeamsForPos(pos);
for (BeaconBeamDTO beam : existingBeamList)
{
this.beaconRenderHandler.stopRenderingBeaconAtPos(beam.blockPos);
}
}
}
@@ -1,9 +1,5 @@
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.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
@@ -14,6 +10,7 @@ import org.jetbrains.annotations.NotNull;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Set;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
@@ -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 */
protected final KeyedLockContainer<Long> saveLockContainer = new KeyedLockContainer<>();
@@ -60,16 +57,15 @@ public class DelayedFullDataSourceSaveCache implements AutoCloseable
public DelayedFullDataSourceSaveCache(@NotNull ISaveDataSourceFunc onSaveTimeoutAsyncFunc, int saveDelayInMs)
{
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.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));
}
@@ -83,61 +79,98 @@ public class DelayedFullDataSourceSaveCache implements AutoCloseable
* Writing into memory is done synchronously so inputDataSource can
* be closed after this method finishes.
*/
public void writeDataSourceToMemoryAndQueueSave(FullDataSourceV2 inputDataSource)
public void writeDataSourceToMemoryAndQueueSave(@NotNull FullDataSourceV2 inputDataSource)
{
long inputPos = inputDataSource.getPos();
ReentrantLock lock = this.saveLockContainer.getLockForPos(inputPos);
ReentrantLock lockForPos = this.saveLockContainer.getLockForPos(inputPos);
try
{
lock.lock();
lockForPos.lock();
FullDataSourceV2 memoryDataSource = this.dataSourceByPosition.getIfPresent(inputPos);
if (memoryDataSource == null)
FullDataSourceV2 memoryDataSource;
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.update(inputDataSource);
this.dataSourceByPosition.put(inputPos, memoryDataSource);
}
finally
{
lock.unlock();
}
}
public int getUnsavedCount() { return (int)this.dataSourceByPosition.size(); }
public void handleDataSourceRemoval(RemovalNotification<Long, FullDataSourceV2> removalNotification)
{
RemovalCause cause = removalNotification.getCause();
if (cause == RemovalCause.EXPIRED
|| cause == RemovalCause.COLLECTED
|| 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) ->
{
try
{
dataSource.close();
}
catch (Exception e)
{
LOGGER.error("Unable to close datasource ["+ DhSectionPos.toString(dataSource.getPos()) +"], removal cause: ["+cause+"], error: ["+e.getMessage()+"].", e);
}
return null;
});
pair = new DataSourceSavedTimePair(memoryDataSource);
this.dataSourceByPosition.put(inputPos, pair);
}
else
{
LOGGER.error("Unable to close null cached data source.");
memoryDataSource = pair.dataSource;
}
// write the new data into memory
memoryDataSource.update(inputDataSource);
// keep track of when the last time we saved something was
pair.updateLastWrittenTimestamp();
}
finally
{
lockForPos.unlock();
}
}
/** when this method is called the datasource should no longer be in the memory cache */
public void handleDataSourceRemoval(@NotNull FullDataSourceV2 removedDataSource)
{
this.onSaveTimeoutAsyncFunc.saveAsync(removedDataSource)
.handle((voidObj, throwable) ->
{
try
{
// if this close method is fired multiple times
// monoliths can appear due to concurrent writing to the
// backend arrays
removedDataSource.close();
}
catch (Exception e)
{
LOGGER.error("Unable to close datasource ["+ DhSectionPos.toString(removedDataSource.getPos()) +"], error: ["+e.getMessage()+"].", e);
}
return null;
});
}
//==============//
// List methods //
//==============//
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. */
public void cleanUp(boolean flushAll)
{
Enumeration<Long> keyIterator = this.dataSourceByPosition.keys();
while (keyIterator.hasMoreElements())
{
Long pos = keyIterator.nextElement();
ReentrantLock posLock = this.saveLockContainer.getLockForPos(pos);
try
{
posLock.lock();
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
{
posLock.unlock();
}
}
}
@@ -170,7 +203,7 @@ public class DelayedFullDataSourceSaveCache implements AutoCloseable
}
else
{
cache.dataSourceByPosition.cleanUp();
cache.cleanUp(false);
}
});
}
@@ -213,4 +246,36 @@ public class DelayedFullDataSourceSaveCache implements AutoCloseable
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);
}
}
}
@@ -9,6 +9,7 @@ import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV1DTO;
import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo;
import com.seibel.distanthorizons.core.sql.repo.FullDataSourceV1Repo;
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 it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.Logger;
@@ -21,7 +22,6 @@ import java.util.ArrayList;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.locks.ReentrantLock;
public class FullDataSourceProviderV1<TDhLevel extends IDhLevel>
@@ -79,7 +79,10 @@ public class FullDataSourceProviderV1<TDhLevel extends IDhLevel>
protected FullDataSourceV1 createDataSourceFromDto(FullDataSourceV1DTO dto) throws InterruptedException, IOException, DataCorruptedException
{
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;
}
@@ -68,9 +68,9 @@ public class FullDataSourceProviderV2
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 = 50;
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 final int MAX_UPDATE_TASK_COUNT = NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD * Config.Common.MultiThreading.numberOfThreads.get();
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;
@@ -103,7 +103,7 @@ public class FullDataSourceProviderV2
* Tracks which positions are currently being updated
* to prevent duplicate concurrent updates.
*/
public final Set<Long> parentUpdatingPosSet = ConcurrentHashMap.newKeySet();
public final Set<Long> updatingPosSet = ConcurrentHashMap.newKeySet();
// TODO only run thread if modifications happened recently
/**
@@ -225,106 +225,11 @@ public class FullDataSourceProviderV2
targetBlockPos = MC_CLIENT.getPlayerBlockPos();
}
// queue parent updates
if (executor.getQueueSize() < MAX_UPDATE_TASK_COUNT
&& this.parentUpdatingPosSet.size() < MAX_UPDATE_TASK_COUNT)
this.runParentUpdates(executor, targetBlockPos);
if (Config.Common.LodBuilding.Experimental.upsampleLowerDetailLodsToFillHoles.get())
{
// get the positions that need to be applied to their parents
LongArrayList parentUpdatePosList = this.repo.getPositionsToUpdate(targetBlockPos.getX(), targetBlockPos.getZ(), MAX_UPDATE_TASK_COUNT);
// 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.parentUpdatingPosSet.size() > MAX_UPDATE_TASK_COUNT
|| !this.parentUpdatingPosSet.add(parentUpdatePos))
{
break;
}
try
{
executor.execute(() ->
{
ReentrantLock parentWriteLock = this.updateLockProvider.getLock(parentUpdatePos);
boolean parentLocked = false;
try
{
//LOGGER.info("updating parent: "+parentUpdatePos);
// Locking the parent before the children should prevent deadlocks.
// TryLock is used instead of lock so this thread can handle a different update.
if (parentWriteLock.tryLock())
{
parentLocked = true;
this.lockedPosSet.add(parentUpdatePos);
// apply each child pos to the parent
for (Long childPos : updatePosByParentPos.get(parentUpdatePos))
{
ReentrantLock childReadLock = this.updateLockProvider.getLock(childPos);
try
{
childReadLock.lock();
this.lockedPosSet.add(childPos);
try (FullDataSourceV2 dataSource = this.get(childPos))
{
// can return null when the file handler is being shut down
if (dataSource != null)
{
this.updateDataSourceAtPos(parentUpdatePos, dataSource, false);
this.repo.setApplyToParent(childPos, false);
}
}
}
catch (Exception e)
{
LOGGER.error("Unexpected in update for parent pos: [" + DhSectionPos.toString(parentUpdatePos) + "] Error: [" + e.getMessage() + "].", e);
}
finally
{
childReadLock.unlock();
this.lockedPosSet.remove(childPos);
}
}
}
}
finally
{
if (parentLocked)
{
parentWriteLock.unlock();
this.lockedPosSet.remove(parentUpdatePos);
}
this.parentUpdatingPosSet.remove(parentUpdatePos);
}
});
}
catch (RejectedExecutionException ignore)
{ /* the executor was shut down, it should be back up shortly and able to accept new jobs */ }
catch (Exception e)
{
this.parentUpdatingPosSet.remove(parentUpdatePos);
throw e;
}
}
this.runChildUpdates(executor, targetBlockPos);
}
}
@@ -340,6 +245,248 @@ public class FullDataSourceProviderV2
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;
}
}
}
}
@@ -645,7 +792,7 @@ public class FullDataSourceProviderV2
this.queuedUpdateCountsByPos
.forEach((pos, updateCountRef) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 80f + (updateCountRef.get() * 16f), 0.20f, Color.WHITE)); });
this.parentUpdatingPosSet
this.updatingPosSet
.forEach((pos) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 80f, 0.20f, Color.MAGENTA)); });
}
@@ -34,7 +34,6 @@ import com.seibel.distanthorizons.core.pooling.PhantomArrayListCheckout;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListPool;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
@@ -88,8 +87,20 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// event listeners //
//=================//
public void addWorldGenCompleteListener(IOnWorldGenCompleteListener listener) { this.onWorldGenTaskCompleteListeners.add(listener); }
public void removeWorldGenCompleteListener(IOnWorldGenCompleteListener listener) { this.onWorldGenTaskCompleteListeners.remove(listener); }
public void addWorldGenCompleteListener(IOnWorldGenCompleteListener listener)
{
synchronized (this.onWorldGenTaskCompleteListeners)
{
this.onWorldGenTaskCompleteListeners.add(listener);
}
}
public void removeWorldGenCompleteListener(IOnWorldGenCompleteListener listener)
{
synchronized (this.onWorldGenTaskCompleteListeners)
{
this.onWorldGenTaskCompleteListeners.remove(listener);
}
}
@@ -129,10 +140,14 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// TODO only fire after the section has finished generated or once every X seconds
private void fireOnGenPosSuccessListeners(long pos)
{
// fire the event listeners
for (IOnWorldGenCompleteListener listener : this.onWorldGenTaskCompleteListeners)
// synchronized to prevent a rare issue where the world generator is being shut down while this listener is firing
synchronized (this.onWorldGenTaskCompleteListeners)
{
listener.onWorldGenTaskComplete(pos);
// fire the event listeners
for (IOnWorldGenCompleteListener listener : this.onWorldGenTaskCompleteListeners)
{
listener.onWorldGenTaskComplete(pos);
}
}
}
@@ -195,16 +210,18 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
}
PriorityTaskPicker.Executor updateExecutor = ThreadPoolUtil.getUpdatePropagatorExecutor();
if (updateExecutor == null || updateExecutor.getQueueSize() >= MAX_UPDATE_TASK_COUNT / 2)
PriorityTaskPicker.Executor renderLoadExecutor = ThreadPoolUtil.getRenderLoadingExecutor();
if (renderLoadExecutor == null
|| renderLoadExecutor.getQueueSize() >= getMaxUpdateTaskCount() / 2)
{
// don't queue additional world gen requests if the updater is behind
// 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 fileExecutor = ThreadPoolUtil.getFileHandlerExecutor();
if (fileExecutor == null || fileExecutor.getQueueSize() >= MAX_UPDATE_TASK_COUNT / 2)
PriorityTaskPicker.Executor fileHandlerExecutor = ThreadPoolUtil.getFileHandlerExecutor();
if (fileHandlerExecutor == null
|| fileHandlerExecutor.getQueueSize() >= getMaxUpdateTaskCount() / 2)
{
// don't queue additional world gen requests if the file handler is overwhelmed,
// otherwise LODs may not load in properly
@@ -229,9 +246,15 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// don't queue additional world gen requests if there are
// a lot of data sources in memory
// (this is done to prevent infinite memory growth)
// clear out the data sources that are in memory so
// we can start queuing new world gen tasks
this.delayedFullDataSourceSaveCache.flush();
return false;
}
int availableTaskSlots = maxWorldGenQueueCount - worldGenQueue.getWaitingTaskCount();
if (availableTaskSlots <= 0)
{
@@ -298,7 +321,12 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
public boolean isFullyGenerated(ByteArrayList columnGenerationSteps)
{
return IntStream.range(0, columnGenerationSteps.size())
.noneMatch(i -> columnGenerationSteps.getByte(i) == EDhApiWorldGenerationStep.EMPTY.value);
.noneMatch(i ->
{
byte value = columnGenerationSteps.getByte(i);
return value == EDhApiWorldGenerationStep.EMPTY.value
|| value == EDhApiWorldGenerationStep.DOWN_SAMPLED.value;
});
}
public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Generated Provider");
@@ -328,7 +356,8 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// check if any positions are ungenerated
for (int i = 0; i < columnGenStepArray.size(); i++)
{
if (columnGenStepArray.getByte(i) == EDhApiWorldGenerationStep.EMPTY.value)
if (columnGenStepArray.getByte(i) == EDhApiWorldGenerationStep.EMPTY.value
|| columnGenStepArray.getByte(i) == EDhApiWorldGenerationStep.DOWN_SAMPLED.value)
{
positionFullyGenerated = false;
break;
@@ -393,7 +422,8 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
}
}
if (currentMinWorldGenStep == EDhApiWorldGenerationStep.EMPTY)
if (currentMinWorldGenStep == EDhApiWorldGenerationStep.EMPTY
|| currentMinWorldGenStep == EDhApiWorldGenerationStep.DOWN_SAMPLED)
{
// queue the task
break checkWorldGenLoop;
@@ -402,7 +432,8 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
}
}
if (currentMinWorldGenStep != EDhApiWorldGenerationStep.EMPTY)
if (currentMinWorldGenStep != EDhApiWorldGenerationStep.EMPTY
&& currentMinWorldGenStep != EDhApiWorldGenerationStep.DOWN_SAMPLED)
{
// no world gen needed for this position
return;
@@ -455,6 +486,23 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
};
}
@Override
public CompletableFuture<Boolean> shouldGenerateSplitChild(long pos)
{
return GeneratedFullDataSourceProvider.this.getAsync(pos).thenApply(fullDataSource ->
{
//noinspection TryFinallyCanBeTryWithResources
try
{
return !GeneratedFullDataSourceProvider.this.isFullyGenerated(fullDataSource.columnGenerationSteps);
}
finally
{
fullDataSource.close();
}
});
}
}
private CompletableFuture<Void> onDataSourceSaveAsync(FullDataSourceV2 fullDataSource)
{
@@ -99,7 +99,10 @@ public class RemoteFullDataSourceProvider extends GeneratedFullDataSourceProvide
Long timestamp = this.getTimestampForPos(pos);
if (timestamp != null)
{
this.syncOnLoadRequestQueue.submitRequest(pos, timestamp, this::updateDataSource);
this.syncOnLoadRequestQueue.submitRequest(pos, timestamp, fullDataSource ->
{
this.updateDataSourceAsync(fullDataSource).whenComplete((result, throwable) -> fullDataSource.close());
});
}
return super.get(pos);
@@ -106,6 +106,19 @@ public class ClientOnlySaveStructure implements ISaveStructure
});
}
@Override
public File getPre23SaveFolder(ILevelWrapper levelWrapper)
{
// Allow API users to override the save folder
IDhApiSaveStructure saveStructureOverride = OverrideInjector.INSTANCE.get(IDhApiSaveStructure.class);
if (saveStructureOverride != null)
{
return this.getSaveFolder(levelWrapper);
}
return getSaveFolderByLevelId(levelWrapper.getDimensionType().getName());
}
//================//
@@ -34,5 +34,7 @@ public interface ISaveStructure extends AutoCloseable
*/
File getSaveFolder(ILevelWrapper levelWrapper);
File getPre23SaveFolder(ILevelWrapper levelWrapper);
}
@@ -75,6 +75,8 @@ public class LocalSaveStructure implements ISaveStructure
});
}
@Override
public File getPre23SaveFolder(ILevelWrapper levelWrapper) { return this.getSaveFolder(levelWrapper); }
//==================//
@@ -110,7 +110,7 @@ public class BatchGenerator implements IDhApiWorldGenerator
targetStep = EDhApiWorldGenerationStep.FEATURES;
break;
case INTERNAL_SERVER:
targetStep = EDhApiWorldGenerationStep.LIGHT; // TODO using something other than LIGHT would be good for clarity
targetStep = EDhApiWorldGenerationStep.LIGHT;
break;
}
@@ -328,7 +328,8 @@ public class DhLightingEngine
continue;
}
if (relNeighbourBlockPos.getY() < neighbourChunk.getMinNonEmptyHeight() || relNeighbourBlockPos.getY() > neighbourChunk.getExclusiveMaxBuildHeight())
if (relNeighbourBlockPos.getY() < neighbourChunk.getMinNonEmptyHeight()
|| relNeighbourBlockPos.getY() >= neighbourChunk.getExclusiveMaxBuildHeight())
{
// the light pos is outside the chunk's min/max height,
// this can happen if given a chunk that hasn't finished generating
@@ -9,6 +9,7 @@ import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSource
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.util.FormatUtil;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.objects.RollingAverage;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
@@ -167,17 +168,15 @@ public class PregenManager
this.generatedPercentage.update((double) this.nextSectionSpiralIndex.get() / this.sectionsToGenerate);
double chunksToGenerate = Math.ceil(Math.sqrt(this.sectionsToGenerate) / 2 * 4 * 10) / 10; // ceil to nearest 0.1
int chunkRatePerSecond = (int) (1000 / this.averageTaskCompletionIntervalMs.getAverage() * 4 * 4);
double etaMs = this.averageTaskCompletionIntervalMs.getAverage() * (this.sectionsToGenerate - this.nextSectionSpiralIndex.get());
return MessageFormat.format("Generated radius: {0,number,#.###} / {1,number,#.#} chunks ({2,number,#.###%}), ETA: {3}",
return MessageFormat.format("Generated radius: {0,number,#.###} / {1,number,#.#} chunks ({2} cps, {3,number,#.###%}), ETA: {4}",
this.generatedRadius.getValue(),
chunksToGenerate,
chunkRatePerSecond,
this.generatedPercentage.getValue(),
Duration.ofMillis((long) etaMs).toString()
.substring(2)
.replaceAll("(\\d[HMS])(?!$)", "$1 ")
.replaceAll("\\.\\d+", "")
.toLowerCase()
FormatUtil.formatEta(Duration.ofMillis((long) etaMs))
);
}
@@ -16,6 +16,7 @@ import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.*;
public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQueue implements IFullDataSourceRetrievalQueue, IDebugRenderable
@@ -47,7 +48,7 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue
public void startAndSetTargetPos(DhBlockPos2D targetPos) { super.tick(targetPos); }
@Override
public byte lowestDataDetail() { return LodUtil.BLOCK_DETAIL_LEVEL + 12; } // TODO should be the same as what the server's update propgator can provide
public byte lowestDataDetail() { return LodUtil.BLOCK_DETAIL_LEVEL + 12; } // TODO should be the same as what the server's update propagator can provide
@Override
public byte highestDataDetail() { return LodUtil.BLOCK_DETAIL_LEVEL; }
@@ -56,7 +57,11 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue
{
long generationStartMsTime = System.currentTimeMillis();
return super.submitRequest(sectionPos, tracker.getDataSourceConsumer())
return super.submitRequest(sectionPos, fullDataSource -> {
Objects.requireNonNull(tracker.getDataSourceConsumer()).accept(fullDataSource);
fullDataSource.close();
})
.thenApply(requestResult ->
{
long totalGenTimeInMs = System.currentTimeMillis() - generationStartMsTime;
@@ -74,7 +79,14 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue
return WorldGenResult.CreateFail();
case REQUIRES_SPLITTING:
List<CompletableFuture<WorldGenResult>> childFutures = new ArrayList<>(4);
DhSectionPos.forEachChild(sectionPos, childPos -> childFutures.add(this.submitRetrievalTask(childPos, requiredDataDetail, tracker)));
DhSectionPos.forEachChild(sectionPos, childPos -> {
tracker.shouldGenerateSplitChild(childPos).thenAccept(shouldGenerate -> {
if (shouldGenerate)
{
childFutures.add(this.submitRetrievalTask(childPos, requiredDataDetail, tracker));
}
});
});
return WorldGenResult.CreateSplit(childFutures);
}
@@ -111,6 +123,18 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue
return DhSectionPos.getChebyshevSignedBlockDistance(sectionPos, targetPos) <= this.networkState.sessionConfig.getMaxGenerationRequestDistance() * 16;
}
@Override
protected boolean onBeforeRequest(long sectionPos, CompletableFuture<ERequestResult> future)
{
if (DhSectionPos.getDetailLevel(sectionPos) > DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL
&& !Config.Server.Experimental.enableNSizedGeneration.get())
{
future.complete(ERequestResult.REQUIRES_SPLITTING);
return false;
}
return true;
}
@Override
protected String getQueueName() { return "World Remote Generation Queue"; }
@@ -31,6 +31,7 @@ import com.seibel.distanthorizons.core.generation.tasks.InProgressWorldGenTaskGr
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenTask;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenTaskGroup;
import com.seibel.distanthorizons.core.level.IDhServerLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
@@ -75,6 +76,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
private final IDhApiWorldGenerator generator;
private final IDhServerLevel level;
/** contains the positions that need to be generated */
private final ConcurrentHashMap<Long, WorldGenTask> waitingTasks = new ConcurrentHashMap<>();
@@ -103,7 +105,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
private int estimatedRemainingTaskCount = 0;
private int estimatedRemainingChunkCount = 0;
private final RollingAverage rollingAverageChunkGenTimeInMs = new RollingAverage(1_000);
private final RollingAverage rollingAverageChunkGenTimeInMs = new RollingAverage(Runtime.getRuntime().availableProcessors() * 500);
public RollingAverage getRollingAverageChunkGenTimeInMs() { return this.rollingAverageChunkGenTimeInMs; }
@@ -112,10 +114,11 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
// constructors //
//==============//
public WorldGenerationQueue(IDhApiWorldGenerator generator)
public WorldGenerationQueue(IDhApiWorldGenerator generator, IDhServerLevel level)
{
LOGGER.info("Creating world gen queue");
this.generator = generator;
this.level = level;
this.lowestDataDetail = generator.getLargestDataDetailLevel();
this.highestDataDetail = generator.getSmallestDataDetailLevel();
@@ -184,45 +187,40 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
// update the target pos
this.generationTargetPos = targetPos;
// ensure the queuing thread is running
if (!this.generationQueueRunning)
{
this.startWorldGenQueuingThread();
}
// needs to be called at least once to start the queue
this.tryQueueNewWorldGenRequestsAsync();
}
private void startWorldGenQueuingThread()
private synchronized void tryQueueNewWorldGenRequestsAsync()
{
if (!DhApiWorldProxy.INSTANCE.worldLoaded()
|| DhApiWorldProxy.INSTANCE.getReadOnly())
{
return;
}
if (this.generationQueueRunning)
{
return;
}
this.generationQueueRunning = true;
// queue world generation tasks on its own thread since this process is very slow and would lag the server thread
this.queueingThread.execute(() ->
{
try
{
// loop until the generator is shutdown
while (!Thread.interrupted() && DhApiWorldProxy.INSTANCE.worldLoaded() && !DhApiWorldProxy.INSTANCE.getReadOnly())
this.generator.preGeneratorTaskStart();
// queue generation tasks until the generator is full, or there are no more tasks to generate
boolean taskStarted = true;
while (!this.isGeneratorBusy()
&& taskStarted)
{
this.generator.preGeneratorTaskStart();
// queue generation tasks until the generator is full, or there are no more tasks to generate
boolean taskStarted = true;
while (!this.isGeneratorBusy() && taskStarted)
{
taskStarted = this.startNextWorldGenTask(this.generationTargetPos);
if (!taskStarted)
{
int debugPointOne = 0;
}
}
// if there aren't any new tasks, wait a second before checking again // TODO replace with a listener instead
Thread.sleep(1000);
taskStarted = this.startNextWorldGenTask(this.generationTargetPos);
}
}
catch (InterruptedException e)
{
/* do nothing, this means the thread is being shut down */
}
catch (Exception e)
{
LOGGER.error("queueing exception: " + e.getMessage(), e);
@@ -233,7 +231,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
}
});
}
public boolean isGeneratorBusy()
private boolean isGeneratorBusy()
{
PriorityTaskPicker.Executor executor = ThreadPoolUtil.getWorldGenExecutor();
if (executor == null)
@@ -246,7 +244,6 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
int maxWorldGenTaskCount = worldGenThreadCount * MAX_QUEUED_TASKS_PER_THREAD;
return executor.getQueueSize() > maxWorldGenTaskCount;
}
/**
* @param targetPos the position to center the generation around
* @return false if no tasks were found to generate
@@ -379,6 +376,10 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
{
LOGGER.error("Unexpected error completing world gen task at pos: ["+DhSectionPos.toString(taskPos)+"].", e);
}
finally
{
this.tryQueueNewWorldGenRequestsAsync();
}
});
this.inProgressGenTasksByLodPos.put(taskPos, newTaskGroup);
@@ -409,8 +410,15 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
{
try
{
IChunkWrapper chunk = WRAPPER_FACTORY.createChunkWrapper(generatedObjectArray);
try (FullDataSourceV2 dataSource = LodDataBuilder.createFromChunk(chunk))
IChunkWrapper chunkWrapper = WRAPPER_FACTORY.createChunkWrapper(generatedObjectArray);
// TODO light data should be pulled (if possible) from the ChunkAccess object itself via ChunkFileReader.readLight
// but this should work for now
ArrayList<IChunkWrapper> nearbyChunkList = new ArrayList<IChunkWrapper>();
nearbyChunkList.add(chunkWrapper);
DhLightingEngine.INSTANCE.bakeChunkBlockLighting(chunkWrapper, nearbyChunkList, this.level.hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT);
try (FullDataSourceV2 dataSource = LodDataBuilder.createFromChunk(this.level.getLevelWrapper(), chunkWrapper))
{
LodUtil.assertTrue(dataSource != null);
dataSourceConsumer.accept(dataSource);
@@ -439,9 +447,8 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
ThreadPoolUtil.getWorldGenExecutor(),
(DhApiChunk dataPoints) ->
{
try
try(FullDataSourceV2 dataSource = LodDataBuilder.createFromApiChunkData(dataPoints, this.generator.runApiValidation()))
{
FullDataSourceV2 dataSource = LodDataBuilder.createFromApiChunkData(dataPoints, this.generator.runApiValidation());
dataSourceConsumer.accept(dataSource);
}
catch (DataCorruptedException | IllegalArgumentException e)
@@ -464,6 +471,12 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
// set here so the API user doesn't have to pass in this value anywhere themselves
pooledDataSource.setRunApiChunkValidation(this.generator.runApiValidation());
// only apply to children if we aren't at the bottom of the tree
pooledDataSource.applyToChildren = DhSectionPos.getDetailLevel(pooledDataSource.getPos()) > DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL;
pooledDataSource.applyToParent = DhSectionPos.getDetailLevel(pooledDataSource.getPos()) < DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL + 12;
return this.generator.generateLod(
chunkPosMin.getX(), chunkPosMin.getZ(),
DhSectionPos.getX(requestPos), DhSectionPos.getZ(requestPos),
@@ -471,11 +484,19 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
pooledDataSource,
generatorMode,
ThreadPoolUtil.getWorldGenExecutor(),
(IDhApiFullDataSource dataSource) ->
(IDhApiFullDataSource apiDataSource) ->
{
try
{
dataSourceConsumer.accept((FullDataSourceV2)dataSource);
FullDataSourceV2 fullDataSource = (FullDataSourceV2) apiDataSource;
try
{
dataSourceConsumer.accept(fullDataSource);
}
finally
{
fullDataSource.close();
}
}
catch (IllegalArgumentException e)
{
@@ -642,8 +663,25 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
@Override
public void debugRender(DebugRenderer renderer)
{
this.waitingTasks.keySet().forEach((pos) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 64f, 0.05f, Color.blue)); });
this.inProgressGenTasksByLodPos.forEach((pos, t) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 64f, 0.05f, Color.red)); });
// show the wireframe a bit lower than world max height,
// since most worlds don't render all the way up to the max height
int levelHeightRange = (this.level.getMaxY() - this.level.getMinY());
int maxY = this.level.getMaxY() - (levelHeightRange / 2);
// blue - queued
this.waitingTasks.keySet().forEach((pos) ->
{
renderer.renderBox(
new DebugRenderer.Box(pos, this.level.getMinY(), maxY, 0.05f, Color.blue));
});
// red - in progress
this.inProgressGenTasksByLodPos.forEach((pos, t) ->
{
renderer.renderBox(
new DebugRenderer.Box(pos, this.level.getMinY(), maxY, 0.05f, Color.red));
});
}
@@ -22,6 +22,7 @@ package com.seibel.distanthorizons.core.generation.tasks;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import javax.annotation.Nullable;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
/**
@@ -33,4 +34,6 @@ public interface IWorldGenTaskTracker
@Nullable
Consumer<FullDataSourceV2> getDataSourceConsumer();
CompletableFuture<Boolean> shouldGenerateSplitChild(long pos);
}
@@ -76,7 +76,7 @@ public enum EPlatform
{ // For MacOS it should either output "Mac OS X" or "Darwin" depending on the version of MacOS
current = MACOS;
}
else if (osName.startsWith("bsd"))
else if (osName.startsWith("bsd") || osName.startsWith("freebsd"))
{ // Depending on the BSD distro this will be different
current = BSD;
}
@@ -28,6 +28,7 @@ import com.seibel.distanthorizons.core.jar.installer.GitlabGetter;
import com.seibel.distanthorizons.core.jar.installer.ModrinthGetter;
import com.seibel.distanthorizons.core.jar.installer.WebDownloader;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.wrapperInterfaces.IVersionConstants;
import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.coreapi.util.StringUtil;
@@ -42,8 +43,12 @@ import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.text.NumberFormat;
import java.util.Collections;
import java.util.Comparator;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipFile;
/**
* Used to update the mod automatically
@@ -106,7 +111,7 @@ public class SelfUpdater
}
if (!ModrinthGetter.mcVersions.contains(mcVersion))
{
LOGGER.warn("Minecraft version ["+ mcVersion +"] is not findable on Modrinth, only findable versions are ["+ StringUtil.join(",", ModrinthGetter.mcVersions) +"]");
LOGGER.warn("Minecraft version ["+ mcVersion +"] is not findable on Modrinth, only findable versions are ["+ StringUtil.join(", ", ModrinthGetter.mcVersions) +"]");
return false;
}
@@ -253,94 +258,177 @@ public class SelfUpdater
deleteOldJarOnJvmShutdown = true;
LOGGER.info("Distant Horizons successfully updated. It will apply on game's relaunch");
String message = "Distant Horizons successfully updated. It will apply on game's relaunch";
LOGGER.info(message);
new Thread(() ->
{
String message = "Distant Horizons updated, this will be applied on game restart.";
if (!GraphicsEnvironment.isHeadless())
try
{
TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, message, "ok", "info", false);
}
else
{
LOGGER.info(message);
}
catch (Exception ignore) { }
}).start();
return true;
}
catch (Exception e)
{
LOGGER.warn("Failed to update Distant Horizons to version [" + ModrinthGetter.getLatestNameForVersion(minecraftVersion) + "], error: ["+e.getMessage()+"].", e);
// delete the update file to prevent issues with a corrupt jar floating around
try
{
Files.deleteIfExists(file.toPath());
}
catch (Exception deleteCorruptFileException)
{
LOGGER.error("Unable to delete corrupted update file at ["+file.toPath()+"], error: ["+deleteCorruptFileException.getMessage()+"].", deleteCorruptFileException);
}
String message = "Failed to update Distant Horizons to version [" + ModrinthGetter.getLatestNameForVersion(minecraftVersion) + "], error: ["+e.getMessage()+"].";
LOGGER.error(message, e);
try
{
TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, message, "ok", "error", false);
}
catch (Exception ignore) { }
return false;
}
}
public static boolean updateNightlyMod(String minecraftVersion, File file)
{
if (GitlabGetter.INSTANCE.projectPipelines.size() == 0)
if (GitlabGetter.INSTANCE.projectPipelines.isEmpty())
{
LOGGER.warn("Failed to find any nightly builds for the minecraft version ["+minecraftVersion+"] update canceled.");
return false;
}
Path mergedZipPath = null;
try
{
LOGGER.info("Attempting to auto update Distant Horizons.");
Files.createDirectories(file.getParentFile().toPath());
File mergedZip = file.getParentFile().toPath().resolve("merged.zip").toFile();
mergedZipPath = file.getParentFile().toPath().resolve("merged.zip");
WebDownloader.downloadAsFile(GitlabGetter.INSTANCE.getDownloads(GitlabGetter.INSTANCE.projectPipelines.get(0).get("id")).get(minecraftVersion), mergedZipPath.toFile());
WebDownloader.downloadAsFile(GitlabGetter.INSTANCE.getDownloads(GitlabGetter.INSTANCE.projectPipelines.get(0).get("id")).get(minecraftVersion), mergedZip);
ZipInputStream zis = new ZipInputStream(new FileInputStream(mergedZip));
ZipEntry zipEntry = zis.getNextEntry();
while (zipEntry != null)
try (ZipFile zipFile = new ZipFile(mergedZipPath.toFile()))
{
if (!zipEntry.isDirectory() && zipEntry.getName().contains("Merged")) // Look until the merged jar is found
ZipEntry zipEntry =
Collections.list(zipFile.entries()).stream()
.max(Comparator.comparingInt(entry -> entry.getName().length()))
// shouldn't happen, but just in case
.orElseThrow(() -> new Exception("Unable to find jar in zip. Is the downloaded zip empty?"));
// expected values as defined by the zip
long expectedCheckSum = zipEntry.getCrc();
int expectedSize = (int)zipEntry.getSize();
// read in the file content
byte[] buffer = new byte[expectedSize];
CRC32 crcCheckSumGenerator = new CRC32();
InputStream inputStream = zipFile.getInputStream(zipEntry);
int byteReadIndex = 0;
try
{
// write file content
FileOutputStream fos = new FileOutputStream(file);
byte[] buffer = new byte[1024];
NumberFormat outputFormat = NumberFormat.getNumberInstance();
int len;
while ((len = zis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
fos.close();
deleteOldJarOnJvmShutdown = true;
LOGGER.info("Distant Horizons successfully updated. It will apply on game's relaunch");
new Thread(() ->
int nextByte = inputStream.read();
while (nextByte != -1)
{
String message = "Distant Horizons updated, this will be applied on game restart.";
if (!GraphicsEnvironment.isHeadless())
buffer[byteReadIndex] = (byte) nextByte;
crcCheckSumGenerator.update(nextByte);
nextByte = inputStream.read();
byteReadIndex++;
// TODO it would be better to change this divisor based on the expected size,
// so it would always be split up into 100 1% increments
// but this will work for now when the expected size is about 17 MB, this will log about 170 times
if (byteReadIndex % 100_000 == 0)
{
TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, message, "ok", "info", false);
LOGGER.info("Decompressing ["+outputFormat.format(((double)byteReadIndex / expectedSize)*100.0)+"]%");
}
else
{
LOGGER.info(message);
}
}).start();
zis.close();
Files.deleteIfExists(newFileLocation.getParentFile().toPath().resolve("merged.zip"));
return true;
}
}
catch (EOFException ignore) { /* shouldn't happen, but just in case */ }
// confirm we read the whole file
if (byteReadIndex != expectedSize) // +1 on the index isn't necessary since the readIndex will always end +1 from where it started
{
LOGGER.warn("Distant Horizons update decompression failed, aborting install");
throw new Exception("Decompression failed");
}
zipEntry = zis.getNextEntry();
// confirm the checksum is correct (IE we decompressed correctly)
long actualChecksum = crcCheckSumGenerator.getValue();
if (actualChecksum != expectedCheckSum)
{
LOGGER.warn("Distant Horizons checksum mismatch, aborting install");
throw new Exception("Checksum Mismatch");
}
Files.write(file.toPath(), buffer);
}
zis.close();
return false;
Files.deleteIfExists(mergedZipPath);
deleteOldJarOnJvmShutdown = true;
String message = "Distant Horizons updated, this will be applied on game restart.";
LOGGER.info(message);
new Thread(() ->
{
try
{
TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, message, "ok", "info", false);
}
catch (Exception ignore) { }
}).start();
return true;
}
catch (Exception e)
{
LOGGER.warn("Failed to update [" + ModInfo.READABLE_NAME + "] to version [" + GitlabGetter.INSTANCE.projectPipelines.get(0).get("sha") + "].", e);
// delete the update jar to prevent issues with a corrupt jar floating around
try
{
Files.deleteIfExists(file.toPath());
}
catch (Exception deleteCorruptFileException)
{
LOGGER.error("Unable to delete corrupted update jar file at ["+file.toPath()+"], error: ["+deleteCorruptFileException.getMessage()+"].", deleteCorruptFileException);
}
// delete the update zip so we can clean up
try
{
if (mergedZipPath != null)
{
Files.deleteIfExists(mergedZipPath);
}
}
catch (Exception deleteCorruptFileException)
{
LOGGER.error("Unable to delete corrupted update zip file at ["+mergedZipPath+"], error: ["+deleteCorruptFileException.getMessage()+"].", deleteCorruptFileException);
}
String message = "Failed to update [" + ModInfo.READABLE_NAME + "] to version [" + GitlabGetter.INSTANCE.projectPipelines.get(0).get("sha") + "], error: ["+e.getMessage()+"].";
LOGGER.error(message, e);
try
{
TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, message, "ok", "error", false);
}
catch (Exception ignore) { }
return false;
}
}
@@ -19,34 +19,50 @@
package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiBlockMaterial;
import com.seibel.distanthorizons.api.interfaces.render.IDhApiRenderableBoxGroup;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiChunkModifiedEvent;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.api.objects.math.DhApiVec3d;
import com.seibel.distanthorizons.api.objects.render.DhApiRenderableBox;
import com.seibel.distanthorizons.api.objects.render.DhApiRenderableBoxGroupShading;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.beacon.BeaconBeamDataHandler;
import com.seibel.distanthorizons.core.file.fullDatafile.DelayedFullDataSourceSaveCache;
import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.render.renderer.generic.CloudRenderHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericRenderObjectFactory;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.sql.dto.ChunkHashDTO;
import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo;
import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo;
import com.seibel.distanthorizons.core.sql.repo.ChunkHashRepo;
import com.seibel.distanthorizons.core.util.ColorUtil;
import com.seibel.distanthorizons.core.util.KeyedLockContainer;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.coreapi.util.MathUtil;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.io.File;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
public abstract class AbstractDhLevel implements IDhLevel
{
@@ -59,6 +75,8 @@ public abstract class AbstractDhLevel implements IDhLevel
@Nullable
public BeaconBeamRepo beaconBeamRepo;
protected final KeyedLockContainer<Long> beaconUpdateLockContainer = new KeyedLockContainer<>();
protected final DelayedFullDataSourceSaveCache delayedFullDataSourceSaveCache = new DelayedFullDataSourceSaveCache(this::onDataSourceSaveAsync, 3_000);
/** contains the {@link DhChunkPos} for each {@link DhSectionPos} that are queued to save */
protected final ConcurrentHashMap<Long, HashSet<DhChunkPos>> updatedChunkPosSetBySectionPos = new ConcurrentHashMap<>();
@@ -67,7 +85,8 @@ public abstract class AbstractDhLevel implements IDhLevel
/** Will be null if clouds shouldn't be rendered for this level. */
@Nullable
protected CloudRenderHandler cloudRenderHandler;
protected BeaconBeamDataHandler beaconBeamDataHandler;
private IDhApiRenderableBoxGroup unexploredFogRenderableBoxGroup;
@@ -75,7 +94,7 @@ public abstract class AbstractDhLevel implements IDhLevel
// constructor //
//=============//
protected AbstractDhLevel() { }
protected AbstractDhLevel() { }
/**
* Creating the repos requires access to the level file, which isn't
@@ -126,13 +145,6 @@ public abstract class AbstractDhLevel implements IDhLevel
}
}
}
// shouldn't happen, but just in case
if (this.beaconBeamRepo != null)
{
this.beaconBeamDataHandler = new BeaconBeamDataHandler(this.beaconBeamRepo, genericRenderer);
}
}
@@ -144,7 +156,7 @@ public abstract class AbstractDhLevel implements IDhLevel
@Override
public void updateChunkAsync(IChunkWrapper chunkWrapper, int chunkHash)
{
try (FullDataSourceV2 dataSource = FullDataSourceV2.createFromChunk(chunkWrapper))
try (FullDataSourceV2 dataSource = FullDataSourceV2.createFromChunk(this.getLevelWrapper(), chunkWrapper))
{
if (dataSource == null)
{
@@ -228,31 +240,139 @@ public abstract class AbstractDhLevel implements IDhLevel
//=================//
@Override
public void updateBeaconBeamsForChunk(IChunkWrapper chunkToUpdate, ArrayList<IChunkWrapper> nearbyChunkList)
public void updateBeaconBeamsForSectionPos(long sectionPos, List<BeaconBeamDTO> activeBeamList)
{
if (this.beaconBeamDataHandler != null)
int minBlockX = DhSectionPos.getMinCornerBlockX(sectionPos);
int minBlockZ = DhSectionPos.getMinCornerBlockZ(sectionPos);
// TODO special logic had to be done for DhChunkPos.getMaxBlock,
// does that need to be done here?
// The DhChunkPos issue caused beacons to appear/disappear incorrectly on negative chunk borders
int maxBlockX = minBlockX + DhSectionPos.getBlockWidth(sectionPos);
int maxBlockZ = minBlockZ + DhSectionPos.getBlockWidth(sectionPos);
this.updateBeaconBeamsBetweenBlockPos(
sectionPos,
minBlockX, maxBlockX,
minBlockZ, maxBlockZ,
activeBeamList
);
}
@Override
public void updateBeaconBeamsForChunkPos(DhChunkPos chunkPos, List<BeaconBeamDTO> activeBeamList)
{
long sectionPos = DhSectionPos.encodeContaining(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, chunkPos);
int minBlockX = chunkPos.getMinBlockX();
int minBlockZ = chunkPos.getMinBlockZ();
int maxBlockX = chunkPos.getMaxBlockX();
int maxBlockZ = chunkPos.getMaxBlockZ();
//LOGGER.info("beacons ["+activeBeamList.size()+"] at ["+chunkPos+"] x["+minBlockX+"]-["+maxBlockX+"] z["+minBlockZ+"]-["+maxBlockZ+"].");
this.updateBeaconBeamsBetweenBlockPos(
sectionPos,
minBlockX, maxBlockX,
minBlockZ, maxBlockZ,
activeBeamList
);
}
private void updateBeaconBeamsBetweenBlockPos(
long sectionPosForLock,
int minBlockX, int maxBlockX,
int minBlockZ, int maxBlockZ,
List<BeaconBeamDTO> activeBeamList
) // TODO min/max block pos instead
{
if (this.beaconBeamRepo == null)
{
List<BeaconBeamDTO> activeBeamList = chunkToUpdate.getAllActiveBeacons(nearbyChunkList);
this.beaconBeamDataHandler.setBeaconBeamsForChunk(chunkToUpdate.getChunkPos(), activeBeamList);
return;
}
// locked to prevent two threads from updating the same section at the same time
ReentrantLock lock = this.beaconUpdateLockContainer.getLockForPos(sectionPosForLock); // TODO this can cause a lot of slow-downs
try
{
lock.lock();
HashSet<DhBlockPos> allPosSet = new HashSet<>();
// sort new beams
HashMap<DhBlockPos, BeaconBeamDTO> activeBeamByPos = new HashMap<>(activeBeamList.size());
for (BeaconBeamDTO beam : activeBeamList)
{
activeBeamByPos.put(beam.blockPos, beam);
allPosSet.add(beam.blockPos);
}
// get existing beams
List<BeaconBeamDTO> existingBeamList = this.beaconBeamRepo.getAllBeamsInBlockPosRange(
minBlockX, maxBlockX,
minBlockZ, maxBlockZ);
HashMap<DhBlockPos, BeaconBeamDTO> existingBeamByPos = new HashMap<>(existingBeamList.size());
for (BeaconBeamDTO beam : existingBeamList)
{
existingBeamByPos.put(beam.blockPos, beam);
allPosSet.add(beam.blockPos);
}
for (DhBlockPos beaconPos : allPosSet)
{
if (minBlockX <= beaconPos.getX() && beaconPos.getX() <= maxBlockX
&& minBlockZ <= beaconPos.getZ() && beaconPos.getZ() <= maxBlockZ)
{
//// don't modify beacons outside the updated range
//continue;
}
else
{
continue;
}
BeaconBeamDTO existingBeam = existingBeamByPos.get(beaconPos);
BeaconBeamDTO activeBeam = activeBeamByPos.get(beaconPos);
if (activeBeam != null)
{
//LOGGER.info("add beacon ["+activeBeam.blockPos+"] x["+minBlockX+"]-["+maxBlockX+"] z["+minBlockZ+"]-["+maxBlockZ+"].");
if (existingBeam == null)
{
// new beam found, add to DB
this.beaconBeamRepo.save(activeBeam);
}
else
{
// beam still exists in chunk
if (!existingBeam.color.equals(activeBeam.color))
{
// beam colors were changed
this.beaconBeamRepo.save(activeBeam);
}
}
}
else if (existingBeam != null)
{
// beam no longer exists at position, remove from DB
this.beaconBeamRepo.deleteWithKey(beaconPos);
//LOGGER.info("remove beacon ["+beaconPos+"] x["+minBlockX+"]-["+maxBlockX+"] z["+minBlockZ+"]-["+maxBlockZ+"].");
}
}
}
finally
{
lock.unlock();
}
}
@Override
public void loadBeaconBeamsInPos(long pos)
{
if (this.beaconBeamDataHandler != null)
{
this.beaconBeamDataHandler.loadBeaconBeamsInPos(pos);
}
}
@Override
public void unloadBeaconBeamsInPos(long pos)
{
if (this.beaconBeamDataHandler != null)
{
this.beaconBeamDataHandler.unloadBeaconBeamsInPos(pos);
}
}
@Nullable
public BeaconBeamRepo getBeaconBeamRepo() { return this.beaconBeamRepo; }
@@ -272,6 +392,15 @@ public abstract class AbstractDhLevel implements IDhLevel
this.beaconBeamRepo.close();
}
GenericObjectRenderer genericRenderer = this.getGenericRenderer();
if (genericRenderer != null
&& this.unexploredFogRenderableBoxGroup != null)
{
genericRenderer.remove(this.unexploredFogRenderableBoxGroup.getId());
}
this.delayedFullDataSourceSaveCache.close();
}
@@ -23,7 +23,6 @@ import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.math.Vec3d;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
@@ -31,7 +30,6 @@ import org.apache.logging.log4j.Logger;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.*;
public abstract class AbstractDhServerLevel extends AbstractDhLevel implements IDhServerLevel
@@ -154,7 +152,8 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I
if (Config.Server.generationBoundsRadius.get() > 0)
{
if (DhSectionPos.getChebyshevSignedBlockDistance(message.sectionPos, new DhBlockPos2D(
Config.Server.generationBoundsX.get(), Config.Server.generationBoundsZ.get()
serverPlayerState.sessionConfig.getGenerationBoundsX(),
serverPlayerState.sessionConfig.getGenerationBoundsZ()
)) > Config.Server.generationBoundsRadius.get())
{
message.sendResponse(new RequestOutOfRangeException("Section out of allowed bounds"));
@@ -162,7 +161,7 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I
}
}
if (Config.Server.generateOnlyInHighestDetail.get() && DhSectionPos.getDetailLevel(message.sectionPos) != DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL)
if (!Config.Server.Experimental.enableNSizedGeneration.get() && DhSectionPos.getDetailLevel(message.sectionPos) != DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL)
{
message.sendResponse(new SectionRequiresSplittingException("Only highest-detail sections are allowed"));
return;
@@ -262,30 +261,27 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I
}
LodUtil.assertTrue(this.beaconBeamRepo != null, "beaconBeamRepo should not be null");
try (FullDataPayload payload = new FullDataPayload(data, this.beaconBeamRepo.getAllBeamsForPos(data.getPos())))
FullDataPayload payload = new FullDataPayload(data, this.beaconBeamRepo.getAllBeamsForPos(data.getPos()));
for (ServerPlayerState serverPlayerState : this.serverPlayerStateManager.getReadyPlayers())
{
for (ServerPlayerState serverPlayerState : this.serverPlayerStateManager.getReadyPlayers())
if (serverPlayerState.getServerPlayer().getLevel() != this.serverLevelWrapper)
{
if (serverPlayerState.getServerPlayer().getLevel() != this.serverLevelWrapper)
continue;
}
if (!serverPlayerState.sessionConfig.isRealTimeUpdatesEnabled())
{
continue;
}
Vec3d playerPosition = serverPlayerState.getServerPlayer().getPosition();
int distanceFromPlayer = DhSectionPos.getChebyshevSignedBlockDistance(data.getPos(), new DhBlockPos2D((int) playerPosition.x, (int) playerPosition.z)) / 16;
if (distanceFromPlayer <= serverPlayerState.sessionConfig.getMaxUpdateDistanceRadius())
{
serverPlayerState.fullDataPayloadSender.sendInChunks(payload, () ->
{
continue;
}
if (!serverPlayerState.sessionConfig.isRealTimeUpdatesEnabled())
{
continue;
}
Vec3d playerPosition = serverPlayerState.getServerPlayer().getPosition();
int distanceFromPlayer = DhSectionPos.getChebyshevSignedBlockDistance(data.getPos(), new DhBlockPos2D((int) playerPosition.x, (int) playerPosition.z)) / 16;
if (distanceFromPlayer >= serverPlayerState.getServerPlayer().getViewDistance()
&& distanceFromPlayer <= serverPlayerState.sessionConfig.getMaxUpdateDistanceRadius())
{
serverPlayerState.fullDataPayloadSender.sendInChunks(payload, () ->
{
serverPlayerState.networkSession.sendMessage(new FullDataPartialUpdateMessage(this.serverLevelWrapper, payload));
});
}
serverPlayerState.networkSession.sendMessage(new FullDataPartialUpdateMessage(this.serverLevelWrapper, payload));
});
}
}
});
@@ -332,6 +328,8 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I
@Override
public int getMinY() { return this.getLevelWrapper().getMinHeight(); }
@Override
public int getMaxY() { return this.getLevelWrapper().getMaxHeight(); }
@Override
public IServerLevelWrapper getServerLevelWrapper() { return this.serverLevelWrapper; }
@@ -258,7 +258,11 @@ public class ClientLevelModule implements Closeable, AbstractDataSourceHandler.I
public void clearRenderCache()
{
this.clientLevel.getClientLevelWrapper().clearBlockColorCache();
IClientLevelWrapper clientLevelWrapper = this.clientLevel.getClientLevelWrapper();
if (clientLevelWrapper != null)
{
clientLevelWrapper.clearBlockColorCache();
}
ClientRenderState ClientRenderState = this.ClientRenderStateRef.get();
if (ClientRenderState != null && ClientRenderState.quadtree != null)
@@ -19,46 +19,52 @@
package com.seibel.distanthorizons.core.level;
import com.google.common.cache.CacheBuilder;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.core.config.AppliedConfigState;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.file.beacon.BeaconBeamDataHandler;
import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.file.fullDatafile.RemoteFullDataSourceProvider;
import com.seibel.distanthorizons.core.file.structure.ISaveStructure;
import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState;
import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoadRequestQueue;
import com.seibel.distanthorizons.core.network.event.ScopedNetworkEventSource;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.render.RenderBufferHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import javax.annotation.CheckForNull;
import java.awt.*;
import java.io.File;
import java.util.*;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
/** The level used when connected to a server */
public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
protected static final ConfigBasedLogger NETWORK_LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Common.Logging.logNetworkEvent.get());
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
public final ClientLevelModule clientside;
@@ -71,8 +77,14 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
@Nullable
private final ScopedNetworkEventSource networkEventSource;
private final Set<DhChunkPos> loadedOnceChunks = Collections.newSetFromMap(
CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.<DhChunkPos, Boolean>build()
.asMap()
);
public final WorldGenModule worldGenModule;
public final AppliedConfigState<Boolean> worldGeneratorEnabledConfig;
@Nullable
private final SyncOnLoadRequestQueue syncOnLoadRequestQueue;
@@ -87,10 +99,21 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
{ this(saveStructure, clientLevelWrapper, null, true, networkState); }
public DhClientLevel(ISaveStructure saveStructure, IClientLevelWrapper clientLevelWrapper, @Nullable File fullDataSaveDirOverride, boolean enableRendering, @Nullable ClientNetworkState networkState)
{
if (saveStructure.getSaveFolder(clientLevelWrapper).mkdirs())
File saveFolder = saveStructure.getSaveFolder(clientLevelWrapper);
File pre23Folder = saveStructure.getPre23SaveFolder(clientLevelWrapper);
if (pre23Folder.exists())
{
if (!pre23Folder.renameTo(saveFolder))
{
throw new RuntimeException("Could not move old save data folder: " + pre23Folder.getAbsolutePath() + " to " + saveFolder.getAbsolutePath());
}
}
else if (saveStructure.getSaveFolder(clientLevelWrapper).mkdirs())
{
LOGGER.warn("unable to create data folder.");
}
this.levelWrapper = clientLevelWrapper;
this.levelWrapper.setParentLevel(this);
this.saveStructure = saveStructure;
@@ -109,7 +132,6 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
}
this.dataFileHandler = new RemoteFullDataSourceProvider(this, saveStructure, fullDataSaveDirOverride, this.syncOnLoadRequestQueue);
this.worldGeneratorEnabledConfig = new AppliedConfigState<>(Config.Common.WorldGenerator.enableDistantGeneration);
this.worldGenModule = new WorldGenModule(this, this.dataFileHandler, () -> new WorldGenState(this, networkState));
this.clientside = new ClientLevelModule(this);
@@ -135,18 +157,38 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
return;
}
try (FullDataSourceV2DTO dataSourceDto = this.networkState.fullDataPayloadReceiver.decodeDataSourceAndReleaseBuffer(message.payload))
try (FullDataSourceV2DTO dataSourceDto = this.networkState.fullDataPayloadReceiver.decodeDataSource(message.payload))
{
if (!message.isSameLevelAs(this.levelWrapper))
boolean isSameLevel = message.isSameLevelAs(this.levelWrapper);
NETWORK_LOGGER.debug("Buffer {} isSameLevel: {}", message.payload.dtoBufferId, isSameLevel);
if (!isSameLevel)
{
return;
}
this.beaconBeamDataHandler.setBeaconBeamsForPos(dataSourceDto.pos, message.payload.beaconBeams);
try (FullDataSourceV2 fullDataSource = dataSourceDto.createDataSource(this.levelWrapper))
Executor executor = ThreadPoolUtil.getFileHandlerExecutor();
if (executor != null)
{
this.updateDataSourcesAsync(fullDataSource);
executor.execute(() ->
{
try
{
// TODO this has a lock which can cause stuttering/lag issues
this.updateBeaconBeamsForSectionPos(dataSourceDto.pos, message.payload.beaconBeams);
}
catch (Exception e)
{
LOGGER.error("Unexpected erorr while updating full data source, error: ["+e.getMessage()+"].", e);
}
});
}
FullDataSourceV2 fullDataSource = dataSourceDto.createDataSource(this.levelWrapper);
this.updateDataSourcesAsync(fullDataSource)
.whenComplete((result, e) -> fullDataSource.close());
}
catch (Exception e)
{
@@ -249,9 +291,6 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
// getters //
//=========//
@Override
public int computeBaseColor(DhBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper block) { return this.levelWrapper.getBlockColor(pos, biome, block); }
@Override
public IClientLevelWrapper getClientLevelWrapper() { return this.levelWrapper; }
@@ -266,6 +305,8 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
@Override
public int getMinY() { return this.levelWrapper.getMinHeight(); }
@Override
public int getMaxY() { return this.levelWrapper.getMaxHeight(); }
@Override
public FullDataSourceProviderV2 getFullDataProvider() { return this.dataFileHandler; }
@@ -284,7 +325,16 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
ClientLevelModule.ClientRenderState renderState = this.clientside.ClientRenderStateRef.get();
return (renderState != null) ? renderState.renderBufferHandler : null;
}
public BeaconBeamDataHandler getBeaconBeamDataHandler() { return this.beaconBeamDataHandler; }
public boolean shouldProcessChunkUpdate(DhChunkPos chunkPos)
{
if (this.networkState == null || !this.networkState.isReady())
{
return true;
}
return !this.networkState.sessionConfig.isRealTimeUpdatesEnabled() || this.loadedOnceChunks.add(chunkPos);
}
@@ -35,6 +35,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftCli
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.util.List;
@@ -93,20 +94,7 @@ public class DhClientServerLevel extends AbstractDhServerLevel implements IDhCli
// level handling //
//================//
@Override //FIXME this can fail if the clientLevel isn't available yet, maybe in that case we could return -1 and handle it upstream?
public int computeBaseColor(DhBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper block)
{
IClientLevelWrapper clientLevel = this.getClientLevelWrapper();
if (clientLevel == null)
{
return 0;
}
else
{
return clientLevel.getBlockColor(pos, biome, block);
}
}
@Nullable
@Override
public IClientLevelWrapper getClientLevelWrapper() { return MC_CLIENT.getWrappedClientLevel(); }
@@ -25,6 +25,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrappe
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import org.jetbrains.annotations.Nullable;
public interface IDhClientLevel extends IDhLevel
{
@@ -33,8 +34,7 @@ public interface IDhClientLevel extends IDhLevel
void render(DhApiRenderParam renderEventParam, IProfilerWrapper profiler);
void renderDeferred(DhApiRenderParam renderEventParam, IProfilerWrapper profiler);
int computeBaseColor(DhBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper block);
@Nullable
IClientLevelWrapper getClientLevelWrapper();
/**

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