Compare commits

...

377 Commits

Author SHA1 Message Date
James Seibel 31b6d2dd05 Fix debug wireframes not rendering 2026-03-10 19:06:02 -05:00
James Seibel d75b65e6e7 hide render API config from UI 2026-03-10 18:44:48 -05:00
James Seibel 1bbe41c068 move GL shaders into the correct folder 2026-03-10 17:56:05 -05:00
James Seibel e426fc2380 Move lightmap wrapper methods into common 2026-03-10 17:33:55 -05:00
James Seibel 4e908b5b15 Make render interfaces consistent 2026-03-10 17:20:19 -05:00
James Seibel b51ab3d9cd clear up render task logging 2026-03-10 17:03:13 -05:00
James Seibel 9e7d0a1538 method rename 2026-03-10 16:58:57 -05:00
James Seibel 5b0bf59f00 fix iris rendering 2026-03-10 16:44:32 -05:00
James Seibel 0362d89173 Separate out some rendering logic 2026-03-10 14:50:53 -05:00
James Seibel 1b0f93db07 Changes for blaze/gl rendering config 2026-03-10 11:40:36 -05:00
James Seibel 559bad5676 Add rendering API definition 2026-03-09 20:12:40 -05:00
James Seibel f145444fd4 fix race condition in SharedApi setup 2026-03-09 20:07:31 -05:00
James Seibel 10b08ca116 Fix ClientApi profiler adding an incorrect layer 2026-03-09 19:14:21 -05:00
James Seibel 1dd244e889 Rename and reorganize render pass interfaces 2026-03-09 18:59:19 -05:00
James Seibel 2ea3d645e8 remove deprication warnings 2026-03-09 17:39:58 -05:00
James Seibel 49e34d78a5 update debug wireframe renderer 2026-03-09 17:35:24 -05:00
James Seibel 17cdb0f745 re-add some core rendering handlers 2026-03-09 16:35:17 -05:00
James Seibel 67b2467bee change where vertex size is found 2026-03-09 16:34:25 -05:00
James Seibel 82c832a4af Add RenderThreadTaskHandler 2026-03-09 16:30:45 -05:00
James Seibel c84dbfceaf update some imports 2026-03-09 16:26:48 -05:00
James Seibel 27b66940be Start moving OpenGL rendering to common 1 2026-03-09 16:22:55 -05:00
James Seibel 8240101a46 javadoc 2026-03-09 13:58:29 -05:00
James Seibel e1a932cf38 Only upload unique LOD uniforms once 2026-03-09 13:51:44 -05:00
James Seibel 39dd1c8509 renderable box cleanup 2026-03-09 12:28:49 -05:00
James Seibel fee0aadcbe MC -> blaze renaming 2026-03-09 12:19:53 -05:00
James Seibel a8c15d22c3 rename vbo containers 2026-03-09 11:16:50 -05:00
James Seibel bd833ba510 move blaze shader files 2026-03-09 09:57:03 -05:00
James Seibel a5a9a62e89 Fix ssao application 2026-03-08 21:14:48 -05:00
James Seibel e790bfb7e8 merge apply shaders 2026-03-07 14:32:02 -06:00
James Seibel 0f539f3a6f start separating out uniform logic 2026-03-05 17:32:19 -06:00
James Seibel d3f28f064b fix chunk update queue count flipped 2026-03-05 07:23:13 -06:00
James Seibel 40703db763 uncomment deferred rendering 2026-03-05 07:22:22 -06:00
James Seibel 2a554395f7 debug rendering 2026-03-04 18:07:49 -06:00
James Seibel c3dce412fa Add far fade 2026-03-04 07:38:49 -06:00
James Seibel 225c2df8df add fog 2026-03-03 07:47:58 -06:00
James Seibel 84c212a780 add SSAO 2026-03-02 07:45:25 -06:00
James Seibel 3d9ae5f088 generic object rendering 2026-03-01 19:32:54 -06:00
James Seibel 200e089a33 re-add lighting 2026-02-28 12:13:34 -06:00
James Seibel 3f509be195 remove unneeded partial ticks from old vanilla fade 2026-02-28 11:34:48 -06:00
James Seibel 1a2c0b0be1 re-add vanilla fading 2026-02-28 11:34:39 -06:00
James Seibel c6a4355718 minor render cleanup 2026-02-28 08:13:26 -06:00
James Seibel 3e80961d18 Rough initial LOD renderering 2026-02-26 16:55:54 -06:00
James Seibel 9f483ee07a Fix fade test shader 2026-02-24 22:03:43 -06:00
James Seibel d1ba402f4d start of vanilla fade logic 2026-02-24 09:57:03 -06:00
James Seibel 87cead607f move test shader folder 2026-02-24 07:02:05 -06:00
James Seibel 1b2992f1dc proof-of-concept test renderer 2026-02-23 12:31:21 -06:00
James Seibel ed0e94ccb7 Fix chunks applying to the wrong dimension 2026-02-18 22:05:23 -06:00
James Seibel 364f8f7afc improve transformer comment 2026-02-18 07:08:44 -06:00
James Seibel 962f20460c Fix dark LODs with false tintWithAvoidedBlocks 2026-02-17 07:47:30 -06:00
James Seibel ce17ac71c6 Remove buffer mapping 2026-02-17 07:16:42 -06:00
James Seibel 5adb9afa0a always disable instanced rendering on mac 2026-02-16 07:16:32 -06:00
James Seibel 3c80c11888 comment on a partially broken unit test 2026-02-14 22:34:17 -06:00
James Seibel 0714697e05 Fix beacon update locks 2026-02-14 22:01:25 -06:00
James Seibel df52f41b87 Fix beacons not reloading (and improve logic) 2026-02-14 21:32:30 -06:00
James Seibel 66a1d0296e maybe fix deprecated warn in OptifineAcessor 2026-02-14 21:31:51 -06:00
James Seibel c827817a94 ClientLevelModule cleanup 2026-02-14 11:10:35 -06:00
James Seibel 9895676b63 Increase startup timeout for MAC 2026-02-14 08:32:44 -06:00
James Seibel 5d5c94e652 Force enable fog if MC is rendering fog
done to fix underwater/blindness rendering
2026-02-14 08:24:22 -06:00
James Seibel 0a568571d9 Add logs when changing vanilla settings 2026-02-13 07:49:31 -06:00
James Seibel 291c0470ad auto disable fancy graphics if enabled 2026-02-13 07:35:50 -06:00
James Seibel 2f4587579f Change quadElementBuffer to only use DATA 2026-02-13 07:35:38 -06:00
James Seibel 2d70388587 Maybe fix Mac crashing with sodium on world start? 2026-02-12 07:27:11 -06:00
James Seibel a758c0bb3f regions to initalizer 2026-02-12 07:13:05 -06:00
James Seibel dd6aed273d Hopefully fix a rare concurrency issue in buffer Builder 2026-02-11 07:45:42 -06:00
James Seibel e2d663ee34 remove some unneeded concurrency checks in render setup 2026-02-11 07:34:55 -06:00
James Seibel dfc920d9bb Fix a harmless error message 2026-02-11 07:15:49 -06:00
James Seibel c91631809e re-add chat warning if G1GC is used 2026-02-11 07:09:33 -06:00
James Seibel 457bbebbdd Reduce memory allocation slightly during LOD loading 2026-02-10 07:32:13 -06:00
James Seibel 4d3242a370 improve concurrency handling in LOD render loading 2026-02-09 07:44:03 -06:00
James Seibel f0d71027f1 Fix loosing some thread pool tasks 2026-02-09 07:43:53 -06:00
James Seibel a98d9239e6 document Vbo.getVertexCount() 2 2026-02-08 21:37:33 -06:00
James Seibel 365edd48f8 document Vbo.getVertexCount() 2026-02-08 21:37:17 -06:00
James Seibel 9f22e3f2b3 Improve render load error handling 2026-02-08 21:06:01 -06:00
James Seibel 78f83197d7 Hide non-rendering levels in the F3 screen 2026-02-08 20:42:32 -06:00
James Seibel 42ae79b76c remove deprecated OS enum (replaced by EPlatform) 2026-02-08 20:12:26 -06:00
James Seibel 1178ef0706 Remove unused ID mappings after data update
Requires re-downsampling all LODs
2026-02-08 19:56:24 -06:00
James Seibel a506d2ef1f FullDataSource regions 2026-02-08 19:55:09 -06:00
James Seibel 92d9e631a7 Fix update propagator only re-queueing once queue empty 2026-02-08 19:53:03 -06:00
James Seibel 8afe388eb6 DataIdMap to string 2026-02-08 19:52:32 -06:00
James Seibel df1dae70c2 FullDataPointUtil reformat 2026-02-08 19:52:23 -06:00
James Seibel 777ed9215a datasource v1 mapping decode cleanup 2026-02-08 17:25:27 -06:00
James Seibel 25f4bde825 delayed full data cache regions 2026-02-07 18:37:21 -06:00
James Seibel fbb1beb359 ConfigEntry tostring for debugging 2026-02-07 18:15:12 -06:00
James Seibel f0852235b2 LodQuadTree cleanup and todo remove 2026-02-07 18:13:27 -06:00
James Seibel 4faa82e895 DhServerLevel use base shouldDoWorldGen()
@pshsh @s809 do you know why DhServerLevel overrode AbstractDhServerLevel?
In a quick test it appears using the super method worked just fine.
2026-02-07 17:59:08 -06:00
James Seibel 7201df1eff retrieval queue cleanup 2026-02-07 17:57:03 -06:00
James Seibel b6b13843eb shader constructor cleanup 2026-02-07 17:46:30 -06:00
James Seibel 10208d4dfa fix vanilla fade order 2026-02-07 17:00:30 -06:00
James Seibel d04e156dd1 Move some world gen queue limiting into the LodQuadTree 2026-02-07 16:37:56 -06:00
James Seibel 19412f80c5 cleanup cloud render handler 2026-02-07 16:13:33 -06:00
James Seibel dea3557546 Add colors to the F3 screen 2026-02-07 14:14:13 -06:00
James Seibel a3c72dbcbc clientApi remove finished todo comments 2026-02-07 13:28:22 -06:00
James Seibel dffad16d27 Clean up ColumnArrayView 2026-02-07 13:20:06 -06:00
James Seibel 0a3756eb9d Remove swing UI classes 2026-02-07 13:19:44 -06:00
James Seibel 821acaa0b9 MovableGridRingList cleanup 2026-02-07 12:05:18 -06:00
James Seibel 0fc5c55712 ArrayGridList pos object merge 2026-02-07 11:55:16 -06:00
James Seibel c13d04ff2b vanillaFade shader cleanup 2026-02-07 11:54:44 -06:00
James Seibel c9925b0b6c full data point ID map regions 2026-02-07 11:00:51 -06:00
James Seibel 725602e61f handle extra sql db closed 2026-02-07 10:18:40 -06:00
James Seibel bdeeb4c93b remove TODO for LWJGL versioning 2026-02-07 10:01:05 -06:00
James Seibel b3185c00d6 Replace DhLodPos with DhSectionPos 2026-02-07 08:40:17 -06:00
James Seibel ce20d3da32 debug render cleanup 2026-02-07 08:31:51 -06:00
James Seibel 69653336bf Remove most SSAO configs
No one seems to need or use them so we can just hard-code in the values we know look good.
2026-02-05 21:58:30 -06:00
James Seibel 062f11df21 Change graphics configs from doubles to floats 2026-02-05 21:57:17 -06:00
James Seibel 9e68d11608 re-add ignored blocks to config UI 2026-02-05 21:37:37 -06:00
James Seibel 3d349cb292 Only allow rendering clouds in config set dimensions 2026-02-05 21:37:29 -06:00
James Seibel d13bd9c4d3 clean up QuadTree unit tests 2026-02-05 21:12:25 -06:00
James Seibel 4a256683ab comments and region for update prop threading 2026-02-05 18:01:21 -06:00
James Seibel 7325d57e88 Make GLState AutoClosable for more robustness 2026-02-05 17:36:40 -06:00
James Seibel 7348cb1c37 Remove incomplete standalone GUI and comment incomplete DB parsing 2026-02-05 17:29:19 -06:00
James Seibel 007a3bdefa remove legacy logo SVG
Still available in the core repo's misc folder
2026-02-05 17:11:29 -06:00
James Seibel 6d95eb1099 fix magic numbers for lightmap binding 2026-02-05 17:11:09 -06:00
James Seibel 86e21cdaf6 remove unused VertexFormats 2026-02-05 17:02:15 -06:00
James Seibel d0bb03288d Fix fog falloff config not being used 2026-02-05 16:57:39 -06:00
James Seibel be3e618aca shader reformatting 2026-02-05 16:49:30 -06:00
James Seibel 35d3614c88 add a TODO to GenFullDataSourceProvider 2026-02-05 07:40:47 -06:00
James Seibel ba32af2c58 improve priority task picker task counting 2026-02-05 07:40:34 -06:00
James Seibel 2a681ef8e6 Pool ZStd decompress contexts 2026-02-05 07:40:11 -06:00
James Seibel 24684b7760 Fix render loading queuing incorrectly 2026-02-05 07:39:58 -06:00
James Seibel d3d4312ff6 Handle cave block culling more generically 2026-02-04 07:47:54 -06:00
James Seibel 6086a8a957 null check to AbstractConfigBase 2026-02-04 07:47:18 -06:00
James Seibel cd2530ca88 cleanup TODO comments 2026-02-04 07:47:08 -06:00
James Seibel af0e5699e2 Fix !1088 (API config.getApiValue() not returning null) 2026-02-04 07:19:50 -06:00
James Seibel d0e14ac408 LZMA cache commenting 2026-02-03 21:32:16 -06:00
James Seibel 0636712f80 Fix a rare world gen phantom closing issue 2026-02-03 21:32:10 -06:00
James Seibel 9bfd2812a6 fix near clip plane flickering 2026-02-03 21:30:16 -06:00
James Seibel 5f12c9e1f3 rename normal.vert -> quadApply.vert 2026-02-03 20:50:45 -06:00
James Seibel f8f0e3f4b9 move phantom pool into a new namespace 2026-02-03 20:48:07 -06:00
James Seibel ac36ce4560 refactoring and TODO cleanup 2026-02-03 20:47:06 -06:00
James Seibel f2612f39c0 cleanup world gen queue todo comments 2026-02-03 20:35:15 -06:00
James Seibel e7e7bc866d Clean up RenderUtil near clip methods 2026-02-03 20:34:54 -06:00
James Seibel 39eed05d83 Fix Debug renderer on newer MC versions 2026-02-03 19:59:12 -06:00
James Seibel b0624c8714 up the api version 5.1.0 -> 6.0.0 2026-02-03 07:05:50 -06:00
James Seibel 40351d1894 Require a data cache for API Repo methods 2026-02-03 07:05:13 -06:00
James Seibel a488478b1d null check in level loading 2026-02-02 07:18:00 -06:00
James Seibel c4b49ef308 Remove MC Texture LodBias config 2026-02-02 07:17:49 -06:00
James Seibel b511ab4fb3 Add RenderWrapper.getPartialTickTime() 2026-02-02 07:09:06 -06:00
James Seibel bbf69c7911 Make generic object updating async 2026-01-31 17:33:03 -06:00
James Seibel 6de41cd384 LodRenderSection remove unused code 2026-01-31 16:01:34 -06:00
James Seibel 7e7ccf1f38 unindent RenberableBoxGroup 2026-01-31 12:50:16 -06:00
James Seibel 5a6aa00ae7 RenberableBoxGroup regions 2026-01-31 12:49:58 -06:00
James Seibel 9c71540928 clean up bufferQuad arg names 2 2026-01-31 12:49:49 -06:00
James Seibel ff1859de8d clean up BufferQuad arg names 2026-01-31 12:42:03 -06:00
James Seibel 00be9a3c4f Handle MC running at 0 FPS 2026-01-31 10:22:23 -06:00
James Seibel dfed2c8966 GLproxy regions 2026-01-31 09:16:44 -06:00
James Seibel b20aebb09c generic renderer regions 2026-01-31 09:15:22 -06:00
James Seibel 9cbe98f81b clean up more TODO comments 2026-01-30 07:49:24 -06:00
James Seibel a9bfc3fbe1 Clean up more TODOs 2026-01-29 07:48:10 -06:00
James Seibel c30e410132 Clean up some API TODOs 2026-01-29 07:14:30 -06:00
James Seibel 11cd36cdec Fix TODO in DependencyInjector 2026-01-28 07:20:05 -06:00
James Seibel 59e35ffad3 Fix beacons not rendering 2026-01-27 20:50:53 -06:00
James Seibel 7b26c0baeb Fix paused executors not running tasks 2026-01-27 20:49:54 -06:00
James Seibel 60e53ff20b remove debug log 2026-01-27 20:49:42 -06:00
James Seibel 0f756a370b Queue gen tasks outside the tree update thread 2026-01-27 20:06:25 -06:00
James Seibel f0f525ef79 Fix ZStd decompressions streams being to long 2026-01-27 20:05:50 -06:00
James Seibel 59a8e31507 Reduce GC pressure when loading LODs from disk 2026-01-27 18:21:08 -06:00
James Seibel 6d3288ec42 LodRenderSection remove rate limit and improve task ordering 2026-01-26 20:29:55 -06:00
James Seibel 4e979b1f00 LodQuadBuilder regions 2026-01-26 06:52:45 -06:00
James Seibel 396545340e Move Thread Pool stat string method 2026-01-26 06:52:07 -06:00
James Seibel 87bfbf1afe Add a bunch of TODO comments 2026-01-26 06:51:05 -06:00
James Seibel 4346a2e803 Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2026-01-24 13:39:00 -06:00
James Seibel 6c228c1a17 Move Quad tree related objects 2026-01-24 13:38:02 -06:00
James Seibel d90361af59 Change LOD loading to start at lowest detail 2026-01-24 13:37:23 -06:00
s809 a0f06e4451 Merge branch 'feature/split-generation-toggles' 2026-01-18 22:39:43 +05:00
s809 2a4bfef7a6 Add correct descriptions 2026-01-18 22:38:01 +05:00
James Seibel 37d08b6dfa pause world gen when moving quickly 2026-01-17 17:03:43 -06:00
James Seibel eadf19405e add changes from DhRenderState 2026-01-17 16:15:46 -06:00
James Seibel f0757296f8 Add dynamic overdraw distance based on camera speed 2026-01-17 16:12:06 -06:00
James Seibel 3e29b361e6 Improve clientApi code regions 2026-01-17 10:46:45 -06:00
James Seibel 7b6fd03d78 Change render wrapper get Texture error returns 2026-01-17 09:56:21 -06:00
s809 7be65a2258 Split off server generation into a separate toggle 2026-01-17 01:43:05 +05:00
s809 1a540cf2bc Make sure payload chunk is readable 2026-01-14 22:17:46 +05:00
James Seibel 20fc2efb46 Improve concurrent iterating in QuadTree 2026-01-10 17:03:43 -06:00
James Seibel d8beba2498 minor cleanup in LodBufferContainer cleanup 2026-01-10 17:02:56 -06:00
James Seibel 9f0cb5a394 Add forge specific icon/logo
Done to fix a forge limitation where logos can't contain a file pathhttps://github.com/MinecraftForge/MinecraftForge/issues/7348
2026-01-10 11:56:08 -06:00
James Seibel df63401d11 DB updater use correct classloader 2026-01-10 08:21:09 -06:00
James Seibel db95951ade minor reformat and comment 2026-01-10 08:20:44 -06:00
s809 1e020f93a6 Reapply "Run plugin messages on a DH thread"
This reverts commit ff3145336d.
2026-01-09 20:29:23 +05:00
James Seibel 7aee6dfb44 Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2026-01-07 07:50:25 -06:00
James Seibel 546a51a295 expand distant beacon beams for visiblity 2026-01-07 07:50:22 -06:00
James Seibel ec7e791e9f Change EMinecraftColor -> MinecraftTextFormat
No need for an enum when all the values are strings
2026-01-06 07:10:40 -06:00
James Seibel d60dec3d82 Merge branch 'main' into 'main'
Fix typo in high vanilla render distance warning

See merge request distant-horizons-team/distant-horizons-core!94
2026-01-05 13:00:17 +00:00
s809 89a80103f0 Wrong message target 2026-01-04 20:04:30 +05:00
s809 8e14a7223c Add a chat message for incompatible messages 2026-01-04 19:36:24 +05:00
meanwhile131 7cf1e901f5 Fix typo in high vanilla render distance warning 2026-01-01 15:06:30 +04:00
James Seibel ba923fa829 Fix neoforge thread causing resource loading to fail 2025-12-26 14:13:27 -06:00
s809 505dbe2f62 Replace the failure state with future exceptions 2025-12-27 00:51:30 +05:00
James Seibel 48c5828e8f up version number 2.4.5 -> 2.4.6-dev 2025-12-24 22:41:27 -06:00
James Seibel eb2317934f up version number 2.4.4 -> 2.4.5 2025-12-24 22:06:53 -06:00
James Seibel 60537cda1b Replace MC color code strings with an enum 2025-12-24 22:04:50 -06:00
James Seibel 508ff2b776 Fix null pointer in ChunkUpdateQueueManager 2025-12-24 21:53:39 -06:00
James Seibel 7c4ac2bd7e remove dev from version number 2025-12-23 22:55:40 -06:00
James Seibel 8c13c2cf47 Fix toggling world gen not recreating queue 2025-12-23 22:55:40 -06:00
James Seibel 802019ff72 up DH api version 5.0.0 -> 5.1.0 2025-12-23 20:01:06 -06:00
James Seibel 141890556c Revert "remove deprecated getHeight() from DhApiLevelWrapper"
This reverts commit 50bdb73a52.
2025-12-23 19:56:28 -06:00
James Seibel 353838db41 add experimental option to ignore rendering dimensions by name 2025-12-23 12:22:00 -06:00
James Seibel f1547477c9 add clientLevelWrapper to DhApiRenderParam 2025-12-23 12:20:42 -06:00
James Seibel 535a645a84 minor internal API cleanup 2025-12-23 12:19:14 -06:00
James Seibel 2dc7f02b32 Remove experimental option onlyLoadCenterLods
option is now merged into main
2025-12-23 12:18:28 -06:00
James Seibel 50bdb73a52 remove deprecated getHeight() from DhApiLevelWrapper
use getMaxHeight() instead
2025-12-23 12:06:15 -06:00
James Seibel 53e6c95432 commenting DhTerrainShaderProgram 2025-12-23 08:57:51 -06:00
James Seibel 36f0029e45 Fix earth curvature shader compiling 2025-12-23 08:47:44 -06:00
s809 5067e970a2 Use another method to create a buffer 2025-12-23 12:50:02 +05:00
James Seibel 167ca94e69 Remove deprecated disableVanillaFog config 2025-12-22 20:31:24 -06:00
James Seibel 8d94b86bfd Hide network config changes by default 2025-12-22 14:51:29 -06:00
James Seibel a29567430e Net only log changed config values 2025-12-22 14:37:34 -06:00
James Seibel fb2dae48e2 re-enable remote timestamp getting 2025-12-22 14:21:12 -06:00
James Seibel 948b4bfd9c comment out debug log 2025-12-22 14:17:57 -06:00
James Seibel ca44256ca9 disable full data debug phantom array stacks 2025-12-22 14:17:47 -06:00
James Seibel a29b6a5aab remove unnecessary config appearance check 2025-12-22 14:17:13 -06:00
James Seibel 868254ccc8 try fixing rare leak in delayed data source cache
Didn't fix the problem, but shouldn't hurt
2025-12-22 14:16:34 -06:00
James Seibel 195fde8d73 quad tree spilt request cleanup 2025-12-22 13:58:26 -06:00
James Seibel ce7b9b94b6 fix/improve world gen/retrieval error handling 2025-12-22 13:58:26 -06:00
James Seibel 1f0c2e286a fix network splitting requests 2025-12-22 13:58:26 -06:00
James Seibel f79fd5e06f error handling in AbstactDhLevel chunk update 2025-12-22 13:58:26 -06:00
James Seibel 47c1d3955f failed attmpt to fix leaks
Breaks split world gen requests
2025-12-22 13:57:49 -06:00
James Seibel 2c5f5a3d4c minor refactors 2025-12-22 09:46:21 -06:00
James Seibel 81c533051e close errored data sources in full data provider 2025-12-22 08:35:15 -06:00
James Seibel 5cbe5ecfd8 Fix dis/re-enabling world gen queuing 2025-12-21 19:44:48 -06:00
James Seibel d4b4d28c9f Fix null error log in Data source provider 2025-12-21 08:53:27 -06:00
James Seibel b8e653b5f7 Fix phantom checkout not updating stack trace 2025-12-21 08:52:44 -06:00
James Seibel 80fea09598 Fix concurrency error in LodQuadTree 2025-12-21 08:52:25 -06:00
James Seibel 1d4f914a9f Merge branch 'worldGenRefactor' 2025-12-20 10:53:39 -06:00
James Seibel bf92dea2eb reduce stuttering at the cost of lighting quality 2025-12-20 10:52:51 -06:00
s809 2dd675b8da Handle generated LOD updates outside the render thread 2025-12-20 15:22:26 +05:00
s809 ff3145336d Revert "Run plugin messages on a DH thread"
This reverts commit 280181c91e.
2025-12-20 14:32:39 +05:00
James Seibel 280181c91e Run plugin messages on a DH thread 2025-12-19 16:54:29 -06:00
James Seibel 60232e713b refactor world gen queue 2025-12-19 16:54:07 -06:00
James Seibel 55d9030954 Remove extra particle for world gen 2025-12-18 10:20:01 -06:00
James Seibel 452bd75f5d remove chunkWrapper.isStillValid() 2025-12-18 10:18:07 -06:00
James Seibel 72be1e2602 Remove LodRenderSection.isFullyGenerated() 2025-12-18 10:17:36 -06:00
James Seibel 1c30213aca up version number 2.4.3 -> 2.4.4-dev 2025-12-18 10:04:41 -06:00
James Seibel e9a044308f remove dev from version number 2025-12-18 09:35:07 -06:00
James Seibel 1aabc0c792 remove chunkWrapper.isStillValid() 2025-12-18 09:35:02 -06:00
James Seibel 4a1513ed65 fix compiling 2025-12-17 22:41:22 -06:00
James Seibel 6d98c9cb84 start world gen refactoring 2025-12-17 22:39:23 -06:00
James Seibel b1b0642fbe LodRenderSection commenting/regions 2025-12-17 09:32:12 -06:00
James Seibel eecb28d11f Fix GLProxy error in multiplayer
Make some GLProxy methods static to prevent setup order issues
2025-12-17 09:02:07 -06:00
James Seibel 90564f2537 fix javadoc in LevelWrapper 2025-12-16 16:39:03 -06:00
James Seibel ded0b979cf Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2025-12-16 14:45:57 -06:00
James Seibel ed9cc5485c Add SSAO fade out distance 2025-12-16 14:45:53 -06:00
s809 cbd5974657 Fix packet handle errors not showing on F3 screen 2025-12-17 00:15:55 +05:00
James Seibel 0e5fba58ab minor shader program refactor 2025-12-16 09:13:22 -06:00
James Seibel 2943e63382 slight light engine optimization 2025-12-15 14:37:15 -06:00
James Seibel 30564aade7 up version number 2.4.2 -> 2.4.3-dev 2025-12-15 10:17:28 -06:00
James Seibel aabb90ada6 remove dev from version number 2025-12-15 09:00:15 -06:00
James Seibel 963a8dc53f comment LevelWrapper getDimensionName() 2025-12-15 08:55:40 -06:00
James Seibel aa6d69385b Move GC warning into the log 2025-12-15 08:44:06 -06:00
James Seibel f42c9cf8fb Improve initial library check error handling 2025-12-14 22:29:08 -06:00
James Seibel 92e0011c8d Fix auto update success dialog 2025-12-14 21:50:56 -06:00
James Seibel c20d95a7c7 improve spacing for self updater version log 2025-12-14 21:21:45 -06:00
James Seibel 353aa1ed2c maybe improve ZStd version check 2025-12-14 21:20:42 -06:00
James Seibel 5aa43ebcc8 hide LODs when underwater 2025-12-14 17:22:35 -06:00
James Seibel b6145461b6 add note to ignored block CSV 2025-12-14 17:02:53 -06:00
James Seibel 478e431076 up version number 2.4.1 -> 2.4.2-dev 2025-12-14 17:00:34 -06:00
James Seibel 6feb7f1b42 remove dev from version number 2025-12-14 13:46:04 -06:00
James Seibel 016fc66293 Print a warning if G1GC is used
G1GC is known to cause stuttering
2025-12-13 16:46:59 -06:00
James Seibel 6d3e30d425 add Zstd decompress lib check in initalizer 2025-12-13 15:48:05 -06:00
James Seibel 5be5c5a5bc replace client ticks with a timer
Prevents DH loading issues when MC ticks are paused
2025-12-13 11:19:33 -06:00
James Seibel ed5aeb8951 minor texture setup reformatting 2025-12-13 10:43:01 -06:00
James Seibel 7f0ddadf26 up version number 2.4.0 -> 2.4.1-dev 2025-12-13 10:20:44 -06:00
James Seibel a2c61ed278 up version number 2.3.7 -> 2.4.0 2025-12-13 10:19:50 -06:00
James Seibel 99eb4ac8a1 Fix infinite loop in DhSectionPos 2025-12-13 09:10:12 -06:00
James Seibel c75902d9d6 debug particle cleanup 2025-12-13 08:50:15 -06:00
James Seibel 1743949ba5 fix GeneratedFullDataSourceProvider not adding update listener 2025-12-13 08:49:45 -06:00
James Seibel a74a37a0e8 world gen queue refactoring 2025-12-13 08:49:31 -06:00
James Seibel 4ed7941288 fix missing localization 2025-12-12 07:45:12 -06:00
James Seibel ec59a5f754 comment cleanup and enum renaming for API use 2025-12-11 07:35:37 -06:00
James Seibel 895e04b7cc Remove unused wrapper functions and refactor 2025-12-10 18:50:35 -06:00
James Seibel 8f0930fa02 Allow world gen limits on singleplayer 2025-12-10 07:09:29 -06:00
James Seibel c1c4328fa5 rename API getSoftCache -> createSoftCahe 2025-12-09 20:57:27 -06:00
James Seibel 91240e4f7a disable mip-mapping on textures
necessary to fix MC 1.21.11 rendering
2025-12-09 20:57:09 -06:00
James Seibel 17c61a97cc revert long windows filepath char 2025-12-09 07:21:40 -06:00
James Seibel b78b852ffb Merge branch 'batchGenRefactor' 2025-12-09 07:16:18 -06:00
James Seibel 26d4220967 Add logging/messaging for corrupted DB files 2025-12-09 07:12:33 -06:00
James Seibel 5edc73cc03 enable long file paths for the config file 2025-12-06 12:28:22 -06:00
James Seibel 6fcfc9379e Fix repo unit tests 2025-12-06 12:27:53 -06:00
James Seibel 149fbccfa5 Merge branch 'batchGenRefactor' 2025-12-06 12:19:17 -06:00
James Seibel 5ca754d2ac Fix world gen progress config resetting on reboot 2025-12-06 09:18:34 -06:00
James Seibel f13744e858 Add thread pool priority setting
Setting this to 1 higher than C2ME can reduce issues with Chunky overwhelming DH.
2025-12-05 07:35:16 -06:00
James Seibel 64ac218003 Improve empty LOD debugging slightly 2025-12-05 07:28:57 -06:00
James Seibel 385bd326cf minor world gen related refactoring 2025-12-04 07:39:09 -06:00
James Seibel 4e9559f230 enable long file paths on windows for the DB 2025-12-02 07:07:17 -06:00
James Seibel 6ea864ef6b TEST 2025-11-29 09:59:33 -06:00
James Seibel 4e96728c25 maybe fix concurrency error during world gen shutdown 2025-11-28 16:29:47 -06:00
James Seibel 1c44ef7f0c minor reformatting 2025-11-28 16:23:36 -06:00
James Seibel 227d0d09ba fix getDataPointAtBlockPos() relative Y 2025-11-28 15:53:47 -06:00
James Seibel d7ba3fa724 fix LOD only mode when transparency is disabled 2025-11-28 15:53:38 -06:00
James Seibel 7e46adf469 add the ability to ignore update chunk pos 2025-11-28 10:48:42 -06:00
James Seibel f43e2fa441 don't render thick snow layers 2025-11-28 09:39:03 -06:00
James Seibel f9819d3d46 fix vanilla fading for MC versions before 1.21.5 2025-11-28 08:42:20 -06:00
James Seibel 19b23bea5f add slow world gen warning config 2025-11-27 09:59:16 -06:00
James Seibel d1c0f7ebb4 Update .editorconfig 2025-11-26 13:55:33 -06:00
James Seibel 5a4ddafbbb Z_std_stream localization 2025-11-26 13:52:17 -06:00
James Seibel 7c40d96f2e DhApiTerrainDataPoint to string 2025-11-26 13:52:07 -06:00
James Seibel b535be16c0 auto merge API world gen data
done to reduce memory use with broken API world generators
2025-11-26 13:51:58 -06:00
James Seibel 22f5608f9a hide the compressor config option 2025-11-24 14:31:42 -06:00
James Seibel a498422843 stream cleanup 3 2025-11-24 14:30:17 -06:00
James Seibel bfd6efb4a4 handle ZStd streams 2025-11-24 14:28:06 -06:00
James Seibel c8c9df3a34 data stream cleanup 2025-11-24 14:15:23 -06:00
James Seibel 3349e5b898 clean up DhDataInputStream 2025-11-24 13:51:48 -06:00
James Seibel ed7511ff6a proof-of-concept block Zstd compression 2025-11-24 12:40:49 -06:00
James Seibel 8516e8f9ab re-enable varint unit tests 2025-11-24 12:38:34 -06:00
James Seibel 47a4d1535f minor variable refactoring 2025-11-22 11:01:53 -06:00
James Seibel 33a55dc7cd Delete EventTimer.java 2025-11-22 09:30:00 -06:00
James Seibel 1b4f9e8942 minor throw/this cleanup 2025-11-22 09:24:31 -06:00
James Seibel 2537c4a259 Rename IBatchGeneratorEvnWrapper 2025-11-22 08:16:30 -06:00
James Seibel b74b6e8068 minor RollingAverage refactor 2025-11-22 08:16:11 -06:00
James Seibel 25979d6a76 Move some exception logic into ExceptionUtil 2025-11-21 06:59:03 -06:00
James Seibel 3f287388d5 re-add biome blending to API config options 2025-11-18 07:42:43 -06:00
James Seibel 72d2ba6aae comment out phantom buffer cleanup log 2025-11-18 07:32:58 -06:00
James Seibel 611ed4e24a add mod note in memory low message 2025-11-18 07:32:48 -06:00
James Seibel eac7a38e73 hopefully reduce the chance of downsampling holes 2025-11-18 07:32:18 -06:00
James Seibel afd7da7763 Optimize full data update processing 2025-11-18 07:16:50 -06:00
James Seibel ff7abb6a18 Fix rendering when Iris isn't installed 2025-11-16 16:11:40 -06:00
James Seibel ca3f5da5de Add unit test for data source merging speed 2025-11-16 15:30:16 -06:00
James Seibel 69012ab7e6 rename and cleanup data source update methods 2025-11-16 15:29:13 -06:00
James Seibel e5e502b4f8 Remove unused/broken FullData LevelMinY 2025-11-15 19:09:16 -06:00
James Seibel 42dc0903de Fix shaders when far clip fading is active 2025-11-15 18:20:47 -06:00
James Seibel 4b20637e47 Fix WorldGen after restarting generation 2025-11-15 12:07:53 -06:00
James Seibel 3257ae8480 replace server tick/world gen tick with a timer 2025-11-15 09:47:15 -06:00
James Seibel a6ddc561a0 up protocol version 12 -> 13 2025-11-15 09:42:38 -06:00
James Seibel 7c82c9eb7b add adj data to DTO en/decoding 2025-11-15 09:42:20 -06:00
James Seibel 3c62e18502 Fix gitlab getter Long/Int cast 2025-11-15 07:55:56 -06:00
James Seibel eea5198fb6 Merge branch 'adjData' 2025-11-14 07:46:37 -06:00
James Seibel b82a59ecbc Speed up shutdown and reduce logging 2025-11-14 07:46:02 -06:00
James Seibel 6bfcf36687 Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2025-11-13 07:19:19 -06:00
James Seibel 6fe0df7d0f Don't duplicate adjacent data 2025-11-13 07:18:09 -06:00
James Seibel b9746381eb Add varint encoding for full data
Closes Merge !93
Thanks Ryan Hitchman!
2025-11-12 07:21:54 -06:00
s809 91dffa3c3e Prevent auto-pause while pregen is running 2025-11-11 23:48:13 +05:00
James Seibel 6eb24ecde1 re-add GPU upload config including "none" 2025-11-10 07:33:03 -06:00
James Seibel 767753c004 add logging to infinite repo unit test 2025-11-10 06:56:24 -06:00
James Seibel 97442f8833 Fix config min/max validation default setup 2025-11-08 19:11:56 -06:00
James Seibel 62359e3dde remove LOD load pref logging 2025-11-08 19:08:30 -06:00
James Seibel b5199cfa87 Optimize ColumnBox building 2025-11-08 18:08:02 -06:00
James Seibel f0acc73c56 Add compass Index to Edirection 2025-11-08 17:48:30 -06:00
James Seibel f9dfc38bf1 Separate BlockBiomeWrapperPair from FullDataPointIdMap 2025-11-08 17:47:50 -06:00
James Seibel 5c5d39738e minor reformating 2025-11-08 17:44:08 -06:00
James Seibel 27fb629c22 default unsafe UI values to config option 2025-11-08 17:41:07 -06:00
James Seibel c374bf7ca8 test 2025-11-08 08:14:03 -06:00
James Seibel 7e04b12e37 Optimize PrefRecorder slightly 2025-11-07 07:41:59 -06:00
James Seibel 67637dbf10 detail level renaming 2025-11-06 21:50:43 -06:00
James Seibel 6456651d27 Handle non-adjacent data conversion 2025-11-06 21:28:25 -06:00
James Seibel 9343854b4a Clean up data source getters 2025-11-06 07:42:58 -06:00
James Seibel 5fd8ed840f Add adjacent data to FullDataDTO for faster loading 2025-11-06 07:35:23 -06:00
James Seibel 4d4d8fd8e9 Split up full data source provider into multiple classes 2025-11-04 07:46:06 -06:00
James Seibel bf05965015 remove IDataSource 2025-11-02 07:20:07 -06:00
James Seibel 47569f2b3c minor dataSourceHandler refactor 2025-11-01 16:33:07 -04:00
James Seibel 0567195f73 minor datasource renaming 2025-11-01 16:27:54 -04:00
James Seibel e355366ffc Clean up EDhDirection 2025-11-01 09:06:53 -04:00
James Seibel 3681d50eb2 minor comment cleanup 2025-11-01 08:42:25 -04:00
James Seibel e0c143881f Fix compression mode javadoc 2025-11-01 08:34:02 -04:00
James Seibel 2a49fdee7f Add experimental loading option and perfRecorder 2025-10-28 07:46:53 -05:00
James Seibel f39e06b6dc remove unused interrupt check 2025-10-28 07:36:28 -05:00
James Seibel 0d5c454dd4 remove unused ColumnQuadView methods 2025-10-28 07:24:24 -05:00
James Seibel 1b447fdc98 Fix logger builder doubling DH name 2025-10-28 07:23:58 -05:00
James Seibel d84ba05380 minor style reformatting 2025-10-27 06:52:36 -05:00
James Seibel 3e7f160fcd Merge Fade apply shaders 2025-10-25 11:54:32 -05:00
James Seibel dcaf334828 use same fade apply frag shader 2025-10-25 11:39:27 -05:00
James Seibel 789306ccff Add far clip fading 2025-10-25 11:06:19 -05:00
James Seibel e33fa3cb5e Rename fade renderer -> Vanilla Fade renderer 2025-10-25 09:36:11 -05:00
James Seibel 8f99117066 Fix iris not setting face culling in the MC state manager 2025-10-25 08:38:29 -05:00
James Seibel 2136c0fe83 framebuffer name consistency fix 2025-10-25 08:37:11 -05:00
James Seibel 7a6cffe19d Move getKeyedLevelDimensionName() to implementation 2025-10-23 07:17:46 -05:00
James Seibel 06bef93c82 run occlusion culling whenever saving a LOD
Also run culling for every column in an LOD, which improves compression by about 20%
- Thanks Scaevolus
2025-10-22 07:25:04 -05:00
James Seibel 939e45ce62 minor RenderBufferHandler optimization and bugfix 2025-10-19 16:40:57 -05:00
James Seibel 7f958269e4 Fix not reloading LODs on horizontal quality change 2025-10-19 16:16:35 -05:00
James Seibel 07e3091d13 Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2025-10-19 16:06:24 -05:00
James Seibel f7ece2b02e Clean up LodRendering logic 2025-10-19 16:06:00 -05:00
s809 bd796c2ce0 Fix handling of empty server keys 2025-10-19 22:58:07 +05:00
s809 4e6be35da9 Merge branch 'feature/server-keys' 2025-10-19 22:57:50 +05:00
James Seibel 0e0e1e1b0f Make LodRenderer a singleton 2025-10-18 11:42:36 -05:00
James Seibel f4ab101403 Dh and level wrapper refactoring and commenting 2025-10-17 07:21:16 -05:00
James Seibel 0902d3f0f5 merge loggers and add logger builder 2025-10-15 17:37:08 -05:00
James Seibel 75c2758fd5 up version number 2.3.6 -> 2.3.7 2025-10-13 18:03:19 -05:00
s809 034ec7d656 Bump protocol version 2025-08-16 21:01:45 +05:00
s809 fb5e15a2f1 Add a server keys feature 2025-08-16 20:59:28 +05:00
403 changed files with 19254 additions and 23684 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ insert_final_newline = false
max_line_length = 1000 max_line_length = 1000
tab_width = 4 tab_width = 4
trim_trailing_whitespace = false trim_trailing_whitespace = false
ij_continuation_indent_size = 8 ij_continuation_indent_size = 4
ij_formatter_off_tag = @formatter:off ij_formatter_off_tag = @formatter:off
ij_formatter_on_tag = @formatter:on ij_formatter_on_tag = @formatter:on
ij_formatter_tags_enabled = true ij_formatter_tags_enabled = true
@@ -39,10 +39,6 @@ package com.seibel.distanthorizons.api.enums;
*/ */
public enum EDhApiDetailLevel public enum EDhApiDetailLevel
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** /**
* detail level: 0 <Br> * detail level: 0 <Br>
* width in Blocks: 1 * width in Blocks: 1
@@ -28,10 +28,6 @@ package com.seibel.distanthorizons.api.enums.config;
*/ */
public enum EDhApiBlocksToAvoid public enum EDhApiBlocksToAvoid
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
NONE(false), NONE(false),
NON_COLLIDING(true); NON_COLLIDING(true);
@@ -22,7 +22,9 @@ package com.seibel.distanthorizons.api.enums.config;
/** /**
* UNCOMPRESSED <br> * UNCOMPRESSED <br>
* LZ4 <br> * LZ4 <br>
* XZ <br><br> * Z_STD <br>
* Z_STD_STREAM <br>
* LZMA2 <br><br>
* *
* Note: speed and compression ratios are examples * Note: speed and compression ratios are examples
* and should only be used for estimated comparisons. * and should only be used for estimated comparisons.
@@ -32,10 +34,6 @@ package com.seibel.distanthorizons.api.enums.config;
*/ */
public enum EDhApiDataCompressionMode public enum EDhApiDataCompressionMode
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** /**
* Should only be used internally and for unit testing. <br><br> * Should only be used internally and for unit testing. <br><br>
* *
@@ -56,18 +54,32 @@ public enum EDhApiDataCompressionMode
LZ4(1), LZ4(1),
/** /**
* Decent speed and good compression. <br><br> * Great speed and good compression. <br><br>
* *
* Read Speed: 2.1 MS / DTO <br>
* Write Speed: 4.9 MS / DTO <br>
* Compression ratio: 0.2606 <br>
*/
Z_STD_BLOCK(4),
/**
* Similar to {@link EDhApiDataCompressionMode#Z_STD_BLOCK}
* except slower. <br><br>
*
* This option is only provided for legacy support when processing old databases. <br><br>
*
* Read Speed: 9.31 MS / DTO <br> * Read Speed: 9.31 MS / DTO <br>
* Write Speed: 15.13 MS / DTO <br> * Write Speed: 15.13 MS / DTO <br>
* Compression ratio: 0.2606 <br> * Compression ratio: 0.2606 <br>
*/ */
Z_STD(2), @Deprecated
@DisallowSelectingViaConfigGui
Z_STD_STREAM(2),
/** /**
* Extremely slow, but very good compression. <br><br> * Extremely slow, but very good compression. <br>
* * Often causes whole computer stuttering due to memory bandwidth saturation. <br><br>
*
* Read Speed: 13.29 MS / DTO <br> * Read Speed: 13.29 MS / DTO <br>
* Write Speed: 70.95 MS / DTO <br> * Write Speed: 70.95 MS / DTO <br>
* Compression ratio: 0.2068 <br> * Compression ratio: 0.2068 <br>
@@ -35,25 +35,12 @@ public enum EDhApiGpuUploadMethod
/** Picks the best option based on the GPU the user has. */ /** Picks the best option based on the GPU the user has. */
AUTO(false, false), AUTO(false, false),
// commented out since it isn't currently in use
//BUFFER_STORAGE_MAPPING(true, true),
/** Fast rendering, no stuttering. */ /** Fast rendering, no stuttering. */
BUFFER_STORAGE(false, true), BUFFER_STORAGE(false, true),
/** Fast rendering but may stutter when uploading. */ /** Fast rendering but may stutter when uploading. */
SUB_DATA(false, false), SUB_DATA(false, false),
/**
* May end up storing buffers in System memory. <br>
* Fast rending if in GPU memory, slow if in system memory, <br>
* but won't stutter when uploading.
*
* @deprecated not currently supported
*/
@Deprecated
BUFFER_MAPPING(true, false),
/** Fast rendering but may stutter when uploading. */ /** Fast rendering but may stutter when uploading. */
DATA(false, false); DATA(false, false);
@@ -29,10 +29,6 @@ package com.seibel.distanthorizons.api.enums.config;
*/ */
public enum EDhApiGrassSideRendering public enum EDhApiGrassSideRendering
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
AS_GRASS, AS_GRASS,
FADE_TO_DIRT, FADE_TO_DIRT,
AS_DIRT; AS_DIRT;
@@ -31,11 +31,6 @@ package com.seibel.distanthorizons.api.enums.config;
*/ */
public enum EDhApiHorizontalQuality public enum EDhApiHorizontalQuality
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
// Note: any quadraticBase less than 2.0f has issues with DetailDistanceUtil, and will always return the lowest detail level. // Note: any quadraticBase less than 2.0f has issues with DetailDistanceUtil, and will always return the lowest detail level.
// So for now we are limiting the lowest value to 2.0 // So for now we are limiting the lowest value to 2.0
// LOWEST was originally 1.0f and LOW was 1.5f // LOWEST was originally 1.0f and LOW was 1.5f
@@ -29,10 +29,6 @@ package com.seibel.distanthorizons.api.enums.config;
*/ */
public enum EDhApiLodShading public enum EDhApiLodShading
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** /**
* Uses Minecraft's shading for LODs. <Br> * Uses Minecraft's shading for LODs. <Br>
* This means if Minecraft's shading is disabled DH's shading will be as well. * This means if Minecraft's shading is disabled DH's shading will be as well.
@@ -17,23 +17,37 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.seibel.distanthorizons.core.generation.tasks; package com.seibel.distanthorizons.api.enums.config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import org.apache.logging.log4j.Level;
import javax.annotation.Nullable;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
/** /**
* @author Leetom * ALL
* @version 2022-11-25 * DEBUG
* INFO
* WARN
* ERROR
* DISABLED
*
* @since API 5.0.0
* @version 2024-4-6
*/ */
public interface IWorldGenTaskTracker public enum EDhApiLoggerLevel
{ {
@Nullable // ordered from most to least broad
Consumer<FullDataSourceV2> getDataSourceConsumer(); ALL(Level.ALL),
DEBUG(Level.DEBUG),
INFO(Level.INFO),
WARN(Level.WARN),
ERROR(Level.ERROR),
DISABLED(Level.OFF),
;
public final Level level;
EDhApiLoggerLevel(Level level)
{ this.level = level; }
CompletableFuture<Boolean> shouldGenerateSplitChild(long pos);
} }
@@ -1,55 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.api.enums.config;
import org.apache.logging.log4j.Level;
/**
* @since API 2.0.0
* @version 2024-4-6
*/
public enum EDhApiLoggerMode
{
DISABLED(Level.OFF, Level.OFF),
LOG_ALL_TO_FILE(Level.ALL, Level.OFF),
LOG_ERROR_TO_CHAT(Level.ALL, Level.ERROR),
LOG_WARNING_TO_CHAT(Level.ALL, Level.WARN),
LOG_INFO_TO_CHAT(Level.ALL, Level.INFO),
LOG_DEBUG_TO_CHAT(Level.ALL, Level.DEBUG),
LOG_ALL_TO_CHAT(Level.ALL, Level.ALL),
LOG_ERROR_TO_CHAT_AND_FILE(Level.ERROR, Level.ERROR),
LOG_WARNING_TO_CHAT_AND_FILE(Level.WARN, Level.WARN),
LOG_INFO_TO_CHAT_AND_FILE(Level.INFO, Level.INFO),
LOG_DEBUG_TO_CHAT_AND_FILE(Level.DEBUG, Level.DEBUG),
LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE(Level.INFO, Level.WARN),
LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE(Level.INFO, Level.ERROR),
LOG_ERROR_TO_CHAT_AND_WARNING_TO_FILE(Level.ERROR, Level.WARN),
;
public final Level levelForFile;
public final Level levelForChat;
EDhApiLoggerMode(Level levelForFile, Level levelForChat)
{
this.levelForFile = levelForFile;
this.levelForChat = levelForChat;
}
}
@@ -64,10 +64,7 @@ public enum EDhApiMaxHorizontalResolution
/** How wide each LOD DataPoint is */ /** How wide each LOD DataPoint is */
public final int dataPointWidth; public final int dataPointWidth;
/** /** This is the same as detailLevel in LodQuadTreeNode */
* This is the same as detailLevel in LodQuadTreeNode,
* lowest is 0 highest is 9
*/
public final byte detailLevel; public final byte detailLevel;
/* Start/End X/Z give the block positions /* Start/End X/Z give the block positions
@@ -33,10 +33,6 @@ package com.seibel.distanthorizons.api.enums.config;
*/ */
public enum EDhApiMcRenderingFadeMode public enum EDhApiMcRenderingFadeMode
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** /**
* No fading is done, there will be a pronounced border between * No fading is done, there will be a pronounced border between
* Minecraft and Distant Horizons. <br> * Minecraft and Distant Horizons. <br>
@@ -0,0 +1,19 @@
package com.seibel.distanthorizons.api.enums.config;
import com.seibel.distanthorizons.coreapi.ModInfo;
/**
* AUTO, <br>
* OPEN_GL, <br>
* BLAZE_3D, <br><br>
*
* @since API 6.0.0
* @version 2026-3-10
*/
public enum EDhApiRenderApi
{
AUTO,
OPEN_GL,
BLAZE_3D;
}
@@ -35,11 +35,6 @@ package com.seibel.distanthorizons.api.enums.config;
*/ */
public enum EDhApiServerFolderNameMode public enum EDhApiServerFolderNameMode
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** Only use the server name */ /** Only use the server name */
NAME_ONLY, NAME_ONLY,
@@ -34,11 +34,6 @@ package com.seibel.distanthorizons.api.enums.config;
@Deprecated // not currently in use, if the config this enum represents is re-implemented, the deprecated flag can be removed @Deprecated // not currently in use, if the config this enum represents is re-implemented, the deprecated flag can be removed
public enum EDhApiVanillaOverdraw public enum EDhApiVanillaOverdraw
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** /**
* Don't draw LODs where a minecraft chunk could be. * Don't draw LODs where a minecraft chunk could be.
* Use Overdraw Offset to tweak the border thickness. * Use Overdraw Offset to tweak the border thickness.
@@ -53,7 +53,7 @@ public enum EDhApiVerticalQuality
public int calculateMaxVerticalData(byte dataDetail) public int calculateMaxNumberOfVerticalSlicesAtDetailLevel(byte dataDetail)
{ {
// for detail levels lower than what the enum defines, use the lowest quality item // for detail levels lower than what the enum defines, use the lowest quality item
int index = MathUtil.clamp(0, dataDetail, this.maxVerticalData.length - 1); int index = MathUtil.clamp(0, dataDetail, this.maxVerticalData.length - 1);
@@ -28,10 +28,6 @@ package com.seibel.distanthorizons.api.enums.config;
*/ */
public enum EDhApiWorldCompressionMode public enum EDhApiWorldCompressionMode
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** /**
* Every block/biome change is recorded in the database. <br> * Every block/biome change is recorded in the database. <br>
* This is what DH 2.0 and 2.0.1 all used by default and will store a lot of data. * This is what DH 2.0 and 2.0.1 all used by default and will store a lot of data.
@@ -35,10 +35,6 @@ import com.seibel.distanthorizons.api.enums.config.DisallowSelectingViaConfigGui
*/ */
public enum EDhApiQualityPreset public enum EDhApiQualityPreset
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
@DisallowSelectingViaConfigGui @DisallowSelectingViaConfigGui
CUSTOM, CUSTOM,
@@ -34,10 +34,6 @@ import com.seibel.distanthorizons.api.enums.config.DisallowSelectingViaConfigGui
*/ */
public enum EDhApiThreadPreset public enum EDhApiThreadPreset
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
@DisallowSelectingViaConfigGui @DisallowSelectingViaConfigGui
CUSTOM, CUSTOM,
@@ -33,11 +33,6 @@ package com.seibel.distanthorizons.api.enums.rendering;
*/ */
public enum EDhApiDebugRendering public enum EDhApiDebugRendering
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** LODs are rendered normally */ /** LODs are rendered normally */
OFF, OFF,
@@ -48,11 +43,7 @@ public enum EDhApiDebugRendering
SHOW_BLOCK_MATERIAL, SHOW_BLOCK_MATERIAL,
/** Only draw overlapping LOD quads. */ /** Only draw overlapping LOD quads. */
SHOW_OVERLAPPING_QUADS, SHOW_OVERLAPPING_QUADS;
/** LOD colors are based on renderSource flags. */
SHOW_RENDER_SOURCE_FLAG;
public static EDhApiDebugRendering next(EDhApiDebugRendering type) public static EDhApiDebugRendering next(EDhApiDebugRendering type)
{ {
@@ -65,7 +56,7 @@ public enum EDhApiDebugRendering
case SHOW_BLOCK_MATERIAL: case SHOW_BLOCK_MATERIAL:
return SHOW_OVERLAPPING_QUADS; return SHOW_OVERLAPPING_QUADS;
case SHOW_OVERLAPPING_QUADS: case SHOW_OVERLAPPING_QUADS:
return SHOW_RENDER_SOURCE_FLAG; return OFF;
default: default:
return OFF; return OFF;
} }
@@ -76,8 +67,6 @@ public enum EDhApiDebugRendering
switch (type) switch (type)
{ {
case OFF: case OFF:
return SHOW_RENDER_SOURCE_FLAG;
case SHOW_RENDER_SOURCE_FLAG:
return SHOW_OVERLAPPING_QUADS; return SHOW_OVERLAPPING_QUADS;
case SHOW_OVERLAPPING_QUADS: case SHOW_OVERLAPPING_QUADS:
return SHOW_DETAIL; return SHOW_DETAIL;
@@ -33,10 +33,6 @@ package com.seibel.distanthorizons.api.enums.rendering;
@Deprecated @Deprecated
public enum EDhApiFogDrawMode public enum EDhApiFogDrawMode
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** /**
* Use whatever Fog setting Optifine is using. * Use whatever Fog setting Optifine is using.
* If Optifine isn't installed this defaults to {@link EDhApiFogDrawMode#FOG_ENABLED}. * If Optifine isn't installed this defaults to {@link EDhApiFogDrawMode#FOG_ENABLED}.
@@ -30,11 +30,6 @@ package com.seibel.distanthorizons.api.enums.rendering;
*/ */
public enum EDhApiFogFalloff public enum EDhApiFogFalloff
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
LINEAR(0), LINEAR(0),
EXPONENTIAL(1), EXPONENTIAL(1),
EXPONENTIAL_SQUARED(2); EXPONENTIAL_SQUARED(2);
@@ -33,11 +33,6 @@ package com.seibel.distanthorizons.api.enums.rendering;
*/ */
public enum EDhApiHeightFogDirection public enum EDhApiHeightFogDirection
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
ABOVE_CAMERA (true, true, false), ABOVE_CAMERA (true, true, false),
BELOW_CAMERA (true, false, true), BELOW_CAMERA (true, false, true),
ABOVE_AND_BELOW_CAMERA (true, true, true), ABOVE_AND_BELOW_CAMERA (true, true, true),
@@ -29,11 +29,6 @@ package com.seibel.distanthorizons.api.enums.rendering;
*/ */
public enum EDhApiRendererMode public enum EDhApiRendererMode
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
DEFAULT, DEFAULT,
DEBUG, DEBUG,
DISABLED; DISABLED;
@@ -29,11 +29,6 @@ package com.seibel.distanthorizons.api.enums.rendering;
*/ */
public enum EDhApiTransparency public enum EDhApiTransparency
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
DISABLED(false, false), DISABLED(false, false),
FAKE(true, true), FAKE(true, true),
COMPLETE(true, false); COMPLETE(true, false);
@@ -34,11 +34,6 @@ package com.seibel.distanthorizons.api.enums.worldGeneration;
*/ */
public enum EDhApiDistantGeneratorMode public enum EDhApiDistantGeneratorMode
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** Don't generate any new terrain, just generate LODs for already generated chunks. */ /** Don't generate any new terrain, just generate LODs for already generated chunks. */
PRE_EXISTING_ONLY((byte) 1), PRE_EXISTING_ONLY((byte) 1),
@@ -31,11 +31,6 @@ package com.seibel.distanthorizons.api.enums.worldGeneration;
*/ */
public enum EDhApiDistantGeneratorProgressDisplayLocation public enum EDhApiDistantGeneratorProgressDisplayLocation
{ {
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
OVERLAY, OVERLAY,
CHAT, CHAT,
LOG, LOG,
@@ -23,10 +23,10 @@ import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigGroup;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue; import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
/** /**
* Distant Horizons' fog configuration. <br><br> * Distant Horizons' SSAO configuration. <br><br>
* *
* @author James Seibel * @author James Seibel
* @version 2022-9-6 * @version 2026-02-05
* @since API 1.0.0 * @since API 1.0.0
*/ */
public interface IDhApiAmbientOcclusionConfig extends IDhApiConfigGroup public interface IDhApiAmbientOcclusionConfig extends IDhApiConfigGroup
@@ -34,32 +34,4 @@ public interface IDhApiAmbientOcclusionConfig extends IDhApiConfigGroup
/** Determines if Ambient Occlusion is rendered */ /** Determines if Ambient Occlusion is rendered */
IDhApiConfigValue<Boolean> enabled(); IDhApiConfigValue<Boolean> enabled();
/**
* Determines how many points in space are sampled for the occlusion test.
* Higher numbers will improve quality and reduce banding, but will increase GPU load.
*/
IDhApiConfigValue<Integer> sampleCount();
/** Determines the radius Screen Space Ambient Occlusion is applied, measured in blocks. */
IDhApiConfigValue<Double> radius();
/** Determines how dark the Screen Space Ambient Occlusion effect will be. */
IDhApiConfigValue<Double> strength();
/** Increasing the value can reduce banding at the cost of reducing the strength of the effect. */
IDhApiConfigValue<Double> bias();
/**
* Determines how dark the occlusion shadows can be. <br>
* 0 = totally black at the corners <br>
* 1 = no shadow
*/
IDhApiConfigValue<Double> minLight();
/**
* The radius, measured in pixels, that blurring is calculated. <br>
* Higher numbers will reduce banding at the cost of GPU performance.
*/
IDhApiConfigValue<Integer> blurRadius();
} }
@@ -44,7 +44,7 @@ public interface IDhApiFarFogConfig extends IDhApiConfigGroup
* 0.0 = fog starts at the camera <br> * 0.0 = fog starts at the camera <br>
* 1.0 = fog starts at the edge of the fake chunk render distance <br> * 1.0 = fog starts at the edge of the fake chunk render distance <br>
*/ */
IDhApiConfigValue<Double> farFogStartDistance(); IDhApiConfigValue<Float> farFogStartDistance();
/** /**
* Defines where the fog ends as a percent of the radius * Defines where the fog ends as a percent of the radius
@@ -54,18 +54,18 @@ public interface IDhApiFarFogConfig extends IDhApiConfigGroup
* 0.0 = fog ends at the camera <br> * 0.0 = fog ends at the camera <br>
* 1.0 = fog ends at the edge of the fake chunk render distance <br> * 1.0 = fog ends at the edge of the fake chunk render distance <br>
*/ */
IDhApiConfigValue<Double> farFogEndDistance(); IDhApiConfigValue<Float> farFogEndDistance();
/** Defines how opaque the fog is at its thinnest point. */ /** Defines how opaque the fog is at its thinnest point. */
IDhApiConfigValue<Double> farFogMinThickness(); IDhApiConfigValue<Float> farFogMinThickness();
/** Defines how opaque the fog is at its thickest point. */ /** Defines how opaque the fog is at its thickest point. */
IDhApiConfigValue<Double> farFogMaxThickness(); IDhApiConfigValue<Float> farFogMaxThickness();
/** Defines how the fog changes in thickness. */ /** Defines how the fog changes in thickness. */
IDhApiConfigValue<EDhApiFogFalloff> farFogFalloff(); IDhApiConfigValue<EDhApiFogFalloff> farFogFalloff();
/** Defines the fog density. */ /** Defines the fog density. */
IDhApiConfigValue<Double> farFogDensity(); IDhApiConfigValue<Float> farFogDensity();
} }
@@ -104,7 +104,7 @@ public interface IDhApiGraphicsConfig extends IDhApiConfigGroup
* 2 = blending of 5x5 <br> * 2 = blending of 5x5 <br>
* ... <br> * ... <br>
*/ */
// IDhApiConfigValue<Integer> getBiomeBlending(); IDhApiConfigValue<Integer> getBiomeBlending();
@@ -124,19 +124,19 @@ public interface IDhApiGraphicsConfig extends IDhApiConfigGroup
* *
* @since API 2.0.0 * @since API 2.0.0
*/ */
IDhApiConfigValue<Double> overdrawPreventionRadius(); IDhApiConfigValue<Float> overdrawPreventionRadius();
/** /**
* Modifies how bright fake chunks are. <br> * Modifies how bright fake chunks are. <br>
* This is done when generating the vertex data and is applied before any shaders. * This is done when generating the vertex data and is applied before any shaders.
*/ */
IDhApiConfigValue<Double> brightnessMultiplier(); IDhApiConfigValue<Float> brightnessMultiplier();
/** /**
* Modifies how saturated fake chunks are. <br> * Modifies how saturated fake chunks are. <br>
* This is done when generating the vertex data and is applied before any shaders. * This is done when generating the vertex data and is applied before any shaders.
*/ */
IDhApiConfigValue<Double> saturationMultiplier(); IDhApiConfigValue<Float> saturationMultiplier();
/** Defines if Distant Horizons should attempt to cull fake chunk cave geometry. */ /** Defines if Distant Horizons should attempt to cull fake chunk cave geometry. */
IDhApiConfigValue<Boolean> caveCullingEnabled(); IDhApiConfigValue<Boolean> caveCullingEnabled();
@@ -150,12 +150,6 @@ public interface IDhApiGraphicsConfig extends IDhApiConfigGroup
/** If enabled vanilla chunk rendering is disabled and only fake chunks are rendered. */ /** If enabled vanilla chunk rendering is disabled and only fake chunks are rendered. */
IDhApiConfigValue<Boolean> lodOnlyMode(); IDhApiConfigValue<Boolean> lodOnlyMode();
/**
* Setting this to a non-zero number will modify vanilla Minecraft's LOD Bias,
* increasing how quickly its textures fade away.
*/
IDhApiConfigValue<Double> lodBias();
/** /**
* Determines how LODs should be shaded. * Determines how LODs should be shaded.
* *
@@ -52,24 +52,24 @@ public interface IDhApiHeightFogConfig extends IDhApiConfigGroup
* Defines the height fog's base height if {@link IDhApiHeightFogConfig#heightFogDirection()} * Defines the height fog's base height if {@link IDhApiHeightFogConfig#heightFogDirection()}
* is set to use a specific height. * is set to use a specific height.
*/ */
IDhApiConfigValue<Double> heightFogBaseHeight(); IDhApiConfigValue<Float> heightFogBaseHeight();
/** Defines the height fog's starting height as a percent of the world height. */ /** Defines the height fog's starting height as a percent of the world height. */
IDhApiConfigValue<Double> heightFogStartingHeightPercent(); IDhApiConfigValue<Float> heightFogStartingHeightPercent();
/** Defines the height fog's ending height as a percent of the world height. */ /** Defines the height fog's ending height as a percent of the world height. */
IDhApiConfigValue<Double> heightFogEndingHeightPercent(); IDhApiConfigValue<Float> heightFogEndingHeightPercent();
/** Defines how opaque the height fog is at its thinnest point. */ /** Defines how opaque the height fog is at its thinnest point. */
IDhApiConfigValue<Double> heightFogMinThickness(); IDhApiConfigValue<Float> heightFogMinThickness();
/** Defines how opaque the height fog is at its thickest point. */ /** Defines how opaque the height fog is at its thickest point. */
IDhApiConfigValue<Double> heightFogMaxThickness(); IDhApiConfigValue<Float> heightFogMaxThickness();
/** Defines how the height fog changes in thickness. */ /** Defines how the height fog changes in thickness. */
IDhApiConfigValue<EDhApiFogFalloff> heightFogFalloff(); IDhApiConfigValue<EDhApiFogFalloff> heightFogFalloff();
/** Defines the height fog's density. */ /** Defines the height fog's density. */
IDhApiConfigValue<Double> heightFogDensity(); IDhApiConfigValue<Float> heightFogDensity();
} }
@@ -37,8 +37,8 @@ public interface IDhApiNoiseTextureConfig extends IDhApiConfigGroup
/** Defines how many steps of noise should be applied. */ /** Defines how many steps of noise should be applied. */
IDhApiConfigValue<Integer> noiseSteps(); IDhApiConfigValue<Integer> noiseSteps();
/** Defines how intense the noise will be. */ /** Defines how intense the noise will be, between 0.0 and 1.0. */
IDhApiConfigValue<Double> noiseIntensity(); IDhApiConfigValue<Float> noiseIntensity();
/** /**
* Defines how far should the noise texture render before it fades away. (in blocks) <br> * Defines how far should the noise texture render before it fades away. (in blocks) <br>
@@ -1,15 +1,18 @@
package com.seibel.distanthorizons.api.interfaces.data; package com.seibel.distanthorizons.api.interfaces.data;
/** /**
* Can be used to drastically speed up repeat read operations in {@link IDhApiTerrainDataRepo}. * Can be used to drastically speed up repeat read operations in {@link IDhApiTerrainDataRepo}. <br><br>
* *
* Once you are done with this cache, closing it will free up any objects
* the cache is holding. This can reduce Garbage Collector overhead and reduce stuttering.
*
* @see IDhApiTerrainDataRepo * @see IDhApiTerrainDataRepo
* *
* @author James Seibel * @author James Seibel
* @version 2024-7-14 * @version 2026-1-29
* @since API 3.0.0 * @since API 3.0.0
*/ */
public interface IDhApiTerrainDataCache // TODO should this be AutoClosable? public interface IDhApiTerrainDataCache extends AutoCloseable
{ {
/** /**
* Removes any data that's currently stored in this cache. * Removes any data that's currently stored in this cache.
@@ -32,7 +32,7 @@ import com.seibel.distanthorizons.api.objects.data.DhApiTerrainDataPoint;
* @see IDhApiTerrainDataCache * @see IDhApiTerrainDataCache
* *
* @author James Seibel * @author James Seibel
* @version 2023-6-22 * @version 2026-02-03
* @since API 1.0.0 * @since API 1.0.0
*/ */
public interface IDhApiTerrainDataRepo public interface IDhApiTerrainDataRepo
@@ -42,24 +42,17 @@ public interface IDhApiTerrainDataRepo
// getters // // getters //
//=========// //=========//
/** @see IDhApiTerrainDataRepo#getSingleDataPointAtBlockPos(IDhApiLevelWrapper, int, int, int, IDhApiTerrainDataCache) */
default DhApiResult<DhApiTerrainDataPoint> getSingleDataPointAtBlockPos(IDhApiLevelWrapper levelWrapper, int blockPosX, int blockPosY, int blockPosZ) { return this.getSingleDataPointAtBlockPos(levelWrapper, blockPosX, blockPosY, blockPosZ, null); }
/** /**
* Returns the terrain datapoint at the given block position, at/or containing the given Y position. * Returns the terrain datapoint at the given block position, at/or containing the given Y position.
* @since API 3.0.0 * @since API 3.0.0
*/ */
DhApiResult<DhApiTerrainDataPoint> getSingleDataPointAtBlockPos(IDhApiLevelWrapper levelWrapper, int blockPosX, int blockPosY, int blockPosZ, IDhApiTerrainDataCache dataCache); DhApiResult<DhApiTerrainDataPoint> getSingleDataPointAtBlockPos(IDhApiLevelWrapper levelWrapper, int blockPosX, int blockPosY, int blockPosZ, IDhApiTerrainDataCache dataCache);
/** @see IDhApiTerrainDataRepo#getColumnDataAtBlockPos(IDhApiLevelWrapper, int, int, IDhApiTerrainDataCache) */
default DhApiResult<DhApiTerrainDataPoint[]> getColumnDataAtBlockPos(IDhApiLevelWrapper levelWrapper, int blockPosX, int blockPosZ) { return this.getColumnDataAtBlockPos(levelWrapper, blockPosX, blockPosZ, null); }
/** /**
* Returns every datapoint in the column located at the given block X and Z position top to bottom. * Returns every datapoint in the column located at the given block X and Z position top to bottom.
* @since API 3.0.0 * @since API 3.0.0
*/ */
DhApiResult<DhApiTerrainDataPoint[]> getColumnDataAtBlockPos(IDhApiLevelWrapper levelWrapper, int blockPosX, int blockPosZ, IDhApiTerrainDataCache dataCache); DhApiResult<DhApiTerrainDataPoint[]> getColumnDataAtBlockPos(IDhApiLevelWrapper levelWrapper, int blockPosX, int blockPosZ, IDhApiTerrainDataCache dataCache);
/** @see IDhApiTerrainDataRepo#getAllTerrainDataAtChunkPos(IDhApiLevelWrapper, int, int, IDhApiTerrainDataCache) */
default DhApiResult<DhApiTerrainDataPoint[][][]> getAllTerrainDataAtChunkPos(IDhApiLevelWrapper levelWrapper, int chunkPosX, int chunkPosZ) { return this.getAllTerrainDataAtChunkPos(levelWrapper, chunkPosX, chunkPosZ, null); }
/** /**
* Returns every datapoint in the given chunk's X and Z position. <br><br> * Returns every datapoint in the given chunk's X and Z position. <br><br>
* *
@@ -71,8 +64,6 @@ public interface IDhApiTerrainDataRepo
*/ */
DhApiResult<DhApiTerrainDataPoint[][][]> getAllTerrainDataAtChunkPos(IDhApiLevelWrapper levelWrapper, int chunkPosX, int chunkPosZ, IDhApiTerrainDataCache dataCache); DhApiResult<DhApiTerrainDataPoint[][][]> getAllTerrainDataAtChunkPos(IDhApiLevelWrapper levelWrapper, int chunkPosX, int chunkPosZ, IDhApiTerrainDataCache dataCache);
/** @see IDhApiTerrainDataRepo#getAllTerrainDataAtRegionPos(IDhApiLevelWrapper, int, int, IDhApiTerrainDataCache) */
default DhApiResult<DhApiTerrainDataPoint[][][]> getAllTerrainDataAtRegionPos(IDhApiLevelWrapper levelWrapper, int regionPosX, int regionPosZ) { return this.getAllTerrainDataAtRegionPos(levelWrapper, regionPosX, regionPosZ, null); }
/** /**
* Returns every datapoint in the given region's X and Z position. <br><br> * Returns every datapoint in the given region's X and Z position. <br><br>
* *
@@ -84,8 +75,6 @@ public interface IDhApiTerrainDataRepo
*/ */
DhApiResult<DhApiTerrainDataPoint[][][]> getAllTerrainDataAtRegionPos(IDhApiLevelWrapper levelWrapper, int regionPosX, int regionPosZ, IDhApiTerrainDataCache dataCache); DhApiResult<DhApiTerrainDataPoint[][][]> getAllTerrainDataAtRegionPos(IDhApiLevelWrapper levelWrapper, int regionPosX, int regionPosZ, IDhApiTerrainDataCache dataCache);
/** @see IDhApiTerrainDataRepo#getAllTerrainDataAtDetailLevelAndPos(IDhApiLevelWrapper, byte, int, int, IDhApiTerrainDataCache) */
default DhApiResult<DhApiTerrainDataPoint[][][]> getAllTerrainDataAtRegionPos(IDhApiLevelWrapper levelWrapper, byte detailLevel, int posX, int posZ) { return this.getAllTerrainDataAtDetailLevelAndPos(levelWrapper, detailLevel, posX, posZ, null); }
/** /**
* Returns every datapoint in the column located at the given detail level and X/Z position. <br> * Returns every datapoint in the column located at the given detail level and X/Z position. <br>
* This can be used to return terrain data for non-standard sizes (IE 2x2 blocks or 2x2 chunks). * This can be used to return terrain data for non-standard sizes (IE 2x2 blocks or 2x2 chunks).
@@ -101,21 +90,6 @@ public interface IDhApiTerrainDataRepo
/** @see IDhApiTerrainDataRepo#raycast(IDhApiLevelWrapper, double, double, double, float, float, float, int, IDhApiTerrainDataCache) */
default DhApiResult<DhApiRaycastResult> raycast(
IDhApiLevelWrapper levelWrapper,
double rayOriginX, double rayOriginY, double rayOriginZ,
float rayDirectionX, float rayDirectionY, float rayDirectionZ,
int maxRayBlockLength)
{
return this.raycast(
levelWrapper,
rayOriginX, rayOriginY, rayOriginZ,
rayDirectionX, rayDirectionY, rayDirectionZ,
maxRayBlockLength,
null);
}
/** /**
* Returns the datapoint and position of the LOD * Returns the datapoint and position of the LOD
* at the end of the given ray. <br><br> * at the end of the given ray. <br><br>
@@ -161,9 +135,16 @@ public interface IDhApiTerrainDataRepo
//=========// //=========//
/** /**
* Creates a new cache you manage that can be used to speed up repeat
* read operations. <br>
* Without a cache each operation must: hit the backing database file,
* decompress it, and parse it; which is a fairly slow process. <br>
*
* @return a {@link IDhApiTerrainDataCache} backed by {@link java.lang.ref.SoftReference}'s. * @return a {@link IDhApiTerrainDataCache} backed by {@link java.lang.ref.SoftReference}'s.
* @since API 3.0.0 * @since API 5.0.0
*/ */
IDhApiTerrainDataCache getSoftCache(); IDhApiTerrainDataCache createSoftCache();
} }
@@ -40,7 +40,7 @@ public interface IDhApiEventInjector extends IDependencyInjector<IDhApiEvent>
* @return true if the handler was unbound, false if the handler wasn't bound. * @return true if the handler was unbound, false if the handler wasn't bound.
* @throws IllegalArgumentException if the implementation object doesn't implement the interface * @throws IllegalArgumentException if the implementation object doesn't implement the interface
*/ */
// Note to self: Don't try adding a generic type to IDhApiEvent, the constructor won't accept it // Note to DH Devs: Don't try adding a generic type to IDhApiEvent, the constructor won't accept it
boolean unbind(Class<? extends IDhApiEvent> dependencyInterface, Class<? extends IDhApiEvent> dependencyClassToRemove) throws IllegalArgumentException; boolean unbind(Class<? extends IDhApiEvent> dependencyInterface, Class<? extends IDhApiEvent> dependencyClassToRemove) throws IllegalArgumentException;
@@ -169,7 +169,7 @@ public interface IDhApiWorldGenerator extends Closeable, IDhApiOverrideable
* *
* After the {@link IDhApiWorldGenerator} has been generated, it should be passed into the * After the {@link IDhApiWorldGenerator} has been generated, it should be passed into the
* resultConsumer's {@link Consumer#accept(Object)} method. * resultConsumer's {@link Consumer#accept(Object)} method.
* Note: if air blocks aren't included in the with the {@link DhApiChunk} with proper lighting, lower detail levels will appear as black/unlit. * Note: if air blocks aren't included in the {@link IDhApiFullDataSource} with proper lighting, lower detail levels will appear as black/unlit.
* *
* @implNote the default implementation of this method throws an {@link UnsupportedOperationException}, * @implNote the default implementation of this method throws an {@link UnsupportedOperationException},
* and must be overridden when {@link #getReturnType()} returns {@link EDhApiWorldGeneratorReturnType#API_CHUNKS}. * and must be overridden when {@link #getReturnType()} returns {@link EDhApiWorldGeneratorReturnType#API_CHUNKS}.
@@ -3,6 +3,7 @@ package com.seibel.distanthorizons.api.interfaces.render;
import com.seibel.distanthorizons.api.objects.math.DhApiVec3d; import com.seibel.distanthorizons.api.objects.math.DhApiVec3d;
import com.seibel.distanthorizons.api.objects.math.DhApiVec3f; import com.seibel.distanthorizons.api.objects.math.DhApiVec3f;
import com.seibel.distanthorizons.api.objects.render.DhApiRenderableBox; import com.seibel.distanthorizons.api.objects.render.DhApiRenderableBox;
import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable;
import java.util.List; import java.util.List;
@@ -18,7 +19,7 @@ import java.util.List;
* @version 2024-7-3 * @version 2024-7-3
* @since API 3.0.0 * @since API 3.0.0
*/ */
public interface IDhApiCustomRenderObjectFactory public interface IDhApiCustomRenderObjectFactory extends IBindable
{ {
/** /**
* Creates a {@link IDhApiRenderableBoxGroup} from for the given {@link DhApiRenderableBox} * Creates a {@link IDhApiRenderableBoxGroup} from for the given {@link DhApiRenderableBox}
@@ -73,7 +73,7 @@ public interface IDhApiRenderableBoxGroup extends List<DhApiRenderableBox>
* This is a good place to change the origin or notify of any box changes. * This is a good place to change the origin or notify of any box changes.
*/ */
void setPreRenderFunc(Consumer<DhApiRenderParam> renderEventParam); void setPreRenderFunc(Consumer<DhApiRenderParam> renderEventParam);
void setPostRenderFunc(Consumer<DhApiRenderParam> renderEventParam); // TODO name? void setPostRenderFunc(Consumer<DhApiRenderParam> renderEventParam);
/** /**
* If a cube's color, position, or other property is changed this method * If a cube's color, position, or other property is changed this method
@@ -33,7 +33,7 @@ import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhAp
* @since API 2.0.0 * @since API 2.0.0
* @deprecated Replaced by {@link DhApiBeforeColorDepthTextureCreatedEvent} since this event's name isn't obvious when it fires. * @deprecated Replaced by {@link DhApiBeforeColorDepthTextureCreatedEvent} since this event's name isn't obvious when it fires.
*/ */
@Deprecated @Deprecated // internal notes: this method must be kept around due to Iris using it and we don't want to break old Iris support
public abstract class DhApiColorDepthTextureCreatedEvent implements IDhApiEvent<DhApiColorDepthTextureCreatedEvent.EventParam> public abstract class DhApiColorDepthTextureCreatedEvent implements IDhApiEvent<DhApiColorDepthTextureCreatedEvent.EventParam>
{ {
/** Fired before Distant Horizons creates. */ /** Fired before Distant Horizons creates. */
@@ -20,6 +20,8 @@
package com.seibel.distanthorizons.api.methods.events.sharedParameterObjects; package com.seibel.distanthorizons.api.methods.events.sharedParameterObjects;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass;
import com.seibel.distanthorizons.api.interfaces.world.IDhApiLevelWrapper;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiWorldLoadEvent;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam; import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam;
import com.seibel.distanthorizons.api.objects.math.DhApiMat4f; import com.seibel.distanthorizons.api.objects.math.DhApiMat4f;
@@ -27,7 +29,7 @@ import com.seibel.distanthorizons.api.objects.math.DhApiMat4f;
* Contains information relevant to Distant Horizons and Minecraft rendering. * Contains information relevant to Distant Horizons and Minecraft rendering.
* *
* @author James Seibel * @author James Seibel
* @version 2024-1-31 * @version 2025-12-23
* @since API 1.0.0 * @since API 1.0.0
*/ */
public class DhApiRenderParam implements IDhApiEventParam public class DhApiRenderParam implements IDhApiEventParam
@@ -38,7 +40,7 @@ public class DhApiRenderParam implements IDhApiEventParam
/** Indicates how far into this tick the frame is. */ /** Indicates how far into this tick the frame is. */
public final float partialTicks; public final float partialTicks;
/** /**
* Indicates DH's near clip plane, measured in blocks. * Indicates DH's near clip plane, measured in blocks.
* Note: this may change based on time, player speed, and other factors. * Note: this may change based on time, player speed, and other factors.
*/ */
@@ -58,25 +60,34 @@ public class DhApiRenderParam implements IDhApiEventParam
public final DhApiMat4f dhProjectionMatrix; public final DhApiMat4f dhProjectionMatrix;
/** The model view matrix Distant Horizons is using to render this frame. */ /** The model view matrix Distant Horizons is using to render this frame. */
public final DhApiMat4f dhModelViewMatrix; public final DhApiMat4f dhModelViewMatrix;
/** combination of the MVM and projection matrices */
public final DhApiMat4f dhMvmProjMatrix;
public final int worldYOffset; public final int worldYOffset;
/**
* The level currently being rendered.
*
* @since API 5.1.0
*/
public final IDhApiLevelWrapper clientLevelWrapper;
//==============// //==============//
// constructors // // constructors //
//==============// //==============//
public DhApiRenderParam(DhApiRenderParam parent) public DhApiRenderParam(DhApiRenderParam parent)
{ {
this( this(
parent.renderPass, parent.renderPass,
parent.partialTicks, parent.partialTicks,
parent.nearClipPlane, parent.farClipPlane, parent.nearClipPlane, parent.farClipPlane,
parent.mcProjectionMatrix.copy(), parent.mcModelViewMatrix.copy(), parent.mcProjectionMatrix.copy(), parent.mcModelViewMatrix.copy(),
parent.dhProjectionMatrix.copy(), parent.dhModelViewMatrix.copy(), parent.dhProjectionMatrix.copy(), parent.dhModelViewMatrix.copy(),
parent.worldYOffset parent.worldYOffset,
parent.clientLevelWrapper
); );
} }
public DhApiRenderParam( public DhApiRenderParam(
@@ -85,8 +96,9 @@ public class DhApiRenderParam implements IDhApiEventParam
float nearClipPlane, float farClipPlane, float nearClipPlane, float farClipPlane,
DhApiMat4f newMcProjectionMatrix, DhApiMat4f newMcModelViewMatrix, DhApiMat4f newMcProjectionMatrix, DhApiMat4f newMcModelViewMatrix,
DhApiMat4f newDhProjectionMatrix, DhApiMat4f newDhModelViewMatrix, DhApiMat4f newDhProjectionMatrix, DhApiMat4f newDhModelViewMatrix,
int worldYOffset int worldYOffset,
) IDhApiLevelWrapper clientLevelWrapper
)
{ {
this.renderPass = renderPass; this.renderPass = renderPass;
@@ -101,7 +113,12 @@ public class DhApiRenderParam implements IDhApiEventParam
this.dhProjectionMatrix = newDhProjectionMatrix; this.dhProjectionMatrix = newDhProjectionMatrix;
this.dhModelViewMatrix = newDhModelViewMatrix; this.dhModelViewMatrix = newDhModelViewMatrix;
DhApiMat4f combinedMatrix = new DhApiMat4f(this.dhProjectionMatrix);
combinedMatrix.multiply(this.dhModelViewMatrix);
this.dhMvmProjMatrix = combinedMatrix;
this.worldYOffset = worldYOffset; this.worldYOffset = worldYOffset;
this.clientLevelWrapper = clientLevelWrapper;
} }
@@ -111,10 +128,9 @@ public class DhApiRenderParam implements IDhApiEventParam
// base overrides // // base overrides //
//================// //================//
@Override @Override
public DhApiRenderParam copy() public DhApiRenderParam copy() { return new DhApiRenderParam(this); }
{
return new DhApiRenderParam(this);
}
} }
@@ -31,7 +31,7 @@ import java.util.List;
* Contains a list of {@link DhApiTerrainDataPoint} representing the blocks in a Minecraft chunk. * Contains a list of {@link DhApiTerrainDataPoint} representing the blocks in a Minecraft chunk.
* *
* @author Builderb0y, James Seibel * @author Builderb0y, James Seibel
* @version 2024-7-21 * @version 2025-12-11
* @since API 2.0.0 * @since API 2.0.0
* *
* @see IDhApiWrapperFactory * @see IDhApiWrapperFactory
@@ -54,27 +54,12 @@ public class DhApiChunk
// constructors // // constructors //
//==============// //==============//
/** /** @since API 3.0.0 */
* Deprecated due to the topYBlockPos and bottomYBlockPos variables being put in the wrong order.
* They should have been in bottom -> top order.
*
* @see DhApiChunk#create(int, int, int, int)
*/
@Deprecated
public DhApiChunk(int chunkPosX, int chunkPosZ, int topYBlockPos, int bottomYBlockPos)
{ this(chunkPosX, chunkPosZ, bottomYBlockPos, topYBlockPos, false); }
/**
* @since API 3.0.0
*/
public static DhApiChunk create(int chunkPosX, int chunkPosZ, int bottomYBlockPos, int topYBlockPos) public static DhApiChunk create(int chunkPosX, int chunkPosZ, int bottomYBlockPos, int topYBlockPos)
{ return new DhApiChunk(chunkPosX, chunkPosZ, bottomYBlockPos, topYBlockPos, false); } { return new DhApiChunk(chunkPosX, chunkPosZ, bottomYBlockPos, topYBlockPos); }
/** /** Only visible to internal DH methods */
* Only visible to internal DH methods private DhApiChunk(int chunkPosX, int chunkPosZ, int bottomYBlockPos, int topYBlockPos)
* @param ignoredParameter is only present to differentiate the two constructors and isn't actually used
*/
private DhApiChunk(int chunkPosX, int chunkPosZ, int bottomYBlockPos, int topYBlockPos, boolean ignoredParameter)
{ {
this.chunkPosX = chunkPosX; this.chunkPosX = chunkPosX;
this.chunkPosZ = chunkPosZ; this.chunkPosZ = chunkPosZ;
@@ -29,7 +29,7 @@ import java.util.ArrayList;
* Holds a single datapoint of terrain data. * Holds a single datapoint of terrain data.
* *
* @author James Seibel * @author James Seibel
* @version 2024-7-20 * @version 2025-11-15
* @since API 1.0.0 * @since API 1.0.0
*/ */
public class DhApiTerrainDataPoint public class DhApiTerrainDataPoint
@@ -47,6 +47,10 @@ public class DhApiTerrainDataPoint
public final int blockLightLevel; public final int blockLightLevel;
public final int skyLightLevel; public final int skyLightLevel;
/**
* An unsigned block position of the bottom vertex for this LOD relative to the level's minimum height.
* Should be greater than or equal to 0.
*/
public final int bottomYBlockPos; public final int bottomYBlockPos;
public final int topYBlockPos; public final int topYBlockPos;
@@ -59,28 +63,7 @@ public class DhApiTerrainDataPoint
// constructors // // constructors //
//==============// //==============//
/** /** @since API 3.0.0 */
* Deprecated due to the topYBlockPos and bottomYBlockPos variables being put in the wrong order.
* They should have been in bottom -> top order.
*
* @see DhApiTerrainDataPoint#create(byte, int, int, int, int, IDhApiBlockStateWrapper, IDhApiBiomeWrapper)
*/
@Deprecated
public DhApiTerrainDataPoint(
byte detailLevel,
int blockLightLevel, int skyLightLevel,
int topYBlockPos, int bottomYBlockPos,
IDhApiBlockStateWrapper blockStateWrapper, IDhApiBiomeWrapper biomeWrapper)
{
this(detailLevel, blockLightLevel, skyLightLevel,
bottomYBlockPos, topYBlockPos,
blockStateWrapper, biomeWrapper,
false);
}
/**
* @since API 3.0.0
*/
public static DhApiTerrainDataPoint create( public static DhApiTerrainDataPoint create(
byte detailLevel, byte detailLevel,
int blockLightLevel, int skyLightLevel, int blockLightLevel, int skyLightLevel,
@@ -91,20 +74,15 @@ public class DhApiTerrainDataPoint
return new DhApiTerrainDataPoint( return new DhApiTerrainDataPoint(
detailLevel, blockLightLevel, skyLightLevel, detailLevel, blockLightLevel, skyLightLevel,
bottomYBlockPos, topYBlockPos, bottomYBlockPos, topYBlockPos,
blockStateWrapper, biomeWrapper, blockStateWrapper, biomeWrapper);
false);
} }
/** /** Only visible to internal DH methods */
* Only visible to internal DH methods
* @param ignoredParameter is only present to differentiate the two constructors and isn't actually used
*/
private DhApiTerrainDataPoint( private DhApiTerrainDataPoint(
byte detailLevel, byte detailLevel,
int blockLightLevel, int skyLightLevel, int blockLightLevel, int skyLightLevel,
int bottomYBlockPos, int topYBlockPos, int bottomYBlockPos, int topYBlockPos,
IDhApiBlockStateWrapper blockStateWrapper, IDhApiBiomeWrapper biomeWrapper, IDhApiBlockStateWrapper blockStateWrapper, IDhApiBiomeWrapper biomeWrapper
boolean ignoredParameter
) )
{ {
this.detailLevel = detailLevel; this.detailLevel = detailLevel;
@@ -118,4 +96,24 @@ public class DhApiTerrainDataPoint
this.biomeWrapper = biomeWrapper; this.biomeWrapper = biomeWrapper;
} }
//================//
// base overrides //
//================//
@Override
public String toString()
{
return "[Block:" + this.blockStateWrapper.getSerialString() +
",Biome:" + this.biomeWrapper.getName() +
",TopY:" + this.topYBlockPos +
",BottomY:" + this.bottomYBlockPos +
",BlockLight:" + this.blockLightLevel +
",SkyLight:" + this.skyLightLevel +
"]";
}
} }
@@ -23,6 +23,7 @@ public class DhApiRenderableBox
public DhApiVec3d maxPos; public DhApiVec3d maxPos;
public Color color; public Color color;
/** @see EDhApiBlockMaterial */
public byte material; public byte material;
@@ -43,9 +43,11 @@ public class DependencyInjector<BindableType extends IBindable> implements IDepe
protected final boolean allowDuplicateBindings; protected final boolean allowDuplicateBindings;
//==============// //==============//
// constructors // // constructors //
//==============// //==============//
//region
public DependencyInjector(Class<BindableType> newBindableInterface, boolean newAllowDuplicateBindings) public DependencyInjector(Class<BindableType> newBindableInterface, boolean newAllowDuplicateBindings)
{ {
@@ -53,11 +55,14 @@ public class DependencyInjector<BindableType extends IBindable> implements IDepe
this.allowDuplicateBindings = newAllowDuplicateBindings; this.allowDuplicateBindings = newAllowDuplicateBindings;
} }
//endregion
//=========// //=========//
// binding // // binding //
//=========// //=========//
//region
@Override @Override
public void bind(Class<? extends BindableType> dependencyInterface, BindableType dependencyImplementation) throws IllegalStateException, IllegalArgumentException public void bind(Class<? extends BindableType> dependencyInterface, BindableType dependencyImplementation) throws IllegalStateException, IllegalArgumentException
@@ -69,6 +74,11 @@ public class DependencyInjector<BindableType extends IBindable> implements IDepe
throw new IllegalStateException("The dependency [" + dependencyInterface.getSimpleName() + "] has already been bound."); throw new IllegalStateException("The dependency [" + dependencyInterface.getSimpleName() + "] has already been bound.");
} }
if (dependencyImplementation == null)
{
throw new NullPointerException("Can't bind null to ["+dependencyInterface.getSimpleName()+"]");
}
// make sure the given dependency implements the necessary interfaces // make sure the given dependency implements the necessary interfaces
boolean implementsInterface = this.checkIfClassImplements(dependencyImplementation.getClass(), dependencyInterface) || boolean implementsInterface = this.checkIfClassImplements(dependencyImplementation.getClass(), dependencyInterface) ||
@@ -131,13 +141,27 @@ public class DependencyInjector<BindableType extends IBindable> implements IDepe
@Override @Override
public boolean checkIfClassExtends(Class<?> classToTest, Class<?> extensionToLookFor) { return extensionToLookFor.isAssignableFrom(classToTest); } public boolean checkIfClassExtends(Class<?> classToTest, Class<?> extensionToLookFor) { return extensionToLookFor.isAssignableFrom(classToTest); }
//endregion
//===========// //===========//
// unbinding // // unbinding //
//===========// //===========//
//region
public void replaceBinding(Class<? extends BindableType> dependencyInterface, BindableType dependencyImplementation) throws IllegalStateException, IllegalArgumentException
{
this.unbindAll(dependencyInterface);
this.bind(dependencyInterface, dependencyImplementation);
}
public void unbindAll(Class<? extends BindableType> dependencyInterface) throws IllegalStateException, IllegalArgumentException
{
// remove the dependency if present
this.dependencies.remove(dependencyInterface);
}
// 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 public void unbind(Class<? extends BindableType> dependencyInterface, BindableType dependencyImplementation) throws IllegalStateException, IllegalArgumentException
{ {
// check if this object is bound // check if this object is bound
@@ -174,30 +198,27 @@ public class DependencyInjector<BindableType extends IBindable> implements IDepe
this.dependencies.remove(dependencyInterface); this.dependencies.remove(dependencyInterface);
} }
//endregion
//=========// //=========//
// getters // // getters //
//=========// //=========//
//region
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public <T extends BindableType> T get(Class<T> interfaceClass) throws ClassCastException public <T extends BindableType> T get(Class<T> interfaceClass) throws ClassCastException
{ { return (T) this.getInternalLogic(interfaceClass, false).get(0); }
return (T) this.getInternalLogic(interfaceClass, false).get(0);
}
@Override @Override
public <T extends BindableType> ArrayList<T> getAll(Class<T> interfaceClass) throws ClassCastException public <T extends BindableType> ArrayList<T> getAll(Class<T> interfaceClass) throws ClassCastException
{ { return this.getInternalLogic(interfaceClass, false); }
return this.getInternalLogic(interfaceClass, false);
}
@Override @Override
public <T extends BindableType> T get(Class<T> interfaceClass, boolean allowIncompleteDependencies) throws ClassCastException public <T extends BindableType> T get(Class<T> interfaceClass, boolean allowIncompleteDependencies) throws ClassCastException
{ { return (T) this.getInternalLogic(interfaceClass, allowIncompleteDependencies).get(0); }
return (T) this.getInternalLogic(interfaceClass, allowIncompleteDependencies).get(0);
}
/** /**
* Always returns a list of size 1 or greater, * Always returns a list of size 1 or greater,
@@ -230,6 +251,7 @@ public class DependencyInjector<BindableType extends IBindable> implements IDepe
return emptyList; return emptyList;
} }
//endregion
/** Removes all bound dependencies. */ /** Removes all bound dependencies. */
@@ -31,26 +31,26 @@ public final class ModInfo
public static final String DEDICATED_SERVER_INITIAL_PATH = "dedicated_server_initial"; public static final String DEDICATED_SERVER_INITIAL_PATH = "dedicated_server_initial";
/** Incremented every time any packets are added, changed or removed, with a few exceptions. */ /** Incremented every time any packets are added, changed or removed, with a few exceptions. */
public static final int PROTOCOL_VERSION = 11; public static final int PROTOCOL_VERSION = 13;
public static final String WRAPPER_PACKET_PATH = "message"; public static final String WRAPPER_PACKET_PATH = "message";
/** The internal mod name */ /** The internal mod name */
public static final String NAME = "DistantHorizons"; public static final String NAME = "DistantHorizons";
/** Human-readable version of NAME */ /** Human-readable version of NAME */
public static final String READABLE_NAME = "Distant Horizons"; public static final String READABLE_NAME = "Distant Horizons";
public static final String VERSION = "2.3.6-b"; public static final String VERSION = "2.4.6-b-dev";
/** Returns true if the current build is an unstable developer build, false otherwise. */ /** Returns true if the current build is an unstable developer build, false otherwise. */
public static final boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev"); public static final boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev");
/** This version should only be updated when breaking changes are introduced to the DH API */ /** This version should only be updated when breaking changes are introduced to the DH API */
public static final int API_MAJOR_VERSION = 4; public static final int API_MAJOR_VERSION = 6;
/** This version should be updated whenever new methods are added to the DH API */ /** This version should be updated whenever new methods are added to the DH API */
public static final int API_MINOR_VERSION = 1; public static final int API_MINOR_VERSION = 0;
/** This version should be updated whenever non-breaking fixes are added to the DH API */ /** This version should be updated whenever non-breaking fixes are added to the DH API */
public static final int API_PATCH_VERSION = 0; public static final int API_PATCH_VERSION = 0;
/** If the config file has an older version it'll be re-created from scratch. */ /** If the config file has an older version it'll be re-created from scratch. */
public static final int CONFIG_FILE_VERSION = 3; public static final int CONFIG_FILE_VERSION = 4;
/** All DH owned threads should start with this string to allow for easier debugging and profiling. */ /** All DH owned threads should start with this string to allow for easier debugging and profiling. */
public static final String THREAD_NAME_PREFIX = "DH-"; public static final String THREAD_NAME_PREFIX = "DH-";
+7 -5
View File
@@ -22,21 +22,23 @@ project.ext.lwjglNatives = "natives-" + os.toFamilyName()
dependencies { // All of these dependencies are in Vanilla Minecraft, but we need to depend on it as we arent importing Minecraft in the core dependencies { // All of these dependencies are in Vanilla Minecraft, but we need to depend on it as we arent importing Minecraft in the core
// Imports most of lwjgl's libraries (well, only the ones that we need) // Imports most of lwjgl's libraries (well, only the ones that we need)
implementation platform("org.lwjgl:lwjgl-bom:${rootProject.lwjgl_version}") // TODO: Use Minecraft's version for lwjgl_version (which changes in nearly every version) instead of a hard defined version for all versions implementation platform("org.lwjgl:lwjgl-bom:${rootProject.lwjgl_version}")
// REMEMBER: Dont shadow stuff here, these are just the libs that are included in Minecraft so that the core can use // REMEMBER: Dont shadow stuff here, these are just the libs that are included in Minecraft so that the core can use
implementation "org.lwjgl:lwjgl" implementation "org.lwjgl:lwjgl"
implementation "org.lwjgl:lwjgl-assimp" implementation "org.lwjgl:lwjgl-assimp"
implementation "org.lwjgl:lwjgl-glfw" implementation "org.lwjgl:lwjgl-glfw"
implementation "org.lwjgl:lwjgl-openal" // OpenGL is removed since DH now handles rendering in the "Common" project
implementation "org.lwjgl:lwjgl-opengl" // so we can use OpenGL for old MC versions and Blaze3D (IE Vulkan) for newer ones
// implementation "org.lwjgl:lwjgl-openal"
// implementation "org.lwjgl:lwjgl-opengl"
implementation "org.lwjgl:lwjgl-stb" implementation "org.lwjgl:lwjgl-stb"
implementation "org.lwjgl:lwjgl-tinyfd" implementation "org.lwjgl:lwjgl-tinyfd"
runtimeOnly "org.lwjgl:lwjgl::$lwjglNatives" runtimeOnly "org.lwjgl:lwjgl::$lwjglNatives"
runtimeOnly "org.lwjgl:lwjgl-assimp::$lwjglNatives" runtimeOnly "org.lwjgl:lwjgl-assimp::$lwjglNatives"
runtimeOnly "org.lwjgl:lwjgl-glfw::$lwjglNatives" runtimeOnly "org.lwjgl:lwjgl-glfw::$lwjglNatives"
runtimeOnly "org.lwjgl:lwjgl-openal::$lwjglNatives" // runtimeOnly "org.lwjgl:lwjgl-openal::$lwjglNatives"
runtimeOnly "org.lwjgl:lwjgl-opengl::$lwjglNatives" // runtimeOnly "org.lwjgl:lwjgl-opengl::$lwjglNatives"
runtimeOnly "org.lwjgl:lwjgl-stb::$lwjglNatives" runtimeOnly "org.lwjgl:lwjgl-stb::$lwjglNatives"
runtimeOnly "org.lwjgl:lwjgl-tinyfd::$lwjglNatives" runtimeOnly "org.lwjgl:lwjgl-tinyfd::$lwjglNatives"
@@ -20,36 +20,49 @@
package com.seibel.distanthorizons.core; package com.seibel.distanthorizons.core;
import com.github.luben.zstd.ZstdOutputStream; import com.github.luben.zstd.ZstdOutputStream;
import com.seibel.distanthorizons.api.interfaces.render.IDhApiCustomRenderObjectFactory;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiBeforeRenderEvent;
import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.eventHandlers.IgnoredDimensionCsvHandler;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericRenderObjectFactory; import com.seibel.distanthorizons.core.enums.MinecraftTextFormat;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.sql.DatabaseUpdater; import com.seibel.distanthorizons.core.sql.DatabaseUpdater;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.core.world.DhApiWorldProxy; import com.seibel.distanthorizons.core.world.DhApiWorldProxy;
import com.seibel.distanthorizons.core.api.external.methods.config.DhApiConfig; import com.seibel.distanthorizons.core.api.external.methods.config.DhApiConfig;
import com.seibel.distanthorizons.core.api.external.methods.data.DhApiTerrainDataRepo; import com.seibel.distanthorizons.core.api.external.methods.data.DhApiTerrainDataRepo;
import com.seibel.distanthorizons.api.DhApi; import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.core.render.DhApiRenderProxy; import com.seibel.distanthorizons.core.render.DhApiRenderProxy;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import net.jpountz.lz4.LZ4FrameOutputStream; import net.jpountz.lz4.LZ4FrameOutputStream;
import org.apache.logging.log4j.LogManager; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.apache.logging.log4j.Logger;
import org.sqlite.SQLiteJDBCLoader; import org.sqlite.SQLiteJDBCLoader;
import org.sqlite.util.OSInfo;
import org.tukaani.xz.XZOutputStream; import org.tukaani.xz.XZOutputStream;
import java.awt.*; import java.lang.management.GarbageCollectorMXBean;
import java.io.File; import java.lang.management.ManagementFactory;
import java.util.List;
/** Handles first time Core setup. */ /** Handles first time Core setup. */
public class Initializer public class Initializer
{ {
private static final Logger LOGGER = LogManager.getLogger(ModInfo.NAME + "-" + Initializer.class.getSimpleName()); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
public static void init() public static void init()
{ {
//============================//
// check referenced libraries //
//============================//
//region
LOGGER.info("Running library validation...");
// confirm that all referenced libraries are available to use // confirm that all referenced libraries are available to use
try try
{ {
@@ -57,6 +70,17 @@ public class Initializer
// will throw an error (not an exception) // will throw an error (not an exception)
Class<?> lz4Compressor = LZ4FrameOutputStream.class; Class<?> lz4Compressor = LZ4FrameOutputStream.class;
Class<?> zstdCompressor = ZstdOutputStream.class; Class<?> zstdCompressor = ZstdOutputStream.class;
{
byte[] testCompressByteArray = new byte[1024];
for (int i = 0; i < testCompressByteArray.length; i++)
{
testCompressByteArray[i] = (byte) (i % 126);
}
byte[] compressedBytes = com.github.luben.zstd.Zstd.compress(testCompressByteArray);
com.github.luben.zstd.Zstd.decompress(compressedBytes);
}
Class<?> lzmaCompressor = XZOutputStream.class; Class<?> lzmaCompressor = XZOutputStream.class;
//Class<?> networking = ByteBuf.class; //Class<?> networking = ByteBuf.class;
Class<?> config = com.electronwill.nightconfig.core.Config.class; Class<?> config = com.electronwill.nightconfig.core.Config.class;
@@ -73,12 +97,18 @@ public class Initializer
} }
catch (Throwable e) catch (Throwable e)
{ {
LOGGER.fatal("Critical programmer error: One or more libraries aren't present. Error: [" + e.getMessage() + "].", e); MC_CLIENT.crashMinecraft("Distant Horizons critical setup error: One or more libraries are either in-accessible, corrupted, or overwritten by another mod. Error: [" + e.getMessage() + "].", e);
// throwing here should crash the game, notifying the developer that something is wrong
throw new RuntimeException(e);
} }
// confirm the resource directory is present //endregion
//==========================//
// check resource directory //
//==========================//
//region
try try
{ {
int scriptCount = DatabaseUpdater.getAutoUpdateScriptCount(); int scriptCount = DatabaseUpdater.getAutoUpdateScriptCount();
@@ -89,10 +119,18 @@ public class Initializer
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.fatal("Critical programmer error: Can't read SQL Scripts resource folder is either missing or malformed. Error: [" + e.getMessage() + "]."); MC_CLIENT.crashMinecraft("Critical programmer error: Can't read SQL Scripts resource folder is either missing or malformed. Error: [" + e.getMessage() + "].", e);
throw new RuntimeException(e);
} }
//endregion
//===========================//
// Java AWT Headless setting //
//===========================//
//region
// This code has been disabled since it can cause Mac // This code has been disabled since it can cause Mac
// to lock up and refuse the load (there's a bug with Java.awt texture loading) // to lock up and refuse the load (there's a bug with Java.awt texture loading)
//if (MC_CLIENT != null) //if (MC_CLIENT != null)
@@ -109,18 +147,99 @@ public class Initializer
// } // }
//} //}
//endregion
//===================//
// API delayed setup //
//===================//
//region
// link Core's config to the API // link Core's config to the API
DhApi.Delayed.configs = DhApiConfig.INSTANCE; DhApi.Delayed.configs = DhApiConfig.INSTANCE;
DhApi.Delayed.terrainRepo = DhApiTerrainDataRepo.INSTANCE; DhApi.Delayed.terrainRepo = DhApiTerrainDataRepo.INSTANCE;
DhApi.Delayed.worldProxy = DhApiWorldProxy.INSTANCE; DhApi.Delayed.worldProxy = DhApiWorldProxy.INSTANCE;
DhApi.Delayed.renderProxy = DhApiRenderProxy.INSTANCE; DhApi.Delayed.renderProxy = DhApiRenderProxy.INSTANCE;
DhApi.Delayed.customRenderObjectFactory = GenericRenderObjectFactory.INSTANCE;
DhApi.Delayed.wrapperFactory = SingletonInjector.INSTANCE.get(IWrapperFactory.class); DhApi.Delayed.wrapperFactory = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
if (DhApi.Delayed.wrapperFactory == null) if (DhApi.Delayed.wrapperFactory == null)
{ {
LOGGER.error("Programmer Error: No ["+IWrapperFactory.class.getSimpleName()+"] assigned to the DhApi."); MC_CLIENT.crashMinecraft("Programmer Error: No ["+IWrapperFactory.class.getSimpleName()+"] assigned to the DhApi.", new Exception());
} }
DhApi.Delayed.customRenderObjectFactory = SingletonInjector.INSTANCE.get(IDhApiCustomRenderObjectFactory.class);
if (DhApi.Delayed.customRenderObjectFactory == null)
{
MC_CLIENT.crashMinecraft("Programmer Error: No ["+IDhApiCustomRenderObjectFactory.class.getSimpleName()+"] assigned to the DhApi.", new Exception());
}
DhApi.events.bind(DhApiBeforeRenderEvent.class, IgnoredDimensionCsvHandler.INSTANCE);
//endregion
//==============================//
// G1 Garbage collector warning //
//==============================//
//region
// log a warning if G1GC is being used
// (this garbage collector is known to cause stuttering)
{
boolean g1GcInUse = false;
StringBuilder garbageCollectorNames = new StringBuilder();
List<GarbageCollectorMXBean> gcMxBeans = ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean gcMxBean : gcMxBeans)
{
if (!garbageCollectorNames.toString().isEmpty())
{
garbageCollectorNames.append(", ");
}
garbageCollectorNames.append(gcMxBean.getName());
// "G1 Young Generation" // "G1 Concurrent GC" // "G1 Old Generation"
if (gcMxBean.getName().toLowerCase().contains("g1 "))
{
g1GcInUse = true;
}
}
LOGGER.info("Garbage collectors: ["+garbageCollectorNames+"]");
if (g1GcInUse)
{
String warningMessageHeader = "Distant Horizons: G1 Garbage collector detected.";
String warningMessageBody =
"This can cause FPS stuttering. \n" +
"It's recommended to use a concurrent garbage collector \n" +
"like ZGC (Java 21+) or Shenandoah (Java 8 through 17) \n" +
"for a smoother experience."
;
if (Config.Common.Logging.Warning.logGarbageCollectorWarning.get())
{
LOGGER.warn(
warningMessageHeader + "\n" +
warningMessageBody +
"");
}
if (Config.Common.Logging.Warning.showGarbageCollectorWarning.get())
{
ClientApi.INSTANCE.showChatMessageNextFrame(
MinecraftTextFormat.ORANGE + warningMessageHeader + MinecraftTextFormat.CLEAR_FORMATTING + "\n" +
warningMessageBody +
"");
}
}
}
//endregion
} }
} }
@@ -37,28 +37,4 @@ public class DhApiAmbientOcclusionConfig implements IDhApiAmbientOcclusionConfig
public IDhApiConfigValue<Boolean> enabled() public IDhApiConfigValue<Boolean> enabled()
{ return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Graphics.Ssao.enableSsao); } { return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Graphics.Ssao.enableSsao); }
@Override
public IDhApiConfigValue<Integer> sampleCount()
{ return new DhApiConfigValue<Integer, Integer>(Config.Client.Advanced.Graphics.Ssao.sampleCount); }
@Override
public IDhApiConfigValue<Double> radius()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Ssao.radius); }
@Override
public IDhApiConfigValue<Double> strength()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Ssao.strength); }
@Override
public IDhApiConfigValue<Double> bias()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Ssao.bias); }
@Override
public IDhApiConfigValue<Double> minLight()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Ssao.minLight); }
@Override
public IDhApiConfigValue<Integer> blurRadius()
{ return new DhApiConfigValue<Integer, Integer>(Config.Client.Advanced.Graphics.Ssao.blurRadius); }
} }
@@ -34,27 +34,27 @@ public class DhApiFarFogConfig implements IDhApiFarFogConfig
@Override @Override
public IDhApiConfigValue<Double> farFogStartDistance() public IDhApiConfigValue<Float> farFogStartDistance()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Fog.farFogStart); } { return new DhApiConfigValue<Float, Float>(Config.Client.Advanced.Graphics.Fog.farFogStart); }
@Override @Override
public IDhApiConfigValue<Double> farFogEndDistance() public IDhApiConfigValue<Float> farFogEndDistance()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Fog.farFogEnd); } { return new DhApiConfigValue<Float, Float>(Config.Client.Advanced.Graphics.Fog.farFogEnd); }
@Override @Override
public IDhApiConfigValue<Double> farFogMinThickness() public IDhApiConfigValue<Float> farFogMinThickness()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Fog.farFogMin); } { return new DhApiConfigValue<Float, Float>(Config.Client.Advanced.Graphics.Fog.farFogMin); }
@Override @Override
public IDhApiConfigValue<Double> farFogMaxThickness() public IDhApiConfigValue<Float> farFogMaxThickness()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Fog.farFogMax); } { return new DhApiConfigValue<Float, Float>(Config.Client.Advanced.Graphics.Fog.farFogMax); }
@Override @Override
public IDhApiConfigValue<EDhApiFogFalloff> farFogFalloff() public IDhApiConfigValue<EDhApiFogFalloff> farFogFalloff()
{ return new DhApiConfigValue<EDhApiFogFalloff, EDhApiFogFalloff>(Config.Client.Advanced.Graphics.Fog.farFogFalloff); } { return new DhApiConfigValue<EDhApiFogFalloff, EDhApiFogFalloff>(Config.Client.Advanced.Graphics.Fog.farFogFalloff); }
@Override @Override
public IDhApiConfigValue<Double> farFogDensity() public IDhApiConfigValue<Float> farFogDensity()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Fog.farFogDensity); } { return new DhApiConfigValue<Float, Float>(Config.Client.Advanced.Graphics.Fog.farFogDensity); }
} }
@@ -28,6 +28,7 @@ import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiHeightFogCo
import com.seibel.distanthorizons.core.config.api.DhApiConfigValue; import com.seibel.distanthorizons.core.config.api.DhApiConfigValue;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.api.converters.ApiFogDrawModeConverter; import com.seibel.distanthorizons.core.config.api.converters.ApiFogDrawModeConverter;
import com.seibel.distanthorizons.core.config.api.converters.InvertedBoolConverter;
public class DhApiFogConfig implements IDhApiFogConfig public class DhApiFogConfig implements IDhApiFogConfig
{ {
@@ -67,7 +68,7 @@ public class DhApiFogConfig implements IDhApiFogConfig
@Override @Override
@Deprecated @Deprecated
public IDhApiConfigValue<Boolean> disableVanillaFog() public IDhApiConfigValue<Boolean> disableVanillaFog()
{ return new DhApiConfigValue<>(Config.Client.Advanced.Graphics.Fog.disableVanillaFog); } { return new DhApiConfigValue<>(Config.Client.Advanced.Graphics.Fog.enableVanillaFog, new InvertedBoolConverter()); }
@Override @Override
public IDhApiConfigValue<Boolean> enableVanillaFog() public IDhApiConfigValue<Boolean> enableVanillaFog()
{ return new DhApiConfigValue<>(Config.Client.Advanced.Graphics.Fog.enableVanillaFog); } { return new DhApiConfigValue<>(Config.Client.Advanced.Graphics.Fog.enableVanillaFog); }
@@ -97,10 +97,9 @@ public class DhApiGraphicsConfig implements IDhApiGraphicsConfig
public IDhApiConfigValue<Boolean> tintWithAvoidedBlocks() public IDhApiConfigValue<Boolean> tintWithAvoidedBlocks()
{ return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Graphics.Quality.tintWithAvoidedBlocks); } { return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Graphics.Quality.tintWithAvoidedBlocks); }
// TODO re-implement @Override
// @Override public IDhApiConfigValue<Integer> getBiomeBlending()
// public IDhApiConfigValue<Integer> getBiomeBlending() { return new DhApiConfigValue<Integer, Integer>(Config.Client.Advanced.Graphics.Quality.lodBiomeBlending); }
// { return new DhApiConfigValue<Integer, Integer>(Quality.lodBiomeBlending); }
@@ -109,16 +108,16 @@ public class DhApiGraphicsConfig implements IDhApiGraphicsConfig
//===========================// //===========================//
@Override @Override
public IDhApiConfigValue<Double> overdrawPreventionRadius() public IDhApiConfigValue<Float> overdrawPreventionRadius()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Culling.overdrawPrevention); } { return new DhApiConfigValue<Float, Float>(Config.Client.Advanced.Graphics.Culling.overdrawPrevention); }
@Override @Override
public IDhApiConfigValue<Double> brightnessMultiplier() public IDhApiConfigValue<Float> brightnessMultiplier()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Quality.brightnessMultiplier); } { return new DhApiConfigValue<Float, Float>(Config.Client.Advanced.Graphics.Quality.brightnessMultiplier); }
@Override @Override
public IDhApiConfigValue<Double> saturationMultiplier() public IDhApiConfigValue<Float> saturationMultiplier()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Quality.saturationMultiplier); } { return new DhApiConfigValue<Float, Float>(Config.Client.Advanced.Graphics.Quality.saturationMultiplier); }
@Override @Override
public IDhApiConfigValue<Boolean> caveCullingEnabled() public IDhApiConfigValue<Boolean> caveCullingEnabled()
@@ -136,10 +135,6 @@ public class DhApiGraphicsConfig implements IDhApiGraphicsConfig
public IDhApiConfigValue<Boolean> lodOnlyMode() public IDhApiConfigValue<Boolean> lodOnlyMode()
{ return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.lodOnlyMode); } { return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.lodOnlyMode); }
@Override
public IDhApiConfigValue<Double> lodBias()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Quality.lodBias); }
@Override @Override
public IDhApiConfigValue<EDhApiLodShading> lodShading() public IDhApiConfigValue<EDhApiLodShading> lodShading()
{ return new DhApiConfigValue<EDhApiLodShading, EDhApiLodShading>(Config.Client.Advanced.Graphics.Quality.lodShading); } { return new DhApiConfigValue<EDhApiLodShading, EDhApiLodShading>(Config.Client.Advanced.Graphics.Quality.lodShading); }
@@ -44,31 +44,31 @@ public class DhApiHeightFogConfig implements IDhApiHeightFogConfig
{ return new DhApiConfigValue<EDhApiHeightFogDirection, EDhApiHeightFogDirection>(Config.Client.Advanced.Graphics.Fog.HeightFog.heightFogDirection); } { return new DhApiConfigValue<EDhApiHeightFogDirection, EDhApiHeightFogDirection>(Config.Client.Advanced.Graphics.Fog.HeightFog.heightFogDirection); }
@Override @Override
public IDhApiConfigValue<Double> heightFogBaseHeight() public IDhApiConfigValue<Float> heightFogBaseHeight()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Fog.HeightFog.heightFogBaseHeight); } { return new DhApiConfigValue<Float, Float>(Config.Client.Advanced.Graphics.Fog.HeightFog.heightFogBaseHeight); }
@Override @Override
public IDhApiConfigValue<Double> heightFogStartingHeightPercent() public IDhApiConfigValue<Float> heightFogStartingHeightPercent()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Fog.HeightFog.heightFogStart); } { return new DhApiConfigValue<Float, Float>(Config.Client.Advanced.Graphics.Fog.HeightFog.heightFogStart); }
@Override @Override
public IDhApiConfigValue<Double> heightFogEndingHeightPercent() public IDhApiConfigValue<Float> heightFogEndingHeightPercent()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Fog.HeightFog.heightFogEnd); } { return new DhApiConfigValue<Float, Float>(Config.Client.Advanced.Graphics.Fog.HeightFog.heightFogEnd); }
@Override @Override
public IDhApiConfigValue<Double> heightFogMinThickness() public IDhApiConfigValue<Float> heightFogMinThickness()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Fog.HeightFog.heightFogMin); } { return new DhApiConfigValue<Float, Float>(Config.Client.Advanced.Graphics.Fog.HeightFog.heightFogMin); }
@Override @Override
public IDhApiConfigValue<Double> heightFogMaxThickness() public IDhApiConfigValue<Float> heightFogMaxThickness()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Fog.HeightFog.heightFogMax); } { return new DhApiConfigValue<Float, Float>(Config.Client.Advanced.Graphics.Fog.HeightFog.heightFogMax); }
@Override @Override
public IDhApiConfigValue<EDhApiFogFalloff> heightFogFalloff() public IDhApiConfigValue<EDhApiFogFalloff> heightFogFalloff()
{ return new DhApiConfigValue<EDhApiFogFalloff, EDhApiFogFalloff>(Config.Client.Advanced.Graphics.Fog.HeightFog.heightFogFalloff); } { return new DhApiConfigValue<EDhApiFogFalloff, EDhApiFogFalloff>(Config.Client.Advanced.Graphics.Fog.HeightFog.heightFogFalloff); }
@Override @Override
public IDhApiConfigValue<Double> heightFogDensity() public IDhApiConfigValue<Float> heightFogDensity()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.Fog.HeightFog.heightFogDensity); } { return new DhApiConfigValue<Float, Float>(Config.Client.Advanced.Graphics.Fog.HeightFog.heightFogDensity); }
} }
@@ -41,8 +41,8 @@ public class DhApiNoiseTextureConfig implements IDhApiNoiseTextureConfig
{ return new DhApiConfigValue<Integer, Integer>(Config.Client.Advanced.Graphics.NoiseTexture.noiseSteps); } { return new DhApiConfigValue<Integer, Integer>(Config.Client.Advanced.Graphics.NoiseTexture.noiseSteps); }
@Override @Override
public IDhApiConfigValue<Double> noiseIntensity() public IDhApiConfigValue<Float> noiseIntensity()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Graphics.NoiseTexture.noiseIntensity); } { return new DhApiConfigValue<Float, Float>(Config.Client.Advanced.Graphics.NoiseTexture.noiseIntensity); }
@Override @Override
public IDhApiConfigValue<Integer> noiseDropoff() public IDhApiConfigValue<Integer> noiseDropoff()
@@ -2,10 +2,10 @@ package com.seibel.distanthorizons.core.api.external.methods.data;
import com.seibel.distanthorizons.api.interfaces.data.IDhApiTerrainDataCache; import com.seibel.distanthorizons.api.interfaces.data.IDhApiTerrainDataCache;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongSet; import it.unimi.dsi.fastutil.longs.LongSet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.lang.ref.SoftReference; import java.lang.ref.SoftReference;
@@ -13,15 +13,16 @@ import java.lang.ref.SoftReference;
public class DhApiTerrainDataCache implements IDhApiTerrainDataCache public class DhApiTerrainDataCache implements IDhApiTerrainDataCache
{ {
private final Object modificationLock = new Object(); private final Object modificationLock = new Object();
private Long2ReferenceOpenHashMap<SoftReference<FullDataSourceV2>> posToFullDataRef = new Long2ReferenceOpenHashMap<>(); private final Long2ReferenceOpenHashMap<SoftReference<FullDataSourceV2>> posToFullDataRef = new Long2ReferenceOpenHashMap<>();
private static final Logger LOGGER = LogManager.getLogger(DhApiTerrainDataCache.class.getSimpleName()); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
//==================// //==================//
// internal methods // // internal methods //
//==================// //==================//
//region
public void add(long pos, FullDataSourceV2 dataSource) public void add(long pos, FullDataSourceV2 dataSource)
{ {
@@ -48,11 +49,14 @@ public class DhApiTerrainDataCache implements IDhApiTerrainDataCache
} }
} }
//endregion
//=============// //=============//
// API methods // // API methods //
//=============// //=============//
//region
@Override @Override
public void clear() public void clear()
@@ -82,6 +86,23 @@ public class DhApiTerrainDataCache implements IDhApiTerrainDataCache
} }
} }
//endregion
//================//
// base overrides //
//================//
//region
@Override
public void close() { this.clear(); }
@Override
public String toString() { return "Size: " + this.posToFullDataRef.size(); }
//endregion
} }
@@ -31,9 +31,10 @@ import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.pos.DhLodPos; 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.DhSectionPos;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.render.renderer.AbstractDebugWireframeRenderer;
import com.seibel.distanthorizons.core.util.DhApiTerrainDataPointUtil; import com.seibel.distanthorizons.core.util.DhApiTerrainDataPointUtil;
import com.seibel.distanthorizons.core.util.FullDataPointUtil; import com.seibel.distanthorizons.core.util.FullDataPointUtil;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
@@ -48,8 +49,7 @@ import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import com.seibel.distanthorizons.core.util.math.Vec3d; import com.seibel.distanthorizons.core.util.math.Vec3d;
import com.seibel.distanthorizons.core.util.math.Vec3i; import com.seibel.distanthorizons.core.util.math.Vec3i;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.LogManager; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.awt.*; import java.awt.*;
@@ -64,7 +64,9 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
{ {
public static DhApiTerrainDataRepo INSTANCE = new DhApiTerrainDataRepo(); public static DhApiTerrainDataRepo INSTANCE = new DhApiTerrainDataRepo();
private static final Logger LOGGER = LogManager.getLogger(DhApiTerrainDataRepo.class.getSimpleName()); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final AbstractDebugWireframeRenderer DEBUG_RENDERER = SingletonInjector.INSTANCE.get(AbstractDebugWireframeRenderer.class);
// debugging values // debugging values
private static volatile boolean debugThreadRunning = false; private static volatile boolean debugThreadRunning = false;
@@ -89,30 +91,30 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
//================// //================//
@Override @Override
public DhApiResult<DhApiTerrainDataPoint> getSingleDataPointAtBlockPos(IDhApiLevelWrapper levelWrapper, int blockPosX, int blockPosY, int blockPosZ, @Nullable IDhApiTerrainDataCache dataCache) public DhApiResult<DhApiTerrainDataPoint> getSingleDataPointAtBlockPos(IDhApiLevelWrapper levelWrapper, int blockPosX, int blockPosY, int blockPosZ, IDhApiTerrainDataCache dataCache)
{ return getTerrainDataAtBlockYPos(levelWrapper, new DhLodPos(LodUtil.BLOCK_DETAIL_LEVEL, blockPosX, blockPosZ), blockPosY, dataCache); } { return getTerrainDataAtBlockYPos(levelWrapper, DhSectionPos.encode(LodUtil.BLOCK_DETAIL_LEVEL, blockPosX, blockPosZ), blockPosY, dataCache); }
@Override @Override
public DhApiResult<DhApiTerrainDataPoint[]> getColumnDataAtBlockPos(IDhApiLevelWrapper levelWrapper, int blockPosX, int blockPosZ, @Nullable IDhApiTerrainDataCache dataCache) public DhApiResult<DhApiTerrainDataPoint[]> getColumnDataAtBlockPos(IDhApiLevelWrapper levelWrapper, int blockPosX, int blockPosZ, IDhApiTerrainDataCache dataCache)
{ return getTerrainDataColumnArray(levelWrapper, new DhLodPos(LodUtil.BLOCK_DETAIL_LEVEL, blockPosX, blockPosZ), null, dataCache); } { return getTerrainDataColumnArray(levelWrapper, DhSectionPos.encode(LodUtil.BLOCK_DETAIL_LEVEL, blockPosX, blockPosZ), null, dataCache); }
@Override @Override
public DhApiResult<DhApiTerrainDataPoint[][][]> getAllTerrainDataAtChunkPos(IDhApiLevelWrapper levelWrapper, int chunkPosX, int chunkPosZ, @Nullable IDhApiTerrainDataCache dataCache) public DhApiResult<DhApiTerrainDataPoint[][][]> getAllTerrainDataAtChunkPos(IDhApiLevelWrapper levelWrapper, int chunkPosX, int chunkPosZ, IDhApiTerrainDataCache dataCache)
{ return getTerrainDataOverAreaForPositionDetailLevel(levelWrapper, new DhLodPos(LodUtil.CHUNK_DETAIL_LEVEL, chunkPosX, chunkPosZ), dataCache); } { return getTerrainDataOverAreaForPositionDetailLevel(levelWrapper, DhSectionPos.encode(LodUtil.CHUNK_DETAIL_LEVEL, chunkPosX, chunkPosZ), dataCache); }
@Override @Override
public DhApiResult<DhApiTerrainDataPoint[][][]> getAllTerrainDataAtRegionPos(IDhApiLevelWrapper levelWrapper, int regionPosX, int regionPosZ, @Nullable IDhApiTerrainDataCache dataCache) public DhApiResult<DhApiTerrainDataPoint[][][]> getAllTerrainDataAtRegionPos(IDhApiLevelWrapper levelWrapper, int regionPosX, int regionPosZ, IDhApiTerrainDataCache dataCache)
{ return getTerrainDataOverAreaForPositionDetailLevel(levelWrapper, new DhLodPos(LodUtil.REGION_DETAIL_LEVEL, regionPosX, regionPosZ), dataCache); } { return getTerrainDataOverAreaForPositionDetailLevel(levelWrapper, DhSectionPos.encode(LodUtil.REGION_DETAIL_LEVEL, regionPosX, regionPosZ), dataCache); }
@Override @Override
public DhApiResult<DhApiTerrainDataPoint[][][]> getAllTerrainDataAtDetailLevelAndPos(IDhApiLevelWrapper levelWrapper, byte detailLevel, int posX, int posZ, @Nullable IDhApiTerrainDataCache dataCache) public DhApiResult<DhApiTerrainDataPoint[][][]> getAllTerrainDataAtDetailLevelAndPos(IDhApiLevelWrapper levelWrapper, byte detailLevel, int posX, int posZ, IDhApiTerrainDataCache dataCache)
{ return getTerrainDataOverAreaForPositionDetailLevel(levelWrapper, new DhLodPos(detailLevel, posX, posZ), dataCache); } { return getTerrainDataOverAreaForPositionDetailLevel(levelWrapper, DhSectionPos.encode(detailLevel, posX, posZ), dataCache); }
// private getters // // private getters //
/** Returns a single API terrain datapoint that contains the given Y block position */ /** Returns a single API terrain datapoint that contains the given Y block position */
private static DhApiResult<DhApiTerrainDataPoint> getTerrainDataAtBlockYPos(IDhApiLevelWrapper levelWrapper, DhLodPos requestedColumnPos, Integer blockYPos, @Nullable IDhApiTerrainDataCache dataCache) private static DhApiResult<DhApiTerrainDataPoint> getTerrainDataAtBlockYPos(IDhApiLevelWrapper levelWrapper, long requestedColumnPos, Integer blockYPos, IDhApiTerrainDataCache dataCache)
{ {
DhApiResult<DhApiTerrainDataPoint[]> result = getTerrainDataColumnArray(levelWrapper, requestedColumnPos, blockYPos, dataCache); DhApiResult<DhApiTerrainDataPoint[]> result = getTerrainDataColumnArray(levelWrapper, requestedColumnPos, blockYPos, dataCache);
if (result.success && result.payload.length > 0) if (result.success && result.payload.length > 0)
@@ -126,7 +128,7 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
} }
/** /**
* Returns all the block columns represented by the given {@link DhLodPos}. <br> * Returns all the block columns represented by the given {@link DhSectionPos}. <br>
* IE, A position with the detail level: <br> * IE, A position with the detail level: <br>
* 0 (block): will return a 1x1 matrix of data. (don't do this, we have a specific method for that.) <br> * 0 (block): will return a 1x1 matrix of data. (don't do this, we have a specific method for that.) <br>
* 1 (2 blocks): will return a 2x2 matrix of data. <br> * 1 (2 blocks): will return a 2x2 matrix of data. <br>
@@ -135,11 +137,14 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
* will stop and return the in progress data if any errors are encountered. * will stop and return the in progress data if any errors are encountered.
*/ */
private static DhApiResult<DhApiTerrainDataPoint[][][]> getTerrainDataOverAreaForPositionDetailLevel( private static DhApiResult<DhApiTerrainDataPoint[][][]> getTerrainDataOverAreaForPositionDetailLevel(
IDhApiLevelWrapper levelWrapper, DhLodPos requestedAreaPos, IDhApiLevelWrapper levelWrapper, long requestedAreaPos,
@Nullable IDhApiTerrainDataCache dataCache) IDhApiTerrainDataCache dataCache)
{ {
DhLodPos startingBlockPos = requestedAreaPos.getCornerLodPos(LodUtil.BLOCK_DETAIL_LEVEL); byte requestedDetailLevel = DhSectionPos.getDetailLevel(requestedAreaPos);
int widthOfAreaInBlocks = BitShiftUtil.powerOfTwo(requestedAreaPos.detailLevel); long startingBlockPos = DhSectionPos.encode(LodUtil.BLOCK_DETAIL_LEVEL,
DhSectionPos.getX(requestedAreaPos) * BitShiftUtil.powerOfTwo(requestedDetailLevel - LodUtil.BLOCK_DETAIL_LEVEL),
DhSectionPos.getZ(requestedAreaPos) * BitShiftUtil.powerOfTwo(requestedDetailLevel - LodUtil.BLOCK_DETAIL_LEVEL));
int widthOfAreaInBlocks = BitShiftUtil.powerOfTwo(requestedDetailLevel);
DhApiTerrainDataPoint[][][] returnArray = new DhApiTerrainDataPoint[widthOfAreaInBlocks][widthOfAreaInBlocks][]; DhApiTerrainDataPoint[][][] returnArray = new DhApiTerrainDataPoint[widthOfAreaInBlocks][widthOfAreaInBlocks][];
int dataColumnsReturned = 0; int dataColumnsReturned = 0;
@@ -149,7 +154,7 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
{ {
for (int z = 0; z < widthOfAreaInBlocks; z++) for (int z = 0; z < widthOfAreaInBlocks; z++)
{ {
DhLodPos blockColumnPos = new DhLodPos(LodUtil.BLOCK_DETAIL_LEVEL, startingBlockPos.x + x, startingBlockPos.z + z); long blockColumnPos = DhSectionPos.encode(LodUtil.BLOCK_DETAIL_LEVEL, DhSectionPos.getX(startingBlockPos) + x, DhSectionPos.getZ(startingBlockPos) + z);
DhApiResult<DhApiTerrainDataPoint[]> result = getTerrainDataColumnArray(levelWrapper, blockColumnPos, null, dataCache); DhApiResult<DhApiTerrainDataPoint[]> result = getTerrainDataColumnArray(levelWrapper, blockColumnPos, null, dataCache);
if (result.success) if (result.success)
{ {
@@ -175,8 +180,8 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
*/ */
private static DhApiResult<DhApiTerrainDataPoint[]> getTerrainDataColumnArray( private static DhApiResult<DhApiTerrainDataPoint[]> getTerrainDataColumnArray(
IDhApiLevelWrapper levelWrapper, IDhApiLevelWrapper levelWrapper,
DhLodPos requestedColumnPos, Integer nullableBlockYPos, long requestedColumnPos, Integer nullableBlockYPos,
@Nullable IDhApiTerrainDataCache apiDataCache) IDhApiTerrainDataCache apiDataCache)
{ {
//============// //============//
// validation // // validation //
@@ -197,9 +202,14 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
ILevelWrapper coreLevelWrapper = (ILevelWrapper) levelWrapper; ILevelWrapper coreLevelWrapper = (ILevelWrapper) levelWrapper;
// the data cache can be null, but must be our own implementation // require a data cache to prevent horrible performance (especially on ray-casts)
if (apiDataCache != null if (apiDataCache == null)
&& !(apiDataCache instanceof DhApiTerrainDataCache)) {
return DhApiResult.createFail("Missing [" + IDhApiTerrainDataCache.class.getSimpleName() + "], if a cache isn't provided your repo operations will be significantly slower.");
}
// the data cache must be our own implementation
if (!(apiDataCache instanceof DhApiTerrainDataCache))
{ {
return DhApiResult.createFail("Unsupported [" + IDhApiTerrainDataCache.class.getSimpleName() + "] implementation, only the core class [" + DhApiTerrainDataCache.class.getSimpleName() + "] is a valid parameter."); return DhApiResult.createFail("Unsupported [" + IDhApiTerrainDataCache.class.getSimpleName() + "] implementation, only the core class [" + DhApiTerrainDataCache.class.getSimpleName() + "] is a valid parameter.");
} }
@@ -213,12 +223,12 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
} }
// get the detail levels for this request // get the detail levels for this request
byte requestedDetailLevel = requestedColumnPos.detailLevel; byte requestedDetailLevel = DhSectionPos.getDetailLevel(requestedColumnPos);
byte sectionDetailLevel = (byte) (requestedDetailLevel + DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); byte sectionDetailLevel = (byte) (requestedDetailLevel + DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
// get the positions for this request // get the positions for this request
long sectionPos = requestedColumnPos.getSectionPosWithSectionDetailLevel(sectionDetailLevel); long sectionPos = DhSectionPos.convertToDetailLevel(requestedColumnPos, sectionDetailLevel);
DhLodPos relativePos = requestedColumnPos.getDhSectionRelativePositionForDetailLevel(); long relativePos = DhSectionPos.getDhSectionRelativePositionForDetailLevel(requestedColumnPos, DhSectionPos.getDetailLevel(requestedColumnPos));
@@ -258,7 +268,7 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
//===============================// //===============================//
FullDataPointIdMap mapping = dataSource.mapping; FullDataPointIdMap mapping = dataSource.mapping;
LongArrayList dataColumn = dataSource.get(relativePos.x, relativePos.z); LongArrayList dataColumn = dataSource.getColumnAtRelPos(DhSectionPos.getX(relativePos), DhSectionPos.getZ(relativePos));
if (dataColumn != null) if (dataColumn != null)
{ {
int dataColumnIndexCount = dataColumn.size(); int dataColumnIndexCount = dataColumn.size();
@@ -277,7 +287,7 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
if (!getSpecificYCoordinate) if (!getSpecificYCoordinate)
{ {
// if we aren't look for a specific datapoint, add each datapoint to the return array // if we aren't look for a specific datapoint, add each datapoint to the return array
returnArray[i] = DhApiTerrainDataPointUtil.createApiDatapoint(levelWrapper, mapping, requestedDetailLevel, dataPoint); returnArray[i] = DhApiTerrainDataPointUtil.createApiDatapoint(levelWrapper.getMinHeight(), mapping, requestedDetailLevel, dataPoint);
} }
else else
{ {
@@ -294,7 +304,7 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
if (bottomY <= requestedY && requestedY < topY) // blockPositions start from the bottom of the block, thus "<=" for bottomY, just "<" for topY if (bottomY <= requestedY && requestedY < topY) // blockPositions start from the bottom of the block, thus "<=" for bottomY, just "<" for topY
{ {
// this datapoint contains the requested block position, return it // this datapoint contains the requested block position, return it
DhApiTerrainDataPoint apiTerrainData = DhApiTerrainDataPointUtil.createApiDatapoint(levelWrapper, mapping, requestedDetailLevel, dataPoint); DhApiTerrainDataPoint apiTerrainData = DhApiTerrainDataPointUtil.createApiDatapoint(levelWrapper.getMinHeight(), mapping, requestedDetailLevel, dataPoint);
return DhApiResult.createSuccess(new DhApiTerrainDataPoint[]{apiTerrainData}); return DhApiResult.createSuccess(new DhApiTerrainDataPoint[]{apiTerrainData});
} }
} }
@@ -345,7 +355,10 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
@Nullable @Nullable
IDhApiTerrainDataCache dataCache) IDhApiTerrainDataCache dataCache)
{ {
return this.raycastLodData(levelWrapper, new Vec3d(rayOriginX, rayOriginY, rayOriginZ), new Vec3f(rayDirectionX, rayDirectionY, rayDirectionZ), maxRayBlockLength, dataCache); return this.raycastLodData(levelWrapper,
new Vec3d(rayOriginX, rayOriginY, rayOriginZ),
new Vec3f(rayDirectionX, rayDirectionY, rayDirectionZ),
maxRayBlockLength, dataCache);
} }
/** /**
@@ -363,8 +376,8 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
{ {
rayDirection.normalize(); rayDirection.normalize();
int minBlockHeight = levelWrapper.getMinHeight(); int minLevelBlockHeight = levelWrapper.getMinHeight();
int maxBlockHeight = levelWrapper.getMaxHeight(); int maxLevelBlockHeight = levelWrapper.getMaxHeight();
@@ -380,7 +393,8 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
DhApiRaycastResult closetFoundDataPoint = null; DhApiRaycastResult closetFoundDataPoint = null;
while (blockPos.y >= minBlockHeight && blockPos.y < maxBlockHeight while (blockPos.y >= minLevelBlockHeight
&& blockPos.y < maxLevelBlockHeight
&& currentLength <= maxRayBlockLength) && currentLength <= maxRayBlockLength)
{ {
// get the LOD columns around this position // get the LOD columns around this position
@@ -403,7 +417,8 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
{ {
// does this LOD contain the given Y position? // does this LOD contain the given Y position?
Vec3i dataPointPos = new Vec3i(columnPos.x, dataPoint.bottomYBlockPos, columnPos.z); Vec3i dataPointPos = new Vec3i(columnPos.x, dataPoint.bottomYBlockPos, columnPos.z);
if (exactPos.y >= dataPoint.bottomYBlockPos && exactPos.y <= dataPoint.topYBlockPos) if (exactPos.y >= dataPoint.bottomYBlockPos
&& exactPos.y <= dataPoint.topYBlockPos)
{ {
if (closetFoundDataPoint == null) if (closetFoundDataPoint == null)
{ {
@@ -503,7 +518,7 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
// this will throw a cast exception if the chunk object array isn't correct // this will throw a cast exception if the chunk object array isn't correct
IChunkWrapper chunk = SingletonInjector.INSTANCE.get(IWrapperFactory.class).createChunkWrapper(chunkObjectArray); IChunkWrapper chunk = SingletonInjector.INSTANCE.get(IWrapperFactory.class).createChunkWrapper(chunkObjectArray);
SharedApi.INSTANCE.applyChunkUpdate(chunk, dhLevel.getLevelWrapper(), true, true); SharedApi.INSTANCE.applyChunkUpdate(chunk, dhLevel.getLevelWrapper());
return DhApiResult.createSuccess(); return DhApiResult.createSuccess();
@@ -516,7 +531,7 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
//=============// //=============//
@Override @Override
public IDhApiTerrainDataCache getSoftCache() { return new DhApiTerrainDataCache(); } public IDhApiTerrainDataCache createSoftCache() { return new DhApiTerrainDataCache(); }
@@ -536,10 +551,10 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
Thread thread = new Thread(() -> { Thread thread = new Thread(() -> {
try try
{ {
DhApiResult<DhApiTerrainDataPoint> single = getTerrainDataAtBlockYPos(levelWrapper, new DhLodPos(LodUtil.BLOCK_DETAIL_LEVEL, blockPosX, blockPosZ), blockPosY, debugDataCache); DhApiResult<DhApiTerrainDataPoint> single = getTerrainDataAtBlockYPos(levelWrapper, DhSectionPos.encode(LodUtil.BLOCK_DETAIL_LEVEL, blockPosX, blockPosZ), blockPosY, debugDataCache);
DhApiResult<DhApiTerrainDataPoint[]> column = getTerrainDataColumnArray(levelWrapper, new DhLodPos(LodUtil.BLOCK_DETAIL_LEVEL, blockPosX, blockPosZ), null, debugDataCache); DhApiResult<DhApiTerrainDataPoint[]> column = getTerrainDataColumnArray(levelWrapper, DhSectionPos.encode(LodUtil.BLOCK_DETAIL_LEVEL, blockPosX, blockPosZ), null, debugDataCache);
DhLodPos chunkPos = new DhLodPos(LodUtil.BLOCK_DETAIL_LEVEL, blockPosX, blockPosZ).convertToDetailLevel(LodUtil.CHUNK_DETAIL_LEVEL); long chunkPos = DhSectionPos.encodeContaining(LodUtil.CHUNK_DETAIL_LEVEL, new DhChunkPos(blockPosX, blockPosZ));
DhApiResult<DhApiTerrainDataPoint[][][]> area = getTerrainDataOverAreaForPositionDetailLevel(levelWrapper, chunkPos, debugDataCache); DhApiResult<DhApiTerrainDataPoint[][][]> area = getTerrainDataOverAreaForPositionDetailLevel(levelWrapper, chunkPos, debugDataCache);
@@ -572,12 +587,16 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
} }
// draw raycast position // draw raycast position
if (rayCast.success && rayCast.payload != null) if (rayCast.success
&& rayCast.payload != null)
{ {
DebugRenderer.makeParticle( DEBUG_RENDERER.makeParticle(
new DebugRenderer.BoxParticle( new AbstractDebugWireframeRenderer.BoxParticle(
new DebugRenderer.Box( new AbstractDebugWireframeRenderer.Box(
DhSectionPos.encode((byte) 0, rayCast.payload.pos.x, rayCast.payload.pos.z), rayCast.payload.dataPoint.bottomYBlockPos, rayCast.payload.dataPoint.topYBlockPos, -0.1f, Color.RED), DhSectionPos.encode((byte) 0, rayCast.payload.pos.x, rayCast.payload.pos.z),
rayCast.payload.dataPoint.bottomYBlockPos,
rayCast.payload.dataPoint.topYBlockPos,
-0.1f, Color.RED),
1.0, 0f 1.0, 0f
) )
); );
@@ -23,18 +23,28 @@ import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.api.enums.config.EDhApiMcRenderingFadeMode; import com.seibel.distanthorizons.api.enums.config.EDhApiMcRenderingFadeMode;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.*; import com.seibel.distanthorizons.api.methods.events.abstractEvents.*;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; import com.seibel.distanthorizons.core.api.internal.rendering.DhRenderState;
import com.seibel.distanthorizons.core.api.internal.rendering.RenderState; import com.seibel.distanthorizons.core.enums.MinecraftTextFormat;
import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure; import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.network.messages.MessageRegistry; import com.seibel.distanthorizons.core.network.messages.MessageRegistry;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.render.DhApiRenderProxy; import com.seibel.distanthorizons.core.render.DhApiRenderProxy;
import com.seibel.distanthorizons.core.render.renderer.FadeRenderer; import com.seibel.distanthorizons.core.render.RenderParams;
import com.seibel.distanthorizons.core.render.RenderThreadTaskHandler;
import com.seibel.distanthorizons.core.render.renderer.*;
import com.seibel.distanthorizons.core.util.TimerUtil; import com.seibel.distanthorizons.core.util.TimerUtil;
import com.seibel.distanthorizons.core.util.math.Vec3d;
import com.seibel.distanthorizons.core.util.objects.Pair; import com.seibel.distanthorizons.core.util.objects.Pair;
import com.seibel.distanthorizons.core.util.objects.RollingAverage;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.renderPass.IDhMetaRenderer;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.renderPass.IDhTerrainRenderer;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.renderPass.IDhVanillaFadeRenderer;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.renderPass.IDhTestTriangleRenderer;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.network.session.NetworkSession; import com.seibel.distanthorizons.core.network.session.NetworkSession;
@@ -43,30 +53,22 @@ import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel; import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.logging.ConfigBasedSpamLogger;
import com.seibel.distanthorizons.core.logging.SpamReducedLogger;
import com.seibel.distanthorizons.core.util.math.Mat4f;
import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.render.renderer.TestRenderer;
import com.seibel.distanthorizons.core.util.RenderUtil;
import com.seibel.distanthorizons.core.world.AbstractDhWorld; import com.seibel.distanthorizons.core.world.AbstractDhWorld;
import com.seibel.distanthorizons.core.world.DhClientServerWorld;
import com.seibel.distanthorizons.core.world.DhClientWorld; import com.seibel.distanthorizons.core.world.DhClientWorld;
import com.seibel.distanthorizons.core.world.IDhClientWorld;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import org.apache.logging.log4j.LogManager; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import java.io.File; import java.io.File;
import java.util.*; import java.util.*;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
/** /**
* This holds the methods that should be called * This holds the methods that should be called
@@ -75,18 +77,14 @@ import java.util.concurrent.TimeUnit;
*/ */
public class ClientApi public class ClientApi
{ {
private static final Logger LOGGER = LogManager.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final DhLogger RATE_LIMITED_LOGGER = new DhLoggerBuilder().maxCountPerSecond(1).build();
public static boolean prefLoggerEnabled = false;
public static final ClientApi INSTANCE = new ClientApi(); public static final ClientApi INSTANCE = new ClientApi();
public static final TestRenderer TEST_RENDERER = new TestRenderer();
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
public static final long SPAM_LOGGER_FLUSH_NS = TimeUnit.NANOSECONDS.convert(1, TimeUnit.SECONDS);
/** this includes the is dev build message and low allocated memory warning */ /** this includes the is dev build message and low allocated memory warning */
private static final int MS_BETWEEN_STATIC_STARTUP_MESSAGES = 4_000; private static final int MS_BETWEEN_STATIC_STARTUP_MESSAGES = 4_000;
@@ -98,14 +96,20 @@ public class ClientApi
* *
* Only downside is making sure each variable is populated before rendering. * Only downside is making sure each variable is populated before rendering.
*/ */
public static final RenderState RENDER_STATE = new RenderState(); public static final DhRenderState RENDER_STATE = new DhRenderState();
/**
* 50ms = 20 FPS
* @link https://fpstoms.com/
* @see ClientApi#cameraSpeedRollingAverage
*/
private static final long MIN_MS_BETWEEN_SPEED_CHECKS = 50;
private boolean isDevBuildMessagePrinted = false; private boolean isDevBuildMessagePrinted = false;
private boolean lowMemoryWarningPrinted = false; private boolean lowMemoryWarningPrinted = false;
private boolean highVanillaRenderDistanceWarningPrinted = false; private boolean highVanillaRenderDistanceWarningPrinted = false;
/** when the last static */
private long lastStaticWarningMessageSentMsTime = 0L; private long lastStaticWarningMessageSentMsTime = 0L;
private final Queue<String> chatMessageQueueForNextFrame = new LinkedBlockingQueue<>(); private final Queue<String> chatMessageQueueForNextFrame = new LinkedBlockingQueue<>();
@@ -113,8 +117,6 @@ public class ClientApi
public boolean rendererDisabledBecauseOfExceptions = false; public boolean rendererDisabledBecauseOfExceptions = false;
private long lastFlushNanoTime = 0;
private final ClientPluginChannelApi pluginChannelApi = new ClientPluginChannelApi(this::clientLevelLoadEvent, this::clientLevelUnloadEvent); private final ClientPluginChannelApi pluginChannelApi = new ClientPluginChannelApi(this::clientLevelLoadEvent, this::clientLevelUnloadEvent);
/** Delay loading the first level to give the server some time to respond with level to actually load */ /** Delay loading the first level to give the server some time to respond with level to actually load */
@@ -126,8 +128,24 @@ public class ClientApi
/** Holds any chunks that were loaded before the {@link ClientApi#clientLevelLoadEvent(IClientLevelWrapper)} was fired. */ /** Holds any chunks that were loaded before the {@link ClientApi#clientLevelLoadEvent(IClientLevelWrapper)} was fired. */
public final HashMap<Pair<IClientLevelWrapper, DhChunkPos>, IChunkWrapper> waitingChunkByClientLevelAndPos = new HashMap<>(); public final HashMap<Pair<IClientLevelWrapper, DhChunkPos>, IChunkWrapper> waitingChunkByClientLevelAndPos = new HashMap<>();
/** re-set every frame during the opaque rendering stage */ /** publicly available so {@link F3Screen} can display the error */
private boolean renderingCancelledForThisFrame; @Nullable
public String lastRenderParamValidationMessage = null;
/**
* measured in blocks/second <br>
*
* The number of points tracked here is related
* to the rate at which we check for speed.
* So if the ms_between is changed the number of points
* tracked should also be to keep the ratio roughly the same.
* @see ClientApi#MIN_MS_BETWEEN_SPEED_CHECKS
*/
public RollingAverage cameraSpeedRollingAverage = new RollingAverage(40);
private Vec3d lastCameraPosForSpeedCheck = new Vec3d();
private long msSinceLastSpeedCheck = 0L;
@@ -142,6 +160,7 @@ public class ClientApi
//==============// //==============//
// world events // // world events //
//==============// //==============//
///region
/** /**
* May be fired slightly before or after the associated * May be fired slightly before or after the associated
@@ -167,17 +186,16 @@ public class ClientApi
if (Config.Common.Logging.Warning.showReplayWarningOnStartup.get()) if (Config.Common.Logging.Warning.showReplayWarningOnStartup.get())
{ {
MC_CLIENT.sendChatMessage("\u00A76" + "Distant Horizons: Replay detected." + "\u00A7r"); // gold color MC_CLIENT.sendChatMessage(MinecraftTextFormat.ORANGE + "Distant Horizons: Replay detected." + MinecraftTextFormat.CLEAR_FORMATTING);
MC_CLIENT.sendChatMessage("DH may behave strangely or have missing functionality."); MC_CLIENT.sendChatMessage("DH may behave strangely or have missing functionality.");
MC_CLIENT.sendChatMessage("In order to use pre-generated LODs, put your DH database(s) in:"); MC_CLIENT.sendChatMessage("In order to use pre-generated LODs, put your DH database(s) in:");
MC_CLIENT.sendChatMessage("\u00A77"+".Minecraft" + File.separator + ClientOnlySaveStructure.SERVER_DATA_FOLDER_NAME + File.separator + ClientOnlySaveStructure.REPLAY_SERVER_FOLDER_NAME + File.separator + "DIMENSION_NAME"+"\u00A7r"); // light gray color MC_CLIENT.sendChatMessage(MinecraftTextFormat.GRAY +".Minecraft" + File.separator + ClientOnlySaveStructure.SERVER_DATA_FOLDER_NAME + File.separator + ClientOnlySaveStructure.REPLAY_SERVER_FOLDER_NAME + File.separator + "DIMENSION_NAME"+ MinecraftTextFormat.CLEAR_FORMATTING);
MC_CLIENT.sendChatMessage("This can be disabled in DH's config under Advanced -> Logging."); MC_CLIENT.sendChatMessage("This message can be disabled in DH's config under Advanced -> Logging.");
MC_CLIENT.sendChatMessage(""); MC_CLIENT.sendChatMessage("");
} }
} }
// firing after clientLevelLoadEvent
// TODO if level has prepped to load it should fire level load event
DhClientWorld world = new DhClientWorld(); DhClientWorld world = new DhClientWorld();
SharedApi.setDhWorld(world); SharedApi.setDhWorld(world);
@@ -220,11 +238,14 @@ public class ClientApi
this.waitingClientLevels.clear(); this.waitingClientLevels.clear();
} }
///endregion
//==============// //==============//
// level events // // level events //
//==============// //==============//
///region
public void clientLevelUnloadEvent(IClientLevelWrapper level) public void clientLevelUnloadEvent(IClientLevelWrapper level)
{ {
@@ -241,7 +262,6 @@ public class ClientApi
if (world != null) if (world != null)
{ {
world.unloadLevel(level); world.unloadLevel(level);
SharedApi.INSTANCE.clearQueuedChunkUpdates();
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(level)); ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(level));
} }
else else
@@ -256,8 +276,15 @@ public class ClientApi
} }
} }
public void clientLevelLoadEvent(IClientLevelWrapper levelWrapper) public void clientLevelLoadEvent(@Nullable IClientLevelWrapper levelWrapper)
{ {
// can happen if there was an issue during level load
if (levelWrapper == null)
{
return;
}
// wait a moment before loading the level to give the server a chance to handle the client's login request // wait a moment before loading the level to give the server a chance to handle the client's login request
if (MC_CLIENT.clientConnectedToDedicatedServer()) if (MC_CLIENT.clientConnectedToDedicatedServer())
{ {
@@ -318,7 +345,7 @@ public class ClientApi
if (levelWrapper.equals(level)) if (levelWrapper.equals(level))
{ {
IChunkWrapper chunkWrapper = this.waitingChunkByClientLevelAndPos.get(levelChunkPair); IChunkWrapper chunkWrapper = this.waitingChunkByClientLevelAndPos.get(levelChunkPair);
SharedApi.INSTANCE.chunkLoadEvent(chunkWrapper, levelWrapper); SharedApi.INSTANCE.applyChunkUpdate(chunkWrapper, levelWrapper);
keysToRemove.add(levelChunkPair); keysToRemove.add(levelChunkPair);
} }
} }
@@ -330,54 +357,14 @@ public class ClientApi
} }
} }
///endregion
//===============//
// render events //
//===============//
public void clientTickEvent()
{
IProfilerWrapper profiler = MC_CLIENT.getProfiler();
profiler.push("DH-ClientTick");
try
{
boolean doFlush = System.nanoTime() - this.lastFlushNanoTime >= SPAM_LOGGER_FLUSH_NS;
if (doFlush)
{
this.lastFlushNanoTime = System.nanoTime();
SpamReducedLogger.flushAll();
}
ConfigBasedLogger.updateAll();
ConfigBasedSpamLogger.updateAll(doFlush);
IDhClientWorld clientWorld = SharedApi.getIDhClientWorld();
if (clientWorld != null)
{
clientWorld.clientTick();
// Ignore local world gen, as it's managed by server ticking
if (!(clientWorld instanceof DhClientServerWorld))
{
SharedApi.worldGenTick(clientWorld::worldGenTick);
}
}
}
catch (Exception e)
{
// handle errors here to prevent blowing up a mixin or API up stream
LOGGER.error("Unexpected error in ClientApi.clientTickEvent(), error: "+e.getMessage(), e);
}
profiler.pop();
}
//============// //============//
// networking // // networking //
//============// //============//
///region
/** /**
* Forwards a decoded message into the registered handlers. * Forwards a decoded message into the registered handlers.
@@ -386,18 +373,38 @@ public class ClientApi
*/ */
public void pluginMessageReceived(@NotNull AbstractNetworkMessage message) public void pluginMessageReceived(@NotNull AbstractNetworkMessage message)
{ {
NetworkSession networkSession = this.pluginChannelApi.networkSession; @Nullable ThreadPoolExecutor executor = ThreadPoolUtil.networkClientHandlerExecutor();
if (networkSession != null) if (executor == null)
{ {
networkSession.tryHandleMessage(message); LOGGER.warn("warn");
return;
}
try
{
executor.execute(() ->
{
NetworkSession networkSession = this.pluginChannelApi.networkSession;
if (networkSession != null)
{
networkSession.tryHandleMessage(message);
}
});
}
catch (RejectedExecutionException e)
{
LOGGER.warn("Plugin message executor rejected");
} }
} }
///endregion
//===========//
// rendering // //===============//
//===========// // LOD rendering //
//===============//
///region
/** Should be called before {@link ClientApi#renderDeferredLodsForShaders} */ /** Should be called before {@link ClientApi#renderDeferredLodsForShaders} */
public void renderLods() { this.renderLodLayer(false); } public void renderLods() { this.renderLodLayer(false); }
@@ -408,30 +415,101 @@ public class ClientApi
*/ */
public void renderDeferredLodsForShaders() { this.renderLodLayer(true); } public void renderDeferredLodsForShaders() { this.renderLodLayer(true); }
public static long firstRenderTimeMs = 0;
private void renderLodLayer(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();
IProfilerWrapper profiler = MC_CLIENT.getProfiler(); IProfilerWrapper profiler = MC_CLIENT.getProfiler();
profiler.pop(); // get out of "terrain"
profiler.push("DH-RenderLevel"); profiler.push("DH-RenderLevel");
// render parameter setup // //===========//
// debugging //
//===========//
//region
//DhApiTerrainDataRepo.asyncDebugMethod(
// RENDER_STATE.clientLevelWrapper,
// MC_CLIENT.getPlayerBlockPos().getX(),
// MC_CLIENT.getPlayerBlockPos().getY(),
// MC_CLIENT.getPlayerBlockPos().getZ()
//);
//endregion
//=====================//
// render thread tasks //
//=====================//
///region
// only run these tasks once per frame
if (!renderingDeferredLayer)
{
profiler.push("DH render thread tasks");
//===============//
// chat messages //
//===============//
this.sendQueuedChatMessages();
//======================//
// GL Proxy queued jobs //
//======================//
try
{
// these tasks always need to be called, regardless of whether the renderer is enabled or not to prevent memory leaks
RenderThreadTaskHandler.INSTANCE.runRenderThreadTasks();
}
catch (Exception e)
{
LOGGER.error("Unexpected issue running render thread tasks, error: [" + e.getMessage() + "].", e);
}
//==============//
// camera speed //
//==============//
long nowMs = System.currentTimeMillis();
if (this.msSinceLastSpeedCheck + MIN_MS_BETWEEN_SPEED_CHECKS < nowMs)
{
// calc time since last check
double secSinceLastCheck = (nowMs - this.msSinceLastSpeedCheck) / 1_000.0;
this.msSinceLastSpeedCheck = nowMs;
// get the distance traveled since last frame
Vec3d camPos = MC_RENDER.getCameraExactPosition();
double distanceInBlocks = camPos.getDistance(this.lastCameraPosForSpeedCheck);
double speed = distanceInBlocks / secSinceLastCheck;
// record new values for next check
this.cameraSpeedRollingAverage.add(speed);
this.lastCameraPosForSpeedCheck = camPos;
}
profiler.pop();
}
///endregion
//=================//
// parameter setup //
//=================//
///region
EDhApiRenderPass renderPass; EDhApiRenderPass renderPass;
if (DhApiRenderProxy.INSTANCE.getDeferTransparentRendering()) if (DhApiRenderProxy.INSTANCE.getDeferTransparentRendering())
@@ -450,86 +528,76 @@ public class ClientApi
renderPass = EDhApiRenderPass.OPAQUE_AND_TRANSPARENT; renderPass = EDhApiRenderPass.OPAQUE_AND_TRANSPARENT;
} }
DhApiRenderParam renderEventParam = // A global render state variable is used since MC has split up their
new DhApiRenderParam( // render prep and actual rendering into different threads/methods
renderPass, // this is annoying since it's possible to start a render with only
partialTicks, // partially complete info, but there isn't a better option at the moment
RenderUtil.getNearClipPlaneDistanceInBlocks(partialTicks), RenderUtil.getFarClipPlaneDistanceInBlocks(), RenderParams renderParams = new RenderParams(renderPass, RENDER_STATE);
mcProjectionMatrix, mcModelViewMatrix,
RenderUtil.createLodProjectionMatrix(mcProjectionMatrix, partialTicks), RenderUtil.createLodModelViewMatrix(mcModelViewMatrix), ///endregion
levelWrapper.getMinHeight()
);
//Mat4f mcCombined = mcModelViewMatrix.copy(); //============//
//mcCombined.multiply(mcProjectionMatrix); // validation //
// //============//
//com.seibel.distanthorizons.api.objects.math.DhApiMat4f dhCombined = renderEventParam.dhModelViewMatrix.copy(); ///region
//dhCombined.multiply(renderEventParam.dhProjectionMatrix);
// if (firstRenderTimeMs == 0)
//LOGGER.info("\n\n" + {
// "API\n" + firstRenderTimeMs = System.currentTimeMillis();
// "Mc MVM: \n" + mcModelViewMatrix.toString() + "\n" + }
// "Mc Proj: \n" + mcProjectionMatrix + "\n" +
// "Mc Combined:\n" + mcCombined.toString() + "\n" + String validationMessage = renderParams.getValidationErrorMessage(firstRenderTimeMs);
// "\n" + if (validationMessage != null)
// "DH MVM: \n" + renderEventParam.dhModelViewMatrix.toString() + "\n" + {
// "DH Proj: \n" + renderEventParam.dhProjectionMatrix + "\n" + // store the error message so it can be seen on the F3 screen
// "DH Combined:\n" + mcCombined.toString() this.lastRenderParamValidationMessage = validationMessage;
//); return;
}
else
{
this.lastRenderParamValidationMessage = null;
}
if (this.rendererDisabledBecauseOfExceptions)
{
// re-enable rendering if the user toggles DH rendering
if (!Config.Client.quickEnableRendering.get())
{
LOGGER.info("DH Renderer re-enabled after exception. Some rendering issues may occur. Please reboot Minecraft if you see any rendering issues.");
this.rendererDisabledBecauseOfExceptions = false;
Config.Client.quickEnableRendering.set(true);
}
return;
}
if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DISABLED)
{
return;
}
///endregion
// render validation // //===========//
// rendering //
//===========//
///region
try try
{ {
// TODO write this message to the F3 menu so people can see when a different mod screws with the lightmap
String reasonLodsCannotRender = RenderUtil.shouldLodsRender(levelWrapper, renderEventParam);
if (reasonLodsCannotRender != null)
{
return;
}
IDhClientWorld dhClientWorld = SharedApi.getIDhClientWorld();
if (dhClientWorld == null)
{
return;
}
IDhClientLevel level = (IDhClientLevel) dhClientWorld.getLevel(levelWrapper);
if (level == null)
{
return;
}
if (this.rendererDisabledBecauseOfExceptions)
{
// re-enable rendering if the user toggles DH rendering
if (!Config.Client.quickEnableRendering.get())
{
LOGGER.info("DH Renderer re-enabled after exception. Some rendering issues may occur. Please reboot Minecraft if you see any rendering issues.");
this.rendererDisabledBecauseOfExceptions = false;
Config.Client.quickEnableRendering.set(true);
}
return;
}
// render pass // // render pass //
if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT)
if (!renderingDeferredLayer)
{ {
if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT) if (!renderingDeferredLayer)
{ {
this.renderingCancelledForThisFrame = ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeRenderEvent.class, renderEventParam); boolean renderingCancelled = ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeRenderEvent.class, renderParams);
if (!this.renderingCancelledForThisFrame) if (!renderingCancelled)
{ {
level.render(renderEventParam, profiler); LodRenderer.INSTANCE.render(renderParams, profiler);
} }
if (!DhApi.Delayed.renderProxy.getDeferTransparentRendering()) if (!DhApi.Delayed.renderProxy.getDeferTransparentRendering())
@@ -537,25 +605,42 @@ public class ClientApi
ApiEventInjector.INSTANCE.fireAllEvents(DhApiAfterRenderEvent.class, null); ApiEventInjector.INSTANCE.fireAllEvents(DhApiAfterRenderEvent.class, null);
} }
} }
else if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEBUG) else
{ {
profiler.push("Render Debug"); boolean renderingCancelled = ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeDeferredRenderEvent.class, renderParams);
ClientApi.TEST_RENDERER.render(); if (!renderingCancelled)
profiler.pop(); {
LodRenderer.INSTANCE.renderDeferred(renderParams, profiler);
}
if (DhApi.Delayed.renderProxy.getDeferTransparentRendering())
{
ApiEventInjector.INSTANCE.fireAllEvents(DhApiAfterRenderEvent.class, null);
}
} }
} }
else else
{ {
boolean renderingCancelled = ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeDeferredRenderEvent.class, renderEventParam); if (!renderingDeferredLayer)
if (!renderingCancelled)
{ {
level.renderDeferred(renderEventParam, profiler); IDhMetaRenderer metaRenderer = SingletonInjector.INSTANCE.get(IDhMetaRenderer.class);
} IDhTestTriangleRenderer testRenderer = SingletonInjector.INSTANCE.get(IDhTestTriangleRenderer.class);
if (testRenderer != null
&& metaRenderer != null)
if (DhApi.Delayed.renderProxy.getDeferTransparentRendering()) {
{ // meta renderer needed for render state/texture
ApiEventInjector.INSTANCE.fireAllEvents(DhApiAfterRenderEvent.class, null); // for setup on some APIs (IE openGL)
metaRenderer.runRenderPassSetup(renderParams);
testRenderer.render(renderParams);
metaRenderer.runRenderPassCleanup(renderParams);
}
else
{
RATE_LIMITED_LOGGER.warn("Unable to find singleton [" + IDhTestTriangleRenderer.class.getSimpleName() + "]");
}
} }
} }
} }
@@ -564,43 +649,54 @@ public class ClientApi
this.rendererDisabledBecauseOfExceptions = true; this.rendererDisabledBecauseOfExceptions = true;
LOGGER.error("Unexpected Renderer error in render pass [" + renderPass + "]. Error: " + e.getMessage(), e); LOGGER.error("Unexpected Renderer error in render pass [" + renderPass + "]. Error: " + e.getMessage(), e);
MC_CLIENT.sendChatMessage("\u00A74\u00A7l\u00A7uERROR: Distant Horizons renderer has encountered an exception!"); MC_CLIENT.sendChatMessage(MinecraftTextFormat.DARK_RED + "" + MinecraftTextFormat.BOLD + "ERROR: Distant Horizons renderer has encountered an exception!" + MinecraftTextFormat.CLEAR_FORMATTING);
MC_CLIENT.sendChatMessage("\u00A74Renderer disabled to try preventing GL state corruption."); MC_CLIENT.sendChatMessage(MinecraftTextFormat.DARK_RED + "Renderer disabled to try preventing GL state corruption." + MinecraftTextFormat.CLEAR_FORMATTING);
MC_CLIENT.sendChatMessage("\u00A74Toggle DH rendering via the config UI to re-activate DH rendering."); MC_CLIENT.sendChatMessage(MinecraftTextFormat.DARK_RED + "Toggle DH rendering via the config UI to re-activate DH rendering." + MinecraftTextFormat.CLEAR_FORMATTING);
MC_CLIENT.sendChatMessage("\u00A74Error: " + e); MC_CLIENT.sendChatMessage(MinecraftTextFormat.DARK_RED + "Error: " + MinecraftTextFormat.CLEAR_FORMATTING + e);
}
finally
{
try
{
// these tasks always need to be called, regardless of whether the renderer is enabled or not to prevent memory leaks
GLProxy.getInstance().runRenderThreadTasks();
}
catch (Exception e)
{
LOGGER.error("Unexpected issue running render thread tasks.", e);
}
profiler.pop(); // end LOD
profiler.push("terrain"); // go back into "terrain"
} }
///endregion
profiler.pop(); // end LOD
} }
///endregion
//================//
// fade rendering //
//================//
///region
/** /**
* The first fade pass. * The first fade pass.
* Called after MC finishes rendering the opaque passes. * Called after MC finishes rendering the opaque passes.
*/ */
public void renderFadeOpaque() public void renderFadeOpaque()
{ {
IDhVanillaFadeRenderer fadeRenderer = SingletonInjector.INSTANCE.get(IDhVanillaFadeRenderer.class);
if (fadeRenderer == null)
{
return;
}
// only fade when DH is rendering // only fade when DH is rendering
if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT if (Config.Client.Advanced.Debugging.rendererMode.get() != EDhApiRendererMode.DISABLED
// only fade when requested &&
&& Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.get() == EDhApiMcRenderingFadeMode.DOUBLE_PASS (
// only fade when requested
Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.get() == EDhApiMcRenderingFadeMode.DOUBLE_PASS
// or if LOD-only mode is enabled (fading is used to remove the MC render pass)
|| Config.Client.Advanced.Debugging.lodOnlyMode.get()
)
// don't fade when Iris shaders are active, otherwise the rendering can get weird // don't fade when Iris shaders are active, otherwise the rendering can get weird
&& !DhApiRenderProxy.INSTANCE.getDeferTransparentRendering()) && !DhApiRenderProxy.INSTANCE.getDeferTransparentRendering())
{ {
FadeRenderer.INSTANCE.render(RENDER_STATE.mcModelViewMatrix, RENDER_STATE.mcProjectionMatrix, RENDER_STATE.frameTime, RENDER_STATE.clientLevelWrapper); RenderParams renderParams = new RenderParams(EDhApiRenderPass.OPAQUE, RENDER_STATE);
fadeRenderer.render(renderParams);
} }
} }
/** /**
@@ -610,8 +706,14 @@ public class ClientApi
*/ */
public void renderFadeTransparent() public void renderFadeTransparent()
{ {
IDhVanillaFadeRenderer fadeRenderer = SingletonInjector.INSTANCE.get(IDhVanillaFadeRenderer.class);
if (fadeRenderer == null)
{
return;
}
// only fade when DH is rendering // only fade when DH is rendering
if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT) if (Config.Client.Advanced.Debugging.rendererMode.get() != EDhApiRendererMode.DISABLED)
{ {
boolean renderFade = boolean renderFade =
( (
@@ -624,17 +726,20 @@ public class ClientApi
&& !DhApiRenderProxy.INSTANCE.getDeferTransparentRendering(); && !DhApiRenderProxy.INSTANCE.getDeferTransparentRendering();
if (renderFade) if (renderFade)
{ {
FadeRenderer.INSTANCE.render(RENDER_STATE.mcModelViewMatrix, RENDER_STATE.mcProjectionMatrix, RENDER_STATE.frameTime, RENDER_STATE.clientLevelWrapper); RenderParams renderParams = new RenderParams(EDhApiRenderPass.TRANSPARENT, RENDER_STATE);
fadeRenderer.render(renderParams);
} }
} }
} }
///endregion
//=================// //==========//
// DEBUG USE // // keyboard //
//=================// //==========//
///region
/** Trigger once on key press, with CLIENT PLAYER. */ /** Trigger once on key press, with CLIENT PLAYER. */
public void keyPressedEvent(int glfwKey) public void keyPressedEvent(int glfwKey)
@@ -646,23 +751,32 @@ public class ClientApi
} }
if (glfwKey == GLFW.GLFW_KEY_F8) if (glfwKey == GLFW.GLFW_KEY_F6)
{
Config.Client.Advanced.Debugging.debugRendering.set(EDhApiDebugRendering.next(Config.Client.Advanced.Debugging.debugRendering.get()));
MC_CLIENT.sendChatMessage("F8: Set debug mode to " + Config.Client.Advanced.Debugging.debugRendering.get());
}
else if (glfwKey == GLFW.GLFW_KEY_F6)
{ {
Config.Client.Advanced.Debugging.rendererMode.set(EDhApiRendererMode.next(Config.Client.Advanced.Debugging.rendererMode.get())); Config.Client.Advanced.Debugging.rendererMode.set(EDhApiRendererMode.next(Config.Client.Advanced.Debugging.rendererMode.get()));
MC_CLIENT.sendChatMessage("F6: Set rendering to " + Config.Client.Advanced.Debugging.rendererMode.get()); MC_CLIENT.sendChatMessage("F6: Set rendering to " + Config.Client.Advanced.Debugging.rendererMode.get());
} }
else if (glfwKey == GLFW.GLFW_KEY_P) else if (glfwKey == GLFW.GLFW_KEY_F7)
{ {
prefLoggerEnabled = !prefLoggerEnabled; Config.Client.Advanced.Debugging.lodOnlyMode.set(!Config.Client.Advanced.Debugging.lodOnlyMode.get());
MC_CLIENT.sendChatMessage("P: Debug Pref Logger is " + (prefLoggerEnabled ? "enabled" : "disabled")); MC_CLIENT.sendChatMessage("F7: Set LOD only mode to " + Config.Client.Advanced.Debugging.lodOnlyMode.get());
}
else if (glfwKey == GLFW.GLFW_KEY_F8)
{
Config.Client.Advanced.Debugging.debugRendering.set(EDhApiDebugRendering.next(Config.Client.Advanced.Debugging.debugRendering.get()));
MC_CLIENT.sendChatMessage("F8: Set debug mode to " + Config.Client.Advanced.Debugging.debugRendering.get());
} }
} }
///endregion
//======//
// chat //
//======//
///region
private void sendQueuedChatMessages() private void sendQueuedChatMessages()
{ {
// this includes if the current build is a dev build // this includes if the current build is a dev build
@@ -705,15 +819,15 @@ public class ClientApi
{ {
// dev build // dev build
if (ModInfo.IS_DEV_BUILD if (ModInfo.IS_DEV_BUILD
&& !this.isDevBuildMessagePrinted && MC_CLIENT.playerExists()) && !this.isDevBuildMessagePrinted
&& MC_CLIENT.playerExists())
{ {
this.isDevBuildMessagePrinted = true; this.isDevBuildMessagePrinted = true;
this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis(); this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis();
// remind the user that this is a development build // remind the user that this is a development build
String message = String message =
// green text MinecraftTextFormat.DARK_GREEN + "Distant Horizons: nightly/unstable build, version: [" + ModInfo.VERSION+"]." + MinecraftTextFormat.CLEAR_FORMATTING + "\n" +
"\u00A72" + "Distant Horizons: nightly/unstable build, version: [" + ModInfo.VERSION+"]." + "\u00A7r\n" +
"Issues may occur with this version.\n" + "Issues may occur with this version.\n" +
"Here be dragons!\n"; "Here be dragons!\n";
MC_CLIENT.sendChatMessage(message); MC_CLIENT.sendChatMessage(message);
@@ -737,7 +851,7 @@ public class ClientApi
{ {
String message = String message =
// orange text // orange text
"\u00A76" + "Distant Horizons: Low memory detected." + "\u00A7r \n" + MinecraftTextFormat.ORANGE + "Distant Horizons: Low memory detected." + MinecraftTextFormat.CLEAR_FORMATTING + "\n" +
"Stuttering or low FPS may occur. \n" + "Stuttering or low FPS may occur. \n" +
"Please increase Minecraft's available memory to 4 GB or more. \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"; "This warning can be disabled in DH's config under Advanced -> Logging. \n";
@@ -751,22 +865,21 @@ public class ClientApi
if (!this.highVanillaRenderDistanceWarningPrinted if (!this.highVanillaRenderDistanceWarningPrinted
&& Config.Common.Logging.Warning.showHighVanillaRenderDistanceWarning.get()) && Config.Common.Logging.Warning.showHighVanillaRenderDistanceWarning.get())
{ {
this.highVanillaRenderDistanceWarningPrinted = true;
// DH generally doesn't need a vanilla render distance above 12 // DH generally doesn't need a vanilla render distance above 12
if (MC_RENDER.getRenderDistance() > 12) if (MC_RENDER.getRenderDistance() > 12)
{ {
this.highVanillaRenderDistanceWarningPrinted = true;
this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis(); this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis();
String message = String message =
// yellow text MinecraftTextFormat.YELLOW + "Distant Horizons: High vanilla render distance detected." + MinecraftTextFormat.CLEAR_FORMATTING + "\n" +
"\u00A7e" + "Distant Horizons: High vanilla render distance detected." + "\u00A7r \n" + "Using a high vanilla render distance uses a lot of CPU power \n" +
"Using a high vanilla render distance uses a lot of CPU power \n" + "and doesn't improve graphics much after about 12.\n" +
"and doesn't improve graphics much after about 12.\n" + "Lowering your vanilla render distance will give you better FPS\n" +
"Lowing your vanilla render distance will give you better FPS\n" + "and reduce stuttering at a similar visual quality.\n" +
"and reduce stuttering at a similar visual quality.\n" + MinecraftTextFormat.GRAY + "A vanilla render distance of 8 is recommended." + MinecraftTextFormat.CLEAR_FORMATTING + "\n" +
// gray text "This message can be disabled in DH's config under Advanced -> Logging.\n";
"\u00A77" + "A vanilla render distance of 8 is recommended." + "\u00A7r \n" +
"This message can be disabled in DH's config under Advanced -> Logging.\n";
MC_CLIENT.sendChatMessage(message); MC_CLIENT.sendChatMessage(message);
} }
} }
@@ -796,6 +909,8 @@ public class ClientApi
*/ */
public void showOverlayMessageNextFrame(String message) { this.overlayMessageQueueForNextFrame.add(message); } public void showOverlayMessageNextFrame(String message) { this.overlayMessageQueueForNextFrame.add(message); }
///endregion
} }
@@ -4,13 +4,14 @@ import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager; import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager;
import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel; import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent; import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent;
import com.seibel.distanthorizons.core.network.messages.base.LevelInitMessage; import com.seibel.distanthorizons.core.network.messages.base.LevelInitMessage;
import com.seibel.distanthorizons.core.network.session.NetworkSession; import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.render.RenderThreadTaskHandler;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import org.apache.logging.log4j.LogManager;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -22,8 +23,10 @@ import java.util.function.Consumer;
*/ */
public class ClientPluginChannelApi public class ClientPluginChannelApi
{ {
private static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(), private static final DhLogger LOGGER = new DhLoggerBuilder()
() -> Config.Common.Logging.logNetworkEvent.get()); .fileLevelConfig(Config.Common.Logging.logNetworkEventToFile)
.build();
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IKeyedClientLevelManager KEYED_CLIENT_LEVEL_MANAGER = SingletonInjector.INSTANCE.get(IKeyedClientLevelManager.class); private static final IKeyedClientLevelManager KEYED_CLIENT_LEVEL_MANAGER = SingletonInjector.INSTANCE.get(IKeyedClientLevelManager.class);
@@ -75,14 +78,19 @@ public class ClientPluginChannelApi
private void onLevelInitMessage(LevelInitMessage msg) private void onLevelInitMessage(LevelInitMessage msg)
{ {
if (!msg.levelKey.matches(LevelInitMessage.VALIDATION_REGEX)) if (!msg.serverKey.isEmpty() && !msg.serverKey.matches(LevelInitMessage.SERVER_KEY_REGEX))
{
throw new IllegalArgumentException("Server sent invalid server key.");
}
if (!msg.levelKey.matches(LevelInitMessage.LEVEL_KEY_REGEX))
{ {
throw new IllegalArgumentException("Server sent invalid level key."); throw new IllegalArgumentException("Server sent invalid level key.");
} }
LOGGER.info("Server level key received: [" + msg.levelKey + "]."); LOGGER.info("Server level key received: [" + msg.levelKey + "].");
MC.executeOnRenderThread(() -> RenderThreadTaskHandler.INSTANCE.queueRunningOnRenderThread(() ->
{ {
IClientLevelWrapper clientLevel = MC.getWrappedClientLevel(true); IClientLevelWrapper clientLevel = MC.getWrappedClientLevel(true);
IServerKeyedClientLevel existingKeyedClientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel(); IServerKeyedClientLevel existingKeyedClientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel();
@@ -105,10 +113,12 @@ public class ClientPluginChannelApi
this.levelUnloadHandler.accept(clientLevel); this.levelUnloadHandler.accept(clientLevel);
} }
if (existingKeyedClientLevel == null || !existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey)) if (existingKeyedClientLevel == null
|| !existingKeyedClientLevel.getServerKey().equals(msg.serverKey)
|| !existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey))
{ {
LOGGER.info("Loading level with key: [" + msg.levelKey + "]."); LOGGER.info("Loading level with key: [" + msg.levelKey + "].");
IServerKeyedClientLevel keyedLevel = KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel, msg.levelKey); IServerKeyedClientLevel keyedLevel = KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel, msg.serverKey, msg.levelKey);
this.levelLoadHandler.accept(keyedLevel); this.levelLoadHandler.accept(keyedLevel);
} }
}); });
@@ -30,7 +30,7 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/** /**
@@ -41,7 +41,7 @@ public class ServerApi
{ {
public static final ServerApi INSTANCE = new ServerApi(); public static final ServerApi INSTANCE = new ServerApi();
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
@@ -53,30 +53,6 @@ public class ServerApi
//=============//
// tick events //
//=============//
public void serverTickEvent()
{
try
{
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld();
if (serverWorld != null)
{
serverWorld.serverTick();
SharedApi.worldGenTick(serverWorld::worldGenTick);
}
}
catch (Exception e)
{
// try catch is necessary to prevent crashing the internal server when an exception is thrown
LOGGER.error("ServerTickEvent error: " + e.getMessage(), e);
}
}
//===============// //===============//
// server events // // server events //
//===============// //===============//
@@ -106,15 +82,15 @@ public class ServerApi
// level events // // level events //
//==============// //==============//
public void serverLevelLoadEvent(IServerLevelWrapper level) public void serverLevelLoadEvent(IServerLevelWrapper levelWrapper)
{ {
LOGGER.debug("Server Level " + level + " loading"); LOGGER.debug("Server Level " + levelWrapper + " loading");
AbstractDhWorld serverWorld = SharedApi.getAbstractDhWorld(); AbstractDhWorld serverWorld = SharedApi.getAbstractDhWorld();
if (serverWorld != null) if (serverWorld != null)
{ {
serverWorld.getOrLoadLevel(level); serverWorld.getOrLoadLevel(levelWrapper);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level)); ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(levelWrapper));
} }
} }
public void serverLevelUnloadEvent(IServerLevelWrapper level) public void serverLevelUnloadEvent(IServerLevelWrapper level)
@@ -125,7 +101,6 @@ public class ServerApi
if (serverWorld != null) if (serverWorld != null)
{ {
serverWorld.unloadLevel(level); serverWorld.unloadLevel(level);
SharedApi.INSTANCE.clearQueuedChunkUpdates();
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(level)); ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(level));
} }
} }
@@ -136,8 +111,8 @@ public class ServerApi
// chunk modified events // // chunk modified events //
//=======================// //=======================//
public void serverChunkLoadEvent(IChunkWrapper chunkWrapper, ILevelWrapper level) { SharedApi.INSTANCE.applyChunkUpdate(chunkWrapper, level, false, false); } public void serverChunkLoadEvent(IChunkWrapper chunkWrapper, ILevelWrapper level) { SharedApi.INSTANCE.applyChunkUpdate(chunkWrapper, level); }
public void serverChunkSaveEvent(IChunkWrapper chunkWrapper, ILevelWrapper level) { SharedApi.INSTANCE.applyChunkUpdate(chunkWrapper, level, true, false); } public void serverChunkSaveEvent(IChunkWrapper chunkWrapper, ILevelWrapper level) { SharedApi.INSTANCE.applyChunkUpdate(chunkWrapper, level); }
@@ -152,7 +127,7 @@ public class ServerApi
return; return;
} }
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); IDhServerWorld serverWorld = SharedApi.tryGetDhServerWorld();
LOGGER.info("Player ["+player.getName()+"] joined."); LOGGER.info("Player ["+player.getName()+"] joined.");
if (serverWorld != null) if (serverWorld != null)
{ {
@@ -166,7 +141,7 @@ public class ServerApi
return; return;
} }
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); IDhServerWorld serverWorld = SharedApi.tryGetDhServerWorld();
LOGGER.info("Player ["+player.getName()+"] disconnected."); LOGGER.info("Player ["+player.getName()+"] disconnected.");
if (serverWorld != null) if (serverWorld != null)
{ {
@@ -180,7 +155,7 @@ public class ServerApi
return; return;
} }
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); IDhServerWorld serverWorld = SharedApi.tryGetDhServerWorld();
LOGGER.info("Player ["+player.getName()+"] changed level: ["+originLevel.getKeyedLevelDimensionName()+"] -> ["+destinationLevel.getKeyedLevelDimensionName()+"]."); LOGGER.info("Player ["+player.getName()+"] changed level: ["+originLevel.getKeyedLevelDimensionName()+"] -> ["+destinationLevel.getKeyedLevelDimensionName()+"].");
if (serverWorld != null) if (serverWorld != null)
{ {
@@ -200,7 +175,7 @@ public class ServerApi
return; return;
} }
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); IDhServerWorld serverWorld = SharedApi.tryGetDhServerWorld();
if (serverWorld != null) if (serverWorld != null)
{ {
serverWorld.getServerPlayerStateManager().handlePluginMessage(player, message); serverWorld.getServerPlayerStateManager().handlePluginMessage(player, message);
@@ -24,30 +24,25 @@ import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiWorldUn
import com.seibel.distanthorizons.core.Initializer; import com.seibel.distanthorizons.core.Initializer;
import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateData; import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateData;
import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateQueueManager; import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateQueueManager;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.api.internal.chunkUpdating.WorldChunkUpdateManager;
import com.seibel.distanthorizons.core.config.eventHandlers.IgnoredDimensionCsvHandler;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.level.DhClientLevel; import com.seibel.distanthorizons.core.level.DhClientLevel;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.render.renderer.AbstractDebugWireframeRenderer;
import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo; import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.objects.Pair; import com.seibel.distanthorizons.core.util.objects.Pair;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.world.*; import com.seibel.distanthorizons.core.world.*;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.*;
@@ -58,44 +53,35 @@ public class SharedApi
{ {
public static final SharedApi INSTANCE = new SharedApi(); public static final SharedApi INSTANCE = new SharedApi();
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
/** will be null on the server-side */ /** will be null on the server-side */
@Nullable @Nullable
private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
/** will be null on the server-side */
@Nullable
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
public static final ChunkUpdateQueueManager CHUNK_UPDATE_QUEUE_MANAGER = new ChunkUpdateQueueManager(); public static final WorldChunkUpdateManager WORLD_CHUNK_UPDATE_MANAGER = WorldChunkUpdateManager.INSTANCE; // local fariable for quick access
/**
* 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
*/
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 */
public static final int MIN_MS_BETWEEN_OVERLOADED_LOG_MESSAGE = 30_000;
@Nullable @Nullable
private static AbstractDhWorld currentWorld; private static AbstractDhWorld currentWorld;
private static int lastWorldGenTickDelta = 0;
//=============// //=============//
// constructor // // constructor //
//=============// //=============//
//region
private SharedApi() { } private SharedApi() { }
public static void init() { Initializer.init(); } public static void init() { Initializer.init(); }
//endregion
//===============// //===============//
// world methods // // world methods //
//===============// //===============//
//region
public static EWorldEnvironment getEnvironment() { return (currentWorld == null) ? null : currentWorld.environment; } public static EWorldEnvironment getEnvironment() { return (currentWorld == null) ? null : currentWorld.environment; }
@@ -119,7 +105,10 @@ public class SharedApi
else else
{ {
ThreadPoolUtil.shutdownThreadPools(); ThreadPoolUtil.shutdownThreadPools();
DebugRenderer.clearRenderables();
// delayed get because SharedApi will be created before the singleton has been bound
AbstractDebugWireframeRenderer debugWireframeRenderer = SingletonInjector.INSTANCE.get(AbstractDebugWireframeRenderer.class);
debugWireframeRenderer.clearRenderables();
if (MC_RENDER != null) if (MC_RENDER != null)
{ {
@@ -129,7 +118,7 @@ public class SharedApi
// shouldn't be necessary, but if we missed closing one of the connections this should make sure they're all closed // shouldn't be necessary, but if we missed closing one of the connections this should make sure they're all closed
AbstractDhRepo.closeAllConnections(); AbstractDhRepo.closeAllConnections();
// needs to be closed on world shutdown to clear out un-processed chunks // needs to be closed on world shutdown to clear out un-processed chunks
CHUNK_UPDATE_QUEUE_MANAGER.clear(); WORLD_CHUNK_UPDATE_MANAGER.clear();
// recommend that the garbage collector cleans up any objects from the old world and thread pools // recommend that the garbage collector cleans up any objects from the old world and thread pools
System.gc(); System.gc();
@@ -141,63 +130,59 @@ public class SharedApi
} }
} }
public static void worldGenTick(Runnable worldGenRunnable)
{
lastWorldGenTickDelta--;
if (lastWorldGenTickDelta <= 0)
{
worldGenRunnable.run();
lastWorldGenTickDelta = 20;
}
}
@Nullable @Nullable
public static AbstractDhWorld getAbstractDhWorld() { return currentWorld; } public static AbstractDhWorld getAbstractDhWorld() { return currentWorld; }
/** returns null if the {@link SharedApi#currentWorld} isn't a {@link DhClientServerWorld} */
public static DhClientServerWorld getDhClientServerWorld() { return (currentWorld instanceof DhClientServerWorld) ? (DhClientServerWorld) currentWorld : null; }
/** returns null if the {@link SharedApi#currentWorld} isn't a {@link DhClientWorld} or {@link DhClientServerWorld} */ /** returns null if the {@link SharedApi#currentWorld} isn't a {@link DhClientWorld} or {@link DhClientServerWorld} */
public static IDhClientWorld getIDhClientWorld() { return (currentWorld instanceof IDhClientWorld) ? (IDhClientWorld) currentWorld : null; } @Nullable
public static IDhClientWorld tryGetDhClientWorld() { return (currentWorld instanceof IDhClientWorld) ? (IDhClientWorld) currentWorld : null; }
/** returns null if the {@link SharedApi#currentWorld} isn't a {@link DhServerWorld} or {@link DhClientServerWorld} */ /** returns null if the {@link SharedApi#currentWorld} isn't a {@link DhServerWorld} or {@link DhClientServerWorld} */
public static IDhServerWorld getIDhServerWorld() { return (currentWorld instanceof IDhServerWorld) ? (IDhServerWorld) currentWorld : null; } @Nullable
public static IDhServerWorld tryGetDhServerWorld() { return (currentWorld instanceof IDhServerWorld) ? (IDhServerWorld) currentWorld : null; }
//endregion
//==============// //==============//
// chunk update // // chunk update //
//==============// //==============//
//region
/** /**
* Used to prevent getting a full chunk from MC if it isn't necessary. <br> * Used to prevent getting a full chunk from MC if it isn't necessary. <br>
* This is important since asking MC for a chunk is slow and may block the render thread. * This is important since asking MC for a chunk is slow and may block the render thread.
*/ */
public static boolean isChunkAtBlockPosAlreadyUpdating(int blockPosX, int blockPosZ) public static boolean isChunkAtBlockPosAlreadyUpdating(ILevelWrapper levelWrapper, int blockPosX, int blockPosZ)
{ return CHUNK_UPDATE_QUEUE_MANAGER.contains(new DhChunkPos(new DhBlockPos2D(blockPosX, blockPosZ))); }
public static boolean isChunkAtChunkPosAlreadyUpdating(int chunkPosX, int chunkPosZ)
{ return CHUNK_UPDATE_QUEUE_MANAGER.contains(new DhChunkPos(chunkPosX, chunkPosZ)); }
/**
* This is often fired when unloading a level.
* This is done to prevent overloading the system when
* rapidly changing dimensions.
* (IE prevent DH from infinitely allocating memory
*/
public void clearQueuedChunkUpdates() { CHUNK_UPDATE_QUEUE_MANAGER.clear(); }
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, false); }
public void chunkLoadEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true, true); }
//public void applyChunkUpdate(IChunkWrapper chunkWrapper, ILevelWrapper level, boolean canGetNeighboringChunks) { this.applyChunkUpdate(chunkWrapper, level, canGetNeighboringChunks, false); }
public void applyChunkUpdate(IChunkWrapper chunkWrapper, ILevelWrapper level, boolean canGetNeighboringChunks, boolean newlyLoaded)
{ {
//========================// ChunkUpdateQueueManager manager = WORLD_CHUNK_UPDATE_MANAGER.getByLevelWrapper(levelWrapper);
// world and level checks // if (manager == null)
//========================// {
return true;
}
return manager.contains(new DhChunkPos(new DhBlockPos2D(blockPosX, blockPosZ)));
}
public static boolean isChunkAtChunkPosAlreadyUpdating(ILevelWrapper levelWrapper, int chunkPosX, int chunkPosZ)
{
ChunkUpdateQueueManager manager = WORLD_CHUNK_UPDATE_MANAGER.getByLevelWrapper(levelWrapper);
if (manager == null)
{
return true;
}
return manager.contains(new DhChunkPos(chunkPosX, chunkPosZ));
}
public void applyChunkUpdate(IChunkWrapper chunkWrapper, ILevelWrapper levelWrapper)
{
//===================//
// validation checks //
//===================//
if (chunkWrapper == null) if (chunkWrapper == null)
{ {
@@ -208,11 +193,11 @@ public class SharedApi
AbstractDhWorld dhWorld = SharedApi.getAbstractDhWorld(); AbstractDhWorld dhWorld = SharedApi.getAbstractDhWorld();
if (dhWorld == null) if (dhWorld == null)
{ {
if (level instanceof IClientLevelWrapper) if (levelWrapper instanceof IClientLevelWrapper)
{ {
// If the client world isn't loaded yet, keep track of which chunks were loaded so we can use them later. // If the client world isn't loaded yet, keep track of which chunks were loaded so we can use them later.
// This may happen if the client world and client level load events happen out of order // This may happen if the client world and client level load events happen out of order
IClientLevelWrapper clientLevel = (IClientLevelWrapper) level; IClientLevelWrapper clientLevel = (IClientLevelWrapper) levelWrapper;
ClientApi.INSTANCE.waitingChunkByClientLevelAndPos.replace(new Pair<>(clientLevel, chunkWrapper.getChunkPos()), chunkWrapper); ClientApi.INSTANCE.waitingChunkByClientLevelAndPos.replace(new Pair<>(clientLevel, chunkWrapper.getChunkPos()), chunkWrapper);
} }
@@ -225,21 +210,21 @@ public class SharedApi
return; return;
} }
// only continue if the level is loaded // only continue if the level is loaded
IDhLevel dhLevel = dhWorld.getLevel(level); IDhLevel dhLevel = dhWorld.getLevel(levelWrapper);
if (dhLevel == null) if (dhLevel == null)
{ {
if (level instanceof IClientLevelWrapper) if (levelWrapper instanceof IClientLevelWrapper)
{ {
// the client level isn't loaded yet // the client level isn't loaded yet
IClientLevelWrapper clientLevel = (IClientLevelWrapper) level; IClientLevelWrapper clientLevel = (IClientLevelWrapper) levelWrapper;
ClientApi.INSTANCE.waitingChunkByClientLevelAndPos.replace(new Pair<>(clientLevel, chunkWrapper.getChunkPos()), chunkWrapper); ClientApi.INSTANCE.waitingChunkByClientLevelAndPos.replace(new Pair<>(clientLevel, chunkWrapper.getChunkPos()), chunkWrapper);
} }
return; return;
} }
// ignore chunk updates if the network should handle them
if (dhLevel instanceof DhClientLevel) if (dhLevel instanceof DhClientLevel)
{ {
if (!((DhClientLevel) dhLevel).shouldProcessChunkUpdate(chunkWrapper.getChunkPos())) if (!((DhClientLevel) dhLevel).shouldProcessChunkUpdate(chunkWrapper.getChunkPos()))
@@ -248,291 +233,67 @@ public class SharedApi
} }
} }
// shoudln't normally happen, but just in case // ignore chunk updates for non-rendered levels
if (CHUNK_UPDATE_QUEUE_MANAGER.contains(chunkWrapper.getChunkPos())) String dimName = dhLevel.getLevelWrapper().getDimensionName();
if (IgnoredDimensionCsvHandler.INSTANCE.dimensionNameShouldBeIgnored(dimName))
{
return;
}
ChunkUpdateQueueManager chunkManager = WORLD_CHUNK_UPDATE_MANAGER.getByLevelWrapper(levelWrapper);
// ignore the wrong level wrapper type or
// if the chunk is already queued for handling
if (chunkManager == null
|| chunkManager.contains(chunkWrapper.getChunkPos()))
{ {
// TODO this will prevent some LODs from updating across dimensions if multiple levels are loaded
return; return;
} }
queueChunkUpdate(chunkManager, chunkWrapper, dhLevel);
//===============================//
// update the necessary chunk(s) //
//===============================//
if (!canGetNeighboringChunks)
{
// only update the center chunk
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, neighboringChunkList, dhLevel, true);
}
}
private static ArrayList<IChunkWrapper> getNeighborChunkListForChunk(IChunkWrapper chunkWrapper, IDhLevel dhLevel)
{
// get the neighboring chunk list
ArrayList<IChunkWrapper> neighborChunkList = new ArrayList<>(9);
for (int xOffset = -1; xOffset <= 1; xOffset++)
{
for (int zOffset = -1; zOffset <= 1; zOffset++)
{
if (xOffset == 0 && zOffset == 0)
{
// center chunk
neighborChunkList.add(chunkWrapper);
}
else
{
// neighboring chunk
DhChunkPos neighborPos = new DhChunkPos(chunkWrapper.getChunkPos().getX() + xOffset, chunkWrapper.getChunkPos().getZ() + zOffset);
IChunkWrapper neighborChunk = dhLevel.getLevelWrapper().tryGetChunk(neighborPos);
if (neighborChunk != null)
{
neighborChunkList.add(neighborChunk);
}
}
}
}
return neighborChunkList;
} }
private static void queueChunkUpdate(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighborChunkList, IDhLevel dhLevel, boolean canGetNeighboringChunks) private static void queueChunkUpdate(ChunkUpdateQueueManager chunkManager, IChunkWrapper chunkWrapper, IDhLevel dhLevel)
{ {
// return if the chunk is already queued // return if the chunk is already queued
if (CHUNK_UPDATE_QUEUE_MANAGER.contains(chunkWrapper.getChunkPos())) if (chunkManager.contains(chunkWrapper.getChunkPos()))
{ {
return; return;
} }
// add chunk update data to preUpdate queue // add chunk update data to preUpdate queue
ChunkUpdateData updateData = new ChunkUpdateData(chunkWrapper, neighborChunkList, dhLevel, canGetNeighboringChunks); ChunkUpdateData updateData = new ChunkUpdateData(chunkWrapper, dhLevel);
CHUNK_UPDATE_QUEUE_MANAGER.addItemToPreUpdateQueue(chunkWrapper.getChunkPos(), updateData); chunkManager.addItemToPreUpdateQueue(chunkWrapper.getChunkPos(), updateData);
// queue updates up to the number of CPU cores allocated for the job
// (this prevents doing extra work queuing tasks that may not be necessary)
// and makes sure the chunks closest to the player are updated first
PriorityTaskPicker.Executor executor = ThreadPoolUtil.getChunkToLodBuilderExecutor();
if (executor != null && executor.getQueueSize() < executor.getPoolSize())
{
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 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 // queue the next position if there are still positions to process
AbstractExecutorService executor = ThreadPoolUtil.getChunkToLodBuilderExecutor(); AbstractExecutorService executor = ThreadPoolUtil.getChunkToLodBuilderExecutor();
if (executor != null && !CHUNK_UPDATE_QUEUE_MANAGER.isEmpty()) if (executor != null)
{ {
try try
{ {
executor.execute(SharedApi::processQueue); executor.execute(WORLD_CHUNK_UPDATE_MANAGER::processEachQueue);
} }
catch (RejectedExecutionException ignore) catch (RejectedExecutionException ignore)
{ {
// the executor was shut down, it should be back up shortly and able to accept new jobs // the executor was shut down, it should be back up shortly and able to accept new jobs
} }
} }
} }
private static void processQueuedChunkPreUpdate() //endregion
{
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());
ChunkUpdateData updateData = CHUNK_UPDATE_QUEUE_MANAGER.updateQueue.popClosest();
if (updateData == null)
{
return;
}
IChunkWrapper chunkWrapper = updateData.chunkWrapper;
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
{
// 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);
}
}
//=========// //=========//
// F3 Menu // // F3 Menu //
//=========// //=========//
//region
public String getDebugMenuString() public ArrayList<String> getDebugMenuString() { return WORLD_CHUNK_UPDATE_MANAGER.getDebugMenuString(); }
{
String preUpdatingCountStr = F3Screen.NUMBER_FORMAT.format(CHUNK_UPDATE_QUEUE_MANAGER.preUpdateQueue.getQueuedCount()); //endregion
String updatingCountStr = F3Screen.NUMBER_FORMAT.format(CHUNK_UPDATE_QUEUE_MANAGER.updateQueue.getQueuedCount());
String queuedCountStr = F3Screen.NUMBER_FORMAT.format(CHUNK_UPDATE_QUEUE_MANAGER.getQueuedCount());
String maxUpdateCountStr = F3Screen.NUMBER_FORMAT.format(CHUNK_UPDATE_QUEUE_MANAGER.maxSize);
return "Queued chunk updates: "+"( "+preUpdatingCountStr+" + "+updatingCountStr+" ) [ "+queuedCountStr+" / "+maxUpdateCountStr+" ]";
}
@@ -2,6 +2,7 @@ package com.seibel.distanthorizons.core.api.internal.chunkUpdating;
import com.seibel.distanthorizons.core.api.internal.SharedApi; import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import org.jetbrains.annotations.Nullable;
import java.util.Comparator; import java.util.Comparator;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@@ -105,6 +106,7 @@ public class ChunkPosQueue
this.furthestQueue.remove(closest); this.furthestQueue.remove(closest);
return this.updateDataByChunkPos.remove(closest); return this.updateDataByChunkPos.remove(closest);
} }
@Nullable
public ChunkUpdateData popFurthest() public ChunkUpdateData popFurthest()
{ {
if (this.furthestQueue.isEmpty()) if (this.furthestQueue.isEmpty())
@@ -9,18 +9,13 @@ import java.util.ArrayList;
public class ChunkUpdateData public class ChunkUpdateData
{ {
public IChunkWrapper chunkWrapper; public IChunkWrapper chunkWrapper;
@Nullable
public ArrayList<IChunkWrapper> neighborChunkList;
public IDhLevel dhLevel; public IDhLevel dhLevel;
public boolean canGetNeighboringChunks;
public ChunkUpdateData(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighborChunkList, IDhLevel dhLevel, boolean canGetNeighborChunks) public ChunkUpdateData(IChunkWrapper chunkWrapper, IDhLevel dhLevel)
{ {
this.chunkWrapper = chunkWrapper; this.chunkWrapper = chunkWrapper;
this.neighborChunkList = neighborChunkList;
this.dhLevel = dhLevel; this.dhLevel = dhLevel;
this.canGetNeighboringChunks = canGetNeighborChunks;
} }
} }
@@ -1,30 +1,79 @@
package com.seibel.distanthorizons.core.api.internal.chunkUpdating; package com.seibel.distanthorizons.core.api.internal.chunkUpdating;
import com.google.common.cache.CacheBuilder;
import com.seibel.distanthorizons.core.api.internal.ClientApi; import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.api.internal.SharedApi; import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.MinecraftTextFormat;
import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.world.EWorldEnvironment; import com.seibel.distanthorizons.core.world.EWorldEnvironment;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.*;
/**
* @see WorldChunkUpdateManager
*/
public class ChunkUpdateQueueManager public class ChunkUpdateQueueManager
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
/**
* 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
*/
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 */
public static final int MIN_MS_BETWEEN_OVERLOADED_LOG_MESSAGE = 30_000;
private final Set<DhChunkPos> ignoredChunkPosSet = Collections.newSetFromMap(new ConcurrentHashMap<>());
private static long lastOverloadedLogMessageMsTime = 0;
public final ChunkPosQueue updateQueue; public final ChunkPosQueue updateQueue;
public final ChunkPosQueue preUpdateQueue; public final ChunkPosQueue preUpdateQueue;
public final ConcurrentMap<DhChunkPos, IChunkWrapper> queuedChunkWrapperByChunkPos = CacheBuilder.newBuilder()
.expireAfterWrite(20, TimeUnit.SECONDS)
.<DhChunkPos, IChunkWrapper>build()
.asMap();
/** dynamically changes based on the number of threads currently available */
public int maxSize = 500; public int maxSize = 500;
private static long lastOverloadedLogMessageMsTime = 0; /** used to prevent flickering */
public long lastMsTimeShownActiveInF3Screen = System.currentTimeMillis();
//=============// //=============//
// constructor // // constructor //
//=============// //=============//
//region
public ChunkUpdateQueueManager() public ChunkUpdateQueueManager()
{ {
@@ -32,21 +81,31 @@ public class ChunkUpdateQueueManager
this.preUpdateQueue = new ChunkPosQueue(); this.preUpdateQueue = new ChunkPosQueue();
} }
//endregion
//==================// //==================//
// list/set methods // // list/set methods //
//==================// //==================//
//region
public boolean contains(DhChunkPos pos) { return this.updateQueue.contains(pos) || this.preUpdateQueue.contains(pos); } public boolean contains(DhChunkPos pos)
{
return this.updateQueue.contains(pos)
|| this.ignoredChunkPosSet.contains(pos)
|| this.preUpdateQueue.contains(pos);
}
public void clear() public void clear()
{ {
this.updateQueue.clear(); this.updateQueue.clear();
this.preUpdateQueue.clear(); this.preUpdateQueue.clear();
this.ignoredChunkPosSet.clear();
} }
public int getQueuedCount() { return this.updateQueue.getQueuedCount() + this.preUpdateQueue.getQueuedCount(); } public int getQueuedCount() { return this.updateQueue.getQueuedCount() + this.preUpdateQueue.getQueuedCount(); }
public boolean isEmpty()
public boolean updateQueuesEmpty()
{ {
return this.updateQueue.isEmpty() return this.updateQueue.isEmpty()
&& this.preUpdateQueue.isEmpty(); && this.preUpdateQueue.isEmpty();
@@ -57,22 +116,27 @@ public class ChunkUpdateQueueManager
* If there are no more slots, replaces the item furthest from the center in the update queue. * If there are no more slots, replaces the item furthest from the center in the update queue.
*/ */
public void addItemToPreUpdateQueue(DhChunkPos pos, ChunkUpdateData updateData) public void addItemToPreUpdateQueue(DhChunkPos pos, ChunkUpdateData updateData)
{ this.addItemToQueue(pos, updateData, this.preUpdateQueue); }
public void addItemToUpdateQueue(DhChunkPos pos, ChunkUpdateData updateData)
{ this.addItemToQueue(pos, updateData, this.updateQueue); }
private void addItemToQueue(DhChunkPos pos, ChunkUpdateData updateData, ChunkPosQueue queue)
{ {
int remainingSlots = this.maxSize - this.getQueuedCount(); int remainingSlots = this.maxSize - this.getQueuedCount();
// If no slots are left, get one by removing the item furthest from the center // If no slots are left, get one by removing the item furthest from the center
if (remainingSlots <= 0) if (remainingSlots <= 0)
{ {
if (!this.updateQueue.isEmpty()) ChunkUpdateData removedData = queue.popFurthest();
if (removedData != null)
{ {
this.updateQueue.popFurthest(); this.queuedChunkWrapperByChunkPos.remove(removedData.chunkWrapper.getChunkPos());
}
else
{
this.preUpdateQueue.popFurthest();
} }
} }
this.preUpdateQueue.addItem(pos, updateData);
queue.addItem(pos,updateData);
this.queuedChunkWrapperByChunkPos.putIfAbsent(pos, updateData.chunkWrapper);
remainingSlots = this.maxSize - this.getQueuedCount(); remainingSlots = this.maxSize - this.getQueuedCount();
if (remainingSlots <= 0) if (remainingSlots <= 0)
@@ -81,37 +145,19 @@ public class ChunkUpdateQueueManager
} }
} }
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() private void sendOverloadMessage()
{ {
// limit how often an overloaded message can be sent // limit how often an overloaded message can be sent
long msBetweenLastLog = System.currentTimeMillis() - lastOverloadedLogMessageMsTime; long msBetweenLastLog = System.currentTimeMillis() - lastOverloadedLogMessageMsTime;
if (msBetweenLastLog >= SharedApi.MIN_MS_BETWEEN_OVERLOADED_LOG_MESSAGE) if (msBetweenLastLog >= MIN_MS_BETWEEN_OVERLOADED_LOG_MESSAGE)
{ {
lastOverloadedLogMessageMsTime = System.currentTimeMillis(); lastOverloadedLogMessageMsTime = System.currentTimeMillis();
String message = "\u00A76" + "Distant Horizons overloaded, too many chunks queued for LOD processing. " + "\u00A7r" + String message = MinecraftTextFormat.ORANGE + "Distant Horizons overloaded, too many chunks queued for LOD processing. " + MinecraftTextFormat.CLEAR_FORMATTING +
"\nThis may result in holes in your LODs. " + "\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. " + "\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)."; "\nMax queue count [" + this.maxSize + "] ([" + MAX_UPDATING_CHUNK_COUNT_PER_THREAD_AND_PLAYER + "] per thread+players).";
boolean showWarningInChat = Config.Common.Logging.Warning.showUpdateQueueOverloadedChatWarning.get(); boolean showWarningInChat = Config.Common.Logging.Warning.showUpdateQueueOverloadedChatWarning.get();
if (showWarningInChat) if (showWarningInChat)
@@ -129,11 +175,200 @@ public class ChunkUpdateQueueManager
} }
} }
/**
* Tries to return a cloned chunk wrapper from memory.
* Returns null if no chunk is available.
* <br><br>
* This is done instead of accessing the MC level since
* accessing the level often requires running on the render or server
* thread, which causes stuttering.
*/
@Nullable
public IChunkWrapper tryGetChunk(DhChunkPos pos)
{
IChunkWrapper existingWrapper = this.queuedChunkWrapperByChunkPos.get(pos);
if (existingWrapper == null)
{
return null;
}
return existingWrapper.copy();
}
//endregion
//=========//
// ignores //
//=========//
//region
public void addPosToIgnore(DhChunkPos chunkPos) { this.ignoredChunkPosSet.add(chunkPos); }
public void removePosToIgnore(DhChunkPos chunkPos) { this.ignoredChunkPosSet.remove(chunkPos); }
//endregion
//===================//
// update processing //
//===================//
//region
public void processQueue()
{
// update the center & max size of the queue manager
int maxUpdateSizeMultiplier;
if (MC_CLIENT != null && MC_CLIENT.playerExists())
{
// Local worlds & multiplayer
this.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();
}
this.maxSize = MAX_UPDATING_CHUNK_COUNT_PER_THREAD_AND_PLAYER
* Config.Common.MultiThreading.numberOfThreads.get()
* maxUpdateSizeMultiplier;
//===============================//
// update the necessary chunk(s) //
//===============================//
this.processQueuedChunkPreUpdate();
this.processQueuedChunkUpdate();
// queue the next position if there are still positions to process
AbstractExecutorService executor = ThreadPoolUtil.getChunkToLodBuilderExecutor();
if (executor != null && !this.updateQueuesEmpty())
{
try
{
executor.execute(this::processQueue);
}
catch (RejectedExecutionException ignore)
{
// the executor was shut down, it should be back up shortly and able to accept new jobs
}
}
}
private void processQueuedChunkPreUpdate()
{
ChunkUpdateData preUpdateData = this.preUpdateQueue.popClosest();
if (preUpdateData == null)
{
return;
}
IDhLevel dhLevel = preUpdateData.dhLevel;
IChunkWrapper chunkWrapper = preUpdateData.chunkWrapper;
chunkWrapper.createDhHeightMaps();
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;
}
}
this.addItemToUpdateQueue(chunkWrapper.getChunkPos(), preUpdateData);
}
catch (Exception e)
{
LOGGER.error("Unexpected error when pre-updating chunk at pos: [" + chunkWrapper.getChunkPos() + "]", e);
}
}
private void processQueuedChunkUpdate()
{
ChunkUpdateData updateData = this.updateQueue.popClosest();
if (updateData == null)
{
return;
}
IChunkWrapper chunkWrapper = updateData.chunkWrapper;
IDhLevel dhLevel = updateData.dhLevel;
ILevelWrapper levelWrapper = dhLevel.getLevelWrapper();
// having a list of the nearby chunks is needed for lighting and beacon generation
ArrayList<IChunkWrapper> nearbyChunkList = this.tryGetNeighborChunkListForChunk(chunkWrapper);
try
{
// sky lighting is populated later at the data source level
DhLightingEngine.INSTANCE.bakeChunkBlockLighting(chunkWrapper, nearbyChunkList, levelWrapper.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);
}
this.queuedChunkWrapperByChunkPos.remove(updateData.chunkWrapper.getChunkPos());
}
private ArrayList<IChunkWrapper> tryGetNeighborChunkListForChunk(IChunkWrapper chunkWrapper)
{
// get the neighboring chunk list
ArrayList<IChunkWrapper> neighborChunkList = new ArrayList<>(9);
for (int xOffset = -1; xOffset <= 1; xOffset++)
{
for (int zOffset = -1; zOffset <= 1; zOffset++)
{
if (xOffset == 0 && zOffset == 0)
{
// center chunk
neighborChunkList.add(chunkWrapper);
}
else
{
// neighboring chunk
DhChunkPos neighborPos = new DhChunkPos(chunkWrapper.getChunkPos().getX() + xOffset, chunkWrapper.getChunkPos().getZ() + zOffset);
IChunkWrapper neighborChunk = this.tryGetChunk(neighborPos);
if (neighborChunk != null)
{
neighborChunkList.add(neighborChunk);
}
}
}
}
return neighborChunkList;
}
//endregion
//==================// //==================//
// position methods // // position methods //
//==================// //==================//
//region
public void setCenter(DhChunkPos newCenter) public void setCenter(DhChunkPos newCenter)
{ {
@@ -141,5 +376,33 @@ public class ChunkUpdateQueueManager
this.preUpdateQueue.setCenter(newCenter); this.preUpdateQueue.setCenter(newCenter);
} }
//endregion
//=========//
// F3 Menu //
//=========//
//region
public String getDebugMenuString()
{
String y = MinecraftTextFormat.YELLOW;
String o = MinecraftTextFormat.ORANGE;
String cf = MinecraftTextFormat.CLEAR_FORMATTING;
String preUpdatingCountStr = F3Screen.NUMBER_FORMAT.format(this.preUpdateQueue.getQueuedCount());
String updatingCountStr = F3Screen.NUMBER_FORMAT.format(this.updateQueue.getQueuedCount());
String queuedCountStr = F3Screen.NUMBER_FORMAT.format(this.getQueuedCount());
String maxUpdateCountStr = F3Screen.NUMBER_FORMAT.format(this.maxSize);
return "Queued chunk updates: "+"("+y+preUpdatingCountStr+cf+" + "+o+updatingCountStr+cf+") ["+queuedCountStr+"/"+maxUpdateCountStr+"]";
}
//endregion
} }
@@ -0,0 +1,180 @@
package com.seibel.distanthorizons.core.api.internal.chunkUpdating;
import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.world.AbstractDhWorld;
import com.seibel.distanthorizons.core.world.EWorldEnvironment;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Holds all the {@link ChunkUpdateQueueManager} for a loaded world.
* Different queues are needed for each level to prevent
* chunks from bleeding between levels (IE a nether chunk applied to the overworld).
*
* @see ChunkUpdateQueueManager
*/
public class WorldChunkUpdateManager
{
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
/** singleton since we only expect to have one world loaded at a time */
public static final WorldChunkUpdateManager INSTANCE = new WorldChunkUpdateManager();
/**
* Queues are only removed during world shutdown.
* The assumption is that there will be a limited number of {@link ILevelWrapper}'s
* for a given world.
*/
private final ConcurrentHashMap<ILevelWrapper, ChunkUpdateQueueManager> updateQueueByLevelWrapper = new ConcurrentHashMap<>();
//=============//
// constructor //
//=============//
//region
private WorldChunkUpdateManager() { }
//endregion
//=================//
// manager methods //
//=================//
//region
/**
* @return null if the world is unloaded or the given level wrapper is the wrong type
*/
@Nullable
public ChunkUpdateQueueManager getByLevelWrapper(ILevelWrapper levelWrapper)
{
AbstractDhWorld world = SharedApi.getAbstractDhWorld();
if (world == null)
{
return null;
}
// we only want to load chunks for certain level wrappers
// this is done specifically on a local-server to prevent
// loading both the server and client level wrappers
if (world.environment == EWorldEnvironment.CLIENT_ONLY
// when connected to a server we should only ever load client wrappers anyway
// but this check confirms it
&& !(levelWrapper instanceof IClientLevelWrapper))
{
return null;
}
else if (
(world.environment == EWorldEnvironment.SERVER_ONLY
|| world.environment == EWorldEnvironment.CLIENT_SERVER)
// when hosting a server we only care about the server wrappers
&& !(levelWrapper instanceof IServerLevelWrapper))
{
return null;
}
ChunkUpdateQueueManager queueManager = this.updateQueueByLevelWrapper.get(levelWrapper);
if (queueManager != null)
{
return queueManager;
}
return this.updateQueueByLevelWrapper.compute(levelWrapper,
(ILevelWrapper newLevelWrapper, ChunkUpdateQueueManager oldQueueManager) ->
{
if (oldQueueManager != null)
{
return oldQueueManager;
}
oldQueueManager = new ChunkUpdateQueueManager();
return oldQueueManager;
});
}
public void processEachQueue()
{
this.updateQueueByLevelWrapper.forEach(
(ILevelWrapper levelWrapper, ChunkUpdateQueueManager updateManager) ->
{
updateManager.processQueue();
});
}
public int getTotalQueuedCount()
{
AtomicInteger queueCountRef = new AtomicInteger(0);
this.updateQueueByLevelWrapper.forEach(
(ILevelWrapper levelWrapper, ChunkUpdateQueueManager updateManager) ->
{
queueCountRef.addAndGet(updateManager.getQueuedCount());
});
return queueCountRef.get();
}
public void clear() { this.updateQueueByLevelWrapper.clear(); }
//endregion
//=========//
// F3 Menu //
//=========//
//region
public ArrayList<String> getDebugMenuString()
{
ArrayList<String> stringList = new ArrayList<>();
stringList.add("");// placeholder for the total count
// add each queue to the list
AtomicInteger totalQueueCountRef = new AtomicInteger(0);
AtomicInteger activeQueueCountRef = new AtomicInteger(0);
this.updateQueueByLevelWrapper.forEach(
(ILevelWrapper levelWrapper, ChunkUpdateQueueManager updateManager) ->
{
// is this queue active?
if (!updateManager.updateQueuesEmpty())
{
updateManager.lastMsTimeShownActiveInF3Screen = System.currentTimeMillis();
activeQueueCountRef.incrementAndGet();
}
// show this queue if it hasn't been empty long enough
// (done to prevent flickering on the F3 screen when the queue rapidly fills/empties)
long timeSinceQueueLastShownActiveMs = System.currentTimeMillis() - updateManager.lastMsTimeShownActiveInF3Screen;
if (timeSinceQueueLastShownActiveMs < 4_000)
{
stringList.add(levelWrapper.getDimensionName() + ": " + updateManager.getDebugMenuString());
}
totalQueueCountRef.incrementAndGet();
});
// replace the first line with the number of total/active queues
// (helpful if we need to diagnose a leak due to a massive number of queue level wrappers)
stringList.set(0, "Chunk Update Queues: "+activeQueueCountRef.get()+"/"+totalQueueCountRef.get());
return stringList;
}
//endregion
}
@@ -2,6 +2,7 @@ package com.seibel.distanthorizons.core.api.internal.rendering;
import com.seibel.distanthorizons.core.api.internal.ClientApi; import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.util.math.Mat4f; import com.seibel.distanthorizons.core.util.math.Mat4f;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
/** /**
@@ -9,13 +10,36 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapp
* *
* @see ClientApi * @see ClientApi
*/ */
public class RenderState public class DhRenderState
{ {
public Mat4f mcModelViewMatrix = null; public Mat4f mcModelViewMatrix = null;
public Mat4f mcProjectionMatrix = null; public Mat4f mcProjectionMatrix = null;
public float frameTime = -1; /**
* percentage of time into the current client tick. <br><br>
*
* Can be converted to a millisecond frametime
* (IE time between frames in milliseconds) using the formula: <br>
* <code>
* (partialTickTime/20*1000)
* </code> <br>
* IE 60 FPS = 16.6 MS <br>
*
* @link https://fpstoms.com/
* @see IMinecraftRenderWrapper#getPartialTickTime()
*/
public float partialTickTime = -1;
public IClientLevelWrapper clientLevelWrapper = null; public IClientLevelWrapper clientLevelWrapper = null;
/**
* This will generally be true if the player is: <br>
* - blinded <br>
* - under lava/water <br>
* <br>
* In those cases some rendering logic may need to be changed
* to look correct.
*/
public boolean vanillaFogEnabled = false;
//========// //========//
@@ -38,7 +62,7 @@ public class RenderState
errorReasons += "no Projection Matrix, "; errorReasons += "no Projection Matrix, ";
} }
if (this.frameTime == -1) if (this.partialTickTime == -1)
{ {
errorReasons += "no Frame Time, "; errorReasons += "no Frame Time, ";
} }
@@ -51,13 +75,6 @@ public class RenderState
return errorReasons; 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 public void canRenderOrThrow() throws IllegalStateException
{ {
String errorReasons = this.unableToRenderBecause(); String errorReasons = this.unableToRenderBecause();
@@ -33,13 +33,12 @@ import com.seibel.distanthorizons.core.config.types.enums.*;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.util.NativeDialogUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.lwjgl.util.tinyfd.TinyFileDialogs;
import java.awt.*;
import java.io.File; import java.io.File;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
@@ -54,7 +53,7 @@ import java.util.List;
@SuppressWarnings("ConcatenationWithEmptyString") @SuppressWarnings("ConcatenationWithEmptyString")
public class Config public class Config
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public static ConfigCategory client = new ConfigCategory.Builder().set(Client.class).build(); public static ConfigCategory client = new ConfigCategory.Builder().set(Client.class).build();
@@ -102,11 +101,9 @@ public class Config
.build(); .build();
public static ConfigUiLinkedEntry quickEnableWorldGenerator = new ConfigUiLinkedEntry(Common.WorldGenerator.enableDistantGeneration); public static ConfigUiLinkedEntry quickEnableWorldGenerator = new ConfigUiLinkedEntry(Common.WorldGenerator.enableDistantGeneration);
public static ConfigUiLinkedEntry quickEnableServerGeneration = new ConfigUiLinkedEntry(Server.enableServerGeneration);
public static ConfigEntry<Boolean> quickShowWorldGenProgress = new ConfigEntry.Builder<Boolean>() public static ConfigUiLinkedEntry quickShowWorldGenProgress = new ConfigUiLinkedEntry(Common.WorldGenerator.showGenerationProgress);
.set(false) // TODO should be set by the underlying world gen progress button, not a static default
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.build();
public static ConfigUiLinkedEntry quickLodCloudRendering = new ConfigUiLinkedEntry(Advanced.Graphics.GenericRendering.enableCloudRendering); public static ConfigUiLinkedEntry quickLodCloudRendering = new ConfigUiLinkedEntry(Advanced.Graphics.GenericRendering.enableCloudRendering);
@@ -126,7 +123,6 @@ public class Config
{ {
// common config links need to have their destination // common config links need to have their destination
// since they aren't part of "client" config class // since they aren't part of "client" config class
// TODO determine their destination programically instead of hard coding the value
public static ConfigUIComment advancedHeader = new ConfigUIComment.Builder().setParentConfigClass(Advanced.class).build(); public static ConfigUIComment advancedHeader = new ConfigUIComment.Builder().setParentConfigClass(Advanced.class).build();
@@ -172,6 +168,20 @@ public class Config
public static ConfigCategory culling = new ConfigCategory.Builder().set(Culling.class).build(); public static ConfigCategory culling = new ConfigCategory.Builder().set(Culling.class).build();
public static ConfigUISpacer cullingSpacer = new ConfigUISpacer.Builder().build(); public static ConfigUISpacer cullingSpacer = new ConfigUISpacer.Builder().build();
public static ConfigEntry<Boolean> overrideVanillaGraphicsSettings = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment("" +
"If true some vanilla graphics settings will be automatically changed \n" +
"during DH setup to provide a better experience. \n" +
" \n" +
"IE disabling vanilla clouds (which render on top of DH LODs), \n" +
" and chunk fading (DH already fades MC chunks) \n" +
"")
.build();
public static ConfigUISpacer overrideVanillaSpacer = new ConfigUISpacer.Builder().build();
public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build(); public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build();
@@ -193,6 +203,7 @@ public class Config
+ "This indicates how quickly LODs decrease in quality the further away they are. \n" + "This indicates how quickly LODs decrease in quality the further away they are. \n"
+ "Higher settings will render higher quality fake chunks farther away, \n" + "Higher settings will render higher quality fake chunks farther away, \n"
+ "but will increase memory and GPU usage.") + "but will increase memory and GPU usage.")
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build(); .build();
public static ConfigEntry<EDhApiMaxHorizontalResolution> maxHorizontalResolution = new ConfigEntry.Builder<EDhApiMaxHorizontalResolution>() public static ConfigEntry<EDhApiMaxHorizontalResolution> maxHorizontalResolution = new ConfigEntry.Builder<EDhApiMaxHorizontalResolution>()
@@ -259,13 +270,6 @@ public class Config
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE) .addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build(); .build();
public static ConfigEntry<Double> lodBias = new ConfigEntry.Builder<Double>()
.setMinDefaultMax(0d, 0d, null)
.comment(""
+ "What the value should vanilla Minecraft's texture LodBias be? \n"
+ "If set to 0 the mod wont overwrite vanilla's default (which so happens to also be 0)")
.build();
public static ConfigEntry<EDhApiLodShading> lodShading = new ConfigEntry.Builder<EDhApiLodShading>() public static ConfigEntry<EDhApiLodShading> lodShading = new ConfigEntry.Builder<EDhApiLodShading>()
.set(EDhApiLodShading.AUTO) .set(EDhApiLodShading.AUTO)
.comment("" .comment(""
@@ -311,8 +315,16 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Double> brightnessMultiplier = new ConfigEntry.Builder<Double>() // TODO: Make this a float (the ClassicConfigGUI doesnt support floats) public static ConfigEntry<Boolean> dhFadeFarClipPlane = new ConfigEntry.Builder<Boolean>()
.set(1.0) .set(true)
.comment(""
+ "Should DH fade out before reaching the far clip plane? \n"
+ "This is helpful to prevent DH clouds from cutting off in the distance. \n"
+ "")
.build();
public static ConfigEntry<Float> brightnessMultiplier = new ConfigEntry.Builder<Float>()
.set(1.0f)
.comment("" .comment(""
+ "How bright LOD colors are. \n" + "How bright LOD colors are. \n"
+ "\n" + "\n"
@@ -322,8 +334,8 @@ public class Config
.addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE) .addListener(ReloadLodsConfigEventHandler.DELAYED_INSTANCE)
.build(); .build();
public static ConfigEntry<Double> saturationMultiplier = new ConfigEntry.Builder<Double>() // TODO: Make this a float (the ClassicConfigGUI doesnt support floats) public static ConfigEntry<Float> saturationMultiplier = new ConfigEntry.Builder<Float>()
.set(1.0) .set(1.0f)
.comment("" .comment(""
+ "How saturated LOD colors are. \n" + "How saturated LOD colors are. \n"
+ "\n" + "\n"
@@ -355,52 +367,6 @@ public class Config
.comment("Enable Screen Space Ambient Occlusion") .comment("Enable Screen Space Ambient Occlusion")
.build(); .build();
public static ConfigEntry<Integer> sampleCount = new ConfigEntry.Builder<Integer>()
.set(6)
.comment("" +
"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." +
"")
.build();
public static ConfigEntry<Double> radius = new ConfigEntry.Builder<Double>()
.set(4.0)
.comment("" +
"Determines the radius Screen Space Ambient Occlusion is applied, measured in blocks." +
"")
.build();
public static ConfigEntry<Double> strength = new ConfigEntry.Builder<Double>()
.set(0.2)
.comment("" +
"Determines how dark the Screen Space Ambient Occlusion effect will be." +
"")
.build();
public static ConfigEntry<Double> bias = new ConfigEntry.Builder<Double>()
.set(0.02)
.comment("" +
"Increasing the value can reduce banding at the cost of reducing the strength of the effect." +
"")
.build();
public static ConfigEntry<Double> minLight = new ConfigEntry.Builder<Double>()
.set(0.25)
.comment("" +
"Determines how dark the occlusion shadows can be. \n" +
"0 = totally black at the corners \n" +
"1 = no shadow" +
"")
.build();
public static ConfigEntry<Integer> blurRadius = new ConfigEntry.Builder<Integer>()
.set(2)
.comment("" +
"The radius, measured in pixels, that blurring is calculated for the SSAO. \n" +
"Higher numbers will reduce banding at the cost of GPU performance." +
"")
.build();
} }
public static class GenericRendering public static class GenericRendering
@@ -431,6 +397,15 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Boolean> expandDistantBeacons = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "If true LOD beacon beams will be rendered wider at extreme distances, \n"
+ "making them easier to see. \n"
+ "If false all LOD beacon beams will only ever be 1 block wide. \n"
+ "")
.build();
public static ConfigEntry<Boolean> enableCloudRendering = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableCloudRendering = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.comment("" .comment(""
@@ -438,6 +413,18 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<String> dimensionEnabledCloudRenderingCsv = new ConfigEntry.Builder<String>()
.set("minecraft:overworld")
.setAppearance(EConfigEntryAppearance.ALL)
.comment(""
+ "A comma separated separated list of dimension resource locations where DH clouds will render.\n"
+ "\n"
+ "Example: \"minecraft:overworld,minecraft:the_end\"\n"
+ "\n"
+ "Changes will only be seen when the world is re-loaded.\n"
+ "")
.build();
public static ConfigEntry<Boolean> enableInstancedRendering = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableInstancedRendering = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.comment("" .comment(""
@@ -449,8 +436,8 @@ public class Config
public static class Fog public static class Fog
{ {
private static final Double FOG_RANGE_MIN = 0.0; private static final Float FOG_RANGE_MIN = 0.0f;
private static final Double FOG_RANGE_MAX = Math.sqrt(2.0); private static final Float FOG_RANGE_MAX = (float)Math.sqrt(2.0);
@@ -479,16 +466,11 @@ public class Config
+ "Note: Other mods may conflict with this setting. \n" + "Note: Other mods may conflict with this setting. \n"
+ "") + "")
.build(); .build();
@Deprecated
public static ConfigEntry<Boolean> disableVanillaFog = new ConfigEntry.Builder<Boolean>()
.set(!enableVanillaFog.get())
.setAppearance(EConfigEntryAppearance.ONLY_IN_API)
.build();
public static ConfigEntry<Double> farFogStart = new ConfigEntry.Builder<Double>() public static ConfigEntry<Float> farFogStart = new ConfigEntry.Builder<Float>()
.setMinDefaultMax(FOG_RANGE_MIN, 0.4, FOG_RANGE_MAX) .setMinDefaultMax(FOG_RANGE_MIN, 0.4f, FOG_RANGE_MAX)
.comment("" .comment(""
+ "At what distance should the far fog start? \n" + "At what distance should the far fog start? \n"
+ "\n" + "\n"
@@ -497,8 +479,8 @@ public class Config
+ "1.414: Fog starts at the corner of the vanilla render distance.") + "1.414: Fog starts at the corner of the vanilla render distance.")
.build(); .build();
public static ConfigEntry<Double> farFogEnd = new ConfigEntry.Builder<Double>() public static ConfigEntry<Float> farFogEnd = new ConfigEntry.Builder<Float>()
.setMinDefaultMax(FOG_RANGE_MIN, 1.0, FOG_RANGE_MAX) .setMinDefaultMax(FOG_RANGE_MIN, 1.0f, FOG_RANGE_MAX)
.comment("" .comment(""
+ "Where should the far fog end? \n" + "Where should the far fog end? \n"
+ "\n" + "\n"
@@ -507,8 +489,8 @@ public class Config
+ "1.414: Fog ends at the corner of the vanilla render distance.") + "1.414: Fog ends at the corner of the vanilla render distance.")
.build(); .build();
public static ConfigEntry<Double> farFogMin = new ConfigEntry.Builder<Double>() public static ConfigEntry<Float> farFogMin = new ConfigEntry.Builder<Float>()
.setMinDefaultMax(-5.0, 0.0, FOG_RANGE_MAX) .setMinDefaultMax(-5.0f, 0.0f, FOG_RANGE_MAX)
.comment("" .comment(""
+ "What is the minimum fog thickness? \n" + "What is the minimum fog thickness? \n"
+ "\n" + "\n"
@@ -516,8 +498,8 @@ public class Config
+ "1.0: Fully opaque fog.") + "1.0: Fully opaque fog.")
.build(); .build();
public static ConfigEntry<Double> farFogMax = new ConfigEntry.Builder<Double>() public static ConfigEntry<Float> farFogMax = new ConfigEntry.Builder<Float>()
.setMinDefaultMax(FOG_RANGE_MIN, 1.0, 5.0) .setMinDefaultMax(FOG_RANGE_MIN, 1.0f, 5.0f)
.comment("" .comment(""
+ "What is the maximum fog thickness? \n" + "What is the maximum fog thickness? \n"
+ "\n" + "\n"
@@ -535,8 +517,8 @@ public class Config
+ EDhApiFogFalloff.EXPONENTIAL_SQUARED + ": 1/(e^((distance*density)^2)") + EDhApiFogFalloff.EXPONENTIAL_SQUARED + ": 1/(e^((distance*density)^2)")
.build(); .build();
public static ConfigEntry<Double> farFogDensity = new ConfigEntry.Builder<Double>() public static ConfigEntry<Float> farFogDensity = new ConfigEntry.Builder<Float>()
.setMinDefaultMax(0.01, 2.5, 50.0) .setMinDefaultMax(0.01f, 2.5f, 50.0f)
.comment("" .comment(""
+ "Used in conjunction with the Fog Falloff.") + "Used in conjunction with the Fog Falloff.")
.build(); .build();
@@ -545,14 +527,6 @@ public class Config
static
{
disableVanillaFog.addListener(
new ConfigChangeListener<Boolean>(disableVanillaFog,
(disableVanillaFog) -> enableVanillaFog.setApiValue(disableVanillaFog))
);
}
public static class HeightFog public static class HeightFog
{ {
public static ConfigUIComment heightFogHeader = new ConfigUIComment.Builder().setParentConfigClass(HeightFog.class).build(); public static ConfigUIComment heightFogHeader = new ConfigUIComment.Builder().setParentConfigClass(HeightFog.class).build();
@@ -590,13 +564,13 @@ public class Config
+ EDhApiHeightFogDirection.ABOVE_AND_BELOW_SET_HEIGHT + ": Height fog starts from a set height and goes towards both the sky and void") + EDhApiHeightFogDirection.ABOVE_AND_BELOW_SET_HEIGHT + ": Height fog starts from a set height and goes towards both the sky and void")
.build(); .build();
public static ConfigEntry<Double> heightFogBaseHeight = new ConfigEntry.Builder<Double>() public static ConfigEntry<Float> heightFogBaseHeight = new ConfigEntry.Builder<Float>()
.setMinDefaultMax(-4096.0, 80.0, 4096.0) .setMinDefaultMax(-4096.0f, 80.0f, 4096.0f)
.comment("If the height fog is calculated around a set height, what is that height position?") .comment("If the height fog is calculated around a set height, what is that height position?")
.build(); .build();
public static ConfigEntry<Double> heightFogStart = new ConfigEntry.Builder<Double>() public static ConfigEntry<Float> heightFogStart = new ConfigEntry.Builder<Float>()
.setMinDefaultMax(FOG_RANGE_MIN, 0.0, FOG_RANGE_MAX) .setMinDefaultMax(FOG_RANGE_MIN, 0.0f, FOG_RANGE_MAX)
.comment("" .comment(""
+ "Should the start of the height fog be offset? \n" + "Should the start of the height fog be offset? \n"
+ "\n" + "\n"
@@ -604,8 +578,8 @@ public class Config
+ "1.0: Fog start with offset of the entire world's height. (Includes depth)") + "1.0: Fog start with offset of the entire world's height. (Includes depth)")
.build(); .build();
public static ConfigEntry<Double> heightFogEnd = new ConfigEntry.Builder<Double>() public static ConfigEntry<Float> heightFogEnd = new ConfigEntry.Builder<Float>()
.setMinDefaultMax(FOG_RANGE_MIN, 0.6, FOG_RANGE_MAX) .setMinDefaultMax(FOG_RANGE_MIN, 0.6f, FOG_RANGE_MAX)
.comment("" .comment(""
+ "Should the end of the height fog be offset? \n" + "Should the end of the height fog be offset? \n"
+ "\n" + "\n"
@@ -613,8 +587,8 @@ public class Config
+ "1.0: Fog end with offset of the entire world's height. (Include depth)") + "1.0: Fog end with offset of the entire world's height. (Include depth)")
.build(); .build();
public static ConfigEntry<Double> heightFogMin = new ConfigEntry.Builder<Double>() public static ConfigEntry<Float> heightFogMin = new ConfigEntry.Builder<Float>()
.setMinDefaultMax(0.0, 0.0, FOG_RANGE_MAX) .setMinDefaultMax(0.0f, 0.0f, FOG_RANGE_MAX)
.comment("" .comment(""
+ "What is the minimum fog thickness? \n" + "What is the minimum fog thickness? \n"
+ "\n" + "\n"
@@ -622,8 +596,8 @@ public class Config
+ "1.0: Fully opaque fog.") + "1.0: Fully opaque fog.")
.build(); .build();
public static ConfigEntry<Double> heightFogMax = new ConfigEntry.Builder<Double>() public static ConfigEntry<Float> heightFogMax = new ConfigEntry.Builder<Float>()
.setMinDefaultMax(FOG_RANGE_MIN, 1.0, 5.0) .setMinDefaultMax(FOG_RANGE_MIN, 1.0f, 5.0f)
.comment("" .comment(""
+ "What is the maximum fog thickness? \n" + "What is the maximum fog thickness? \n"
+ "\n" + "\n"
@@ -641,8 +615,8 @@ public class Config
+ EDhApiFogFalloff.EXPONENTIAL_SQUARED + ": 1/(e^((height*density)^2)") + EDhApiFogFalloff.EXPONENTIAL_SQUARED + ": 1/(e^((height*density)^2)")
.build(); .build();
public static ConfigEntry<Double> heightFogDensity = new ConfigEntry.Builder<Double>() public static ConfigEntry<Float> heightFogDensity = new ConfigEntry.Builder<Float>()
.setMinDefaultMax(0.01, 20.0, 50.0) .setMinDefaultMax(0.01f, 20.0f, 50.0f)
.comment("What is the height fog's density?") .comment("What is the height fog's density?")
.build(); .build();
@@ -670,13 +644,13 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Double> noiseIntensity = new ConfigEntry.Builder<Double>() // TODO: Make this a float (the ClassicConfigGUI doesn't support floats) public static ConfigEntry<Float> noiseIntensity = new ConfigEntry.Builder<Float>()
.setMinDefaultMax(0d, 5d, 100d) // TODO: Once this becomes a float make it 0-1 instead of 0-100 (I did this cus doubles only allow 2 decimal places) .setMinDefaultMax(0f, 0.05f, 1f)
.comment("" .comment(""
+ "How intense should the noise should be?") + "How intense should the noise should be?")
.build(); .build();
public static ConfigEntry<Integer> noiseDropoff = new ConfigEntry.Builder<Integer>() // TODO: Make this a float (the ClassicConfigGUI doesn't support floats) public static ConfigEntry<Integer> noiseDropoff = new ConfigEntry.Builder<Integer>()
.setMinDefaultMax(0, 1024, null) .setMinDefaultMax(0, 1024, null)
.comment("" .comment(""
+ "Defines how far should the noise texture render before it fades away. (in blocks) \n" + "Defines how far should the noise texture render before it fades away. (in blocks) \n"
@@ -690,22 +664,33 @@ public class Config
{ {
public static ConfigUIComment cullingHeader = new ConfigUIComment.Builder().setParentConfigClass(Culling.class).build(); public static ConfigUIComment cullingHeader = new ConfigUIComment.Builder().setParentConfigClass(Culling.class).build();
public static ConfigEntry<Double> overdrawPrevention = new ConfigEntry.Builder<Double>() public static ConfigEntry<Float> overdrawPrevention = new ConfigEntry.Builder<Float>()
.setMinDefaultMax(0.0, 0.0, 1.0) // TODO change -1 to auto .setMinDefaultMax(-1.0f, -1.0f, 1.0f)
.comment("" .comment(""
+ "Determines how far from the camera Distant Horizons will start rendering. \n" + "Determines how far from the camera Distant Horizons will start rendering. \n"
+ "Measured as a percentage of the vanilla render distance.\n" + "Measured as a percentage of the vanilla render distance.\n"
+ "\n" + "\n"
+ "0 = auto, overdraw will change based on the vanilla render distance.\n" + "-1 = auto, overdraw will change based on the vanilla render distance.\n"
+ "\n" + "\n"
+ "Higher values will prevent LODs from rendering behind vanilla blocks at a higher distance,\n" + "Higher values will prevent LODs from rendering behind vanilla blocks at a higher distance,\n"
+ "but may cause holes in the world. \n" + "but may cause holes in the world. \n"
+ "Holes are most likely to appear when flying through unloaded terrain. \n" + "Holes are most likely to appear when flying through unloaded terrain. \n"
+ "\n" + "\n"
+ "Increasing the vanilla render distance increases the effectiveness of this setting." + "Increasing the vanilla render distance increases the effectiveness of this setting."
+ "") + "")
.build(); .build();
public static ConfigEntry<Boolean> reduceOverdrawWithFastMovement = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "If set to true the overdraw prevention radius will get closer\n"
+ "to the camera when flying/moving quickly.\n"
+ "\n"
+ "This helps reduce issues where Minecraft can't load or\n"
+ "generate chunks fast enough to keep up with DH.\n"
+ "")
.build();
public static ConfigEntry<Boolean> enableCaveCulling = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableCaveCulling = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.comment("" .comment(""
@@ -760,21 +745,24 @@ public class Config
+ "Disable this if shadows render incorrectly.") + "Disable this if shadows render incorrectly.")
.build(); .build();
public static ConfigEntry<String> ignoredRenderBlockCsv = new ConfigEntry.Builder<String>() // TODO accept wildcards public static ConfigEntry<String> ignoredRenderBlockCsv = new ConfigEntry.Builder<String>()
.set("minecraft:barrier,minecraft:structure_void,minecraft:light,minecraft:tripwire,minecraft:brown_mushroom") .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 .setAppearance(EConfigEntryAppearance.ALL)
.comment("" .comment(""
+ "A comma separated list of block resource locations that won't be rendered by DH. \n" + "A comma separated list of block resource locations that won't be rendered by DH. \n"
+ "Air is always included in this list. \n" + "Air is always included in this list. \n"
+ "Requires a restart to change. \n" + "Requires a restart to change. \n"
+ "\n"
+ "Note:\n"
+ "If you see gaps, or holes you may have to change\n"
+ "worldCompression to ["+EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS+"] and re-generate the LODs.\n"
+ "Black spots may happen occur to block lighting being zero for covered blocks.\n"
+ "") + "")
.build(); .build();
public static ConfigEntry<String> ignoredRenderCaveBlockCsv = new ConfigEntry.Builder<String>() // TODO accept wildcards public static ConfigEntry<String> ignoredRenderCaveBlockCsv = new ConfigEntry.Builder<String>()
.set("minecraft:glow_lichen,minecraft:rail,minecraft:water,minecraft:lava,minecraft:bubble_column," + .set("") // config is empty since most cave blocks will be automatically ignored due to being: transparent, non-solid, or liquids, but new blocks can be added here if needed
"minecraft:cave_vines_plant,minecraft:vine,minecraft:cave_vines,minecraft:short_grass,minecraft:tall_grass," + .setAppearance(EConfigEntryAppearance.ALL)
"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("" .comment(""
+ "A comma separated list of block resource locations that shouldn't be rendered \n" + "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" + "if they are in a 0 sky light underground area. \n"
@@ -815,7 +803,7 @@ public class Config
public static ConfigUIComment experimentalHeader = new ConfigUIComment.Builder().setParentConfigClass(Experimental.class).build(); public static ConfigUIComment experimentalHeader = new ConfigUIComment.Builder().setParentConfigClass(Experimental.class).build();
public static ConfigEntry<Integer> earthCurveRatio = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> earthCurveRatio = new ConfigEntry.Builder<Integer>()
.setMinDefaultMax(0, 0, 5000) .setMinDefaultMax(-5000, 0, 5000)
.comment("" .comment(""
+ "This is the earth size ratio when applying the curvature shader effect. \n" + "This is the earth size ratio when applying the curvature shader effect. \n"
+ "Note: Enabling this feature may cause rendering bugs. \n" + "Note: Enabling this feature may cause rendering bugs. \n"
@@ -825,11 +813,40 @@ public class Config
+ "100 = 1 to 100 (63,710 blocks) \n" + "100 = 1 to 100 (63,710 blocks) \n"
+ "10000 = 1 to 10000 (637.1 blocks) \n" + "10000 = 1 to 10000 (637.1 blocks) \n"
+ "\n" + "\n"
+ "Note: Due to current limitations, the min value is 50 \n" + "Note: Due to current limitations, the min value is ["+WorldCurvatureConfigEventHandler.MIN_VALID_CURVE_VALUE+"] \n"
+ "and the max value is 5000. Any values outside this range \n" + "and the max value is 5000. Any values outside this range \n"
+ "will be set to 0 (disabled).") + "will be set to 0 (disabled).")
.addListener(WorldCurvatureConfigEventHandler.INSTANCE) .addListener(WorldCurvatureConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<String> ignoredDimensionCsv = new ConfigEntry.Builder<String>()
.set("")
.comment(""
+ "A comma separated list of dimension resource locations where DH won't render. \n"
+ "\n"
+ "Example: \"minecraft:the_nether,minecraft:the_end\"\n"
+ "\n"
+ "Note:\n"
+ "Some DH settings will be disabled and/or changed to improve \n"
+ "visuals when DH rendering is disabled. \n"
+ "")
.addListener(IgnoredDimensionCsvHandler.INSTANCE)
.build();
public static ConfigEntry<EDhApiRenderApi> renderingApi = new ConfigEntry.Builder<EDhApiRenderApi>()
.set(EDhApiRenderApi.AUTO)
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // very experimental option and only supported
.comment(""
+ "Requires a restart to change. \n"
+ " \n"
+ "Options: \n"
+ EDhApiRenderApi.AUTO + " - changes based on the most likely API for that MC version \n"
+ EDhApiRenderApi.OPEN_GL + " - Default \n"
+ EDhApiRenderApi.BLAZE_3D + " - Only supported on MC 1.21.11 \n"
+ "")
.build();
} }
} }
@@ -925,13 +942,15 @@ public class Config
+ "") + "")
.build(); .build();
// TODO add LOD-only mode to this
public static ConfigEntry<Boolean> enableDebugKeybindings = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableDebugKeybindings = new ConfigEntry.Builder<Boolean>()
.set(false) .set(false)
.comment("" .comment(""
+ "If true the F8 key can be used to cycle through the different debug modes. \n" + "If true several keys can be used to toggle debug states. \n"
+ "and the F6 key can be used to enable and disable LOD rendering.") + "F6 - enable/disable LOD rendering \n"
.build(); + "F7 - enable/disable LOD only rendering \n"
+ "F8 - cycle through the different debug rendering modes \n"
+ "")
.build();
public static ConfigEntry<Boolean> enableWhiteWorld = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableWhiteWorld = new ConfigEntry.Builder<Boolean>()
.set(false) .set(false)
@@ -1061,11 +1080,10 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Boolean> validateBufferIdsBeforeRendering = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<EDhApiGpuUploadMethod> glUploadMode = new ConfigEntry.Builder<EDhApiGpuUploadMethod>()
.set(false) .set(EDhApiGpuUploadMethod.AUTO)
.comment("" .comment(""
+ "Massively reduces FPS. \n" + "\n"
+ "Should only be used if mysterious EXCEPTION_ACCESS_VIOLATION crashes are happening in DH's rendering code for troubleshooting. \n"
+ "") + "")
.build(); .build();
@@ -1154,6 +1172,11 @@ public class Config
.comment("Shows what levels are loaded and world gen/rendering info about those levels.") .comment("Shows what levels are loaded and world gen/rendering info about those levels.")
.build(); .build();
public static ConfigEntry<Boolean> onlyShowRenderingLevels = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment("Only show levels that DH is actively rendering.")
.build();
} }
/** This class is used to debug the different features of the config GUI */ /** This class is used to debug the different features of the config GUI */
@@ -1212,23 +1235,17 @@ public class Config
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // no GUI renderer set up currently .setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // no GUI renderer set up currently
.build(); .build();
public static ConfigUIButton uiButtonTest = new ConfigUIButton(() -> public static ConfigUIButton uiButtonTest = new ConfigUIButton(() ->
{ {
// running on a separate thread is necessary to prevent locking // running on a separate thread is necessary to prevent locking
new Thread(() -> new Thread(() -> onButtonPressed()).start();
{
if (!GraphicsEnvironment.isHeadless())
{
LOGGER.info("Attempting to show tinyfd message box...");
boolean buttonPress = TinyFileDialogs.tinyfd_messageBox("Button pressed!", "UITester dialog", "ok", "info", false);
LOGGER.info("dialog returned with ["+(buttonPress ? "TRUE" : "FALSE")+"]");
}
else
{
LOGGER.info("button pressed!");
}
}).start();
}); });
public static void onButtonPressed()
{
LOGGER.info("Attempting to show tinyfd message box...");
boolean buttonPress = NativeDialogUtil.showDialog("Button pressed!", "UITester dialog", "ok", "info");
LOGGER.info("dialog returned with ["+(buttonPress ? "TRUE" : "FALSE")+"]");
}
public static ConfigCategory categoryTest = new ConfigCategory.Builder().set(CategoryTest.class).build(); public static ConfigCategory categoryTest = new ConfigCategory.Builder().set(CategoryTest.class).build();
@@ -1344,6 +1361,37 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Integer> generationCenterChunkX = new ConfigEntry.Builder<Integer>()
.setChatCommandName("generation.bounds.centerChunk.x")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.setMinDefaultMax(Integer.MIN_VALUE, 0, Integer.MAX_VALUE)
.comment("" +
"The center X chunk position that the world gen max radius is centered around. \n" +
"")
.build();
public static ConfigEntry<Integer> generationCenterChunkZ = new ConfigEntry.Builder<Integer>()
.setChatCommandName("generation.bounds.centerChunk.z")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.setMinDefaultMax(Integer.MIN_VALUE, 0, Integer.MAX_VALUE)
.comment("" +
"The center Z chunk position that the world gen max radius is centered around. \n" +
"")
.build();
public static ConfigEntry<Integer> generationMaxChunkRadius = new ConfigEntry.Builder<Integer>()
.setChatCommandName("generation.bounds.radiusInChunks")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.setMinDefaultMax(0, 0, Integer.MAX_VALUE)
.comment("" +
"The max radius in chunks around the central point where world generation is allowed. \n" +
"If this value is set to 0, generation bounds are disabled and the render distance will be used. \n" +
"\n" +
"This should only be set if you have a pre-generated world that has a very limited size. \n" +
"Setting this on a normal MC world will prevent the world generator from filling \n" +
"out your render distance. \n" +
"")
.build();
} }
public static class LodBuilding public static class LodBuilding
@@ -1354,7 +1402,7 @@ public class Config
.set(false) .set(false)
// enabling this can be quite detrimental to performance, // enabling this can be quite detrimental to performance,
// so hiding it in the config file should reduce people accidentally enabling it // so hiding it in the config file should reduce people accidentally enabling it
.setAppearance(isRunningInDevEnvironment() ? EConfigEntryAppearance.ALL : EConfigEntryAppearance.ONLY_IN_FILE) .setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.comment("" .comment(""
+ "Enabling this will drastically increase chunk processing time\n" + "Enabling this will drastically increase chunk processing time\n"
+ "and you may need to increase your CPU load to handle it.\n" + "and you may need to increase your CPU load to handle it.\n"
@@ -1369,37 +1417,9 @@ public class Config
.build(); .build();
public static ConfigEntry<EDhApiDataCompressionMode> dataCompression = new ConfigEntry.Builder<EDhApiDataCompressionMode>() public static ConfigEntry<EDhApiDataCompressionMode> dataCompression = new ConfigEntry.Builder<EDhApiDataCompressionMode>()
.set(EDhApiDataCompressionMode.Z_STD) .set(EDhApiDataCompressionMode.Z_STD_BLOCK)
.comment("" // only visible via the API since there is no reason to use any compressor except ZStandard as of 2025-11-24
+ "What algorithm should be used to compress new LOD data? \n" .setAppearance(EConfigEntryAppearance.ONLY_IN_API)
+ "This setting will only affect new or updated LOD data, \n"
+ "any data already generated when this setting is changed will be\n"
+ "unaffected until it needs to be re-written to the database.\n"
+ "\n"
+ EDhApiDataCompressionMode.UNCOMPRESSED + " \n"
+ "Should only be used for testing, is worse in every way vs ["+EDhApiDataCompressionMode.LZ4+"].\n"
+ "Expected Compression Ratio: 1.0\n"
+ "Estimated average DTO read speed: 6.09 milliseconds\n"
+ "Estimated average DTO write speed: 6.01 milliseconds\n"
+ "\n"
+ EDhApiDataCompressionMode.LZ4 + " \n"
+ "A good option if you're CPU limited and have plenty of hard drive space.\n"
+ "Expected Compression Ratio: 0.4513\n"
+ "Estimated average DTO read speed: 3.25 ms\n"
+ "Estimated average DTO write speed: 5.99 ms\n"
+ "\n"
+ EDhApiDataCompressionMode.Z_STD + " \n"
+ "A good option if you're CPU limited and have plenty of hard drive space.\n"
+ "Expected Compression Ratio: 0.2606\n"
+ "Estimated average DTO read speed: 9.31 ms\n"
+ "Estimated average DTO write speed: 15.13 ms\n"
+ "\n"
+ EDhApiDataCompressionMode.LZMA2 + " \n"
+ "Slow but very good compression.\n"
+ "Expected Compression Ratio: 0.2\n"
+ "Estimated average DTO read speed: 13.29 ms\n"
+ "Estimated average DTO write speed: 70.95 ms\n"
+ "")
.build(); .build();
public static ConfigEntry<EDhApiWorldCompressionMode> worldCompression = new ConfigEntry.Builder<EDhApiWorldCompressionMode>() public static ConfigEntry<EDhApiWorldCompressionMode> worldCompression = new ConfigEntry.Builder<EDhApiWorldCompressionMode>()
@@ -1422,49 +1442,6 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Boolean> recalculateChunkHeightmaps = new ConfigEntry.Builder<Boolean>()
.set(false)
.comment(""
+ "True: Recalculate chunk height maps before chunks can be used by DH.\n"
+ " This can fix problems with worlds created by World Painter or \n"
+ " other external tools where the heightmap format may be incorrect. \n"
+ "False: Assume any height maps handled by Minecraft are correct. \n"
+ "\n"
+ "Fastest: False\n"
+ "Most Compatible: True\n"
+ "")
.build();
public static ConfigEntry<Boolean> pullLightingForPregeneratedChunks = new ConfigEntry.Builder<Boolean>()
.set(false)
.comment(""
+ "If true LOD generation for pre-existing chunks will attempt to pull the lighting data \n"
+ "saved in Minecraft's Region files. \n"
+ "If false DH will pull in chunks without lighting and re-light them. \n"
+ " \n"
+ "Setting this to true will result in faster LOD generation \n"
+ "for already generated worlds, but is broken by most lighting mods. \n"
+ " \n"
+ "Set this to false if LODs are black. \n"
+ "")
.build();
public static ConfigEntry<Boolean> assumePreExistingChunksAreFinished = new ConfigEntry.Builder<Boolean>()
.set(false)
.comment(""
+ "When DH pulls in pre-existing chunks it will attempt to \n"
+ "run any missing world generation steps; for example: \n"
+ "if a chunk has the status SURFACE, DH will skip BIOMES \n"
+ "and SURFACE, but will run FEATURES. \n"
+ " \n"
+ "However if for some reason the chunks are malformed \n"
+ "or there's some other issue that causes the status \n"
+ "to be incorrect that can either cause world gen \n"
+ "lock-ups and/or crashes. \n"
+ "If either of those happen try setting this to True. \n"
+ "")
.build();
public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build(); public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build();
@@ -1506,6 +1483,7 @@ public class Config
+ "How many threads should be used by Distant Horizons? \n" + "How many threads should be used by Distant Horizons? \n"
+ "") + "")
.build(); .build();
public static final ConfigEntry<Double> threadRunTimeRatio = new ConfigEntry.Builder<Double>() public static final ConfigEntry<Double> threadRunTimeRatio = new ConfigEntry.Builder<Double>()
.setChatCommandName("threading.threadRunTimeRatio") .setChatCommandName("threading.threadRunTimeRatio")
.setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getDefaultRunTimeRatio(), 1.0) .setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getDefaultRunTimeRatio(), 1.0)
@@ -1519,6 +1497,19 @@ public class Config
"") "")
.build(); .build();
public static final ConfigEntry<Integer> threadPriority = new ConfigEntry.Builder<Integer>()
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // only in file since this requires a MC reboot to change
.setMinDefaultMax(Thread.MIN_PRIORITY, // 1
Thread.NORM_PRIORITY, // 5 (1 higher than C2ME's default priority of 4 which can help reduce issues with Chunky)
Thread.MAX_PRIORITY) // 10
.comment(""
+ "What Java thread priority should DH's primary thread pools run with? \n"
+ "\n"
+ "You probably don't need to change this unless you are also \n"
+ "running C2ME and are seeing thread starvation in either C2ME or DH. \n"
+ "")
.build();
} }
@@ -1527,54 +1518,79 @@ public class Config
{ {
public static ConfigUIComment loggingHeader = new ConfigUIComment.Builder().setParentConfigClass(Logging.class).build(); 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<EDhApiLoggerLevel> globalFileMaxLevel = new ConfigEntry.Builder<EDhApiLoggerLevel>()
public static ConfigEntry<EDhApiLoggerMode> logWorldGenEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() .setChatCommandName("logging.globalFileMaxLevel")
.set(EDhApiLoggerLevel.INFO)
.comment(""
+ ""
+ "")
.build();
public static ConfigEntry<EDhApiLoggerLevel> globalChatMaxLevel = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.setChatCommandName("logging.globalChatMaxLevel")
.set(EDhApiLoggerLevel.ERROR)
.comment(""
+ ""
+ "")
.build();
public static ConfigUISpacer globalLoggingSpacer = new ConfigUISpacer.Builder().build();
public static ConfigEntry<EDhApiLoggerLevel> logWorldGenEventToFile = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.setChatCommandName("logging.logWorldGenEvent") .setChatCommandName("logging.logWorldGenEvent")
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerLevel.INFO)
.comment("" .comment(""
+ "If enabled, the mod will log information about the world generation process. \n" + "If enabled, the mod will log information about the world generation process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logWorldGenPerformance = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerLevel> logWorldGenChunkLoadEventToFile = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.setChatCommandName("logging.logWorldGenPerformance")
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE)
.comment(""
+ "If enabled, the mod will log performance about the world generation process. \n"
+ "This can be useful for debugging.")
.build();
public static ConfigEntry<EDhApiLoggerMode> logWorldGenLoadEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setChatCommandName("logging.logWorldGenLoadEvent") .setChatCommandName("logging.logWorldGenLoadEvent")
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerLevel.INFO)
.comment("" .comment(""
+ "If enabled, the mod will log information about the world generation process. \n" + "If enabled, the mod will log information about the world generation process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logRendererBufferEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerLevel> logRendererEventToFile = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerLevel.INFO)
.comment("" .comment(""
+ "If enabled, the mod will log information about the renderer buffer process. \n" + "If enabled, the mod will log information about the renderer setup, cleanup, and any issues it may encounter. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logRendererGLEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerLevel> logRendererGLEventToFile = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerLevel.INFO)
.comment("" .comment(""
+ "If enabled, the mod will log information about the renderer OpenGL process. \n" + "If enabled, the mod will log information about the renderer OpenGL process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logNetworkEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerLevel> logRendererGLEventToChat = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.set(EDhApiLoggerLevel.ERROR)
.comment(""
+ "If enabled, the mod will log information about the renderer OpenGL process. \n"
+ "This can be useful for debugging.")
.build();
public static ConfigEntry<EDhApiLoggerLevel> logNetworkEventToFile = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.setChatCommandName("logging.logNetworkEvent") .setChatCommandName("logging.logNetworkEvent")
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_WARNING_TO_FILE) .set(EDhApiLoggerLevel.INFO)
.comment("" .comment(""
+ "If enabled, the mod will log information about network operations. \n" + "If enabled, the mod will log information about network operations. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.build(); .build();
public static ConfigEntry<EDhApiLoggerLevel> logConnectionConfigChangesToFile = new ConfigEntry.Builder<EDhApiLoggerLevel>()
.setChatCommandName("logging.logConnectionConfigChanges")
.set(EDhApiLoggerLevel.WARN)
.comment(""
+ "If enabled, config changes sent by the server will be logged. \n"
+ "")
.build();
public static ConfigCategory warning = new ConfigCategory.Builder().set(Warning.class).build(); public static ConfigCategory warning = new ConfigCategory.Builder().set(Warning.class).build();
@@ -1623,6 +1639,14 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Boolean> showSlowWorldGenSettingWarnings = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "If enabled, a chat message will be displayed when DH has too many chunks \n"
+ "queued for updating. \n"
+ "")
.build();
public static ConfigEntry<Boolean> showModCompatibilityWarningsOnStartup = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> showModCompatibilityWarningsOnStartup = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.comment("" .comment(""
@@ -1631,6 +1655,24 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Boolean> logGarbageCollectorWarning = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "If enabled, a message will be logged if the garbage \n"
+ "collector Java is currently using is known \n"
+ "to cause frame stuttering and/or other issues. \n"
+ "")
.build();
public static ConfigEntry<Boolean> showGarbageCollectorWarning = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "If enabled, a chat message will be displayed if the garbage \n"
+ "collector Java is currently using is known \n"
+ "to cause frame stuttering and/or other issues. \n"
+ "")
.build();
} }
} }
@@ -1649,6 +1691,28 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Integer> serverId = new ConfigEntry.Builder<Integer>()
.set(new Random().nextInt())
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.comment(""
+ "DO NOT CHANGE UNLESS YOU KNOW WHAT YOU'RE DOING.\n"
+ "Autogenerated ID used to prevent multiple independent servers from accidentally\n"
+ "writing over each other's LODs when the same serverKey is set on both.\n"
+ "")
.build();
public static ConfigEntry<String> serverKey = new ConfigEntry.Builder<String>()
.setChatCommandName("levelKeys.serverKey")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.set("")
.comment(""
+ "Custom server key used which can be used to always reuse the same LOD data folder,\n"
+ "for cases when the server doesn't have a static IP for some reason.\n"
+ "If this value is empty, the client itself decides which folder name to use.\n"
+ "Requires rejoining the server to apply after changing.\n"
+ "")
.build();
public static ConfigEntry<String> levelKeyPrefix = new ConfigEntry.Builder<String>() public static ConfigEntry<String> levelKeyPrefix = new ConfigEntry.Builder<String>()
.setChatCommandName("levelKeys.prefix") .setChatCommandName("levelKeys.prefix")
.set("") .set("")
@@ -1661,6 +1725,15 @@ public class Config
// Generation // Generation
public static ConfigEntry<Boolean> enableServerGeneration = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "When enabled, Distant Horizons will attempt to download missing LODs from the server.\n"
+ "\n"
+ "Note: the server must have Distant Generation enabled for it to work."
+ "")
.build();
public static ConfigEntry<Integer> generationRequestRateLimit = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> generationRequestRateLimit = new ConfigEntry.Builder<Integer>()
.setChatCommandName("generation.requestRateLimit") .setChatCommandName("generation.requestRateLimit")
.setMinDefaultMax(1, 20, 100) .setMinDefaultMax(1, 20, 100)
@@ -1678,32 +1751,6 @@ public class Config
"") "")
.build(); .build();
public static ConfigEntry<Integer> generationBoundsX = new ConfigEntry.Builder<Integer>()
.setChatCommandName("generation.bounds.x")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.setMinDefaultMax(Integer.MIN_VALUE, 0, Integer.MAX_VALUE)
.comment("" +
"Defines the X-coordinate of the central point for generation boundaries, in blocks. \n" +
"")
.build();
public static ConfigEntry<Integer> generationBoundsZ = new ConfigEntry.Builder<Integer>()
.setChatCommandName("generation.bounds.z")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.setMinDefaultMax(Integer.MIN_VALUE, 0, Integer.MAX_VALUE)
.comment("" +
"Defines the Z-coordinate of the central point for generation boundaries, in blocks. \n" +
"")
.build();
public static ConfigEntry<Integer> generationBoundsRadius = new ConfigEntry.Builder<Integer>()
.setChatCommandName("generation.bounds.radius")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.setMinDefaultMax(0, 0, Integer.MAX_VALUE)
.comment("" +
"Defines the radius around the central point within which generation is allowed, in blocks. \n" +
"If this value is set to 0, generation bounds are disabled." +
"")
.build();
// Real-time updates // Real-time updates
public static ConfigEntry<Boolean> enableRealTimeUpdates = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableRealTimeUpdates = new ConfigEntry.Builder<Boolean>()
@@ -1824,7 +1871,8 @@ public class Config
ThreadPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues(); ThreadPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
RenderQualityPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues(); RenderQualityPresetConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
QuickRenderToggleConfigEventHandler.INSTANCE.setUiOnlyConfigValues(); QuickRenderToggleConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
QuickShowWorldGenProgressConfigEventHandler.INSTANCE.setUiOnlyConfigValues();
IgnoredDimensionCsvHandler.INSTANCE.onConfigValueSet();
} }
catch (Exception e) catch (Exception e)
{ {
@@ -26,7 +26,7 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.wrapperInterfaces.config.ILangWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.config.ILangWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.nio.file.Path; import java.nio.file.Path;
@@ -44,7 +44,7 @@ import java.util.*;
*/ */
public class ConfigHandler public class ConfigHandler
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class); private static final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
@@ -62,7 +62,7 @@ public class ConfigHandler
* <br> {@link String} * <br> {@link String}
* <br> * <br>
* <br> // Below, "T" should be a value from above * <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> // Note: This is not checked, so we trust that you are doing the right thing
* <br> List<T> * <br> List<T>
* <br> ArrayList<T> * <br> ArrayList<T>
* <br> Map<String, T> * <br> Map<String, T>
@@ -123,6 +123,7 @@ public class ConfigHandler
this.initNestedClass(Config.class, ""); // Init root category this.initNestedClass(Config.class, ""); // Init root category
this.configFileHandler.loadFromFile(); this.configFileHandler.loadFromFile();
this.runMinMaxValidation = !Config.Client.Advanced.Debugging.allowUnsafeValues.get();
this.isLoaded = true; this.isLoaded = true;
LOGGER.info("[" + ModInfo.NAME + "] Config initialised"); LOGGER.info("[" + ModInfo.NAME + "] Config initialised");
@@ -260,7 +261,6 @@ public class ConfigHandler
if (ConfigUIComment.class.isAssignableFrom(entry.getClass()) if (ConfigUIComment.class.isAssignableFrom(entry.getClass())
&& ((ConfigUIComment)entry).parentConfigPath != null) && ((ConfigUIComment)entry).parentConfigPath != null)
{ {
// TODO this could potentially add the same item multiple times
entryPrefix = "distanthorizons.config." + ((ConfigUIComment)entry).parentConfigPath; entryPrefix = "distanthorizons.config." + ((ConfigUIComment)entry).parentConfigPath;
} }
@@ -46,6 +46,12 @@ public class DhApiConfigValue<coreType, apiType> implements IDhApiConfigValue<ap
private final IConverter<coreType, apiType> configConverter; private final IConverter<coreType, apiType> configConverter;
//==============//
// constructors //
//==============//
//region
/** /**
* This constructor should only be called internally. <br> * This constructor should only be called internally. <br>
* There is no reason for API users to create this object. <br><br> * There is no reason for API users to create this object. <br><br>
@@ -69,11 +75,29 @@ public class DhApiConfigValue<coreType, apiType> implements IDhApiConfigValue<ap
this.configConverter = newConverter; this.configConverter = newConverter;
} }
//endregion
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()); }
//===========//
// overrides //
//===========//
//region
@Override public apiType getValue() { return this.configConverter.convertToApiType(this.configBase.get()); }
@Override public apiType getTrueValue() { return this.configConverter.convertToApiType(this.configBase.getTrueValue()); }
@Override public apiType getApiValue()
{
// if no API value is set, this should return null
if (this.configBase.getApiValue() == null)
{
return null;
}
return this.configConverter.convertToApiType(this.configBase.getApiValue());
}
@Override
public boolean setValue(apiType newValue) public boolean setValue(apiType newValue)
{ {
if (this.configBase.getAllowApiOverride()) if (this.configBase.getAllowApiOverride())
@@ -87,12 +111,12 @@ public class DhApiConfigValue<coreType, apiType> implements IDhApiConfigValue<ap
} }
} }
@Override
public boolean clearValue() public boolean clearValue()
{ {
if (this.configBase.getAllowApiOverride()) if (this.configBase.getAllowApiOverride())
{ {
// no converter should be used here since null objects may need to be handled differently // no converter should be used here since null objects may need to be handled differently
// TODO the API should just have a bool to keep track of whether the API value is in use instead of using NULL
this.configBase.setApiValue(null); this.configBase.setApiValue(null);
return true; return true;
} }
@@ -102,13 +126,15 @@ public class DhApiConfigValue<coreType, apiType> implements IDhApiConfigValue<ap
} }
} }
@Override
public boolean getCanBeOverrodeByApi() { return this.configBase.getAllowApiOverride(); } public boolean getCanBeOverrodeByApi() { return this.configBase.getAllowApiOverride(); }
public apiType getDefaultValue() { return this.configConverter.convertToApiType(this.configBase.getDefaultValue()); } @Override public apiType getDefaultValue() { return this.configConverter.convertToApiType(this.configBase.getDefaultValue()); }
public apiType getMaxValue() { return this.configConverter.convertToApiType(this.configBase.getMax()); } @Override public apiType getMaxValue() { return this.configConverter.convertToApiType(this.configBase.getMax()); }
public apiType getMinValue() { return this.configConverter.convertToApiType(this.configBase.getMin()); } @Override public apiType getMinValue() { return this.configConverter.convertToApiType(this.configBase.getMin()); }
@Override
public void addChangeListener(Consumer<apiType> onValueChangeFunc) public void addChangeListener(Consumer<apiType> onValueChangeFunc)
{ {
this.configBase.addValueChangeListener((coreValue) -> this.configBase.addValueChangeListener((coreValue) ->
@@ -118,4 +144,6 @@ public class DhApiConfigValue<coreType, apiType> implements IDhApiConfigValue<ap
}); });
} }
//endregion
} }
@@ -17,34 +17,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.seibel.distanthorizons.core.enums.worldGeneration; package com.seibel.distanthorizons.core.config.api.converters;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConverter;
/** /**
* MULTI_THREADED, <br> * Used to support deprecated config options that may be identical
* SINGLE_THREADED, <br> * in implementation but with the On/Off values flipped.
* SERVER_THREAD, <br>
* *
* @author James Seibel * @author James Seibel
* @version 7-25-2022 * @version 2025-12-22
*/ */
public enum EWorldGenThreadMode public class InvertedBoolConverter implements IConverter<Boolean, Boolean>
{ {
/**
* This world generator can be run on an unlimited number
* of concurrent threads.
*/
MULTI_THREADED,
/** @Override
* This world generator can only be run on one thread at public Boolean convertToCoreType(Boolean core)
* a time, however that thread can run concurrently { return !core; }
* to Minecraft's server thread.
*/ @Override
SINGLE_THREADED, public Boolean convertToApiType(Boolean api)
{ return !api; }
/**
* This world generator can only be run on Minecraft's
* server thread.
*/
SERVER_THREAD,
} }
@@ -0,0 +1,125 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.config.eventHandlers;
import com.seibel.distanthorizons.api.enums.config.EDhApiMcRenderingFadeMode;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiBeforeRenderEvent;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiCancelableEventParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.coreapi.util.StringUtil;
public class IgnoredDimensionCsvHandler extends DhApiBeforeRenderEvent implements IConfigListener
{
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public static IgnoredDimensionCsvHandler INSTANCE = new IgnoredDimensionCsvHandler();
private String[] dimensionNames = null;
//=============//
// constructor //
//=============//
/** private since we only ever need one handler at a time */
private IgnoredDimensionCsvHandler() { }
//=================//
// config handling //
//=================//
@Override
public void onConfigValueSet()
{
String ignoredDimensionCsvString = Config.Client.Advanced.Graphics.Experimental.ignoredDimensionCsv.get();
if (ignoredDimensionCsvString == null
|| ignoredDimensionCsvString.isEmpty())
{
LOGGER.info("Dimension ignoring disabled, DH will render all dimensions.");
this.dimensionNames = null;
}
else
{
try
{
this.dimensionNames = ignoredDimensionCsvString.split(",");
LOGGER.info("DH set to ignore dimensions: ["+ StringUtil.join(", ", this.dimensionNames)+"].");
}
catch (Exception e)
{
LOGGER.error("Failed to separate ignored dimensions from CSV string, error: ["+e.getMessage()+"].", e);
this.dimensionNames = null;
}
}
}
//===================//
// external handling //
//===================//
@Override
public void beforeRender(DhApiCancelableEventParam<DhApiRenderParam> event)
{
String dimName = event.value.clientLevelWrapper.getDimensionName();
if (IgnoredDimensionCsvHandler.INSTANCE.dimensionNameShouldBeIgnored(dimName))
{
event.cancelEvent();
Config.Client.Advanced.Graphics.Fog.enableVanillaFog.setApiValue(true);
Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.setApiValue(EDhApiMcRenderingFadeMode.NONE);
}
else
{
Config.Client.Advanced.Graphics.Fog.enableVanillaFog.setApiValue(null);
Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.setApiValue(null);
}
}
public boolean dimensionNameShouldBeIgnored(String dimName)
{
if (this.dimensionNames == null
|| this.dimensionNames.length == 0)
{
return false;
}
for (int i = 0; i < this.dimensionNames.length; i++)
{
String dimNameToIgnore = this.dimensionNames[i];
if (dimName.equalsIgnoreCase(dimNameToIgnore))
{
return true;
}
}
return false;
}
}
@@ -35,7 +35,7 @@ public class WorldCurvatureConfigEventHandler implements IConfigListener
{ {
public static WorldCurvatureConfigEventHandler INSTANCE = new WorldCurvatureConfigEventHandler(); public static WorldCurvatureConfigEventHandler INSTANCE = new WorldCurvatureConfigEventHandler();
private static final int MIN_VALID_CURVE_VALUE = 50; public static final int MIN_VALID_CURVE_VALUE = 50;
/** private since we only ever need one handler at a time */ /** private since we only ever need one handler at a time */
@@ -52,6 +52,11 @@ public class WorldCurvatureConfigEventHandler implements IConfigListener
// shouldn't update the UI, otherwise we may end up fighting the user // shouldn't update the UI, otherwise we may end up fighting the user
Config.Client.Advanced.Graphics.Experimental.earthCurveRatio.set(MIN_VALID_CURVE_VALUE); Config.Client.Advanced.Graphics.Experimental.earthCurveRatio.set(MIN_VALID_CURVE_VALUE);
} }
else if (curveRatio < 0 && curveRatio > -MIN_VALID_CURVE_VALUE)
{
// same as above, but in the negative direction
Config.Client.Advanced.Graphics.Experimental.earthCurveRatio.set(-MIN_VALID_CURVE_VALUE);
}
} }
@@ -24,18 +24,19 @@ import com.seibel.distanthorizons.core.config.ConfigPresetOptions;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener; import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
import com.seibel.distanthorizons.core.config.types.AbstractConfigBase; import com.seibel.distanthorizons.core.config.types.AbstractConfigBase;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.util.TimerUtil; import com.seibel.distanthorizons.core.util.TimerUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.config.IConfigGui; import com.seibel.distanthorizons.core.wrapperInterfaces.config.IConfigGui;
import com.seibel.distanthorizons.coreapi.util.StringUtil; import com.seibel.distanthorizons.coreapi.util.StringUtil;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.*;
public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<?>> implements IConfigListener public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<?>> implements IConfigListener
{ {
private static final Logger LOGGER = LogManager.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final long MS_DELAY_BEFORE_APPLYING_PRESET = 3_000; private static final long MS_DELAY_BEFORE_APPLYING_PRESET = 3_000;
@Nullable @Nullable
@@ -139,7 +140,7 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
LOGGER.info("changing preset to: " + newPresetEnum); LOGGER.debug("changing preset to: [" + newPresetEnum + "].");
this.changingPreset = true; this.changingPreset = true;
// update the controlled config values // update the controlled config values
@@ -151,7 +152,7 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
this.setUiOnlyConfigValues(); this.setUiOnlyConfigValues();
this.changingPreset = false; this.changingPreset = false;
LOGGER.info("preset active: " + newPresetEnum); LOGGER.debug("preset active: [" + newPresetEnum + "].");
} }
/** /**
@@ -1,66 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.config.eventHandlers.presets;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorProgressDisplayLocation;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
public class QuickShowWorldGenProgressConfigEventHandler
{
public static QuickShowWorldGenProgressConfigEventHandler INSTANCE = new QuickShowWorldGenProgressConfigEventHandler();
private final ConfigChangeListener<Boolean> quickChangeListener;
private final ConfigChangeListener<EDhApiDistantGeneratorProgressDisplayLocation> fullChangeListener;
/** private since we only ever need one handler at a time */
private QuickShowWorldGenProgressConfigEventHandler()
{
this.quickChangeListener = new ConfigChangeListener<>(Config.Client.quickShowWorldGenProgress,
(val) ->
{
boolean quickShowProgress = Config.Client.quickShowWorldGenProgress.get();
Config.Common.WorldGenerator.showGenerationProgress.set(
quickShowProgress
? EDhApiDistantGeneratorProgressDisplayLocation.OVERLAY
: EDhApiDistantGeneratorProgressDisplayLocation.DISABLED);
});
this.fullChangeListener = new ConfigChangeListener<>(Config.Common.WorldGenerator.showGenerationProgress,
(val) ->
{
boolean showProgress = Config.Common.WorldGenerator.showGenerationProgress.get() != EDhApiDistantGeneratorProgressDisplayLocation.DISABLED;
Config.Client.quickShowWorldGenProgress.setWithoutFiringEvents(showProgress);
});
}
/**
* Set the UI only config based on what is set in the file. <br>
* This should only be called once.
*/
public void setUiOnlyConfigValues()
{
boolean showProgress = Config.Common.WorldGenerator.showGenerationProgress.get() != EDhApiDistantGeneratorProgressDisplayLocation.DISABLED;
Config.Client.quickShowWorldGenProgress.set(showProgress);
}
}
@@ -29,8 +29,8 @@ import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.ConfigPresetOptions; import com.seibel.distanthorizons.core.config.ConfigPresetOptions;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.config.types.AbstractConfigBase; import com.seibel.distanthorizons.core.config.types.AbstractConfigBase;
import org.apache.logging.log4j.LogManager; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import java.util.*; import java.util.*;
@@ -39,7 +39,7 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
{ {
public static final RenderQualityPresetConfigEventHandler INSTANCE = new RenderQualityPresetConfigEventHandler(); public static final RenderQualityPresetConfigEventHandler INSTANCE = new RenderQualityPresetConfigEventHandler();
private static final Logger LOGGER = LogManager.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private final ConfigPresetOptions<EDhApiQualityPreset, EDhApiMaxHorizontalResolution> drawResolution = new ConfigPresetOptions<>(Config.Client.Advanced.Graphics.Quality.maxHorizontalResolution, private final ConfigPresetOptions<EDhApiQualityPreset, EDhApiMaxHorizontalResolution> drawResolution = new ConfigPresetOptions<>(Config.Client.Advanced.Graphics.Quality.maxHorizontalResolution,
@@ -96,6 +96,15 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.put(EDhApiQualityPreset.HIGH, EDhApiMcRenderingFadeMode.DOUBLE_PASS); this.put(EDhApiQualityPreset.HIGH, EDhApiMcRenderingFadeMode.DOUBLE_PASS);
this.put(EDhApiQualityPreset.EXTREME, EDhApiMcRenderingFadeMode.DOUBLE_PASS); this.put(EDhApiQualityPreset.EXTREME, EDhApiMcRenderingFadeMode.DOUBLE_PASS);
}}); }});
private final ConfigPresetOptions<EDhApiQualityPreset, Boolean> dhFadeFarClipPlane = new ConfigPresetOptions<>(Config.Client.Advanced.Graphics.Quality.dhFadeFarClipPlane,
new HashMap<EDhApiQualityPreset, Boolean>()
{{
this.put(EDhApiQualityPreset.MINIMUM, false);
this.put(EDhApiQualityPreset.LOW, false);
this.put(EDhApiQualityPreset.MEDIUM, true);
this.put(EDhApiQualityPreset.HIGH, true);
this.put(EDhApiQualityPreset.EXTREME, true);
}});
private final ConfigPresetOptions<EDhApiQualityPreset, Boolean> dhDither = new ConfigPresetOptions<>(Config.Client.Advanced.Graphics.Quality.ditherDhFade, private final ConfigPresetOptions<EDhApiQualityPreset, Boolean> dhDither = new ConfigPresetOptions<>(Config.Client.Advanced.Graphics.Quality.ditherDhFade,
new HashMap<EDhApiQualityPreset, Boolean>() new HashMap<EDhApiQualityPreset, Boolean>()
{{ {{
@@ -139,6 +148,7 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.configList.add(this.horizontalQuality); this.configList.add(this.horizontalQuality);
this.configList.add(this.transparency); this.configList.add(this.transparency);
this.configList.add(this.ssaoEnabled); this.configList.add(this.ssaoEnabled);
this.configList.add(this.dhFadeFarClipPlane);
this.configList.add(this.vanillaFade); this.configList.add(this.vanillaFade);
this.configList.add(this.dhDither); this.configList.add(this.dhDither);
this.configList.add(this.caveCulling); this.configList.add(this.caveCulling);
@@ -24,9 +24,10 @@ import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.ConfigPresetOptions; import com.seibel.distanthorizons.core.config.ConfigPresetOptions;
import com.seibel.distanthorizons.core.config.types.AbstractConfigBase; import com.seibel.distanthorizons.core.config.types.AbstractConfigBase;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.coreapi.util.MathUtil; import com.seibel.distanthorizons.coreapi.util.MathUtil;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
@@ -37,7 +38,7 @@ public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHan
{ {
public static final ThreadPresetConfigEventHandler INSTANCE = new ThreadPresetConfigEventHandler(); public static final ThreadPresetConfigEventHandler INSTANCE = new ThreadPresetConfigEventHandler();
private static final Logger LOGGER = LogManager.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public static int getDefaultThreadCount() { return getThreadCountByPercent(0.5); } public static int getDefaultThreadCount() { return getThreadCountByPercent(0.5); }
@@ -24,12 +24,14 @@ import com.seibel.distanthorizons.core.config.ConfigHandler;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.config.types.AbstractConfigBase; import com.seibel.distanthorizons.core.config.types.AbstractConfigBase;
import com.seibel.distanthorizons.core.config.types.ConfigEntry; import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.jar.EPlatform;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -43,13 +45,11 @@ import java.util.concurrent.locks.ReentrantLock;
*/ */
public class ConfigFileHandler public class ConfigFileHandler
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public final Path configPath; public final Path configPath;
private final Logger logger;
/** This is the object for night-config */ /** This is the object for night-config */
private final CommentedFileConfig nightConfig; private final CommentedFileConfig nightConfig;
@@ -64,7 +64,6 @@ public class ConfigFileHandler
public ConfigFileHandler(Path configPath) public ConfigFileHandler(Path configPath)
{ {
this.logger = LogManager.getLogger(this.getClass().getSimpleName() + ", " + ModInfo.ID);
this.configPath = configPath; this.configPath = configPath;
this.nightConfig = CommentedFileConfig this.nightConfig = CommentedFileConfig
@@ -157,18 +156,18 @@ public class ConfigFileHandler
} }
else if (currentCfgVersion > ModInfo.CONFIG_FILE_VERSION) else if (currentCfgVersion > ModInfo.CONFIG_FILE_VERSION)
{ {
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"); LOGGER.warn("Found config version [" + currentCfgVersion + "] which is newer than current mods config version of [" + ModInfo.CONFIG_FILE_VERSION + "]. You may have downgraded the mod and items may have been moved, you have been warned");
} }
else // if (currentCfgVersion < configBase.configVersion) else // if (currentCfgVersion < configBase.configVersion)
{ {
this.logger.warn(ModInfo.NAME + " config is of an older version, currently there is no config updater... so resetting config"); LOGGER.warn(ModInfo.NAME + " config is of an older version, currently there is no config updater... so resetting config");
try try
{ {
Files.delete(this.configPath); Files.delete(this.configPath);
} }
catch (Exception e) catch (Exception e)
{ {
this.logger.error(e); LOGGER.error("Unable to delete outdated config file at: ["+this.configPath+"], error: ["+e.getMessage()+"].", e);
} }
} }
@@ -242,8 +241,8 @@ public class ConfigFileHandler
} }
else if (entry.getTrueValue() == null) else if (entry.getTrueValue() == null)
{ {
// TODO when can this happen? // shouldn't happen, but just in case
throw new IllegalArgumentException("Entry [" + entry.getNameAndCategory() + "] is null, this may be a problem with [" + ModInfo.NAME + "]. Please contact the authors."); throw new IllegalArgumentException("ConfigEntry [" + entry.getNameAndCategory() + "] is null, how did this happen?");
} }
workConfig.set(entry.getNameAndCategory(), ConfigTypeConverters.attemptToConvertToString(entry.getType(), entry.getTrueValue())); workConfig.set(entry.getNameAndCategory(), ConfigTypeConverters.attemptToConvertToString(entry.getType(), entry.getTrueValue()));
@@ -281,7 +280,7 @@ public class ConfigFileHandler
Object convertedValue = ConfigTypeConverters.attemptToConvertFromString(expectedValueClass, value); Object convertedValue = ConfigTypeConverters.attemptToConvertFromString(expectedValueClass, value);
if (!convertedValue.getClass().equals(expectedValueClass)) if (!convertedValue.getClass().equals(expectedValueClass))
{ {
this.logger.error("Unable to convert config value ["+value+"] from ["+(value != null ? value.getClass() : "NULL")+"] to ["+expectedValueClass+"] for config ["+entry.name+"], " + LOGGER.error("Unable to convert config value ["+value+"] from ["+(value != null ? value.getClass() : "NULL")+"] to ["+expectedValueClass+"] for config ["+entry.name+"], " +
"the default config value will be used instead ["+entry.getDefaultValue()+"]. " + "the default config value will be used instead ["+entry.getDefaultValue()+"]. " +
"Make sure a converter is defined in ["+ConfigTypeConverters.class.getSimpleName()+"]."); "Make sure a converter is defined in ["+ConfigTypeConverters.class.getSimpleName()+"].");
convertedValue = entry.getDefaultValue(); convertedValue = entry.getDefaultValue();
@@ -290,13 +289,13 @@ public class ConfigFileHandler
if (entry.getTrueValue() == null) if (entry.getTrueValue() == null)
{ {
this.logger.warn("Entry [" + entry.getNameAndCategory() + "] returned as null from the config. Using default value."); LOGGER.warn("BlockBiomeWrapperPair [" + entry.getNameAndCategory() + "] returned as null from the config. Using default value.");
entry.setWithoutFiringEvents(entry.getDefaultValue()); entry.setWithoutFiringEvents(entry.getDefaultValue());
} }
} }
catch (Exception e) catch (Exception e)
{ {
this.logger.warn("Entry [" + entry.getNameAndCategory() + "] had an invalid value when loading the config. Using default value."); LOGGER.warn("BlockBiomeWrapperPair [" + entry.getNameAndCategory() + "] had an invalid value when loading the config. Using default value.");
entry.setWithoutFiringEvents(entry.getDefaultValue()); entry.setWithoutFiringEvents(entry.getDefaultValue());
} }
} }
@@ -350,18 +349,21 @@ public class ConfigFileHandler
} }
catch (Exception e) catch (Exception e)
{ {
this.logger.warn("Loading file failed because of this expectation:\n" + e); LOGGER.warn("Loading file failed because of this expectation:\n" + e);
reCreateFile(this.configPath); reCreateFile(this.configPath);
nightConfig.load(); nightConfig.load();
} }
} }
catch (Exception ex) catch (Exception e)
{ {
System.out.println("Creating file failed"); LOGGER.error("File creation failed at ["+this.configPath+"], error: ["+e.getMessage()+"].", e);
this.logger.error(ex);
SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class).crashMinecraft("Loading file and resetting config file failed at path [" + this.configPath + "]. Please check the file is ok and you have the permissions", ex); // delayed MC getter since this object may be created before
// the singleton has been bound
IMinecraftClientWrapper mc = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
mc.crashMinecraft("Loading file and resetting config file failed at path [" + this.configPath + "]. Please check the file is ok and you have the permissions", e);
} }
} }
@@ -23,7 +23,7 @@ import com.electronwill.nightconfig.core.Config;
import com.electronwill.nightconfig.core.io.ParsingMode; import com.electronwill.nightconfig.core.io.ParsingMode;
import com.electronwill.nightconfig.json.JsonFormat; import com.electronwill.nightconfig.json.JsonFormat;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -36,7 +36,7 @@ import java.util.Map;
*/ */
public class ConfigTypeConverters public class ConfigTypeConverters
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
// Once you've made a converter add it to here where the first value is the type you want to convert and the 2nd value is the converter // Once you've made a converter add it to here where the first value is the type you want to convert and the 2nd value is the converter
public static final Map<Class<?>, ConverterBase> convertObjects = new HashMap<Class<?>, ConverterBase>() public static final Map<Class<?>, ConverterBase> convertObjects = new HashMap<Class<?>, ConverterBase>()
@@ -1,196 +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.core.jar.EPlatform;
import org.jetbrains.annotations.NotNull;
import org.lwjgl.system.jawt.JAWT;
import org.lwjgl.system.macosx.*;
import java.awt.*;
import java.lang.reflect.*;
import java.util.regex.*;
import static org.lwjgl.glfw.GLFWNativeCocoa.*;
import static org.lwjgl.glfw.GLFWNativeWin32.*;
import static org.lwjgl.glfw.GLFWNativeX11.*;
import static org.lwjgl.system.JNI.*;
import static org.lwjgl.system.jawt.JAWTFunctions.*;
import static org.lwjgl.system.macosx.ObjCRuntime.*;
// Some of the code is from https://github.com/LWJGL/lwjgl3/blob/master/modules/samples/src/test/java/org/lwjgl/demo/system/jawt/EmbeddedFrameUtil.java
// which is licensed under https://www.lwjgl.org/license
/**
* Some utils for embedding awt and swing items into lwjgl windows
*
* @author Ran
* @author coolGi
*/
public final class EmbeddedFrameUtil
{
private static final int JAVA_VERSION;
private static final JAWT awt;
static
{
Pattern p = Pattern.compile("^(?:1[.])?([1-9][0-9]*)[.-]");
Matcher m = p.matcher(System.getProperty("java.version"));
if (!m.find())
{
throw new IllegalStateException("Failed to parse java.version");
}
JAVA_VERSION = Integer.parseInt(m.group(1));
awt = JAWT.calloc();
awt.version(JAVA_VERSION < 9 ? JAWT_VERSION_1_4 : JAWT_VERSION_9);
if (!JAWT_GetAWT(awt))
{
throw new RuntimeException("GetAWT failed");
}
}
private static String getEmbeddedFrameImpl()
{
switch (EPlatform.get())
{
case LINUX:
return "sun.awt.X11.XEmbeddedFrame";
case WINDOWS:
return "sun.awt.windows.WEmbeddedFrame";
case MACOS:
return "sun.lwawt.macosx.CViewEmbeddedFrame";
default:
throw new IllegalStateException();
}
}
private static long getEmbeddedFrameHandle(long window)
{
switch (EPlatform.get())
{
case LINUX:
return glfwGetX11Window(window);
case WINDOWS:
return glfwGetWin32Window(window);
case MACOS:
long objc_msgSend = ObjCRuntime.getLibrary().getFunctionAddress("objc_msgSend");
return invokePPP(glfwGetCocoaWindow(window), sel_getUid("contentView"), objc_msgSend);
default:
throw new IllegalStateException();
}
}
public static Frame embeddedFrameCreate(long window)
{
if (JAVA_VERSION < 9)
{
try
{
@SuppressWarnings("unchecked")
Class<? extends Frame> EmdeddedFrame = (Class<? extends Frame>) Class.forName(getEmbeddedFrameImpl());
Constructor<? extends Frame> c = EmdeddedFrame.getConstructor(long.class);
return c.newInstance(getEmbeddedFrameHandle(window));
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
else
{
return nJAWT_CreateEmbeddedFrame(getEmbeddedFrameHandle(window), awt.CreateEmbeddedFrame());
}
}
static void embeddedFrameSynthesizeWindowActivation(Frame embeddedFrame, boolean doActivate)
{
if (JAVA_VERSION < 9)
{
try
{
embeddedFrame
.getClass()
.getMethod("synthesizeWindowActivation", boolean.class)
.invoke(embeddedFrame, doActivate);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
else
{
JAWT_SynthesizeWindowActivation(embeddedFrame, doActivate, awt.SynthesizeWindowActivation());
}
}
public static void embeddedFrameSetBounds(Frame embeddedFrame, int x, int y, int width, int height)
{
if (JAVA_VERSION < 9)
{
try
{
Method setLocationPrivate = embeddedFrame
.getClass()
.getSuperclass()
.getDeclaredMethod("setBoundsPrivate", int.class, int.class, int.class, int.class);
setLocationPrivate.setAccessible(true);
setLocationPrivate.invoke(embeddedFrame, x, y, width, height);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
else
{
JAWT_SetBounds(embeddedFrame, x, y, width, height, awt.SetBounds());
}
}
public static void hideFrame(@NotNull Frame embeddedFrame)
{
embeddedFrame.setVisible(false);
embeddedFrameSynthesizeWindowActivation(embeddedFrame, false);
}
public static void showFrame(@NotNull Frame embeddedFrame)
{
embeddedFrameSynthesizeWindowActivation(embeddedFrame, true);
embeddedFrame.setVisible(true);
}
public static void placeAtCenter(Frame embeddedFrame, int windowWidth, int windowHeight, int frameWidth, int frameHeight, float scale)
{
float scaleFactor = (100.0F - scale) / 100.0F;
float newWidth = frameWidth * scaleFactor;
float newHeight = frameHeight * scaleFactor;
float newX = (windowWidth - newWidth) / 2F;
float newY = (windowHeight - newHeight) / 2F;
embeddedFrameSetBounds(embeddedFrame, Math.round(newX), Math.round(newY), Math.round(newWidth), Math.round(newHeight));
}
}
@@ -1,164 +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 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;
/**
*
*/
public class JavaScreenHandlerScreen extends AbstractScreen
{
public static Frame frame;
public static boolean firstRun = true;
public final Component jComponent;
static
{
// Note: this code can cause Mac
// to lock up and refuse the load (there's a bug with Java.awt texture loading)
// Needs to be called before any Swing code is called, otherwise
// Swing will get stuck thinking it's headless
System.setProperty("java.awt.headless", "false");
}
public JavaScreenHandlerScreen(@NotNull Component component)
{
this.jComponent = component;
}
@Override
public void init()
{
if (firstRun)
{
frame = EmbeddedFrameUtil.embeddedFrameCreate(this.minecraftWindow); // Don't call this multiple times
}
frame.add(this.jComponent);
frame.setBackground(new Color(0, 125, 155));
JavaScreenHandlerScreen thiss = this;
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
public void keyTyped(KeyEvent keyEvent) { }
@Override
public void keyReleased(KeyEvent keyEvent) { }
});
if (firstRun)
{
EmbeddedFrameUtil.embeddedFrameSetBounds(frame, 0, 0, this.width, this.height);
firstRun = false;
}
EmbeddedFrameUtil.showFrame(frame);
}
/** A testing/debug screen */
public static class ExampleScreen extends JComponent
{
public ExampleScreen()
{
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");
}
}
}
@Override
public void render(float delta)
{
// TODO: Make screen only update on this being called
}
@Override
public void onResize()
{
EmbeddedFrameUtil.embeddedFrameSetBounds(frame, 0, 0, this.width, this.height);
}
@Override
public void onClose()
{
frame.remove(this.jComponent);
EmbeddedFrameUtil.hideFrame(frame);
}
}
@@ -52,11 +52,17 @@ public abstract class AbstractConfigBase<T>
protected AbstractConfigBase(EConfigEntryAppearance appearance, T defaultValue) protected AbstractConfigBase(EConfigEntryAppearance appearance, T defaultValue)
{ {
this.defaultValue = defaultValue; this.defaultValue = defaultValue;
if (this.defaultValue == null)
{
throw new IllegalArgumentException("defaultValue cannot be null");
}
this.value = defaultValue; this.value = defaultValue;
this.appearance = appearance; this.appearance = appearance;
Class<?> defaultValueClass = defaultValue.getClass(); Class<?> defaultValueClass = defaultValue.getClass();
this.isFloatingPointNumber = (defaultValueClass == Double.class || defaultValueClass == Float.class); this.isFloatingPointNumber = (defaultValueClass == Double.class || defaultValueClass == Float.class);
} }
@@ -60,6 +60,7 @@ public class ConfigEntry<T> extends AbstractConfigBase<T>
//=============// //=============//
// constructor // // constructor //
//=============// //=============//
//region
private ConfigEntry( private ConfigEntry(
EConfigEntryAppearance appearance, EConfigEntryAppearance appearance,
@@ -78,11 +79,14 @@ public class ConfigEntry<T> extends AbstractConfigBase<T>
this.listenerList = listenerList; this.listenerList = listenerList;
} }
//endregion
//==========================// //==========================//
// property getters/setters // // property getters/setters //
//==========================// //==========================//
//region
/** the string used when entering the config into the command line or chat */ /** the string used when entering the config into the command line or chat */
public String getChatCommandName() { return this.chatCommandName; } public String getChatCommandName() { return this.chatCommandName; }
@@ -100,16 +104,22 @@ public class ConfigEntry<T> extends AbstractConfigBase<T>
public T getMax() { return this.max; } public T getMax() { return this.max; }
public void setMax(T newMax) { this.max = newMax; } public void setMax(T newMax) { this.max = newMax; }
//endregion
//===============// //===============//
// value setters // // value setters //
//===============// //===============//
//region
public void setApiValue(T newApiValue) public void setApiValue(T newApiValue)
{ {
this.apiValue = newApiValue; this.apiValue = newApiValue;
this.listenerList.forEach(IConfigListener::onConfigValueSet); synchronized (this.listenerList)
{
this.listenerList.forEach(IConfigListener::onConfigValueSet);
}
} }
public boolean apiIsOverriding() public boolean apiIsOverriding()
@@ -129,7 +139,11 @@ public class ConfigEntry<T> extends AbstractConfigBase<T>
public void setWithoutSaving(T newValue) public void setWithoutSaving(T newValue)
{ {
super.set(newValue); super.set(newValue);
this.listenerList.forEach(IConfigListener::onConfigValueSet);
synchronized (this.listenerList)
{
this.listenerList.forEach(IConfigListener::onConfigValueSet);
}
} }
@Override @Override
public void set(T newValue) public void set(T newValue)
@@ -141,19 +155,30 @@ public class ConfigEntry<T> extends AbstractConfigBase<T>
public void uiSetWithoutSaving(T newValue) public void uiSetWithoutSaving(T newValue)
{ {
this.setWithoutSaving(newValue); this.setWithoutSaving(newValue);
this.listenerList.forEach(IConfigListener::onUiModify);
synchronized (this.listenerList)
{
this.listenerList.forEach(IConfigListener::onUiModify);
}
} }
public void uiSet(T newValue) public void uiSet(T newValue)
{ {
this.set(newValue); this.set(newValue);
this.listenerList.forEach(IConfigListener::onUiModify);
synchronized (this.listenerList)
{
this.listenerList.forEach(IConfigListener::onUiModify);
}
} }
//endregion
//===============// //===============//
// value getters // // value getters //
//===============// //===============//
//region
@Override @Override
public T get() public T get()
@@ -174,11 +199,14 @@ public class ConfigEntry<T> extends AbstractConfigBase<T>
@Nullable @Nullable
public T getApiValue() { return this.apiValue; } public T getApiValue() { return this.apiValue; }
//endregion
//===========// //===========//
// listeners // // listeners //
//===========// //===========//
//region
/** Fired whenever the config value changes to a new value. */ /** Fired whenever the config value changes to a new value. */
public void addValueChangeListener(Consumer<T> onValueChangeFunc) public void addValueChangeListener(Consumer<T> onValueChangeFunc)
@@ -187,26 +215,38 @@ public class ConfigEntry<T> extends AbstractConfigBase<T>
this.addListener(changeListener); this.addListener(changeListener);
} }
/** Fired whenever the config value is updated, including when the value doesn't change (IE when the UI changes state or the config is reloaded). */ /** Fired whenever the config value is updated, including when the value doesn't change (IE when the UI changes state or the config is reloaded). */
public void addListener(IConfigListener newListener) { this.listenerList.add(newListener); } public void addListener(IConfigListener newListener)
//public void removeValueChangeListener(Consumer<T> onValueChangeFunc) { } // not currently implemented
public void removeListener(IConfigListener oldListener) { this.listenerList.remove(oldListener); }
public void clearListeners() { this.listenerList.clear(); }
public ArrayList<IConfigListener> getListeners() { return this.listenerList; }
/** Replaces the listener list */
public void setListeners(ArrayList<IConfigListener> newListeners)
{ {
this.listenerList.clear(); synchronized (this.listenerList)
this.listenerList.addAll(newListeners); {
this.listenerList.add(newListener);
}
} }
public void setListeners(IConfigListener... newListeners) { this.listenerList.addAll(Arrays.asList(newListeners)); }
public void removeListener(IConfigListener oldListener)
{
synchronized (this.listenerList)
{
this.listenerList.remove(oldListener);
}
}
public void clearListeners()
{
synchronized (this.listenerList)
{
this.listenerList.clear();
}
}
//endregion
//====================// //====================//
// min/max validation // // min/max validation //
//====================// //====================//
//region
/** Checks if this config's current value is valid */ /** Checks if this config's current value is valid */
public EConfigValidity getValidity() { return this.getValidity(this.value, this.min, this.max); } public EConfigValidity getValidity() { return this.getValidity(this.value, this.min, this.max); }
@@ -257,22 +297,31 @@ public class ConfigEntry<T> extends AbstractConfigBase<T>
} }
} }
//endregion
//===============// //===============//
// file handling // // file handling //
//===============// //===============//
//region
/** This should normally not be called since set() automatically calls this */ /** This should normally not be called since set() automatically calls this */
public void save() { ConfigHandler.INSTANCE.configFileHandler.saveEntry(this); } public void save() { ConfigHandler.INSTANCE.configFileHandler.saveEntry(this); }
/** This should normally not be called except for special circumstances */ /** This should normally not be called except for special circumstances */
public void load() { ConfigHandler.INSTANCE.configFileHandler.loadEntry(this); } public void load() { ConfigHandler.INSTANCE.configFileHandler.loadEntry(this); }
//endregion
//================// //================//
// base overrides // // base overrides //
//================// //================//
//region
@Override
public String toString() { return this.name + ": [" + this.get() + "]"; }
public boolean equals(AbstractConfigBase<?> obj) public boolean equals(AbstractConfigBase<?> obj)
{ {
@@ -294,11 +343,14 @@ public class ConfigEntry<T> extends AbstractConfigBase<T>
} }
} }
//endregion
//=========// //=========//
// builder // // builder //
//=========// //=========//
//region
public static class Builder<T> extends AbstractConfigBase.Builder<T, Builder<T>> public static class Builder<T> extends AbstractConfigBase.Builder<T, Builder<T>>
{ {
@@ -397,4 +449,8 @@ public class ConfigEntry<T> extends AbstractConfigBase<T>
} }
//endregion
} }
@@ -23,7 +23,7 @@ import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.types.enums.EConfigCommentTextPosition; import com.seibel.distanthorizons.core.config.types.enums.EConfigCommentTextPosition;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance; import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -34,7 +34,7 @@ import org.jetbrains.annotations.Nullable;
*/ */
public class ConfigUIComment extends AbstractConfigBase<String> public class ConfigUIComment extends AbstractConfigBase<String>
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public String parentConfigPath = null; public String parentConfigPath = null;
@@ -0,0 +1,138 @@
package com.seibel.distanthorizons.core.dataObjects;
import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import java.util.concurrent.ConcurrentHashMap;
/**
* A pooled compound key between the biome and blockState. <br>
* These objects are pooled since we will need this compound key
* many times.
*
* @see FullDataPointIdMap
* @see IBlockStateWrapper
* @see IBiomeWrapper
*/
public class BlockBiomeWrapperPair
{
private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
/** two levels are present so we don't need to use a key object */
private static final ConcurrentHashMap<IBlockStateWrapper, ConcurrentHashMap<IBiomeWrapper, BlockBiomeWrapperPair>> CACHED_PAIR_BY_BIOME_BY_BLOCK = new ConcurrentHashMap<>();
public final IBiomeWrapper biome;
public final IBlockStateWrapper blockState;
private int hashCode = 0;
private boolean hashGenerated = false;
private String serialString = null;
//=============//
// constructor //
//=============//
public static BlockBiomeWrapperPair get(IBlockStateWrapper blockState, IBiomeWrapper biome)
{
// check for existing entry
ConcurrentHashMap<IBiomeWrapper, BlockBiomeWrapperPair> pairByBiomeWrapper = CACHED_PAIR_BY_BIOME_BY_BLOCK.get(blockState);
if (pairByBiomeWrapper != null)
{
BlockBiomeWrapperPair pair = pairByBiomeWrapper.get(biome);
if (pair != null)
{
return pair;
}
}
// Lazily create the inner map and new BlockBiomeWrapperPair
return CACHED_PAIR_BY_BIOME_BY_BLOCK
.computeIfAbsent(blockState, newBlockState -> new ConcurrentHashMap<>())
.computeIfAbsent(biome, newBiome -> new BlockBiomeWrapperPair(biome, blockState));
}
private BlockBiomeWrapperPair(IBiomeWrapper biome, IBlockStateWrapper blockState)
{
this.biome = biome;
this.blockState = blockState;
}
//===========//
// overrides //
//===========//
/**
* Reminder: this hash code won't always be unique, collisions can occur;
* because of that this hash shouldn't be the only unique identifier for this object.
*/
@Override
public int hashCode()
{
// cache the hash code to improve speed
if (!this.hashGenerated)
{
this.hashCode = generateHashCode(this);
this.hashGenerated = true;
}
return this.hashCode;
}
private static int generateHashCode(BlockBiomeWrapperPair pair) { return generateHashCode(pair.biome, pair.blockState); }
private static int generateHashCode(IBiomeWrapper biome, IBlockStateWrapper blockState)
{
final int prime = 31;
int result = 1;
// the biome and blockstate hashcode should be already calculated by the time
// we get here, so this operation should be very fast
result = prime * result + (biome == null ? 0 : biome.hashCode());
result = prime * result + (blockState == null ? 0 : blockState.hashCode());
return result;
}
@Override
public boolean equals(Object otherObj)
{
if (otherObj == this)
{
return true;
}
if (!(otherObj instanceof BlockBiomeWrapperPair))
{
return false;
}
BlockBiomeWrapperPair other = (BlockBiomeWrapperPair) otherObj;
return other.biome.getSerialString().equals(this.biome.getSerialString())
&& other.blockState.getSerialString().equals(this.blockState.getSerialString());
}
@Override
public String toString() { return this.serialize(); }
//=============//
// serializing //
//=============//
public String serialize()
{
if (this.serialString == null)
{
this.serialString = this.biome.getSerialString() + FullDataPointIdMap.BLOCK_STATE_SEPARATOR_STRING + this.blockState.getSerialString();
}
return this.serialString;
}
}
@@ -19,39 +19,43 @@
package com.seibel.distanthorizons.core.dataObjects.fullData; package com.seibel.distanthorizons.core.dataObjects.fullData;
import com.seibel.distanthorizons.core.dataObjects.BlockBiomeWrapperPair;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.util.objects.pooling.PhantomArrayListCheckout;
import com.seibel.distanthorizons.core.util.objects.pooling.PhantomArrayListPool;
import com.seibel.distanthorizons.core.util.objects.pooling.StringPool;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream; import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream;
import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataOutputStream; import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataOutputStream;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import org.apache.logging.log4j.LogManager; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.apache.logging.log4j.Logger; import it.unimi.dsi.fastutil.chars.CharArrayList;
import org.jetbrains.annotations.NotNull;
import java.io.*; import java.io.*;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
* WARNING: This is not THREAD-SAFE! <br><br> * Used to map a numerical IDs to a Biome/BlockState pair. <br>
* * Note: This is not thread safe. <br>
* Used to map a numerical IDs to a Biome/BlockState pair. <br><br>
*
* TODO the serializing of this map might be really big
* since it stringifies every block and biome name, which is quite bulky.
* It might be worth while to have a biome and block ID that then both get mapped
* to the data point ID to reduce file size.
* And/or it would be good to dynamically remove IDs that aren't currently in use.
* *
* @author Leetom * @author Leetom
*/ */
public class FullDataPointIdMap public class FullDataPointIdMap
{ {
private static final Logger LOGGER = LogManager.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("IdMap");
/** /**
* Should only be enabled when debugging. * Should only be enabled when debugging.
* Has the system check if any duplicate Entries were read/written * Has the system check if any duplicate Entries were read/written
@@ -59,15 +63,15 @@ public class FullDataPointIdMap
*/ */
private static final boolean RUN_SERIALIZATION_DUPLICATE_VALIDATION = false; private static final boolean RUN_SERIALIZATION_DUPLICATE_VALIDATION = false;
/** Distant Horizons - Block State Wrapper */ /** Distant Horizons - Block State Wrapper */
private static final String BLOCK_STATE_SEPARATOR_STRING = "_DH-BSW_"; public static final String BLOCK_STATE_SEPARATOR_STRING = "_DH-BSW_";
/** should only be used for debugging */ /** should only be used for debugging */
private long pos; private long pos;
/** The index should be the same as the Entry's ID */ /** The index should be the same as the BlockBiomeWrapperPair's ID */
private final ArrayList<Entry> entryList = new ArrayList<>(); private final ArrayList<BlockBiomeWrapperPair> blockBiomePairList = new ArrayList<>();
private final ConcurrentHashMap<Entry, Integer> idMap = new ConcurrentHashMap<>(); private final ConcurrentHashMap<BlockBiomeWrapperPair, Integer> idMap = new ConcurrentHashMap<>();
private int cachedHashCode = 0; private int cachedHashCode = 0;
@@ -76,59 +80,66 @@ public class FullDataPointIdMap
//=============// //=============//
// constructor // // constructor //
//=============// //=============//
//region
public FullDataPointIdMap(long pos) { this.pos = pos; } public FullDataPointIdMap(long pos) { this.pos = pos; }
//endregion
//=========// //=========//
// getters // // getters //
//=========// //=========//
//region
/** @see FullDataPointIdMap#getEntry(int) */ /** @see FullDataPointIdMap#getEntry(int) */
public IBiomeWrapper getBiomeWrapper(int id) throws IndexOutOfBoundsException { return this.getEntry(id).biome; } public IBiomeWrapper getBiomeWrapper(int id) throws IndexOutOfBoundsException { return this.getEntry(id).biome; }
/** @see FullDataPointIdMap#getEntry(int) */ /** @see FullDataPointIdMap#getEntry(int) */
public IBlockStateWrapper getBlockStateWrapper(int id) throws IndexOutOfBoundsException { return this.getEntry(id).blockState; } public IBlockStateWrapper getBlockStateWrapper(int id) throws IndexOutOfBoundsException { return this.getEntry(id).blockState; }
/** @throws IndexOutOfBoundsException if the given ID isn't in the {@link FullDataPointIdMap#entryList} */ /** @throws IndexOutOfBoundsException if the given ID isn't in the {@link FullDataPointIdMap#blockBiomePairList} */
private Entry getEntry(int id) throws IndexOutOfBoundsException private BlockBiomeWrapperPair getEntry(int id) throws IndexOutOfBoundsException
{ {
Entry entry; BlockBiomeWrapperPair pair;
try try
{ {
entry = this.entryList.get(id); pair = this.blockBiomePairList.get(id);
} }
catch (IndexOutOfBoundsException e) catch (IndexOutOfBoundsException e)
{ {
throw new IndexOutOfBoundsException("FullData ID Map out of sync for pos: "+this.pos+". ID: ["+id+"] greater than the number of known ID's: ["+this.entryList.size()+"]."); throw new IndexOutOfBoundsException("FullData ID Map out of sync for pos: "+DhSectionPos.toString(this.pos)+". ID: ["+id+"] greater than the number of known ID's: ["+this.blockBiomePairList.size()+"].");
} }
return entry; return pair;
} }
/** @return -1 if the list is empty */ /** @return -1 if the list is empty */
public int getMaxValidId() { return this.entryList.size() - 1; } public int getMaxValidId() { return this.blockBiomePairList.size() - 1; }
public int size() { return this.entryList.size(); } public int size() { return this.blockBiomePairList.size(); }
public boolean isEmpty() { return this.entryList.isEmpty(); } public boolean isEmpty() { return this.blockBiomePairList.isEmpty(); }
public long getPos() { return this.pos; } public long getPos() { return this.pos; }
//endregion
//=========// //=========//
// setters // // setters //
//=========// //=========//
//region
/** /**
* If an entry with the given values already exists nothing will * If an entry with the given values already exists nothing will
* be added but the existing item's ID will still be returned. * be added but the existing item's ID will still be returned.
*/ */
public int addIfNotPresentAndGetId(IBiomeWrapper biome, IBlockStateWrapper blockState) { return this.addIfNotPresentAndGetId(Entry.getEntry(biome, blockState)); } public int addIfNotPresentAndGetId(IBiomeWrapper biome, IBlockStateWrapper blockState) { return this.addIfNotPresentAndGetId(BlockBiomeWrapperPair.get(blockState, biome)); }
private int addIfNotPresentAndGetId(Entry biomeBlockStateEntry) private int addIfNotPresentAndGetId(BlockBiomeWrapperPair pair)
{ {
// try getting the existing ID // try getting the existing ID
Integer nullableId = this.idMap.get(biomeBlockStateEntry); Integer nullableId = this.idMap.get(pair);
if (nullableId != null) if (nullableId != null)
{ {
return nullableId; return nullableId;
@@ -136,7 +147,7 @@ public class FullDataPointIdMap
// create the new ID // create the new ID
return this.idMap.compute(biomeBlockStateEntry, (Entry newBiomeBlockStateEntry, Integer currentId) -> return this.idMap.compute(pair, (BlockBiomeWrapperPair newPair, Integer currentId) ->
{ {
if (currentId != null) if (currentId != null)
{ {
@@ -145,8 +156,8 @@ public class FullDataPointIdMap
// Add the new ID // Add the new ID
currentId = this.entryList.size(); currentId = this.blockBiomePairList.size();
this.entryList.add(biomeBlockStateEntry); this.blockBiomePairList.add(newPair);
// invalidate the cached hash code // invalidate the cached hash code
this.cachedHashCode = 0; this.cachedHashCode = 0;
@@ -156,7 +167,7 @@ public class FullDataPointIdMap
} }
/** /**
* Adds every {@link Entry} from inputMap into this map. <br> * Adds every {@link BlockBiomeWrapperPair} from inputMap into this map. <br>
* Allows duplicate entries. <br><br> * Allows duplicate entries. <br><br>
* *
* Allowing duplicate entries should be done if a datasource is just being read in and * Allowing duplicate entries should be done if a datasource is just being read in and
@@ -166,19 +177,19 @@ public class FullDataPointIdMap
*/ */
public void addAll(FullDataPointIdMap inputMap) public void addAll(FullDataPointIdMap inputMap)
{ {
ArrayList<Entry> entriesToMerge = inputMap.entryList; ArrayList<BlockBiomeWrapperPair> pairsToMerge = inputMap.blockBiomePairList;
for (int i = 0; i < entriesToMerge.size(); i++) for (int i = 0; i < pairsToMerge.size(); i++)
{ {
Entry entity = entriesToMerge.get(i); BlockBiomeWrapperPair pair = pairsToMerge.get(i);
this.add(entity); this.add(pair);
} }
} }
/** allows for adding duplicate {@link Entry} */ /** allows for adding duplicate {@link BlockBiomeWrapperPair} */
private void add(Entry biomeBlockStateEntry) private void add(BlockBiomeWrapperPair pair)
{ {
int id = this.entryList.size(); int id = this.blockBiomePairList.size();
this.entryList.add(biomeBlockStateEntry); this.blockBiomePairList.add(pair);
this.idMap.put(biomeBlockStateEntry, id); this.idMap.put(pair, id);
// invalidate the cached hash code // invalidate the cached hash code
this.cachedHashCode = 0; this.cachedHashCode = 0;
@@ -195,63 +206,67 @@ public class FullDataPointIdMap
*/ */
public int[] mergeAndReturnRemappedEntityIds(FullDataPointIdMap inputMap) public int[] mergeAndReturnRemappedEntityIds(FullDataPointIdMap inputMap)
{ {
ArrayList<Entry> entriesToMerge = inputMap.entryList; ArrayList<BlockBiomeWrapperPair> entriesToMerge = inputMap.blockBiomePairList;
int[] remappedEntryIds = new int[entriesToMerge.size()]; int[] remappedPairIds = new int[entriesToMerge.size()];
for (int i = 0; i < entriesToMerge.size(); i++) for (int i = 0; i < entriesToMerge.size(); i++)
{ {
Entry entity = entriesToMerge.get(i); BlockBiomeWrapperPair entity = entriesToMerge.get(i);
int id = this.addIfNotPresentAndGetId(entity); int id = this.addIfNotPresentAndGetId(entity);
remappedEntryIds[i] = id; remappedPairIds[i] = id;
} }
return remappedEntryIds; return remappedPairIds;
} }
/** Should only be used if this map is going to be reused, otherwise bad things will happen. */ /** Should only be used if this map is going to be reused, otherwise bad things will happen. */
public void clear(long pos) public void clear(long pos)
{ {
this.pos = pos; this.pos = pos;
this.entryList.clear(); this.blockBiomePairList.clear();
this.idMap.clear(); this.idMap.clear();
this.cachedHashCode = 0; this.cachedHashCode = 0;
} }
//endregion
//=============// //=============//
// serializing // // serializing //
//=============// //=============//
//region
/** Serializes all contained entries into the given stream, formatted in UTF */ /** Serializes all contained entries into the given stream, formatted in UTF */
public void serialize(DhDataOutputStream outputStream) throws IOException public void serialize(DhDataOutputStream outputStream) throws IOException
{ {
outputStream.writeInt(this.entryList.size()); outputStream.writeInt(this.blockBiomePairList.size());
// only used when debugging // only used when debugging
HashMap<String, FullDataPointIdMap.Entry> dataPointEntryBySerialization = new HashMap<>(); HashMap<String, BlockBiomeWrapperPair> dataPointEntryBySerialization = new HashMap<>();
for (Entry entry : this.entryList) for (BlockBiomeWrapperPair pair : this.blockBiomePairList)
{ {
String entryString = entry.serialize(); String entryString = pair.serialize();
outputStream.writeUTF(entryString); outputStream.writeUTF(entryString);
if (RUN_SERIALIZATION_DUPLICATE_VALIDATION) if (RUN_SERIALIZATION_DUPLICATE_VALIDATION)
{ {
if (dataPointEntryBySerialization.containsKey(entryString)) if (dataPointEntryBySerialization.containsKey(entryString))
{ {
LOGGER.error("Duplicate serialized entry found with serial: " + entryString); LOGGER.error("Duplicate serialized pair found with serial: " + entryString);
} }
if (dataPointEntryBySerialization.containsValue(entry)) if (dataPointEntryBySerialization.containsValue(pair))
{ {
LOGGER.error("Duplicate serialized entry found with value: " + entry.serialize()); LOGGER.error("Duplicate serialized pair found with value: " + pair.serialize());
} }
dataPointEntryBySerialization.put(entryString, entry); dataPointEntryBySerialization.put(entryString, pair);
} }
} }
} }
/** Creates a new IdBiomeBlockStateMap from the given UTF formatted stream */ /** Clears and populates the given {@link FullDataPointIdMap} from the given UTF formatted stream */
public static FullDataPointIdMap deserialize(DhDataInputStream inputStream, long pos, ILevelWrapper levelWrapper) throws IOException, InterruptedException, DataCorruptedException public static void deserialize(@NotNull FullDataPointIdMap map, DhDataInputStream inputStream, long pos, ILevelWrapper levelWrapper)
throws IOException, InterruptedException, DataCorruptedException
{ {
int entityCount = inputStream.readInt(); int entityCount = inputStream.readInt();
if (entityCount < 0) if (entityCount < 0)
@@ -259,52 +274,140 @@ public class FullDataPointIdMap
throw new DataCorruptedException("FullDataPointIdMap deserialize entry count should have a number greater than or equal to 0, returned value ["+entityCount+"]."); throw new DataCorruptedException("FullDataPointIdMap deserialize entry count should have a number greater than or equal to 0, returned value ["+entityCount+"].");
} }
// clearing the old values is necessary so we can re-use the same map multiple times
map.clear(pos);
// only used when debugging // only used when debugging
HashMap<String, FullDataPointIdMap.Entry> dataPointEntryBySerialization = new HashMap<>(); HashMap<String, BlockBiomeWrapperPair> dataPointEntryBySerialization = new HashMap<>();
FullDataPointIdMap newMap = new FullDataPointIdMap(pos); try(PhantomArrayListCheckout checkout = ARRAY_LIST_POOL.checkoutArrays(0, 0, 0, 3))
for (int i = 0; i < entityCount; i++)
{ {
// necessary to prevent issues with deserializing objects after the level has been closed for (int i = 0; i < entityCount; i++)
if (Thread.interrupted())
{ {
throw new InterruptedException(FullDataPointIdMap.class.getSimpleName() + " task interrupted."); // necessary to prevent issues with deserializing objects after the level has been closed
} if (Thread.interrupted())
String entryString = inputStream.readUTF();
Entry newEntry = Entry.deserialize(entryString, levelWrapper);
newMap.entryList.add(newEntry);
if (RUN_SERIALIZATION_DUPLICATE_VALIDATION)
{
if (dataPointEntryBySerialization.containsKey(entryString))
{ {
LOGGER.error("Duplicate deserialized entry found with serial: " + entryString); throw new InterruptedException("[" + FullDataPointIdMap.class.getSimpleName() + "] deserializing interrupted.");
} }
if (dataPointEntryBySerialization.containsValue(newEntry))
int length = inputStream.readUnsignedShort();
CharArrayList fullCharList = checkout.getCharArray(0, length);
CharArrayList biomeCharList = checkout.getCharArray(1, length);
CharArrayList blockCharList = checkout.getCharArray(2, length);
// parse the full UTF string
for (int stringIndex = 0; stringIndex < length; stringIndex++)
{ {
LOGGER.error("Duplicate deserialized entry found with value: " + newEntry.serialize()); byte b = inputStream.readByte();
char c = (char) (b & 0xFF);
fullCharList.set(stringIndex, c);
}
splitCharArray(fullCharList, biomeCharList, blockCharList);
String biomeString = StringPool.INSTANCE.getPooledString(biomeCharList);
IBiomeWrapper biome = WRAPPER_FACTORY.deserializeBiomeWrapperOrGetDefault(biomeString, levelWrapper);
String blockStateString = StringPool.INSTANCE.getPooledString(blockCharList);
IBlockStateWrapper blockState = WRAPPER_FACTORY.deserializeBlockStateWrapperOrGetDefault(blockStateString, levelWrapper);
BlockBiomeWrapperPair newPair = BlockBiomeWrapperPair.get(blockState, biome);
map.blockBiomePairList.add(newPair);
if (RUN_SERIALIZATION_DUPLICATE_VALIDATION)
{
String entryString = StringPool.INSTANCE.getPooledString(fullCharList);
if (dataPointEntryBySerialization.containsKey(entryString))
{
LOGGER.error("Duplicate deserialized entry found with serial: " + entryString);
}
if (dataPointEntryBySerialization.containsValue(newPair))
{
LOGGER.error("Duplicate deserialized entry found with value: " + newPair.serialize());
}
dataPointEntryBySerialization.put(entryString, newPair);
} }
dataPointEntryBySerialization.put(entryString, newEntry);
} }
} }
if (newMap.size() != entityCount) if (map.size() != entityCount)
{ {
// if the mappings are out of sync then the LODs will render incorrectly due to IDs being wrong // if the mappings are out of sync then the LODs will render incorrectly due to IDs being wrong
LodUtil.assertNotReach("ID maps failed to deserialize for pos: ["+ DhSectionPos.toString(pos)+"], incorrect entity count. Expected count ["+entityCount+"], actual count ["+newMap.size()+"]"); LodUtil.assertNotReach("ID maps failed to deserialize for pos: ["+ DhSectionPos.toString(pos)+"], incorrect entity count. Expected count ["+entityCount+"], actual count ["+map.size()+"]");
}
}
/**
* Splits up the given input {@link CharArrayList} into the
* necessary biome and blockstate lists based on the location of
* {@link FullDataPointIdMap#BLOCK_STATE_SEPARATOR_STRING}
*/
private static void splitCharArray(
CharArrayList input,
CharArrayList biomeString, CharArrayList blockString) throws DataCorruptedException
{
boolean separatorFound = false;
int foundStartIndex = -1;
int separatorIndex = 0;
for (int inputIndex = 0; inputIndex < input.size(); inputIndex++)
{
char ch = input.getChar(inputIndex);
if (ch == FullDataPointIdMap.BLOCK_STATE_SEPARATOR_STRING.charAt(separatorIndex))
{
if (!separatorFound)
{
foundStartIndex = inputIndex;
}
separatorFound = true;
separatorIndex++;
}
else
{
separatorFound = false;
foundStartIndex = -1;
separatorIndex = 0;
}
if (separatorFound
&& separatorIndex == FullDataPointIdMap.BLOCK_STATE_SEPARATOR_STRING.length())
{
break;
}
} }
return newMap;
if (foundStartIndex == -1)
{
throw new DataCorruptedException("Failed to deserialize BiomeBlockStateEntry ["+input.toString()+"], unable to find separator.");
}
biomeString.clear();
for (int i = 0; i < foundStartIndex; i++)
{
biomeString.push(input.getChar(i));
}
blockString.clear();
for (int i = foundStartIndex + FullDataPointIdMap.BLOCK_STATE_SEPARATOR_STRING.length();
i < input.size();
i++)
{
blockString.push(input.getChar(i));
}
} }
//endregion
//===========// //===========//
// overrides // // overrides //
//===========// //===========//
//region
@Override
public String toString() { return DhSectionPos.toString(this.pos) + " size: " + this.blockBiomePairList.size(); }
@Override @Override
public boolean equals(Object other) public boolean equals(Object other)
@@ -333,149 +436,15 @@ public class FullDataPointIdMap
private void generateHashCode() private void generateHashCode()
{ {
int result = DhSectionPos.hashCode(this.pos); int result = DhSectionPos.hashCode(this.pos);
for (int i = 0; i < this.entryList.size(); i++) for (int i = 0; i < this.blockBiomePairList.size(); i++)
{ {
result = 31 * result + this.entryList.hashCode(); result = 31 * result + this.blockBiomePairList.hashCode();
} }
this.cachedHashCode = result; this.cachedHashCode = result;
} }
//endregion
//==============//
// helper class //
//==============//
private static final class Entry
{
private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
/** two levels are present so we don't need to use a key object */
private static final ConcurrentHashMap<IBiomeWrapper, ConcurrentHashMap<IBlockStateWrapper, Entry>> ENTRY_BY_BLOCKSTATE_BY_BIOMEWRAPPER = new ConcurrentHashMap<>();
public final IBiomeWrapper biome;
public final IBlockStateWrapper blockState;
private int hashCode = 0;
private boolean hashGenerated = false;
private String serialString = null;
//=============//
// constructor //
//=============//
public static Entry getEntry(IBiomeWrapper biome, IBlockStateWrapper blockState)
{
// check for existing entry
ConcurrentHashMap<IBlockStateWrapper, Entry> entryByBlockState = ENTRY_BY_BLOCKSTATE_BY_BIOMEWRAPPER.get(biome);
if (entryByBlockState != null)
{
Entry entry = entryByBlockState.get(blockState);
if (entry != null)
{
return entry;
}
}
// 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)
{
this.biome = biome;
this.blockState = blockState;
}
//===========//
// overrides //
//===========//
/**
* Reminder: this hash code won't always be unique, collisions can occur;
* because of that this hash shouldn't be the only unique identifier for this object.
*/
@Override
public int hashCode()
{
// cache the hash code to improve speed
if (!this.hashGenerated)
{
this.hashCode = generateHashCode(this);
this.hashGenerated = true;
}
return this.hashCode;
}
private static int generateHashCode(Entry entry) { return generateHashCode(entry.biome, entry.blockState); }
private static int generateHashCode(IBiomeWrapper biome, IBlockStateWrapper blockState)
{
final int prime = 31;
int result = 1;
// the biome and blockstate hashcode should be already calculated by the time
// we get here, so this operation should be very fast
result = prime * result + (biome == null ? 0 : biome.hashCode());
result = prime * result + (blockState == null ? 0 : blockState.hashCode());
return result;
}
@Override
public boolean equals(Object otherObj)
{
if (otherObj == this)
{
return true;
}
if (!(otherObj instanceof Entry))
{
return false;
}
Entry other = (Entry) otherObj;
return other.biome.getSerialString().equals(this.biome.getSerialString())
&& other.blockState.getSerialString().equals(this.blockState.getSerialString());
}
@Override
public String toString() { return this.serialize(); }
//=================//
// (de)serializing //
//=================//
public String serialize()
{
if (this.serialString == null)
{
this.serialString = this.biome.getSerialString() + BLOCK_STATE_SEPARATOR_STRING + this.blockState.getSerialString();
}
return this.serialString;
}
public static Entry deserialize(String str, ILevelWrapper levelWrapper) throws DataCorruptedException
{
int separatorIndex = str.indexOf(BLOCK_STATE_SEPARATOR_STRING);
if (separatorIndex == -1)
{
throw new DataCorruptedException("Failed to deserialize BiomeBlockStateEntry ["+str+"], unable to find separator.");
}
IBiomeWrapper biome = WRAPPER_FACTORY.deserializeBiomeWrapperOrGetDefault(str.substring(0, separatorIndex), levelWrapper);
IBlockStateWrapper blockState = WRAPPER_FACTORY.deserializeBlockStateWrapperOrGetDefault(str.substring(separatorIndex+BLOCK_STATE_SEPARATOR_STRING.length()), levelWrapper);
return Entry.getEntry(biome, blockState);
}
}
} }
@@ -20,8 +20,8 @@
package com.seibel.distanthorizons.core.dataObjects.fullData.sources; package com.seibel.distanthorizons.core.dataObjects.fullData.sources;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep;
import com.seibel.distanthorizons.core.file.IDataSource;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV1DTO; import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV1DTO;
@@ -33,7 +33,6 @@ import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataOutputStre
import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import org.apache.logging.log4j.Logger;
import java.io.*; import java.io.*;
import java.util.Arrays; import java.util.Arrays;
@@ -47,9 +46,9 @@ import java.util.Arrays;
* @see FullDataPointUtil * @see FullDataPointUtil
* @see FullDataSourceV2 * @see FullDataSourceV2
*/ */
public class FullDataSourceV1 implements IDataSource<IDhLevel> public class FullDataSourceV1
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public static final byte SECTION_SIZE_OFFSET = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; public static final byte SECTION_SIZE_OFFSET = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL;
/** measured in dataPoints */ /** measured in dataPoints */
@@ -94,28 +93,13 @@ public class FullDataSourceV1 implements IDataSource<IDhLevel>
} }
//======//
// data //
//======//
@Deprecated
@Override
public boolean update(FullDataSourceV2 dataSource, IDhLevel level) { throw new UnsupportedOperationException("Deprecated"); }
//=====================// //=====================//
// setters and getters // // setters and getters //
//=====================// //=====================//
@Override
public Long getKey() { return this.pos; } public Long getKey() { return this.pos; }
@Override
public String getKeyDisplayString() { return DhSectionPos.toString(this.pos); } public String getKeyDisplayString() { return DhSectionPos.toString(this.pos); }
@Override
public long getPos() { return this.pos; } public long getPos() { return this.pos; }
public void resizeDataStructuresForRepopulation(long pos) public void resizeDataStructuresForRepopulation(long pos)
@@ -124,7 +108,6 @@ public class FullDataSourceV1 implements IDataSource<IDhLevel>
this.pos = pos; this.pos = pos;
} }
@Override
public byte getDataDetailLevel() { return (byte) (DhSectionPos.getDetailLevel(this.pos) - SECTION_SIZE_OFFSET); } public byte getDataDetailLevel() { return (byte) (DhSectionPos.getDetailLevel(this.pos) - SECTION_SIZE_OFFSET); }
public boolean isEmpty() { return this.isEmpty; } public boolean isEmpty() { return this.isEmpty; }
@@ -183,8 +166,7 @@ public class FullDataSourceV1 implements IDataSource<IDhLevel>
this.setDataPoints(dataPoints); this.setDataPoints(dataPoints);
FullDataPointIdMap mapping = this.readIdMappings(inputStream, level.getLevelWrapper()); this.readIdMappings(this.mapping, inputStream, level.getLevelWrapper());
this.setIdMapping(mapping);
} }
@@ -197,7 +179,7 @@ public class FullDataSourceV1 implements IDataSource<IDhLevel>
{ {
outputStream.writeInt(this.getDataDetailLevel()); outputStream.writeInt(this.getDataDetailLevel());
outputStream.writeInt(WIDTH); outputStream.writeInt(WIDTH);
outputStream.writeInt(level.getMinY()); outputStream.writeInt(level.getLevelWrapper().getMinHeight());
outputStream.writeByte(this.worldGenStep.value); outputStream.writeByte(this.worldGenStep.value);
} }
@@ -206,19 +188,19 @@ public class FullDataSourceV1 implements IDataSource<IDhLevel>
int dataDetail = inputStream.readInt(); int dataDetail = inputStream.readInt();
if (dataDetail != dto.dataDetailLevel) if (dataDetail != dto.dataDetailLevel)
{ {
throw new IOException(LodUtil.formatLog("Data level mismatch. Expected: ["+dto.dataDetailLevel+"], found ["+dataDetail+"].")); throw new IOException("Data level mismatch. Expected: ["+dto.dataDetailLevel+"], found ["+dataDetail+"].");
} }
int width = inputStream.readInt(); int width = inputStream.readInt();
if (width != WIDTH) if (width != WIDTH)
{ {
throw new IOException(LodUtil.formatLog("Section width mismatch: " + width + " != " + WIDTH + " (Currently only 1 section width is supported)")); throw new IOException("Section width mismatch: [" + width + "] != [" + WIDTH + "] (Currently only 1 section width is supported)");
} }
int minY = inputStream.readInt(); int minY = inputStream.readInt();
if (minY != level.getMinY()) if (minY != level.getLevelWrapper().getMinHeight())
{ {
LOGGER.warn("Data minY mismatch: " + minY + " != " + level.getMinY() + ". Will ignore data's y level"); LOGGER.warn("Data minY mismatch: [" + minY + "] != [" + level.getLevelWrapper().getMinHeight() + "]. Will ignore data's y level");
} }
byte worldGenByte = inputStream.readByte(); byte worldGenByte = inputStream.readByte();
@@ -364,7 +346,7 @@ public class FullDataSourceV1 implements IDataSource<IDhLevel>
outputStream.writeInt(DATA_GUARD_BYTE); outputStream.writeInt(DATA_GUARD_BYTE);
this.mapping.serialize(outputStream); this.mapping.serialize(outputStream);
} }
public FullDataPointIdMap readIdMappings(DhDataInputStream inputStream, ILevelWrapper levelWrapper) throws IOException, InterruptedException, DataCorruptedException public void readIdMappings(FullDataPointIdMap map, DhDataInputStream inputStream, ILevelWrapper levelWrapper) throws IOException, InterruptedException, DataCorruptedException
{ {
int guardByte = inputStream.readInt(); int guardByte = inputStream.readInt();
if (guardByte != DATA_GUARD_BYTE) if (guardByte != DATA_GUARD_BYTE)
@@ -372,18 +354,8 @@ public class FullDataSourceV1 implements IDataSource<IDhLevel>
throw new IOException("Invalid data content end guard for ID mapping"); throw new IOException("Invalid data content end guard for ID mapping");
} }
return FullDataPointIdMap.deserialize(inputStream, this.pos, levelWrapper); FullDataPointIdMap.deserialize(map, inputStream, this.pos, levelWrapper);
} }
public void setIdMapping(FullDataPointIdMap mappings) { this.mapping.mergeAndReturnRemappedEntityIds(mappings); }
//==================//
// override methods //
//==================//
@Override
public void close()
{ /* not currently needed */ }
@@ -1,96 +0,0 @@
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,15 @@
package com.seibel.distanthorizons.core.dataObjects.render; package com.seibel.distanthorizons.core.dataObjects.render;
import com.seibel.distanthorizons.api.enums.config.EDhApiVerticalQuality;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pooling.AbstractPhantomArrayList; import com.seibel.distanthorizons.core.util.objects.pooling.AbstractPhantomArrayList;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListPool; import com.seibel.distanthorizons.core.util.objects.pooling.PhantomArrayListPool;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnRenderView;
import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView;
import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnQuadView;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import com.seibel.distanthorizons.core.util.ColorUtil;
import com.seibel.distanthorizons.core.util.RenderDataPointUtil; import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import java.util.concurrent.atomic.AtomicLong;
/** /**
* Stores the render data used to generate OpenGL buffers. * Stores the render data used to generate OpenGL buffers.
@@ -41,99 +36,98 @@ import java.util.concurrent.atomic.AtomicLong;
*/ */
public class ColumnRenderSource extends AbstractPhantomArrayList public class ColumnRenderSource extends AbstractPhantomArrayList
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
public static final boolean DO_SAFETY_CHECKS = ModInfo.IS_DEV_BUILD; /** measured in data columns */
public static final byte SECTION_SIZE_OFFSET = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; public static final int WIDTH = 64;
/** width of this data in columns */
public static final int SECTION_SIZE = BitShiftUtil.powerOfTwo(SECTION_SIZE_OFFSET); // 64
public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Render Source"); public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Render Source");
/** will be zero if an empty data source was created */ /**
public int verticalDataCount; * will be zero if an empty data source was created
* @see EDhApiVerticalQuality#calculateMaxNumberOfVerticalSlicesAtDetailLevel(byte)
*/
public int maxVerticalSliceCount;
public long pos; public long pos;
public int yOffset; public int yOffset;
public final LongArrayList renderDataContainer; public final LongArrayList renderDataContainer;
public final DebugSourceFlag[] debugSourceFlags;
private boolean isEmpty = true; private boolean isEmpty = true;
public AtomicLong localVersion = new AtomicLong(0); // used to track changes to the data source, so that buffers can be updated when necessary
//==============// //==============//
// constructors // // constructors //
//==============// //==============//
//region
public static ColumnRenderSource createEmpty(long pos, int maxVerticalSize, int yOffset) public static ColumnRenderSource createEmpty(long pos, int maxVertSliceCount, int yOffset)
{ return new ColumnRenderSource(pos, maxVerticalSize, yOffset); } { return new ColumnRenderSource(pos, maxVertSliceCount, yOffset); }
/** /**
* Creates an empty ColumnRenderSource. * Creates an empty ColumnRenderSource.
* *
* @param pos the relative position of the container * @param pos the relative position of the container
* @param maxVerticalSize the maximum vertical size of the container * @param maxVertSliceCount the maximum vertical size of the container
*/ */
private ColumnRenderSource(long pos, int maxVerticalSize, int yOffset) private ColumnRenderSource(long pos, int maxVertSliceCount, int yOffset)
{ {
super(ARRAY_LIST_POOL, 0, 0, 1); super(ARRAY_LIST_POOL, 0, 0, 1, 0);
this.pos = pos; this.pos = pos;
this.yOffset = yOffset; this.yOffset = yOffset;
this.verticalDataCount = maxVerticalSize; this.maxVerticalSliceCount = maxVertSliceCount;
this.renderDataContainer = this.pooledArraysCheckout.getLongArray(0, SECTION_SIZE * SECTION_SIZE * this.verticalDataCount); this.renderDataContainer = this.pooledArraysCheckout.getLongArray(0, WIDTH * WIDTH * this.maxVerticalSliceCount);
this.debugSourceFlags = new DebugSourceFlag[SECTION_SIZE * SECTION_SIZE];
} }
//endregion
//========================// //========================//
// datapoint manipulation // // datapoint manipulation //
//========================// //========================//
//region
public long getDataPoint(int posX, int posZ, int verticalIndex) { return this.renderDataContainer.getLong(posX * SECTION_SIZE * this.verticalDataCount + posZ * this.verticalDataCount + verticalIndex); } public long getDataPoint(int posX, int posZ, int verticalIndex) { return this.renderDataContainer.getLong(posX * WIDTH * this.maxVerticalSliceCount + posZ * this.maxVerticalSliceCount + verticalIndex); }
public ColumnArrayView getVerticalDataPointView(int posX, int posZ) public void populateColumnView(ColumnRenderView view, int posX, int posZ) throws IllegalArgumentException
{ {
int offset = posX * SECTION_SIZE * this.verticalDataCount + posZ * this.verticalDataCount; int offset = posX * WIDTH * this.maxVerticalSliceCount + posZ * this.maxVerticalSliceCount;
// don't allow returning views that are outside this render source's bounds // don't allow returning views that are outside this render source's bounds
if (offset >= this.renderDataContainer.size()) if (offset >= this.renderDataContainer.size())
{ {
return null; throw new IllegalArgumentException("Column View offset ["+offset+"] greater than parent render data container ["+DhSectionPos.toString(this.pos)+"] size ["+this.renderDataContainer.size()+"].");
} }
else if (posX < 0 || posX >= SECTION_SIZE else if (posX < 0 || posX >= WIDTH
|| posZ < 0 || posZ >= SECTION_SIZE) || posZ < 0 || posZ >= WIDTH)
{ {
return null; throw new IllegalArgumentException("Column View pos outside valid range ["+posX+","+posZ+"].");
} }
return new ColumnArrayView(this.renderDataContainer, this.verticalDataCount, view.populate(
offset, this.verticalDataCount); this.renderDataContainer, this.maxVerticalSliceCount,
offset, this.maxVerticalSliceCount);
} }
public ColumnQuadView getFullQuadView() { return this.getQuadViewOverRange(0, 0, SECTION_SIZE, SECTION_SIZE); } //endregion
public ColumnQuadView getQuadViewOverRange(int quadX, int quadZ, int quadXSize, int quadZSize) { return new ColumnQuadView(this.renderDataContainer, SECTION_SIZE, this.verticalDataCount, quadX, quadZ, quadXSize, quadZSize); }
//=====================// //=====================//
// data helper methods // // data helper methods //
//=====================// //=====================//
//region
public Long getPos() { return this.pos; } public Long getPos() { return this.pos; }
public Long getKey() { return this.pos; } public Long getKey() { return this.pos; }
public String getKeyDisplayString() { return DhSectionPos.toString(this.pos); }
public byte getDataDetailLevel() { return (byte) (DhSectionPos.getDetailLevel(this.pos) - SECTION_SIZE_OFFSET); } public byte getDataDetailLevel() { return (byte) (DhSectionPos.getDetailLevel(this.pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); }
public boolean isEmpty() { return this.isEmpty; } public boolean isEmpty() { return this.isEmpty; }
public void markNotEmpty() { this.isEmpty = false; } public void markNotEmpty() { this.isEmpty = false; }
@@ -146,18 +140,20 @@ public class ColumnRenderSource extends AbstractPhantomArrayList
return false; return false;
} }
try (ColumnRenderView columnView = ColumnRenderView.getPooled())
for (int x = 0; x < SECTION_SIZE; x++)
{ {
for (int z = 0; z < SECTION_SIZE; z++) for (int x = 0; x < WIDTH; x++)
{ {
ColumnArrayView columnArrayView = this.getVerticalDataPointView(x,z); for (int z = 0; z < WIDTH; z++)
for (int i = 0; i < columnArrayView.size; i++)
{ {
long dataPoint = columnArrayView.get(i); this.populateColumnView(columnView, x, z);
if (!RenderDataPointUtil.isVoid(dataPoint)) for (int i = 0; i < columnView.size; i++)
{ {
return true; long dataPoint = columnView.get(i);
if (!RenderDataPointUtil.hasZeroHeight(dataPoint))
{
return true;
}
} }
} }
} }
@@ -166,31 +162,14 @@ public class ColumnRenderSource extends AbstractPhantomArrayList
return false; return false;
} }
//endregion
//=======//
// debug //
//=======//
/** Sets the debug flag for the given area */
public void fillDebugFlag(int xStart, int zStart, int xWidth, int zWidth, DebugSourceFlag flag)
{
for (int x = xStart; x < xStart + xWidth; x++)
{
for (int z = zStart; z < zStart + zWidth; z++)
{
this.debugSourceFlags[x * SECTION_SIZE + z] = flag;
}
}
}
public DebugSourceFlag debugGetFlag(int ox, int oz) { return this.debugSourceFlags[ox * SECTION_SIZE + oz]; }
//==============// //==============//
// base methods // // base methods //
//==============// //==============//
//region
@Override @Override
public String toString() public String toString()
@@ -208,11 +187,11 @@ public class ColumnRenderSource extends AbstractPhantomArrayList
{ {
for (int x = 0; x < size; x++) for (int x = 0; x < size; x++)
{ {
for (int y = 0; y < this.verticalDataCount; y++) for (int y = 0; y < this.maxVerticalSliceCount; y++)
{ {
//Converting the dataToHex //Converting the dataToHex
stringBuilder.append(Long.toHexString(this.getDataPoint(x, z, y))); stringBuilder.append(Long.toHexString(this.getDataPoint(x, z, y)));
if (y != this.verticalDataCount - 1) if (y != this.maxVerticalSliceCount - 1)
stringBuilder.append(SUBDATA_DELIMITER); stringBuilder.append(SUBDATA_DELIMITER);
} }
@@ -226,21 +205,8 @@ public class ColumnRenderSource extends AbstractPhantomArrayList
return stringBuilder.toString(); return stringBuilder.toString();
} }
//endregion
//==============//
// helper enums //
//==============//
public enum DebugSourceFlag
{
FULL(ColorUtil.BLUE),
DIRECT(ColorUtil.WHITE),
FILE(ColorUtil.BROWN);
public final int color;
DebugSourceFlag(int color) { this.color = color; }
}
} }
@@ -35,7 +35,7 @@ public final class BufferQuad
public static final int NORMAL_MAX_QUAD_WIDTH = 2048; public static final int NORMAL_MAX_QUAD_WIDTH = 2048;
/** /**
* The maximum number of blocks wide a quad can be * The maximum number of blocks wide a quad can be
* when {@link Config.Client.Advanced.Graphics.AdvancedGraphics#earthCurveRatio earthCurveRatio} * when {@link Config.Client.Advanced.Graphics.Experimental#earthCurveRatio earthCurveRatio}
* is enabled. * is enabled.
*/ */
public static final int MAX_QUAD_WIDTH_FOR_EARTH_CURVATURE = LodUtil.CHUNK_WIDTH; public static final int MAX_QUAD_WIDTH_FOR_EARTH_CURVATURE = LodUtil.CHUNK_WIDTH;
@@ -47,7 +47,7 @@ public final class BufferQuad
public short widthEastWest; public short widthEastWest;
/** This is both North/South and Up/Down since the merging logic is the same either way */ /** This is both North/South and Up/Down since the merging logic is the same either way */
public short widthNorthSouthOrUpDown; public short widthNorthSouthOrHeight;
public final int color; public final int color;
/** used by the Iris shader mod to determine how each LOD should be rendered */ /** used by the Iris shader mod to determine how each LOD should be rendered */
@@ -62,20 +62,24 @@ public final class BufferQuad
BufferQuad( BufferQuad(
short x, short y, short z, short widthEastWest, short widthNorthSouthOrUpDown, short x, short y, short z, short widthEastWest, short widthNorthSouthOrHeight,
int color, byte irisBlockMaterialId, byte skylight, byte blockLight, int color, byte irisBlockMaterialId, byte skylight, byte blockLight,
EDhDirection direction) EDhDirection direction)
{ {
if (widthEastWest == 0 || widthNorthSouthOrUpDown == 0) if (widthEastWest == 0 || widthNorthSouthOrHeight == 0)
{
throw new IllegalArgumentException("Size 0 quad!"); throw new IllegalArgumentException("Size 0 quad!");
if (widthEastWest < 0 || widthNorthSouthOrUpDown < 0) }
if (widthEastWest < 0 || widthNorthSouthOrHeight < 0)
{
throw new IllegalArgumentException("Negative sized quad!"); throw new IllegalArgumentException("Negative sized quad!");
}
this.x = x; this.x = x;
this.y = y; this.y = y;
this.z = z; this.z = z;
this.widthEastWest = widthEastWest; this.widthEastWest = widthEastWest;
this.widthNorthSouthOrUpDown = widthNorthSouthOrUpDown; this.widthNorthSouthOrHeight = widthNorthSouthOrHeight;
this.color = color; this.color = color;
this.irisBlockMaterialId = irisBlockMaterialId; this.irisBlockMaterialId = irisBlockMaterialId;
this.skyLight = skylight; this.skyLight = skylight;
@@ -99,7 +103,7 @@ public final class BufferQuad
if (compareDirection == BufferMergeDirectionEnum.EastWest) if (compareDirection == BufferMergeDirectionEnum.EastWest)
{ {
switch (this.direction.getAxis()) switch (this.direction.axis)
{ {
case X: case X:
return threeDimensionalCompare(this.x, this.y, this.z, quad.x, quad.y, quad.z); return threeDimensionalCompare(this.x, this.y, this.z, quad.x, quad.y, quad.z);
@@ -109,12 +113,12 @@ public final class BufferQuad
return threeDimensionalCompare(this.z, this.y, this.x, quad.z, quad.y, quad.x); return threeDimensionalCompare(this.z, this.y, this.x, quad.z, quad.y, quad.x);
default: default:
throw new IllegalArgumentException("Invalid Axis enum: " + this.direction.getAxis()); throw new IllegalArgumentException("Invalid Axis enum: [" + this.direction.axis + "].");
} }
} }
else else
{ {
switch (this.direction.getAxis()) switch (this.direction.axis)
{ {
case X: case X:
return threeDimensionalCompare(this.x, this.z, this.y, quad.x, quad.z, quad.y); return threeDimensionalCompare(this.x, this.z, this.y, quad.x, quad.z, quad.y);
@@ -124,7 +128,7 @@ public final class BufferQuad
return threeDimensionalCompare(this.z, this.x, this.y, quad.z, quad.x, quad.y); return threeDimensionalCompare(this.z, this.x, this.y, quad.z, quad.x, quad.y);
default: default:
throw new IllegalArgumentException("Invalid Axis enum: " + this.direction.getAxis()); throw new IllegalArgumentException("Invalid Axis enum: [" + this.direction.axis + "].");
} }
} }
} }
@@ -157,8 +161,8 @@ public final class BufferQuad
return false; return false;
// make sure these quads share the same perpendicular axis // make sure these quads share the same perpendicular axis
if ((mergeDirection == BufferMergeDirectionEnum.EastWest && this.y != quad.y) || if ((mergeDirection == BufferMergeDirectionEnum.EastWest && this.y != quad.y)
(mergeDirection == BufferMergeDirectionEnum.NorthSouthOrUpDown && this.x != quad.x)) || (mergeDirection == BufferMergeDirectionEnum.NorthSouthOrUpDown && this.x != quad.x))
{ {
return false; return false;
} }
@@ -169,7 +173,7 @@ public final class BufferQuad
short thisParallelCompareStartPos; // edge parallel to the merge direction short thisParallelCompareStartPos; // edge parallel to the merge direction
short otherPerpendicularCompareStartPos; short otherPerpendicularCompareStartPos;
short otherParallelCompareStartPos; short otherParallelCompareStartPos;
switch (this.direction.getAxis()) switch (this.direction.axis)
{ {
default: // shouldn't normally happen, just here to make the compiler happy default: // shouldn't normally happen, just here to make the compiler happy
case X: case X:
@@ -238,17 +242,17 @@ public final class BufferQuad
if (mergeDirection == BufferMergeDirectionEnum.EastWest) if (mergeDirection == BufferMergeDirectionEnum.EastWest)
{ {
thisPerpendicularCompareWidth = this.widthEastWest; thisPerpendicularCompareWidth = this.widthEastWest;
thisParallelCompareWidth = this.widthNorthSouthOrUpDown; thisParallelCompareWidth = this.widthNorthSouthOrHeight;
otherPerpendicularCompareWidth = quad.widthEastWest; otherPerpendicularCompareWidth = quad.widthEastWest;
otherParallelCompareWidth = quad.widthNorthSouthOrUpDown; otherParallelCompareWidth = quad.widthNorthSouthOrHeight;
} }
else else
{ {
thisPerpendicularCompareWidth = this.widthNorthSouthOrUpDown; thisPerpendicularCompareWidth = this.widthNorthSouthOrHeight;
thisParallelCompareWidth = this.widthEastWest; thisParallelCompareWidth = this.widthEastWest;
otherPerpendicularCompareWidth = quad.widthNorthSouthOrUpDown; otherPerpendicularCompareWidth = quad.widthNorthSouthOrHeight;
otherParallelCompareWidth = quad.widthEastWest; otherParallelCompareWidth = quad.widthEastWest;
} }
@@ -318,7 +322,7 @@ public final class BufferQuad
// merge the two quads // merge the two quads
if (mergeDirection == BufferMergeDirectionEnum.NorthSouthOrUpDown) if (mergeDirection == BufferMergeDirectionEnum.NorthSouthOrUpDown)
{ {
this.widthNorthSouthOrUpDown += quad.widthNorthSouthOrUpDown; this.widthNorthSouthOrHeight += quad.widthNorthSouthOrHeight;
} }
else // if (mergeDirection == MergeDirection.EastWest) else // if (mergeDirection == MergeDirection.EastWest)
{ {
@@ -23,38 +23,25 @@ import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.util.objects.pooling.PhantomArrayListCheckout;
import com.seibel.distanthorizons.core.util.ColorUtil; import com.seibel.distanthorizons.core.util.ColorUtil;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.RenderDataPointUtil; import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnRenderView;
import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import com.seibel.distanthorizons.core.render.renderer.LodRenderer;
import com.seibel.distanthorizons.coreapi.util.MathUtil; import com.seibel.distanthorizons.coreapi.util.MathUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
public class ColumnBox public class ColumnBox
{ {
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
/**
* if the skylight has this value that means
* no data is expected
*/
private static final byte SKYLIGHT_EMPTY = -1;
/** /**
* if the skylight has this value that means * if the skylight has this value that means
* that block position is covered/occuled by an adjacent block/column. * that block position is covered/occluded by an adjacent block/column.
*/ */
private static final byte SKYLIGHT_COVERED = -2; private static final byte SKYLIGHT_COVERED = -1;
private static final ThreadLocal<byte[]> THREAD_LOCAL_SKY_LIGHT_ARRAY = ThreadLocal.withInitial(() ->
{
byte[] array = new byte[RenderDataPointUtil.MAX_WORLD_Y_SIZE];
Arrays.fill(array, SKYLIGHT_EMPTY);
return array;
});
@@ -63,33 +50,36 @@ public class ColumnBox
//=========// //=========//
public static void addBoxQuadsToBuilder( public static void addBoxQuadsToBuilder(
LodQuadBuilder builder, IDhClientLevel clientLevel, LodQuadBuilder builder, PhantomArrayListCheckout phantomArrayCheckout, IDhClientLevel clientLevel,
short xSize, short ySize, short zSize, short width, short yHeight,
short minX, short minY, short minZ, short minX, short minY, short minZ,
int color, byte irisBlockMaterialId, byte skyLight, byte blockLight, int color, byte irisBlockMaterialId, byte skyLight, byte blockLight,
long topData, long bottomData, ColumnArrayView[] adjData, boolean[] isAdjDataSameDetailLevel) long topData, long bottomData, ColumnRenderView[] adjData, boolean[] isAdjDataSameDetailLevel)
{ {
//================// //================//
// variable setup // // variable setup //
//================// //================//
short maxX = (short) (minX + xSize); short maxX = (short) (minX + width);
short maxY = (short) (minY + ySize); short maxY = (short) (minY + yHeight);
short maxZ = (short) (minZ + zSize); short maxZ = (short) (minZ + width);
byte skyLightTop = skyLight; byte skyLightTop = skyLight;
byte skyLightBot = RenderDataPointUtil.doesDataPointExist(bottomData) ? RenderDataPointUtil.getLightSky(bottomData) : 0; byte skyLightBot = RenderDataPointUtil.doesDataPointExist(bottomData) ? RenderDataPointUtil.getLightSky(bottomData) : 0;
boolean isTransparent = ColorUtil.getAlpha(color) < 255 && LodRenderer.transparencyEnabled; boolean transparencyEnabled = Config.Client.Advanced.Graphics.Quality.transparency.get().transparencyEnabled;
boolean fakeOceanFloor = Config.Client.Advanced.Graphics.Quality.transparency.get().fakeTransparencyEnabled;
boolean isTransparent = ColorUtil.getAlpha(color) < 255 && transparencyEnabled;
boolean overVoid = !RenderDataPointUtil.doesDataPointExist(bottomData); boolean overVoid = !RenderDataPointUtil.doesDataPointExist(bottomData);
boolean isTopTransparent = RenderDataPointUtil.getAlpha(topData) < 255 && LodRenderer.transparencyEnabled; boolean isTopTransparent = RenderDataPointUtil.getAlpha(topData) < 255 && transparencyEnabled;
boolean isBottomTransparent = RenderDataPointUtil.getAlpha(bottomData) < 255 && LodRenderer.transparencyEnabled; boolean isBottomTransparent = RenderDataPointUtil.getAlpha(bottomData) < 255 && transparencyEnabled;
// defaulting to a value far below what we can normally render means we // defaulting to a value far below what we can normally render means we
// don't need to have an additional "is cave culling enabled" check // don't need to have an additional "is cave culling enabled" check
int caveCullingMaxY = Integer.MIN_VALUE; int caveCullingMaxY = Integer.MIN_VALUE;
if (Config.Client.Advanced.Graphics.Culling.enableCaveCulling.get()) if (Config.Client.Advanced.Graphics.Culling.enableCaveCulling.get())
{ {
caveCullingMaxY = Config.Client.Advanced.Graphics.Culling.caveCullingHeight.get() - clientLevel.getMinY(); caveCullingMaxY = Config.Client.Advanced.Graphics.Culling.caveCullingHeight.get() - clientLevel.getLevelWrapper().getMinHeight();
} }
@@ -103,20 +93,20 @@ public class ColumnBox
// fake ocean transparency // fake ocean transparency
if (LodRenderer.transparencyEnabled && LodRenderer.fakeOceanFloor) if (transparencyEnabled && fakeOceanFloor)
{ {
if (!isTransparent && isTopTransparent && RenderDataPointUtil.doesDataPointExist(topData)) if (!isTransparent && isTopTransparent && RenderDataPointUtil.doesDataPointExist(topData))
{ {
skyLightTop = (byte) MathUtil.clamp(0, 15 - (RenderDataPointUtil.getYMax(topData) - minY), 15); skyLightTop = (byte) MathUtil.clamp(0, 15 - (RenderDataPointUtil.getYMax(topData) - minY), 15);
ySize = (short) (RenderDataPointUtil.getYMax(topData) - minY - 1); yHeight = (short) (RenderDataPointUtil.getYMax(topData) - minY - 1);
} }
else if (isTransparent && !isBottomTransparent && RenderDataPointUtil.doesDataPointExist(bottomData)) else if (isTransparent && !isBottomTransparent && RenderDataPointUtil.doesDataPointExist(bottomData))
{ {
minY = (short) (minY + ySize - 1); minY = (short) (minY + yHeight - 1);
ySize = 1; yHeight = 1;
} }
maxY = (short) (minY + ySize); maxY = (short) (minY + yHeight);
} }
@@ -125,16 +115,26 @@ public class ColumnBox
// add top and bottom faces // // add top and bottom faces //
//==========================// //==========================//
boolean skipTop = RenderDataPointUtil.doesDataPointExist(topData) && (RenderDataPointUtil.getYMin(topData) == maxY) && !isTopTransparent; // top face
if (!skipTop)
{ {
builder.addQuadUp(minX, maxY, minZ, xSize, zSize, ColorUtil.applyShade(color, MC.getShade(EDhDirection.UP)), irisBlockMaterialId, skyLightTop, blockLight); boolean skipTop = RenderDataPointUtil.doesDataPointExist(topData)
&& (RenderDataPointUtil.getYMin(topData) == maxY)
&& !isTopTransparent;
if (!skipTop)
{
builder.addQuadUp(minX, maxY, minZ, width, ColorUtil.applyShade(color, MC_RENDER.getShade(EDhDirection.UP)), irisBlockMaterialId, skyLightTop, blockLight);
}
} }
boolean skipBottom = RenderDataPointUtil.doesDataPointExist(bottomData) && (RenderDataPointUtil.getYMax(bottomData) == minY) && !isBottomTransparent; // bottom face
if (!skipBottom)
{ {
builder.addQuadDown(minX, minY, minZ, xSize, zSize, ColorUtil.applyShade(color, MC.getShade(EDhDirection.DOWN)), irisBlockMaterialId, skyLightBot, blockLight); boolean skipBottom = RenderDataPointUtil.doesDataPointExist(bottomData)
&& (RenderDataPointUtil.getYMax(bottomData) == minY)
&& !isBottomTransparent;
if (!skipBottom)
{
builder.addQuadDown(minX, minY, minZ, width, ColorUtil.applyShade(color, MC_RENDER.getShade(EDhDirection.DOWN)), irisBlockMaterialId, skyLightBot, blockLight);
}
} }
@@ -145,258 +145,285 @@ public class ColumnBox
// NORTH face // NORTH face
{ {
ColumnArrayView adjCol = adjData[EDhDirection.NORTH.ordinal() - 2]; // TODO can we use something other than ordinal-2? ColumnRenderView adjCol = adjData[EDhDirection.NORTH.compassIndex];
boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.NORTH.ordinal() - 2]; boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.NORTH.compassIndex];
// if the adjacent column is null that generally means the adjacent area hasn't been generated yet // if the adjacent column is null that generally means the adjacent area hasn't been generated yet
if (adjCol == null) if (adjCol == null)
{ {
// Add an adjacent face if this is opaque face or transparent over the void. // Add an adjacent face if this is opaque face or transparent over the void.
if (!isTransparent || overVoid) if (!isTransparent || overVoid)
{ {
builder.addQuadAdj(EDhDirection.NORTH, minX, minY, minZ, xSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); builder.addQuadAdj(
EDhDirection.NORTH,
minX, minY, minZ,
width, yHeight,
color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight);
} }
} }
else else
{ {
makeAdjVerticalQuad(builder, adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.NORTH, minX, minY, minZ, xSize, ySize, makeAdjVerticalQuad(
builder, phantomArrayCheckout,
adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.NORTH,
minX, minY, minZ, width, yHeight,
color, irisBlockMaterialId, blockLight); color, irisBlockMaterialId, blockLight);
} }
} }
// SOUTH face // SOUTH face
{ {
ColumnArrayView adjCol = adjData[EDhDirection.SOUTH.ordinal() - 2]; ColumnRenderView adjCol = adjData[EDhDirection.SOUTH.compassIndex];
boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.SOUTH.ordinal() - 2]; boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.SOUTH.compassIndex];
if (adjCol == null) if (adjCol == null)
{ {
if (!isTransparent || overVoid) if (!isTransparent || overVoid)
{ {
builder.addQuadAdj(EDhDirection.SOUTH, minX, minY, maxZ, xSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); builder.addQuadAdj(
EDhDirection.SOUTH,
minX, minY, maxZ,
width, yHeight,
color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight);
} }
} }
else else
{ {
makeAdjVerticalQuad(builder, adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.SOUTH, minX, minY, maxZ, xSize, ySize, makeAdjVerticalQuad(
builder, phantomArrayCheckout,
adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.SOUTH,
minX, minY, maxZ, width, yHeight,
color, irisBlockMaterialId, blockLight); color, irisBlockMaterialId, blockLight);
} }
} }
// WEST face // WEST face
{ {
ColumnArrayView adjCol = adjData[EDhDirection.WEST.ordinal() - 2]; ColumnRenderView adjCol = adjData[EDhDirection.WEST.compassIndex];
boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.WEST.ordinal() - 2]; boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.WEST.compassIndex];
if (adjCol == null) if (adjCol == null)
{ {
if (!isTransparent || overVoid) if (!isTransparent || overVoid)
{ {
builder.addQuadAdj(EDhDirection.WEST, minX, minY, minZ, zSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); builder.addQuadAdj(
EDhDirection.WEST,
minX, minY, minZ,
width, yHeight,
color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight);
} }
} }
else else
{ {
makeAdjVerticalQuad(builder, adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.WEST, minX, minY, minZ, zSize, ySize, makeAdjVerticalQuad(
builder, phantomArrayCheckout,
adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.WEST,
minX, minY, minZ, width, yHeight,
color, irisBlockMaterialId, blockLight); color, irisBlockMaterialId, blockLight);
} }
} }
// EAST face // EAST face
{ {
ColumnArrayView adjCol = adjData[EDhDirection.EAST.ordinal() - 2]; ColumnRenderView adjCol = adjData[EDhDirection.EAST.compassIndex];
boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.EAST.ordinal() - 2]; boolean adjSameDetailLevel = isAdjDataSameDetailLevel[EDhDirection.EAST.compassIndex];
if (adjCol == null) if (adjCol == null)
{ {
if (!isTransparent || overVoid) if (!isTransparent || overVoid)
{ {
builder.addQuadAdj(EDhDirection.EAST, maxX, minY, minZ, zSize, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); builder.addQuadAdj(
EDhDirection.EAST,
maxX, minY, minZ,
width, yHeight,
color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight);
} }
} }
else else
{ {
makeAdjVerticalQuad(builder, adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.EAST, maxX, minY, minZ, zSize, ySize, makeAdjVerticalQuad(
builder, phantomArrayCheckout,
adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.EAST,
maxX, minY, minZ, width, yHeight,
color, irisBlockMaterialId, blockLight); color, irisBlockMaterialId, blockLight);
} }
} }
} }
private static void makeAdjVerticalQuad( private static void makeAdjVerticalQuad(
LodQuadBuilder builder, @NotNull ColumnArrayView adjColumnView, boolean adjacentIsSameDetailLevel, int caveCullingMaxY, EDhDirection direction, LodQuadBuilder builder, PhantomArrayListCheckout phantomArrayCheckout,
short x, short yMin, short z, short horizontalWidth, short ySize, @NotNull ColumnRenderView adjColumnView, boolean adjacentIsSameDetailLevel, int caveCullingMaxY, EDhDirection direction,
int color, byte irisBlockMaterialId, byte blockLight) short x, short yMin, short z, short horizontalWidth, short ySize,
int color, byte irisBlockMaterialId, byte blockLight)
{ {
// pooled arrays
LongArrayList segments = phantomArrayCheckout.getLongArray(0, 0);
LongArrayList newSegments = phantomArrayCheckout.getLongArray(1, 0);
//==================// //==================//
// create face with // // create face with //
// no adjacent data // // no adjacent data //
//==================// //==================//
color = ColorUtil.applyShade(color, MC.getShade(direction)); color = ColorUtil.applyShade(color, MC_RENDER.getShade(direction));
// if there isn't any data adjacent to this LOD, if (adjColumnView.size == 0
// just add the full vertical quad || RenderDataPointUtil.hasZeroHeight(adjColumnView.get(0)))
if (adjColumnView.size == 0 || RenderDataPointUtil.isVoid(adjColumnView.get(0)))
{ {
builder.addQuadAdj(direction, x, yMin, z, horizontalWidth, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight); builder.addQuadAdj(direction, x, yMin, z, horizontalWidth, ySize, color, irisBlockMaterialId, LodUtil.MAX_MC_LIGHT, blockLight);
return; return;
} }
//===========================// //=================================//
// Determine face visibility // // determine face visibility/light //
// based on it's neighbors // //=================================//
//===========================//
short yMax = (short) (yMin + ySize); // min is inclusive, max is exclusive boolean transparencyEnabled = Config.Client.Advanced.Graphics.Quality.transparency.get().transparencyEnabled;
byte[] skyLightAtInputPos = THREAD_LOCAL_SKY_LIGHT_ARRAY.get(); boolean inputTransparent = ColorUtil.getAlpha(color) < 255 && transparencyEnabled;
short yMax = (short) (yMin + ySize);
try
int adjCount = adjColumnView.size;
// Start with the entire range at max light
segments.add(YSegmentUtil.encode(yMin, yMax, LodUtil.MAX_MC_LIGHT));
// Process each adjacent datapoint and split segments as needed
for (int adjIndex = 0; adjIndex < adjCount; adjIndex++)
{ {
// set the initial sky-lights for this face, long adjPoint = adjColumnView.get(adjIndex);
// if nothing overlaps or overhangs the face should have max sky light short adjMinY = RenderDataPointUtil.getYMin(adjPoint);
Arrays.fill(skyLightAtInputPos, yMin, yMax, LodUtil.MAX_MC_LIGHT); short adjMaxY = RenderDataPointUtil.getYMax(adjPoint);
// iterate top down // skip empty adjacent points
int adjCount = adjColumnView.size(); // or points below this one
for (int adjIndex = 0; adjIndex < adjCount; adjIndex++) if (!RenderDataPointUtil.doesDataPointExist(adjPoint)
|| RenderDataPointUtil.hasZeroHeight(adjPoint)
|| yMax <= adjMinY)
{ {
long adjPoint = adjColumnView.get(adjIndex); continue;
short adjMinY = RenderDataPointUtil.getYMin(adjPoint);
short adjMaxY = RenderDataPointUtil.getYMax(adjPoint);
// skip empty adjacent datapoints
if (!RenderDataPointUtil.doesDataPointExist(adjPoint)
|| RenderDataPointUtil.isVoid(adjPoint))
{
continue;
}
// skip this adjacent datapoint if it's above the input datapoint (since it can't affect the input data point)
if (yMax <= adjMinY)
{
continue;
}
long adjAbovePoint = (adjIndex != 0) ? adjColumnView.get(adjIndex - 1) : RenderDataPointUtil.EMPTY_DATA;
long adjBelowPoint = (adjIndex + 1 < adjCount) ? adjColumnView.get(adjIndex + 1) : RenderDataPointUtil.EMPTY_DATA;
// if the adjacent data point is over the void
// don't consider it as transparent
boolean adjOverVoid = !RenderDataPointUtil.doesDataPointExist(adjBelowPoint);
boolean adjTransparent = !adjOverVoid && RenderDataPointUtil.getAlpha(adjPoint) < 255 && LodRenderer.transparencyEnabled;
//=================================//
// set sky light based on adjacent //
//=================================//
// set light based on overlapping adjacent
if (!adjTransparent)
{
// adj opaque
// mark positions adjacent is covering
byte adjSkyLight = RenderDataPointUtil.getLightSky(adjPoint);
for (int i = adjMinY; i < adjMaxY; i++)
{
byte skyLightAtPos = skyLightAtInputPos[i];
// if the adjacent is a different detail level, we want to render adjacent opaque
// faces to try and reduce the chance of holes on detail level borders
boolean adjacentCoversThis =
// if the adjacent is the same detail level, no special handling is necessary
!adjacentIsSameDetailLevel
// if the adjacent face is underground we probably don't need it
&& RenderDataPointUtil.getYMax(adjPoint) >= caveCullingMaxY
// check if this face is on a border
&&
(
(x == 0 && direction == EDhDirection.WEST)
|| (z == 0 && direction == EDhDirection.NORTH)
// TODO why does 256 represent a border? aren't LODs only 64 datapoints wide?
|| (x == 256 && direction == EDhDirection.EAST)
|| (z == 256 && direction == EDhDirection.SOUTH)
);
byte newSkyLightAtPos = adjacentCoversThis ? adjSkyLight : SKYLIGHT_COVERED;
skyLightAtInputPos[i] = (byte) Math.min(newSkyLightAtPos, skyLightAtPos);
}
}
else
{
// adjacent is transparent,
// use datapoint below adjacent for lighting
byte belowSkyLight = RenderDataPointUtil.getLightSky(adjBelowPoint);
for (int i = adjMinY; i < adjMaxY; i++)
{
byte skyLightAtPos = skyLightAtInputPos[i];
skyLightAtInputPos[i] = (byte) Math.min(belowSkyLight, skyLightAtPos);
}
}
// fill in sky light up to the next DP,
// this is done to handle overhangs
byte adjSkyLight = RenderDataPointUtil.getLightSky(adjPoint);
int adjAboveMinY = RenderDataPointUtil.getYMin(adjAbovePoint);
for (int i = adjMaxY; i < adjAboveMinY; i++)
{
byte skyLightAtPos = skyLightAtInputPos[i];
skyLightAtInputPos[i] = (byte) Math.min(adjSkyLight, skyLightAtPos);
}
} }
long adjAbovePoint = (adjIndex != 0) ? adjColumnView.get(adjIndex - 1) : RenderDataPointUtil.EMPTY_DATA;
long adjBelowPoint = (adjIndex + 1 < adjCount) ? adjColumnView.get(adjIndex + 1) : RenderDataPointUtil.EMPTY_DATA;
//=======================// boolean adjOverVoid = !RenderDataPointUtil.doesDataPointExist(adjBelowPoint);
// create vertical faces // boolean adjTransparent =
//=======================// !adjOverVoid
&& RenderDataPointUtil.getAlpha(adjPoint) < 255
&& transparencyEnabled;
boolean inputTransparent = ColorUtil.getAlpha(color) < 255 && LodRenderer.transparencyEnabled; byte adjSkyLight = RenderDataPointUtil.getLightSky(adjPoint);
byte lastSkyLight = skyLightAtInputPos[yMin]; byte lightToApply;
int quadBottomY = yMin;
int quadTopY = -1;
// walk up the sky lights and create a new face if (!adjTransparent)
// whenever the light changes to different valid value
for (int i = yMin; i < yMax; i++)
{ {
byte skyLight = skyLightAtInputPos[i]; // Adjacent is opaque
if (skyLight != lastSkyLight) boolean adjacentCoversThis =
{ !adjacentIsSameDetailLevel
// the sky light changed, create the in-progress face && RenderDataPointUtil.getYMax(adjPoint) >= caveCullingMaxY
tryAddVerticalFaceWithSkyLightToBuilder( &&
builder, direction, (
x, z, horizontalWidth, (x == 0 && direction == EDhDirection.WEST)
color, irisBlockMaterialId, blockLight, || (z == 0 && direction == EDhDirection.NORTH)
lastSkyLight, inputTransparent, quadTopY, quadBottomY || (x == 256 && direction == EDhDirection.EAST)
); || (z == 256 && direction == EDhDirection.SOUTH)
);
lastSkyLight = skyLight;
quadBottomY = i;
}
quadTopY = (i + 1); lightToApply = adjacentCoversThis ? adjSkyLight : SKYLIGHT_COVERED;
}
else
{
// Adjacent is transparent, use below light
lightToApply = RenderDataPointUtil.getLightSky(adjBelowPoint);
} }
// add the in-progress face if present
if (quadTopY != -1) // Apply light to the range [adjMinY, adjMaxY)
applyLightToRange(segments, newSegments, adjMinY, adjMaxY, lightToApply);
// Fill overhang area [adjMaxY, adjAboveMinY) with adjSkyLight
short adjAboveMinY = RenderDataPointUtil.getYMin(adjAbovePoint);
if (adjMaxY < adjAboveMinY)
{ {
tryAddVerticalFaceWithSkyLightToBuilder( applyLightToRange(segments, newSegments, adjMaxY, adjAboveMinY, adjSkyLight);
builder, direction,
x, z, horizontalWidth,
color, irisBlockMaterialId, blockLight,
lastSkyLight, inputTransparent, quadTopY, quadBottomY
);
} }
} }
finally
//=======================//
// Create vertical faces //
// from segments //
//=======================//
for (int i = 0; i < segments.size(); i++)
{ {
// clean up the array before the next thread uses it long segment = segments.getLong(i);
// (may be unnecessary since we only work between the yMin-yMax anyway, but is helpful for debugging) tryAddVerticalFaceWithSkyLightToBuilder(
Arrays.fill(skyLightAtInputPos, yMin, yMax, SKYLIGHT_EMPTY); builder, direction,
x, z, horizontalWidth,
color, irisBlockMaterialId, blockLight,
YSegmentUtil.getSkyLight(segment), inputTransparent, YSegmentUtil.getEndY(segment), YSegmentUtil.getStartY(segment)
);
} }
} }
/**
* Apply the new light value over the given y range,
* splitting segments as needed
* <p>
* source: claude.ai
*/
private static void applyLightToRange(
LongArrayList segments, LongArrayList newSegments,
short rangeStart, short rangeEnd,
byte newLight)
{
// clear the pooled array that the new segments will go into
newSegments.clear();
for (int i = 0; i < segments.size(); i++)
{
long seg = segments.getLong(i);
short endY = YSegmentUtil.getEndY(seg);
short startY = YSegmentUtil.getStartY(seg);
byte skyLight = YSegmentUtil.getSkyLight(seg);
// No overlap
if (endY <= rangeStart
|| startY >= rangeEnd)
{
newSegments.add(seg);
continue;
}
// Partial or complete overlap - need to split
// Part before the range
if (startY < rangeStart)
{
newSegments.add(YSegmentUtil.encode(startY, rangeStart, skyLight));
}
// Overlapping part - take minimum light
short overlapStart = (short)Math.max(startY, rangeStart);
short overlapEnd = (short)Math.min(endY, rangeEnd);
byte minLight = (byte) Math.min(newLight, skyLight);
newSegments.add(YSegmentUtil.encode(overlapStart, overlapEnd, minLight));
// Part after the range
if (endY > rangeEnd)
{
newSegments.add(YSegmentUtil.encode(rangeEnd, endY, skyLight));
}
}
segments.clear();
segments.addAll(newSegments);
}
private static void tryAddVerticalFaceWithSkyLightToBuilder( private static void tryAddVerticalFaceWithSkyLightToBuilder(
LodQuadBuilder builder, EDhDirection direction, LodQuadBuilder builder, EDhDirection direction,
short x, short z, short horizontalWidth, short x, short z, short horizontalWidth,
@@ -405,22 +432,72 @@ public class ColumnBox
) )
{ {
// invalid positions will have a negative skylight // invalid positions will have a negative skylight
if (lastSkyLight >= 0) if (lastSkyLight < 0)
{ {
// Don't add transparent vertical faces return;
// unless the adjacent position is empty.
// This is done to prevent walls between water blocks in the ocean.
if (!inputTransparent
|| (lastSkyLight == LodUtil.MAX_MC_LIGHT))
{
// don't add negative/empty height faces
short height = (short) (quadTopY - quadBottomY);
if (height > 0)
{
builder.addQuadAdj(direction, x, (short) quadBottomY, z, horizontalWidth, height, color, irisBlockMaterialId, lastSkyLight, blockLight);
}
}
} }
// Don't add transparent vertical faces
// unless the adjacent position is empty.
// This is done to prevent walls between water blocks in the ocean.
if (inputTransparent
&& (lastSkyLight != LodUtil.MAX_MC_LIGHT))
{
return;
}
// don't add negative/empty height faces
short height = (short) (quadTopY - quadBottomY);
if (height <= 0)
{
return;
}
builder.addQuadAdj(
direction,
x, (short) quadBottomY, z,
horizontalWidth, height,
color, irisBlockMaterialId, lastSkyLight, blockLight);
}
//================//
// helper classes //
//================//
/**
* encodes height/light data into a long
* to reduce object allocations.
*/
private static class YSegmentUtil
{
private static final int HEIGHT_WIDTH = Short.SIZE;
private static final int SKY_LIGHT_WIDTH = Byte.SIZE;
private static final int START_Y_MASK = (int) Math.pow(2, HEIGHT_WIDTH) - 1;
private static final int END_Y_MASK = (int) Math.pow(2, HEIGHT_WIDTH) - 1;
private static final int SKY_LIGHT_MASK = (int) Math.pow(2, SKY_LIGHT_WIDTH) - 1;
private static final int START_Y_OFFSET = 0;
private static final int END_Y_OFFSET = START_Y_OFFSET + HEIGHT_WIDTH;
private static final int SKY_LIGHT_OFFSET = END_Y_OFFSET + HEIGHT_WIDTH;
public static long encode(short startY, short endY, byte skyLight)
{
long data = 0L;
data |= (long) (startY & START_Y_MASK) << START_Y_OFFSET;
data |= (long) (endY & END_Y_MASK) << END_Y_OFFSET;
data |= (long) (skyLight & SKY_LIGHT_MASK) << SKY_LIGHT_OFFSET;
return data;
}
public static short getStartY(long data) { return (short) ((data >> START_Y_OFFSET) & START_Y_MASK); }
public static short getEndY(long data) { return (short) ((data >> END_Y_OFFSET) & END_Y_MASK); }
public static byte getSkyLight(long data) { return (byte) ((data >> SKY_LIGHT_OFFSET) & SKY_LIGHT_MASK); }
} }
@@ -1,376 +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.dataObjects.render.bufferBuilding;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.render.glObject.buffer.GLVertexBuffer;
import com.seibel.distanthorizons.core.render.renderer.LodRenderer;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.objects.StatsMap;
import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod;
import org.apache.logging.log4j.Logger;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.concurrent.*;
/**
* Java representation of one or more OpenGL buffers for rendering.
*
* @see ColumnRenderBufferBuilder
*/
public class ColumnRenderBuffer implements AutoCloseable
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
/** number of bytes a single quad takes */
public static final int QUADS_BYTE_SIZE = LodUtil.LOD_VERTEX_FORMAT.getByteSize() * 4;
/** how big a single VBO can be in bytes */
public static final int MAX_VBO_BYTE_SIZE = 10 * 1024 * 1024; // 10 MB
public static final int MAX_QUADS_PER_BUFFER = MAX_VBO_BYTE_SIZE / QUADS_BYTE_SIZE;
public static final int FULL_SIZED_BUFFER = MAX_QUADS_PER_BUFFER * QUADS_BYTE_SIZE;
public final DhBlockPos blockPos;
public boolean buffersUploaded = false;
private GLVertexBuffer[] vbos;
private GLVertexBuffer[] vbosTransparent;
private CompletableFuture<ColumnRenderBuffer> uploadFuture = null;
//==============//
// constructors //
//==============//
public ColumnRenderBuffer(DhBlockPos blockPos)
{
this.blockPos = blockPos;
this.vbos = new GLVertexBuffer[0];
this.vbosTransparent = new GLVertexBuffer[0];
}
//==================//
// buffer uploading //
//==================//
/** Should be run on a DH thread. */
public synchronized CompletableFuture<ColumnRenderBuffer> makeAndUploadBuffersAsync(LodQuadBuilder builder, EDhApiGpuUploadMethod gpuUploadMethod)
{
// separate variable to prevent race condition when checking null
CompletableFuture<ColumnRenderBuffer> future = this.uploadFuture;
if (future != null)
{
// upload already in process
return future;
}
// new upload needed
future = new CompletableFuture<>();
this.uploadFuture = future;
// make the buffers
ArrayList<ByteBuffer> opaqueBuffers = builder.makeOpaqueVertexBuffers();
ArrayList<ByteBuffer> transparentBuffers = builder.makeTransparentVertexBuffers();
this.vbos = resizeBuffer(this.vbos, opaqueBuffers.size());
this.vbosTransparent = resizeBuffer(this.vbosTransparent, transparentBuffers.size());
// upload on MC's render thread
GLProxy.getInstance().queueRunningOnRenderThread(() ->
{
try
{
// skip this event if requested
if (Thread.interrupted() || this.uploadFuture.isCancelled())
{
throw new InterruptedException();
}
// upload on the render thread
uploadBuffersDirect(this.vbos, opaqueBuffers, gpuUploadMethod);
uploadBuffersDirect(this.vbosTransparent, transparentBuffers, gpuUploadMethod);
this.buffersUploaded = true;
// success
this.uploadFuture.complete(this);
this.uploadFuture = null;
}
catch (InterruptedException ignore)
{
this.uploadFuture.complete(this);
this.uploadFuture = null;
}
catch (Exception e)
{
LOGGER.error("Unexpected issue uploading buffer ["+this.blockPos +"], error: ["+e.getMessage()+"].", e);
this.uploadFuture.completeExceptionally(e);
this.uploadFuture = null;
}
finally
{
// all the buffers must be manually freed to prevent memory leaks
for (ByteBuffer buffer : opaqueBuffers)
{
MemoryUtil.memFree(buffer);
}
for (ByteBuffer buffer : transparentBuffers)
{
MemoryUtil.memFree(buffer);
}
}
});
return future;
}
private static GLVertexBuffer[] resizeBuffer(GLVertexBuffer[] vbos, int newSize)
{
if (vbos.length == newSize)
{
return vbos;
}
GLVertexBuffer[] newVbos = new GLVertexBuffer[newSize];
System.arraycopy(vbos, 0, newVbos, 0, Math.min(vbos.length, newSize));
if (newSize < vbos.length)
{
for (int i = newSize; i < vbos.length; i++)
{
if (vbos[i] != null)
{
vbos[i].close();
}
}
}
return newVbos;
}
private static void uploadBuffersDirect(GLVertexBuffer[] vbos, ArrayList<ByteBuffer> byteBuffers, EDhApiGpuUploadMethod method) throws InterruptedException
{
int vboIndex = 0;
for (int i = 0; i < byteBuffers.size(); i++)
{
if (vboIndex >= vbos.length)
{
throw new RuntimeException("Too many vertex buffers!!");
}
// get or create the VBO
if (vbos[vboIndex] == null)
{
vbos[vboIndex] = new GLVertexBuffer(method.useBufferStorage);
}
GLVertexBuffer vbo = vbos[vboIndex];
ByteBuffer buffer = byteBuffers.get(i);
int size = buffer.limit() - buffer.position();
try
{
vbo.bind();
vbo.uploadBuffer(buffer, size / LodUtil.LOD_VERTEX_FORMAT.getByteSize(), method, FULL_SIZED_BUFFER);
}
catch (Exception e)
{
vbos[vboIndex] = null;
vbo.close();
LOGGER.error("Failed to upload buffer: ", e);
}
vboIndex++;
}
if (vboIndex < vbos.length)
{
throw new RuntimeException("Too few vertex buffers!!");
}
}
//========//
// render //
//========//
/** @return true if something was rendered, false otherwise */
public boolean renderOpaque(LodRenderer renderContext, DhApiRenderParam renderEventParam)
{
boolean hasRendered = false;
renderContext.setModelViewMatrixOffset(this.blockPos, renderEventParam);
for (GLVertexBuffer vbo : this.vbos)
{
if (vbo == null)
{
continue;
}
if (vbo.getVertexCount() == 0)
{
continue;
}
hasRendered = true;
renderContext.drawVbo(vbo, this);
}
return hasRendered;
}
/** @return true if something was rendered, false otherwise */
public boolean renderTransparent(LodRenderer renderContext, DhApiRenderParam renderEventParam)
{
boolean hasRendered = false;
try
{
// can throw an IllegalStateException if the GL program was freed before it should've been
renderContext.setModelViewMatrixOffset(this.blockPos, renderEventParam);
for (GLVertexBuffer vbo : this.vbosTransparent)
{
if (vbo == null)
{
continue;
}
if (vbo.getVertexCount() == 0)
{
continue;
}
hasRendered = true;
renderContext.drawVbo(vbo, this);
}
}
catch (IllegalStateException e)
{
LOGGER.error("renderContext program doesn't exist for pos: "+this.blockPos, e);
}
return hasRendered;
}
//================//
// helper methods //
//================//
/** can be used when debugging */
public boolean hasNonNullVbos() { return this.vbos != null || this.vbosTransparent != null; }
/** can be used when debugging */
public int vboBufferCount()
{
int count = 0;
if (this.vbos != null)
{
count += this.vbos.length;
}
if (this.vbosTransparent != null)
{
count += this.vbosTransparent.length;
}
return count;
}
public boolean uploadInProgress() { return this.uploadFuture != null; }
public void debugDumpStats(StatsMap statsMap)
{
statsMap.incStat("RenderBuffers");
statsMap.incStat("SimpleRenderBuffers");
for (GLVertexBuffer vertexBuffer : vbos)
{
if (vertexBuffer != null)
{
statsMap.incStat("VBOs");
if (vertexBuffer.getSize() == FULL_SIZED_BUFFER)
{
statsMap.incStat("FullsizedVBOs");
}
if (vertexBuffer.getSize() == 0)
{
GLProxy.GL_LOGGER.warn("VBO with size 0");
}
statsMap.incBytesStat("TotalUsage", vertexBuffer.getSize());
}
}
}
//================//
// base overrides //
//================//
/**
* This method is called when object is no longer in use.
* Called either after uploadBuffers() returned false (On buffer Upload
* thread), or by others when the object is not being used. (not in build,
* upload, or render state).
*/
@Override
public void close()
{
this.buffersUploaded = false;
GLProxy.getInstance().queueRunningOnRenderThread(() ->
{
for (GLVertexBuffer buffer : this.vbos)
{
if (buffer != null)
{
buffer.destroyAsync();
}
}
for (GLVertexBuffer buffer : this.vbosTransparent)
{
if (buffer != null)
{
buffer.destroyAsync();
}
}
});
}
}
@@ -25,20 +25,16 @@ import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource; import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource;
import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.util.objects.pooling.PhantomArrayListCheckout;
import com.seibel.distanthorizons.core.util.objects.pooling.PhantomArrayListPool;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.util.ColorUtil; import com.seibel.distanthorizons.core.util.ColorUtil;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.RenderDataPointUtil; import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
import com.seibel.distanthorizons.core.util.objects.UncheckedInterruptedException; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnRenderView;
import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -49,9 +45,9 @@ import java.util.concurrent.CompletableFuture;
*/ */
public class ColumnRenderBufferBuilder public class ColumnRenderBufferBuilder
{ {
public static final ConfigBasedLogger EVENT_LOGGER = new ConfigBasedLogger(LogManager.getLogger(), private static final DhLogger LOGGER = new DhLoggerBuilder().build();
() -> Config.Common.Logging.logRendererBufferEvent.get());
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Column Buffer Builder");
@@ -60,14 +56,15 @@ public class ColumnRenderBufferBuilder
//==============// //==============//
/** @link adjData should be null for adjacent sections that cross detail level boundaries */ /** @link adjData should be null for adjacent sections that cross detail level boundaries */
public static CompletableFuture<ColumnRenderBuffer> uploadBuffersAsync( public static CompletableFuture<LodBufferContainer> uploadBuffersAsync(
IDhClientLevel clientLevel, IDhClientLevel clientLevel,
long pos, long pos,
LodQuadBuilder quadBuilder LodQuadBuilder quadBuilder
) )
{ {
ColumnRenderBuffer buffer = new ColumnRenderBuffer(new DhBlockPos(DhSectionPos.getMinCornerBlockX(pos), clientLevel.getMinY(), DhSectionPos.getMinCornerBlockZ(pos))); DhBlockPos minBlockPos = new DhBlockPos(DhSectionPos.getMinCornerBlockX(pos), clientLevel.getLevelWrapper().getMinHeight(), DhSectionPos.getMinCornerBlockZ(pos));
CompletableFuture<ColumnRenderBuffer> uploadFuture = buffer.makeAndUploadBuffersAsync(quadBuilder, GLProxy.getInstance().getGpuUploadMethod()); LodBufferContainer bufferContainer = new LodBufferContainer(pos, minBlockPos);
CompletableFuture<LodBufferContainer> uploadFuture = bufferContainer.makeAndUploadBuffersAsync(quadBuilder);
uploadFuture.whenComplete((uploadedBuffer, exception) -> uploadFuture.whenComplete((uploadedBuffer, exception) ->
{ {
// clean up if not uploaded // clean up if not uploaded
@@ -109,208 +106,223 @@ public class ColumnRenderBufferBuilder
// build each column // // build each column //
//===================// //===================//
byte thisDetailLevel = renderSource.getDataDetailLevel(); // pooled arrays for ColumnBox use
for (int relX = 0; relX < ColumnRenderSource.SECTION_SIZE; relX++) try (PhantomArrayListCheckout phantomArrayCheckout = ARRAY_LIST_POOL.checkoutLongArrays(2);
ColumnRenderView columnRenderData = ColumnRenderView.getPooled();
ColumnRenderView northAdjView = ColumnRenderView.getPooled();
ColumnRenderView southAdjView = ColumnRenderView.getPooled();
ColumnRenderView eastAdjView = ColumnRenderView.getPooled();
ColumnRenderView westAdjView = ColumnRenderView.getPooled())
{ {
for (int relZ = 0; relZ < ColumnRenderSource.SECTION_SIZE; relZ++) ColumnRenderView[] adjColumnViews = new ColumnRenderView[EDhDirection.CARDINAL_COMPASS.length];
adjColumnViews[EDhDirection.NORTH.compassIndex] = northAdjView;
adjColumnViews[EDhDirection.SOUTH.compassIndex] = southAdjView;
adjColumnViews[EDhDirection.EAST.compassIndex] = eastAdjView;
adjColumnViews[EDhDirection.WEST.compassIndex] = westAdjView;
byte thisDetailLevel = renderSource.getDataDetailLevel();
for (int relX = 0; relX < ColumnRenderSource.WIDTH; relX++)
{ {
// stop the builder if requested for (int relZ = 0; relZ < ColumnRenderSource.WIDTH; relZ++)
UncheckedInterruptedException.throwIfInterrupted(); {
renderSource.populateColumnView(columnRenderData, relX, relZ);
// ignore empty/null columns
ColumnArrayView columnRenderData = renderSource.getVerticalDataPointView(relX, relZ); // ignore empty columns
if (columnRenderData.size() == 0 if (columnRenderData.size == 0
|| !RenderDataPointUtil.doesDataPointExist(columnRenderData.get(0)) || !RenderDataPointUtil.doesDataPointExist(columnRenderData.get(0))
|| RenderDataPointUtil.isVoid(columnRenderData.get(0))) || RenderDataPointUtil.hasZeroHeight(columnRenderData.get(0)))
{
continue;
}
//=============//
// debug limit //
//=============//
// can be used to limit the buffer building to a specific relative position.
// useful for debugging a single column
if (columnBuilderDebugEnabled)
{
int wantedX = Config.Client.Advanced.Debugging.ColumnBuilderDebugging.columnBuilderDebugXRow.get();
if (wantedX >= 0 && relX != wantedX)
{ {
continue; continue;
} }
int wantedZ = Config.Client.Advanced.Debugging.ColumnBuilderDebugging.columnBuilderDebugZRow.get();
if (wantedZ >= 0 && relZ != wantedZ)
//=============//
// debug limit //
//=============//
// can be used to limit the buffer building to a specific relative position.
// useful for debugging a single column
if (columnBuilderDebugEnabled)
{ {
continue; int wantedX = Config.Client.Advanced.Debugging.ColumnBuilderDebugging.columnBuilderDebugXRow.get();
} if (wantedX >= 0 && relX != wantedX)
}
//==================================//
// get adjacent render data columns //
//==================================//
ColumnArrayView[] adjColumnViews = new ColumnArrayView[EDhDirection.ADJ_DIRECTIONS.length];
for (EDhDirection lodDirection : EDhDirection.ADJ_DIRECTIONS)
{
try
{
int xAdj = relX + lodDirection.getNormal().x;
int zAdj = relZ + lodDirection.getNormal().z;
boolean isCrossRenderSourceBoundary =
(xAdj < 0 || xAdj >= ColumnRenderSource.SECTION_SIZE) ||
(zAdj < 0 || zAdj >= ColumnRenderSource.SECTION_SIZE);
ColumnRenderSource adjRenderSource;
byte adjDetailLevel;
//=========================//
// get the adjacent render //
// source if present //
//=========================//
if (!isCrossRenderSourceBoundary)
{ {
// the adjacent position is inside this same render source continue;
adjRenderSource = renderSource;
adjDetailLevel = thisDetailLevel;
} }
else int wantedZ = Config.Client.Advanced.Debugging.ColumnBuilderDebugging.columnBuilderDebugZRow.get();
{ if (wantedZ >= 0 && relZ != wantedZ)
// the adjacent position is outside this render source
// skip empty sections
adjRenderSource = adjRegions[lodDirection.ordinal() - 2];
if (adjRenderSource == null)
{
continue;
}
adjDetailLevel = adjRenderSource.getDataDetailLevel();
if (adjDetailLevel == thisDetailLevel)
{
// if the adjacent position is outside this render source,
// wrap the position around so it's inside the adjacent source
if (xAdj < 0)
{
xAdj += ColumnRenderSource.SECTION_SIZE;
}
if (xAdj >= ColumnRenderSource.SECTION_SIZE)
{
xAdj -= ColumnRenderSource.SECTION_SIZE;
}
if (zAdj < 0)
{
zAdj += ColumnRenderSource.SECTION_SIZE;
}
if (zAdj >= ColumnRenderSource.SECTION_SIZE)
{
zAdj -= ColumnRenderSource.SECTION_SIZE;
}
}
}
//========================//
// get the adjacent views //
//========================//
// the old logic handled additional cases, but they never appeared to fire,
// so just these two cases should be fine
boolean expectedDetailLevels = (adjDetailLevel == thisDetailLevel) || (adjDetailLevel > thisDetailLevel);
if (!expectedDetailLevels)
{
LodUtil.assertNotReach("Mismatch between adjacent detail level ["+adjDetailLevel+"] and this render source's detail level ["+thisDetailLevel+"]. Detail levels should be adj >= this.");
}
adjColumnViews[lodDirection.ordinal() - 2] = adjRenderSource.getVerticalDataPointView(xAdj, zAdj);
}
catch (RuntimeException e)
{
EVENT_LOGGER.warn("Failed to get adj data for relative pos: [" + thisDetailLevel + ":" + relX + "," + relZ + "] at [" + lodDirection + "], Error: "+e.getMessage(), e);
}
} // for adjacent directions
//==========================//
// build this render column //
//==========================//
ColumnRenderSource.DebugSourceFlag debugSourceFlag = renderSource.debugGetFlag(relX, relZ);
// We render every vertical lod present in this position
// We only stop when we find a block that is void or non-existing block
for (int i = 0; i < columnRenderData.size(); i++)
{
// can be uncommented to limit which vertical LOD is generated
if (Config.Client.Advanced.Debugging.ColumnBuilderDebugging.columnBuilderDebugEnable.get())
{
int wantedColumnIndex = Config.Client.Advanced.Debugging.ColumnBuilderDebugging.columnBuilderDebugColumnIndex.get();
if (wantedColumnIndex >= 0 && i != wantedColumnIndex)
{ {
continue; continue;
} }
} }
long data = columnRenderData.get(i);
// If the data is not render-able (Void or non-existing) we stop since there is
// no data left in this position //==================================//
if (RenderDataPointUtil.isVoid(data) || !RenderDataPointUtil.doesDataPointExist(data)) // get adjacent render data columns //
//==================================//
// clear the old data so we can handle if one of the adjacent columns is missing/empty
adjColumnViews[EDhDirection.NORTH.compassIndex].clear();
adjColumnViews[EDhDirection.SOUTH.compassIndex].clear();
adjColumnViews[EDhDirection.EAST.compassIndex].clear();
adjColumnViews[EDhDirection.WEST.compassIndex].clear();
for (EDhDirection direction : EDhDirection.CARDINAL_COMPASS)
{ {
break; try
} {
int xAdj = relX + direction.normal.x;
int zAdj = relZ + direction.normal.z;
boolean isCrossRenderSourceBoundary =
(xAdj < 0 || xAdj >= ColumnRenderSource.WIDTH)
|| (zAdj < 0 || zAdj >= ColumnRenderSource.WIDTH);
ColumnRenderSource adjRenderSource;
byte adjDetailLevel;
//=========================//
// get the adjacent render //
// source if present //
//=========================//
if (!isCrossRenderSourceBoundary)
{
// the adjacent position is inside this same render source
adjRenderSource = renderSource;
adjDetailLevel = thisDetailLevel;
}
else
{
// the adjacent position is outside this render source
// skip empty sections
adjRenderSource = adjRegions[direction.compassIndex];
if (adjRenderSource == null)
{
continue;
}
adjDetailLevel = adjRenderSource.getDataDetailLevel();
if (adjDetailLevel == thisDetailLevel)
{
// if the adjacent position is outside this render source,
// wrap the position around so it's inside the adjacent source
if (xAdj < 0)
{
xAdj += ColumnRenderSource.WIDTH;
}
if (xAdj >= ColumnRenderSource.WIDTH)
{
xAdj -= ColumnRenderSource.WIDTH;
}
if (zAdj < 0)
{
zAdj += ColumnRenderSource.WIDTH;
}
if (zAdj >= ColumnRenderSource.WIDTH)
{
zAdj -= ColumnRenderSource.WIDTH;
}
}
}
//========================//
// get the adjacent views //
//========================//
// the old logic handled additional cases, but they never appeared to fire,
// so just these two cases should be fine
boolean expectedDetailLevels = (adjDetailLevel == thisDetailLevel) || (adjDetailLevel > thisDetailLevel);
if (!expectedDetailLevels)
{
LodUtil.assertNotReach("Mismatch between adjacent detail level ["+adjDetailLevel+"] and this render source's detail level ["+thisDetailLevel+"]. Detail levels should be adj >= this.");
}
adjRenderSource.populateColumnView(adjColumnViews[direction.compassIndex], xAdj, zAdj);
}
catch (RuntimeException e)
{
LOGGER.warn("Failed to get adj data for relative pos: [" + thisDetailLevel + ":" + relX + "," + relZ + "] at [" + direction + "], Error: [" + e.getMessage() + "].", e);
}
} // for adjacent directions
long topDataPoint = (i - 1) >= 0 ? columnRenderData.get(i - 1) : RenderDataPointUtil.EMPTY_DATA;
long bottomDataPoint = (i + 1) < columnRenderData.size() ? columnRenderData.get(i + 1) : RenderDataPointUtil.EMPTY_DATA;
addLodToBuffer(
clientLevel, //==========================//
data, topDataPoint, bottomDataPoint, // build this render column //
//==========================//
for (int i = 0; i < columnRenderData.size; i++)
{
// can be uncommented to limit which vertical LOD is generated
if (Config.Client.Advanced.Debugging.ColumnBuilderDebugging.columnBuilderDebugEnable.get())
{
int wantedColumnIndex = Config.Client.Advanced.Debugging.ColumnBuilderDebugging.columnBuilderDebugColumnIndex.get();
if (wantedColumnIndex >= 0
&& i != wantedColumnIndex)
{
continue;
}
}
long data = columnRenderData.get(i);
// If the data is not render-able (Void or non-existing) we stop since there is
// no data left in this position
if (RenderDataPointUtil.hasZeroHeight(data)
|| !RenderDataPointUtil.doesDataPointExist(data))
{
break;
}
long topDataPoint = (i - 1) >= 0 ? columnRenderData.get(i - 1) : RenderDataPointUtil.EMPTY_DATA;
long bottomDataPoint = (i + 1) < columnRenderData.size ? columnRenderData.get(i + 1) : RenderDataPointUtil.EMPTY_DATA;
addRenderDataPointToBuilder(
clientLevel, phantomArrayCheckout,
data, topDataPoint, bottomDataPoint,
adjColumnViews, isSameDetailLevel, adjColumnViews, isSameDetailLevel,
thisDetailLevel, relX, relZ, thisDetailLevel, relX, relZ,
quadBuilder, debugSourceFlag); quadBuilder);
} }
}// for z
}// for z }// for x
}// for x }// phantom checkout
quadBuilder.mergeQuads(); quadBuilder.mergeQuads();
} }
private static void addLodToBuffer( private static void addRenderDataPointToBuilder(
IDhClientLevel clientLevel, IDhClientLevel clientLevel, PhantomArrayListCheckout phantomArrayCheckout,
long data, long topData, long bottomData, long renderData, long topRenderData, long bottomRenderData,
ColumnArrayView[] adjColumnViews, boolean[] isSameDetailLevel, ColumnRenderView[] adjColumnViews, boolean[] isSameDetailLevel,
byte detailLevel, int renderSourceOffsetPosX, int renderSourceOffsetPosZ, byte detailLevel, int renderSourceOffsetPosX, int renderSourceOffsetPosZ,
LodQuadBuilder quadBuilder, ColumnRenderSource.DebugSourceFlag debugSource) LodQuadBuilder quadBuilder)
{ {
long sectionPos = DhSectionPos.encode(detailLevel, renderSourceOffsetPosX, renderSourceOffsetPosZ); long sectionPos = DhSectionPos.encode(detailLevel, renderSourceOffsetPosX, renderSourceOffsetPosZ);
short width = (short) BitShiftUtil.powerOfTwo(detailLevel); short blockWidth = (short) DhSectionPos.getDetailLevelWidthInBlocks(detailLevel);
short xMin = (short) DhSectionPos.getMinCornerBlockX(sectionPos); short blockMinX = (short) DhSectionPos.getMinCornerBlockX(sectionPos);
short yMin = RenderDataPointUtil.getYMin(data); short blockMinY = RenderDataPointUtil.getYMin(renderData);
short zMin = (short) DhSectionPos.getMinCornerBlockZ(sectionPos); short blockMinZ = (short) DhSectionPos.getMinCornerBlockZ(sectionPos);
short ySize = (short) (RenderDataPointUtil.getYMax(data) - yMin); short blockMaxY = (short) (RenderDataPointUtil.getYMax(renderData) - blockMinY);
if (ySize == 0) if (blockMaxY == 0)
{ {
return; return;
} }
else if (ySize < 0) else if (blockMaxY < 0)
{ {
throw new IllegalArgumentException("Negative y size for the data! Data: [" + RenderDataPointUtil.toString(data) + "]."); throw new IllegalArgumentException("Negative y size for the renderDataPoint! Data: [" + RenderDataPointUtil.toString(renderData) + "].");
} }
byte blockMaterialId = RenderDataPointUtil.getBlockMaterialId(data); byte blockMaterialId = RenderDataPointUtil.getBlockMaterialId(renderData);
@@ -321,15 +333,15 @@ public class ColumnRenderBufferBuilder
{ {
case OFF: case OFF:
{ {
float saturationMultiplier = Config.Client.Advanced.Graphics.Quality.saturationMultiplier.get().floatValue(); float saturationMultiplier = Config.Client.Advanced.Graphics.Quality.saturationMultiplier.get();
float brightnessMultiplier = Config.Client.Advanced.Graphics.Quality.brightnessMultiplier.get().floatValue(); float brightnessMultiplier = Config.Client.Advanced.Graphics.Quality.brightnessMultiplier.get();
if (saturationMultiplier == 1.0 && brightnessMultiplier == 1.0) if (saturationMultiplier == 1.0 && brightnessMultiplier == 1.0)
{ {
color = RenderDataPointUtil.getColor(data); color = RenderDataPointUtil.getColor(renderData);
} }
else else
{ {
float[] ahsv = ColorUtil.argbToAhsv(RenderDataPointUtil.getColor(data)); float[] ahsv = ColorUtil.argbToAhsv(RenderDataPointUtil.getColor(renderData));
color = ColorUtil.ahsvToArgb(ahsv[0], ahsv[1], ahsv[2] * saturationMultiplier, ahsv[3] * brightnessMultiplier); color = ColorUtil.ahsvToArgb(ahsv[0], ahsv[1], ahsv[2] * saturationMultiplier, ahsv[3] * brightnessMultiplier);
} }
break; break;
@@ -408,25 +420,19 @@ public class ColumnRenderBufferBuilder
fullBright = true; fullBright = true;
break; break;
} }
case SHOW_RENDER_SOURCE_FLAG:
{
color = debugSource == null ? ColorUtil.RED : debugSource.color;
fullBright = true;
break;
}
default: default:
throw new IllegalArgumentException("Unknown debug mode: " + debugging); throw new IllegalArgumentException("Unknown debug mode: " + debugging);
} }
ColumnBox.addBoxQuadsToBuilder( ColumnBox.addBoxQuadsToBuilder(
quadBuilder, clientLevel, quadBuilder, phantomArrayCheckout, clientLevel,
width, ySize, width, blockWidth, blockMaxY,
xMin, yMin, zMin, blockMinX, blockMinY, blockMinZ,
color, color,
blockMaterialId, blockMaterialId,
RenderDataPointUtil.getLightSky(data), RenderDataPointUtil.getLightSky(renderData),
fullBright ? 15 : RenderDataPointUtil.getLightBlock(data), fullBright ? LodUtil.MAX_MC_LIGHT : RenderDataPointUtil.getLightBlock(renderData),
topData, bottomData, adjColumnViews, isSameDetailLevel); topRenderData, bottomRenderData, adjColumnViews, isSameDetailLevel);
} }
} }
@@ -0,0 +1,320 @@
/*
* 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.dataObjects.render.bufferBuilding;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.render.RenderThreadTaskHandler;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.objects.ILodContainerUniformBufferWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.renderPass.IDhTerrainRenderer;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.objects.IVertexBufferWrapper;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
/**
* Java representation of one or more OpenGL buffers for rendering.
*
* @see ColumnRenderBufferBuilder
*/
public class LodBufferContainer implements AutoCloseable
{
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
/** the position closest to minimum X/Z infinity and the level's lowest Y */
public final DhBlockPos minCornerBlockPos;
public final long pos;
public boolean buffersUploaded = false;
public IVertexBufferWrapper[] vbos;
public IVertexBufferWrapper[] vbosTransparent;
public ILodContainerUniformBufferWrapper uniformContainer = WRAPPER_FACTORY.createLodContainerUniformWrapper();
private final AtomicReference<CompletableFuture<LodBufferContainer>> uploadFutureRef = new AtomicReference<>(null);
//==============//
// constructors //
//==============//
//region
public LodBufferContainer(long pos, DhBlockPos minCornerBlockPos)
{
this.pos = pos;
this.minCornerBlockPos = minCornerBlockPos;
this.vbos = new IVertexBufferWrapper[0];
this.vbosTransparent = new IVertexBufferWrapper[0];
this.uniformContainer.createUniformData(this);
}
//endregion
//==================//
// buffer uploading //
//==================//
//region
/** Should be run on a DH thread. */
public synchronized CompletableFuture<LodBufferContainer> makeAndUploadBuffersAsync(LodQuadBuilder builder)
{
// separate variable to prevent race condition when checking null
CompletableFuture<LodBufferContainer> oldFuture = this.uploadFutureRef.get();
if (oldFuture != null)
{
// upload already in process
return oldFuture;
}
// new upload needed
CompletableFuture<LodBufferContainer> future = new CompletableFuture<>();
future.handle((lodBufferContainer, throwable) ->
{
if (!this.uploadFutureRef.compareAndSet(future, null))
{
LOGGER.warn("upload future ref changed for pos ["+DhSectionPos.toString(this.pos)+"].");
}
return null;
});
if (!this.uploadFutureRef.compareAndSet(null, future))
{
oldFuture = this.uploadFutureRef.get();
LodUtil.assertTrue(oldFuture != null, "Concurrency error");
return oldFuture;
}
// make the buffers
ArrayList<ByteBuffer> opaqueBuffers = builder.makeOpaqueVertexBuffers();
ArrayList<ByteBuffer> transparentBuffers = builder.makeTransparentVertexBuffers();
this.vbos = resizeBufferArray(this.vbos, opaqueBuffers.size());
this.vbosTransparent = resizeBufferArray(this.vbosTransparent, transparentBuffers.size());
// upload on MC's render thread
RenderThreadTaskHandler.INSTANCE.queueRunningOnRenderThread(() ->
{
try
{
// skip this event if requested
if (Thread.interrupted()
|| future.isCancelled())
{
throw new InterruptedException();
}
// upload on the render thread
uploadBuffers(this.vbos, opaqueBuffers);
uploadBuffers(this.vbosTransparent, transparentBuffers);
this.buffersUploaded = true;
// success
future.complete(this);
}
catch (InterruptedException ignore)
{
future.complete(this);
}
catch (Exception e)
{
LOGGER.error("Unexpected issue uploading buffer ["+this.minCornerBlockPos +"], error: ["+e.getMessage()+"].", e);
future.completeExceptionally(e);
}
finally
{
// all the buffers must be manually freed to prevent memory leaks
for (ByteBuffer buffer : opaqueBuffers)
{
MemoryUtil.memFree(buffer);
}
for (ByteBuffer buffer : transparentBuffers)
{
MemoryUtil.memFree(buffer);
}
}
});
return future;
}
private static IVertexBufferWrapper[] resizeBufferArray(IVertexBufferWrapper[] vbos, int newSize)
{
if (vbos.length == newSize)
{
return vbos;
}
IVertexBufferWrapper[] newVbos = new IVertexBufferWrapper[newSize];
System.arraycopy(vbos, 0, newVbos, 0, Math.min(vbos.length, newSize));
if (newSize < vbos.length)
{
for (int i = newSize; i < vbos.length; i++)
{
if (vbos[i] != null)
{
vbos[i].close();
}
}
}
return newVbos;
}
private static void uploadBuffers(IVertexBufferWrapper[] vbos, ArrayList<ByteBuffer> byteBuffers) throws InterruptedException
{
int vboIndex = 0;
for (int i = 0; i < byteBuffers.size(); i++)
{
if (vboIndex >= vbos.length)
{
throw new RuntimeException("Too many vertex buffers!!");
}
// get or create the VBO
if (vbos[vboIndex] == null)
{
vbos[vboIndex] = SingletonInjector.INSTANCE.get(IWrapperFactory.class).createVboWrapper("distantHorizons:McLodRenderer");
}
IVertexBufferWrapper vbo = vbos[vboIndex];
ByteBuffer buffer = byteBuffers.get(i);
int size = buffer.limit() - buffer.position();
int vertexCount = size / LodQuadBuilder.BYTES_PER_VERTEX;
try
{
vbo.upload(buffer, vertexCount);
}
catch (Exception e)
{
vbos[vboIndex] = null;
vbo.close();
LOGGER.error("Failed to upload buffer. Error: ["+e.getMessage()+"].", e);
}
vboIndex++;
}
if (vboIndex < vbos.length)
{
throw new RuntimeException("Too few vertex buffers!!");
}
}
//endregion
//================//
// helper methods //
//================//
//region
/** can be used when debugging */
public boolean hasNonNullVbos() { return this.vbos != null || this.vbosTransparent != null; }
/** can be used when debugging */
public int vboBufferCount()
{
int count = 0;
if (this.vbos != null)
{
count += this.vbos.length;
}
if (this.vbosTransparent != null)
{
count += this.vbosTransparent.length;
}
return count;
}
public boolean uploadInProgress() { return this.uploadFutureRef.get() != null; }
//endregion
//================//
// base overrides //
//================//
//region
/**
* This method is called when object is no longer in use.
* Called either after uploadBuffers() returned false (On buffer Upload
* thread), or by others when the object is not being used. (not in build,
* upload, or render state).
*/
@Override
public void close()
{
this.buffersUploaded = false;
RenderThreadTaskHandler.INSTANCE.queueRunningOnRenderThread(() ->
{
for (IVertexBufferWrapper buffer : this.vbos)
{
if (buffer != null)
{
buffer.close();
}
}
for (IVertexBufferWrapper buffer : this.vbosTransparent)
{
if (buffer != null)
{
buffer.close();
}
}
this.uniformContainer.close();
});
}
//endregion
}
@@ -20,9 +20,7 @@
package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding; package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import com.seibel.distanthorizons.api.enums.config.EDhApiGrassSideRendering; import com.seibel.distanthorizons.api.enums.config.EDhApiGrassSideRendering;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiBlockMaterial; import com.seibel.distanthorizons.api.enums.rendering.EDhApiBlockMaterial;
@@ -30,15 +28,12 @@ import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.render.glObject.buffer.GLVertexBuffer;
import com.seibel.distanthorizons.core.util.ColorUtil; import com.seibel.distanthorizons.core.util.ColorUtil;
import com.seibel.distanthorizons.api.enums.config.EDhApiGpuUploadMethod; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.render.renderPass.IDhTerrainRenderer;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.coreapi.util.MathUtil;
import org.apache.logging.log4j.Logger;
import org.lwjgl.system.MemoryUtil; import org.lwjgl.system.MemoryUtil;
/** /**
@@ -48,8 +43,8 @@ import org.lwjgl.system.MemoryUtil;
*/ */
public class LodQuadBuilder public class LodQuadBuilder
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private final ArrayList<BufferQuad>[] opaqueQuads = (ArrayList<BufferQuad>[]) new ArrayList[6]; private final ArrayList<BufferQuad>[] opaqueQuads = (ArrayList<BufferQuad>[]) new ArrayList[6];
@@ -62,55 +57,59 @@ public class LodQuadBuilder
private final EDhApiDebugRendering debugRenderingMode; private final EDhApiDebugRendering debugRenderingMode;
private final EDhApiGrassSideRendering grassSideRenderingMode; private final EDhApiGrassSideRendering grassSideRenderingMode;
/** the number of bytes for */
public static final int BYTES_PER_VERTEX = 14;
public static final int[][][] DIRECTION_VERTEX_IBO_QUAD = new int[][][] public static final int[][][] DIRECTION_VERTEX_IBO_QUAD = new int[][][]
{ ///region
// X,Z // {
{ // UP // X,Z //
{1, 0}, // 0 { // UP
{1, 1}, // 1 {1, 0}, // 0
{0, 1}, // 2 {1, 1}, // 1
{0, 0}, // 3 {0, 1}, // 2
}, {0, 0}, // 3
{ // DOWN },
{0, 0}, // 0 { // DOWN
{0, 1}, // 1 {0, 0}, // 0
{1, 1}, // 2 {0, 1}, // 1
{1, 0}, // 3 {1, 1}, // 2
}, {1, 0}, // 3
},
// X,Y //
{ // NORTH // X,Y //
{0, 0}, // 0 { // NORTH
{0, 1}, // 1 {0, 0}, // 0
{1, 1}, // 2 {0, 1}, // 1
{1, 1}, // 2
{1, 0}, // 3
}, {1, 0}, // 3
{ // SOUTH },
{1, 0}, // 0 { // SOUTH
{1, 1}, // 1 {1, 0}, // 0
{0, 1}, // 2 {1, 1}, // 1
{0, 1}, // 2
{0, 0}, // 3
}, {0, 0}, // 3
},
// Z,Y //
{ // WEST // Z,Y //
{0, 0}, // 0 { // WEST
{1, 0}, // 1 {0, 0}, // 0
{1, 1}, // 2 {1, 0}, // 1
{1, 1}, // 2
{0, 1}, // 3
}, {0, 1}, // 3
{ // EAST },
{0, 1}, // 0 { // EAST
{1, 1}, // 1 {0, 1}, // 0
{1, 0}, // 2 {1, 1}, // 1
{1, 0}, // 2
{0, 0}, // 3
}, {0, 0}, // 3
}; },
};
///endregion
private int premergeCount = 0; private int premergeCount = 0;
@@ -119,6 +118,7 @@ public class LodQuadBuilder
//=============// //=============//
// constructor // // constructor //
//=============// //=============//
//region
public LodQuadBuilder(boolean doTransparency, IClientLevelWrapper clientLevelWrapper) public LodQuadBuilder(boolean doTransparency, IClientLevelWrapper clientLevelWrapper)
{ {
@@ -136,15 +136,19 @@ public class LodQuadBuilder
} }
//endregion
//===========// //===========//
// add quads // // add quads //
//===========// //===========//
///region
public void addQuadAdj( public void addQuadAdj(
EDhDirection dir, short x, short y, short z, EDhDirection dir,
short widthEastWest, short widthNorthSouthOrUpDown, short x, short y, short z,
short width, short height,
int color, byte irisBlockMaterialId, byte skyLight, byte blockLight) int color, byte irisBlockMaterialId, byte skyLight, byte blockLight)
{ {
if (dir == EDhDirection.DOWN) if (dir == EDhDirection.DOWN)
@@ -152,13 +156,23 @@ public class LodQuadBuilder
throw new IllegalArgumentException("addQuadAdj() is only for adj direction! Not UP or Down!"); throw new IllegalArgumentException("addQuadAdj() is only for adj direction! Not UP or Down!");
} }
BufferQuad quad = new BufferQuad(x, y, z, widthEastWest, widthNorthSouthOrUpDown, color, irisBlockMaterialId, skyLight, blockLight, dir);
ArrayList<BufferQuad> quadList = (this.doTransparency && ColorUtil.getAlpha(color) < 255) ? this.transparentQuads[dir.ordinal()] : this.opaqueQuads[dir.ordinal()]; ArrayList<BufferQuad> quadList;
if (!quadList.isEmpty() && if (this.doTransparency && ColorUtil.getAlpha(color) < 255)
( {
quadList.get(quadList.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.EastWest) quadList = this.transparentQuads[dir.ordinal()];
|| quadList.get(quadList.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.NorthSouthOrUpDown)) }
) else
{
quadList = this.opaqueQuads[dir.ordinal()];
}
BufferQuad quad = new BufferQuad(x, y, z, width, height, color, irisBlockMaterialId, skyLight, blockLight, dir);
if (!quadList.isEmpty()
&& (
quadList.get(quadList.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.EastWest)
|| quadList.get(quadList.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.NorthSouthOrUpDown))
)
{ {
this.premergeCount++; this.premergeCount++;
return; return;
@@ -168,27 +182,35 @@ public class LodQuadBuilder
} }
// XZ // XZ
public void addQuadUp(short minX, short maxY, short minZ, short widthEastWest, short widthNorthSouthOrUpDown, int color, byte irisBlockMaterialId, byte skylight, byte blocklight) // TODO argument names are wrong public void addQuadUp(short minX, short maxY, short minZ, short width, int color, byte irisBlockMaterialId, byte skylight, byte blocklight)
{ {
BufferQuad quad = new BufferQuad(minX, maxY, minZ, widthEastWest, widthNorthSouthOrUpDown, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.UP);
boolean isTransparent = (this.doTransparency && ColorUtil.getAlpha(color) < 255); boolean isTransparent = (this.doTransparency && ColorUtil.getAlpha(color) < 255);
ArrayList<BufferQuad> quadList = isTransparent ? this.transparentQuads[EDhDirection.UP.ordinal()] : this.opaqueQuads[EDhDirection.UP.ordinal()]; ArrayList<BufferQuad> quadList = isTransparent
? this.transparentQuads[EDhDirection.UP.ordinal()]
: this.opaqueQuads[EDhDirection.UP.ordinal()];
BufferQuad quad = new BufferQuad(minX, maxY, minZ, width, width, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.UP);
quadList.add(quad); quadList.add(quad);
} }
public void addQuadDown(short x, short y, short z, short width, short wz, int color, byte irisBlockMaterialId, byte skylight, byte blocklight) public void addQuadDown(short x, short y, short z, short width, int color, byte irisBlockMaterialId, byte skylight, byte blocklight)
{ {
BufferQuad quad = new BufferQuad(x, y, z, width, wz, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.DOWN); ArrayList<BufferQuad> quadArray = (this.doTransparency && ColorUtil.getAlpha(color) < 255)
ArrayList<BufferQuad> qs = (doTransparency && ColorUtil.getAlpha(color) < 255) ? this.transparentQuads[EDhDirection.DOWN.ordinal()]
? transparentQuads[EDhDirection.DOWN.ordinal()] : opaqueQuads[EDhDirection.DOWN.ordinal()]; : this.opaqueQuads[EDhDirection.DOWN.ordinal()];
qs.add(quad);
BufferQuad quad = new BufferQuad(x, y, z, width, width, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.DOWN);
quadArray.add(quad);
} }
///endregion
//=================// //=================//
// data finalizing // // data finalizing //
//=================// //=================//
///region
/** Uses Greedy meshing to merge this builder's Quads. */ /** Uses Greedy meshing to merge this builder's Quads. */
public void mergeQuads() public void mergeQuads()
@@ -257,11 +279,14 @@ public class LodQuadBuilder
return mergeCount; return mergeCount;
} }
///endregion
//==============// //==============//
// buffer setup // // buffer setup //
//==============// //==============//
///region
public ArrayList<ByteBuffer> makeOpaqueVertexBuffers() { return this.makeVertexBuffers(this.opaqueQuads); } public ArrayList<ByteBuffer> makeOpaqueVertexBuffers() { return this.makeVertexBuffers(this.opaqueQuads); }
public ArrayList<ByteBuffer> makeTransparentVertexBuffers() { return this.makeVertexBuffers(this.transparentQuads); } public ArrayList<ByteBuffer> makeTransparentVertexBuffers() { return this.makeVertexBuffers(this.transparentQuads); }
@@ -285,7 +310,7 @@ public class LodQuadBuilder
// create a new buffer // create a new buffer
if (buffer == null || !buffer.hasRemaining()) if (buffer == null || !buffer.hasRemaining())
{ {
buffer = MemoryUtil.memAlloc(ColumnRenderBuffer.FULL_SIZED_BUFFER); buffer = MemoryUtil.memAlloc(getMaxBufferByteSize());
byteBufferList.add(buffer); byteBufferList.add(buffer);
} }
@@ -307,9 +332,9 @@ public class LodQuadBuilder
{ {
int[][] quadBase = DIRECTION_VERTEX_IBO_QUAD[quad.direction.ordinal()]; int[][] quadBase = DIRECTION_VERTEX_IBO_QUAD[quad.direction.ordinal()];
short widthEastWest = quad.widthEastWest; short widthEastWest = quad.widthEastWest;
short widthNorthSouth = quad.widthNorthSouthOrUpDown; short widthNorthSouth = quad.widthNorthSouthOrHeight;
byte normalIndex = (byte) quad.direction.ordinal(); byte normalIndex = (byte) quad.direction.ordinal();
EDhDirection.Axis axis = quad.direction.getAxis(); EDhDirection.Axis axis = quad.direction.axis;
for (int i = 0; i < quadBase.length; i++) for (int i = 0; i < quadBase.length; i++)
{ {
short dx, dy, dz; short dx, dy, dz;
@@ -357,18 +382,18 @@ public class LodQuadBuilder
if (this.grassSideRenderingMode != EDhApiGrassSideRendering.AS_GRASS) if (this.grassSideRenderingMode != EDhApiGrassSideRendering.AS_GRASS)
{ {
// only change the vertex color if it's on the side or bottom // only change the vertex color if it's on the side or bottom
if (quad.direction.getAxis().isHorizontal() || quad.direction == EDhDirection.DOWN) if (quad.direction.axis.isHorizontal() || quad.direction == EDhDirection.DOWN)
{ {
if (this.grassSideRenderingMode == EDhApiGrassSideRendering.AS_DIRT if (this.grassSideRenderingMode == EDhApiGrassSideRendering.AS_DIRT
// if we want the color to fade, only apply the dirt color to the bottom vertices // if we want the color to fade, only apply the dirt color to the bottom vertices
|| (this.grassSideRenderingMode == EDhApiGrassSideRendering.FADE_TO_DIRT && quadBase[i][1] == 0) || (this.grassSideRenderingMode == EDhApiGrassSideRendering.FADE_TO_DIRT && quadBase[i][1] == 0)
// always render the bottom as dirt // always render the bottom as dirt
|| quad.direction == EDhDirection.DOWN) || quad.direction == EDhDirection.DOWN)
{ {
// for horizontal and bottom faces of grass blocks, use the dirt color to // for horizontal and bottom faces of grass blocks, use the dirt color to
// prevent green cliff walls // prevent green cliff walls
color = this.clientLevelWrapper.getDirtBlockColor(); color = this.clientLevelWrapper.getDirtBlockColor();
color = ColorUtil.applyShade(color, MC.getShade(quad.direction)); color = ColorUtil.applyShade(color, MC_RENDER.getShade(quad.direction));
} }
} }
} }
@@ -387,31 +412,33 @@ public class LodQuadBuilder
} }
private void putVertex(ByteBuffer bb, short x, short y, short z, int color, byte normalIndex, byte irisBlockMaterialId, byte skylight, byte blocklight, int mx, int my, int mz) private void putVertex(ByteBuffer bb, short x, short y, short z, int color, byte normalIndex, byte irisBlockMaterialId, byte skylight, byte blocklight, int mx, int my, int mz)
{ {
skylight %= 16;
blocklight %= 16;
bb.putShort(x); bb.putShort(x);
bb.putShort(y); bb.putShort(y);
bb.putShort(z); bb.putShort(z);
short meta = 0; short meta = 0;
meta |= (skylight | (blocklight << 4)); {
byte mirco = 0; skylight %= 16;
// mirco offset which is a xyz 2bit value blocklight %= 16;
// 0b00 = no offset meta |= (short) (skylight | (blocklight << 4));
// 0b01 = positive offset
// 0b11 = negative offset byte mircoOffset = 0;
// format is: 0b00zzyyxx // mirco offset which is a xyz 2bit value
if (mx != 0) mirco |= mx > 0 ? 0b01 : 0b11; // 0b00 = no offset
if (my != 0) mirco |= my > 0 ? 0b0100 : 0b1100; // 0b01 = positive offset
if (mz != 0) mirco |= mz > 0 ? 0b010000 : 0b110000; // 0b11 = negative offset
meta |= mirco << 8; // format is: 0b00zzyyxx
if (mx != 0) { mircoOffset |= (byte) (mx > 0 ? 0b01 : 0b11); }
if (my != 0) { mircoOffset |= (byte) (my > 0 ? 0b0100 : 0b1100); }
if (mz != 0) { mircoOffset |= (byte) (mz > 0 ? 0b010000 : 0b110000); }
meta |= (short) (mircoOffset << 8);
}
bb.putShort(meta); bb.putShort(meta);
byte r = (byte) ColorUtil.getRed(color); byte r = (byte) ColorUtil.getRed(color);
byte g = (byte) ColorUtil.getGreen(color); byte g = (byte) ColorUtil.getGreen(color);
byte b = (byte) ColorUtil.getBlue(color); byte b = (byte) ColorUtil.getBlue(color);
byte a = this.doTransparency ? (byte) ColorUtil.getAlpha(color) : (byte) 255; // TODO should this be called here or happen somewhere else? byte a = this.doTransparency ? (byte) ColorUtil.getAlpha(color) : (byte) 255;
bb.put(r); bb.put(r);
bb.put(g); bb.put(g);
bb.put(b); bb.put(b);
@@ -423,11 +450,14 @@ public class LodQuadBuilder
bb.putShort((short) 0); // padding to make sure the vertex format as a whole is a multiple of 4 bb.putShort((short) 0); // padding to make sure the vertex format as a whole is a multiple of 4
} }
///endregion
//=========// //=========//
// getters // // getters //
//=========// //=========//
///region
public int getCurrentOpaqueQuadsCount() public int getCurrentOpaqueQuadsCount()
{ {
@@ -455,17 +485,33 @@ public class LodQuadBuilder
return i; return i;
} }
/** Returns how many GpuBuffers will be needed to render opaque quads in this builder. */ private static int maxBufferByteSize = -1;
public int getCurrentNeededOpaqueVertexBufferCount() { return MathUtil.ceilDiv(this.getCurrentOpaqueQuadsCount(), ColumnRenderBuffer.MAX_QUADS_PER_BUFFER); } /**
/** Returns how many GpuBuffers will be needed to render transparent quads in this builder. */ * The max number of bytes we allow for a single Vertex buffer.
public int getCurrentNeededTransparentVertexBufferCount() * If an LOD has more data than this it will be split
* up into multiple buffers.
*/
public static int getMaxBufferByteSize()
{ {
if (!this.doTransparency) if (maxBufferByteSize != -1)
{ {
return 0; return maxBufferByteSize;
} }
return MathUtil.ceilDiv(this.getCurrentTransparentQuadsCount(), ColumnRenderBuffer.MAX_QUADS_PER_BUFFER); // number of bytes a single quad takes
int QUADS_BYTE_SIZE = BYTES_PER_VERTEX * 4;
// how big a single VBO can be in bytes
int MAX_VBO_BYTE_SIZE = 10 * 1024 * 1024; // 10 MB
int MAX_QUADS_PER_BUFFER = MAX_VBO_BYTE_SIZE / QUADS_BYTE_SIZE;
int FULL_SIZED_BUFFER = MAX_QUADS_PER_BUFFER * QUADS_BYTE_SIZE;
maxBufferByteSize = FULL_SIZED_BUFFER;
return FULL_SIZED_BUFFER;
} }
///endregion
} }
@@ -1,255 +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.dataObjects.render.columnViews;
import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource;
import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
public final class ColumnArrayView implements IColumnDataView
{
public final LongArrayList data;
/**
* How many data points are currently being represented by this view. <br>
* Will be equal to or less than {@link ColumnArrayView#verticalSize}.
*/
public final int size;
/**
* Vertical size in data points. <Br>
* Can be 0 if this column was created for an empty data source.
*/
public final int verticalSize;
/**
* Where the relative starting index is in the {@link ColumnArrayView#data} array
* if this view is representing part of a {@link ColumnRenderSource}.
*/
public final int offset;
//=============//
// constructor //
//=============//
/** @throws IllegalArgumentException if the offset is greater than the data's size */
public ColumnArrayView(LongArrayList data, int size, int offset, int verticalSize) throws IllegalArgumentException
{
this.data = data;
this.size = size;
this.offset = offset;
this.verticalSize = verticalSize;
if (this.data.size() < this.offset)
{
throw new IllegalArgumentException("data size ["+this.data.size()+"] is shorter than offset ["+this.offset+"].");
}
}
//=====================//
// getters and setters //
//=====================//
@Override
public long get(int index)
{
try
{
return this.data.getLong(index + this.offset);
}
catch (IndexOutOfBoundsException e)
{
// we can fairly confidently say this is a concurrent exception over an actual
// index out of bounds, since we're generally iterating over the whole
// array any time we use this getter.
throw new ConcurrentModificationException("Potential concurrent modification detected. Make sure the parent ColumnRenderSource isn't being closed before the ColumnArrayView processing is complete.", e);
}
}
public void set(int index, long value) { data.set(index + offset, value); }
@Override
public int size() { return size; }
@Override
public int verticalSize() { return verticalSize; }
@Override
public int dataCount() { return (this.verticalSize != 0) ? (this.size / this.verticalSize) : 0; } // TODO what does the divide by mean?
@Override
public ColumnArrayView subView(int dataIndexStart, int dataCount)
{
return new ColumnArrayView(data, dataCount * verticalSize, offset + dataIndexStart * verticalSize, verticalSize);
}
public void fill(long value) { Arrays.fill(data.elements(), offset, offset + size, value); }
public void copyFrom(IColumnDataView source) { copyFrom(source, 0); }
public void copyFrom(IColumnDataView source, int outputDataIndexOffset)
{
if (source.verticalSize() > verticalSize)
{
throw new IllegalArgumentException("source verticalSize must be <= self's verticalSize to copy");
}
else if (source.dataCount() + outputDataIndexOffset > dataCount())
{
throw new IllegalArgumentException("dataIndexStart + source.dataCount() must be <= self.dataCount() to copy");
}
else if (source.verticalSize() != verticalSize)
{
for (int i = 0; i < source.dataCount(); i++)
{
int outputOffset = offset + outputDataIndexOffset * verticalSize + i * verticalSize;
source.subView(i, 1).copyTo(data.elements(), outputOffset, source.verticalSize());
Arrays.fill(data.elements(), outputOffset + source.verticalSize(),
outputOffset + verticalSize, 0);
}
}
else
{
source.copyTo(data.elements(), offset + outputDataIndexOffset * verticalSize, source.size());
}
}
@Override
public void copyTo(long[] target, int offset, int size) { System.arraycopy(data.elements(), this.offset, target, offset, size); }
public boolean mergeWith(ColumnArrayView source, boolean override)
{
if (size != source.size)
{
throw new IllegalArgumentException("Cannot merge views of different sizes");
}
if (verticalSize != source.verticalSize)
{
throw new IllegalArgumentException("Cannot merge views of different vertical sizes");
}
boolean anyChange = false;
for (int o = 0; o < (source.size() * verticalSize); o += verticalSize)
{
if (override)
{
if (RenderDataPointUtil.compareDatapointPriority(source.get(o), get(o)) >= 0)
{
anyChange = true;
System.arraycopy(source.data, source.offset + o, data, offset + o, verticalSize);
}
}
else
{
if (RenderDataPointUtil.compareDatapointPriority(source.get(o), get(o)) > 0)
{
anyChange = true;
System.arraycopy(source.data, source.offset + o, data, offset + o, verticalSize);
}
}
}
return anyChange;
}
public void changeVerticalSizeFrom(IColumnDataView source)
{
if (this.dataCount() != source.dataCount())
{
throw new IllegalArgumentException("Cannot copy and resize to views with different dataCounts");
}
if (this.verticalSize >= source.verticalSize())
{
this.copyFrom(source);
}
else
{
for (int i = 0; i < this.dataCount(); i++)
{
RenderDataPointUtil.mergeMultiData(source.subView(i, 1), subView(i, 1));
}
}
}
public void mergeMultiDataFrom(IColumnDataView source)
{
if (dataCount() != 1)
{
throw new IllegalArgumentException("output dataCount must be 1");
}
RenderDataPointUtil.mergeMultiData(source, this);
}
//================//
// base overrides //
//================//
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("S:").append(size);
sb.append(" V:").append(verticalSize);
sb.append(" O:").append(offset);
sb.append(" [");
for (int i = 0; i < size; i++)
{
sb.append(RenderDataPointUtil.toString(data.getLong(offset + i)));
if (i < size - 1)
{
sb.append(",\n");
}
}
sb.append("]");
return sb.toString();
}
public int getDataHash()
{
return arrayHash(data, offset, size);
}
private static int arrayHash(LongArrayList a, int offset, int length)
{
if (a == null)
{
return 0;
}
int result = 1;
int end = offset + length;
for (int i = offset; i < end; i++)
{
long element = a.getLong(i);
int elementHash = (int) (element ^ (element >>> 32));
result = 31 * result + elementHash;
}
return result;
}
}
@@ -1,179 +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.dataObjects.render.columnViews;
import it.unimi.dsi.fastutil.longs.LongArrayList;
public class ColumnQuadView implements IColumnDataView
{
private final LongArrayList data;
private final int perColumnOffset; // per column (of columns of data) offset in longs
private final int xSize; // x size in datapoints
private final int zSize; // x size in datapoints
private final int offset; // offset in longs
private final int vertSize; // vertical size in longs
public ColumnQuadView(LongArrayList data, int dataZWidth, int dataVertSize, int viewXOffset, int viewZOffset, int xSize, int zSize)
{
if (viewXOffset + xSize > (data.size() / (dataZWidth * dataVertSize)) || viewZOffset + zSize > dataZWidth)
{
throw new IllegalArgumentException("View is out of bounds");
}
this.data = data;
this.xSize = xSize;
this.zSize = zSize;
this.vertSize = dataVertSize;
this.perColumnOffset = dataZWidth * dataVertSize;
this.offset = viewXOffset * perColumnOffset + viewZOffset * dataVertSize;
}
private ColumnQuadView(LongArrayList data, int perColumnOffset, int offset, int vertSize, int xSize, int zSize)
{
this.data = data;
this.perColumnOffset = perColumnOffset;
this.offset = offset;
this.vertSize = vertSize;
this.xSize = xSize;
this.zSize = zSize;
}
@Override
public long get(int index)
{
int x = index / perColumnOffset;
int z = (index % perColumnOffset) / vertSize;
int v = index % vertSize;
return get(x, z, v);
}
public long get(int x, int z, int v)
{
return data.getLong(offset + x * perColumnOffset + z * vertSize + v);
}
public long set(int x, int z, int v, long value)
{
return data.set(offset + x * perColumnOffset + z * vertSize + v, value);
}
public ColumnArrayView get(int x, int z)
{
return new ColumnArrayView(data, vertSize, offset + x * perColumnOffset + z * vertSize, vertSize);
}
public ColumnArrayView getRow(int x)
{
return new ColumnArrayView(data, zSize * vertSize, offset + x * perColumnOffset, vertSize);
}
public void set(int x, int z, IColumnDataView singleColumn)
{
if (singleColumn.verticalSize() != vertSize) throw new IllegalArgumentException("Vertical size of singleColumn must be equal to vertSize");
if (singleColumn.dataCount() != 1) throw new IllegalArgumentException("SingleColumn must contain exactly one data point");
singleColumn.copyTo(data.elements(), offset + x * perColumnOffset + z * vertSize, singleColumn.size());
}
@Override
public int size()
{
return xSize * zSize * vertSize;
}
@Override
public int verticalSize()
{
return vertSize;
}
@Override
public int dataCount()
{
return xSize * zSize;
}
@Override
public IColumnDataView subView(int dataIndexStart, int dataCount)
{
if (dataCount != 1) throw new UnsupportedOperationException("Fixme: subView for QUadView only support one data point!");
int x = dataIndexStart / xSize;
int z = dataIndexStart % xSize;
return new ColumnArrayView(data, vertSize * dataCount, offset + x * perColumnOffset + z * vertSize, vertSize);
}
public ColumnQuadView subView(int xOffset, int zOffset, int xSize, int zSize)
{
if (xOffset + xSize > this.xSize || zOffset + zSize > this.zSize) throw new IllegalArgumentException("SubView is out of bounds");
return new ColumnQuadView(data, perColumnOffset, offset + xOffset * perColumnOffset + zOffset * vertSize, vertSize, xSize, zSize);
}
@Override
public void copyTo(long[] target, int offset, int size)
{
if (size != this.size() && size > zSize * vertSize) throw new UnsupportedOperationException("Not supported yet");
if (size <= xSize * vertSize)
{
System.arraycopy(data, this.offset, target, offset, size);
}
else
{
for (int x = 0; x < xSize; x++)
{
System.arraycopy(data, this.offset + x * perColumnOffset, target, offset + x * xSize * vertSize, xSize * vertSize);
}
}
}
public void copyTo(ColumnQuadView target)
{
if (target.xSize != xSize || target.zSize != zSize)
throw new IllegalArgumentException("Target view must have same size as this view");
for (int x = 0; x < xSize; x++)
{
target.getRow(x).changeVerticalSizeFrom(getRow(x));
}
}
public void mergeMultiColumnFrom(ColumnQuadView source)
{
if (source.xSize == xSize && source.zSize == zSize)
{
source.copyTo(this);
return;
}
if (source.xSize < xSize || source.zSize < zSize)
throw new IllegalArgumentException("Source view must have same or larger size as this view");
int srcXPerTrgX = source.xSize / xSize;
int srcZPerTrgZ = source.zSize / zSize;
if (source.xSize % xSize != 0 || source.zSize % zSize != 0)
throw new IllegalArgumentException("Source view's size must be a multiple of this view's size");
for (int x = 0; x < xSize; x++)
{
for (int z = 0; z < zSize; z++)
{
ColumnQuadView srcBlock = source.subView(x * srcXPerTrgX, z * srcZPerTrgZ, srcXPerTrgX, srcZPerTrgZ);
get(x, z).mergeMultiDataFrom(srcBlock);
}
}
}
}
@@ -0,0 +1,309 @@
/*
* 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.dataObjects.render.columnViews;
import com.seibel.distanthorizons.api.enums.config.EDhApiVerticalQuality;
import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource;
import com.seibel.distanthorizons.core.util.RenderDataPointReducingList;
import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* Maps to part of a {@link ColumnRenderSource} for easier handling.
*
* @see ColumnRenderSource
*/
public final class ColumnRenderView implements AutoCloseable
{
private static final ConcurrentLinkedQueue<ColumnRenderView> POOL = new ConcurrentLinkedQueue<>();
public LongArrayList data;
/**
* How many data points are currently being represented by this view. <br>
* Will be equal to or less than {@link ColumnRenderView#maxVerticalSliceCount}.
*/
public int size;
/**
* Vertical size in data points. <Br>
* Can be 0 if this column was created for an empty data source.
* @see EDhApiVerticalQuality#calculateMaxNumberOfVerticalSlicesAtDetailLevel(byte)
*/
public int maxVerticalSliceCount;
/**
* Where the relative starting index is in the {@link ColumnRenderView#data} array
* if this view is representing part of a {@link ColumnRenderSource}.
*/
public int offset;
//=============//
// constructor //
//=============//
//region
private ColumnRenderView() { }
/**
* returns an un-populated view. <br>
* {@link ColumnRenderView#populate(LongArrayList, int, int, int)} must be called before the
* view can be used.s
*/
public static ColumnRenderView getPooled() { return getPooled(null, 0, 0, 0); }
public static ColumnRenderView getPooled(LongArrayList data, int size, int offset, int maxVerticalSliceCount) throws IllegalArgumentException
{
// try getting an existing pooled object first
ColumnRenderView view = POOL.poll();
if (view == null)
{
// no pooled object
view = new ColumnRenderView();
}
// data will be null if the object will be populated at a later date
if (data != null)
{
view.populate(data, size, offset, maxVerticalSliceCount);
}
return view;
}
/**
* Mutates this object so the necessary data is visible.
* @throws IllegalArgumentException if the offset is greater than the data's size
*/
public void populate(LongArrayList data, int size, int offset, int maxVerticalSliceCount) throws IllegalArgumentException
{
this.data = data;
this.size = size;
this.offset = offset;
this.maxVerticalSliceCount = maxVerticalSliceCount;
if (this.data.size() < this.offset)
{
throw new IllegalArgumentException("data size ["+this.data.size()+"] is shorter than offset ["+this.offset+"].");
}
}
public void clear()
{
this.data = null;
this.size = 0;
this.offset = 0;
this.maxVerticalSliceCount = 0;
}
//endregion
//=====================//
// getters and setters //
//=====================//
//region
public long get(int index)
{
try
{
return this.data.getLong(index + this.offset);
}
catch (IndexOutOfBoundsException e)
{
// we can fairly confidently say this is a concurrent exception over an actual
// index out of bounds, since we're generally iterating over the whole
// array any time we use this getter.
throw new ConcurrentModificationException("Potential concurrent modification detected. Make sure the parent ColumnRenderSource isn't being closed before the ColumnRenderView processing is complete.", e);
}
}
public void set(int index, long value) { this.data.set(index + this.offset, value); }
public void fill(long value) { Arrays.fill(this.data.elements(), this.offset, this.offset + this.size, value); }
//endregion
//=========//
// subview //
//=========//
//region
/** should be called in a try-finally block for automatic cleanup */
public ColumnRenderView subView(int dataIndexStart, int dataCount)
{
return ColumnRenderView.getPooled(
this.data,
dataCount * this.maxVerticalSliceCount,
this.offset + dataIndexStart * this.maxVerticalSliceCount,
this.maxVerticalSliceCount);
}
/** can be used to determine sub-view starting indexes */
public int subViewCount() { return (this.maxVerticalSliceCount != 0) ? (this.size / this.maxVerticalSliceCount) : 0; }
//endregion
//======================//
// change vertical size //
//======================//
//region
public void changeVerticalSizeFrom(ColumnRenderView source, RenderDataPointReducingList reducingList)
{
if (this.subViewCount() != source.subViewCount())
{
throw new IllegalArgumentException("Cannot copy and resize to views with different dataCounts");
}
if (this.maxVerticalSliceCount >= source.maxVerticalSliceCount)
{
this.copyFrom(source, 0);
}
else
{
for (int i = 0; i < this.subViewCount(); i++)
{
try(ColumnRenderView sourceSubView = source.subView(i, 1);
ColumnRenderView thisSubView = this.subView(i, 1))
{
mergeMultiData(sourceSubView, reducingList, thisSubView);
}
}
}
}
private void copyFrom(ColumnRenderView source, int outputDataIndexOffset)
{
if (source.maxVerticalSliceCount > this.maxVerticalSliceCount)
{
throw new IllegalArgumentException("source verticalSize must be <= self's verticalSize to copy");
}
else if (source.subViewCount() + outputDataIndexOffset > this.subViewCount())
{
throw new IllegalArgumentException("dataIndexStart + source.dataCount() must be <= self.dataCount() to copy");
}
else if (source.maxVerticalSliceCount != this.maxVerticalSliceCount)
{
for (int i = 0; i < source.subViewCount(); i++)
{
int outputOffset = this.offset + (outputDataIndexOffset * this.maxVerticalSliceCount) + (i * this.maxVerticalSliceCount);
try(ColumnRenderView subView = source.subView(i, 1))
{
subView.copyTo(this.data.elements(), outputOffset, source.maxVerticalSliceCount);
Arrays.fill(this.data.elements(),
outputOffset + source.maxVerticalSliceCount,
outputOffset + this.maxVerticalSliceCount,
0);
}
}
}
else
{
source.copyTo(this.data.elements(), this.offset + outputDataIndexOffset * this.maxVerticalSliceCount, source.size);
}
}
private void copyTo(long[] target, int offset, int size) { System.arraycopy(this.data.elements(), this.offset, target, offset, size); }
/**
* This method merge column of multiple data together
*
* @param sourceData one or more columns of data
* @param output one column of space for the result to be written to
*/
private static void mergeMultiData(ColumnRenderView sourceData, RenderDataPointReducingList reducingList, ColumnRenderView output)
{
int target = output.maxVerticalSliceCount;
if (target <= 0)
{
// I expect this to never be the case,
// but RenderDataPointReducingList handles it sanely,
// so I might as well handle it sanely here too.
output.fill(RenderDataPointUtil.EMPTY_DATA);
}
else if (target == 1)
{
output.set(0, RenderDataPointReducingList.reduceToOne(sourceData));
for (int index = 1, size = output.size; index < size; index++)
{
output.set(index, RenderDataPointUtil.EMPTY_DATA);
}
}
else
{
reducingList.populate(sourceData);
reducingList.reduce(output.maxVerticalSliceCount);
reducingList.copyTo(output);
}
}
//endregion
//================//
// base overrides //
//================//
//region
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("S:").append(this.size);
sb.append(" V:").append(this.maxVerticalSliceCount);
sb.append(" O:").append(this.offset);
sb.append(" [");
for (int i = 0; i < this.size; i++)
{
sb.append(RenderDataPointUtil.toString(this.data.getLong(this.offset + i)));
if (i < this.size - 1)
{
sb.append(",\n");
}
}
sb.append("]");
return sb.toString();
}
@Override
public void close()
{
// no validation is done to make sure this object is only added to the pool once
// please only use this object in a try-finally so the close is handled implicitly
POOL.add(this);
}
//endregion
}
@@ -1,59 +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.dataObjects.render.columnViews;
import it.unimi.dsi.fastutil.longs.LongIterator;
import java.util.Iterator;
public interface IColumnDataView
{
long get(int index);
// FIXME probably horizontal size in blocks?
int size();
default LongIterator iterator()
{
return new LongIterator()
{
private int index = 0;
private final int size = IColumnDataView.this.size();
@Override
public boolean hasNext() { return this.index < this.size; }
@Override
public long nextLong() { return IColumnDataView.this.get(this.index++); }
};
}
// FIXME measured in blocks?
int verticalSize();
// FIXME how many datapoints in this LOD?
int dataCount();
IColumnDataView subView(int dataIndexStart, int dataCount);
void copyTo(long[] target, int offset, int count);
}
@@ -0,0 +1,199 @@
package com.seibel.distanthorizons.core.dataObjects.transformers;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.util.FullDataPointUtil;
import com.seibel.distanthorizons.core.util.LodUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.jetbrains.annotations.NotNull;
public class FullDataOcclusionCuller
{
/**
* Mutates the given datasource so blocks that aren't visible
* (IE completely surrounded by other opaque blocks)
* are removed from the data column.
*
* @param dataSource
* @param relX relative X position in the datasource
* @param relZ relative Z position in the datasource
*/
public static void cullHiddenDatapointsInColumn(
FullDataSourceV2 dataSource,
int relX, int relZ
)
{
LongArrayList centerColumn = dataSource.getColumnAtRelPos(relX, relZ);
LongArrayList posXColumn = dataSource.tryGetColumnAtRelPos(relX + 1, relZ);
LongArrayList negXColumn = dataSource.tryGetColumnAtRelPos(relX - 1, relZ);
LongArrayList posZColumn = dataSource.tryGetColumnAtRelPos(relX, relZ + 1);
LongArrayList negZColumn = dataSource.tryGetColumnAtRelPos(relX, relZ - 1);
if (posXColumn == null || posXColumn.size() == 0
|| negXColumn == null || negXColumn.size() == 0
|| posZColumn == null || posZColumn.size() == 0
|| negZColumn == null || negZColumn.size() == 0)
{
// if any adjacent columns are empty then we can't
// cull this column, since at least one side will be open
// to air/void
return;
}
int centerIndex = centerColumn.size() - 1;
int posXIndex = (posXColumn.size() - 1);
int negXIndex = (negXColumn.size() - 1);
int posZIndex = (posZColumn.size() - 1);
int negZIndex = (negZColumn.size() - 1);
for (; centerIndex >= 0; centerIndex--)
{
long currentPoint = centerColumn.getLong(centerIndex);
// Translucent data points are not eligible to be culled.
if (isTranslucent(dataSource, currentPoint))
{
continue;
}
// the top segment should never be culled.
if (centerIndex == 0
|| isTranslucent(dataSource, centerColumn.getLong(centerIndex - 1)))
{
continue;
}
// the bottom segment can sometimes be culled.
// assume it will not be seen from below,
// because this would imply the player is in the void.
if (centerIndex + 1 < centerColumn.size()
&& isTranslucent(dataSource, centerColumn.getLong(centerIndex + 1)))
{
continue;
}
// the lowest/bedrock segment should not be culled
if (centerIndex + 1 == centerColumn.size())
{
continue;
}
posXIndex = checkOcclusion(dataSource, currentPoint, posXColumn, posXIndex);
if (posXIndex < 0)
{
posXIndex = ~posXIndex;
continue;
}
negXIndex = checkOcclusion(dataSource, currentPoint, negXColumn, negXIndex);
if (negXIndex < 0)
{
negXIndex = ~negXIndex;
continue;
}
posZIndex = checkOcclusion(dataSource, currentPoint, posZColumn, posZIndex);
if (posZIndex < 0)
{
posZIndex = ~posZIndex;
continue;
}
negZIndex = checkOcclusion(dataSource, currentPoint, negZColumn, negZIndex);
if (negZIndex < 0)
{
negZIndex = ~negZIndex;
continue;
}
// Current point is fully surrounded. remove it.
centerColumn.removeLong(centerIndex);
// Make the above data point cover the area that the current point used to occupy.
// The element that was at `centerIndex - 1` is still at that position even after removal of centerIndex.
long above = centerColumn.getLong(centerIndex - 1);
above = FullDataPointUtil.setBottomY(above, FullDataPointUtil.getBottomY(currentPoint));
above = FullDataPointUtil.setHeight(above, FullDataPointUtil.getHeight(currentPoint) + FullDataPointUtil.getHeight(above));
centerColumn.set(centerIndex - 1, above);
}
}
/**
checks if centerPoint is "covered" by opaque data points in adjacentColumn.
centerPoint counts as covered if, and only if, for all Y levels in its height range,
there exists an opaque data point in adjacentColumn which overlaps with that Y level.
@param source used to lookup blocks (and their opacities) based on their IDs.
@param centerPoint the point being checked to see if it's fully covered.
@param adjacentColumn the data points which might cover centerPoint.
@param adjacentIndex the starting index in adjacentColumn to start scanning at.
indices greater than adjacentIndex have already been checked and confirmed to
not overlap or only overlap partially with centerPoint's Y range.
@return if centerPoint is covered, returns the index of the segment which finishes covering it.
the start of the covering may be a smaller index. in this case, the returned index may be used
as the adjacentIndex provided to this method on the next iteration which yields a new centerPoint.
if centerPoint is NOT covered, returns the bitwise negation of the index of the
segment which did not cover it. this guarantees that the returned value is negative.
the caller should check for negative return values and manually un-negate them to proceed with the loop.
in other words, this function returns the index of the next adjacent data
point to use in the loop, AND a boolean indicating whether or not the
centerPoint is covered; both are packed into the same int, and returned.
*/
private static int checkOcclusion(@NotNull FullDataSourceV2 source, long centerPoint, @NotNull LongArrayList adjacentColumn, int adjacentIndex)
{
// check if this point is adjacent to an empty column
// if so it will always be shown
if (adjacentColumn.isEmpty())
{
return ~adjacentIndex;
}
else if (adjacentColumn.size() == 1
&& adjacentColumn.getLong(0) == FullDataPointUtil.EMPTY_DATA_POINT)
{
return ~adjacentIndex;
}
int bottomOfCenter = FullDataPointUtil.getBottomY(centerPoint);
int topOfCenter = bottomOfCenter + FullDataPointUtil.getHeight(centerPoint);
for (; adjacentIndex >= 0; adjacentIndex--)
{
long adjacentPoint = adjacentColumn.getLong(adjacentIndex);
int topOfAdjacent = FullDataPointUtil.getBottomY(adjacentPoint) + FullDataPointUtil.getHeight(adjacentPoint);
if (topOfAdjacent <= bottomOfCenter)
{
// the adjacent point is below the center point,
// check the next one
continue;
}
else if (isTranslucent(source, adjacentPoint))
{
// this point is adjacent to a transparent LOD and should be shown
return ~adjacentIndex;
}
else if (topOfAdjacent >= topOfCenter)
{
// the adjacent point covers the center point
return adjacentIndex;
}
}
// the Adjacent column ends before center column does,
// this point should be visible
return ~adjacentIndex;
}
private static boolean isTranslucent(FullDataSourceV2 source, long point)
{
int id = FullDataPointUtil.getId(point);
int opacity = source.mapping.getBlockStateWrapper(id).getOpacity();
return opacity < LodUtil.BLOCK_FULLY_OPAQUE;
}
}
@@ -24,23 +24,23 @@ import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap; import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource; import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource;
import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnRenderView;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListCheckout;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListPool;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPosMutable; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPosMutable;
import com.seibel.distanthorizons.core.util.objects.pooling.PhantomArrayListCheckout;
import com.seibel.distanthorizons.core.util.objects.pooling.PhantomArrayListPool;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.util.*; import com.seibel.distanthorizons.core.util.*;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.HashSet; import java.util.HashSet;
@@ -50,14 +50,14 @@ import java.util.HashSet;
*/ */
public class FullDataToRenderDataTransformer public class FullDataToRenderDataTransformer
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class); private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final LongOpenHashSet brokenPos = new LongOpenHashSet(); private static final LongOpenHashSet BROKEN_POS_SET = new LongOpenHashSet();
private static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Data Transformer");
public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Data Transformer"); private static HashSet<IBlockStateWrapper> snowLayerBlockStates = null;
@@ -66,7 +66,8 @@ public class FullDataToRenderDataTransformer
//==============================// //==============================//
@Nullable @Nullable
public static ColumnRenderSource transformFullDataToRenderSource(@Nullable FullDataSourceV2 fullDataSource, @Nullable IClientLevelWrapper levelWrapper) public static ColumnRenderSource transformFullDataToRenderSource(
@Nullable FullDataSourceV2 fullDataSource, @Nullable IClientLevelWrapper levelWrapper)
{ {
if (fullDataSource == null) if (fullDataSource == null)
{ {
@@ -102,16 +103,17 @@ public class FullDataToRenderDataTransformer
* @throws InterruptedException Can be caused by interrupting the thread upstream. * @throws InterruptedException Can be caused by interrupting the thread upstream.
* Generally thrown if the method is running after the client leaves the current world. * Generally thrown if the method is running after the client leaves the current world.
*/ */
private static ColumnRenderSource transformCompleteFullDataToColumnData(IClientLevelWrapper levelWrapper, FullDataSourceV2 fullDataSource) throws InterruptedException private static ColumnRenderSource transformCompleteFullDataToColumnData(
IClientLevelWrapper levelWrapper, FullDataSourceV2 fullDataSource) throws InterruptedException
{ {
final long pos = fullDataSource.getPos(); final long pos = fullDataSource.getPos();
final byte dataDetail = fullDataSource.getDataDetailLevel(); final byte dataDetail = fullDataSource.getDataDetailLevel();
final int vertSize = Config.Client.Advanced.Graphics.Quality.verticalQuality.get().calculateMaxVerticalData(fullDataSource.getDataDetailLevel()); final int maxVertSliceCount = Config.Client.Advanced.Graphics.Quality.verticalQuality.get().calculateMaxNumberOfVerticalSlicesAtDetailLevel(fullDataSource.getDataDetailLevel());
final ColumnRenderSource columnSource = ColumnRenderSource.createEmpty(pos, vertSize, levelWrapper.getMinHeight()); final ColumnRenderSource columnSource = ColumnRenderSource.createEmpty(pos, maxVertSliceCount, levelWrapper.getMinHeight());
if (fullDataSource.isEmpty) if (fullDataSource.isEmpty)
{ {
return columnSource; return columnSource;
@@ -121,34 +123,40 @@ public class FullDataToRenderDataTransformer
int baseX = DhSectionPos.getMinCornerBlockX(pos); int baseX = DhSectionPos.getMinCornerBlockX(pos);
int baseZ = DhSectionPos.getMinCornerBlockZ(pos); int baseZ = DhSectionPos.getMinCornerBlockZ(pos);
for (int x = 0; x < FullDataSourceV2.WIDTH; x++) try(ColumnRenderView columnArrayView = ColumnRenderView.getPooled();
PhantomArrayListCheckout phantomCheckout = ARRAY_LIST_POOL.checkoutLongArrays(1);
ColumnRenderView tempExpandingColumnView = ColumnRenderView.getPooled();
RenderDataPointReducingList reducingList = new RenderDataPointReducingList())
{ {
for (int z = 0; z < FullDataSourceV2.WIDTH; z++) for (int x = 0; x < FullDataSourceV2.WIDTH; x++)
{ {
throwIfThreadInterrupted(); for (int z = 0; z < FullDataSourceV2.WIDTH; z++)
{
ColumnArrayView columnArrayView = columnSource.getVerticalDataPointView(x, z); columnSource.populateColumnView(columnArrayView, x, z);
LongArrayList dataColumn = fullDataSource.get(x, z); LongArrayList dataColumn = fullDataSource.getColumnAtRelPos(x, z);
updateOrReplaceRenderDataViewColumnWithFullDataColumn( updateOrReplaceRenderDataViewColumnWithFullDataColumn(
levelWrapper, fullDataSource, levelWrapper, fullDataSource,
// bitshift is to account for LODs with a detail level greater than 0 so the block pos is correct // bit shift is to account for LODs with a detail level greater than 0 so the block pos is correct
baseX + BitShiftUtil.pow(x,dataDetail), baseZ + BitShiftUtil.pow(z,dataDetail), baseX + BitShiftUtil.pow(x, dataDetail), baseZ + BitShiftUtil.pow(z, dataDetail),
columnArrayView, dataColumn); columnArrayView, dataColumn,
// pooled references so we don't need to re-allocate/get them 4000 times per render source
phantomCheckout, tempExpandingColumnView, reducingList);
}
} }
} }
columnSource.fillDebugFlag(0, 0, ColumnRenderSource.SECTION_SIZE, ColumnRenderSource.SECTION_SIZE, ColumnRenderSource.DebugSourceFlag.FULL);
return columnSource; return columnSource;
} }
/** Updates the given {@link ColumnArrayView} to match the incoming Full data {@link LongArrayList} */ /** Updates the given {@link ColumnRenderView} to match the incoming Full data {@link LongArrayList} */
public static void updateOrReplaceRenderDataViewColumnWithFullDataColumn( public static void updateOrReplaceRenderDataViewColumnWithFullDataColumn(
IClientLevelWrapper levelWrapper, IClientLevelWrapper levelWrapper,
FullDataSourceV2 fullDataSource, int blockX, int blockZ, FullDataSourceV2 fullDataSource, int blockX, int blockZ,
ColumnArrayView columnArrayView, ColumnRenderView columnArrayView,
LongArrayList fullDataColumn) LongArrayList fullDataColumn,
// pooled references
PhantomArrayListCheckout phantomCheckout, ColumnRenderView tempExpandingColumnView, RenderDataPointReducingList reducingList)
{ {
// we can't do anything if the full data is missing or empty // we can't do anything if the full data is missing or empty
if (fullDataColumn == null if (fullDataColumn == null
@@ -158,33 +166,26 @@ public class FullDataToRenderDataTransformer
} }
int fullDataLength = fullDataColumn.size(); int fullDataLength = fullDataColumn.size();
if (fullDataLength <= columnArrayView.verticalSize()) if (fullDataLength <= columnArrayView.maxVerticalSliceCount)
{ {
// Directly use the arrayView since it fits. // Directly use the arrayView since it fits.
setRenderColumnView(levelWrapper, fullDataSource, blockX, blockZ, columnArrayView, fullDataColumn); setRenderColumnView(levelWrapper, fullDataSource, blockX, blockZ, columnArrayView, fullDataColumn);
} }
else else
{ {
PhantomArrayListCheckout checkout = ARRAY_LIST_POOL.checkoutArrays(0, 0, 1); LongArrayList dataArrayList = phantomCheckout.getLongArray(0, fullDataLength);
LongArrayList dataArrayList = checkout.getLongArray(0, fullDataLength);
try // expand the ColumnArrayView to fit the new larger max vertical size
{ tempExpandingColumnView.populate(dataArrayList, fullDataLength, 0, fullDataLength);
// expand the ColumnArrayView to fit the new larger max vertical size setRenderColumnView(levelWrapper, fullDataSource, blockX, blockZ, tempExpandingColumnView, fullDataColumn);
ColumnArrayView newColumnArrayView = new ColumnArrayView(dataArrayList, fullDataLength, 0, fullDataLength);
setRenderColumnView(levelWrapper, fullDataSource, blockX, blockZ, newColumnArrayView, fullDataColumn); columnArrayView.changeVerticalSizeFrom(tempExpandingColumnView, reducingList);
columnArrayView.changeVerticalSizeFrom(newColumnArrayView);
}
finally
{
ARRAY_LIST_POOL.returnCheckout(checkout);
}
} }
} }
private static void setRenderColumnView( private static void setRenderColumnView(
IClientLevelWrapper levelWrapper, FullDataSourceV2 fullDataSource, IClientLevelWrapper levelWrapper, FullDataSourceV2 fullDataSource,
int blockX, int blockZ, int blockX, int blockZ,
ColumnArrayView renderColumnData, LongArrayList fullColumnData) ColumnRenderView renderColumnData, LongArrayList fullColumnData)
{ {
//===============// //===============//
// config values // // config values //
@@ -193,8 +194,18 @@ public class FullDataToRenderDataTransformer
boolean ignoreNonCollidingBlocks = (Config.Client.Advanced.Graphics.Quality.blocksToIgnore.get() == EDhApiBlocksToAvoid.NON_COLLIDING); boolean ignoreNonCollidingBlocks = (Config.Client.Advanced.Graphics.Quality.blocksToIgnore.get() == EDhApiBlocksToAvoid.NON_COLLIDING);
boolean colorBelowWithAvoidedBlocks = Config.Client.Advanced.Graphics.Quality.tintWithAvoidedBlocks.get(); boolean colorBelowWithAvoidedBlocks = Config.Client.Advanced.Graphics.Quality.tintWithAvoidedBlocks.get();
HashSet<IBlockStateWrapper> blockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredBlocks(levelWrapper); ObjectOpenHashSet<IBlockStateWrapper> blockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredBlocks(levelWrapper);
HashSet<IBlockStateWrapper> caveBlockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredCaveBlocks(levelWrapper); ObjectOpenHashSet<IBlockStateWrapper> caveBlockStatesToIgnore = WRAPPER_FACTORY.getRendererIgnoredCaveBlocks(levelWrapper);
// build snow block cache if needed
if (snowLayerBlockStates == null)
{
snowLayerBlockStates = new HashSet<>();
// ignore snow layers 1-3, everything above should be considered a full block
snowLayerBlockStates.add(WRAPPER_FACTORY.deserializeBlockStateWrapperOrGetDefault("minecraft:snow_STATE_{layers:1}", levelWrapper));
snowLayerBlockStates.add(WRAPPER_FACTORY.deserializeBlockStateWrapperOrGetDefault("minecraft:snow_STATE_{layers:2}", levelWrapper));
snowLayerBlockStates.add(WRAPPER_FACTORY.deserializeBlockStateWrapperOrGetDefault("minecraft:snow_STATE_{layers:3}", levelWrapper));
}
int caveCullingMaxY = Config.Client.Advanced.Graphics.Culling.caveCullingHeight.get() - levelWrapper.getMinHeight(); int caveCullingMaxY = Config.Client.Advanced.Graphics.Culling.caveCullingHeight.get() - levelWrapper.getMinHeight();
boolean caveCullingEnabled = boolean caveCullingEnabled =
@@ -250,9 +261,9 @@ public class FullDataToRenderDataTransformer
} }
catch (IndexOutOfBoundsException e) catch (IndexOutOfBoundsException e)
{ {
if (!brokenPos.contains(fullDataMapping.getPos())) if (!BROKEN_POS_SET.contains(fullDataMapping.getPos()))
{ {
brokenPos.add(fullDataMapping.getPos()); BROKEN_POS_SET.add(fullDataMapping.getPos());
String levelId = levelWrapper.getDhIdentifier(); String levelId = levelWrapper.getDhIdentifier();
LOGGER.warn("Unable to get data point with id ["+id+"] " + LOGGER.warn("Unable to get data point with id ["+id+"] " +
"(Max possible ID: ["+fullDataMapping.getMaxValidId()+"]) " + "(Max possible ID: ["+fullDataMapping.getMaxValidId()+"]) " +
@@ -273,26 +284,30 @@ public class FullDataToRenderDataTransformer
//====================// //====================//
boolean ignoreBlock = blockStatesToIgnore.contains(block); boolean ignoreBlock = blockStatesToIgnore.contains(block);
boolean caveBlock = caveBlockStatesToIgnore.contains(block); // TODO caves should also ignore transparent/non-solid blocks (IE grass and plants) wthout each being defined boolean caveBlock = caveBlockStatesToIgnore.contains(block);
if (caveBlock) if (caveBlock
// caves also ignore transparent/non-solid blocks (IE grass and plants) without each being defined
|| !block.isSolid()
|| block.isLiquid()
|| block.getOpacity() < LodUtil.BLOCK_FULLY_OPAQUE)
{ {
if (caveCullingEnabled if (caveCullingEnabled
// assume this data point is underground if it has no sky-light // assume this data point is underground if it has no sky-light
&& skyLight == LodUtil.MIN_MC_LIGHT && skyLight == LodUtil.MIN_MC_LIGHT
// ignore caves above a certain height to prevent floating islands from having walls underneath them // ignore caves above a certain height to prevent floating islands from having walls underneath them
&& topY < caveCullingMaxY && topY < caveCullingMaxY
// cave culling shouldn't happen when at the top of the world // cave culling shouldn't happen when at the top of the world
&& renderDataIndex != 0 && fullDataIndex != 0 && renderDataIndex != 0 && fullDataIndex != 0
// cave culling can't happen when at the bottom of the world // cave culling can't happen when at the bottom of the world
&& (fullDataIndex+1) < fullColumnData.size()) && (fullDataIndex + 1) < fullColumnData.size())
{ {
// we need to get the next sky/block lights because // we need to get the next sky/block lights because
// the air block here will always have a light of 0/0 due to only the top of the LOD's light being saved. // the air block here will always have a light of 0/0 due to only the top of the LOD's light being saved.
long nextFullData = fullColumnData.getLong(fullDataIndex+1); long nextFullData = fullColumnData.getLong(fullDataIndex + 1);
int nextSkyLight = FullDataPointUtil.getSkyLight(nextFullData); int nextSkyLight = FullDataPointUtil.getSkyLight(nextFullData);
if (nextSkyLight == LodUtil.MIN_MC_LIGHT if (nextSkyLight == LodUtil.MIN_MC_LIGHT
&& ColorUtil.getAlpha(lastColor) == 255) && ColorUtil.getAlpha(lastColor) == 255)
{ {
// replace the previous block with new bottom // replace the previous block with new bottom
long columnData = renderColumnData.get(renderDataIndex - 1); long columnData = renderColumnData.get(renderDataIndex - 1);
@@ -322,25 +337,58 @@ public class FullDataToRenderDataTransformer
// non-solid block check // // non-solid block check //
//=======================// //=======================//
if (ignoreNonCollidingBlocks boolean ignoreNonSolidBlock =
&& !block.isSolid() ignoreNonCollidingBlocks
&& !block.isLiquid() && !block.isSolid()
&& block.getOpacity() != LodUtil.BLOCK_FULLY_OPAQUE) && !block.isLiquid()
&& block.getOpacity() != LodUtil.BLOCK_FULLY_OPAQUE;
// merge snow into the block below it
if (snowLayerBlockStates.contains(block))
{ {
// sometimes a snow datapoint will be multiple blocks tall,
// in that case we just want to drop the top by 1
blockHeight -= 1;
if (blockHeight == 0)
{
// this snow block was entirely removed, just color the block below it
ignoreNonSolidBlock = true;
// snow is a special case where it should always tint the block
// below it, if not done grass will appear as gray
int snowColor = levelWrapper.getBlockColor(mutableBlockPos, biome, fullDataSource, block);
colorToApplyToNextBlock = ColorUtil.setAlpha(snowColor, 255);
}
}
if (ignoreNonSolidBlock)
{
int ignoredColor = levelWrapper.getBlockColor(mutableBlockPos, biome, fullDataSource, block);
int ignoredAlpha = ColorUtil.getAlpha(ignoredColor);
if (colorBelowWithAvoidedBlocks) if (colorBelowWithAvoidedBlocks)
{ {
int tempColor = levelWrapper.getBlockColor(mutableBlockPos, biome, fullDataSource, block);
// don't transfer the color when alpha is 0 // don't transfer the color when alpha is 0
// this prevents issues if grass is transparent // this prevents issues if grass is transparent
if (ColorUtil.getAlpha(tempColor) != 0) if (ignoredAlpha != 0)
{ {
colorToApplyToNextBlock = ColorUtil.setAlpha(tempColor,255); colorToApplyToNextBlock = ColorUtil.setAlpha(ignoredColor, 255);
skylightToApplyToNextBlock = skyLight;
blocklightToApplyToNextBlock = blockLight;
} }
} }
// Don't transfer the lighting when alpha is 0
// (the block below should have its own lighting).
if (ignoredAlpha != 0)
{
// Lighting is transferred even when "colorBelowWithAvoidedBlocks"
// is false, since otherwise the blocks underneath may have a light value of "0"
// which makes things look darker than they should.
// This can specifically manifest as grid lines on LOD borders
// (not entire sure why grid lines on LOD borders, maybe it has to do with the fact that those LODs aren't occluded?).
skylightToApplyToNextBlock = skyLight;
blocklightToApplyToNextBlock = blockLight;
}
// skip this non-colliding block // skip this non-colliding block
continue; continue;
} }
@@ -351,6 +399,20 @@ public class FullDataToRenderDataTransformer
{ {
// use this block's color // use this block's color
color = levelWrapper.getBlockColor(mutableBlockPos, biome, fullDataSource, block); color = levelWrapper.getBlockColor(mutableBlockPos, biome, fullDataSource, block);
// use the skylight override if present
if (skylightToApplyToNextBlock != -1)
{
skyLight = skylightToApplyToNextBlock;
// remove the override so we don't accidentally override the next datapoint
skylightToApplyToNextBlock = -1;
}
if (blocklightToApplyToNextBlock != -1)
{
blockLight = blocklightToApplyToNextBlock;
blocklightToApplyToNextBlock = -1;
}
} }
else else
{ {
@@ -368,7 +430,9 @@ public class FullDataToRenderDataTransformer
//=============================// //=============================//
// check if they share a top-bottom face and if they have same color // check if they share a top-bottom face and if they have same color
if (color == lastColor && bottomY + blockHeight == lastBottom && renderDataIndex > 0) if (color == lastColor
&& bottomY + blockHeight == lastBottom
&& renderDataIndex > 0)
{ {
//replace the previous block with new bottom //replace the previous block with new bottom
long columnData = renderColumnData.get(renderDataIndex - 1); long columnData = renderColumnData.get(renderDataIndex - 1);
@@ -396,21 +460,4 @@ public class FullDataToRenderDataTransformer
//================//
// helper methods //
//================//
/**
* Called in loops that may run for an extended period of time. <br>
* This is necessary to allow canceling these transformers since running
* them after the client has left a given world will throw exceptions.
*/
private static void throwIfThreadInterrupted() throws InterruptedException
{
if (Thread.interrupted())
{
throw new InterruptedException(FullDataToRenderDataTransformer.class.getSimpleName() + " task interrupted.");
}
}
} }
@@ -24,8 +24,6 @@ import java.util.List;
import com.seibel.distanthorizons.api.enums.config.EDhApiWorldCompressionMode; import com.seibel.distanthorizons.api.enums.config.EDhApiWorldCompressionMode;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep;
import com.seibel.distanthorizons.api.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.methods.events.abstractEvents.DhApiChunkProcessingEvent;
import com.seibel.distanthorizons.api.objects.data.DhApiChunk; import com.seibel.distanthorizons.api.objects.data.DhApiChunk;
import com.seibel.distanthorizons.api.objects.data.DhApiTerrainDataPoint; import com.seibel.distanthorizons.api.objects.data.DhApiTerrainDataPoint;
@@ -46,12 +44,12 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.Logger; import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
public class LodDataBuilder public class LodDataBuilder
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class); private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
private static final IBlockStateWrapper AIR = WRAPPER_FACTORY.getAirBlockStateWrapper(); private static final IBlockStateWrapper AIR = WRAPPER_FACTORY.getAirBlockStateWrapper();
@@ -76,7 +74,7 @@ public class LodDataBuilder
long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ); long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ);
FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos); FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos);
dataSource.isEmpty = false; dataSource.isEmpty = false; // this will be set to "true" if any blocks are found
// chunk updates always propagate up // chunk updates always propagate up
dataSource.applyToParent = true; dataSource.applyToParent = true;
@@ -118,7 +116,6 @@ public class LodDataBuilder
//==========================// //==========================//
EDhApiWorldCompressionMode worldCompressionMode = Config.Common.LodBuilding.worldCompression.get(); EDhApiWorldCompressionMode worldCompressionMode = Config.Common.LodBuilding.worldCompression.get();
boolean ignoreHiddenBlocks = (worldCompressionMode != EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS);
try try
{ {
@@ -142,7 +139,7 @@ public class LodDataBuilder
int columnZ = relBlockZ + chunkOffsetZ; int columnZ = relBlockZ + chunkOffsetZ;
// Get column data // Get column data
LongArrayList longs = dataSource.get(columnX, columnZ); LongArrayList longs = dataSource.getColumnAtRelPos(columnX, columnZ);
if (longs == null) if (longs == null)
{ {
longs = new LongArrayList(dataCapacity); longs = new LongArrayList(dataCapacity);
@@ -157,7 +154,7 @@ public class LodDataBuilder
IBlockStateWrapper currentBlockState = AIR; IBlockStateWrapper currentBlockState = AIR;
int mappedId = dataSource.mapping.addIfNotPresentAndGetId(currentBiome, currentBlockState); 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? // Determine lighting (we are at the height limit. There are no torches here, and sky is not obscured.)
byte blockLight = LodUtil.MIN_MC_LIGHT; byte blockLight = LodUtil.MIN_MC_LIGHT;
byte skyLight = LodUtil.MAX_MC_LIGHT; byte skyLight = LodUtil.MAX_MC_LIGHT;
@@ -247,6 +244,15 @@ public class LodDataBuilder
blockLight = newBlockLight; blockLight = newBlockLight;
skyLight = newSkyLight; skyLight = newSkyLight;
lastY = y; lastY = y;
// mark the data source as non-empty if we find any non-air blocks
if (dataSource.isEmpty
&& newBlockState != null
&& !newBlockState.isAir())
{
dataSource.isEmpty = false;
}
} }
} }
@@ -257,11 +263,6 @@ public class LodDataBuilder
dataSource.setSingleColumn(longs, columnX, columnZ, EDhApiWorldGenerationStep.LIGHT, worldCompressionMode); dataSource.setSingleColumn(longs, columnX, columnZ, EDhApiWorldGenerationStep.LIGHT, worldCompressionMode);
} }
} }
if (ignoreHiddenBlocks)
{
cullHiddenBlocks(dataSource, chunkOffsetX, chunkOffsetZ);
}
} }
catch (DataCorruptedException e) catch (DataCorruptedException e)
{ {
@@ -269,157 +270,9 @@ public class LodDataBuilder
return null; return null;
} }
LodUtil.assertTrue(!dataSource.isEmpty);
return dataSource; return dataSource;
} }
private static void cullHiddenBlocks(FullDataSourceV2 dataSource, int chunkOffsetX, int chunkOffsetZ)
{
for (int relZ = 1; relZ < LodUtil.CHUNK_WIDTH - 1; relZ++)
{
for (int relX = 1; relX < LodUtil.CHUNK_WIDTH - 1; relX++)
{
LongArrayList
centerColumn = dataSource.get(relX + chunkOffsetX, relZ + chunkOffsetZ),
posXColumn = dataSource.get(relX + chunkOffsetX + 1, relZ + chunkOffsetZ),
negXColumn = dataSource.get(relX + chunkOffsetX - 1, relZ + chunkOffsetZ),
posZColumn = dataSource.get(relX + chunkOffsetX, relZ + chunkOffsetZ + 1),
negZColumn = dataSource.get(relX + chunkOffsetX, relZ + chunkOffsetZ - 1);
int
centerIndex = centerColumn.size() - 1,
posXIndex = posXColumn.size() - 1,
negXIndex = negXColumn.size() - 1,
posZIndex = posZColumn.size() - 1,
negZIndex = negZColumn.size() - 1;
for (; centerIndex >= 0; centerIndex--)
{
long currentPoint = centerColumn.getLong(centerIndex);
// Translucent data points are not eligible to be culled.
if (isTranslucent(dataSource, currentPoint))
{
continue;
}
// the top segment should never be culled.
if (centerIndex == 0
|| isTranslucent(dataSource, centerColumn.getLong(centerIndex - 1))
)
{
continue;
}
// the bottom segment can sometimes be culled.
// assume it will not be seen from below,
// because this would imply the player is in the void.
if (centerIndex + 1 < centerColumn.size()
&& isTranslucent(dataSource, centerColumn.getLong(centerIndex + 1))
)
{
continue;
}
// the lowest/bedrock segment should not be culled
if (centerIndex + 1 == centerColumn.size())
{
continue;
}
posXIndex = checkOcclusion(dataSource, currentPoint, posXColumn, posXIndex);
if (posXIndex < 0)
{
posXIndex = ~posXIndex;
continue;
}
negXIndex = checkOcclusion(dataSource, currentPoint, negXColumn, negXIndex);
if (negXIndex < 0)
{
negXIndex = ~negXIndex;
continue;
}
posZIndex = checkOcclusion(dataSource, currentPoint, posZColumn, posZIndex);
if (posZIndex < 0)
{
posZIndex = ~posZIndex;
continue;
}
negZIndex = checkOcclusion(dataSource, currentPoint, negZColumn, negZIndex);
if (negZIndex < 0)
{
negZIndex = ~negZIndex;
continue;
}
// Current point is fully surrounded. remove it.
centerColumn.removeLong(centerIndex);
// Make the above data point cover the area that the current point used to occupy.
// The element that was at `centerIndex - 1` is still at that position even after removal of centerIndex.
long above = centerColumn.getLong(centerIndex - 1);
above = FullDataPointUtil.setBottomY(above, FullDataPointUtil.getBottomY(currentPoint));
above = FullDataPointUtil.setHeight(above, FullDataPointUtil.getHeight(currentPoint) + FullDataPointUtil.getHeight(above));
centerColumn.set(centerIndex - 1, above);
}
}
}
}
/**
checks if centerPoint is "covered" by opaque data points in adjacentColumn.
centerPoint counts as covered if, and only if, for all Y levels in its height range,
there exists an opaque data point in adjacentColumn which overlaps with that Y level.
@param source used to lookup blocks (and their opacities) based on their IDs.
@param centerPoint the point being checked to see if it's fully covered.
@param adjacentColumn the data points which might cover centerPoint.
@param adjacentIndex the starting index in adjacentColumn to start scanning at.
indices greater than adjacentIndex have already been checked and confirmed to
not overlap or only overlap partially with centerPoint's Y range.
@return if centerPoint is covered, returns the index of the segment which finishes covering it.
the start of the covering may be a smaller index. in this case, the returned index may be used
as the adjacentIndex provided to this method on the next iteration which yields a new centerPoint.
if centerPoint is NOT covered, returns the bitwise negation of the index of the
segment which did not cover it. this guarantees that the returned value is negative.
the caller should check for negative return values and manually un-negate them to proceed with the loop.
in other words, this function returns the index of the next adjacent data
point to use in the loop, AND a boolean indicating whether or not the
centerPoint is covered; both are packed into the same int, and returned.
*/
private static int checkOcclusion(FullDataSourceV2 source, long centerPoint, LongArrayList adjacentColumn, int adjacentIndex)
{
int bottomOfCenter = FullDataPointUtil.getBottomY(centerPoint);
int topOfCenter = bottomOfCenter + FullDataPointUtil.getHeight(centerPoint);
for (; adjacentIndex >= 0; adjacentIndex--)
{
long adjacentPoint = adjacentColumn.getLong(adjacentIndex);
int topOfAdjacent = FullDataPointUtil.getBottomY(adjacentPoint) + FullDataPointUtil.getHeight(adjacentPoint);
if (topOfAdjacent <= bottomOfCenter)
{
continue;
}
else if (isTranslucent(source, adjacentPoint))
{
return ~adjacentIndex;
}
else if (topOfAdjacent >= topOfCenter)
{
return adjacentIndex;
}
}
throw new LodUtil.AssertFailureException("Adjacent column ends before center column does.");
}
private static boolean isTranslucent(FullDataSourceV2 source, long point) {
return source.mapping.getBlockStateWrapper(FullDataPointUtil.getId(point)).getOpacity() < LodUtil.BLOCK_FULLY_OPAQUE;
}
/** @throws ClassCastException if an API user returns the wrong object type(s) */ /** @throws ClassCastException if an API user returns the wrong object type(s) */
@@ -444,21 +297,34 @@ public class LodDataBuilder
for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++) for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++)
{ {
List<DhApiTerrainDataPoint> columnDataPoints = apiChunk.getDataPoints(relBlockX, relBlockZ); List<DhApiTerrainDataPoint> columnDataPoints = apiChunk.getDataPoints(relBlockX, relBlockZ);
LodDataBuilder.correctDataColumnOrder(columnDataPoints);
// mark the data source as non-empty if we find any non-air blocks
if (dataSource.isEmpty)
{
for (int i = 0; i < columnDataPoints.size(); i++)
{
DhApiTerrainDataPoint dataPoint = columnDataPoints.get(i);
if (dataPoint.blockStateWrapper != null
&& !dataPoint.blockStateWrapper.isAir())
{
dataSource.isEmpty = false;
break;
}
}
}
LodDataBuilder.putListInTopDownOrder(columnDataPoints);
if (runAdditionalValidation) if (runAdditionalValidation)
{ {
validateOrThrowApiDataColumn(columnDataPoints); validateOrThrowApiDataColumn(columnDataPoints);
} }
LongArrayList packedDataPoints = convertApiDataPointListToPackedLongArray(columnDataPoints, dataSource, apiChunk.bottomYBlockPos); LongArrayList packedDataPoints = convertApiDataPointListToPackedLongArray(columnDataPoints, dataSource, apiChunk.bottomYBlockPos, runAdditionalValidation);
// TODO add the ability for API users to define a different compression mode
// or add a "unkown" compression mode
dataSource.setSingleColumn( dataSource.setSingleColumn(
packedDataPoints, packedDataPoints,
relBlockX + relSourceBlockX, relBlockZ + relSourceBlockZ, relBlockX + relSourceBlockX, relBlockZ + relSourceBlockZ,
EDhApiWorldGenerationStep.LIGHT, EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS); EDhApiWorldGenerationStep.LIGHT, EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS);
dataSource.isEmpty = false;
} }
} }
return dataSource; return dataSource;
@@ -472,41 +338,97 @@ public class LodDataBuilder
/** @see FullDataPointUtil */ /** @see FullDataPointUtil */
public static LongArrayList convertApiDataPointListToPackedLongArray( public static LongArrayList convertApiDataPointListToPackedLongArray(
@Nullable List<DhApiTerrainDataPoint> columnDataPoints, FullDataSourceV2 dataSource, @Nullable List<DhApiTerrainDataPoint> topDownColumnDataPoints, FullDataSourceV2 dataSource,
int bottomYBlockPos) throws DataCorruptedException int bottomYBlockPos, boolean runAdditionalValidation) throws DataCorruptedException
{ {
// this null check does 2 nice things at the same time: if (topDownColumnDataPoints == null
// if columnDataPoints is null, || topDownColumnDataPoints.size() == 0)
// then packedDataPoints will be of length 0
// AND the below loop won't run.
int size = (columnDataPoints != null) ? columnDataPoints.size() : 0;
// TODO make missing air LODs
// TODO merge duplicate datapoints
LongArrayList packedDataPoints = new LongArrayList(new long[size]);
for (int index = 0; index < size; index++)
{ {
DhApiTerrainDataPoint dataPoint = columnDataPoints.get(index); return new LongArrayList(0);
}
// array to store data
int size = topDownColumnDataPoints.size();
LongArrayList packedDataPoints = new LongArrayList(size);
packedDataPoints.clear();
if (runAdditionalValidation)
{
// check for missing data
int lastTopY = Integer.MAX_VALUE;
for (int i = 0; i < size; i++)
{
DhApiTerrainDataPoint apiDataPoint = topDownColumnDataPoints.get(i);
if (lastTopY != apiDataPoint.topYBlockPos
// the first index won't have a lastTopY value
&& i != 0)
{
throw new DataCorruptedException("LOD data has a gap between ["+lastTopY+"] and ["+apiDataPoint.bottomYBlockPos+"]. Empty areas should be filled with air datapoints so light propagates correctly.");
}
lastTopY = apiDataPoint.bottomYBlockPos;
}
}
// go through data from top down
long lastDataPoint = FullDataPointUtil.EMPTY_DATA_POINT;
for (int i = 0; i < size; i++)
{
DhApiTerrainDataPoint apiDataPoint = topDownColumnDataPoints.get(i);
int id = dataSource.mapping.addIfNotPresentAndGetId( int thisId = dataSource.mapping.addIfNotPresentAndGetId(
(IBiomeWrapper) (dataPoint.biomeWrapper), (IBiomeWrapper) (apiDataPoint.biomeWrapper),
(IBlockStateWrapper) (dataPoint.blockStateWrapper) (IBlockStateWrapper) (apiDataPoint.blockStateWrapper)
); );
int thisHeight = (apiDataPoint.topYBlockPos - apiDataPoint.bottomYBlockPos);
packedDataPoints.set(index, FullDataPointUtil.encode( int lastId = FullDataPointUtil.getId(lastDataPoint);
id, byte lastBlockLight = (byte)FullDataPointUtil.getBlockLight(lastDataPoint);
dataPoint.topYBlockPos - dataPoint.bottomYBlockPos, byte lastSkyLight = (byte)FullDataPointUtil.getSkyLight(lastDataPoint);
dataPoint.bottomYBlockPos - bottomYBlockPos,
(byte) (dataPoint.blockLightLevel),
(byte) (dataPoint.skyLightLevel) // if the ID and light are the same, merge the height
)); if (thisId == lastId
&& apiDataPoint.blockLightLevel == lastBlockLight
&& apiDataPoint.skyLightLevel == lastSkyLight
// the first index should always be added to the list
&& i != 0 )
{
// add adjacent height
int lastHeight = FullDataPointUtil.getHeight(lastDataPoint);
int newHeight = (lastHeight + thisHeight);
lastDataPoint = FullDataPointUtil.setHeight(lastDataPoint, newHeight);
// subtract bottom Y
int lastBottomY = FullDataPointUtil.getBottomY(lastDataPoint);
int newBottomY = lastBottomY - thisHeight;
lastDataPoint = FullDataPointUtil.setBottomY(lastDataPoint, newBottomY);
packedDataPoints.set(packedDataPoints.size()-1, lastDataPoint);
}
else
{
// data changed, create a new datapoint
long dataPoint = FullDataPointUtil.encode(
thisId,
thisHeight,
apiDataPoint.bottomYBlockPos - bottomYBlockPos,
(byte) (apiDataPoint.blockLightLevel),
(byte) (apiDataPoint.skyLightLevel)
);
lastDataPoint = dataPoint;
packedDataPoints.add(dataPoint);
}
} }
return packedDataPoints; return packedDataPoints;
} }
/** also corrects the order if it's backwards */ public static void putListInTopDownOrder(List<DhApiTerrainDataPoint> dataPoints)
public static void correctDataColumnOrder(List<DhApiTerrainDataPoint> dataPoints)
{ {
// order doesn't need to be checked if there is 0 or 1 items // order doesn't need to be checked if there is 0 or 1 items
if (dataPoints.size() > 1) if (dataPoints.size() > 1)

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