Compare commits

..

340 Commits

Author SHA1 Message Date
s809 5b746a9534 Incomplete instance based config 2024-09-20 19:49:02 +05:00
James Seibel f7bf05b62f Update network semaphore comments 2024-09-17 07:40:28 -05:00
s809 840b0a7fe2 Bump protocol version because of removed InvalidSectionPosException 2024-09-17 12:41:46 +05:00
s809 5369bf628a Fix naming of some things and comments 2024-09-17 09:42:22 +05:00
James Seibel 79d2269218 Fix very high file handler jobs and pool some render data sources 2024-09-16 20:35:57 -05:00
James Seibel f21c791269 fix comment 2024-09-15 21:15:57 -05:00
James Seibel 11e58eecda Up API version 3.0.1 -> 4.0.0 2024-09-15 20:36:10 -05:00
James Seibel fbf13833a0 Up version 2.2.2 -> 2.3.0 2024-09-15 20:35:54 -05:00
James Seibel 2528f4a725 Merge server side branch and refactor 2024-09-15 20:35:38 -05:00
James Seibel 6d6cbd8a44 add more getDimensinoName() calls and minor cleanup 2024-09-11 17:11:55 -05:00
James Seibel 2fe3c261b0 Add DhClientWorld F3 debug string prep 2024-09-11 17:05:57 -05:00
James Seibel 5086f40d03 Add commit info to F3 screen 2024-09-11 17:04:18 -05:00
James Seibel 7766c49cbd Add DhChunkPos Vec3d constructor 2024-09-11 07:41:56 -05:00
James Seibel c6d86cfa3b remove unneeded IDimensionTypeWrapper.getTeleportationScale() 2024-09-11 07:40:44 -05:00
James Seibel 935cfec3d4 Add network compression thread config and pools 2024-09-11 07:37:39 -05:00
James Seibel ce2e64dc7e ClientOnlySaveStructure rename getDhDataFoldersForDimension -> getDhDataFoldersForLevel 2024-09-11 07:31:24 -05:00
James Seibel 15774ffe2a Only attempt to set Java Swing headless on clients 2024-09-11 07:25:01 -05:00
James Seibel 218cb04696 Don't load the client level in shouldLodsRender()
This might cause issues with the Replay mod, but we'll see
2024-09-11 07:20:29 -05:00
James Seibel 45fc36543b Add ILevelWrapper.getDimensionName() 2024-09-11 07:14:21 -05:00
James Seibel 554bb89690 remove unused message classes 2024-09-10 21:46:47 -05:00
James Seibel 2aa048b0cb Don't send chat messages on dedicated servers for configBasedLogger 2024-09-10 21:46:16 -05:00
James Seibel 570619b114 Ignore config preset UI updating when on the sever 2024-09-10 21:17:50 -05:00
James Seibel fb3e47ec3f Add FullDataSourceRepo.getTimestampForPos() 2024-09-10 20:46:18 -05:00
James Seibel 0f27dd79d7 minor refactoring and style cleanup 2024-09-10 07:24:56 -05:00
James Seibel abe0e284aa Remove unneeded networking classes 2024-09-10 07:15:37 -05:00
James Seibel 7a97b9dcbf Fix low quality LODs not loading when flying in a new straight line 2024-09-09 07:41:12 -05:00
James Seibel 23c98e2253 Clean up LodRenderSection async loading logic 2024-09-09 07:37:08 -05:00
James Seibel 06cce40ac6 hide attempting to... warnings for closed databases 2024-09-09 07:36:19 -05:00
s809 7cd1a37914 Restore ordering of session config entries 2024-09-08 21:21:38 +05:00
s809 8ca2052748 Refactor session config 2024-09-08 19:05:42 +05:00
James Seibel ce4259d98f add brown mushrooms to the list of ignored blocks to fix swamp issues 2024-09-07 14:22:41 -05:00
James Seibel d96ba5ae54 Add faster sky light engine from Builderb0y
Closes !67
2024-09-07 12:07:48 -05:00
s809 576d0f5666 Fix crash on F3 when commit hash was failed to retrieve 2024-09-07 21:47:43 +05:00
s809 877588ebed Fix rare NPE when receiving LODs 2024-09-07 21:47:02 +05:00
James Seibel 3bee25053f Add missing LightingTestChunkWrapper methods 2024-09-06 21:55:48 -05:00
James Seibel a75d3ec5b0 Fix unit test compiling 2024-09-06 19:36:32 -05:00
James Seibel d5222ed20f Closes #805 (Hide "Distant Horizons overloaded")
Also update chunks closest to the player first
2024-09-06 18:22:57 -05:00
s809 875b5fffcc Fix client crashing 2024-09-05 23:45:04 +05:00
James Seibel d03a887620 Add BuilderB0y's getBlockState optimization 2024-09-05 07:50:18 -05:00
s809 6293cc8c27 Prefill levelKeyPrefix in new worlds 2024-09-05 14:22:00 +05:00
James Seibel 847cfa3ca9 Add current size to rolling average 2024-09-04 16:40:17 -05:00
s809 0361e5c69b Fix compilation 2024-09-04 22:53:02 +05:00
s809 654355c991 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core into serverside 2024-09-04 18:22:47 +05:00
James Seibel 4712a77d00 Up version number 2.2.1 -> 2.2.2-dev 2024-09-04 06:58:59 -05:00
James Seibel 57c5b2d5fc Up version 2.2.1-dev -> 2.2.1 and API 3.0.0 -> 3.0.1 2024-09-04 06:58:20 -05:00
s809 a2949b8124 Change some string & remove unused config item 2024-09-03 16:52:10 +05:00
James Seibel 50c5701836 Fix referencing unavailable GLFW methods for MC 1.18.2 and lower 2024-09-02 08:00:43 -05:00
s809 6fe0477ca7 Limit number of retries on request errors
Add a delay if rate limit is hit
2024-09-02 15:31:39 +05:00
James Seibel 18e075538d Fix LODs flashing when moving and improve LodQuadTree code 2024-09-01 21:10:22 -05:00
James Seibel b00e8a08e9 add annotations to QuadTree 2024-09-01 21:09:50 -05:00
James Seibel ac4ab11a74 Fix LodRenderSection and QuadNode toString methods 2024-09-01 21:09:17 -05:00
James Seibel c26631db57 Cull beacons based on X/Z distance instead of 3D distance 2024-09-01 17:28:08 -05:00
James Seibel 1daa06fff4 Disable instanced rendering on Mac when Sodium is present
Closes !793 (Generic Rendering crashes with Sodium on M1 Mac)
2024-09-01 17:02:43 -05:00
James Seibel f3ef6f25f4 Fix some beacon rendering/updating issues 2024-09-01 16:36:37 -05:00
James Seibel ec012d9fd6 Fix glass panes not affecting beacon colors 2024-09-01 15:04:24 -05:00
James Seibel fc90cf3377 add disableUnchangedChunkCheck config 2024-08-31 22:11:25 -05:00
James Seibel e1e42d1caf Fix cloud color not matching MC 2024-08-31 21:55:54 -05:00
James Seibel 95ce29e355 Update all pos objects to use getters to match with DhBlockPos
I'd prefer not to need getters/setters since it's cleaner to just call pos.x, but that doesn't allow for immutable/mutable distinction.
2024-08-31 20:50:22 -05:00
James Seibel 0fd818b077 Allow users to re-activate DH rendering if a rendering error is thrown 2024-08-31 20:20:51 -05:00
James Seibel ba59daf747 Add beacon nearby culling 2024-08-31 20:20:24 -05:00
James Seibel b7d94c2ed1 Fix RenderableBoxGroup not supporting clear() 2024-08-31 20:06:04 -05:00
James Seibel 7a057a8d53 fix Lod builder not showing the correct stacktrace in Intellij 2024-08-31 15:04:30 -05:00
James Seibel 49c6ab97a9 Remove unused threadpool 2024-08-31 15:04:12 -05:00
James Seibel ed0d80b37e Reduce deplayed LOD modified save from 2 sec -> 500 ms
This should make some LOD update operations appear faster
2024-08-31 15:01:37 -05:00
James Seibel 9768728c92 Add RollingAverage object 2024-08-31 14:58:08 -05:00
James Seibel 0c68544f2f Fix DhLightingEngine putting lights at relative (0,0) and add debug logic 2024-08-31 12:45:39 -05:00
s809 048b36f80d Add comment 2024-08-31 20:50:50 +05:00
s809 40f902e2f5 Delay loading first level when on server 2024-08-31 20:46:58 +05:00
James Seibel b1f154a0ea Add DhBlockPosMutable and make the original immutable
This is to prevent issues with some methods accidentally mutating shared positions
2024-08-30 07:35:59 -05:00
James Seibel 628c9b071f DhBlockPos cleanup 2024-08-29 20:05:26 -05:00
James Seibel ed39b6181f Fix DH beacon detection logic mutating input block pos
alternate title: Fix DH beacon detection logic breaking the lighting engine
2024-08-29 19:54:36 -05:00
James Seibel 1d6d712483 Fix LODs not updating in the nether or when blocks are changed underground 2024-08-29 07:32:55 -05:00
s809 62ddb46674 Restore LOD fetching by distance 2024-08-29 17:09:08 +05:00
James Seibel 73c4f0ffcd Add a quick DH cloud UI config 2024-08-28 07:16:20 -05:00
s809 375cd44cbd Do not start generator until server responds with config 2024-08-27 16:35:22 +05:00
s809 ce057525d3 Show incompatible protocol version in F3 2024-08-27 16:02:15 +05:00
s809 01c879951c Fix sync on login 2024-08-26 16:53:59 +05:00
s809 4f19f05f3b Change naming of rate limit related items 2024-08-26 00:39:28 +05:00
s809 47f68c7ed3 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core into serverside 2024-08-24 02:58:17 +05:00
s809 39db421aa1 Remove mentions of generation task priorities 2024-08-23 17:28:15 +05:00
s809 f4b0c08822 Fix network compression pool not shutting down 2024-08-23 17:11:32 +05:00
s809 f17c3fa267 "Fix" buffer release errors in FullDataPayload 2024-08-23 14:16:20 +05:00
James Seibel 48d1005be6 Up version number 2.2.0 -> 2.2.1-dev 2024-08-20 19:16:18 -05:00
s809 81e6f55dbf Use same packet resource for all versions 2024-08-19 17:55:59 +05:00
s809 b5e2019d28 Add logging of request group lifecycle 2024-08-15 15:46:24 +05:00
s809 84dca85447 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core into serverside 2024-08-11 20:59:23 +05:00
s809 8c3e8136be Do not clear keyed level on close event 2024-08-10 23:53:22 +05:00
s809 606c157958 Make encode/decode error handling work correctly 2024-08-10 23:26:04 +05:00
s809 ba3677b641 Make error handling somewhat work 2024-08-10 19:49:33 +05:00
s809 dee13a4ec4 Merge branch 'serverside-experimental/prevent-disconnects' into serverside 2024-08-07 22:20:29 +05:00
s809 71e4cd6272 Fix real-time updates 2024-08-06 23:12:13 +05:00
s809 82c5de7dfe Fix Neoforge not being able to connect to vanilla servers 2024-08-05 14:51:17 +05:00
s809 9d11733444 Fix dimension switching (untested) 2024-08-03 15:42:43 +05:00
s809 e62c6a5c55 [skip ci] Prevent disconnects on encode/decode/handle errors 2024-08-01 22:19:10 +05:00
s809 93b57ae2e1 Increase defaults for network compression threads 2024-07-30 10:52:27 +05:00
s809 e5033a0c0f Use FullDataPayload instead of reusing messages 2024-07-29 13:10:04 +05:00
s809 32abe15b09 Add build number on F3 screen 2024-07-28 20:19:38 +05:00
s809 2b65e33aa7 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core into serverside 2024-07-28 19:44:25 +05:00
s809 bc6ab6c840 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core into serverside 2024-07-28 19:01:28 +05:00
s809 ccbb071704 Reapply "Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core into serverside"
This reverts commit d26cb41048.
2024-07-28 17:05:58 +05:00
s809 d26cb41048 Revert "Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core into serverside"
This reverts commit a15aaa573d, reversing
changes made to 83c1a2fd63.
2024-07-27 23:06:34 +05:00
s809 a15aaa573d Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core into serverside 2024-07-27 21:43:21 +05:00
s809 83c1a2fd63 Fix handler registration 2024-07-20 23:56:24 +05:00
s809 0e904a388c Raise full data chunk size limit 2024-07-16 23:56:48 +05:00
s809 3723137fea Remove unused method 2024-07-16 23:52:28 +05:00
s809 71e54cc9ab Up protocol version 2024-07-16 23:51:30 +05:00
s809 b4cf962a85 Fix event handler registration 2024-07-16 23:40:20 +05:00
s809 1a30f240ef Client-side chunk handling 2024-07-16 17:49:09 +05:00
s809 b3b5ac63c8 Server-side logic is pretty much finished 2024-07-16 00:27:22 +05:00
s809 16abc0faa1 [skip ci] Initial chunked full data message payloads 2024-07-15 07:57:20 +05:00
s809 778c2f894e Fix level handling for real time updates 2024-07-12 23:11:36 +05:00
s809 2617cd294d Add cache for ignoring responses for cancelled requests 2024-07-11 23:06:23 +05:00
s809 43b421042d Add a comment to semaphores 2024-07-11 22:08:10 +05:00
s809 af69678545 Fix player tracking on server 2024-07-10 23:51:39 +05:00
s809 85341f7a6d Add missing semaphore release 2024-07-09 18:06:16 +05:00
s809 9e0edd0cf3 Prevent request cancellation deadlock 2024-07-09 14:47:45 +05:00
s809 84e90a7a9b Use dedicated thread pool for data compression 2024-07-08 23:07:58 +05:00
s809 48a8cdc365 Disable parent update propagation 2024-07-07 19:46:47 +05:00
s809 171e6b9bcd Reset state of level detection on world exit 2024-07-07 01:49:44 +05:00
s809 7e48c49e33 Make data source encoding lazy and move it off server thread 2024-07-01 00:15:54 +05:00
s809 1b48d61d3f Rename CloseEvent 2024-06-30 22:35:41 +05:00
s809 47541fa99c Move fixing the dimension name into even more correct place 2024-06-28 23:18:24 +05:00
s809 13638cedee Move fixing the dimension name into correct place 2024-06-28 19:28:36 +05:00
s809 4cdf31cfe8 Fix paths unable to be created on Windows 2024-06-28 17:25:09 +05:00
s809 3ff9a93066 Fix test compilation failing 2024-06-28 15:52:39 +05:00
s809 083a036666 Use level's ResourceLocation instead of dimension type's 2024-06-28 15:28:33 +05:00
s809 a0efe44e8d Clean up package structure 2024-06-26 23:57:37 +05:00
s809 8d110e22dd Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core into refactor/remove-tcp-connection 2024-06-26 15:13:03 +05:00
s809 4d6e11fdeb Fix compilation 2024-06-26 14:55:21 +05:00
s809 454b15ff60 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core into refactor/remove-tcp-connection 2024-06-26 14:54:00 +05:00
s809 d2f4972693 Put messages received before player joining into queue 2024-06-16 00:45:16 +05:00
s809 acb299530d Avoid reloading levels when unnecessary 2024-06-14 16:25:20 +05:00
s809 7c705015e6 Use level keys 2024-06-13 16:44:37 +05:00
s809 bcb21be848 Show dimension names 2024-06-10 23:39:57 +05:00
s809 562594de2f Better error on invalid dimension 2024-06-09 22:01:30 +05:00
s809 03a00bb7ca Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core into refactor/remove-tcp-connection 2024-06-09 21:18:08 +05:00
s809 dc3aa939db Fix logging errors 2024-06-08 00:07:24 +05:00
s809 39391b944f Add toString to messages & fix incorrect call 2024-06-06 21:25:19 +05:00
s809 a6fc8f1702 Fix compilation 2024-06-02 20:00:09 +05:00
s809 b0777789fd Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core into refactor/remove-tcp-connection 2024-06-02 19:58:53 +05:00
s809 24a2a48deb Increase section reload delay 2024-06-01 19:49:23 +05:00
s809 e08faa7943 Fix requests breaking on rejoining 2024-06-01 12:17:04 +05:00
s809 a000afbc60 Kinda works, rejoining is broken 2024-05-29 23:36:34 +05:00
s809 a3e34c9738 [skip ci] Still a mess 2024-05-27 22:01:57 +05:00
s809 1f63bdf124 [skip ci] Somewhere 2024-05-22 23:18:18 +05:00
s809 44205664b5 [skip ci] Incomplete 2024-05-21 22:54:57 +05:00
s809 40d019d7e8 Merge remote-tracking branch 'origin/main' 2024-05-18 22:14:19 +05:00
s809 96755e6174 Move overrides in LAN to config 2024-05-18 22:09:19 +05:00
s809 bd5866787f Update Config.java 2024-05-17 12:41:09 +00:00
s809 d9651bbd27 Merge branch 'main' into 'main'
rewrite config comments

See merge request s809/distant-horizons-core!1
2024-05-17 12:39:54 +00:00
Yeshi0 a1f4442a53 rewrite config comments 2024-05-14 20:16:44 +02:00
s809 e6b140245b Fix awt dependency error 2024-05-12 17:34:58 +05:00
s809 08336027b2 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core into feature/2.0.4 2024-05-11 19:16:01 +05:00
s809 543b5ed49a Fix dimension switching (at cost of breaking immersive portals) 2024-05-10 23:56:07 +05:00
s809 3d86c5c5ee Fix generated sections not appearing 2024-05-10 21:13:40 +05:00
s809 8e1009ab3f Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core into feature/2.0.4 2024-05-05 19:51:21 +05:00
s809 551a5f1a72 Fix updates 2024-05-05 16:55:15 +05:00
s809 2664f96e0f Generation works, updates don't 2024-05-03 22:56:01 +05:00
s809 dde2bcc947 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2024-05-02 21:47:35 +05:00
s809 3e3e97385b Fix 1.16.5 and 1.17.1 builds 2024-04-17 21:02:14 +05:00
s809 05fba12038 Prevent generation when config is not acknowledged by server 2024-04-16 21:55:02 +05:00
s809 c1f2803c56 Add missing newlines for override configs 2024-04-16 21:39:56 +05:00
s809 7006c669f0 More verbose invalid level error 2024-04-16 21:38:37 +05:00
s809 7cd0c956f6 Fix IPv6 addresses not being properly handled 2024-04-16 21:27:26 +05:00
s809 7c33dda11b Add FlushConsolidationHandler 2024-04-16 20:32:50 +05:00
s809 5f9f0f23b8 Fix integer overflows 2024-04-12 23:14:58 +05:00
s809 284191a904 Fix Neoforge 2024-04-12 22:36:04 +05:00
s809 0ad60cd5d9 Merge branch 'feature/plugin-channel' 2024-04-02 22:34:45 +05:00
s809 8485d1585a Remove localhost from check 2024-04-02 22:27:36 +05:00
s809 1cd2f75dd2 Fix compilation 2024-04-01 21:52:20 +05:00
s809 bdc4fa4477 Fix errors when on vanilla server 2024-04-01 00:55:15 +05:00
s809 ff7720a8d6 Adjust server port change behavior 2024-04-01 00:07:32 +05:00
s809 6829709123 Fix reconnection logic 2024-03-30 23:40:05 +05:00
s809 0d355f675a Incomplete 2024-03-26 00:30:57 +05:00
s809 af2f0e8582 Fix Forge 2024-03-18 21:37:58 +05:00
s809 3362570123 Add thread pool task buildup limiter 2024-03-16 12:28:38 +05:00
s809 e008eb8a13 Server side plugin networking
Untested port for Forge
2024-03-14 21:46:23 +05:00
s809 1f438d8f87 Fix world setting 2024-03-10 21:09:52 +05:00
s809 278ae04532 Initial buggy plugin channel support 2024-03-05 22:09:30 +05:00
s809 2d4f033891 Do not wait for world gen tasks to stop while shutting down queue 2024-02-25 18:05:45 +05:00
s809 1fd85f2249 Show server side messages in F3 on disconnect 2024-02-24 22:48:24 +05:00
s809 d966697ecb Change some wording 2024-02-24 21:37:54 +05:00
s809 2e6c0ba356 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2024-02-22 23:06:21 +05:00
s809 ebb47990f4 Fix immersive portals 2024-02-15 23:15:01 +05:00
s809 15fe39c1bb Fix connection exception handling 2024-02-13 22:03:55 +05:00
s809 7bb0ec3148 Fix compilation 2024-02-12 21:39:30 +05:00
s809 6fcfacd346 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2024-02-12 21:38:58 +05:00
s809 48212ba746 Add another config option
Fix rate limiting issues (kinda)
2024-02-11 19:39:35 +05:00
s809 7bf9ce141a Avoid doing requests on join 2024-02-10 17:06:49 +05:00
s809 2353bd7545 Fix compilation 2024-02-07 21:03:37 +05:00
s809 02ddf5773c Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2024-02-07 20:49:49 +05:00
s809 3e37e9ee08 Remove unused packets 2024-02-04 21:57:57 +05:00
s809 b1b907bfe5 Silence a warning 2024-02-04 21:33:21 +05:00
s809 d347dbd222 Fix typo 2024-02-04 21:29:21 +05:00
s809 44527630da Fix queue not filled when generation is toggled 2024-02-04 21:00:02 +05:00
s809 df6ac6d740 Add connection state on F3 screen
Hide refresh queue from debug when disabled
2024-02-04 20:20:57 +05:00
s809 2ce08f8957 Merge remote-tracking branch 'origin/main' 2024-02-04 02:36:04 +05:00
s809 1baa666d23 Add priority distance limiting by ratio 2024-02-04 02:34:53 +05:00
s809 e5e4551038 Some logging changes 2024-02-03 23:02:31 +05:00
s809 0cfbe09558 Use ConfigBasedLogger
Add sub-sectioning to server networking section
2024-02-03 22:24:34 +05:00
s809 7d72e82325 Change a comment in Config 2024-02-03 20:17:21 +05:00
s809 3f823c58cc Fix exception while saving new data 2024-02-02 05:48:36 +00:00
s809 3932ea21c2 Limit rate+concurrency instead of only concurrency
Rename post-relog update to Login sync
2024-01-29 22:36:13 +05:00
s809 c9d426e581 Refactor ILevelRelatedMessage and RemotePlayerConnectionHandler 2024-01-28 22:37:50 +05:00
s809 61c516df1d Post-relog updates 2024-01-27 19:45:30 +05:00
s809 7e2019abd4 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2024-01-21 17:20:49 +05:00
s809 f0f3614b9d Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2024-01-16 20:17:43 +05:00
s809 573a284580 Fix generation 2024-01-13 19:44:48 +05:00
s809 310c890474 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2024-01-13 16:51:12 +05:00
s809 8a309b56d9 Fix crash when changing config while disconnected 2024-01-13 16:08:49 +05:00
s809 adc3f15c97 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2024-01-08 17:50:00 +05:00
s809 962f523af9 Fix 1.16.5 and 1.17.1 builds 2024-01-05 22:37:27 +05:00
s809 dbc1ad4cb7 Add names to network threads 2024-01-05 22:08:37 +05:00
s809 8b0f6a4414 Daemonize timers 2024-01-05 22:08:16 +05:00
s809 502aaf6a8a Add a config to delay generation requests, for cases when gen tasks are not filled properly yet 2023-12-28 18:58:50 +05:00
s809 5c644fbf5b Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-12-24 19:16:03 +05:00
s809 da60ca7560 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-12-21 19:17:51 +05:00
s809 a5a56c7eb7 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-12-21 11:47:51 +05:00
s809 ff8b1f24d0 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-12-20 23:18:57 +05:00
s809 904c1a7d71 Add protection against hang on shutdown 2023-12-19 23:20:23 +05:00
s809 02aca6f044 Allow section state requests to be controlled separately 2023-12-19 22:52:19 +05:00
s809 b4ea632b93 Add kick trigger on repeated rate limit hitting
Remove some unused code
2023-12-18 23:10:39 +05:00
s809 a2cc1c2913 Revert "Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core"
This reverts commit 522a799516, reversing
changes made to 04e43ebec8.
2023-12-18 22:15:34 +05:00
s809 522a799516 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-12-18 19:17:14 +05:00
s809 04e43ebec8 Remove current post-relog update feature 2023-12-18 19:13:57 +05:00
s809 077d72c23b Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-12-15 20:29:15 +05:00
s809 cb95c45d76 Fix compilation 2023-12-06 22:06:37 +05:00
s809 a9de079132 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-12-06 21:09:19 +05:00
s809 166cc55e3c Abstract away ChannelHandlerContext from user code 2023-11-27 23:46:05 +05:00
s809 f36bffa4b7 Add missing config entries in chat commands 2023-11-26 21:32:50 +05:00
s809 6da1e75dad Fix server side generation setting being ignored 2023-11-25 18:04:25 +05:00
s809 82d66ca392 Basic chat commands 2023-11-25 17:44:51 +05:00
s809 e98cf1f2b5 Hide all unrelated config entries 2023-11-23 20:58:09 +05:00
s809 77bd333fff Fix RejectedExecutionException when leaving server 2023-11-20 17:18:05 +05:00
s809 b791a185a2 Deduplicate code in MultiplayerConfigChangeListener 2023-11-20 16:54:17 +05:00
s809 c64d7fedd2 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-11-19 22:56:56 +05:00
s809 d2f5c02238 Bandaid fix to prevent duplicate section requests 2023-11-12 17:17:15 +05:00
s809 9cfcf37fb3 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-11-11 19:20:33 +05:00
s809 84d36df388 Do not clean up rendering on a dedicated server 2023-11-04 16:56:15 +05:00
s809 880abd0124 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-11-03 21:05:27 +05:00
s809 72274bfd7d Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-10-30 15:26:29 +05:00
s809 9640169be9 Make post-relog update independent from generation toggle 2023-10-23 23:18:52 +05:00
Steveplays28 9daf0c7317 chore: Add missing language entries for the server networking config 2023-10-20 14:07:42 +02:00
Steveplays28 a4df5a8ed8 style: Rename renderDistance to renderDistanceRadius to stay in sync with the main config 2023-10-20 14:03:19 +02:00
Steveplays28 df3d20f94e fix: Fix getting LOD render distance radius from the config
This config option was renamed recently, to fix LODs not rendering out to the LOD render distance border. Also removed an unused import.
2023-10-20 13:49:55 +02:00
Steveplays28 f3b40f51a2 Merge branch 'main-upstream'
# Conflicts:
#	core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataFileHandler.java
#	core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java
#	core/src/main/java/com/seibel/distanthorizons/core/level/DhServerLevel.java
2023-10-20 13:41:33 +02:00
s809 e3d7598501 Fix compilation 2023-10-14 22:13:01 +05:00
s809 596b822a5d Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-10-14 22:09:15 +05:00
s809 e9e7ac48b1 Fix compilation 2023-09-28 23:28:18 +05:00
s809 2e2254fbbd Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-09-28 22:54:35 +05:00
s809 8bda3351b8 Make worldGenLoopingQueue lock free 2023-09-27 21:40:59 +05:00
Steveplays28 532f3adba8 Merge branch 'main-upstream' 2023-09-27 14:09:04 +02:00
s809 cc8b97f2fe Improve management of frequent real time updates 2023-09-26 22:03:04 +05:00
s809 db31c54309 Fix Enable Distant Generation config being ignored 2023-09-26 13:21:37 +05:00
s809 375e81b17d Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-09-26 11:40:38 +05:00
s809 4fb9126d49 Fix unloaded file check 2023-09-24 18:48:14 +05:00
Steveplays28 ed2fca240e fix: Fix compiling after merge 2023-09-24 11:33:49 +02:00
Steveplays28 f5549f2ebe Merge remote-tracking branch 'origin-core/main' 2023-09-24 11:28:38 +02:00
Steveplays28 18e9cfe2a5 Merge branch 'main-upstream' 2023-09-24 11:24:55 +02:00
s809 e4e6753a02 Add gen task rate display 2023-09-24 14:24:31 +05:00
Steveplays28 8e7855405d Merge remote-tracking branch 'origin-core/main' 2023-09-23 16:34:07 +02:00
Steveplays28 381cae7307 feat: Add config option for server port 2023-09-23 16:33:43 +02:00
s809 e94d67916d Fix compiling
Remove unused config category
Use pooling when receiving sections
Fix use of real time update config
Fix debug renderer not unregistered on queue close
2023-09-23 15:32:24 +05:00
Steveplays28 c98f9476cb Merge remote-tracking branch 'upstream-core/main'
# Conflicts:
#	core/src/main/java/com/seibel/distanthorizons/core/api/external/methods/config/client/DhApiDebuggingConfig.java
#	core/src/main/java/com/seibel/distanthorizons/core/config/Config.java
#	core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/CompleteFullDataSource.java
#	core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/HighDetailIncompleteFullDataSource.java
#	core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataFileHandler.java
#	core/src/main/java/com/seibel/distanthorizons/core/file/renderfile/RenderDataMetaFile.java
#	core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java
#	core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java
#	core/src/main/java/com/seibel/distanthorizons/core/render/renderer/DebugRenderer.java
#	core/src/main/java/com/seibel/distanthorizons/core/render/renderer/LodRenderer.java
2023-09-22 15:40:31 +02:00
s809 6c36f3c7e9 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-09-21 21:04:58 +05:00
s809 877a89d17a Add config for a few slow features 2023-09-21 21:00:23 +05:00
s809 d325a69e3f Move context levels to requests 2023-09-21 10:16:34 +05:00
s809 08704aad2c Allow selecting specific IDebugRenderable's for rendering 2023-09-19 21:32:59 +05:00
s809 a80bb082b3 Validate response types 2023-09-19 20:19:25 +05:00
s809 fccd197cd5 Fix chunk updates sent to incomplete connections
Hide useless warns, for cases when listener is not configured yet
2023-09-19 18:53:10 +05:00
s809 2cfc2c81c8 Fix future id collisions between c<->s
(cause of occasional hangs on disconnection)
Add packet trace logging
2023-09-19 17:28:26 +05:00
s809 6f4e105542 Fix compiling 2023-09-18 13:44:19 +05:00
s809 633b3f2033 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-09-18 13:24:43 +05:00
Steveplays28 f0a62c813a Merge remote-tracking branch 'upstream-core/main' 2023-09-12 17:35:29 +02:00
s809 21f03526f8 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-09-10 12:03:29 +05:00
Steveplays28 0e7a96d308 fix: Fix missing IServerKeyedClientLevel import 2023-09-09 21:03:17 +02:00
Steveplays28 87572246a6 Merge remote-tracking branch 'upstream-core/main' 2023-09-09 20:53:44 +02:00
Steveplays28 025484d5b4 Merge remote-tracking branch 'upstream-core/main' 2023-09-09 14:47:07 +02:00
Steveplays28 15e4b1316e Merge remote-tracking branch 'upstream-core/main' 2023-09-08 19:54:43 +02:00
s809 e02eddc60e Fix compiling 2023-09-07 21:07:16 +05:00
s809 1e1ddd505b Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-09-07 20:07:30 +05:00
s809 415f16507b Add post-rejoin updates 2023-09-02 01:23:56 +05:00
s809 cb95978502 Fix compilation 2023-08-27 19:56:12 +05:00
s809 1b729f3fe7 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-08-27 19:10:25 +05:00
s809 d84e097fa2 Merge branch 'client-level-error' of https://gitlab.com/s809/distant-horizons-core into client-level-error 2023-08-27 18:43:06 +05:00
s809 51ad901206 Fix loading ClientLevel on server 2023-08-27 18:41:47 +05:00
s809 517925a207 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-08-26 21:33:44 +05:00
s809 c5abc22c58 Merge branch 'feat/server-updates' 2023-08-24 00:01:07 +05:00
s809 2330377212 Send updates at chunk level instead of sections 2023-08-23 23:55:16 +05:00
s809 9db56bbf87 test 2023-08-22 20:41:42 +05:00
s809 52a90fec6c Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core into feat/server-updates 2023-08-22 19:12:43 +05:00
Steveplays28 3630dc724d fix: Add null check to config GUI when adding a screen change listener 2023-08-22 10:52:19 +02:00
s809 627bfbc007 Incomplete 2023-08-21 22:08:35 +05:00
Steveplays28 d9283e938b feat: Update rendering block ignores
Barrier blocks, structure void blocks, light blocks, and air blocks now share 2 `HashMap`s that define blocks that should be ignored by the LOD builder.
2023-08-21 03:41:27 +02:00
s809 ce4d50654d Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-08-19 22:28:13 +05:00
s809 7568ab56f6 Fix hang when near completion of loading from server
Replace total value with sum of pending+finished requests
2023-08-19 22:21:01 +05:00
s809 bf58654a10 Fix prioritization not working properly 2023-08-19 21:22:51 +05:00
Steveplays28 e4576c7d51 Merge remote-tracking branch 'upstream-core/main' 2023-08-19 11:41:38 +02:00
Steveplays28 a1f07c075e Merge remote-tracking branch 'upstream-core/main' 2023-08-18 15:12:11 +02:00
s809 84c4ad6cdd Add missing level wrapper argument (and a bunch of null checks) 2023-08-18 15:04:59 +02:00
Steveplays28 47aef1f349 Merge remote-tracking branch 'upstream-core/main'
# Conflicts:
#	core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java
#	core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java
#	core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/FullDataPointIdMap.java
#	core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/CompleteFullDataSource.java
#	core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/HighDetailIncompleteFullDataSource.java
#	core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/LowDetailIncompleteFullDataSource.java
#	core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataFileHandler.java
#	core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/RemoteFullDataFileHandler.java
#	core/src/main/java/com/seibel/distanthorizons/core/level/ClientLevelModule.java
#	core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java
#	core/src/main/java/com/seibel/distanthorizons/core/level/DhClientServerLevel.java
#	core/src/main/java/com/seibel/distanthorizons/core/level/DhServerLevel.java
#	core/src/main/java/com/seibel/distanthorizons/core/level/IDhServerLevel.java
#	core/src/main/java/com/seibel/distanthorizons/core/level/ServerLevelModule.java
#	core/src/main/java/com/seibel/distanthorizons/core/network/NetworkEventSource.java
#	core/src/main/java/com/seibel/distanthorizons/core/network/messages/AckMessage.java
#	core/src/main/java/com/seibel/distanthorizons/core/network/messages/CloseReasonMessage.java
#	core/src/main/java/com/seibel/distanthorizons/core/network/messages/HelloMessage.java
#	core/src/main/java/com/seibel/distanthorizons/core/network/messages/PlayerUUIDMessage.java
#	core/src/main/java/com/seibel/distanthorizons/core/network/messages/RemotePlayerConfigMessage.java
#	core/src/main/java/com/seibel/distanthorizons/core/network/objects/RemotePlayer.java
#	core/src/main/java/com/seibel/distanthorizons/core/network/protocol/EMessageHandlerSide.java
#	core/src/main/java/com/seibel/distanthorizons/core/network/protocol/INetworkMessage.java
#	core/src/main/java/com/seibel/distanthorizons/core/network/protocol/MessageRegistry.java
#	core/src/main/java/com/seibel/distanthorizons/core/network/protocol/NetworkChannelInitializer.java
#	core/src/main/java/com/seibel/distanthorizons/core/network/protocol/NetworkExceptionHandler.java
#	core/src/main/java/com/seibel/distanthorizons/core/world/DhClientWorld.java
#	core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java
#	core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/block/IBlockStateWrapper.java
#	core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/misc/IServerPlayerWrapper.java
#	core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/IBiomeWrapper.java
2023-08-16 11:36:00 +02:00
Steveplays28 1e7a25046a fix: Fix BlockStateWrapper and Biome serializing to empty strings 2023-08-15 00:03:13 +02:00
Steveplays28 87a9e93278 fix: Fix server trying to access client instance 2023-08-14 19:23:39 +02:00
Steveplays28 2d3062008e Merge remote-tracking branch 'upstream-core/main' 2023-08-14 15:40:41 +02:00
Steveplays28 8dcec7a1bd refactor: Re-add old serialization method
This is used by some override methods that don't have access to the level.
These old serialization methods have `FIXME`s so they're easy to find in the future.
2023-08-13 23:18:55 +02:00
Steveplays28 d839b6e4bd Merge remote-tracking branch 'upstream-core/main'
# Conflicts:
#	core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/FullDataPointIdMap.java
#	core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/CompleteFullDataSource.java
#	core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/HighDetailIncompleteFullDataSource.java
#	core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/LowDetailIncompleteFullDataSource.java
#	core/src/main/java/com/seibel/distanthorizons/core/pos/DhSectionPos.java
2023-08-13 22:59:43 +02:00
s809 0c155ac8cd Generation task prioritization (loaded > unloaded > ungenerated) 2023-08-13 18:59:08 +05:00
s809 271b193543 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-08-13 01:53:06 +05:00
s809 e4ac25f4ce Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-08-08 20:57:30 +05:00
s809 52f9e3e9e4 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-08-08 12:35:21 +05:00
s809 6bf32ff85c Add debug renderer for remote generation queue 2023-08-05 22:30:33 +05:00
s809 9bd432ad7f Fix memory leak related to requests 2023-08-04 22:12:09 +05:00
s809 04fc4aa676 Remove unnecessary logging 2023-08-04 21:51:53 +05:00
s809 8ee4b8b4c9 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-08-04 21:10:41 +05:00
s809 5767668efa Somewhat proper request cancellation 2023-08-04 21:04:34 +05:00
s809 76b226b865 Fix switching dimensions (request cancellation is broken) 2023-08-04 13:06:56 +05:00
s809 1788c18d59 Everything I've done so far (not working/finished) 2023-08-01 22:31:16 +05:00
s809 2251cd4c25 Attempt to fix dimension switching 2023-08-01 17:04:40 +05:00
s809 781b588980 Implement server-side rate limiting 2023-08-01 12:55:36 +05:00
s809 74ac4fe64f Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-07-31 17:43:16 +05:00
s809 6d1f9803ce Avoid losing requests due to concurrency 2023-07-31 17:42:14 +05:00
s809 95d721e1a3 World generation 2023-07-31 15:23:56 +05:00
s809 d2f2a3b8aa Attempt to use GeneratedFullDataFileHandler 2023-07-24 19:59:16 +05:00
s809 768ce5b8fd something 2023-07-24 19:16:05 +05:00
s809 3e9f741d62 Implement transferring of missing full data source types 2023-07-24 14:15:03 +05:00
s809 133f007bde Fix mistake 2023-07-24 11:01:10 +05:00
s809 a623a3bfab Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-07-24 10:41:17 +05:00
s809 4157a39589 Somewhat working generation 2023-07-24 01:46:09 +05:00
s809 10e2873497 Downloads already visited chunks 2023-07-23 23:48:42 +05:00
s809 d7ef6c8a72 Got chunks to generate on server 2023-07-23 20:34:40 +05:00
s809 d29ba9d423 Requests now work properly
Merge request tracker into event source
2023-07-23 14:39:29 +05:00
s809 0c60395426 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-07-23 11:08:31 +05:00
s809 4ac1c0d4b3 Sending some responses
Rejoin is horribly broken
2023-07-19 22:29:33 +05:00
Steveplays28 9a0c9e9b7d feat: Implement syncing of DhSectionPos for chunk requests/responses 2023-07-19 16:48:43 +02:00
s809 79408c081a Close network request tacker properly 2023-07-19 17:40:38 +05:00
s809 218411902e Incomplete something 2023-07-19 15:54:17 +05:00
s809 a42f8367a4 Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-07-19 12:20:11 +05:00
s809 02acfaa3ed Finer control over message handling (untested) 2023-07-19 12:19:39 +05:00
s809 9a2799e83b Merge branch 'main' of https://gitlab.com/jeseibel/distant-horizons-core 2023-07-16 12:47:44 +05:00
s809 73e6ce75b0 Replace "tell version to client" part with simple ack because YAGNI 2023-07-16 12:38:32 +05:00
178 changed files with 7914 additions and 3896 deletions
@@ -180,7 +180,7 @@ public class DhApi
* This version should be updated whenever non-breaking fixes are added to the Distant Horizons API. * This version should be updated whenever non-breaking fixes are added to the Distant Horizons API.
* @since API 1.0.0 * @since API 1.0.0
*/ */
public static int getApiPatchVersion() { return ModInfo.API_PATH_VERSION; } public static int getApiPatchVersion() { return ModInfo.API_PATCH_VERSION; }
/** /**
* Returns the mod's semantic version number in the format: Major.Minor.Patch * Returns the mod's semantic version number in the format: Major.Minor.Patch
@@ -28,8 +28,6 @@ import com.seibel.distanthorizons.api.interfaces.IDhApiUnsafeWrapper;
*/ */
public interface IDhApiDimensionTypeWrapper extends IDhApiUnsafeWrapper public interface IDhApiDimensionTypeWrapper extends IDhApiUnsafeWrapper
{ {
String getDimensionName();
boolean hasCeiling(); boolean hasCeiling();
boolean hasSkyLight(); boolean hasSkyLight();
@@ -35,6 +35,9 @@ public interface IDhApiLevelWrapper extends IDhApiUnsafeWrapper
{ {
IDhApiDimensionTypeWrapper getDimensionType(); IDhApiDimensionTypeWrapper getDimensionType();
/** @since API 4.0.0 */
String getDimensionName();
EDhApiLevelType getLevelType(); EDhApiLevelType getLevelType();
boolean hasCeiling(); boolean hasCeiling();
@@ -26,27 +26,28 @@ package com.seibel.distanthorizons.coreapi;
public final class ModInfo public final class ModInfo
{ {
public static final String ID = "distanthorizons"; public static final String ID = "distanthorizons";
/** The internal protocol version used for networking */
public static final int PROTOCOL_VERSION = 1; public static final String RESOURCE_NAMESPACE = "distant_horizons";
/** The protocol version used for multiverse networking */ public static final String DEDICATED_SERVER_INITIAL_PATH = "dedicated_server_initial";
public static final int MULTIVERSE_PLUGIN_PROTOCOL_VERSION = 1;
/** Incremented every time any packets are added, changed or removed, with a few exceptions. */
public static final int PROTOCOL_VERSION = 4;
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.2.0-a"; public static final String VERSION = "2.3.0-a-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 boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev"); public static 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 = 3; public static final int API_MAJOR_VERSION = 4;
/** This version should be updated whenever new methods are added to the DH API */ /** This version should be updated whenever new methods are added to the DH API */
public static final int API_MINOR_VERSION = 0; public static final int API_MINOR_VERSION = 0;
/** This version should be updated whenever non-breaking fixes are added to the DH API */ /** This version should be updated whenever non-breaking fixes are added to the DH API */
public static final int API_PATH_VERSION = 0; public static final int API_PATCH_VERSION = 0;
public static final String NETWORKING_RESOURCE_NAMESPACE = "distant_horizons";
public static final String MULTIVERSE_PLUGIN_NAMESPACE = "world_control";
/** 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-";
@@ -23,6 +23,7 @@ import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericRenderObjectFactory; import com.seibel.distanthorizons.core.render.renderer.generic.GenericRenderObjectFactory;
import com.seibel.distanthorizons.core.sql.DatabaseUpdater; import com.seibel.distanthorizons.core.sql.DatabaseUpdater;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.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;
@@ -40,6 +41,8 @@ import java.awt.*;
public class Initializer public class Initializer
{ {
private static final Logger LOGGER = LogManager.getLogger(ModInfo.NAME + "-" + Initializer.class.getSimpleName()); private static final Logger LOGGER = LogManager.getLogger(ModInfo.NAME + "-" + Initializer.class.getSimpleName());
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
public static void init() public static void init()
{ {
@@ -77,7 +80,8 @@ public class Initializer
throw new RuntimeException(e); throw new RuntimeException(e);
} }
if (MC_CLIENT != null)
{
// attempt to set up Swing so we can display dialogs (popup windows) // attempt to set up Swing so we can display dialogs (popup windows)
System.setProperty("java.awt.headless", "false"); System.setProperty("java.awt.headless", "false");
if (GraphicsEnvironment.isHeadless()) if (GraphicsEnvironment.isHeadless())
@@ -88,6 +92,7 @@ public class Initializer
{ {
LOGGER.info("Java.awt.headless set to true. Distant Horizons can correctly display error and info dialog windows."); LOGGER.info("Java.awt.headless set to true. Distant Horizons can correctly display error and info dialog windows.");
} }
}
// link Core's config to the API // link Core's config to the API
DhApi.Delayed.configs = DhApiConfig.INSTANCE; DhApi.Delayed.configs = DhApiConfig.INSTANCE;
@@ -33,19 +33,19 @@ public class DhApiDebuggingConfig implements IDhApiDebuggingConfig
public IDhApiConfigValue<EDhApiDebugRendering> debugRendering() @Override public IDhApiConfigValue<EDhApiDebugRendering> debugRendering()
{ return new DhApiConfigValue<EDhApiDebugRendering, EDhApiDebugRendering>(Config.Client.Advanced.Debugging.debugRendering); } { return new DhApiConfigValue<EDhApiDebugRendering, EDhApiDebugRendering>(Config.Client.Advanced.Debugging.debugRendering); }
public IDhApiConfigValue<Boolean> debugKeybindings() @Override public IDhApiConfigValue<Boolean> debugKeybindings()
{ return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.enableDebugKeybindings); } { return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.enableDebugKeybindings); }
public IDhApiConfigValue<Boolean> renderWireframe() @Override public IDhApiConfigValue<Boolean> renderWireframe()
{ return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.renderWireframe); } { return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.renderWireframe); }
public IDhApiConfigValue<Boolean> lodOnlyMode() @Override public IDhApiConfigValue<Boolean> lodOnlyMode()
{ return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.lodOnlyMode); } { return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.lodOnlyMode); }
public IDhApiConfigValue<Boolean> debugWireframeRendering() @Override public IDhApiConfigValue<Boolean> debugWireframeRendering()
{ return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.DebugWireframe.enableRendering); } { return new DhApiConfigValue<Boolean, Boolean>(Config.Client.Advanced.Debugging.DebugWireframe.enableRendering); }
} }
@@ -33,10 +33,10 @@ public class DhApiMultiplayerConfig implements IDhApiMultiplayerConfig
public IDhApiConfigValue<EDhApiServerFolderNameMode> folderSavingMode() @Override public IDhApiConfigValue<EDhApiServerFolderNameMode> folderSavingMode()
{ return new DhApiConfigValue<EDhApiServerFolderNameMode, EDhApiServerFolderNameMode>(Config.Client.Advanced.Multiplayer.serverFolderNameMode); } { return new DhApiConfigValue<EDhApiServerFolderNameMode, EDhApiServerFolderNameMode>(Config.Client.Advanced.Multiplayer.serverFolderNameMode); }
public IDhApiConfigValue<Double> multiverseSimilarityRequirement() @Override public IDhApiConfigValue<Double> multiverseSimilarityRequirement()
{ return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Multiplayer.multiverseSimilarityRequiredPercent); } { return new DhApiConfigValue<Double, Double>(Config.Client.Advanced.Multiplayer.multiverseSimilarityRequiredPercent); }
} }
@@ -24,18 +24,20 @@ 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.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure; import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure;
import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager;
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.util.TimerUtil;
import com.seibel.distanthorizons.core.util.objects.Pair; import com.seibel.distanthorizons.core.util.objects.Pair;
import com.seibel.distanthorizons.core.world.*;
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.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.session.NetworkSession;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering; 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.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.logging.ConfigBasedSpamLogger; import com.seibel.distanthorizons.core.logging.ConfigBasedSpamLogger;
import com.seibel.distanthorizons.core.logging.SpamReducedLogger; import com.seibel.distanthorizons.core.logging.SpamReducedLogger;
@@ -43,21 +45,21 @@ import com.seibel.distanthorizons.core.util.math.Mat4f;
import com.seibel.distanthorizons.core.render.glObject.GLProxy; import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.render.renderer.TestRenderer; import com.seibel.distanthorizons.core.render.renderer.TestRenderer;
import com.seibel.distanthorizons.core.util.RenderUtil; import com.seibel.distanthorizons.core.util.RenderUtil;
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.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.IMinecraftRenderWrapper;
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 io.netty.buffer.ByteBuf;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.NotNull;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import java.io.File; import java.io.File;
import java.util.HashMap; import java.util.*;
import java.util.HashSet;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -75,9 +77,7 @@ public class ClientApi
public static final ClientApi INSTANCE = new ClientApi(); public static final ClientApi INSTANCE = new ClientApi();
public static final TestRenderer TEST_RENDERER = new TestRenderer(); public static final TestRenderer TEST_RENDERER = new TestRenderer();
private static final IMinecraftClientWrapper MC = 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 IKeyedClientLevelManager KEYED_CLIENT_LEVEL_MANAGER = SingletonInjector.INSTANCE.get(IKeyedClientLevelManager.class);
public static final long SPAM_LOGGER_FLUSH_NS = TimeUnit.NANOSECONDS.convert(1, TimeUnit.SECONDS); public static final long SPAM_LOGGER_FLUSH_NS = TimeUnit.NANOSECONDS.convert(1, TimeUnit.SECONDS);
@@ -90,10 +90,11 @@ public class ClientApi
private long lastFlushNanoTime = 0; private long lastFlushNanoTime = 0;
private boolean isServerCommunicationEnabled = false; private final ClientPluginChannelApi pluginChannelApi = new ClientPluginChannelApi(this::clientLevelLoadEvent, this::clientLevelUnloadEvent);
/** set to true if any unexpected responses are received from the server */ /** Delay loading the first level to give the server some time to respond with level to actually load */
private boolean serverNetworkingIsMalformed = false; private Timer firstLevelLoadTimer;
private static final long FIRST_LEVEL_LOAD_DELAY_IN_MS = 1_000;
/** Holds any levels that were loaded before the {@link ClientApi#onClientOnlyConnected} was fired. */ /** Holds any levels that were loaded before the {@link ClientApi#onClientOnlyConnected} was fired. */
public final HashSet<IClientLevelWrapper> waitingClientLevels = new HashSet<>(); public final HashSet<IClientLevelWrapper> waitingClientLevels = new HashSet<>();
@@ -127,8 +128,8 @@ public class ClientApi
public synchronized void onClientOnlyConnected() public synchronized void onClientOnlyConnected()
{ {
// only continue if the client is connected to a different server // only continue if the client is connected to a different server
boolean connectedToServer = MC.clientConnectedToDedicatedServer(); boolean connectedToServer = MC_CLIENT.clientConnectedToDedicatedServer();
boolean connectedToReplay = MC.connectedToReplay(); boolean connectedToReplay = MC_CLIENT.connectedToReplay();
if (connectedToServer || connectedToReplay) if (connectedToServer || connectedToReplay)
{ {
if (connectedToServer) if (connectedToServer)
@@ -141,19 +142,22 @@ public class ClientApi
if (Config.Client.Advanced.Logging.showReplayWarningOnStartup.get()) if (Config.Client.Advanced.Logging.showReplayWarningOnStartup.get())
{ {
MC.sendChatMessage("\u00A76" + "Distant Horizons: Replay detected." + "\u00A7r"); // gold color MC_CLIENT.sendChatMessage("\u00A76" + "Distant Horizons: Replay detected." + "\u00A7r"); // gold color
MC.sendChatMessage("DH may behave strangely or have missing functionality."); MC_CLIENT.sendChatMessage("DH may behave strangely or have missing functionality.");
MC.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.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("\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.sendChatMessage("This can be disabled in DH's config under Advanced -> Logging."); MC_CLIENT.sendChatMessage("This can be disabled in DH's config under Advanced -> Logging.");
MC.sendChatMessage(""); MC_CLIENT.sendChatMessage("");
} }
} }
// firing after clientLevelLoadEvent // firing after clientLevelLoadEvent
// TODO if level has prepped to load it should fire level load event // TODO if level has prepped to load it should fire level load event
SharedApi.setDhWorld(new DhClientWorld()); DhClientWorld world = new DhClientWorld();
SharedApi.setDhWorld(world);
this.pluginChannelApi.onJoinServer(world.networkState.getSession());
world.networkState.sendConfigMessage();
LOGGER.info("Loading [" + this.waitingClientLevels.size() + "] waiting client level wrappers."); LOGGER.info("Loading [" + this.waitingClientLevels.size() + "] waiting client level wrappers.");
for (IClientLevelWrapper level : this.waitingClientLevels) for (IClientLevelWrapper level : this.waitingClientLevels)
@@ -168,6 +172,13 @@ public class ClientApi
/** Synchronized to prevent a rare issue where multiple disconnect events are triggered on top of each other. */ /** Synchronized to prevent a rare issue where multiple disconnect events are triggered on top of each other. */
public synchronized void onClientOnlyDisconnected() public synchronized void onClientOnlyDisconnected()
{ {
// clear the first time timer
if (this.firstLevelLoadTimer != null)
{
this.firstLevelLoadTimer.cancel();
this.firstLevelLoadTimer = null;
}
AbstractDhWorld world = SharedApi.getAbstractDhWorld(); AbstractDhWorld world = SharedApi.getAbstractDhWorld();
if (world != null) if (world != null)
{ {
@@ -177,11 +188,7 @@ public class ClientApi
SharedApi.setDhWorld(null); SharedApi.setDhWorld(null);
} }
// clear the previous server's information this.pluginChannelApi.reset();
this.isServerCommunicationEnabled = false;
this.serverNetworkingIsMalformed = false;
KEYED_CLIENT_LEVEL_MANAGER.setUseOverrideWrapper(false);
KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(null);
// remove any waiting items // remove any waiting items
this.waitingChunkByClientLevelAndPos.clear(); this.waitingChunkByClientLevelAndPos.clear();
@@ -194,16 +201,16 @@ public class ClientApi
// level events // // level events //
//==============// //==============//
public void clientLevelUnloadEvent(@Nullable IClientLevelWrapper level) public void clientLevelUnloadEvent(IClientLevelWrapper level)
{ {
try try
{ {
if (level == null) LOGGER.info("Unloading client level [" + level + "]-["+level.getDimensionName()+"].");
if (level instanceof IServerKeyedClientLevel)
{ {
// can happen on certain multiverse servers this.pluginChannelApi.onClientLevelUnload();
return;
} }
LOGGER.info("Unloading client level [" + level + "]-["+level.getDimensionType().getDimensionName()+"].");
AbstractDhWorld world = SharedApi.getAbstractDhWorld(); AbstractDhWorld world = SharedApi.getAbstractDhWorld();
if (world != null) if (world != null)
@@ -223,30 +230,42 @@ public class ClientApi
} }
} }
public void clientLevelLoadEvent(@Nullable IClientLevelWrapper level) { this.clientLevelLoadEvent(level, false); } public void clientLevelLoadEvent(IClientLevelWrapper level)
public void multiverseClientLevelLoadEvent(@Nullable IClientLevelWrapper level) { this.clientLevelLoadEvent(level, true); }
private void clientLevelLoadEvent(@Nullable IClientLevelWrapper level, boolean isServerCommunication)
{ {
// 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 (this.firstLevelLoadTimer == null)
{
this.firstLevelLoadTimer = TimerUtil.CreateTimer("FirstLevelLoadTimer");
this.firstLevelLoadTimer.schedule(new TimerTask()
{
@Override
public void run() { ClientApi.this.clientLevelLoadEvent(level); }
}, FIRST_LEVEL_LOAD_DELAY_IN_MS);
return;
}
this.firstLevelLoadTimer.cancel();
}
try try
{ {
if (this.isServerCommunicationEnabled && !isServerCommunication) LOGGER.info("Loading client level [" + level + "]-["+level.getDimensionName()+"].");
{
LOGGER.info("Server supports communication, deferring loading.");
return;
}
if (level == null)
{
// can happen on certain multiverse servers
return;
}
LOGGER.info("Loading " + (isServerCommunication ? "Multiverse" : "") + " client level [" + level + "]-["+level.getDimensionType().getDimensionName()+"].");
AbstractDhWorld world = SharedApi.getAbstractDhWorld(); AbstractDhWorld world = SharedApi.getAbstractDhWorld();
if (world != null) if (world != null)
{ {
if (!this.pluginChannelApi.allowLevelLoading(level))
{
LOGGER.info("Levels in this connection are managed by the server, skipping auto-load.");
// Instead of attempting to load themselves, send the config and wait for a server provided level key.
((DhClientWorld) world).networkState.sendConfigMessage();
return;
}
world.getOrLoadLevel(level); world.getOrLoadLevel(level);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level)); ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level));
@@ -295,7 +314,7 @@ public class ClientApi
{ {
LOGGER.info("Renderer shutting down."); LOGGER.info("Renderer shutting down.");
IProfilerWrapper profiler = MC.getProfiler(); IProfilerWrapper profiler = MC_CLIENT.getProfiler();
profiler.push("DH-RendererShutdown"); profiler.push("DH-RendererShutdown");
profiler.pop(); profiler.pop();
@@ -305,7 +324,7 @@ public class ClientApi
{ {
LOGGER.info("Renderer starting up."); LOGGER.info("Renderer starting up.");
IProfilerWrapper profiler = MC.getProfiler(); IProfilerWrapper profiler = MC_CLIENT.getProfiler();
profiler.push("DH-RendererStartup"); profiler.push("DH-RendererStartup");
// make sure the GLProxy is created before the LodBufferBuilder needs it // make sure the GLProxy is created before the LodBufferBuilder needs it
@@ -315,7 +334,7 @@ public class ClientApi
public void clientTickEvent() public void clientTickEvent()
{ {
IProfilerWrapper profiler = MC.getProfiler(); IProfilerWrapper profiler = MC_CLIENT.getProfiler();
profiler.push("DH-ClientTick"); profiler.push("DH-ClientTick");
try try
@@ -337,7 +356,7 @@ public class ClientApi
// Ignore local world gen, as it's managed by server ticking // Ignore local world gen, as it's managed by server ticking
if (!(clientWorld instanceof DhClientServerWorld)) if (!(clientWorld instanceof DhClientServerWorld))
{ {
SharedApi.worldGenTick(clientWorld::doWorldGen); SharedApi.worldGenTick(clientWorld::worldGenTick);
} }
} }
} }
@@ -356,123 +375,14 @@ public class ClientApi
// networking // // networking //
//============// //============//
// /** @param byteBuf is Netty's {@link ByteBuffer} wrapper. */ public void pluginMessageReceived(@NotNull AbstractNetworkMessage message)
// public void serverMessageReceived(ByteBuf byteBuf) {
// { NetworkSession networkSession = this.pluginChannelApi.networkSession;
// if (!Config.Client.Advanced.Multiplayer.enableMultiverseNetworking.get()) if (networkSession != null)
// { {
// // multiverse networking disabled, ignore anything sent from the server networkSession.tryHandleMessage(message);
// return; }
// } }
//
//
//
// // either value can be set to true to debug the received byte stream
// boolean stopAndDisplayInputAsByteArray = false;
// boolean stopAndDisplayInputAsString = false;
// if (stopAndDisplayInputAsByteArray || stopAndDisplayInputAsString)
// {
// String messageString = "";
// if (stopAndDisplayInputAsByteArray)
// {
// int byteCount = byteBuf.readableBytes();
// byte[] arr = new byte[byteCount];
// StringBuilder stringBuilder = new StringBuilder("Server message received: [");
// for (int i = 0; i < byteCount; i++)
// {
// arr[i] = byteBuf.readByte();
// stringBuilder.append(arr[i]);
// }
// stringBuilder.append("]");
//
// messageString = stringBuilder.toString();
// }
// else if (stopAndDisplayInputAsString)
// {
// messageString = byteBuf.toString(StandardCharsets.UTF_8);
// }
//
// // this is logged as an error so it is easier to see in an Intellij log
// LOGGER.error(messageString);
// return;
// }
//
//
//
//
// // It is important to ensure malicious server input is ignored.
// if (this.serverNetworkingIsMalformed)
// {
// return;
// }
//
// // check that the incoming message is within the expected size
// short commandLength = byteBuf.readShort();
// if (commandLength < 1 || commandLength > 32)
// {
// LOGGER.error("Server command length ["+commandLength+"] outside the expected range of 1 to 32 (inclusive).");
// ClientApi.INSTANCE.serverNetworkingIsMalformed = true;
// return;
// }
//
// // parse the command
// String eventType;
// try
// {
// eventType = byteBuf.readCharSequence(commandLength, StandardCharsets.UTF_8).toString();
// }
// catch (Exception e)
// {
// LOGGER.error("Server sent un-parsable command. Error: "+e.getMessage());
// return;
// }
//
// switch (eventType)
// {
// case "ServerCommsEnabled":
// LOGGER.info("Server supports DH multiverse protocol.");
// ClientApi.INSTANCE.isServerCommunicationEnabled = true;
// KEYED_CLIENT_LEVEL_MANAGER.setUseOverrideWrapper(true);
// MC.executeOnRenderThread(() ->
// {
// // Unload the current world, since it may be wrong.
// // A followup WorldChanged event should be received from the server soon after this.
// LOGGER.info("Unloading current client level so the server can define the correct multiverse level.");
// this.clientLevelUnloadEvent((IClientLevelWrapper) MC.getWrappedClientWorld());
// });
// break;
//
// case "LevelChanged":
// short levelKeyLength = byteBuf.readShort();
// if (levelKeyLength < 1 || levelKeyLength > 128) // TODO 128 should be put into a constant somewhere
// {
// LOGGER.error("Server [LevelChanged] command length ["+commandLength+"] outside the expected range of 1 to 128 (inclusive).");
// this.serverNetworkingIsMalformed = true;
// return;
// }
//
// String levelKey = byteBuf.readCharSequence(levelKeyLength, StandardCharsets.UTF_8).toString();
// if (!levelKey.matches("[a-zA-Z0-9_]+"))
// {
// LOGGER.error("Server sent invalid world key name, and is being ignored.");
// this.isServerCommunicationEnabled = false;
// this.serverNetworkingIsMalformed = true;
// return;
// }
//
// LOGGER.info("Server level change event received, changing the level to ["+levelKey+"].");
// MC.executeOnRenderThread(() -> {
// if (MC.getWrappedClientWorld() != null)
// {
// this.clientLevelUnloadEvent((IClientLevelWrapper) MC.getWrappedClientWorld());
// }
// IServerKeyedClientLevel clientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel(MC.getWrappedClientWorld(), levelKey);
// KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel);
// this.multiverseClientLevelLoadEvent(clientLevel);
// });
// break;
// }
// }
@@ -499,7 +409,7 @@ public class ClientApi
this.sendQueuedChatMessages(); this.sendQueuedChatMessages();
IProfilerWrapper profiler = MC.getProfiler(); IProfilerWrapper profiler = MC_CLIENT.getProfiler();
profiler.pop(); // get out of "terrain" profiler.pop(); // get out of "terrain"
profiler.push("DH-RenderLevel"); profiler.push("DH-RenderLevel");
@@ -550,10 +460,24 @@ public class ClientApi
{ {
return; return;
} }
IDhClientLevel level = dhClientWorld.getOrLoadClientLevel(levelWrapper);
IDhClientLevel level = (IDhClientLevel) dhClientWorld.getLevel(levelWrapper);
if (level == null)
{
return;
}
if (this.rendererDisabledBecauseOfExceptions) if (this.rendererDisabledBecauseOfExceptions)
{ {
// re-enable rendering if the user toggles DH rendering
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; return;
} }
@@ -603,10 +527,10 @@ 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.sendChatMessage("\u00A74\u00A7l\u00A7uERROR: Distant Horizons renderer has encountered an exception!"); MC_CLIENT.sendChatMessage("\u00A74\u00A7l\u00A7uERROR: Distant Horizons renderer has encountered an exception!");
MC.sendChatMessage("\u00A74Renderer is now disabled to prevent further issues."); MC_CLIENT.sendChatMessage("\u00A74Renderer disabled to try preventing GL state corruption.");
MC.sendChatMessage("\u00A74Please restart your game to re-enable Distant Horizons' LOD rendering."); MC_CLIENT.sendChatMessage("\u00A74Toggle DH rendering via the config UI to re-activate DH rendering.");
MC.sendChatMessage("\u00A74Exception detail: " + e); MC_CLIENT.sendChatMessage("\u00A74Error: " + e);
} }
finally finally
{ {
@@ -646,24 +570,24 @@ public class ClientApi
if (glfwKey == GLFW.GLFW_KEY_F8) if (glfwKey == GLFW.GLFW_KEY_F8)
{ {
Config.Client.Advanced.Debugging.debugRendering.set(EDhApiDebugRendering.next(Config.Client.Advanced.Debugging.debugRendering.get())); Config.Client.Advanced.Debugging.debugRendering.set(EDhApiDebugRendering.next(Config.Client.Advanced.Debugging.debugRendering.get()));
MC.sendChatMessage("F8: Set debug mode to " + Config.Client.Advanced.Debugging.debugRendering.get()); MC_CLIENT.sendChatMessage("F8: Set debug mode to " + Config.Client.Advanced.Debugging.debugRendering.get());
} }
else if (glfwKey == GLFW.GLFW_KEY_F6) 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.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_P)
{ {
prefLoggerEnabled = !prefLoggerEnabled; prefLoggerEnabled = !prefLoggerEnabled;
MC.sendChatMessage("P: Debug Pref Logger is " + (prefLoggerEnabled ? "enabled" : "disabled")); MC_CLIENT.sendChatMessage("P: Debug Pref Logger is " + (prefLoggerEnabled ? "enabled" : "disabled"));
} }
} }
private void sendQueuedChatMessages() private void sendQueuedChatMessages()
{ {
// dev build // dev build
if (ModInfo.IS_DEV_BUILD && !this.configOverrideReminderPrinted && MC.playerExists()) if (ModInfo.IS_DEV_BUILD && !this.configOverrideReminderPrinted && MC_CLIENT.playerExists())
{ {
this.configOverrideReminderPrinted = true; this.configOverrideReminderPrinted = true;
@@ -673,7 +597,7 @@ public class ClientApi
"\u00A72" + "Distant Horizons: nightly/unstable build, version: [" + ModInfo.VERSION+"]." + "\u00A7r\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.sendChatMessage(message); MC_CLIENT.sendChatMessage(message);
} }
// memory // memory
@@ -694,7 +618,7 @@ public class ClientApi
"Stuttering or low FPS may occur. \n" + "Stuttering or low FPS may occur. \n" +
"Please increase Minecraft's available memory to 4 gigabytes. \n" + "Please increase Minecraft's available memory to 4 gigabytes. \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";
MC.sendChatMessage(message); MC_CLIENT.sendChatMessage(message);
} }
} }
@@ -707,7 +631,7 @@ public class ClientApi
// done to prevent potential null pointers // done to prevent potential null pointers
message = ""; message = "";
} }
MC.sendChatMessage(message); MC_CLIENT.sendChatMessage(message);
} }
} }
@@ -0,0 +1,134 @@
package com.seibel.distanthorizons.core.api.internal;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager;
import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent;
import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage;
import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import org.apache.logging.log4j.LogManager;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.function.Consumer;
/**
* This class is used to manage the level keys.
*/
public class ClientPluginChannelApi
{
private static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Logging.logNetworkEvent.get());
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IKeyedClientLevelManager KEYED_CLIENT_LEVEL_MANAGER = SingletonInjector.INSTANCE.get(IKeyedClientLevelManager.class);
private final Consumer<IServerKeyedClientLevel> levelLoadHandler;
private final Consumer<IClientLevelWrapper> levelUnloadHandler;
@Nullable
public NetworkSession networkSession;
//=============//
// constructor //
//=============//
public ClientPluginChannelApi(Consumer<IServerKeyedClientLevel> levelLoadHandler, Consumer<IClientLevelWrapper> levelUnloadHandler)
{
this.levelLoadHandler = levelLoadHandler;
this.levelUnloadHandler = levelUnloadHandler;
}
//============//
// properties //
//============//
/** @return true if the level loading is handled by the server */
public boolean allowLevelLoading(IClientLevelWrapper level)
{
return (KEYED_CLIENT_LEVEL_MANAGER.hasLevelSet() && level instanceof IServerKeyedClientLevel)
|| !KEYED_CLIENT_LEVEL_MANAGER.hasLevelSet();
}
//================//
// network events //
//================//
/** fired when this client connects to a server with DH support */
public void onJoinServer(@NonNull NetworkSession networkSession)
{
Objects.requireNonNull(networkSession);
this.networkSession = networkSession;
this.networkSession.registerHandler(CurrentLevelKeyMessage.class, this::onCurrentLevelKeyMessage);
this.networkSession.registerHandler(CloseInternalEvent.class, this::onClose);
}
private void onCurrentLevelKeyMessage(CurrentLevelKeyMessage msg)
{
// prefix@namespace:path
// 1-50 characters in total, all parts except namespace can be omitted
if (!msg.levelKey.matches("^(?=.{1,50}$)([a-zA-Z0-9-_]+@)?[a-zA-Z0-9-_]+(:[a-zA-Z0-9-_]+)?$"))
{
throw new IllegalArgumentException("Server sent invalid level key.");
}
LOGGER.info("Server level key received: [" + msg.levelKey + "].");
MC.executeOnRenderThread(() ->
{
IClientLevelWrapper clientLevel = MC.getWrappedClientLevel(true);
IServerKeyedClientLevel existingKeyedClientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel();
if (existingKeyedClientLevel != null)
{
if (!existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey))
{
LOGGER.info("Unloading previous level with key: [" + existingKeyedClientLevel.getServerLevelKey() + "].");
this.levelUnloadHandler.accept(existingKeyedClientLevel);
}
else
{
LOGGER.info("Level key matches the previous level key, ignoring the message.");
}
}
else
{
LOGGER.info("Unloading non-keyed level: [" + clientLevel.getDimensionName() + "].");
this.levelUnloadHandler.accept(clientLevel);
}
if (existingKeyedClientLevel == null || !existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey))
{
LOGGER.info("Loading level with key: [" + msg.levelKey + "].");
IServerKeyedClientLevel keyedLevel = KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel, msg.levelKey);
this.levelLoadHandler.accept(keyedLevel);
}
});
}
public void onClientLevelUnload() { KEYED_CLIENT_LEVEL_MANAGER.clearKeyedLevel(); }
//==========//
// shutdown //
//==========//
private void onClose(CloseInternalEvent event) { this.reset(); }
public void reset()
{
this.networkSession = null;
KEYED_CLIENT_LEVEL_MANAGER.clearKeyedLevel();
}
}
@@ -21,13 +21,9 @@ package com.seibel.distanthorizons.core.api.internal;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelLoadEvent; import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelLoadEvent;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelUnloadEvent; import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelUnloadEvent;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.world.AbstractDhWorld; import com.seibel.distanthorizons.core.world.AbstractDhWorld;
import com.seibel.distanthorizons.core.world.DhClientServerWorld; import com.seibel.distanthorizons.core.world.DhClientServerWorld;
import com.seibel.distanthorizons.core.world.DhServerWorld; import com.seibel.distanthorizons.core.world.DhServerWorld;
@@ -37,6 +33,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
/** /**
* This holds the methods that should be called by the host mod loader (Fabric, * This holds the methods that should be called by the host mod loader (Fabric,
@@ -70,7 +67,7 @@ public class ServerApi
if (serverWorld != null) if (serverWorld != null)
{ {
serverWorld.serverTick(); serverWorld.serverTick();
SharedApi.worldGenTick(serverWorld::doWorldGen); SharedApi.worldGenTick(serverWorld::worldGenTick);
} }
} }
catch (Exception e) catch (Exception e)
@@ -154,7 +151,7 @@ public class ServerApi
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); IDhServerWorld serverWorld = SharedApi.getIDhServerWorld();
if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well
{ {
LOGGER.debug("Waiting for player to connect: " + player.getUUID()); LOGGER.info("Player [" + player.getName()+ "] joined.");
((DhServerWorld) serverWorld).addPlayer(player); ((DhServerWorld) serverWorld).addPlayer(player);
} }
} }
@@ -163,9 +160,27 @@ public class ServerApi
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); IDhServerWorld serverWorld = SharedApi.getIDhServerWorld();
if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well
{ {
LOGGER.debug("Removing player from connect wait list: " + player.getUUID()); LOGGER.info("Player [" + player.getName() + "] disconnected.");
((DhServerWorld) serverWorld).removePlayer(player); ((DhServerWorld) serverWorld).removePlayer(player);
} }
} }
public void serverPlayerLevelChangeEvent(IServerPlayerWrapper player, IServerLevelWrapper originLevel, IServerLevelWrapper destinationLevel)
{
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld();
if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well
{
LOGGER.info("Player [" + player.getName() + "] changed level: ["+originLevel.getKeyedLevelDimensionName()+"] -> ["+destinationLevel.getKeyedLevelDimensionName()+"].");
((DhServerWorld) serverWorld).changePlayerLevel(player, originLevel, destinationLevel);
}
}
public void pluginMessageReceived(IServerPlayerWrapper player, @NotNull AbstractNetworkMessage message)
{
IDhServerWorld serverWorld = SharedApi.getIDhServerWorld();
if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well
{
((DhServerWorld) serverWorld).remotePlayerConnectionHandler.handlePluginMessage(player, message);
}
}
} }
@@ -24,20 +24,18 @@ import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.generation.DhLightingEngine; import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.level.IDhServerLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen; import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.pos.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.DebugRenderer;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
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.LodUtil;
import com.seibel.distanthorizons.core.util.TimerUtil;
import com.seibel.distanthorizons.core.util.objects.Pair; import com.seibel.distanthorizons.core.util.objects.Pair;
import com.seibel.distanthorizons.core.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.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;
@@ -45,10 +43,8 @@ import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.*;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;
/** Contains code and variables used by both {@link ClientApi} and {@link ServerApi} */ /** Contains code and variables used by both {@link ClientApi} and {@link ServerApi} */
public class SharedApi public class SharedApi
@@ -57,17 +53,15 @@ public class SharedApi
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
private static final Set<DhChunkPos> UPDATING_CHUNK_POS_SET = ConcurrentHashMap.newKeySet(); private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
/** how many chunks can be queued for updating per thread, used to prevent updates from infinitely pilling up if the user flys around extremely fast */
private static final int MAX_UPDATING_CHUNK_COUNT_PER_THREAD = 500;
private static final int MIN_MS_BETWEEN_OVERLOADED_LOG_MESSAGE = 30_000;
private static final Timer CHUNK_UPDATE_TIMER = TimerUtil.CreateTimer("ChunkUpdateTimer"); private static final UpdateChunkPosManager UPDATE_POS_MANAGER = new UpdateChunkPosManager();
/** how many chunks can be queued for updating per thread, used to prevent updates from infinitely pilling up if the user flies around extremely fast */
private static final int MAX_UPDATING_CHUNK_COUNT_PER_THREAD = 500;
private static AbstractDhWorld currentWorld; private static AbstractDhWorld currentWorld;
private static int lastWorldGenTickDelta = 0; private static int lastWorldGenTickDelta = 0;
private static long lastOverloadedLogMessageMsTime = 0;
@@ -104,7 +98,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
UPDATING_CHUNK_POS_SET.clear(); UPDATE_POS_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();
@@ -140,10 +134,10 @@ public class SharedApi
* This is important since asking MC for a chunk is slow and may block the render thread. * This is important since asking MC for a chunk is slow and may block the render thread.
*/ */
public static boolean isChunkAtBlockPosAlreadyUpdating(int blockPosX, int blockPosZ) public static boolean isChunkAtBlockPosAlreadyUpdating(int blockPosX, int blockPosZ)
{ return UPDATING_CHUNK_POS_SET.contains(new DhChunkPos(new DhBlockPos2D(blockPosX, blockPosZ))); } { return UPDATE_POS_MANAGER.contains(new DhChunkPos(new DhBlockPos2D(blockPosX, blockPosZ))); }
public static boolean isChunkAtChunkPosAlreadyUpdating(int chunkPosX, int chunkPosZ) public static boolean isChunkAtChunkPosAlreadyUpdating(int chunkPosX, int chunkPosZ)
{ return UPDATING_CHUNK_POS_SET.contains(new DhChunkPos(chunkPosX, chunkPosZ)); } { return UPDATE_POS_MANAGER.contains(new DhChunkPos(chunkPosX, chunkPosZ)); }
/** handles both block place and break events */ /** handles both block place and break events */
@@ -192,43 +186,6 @@ public class SharedApi
//=====================//
// task limiting check //
//=====================//
int currentQueueCount = UPDATING_CHUNK_POS_SET.size();
int maxQueueCount = MAX_UPDATING_CHUNK_COUNT_PER_THREAD * Config.Client.Advanced.MultiThreading.numberOfLodBuilderThreads.get();
if (currentQueueCount >= maxQueueCount)
{
// The maximum number of chunks are already queued, don't add more.
// This is done to prevent overloading the system if the user fly's extremely fast and queues too many chunks
long msBetweenLastLog = System.currentTimeMillis() - lastOverloadedLogMessageMsTime;
if (msBetweenLastLog >= MIN_MS_BETWEEN_OVERLOADED_LOG_MESSAGE)
{
lastOverloadedLogMessageMsTime = System.currentTimeMillis();
String message = "Distant Horizons overloaded, too many chunks queued for updating. " +
"\nThis may result in holes in your LODs. " +
"\nPlease move through the world slower, decrease your vanilla render distance, slow down your world pre-generator, or increase the Distant Horizons' CPU load config. " +
"\nMax queue count ["+maxQueueCount+"] (["+MAX_UPDATING_CHUNK_COUNT_PER_THREAD+"] per thread).";
ClientApi.INSTANCE.showChatMessageNextFrame(message);
LOGGER.warn(message);
}
return;
}
// prevent duplicate update requests
if (UPDATING_CHUNK_POS_SET.contains(chunkWrapper.getChunkPos()))
{
// this chunk is already being updated
return;
}
UPDATING_CHUNK_POS_SET.add(chunkWrapper.getChunkPos());
//===============================// //===============================//
// update the necessary chunk(s) // // update the necessary chunk(s) //
//===============================// //===============================//
@@ -237,7 +194,7 @@ public class SharedApi
{ {
// only update the center chunk // only update the center chunk
bakeChunkLightingAndSendToLevelAsync(chunkWrapper, null, dhLevel); queueChunkUpdate(chunkWrapper, null, dhLevel);
} }
else else
{ {
@@ -258,7 +215,7 @@ public class SharedApi
else else
{ {
// neighboring chunk // neighboring chunk
DhChunkPos neighbourPos = new DhChunkPos(chunkWrapper.getChunkPos().x + xOffset, chunkWrapper.getChunkPos().z + zOffset); DhChunkPos neighbourPos = new DhChunkPos(chunkWrapper.getChunkPos().getX() + xOffset, chunkWrapper.getChunkPos().getZ() + zOffset);
IChunkWrapper neighbourChunk = dhLevel.getLevelWrapper().tryGetChunk(neighbourPos); IChunkWrapper neighbourChunk = dhLevel.getLevelWrapper().tryGetChunk(neighbourPos);
if (neighbourChunk != null) if (neighbourChunk != null)
{ {
@@ -271,30 +228,67 @@ public class SharedApi
// light and send the chunks // light and send the chunks
for (IChunkWrapper litChunk : neighbourChunkList) for (IChunkWrapper litChunk : neighbourChunkList)
{ {
bakeChunkLightingAndSendToLevelAsync(litChunk, neighbourChunkList, dhLevel); queueChunkUpdate(litChunk, neighbourChunkList, dhLevel);
} }
} }
} }
private static void bakeChunkLightingAndSendToLevelAsync(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighbourChunkList, IDhLevel dhLevel) private static void queueChunkUpdate(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighbourChunkList, IDhLevel dhLevel)
{ {
// lighting the chunk needs to be done on a separate thread to prevent lagging any of the event threads if (MC_CLIENT != null && MC_CLIENT.playerExists())
ThreadPoolExecutor executor = ThreadPoolUtil.getLightPopulatorExecutor(); {
if (executor == null) UPDATE_POS_MANAGER.setCenter(MC_CLIENT.getPlayerChunkPos());
UPDATE_POS_MANAGER.maxSize = MAX_UPDATING_CHUNK_COUNT_PER_THREAD * Config.Client.Advanced.MultiThreading.numberOfLodBuilderThreads.get();
}
UpdateChunkData updateData = new UpdateChunkData(chunkWrapper, neighbourChunkList, dhLevel);
UPDATE_POS_MANAGER.addItem(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
ThreadPoolExecutor executor = ThreadPoolUtil.getChunkToLodBuilderExecutor();
if (executor != null && executor.getQueue().size() < executor.getCorePoolSize())
{
try
{
executor.execute(SharedApi::processQueuedChunkUpdate);
}
catch (RejectedExecutionException ignore)
{
// the executor was shut down, it should be back up shortly and able to accept new jobs
}
}
}
/** returning a {@link CompletableFuture} isn't necessary, but allows Intellij to properly show the full stack trace when debugging. */
@SuppressWarnings("UnusedReturnValue")
private static void processQueuedChunkUpdate()
{
//LOGGER.trace(chunkWrapper.getChunkPos() + " " + executor.getActiveCount() + " / " + executor.getQueue().size() + " - " + executor.getCompletedTaskCount());
UpdateChunkData updateData = UPDATE_POS_MANAGER.popClosest();
if (updateData == null)
{ {
return; return;
} }
try IChunkWrapper chunkWrapper = updateData.chunkWrapper;
{ @Nullable ArrayList<IChunkWrapper> neighbourChunkList = updateData.neighbourChunkList;
executor.execute(() -> IDhLevel dhLevel = updateData.dhLevel;
{
//LOGGER.trace(chunkWrapper.getChunkPos() + " " + executor.getActiveCount() + " / " + executor.getQueue().size() + " - " + executor.getCompletedTaskCount());
try try
{ {
boolean checkChunkHash = !Config.Client.Advanced.LodBuilding.disableUnchangedChunkCheck.get();
// check if this chunk has been converted into an LOD already // check if this chunk has been converted into an LOD already
int oldChunkHash = dhLevel.getChunkHash(chunkWrapper.getChunkPos()); // shouldn't happen on the render thread since it may take a few moments to run int 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(); int newChunkHash = chunkWrapper.getBlockBiomeHashCode();
if (checkChunkHash)
{
if (oldChunkHash == newChunkHash) if (oldChunkHash == newChunkHash)
{ {
// if the chunk hashes are the same then we don't need to bother with lighting the chunk // if the chunk hashes are the same then we don't need to bother with lighting the chunk
@@ -306,6 +300,7 @@ public class SharedApi
{ {
//LOGGER.info("g: "+chunkWrapper.getChunkPos()+" "+newChunkHash); //LOGGER.info("g: "+chunkWrapper.getChunkPos()+" "+newChunkHash);
} }
}
// having a list of the nearby chunks is needed for lighting and beacon generation // having a list of the nearby chunks is needed for lighting and beacon generation
@@ -321,49 +316,10 @@ public class SharedApi
} }
// chunk light baking is disabled since profiling revealed it used // sky lighting is populated later at the data source level
// roughly the same amount of time as generating the lighting ourselves and DhLightingEngine.INSTANCE.bakeChunkBlockLighting(chunkWrapper, nearbyChunkList, dhLevel.hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT);
// was much more likely to have issues with corrupt (all black or all bright) chunks
boolean tryUsingMcLightingEngine = false;
if (tryUsingMcLightingEngine)
{
// Save or populate the chunk wrapper's lighting
// this is done so we don't have to worry about MC unloading the lighting data for this chunk
boolean chunkLightPopulated = false;
boolean onlyUseDhLighting = Config.Client.Advanced.LodBuilding.onlyUseDhLightingEngine.get();
if (!onlyUseDhLighting && chunkWrapper.isLightCorrect())
{
// If MC's lighting engine isn't thread safe this may cause the server thread to lag
chunkLightPopulated = chunkWrapper.bakeDhLightingUsingMcLightingEngine(dhLevel.getLevelWrapper());
if (!chunkLightPopulated)
{
// clear any existing data to prevent partial or corrupt lighting
// when re-generating it
chunkWrapper.clearDhBlockLighting();
chunkWrapper.clearDhSkyLighting();
}
}
// something went wrong during the baking process so we have to generate the lighting ourselves
if (!chunkLightPopulated)
{
DhLightingEngine.INSTANCE.lightChunk(chunkWrapper, nearbyChunkList, dhLevel.hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT);
}
}
else
{
DhLightingEngine.INSTANCE.lightChunk(chunkWrapper, nearbyChunkList, dhLevel.hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT);
}
// get this chunk's active beacons
List<BeaconBeamDTO> beaconBeamList = chunkWrapper.getAllActiveBeacons(nearbyChunkList);
dhLevel.setBeaconBeamsForChunk(chunkWrapper.getChunkPos(), beaconBeamList);
dhLevel.updateBeaconBeamsForChunk(chunkWrapper, nearbyChunkList);
dhLevel.updateChunkAsync(chunkWrapper, newChunkHash); dhLevel.updateChunkAsync(chunkWrapper, newChunkHash);
} }
catch (Exception e) catch (Exception e)
@@ -372,26 +328,20 @@ public class SharedApi
} }
finally finally
{ {
// the LOD chunk has finished being updated // queue the next position if there are still positions to process
int updateTimeoutInSec = Config.Client.Advanced.LodBuilding.minTimeBetweenChunkUpdatesInSeconds.get(); ThreadPoolExecutor executor = ThreadPoolUtil.getChunkToLodBuilderExecutor();
if (updateTimeoutInSec != 0) if (executor != null && !UPDATE_POS_MANAGER.positionMap.isEmpty())
{ {
// prevent updating this chunk again until the timeout finishes try
CHUNK_UPDATE_TIMER.schedule(new TimerTask()
{ {
@Override executor.execute(SharedApi::processQueuedChunkUpdate);
public void run() { UPDATING_CHUNK_POS_SET.remove(chunkWrapper.getChunkPos()); }
}, updateTimeoutInSec * 1000L);
} }
else catch (RejectedExecutionException ignore)
{ {
// instantly allow this chunk to be updated again // the executor was shut down, it should be back up shortly and able to accept new jobs
UPDATING_CHUNK_POS_SET.remove(chunkWrapper.getChunkPos());
} }
} }
});
} }
catch (RejectedExecutionException ignore) { /* the executor was shut down, it should be back up shortly and able to accept new jobs */ }
} }
@@ -402,11 +352,185 @@ public class SharedApi
public String getDebugMenuString() public String getDebugMenuString()
{ {
int maxUpdateCount = MAX_UPDATING_CHUNK_COUNT_PER_THREAD * Config.Client.Advanced.MultiThreading.numberOfLodBuilderThreads.get(); String updatingCountStr = F3Screen.NUMBER_FORMAT.format(UPDATE_POS_MANAGER.closestQueue.size());
String updatingCountStr = F3Screen.NUMBER_FORMAT.format(UPDATING_CHUNK_POS_SET.size()); String maxUpdateCountStr = F3Screen.NUMBER_FORMAT.format(UPDATE_POS_MANAGER.maxSize);
String maxUpdateCountStr = F3Screen.NUMBER_FORMAT.format(maxUpdateCount);
return "Queued chunk updates: "+updatingCountStr+" / "+maxUpdateCountStr; return "Queued chunk updates: "+updatingCountStr+" / "+maxUpdateCountStr;
} }
//================//
// helper classes //
//================//
/** contains the objects needed to update a chunk */
private static class UpdateChunkData
{
public IChunkWrapper chunkWrapper;
@Nullable
public ArrayList<IChunkWrapper> neighbourChunkList;
public IDhLevel dhLevel;
public UpdateChunkData(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighbourChunkList, IDhLevel dhLevel)
{
this.chunkWrapper = chunkWrapper;
this.neighbourChunkList = neighbourChunkList;
this.dhLevel = dhLevel;
}
}
/** keeps track of which chunks need to be updated */
private static class UpdateChunkPosManager
{
private final PriorityQueue<DhChunkPos> closestQueue;
private final PriorityQueue<DhChunkPos> furthestQueue;
private final HashMap<DhChunkPos, UpdateChunkData> positionMap;
private final ReentrantLock lock = new ReentrantLock();
private DhChunkPos center;
private int maxSize = 500;
//=============//
// constructor //
//=============//
public UpdateChunkPosManager()
{
this.closestQueue = new PriorityQueue<>(Comparator.comparingDouble(pos -> pos.squaredDistance(this.center)));
this.furthestQueue = new PriorityQueue<>(Comparator.comparingDouble(pos -> ((DhChunkPos)pos).squaredDistance(this.center)).reversed());
this.positionMap = new HashMap<>();
// defaulting to 0,0 is fine since it'll be updated once we start adding items
this.center = new DhChunkPos(0, 0);
}
//==================//
// list/set methods //
//==================//
public boolean contains(DhChunkPos pos)
{
try
{
this.lock.lock();
return this.positionMap.containsKey(pos);
}
finally
{
this.lock.unlock();
}
}
public void clear()
{
try
{
this.lock.lock();
this.positionMap.clear();
this.closestQueue.clear();
this.furthestQueue.clear();
}
finally
{
this.lock.unlock();
}
}
public void addItem(DhChunkPos pos, UpdateChunkData updateData)
{
try
{
this.lock.lock();
if (this.positionMap.containsKey(pos))
{
return;
}
if (this.positionMap.size() >= this.maxSize)
{
// Remove item furthest from the center
DhChunkPos furthest = this.furthestQueue.poll();
this.closestQueue.remove(furthest);
this.positionMap.remove(furthest);
}
this.positionMap.put(pos, updateData);
this.closestQueue.add(pos);
this.furthestQueue.add(pos);
}
finally
{
this.lock.unlock();
}
}
//==================//
// position methods //
//==================//
public void setCenter(DhChunkPos newCenter)
{
// if the rebuild time takes too long
// (in James' testing a queue of 500 items only took around 0.1 milliseconds)
// this equation could be changed to only update after moving 2 or 4 chunks from the center
if (newCenter.equals(this.center))
{
return;
}
try
{
this.lock.lock();
this.center = newCenter;
// rebuild the priority queues to match the new center
this.closestQueue.clear();
this.furthestQueue.clear();
for (DhChunkPos pos : this.positionMap.keySet())
{
this.closestQueue.add(pos);
this.furthestQueue.add(pos);
}
}
finally
{
this.lock.unlock();
}
}
public UpdateChunkData popClosest()
{
if (this.closestQueue.isEmpty())
{
return null;
}
try
{
this.lock.lock();
DhChunkPos closest = this.closestQueue.poll();
this.furthestQueue.remove(closest);
return this.positionMap.remove(closest);
}
finally
{
this.lock.unlock();
}
}
}
} }
@@ -43,6 +43,7 @@ import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
/** /**
@@ -60,59 +61,236 @@ public class Config
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public static ConfigCategory client = new ConfigCategory.Builder().set(Client.class).build(); private static Config GLOBAL_INSTANCE;
public static void initializeGlobalConfig()
public static class Client
{ {
public static ConfigEntry<Boolean> quickEnableRendering = new ConfigEntry.Builder<Boolean>() if (GLOBAL_INSTANCE != null)
.set(true) {
.comment("" throw new IllegalStateException("Cannot reinitialize the Config");
+ "If true, Distant Horizons will render LODs beyond the vanilla render distance." }
+ "") GLOBAL_INSTANCE = new Config();
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI) }
public static Config getGlobalConfig() { return GLOBAL_INSTANCE; }
public final QuickSettings quickSettings = new QuickSettings();
public final Graphics graphics = new Graphics();
public class QuickSettings
{
public final ConfigEntry<Boolean> enableRendering = new ConfigEntry.Builder<Boolean>()
.computed(
() -> Config.this.debugging.rendererMode.get() != EDhApiRendererMode.DISABLED,
enableRendering -> Config.this.debugging.rendererMode.set(enableRendering
? EDhApiRendererMode.DEFAULT
: EDhApiRendererMode.DISABLED
)
)
.build(); .build();
public static ConfigLinkedEntry quickLodChunkRenderDistance = new ConfigLinkedEntry(Advanced.Graphics.Quality.lodChunkRenderDistanceRadius); public final ConfigEntry<Integer> lodChunkRenderDistanceRadius = new ConfigEntry.Builder<Integer>()
.linkedTo(() -> Config.this.graphics.quality.lodChunkRenderDistanceRadius)
public static ConfigEntry<EDhApiQualityPreset> qualityPresetSetting = new ConfigEntry.Builder<EDhApiQualityPreset>()
.set(EDhApiQualityPreset.MEDIUM) // the default value is set via the listener when accessed
.comment(""
+ "Changing this setting will modify a number of different settings that will change the \n"
+ "visual fidelity of the rendered LODs.\n"
+ "\n"
+ "Higher settings will improve the graphical quality while increasing GPU and memory use.\n"
+ "")
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(RenderQualityPresetConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<EDhApiThreadPreset> threadPresetSetting = new ConfigEntry.Builder<EDhApiThreadPreset>() public final ConfigEntry<EDhApiQualityPreset> qualityPreset = new ConfigEntry.Builder<EDhApiQualityPreset>()
.preset(
new ConfigEntry.PresetBuilder<>(EDhApiQualityPreset.CUSTOM)
.addConfigEntry(() -> Config.this.graphics.quality.maxHorizontalResolution, map -> {
map.put(EDhApiQualityPreset.MINIMUM, EDhApiMaxHorizontalResolution.TWO_BLOCKS);
map.put(EDhApiQualityPreset.LOW, EDhApiMaxHorizontalResolution.BLOCK);
map.put(EDhApiQualityPreset.MEDIUM, EDhApiMaxHorizontalResolution.BLOCK);
map.put(EDhApiQualityPreset.HIGH, EDhApiMaxHorizontalResolution.BLOCK);
map.put(EDhApiQualityPreset.EXTREME, EDhApiMaxHorizontalResolution.BLOCK);
})
.addConfigEntry(() -> Config.this.graphics.quality.verticalQuality, map -> {
map.put(EDhApiQualityPreset.MINIMUM, EDhApiVerticalQuality.HEIGHT_MAP);
map.put(EDhApiQualityPreset.LOW, EDhApiVerticalQuality.LOW);
map.put(EDhApiQualityPreset.MEDIUM, EDhApiVerticalQuality.MEDIUM);
map.put(EDhApiQualityPreset.HIGH, EDhApiVerticalQuality.HIGH);
map.put(EDhApiQualityPreset.EXTREME, EDhApiVerticalQuality.EXTREME);
})
.addConfigEntry(() -> Config.this.graphics.quality.horizontalQuality, map -> {
map.put(EDhApiQualityPreset.MINIMUM, EDhApiHorizontalQuality.LOWEST);
map.put(EDhApiQualityPreset.LOW, EDhApiHorizontalQuality.LOW);
map.put(EDhApiQualityPreset.MEDIUM, EDhApiHorizontalQuality.MEDIUM);
map.put(EDhApiQualityPreset.HIGH, EDhApiHorizontalQuality.HIGH);
map.put(EDhApiQualityPreset.EXTREME, EDhApiHorizontalQuality.EXTREME);
})
.addConfigEntry(() -> Config.this.graphics.quality.transparency, map -> {
map.put(EDhApiQualityPreset.MINIMUM, EDhApiTransparency.DISABLED);
map.put(EDhApiQualityPreset.LOW, EDhApiTransparency.DISABLED); // should be fake if/when fake is fixed
map.put(EDhApiQualityPreset.MEDIUM, EDhApiTransparency.COMPLETE);
map.put(EDhApiQualityPreset.HIGH, EDhApiTransparency.COMPLETE);
map.put(EDhApiQualityPreset.EXTREME, EDhApiTransparency.COMPLETE);
})
.addConfigEntry(() -> Config.this.graphics.quality.ssao.enabled, map -> {
map.put(EDhApiQualityPreset.MINIMUM, false);
map.put(EDhApiQualityPreset.LOW, false);
map.put(EDhApiQualityPreset.MEDIUM, true);
map.put(EDhApiQualityPreset.HIGH, true);
map.put(EDhApiQualityPreset.EXTREME, true);
})
)
.build();
public final ConfigEntry<EDhApiThreadPreset> threadPreset = new ConfigEntry.Builder<EDhApiThreadPreset>()
.set(EDhApiThreadPreset.LOW_IMPACT) // the default value is set via the listener when accessed .set(EDhApiThreadPreset.LOW_IMPACT) // the default value is set via the listener when accessed
.comment(""
+ "Changing this setting will modify a number of different settings that will change \n"
+ "the load that Distant Horizons is allowed to put on your CPU. \n"
+ "\n"
+ "Higher options will improve LOD generation and loading speed, \n"
+ "but will increase CPU load and may introduce stuttering.\n"
+ "\n"
+ "Note: on CPUs with 4 cores or less these settings will be less effective \n"
+ " and some settings will give similar results. \n"
+ "")
.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI) .setAppearance(EConfigEntryAppearance.ONLY_IN_GUI)
.addListener(ThreadPresetConfigEventHandler.INSTANCE) .addListener(ThreadPresetConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigLinkedEntry quickEnableWorldGenerator = new ConfigLinkedEntry(Advanced.WorldGenerator.enableDistantGeneration); public final ConfigLinkedEntry quickEnableWorldGenerator = new ConfigLinkedEntry(_Client.Advanced.WorldGenerator.enableDistantGeneration);
public static ConfigEntry<Boolean> optionsButton = new ConfigEntry.Builder<Boolean>() public final ConfigLinkedEntry quickLodCloudRendering = new ConfigLinkedEntry(_Client.Advanced.Graphics.GenericRendering.enableCloudRendering);
public final ConfigEntry<Boolean> showOptionsButton = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) .setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.comment("" + .comment("" +
"Should Distant Horizon's config button appear in the options screen next to fov slider?") "Should Distant Horizon's config button appear in the options screen next to fov slider?")
.build(); .build();
}
public static class Graphics
{
public final Quality quality = new Quality();
public static class Quality
{
public ConfigEntry<Integer> lodChunkRenderDistanceRadius = new ConfigEntry.Builder<Integer>()
.setServersideShortName("renderDistanceRadius")
.setMinDefaultMax(32, 128, 4096)
.comment("" +
"The radius of the mod's render distance. (measured in chunks)\n" +
"On server changes the distance players will receive real-time updates for, if enabled." +
"\n" +
"Note for servers:\n" +
"This setting does not prevent players from generating farther out.\n" +
"If you want to limit performance impact, change rate limits\n" +
"and thread count/runtime ratio settings instead.\n" +
"It also does not affect the visuals on clients.")
.setPerformance(EConfigEntryPerformance.HIGH)
.setSide(EConfigEntryRelevantSide.BOTH)
.build();
public ConfigEntry<EDhApiMaxHorizontalResolution> maxHorizontalResolution = new ConfigEntry.Builder<EDhApiMaxHorizontalResolution>()
.set(EDhApiMaxHorizontalResolution.BLOCK)
.comment(""
+ "What is the maximum detail LODs should be drawn at? \n"
+ "Higher settings will increase memory and GPU usage. \n"
+ "\n"
+ EDhApiMaxHorizontalResolution.CHUNK + ": render 1 LOD for each Chunk. \n"
+ EDhApiMaxHorizontalResolution.HALF_CHUNK + ": render 4 LODs for each Chunk. \n"
+ EDhApiMaxHorizontalResolution.FOUR_BLOCKS + ": render 16 LODs for each Chunk. \n"
+ EDhApiMaxHorizontalResolution.TWO_BLOCKS + ": render 64 LODs for each Chunk. \n"
+ EDhApiMaxHorizontalResolution.BLOCK + ": render 256 LODs for each Chunk (width of one block). \n"
+ "\n"
+ "Lowest Quality: " + EDhApiMaxHorizontalResolution.CHUNK + "\n"
+ "Highest Quality: " + EDhApiMaxHorizontalResolution.BLOCK)
.setPerformance(EConfigEntryPerformance.MEDIUM)
.build();
public ConfigEntry<EDhApiVerticalQuality> verticalQuality = new ConfigEntry.Builder<EDhApiVerticalQuality>()
.set(EDhApiVerticalQuality.MEDIUM)
.comment(""
+ "This indicates how well LODs will represent \n"
+ "overhangs, caves, floating islands, etc. \n"
+ "Higher options will make the world more accurate, but"
+ "will increase memory and GPU usage. \n"
+ "\n"
+ "Lowest Quality: " + EDhApiVerticalQuality.HEIGHT_MAP + "\n"
+ "Highest Quality: " + EDhApiVerticalQuality.EXTREME)
.setPerformance(EConfigEntryPerformance.VERY_HIGH)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
public ConfigEntry<EDhApiHorizontalQuality> horizontalQuality = new ConfigEntry.Builder<EDhApiHorizontalQuality>()
.set(EDhApiHorizontalQuality.MEDIUM)
.comment(""
+ "This indicates how quickly LODs decrease in quality the further away they are. \n"
+ "Higher settings will render higher quality fake chunks farther away, \n"
+ "but will increase memory and GPU usage.")
.setPerformance(EConfigEntryPerformance.MEDIUM)
.build();
public ConfigEntry<EDhApiTransparency> transparency = new ConfigEntry.Builder<EDhApiTransparency>()
.set(EDhApiTransparency.COMPLETE)
.comment(""
+ "How should LOD transparency be handled. \n"
+ "\n"
+ EDhApiTransparency.COMPLETE + ": LODs will render transparent. \n"
+ EDhApiTransparency.FAKE + ": LODs will be opaque, but shaded to match the blocks underneath. \n"
+ EDhApiTransparency.DISABLED + ": LODs will be opaque. \n"
+ "")
.setPerformance(EConfigEntryPerformance.MEDIUM)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
public ConfigEntry<EDhApiBlocksToAvoid> blocksToIgnore = new ConfigEntry.Builder<EDhApiBlocksToAvoid>()
.set(EDhApiBlocksToAvoid.NON_COLLIDING)
.comment(""
+ "What blocks shouldn't be rendered as LODs? \n"
+ "\n"
+ EDhApiBlocksToAvoid.NONE + ": Represent all blocks in the LODs \n"
+ EDhApiBlocksToAvoid.NON_COLLIDING + ": Only represent solid blocks in the LODs (tall grass, torches, etc. won't count for a LOD's height) \n"
+ "")
.setPerformance(EConfigEntryPerformance.NONE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
public ConfigEntry<Boolean> tintWithAvoidedBlocks = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "Should the blocks underneath avoided blocks gain the color of the avoided block? \n"
+ "\n"
+ "True: a red flower will tint the grass below it red. \n"
+ "False: skipped blocks will not change color of surface below them. "
+ "")
.setPerformance(EConfigEntryPerformance.NONE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
// TODO fixme
//public static ConfigEntry<Integer> lodBiomeBlending = new ConfigEntry.Builder<Integer>()
// .setMinDefaultMax(0,1,7)
// .comment(""
// + "This is the same as vanilla Biome Blending settings for Lod area. \n"
// + " Note that anything other than '0' will greatly effect Lod building time \n"
// + " and increase triangle count. The cost on chunk generation speed is also \n"
// + " quite large if set too high.\n"
// + "\n"
// + " '0' equals to Vanilla Biome Blending of '1x1' or 'OFF', \n"
// + " '1' equals to Vanilla Biome Blending of '3x3', \n"
// + " '2' equals to Vanilla Biome Blending of '5x5'...")
// .build();
}
}
public final Debugging debugging = new Debugging();
public static class Debugging
{
public ConfigEntry<EDhApiRendererMode> rendererMode = new ConfigEntry.Builder<EDhApiRendererMode>()
.set(EDhApiRendererMode.DEFAULT)
.comment(""
+ "What renderer is active? \n"
+ "\n"
+ EDhApiRendererMode.DEFAULT + ": Default lod renderer \n"
+ EDhApiRendererMode.DEBUG + ": Debug testing renderer \n"
+ EDhApiRendererMode.DISABLED + ": Disable rendering")
.build();
}
public static class _Client
{
public static ConfigCategory advanced = new ConfigCategory.Builder().set(Advanced.class).build(); public static ConfigCategory advanced = new ConfigCategory.Builder().set(Advanced.class).build();
@@ -143,102 +321,6 @@ public class Config
public static class Quality public static class Quality
{ {
public static ConfigEntry<EDhApiMaxHorizontalResolution> maxHorizontalResolution = new ConfigEntry.Builder<EDhApiMaxHorizontalResolution>()
.set(EDhApiMaxHorizontalResolution.BLOCK)
.comment(""
+ "What is the maximum detail LODs should be drawn at? \n"
+ "Higher settings will increase memory and GPU usage. \n"
+ "\n"
+ EDhApiMaxHorizontalResolution.CHUNK + ": render 1 LOD for each Chunk. \n"
+ EDhApiMaxHorizontalResolution.HALF_CHUNK + ": render 4 LODs for each Chunk. \n"
+ EDhApiMaxHorizontalResolution.FOUR_BLOCKS + ": render 16 LODs for each Chunk. \n"
+ EDhApiMaxHorizontalResolution.TWO_BLOCKS + ": render 64 LODs for each Chunk. \n"
+ EDhApiMaxHorizontalResolution.BLOCK + ": render 256 LODs for each Chunk (width of one block). \n"
+ "\n"
+ "Lowest Quality: " + EDhApiMaxHorizontalResolution.CHUNK + "\n"
+ "Highest Quality: " + EDhApiMaxHorizontalResolution.BLOCK)
.setPerformance(EConfigEntryPerformance.MEDIUM)
.build();
public static ConfigEntry<Integer> lodChunkRenderDistanceRadius = new ConfigEntry.Builder<Integer>()
.setMinDefaultMax(32, 128, 4096)
.comment("The radius of the mod's render distance. (measured in chunks)")
.setPerformance(EConfigEntryPerformance.HIGH)
.build();
public static ConfigEntry<EDhApiVerticalQuality> verticalQuality = new ConfigEntry.Builder<EDhApiVerticalQuality>()
.set(EDhApiVerticalQuality.MEDIUM)
.comment(""
+ "This indicates how well LODs will represent \n"
+ "overhangs, caves, floating islands, etc. \n"
+ "Higher options will make the world more accurate, but"
+ "will increase memory and GPU usage. \n"
+ "\n"
+ "Lowest Quality: " + EDhApiVerticalQuality.HEIGHT_MAP + "\n"
+ "Highest Quality: " + EDhApiVerticalQuality.EXTREME)
.setPerformance(EConfigEntryPerformance.VERY_HIGH)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
public static ConfigEntry<EDhApiHorizontalQuality> horizontalQuality = new ConfigEntry.Builder<EDhApiHorizontalQuality>()
.set(EDhApiHorizontalQuality.MEDIUM)
.comment(""
+ "This indicates how quickly LODs decrease in quality the further away they are. \n"
+ "Higher settings will render higher quality fake chunks farther away, \n"
+ "but will increase memory and GPU usage.")
.setPerformance(EConfigEntryPerformance.MEDIUM)
.build();
public static ConfigEntry<EDhApiTransparency> transparency = new ConfigEntry.Builder<EDhApiTransparency>()
.set(EDhApiTransparency.COMPLETE)
.comment(""
+ "How should LOD transparency be handled. \n"
+ "\n"
+ EDhApiTransparency.COMPLETE + ": LODs will render transparent. \n"
+ EDhApiTransparency.FAKE + ": LODs will be opaque, but shaded to match the blocks underneath. \n"
+ EDhApiTransparency.DISABLED + ": LODs will be opaque. \n"
+ "")
.setPerformance(EConfigEntryPerformance.MEDIUM)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
public static ConfigEntry<EDhApiBlocksToAvoid> blocksToIgnore = new ConfigEntry.Builder<EDhApiBlocksToAvoid>()
.set(EDhApiBlocksToAvoid.NON_COLLIDING)
.comment(""
+ "What blocks shouldn't be rendered as LODs? \n"
+ "\n"
+ EDhApiBlocksToAvoid.NONE + ": Represent all blocks in the LODs \n"
+ EDhApiBlocksToAvoid.NON_COLLIDING + ": Only represent solid blocks in the LODs (tall grass, torches, etc. won't count for a LOD's height) \n"
+ "")
.setPerformance(EConfigEntryPerformance.NONE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
public static ConfigEntry<Boolean> tintWithAvoidedBlocks = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "Should the blocks underneath avoided blocks gain the color of the avoided block? \n"
+ "\n"
+ "True: a red flower will tint the grass below it red. \n"
+ "False: skipped blocks will not change color of surface below them. "
+ "")
.setPerformance(EConfigEntryPerformance.NONE)
.addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build();
// TODO fixme
// public static ConfigEntry<Integer> lodBiomeBlending = new ConfigEntry.Builder<Integer>()
// .setMinDefaultMax(0,1,7)
// .comment(""
// + "This is the same as vanilla Biome Blending settings for Lod area. \n"
// + " Note that anything other than '0' will greatly effect Lod building time \n"
// + " and increase triangle count. The cost on chunk generation speed is also \n"
// + " quite large if set too high.\n"
// + "\n"
// + " '0' equals to Vanilla Biome Blending of '1x1' or 'OFF', \n"
// + " '1' equals to Vanilla Biome Blending of '3x3', \n"
// + " '2' equals to Vanilla Biome Blending of '5x5'...")
// .build();
} }
public static class Fog public static class Fog
@@ -699,6 +781,17 @@ public class Config
.addListener(ReloadLodsConfigEventHandler.INSTANCE) .addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
public static ConfigEntry<Boolean> disableBeaconDistanceCulling = new ConfigEntry.Builder<Boolean>()
.set(false)
.comment(""
+ "If true all beacons near the camera won't be drawn to prevent vanilla overdraw. \n"
+ "If false all beacons will be rendered. \n"
+ "\n"
+ "Generally this should be left as false. It's main purpose is for debugging\n"
+ "beacon updating/rendering.\n"
+ "")
.build();
} }
} }
@@ -706,16 +799,16 @@ public class Config
public static class WorldGenerator public static class WorldGenerator
{ {
public static ConfigEntry<Boolean> enableDistantGeneration = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableDistantGeneration = new ConfigEntry.Builder<Boolean>()
.setServersideShortName("enableDistantGeneration")
.set(true) .set(true)
.comment("" .comment(""
+ " Should Distant Horizons slowly generate LODs \n" + " Should Distant Horizons slowly generate LODs \n"
+ " outside the vanilla render distance?\n" + " outside the vanilla render distance?")
+ "\n" .setSide(EConfigEntryRelevantSide.BOTH)
+ " Note: when on a server, distant generation isn't supported \n"
+ " and will always be disabled.")
.build(); .build();
public static ConfigEntry<EDhApiDistantGeneratorMode> distantGeneratorMode = new ConfigEntry.Builder<EDhApiDistantGeneratorMode>() public static ConfigEntry<EDhApiDistantGeneratorMode> distantGeneratorMode = new ConfigEntry.Builder<EDhApiDistantGeneratorMode>()
.setServersideShortName("distantGeneratorMode")
.set(EDhApiDistantGeneratorMode.FEATURES) .set(EDhApiDistantGeneratorMode.FEATURES)
.comment("" .comment(""
+ "How detailed should LODs be generated outside the vanilla render distance? \n" + "How detailed should LODs be generated outside the vanilla render distance? \n"
@@ -745,32 +838,36 @@ public class Config
+ EDhApiDistantGeneratorMode.FEATURES + " \n" + EDhApiDistantGeneratorMode.FEATURES + " \n"
+ "Generate everything except structures. \n" + "Generate everything except structures. \n"
+ "WARNING: This may cause world generator bugs or instability when paired with certain world generator mods. \n" + "WARNING: This may cause world generator bugs or instability when paired with certain world generator mods. \n"
//not currently implemented
//+ "\n"
//+ EDhApiDistantGeneratorMode.FULL + " \n"
//+ "Ask the local server to generate/load each chunk. \n"
//+ "This is the most compatible, but will cause server/simulation lag. \n"
//+ "- Slow (15-50 ms, with spikes up to 200 ms) \n"
+ "") + "")
/* .setSide(EConfigEntryRelevantSide.BOTH)
// FULL isn't currently implemented
+ "\n"
+ EDhApiDistantGeneratorMode.FULL + " \n"
+ "Ask the local server to generate/load each chunk. \n"
+ "This is the most compatible, but will cause server/simulation lag. \n"
+ "- Slow (15-50 ms, with spikes up to 200 ms) \n"
*/
.build(); .build();
public static ConfigEntry<Integer> worldGenerationTimeoutLengthInSeconds = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> worldGenerationTimeoutLengthInSeconds = new ConfigEntry.Builder<Integer>()
.setServersideShortName("worldGenerationTimeout")
.setMinDefaultMax(5, 60 * 3, 60 * 10/*10 minutes*/ ) .setMinDefaultMax(5, 60 * 3, 60 * 10/*10 minutes*/ )
.comment("" .comment(""
+ "How long should a world generator thread run for before timing out? \n" + "How long should a world generator thread run for before timing out? \n"
+ "Note: If you are experiencing timeout errors it is better to lower your CPU usage first \n" + "Note: If you are experiencing timeout errors it is better to lower your CPU usage first \n"
+ "via the thread config before changing this value. \n" + "via the thread config before changing this value. \n"
+ "") + "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
} }
public static class LodBuilding public static class LodBuilding
{ {
@Deprecated
public static ConfigEntry<Integer> minTimeBetweenChunkUpdatesInSeconds = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> minTimeBetweenChunkUpdatesInSeconds = new ConfigEntry.Builder<Integer>()
.setServersideShortName("minTimeBetweenChunkUpdates")
.setMinDefaultMax(0, 1, 60) .setMinDefaultMax(0, 1, 60)
.setAppearance(EConfigEntryAppearance.ONLY_IN_API)
.comment("" .comment(""
+ "Determines how long must pass between LOD chunk updates before another. \n" + "Determines how long must pass between LOD chunk updates before another. \n"
+ "update can occur\n" + "update can occur\n"
@@ -780,6 +877,19 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Boolean> disableUnchangedChunkCheck = new ConfigEntry.Builder<Boolean>()
.set(false)
.comment(""
+ "Normally DH will attempt to skip creating LODs for chunks it's already seen\n"
+ "and that haven't changed.\n"
+ "\n"
+ "However sometimes that logic incorrecly prevents LODs from being updated.\n"
+ "Disabling this check may fix issues where LODs aren't updated after\n"
+ "blocks have been changed.\n"
+ "")
.build();
/** Currently we always use the DH lighting engine because there's a high likelyhood of MC returning incorrect lighting otherwise */
@Deprecated @Deprecated
public static ConfigEntry<Boolean> onlyUseDhLightingEngine = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> onlyUseDhLightingEngine = new ConfigEntry.Builder<Boolean>()
.set(false) .set(false)
@@ -858,7 +968,7 @@ public class Config
.build(); .build();
public static ConfigEntry<String> ignoredRenderBlockCsv = new ConfigEntry.Builder<String>() public static ConfigEntry<String> ignoredRenderBlockCsv = new ConfigEntry.Builder<String>()
.set("minecraft:barrier,minecraft:structure_void,minecraft:light,minecraft:tripwire") .set("minecraft:barrier,minecraft:structure_void,minecraft:light,minecraft:tripwire,minecraft:brown_mushroom")
.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"
+ "Note: air is always included in this list. \n" + "Note: air is always included in this list. \n"
@@ -939,28 +1049,99 @@ public class Config
+ "") + "")
.build(); .build();
// not currently implemented
private static ConfigEntry<Boolean> enableMultiverseNetworking = new ConfigEntry.Builder<Boolean>()
public static ConfigCategory serverNetworking = new ConfigCategory.Builder().set(ServerNetworking.class).build();
public static class ServerNetworking
{
public static ConfigUIComment generalSectionNote = new ConfigUIComment();
public static ConfigEntry<Boolean> enableServerNetworking = new ConfigEntry.Builder<Boolean>()
.setServersideShortName("enableServerNetworking")
.set(true) .set(true)
.comment("" .comment(""
+ "If true Distant Horizons will attempt to communicate with the connected \n" + "WARNING!\n"
+ "server in order to improve multiverse support. \n" + "Server-client networking is not yet fully implemented!\n"
+ "Both the server and client must be running the server-side fork with this option enabled\n"
+ "for Distant Horizons data to be transceived.\n"
+ "\n"
+ "If true, the server and client will attempt to communicate to transceive Distant Horizons data.\n"
+ "This allows for further distant generation and LOD updates on all clients.\n"
+ "\n"
+ "This should only be used on trusted servers with trusted players!\n"
+ "") + "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
// not currently implemented
private static ConfigEntry<Boolean> enableServerNetworking = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> sendLevelKeys = new ConfigEntry.Builder<Boolean>()
.setServersideShortName("sendLevelKeys")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.set(true)
.comment(""
+ "Makes the server send level keys for each world.\n"
+ "Disable this if you use alternative ways to send level keys.\n"
+ "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build();
public static ConfigEntry<String> levelKeyPrefix = new ConfigEntry.Builder<String>()
.setServersideShortName("levelKeyPrefix")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.set(getDefaultLevelKeyPrefix())
.comment(""
+ "Prefix of the level keys sent to the clients.\n"
+ "Should be set to a unique value for each backend server behind a proxy,\n"
+ "or empty if you don't use a proxy.\n"
+ "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build();
public static ConfigUIComment generationSectionNote = new ConfigUIComment();
public static ConfigEntry<Integer> generationRequestRateLimit = new ConfigEntry.Builder<Integer>()
.setServersideShortName("generationRequestRateLimit")
.setMinDefaultMax(1, 20, 100)
.comment(""
+ "How many LOD generation requests per second should a client send? \n"
+ "Also limits the amount of player's requests allowed to stay in the server's queue."
+ "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build();
public static ConfigUIComment realTimeUpdatesSectionNote = new ConfigUIComment();
public static ConfigEntry<Boolean> enableRealTimeUpdates = new ConfigEntry.Builder<Boolean>()
.setServersideShortName("enableRealTimeUpdates")
.set(false) .set(false)
.comment("" .comment(""
+ "Attention: this is only for developers and hasn't been implemented.\n" + "If true, the client will receive real-time LOD updates for chunks outside the client's render distance."
+ "\n"
+ "If true Distant Horizons will attempt to communicate with the connected \n"
+ "server in order to load LODs outside your vanilla render distance. \n"
+ "\n"
+ "Note: This requires DH to be installed on the server in order to function. \n"
+ "") + "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
// TODO rename
public static ConfigUIComment syncOnLoginSectionNote = new ConfigUIComment();
public static ConfigEntry<Boolean> synchronizeOnLogin = new ConfigEntry.Builder<Boolean>()
.setServersideShortName("synchronizeOnLogin")
.set(false)
.comment(""
+ "If true, clients will receive updated LODs on join if any changes occurred since last join."
+ "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build();
public static ConfigEntry<Integer> syncOnLoginRateLimit = new ConfigEntry.Builder<Integer>()
.setServersideShortName("syncOnLoginRateLimit")
.setMinDefaultMax(1, 50, 100)
.comment(""
+ "How many LOD sync requests per second should a client send? \n"
+ "Also limits the amount of player's requests allowed to stay in the server's queue."
+ "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build();
}
} }
public static class MultiThreading public static class MultiThreading
@@ -981,6 +1162,7 @@ public class Config
public static final ConfigEntry<Integer> numberOfWorldGenerationThreads = new ConfigEntry.Builder<Integer>() public static final ConfigEntry<Integer> numberOfWorldGenerationThreads = new ConfigEntry.Builder<Integer>()
.setServersideShortName("numberOfWorldGenerationThreads")
.setMinDefaultMax(1, .setMinDefaultMax(1,
ThreadPresetConfigEventHandler.getWorldGenDefaultThreadCount(), ThreadPresetConfigEventHandler.getWorldGenDefaultThreadCount(),
Runtime.getRuntime().availableProcessors()) Runtime.getRuntime().availableProcessors())
@@ -994,13 +1176,17 @@ public class Config
+ "generation speed, increase this number. \n" + "generation speed, increase this number. \n"
+ "\n" + "\n"
+ THREAD_NOTE) + THREAD_NOTE)
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static final ConfigEntry<Double> runTimeRatioForWorldGenerationThreads = new ConfigEntry.Builder<Double>() public static final ConfigEntry<Double> runTimeRatioForWorldGenerationThreads = new ConfigEntry.Builder<Double>()
.setServersideShortName("runTimeRatioForWorldGenerationThreads")
.setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getWorldGenDefaultRunTimeRatio(), 1.0) .setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getWorldGenDefaultRunTimeRatio(), 1.0)
.comment(THREAD_RUN_TIME_RATIO_NOTE) .comment(THREAD_RUN_TIME_RATIO_NOTE)
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static final ConfigEntry<Integer> numberOfFileHandlerThreads = new ConfigEntry.Builder<Integer>() public static final ConfigEntry<Integer> numberOfFileHandlerThreads = new ConfigEntry.Builder<Integer>()
.setServersideShortName("numberOfFileHandlerThreads")
.setMinDefaultMax(1, .setMinDefaultMax(1,
ThreadPresetConfigEventHandler.getFileHandlerDefaultThreadCount(), ThreadPresetConfigEventHandler.getFileHandlerDefaultThreadCount(),
Runtime.getRuntime().availableProcessors()) Runtime.getRuntime().availableProcessors())
@@ -1012,10 +1198,13 @@ public class Config
+ "quickly flying through existing LODs. \n" + "quickly flying through existing LODs. \n"
+ "\n" + "\n"
+ THREAD_NOTE) + THREAD_NOTE)
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static final ConfigEntry<Double> runTimeRatioForFileHandlerThreads = new ConfigEntry.Builder<Double>() public static final ConfigEntry<Double> runTimeRatioForFileHandlerThreads = new ConfigEntry.Builder<Double>()
.setServersideShortName("runTimeRatioForFileHandlerThreads")
.setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getFileHandlerDefaultRunTimeRatio(), 1.0) .setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getFileHandlerDefaultRunTimeRatio(), 1.0)
.comment(THREAD_RUN_TIME_RATIO_NOTE) .comment(THREAD_RUN_TIME_RATIO_NOTE)
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static final ConfigEntry<Integer> numberOfUpdatePropagatorThreads = new ConfigEntry.Builder<Integer>() public static final ConfigEntry<Integer> numberOfUpdatePropagatorThreads = new ConfigEntry.Builder<Integer>()
@@ -1044,6 +1233,7 @@ public class Config
.build(); .build();
public static final ConfigEntry<Integer> numberOfLodBuilderThreads = new ConfigEntry.Builder<Integer>() public static final ConfigEntry<Integer> numberOfLodBuilderThreads = new ConfigEntry.Builder<Integer>()
.setServersideShortName("numberOfLodBuilderThreads")
.setMinDefaultMax(1, .setMinDefaultMax(1,
ThreadPresetConfigEventHandler.getLodBuilderDefaultThreadCount(), ThreadPresetConfigEventHandler.getLodBuilderDefaultThreadCount(),
Runtime.getRuntime().availableProcessors()) Runtime.getRuntime().availableProcessors())
@@ -1054,12 +1244,16 @@ public class Config
+ "certain graphics settings are changed, and when moving around the world. \n" + "certain graphics settings are changed, and when moving around the world. \n"
+ "\n" + "\n"
+ THREAD_NOTE) + THREAD_NOTE)
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static final ConfigEntry<Double> runTimeRatioForLodBuilderThreads = new ConfigEntry.Builder<Double>() public static final ConfigEntry<Double> runTimeRatioForLodBuilderThreads = new ConfigEntry.Builder<Double>()
.setServersideShortName("runTimeRatioForLodBuilderThreads")
.setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getLodBuilderDefaultRunTimeRatio(), 1.0) .setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getLodBuilderDefaultRunTimeRatio(), 1.0)
.comment(THREAD_RUN_TIME_RATIO_NOTE) .comment(THREAD_RUN_TIME_RATIO_NOTE)
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static final ConfigEntry<Boolean> enableLodBuilderThreadLimiting = new ConfigEntry.Builder<Boolean>() public static final ConfigEntry<Boolean> enableLodBuilderThreadLimiting = new ConfigEntry.Builder<Boolean>()
.setServersideShortName("enableLodBuilderThreadLimiting")
.set(true) .set(true)
.comment("" .comment(""
+ "Should only be disabled if deadlock occurs and LODs refuse to update. \n" + "Should only be disabled if deadlock occurs and LODs refuse to update. \n"
@@ -1067,8 +1261,29 @@ public class Config
+ "\n" + "\n"
+ "Note that if deadlock did occur restarting MC may be necessary to stop the locked threads. \n" + "Note that if deadlock did occur restarting MC may be necessary to stop the locked threads. \n"
+ "") + "")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static final ConfigEntry<Integer> numberOfNetworkCompressionThreads = new ConfigEntry.Builder<Integer>()
.setServersideShortName("numberOfNetworkCompressionThreads")
.setMinDefaultMax(1,
ThreadPresetConfigEventHandler.getNetworkCompressionDefaultThreadCount(),
Runtime.getRuntime().availableProcessors())
.comment(""
+ "How many threads should be used when (de)compressing LODs \n"
+ "that are received/sent over the network?\n"
+ "\n"
+ "This pool doesn't do anything in singleplayer or when connected \n"
+ "to a server that doesn't support DH networking. \n"
+ "\n"
+ THREAD_NOTE)
.setSide(EConfigEntryRelevantSide.BOTH)
.build();
public static final ConfigEntry<Double> runTimeRatioForNetworkCompressionThreads = new ConfigEntry.Builder<Double>()
.setServersideShortName("runTimeRatioForNetworkCompressionThreads")
.setMinDefaultMax(0.01, ThreadPresetConfigEventHandler.getNetworkCompressionDefaultRunTimeRatio(), 1.0)
.comment(THREAD_RUN_TIME_RATIO_NOTE)
.build();
} }
public static class AutoUpdater public static class AutoUpdater
@@ -1102,31 +1317,39 @@ public class Config
// TODO add change all option // TODO add change all option
// TODO default to error chat and info file // TODO default to error chat and info file
public static ConfigEntry<EDhApiLoggerMode> logWorldGenEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logWorldGenEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setServersideShortName("logWorldGenEvent")
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE)
.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.")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logWorldGenPerformance = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logWorldGenPerformance = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setServersideShortName("logWorldGenPerformance")
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE)
.comment("" .comment(""
+ "If enabled, the mod will log performance about the world generation process. \n" + "If enabled, the mod will log performance about the world generation process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logWorldGenLoadEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logWorldGenLoadEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setServersideShortName("logWorldGenPerformance")
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE)
.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.")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logLodBuilderEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logLodBuilderEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setServersideShortName("logLodBuilderEvent")
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE)
.comment("" .comment(""
+ "If enabled, the mod will log information about the LOD generation process. \n" + "If enabled, the mod will log information about the LOD generation process. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logRendererBufferEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logRendererBufferEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
@@ -1144,24 +1367,30 @@ public class Config
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logFileReadWriteEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logFileReadWriteEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setServersideShortName("logFileReadWriteEvent")
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE)
.comment("" .comment(""
+ "If enabled, the mod will log information about file read/write operations. \n" + "If enabled, the mod will log information about file read/write operations. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logFileSubDimEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logFileSubDimEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setServersideShortName("logFileSubDimEvent")
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE)
.comment("" .comment(""
+ "If enabled, the mod will log information about file sub-dimension operations. \n" + "If enabled, the mod will log information about file sub-dimension operations. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
public static ConfigEntry<EDhApiLoggerMode> logNetworkEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logNetworkEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setServersideShortName("logNetworkEvent")
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE)
.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.")
.setSide(EConfigEntryRelevantSide.BOTH)
.build(); .build();
@@ -1573,4 +1802,17 @@ public class Config
} }
} }
private static String getDefaultLevelKeyPrefix()
{
IMinecraftSharedWrapper mcWrapper = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
if (!mcWrapper.isDedicatedServer() || mcWrapper.isWorldNew())
{
return "";
}
else
{
return "server" + ThreadLocalRandom.current().nextInt(1, 1000);
}
}
} }
@@ -0,0 +1,6 @@
package com.seibel.distanthorizons.core.config;
public interface IConfigProvider
{
}
@@ -102,7 +102,7 @@ public class RenderCacheConfigEventHandler
// create a new timer task // create a new timer task
TimerTask timerTask = new TimerTask() TimerTask timerTask = new TimerTask()
{ {
public void run() @Override public void run()
{ {
DhApi.Delayed.renderProxy.clearRenderDataCache(); DhApi.Delayed.renderProxy.clearRenderDataCache();
} }
@@ -32,7 +32,7 @@ public class ResetConfigEventHandler
/** private since we only ever need one handler at a time */ /** private since we only ever need one handler at a time */
private ResetConfigEventHandler() private ResetConfigEventHandler()
{ {
this.configChangeListener = new ConfigChangeListener<>(Config.Client.ResetConfirmation.resetAllSettings, (resetSettings) -> { doStuff(resetSettings); }); this.configChangeListener = new ConfigChangeListener<>(Config.Client.ResetConfirmation.resetAllSettings, (resetSettings) -> { this.doStuff(resetSettings); });
} }
@@ -19,7 +19,6 @@
package com.seibel.distanthorizons.core.config.eventHandlers.presets; package com.seibel.distanthorizons.core.config.eventHandlers.presets;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.ConfigBase; import com.seibel.distanthorizons.core.config.ConfigBase;
import com.seibel.distanthorizons.core.config.ConfigEntryWithPresetOptions; import com.seibel.distanthorizons.core.config.ConfigEntryWithPresetOptions;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener; import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
@@ -30,6 +29,7 @@ import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry;
import com.seibel.distanthorizons.coreapi.util.StringUtil; import com.seibel.distanthorizons.coreapi.util.StringUtil;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.*;
@@ -38,6 +38,7 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
private static final Logger LOGGER = LogManager.getLogger(); private static final Logger LOGGER = LogManager.getLogger();
private static final long MS_DELAY_BEFORE_APPLYING_PRESET = 3_000; private static final long MS_DELAY_BEFORE_APPLYING_PRESET = 3_000;
@Nullable
private static IConfigGui configGui = SingletonInjector.INSTANCE.get(IConfigGui.class); private static IConfigGui configGui = SingletonInjector.INSTANCE.get(IConfigGui.class);
private static boolean guiListenersAdded = false; private static boolean guiListenersAdded = false;
@@ -57,7 +58,11 @@ public abstract class AbstractPresetConfigEventHandler<TPresetEnum extends Enum<
public AbstractPresetConfigEventHandler() public AbstractPresetConfigEventHandler()
{ {
configGui.addOnScreenChangeListener(() -> this.onConfigUiClosed()); // don't update the UI when running on a server
if (configGui != null)
{
configGui.addOnScreenChangeListener(this::onConfigUiClosed);
}
} }
@@ -33,6 +33,7 @@ import org.apache.logging.log4j.Logger;
import java.util.*; import java.util.*;
@SuppressWarnings("FieldCanBeLocal")
public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigEventHandler<EDhApiQualityPreset> public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigEventHandler<EDhApiQualityPreset>
{ {
public static final RenderQualityPresetConfigEventHandler INSTANCE = new RenderQualityPresetConfigEventHandler(); public static final RenderQualityPresetConfigEventHandler INSTANCE = new RenderQualityPresetConfigEventHandler();
@@ -32,6 +32,7 @@ import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@SuppressWarnings("FieldCanBeLocal")
public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHandler<EDhApiThreadPreset> public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHandler<EDhApiThreadPreset>
{ {
public static final ThreadPresetConfigEventHandler INSTANCE = new ThreadPresetConfigEventHandler(); public static final ThreadPresetConfigEventHandler INSTANCE = new ThreadPresetConfigEventHandler();
@@ -128,6 +129,28 @@ public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHan
}}); }});
public static int getNetworkCompressionDefaultThreadCount() { return getThreadCountByPercent(0.3); }
private final ConfigEntryWithPresetOptions<EDhApiThreadPreset, Integer> networkCompressionThreadCount = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.numberOfNetworkCompressionThreads,
new HashMap<EDhApiThreadPreset, Integer>()
{{
this.put(EDhApiThreadPreset.MINIMAL_IMPACT, 1);
this.put(EDhApiThreadPreset.LOW_IMPACT, getNetworkCompressionDefaultThreadCount());
this.put(EDhApiThreadPreset.BALANCED, getThreadCountByPercent(0.4));
this.put(EDhApiThreadPreset.AGGRESSIVE, getThreadCountByPercent(0.6));
this.put(EDhApiThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, getThreadCountByPercent(0.8));
}});
public static double getNetworkCompressionDefaultRunTimeRatio() { return 0.5; }
private final ConfigEntryWithPresetOptions<EDhApiThreadPreset, Double> networkCompressionRunTime = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.MultiThreading.runTimeRatioForNetworkCompressionThreads,
new HashMap<EDhApiThreadPreset, Double>()
{{
this.put(EDhApiThreadPreset.MINIMAL_IMPACT, 0.25);
this.put(EDhApiThreadPreset.LOW_IMPACT, getNetworkCompressionDefaultRunTimeRatio());
this.put(EDhApiThreadPreset.BALANCED, 0.75);
this.put(EDhApiThreadPreset.AGGRESSIVE, 1.0);
this.put(EDhApiThreadPreset.I_PAID_FOR_THE_WHOLE_CPU, 1.0);
}});
//==============// //==============//
// constructors // // constructors //
@@ -149,6 +172,9 @@ public class ThreadPresetConfigEventHandler extends AbstractPresetConfigEventHan
this.configList.add(this.lodBuilderThreadCount); this.configList.add(this.lodBuilderThreadCount);
this.configList.add(this.lodBuilderRunTime); this.configList.add(this.lodBuilderRunTime);
this.configList.add(this.networkCompressionThreadCount);
this.configList.add(this.networkCompressionRunTime);
for (ConfigEntryWithPresetOptions<EDhApiThreadPreset, ?> config : this.configList) for (ConfigEntryWithPresetOptions<EDhApiThreadPreset, ?> config : this.configList)
{ {
@@ -20,11 +20,13 @@
package com.seibel.distanthorizons.core.config.file; package com.seibel.distanthorizons.core.config.file;
import com.electronwill.nightconfig.core.file.CommentedFileConfig; import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryRelevantSide;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.config.ConfigBase; import com.seibel.distanthorizons.core.config.ConfigBase;
import com.seibel.distanthorizons.core.config.types.AbstractConfigType; import com.seibel.distanthorizons.core.config.types.AbstractConfigType;
import com.seibel.distanthorizons.core.config.types.ConfigEntry; import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -40,23 +42,35 @@ import java.nio.file.Path;
*/ */
public class ConfigFileHandling public class ConfigFileHandling
{ {
private static final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
public final ConfigBase configBase; public final ConfigBase configBase;
public final Path configPath; public final Path configPath;
private final Logger LOGGER; 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;
//=============//
// constructor //
//=============//
public ConfigFileHandling(ConfigBase configBase, Path configPath) public ConfigFileHandling(ConfigBase configBase, Path configPath)
{ {
this.LOGGER = LogManager.getLogger(this.getClass().getSimpleName() + ", " + configBase.modID); this.logger = LogManager.getLogger(this.getClass().getSimpleName() + ", " + configBase.modID);
this.configBase = configBase; this.configBase = configBase;
this.configPath = configPath; this.configPath = configPath;
this.nightConfig = CommentedFileConfig.builder(this.configPath.toFile()).build(); this.nightConfig = CommentedFileConfig.builder(this.configPath.toFile()).build();
} }
/** Saves the entire config to the file */ /** Saves the entire config to the file */
public void saveToFile() public void saveToFile()
{ {
@@ -102,7 +116,7 @@ public class ConfigFileHandling
*/ */
public void loadFromFile() public void loadFromFile()
{ {
int currentCfgVersion = configBase.configVersion; int currentCfgVersion = this.configBase.configVersion;
try try
{ {
// Dont load the real `this.nightConfig`, instead create a tempoary one // Dont load the real `this.nightConfig`, instead create a tempoary one
@@ -113,27 +127,29 @@ public class ConfigFileHandling
tmpNightConfig.close(); tmpNightConfig.close();
} catch (Exception ignored) { } } catch (Exception ignored) { }
if (currentCfgVersion == configBase.configVersion) if (currentCfgVersion == this.configBase.configVersion)
{}
else if (currentCfgVersion > configBase.configVersion)
{ {
LOGGER.warn("Found config version [" + String.valueOf(currentCfgVersion) + "] which is newer than current mods config version of [" + String.valueOf(configBase.configVersion) + "]. You may have downgraded the mod and items may have been moved, you have been warned"); // handle normally
}
else if (currentCfgVersion > this.configBase.configVersion)
{
this.logger.warn("Found config version [" + currentCfgVersion + "] which is newer than current mods config version of [" + this.configBase.configVersion + "]. You may have downgraded the mod and items may have been moved, you have been warned");
} }
else // if (currentCfgVersion < configBase.configVersion) else // if (currentCfgVersion < configBase.configVersion)
{ {
LOGGER.warn(configBase.modName +" config is of an older version, currently there is no config updater... so resetting config"); this.logger.warn(this.configBase.modName +" config is of an older version, currently there is no config updater... so resetting config");
try try
{ {
Files.delete(configPath); Files.delete(this.configPath);
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.error(e); this.logger.error(e);
} }
} }
loadFromFile(nightConfig); this.loadFromFile(this.nightConfig);
nightConfig.set("_version", configBase.configVersion); this.nightConfig.set("_version", this.configBase.configVersion);
} }
/** /**
* Loads the entire config from the file * Loads the entire config from the file
@@ -184,24 +200,34 @@ public class ConfigFileHandling
// Save an entry when only given the entry // Save an entry when only given the entry
public void saveEntry(ConfigEntry<?> entry) public void saveEntry(ConfigEntry<?> entry)
{ {
saveEntry(entry, nightConfig); this.saveEntry(entry, this.nightConfig);
nightConfig.save(); this.nightConfig.save();
} }
/** Save an entry */ /** Save an entry */
public void saveEntry(ConfigEntry<?> entry, CommentedFileConfig workConfig) public void saveEntry(ConfigEntry<?> entry, CommentedFileConfig workConfig)
{ {
if (!entry.getAppearance().showInFile) return; if (!entry.getAppearance().showInFile)
if (entry.getTrueValue() == null) {
throw new IllegalArgumentException("Entry [" + entry.getNameWCategory() + "] is null, this may be a problem with [" + configBase.modName + "]. Please contact the authors"); return;
}
else if ((entry.getRelevantSide() == EConfigEntryRelevantSide.CLIENT && MC_SHARED.isDedicatedServer())
|| (entry.getRelevantSide() == EConfigEntryRelevantSide.SERVER && !MC_SHARED.isDedicatedServer()))
{
// don't save server/client specific configs on the opposite
// (this keeps the config file clean of unnecessary items)
return;
}
else if (entry.getTrueValue() == null)
{
// TODO when can this happen?
throw new IllegalArgumentException("Entry [" + entry.getNameWCategory() + "] is null, this may be a problem with [" + this.configBase.modName + "]. Please contact the authors.");
}
workConfig.set(entry.getNameWCategory(), ConfigTypeConverters.attemptToConvertToString(entry.getType(), entry.getTrueValue())); workConfig.set(entry.getNameWCategory(), ConfigTypeConverters.attemptToConvertToString(entry.getType(), entry.getTrueValue()));
} }
/** Loads an entry when only given the entry */ /** Loads an entry when only given the entry */
public void loadEntry(ConfigEntry<?> entry) public void loadEntry(ConfigEntry<?> entry) { this.loadEntry(entry, this.nightConfig); }
{
loadEntry(entry, nightConfig);
}
/** Loads an entry */ /** Loads an entry */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <T> void loadEntry(ConfigEntry<T> entry, CommentedFileConfig nightConfig) public <T> void loadEntry(ConfigEntry<T> entry, CommentedFileConfig nightConfig)
@@ -211,7 +237,7 @@ public class ConfigFileHandling
if (!nightConfig.contains(entry.getNameWCategory())) if (!nightConfig.contains(entry.getNameWCategory()))
{ {
saveEntry(entry, nightConfig); this.saveEntry(entry, nightConfig);
return; return;
} }
@@ -230,7 +256,7 @@ public class ConfigFileHandling
Object convertedValue = ConfigTypeConverters.attemptToConvertFromString(expectedValueClass, value); Object convertedValue = ConfigTypeConverters.attemptToConvertFromString(expectedValueClass, value);
if (!convertedValue.getClass().equals(expectedValueClass)) if (!convertedValue.getClass().equals(expectedValueClass))
{ {
LOGGER.error("Unable to convert config value ["+value+"] from ["+(value != null ? value.getClass() : "NULL")+"] to ["+expectedValueClass+"] for config ["+entry.name+"], " + this.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();
@@ -239,31 +265,39 @@ public class ConfigFileHandling
if (entry.getTrueValue() == null) if (entry.getTrueValue() == null)
{ {
LOGGER.warn("Entry [" + entry.getNameWCategory() + "] returned as null from the config. Using default value."); this.logger.warn("Entry [" + entry.getNameWCategory() + "] returned as null from the config. Using default value.");
entry.pureSet(entry.getDefaultValue()); entry.pureSet(entry.getDefaultValue());
} }
} }
catch (Exception e) catch (Exception e)
{ {
// e.printStackTrace(); // e.printStackTrace();
LOGGER.warn("Entry [" + entry.getNameWCategory() + "] had an invalid value when loading the config. Using default value."); this.logger.warn("Entry [" + entry.getNameWCategory() + "] had an invalid value when loading the config. Using default value.");
entry.pureSet(entry.getDefaultValue()); entry.pureSet(entry.getDefaultValue());
} }
} }
// Creates the comment for an entry when only given the entry // Creates the comment for an entry when only given the entry
public void createComment(ConfigEntry<?> entry) public void createComment(ConfigEntry<?> entry) { this.createComment(entry, this.nightConfig); }
{
createComment(entry, nightConfig);
}
// Creates a comment for an entry // Creates a comment for an entry
public void createComment(ConfigEntry<?> entry, CommentedFileConfig nightConfig) public void createComment(ConfigEntry<?> entry, CommentedFileConfig nightConfig)
{ {
if ( if (!entry.getAppearance().showInFile
!entry.getAppearance().showInFile || || entry.getComment() == null)
entry.getComment() == null {
)
return; return;
}
if ((entry.getRelevantSide() == EConfigEntryRelevantSide.CLIENT && MC_SHARED.isDedicatedServer())
|| (entry.getRelevantSide() == EConfigEntryRelevantSide.SERVER && !MC_SHARED.isDedicatedServer()))
{
// don't save server/client specific configs on the opposite
// (this keeps the config file clean of unnecessary items)
return;
}
String comment = entry.getComment().replaceAll("\n", "\n ").trim(); String comment = entry.getComment().replaceAll("\n", "\n ").trim();
// the new line makes it easier to read and separate configs // the new line makes it easier to read and separate configs
@@ -302,7 +336,7 @@ public class ConfigFileHandling
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.warn("Loading file failed because of this expectation:\n" + e); this.logger.warn("Loading file failed because of this expectation:\n" + e);
reCreateFile(this.configPath); reCreateFile(this.configPath);
@@ -312,7 +346,7 @@ public class ConfigFileHandling
catch (Exception ex) catch (Exception ex)
{ {
System.out.println("Creating file failed"); System.out.println("Creating file failed");
LOGGER.error(ex); this.logger.error(ex);
SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class).crashMinecraft("Loading file and resetting config file failed at path [" + configPath + "]. Please check the file is ok and you have the permissions", ex); SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class).crashMinecraft("Loading file and resetting config file failed at path [" + configPath + "]. Please check the file is ok and you have the permissions", ex);
} }
} }
@@ -20,16 +20,23 @@
package com.seibel.distanthorizons.core.config.types; package com.seibel.distanthorizons.core.config.types;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.Table;
import com.seibel.distanthorizons.core.config.NumberUtil; import com.seibel.distanthorizons.core.config.NumberUtil;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener; import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance; import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryPerformance; import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryPerformance;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryRelevantSide;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry; import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/** /**
* Use for making the config variables * Use for making the config variables
@@ -44,6 +51,10 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
private T min; private T min;
private T max; private T max;
private final ArrayList<IConfigListener> listenerList; private final ArrayList<IConfigListener> listenerList;
private final String serversideShortName;
private final EConfigEntryPerformance performance;
private final EConfigEntryRelevantSide relevantSide;
// API control // // API control //
/** /**
@@ -53,19 +64,25 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
public final boolean allowApiOverride; public final boolean allowApiOverride;
private T apiValue; private T apiValue;
private final EConfigEntryPerformance performance;
/** Creates the entry */ /** Creates the entry */
private ConfigEntry(EConfigEntryAppearance appearance, T value, String comment, T min, T max, boolean allowApiOverride, EConfigEntryPerformance performance, ArrayList<IConfigListener> listenerList) private ConfigEntry(
EConfigEntryAppearance appearance,
T value, String comment, T min, T max,
String serversideShortName, boolean allowApiOverride,
EConfigEntryPerformance performance, EConfigEntryRelevantSide relevantSide,
ArrayList<IConfigListener> listenerList)
{ {
super(appearance, value); super(appearance, value);
this.comment = comment; this.comment = comment;
this.min = min; this.min = min;
this.max = max; this.max = max;
this.serversideShortName = serversideShortName;
this.allowApiOverride = allowApiOverride; this.allowApiOverride = allowApiOverride;
this.performance = performance; this.performance = performance;
this.relevantSide = relevantSide;
this.listenerList = listenerList; this.listenerList = listenerList;
} }
@@ -124,9 +141,9 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
@Override @Override
public T get() public T get()
{ {
if (allowApiOverride && apiValue != null) if (this.allowApiOverride && this.apiValue != null)
{ {
return apiValue; return this.apiValue;
} }
return super.get(); return super.get();
@@ -175,9 +192,18 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
public void clampWithinRange(T min, T max) public void clampWithinRange(T min, T max)
{ {
byte validness = this.isValid(min, max); byte validness = this.isValid(min, max);
if (validness == -1) this.value = (T) NumberUtil.getMinimum(this.value.getClass()); if (validness == -1)
if (validness == 1) this.value = (T) NumberUtil.getMaximum(this.value.getClass()); {
this.value = (T) NumberUtil.getMinimum(this.value.getClass());
} }
if (validness == 1)
{
this.value = (T) NumberUtil.getMaximum(this.value.getClass());
}
}
// TODO is this for command line use?
public String getServersideShortName() { return this.serversideShortName; }
@Override @Override
public String getComment() { return this.comment; } public String getComment() { return this.comment; }
@@ -186,9 +212,11 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
/** Gets the performance impact of an option */ /** Gets the performance impact of an option */
public EConfigEntryPerformance getPerformance() { return this.performance; } public EConfigEntryPerformance getPerformance() { return this.performance; }
/** Gets whether this config should apply to the client, server, or both */
public EConfigEntryRelevantSide getRelevantSide() { return this.relevantSide; }
/** 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) @Override public void addValueChangeListener(Consumer<T> onValueChangeFunc)
{ {
ConfigChangeListener<T> changeListener = new ConfigChangeListener<>(this, onValueChangeFunc); ConfigChangeListener<T> changeListener = new ConfigChangeListener<>(this, onValueChangeFunc);
this.addListener(changeListener); this.addListener(changeListener);
@@ -219,7 +247,7 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
* <p> -1 == number too low * <p> -1 == number too low
*/ */
@Override @Override
public byte isValid() { return isValid(this.value, this.min, this.max); } public byte isValid() { return this.isValid(this.value, this.min, this.max); }
/** /**
* Checks if a new value is valid * Checks if a new value is valid
* *
@@ -291,100 +319,162 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
} }
/** This should normally not be called since set() automatically calls this */ /** This should normally not be called since set() automatically calls this */
public void save() { configBase.configFileINSTANCE.saveEntry(this); } public void save() { this.configBase.configFileINSTANCE.saveEntry(this); }
/** This should normally not be called except for special circumstances */ /** This should normally not be called except for special circumstances */
public void load() { configBase.configFileINSTANCE.loadEntry(this); } public void load() { this.configBase.configFileINSTANCE.loadEntry(this); }
@Override @Override
public boolean equals(IConfigEntry<?> obj) { return obj.getClass() == ConfigEntry.class && equals((ConfigEntry<?>) obj); } public boolean equals(IConfigEntry<?> obj) { return obj.getClass() == ConfigEntry.class && this.equals((ConfigEntry<?>) obj); }
/** Is the value of this equal to another */ /** Is the value of this equal to another */
public boolean equals(ConfigEntry<?> obj) public boolean equals(ConfigEntry<?> obj)
{ {
// Can all of this just be "return this.value.equals(obj.value)"? // Can all of this just be "return this.value.equals(obj.value)"?
if (Number.class.isAssignableFrom(this.value.getClass())) if (Number.class.isAssignableFrom(this.value.getClass()))
{
return this.value == obj.value; return this.value == obj.value;
}
else else
{
return this.value.equals(obj.value); return this.value.equals(obj.value);
} }
}
public static class Builder<TValue> extends AbstractConfigType.Builder<TValue, Builder<TValue>>
public static class Builder<T> extends AbstractConfigType.Builder<T, Builder<T>>
{ {
private String tmpComment = null; private String tmpComment = null;
private T tmpMin = null; private TValue tmpMin = null;
private T tmpMax = null; private TValue tmpMax = null;
protected String tmpServersideShortName = null;
private boolean tmpUseApiOverwrite = true; private boolean tmpUseApiOverwrite = true;
private EConfigEntryPerformance tmpPerformance = EConfigEntryPerformance.DONT_SHOW; private EConfigEntryPerformance tmpPerformance = EConfigEntryPerformance.DONT_SHOW;
private EConfigEntryRelevantSide tmpRelevantSide = EConfigEntryRelevantSide.CLIENT;
protected ArrayList<IConfigListener> tmpIConfigListener = new ArrayList<>(); protected ArrayList<IConfigListener> tmpIConfigListener = new ArrayList<>();
public Builder<T> comment(String newComment) private Supplier<TValue> computedValueGetter;
private Consumer<TValue> computedValueSetter;
private boolean preventAutoApplyUntilExit;
public Builder<TValue> comment(String newComment)
{ {
this.tmpComment = newComment; this.tmpComment = newComment;
return this; return this;
} }
public Builder<TValue> computed(Supplier<TValue> computedValueGetter, Consumer<TValue> computedValueSetter)
{
this.computedValueGetter = computedValueGetter;
this.computedValueSetter = computedValueSetter;
this.setAppearance(EConfigEntryAppearance.ONLY_IN_GUI);
return this;
}
public Builder<TValue> linkedTo(DeferredAccessor<TValue> otherConfigAccessor)
{
return this.computed(
otherConfigAccessor::get,
otherConfigAccessor::set
);
}
public Builder<TValue> preset(PresetBuilder<TValue> presetBuilder)
{
return this.computed(
() -> {
List<TValue> values = presetBuilder.presetValues.entrySet().stream()
.map(entry -> entry.getValue().inverse().get(entry.getKey().get()))
.distinct().limit(2)
.collect(Collectors.toList());
if (values.size() != 1)
{
return presetBuilder.customStateValue;
}
return values.get(0);
},
value -> {
for (Map.Entry<DeferredAccessor<Object>, BiMap<TValue, Object>> entry : presetBuilder.presetValues.entrySet())
{
entry.getKey().set(entry.getValue().get(value));
}
}
);
}
/** Allows most values to be set by 1 setter */ /** Allows most values to be set by 1 setter */
public Builder<T> setMinDefaultMax(T newMin, T newDefault, T newMax) public Builder<TValue> setMinDefaultMax(TValue newMin, TValue newDefault, TValue newMax)
{ {
this.set(newDefault); this.set(newDefault);
this.setMinMax(newMin, newMax); this.setMinMax(newMin, newMax);
return this; return this;
} }
public Builder<T> setMinMax(T newMin, T newMax) public Builder<TValue> setMinMax(TValue newMin, TValue newMax)
{ {
this.tmpMin = newMin; this.tmpMin = newMin;
this.tmpMax = newMax; this.tmpMax = newMax;
return this; return this;
} }
public Builder<T> setMin(T newMin) public Builder<TValue> setMin(TValue newMin)
{ {
this.tmpMin = newMin; this.tmpMin = newMin;
return this; return this;
} }
public Builder<T> setMax(T newMax) public Builder<TValue> setMax(TValue newMax)
{ {
this.tmpMax = newMax; this.tmpMax = newMax;
return this; return this;
} }
public Builder<T> setUseApiOverwrite(boolean newUseApiOverwrite) public Builder<TValue> setServersideShortName(String name)
{
this.tmpServersideShortName = name;
return this;
}
public Builder<TValue> setUseApiOverwrite(boolean newUseApiOverwrite)
{ {
this.tmpUseApiOverwrite = newUseApiOverwrite; this.tmpUseApiOverwrite = newUseApiOverwrite;
return this; return this;
} }
public Builder<T> setPerformance(EConfigEntryPerformance newPerformance) public Builder<TValue> setPerformance(EConfigEntryPerformance newPerformance)
{ {
this.tmpPerformance = newPerformance; this.tmpPerformance = newPerformance;
return this; return this;
} }
public Builder<TValue> setSide(EConfigEntryRelevantSide relevantSide)
{
this.tmpRelevantSide = relevantSide;
return this;
}
public Builder<T> replaceListeners(ArrayList<IConfigListener> newConfigListener)
public Builder<TValue> replaceListeners(ArrayList<IConfigListener> newConfigListener)
{ {
this.tmpIConfigListener = newConfigListener; this.tmpIConfigListener = newConfigListener;
return this; return this;
} }
public Builder<T> addListeners(IConfigListener... newConfigListener) public Builder<TValue> addListeners(IConfigListener... newConfigListener)
{ {
this.tmpIConfigListener.addAll(Arrays.asList(newConfigListener)); this.tmpIConfigListener.addAll(Arrays.asList(newConfigListener));
return this; return this;
} }
public Builder<T> addListener(IConfigListener newConfigListener) public Builder<TValue> addListener(IConfigListener newConfigListener)
{ {
this.tmpIConfigListener.add(newConfigListener); this.tmpIConfigListener.add(newConfigListener);
return this; return this;
} }
public Builder<T> clearListeners() public Builder<TValue> clearListeners()
{ {
this.tmpIConfigListener.clear(); this.tmpIConfigListener.clear();
return this; return this;
@@ -392,9 +482,55 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
public ConfigEntry<T> build() public ConfigEntry<TValue> build()
{ {
return new ConfigEntry<>(this.tmpAppearance, this.tmpValue, this.tmpComment, this.tmpMin, this.tmpMax, this.tmpUseApiOverwrite, this.tmpPerformance, this.tmpIConfigListener); return new ConfigEntry<>(
this.tmpAppearance,
this.tmpValue, this.tmpComment, this.tmpMin, this.tmpMax,
this.tmpServersideShortName, this.tmpUseApiOverwrite,
this.tmpPerformance, this.tmpRelevantSide, this.tmpIConfigListener);
}
}
/**
* Wraps deferred accesses to a ConfigEntry. <br>
* It replaces the direct uses of ConfigEntry in places where Java runtime should not attempt
* to dereference path to said entry, until initialization is done.
*/
@FunctionalInterface
public interface DeferredAccessor<T>
{
ConfigEntry<T> getEntry();
default T get() { return this.getEntry().get(); }
default void set(T value) { this.getEntry().set(value); }
}
public static class PresetBuilder<TValue>
{
public final TValue customStateValue;
public final Map<DeferredAccessor<Object>, BiMap<TValue, Object>> presetValues = new HashMap<>();
public PresetBuilder(TValue customStateValue)
{
this.customStateValue = customStateValue;
}
@SuppressWarnings("unchecked")
public <TControlledEntryValue> PresetBuilder<TValue> addConfigEntry(
DeferredAccessor<TControlledEntryValue> controlledEntrySupplier,
Consumer<Map<TValue, TControlledEntryValue>> presetValueInitializer
)
{
presetValueInitializer.accept(
(Map<TValue, TControlledEntryValue>) this.presetValues.computeIfAbsent(
(DeferredAccessor<Object>) controlledEntrySupplier,
ignored -> HashBiMap.create()
)
);
return this;
} }
} }
@@ -1,63 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.config.types;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance;
/**
* Creates a UI element that copies everything from another element.
* This only effects the UI
*
* @author coolGi
*/
@Deprecated // FIXME doesn't work with localization
public class ConfigLinkedEntry extends AbstractConfigType<AbstractConfigType<?, ?>, ConfigLinkedEntry>
{
public ConfigLinkedEntry(AbstractConfigType<?, ?> value)
{
super(EConfigEntryAppearance.ONLY_IN_GUI, value);
}
/** Appearance shouldn't be changed */
@Override
public void setAppearance(EConfigEntryAppearance newAppearance) { }
/** Value shouldn't be changed after creation */
@Override
public void set(AbstractConfigType<?, ?> newValue) { }
public static class Builder extends AbstractConfigType.Builder<AbstractConfigType<?, ?>, Builder>
{
/** Appearance shouldn't be changed */
@Override
public Builder setAppearance(EConfigEntryAppearance newAppearance)
{
return this;
}
public ConfigLinkedEntry build()
{
return new ConfigLinkedEntry(this.tmpValue);
}
}
}
@@ -17,14 +17,20 @@
* 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.network.protocol; package com.seibel.distanthorizons.core.config.types.enums;
/** /**
* CLIENT, <br> * BOTH, <br>
* SERVER, <br> * SERVER, <br>
* CLIENT, <br>
*
* Defines whether this config entry is for the client, server, or both.
* TODO this needs a better name.
*/ */
public enum EMessageHandlerSide public enum EConfigEntryRelevantSide
{ {
BOTH,
SERVER,
CLIENT, CLIENT,
SERVER
} }
@@ -90,11 +90,13 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
/** /**
* stores how far each column has been generated should start with {@link EDhApiWorldGenerationStep#EMPTY} * stores how far each column has been generated should start with {@link EDhApiWorldGenerationStep#EMPTY}
*
* @see EDhApiWorldGenerationStep * @see EDhApiWorldGenerationStep
*/ */
public byte[] columnGenerationSteps; public byte[] columnGenerationSteps;
/** /**
* stores what world compression was used for each column. * stores what world compression was used for each column.
*
* @see EDhApiWorldCompressionMode * @see EDhApiWorldCompressionMode
*/ */
public byte[] columnWorldCompressionMode; public byte[] columnWorldCompressionMode;
@@ -144,7 +146,7 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
this.columnWorldCompressionMode = columnWorldCompressionMode; this.columnWorldCompressionMode = columnWorldCompressionMode;
} }
public static FullDataSourceV2 createFromChunk(IChunkWrapper chunkWrapper) { return LodDataBuilder.createGeneratedDataSource(chunkWrapper); } public static FullDataSourceV2 createFromChunk(IChunkWrapper chunkWrapper) { return LodDataBuilder.createFromChunk(chunkWrapper); }
public static FullDataSourceV2 createFromLegacyDataSourceV1(FullDataSourceV1 legacyData) public static FullDataSourceV2 createFromLegacyDataSourceV1(FullDataSourceV1 legacyData)
{ {
@@ -719,27 +721,43 @@ public class FullDataSourceV2 implements IDataSource<IDhLevel>
} }
if (value == value0) if (value == value0)
{
count0++; count0++;
}
else if (value == value1) else if (value == value1)
{
count1++; count1++;
}
else if (value == value2) else if (value == value2)
{
count2++; count2++;
}
else else
{
count3++; count3++;
} }
}
// return the most common occurance // return the most common occurance
int maxCount = Math.max(count0, Math.max(count1, Math.max(count2, count3))); int maxCount = Math.max(count0, Math.max(count1, Math.max(count2, count3)));
if (maxCount == count0) if (maxCount == count0)
// if the max count is 1 then we'll just go with the first column // if the max count is 1 then we'll just go with the first column
{
return value0; return value0;
}
else if (maxCount == count1) else if (maxCount == count1)
{
return value1; return value1;
}
else if (maxCount == count2) else if (maxCount == count2)
{
return value2; return value2;
}
else else
{
return value3; return value3;
} }
}
private static int determineAverageValueInColumnSlice(int[] sliceArray) private static int determineAverageValueInColumnSlice(int[] sliceArray)
{ {
if (RUN_UPDATE_DEV_VALIDATION) if (RUN_UPDATE_DEV_VALIDATION)
@@ -25,7 +25,7 @@ import com.seibel.distanthorizons.core.dataObjects.transformers.FullDataToRender
import com.seibel.distanthorizons.core.file.DataSourcePool; import com.seibel.distanthorizons.core.file.DataSourcePool;
import com.seibel.distanthorizons.core.file.IDataSource; import com.seibel.distanthorizons.core.file.IDataSource;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView; import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView;
@@ -67,9 +67,13 @@ public final class BufferQuad
EDhDirection direction) EDhDirection direction)
{ {
if (widthEastWest == 0 || widthNorthSouthOrUpDown == 0) if (widthEastWest == 0 || widthNorthSouthOrUpDown == 0)
{
throw new IllegalArgumentException("Size 0 quad!"); throw new IllegalArgumentException("Size 0 quad!");
}
if (widthEastWest < 0 || widthNorthSouthOrUpDown < 0) if (widthEastWest < 0 || widthNorthSouthOrUpDown < 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;
@@ -95,7 +99,9 @@ public final class BufferQuad
public int compare(BufferQuad quad, BufferMergeDirectionEnum compareDirection) public int compare(BufferQuad quad, BufferMergeDirectionEnum compareDirection)
{ {
if (this.direction != quad.direction) if (this.direction != quad.direction)
{
throw new IllegalArgumentException("The other quad is not in the same direction: " + quad.direction + " vs " + this.direction); throw new IllegalArgumentException("The other quad is not in the same direction: " + quad.direction + " vs " + this.direction);
}
if (compareDirection == BufferMergeDirectionEnum.EastWest) if (compareDirection == BufferMergeDirectionEnum.EastWest)
{ {
@@ -150,11 +156,15 @@ public final class BufferQuad
public boolean tryMerge(BufferQuad quad, BufferMergeDirectionEnum mergeDirection) public boolean tryMerge(BufferQuad quad, BufferMergeDirectionEnum mergeDirection)
{ {
if (quad.hasError || this.hasError) if (quad.hasError || this.hasError)
{
return false; return false;
}
// only merge quads that are in the same direction // only merge quads that are in the same direction
if (this.direction != quad.direction) if (this.direction != quad.direction)
{
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) ||
@@ -21,10 +21,9 @@ package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding;
import com.seibel.distanthorizons.api.DhApi; import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
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.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhBlockPos; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.render.glObject.GLProxy; import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.render.glObject.buffer.GLVertexBuffer; import com.seibel.distanthorizons.core.render.glObject.buffer.GLVertexBuffer;
import com.seibel.distanthorizons.core.render.renderer.LodRenderer; import com.seibel.distanthorizons.core.render.renderer.LodRenderer;
@@ -27,7 +27,7 @@ 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.ConfigBasedLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhBlockPos; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.glObject.GLProxy; import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.util.ColorUtil; import com.seibel.distanthorizons.core.util.ColorUtil;
@@ -111,7 +111,7 @@ 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<ColumnRenderBuffer> uploadBuffersAsync(
IDhClientLevel clientLevel, IDhClientLevel clientLevel,
ColumnRenderSource renderSource, long pos,
LodQuadBuilder quadBuilder LodQuadBuilder quadBuilder
) )
{ {
@@ -132,7 +132,7 @@ public class ColumnRenderBufferBuilder
{ {
try try
{ {
ColumnRenderBuffer buffer = new ColumnRenderBuffer(new DhBlockPos(DhSectionPos.getMinCornerBlockX(renderSource.pos), clientLevel.getMinY(), DhSectionPos.getMinCornerBlockZ(renderSource.pos))); ColumnRenderBuffer buffer = new ColumnRenderBuffer(new DhBlockPos(DhSectionPos.getMinCornerBlockX(pos), clientLevel.getMinY(), DhSectionPos.getMinCornerBlockZ(pos)));
try try
{ {
buffer.uploadBuffer(quadBuilder, GLProxy.getInstance().getGpuUploadMethod()); buffer.uploadBuffer(quadBuilder, GLProxy.getInstance().getGpuUploadMethod());
@@ -158,7 +158,7 @@ public class ColumnRenderBufferBuilder
} }
catch (Throwable e3) catch (Throwable e3)
{ {
LOGGER.error("LodNodeBufferBuilder was unable to upload buffer for pos ["+DhSectionPos.toString(renderSource.pos)+"], error: [" + e3.getMessage() + "].", e3); LOGGER.error("LodNodeBufferBuilder was unable to upload buffer for pos ["+DhSectionPos.toString(pos)+"], error: [" + e3.getMessage() + "].", e3);
throw e3; throw e3;
} }
}, bufferUploaderExecutor); }, bufferUploaderExecutor);
@@ -191,14 +191,14 @@ public class LodQuadBuilder
public void addQuadDown(short x, short y, short z, short width, short wz, int color, byte irisBlockMaterialId, byte skylight, byte blocklight) public void addQuadDown(short x, short y, short z, short width, short wz, int color, byte irisBlockMaterialId, byte skylight, byte blocklight)
{ {
BufferQuad quad = new BufferQuad(x, y, z, width, wz, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.DOWN); BufferQuad quad = new BufferQuad(x, y, z, width, wz, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.DOWN);
ArrayList<BufferQuad> qs = (doTransparency && ColorUtil.getAlpha(color) < 255) ArrayList<BufferQuad> qs = (this.doTransparency && ColorUtil.getAlpha(color) < 255)
? transparentQuads[EDhDirection.DOWN.ordinal()] : opaqueQuads[EDhDirection.DOWN.ordinal()]; ? this.transparentQuads[EDhDirection.DOWN.ordinal()] : this.opaqueQuads[EDhDirection.DOWN.ordinal()];
if (!qs.isEmpty() if (!qs.isEmpty()
&& (qs.get(qs.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.EastWest) && (qs.get(qs.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.EastWest)
|| qs.get(qs.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.NorthSouthOrUpDown)) || qs.get(qs.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.NorthSouthOrUpDown))
) )
{ {
premergeCount++; this.premergeCount++;
return; return;
} }
qs.add(quad); qs.add(quad);
@@ -251,7 +251,9 @@ public class LodQuadBuilder
private static long mergeQuadsInternal(ArrayList<BufferQuad>[] list, int directionIndex, BufferMergeDirectionEnum mergeDirection) private static long mergeQuadsInternal(ArrayList<BufferQuad>[] list, int directionIndex, BufferMergeDirectionEnum mergeDirection)
{ {
if (list[directionIndex].size() <= 1) if (list[directionIndex].size() <= 1)
{
return 0; return 0;
}
list[directionIndex].sort((objOne, objTwo) -> objOne.compare(objTwo, mergeDirection)); list[directionIndex].sort((objOne, objTwo) -> objOne.compare(objTwo, mergeDirection));
@@ -423,9 +425,18 @@ public class LodQuadBuilder
// 0b01 = positive offset // 0b01 = positive offset
// 0b11 = negative offset // 0b11 = negative offset
// format is: 0b00zzyyxx // format is: 0b00zzyyxx
if (mx != 0) mirco |= mx > 0 ? 0b01 : 0b11; if (mx != 0)
if (my != 0) mirco |= my > 0 ? 0b0100 : 0b1100; {
if (mz != 0) mirco |= mz > 0 ? 0b010000 : 0b110000; mirco |= mx > 0 ? 0b01 : 0b11;
}
if (my != 0)
{
mirco |= my > 0 ? 0b0100 : 0b1100;
}
if (mz != 0)
{
mirco |= mz > 0 ? 0b010000 : 0b110000;
}
meta |= mirco << 8; meta |= mirco << 8;
bb.putShort(meta); bb.putShort(meta);
@@ -28,7 +28,7 @@ import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArra
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IDhClientLevel; import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.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.util.ColorUtil; import com.seibel.distanthorizons.core.util.ColorUtil;
import com.seibel.distanthorizons.core.util.FullDataPointUtil; import com.seibel.distanthorizons.core.util.FullDataPointUtil;
@@ -229,7 +229,7 @@ public class FullDataToRenderDataTransformer
if (!brokenPos.contains(fullDataMapping.getPos())) if (!brokenPos.contains(fullDataMapping.getPos()))
{ {
brokenPos.add(fullDataMapping.getPos()); brokenPos.add(fullDataMapping.getPos());
String dimName = level.getLevelWrapper().getDimensionType().getDimensionName(); String dimName = level.getLevelWrapper().getDimensionName();
LOGGER.warn("Unable to get data point with id ["+id+"] " + LOGGER.warn("Unable to get data point with id ["+id+"] " +
"(Max possible ID: ["+fullDataMapping.getMaxValidId()+"]) " + "(Max possible ID: ["+fullDataMapping.getMaxValidId()+"]) " +
"for pos ["+fullDataMapping.getPos()+"] in dimension ["+dimName+"]. " + "for pos ["+fullDataMapping.getPos()+"] in dimension ["+dimName+"]. " +
@@ -31,7 +31,8 @@ import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSour
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.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhBlockPos; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPosMutable;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.util.FullDataPointUtil; import com.seibel.distanthorizons.core.util.FullDataPointUtil;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
@@ -39,6 +40,7 @@ import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IMutableBlockPosWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
@@ -57,17 +59,15 @@ public class LodDataBuilder
// converters // // converters //
//============// //============//
public static FullDataSourceV2 createGeneratedDataSource(IChunkWrapper chunkWrapper) public static FullDataSourceV2 createFromChunk(IChunkWrapper chunkWrapper)
{ {
if (!canGenerateLodFromChunk(chunkWrapper)) // only block lighting is needed here, sky lighting is populated at the data source stage
{ LodUtil.assertTrue(chunkWrapper.isDhBlockLightingCorrect());
return null;
}
int sectionPosX = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().x); int sectionPosX = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getX());
int sectionPosZ = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().z); int sectionPosZ = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getZ());
long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ); long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ);
FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos); FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos);
@@ -77,8 +77,8 @@ public class LodDataBuilder
// compute the chunk dataSource offset // compute the chunk dataSource offset
// this offset is used to determine where in the dataSource this chunk's data should go // this offset is used to determine where in the dataSource this chunk's data should go
int chunkOffsetX = chunkWrapper.getChunkPos().x; int chunkOffsetX = chunkWrapper.getChunkPos().getX();
if (chunkWrapper.getChunkPos().x < 0) if (chunkWrapper.getChunkPos().getX() < 0)
{ {
// expected offset positions: // expected offset positions:
// chunkPos -> offset // chunkPos -> offset
@@ -105,8 +105,8 @@ public class LodDataBuilder
} }
chunkOffsetX *= LodUtil.CHUNK_WIDTH; chunkOffsetX *= LodUtil.CHUNK_WIDTH;
int chunkOffsetZ = chunkWrapper.getChunkPos().z; int chunkOffsetZ = chunkWrapper.getChunkPos().getZ();
if (chunkWrapper.getChunkPos().z < 0) if (chunkWrapper.getChunkPos().getZ() < 0)
{ {
chunkOffsetZ = ((chunkOffsetZ) % FullDataSourceV2.NUMB_OF_CHUNKS_WIDE); chunkOffsetZ = ((chunkOffsetZ) % FullDataSourceV2.NUMB_OF_CHUNKS_WIDE);
if (chunkOffsetZ != 0) if (chunkOffsetZ != 0)
@@ -131,6 +131,9 @@ public class LodDataBuilder
try try
{ {
IMutableBlockPosWrapper mcBlockPos = chunkWrapper.getMutableBlockPosWrapper();
IBlockStateWrapper previousBlockState = null;
int minBuildHeight = chunkWrapper.getMinNonEmptyHeight(); int minBuildHeight = chunkWrapper.getMinNonEmptyHeight();
for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++) for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++)
{ {
@@ -148,8 +151,8 @@ public class LodDataBuilder
if (lastY < chunkWrapper.getMaxBuildHeight()) if (lastY < chunkWrapper.getMaxBuildHeight())
{ {
// FIXME: The lastY +1 offset is to reproduce the old behavior. Remove this when we get per-face lighting // FIXME: The lastY +1 offset is to reproduce the old behavior. Remove this when we get per-face lighting
blockLight = (byte) chunkWrapper.getBlockLight(relBlockX, lastY + 1, relBlockZ); blockLight = (byte) chunkWrapper.getDhBlockLight(relBlockX, lastY + 1, relBlockZ);
skyLight = (byte) chunkWrapper.getSkyLight(relBlockX, lastY + 1, relBlockZ); skyLight = (byte) chunkWrapper.getDhSkyLight(relBlockX, lastY + 1, relBlockZ);
} }
else else
{ {
@@ -162,7 +165,7 @@ public class LodDataBuilder
// determine the starting Y Pos // determine the starting Y Pos
int y = chunkWrapper.getLightBlockingHeightMapValue(relBlockX, relBlockZ); int y = chunkWrapper.getLightBlockingHeightMapValue(relBlockX, relBlockZ);
// go up until we reach open air or the world limit // go up until we reach open air or the world limit
IBlockStateWrapper topBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ); IBlockStateWrapper topBlockState = previousBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ, mcBlockPos, previousBlockState);
while (!topBlockState.isAir() && y < chunkWrapper.getMaxBuildHeight()) while (!topBlockState.isAir() && y < chunkWrapper.getMaxBuildHeight())
{ {
try try
@@ -170,7 +173,7 @@ public class LodDataBuilder
// This is necessary in some edge cases with snow layers and some other blocks that may not appear in the height map but do block light. // This is necessary in some edge cases with snow layers and some other blocks that may not appear in the height map but do block light.
// Interestingly this doesn't appear to be the case in the DhLightingEngine, if this same logic is added there the lighting breaks for the affected blocks. // Interestingly this doesn't appear to be the case in the DhLightingEngine, if this same logic is added there the lighting breaks for the affected blocks.
y++; y++;
topBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ); topBlockState = previousBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ, mcBlockPos, previousBlockState);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -189,9 +192,9 @@ public class LodDataBuilder
for (; y >= minBuildHeight; y--) for (; y >= minBuildHeight; y--)
{ {
IBiomeWrapper newBiome = chunkWrapper.getBiome(relBlockX, y, relBlockZ); IBiomeWrapper newBiome = chunkWrapper.getBiome(relBlockX, y, relBlockZ);
IBlockStateWrapper newBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ); IBlockStateWrapper newBlockState = previousBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ, mcBlockPos, previousBlockState);
byte newBlockLight = (byte) chunkWrapper.getBlockLight(relBlockX, y + 1, relBlockZ); byte newBlockLight = (byte) chunkWrapper.getDhBlockLight(relBlockX, y + 1, relBlockZ);
byte newSkyLight = (byte) chunkWrapper.getSkyLight(relBlockX, y + 1, relBlockZ); byte newSkyLight = (byte) chunkWrapper.getDhSkyLight(relBlockX, y + 1, relBlockZ);
// save the biome/block change // save the biome/block change
if (!newBiome.equals(biome) || !newBlockState.equals(blockState)) if (!newBiome.equals(biome) || !newBlockState.equals(blockState))
@@ -237,7 +240,7 @@ public class LodDataBuilder
private static boolean blockVisible(IChunkWrapper chunkWrapper, int relBlockX, int blockY, int relBlockZ) private static boolean blockVisible(IChunkWrapper chunkWrapper, int relBlockX, int blockY, int relBlockZ)
{ {
DhBlockPos originalBlockPos = new DhBlockPos(relBlockX,blockY,relBlockZ); DhBlockPos originalBlockPos = new DhBlockPos(relBlockX,blockY,relBlockZ);
DhBlockPos testBlockPos = new DhBlockPos(relBlockX,blockY,relBlockZ); final DhBlockPosMutable testBlockPos = new DhBlockPosMutable(relBlockX,blockY,relBlockZ);
// up/down // up/down
if (blockInDirectionVisible(chunkWrapper, EDhDirection.UP, originalBlockPos, testBlockPos)) if (blockInDirectionVisible(chunkWrapper, EDhDirection.UP, originalBlockPos, testBlockPos))
@@ -272,20 +275,20 @@ public class LodDataBuilder
return false; return false;
} }
private static boolean blockInDirectionVisible(IChunkWrapper chunkWrapper, EDhDirection direction, DhBlockPos originalBlockPos, DhBlockPos testBlockPos) private static boolean blockInDirectionVisible(IChunkWrapper chunkWrapper, EDhDirection direction, DhBlockPos originalBlockPos, DhBlockPosMutable testBlockPos)
{ {
originalBlockPos.mutateOffset(direction, testBlockPos); originalBlockPos.mutateOffset(direction, testBlockPos);
// if the block is next to the border of a chunk, assume it's visible // if the block is next to the border of a chunk, assume it's visible
if (testBlockPos.x < 0 || testBlockPos.x >= LodUtil.CHUNK_WIDTH) if (testBlockPos.getX() < 0 || testBlockPos.getX() >= LodUtil.CHUNK_WIDTH)
{ {
return true; return true;
} }
if (testBlockPos.z < 0 || testBlockPos.z >= LodUtil.CHUNK_WIDTH) if (testBlockPos.getZ() < 0 || testBlockPos.getZ() >= LodUtil.CHUNK_WIDTH)
{ {
return true; return true;
} }
if (testBlockPos.y < chunkWrapper.getMinBuildHeight() || testBlockPos.y > chunkWrapper.getMaxBuildHeight()) if (testBlockPos.getY() < chunkWrapper.getMinBuildHeight() || testBlockPos.getY() > chunkWrapper.getMaxBuildHeight())
{ {
return true; return true;
} }
@@ -434,8 +437,6 @@ public class LodDataBuilder
// helper methods // // helper methods //
//================// //================//
public static boolean canGenerateLodFromChunk(IChunkWrapper chunk) { return chunk != null && chunk.isLightCorrect(); }
public static int getXOrZSectionPosFromChunkPos(int chunkXOrZPos) public static int getXOrZSectionPosFromChunkPos(int chunkXOrZPos)
{ {
// get the section position // get the section position
@@ -46,7 +46,7 @@ public abstract class AbstractDataSourceHandler
* The lowest numerical detail level possible. * The lowest numerical detail level possible.
* *
* @see AbstractDataSourceHandler#TOP_SECTION_DETAIL_LEVEL * @see AbstractDataSourceHandler#TOP_SECTION_DETAIL_LEVEL
* */ */
public static final byte MIN_SECTION_DETAIL_LEVEL = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; public static final byte MIN_SECTION_DETAIL_LEVEL = DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL;
@@ -223,6 +223,7 @@ public abstract class AbstractDataSourceHandler
/** /**
* After this method returns the inputData will be written to file. * After this method returns the inputData will be written to file.
*
* @param updatePos the position to update * @param updatePos the position to update
*/ */
protected void updateDataSourceAtPos(long updatePos, @NotNull FullDataSourceV2 inputData, boolean lockOnUpdatePos) protected void updateDataSourceAtPos(long updatePos, @NotNull FullDataSourceV2 inputData, boolean lockOnUpdatePos)
@@ -21,6 +21,7 @@ package com.seibel.distanthorizons.core.file.fullDatafile;
import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode; import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode;
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.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV1; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV1;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
@@ -37,6 +38,7 @@ import com.seibel.distanthorizons.core.sql.repo.FullDataSourceV2Repo;
import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.world.EWorldEnvironment;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -45,11 +47,14 @@ import org.jetbrains.annotations.Nullable;
import java.awt.*; import java.awt.*;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.*; import java.util.*;
import java.util.List;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
/** /**
* Handles reading/writing {@link FullDataSourceV2} * Handles reading/writing {@link FullDataSourceV2}
@@ -102,9 +107,14 @@ public class FullDataSourceProviderV2
// TODO only run thread if modifications happened recently // TODO only run thread if modifications happened recently
/** /**
* This isn't in {@link AbstractDataSourceHandler} since we don't need parent updating logic * This isn't in {@link AbstractDataSourceHandler} since we only want to update
* for render data, only full data. * the newest version of the full data, so if we have providers for either
* render data or old full data, we don't want to update them. <br><br>
*
* Will be null on the dedicated server since updates don't need to be propagated,
* only the highest detail level is needed.
*/ */
@Nullable
private final ThreadPoolExecutor updateQueueProcessor; private final ThreadPoolExecutor updateQueueProcessor;
@@ -121,14 +131,22 @@ public class FullDataSourceProviderV2
DebugRenderer.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showFullDataUpdateStatus); DebugRenderer.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showFullDataUpdateStatus);
String dimensionName = level.getLevelWrapper().getDimensionType().getDimensionName(); String dimensionName = level.getLevelWrapper().getDimensionName();
// start migrating any legacy data sources present in the background // start migrating any legacy data sources present in the background
this.migrationThreadPool = ThreadUtil.makeRateLimitedThreadPool(1, MIGRATION_THREAD_NAME_PREFIX + "["+dimensionName+"]", Config.Client.Advanced.MultiThreading.runTimeRatioForUpdatePropagatorThreads.get(), Thread.MIN_PRIORITY, (Semaphore) null); this.migrationThreadPool = ThreadUtil.makeRateLimitedThreadPool(1, MIGRATION_THREAD_NAME_PREFIX + "["+dimensionName+"]", Config.Client.Advanced.MultiThreading.runTimeRatioForUpdatePropagatorThreads.get(), Thread.MIN_PRIORITY, (Semaphore) null);
this.migrationThreadPool.execute(() -> this.convertLegacyDataSources()); this.migrationThreadPool.execute(this::convertLegacyDataSources);
// update propagation doesn't need to be run on the server since only the highest detail level is needed
if (SharedApi.getEnvironment() != EWorldEnvironment.Server_Only)
{
this.updateQueueProcessor = ThreadUtil.makeSingleThreadPool("Parent Update Queue ["+dimensionName+"]"); this.updateQueueProcessor = ThreadUtil.makeSingleThreadPool("Parent Update Queue ["+dimensionName+"]");
this.updateQueueProcessor.execute(() -> this.runUpdateQueue()); this.updateQueueProcessor.execute(this::runUpdateQueue);
}
else
{
this.updateQueueProcessor = null;
}
} }
@@ -289,7 +307,8 @@ public class FullDataSourceProviderV2
} }
}); });
} }
catch (RejectedExecutionException ignore) { /* the executor was shut down, it should be back up shortly and able to accept new jobs */ } catch (RejectedExecutionException ignore)
{ /* the executor was shut down, it should be back up shortly and able to accept new jobs */ }
catch (Exception e) catch (Exception e)
{ {
this.parentUpdatingPosSet.remove(parentUpdatePos); this.parentUpdatingPosSet.remove(parentUpdatePos);
@@ -299,7 +318,10 @@ public class FullDataSourceProviderV2
} }
} }
catch (InterruptedException ignored) { Thread.currentThread().interrupt(); } catch (InterruptedException ignored)
{
Thread.currentThread().interrupt();
}
catch (Exception e) catch (Exception e)
{ {
LOGGER.error("Unexpected error in the parent update queue thread. Error: " + e.getMessage(), e); LOGGER.error("Unexpected error in the parent update queue thread. Error: " + e.getMessage(), e);
@@ -317,7 +339,7 @@ public class FullDataSourceProviderV2
private void convertLegacyDataSources() private void convertLegacyDataSources()
{ {
String dimensionName = this.level.getLevelWrapper().getDimensionType().getDimensionName(); String dimensionName = this.level.getLevelWrapper().getDimensionName();
LOGGER.info("Attempting to migrate data sources for: ["+dimensionName+"]-["+this.saveDir+"]..."); LOGGER.info("Attempting to migrate data sources for: ["+dimensionName+"]-["+this.saveDir+"]...");
@@ -368,8 +390,8 @@ public class FullDataSourceProviderV2
} }
catch (InterruptedException ignore){} catch (InterruptedException ignore){}
} }
LOGGER.info("Done deleting [" + dimensionName + "] - ["+totalDeleteCount+"] unused data sources."); LOGGER.info("Done deleting [" + dimensionName + "] - ["+totalDeleteCount+"] unused data sources.");
} }
@@ -420,9 +442,7 @@ public class FullDataSourceProviderV2
{ {
newDataSource.close(); newDataSource.close();
} }
catch (Exception ignore) catch (Exception ignore) { }
{
}
}); });
} }
catch (Exception e) catch (Exception e)
@@ -498,7 +518,7 @@ public class FullDataSourceProviderV2
} }
this.migrationStartMessageQueued = true; this.migrationStartMessageQueued = true;
String dimName = this.level.getLevelWrapper().getDimensionType().getDimensionName(); String dimName = this.level.getLevelWrapper().getDimensionName();
ClientApi.INSTANCE.showChatMessageNextFrame( ClientApi.INSTANCE.showChatMessageNextFrame(
"Old Distant Horizons data is being migrated for ["+dimName+"]. \n" + "Old Distant Horizons data is being migrated for ["+dimName+"]. \n" +
"While migrating LODs may load slowly \n" + "While migrating LODs may load slowly \n" +
@@ -509,7 +529,7 @@ public class FullDataSourceProviderV2
private void showMigrationEndMessage(boolean success) private void showMigrationEndMessage(boolean success)
{ {
String dimName = this.level.getLevelWrapper().getDimensionType().getDimensionName(); String dimName = this.level.getLevelWrapper().getDimensionName();
if (success) if (success)
{ {
@@ -594,6 +614,20 @@ public class FullDataSourceProviderV2
*/ */
public int getUnsavedDataSourceCount() { return -1; } public int getUnsavedDataSourceCount() { return -1; }
public boolean fileExists(long pos) { return this.repo.getDataSizeInBytes(pos) > 0; }
//========================//
// multiplayer networking //
//========================//
@Nullable
public Long getTimestampForPos(long pos)
{ return this.repo.getTimestampForPos(pos); }
public Map<Long, Long> getTimestampsForRange(byte detailLevel, int startPosX, int startPosZ, int endPosX, int endPosZ)
{ return this.repo.getTimestampsForRange(detailLevel, startPosX, startPosZ, endPosX, endPosZ); }
//===========// //===========//
@@ -616,7 +650,10 @@ public class FullDataSourceProviderV2
public void close() public void close()
{ {
super.close(); super.close();
if (this.updateQueueProcessor != null)
{
this.updateQueueProcessor.shutdownNow(); this.updateQueueProcessor.shutdownNow();
}
this.legacyFileHandler.close(); this.legacyFileHandler.close();
@@ -23,6 +23,7 @@ import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGeneratio
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure;
import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue; import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue;
import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult;
@@ -36,12 +37,15 @@ import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
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 org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import java.awt.*; import java.awt.*;
import java.io.File;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.IntStream;
public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 implements IDebugRenderable public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 implements IDebugRenderable
{ {
@@ -70,6 +74,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
//=============// //=============//
public GeneratedFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure) { super(level, saveStructure); } public GeneratedFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure) { super(level, saveStructure); }
public GeneratedFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride) { super(level, saveStructure, saveDirOverride); }
@@ -139,7 +144,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
{ {
boolean oldQueueExists = this.worldGenQueueRef.compareAndSet(null, newWorldGenQueue); boolean oldQueueExists = this.worldGenQueueRef.compareAndSet(null, newWorldGenQueue);
LodUtil.assertTrue(oldQueueExists, "previous world gen queue is still here!"); LodUtil.assertTrue(oldQueueExists, "previous world gen queue is still here!");
LOGGER.info("Set world gen queue for level ["+this.level+"]."); LOGGER.info("Set world gen queue for level [" + this.level.getLevelWrapper().getDimensionName() + "].");
} }
@Override @Override
@@ -214,7 +219,7 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
} }
GenTask genTask = new GenTask(genPos); GenTask genTask = new GenTask(genPos);
CompletableFuture<WorldGenResult> worldGenFuture = worldGenQueue.submitGenTask(genPos, (byte) (DhSectionPos.getDetailLevel(genPos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL), genTask); CompletableFuture<WorldGenResult> worldGenFuture = worldGenQueue.submitRetrievalTask(genPos, (byte) (DhSectionPos.getDetailLevel(genPos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL), genTask);
worldGenFuture.whenComplete((genTaskResult, ex) -> this.onWorldGenTaskComplete(genTaskResult, ex)); worldGenFuture.whenComplete((genTaskResult, ex) -> this.onWorldGenTaskComplete(genTaskResult, ex));
return true; return true;
@@ -237,6 +242,12 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
public int getUnsavedDataSourceCount() { return this.delayedFullDataSourceSaveCache.getUnsavedCount(); } public int getUnsavedDataSourceCount() { return this.delayedFullDataSourceSaveCache.getUnsavedCount(); }
public boolean isFullyGenerated(byte[] columnGenerationSteps)
{
return IntStream.range(0, columnGenerationSteps.length)
.noneMatch(i -> columnGenerationSteps[i] == EDhApiWorldGenerationStep.EMPTY.value);
}
@Override @Override
public LongArrayList getPositionsToRetrieve(Long pos) public LongArrayList getPositionsToRetrieve(Long pos)
{ {
@@ -385,16 +396,24 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
public boolean isMemoryAddressValid() { return true; } public boolean isMemoryAddressValid() { return true; }
@Override @Override
public Consumer<FullDataSourceV2> getChunkDataConsumer() public Consumer<FullDataSourceV2> getDataSourceConsumer()
{ {
return (chunkSizedFullDataSource) -> return (chunkSizedFullDataSource) ->
{ {
GeneratedFullDataSourceProvider.this.delayedFullDataSourceSaveCache.queueDataSourceForUpdateAndSave(chunkSizedFullDataSource); GeneratedFullDataSourceProvider.this.delayedFullDataSourceSaveCache.queueDataSourceForUpdateAndSave(chunkSizedFullDataSource);
}; };
} }
} }
private void onDataSourceSave(FullDataSourceV2 fullDataSource) private void onDataSourceSave(FullDataSourceV2 fullDataSource)
{ GeneratedFullDataSourceProvider.this.updateDataSourceAsync(fullDataSource); } {
// block lights should have been populated at the chunkWrapper stage
// waiting to populate the data source's skylight at this stage prevents re-lighting and
// allows us to reduce cross-chunk lighting issues by lighting the whole 4x4 LOD at once
DhLightingEngine.INSTANCE.bakeDataSourceSkyLight(fullDataSource, LodUtil.MAX_MC_LIGHT);
GeneratedFullDataSourceProvider.this.updateDataSourceAsync(fullDataSource);
}
@@ -19,15 +19,119 @@
package com.seibel.distanthorizons.core.file.fullDatafile; package com.seibel.distanthorizons.core.file.fullDatafile;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure;
import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.level.WorldGenModule;
import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoginRequestQueue;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class RemoteFullDataSourceProvider extends FullDataSourceProviderV2 /**
* Only handles {@link SyncOnLoginRequestQueue} requests (IE updating existing LODs based on a timestamp).
* Missing data is handled by {@link WorldGenModule} and {@link RemoteWorldRetrievalQueue}.
*/
public class RemoteFullDataSourceProvider extends GeneratedFullDataSourceProvider
{ {
public RemoteFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure) { super(level, saveStructure); } @Nullable
public RemoteFullDataSourceProvider(IDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride) { super(level, saveStructure, saveDirOverride); } private final SyncOnLoginRequestQueue syncOnLoginRequestQueue;
private final Set<Long> finishedTaskPositions = ConcurrentHashMap.newKeySet();
//=============//
// constructor //
//=============//
public RemoteFullDataSourceProvider(
IDhLevel level, AbstractSaveStructure saveStructure, @Nullable File saveDirOverride,
@Nullable SyncOnLoginRequestQueue syncOnLoginRequestQueue)
{
super(level, saveStructure, saveDirOverride);
this.syncOnLoginRequestQueue = syncOnLoginRequestQueue;
}
//==================//
// override methods //
//==================//
@Override
@Nullable
public FullDataSourceV2 get(long pos)
{
//=======================//
// get local data source //
//=======================//
FullDataSourceV2 fullDataSource = super.get(pos);
if (fullDataSource == null)
{
// we don't have any local data for this position,
// we can't queue updates based on a timestamp
return null;
}
if (this.syncOnLoginRequestQueue == null)
{
// we have local data, but aren't allowed to
// request timestamp updates from the server.
return fullDataSource;
}
//===========================//
// request timestamp updates //
// from server //
//===========================//
// get the timestamp for every maximum detail position in this section
int posToMinimumDetailScale = (DhSectionPos.getDetailLevel(pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL + 1);
Map<Long, Long> timestamps = this.getTimestampsForRange(
DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL,
DhSectionPos.getX(pos) * posToMinimumDetailScale,
DhSectionPos.getZ(pos) * posToMinimumDetailScale,
(DhSectionPos.getX(pos) + 1) * posToMinimumDetailScale - 1,
(DhSectionPos.getZ(pos) + 1) * posToMinimumDetailScale - 1
);
// check if the server has newer versions of these LODs
for (Map.Entry<Long, Long> timestampBySectionPos : timestamps.entrySet())
{
Long subPos = timestampBySectionPos.getKey();
Long subTimestamp = timestampBySectionPos.getValue();
if (this.finishedTaskPositions.add(subPos))
{
this.syncOnLoginRequestQueue.submitRequest(subPos, subTimestamp, this.delayedFullDataSourceSaveCache::queueDataSourceForUpdateAndSave);
}
}
return fullDataSource;
}
//==========//
// shutdown //
//==========//
@Override
public void close()
{
if (this.syncOnLoginRequestQueue != null)
{
this.syncOnLoginRequestQueue.close();
}
super.close();
}
} }
@@ -29,7 +29,6 @@ import com.seibel.distanthorizons.core.util.objects.ParsedIp;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper; import com.seibel.distanthorizons.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.IDimensionTypeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.coreapi.util.StringUtil; import com.seibel.distanthorizons.coreapi.util.StringUtil;
@@ -88,9 +87,9 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
if (newLevelWrapper instanceof IServerKeyedClientLevel) if (newLevelWrapper instanceof IServerKeyedClientLevel)
{ {
IServerKeyedClientLevel keyedClientLevel = (IServerKeyedClientLevel) newLevelWrapper; IServerKeyedClientLevel keyedClientLevel = (IServerKeyedClientLevel) newLevelWrapper;
LOGGER.info("Loading level " + newLevelWrapper.getDimensionType().getDimensionName() + " with key: " + keyedClientLevel.getServerLevelKey()); LOGGER.info("Loading level " + newLevelWrapper.getDimensionName() + " with key: " + keyedClientLevel.getServerLevelKey());
// This world was identified by the server directly, so we can know for sure which folder to use. // This world was identified by the server directly, so we can know for sure which folder to use.
return new File(getSaveStructureFolderPath() + File.separatorChar + keyedClientLevel.getServerLevelKey()); return new File(getSaveStructureFolderPath() + File.separatorChar + keyedClientLevel.getServerLevelKey().replaceAll(":", "@@"));
} }
@@ -102,9 +101,9 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
// create the matcher if one doesn't exist // create the matcher if one doesn't exist
if (this.subDimMatcher == null || !this.subDimMatcher.isFindingLevel(newClientLevelWrapper)) if (this.subDimMatcher == null || !this.subDimMatcher.isFindingLevel(newClientLevelWrapper))
{ {
LOGGER.info("Loading level " + newClientLevelWrapper.getDimensionType().getDimensionName()); LOGGER.info("Loading level " + newClientLevelWrapper.getDimensionName());
List<File> levelFolders = this.getDhDataFoldersForDimension(newClientLevelWrapper.getDimensionType()); List<File> levelFolders = this.getDhDataFoldersForLevel(newClientLevelWrapper);
this.subDimMatcher = new SubDimensionLevelMatcher(newClientLevelWrapper, this.folder, levelFolders); this.subDimMatcher = new SubDimensionLevelMatcher(newClientLevelWrapper, this.folder, levelFolders);
} }
@@ -133,7 +132,7 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
private File getLevelFolderWithoutSimilarityMatching(ILevelWrapper level) private File getLevelFolderWithoutSimilarityMatching(ILevelWrapper level)
{ {
List<File> folders = this.getDhDataFoldersForDimension(level.getDimensionType()); List<File> folders = this.getDhDataFoldersForLevel(level);
if (!folders.isEmpty() && folders.get(0) != null) if (!folders.isEmpty() && folders.get(0) != null)
{ {
// use the first existing sub-dimension // use the first existing sub-dimension
@@ -144,12 +143,12 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
else else
{ {
// no valid sub dimension was found, create a new one // no valid sub dimension was found, create a new one
LOGGER.info("Default Sub Dimension not found. Creating: [" + level.getDimensionType().getDimensionName() + "]"); LOGGER.info("Default Sub Dimension not found. Creating: [" + level.getDimensionName() + "]");
return new File(this.folder, level.getDimensionType().getDimensionName()); return new File(this.folder, level.getDimensionName().replaceAll(":", "@@"));
} }
} }
public List<File> getDhDataFoldersForDimension(IDimensionTypeWrapper dimensionType) public List<File> getDhDataFoldersForLevel(ILevelWrapper level)
{ {
File[] folders = this.folder.listFiles(); File[] folders = this.folder.listFiles();
if (folders == null) if (folders == null)
@@ -158,7 +157,7 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
} }
// filter by dimension name // filter by dimension name
String expectedDimName = dimensionType.getDimensionName(); String expectedDimName = level.getDimensionName();
ArrayList<File> possibleDimFolders = new ArrayList<>(); ArrayList<File> possibleDimFolders = new ArrayList<>();
for (File dimFolder : folders) for (File dimFolder : folders)
{ {
@@ -60,6 +60,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
* @author James Seibel * @author James Seibel
* @version 12-17-2022 * @version 12-17-2022
*/ */
@Deprecated
public class SubDimensionLevelMatcher implements AutoCloseable public class SubDimensionLevelMatcher implements AutoCloseable
{ {
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
@@ -184,11 +185,6 @@ public class SubDimensionLevelMatcher implements AutoCloseable
DhLightingEngine.INSTANCE.lightChunk(newlyLoadedChunk, new ArrayList<>(), MC_CLIENT.getWrappedClientLevel().hasSkyLight() ? 15 : 0); DhLightingEngine.INSTANCE.lightChunk(newlyLoadedChunk, new ArrayList<>(), MC_CLIENT.getWrappedClientLevel().hasSkyLight() ? 15 : 0);
// build the chunk LOD // build the chunk LOD
if (!LodDataBuilder.canGenerateLodFromChunk(newlyLoadedChunk))
{
LOGGER.warn("unable to build lod for chunk:"+newlyLoadedChunk.getChunkPos());
return null;
}
FullDataSourceV2 newChunkSizedFullDataView = FullDataSourceV2.createFromChunk(newlyLoadedChunk); FullDataSourceV2 newChunkSizedFullDataView = FullDataSourceV2.createFromChunk(newlyLoadedChunk);
// convert to a data source for easier comparing // convert to a data source for easier comparing
FullDataSourceV2 newDataSource = FullDataSourceV2.createEmpty(DhSectionPos.encodeContaining(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, this.playerData.playerBlockPos)); FullDataSourceV2 newDataSource = FullDataSourceV2.createEmpty(DhSectionPos.encodeContaining(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, this.playerData.playerBlockPos));
@@ -201,8 +197,8 @@ public class SubDimensionLevelMatcher implements AutoCloseable
//================================// //================================//
// log the start of this attempt // log the start of this attempt
LOGGER.info("Attempting to determine sub-dimension for [" + MC_CLIENT.getWrappedClientLevel().getDimensionType().getDimensionName() + "]"); LOGGER.info("Attempting to determine sub-dimension for [" + MC_CLIENT.getWrappedClientLevel().getDimensionName() + "]");
LOGGER.info("Player block pos in dimension: [" + this.playerData.playerBlockPos.x + "," + this.playerData.playerBlockPos.y + "," + this.playerData.playerBlockPos.z + "]"); LOGGER.info("Player block pos in dimension: [" + this.playerData.playerBlockPos.getX() + "," + this.playerData.playerBlockPos.getY() + "," + this.playerData.playerBlockPos.getZ() + "]");
LOGGER.info("Potential Sub Dimension folders: [" + this.potentialLevelFolders.size() + "]"); LOGGER.info("Potential Sub Dimension folders: [" + this.potentialLevelFolders.size() + "]");
SubDimCompare mostSimilarSubDim = null; SubDimCompare mostSimilarSubDim = null;
@@ -214,7 +210,7 @@ public class SubDimensionLevelMatcher implements AutoCloseable
try try
{ {
// get the data source to compare against // get the data source to compare against
try (IDhLevel tempLevel = new DhClientLevel(new ClientOnlySaveStructure(), this.currentClientLevel, testLevelFolder, false)) try (IDhLevel tempLevel = new DhClientLevel(new ClientOnlySaveStructure(), this.currentClientLevel, testLevelFolder, false, null))
{ {
testFullDataSource = tempLevel.getFullDataProvider().getAsync(DhSectionPos.encodeContaining(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, this.playerData.playerBlockPos)).join(); testFullDataSource = tempLevel.getFullDataProvider().getAsync(DhSectionPos.encodeContaining(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, this.playerData.playerBlockPos)).join();
if (testFullDataSource == null) if (testFullDataSource == null)
@@ -315,7 +311,7 @@ public class SubDimensionLevelMatcher implements AutoCloseable
// get the player data for this dimension folder // get the player data for this dimension folder
SubDimensionPlayerData testPlayerData = new SubDimensionPlayerData(testLevelFolder); SubDimensionPlayerData testPlayerData = new SubDimensionPlayerData(testLevelFolder);
LOGGER.info("Last known player pos: [" + testPlayerData.playerBlockPos.x + "," + testPlayerData.playerBlockPos.y + "," + testPlayerData.playerBlockPos.z + "]"); LOGGER.info("Last known player pos: [" + testPlayerData.playerBlockPos.getX() + "," + testPlayerData.playerBlockPos.getY() + "," + testPlayerData.playerBlockPos.getZ() + "]");
// check if the block positions are close // check if the block positions are close
int playerBlockDist = testPlayerData.playerBlockPos.getManhattanDistance(this.playerData.playerBlockPos); int playerBlockDist = testPlayerData.playerBlockPos.getManhattanDistance(this.playerData.playerBlockPos);
@@ -380,7 +376,7 @@ public class SubDimensionLevelMatcher implements AutoCloseable
} }
private File CreateSubDimFolder(String subDimId) { return new File(this.levelsFolder.getPath() + File.separatorChar + this.currentClientLevel.getDimensionType().getDimensionName(), subDimId); } private File CreateSubDimFolder(String subDimId) { return new File(this.levelsFolder.getPath() + File.separatorChar + this.currentClientLevel.getDimensionName(), subDimId); }
@Override @Override
public void close() { this.matcherThread.shutdownNow(); } public void close() { this.matcherThread.shutdownNow(); }
@@ -22,7 +22,7 @@ package com.seibel.distanthorizons.core.file.subDimMatching;
import com.electronwill.nightconfig.core.file.CommentedFileConfig; import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.pos.DhBlockPos; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
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.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
@@ -127,9 +127,9 @@ public class SubDimensionPlayerData
public void toTomlFile(CommentedFileConfig toml) public void toTomlFile(CommentedFileConfig toml)
{ {
// player block pos // player block pos
toml.add(PLAYER_BLOCK_POS_X_PATH, this.playerBlockPos.x); toml.add(PLAYER_BLOCK_POS_X_PATH, this.playerBlockPos.getX());
toml.add(PLAYER_BLOCK_POS_Y_PATH, this.playerBlockPos.y); toml.add(PLAYER_BLOCK_POS_Y_PATH, this.playerBlockPos.getY());
toml.add(PLAYER_BLOCK_POS_Z_PATH, this.playerBlockPos.z); toml.add(PLAYER_BLOCK_POS_Z_PATH, this.playerBlockPos.getZ());
toml.save(); toml.save();
} }
@@ -138,7 +138,7 @@ public class SubDimensionPlayerData
@Override @Override
public String toString() public String toString()
{ {
return "PlayerBlockPos: [" + this.playerBlockPos.x + "," + this.playerBlockPos.y + "," + this.playerBlockPos.z + "]"; return "PlayerBlockPos: [" + this.playerBlockPos.getX() + "," + this.playerBlockPos.getY() + "," + this.playerBlockPos.getZ() + "]";
} }
} }
@@ -33,7 +33,7 @@ public class AdjacentChunkHolder
{ {
for (int zOffset = -1; zOffset <= 1; zOffset++) for (int zOffset = -1; zOffset <= 1; zOffset++)
{ {
DhChunkPos adjacentPos = new DhChunkPos(centerChunkPos.x + xOffset, centerChunkPos.z + zOffset); DhChunkPos adjacentPos = new DhChunkPos(centerChunkPos.getX() + xOffset, centerChunkPos.getZ() + zOffset);
requestedAdjacentPositions.add(adjacentPos); requestedAdjacentPositions.add(adjacentPos);
} }
} }
@@ -69,13 +69,13 @@ public class AdjacentChunkHolder
DhChunkPos centerPos = this.chunkArray[4].getChunkPos(); DhChunkPos centerPos = this.chunkArray[4].getChunkPos();
DhChunkPos offsetPos = centerWrapper.getChunkPos(); DhChunkPos offsetPos = centerWrapper.getChunkPos();
int offsetX = offsetPos.x - centerPos.x; int offsetX = offsetPos.getX() - centerPos.getX();
if (offsetX < -1 || offsetX > 1) if (offsetX < -1 || offsetX > 1)
{ {
return; return;
} }
int offsetZ = offsetPos.z - centerPos.z; int offsetZ = offsetPos.getZ() - centerPos.getZ();
if (offsetZ < -1 || offsetZ > 1) if (offsetZ < -1 || offsetZ > 1)
{ {
return; return;
@@ -91,18 +91,18 @@ public class AdjacentChunkHolder
int chunkZ = BitShiftUtil.divideByPowerOfTwo(blockZ, 4); int chunkZ = BitShiftUtil.divideByPowerOfTwo(blockZ, 4);
IChunkWrapper centerChunk = this.chunkArray[4]; IChunkWrapper centerChunk = this.chunkArray[4];
DhChunkPos centerPos = centerChunk.getChunkPos(); DhChunkPos centerPos = centerChunk.getChunkPos();
if (centerPos.x == chunkX && centerPos.z == chunkZ) if (centerPos.getX() == chunkX && centerPos.getZ() == chunkZ)
{ {
return centerChunk; return centerChunk;
} }
int offsetX = chunkX - centerPos.x; int offsetX = chunkX - centerPos.getX();
if (offsetX < -1 || offsetX > 1) if (offsetX < -1 || offsetX > 1)
{ {
return null; return null;
} }
int offsetZ = chunkZ - centerPos.z; int offsetZ = chunkZ - centerPos.getZ();
if (offsetZ < -1 || offsetZ > 1) if (offsetZ < -1 || offsetZ > 1)
{ {
return null; return null;
@@ -19,15 +19,23 @@
package com.seibel.distanthorizons.core.generation; package com.seibel.distanthorizons.core.generation;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhBlockPos; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPosMutable;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.util.FullDataPointUtil;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IMutableBlockPosWrapper;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import java.awt.*;
import java.util.*; import java.util.*;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@@ -49,15 +57,59 @@ public class DhLightingEngine
* Since these objects are always mutated anyway, using a {@link ThreadLocal} will allow us to * Since these objects are always mutated anyway, using a {@link ThreadLocal} will allow us to
* only create as many of these {@link DhBlockPos} as necessary. * only create as many of these {@link DhBlockPos} as necessary.
*/ */
private static final ThreadLocal<DhBlockPos> PRIMARY_BLOCK_POS_REF = ThreadLocal.withInitial(() -> new DhBlockPos()); private static final ThreadLocal<DhBlockPosMutable> PRIMARY_BLOCK_POS_REF = ThreadLocal.withInitial(() -> new DhBlockPosMutable());
private static final ThreadLocal<DhBlockPos> SECONDARY_BLOCK_POS_REF = ThreadLocal.withInitial(() -> new DhBlockPos()); private static final ThreadLocal<DhBlockPosMutable> SECONDARY_BLOCK_POS_REF = ThreadLocal.withInitial(() -> new DhBlockPosMutable());
/** if enabled will render each block light value when the chunk lighting engine is run */
private static final boolean RENDER_BLOCK_LIGHT_WIREFRAME = false;
/** if enabled will render each sky light value when the chunk lighting engine is run */
private static final boolean RENDER_SKY_LIGHT_WIREFRAME = false;
/**
* Used for dataSource lighting. <br>
* Packed as alternating x and z offsets.
*/
private static final byte[] ADJACENT_DIRECTION_OFFSETS = new byte[]
{
-1, 0,
+1, 0,
0, -1,
0, +1
};
//=============//
// constructor //
//=============//
private DhLightingEngine() { } private DhLightingEngine() { }
//================//
// chunk lighting //
//================//
/**
* Populates both block and sky lighting.
* @see DhLightingEngine#lightChunk(IChunkWrapper, ArrayList, int, boolean, boolean)
*/
public void lightChunk(
@NotNull IChunkWrapper centerChunk, @NotNull ArrayList<IChunkWrapper> nearbyChunkList,
int maxSkyLight)
{ this.lightChunk(centerChunk, nearbyChunkList, maxSkyLight, true, true); }
/**
* Only populates block lights.
* @see DhLightingEngine#lightChunk(IChunkWrapper, ArrayList, int, boolean, boolean)
*/
public void bakeChunkBlockLighting(
@NotNull IChunkWrapper centerChunk, @NotNull ArrayList<IChunkWrapper> nearbyChunkList,
int maxSkyLight)
{ this.lightChunk(centerChunk, nearbyChunkList, maxSkyLight, true, false); }
/** /**
* Note: depending on the implementation of {@link IChunkWrapper#setDhBlockLight(int, int, int, int)} and {@link IChunkWrapper#setDhSkyLight(int, int, int, int)} * Note: depending on the implementation of {@link IChunkWrapper#setDhBlockLight(int, int, int, int)} and {@link IChunkWrapper#setDhSkyLight(int, int, int, int)}
* the light values may be stored in the wrapper itself instead of the wrapped chunk object. * the light values may be stored in the wrapper itself instead of the wrapped chunk object.
@@ -67,21 +119,21 @@ public class DhLightingEngine
* @param nearbyChunkList should also contain centerChunk * @param nearbyChunkList should also contain centerChunk
* @param maxSkyLight should be a value between 0 and 15 * @param maxSkyLight should be a value between 0 and 15
*/ */
public void lightChunk(@NotNull IChunkWrapper centerChunk, @NotNull ArrayList<IChunkWrapper> nearbyChunkList, int maxSkyLight) private void lightChunk(
@NotNull IChunkWrapper centerChunk, @NotNull ArrayList<IChunkWrapper> nearbyChunkList,
int maxSkyLight, boolean updateBlockLight, boolean updateSkyLight)
{ {
DhChunkPos centerChunkPos = centerChunk.getChunkPos(); DhChunkPos centerChunkPos = centerChunk.getChunkPos();
AdjacentChunkHolder adjacentChunkHolder = new AdjacentChunkHolder(centerChunk); AdjacentChunkHolder adjacentChunkHolder = new AdjacentChunkHolder(centerChunk);
long startTimeNs = System.nanoTime();
// try-finally to handle the stableArray resources // try-finally to handle the stableArray resources
StableLightPosStack blockLightPosQueue = null; StableLightPosStack blockLightWorldPosQueue = null;
StableLightPosStack skyLightPosQueue = null; StableLightPosStack skyLightWorldPosQueue = null;
try try
{ {
blockLightPosQueue = StableLightPosStack.borrowStableLightPosArray(); blockLightWorldPosQueue = StableLightPosStack.borrowStableLightPosArray();
skyLightPosQueue = StableLightPosStack.borrowStableLightPosArray(); skyLightWorldPosQueue = StableLightPosStack.borrowStableLightPosArray();
@@ -92,7 +144,7 @@ public class DhLightingEngine
{ {
for (int zOffset = -1; zOffset <= 1; zOffset++) for (int zOffset = -1; zOffset <= 1; zOffset++)
{ {
DhChunkPos adjacentPos = new DhChunkPos(centerChunkPos.x + xOffset, centerChunkPos.z + zOffset); DhChunkPos adjacentPos = new DhChunkPos(centerChunkPos.getX() + xOffset, centerChunkPos.getZ() + zOffset);
requestedAdjacentPositions.add(adjacentPos); requestedAdjacentPositions.add(adjacentPos);
} }
} }
@@ -100,7 +152,6 @@ public class DhLightingEngine
// find all adjacent chunks // find all adjacent chunks
// and get any necessary info from them // and get any necessary info from them
boolean warningLogged = false;
for (int chunkIndex = 0; chunkIndex < nearbyChunkList.size(); chunkIndex++) // using iterators in high traffic areas can cause GC issues due to allocating a bunch of iterators, use an indexed for-loop instead for (int chunkIndex = 0; chunkIndex < nearbyChunkList.size(); chunkIndex++) // using iterators in high traffic areas can cause GC issues due to allocating a bunch of iterators, use an indexed for-loop instead
{ {
IChunkWrapper chunk = nearbyChunkList.get(chunkIndex); IChunkWrapper chunk = nearbyChunkList.get(chunkIndex);
@@ -112,13 +163,18 @@ public class DhLightingEngine
// add the adjacent chunk // add the adjacent chunk
adjacentChunkHolder.add(chunk); adjacentChunkHolder.add(chunk);
// get and set the adjacent chunk's initial block lights // get and set the adjacent chunk's initial block lights
final DhBlockPos relLightBlockPos = PRIMARY_BLOCK_POS_REF.get(); final DhBlockPosMutable relLightBlockPos = PRIMARY_BLOCK_POS_REF.get();
final DhBlockPos relBlockPos = SECONDARY_BLOCK_POS_REF.get();
ArrayList<DhBlockPos> blockLightPosList = chunk.getBlockLightPosList();
//==================//
// set block lights //
//==================//
if (updateBlockLight)
{
ArrayList<DhBlockPos> blockLightPosList = chunk.getWorldBlockLightPosList();
for (int blockLightIndex = 0; blockLightIndex < blockLightPosList.size(); blockLightIndex++) // using iterators in high traffic areas can cause GC issues due to allocating a bunch of iterators, use an indexed for-loop instead for (int blockLightIndex = 0; blockLightIndex < blockLightPosList.size(); blockLightIndex++) // using iterators in high traffic areas can cause GC issues due to allocating a bunch of iterators, use an indexed for-loop instead
{ {
DhBlockPos blockLightPos = blockLightPosList.get(blockLightIndex); DhBlockPos blockLightPos = blockLightPosList.get(blockLightIndex);
@@ -127,18 +183,26 @@ public class DhLightingEngine
// get the light // get the light
IBlockStateWrapper blockState = chunk.getBlockState(relLightBlockPos); IBlockStateWrapper blockState = chunk.getBlockState(relLightBlockPos);
int lightValue = blockState.getLightEmission(); int lightValue = blockState.getLightEmission();
blockLightPosQueue.push(blockLightPos.x, blockLightPos.y, blockLightPos.z, lightValue); blockLightWorldPosQueue.push(blockLightPos.getX(), blockLightPos.getY(), blockLightPos.getZ(), lightValue);
// set the light // set the light
blockLightPos.mutateToChunkRelativePos(relBlockPos); chunk.setDhBlockLight(relLightBlockPos.getX(), relLightBlockPos.getY(), relLightBlockPos.getZ(), lightValue);
chunk.setDhBlockLight(relBlockPos.x, relBlockPos.y, relBlockPos.z, lightValue); }
} }
//================//
// set sky lights //
//================//
// get and set the adjacent chunk's initial skylights, // get and set the adjacent chunk's initial skylights,
// if the dimension has skylights // if the dimension has skylights
if (maxSkyLight > 0) if (updateSkyLight && maxSkyLight > 0)
{ {
IMutableBlockPosWrapper mcBlockPos = chunk.getMutableBlockPosWrapper();
IBlockStateWrapper previousBlockState = null;
int maxY = chunk.getMaxNonEmptyHeight(); int maxY = chunk.getMaxNonEmptyHeight();
int minY = chunk.getMinBuildHeight(); int minY = chunk.getMinBuildHeight();
@@ -150,7 +214,7 @@ public class DhLightingEngine
// set each pos' sky light all the way down until an opaque block is hit // set each pos' sky light all the way down until an opaque block is hit
for (int y = maxY; y >= minY; y--) for (int y = maxY; y >= minY; y--)
{ {
IBlockStateWrapper block = chunk.getBlockState(relX, y, relZ); IBlockStateWrapper block = previousBlockState = chunk.getBlockState(relX, y, relZ, mcBlockPos, previousBlockState);
if (block != null && block.getOpacity() != LodUtil.BLOCK_FULLY_TRANSPARENT) if (block != null && block.getOpacity() != LodUtil.BLOCK_FULLY_TRANSPARENT)
{ {
// keep moving down until we find a non-transparent block // keep moving down until we find a non-transparent block
@@ -160,11 +224,11 @@ public class DhLightingEngine
// add sky light to the queue // add sky light to the queue
DhBlockPos skyLightPos = new DhBlockPos(chunk.getMinBlockX() + relX, y, chunk.getMinBlockZ() + relZ); DhBlockPos skyLightPos = new DhBlockPos(chunk.getMinBlockX() + relX, y, chunk.getMinBlockZ() + relZ);
skyLightPosQueue.push(skyLightPos.x, skyLightPos.y, skyLightPos.z, maxSkyLight); skyLightWorldPosQueue.push(skyLightPos.getX(), skyLightPos.getY(), skyLightPos.getZ(), maxSkyLight);
// set the chunk's sky light // set the chunk's sky light
skyLightPos.mutateToChunkRelativePos(relBlockPos); skyLightPos.mutateToChunkRelativePos(relLightBlockPos);
chunk.setDhSkyLight(relBlockPos.x, relBlockPos.y, relBlockPos.z, maxSkyLight); chunk.setDhSkyLight(relLightBlockPos.getX(), relLightBlockPos.getY(), relLightBlockPos.getZ(), maxSkyLight);
} }
} }
} }
@@ -180,14 +244,22 @@ public class DhLightingEngine
} }
// block light // block light
this.propagateLightPosList(blockLightPosQueue, adjacentChunkHolder, if (updateBlockLight)
(neighbourChunk, relBlockPos) -> neighbourChunk.getDhBlockLight(relBlockPos.x, relBlockPos.y, relBlockPos.z), {
(neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhBlockLight(relBlockPos.x, relBlockPos.y, relBlockPos.z, newLightValue)); this.propagateChunkLightPosList(blockLightWorldPosQueue, adjacentChunkHolder,
(neighbourChunk, relBlockPos) -> neighbourChunk.getDhBlockLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ()),
(neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhBlockLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ(), newLightValue),
true);
}
// sky light // sky light
this.propagateLightPosList(skyLightPosQueue, adjacentChunkHolder, if (updateSkyLight)
(neighbourChunk, relBlockPos) -> neighbourChunk.getDhSkyLight(relBlockPos.x, relBlockPos.y, relBlockPos.z), {
(neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhSkyLight(relBlockPos.x, relBlockPos.y, relBlockPos.z, newLightValue)); this.propagateChunkLightPosList(skyLightWorldPosQueue, adjacentChunkHolder,
(neighbourChunk, relBlockPos) -> neighbourChunk.getDhSkyLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ()),
(neighbourChunk, relBlockPos, newLightValue) -> neighbourChunk.setDhSkyLight(relBlockPos.getX(), relBlockPos.getY(), relBlockPos.getZ(), newLightValue),
false);
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -195,31 +267,35 @@ public class DhLightingEngine
} }
finally finally
{ {
StableLightPosStack.returnStableLightPosArray(blockLightPosQueue); StableLightPosStack.returnStableLightPosArray(blockLightWorldPosQueue);
StableLightPosStack.returnStableLightPosArray(skyLightPosQueue); StableLightPosStack.returnStableLightPosArray(skyLightWorldPosQueue);
} }
if (updateBlockLight)
centerChunk.setIsDhLightCorrect(true); {
centerChunk.setUseDhLighting(true); centerChunk.setIsDhBlockLightCorrect(true);
}
long endTimeNs = System.nanoTime(); if (updateSkyLight)
float totalTimeMs = (endTimeNs - startTimeNs) / 1_000_000.0f; {
//LOGGER.trace("Finished generating lighting for chunk: [" + centerChunkPos + "] in ["+totalTimeMs+"] milliseconds"); centerChunk.setIsDhSkyLightCorrect(true);
}
} }
/** Applies each {@link LightPos} from the queue to the given set of {@link IChunkWrapper}'s. */ /** Applies each {@link LightPos} from the queue to the given set of {@link IChunkWrapper}'s. */
private void propagateLightPosList( private void propagateChunkLightPosList(
StableLightPosStack lightPosQueue, AdjacentChunkHolder adjacentChunkHolder, StableLightPosStack lightPosQueue, AdjacentChunkHolder adjacentChunkHolder,
IGetLightFunc getLightFunc, ISetLightFunc setLightFunc) IGetLightFunc getLightFunc, ISetLightFunc setLightFunc,
boolean propagatingBlockLights)
{ {
// these objects are saved so they can be mutated throughout the method, // these objects are saved so they can be mutated throughout the method,
// this reduces the number of allocations necessary, reducing GC pressure // this reduces the number of allocations necessary, reducing GC pressure
final LightPos lightPos = new LightPos(0, 0, 0, 0); final LightPos lightPos = new LightPos(0, 0, 0, 0);
final DhBlockPos neighbourBlockPos = PRIMARY_BLOCK_POS_REF.get(); final DhBlockPosMutable neighbourBlockPos = PRIMARY_BLOCK_POS_REF.get();
final DhBlockPos relNeighbourBlockPos = SECONDARY_BLOCK_POS_REF.get(); final DhBlockPosMutable relNeighbourBlockPos = SECONDARY_BLOCK_POS_REF.get();
IMutableBlockPosWrapper mcBlockPos = null;
IBlockStateWrapper previousBlockState = null;
// update each light position // update each light position
while (!lightPosQueue.isEmpty()) while (!lightPosQueue.isEmpty())
@@ -239,14 +315,14 @@ public class DhLightingEngine
// only continue if the light position is inside one of our chunks // only continue if the light position is inside one of our chunks
IChunkWrapper neighbourChunk = adjacentChunkHolder.getByBlockPos(neighbourBlockPos.x, neighbourBlockPos.z); IChunkWrapper neighbourChunk = adjacentChunkHolder.getByBlockPos(neighbourBlockPos.getX(), neighbourBlockPos.getZ());
if (neighbourChunk == null) if (neighbourChunk == null)
{ {
// the light pos is outside our generator's range, ignore it // the light pos is outside our generator's range, ignore it
continue; continue;
} }
if (relNeighbourBlockPos.y < neighbourChunk.getMinNonEmptyHeight() || relNeighbourBlockPos.y > neighbourChunk.getMaxBuildHeight()) if (relNeighbourBlockPos.getY() < neighbourChunk.getMinNonEmptyHeight() || relNeighbourBlockPos.getY() > neighbourChunk.getMaxBuildHeight())
{ {
// the light pos is outside the chunk's min/max height, // the light pos is outside the chunk's min/max height,
// this can happen if given a chunk that hasn't finished generating // this can happen if given a chunk that hasn't finished generating
@@ -263,7 +339,15 @@ public class DhLightingEngine
} }
IBlockStateWrapper neighbourBlockState = neighbourChunk.getBlockState(relNeighbourBlockPos); if (mcBlockPos == null)
{
// it doesn't matter what chunk we get the position object from
// TODO move this getter logic out of ChunkWrapper
mcBlockPos = neighbourChunk.getMutableBlockPosWrapper();
}
IBlockStateWrapper neighbourBlockState = previousBlockState = neighbourChunk.getBlockState(relNeighbourBlockPos, mcBlockPos, previousBlockState);
// Math.max(1, ...) is used so that the propagated light level always drops by at least 1, preventing infinite cycles. // Math.max(1, ...) is used so that the propagated light level always drops by at least 1, preventing infinite cycles.
int targetLevel = lightValue - Math.max(1, neighbourBlockState.getOpacity()); int targetLevel = lightValue - Math.max(1, neighbourBlockState.getOpacity());
if (targetLevel > currentBlockLight) if (targetLevel > currentBlockLight)
@@ -273,16 +357,338 @@ public class DhLightingEngine
// now that light has been propagated to this blockPos // now that light has been propagated to this blockPos
// we need to queue it up so its neighbours can be propagated as well // we need to queue it up so its neighbours can be propagated as well
lightPosQueue.push(neighbourBlockPos.x, neighbourBlockPos.y, neighbourBlockPos.z, targetLevel); lightPosQueue.push(neighbourBlockPos.getX(), neighbourBlockPos.getY(), neighbourBlockPos.getZ(), targetLevel);
} }
} }
} }
// can be enable if troubleshooting lighting issues
if (RENDER_BLOCK_LIGHT_WIREFRAME && propagatingBlockLights)
{
RenderDhLightValuesAsWireframe(adjacentChunkHolder, true);
}
else if (RENDER_SKY_LIGHT_WIREFRAME && !propagatingBlockLights)
{
RenderDhLightValuesAsWireframe(adjacentChunkHolder, false);
}
// propagation complete // propagation complete
} }
//======================//
// data source lighting //
//======================//
/** @author BuilderB0y */
public void bakeDataSourceSkyLight(FullDataSourceV2 dataSource, int maxSkyLight)
{
// create a cache of all the IDs which are completely transparent.
// FullDataPointIdMap is thread-safe with locks, and is also a map lookup,
// and both of these things add a bit of overhead which is not necessary
// in this context.
// note: since IDs map to both biomes and blocks, there can be more than
// one ID which corresponds to air.
BitSet airIDs = new BitSet(dataSource.mapping.size());
for (int id = 0, size = dataSource.mapping.size(); id < size; id++)
{
if (dataSource.mapping.getBlockStateWrapper(id).getOpacity() == 0)
{
airIDs.set(id, true);
}
}
for (int z = 0; z < FullDataSourceV2.WIDTH; z++)
{
for (int x = 0; x < FullDataSourceV2.WIDTH; x++)
{
LongArrayList dataPoints = dataSource.get(x, z);
if (dataPoints != null && !dataPoints.isEmpty())
{
// iterate through the data points in this column top-down
// until we reach light level 0 in some way. at this point,
// no more propagation needs to be performed for this column.
int size = dataPoints.size();
for (int index = 0; index < size; index++)
{
long point = dataPoints.getLong(index);
// if the data point in the column is transparent,
// then fill it with light and then propagate
// that light both horizontally and downwards.
if (airIDs.get(FullDataPointUtil.getId(point)))
{
int skylight;
if (index == 0)
{
// top-most data point in the column.
skylight = maxSkyLight;
}
else
{
// handle down propagation here. sort of.
// down propagation is also handled partially elsewhere.
// basically if the data point above is transparent,
// we copy its light level.
// otherwise, if the data point above is opaque,
// then no light can propagate downwards from it.
// therefore, this data point should be light level 0*
// and no more propagation needs to be performed for this column.
//
// *unless light propagates into it horizontally,
// but that is handled separately.
long above = dataPoints.getLong(index - 1);
if (airIDs.get(FullDataPointUtil.getId(above)))
{
skylight = FullDataPointUtil.getSkyLight(above);
}
else
{
continue;
}
}
// update the data point to contain the correct starting skylight level.
point = FullDataPointUtil.setSkyLight(point, skylight);
dataPoints.set(index, point);
// now for the propagation.
recursivelyLightAdjacentDataPoints(dataSource, airIDs, x, z, point);
}
}
}
}
}
// at this point, all transparent data points have been lit,
// but opaque ones still have light level 0.
// in this loop we make opaque data points copy the light level
// above them if, and only if, the data point above is translucent.
// with one exception: if the data point above is only partially translucent,
// we use a slightly different way of computing how much light it absorbed.
// this is how we handle water and ocean floors.
// note that this alternate logic assumes the
// data point above is being lit from the top.
// this is a fine assumption for water and oceans.
for (LongArrayList list : dataSource.dataPoints)
{
if (list != null)
{
for (int index = 0, size = list.size(); index < size; index++)
{
long dataPoint = list.getLong(index);
if (index == 0)
{
// top data point, assume "above" has the max sky light.
dataPoint = FullDataPointUtil.setSkyLight(dataPoint, maxSkyLight);
list.set(index, dataPoint);
}
else
{
// there is another data point above this one.
// check to see how opaque this data point is first.
// we will check the above one after that.
if (!airIDs.get(FullDataPointUtil.getId(dataPoint)))
{
// this data point is not transparent.
// it should be lit from above.
long above = list.getLong(index - 1);
int aboveLight = FullDataPointUtil.getSkyLight(above);
if (airIDs.get(FullDataPointUtil.getId(above)))
{
// the above data point is transparent,
// and does not absorb any light.
// its light level can be copied as-is.
dataPoint = FullDataPointUtil.setSkyLight(dataPoint, aboveLight);
list.set(index, dataPoint);
}
else
{
// determine how much light should be absorbed by this column
int absorption = dataSource.mapping.getBlockStateWrapper(FullDataPointUtil.getId(above)).getOpacity() * FullDataPointUtil.getHeight(above);
if (absorption < aboveLight)
{
// the above data point is partially translucent,
// and absorbs some light. however, it did not absorb
// enough light to bring the light level down to 0.
// so, the remaining light can still be copied.
dataPoint = FullDataPointUtil.setSkyLight(dataPoint, aboveLight - absorption);
list.set(index, dataPoint);
}
}
}
}
}
}
}
}
/** @author BuilderB0y */
public void recursivelyLightAdjacentDataPoints(
FullDataSourceV2 chunk,
BitSet airIDs,
int relativeX,
int relativeZ,
long dataPoint
)
{
int lightLevel = FullDataPointUtil.getSkyLight(dataPoint);
// early exit condition:
// in this case, propagating light is guaranteed to be 0 at adjacent positions,
// and therefore we do not need to waste time propagating it.
if (lightLevel <= 1)
{
return;
}
int minY = FullDataPointUtil.getBottomY(dataPoint);
int maxY = FullDataPointUtil.getHeight(dataPoint) + minY;
// try to propagate in all 4 directions.
for (int offsetIndex = 0; offsetIndex < ADJACENT_DIRECTION_OFFSETS.length; )
{
int adjacentX = relativeX + ADJACENT_DIRECTION_OFFSETS[offsetIndex++];
int adjacentZ = relativeZ + ADJACENT_DIRECTION_OFFSETS[offsetIndex++];
// check if the adjacent position is within the bounds of this data source...
if (adjacentX >= 0 && adjacentX < FullDataSourceV2.WIDTH && adjacentZ >= 0 && adjacentZ < FullDataSourceV2.WIDTH)
{
LongArrayList adjacentDataPoints = chunk.get(adjacentX, adjacentZ);
// ...and also check to make sure we have some data points
// (potentially transparent ones) to propagate through in the adjacent column.
if (adjacentDataPoints != null)
{
// try to find adjacent data points we can propagate into.
// we go top-down for this, which will be important for some
// later conditions.
int size = adjacentDataPoints.size();
for (int adjacentIndex = 0; adjacentIndex < size; adjacentIndex++)
{
long adjacentDataPoint = adjacentDataPoints.getLong(adjacentIndex);
int adjacentMinY = FullDataPointUtil.getBottomY(adjacentDataPoint);
int adjacentMaxY = FullDataPointUtil.getHeight(adjacentDataPoint) + adjacentMinY;
if (adjacentMinY >= maxY)
{
// if the adjacent data point is completely above this one,
// then there is no overlap between this one and the adjacent one,
// and therefore light cannot propagate here.
// try to propagate to the next data point down from the adjacent one.
continue;
}
else if (adjacentMaxY <= minY)
{
// if the adjacent data point is completely below this one,
// then it also has no overlap and can't propagate,
// but since we're going top-down, neither can any subsequent adjacent data points.
break;
}
else if (!airIDs.get(FullDataPointUtil.getId(adjacentDataPoint)))
{
// assume for now that we cannot propagate into non-transparent data points.
continue; // TODO how does this work with water? Do we care?
}
else
{
// now we can try to propagate.
int adjacentLightLevel = FullDataPointUtil.getSkyLight(adjacentDataPoint);
// if the resulting light level after propagation would INCREASE
// the light level of the adjacent data point, then propagate to it.
// otherwise, don't do that.
if (lightLevel - 1 > adjacentLightLevel)
{
adjacentDataPoint = FullDataPointUtil.setSkyLight(adjacentDataPoint, lightLevel - 1);
adjacentDataPoints.set(adjacentIndex, adjacentDataPoint);
// if propagation succeeded, recursively propagate again starting at the adjacent data point.
recursivelyLightAdjacentDataPoints(chunk, airIDs, adjacentX, adjacentZ, adjacentDataPoint);
}
}
}
}
}
}
}
//===========//
// debugging //
//===========//
/** Draw a wireframe representing each block's light value */
private static void RenderDhLightValuesAsWireframe(AdjacentChunkHolder adjacentChunkHolder, boolean renderBlockLights)
{
for (IChunkWrapper chunk : adjacentChunkHolder.chunkArray)
{
if (chunk == null)
{
continue;
}
int chunkMinX = chunk.getMinBlockX();
int chunkMinZ = chunk.getMinBlockZ();
int minY = chunk.getMinNonEmptyHeight();
int maxY = chunk.getMaxNonEmptyHeight();
// check each position's light
for (int x = 0; x < LodUtil.CHUNK_WIDTH; x++)
{
for (int z = 0; z < LodUtil.CHUNK_WIDTH; z++)
{
for (int y = minY; y < maxY; y++)
{
int lightValue = renderBlockLights? chunk.getDhBlockLight(x, y, z) : chunk.getDhSkyLight(x, y, z);
if (lightValue != LodUtil.MIN_MC_LIGHT)
{
// hotter colors for more intense light
Color color;
if (lightValue >= 14)
{
color = Color.WHITE;
}
else if (lightValue >= 10)
{
color = Color.PINK;
}
else if (lightValue >= 6)
{
color = Color.YELLOW;
}
else if (lightValue >= 4)
{
color = Color.ORANGE;
}
else
{
color = Color.RED;
}
// a color can be set to null if you only want to troubleshoot up to a certain light level
if (color != null)
{
DebugRenderer.makeParticle(
new DebugRenderer.BoxParticle(
new DebugRenderer.Box(DhSectionPos.encode((byte) 0, chunkMinX + x, chunkMinZ + z), y, y + 1, 0.2f, color),
10.0, 0f
)
);
}
}
}
}
}
}
}
//================// //================//
// helper classes // // helper classes //
//================// //================//
@@ -293,7 +699,7 @@ public class DhLightingEngine
@FunctionalInterface @FunctionalInterface
interface ISetLightFunc { void setLight(IChunkWrapper chunk, DhBlockPos pos, int lightValue); } interface ISetLightFunc { void setLight(IChunkWrapper chunk, DhBlockPos pos, int lightValue); }
private static class LightPos extends DhBlockPos private static class LightPos extends DhBlockPosMutable
{ {
public int lightValue; public int lightValue;
@@ -303,6 +709,11 @@ public class DhLightingEngine
this.lightValue = lightValue; this.lightValue = lightValue;
} }
@Override
public String toString() { return this.lightValue+" - ["+ this.x +", "+ this.y +", "+ this.z +"]"; }
} }
/** /**
@@ -402,9 +813,9 @@ public class DhLightingEngine
{ {
int subIndex = this.index * INTS_PER_LIGHT_POS; int subIndex = this.index * INTS_PER_LIGHT_POS;
pos.x = this.lightPositions.getInt(subIndex); pos.setX(this.lightPositions.getInt(subIndex));
pos.y = this.lightPositions.getInt(subIndex + 1); pos.setY(this.lightPositions.getInt(subIndex + 1));
pos.z = this.lightPositions.getInt(subIndex + 2); pos.setZ(this.lightPositions.getInt(subIndex + 2));
pos.lightValue = this.lightPositions.getInt(subIndex + 3); pos.lightValue = this.lightPositions.getInt(subIndex + 3);
this.index--; this.index--;
@@ -21,11 +21,12 @@ package com.seibel.distanthorizons.core.generation;
import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult;
import com.seibel.distanthorizons.core.pos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.LodQuadTree; import com.seibel.distanthorizons.core.render.LodQuadTree;
import java.io.Closeable; import java.io.Closeable;
import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
/** /**
@@ -49,9 +50,17 @@ public interface IFullDataSourceRetrievalQueue extends Closeable
// getters // // getters //
//=========// //=========//
/** the largest numerical detail level */ /**
* The largest numerical detail level. <br>
* Detail level is absolute, not section;
* IE 0 = Block, 1 = 2x2 blocks, etc.
*/
byte lowestDataDetail(); byte lowestDataDetail();
/** the smallest numerical detail level */ /**
* The smallest numerical detail level. <br>
* Detail level is absolute, not section;
* IE 0 = Block, 1 = 2x2 blocks, etc.
*/
byte highestDataDetail(); byte highestDataDetail();
@@ -81,7 +90,7 @@ public interface IFullDataSourceRetrievalQueue extends Closeable
*/ */
void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf); void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf);
CompletableFuture<WorldGenResult> submitGenTask(long pos, byte requiredDataDetail, IWorldGenTaskTracker tracker); CompletableFuture<WorldGenResult> submitRetrievalTask(long pos, byte requiredDataDetail, IWorldGenTaskTracker tracker);
@@ -89,7 +98,10 @@ public interface IFullDataSourceRetrievalQueue extends Closeable
// shutdown // // shutdown //
//==========// //==========//
CompletableFuture<Void> startClosing(boolean cancelCurrentGeneration, boolean alsoInterruptRunning); /** Can be used to let any lingering generation requests finish before fully shutting down the system */
CompletableFuture<Void> startClosingAsync(boolean cancelCurrentGeneration, boolean alsoInterruptRunning);
@Override
void close(); void close();
@@ -105,5 +117,7 @@ public interface IFullDataSourceRetrievalQueue extends Closeable
int getEstimatedTotalTaskCount(); int getEstimatedTotalTaskCount();
void setEstimatedTotalTaskCount(int newEstimate); void setEstimatedTotalTaskCount(int newEstimate);
void addDebugMenuStringsToList(List<String> messageList);
} }
@@ -0,0 +1,84 @@
package com.seibel.distanthorizons.core.generation;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult;
import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.multiplayer.client.AbstractFullDataNetworkRequestQueue;
import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.util.LodUtil;
import org.apache.logging.log4j.Logger;
import java.util.concurrent.*;
public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQueue implements IFullDataSourceRetrievalQueue, IDebugRenderable
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private int estimatedTotalTaskCount;
//=============//
// constructor //
//=============//
public RemoteWorldRetrievalQueue(ClientNetworkState networkState, IDhClientLevel level)
{ super(networkState, level, false, Config.Client.Advanced.Debugging.DebugWireframe.showWorldGenQueue); }
//===========================//
// retrieval queue overrides //
//===========================//
@Override
public void startAndSetTargetPos(DhBlockPos2D targetPos) { super.tick(targetPos); }
@Override
public byte lowestDataDetail() { return LodUtil.BLOCK_DETAIL_LEVEL; }
@Override
public byte highestDataDetail() { return LodUtil.BLOCK_DETAIL_LEVEL; }
@Override
public CompletableFuture<WorldGenResult> submitRetrievalTask(long sectionPos, byte requiredDataDetail, IWorldGenTaskTracker tracker)
{
return super.submitRequest(sectionPos, tracker.getDataSourceConsumer())
.thenApply(retrievalSuccess -> retrievalSuccess
? WorldGenResult.CreateSuccess(sectionPos)
: WorldGenResult.CreateFail());
}
@Override
public CompletableFuture<Void> startClosingAsync(boolean cancelCurrentGeneration, boolean alsoInterruptRunning)
{ return super.startClosingAsync(alsoInterruptRunning); }
//=================================//
// network request queue overrides //
//=================================//
@Override
protected int getRequestRateLimit() { return this.networkState.sessionConfig.getGenerationRequestRateLimit(); }
@Override
protected String getQueueName() { return "World Remote Generation Queue"; }
//===============//
// debug display //
//===============//
@Override
public int getEstimatedTotalTaskCount() { return this.estimatedTotalTaskCount; }
@Override
public void setEstimatedTotalTaskCount(int newEstimate) { this.estimatedTotalTaskCount = newEstimate; }
}
@@ -27,7 +27,7 @@ import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSour
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.generation.tasks.*; import com.seibel.distanthorizons.core.generation.tasks.*;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.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.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
@@ -47,6 +47,7 @@ import org.apache.logging.log4j.Logger;
import java.awt.*; import java.awt.*;
import java.util.*; import java.util.*;
import java.util.List;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -141,7 +142,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
//=================// //=================//
@Override @Override
public CompletableFuture<WorldGenResult> submitGenTask(long pos, byte requiredDataDetail, IWorldGenTaskTracker tracker) public CompletableFuture<WorldGenResult> submitRetrievalTask(long pos, byte requiredDataDetail, IWorldGenTaskTracker tracker)
{ {
// the generator is shutting down, don't add new tasks // the generator is shutting down, don't add new tasks
if (this.generatorClosingFuture != null) if (this.generatorClosingFuture != null)
@@ -456,8 +457,8 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
case VANILLA_CHUNKS: case VANILLA_CHUNKS:
{ {
return this.generator.generateChunks( return this.generator.generateChunks(
chunkPosMin.x, chunkPosMin.getX(),
chunkPosMin.z, chunkPosMin.getZ(),
granularity, granularity,
targetDataDetail, targetDataDetail,
generatorMode, generatorMode,
@@ -467,7 +468,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
try try
{ {
IChunkWrapper chunk = WRAPPER_FACTORY.createChunkWrapper(generatedObjectArray); IChunkWrapper chunk = WRAPPER_FACTORY.createChunkWrapper(generatedObjectArray);
FullDataSourceV2 dataSource = LodDataBuilder.createGeneratedDataSource(chunk); FullDataSourceV2 dataSource = LodDataBuilder.createFromChunk(chunk);
LodUtil.assertTrue(dataSource != null); LodUtil.assertTrue(dataSource != null);
chunkDataConsumer.accept(dataSource); chunkDataConsumer.accept(dataSource);
} }
@@ -482,8 +483,8 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
case API_CHUNKS: case API_CHUNKS:
{ {
return this.generator.generateApiChunks( return this.generator.generateApiChunks(
chunkPosMin.x, chunkPosMin.getX(),
chunkPosMin.z, chunkPosMin.getZ(),
granularity, granularity,
targetDataDetail, targetDataDetail,
generatorMode, generatorMode,
@@ -522,8 +523,8 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
// getters / setters // // getters / setters //
//===================// //===================//
public int getWaitingTaskCount() { return this.waitingTasks.size(); } @Override public int getWaitingTaskCount() { return this.waitingTasks.size(); }
public int getInProgressTaskCount() { return this.inProgressGenTasksByLodPos.size(); } @Override public int getInProgressTaskCount() { return this.inProgressGenTasksByLodPos.size(); }
@Override @Override
public byte lowestDataDetail() { return this.lowestDataDetail; } public byte lowestDataDetail() { return this.lowestDataDetail; }
@@ -535,13 +536,15 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
@Override @Override
public void setEstimatedTotalTaskCount(int newEstimate) { this.estimatedTotalTaskCount = newEstimate; } public void setEstimatedTotalTaskCount(int newEstimate) { this.estimatedTotalTaskCount = newEstimate; }
@Override public void addDebugMenuStringsToList(List<String> messageList) { }
//==========// //==========//
// shutdown // // shutdown //
//==========// //==========//
public CompletableFuture<Void> startClosing(boolean cancelCurrentGeneration, boolean alsoInterruptRunning) @Override public CompletableFuture<Void> startClosingAsync(boolean cancelCurrentGeneration, boolean alsoInterruptRunning)
{ {
LOGGER.info("Closing world gen queue"); LOGGER.info("Closing world gen queue");
this.queueingThread.shutdownNow(); this.queueingThread.shutdownNow();
@@ -592,7 +595,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
if (this.generatorClosingFuture == null) if (this.generatorClosingFuture == null)
{ {
this.startClosing(true, true); this.startClosingAsync(true, true);
} }
LodUtil.assertTrue(this.generatorClosingFuture != null); LodUtil.assertTrue(this.generatorClosingFuture != null);
@@ -668,14 +671,18 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
{ {
index = 4 * X * X - X - Y; index = 4 * X * X - X - Y;
if (X < Y) if (X < Y)
{
index = index - 2 * (X - Y); index = index - 2 * (X - Y);
} }
}
else else
{ {
index = 4 * Y * Y - X - Y; index = 4 * Y * Y - X - Y;
if (X < Y) if (X < Y)
{
index = index + 2 * (X - Y); index = index + 2 * (X - Y);
} }
}
return index; return index;
} }
@@ -21,6 +21,7 @@ package com.seibel.distanthorizons.core.generation.tasks;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import javax.annotation.Nullable;
import java.util.function.Consumer; import java.util.function.Consumer;
/** /**
@@ -32,6 +33,7 @@ public interface IWorldGenTaskTracker
/** Returns true if the task hasn't been garbage collected. */ /** Returns true if the task hasn't been garbage collected. */
boolean isMemoryAddressValid(); boolean isMemoryAddressValid();
Consumer<FullDataSourceV2> getChunkDataConsumer(); @Nullable
Consumer<FullDataSourceV2> getDataSourceConsumer();
} }
@@ -51,7 +51,7 @@ public final class WorldGenTaskGroup
while (tasks.hasNext()) while (tasks.hasNext())
{ {
WorldGenTask task = tasks.next(); WorldGenTask task = tasks.next();
Consumer<FullDataSourceV2> chunkDataConsumer = task.taskTracker.getChunkDataConsumer(); Consumer<FullDataSourceV2> chunkDataConsumer = task.taskTracker.getDataSourceConsumer();
if (chunkDataConsumer == null) if (chunkDataConsumer == null)
{ {
tasks.remove(); tasks.remove();
@@ -103,10 +103,14 @@ public class DarkModeDetector
{ {
// System.out.println(de); // System.out.println(de);
if (de.contains("gnome-session")) // Gnome uses GTK if (de.contains("gnome-session")) // Gnome uses GTK
{
return GTKChecker(); return GTKChecker();
}
if (de.contains("plasma_session")) // KDE plasma uses QT if (de.contains("plasma_session")) // KDE plasma uses QT
{
return QTChecker(); return QTChecker();
} }
}
return GTKChecker(); // GTK works best with non plasma desktops (desktops includes window managers) return GTKChecker(); // GTK works best with non plasma desktops (desktops includes window managers)
} }
@@ -134,7 +138,9 @@ public class DarkModeDetector
while (themeLine != null) while (themeLine != null)
{ // Go through each line till you find "KWinPalette\activeBackground" { // Go through each line till you find "KWinPalette\activeBackground"
if (themeLine.contains("KWinPalette\\activeBackground")) if (themeLine.contains("KWinPalette\\activeBackground"))
{
break; break;
}
themeLine = reader.readLine(); themeLine = reader.readLine();
} }
reader.close(); reader.close();
@@ -145,10 +151,14 @@ public class DarkModeDetector
short g = (short) Integer.parseInt("" + themeLine.charAt(index + 3) + themeLine.charAt(index + 4), 16); short g = (short) Integer.parseInt("" + themeLine.charAt(index + 3) + themeLine.charAt(index + 4), 16);
short b = (short) Integer.parseInt("" + themeLine.charAt(index + 5) + themeLine.charAt(index + 6), 16); short b = (short) Integer.parseInt("" + themeLine.charAt(index + 5) + themeLine.charAt(index + 6), 16);
if ((r + g + b) / 2 >= 128) if ((r + g + b) / 2 >= 128)
{
return false; return false;
}
else else
{
return true; return true;
} }
}
catch (Exception e) catch (Exception e)
{ {
e.printStackTrace(); return false; e.printStackTrace(); return false;
@@ -170,7 +180,9 @@ public class DarkModeDetector
while ((actualReadLine = reader.readLine()) != null) while ((actualReadLine = reader.readLine()) != null)
{ {
if (stringBuilder.length() != 0) if (stringBuilder.length() != 0)
{
stringBuilder.append('\n'); stringBuilder.append('\n');
}
stringBuilder.append(actualReadLine); stringBuilder.append(actualReadLine);
} }
} }
@@ -22,6 +22,7 @@ package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiChunkModifiedEvent; import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiChunkModifiedEvent;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.fullDatafile.DelayedFullDataSourceSaveCache; import com.seibel.distanthorizons.core.file.fullDatafile.DelayedFullDataSourceSaveCache;
import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
@@ -32,6 +33,7 @@ import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.sql.dto.ChunkHashDTO; import com.seibel.distanthorizons.core.sql.dto.ChunkHashDTO;
import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo; import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo;
import com.seibel.distanthorizons.core.sql.repo.ChunkHashRepo; import com.seibel.distanthorizons.core.sql.repo.ChunkHashRepo;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -40,11 +42,9 @@ import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public abstract class AbstractDhLevel implements IDhLevel public abstract class AbstractDhLevel implements IDhLevel
{ {
@@ -57,7 +57,7 @@ public abstract class AbstractDhLevel implements IDhLevel
@Nullable @Nullable
public BeaconBeamRepo beaconBeamRepo; public BeaconBeamRepo beaconBeamRepo;
protected final DelayedFullDataSourceSaveCache delayedFullDataSourceSaveCache = new DelayedFullDataSourceSaveCache(this::onDataSourceSave, 2_000); protected final DelayedFullDataSourceSaveCache delayedFullDataSourceSaveCache = new DelayedFullDataSourceSaveCache(this::onDataSourceSave, 500);
/** contains the {@link DhChunkPos} for each {@link DhSectionPos} that are queued to save via {@link AbstractDhLevel#delayedFullDataSourceSaveCache} */ /** contains the {@link DhChunkPos} for each {@link DhSectionPos} that are queued to save via {@link AbstractDhLevel#delayedFullDataSourceSaveCache} */
protected final ConcurrentHashMap<Long, HashSet<DhChunkPos>> updatedChunkPosSetBySectionPos = new ConcurrentHashMap<>(); protected final ConcurrentHashMap<Long, HashSet<DhChunkPos>> updatedChunkPosSetBySectionPos = new ConcurrentHashMap<>();
protected final ConcurrentHashMap<DhChunkPos, Integer> updatedChunkHashesByChunkPos = new ConcurrentHashMap<>(); protected final ConcurrentHashMap<DhChunkPos, Integer> updatedChunkHashesByChunkPos = new ConcurrentHashMap<>();
@@ -112,12 +112,16 @@ public abstract class AbstractDhLevel implements IDhLevel
{ {
GenericObjectRenderer genericRenderer = this.getGenericRenderer(); GenericObjectRenderer genericRenderer = this.getGenericRenderer();
if (genericRenderer != null) if (genericRenderer != null)
{
// only client levels can render clouds
if (this instanceof IDhClientLevel)
{ {
// only add clouds for certain dimension types // only add clouds for certain dimension types
if (!this.getLevelWrapper().hasCeiling() if (!this.getLevelWrapper().hasCeiling()
&& !this.getLevelWrapper().getDimensionType().isTheEnd()) && !this.getLevelWrapper().getDimensionType().isTheEnd())
{ {
this.cloudRenderHandler = new CloudRenderHandler(this, genericRenderer); this.cloudRenderHandler = new CloudRenderHandler((IDhClientLevel)this, genericRenderer);
}
} }
@@ -166,6 +170,12 @@ public abstract class AbstractDhLevel implements IDhLevel
private void onDataSourceSave(FullDataSourceV2 fullDataSource) private void onDataSourceSave(FullDataSourceV2 fullDataSource)
{ {
// block lights should have been populated at the chunkWrapper stage
// waiting to populate the data source's skylight at this stage prevents re-lighting and
// allows us to reduce cross-chunk lighting issues by lighting the whole 4x4 LOD at once
DhLightingEngine.INSTANCE.bakeDataSourceSkyLight(fullDataSource, this.hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT);
this.updateDataSourcesAsync(fullDataSource).thenRun(() -> this.updateDataSourcesAsync(fullDataSource).thenRun(() ->
{ {
HashSet<DhChunkPos> updatedChunkPosSet = this.updatedChunkPosSetBySectionPos.remove(fullDataSource.getPos()); HashSet<DhChunkPos> updatedChunkPosSet = this.updatedChunkPosSetBySectionPos.remove(fullDataSource.getPos());
@@ -182,7 +192,7 @@ public abstract class AbstractDhLevel implements IDhLevel
ApiEventInjector.INSTANCE.fireAllEvents( ApiEventInjector.INSTANCE.fireAllEvents(
DhApiChunkModifiedEvent.class, DhApiChunkModifiedEvent.class,
new DhApiChunkModifiedEvent.EventParam(this.getLevelWrapper(), chunkPos.x, chunkPos.z)); new DhApiChunkModifiedEvent.EventParam(this.getLevelWrapper(), chunkPos.getX(), chunkPos.getZ()));
} }
} }
}); });
@@ -215,11 +225,12 @@ public abstract class AbstractDhLevel implements IDhLevel
//=================// //=================//
@Override @Override
public void setBeaconBeamsForChunk(DhChunkPos chunkPos, List<BeaconBeamDTO> newBeamList) public void updateBeaconBeamsForChunk(IChunkWrapper chunkToUpdate, ArrayList<IChunkWrapper> nearbyChunkList)
{ {
if (this.beaconRenderHandler != null) if (this.beaconRenderHandler != null)
{ {
this.beaconRenderHandler.setBeaconBeamsForChunk(chunkPos, newBeamList); List<BeaconBeamDTO> activeBeamList = chunkToUpdate.getAllActiveBeacons(nearbyChunkList);
this.beaconRenderHandler.setBeaconBeamsForChunk(chunkToUpdate.getChunkPos(), activeBeamList);
} }
} }
@@ -27,7 +27,7 @@ import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.file.AbstractDataSourceHandler; import com.seibel.distanthorizons.core.file.AbstractDataSourceHandler;
import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.render.LodQuadTree; import com.seibel.distanthorizons.core.render.LodQuadTree;
import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.RenderBufferHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
@@ -126,9 +126,9 @@ public class ClientLevelModule implements Closeable, AbstractDataSourceHandler.I
boolean isBuffersDirty = false; boolean isBuffersDirty = false;
EDhApiDebugRendering newDebugRendering = Config.Client.Advanced.Debugging.debugRendering.get(); EDhApiDebugRendering newDebugRendering = Config.Client.Advanced.Debugging.debugRendering.get();
if (newDebugRendering != lastDebugRendering) if (newDebugRendering != this.lastDebugRendering)
{ {
lastDebugRendering = newDebugRendering; this.lastDebugRendering = newDebugRendering;
isBuffersDirty = true; isBuffersDirty = true;
} }
if (isBuffersDirty) if (isBuffersDirty)
@@ -225,7 +225,7 @@ public class ClientLevelModule implements Closeable, AbstractDataSourceHandler.I
} }
} }
public void close() @Override public void close()
{ {
// shutdown the renderer // shutdown the renderer
ClientRenderState ClientRenderState = this.ClientRenderStateRef.get(); ClientRenderState ClientRenderState = this.ClientRenderStateRef.get();
@@ -20,15 +20,27 @@
package com.seibel.distanthorizons.core.level; package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.core.config.AppliedConfigState;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.file.fullDatafile.RemoteFullDataSourceProvider; import com.seibel.distanthorizons.core.file.fullDatafile.RemoteFullDataSourceProvider;
import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure;
import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhBlockPos; import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState;
import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoginRequestQueue;
import com.seibel.distanthorizons.core.network.event.ScopedNetworkEventSource;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.RenderBufferHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
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.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;
@@ -36,6 +48,8 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import javax.annotation.CheckForNull;
import java.awt.*;
import java.io.File; import java.io.File;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -44,25 +58,58 @@ import java.util.concurrent.CompletableFuture;
public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
public final ClientLevelModule clientside; public final ClientLevelModule clientside;
public final IClientLevelWrapper levelWrapper; public final IClientLevelWrapper levelWrapper;
public final AbstractSaveStructure saveStructure; public final AbstractSaveStructure saveStructure;
public final RemoteFullDataSourceProvider dataFileHandler; public final RemoteFullDataSourceProvider dataFileHandler;
@CheckForNull
private final ClientNetworkState networkState;
@Nullable
private final ScopedNetworkEventSource networkEventSource;
public final WorldGenModule worldGenModule;
public final AppliedConfigState<Boolean> worldGeneratorEnabledConfig;
@Nullable
private final SyncOnLoginRequestQueue syncOnLoginRequestQueue;
//=============// //=============//
// constructor // // constructor //
//=============// //=============//
public DhClientLevel(AbstractSaveStructure saveStructure, IClientLevelWrapper clientLevelWrapper) { this(saveStructure, clientLevelWrapper, null, true); } public DhClientLevel(AbstractSaveStructure saveStructure, IClientLevelWrapper clientLevelWrapper, @Nullable ClientNetworkState networkState) { this(saveStructure, clientLevelWrapper, null, true, networkState); }
public DhClientLevel(AbstractSaveStructure saveStructure, IClientLevelWrapper clientLevelWrapper, @Nullable File fullDataSaveDirOverride, boolean enableRendering) public DhClientLevel(AbstractSaveStructure saveStructure, IClientLevelWrapper clientLevelWrapper, @Nullable File fullDataSaveDirOverride, boolean enableRendering, @Nullable ClientNetworkState networkState)
{ {
if (saveStructure.getFullDataFolder(clientLevelWrapper).mkdirs())
{
LOGGER.warn("unable to create data folder.");
}
this.levelWrapper = clientLevelWrapper; this.levelWrapper = clientLevelWrapper;
this.levelWrapper.setParentLevel(this); this.levelWrapper.setParentLevel(this);
this.saveStructure = saveStructure; this.saveStructure = saveStructure;
this.dataFileHandler = new RemoteFullDataSourceProvider(this, saveStructure, fullDataSaveDirOverride);
this.networkState = networkState;
if (this.networkState != null)
{
this.networkEventSource = new ScopedNetworkEventSource(this.networkState.getSession());
this.syncOnLoginRequestQueue = new SyncOnLoginRequestQueue(this, this.networkState);
this.registerNetworkHandlers();
}
else
{
this.networkEventSource = null;
this.syncOnLoginRequestQueue = null;
}
this.dataFileHandler = new RemoteFullDataSourceProvider(this, saveStructure, fullDataSaveDirOverride, this.syncOnLoginRequestQueue);
this.worldGeneratorEnabledConfig = new AppliedConfigState<>(Config.Client.Advanced.WorldGenerator.enableDistantGeneration);
this.worldGenModule = new WorldGenModule(this);
this.clientside = new ClientLevelModule(this); this.clientside = new ClientLevelModule(this);
this.createAndSetSupportingRepos(this.dataFileHandler.repo.databaseFile); this.createAndSetSupportingRepos(this.dataFileHandler.repo.databaseFile);
@@ -74,6 +121,30 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
LOGGER.info("Started DHLevel for " + this.levelWrapper + " with saves at " + this.saveStructure); LOGGER.info("Started DHLevel for " + this.levelWrapper + " with saves at " + this.saveStructure);
} }
} }
private void registerNetworkHandlers()
{
assert this.networkEventSource != null;
assert this.networkState != null;
this.networkEventSource.registerHandler(FullDataPartialUpdateMessage.class, message ->
{
try
{
FullDataSourceV2DTO dataSourceDto = this.networkState.decodeDataSourceAndReleaseBuffer(message.payload);
if (!message.isSameLevelAs(this.levelWrapper))
{
return;
}
this.updateDataSourcesAsync(dataSourceDto.createPooledDataSource(this.levelWrapper));
}
catch (Exception e)
{
LOGGER.error("Error while updating full data source", e);
}
});
}
@@ -87,6 +158,11 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
try try
{ {
this.clientside.clientTick(); this.clientside.clientTick();
if (this.syncOnLoginRequestQueue != null)
{
this.syncOnLoginRequestQueue.tick(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos()));
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -94,6 +170,53 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
} }
} }
@Override
public void worldGenTick()
{
ClientNetworkState networkState = this.networkState;
boolean isClientUsable = false, isAllowedDimension = false;
if (networkState != null)
{
isClientUsable = networkState.isReady();
isAllowedDimension = MC_CLIENT.getWrappedClientLevel() == this.levelWrapper;
}
boolean shouldDoWorldGen = isClientUsable
&& networkState.sessionConfig.isDistantGenerationEnabled()
&& isAllowedDimension
&& this.clientside.isRendering();
boolean isWorldGenRunning = this.worldGenModule.isWorldGenRunning();
if (shouldDoWorldGen && !isWorldGenRunning)
{
// start world gen
this.worldGenModule.startWorldGen(this.dataFileHandler, new WorldGenState(this, networkState));
// populate the queue based on the current rendering tree
ClientLevelModule.ClientRenderState renderState = this.clientside.ClientRenderStateRef.get();
renderState.quadtree.leafNodeIterator().forEachRemaining(node -> {
this.dataFileHandler.getAsync(node.sectionPos);
});
}
else if (!shouldDoWorldGen && isWorldGenRunning)
{
// stop world gen
this.worldGenModule.stopWorldGen(this.dataFileHandler);
}
if (this.worldGenModule.isWorldGenRunning())
{
this.worldGenModule.worldGenTick(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos()));
}
}
//===========//
// rendering //
//===========//
@Override @Override
public void render(DhApiRenderParam renderEventParam, IProfilerWrapper profiler) public void render(DhApiRenderParam renderEventParam, IProfilerWrapper profiler)
{ this.clientside.render(renderEventParam, profiler); } { this.clientside.render(renderEventParam, profiler); }
@@ -104,9 +227,28 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
//================// //===========//
// level handling // // world gen //
//================// //===========//
@Override
public void onWorldGenTaskComplete(long pos)
{
DebugRenderer.makeParticle(
new DebugRenderer.BoxParticle(
new DebugRenderer.Box(pos, 128f, 156f, 0.09f, Color.red.darker()),
0.2, 32f
)
);
this.clientside.reloadPos(pos);
}
//=========//
// getters //
//=========//
@Override @Override
public int computeBaseColor(DhBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper block) { return this.levelWrapper.getBlockColor(pos, biome, block); } public int computeBaseColor(DhBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper block) { return this.levelWrapper.getBlockColor(pos, biome, block); }
@@ -126,10 +268,34 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
@Override @Override
public int getMinY() { return this.levelWrapper.getMinHeight(); } public int getMinY() { return this.levelWrapper.getMinHeight(); }
@Override
public FullDataSourceProviderV2 getFullDataProvider() { return this.dataFileHandler; }
@Override
public AbstractSaveStructure getSaveStructure() { return this.saveStructure; }
@Override
public boolean hasSkyLight() { return this.levelWrapper.hasSkyLight(); }
@Override
public GenericObjectRenderer getGenericRenderer() { return this.clientside.genericRenderer; }
@Override
public RenderBufferHandler getRenderBufferHandler()
{
ClientLevelModule.ClientRenderState renderState = this.clientside.ClientRenderStateRef.get();
return (renderState != null) ? renderState.renderBufferHandler : null;
}
//===========//
// debugging //
//===========//
@Override @Override
public void addDebugMenuStringsToList(List<String> messageList) public void addDebugMenuStringsToList(List<String> messageList)
{ {
String dimName = this.levelWrapper.getDimensionType().getDimensionName(); String dimName = this.levelWrapper.getDimensionName();
boolean rendering = this.clientside.isRendering(); boolean rendering = this.clientside.isRendering();
messageList.add("["+dimName+"] rendering: "+(rendering ? "yes" : "no")); messageList.add("["+dimName+"] rendering: "+(rendering ? "yes" : "no"));
@@ -152,11 +318,39 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
{ {
messageList.add(" Migration Failed"); messageList.add(" Migration Failed");
} }
// world gen
this.worldGenModule.addDebugMenuStringsToList(messageList);
if (this.syncOnLoginRequestQueue != null)
{
assert this.networkState != null;
if (this.networkState.sessionConfig.getSynchronizeOnLogin())
{
this.syncOnLoginRequestQueue.addDebugMenuStringsToList(messageList);
} }
}
}
//==========//
// shutdown //
//==========//
@Override @Override
public void close() public void close()
{ {
if (this.worldGenModule != null)
{
this.worldGenModule.close();
}
if (this.networkEventSource != null)
{
this.networkEventSource.close();
}
this.levelWrapper.setParentLevel(null); this.levelWrapper.setParentLevel(null);
this.clientside.close(); this.clientside.close();
super.close(); super.close();
@@ -164,23 +358,18 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
LOGGER.info("Closed [" + DhClientLevel.class.getSimpleName() + "] for [" + this.levelWrapper + "]"); LOGGER.info("Closed [" + DhClientLevel.class.getSimpleName() + "] for [" + this.levelWrapper + "]");
} }
@Override
public FullDataSourceProviderV2 getFullDataProvider() { return this.dataFileHandler; }
@Override
public AbstractSaveStructure getSaveStructure() { return this.saveStructure; }
@Override
public boolean hasSkyLight() { return this.levelWrapper.hasSkyLight(); }
@Override //================//
public GenericObjectRenderer getGenericRenderer() { return this.clientside.genericRenderer; } // helper classes //
@Override //================//
public RenderBufferHandler getRenderBufferHandler()
private static class WorldGenState extends WorldGenModule.AbstractWorldGenState
{ {
ClientLevelModule.ClientRenderState renderState = this.clientside.ClientRenderStateRef.get(); WorldGenState(IDhClientLevel level, ClientNetworkState networkState)
return (renderState != null) ? renderState.renderBufferHandler : null; {
this.worldGenerationQueue = new RemoteWorldRetrievalQueue(networkState, level);
}
} }
} }
@@ -28,8 +28,8 @@ import com.seibel.distanthorizons.core.render.RenderBufferHandler;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhBlockPos; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper;
@@ -98,7 +98,7 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev
public void serverTick() { } public void serverTick() { }
@Override @Override
public void doWorldGen() public void worldGenTick()
{ {
this.serverside.worldGeneratorEnabledConfig.pollNewValue(); // if not called the get() line below may not this.serverside.worldGeneratorEnabledConfig.pollNewValue(); // if not called the get() line below may not
boolean shouldDoWorldGen = this.serverside.worldGeneratorEnabledConfig.get() && this.clientside.isRendering(); boolean shouldDoWorldGen = this.serverside.worldGeneratorEnabledConfig.get() && this.clientside.isRendering();
@@ -157,9 +157,9 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev
public void clearRenderCache() { this.clientside.clearRenderCache(); } public void clearRenderCache() { this.clientside.clearRenderCache(); }
@Override @Override
public IServerLevelWrapper getServerLevelWrapper() { return serverLevelWrapper; } public IServerLevelWrapper getServerLevelWrapper() { return this.serverLevelWrapper; }
@Override @Override
public ILevelWrapper getLevelWrapper() { return getServerLevelWrapper(); } public ILevelWrapper getLevelWrapper() { return this.getServerLevelWrapper(); }
@Override @Override
public FullDataSourceProviderV2 getFullDataProvider() { return this.serverside.fullDataFileHandler; } public FullDataSourceProviderV2 getFullDataProvider() { return this.serverside.fullDataFileHandler; }
@@ -167,7 +167,7 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev
@Override @Override
public AbstractSaveStructure getSaveStructure() public AbstractSaveStructure getSaveStructure()
{ {
return serverside.saveStructure; return this.serverside.saveStructure;
} }
@Override @Override
@@ -189,7 +189,7 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev
public void addDebugMenuStringsToList(List<String> messageList) public void addDebugMenuStringsToList(List<String> messageList)
{ {
// header // header
String dimName = this.serverLevelWrapper.getDimensionType().getDimensionName(); String dimName = this.serverLevelWrapper.getDimensionName();
boolean rendering = this.clientside.isRendering(); boolean rendering = this.clientside.isRendering();
messageList.add("["+dimName+"] rendering: "+(rendering ? "yes" : "no")); messageList.add("["+dimName+"] rendering: "+(rendering ? "yes" : "no"));
@@ -216,12 +216,7 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev
// world gen // world gen
WorldGenModule worldGenState = this.serverside.worldGenModule; this.serverside.worldGenModule.addDebugMenuStringsToList(messageList);
String worldGenDisplayString = worldGenState.getDebugMenuString();
if (worldGenDisplayString != null)
{
messageList.add(worldGenDisplayString);
}
} }
@@ -19,33 +19,73 @@
package com.seibel.distanthorizons.core.level; package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure;
import com.seibel.distanthorizons.core.pos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.multiplayer.server.RemotePlayerConnectionHandler;
import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerState;
import com.seibel.distanthorizons.core.network.exceptions.InvalidLevelException;
import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException;
import com.seibel.distanthorizons.core.network.messages.ILevelRelatedMessage;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPayload;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceResponseMessage;
import com.seibel.distanthorizons.core.network.messages.requests.CancelMessage;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.RenderBufferHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.math.Vec3d;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import javax.annotation.CheckForNull;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.Map;
import java.util.concurrent.*;
public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final ConfigBasedLogger NETWORK_LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Logging.logNetworkEvent.get());
/** 1 Mebibyte minus 576 bytes for other info */
public static final int FULL_DATA_SPLIT_SIZE_IN_BYTES = 1_048_000;
public final ServerLevelModule serverside; public final ServerLevelModule serverside;
private final IServerLevelWrapper serverLevelWrapper; private final IServerLevelWrapper serverLevelWrapper;
private final RemotePlayerConnectionHandler remotePlayerConnectionHandler;
/**
* This queue is used for ensuring fair generation speed for each player. <br>
* Every tick the first player gets used for centering generation, and then is immediately moved into the back of the queue. <br>
* TODO only add players that actually have something to generate
*/
private final ConcurrentLinkedQueue<IServerPlayerWrapper> worldGenPlayerCenteringQueue = new ConcurrentLinkedQueue<>();
private final ConcurrentMap<Long, DataSourceRequestGroup> requestGroupByPos = new ConcurrentHashMap<>();
private final ConcurrentMap<Long, DataSourceRequestGroup> requestGroupByFutureId = new ConcurrentHashMap<>();
//=============// //=============//
// constructor // // constructor //
//=============// //=============//
public DhServerLevel(AbstractSaveStructure saveStructure, IServerLevelWrapper serverLevelWrapper) public DhServerLevel(AbstractSaveStructure saveStructure, IServerLevelWrapper serverLevelWrapper, RemotePlayerConnectionHandler remotePlayerConnectionHandler)
{ {
if (saveStructure.getFullDataFolder(serverLevelWrapper).mkdirs()) if (saveStructure.getFullDataFolder(serverLevelWrapper).mkdirs())
{ {
@@ -56,77 +96,393 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
this.createAndSetSupportingRepos(this.serverside.fullDataFileHandler.repo.databaseFile); this.createAndSetSupportingRepos(this.serverside.fullDataFileHandler.repo.databaseFile);
this.runRepoReliantSetup(); this.runRepoReliantSetup();
LOGGER.info("Started DHLevel for {} with saves at {}", serverLevelWrapper, saveStructure); LOGGER.info("Started DHLevel for ["+serverLevelWrapper+"] at ["+saveStructure+"].");
this.remotePlayerConnectionHandler = remotePlayerConnectionHandler;
} }
//=========// //=======//
// methods // // ticks //
//=========// //=======//
public void serverTick() { }
@Override @Override
public CompletableFuture<Void> updateDataSourcesAsync(FullDataSourceV2 data) { return this.getFullDataProvider().updateDataSourceAsync(data); } public void serverTick()
@Override
public int getMinY() { return getLevelWrapper().getMinHeight(); }
@Override
public void close()
{ {
super.close(); // Send finished data source requests
serverside.close(); for (Map.Entry<Long, DataSourceRequestGroup> entry : this.requestGroupByPos.entrySet())
LOGGER.info("Closed DHLevel for {}", getLevelWrapper()); {
DataSourceRequestGroup requestGroup = entry.getValue();
if (requestGroup.fullDataSource == null)
{
continue;
}
NETWORK_LOGGER.debug("["+this.serverLevelWrapper.getDimensionName()+"] Fulfilled request group ["+entry.getKey()+"]");
// Make this group unavailable for adding into
this.requestGroupByPos.remove(entry.getKey());
requestGroup.requestRemoveSemaphore.acquireUninterruptibly(Short.MAX_VALUE);
requestGroup.requestAddSemaphore.acquireUninterruptibly(Short.MAX_VALUE);
ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor();
if (executor == null)
{
LOGGER.warn("Unable to send FullDataSourceResponseMessage - getNetworkCompressionExecutor() is null");
continue;
}
CompletableFuture.runAsync(() ->
{
FullDataPayload payload = new FullDataPayload(requestGroup.fullDataSource);
for (FullDataSourceRequestMessage msg : requestGroup.requestMessages.values())
{
this.requestGroupByFutureId.remove(msg.futureId);
ServerPlayerState serverPlayerState = this.remotePlayerConnectionHandler.getConnectedPlayer(msg.serverPlayer());
if (serverPlayerState == null)
{
continue;
}
serverPlayerState.getRateLimiterSet(this).generationRequestRateLimiter.release();
payload.splitAndSend(FULL_DATA_SPLIT_SIZE_IN_BYTES, msg.getSession()::sendMessage);
msg.sendResponse(new FullDataSourceResponseMessage(payload));
}
}, executor);
}
} }
@Override @Override
public void doWorldGen() public void worldGenTick()
{ {
boolean shouldDoWorldGen = true; //todo; boolean shouldDoWorldGen = true; //todo;
boolean isWorldGenRunning = serverside.worldGenModule.isWorldGenRunning(); boolean isWorldGenRunning = this.serverside.worldGenModule.isWorldGenRunning();
if (shouldDoWorldGen && !isWorldGenRunning) if (shouldDoWorldGen && !isWorldGenRunning)
{ {
// start world gen // start world gen
serverside.worldGenModule.startWorldGen(serverside.fullDataFileHandler, new ServerLevelModule.WorldGenState(this)); this.serverside.worldGenModule.startWorldGen(this.serverside.fullDataFileHandler, new ServerLevelModule.WorldGenState(this));
} }
else if (!shouldDoWorldGen && isWorldGenRunning) else if (!shouldDoWorldGen && isWorldGenRunning)
{ {
// stop world gen // stop world gen
serverside.worldGenModule.stopWorldGen(serverside.fullDataFileHandler); this.serverside.worldGenModule.stopWorldGen(this.serverside.fullDataFileHandler);
} }
if (serverside.worldGenModule.isWorldGenRunning()) if (this.serverside.worldGenModule.isWorldGenRunning())
{ {
serverside.worldGenModule.worldGenTick(new DhBlockPos2D(0, 0)); // todo; IServerPlayerWrapper firstPlayer = this.worldGenPlayerCenteringQueue.peek();
if (firstPlayer == null)
{
return;
}
// Put first player in back before removing from front, so it can be removed by other thread without blocking
// - if it gets removed, remove() below will remove the item we just put instead
this.worldGenPlayerCenteringQueue.add(firstPlayer);
this.worldGenPlayerCenteringQueue.remove(firstPlayer);
Vec3d position = firstPlayer.getPosition();
this.serverside.worldGenModule.worldGenTick(new DhBlockPos2D((int) position.x, (int) position.z));
} }
} }
@Override
public IServerLevelWrapper getServerLevelWrapper() { return serverLevelWrapper; }
//==================//
// network handling //
//==================//
public void registerNetworkHandlers(ServerPlayerState serverPlayerState)
{
serverPlayerState.networkSession.registerHandler(FullDataSourceRequestMessage.class, (message) ->
{
if (!this.messagePlayerInThisLevel(message))
{
// we can't handle players in other levels, don't continue
return;
}
ServerPlayerState.RateLimiterSet rateLimiterSet = serverPlayerState.getRateLimiterSet(this);
if (message.clientTimestamp == null)
{
this.queueWorldGenForRequestMessage(serverPlayerState, message, rateLimiterSet);
}
else
{
this.queueLodSyncForRequestMessage(serverPlayerState, message, rateLimiterSet);
}
});
serverPlayerState.networkSession.registerHandler(CancelMessage.class, msg ->
{
DataSourceRequestGroup requestGroup = this.requestGroupByFutureId.remove(msg.futureId);
if (requestGroup == null)
{
return;
}
// If this fails, the group is being removed and completing cancellation is not necessary
if (requestGroup.requestRemoveSemaphore.tryAcquire())
{
// Prevent adding requests in case the group will be removed by this cancellation
requestGroup.requestAddSemaphore.acquireUninterruptibly(Short.MAX_VALUE);
requestGroup.requestRemoveSemaphore.release();
serverPlayerState.getRateLimiterSet(this).generationRequestRateLimiter.release();
FullDataSourceRequestMessage requestMessage = requestGroup.requestMessages.remove(msg.futureId);
if (requestGroup.requestMessages.isEmpty())
{
NETWORK_LOGGER.debug("["+this.serverLevelWrapper.getDimensionName()+"] Cancelled request group ["+DhSectionPos.toString(requestMessage.sectionPos)+"].");
this.requestGroupByPos.remove(requestMessage.sectionPos);
this.serverside.fullDataFileHandler.removeRetrievalRequestIf(pos -> pos == requestMessage.sectionPos);
}
else
{
requestGroup.requestAddSemaphore.release(Short.MAX_VALUE);
}
}
});
}
private void queueLodSyncForRequestMessage(ServerPlayerState serverPlayerState, FullDataSourceRequestMessage message, ServerPlayerState.RateLimiterSet rateLimiterSet)
{
if (!serverPlayerState.sessionConfig.getSynchronizeOnLogin())
{
message.sendResponse(new RequestRejectedException("Operation is disabled in config."));
return;
}
if (!rateLimiterSet.syncOnLoginRateLimiter.tryAcquire(message))
{
return;
}
// the client timestamp will be null if we want to retrieve the LOD regardless of when it was last updated
long clientTimestamp = (message.clientTimestamp != null) ? message.clientTimestamp : -1;
// the server timestamp will be null if no LOD data exists for this position
Long serverTimestamp = this.serverside.fullDataFileHandler.getTimestampForPos(message.sectionPos);
if (serverTimestamp == null
|| serverTimestamp <= clientTimestamp)
{
// either no data exists to sync, or the client is already up to date
rateLimiterSet.syncOnLoginRateLimiter.release();
message.sendResponse(new FullDataSourceResponseMessage(null));
return;
}
ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor();
if (executor == null)
{
// shouldn't normally happen, but just in case
LOGGER.warn("Unable to send FullDataSourceResponseMessage - getNetworkCompressionExecutor() is null");
return;
}
this.serverside.fullDataFileHandler.getAsync(message.sectionPos).thenAcceptAsync(fullDataSource ->
{
rateLimiterSet.syncOnLoginRateLimiter.release();
FullDataPayload payload = new FullDataPayload(fullDataSource);
payload.splitAndSend(FULL_DATA_SPLIT_SIZE_IN_BYTES, message.getSession()::sendMessage);
message.sendResponse(new FullDataSourceResponseMessage(payload));
}, executor);
}
private void queueWorldGenForRequestMessage(ServerPlayerState serverPlayerState, FullDataSourceRequestMessage message, ServerPlayerState.RateLimiterSet rateLimiterSet)
{
if (!serverPlayerState.sessionConfig.isDistantGenerationEnabled())
{
message.sendResponse(new RequestRejectedException("Operation is disabled in config."));
return;
}
if (!rateLimiterSet.generationRequestRateLimiter.tryAcquire(message))
{
return;
}
while (true)
{
DataSourceRequestGroup requestGroup = this.requestGroupByPos.computeIfAbsent(message.sectionPos, pos ->
{
DataSourceRequestGroup newGroup = new DataSourceRequestGroup();
this.tryFulfillDataSourceRequestGroup(newGroup, pos);
NETWORK_LOGGER.debug("["+this.serverLevelWrapper.getDimensionName()+"] Created request group for pos ["+DhSectionPos.toString(pos)+"].");
return newGroup;
});
// If this fails, loop until either a permit is acquired or the group is removed to create another one
if (!requestGroup.requestAddSemaphore.tryAcquire())
{
Thread.yield();
continue;
}
this.requestGroupByFutureId.put(message.futureId, requestGroup);
requestGroup.requestMessages.put(message.futureId, message);
requestGroup.requestAddSemaphore.release();
break;
}
}
/** May send an error message in response if the message is a {@link AbstractTrackableMessage} */
private <T extends AbstractNetworkMessage> boolean messagePlayerInThisLevel(T message)
{
if (!(message instanceof ILevelRelatedMessage))
{
LodUtil.assertNotReach("Received message ["+ILevelRelatedMessage.class.getSimpleName()+"] does not implement ["+message.getClass().getSimpleName()+"]");
}
// Only handle requests for this level
if (!((ILevelRelatedMessage) message).isSameLevelAs(this.getServerLevelWrapper()))
{
return false;
}
LodUtil.assertTrue(message.getSession().serverPlayer != null);
// Check if the player is in this dimension,
// since handling multiple dimensions isn't allowed
if (message.getSession().serverPlayer.getLevel() != this.getLevelWrapper())
{
// If the message can be replied to - reply with an error, otherwise just ignore
if (message instanceof AbstractTrackableMessage)
{
((AbstractTrackableMessage) message).sendResponse(
new InvalidLevelException(
"Generation not allowed. " +
"Requested dimension: ["+((ILevelRelatedMessage) message).getLevelName()+"], " +
"player dimension: ["+message.getSession().serverPlayer.getLevel().getDimensionName()+"], " +
"handler dimension: ["+this.getLevelWrapper().getDimensionName()+"]"
)
);
}
return false;
}
return true;
}
//===========//
// world gen //
//===========//
@Override @Override
public ILevelWrapper getLevelWrapper() { return getServerLevelWrapper(); } public void onWorldGenTaskComplete(long pos)
{
DataSourceRequestGroup requestGroup = this.requestGroupByPos.get(pos);
if (requestGroup != null)
{
this.tryFulfillDataSourceRequestGroup(requestGroup, pos);
}
}
private void tryFulfillDataSourceRequestGroup(DataSourceRequestGroup requestGroup, long pos)
{
this.serverside.fullDataFileHandler.getAsync(pos).thenAccept(fullDataSource ->
{
if (this.serverside.fullDataFileHandler.isFullyGenerated(fullDataSource.columnGenerationSteps))
{
requestGroup.fullDataSource = fullDataSource;
}
else
{
this.serverside.fullDataFileHandler.queuePositionForRetrieval(pos);
}
});
}
//=================//
// player handling //
//=================//
public void addPlayer(IServerPlayerWrapper serverPlayer) { this.worldGenPlayerCenteringQueue.add(serverPlayer); }
public void removePlayer(IServerPlayerWrapper serverPlayer) { this.worldGenPlayerCenteringQueue.remove(serverPlayer); }
@Override
public CompletableFuture<Void> updateDataSourcesAsync(FullDataSourceV2 data)
{
if (!Config.Client.Advanced.Multiplayer.ServerNetworking.enableRealTimeUpdates.get())
{
return this.getFullDataProvider().updateDataSourceAsync(data);
}
ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor();
if (executor == null)
{
LOGGER.warn("Unable to send FullDataPartialUpdateMessage - getNetworkCompressionExecutor() is null");
return this.getFullDataProvider().updateDataSourceAsync(data);
}
CompletableFuture.runAsync(() ->
{
FullDataPayload payload = new FullDataPayload(data);
for (ServerPlayerState serverPlayerState : this.remotePlayerConnectionHandler.getConnectedPlayers())
{
if (serverPlayerState.getServerPlayer().getLevel() != this.serverLevelWrapper)
{
continue;
}
if (!serverPlayerState.sessionConfig.isRealTimeUpdatesEnabled())
{
continue;
}
Vec3d playerPosition = serverPlayerState.getServerPlayer().getPosition();
int distanceFromPlayer = DhSectionPos.getManhattanBlockDistance(data.getPos(), new DhBlockPos2D((int) playerPosition.x, (int) playerPosition.z)) / 16;
if (distanceFromPlayer >= serverPlayerState.getServerPlayer().getViewDistance()
&& distanceFromPlayer <= serverPlayerState.sessionConfig.getRenderDistanceRadius())
{
payload.splitAndSend(FULL_DATA_SPLIT_SIZE_IN_BYTES, serverPlayerState.networkSession::sendMessage);
serverPlayerState.networkSession.sendMessage(new FullDataPartialUpdateMessage(this.serverLevelWrapper, payload));
}
}
}, executor);
return this.getFullDataProvider().updateDataSourceAsync(data);
}
//=========//
// getters //
//=========//
@Override
public int getMinY() { return this.getLevelWrapper().getMinHeight(); }
@Override
public IServerLevelWrapper getServerLevelWrapper() { return this.serverLevelWrapper; }
@Override
public ILevelWrapper getLevelWrapper() { return this.getServerLevelWrapper(); }
@Override @Override
public FullDataSourceProviderV2 getFullDataProvider() { return this.serverside.fullDataFileHandler; } public FullDataSourceProviderV2 getFullDataProvider() { return this.serverside.fullDataFileHandler; }
@Override @Override
public AbstractSaveStructure getSaveStructure() public AbstractSaveStructure getSaveStructure() { return this.serverside.saveStructure; }
{
return serverside.saveStructure;
}
@Override @Override
public boolean hasSkyLight() { return this.serverLevelWrapper.hasSkyLight(); } public boolean hasSkyLight() { return this.serverLevelWrapper.hasSkyLight(); }
@Override
public void onWorldGenTaskComplete(long pos)
{
//TODO: Send packet to client
}
@Override @Override
public GenericObjectRenderer getGenericRenderer() public GenericObjectRenderer getGenericRenderer()
{ {
@@ -141,6 +497,7 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
} }
//===========// //===========//
// debugging // // debugging //
//===========// //===========//
@@ -148,8 +505,48 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel
@Override @Override
public void addDebugMenuStringsToList(List<String> messageList) public void addDebugMenuStringsToList(List<String> messageList)
{ {
String dimName = this.serverLevelWrapper.getDimensionType().getDimensionName(); String dimName = this.serverLevelWrapper.getDimensionName();
messageList.add("["+dimName+"]"); messageList.add("["+dimName+"]");
} }
//==========//
// shutdown //
//==========//
@Override
public void close()
{
super.close();
this.serverside.close();
LOGGER.info("Closed DHLevel for ["+this.getLevelWrapper()+"].");
}
//================//
// helper classes //
//================//
private static class DataSourceRequestGroup
{
public final ConcurrentMap<Long, FullDataSourceRequestMessage> requestMessages = new ConcurrentHashMap<>();
@CheckForNull
public FullDataSourceV2 fullDataSource;
/**
* These semaphores prevent a given thread from accidentally locking on the same group
* multiple times, as the semaphore is tied to the given thread. <br>
* Reentrant Lock isn't used since it would allow the thread to lock on the same group. <br>
* the Short.MAX_VALUE is just a very large number that should be larger than the number of
* threads we'll have.
*/
public final Semaphore requestAddSemaphore = new Semaphore(Short.MAX_VALUE, true);
/** @see DataSourceRequestGroup#requestAddSemaphore */
public final Semaphore requestRemoveSemaphore = new Semaphore(Short.MAX_VALUE, true);
}
} }
@@ -20,7 +20,7 @@
package com.seibel.distanthorizons.core.level; package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.core.pos.DhBlockPos; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
@@ -21,20 +21,23 @@ package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider;
import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.RenderBufferHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
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 org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
public interface IDhLevel extends AutoCloseable public interface IDhLevel extends AutoCloseable, GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener
{ {
void worldGenTick();
int getMinY(); int getMinY();
/** /**
@@ -48,7 +51,7 @@ public interface IDhLevel extends AutoCloseable
void updateChunkAsync(IChunkWrapper chunk, int newChunkHash); void updateChunkAsync(IChunkWrapper chunk, int newChunkHash);
void loadBeaconBeamsInPos(long pos); void loadBeaconBeamsInPos(long pos);
void setBeaconBeamsForChunk(DhChunkPos chunkPos, List<BeaconBeamDTO> beamList); void updateBeaconBeamsForChunk(IChunkWrapper chunkToUpdate, ArrayList<IChunkWrapper> nearbyChunkList);
void unloadBeaconBeamsInPos(long pos); void unloadBeaconBeamsInPos(long pos);
FullDataSourceProviderV2 getFullDataProvider(); FullDataSourceProviderV2 getFullDataProvider();
@@ -21,9 +21,10 @@ package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
public interface IDhServerLevel extends IDhWorldGenLevel public interface IDhServerLevel extends IDhLevel
{ {
void serverTick(); void serverTick();
IServerLevelWrapper getServerLevelWrapper(); IServerLevelWrapper getServerLevelWrapper();
} }
@@ -19,7 +19,7 @@
package com.seibel.distanthorizons.core.level; package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable; import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable;
/** /**
@@ -28,15 +28,11 @@ import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindab
*/ */
public interface IKeyedClientLevelManager extends IBindable public interface IKeyedClientLevelManager extends IBindable
{ {
IServerKeyedClientLevel getServerKeyedLevel();
/** Called when a client level is wrapped by a ServerEnhancedClientLevel, for integration into mod internals. */ /** Called when a client level is wrapped by a ServerEnhancedClientLevel, for integration into mod internals. */
void setServerKeyedLevel(IServerKeyedClientLevel clientLevel); IServerKeyedClientLevel setServerKeyedLevel(IClientLevelWrapper clientLevel, String levelKey);
IServerKeyedClientLevel getOverrideWrapper();
/** Returns a new instance of a ServerEnhancedClientLevel. */ void clearKeyedLevel();
IServerKeyedClientLevel getServerKeyedLevel(ILevelWrapper level, String serverLevelKey); boolean hasLevelSet();
/** Sets the LOD engine to use the override wrapper, if the server has communication enabled. */
void setUseOverrideWrapper(boolean useOverrideWrapper);
boolean getUseOverrideWrapper();
} }
@@ -23,13 +23,18 @@ import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSource
import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue; import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen; import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.pos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import java.io.Closeable; import java.io.Closeable;
import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
/**
* Handles both single-player/server-side world gen and client side LOD requests.
* TODO rename
*/
public class WorldGenModule implements Closeable public class WorldGenModule implements Closeable
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
@@ -137,20 +142,22 @@ public class WorldGenModule implements Closeable
public boolean isWorldGenRunning() { return this.worldGenStateRef.get() != null; } public boolean isWorldGenRunning() { return this.worldGenStateRef.get() != null; }
public String getDebugMenuString() /** mutates a list so it can be added to an existing {@link IDhLevel}'s debug list */
public void addDebugMenuStringsToList(List<String> messageList)
{ {
AbstractWorldGenState worldGenState = this.worldGenStateRef.get(); AbstractWorldGenState worldGenState = this.worldGenStateRef.get();
if (worldGenState == null) if (worldGenState == null)
{ {
return null; return;
} }
String waitingCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getWaitingTaskCount()); String waitingCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getWaitingTaskCount());
String inProgressCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getInProgressTaskCount()); String inProgressCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getInProgressTaskCount());
String totalCountEstimateStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getEstimatedTotalTaskCount()); String totalCountEstimateStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getEstimatedTotalTaskCount());
messageList.add("World Gen Tasks: "+waitingCountStr+"/"+totalCountEstimateStr+" (in progress: "+inProgressCountStr+")");
return "World Gen Tasks: "+waitingCountStr+"/"+totalCountEstimateStr+" (in progress: "+inProgressCountStr+")"; worldGenState.worldGenerationQueue.addDebugMenuStringsToList(messageList);
} }
@@ -166,16 +173,16 @@ public class WorldGenModule implements Closeable
CompletableFuture<Void> closeAsync(boolean doInterrupt) CompletableFuture<Void> closeAsync(boolean doInterrupt)
{ {
return this.worldGenerationQueue.startClosing(true, doInterrupt) return this.worldGenerationQueue.startClosingAsync(true, doInterrupt)
.exceptionally(ex -> .exceptionally(e ->
{ {
LOGGER.error("Error closing generation queue", ex); LOGGER.error("Error during first stage of generation queue shutdown, Error: ["+e.getMessage()+"].", e);
return null; return null;
} }
).thenRun(this.worldGenerationQueue::close) ).thenRun(this.worldGenerationQueue::close)
.exceptionally(ex -> .exceptionally(e ->
{ {
LOGGER.error("Error closing world gen", ex); LOGGER.error("Error during second stage of generation queue shutdown, Error: ["+e.getMessage()+"].", e);
return null; return null;
}); });
} }
@@ -101,7 +101,8 @@ public class ConfigBasedLogger
else else
logger.log(logLevel, msgStr); logger.log(logLevel, msgStr);
} }
if (mode.levelForChat.isLessSpecificThan(level))
if (MC != null && mode.levelForChat.isLessSpecificThan(level))
{ {
if (param.length > 0 && param[param.length - 1] instanceof Throwable) if (param.length > 0 && param[param.length - 1] instanceof Throwable)
MC.logToChat(level, msgStr + "\n" + MC.logToChat(level, msgStr + "\n" +
@@ -20,12 +20,14 @@
package com.seibel.distanthorizons.core.logging.f3; package com.seibel.distanthorizons.core.logging.f3;
import com.seibel.distanthorizons.core.api.internal.SharedApi; import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.jar.ModJarInfo;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.RenderBufferHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.world.AbstractDhWorld; import com.seibel.distanthorizons.core.world.AbstractDhWorld;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
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 org.apache.logging.log4j.Logger;
@@ -48,7 +50,8 @@ public class F3Screen
/** /**
* F3 menu example: <br> * F3 menu example: <br>
<code> <code>
Distant Horizons v: 2.1.1-a-dev <br><br> Distant Horizons v: 2.1.1-a-dev <br>
Build: 7e163ce6 (main) <br><br>
Queued chunk updates: 0 / 1000 <br> Queued chunk updates: 0 / 1000 <br>
World Gen Tasks: 40/5304, (in progress: 7) <br><br> World Gen Tasks: 40/5304, (in progress: 7) <br><br>
@@ -60,9 +63,9 @@ public class F3Screen
Parent Update #: 12 <br><br> Parent Update #: 12 <br><br>
Client_Server World with 3 levels <br> Client_Server World with 3 levels <br>
[overworld] rendering: Active <br> [minecraft:overworld] rendering: Active <br>
[the_end] rendering: Inactive <br> [minecraft:the_end] rendering: Inactive <br>
[the_nether] rendering: Inactive <br><br> [minecraft:the_nether] rendering: Inactive <br><br>
VBO Render Count: 199/374 <br> VBO Render Count: 199/374 <br>
</code> </code>
@@ -72,6 +75,7 @@ public class F3Screen
ThreadPoolExecutor worldGenPool = ThreadPoolUtil.getWorldGenExecutor(); ThreadPoolExecutor worldGenPool = ThreadPoolUtil.getWorldGenExecutor();
ThreadPoolExecutor fileHandlerPool = ThreadPoolUtil.getFileHandlerExecutor(); ThreadPoolExecutor fileHandlerPool = ThreadPoolUtil.getFileHandlerExecutor();
ThreadPoolExecutor updatePool = ThreadPoolUtil.getUpdatePropagatorExecutor(); ThreadPoolExecutor updatePool = ThreadPoolUtil.getUpdatePropagatorExecutor();
ThreadPoolExecutor lodBuilderPool = ThreadPoolUtil.getChunkToLodBuilderExecutor();
ThreadPoolExecutor bufferBuilderPool = ThreadPoolUtil.getBufferBuilderExecutor(); ThreadPoolExecutor bufferBuilderPool = ThreadPoolUtil.getBufferBuilderExecutor();
ThreadPoolExecutor bufferUploaderPool = ThreadPoolUtil.getBufferUploaderExecutor(); ThreadPoolExecutor bufferUploaderPool = ThreadPoolUtil.getBufferUploaderExecutor();
@@ -81,11 +85,16 @@ public class F3Screen
messageList.add(""); messageList.add("");
messageList.add(ModInfo.READABLE_NAME+": "+ModInfo.VERSION); messageList.add(ModInfo.READABLE_NAME+": "+ModInfo.VERSION);
if (ModInfo.IS_DEV_BUILD)
{
messageList.add("Build: " + StringUtil.shortenString(ModJarInfo.Git_Commit, 8) + " (" + ModJarInfo.Git_Branch + ")");
}
messageList.add(""); messageList.add("");
// thread pools // thread pools
messageList.add(getThreadPoolStatString("World Gen", worldGenPool));//"World Gen Tasks: 40/5304, (in progress: 7)"); messageList.add(getThreadPoolStatString("World Gen", worldGenPool));//"World Gen Tasks: 40/5304, (in progress: 7)");
messageList.add(getThreadPoolStatString("File Handler", fileHandlerPool)); messageList.add(getThreadPoolStatString("File Handler", fileHandlerPool));
messageList.add(getThreadPoolStatString("Update Propagator", updatePool)); messageList.add(getThreadPoolStatString("Update Propagator", updatePool));
messageList.add(getThreadPoolStatString("LOD Builder", lodBuilderPool));
messageList.add(getThreadPoolStatString("Buffer Builder", bufferBuilderPool)); messageList.add(getThreadPoolStatString("Buffer Builder", bufferBuilderPool));
messageList.add(getThreadPoolStatString("Buffer Uploader", bufferUploaderPool)); messageList.add(getThreadPoolStatString("Buffer Uploader", bufferUploaderPool));
messageList.add(""); messageList.add("");
@@ -93,7 +102,7 @@ public class F3Screen
messageList.add(SharedApi.INSTANCE.getDebugMenuString()); messageList.add(SharedApi.INSTANCE.getDebugMenuString());
messageList.add(""); messageList.add("");
// world / levels // world / levels
messageList.add(world.GetDebugMenuString()); world.addDebugMenuStringsToList(messageList);
for (IDhLevel level : levelIterator) for (IDhLevel level : levelIterator)
{ {
level.addDebugMenuStringsToList(messageList); level.addDebugMenuStringsToList(messageList);
@@ -0,0 +1,415 @@
package com.seibel.distanthorizons.core.multiplayer.client;
import com.google.common.base.Stopwatch;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.DhServerLevel;
import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.logging.ConfigBasedSpamLogger;
import com.seibel.distanthorizons.core.network.exceptions.InvalidLevelException;
import com.seibel.distanthorizons.core.network.exceptions.RateLimitedException;
import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException;
import com.seibel.distanthorizons.core.network.session.SessionClosedException;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceResponseMessage;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.TimerUtil;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.core.util.ratelimiting.SupplierBasedRateLimiter;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import org.apache.logging.log4j.LogManager;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import java.awt.*;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRenderable, AutoCloseable
{
private static final ConfigBasedSpamLogger LOGGER = new ConfigBasedSpamLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Logging.logNetworkEvent.get(), 3);
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final Timer TASK_FINISH_TIMER = TimerUtil.CreateTimer("RequestTaskFinishTimer");
private static final int MAX_RETRY_ATTEMPTS = 3;
protected static final long SHUTDOWN_TIMEOUT_SECONDS = 5;
public final ClientNetworkState networkState;
protected final IDhClientLevel level;
private final boolean changedOnly;
private volatile CompletableFuture<Void> closingFuture = null;
protected final ConcurrentMap<Long, RequestQueueEntry> waitingTasksBySectionPos = new ConcurrentHashMap<>();
/**
* This semaphore prevents a given thread from accidentally locking on the same group
* multiple times, as the semaphore is tied to the given thread. <br>
* Reentrant Lock isn't used since it would allow the thread to lock on the same group. <br>
* the Short.MAX_VALUE is just a very large number that should be larger than the number of
* threads we'll have.
*/
private final Semaphore pendingTasksSemaphore = new Semaphore(Short.MAX_VALUE, true);
private final AtomicInteger finishedRequests = new AtomicInteger();
private final AtomicInteger failedRequests = new AtomicInteger();
private final ConfigEntry<Boolean> showDebugWireframeConfig;
private final SupplierBasedRateLimiter<Void> rateLimiter = new SupplierBasedRateLimiter<>(this::getRequestRateLimit);
//=============//
// constructor //
//=============//
public AbstractFullDataNetworkRequestQueue(
ClientNetworkState networkState, IDhClientLevel level,
boolean changedOnly, ConfigEntry<Boolean> showDebugWireframeConfig)
{
this.networkState = networkState;
this.level = level;
this.changedOnly = changedOnly;
this.showDebugWireframeConfig = showDebugWireframeConfig;
DebugRenderer.register(this, this.showDebugWireframeConfig);
}
//==================//
// abstract methods //
//==================//
protected abstract int getRequestRateLimit();
protected abstract String getQueueName();
//====================//
// request submitting //
//====================//
public CompletableFuture<Boolean> submitRequest(long sectionPos, Consumer<FullDataSourceV2> dataSourceConsumer)
{ return this.submitRequest(sectionPos, null, dataSourceConsumer); }
public CompletableFuture<Boolean> submitRequest(long sectionPos, @Nullable Long clientTimestamp, Consumer<FullDataSourceV2> dataSourceConsumer)
{
LodUtil.assertTrue(DhSectionPos.getDetailLevel(sectionPos) == DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL, "Only highest-detail sections are allowed.");
RequestQueueEntry entry = new RequestQueueEntry(dataSourceConsumer, clientTimestamp);
entry.future.whenComplete((success, throwable) ->
{
this.waitingTasksBySectionPos.remove(sectionPos);
this.finishedRequests.incrementAndGet();
if (!success || throwable != null)
{
this.failedRequests.incrementAndGet();
}
});
this.waitingTasksBySectionPos.put(sectionPos, entry);
return entry.future;
}
public synchronized boolean tick(DhBlockPos2D targetPos)
{
if (this.closingFuture != null || !this.networkState.isReady())
{
return false;
}
// queue requests until the queue is full
while (this.getInProgressTaskCount() < this.getWaitingTaskCount()
&& this.getInProgressTaskCount() < this.getRequestRateLimit()
&& this.pendingTasksSemaphore.tryAcquire())
{
if (!this.rateLimiter.tryAcquire())
{
this.pendingTasksSemaphore.release();
break;
}
this.sendNextRequest(targetPos);
}
return true;
}
private void sendNextRequest(DhBlockPos2D targetPos)
{
Map.Entry<Long, RequestQueueEntry> mapEntry = this.waitingTasksBySectionPos.entrySet().stream()
.filter(task -> task.getValue().networkDataSourceFuture == null)
.min((x, y) -> posDistanceSquared(targetPos, x.getKey()) - posDistanceSquared(targetPos, y.getKey()))
.orElse(null);
if (mapEntry == null)
{
this.pendingTasksSemaphore.release();
return;
}
long sectionPos = mapEntry.getKey();
RequestQueueEntry entry = mapEntry.getValue();
CompletableFuture<FullDataSourceResponseMessage> dataSourceFuture = this.networkState.getSession().sendRequest(
new FullDataSourceRequestMessage(this.level.getLevelWrapper(), sectionPos, entry.updateTimestamp),
FullDataSourceResponseMessage.class
);
entry.networkDataSourceFuture = dataSourceFuture;
dataSourceFuture.handle((response, throwable) ->
{
this.pendingTasksSemaphore.release();
try
{
if (throwable != null)
{
throw throwable;
}
if (response.payload != null)
{
FullDataSourceV2DTO dataSourceDto = this.networkState.decodeDataSourceAndReleaseBuffer(response.payload);
ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor();
if (executor == null)
{
LOGGER.warn("Unable to handle FullDataPayload - getNetworkCompressionExecutor() is null");
return null;
}
CompletableFuture.runAsync(() ->
{
try
{
FullDataSourceV2 fullDataSource = dataSourceDto.createPooledDataSource(this.level.getLevelWrapper());
entry.dataSourceConsumer.accept(fullDataSource);
FullDataSourceV2.DATA_SOURCE_POOL.returnPooledDataSource(fullDataSource);
}
catch (IOException | DataCorruptedException | InterruptedException e)
{
throw new RuntimeException(e);
}
}, executor);
}
else
{
LodUtil.assertTrue(this.changedOnly, "Received empty data source response for not changes-only request");
}
}
catch (InvalidLevelException | RequestRejectedException ignored)
{
// We're too late / some cases might trigger a bunch of expected rejections
return entry.future.complete(false);
}
catch (SessionClosedException | CancellationException ignored)
{
// Triggered when level is unloaded
return entry.future.cancel(false);
}
catch (RateLimitedException e)
{
LOGGER.warn("Rate limited by server, re-queueing task [" + DhSectionPos.toString(sectionPos) + "]: " + e.getMessage());
// Skip all requests for 1 second
this.rateLimiter.acquireAll();
entry.networkDataSourceFuture = null;
return null;
}
catch (Throwable e)
{
entry.retryAttempts--;
LOGGER.error("Error while fetching full data source, attempts left: {} / {}", entry.retryAttempts, MAX_RETRY_ATTEMPTS, e);
// Retry logic
if (entry.retryAttempts > 0)
{
entry.networkDataSourceFuture = null;
return null;
}
else
{
return entry.future.complete(false);
}
}
// Hack to work around a race condition
// If you finish the request too quickly, the section will never render
TASK_FINISH_TIMER.schedule(new TimerTask()
{
@Override
public void run()
{
entry.future.complete(true);
}
}, 10000);
return null;
});
}
//=========================================//
// IFullDataSourceRetrievalQueue overrides //
//=========================================//
public void removeRetrievalRequestIf(DhSectionPos.ICancelablePrimitiveLongConsumer removeIf)
{
for (Map.Entry<Long, RequestQueueEntry> mapEntry : this.waitingTasksBySectionPos.entrySet())
{
long pos = mapEntry.getKey();
RequestQueueEntry entry = mapEntry.getValue();
if (removeIf.accept(pos))
{
LOGGER.debug("Removing request " + mapEntry.getKey() + "...");
entry.future.cancel(false);
if (entry.networkDataSourceFuture != null)
{
entry.networkDataSourceFuture.cancel(false);
}
}
}
}
public void addDebugMenuStringsToList(List<String> messageList)
{
messageList.add(this.getQueueName() + " [" + this.level.getClientLevelWrapper().getDimensionName() + "]");
messageList.add("Requests: " + this.finishedRequests + " / " + (this.getWaitingTaskCount() + this.finishedRequests.get()) + " (failed: " + this.failedRequests + ", rate limit: " + this.getRequestRateLimit() + ")");
}
public int getWaitingTaskCount() { return this.waitingTasksBySectionPos.size(); }
public int getInProgressTaskCount() { return Short.MAX_VALUE - this.pendingTasksSemaphore.availablePermits(); }
//==========//
// shutdown //
//==========//
public CompletableFuture<Void> startClosingAsync(boolean alsoInterruptRunning)
{
return this.closingFuture = CompletableFuture.runAsync(() -> {
Stopwatch stopwatch = Stopwatch.createStarted();
do
{
for (RequestQueueEntry entry : this.waitingTasksBySectionPos.values())
{
entry.future.cancel(alsoInterruptRunning);
if (entry.networkDataSourceFuture != null && entry.networkDataSourceFuture.cancel(alsoInterruptRunning))
{
this.pendingTasksSemaphore.release();
}
}
}
while (!this.pendingTasksSemaphore.tryAcquire(Short.MAX_VALUE) && stopwatch.elapsed(TimeUnit.SECONDS) < SHUTDOWN_TIMEOUT_SECONDS);
if (stopwatch.elapsed(TimeUnit.SECONDS) >= SHUTDOWN_TIMEOUT_SECONDS)
{
LOGGER.warn("The request queue [" + this.getQueueName() + "] for level [" + this.level.getLevelWrapper() + "] did not shutdown in [" + SHUTDOWN_TIMEOUT_SECONDS + "] seconds. Some unfinished tasks might be left hanging.");
}
});
}
@Override
public void close()
{
DebugRenderer.unregister(this, this.showDebugWireframeConfig);
}
//===========//
// debugging //
//===========//
@Override
public void debugRender(DebugRenderer renderer)
{
if (MC_CLIENT.getWrappedClientLevel() != this.level.getClientLevelWrapper())
{
return;
}
for (Map.Entry<Long, RequestQueueEntry> mapEntry : this.waitingTasksBySectionPos.entrySet())
{
renderer.renderBox(new DebugRenderer.Box(mapEntry.getKey(), -32f, 64f, 0.05f,
mapEntry.getValue().networkDataSourceFuture != null ? Color.red : Color.gray
));
}
}
//================//
// helper methods //
//================//
protected static int posDistanceSquared(DhBlockPos2D targetPos, long pos)
{ return (int) DhSectionPos.getCenterBlockPos(pos).distSquared(targetPos); }
//================//
// helper classes //
//================//
protected static class RequestQueueEntry
{
/** encapsulates the entire request, including client side queuing and the actual server request */
public final CompletableFuture<Boolean> future = new CompletableFuture<>();
public final Consumer<FullDataSourceV2> dataSourceConsumer;
/** will be null if we want to retrieve the LOD regardless of when it was last updated */
@Nullable
public final Long updateTimestamp;
/** Will be null until the request has been sent to the server */
@CheckForNull
public CompletableFuture<FullDataSourceResponseMessage> networkDataSourceFuture;
/** when this reaches zero then the request will be canceled. */
public int retryAttempts = MAX_RETRY_ATTEMPTS;
//=============//
// constructor //
//=============//
public RequestQueueEntry(
Consumer<FullDataSourceV2> dataSourceConsumer,
@Nullable Long updateTimestamp)
{
this.dataSourceConsumer = dataSourceConsumer;
this.updateTimestamp = updateTimestamp;
}
}
}
@@ -0,0 +1,210 @@
package com.seibel.distanthorizons.core.multiplayer.client;
import com.google.common.cache.CacheBuilder;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig;
import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.network.event.ScopedNetworkEventSource;
import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent;
import com.seibel.distanthorizons.core.network.event.internal.IncompatibleMessageInternalEvent;
import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage;
import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPayload;
import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.coreapi.ModInfo;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.CompositeByteBuf;
import org.apache.logging.log4j.LogManager;
import org.jetbrains.annotations.Nullable;
import java.io.Closeable;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
public class ClientNetworkState implements Closeable
{
protected static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Logging.logNetworkEvent.get());
private final ConcurrentMap<Integer, CompositeByteBuf> fullDataBufferById = CacheBuilder.newBuilder()
.expireAfterAccess(10, TimeUnit.SECONDS)
.<Integer, CompositeByteBuf>build()
.asMap();
private final SessionConfig.AnyChangeListener configAnyChangeListener = new SessionConfig.AnyChangeListener(this::sendConfigMessage);
private final NetworkSession networkSession = new NetworkSession(null);
/**
* Returns the client used by this instance. <p>
* If you need to subscribe to any packet events, create an instance of {@link ScopedNetworkEventSource} using the returned instance.
*/
public NetworkSession getSession() { return this.networkSession; }
public SessionConfig sessionConfig = new SessionConfig();
private volatile boolean configReceived = false;
public boolean isReady() { return this.configReceived; }
private EServerSupportStatus serverSupportStatus = EServerSupportStatus.NONE;
/** Protocol version closest to supported by this mod version */
@Nullable
private Integer closestProtocolVersion;
//=============//
// constructor //
//=============//
public ClientNetworkState()
{
this.networkSession.registerHandler(IncompatibleMessageInternalEvent.class, event ->
{
if (this.closestProtocolVersion == null
|| Math.abs(event.protocolVersion - ModInfo.PROTOCOL_VERSION) < this.closestProtocolVersion)
{
this.closestProtocolVersion = event.protocolVersion;
}
});
this.networkSession.registerHandler(CurrentLevelKeyMessage.class, message ->
{
// we will also receive this message when we have full support
if (this.serverSupportStatus == EServerSupportStatus.NONE)
{
this.serverSupportStatus = EServerSupportStatus.LEVELS_ONLY;
}
});
this.networkSession.registerHandler(SessionConfigMessage.class, message ->
{
this.serverSupportStatus = EServerSupportStatus.FULL;
LOGGER.info("Connection config has been changed: ["+message.config+"].");
this.sessionConfig = message.config;
this.configReceived = true;
});
this.networkSession.registerHandler(CloseInternalEvent.class, message ->
{
this.configReceived = false;
});
this.networkSession.registerHandler(FullDataSplitMessage.class, message ->
{
if (message.isFirst)
{
CompositeByteBuf composite = this.fullDataBufferById.remove(message.bufferId);
if (composite != null)
{
composite.release();
LOGGER.debug("Released full data buffer ["+message.bufferId+"]: ["+composite+"]");
}
}
CompositeByteBuf byteBuffer = this.fullDataBufferById.computeIfAbsent(message.bufferId, bufferId -> ByteBufAllocator.DEFAULT.compositeBuffer());
byteBuffer.addComponent(true, message.buffer);
LOGGER.debug("Full data buffer ["+message.bufferId+"]: ["+byteBuffer+"].");
});
this.networkSession.registerHandler(FullDataPartialUpdateMessage.class, msg ->
{
// Dummy handler to prevent unhandled message warnings
});
}
//==============//
// send message //
//==============//
public FullDataSourceV2DTO decodeDataSourceAndReleaseBuffer(FullDataPayload msg)
{
CompositeByteBuf compositeByteBuffer = this.fullDataBufferById.remove(msg.dtoBufferId);
LodUtil.assertTrue(compositeByteBuffer != null);
try
{
return INetworkObject.decodeToInstance(FullDataSourceV2DTO.CreateEmptyDataSource(), compositeByteBuffer);
}
finally
{
compositeByteBuffer.release();
}
}
public void sendConfigMessage()
{
this.configReceived = false;
this.getSession().sendMessage(new SessionConfigMessage(new SessionConfig()));
}
//===========//
// debugging //
//===========//
public void addDebugMenuStringsToList(List<String> messageList)
{
if (this.networkSession.isClosed())
{
messageList.add("NetworkSession closed: " + this.networkSession.getCloseReason().getMessage());
return;
}
if (this.serverSupportStatus == EServerSupportStatus.NONE && this.closestProtocolVersion != null)
{
messageList.add("Incompatible protocol version: [" + this.closestProtocolVersion + "], required: [" + ModInfo.PROTOCOL_VERSION+ "]");
return;
}
messageList.add(this.serverSupportStatus.message);
}
//==========//
// shutdown //
//==========//
@Override
public void close()
{
this.configAnyChangeListener.close();
this.networkSession.close();
}
//================//
// helper classes //
//================//
/**
* NONE, <br>
* LEVELS_ONLY, <br>
* FULL, <br>
*/
private enum EServerSupportStatus
{
NONE("Server does not support DH"),
LEVELS_ONLY("Server supports shared level keys"),
FULL("Server has full DH support");
public final String message;
EServerSupportStatus(String message) { this.message = message; }
}
}
@@ -0,0 +1,57 @@
package com.seibel.distanthorizons.core.multiplayer.client;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue;
import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
/**
* This queue only handles LOD updates for
* LODs that were changed when the player wasn't online
* and the player already loaded the LODs once.
* {@link RemoteWorldRetrievalQueue} is used for all other requests.
*
* @see Config.Client.Advanced.Multiplayer.ServerNetworking#synchronizeOnLogin
* @see RemoteWorldRetrievalQueue
*/
public class SyncOnLoginRequestQueue extends AbstractFullDataNetworkRequestQueue
{
//=============//
// constructor //
//=============//
public SyncOnLoginRequestQueue(IDhClientLevel level, ClientNetworkState networkState)
{ super(networkState, level, true, Config.Client.Advanced.Debugging.DebugWireframe.showWorldGenQueue); }
//=========//
// getters //
//=========//
@Override
protected int getRequestRateLimit() { return this.networkState.sessionConfig.getSyncOnLoginRateLimit(); }
@Override
protected String getQueueName() { return "Sync On Login Queue"; }
//==================//
// request handling //
//==================//
@Override
public boolean tick(DhBlockPos2D targetPos)
{
if (!this.networkState.sessionConfig.getSynchronizeOnLogin())
{
return false;
}
return super.tick(targetPos);
}
}
@@ -0,0 +1,184 @@
package com.seibel.distanthorizons.core.multiplayer.config;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.network.INetworkObject;
import io.netty.buffer.ByteBuf;
import java.io.Closeable;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
public class SessionConfig implements INetworkObject
{
private static final LinkedHashMap<String, Entry> CONFIG_ENTRIES = new LinkedHashMap<>();
private final LinkedHashMap<String, Object> values = new LinkedHashMap<>();
public SessionConfig constrainingConfig;
//=============//
// constructor //
//=============//
static
{
// Note: config values are ordered by serversideShortName when transmitted
registerConfigEntry(Config.Client.Advanced.Graphics.Quality.lodChunkRenderDistanceRadius, Math::min);
registerConfigEntry(Config.Client.Advanced.WorldGenerator.enableDistantGeneration, (x, y) -> x && y);
registerConfigEntry(Config.Client.Advanced.Multiplayer.ServerNetworking.generationRequestRateLimit, Math::min);
registerConfigEntry(Config.Client.Advanced.Multiplayer.ServerNetworking.enableRealTimeUpdates, (x, y) -> x && y);
registerConfigEntry(Config.Client.Advanced.Multiplayer.ServerNetworking.synchronizeOnLogin, (x, y) -> x && y);
registerConfigEntry(Config.Client.Advanced.Multiplayer.ServerNetworking.syncOnLoginRateLimit, Math::min);
}
public SessionConfig() {}
//===============//
// public values //
//===============//
public int getRenderDistanceRadius() { return this.getValue(Config.Client.Advanced.Graphics.Quality.lodChunkRenderDistanceRadius); }
public boolean isDistantGenerationEnabled() { return this.getValue(Config.Client.Advanced.WorldGenerator.enableDistantGeneration); }
public int getGenerationRequestRateLimit() { return this.getValue(Config.Client.Advanced.Multiplayer.ServerNetworking.generationRequestRateLimit); }
public boolean isRealTimeUpdatesEnabled() { return this.getValue(Config.Client.Advanced.Multiplayer.ServerNetworking.enableRealTimeUpdates); }
public boolean getSynchronizeOnLogin() { return this.getValue(Config.Client.Advanced.Multiplayer.ServerNetworking.synchronizeOnLogin); }
public int getSyncOnLoginRateLimit() { return this.getValue(Config.Client.Advanced.Multiplayer.ServerNetworking.syncOnLoginRateLimit); }
//====================//
// entry registration //
//====================//
private static <T> void registerConfigEntry(ConfigEntry<T> configEntry, BiFunction<T, T, T> valueConstrainer)
{
CONFIG_ENTRIES.put(Objects.requireNonNull(configEntry.getServersideShortName()), new Entry(configEntry, valueConstrainer));
}
//==================//
// internal getters //
//==================//
private <T> T getValue(ConfigEntry<T> configEntry) { return this.getValue(configEntry.getServersideShortName()); }
@SuppressWarnings("unchecked")
private <T> T getValue(String name)
{
Entry entry = CONFIG_ENTRIES.get(name);
T value = (T) this.values.get(name);
if (value == null)
{
value = (T) entry.supplier.get();
}
return (this.constrainingConfig != null
? (T) entry.valueConstrainer.apply(value, this.constrainingConfig.getValue(name))
: value);
}
private Map<String, ?> getValues()
{
return CONFIG_ENTRIES.keySet().stream().collect(Collectors.toMap(
Function.identity(),
this::getValue,
(x, y) -> x,
LinkedHashMap::new
));
}
//===============//
// serialization //
//===============//
@Override
public void encode(ByteBuf outBuffer) { this.writeFixedLengthCollection(outBuffer, this.getValues().values()); }
@Override
public void decode(ByteBuf inBuffer)
{
for (String key : CONFIG_ENTRIES.keySet())
{
Object currentValue = this.getValue(key);
Object newValue = Codec.getCodec(currentValue.getClass()).decode.apply(currentValue, inBuffer);
this.values.put(key, newValue);
}
}
//================//
// base overrides //
//================//
@Override
public String toString()
{
return MoreObjects.toStringHelper(this)
.add("values", this.getValues())
.toString();
}
//================//
// helper classes //
//================//
private static class Entry
{
public final ConfigEntry<Object> supplier;
public final BiFunction<Object, Object, Object> valueConstrainer;
@SuppressWarnings("unchecked")
private <T> Entry(ConfigEntry<T> supplier, BiFunction<T, T, T> valueConstrainer)
{
this.supplier = (ConfigEntry<Object>) supplier;
this.valueConstrainer = (BiFunction<Object, Object, Object>) valueConstrainer;
}
}
/** fires if any config value was changed */
public static class AnyChangeListener implements Closeable
{
private final ArrayList<ConfigChangeListener<?>> changeListeners;
public AnyChangeListener(Runnable runnable)
{
this.changeListeners = new ArrayList<>(CONFIG_ENTRIES.size());
for (Entry entry : CONFIG_ENTRIES.values())
{
this.changeListeners.add(new ConfigChangeListener<>(entry.supplier, ignored -> runnable.run()));
}
}
@Override
public void close()
{
for (ConfigChangeListener<?> changeListener : this.changeListeners)
{
changeListener.close();
}
this.changeListeners.clear();
}
}
}
@@ -0,0 +1,85 @@
package com.seibel.distanthorizons.core.multiplayer.server;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import org.jetbrains.annotations.Nullable;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
public class RemotePlayerConnectionHandler
{
private final ConcurrentMap<IServerPlayerWrapper, ServerPlayerState> connectedPlayerStateByPlayerWrapper = new ConcurrentHashMap<>();
private final ConcurrentMap<IServerPlayerWrapper, Queue<AbstractNetworkMessage>> messageQueueByPlayerWrapper = new ConcurrentHashMap<>();
//========================//
// player joining/leaving //
//========================//
public ServerPlayerState registerJoinedPlayer(IServerPlayerWrapper serverPlayer)
{
ServerPlayerState playerState = new ServerPlayerState(serverPlayer);
this.connectedPlayerStateByPlayerWrapper.put(serverPlayer, playerState);
Queue<AbstractNetworkMessage> queuedMessages = this.messageQueueByPlayerWrapper.get(serverPlayer);
if (queuedMessages != null)
{
NetworkSession networkSession = playerState.networkSession;
for (AbstractNetworkMessage message : queuedMessages)
{
networkSession.tryHandleMessage(message);
}
this.messageQueueByPlayerWrapper.remove(serverPlayer);
}
return playerState;
}
public void unregisterLeftPlayer(IServerPlayerWrapper serverPlayer)
{
ServerPlayerState playerState = this.connectedPlayerStateByPlayerWrapper.remove(serverPlayer);
if (playerState != null)
{
playerState.close();
}
}
//==========//
// messages //
//==========//
public void handlePluginMessage(IServerPlayerWrapper player, AbstractNetworkMessage message)
{
ServerPlayerState playerState = this.connectedPlayerStateByPlayerWrapper.get(player);
if (playerState != null)
{
playerState.networkSession.tryHandleMessage(message);
}
else
{
this.messageQueueByPlayerWrapper.computeIfAbsent(player, k -> new ConcurrentLinkedQueue<>()).add(message);
}
}
//=========//
// getters //
//=========//
@Nullable
public ServerPlayerState getConnectedPlayer(IServerPlayerWrapper player) { return this.connectedPlayerStateByPlayerWrapper.get(player); }
public Iterable<ServerPlayerState> getConnectedPlayers() { return this.connectedPlayerStateByPlayerWrapper.values(); }
}
@@ -0,0 +1,123 @@
package com.seibel.distanthorizons.core.multiplayer.server;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.level.DhServerLevel;
import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig;
import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage;
import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage;
import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent;
import com.seibel.distanthorizons.core.network.exceptions.RateLimitedException;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage;
import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.util.ratelimiting.SupplierBasedRateAndConcurrencyLimiter;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import org.jetbrains.annotations.NotNull;
import java.io.Closeable;
import java.util.concurrent.ConcurrentHashMap;
public class ServerPlayerState implements Closeable
{
private final ConfigChangeListener<String> levelKeyPrefixChangeListener
= new ConfigChangeListener<>(Config.Client.Advanced.Multiplayer.ServerNetworking.levelKeyPrefix, this::onLevelKeyPrefixConfigChanged);
private final SessionConfig.AnyChangeListener configAnyChangeListener = new SessionConfig.AnyChangeListener(this::onSessionConfigChanged);
private String lastLevelKey = "";
public final NetworkSession networkSession;
public IServerPlayerWrapper getServerPlayer() { return this.networkSession.serverPlayer; }
@NotNull
public final SessionConfig sessionConfig = new SessionConfig();
private final ConcurrentHashMap<DhServerLevel, RateLimiterSet> rateLimiterSets = new ConcurrentHashMap<>();
public RateLimiterSet getRateLimiterSet(DhServerLevel level) { return this.rateLimiterSets.computeIfAbsent(level, ignored -> new RateLimiterSet()); }
public void clearRateLimiterSets() { this.rateLimiterSets.clear(); }
//==============//
// constructors //
//==============//
public ServerPlayerState(IServerPlayerWrapper serverPlayer)
{
this.networkSession = new NetworkSession(serverPlayer);
this.networkSession.registerHandler(SessionConfigMessage.class, (sessionConfigMessage) ->
{
this.sessionConfig.constrainingConfig = sessionConfigMessage.config;
this.sendLevelKey();
this.networkSession.sendMessage(new SessionConfigMessage(this.sessionConfig));
});
this.networkSession.registerHandler(CloseInternalEvent.class, event -> {
// No-op. prevents "Unhandled message" log entries
});
}
//=================//
// client updating //
//=================//
private void onLevelKeyPrefixConfigChanged(String newLevelKey) { this.sendLevelKey(); }
private void sendLevelKey()
{
if (Config.Client.Advanced.Multiplayer.ServerNetworking.sendLevelKeys.get())
{
// let the client's know about the change
String levelKey = this.getServerPlayer().getLevel().getKeyedLevelDimensionName();
if (!levelKey.equals(this.lastLevelKey))
{
this.lastLevelKey = levelKey;
this.networkSession.sendMessage(new CurrentLevelKeyMessage(levelKey));
}
}
}
private void onSessionConfigChanged() { this.networkSession.sendMessage(new SessionConfigMessage(this.sessionConfig)); }
//==========//
// shutdown //
//==========//
@Override
public void close()
{
this.levelKeyPrefixChangeListener.close();
this.configAnyChangeListener.close();
this.networkSession.close();
}
//================//
// helper classes //
//================//
public class RateLimiterSet
{
public final SupplierBasedRateAndConcurrencyLimiter<FullDataSourceRequestMessage> generationRequestRateLimiter = new SupplierBasedRateAndConcurrencyLimiter<>(
() -> Config.Client.Advanced.Multiplayer.ServerNetworking.generationRequestRateLimit.get(),
msg -> {
msg.sendResponse(new RateLimitedException("Full data request rate limit: " + ServerPlayerState.this.sessionConfig.getGenerationRequestRateLimit()));
}
);
public final SupplierBasedRateAndConcurrencyLimiter<FullDataSourceRequestMessage> syncOnLoginRateLimiter = new SupplierBasedRateAndConcurrencyLimiter<>(
() -> Config.Client.Advanced.Multiplayer.ServerNetworking.syncOnLoginRateLimit.get(),
msg -> {
msg.sendResponse(new RateLimitedException("Sync on login rate limit: " + ServerPlayerState.this.sessionConfig.getSyncOnLoginRateLimit()));
}
);
}
}
@@ -0,0 +1,228 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network;
import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.Contract;
import javax.annotation.Nullable;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.*;
/** The base for any object that can be sent over the network. */
public interface INetworkObject
{
/** Serializes this object into the given {@link ByteBuf} */
void encode(ByteBuf out);
/** Populates this object's from the given {@link ByteBuf} */
void decode(ByteBuf in);
//========================//
// default/static methods //
//========================//
/**
* @param obj the empty object that will be populated by the incoming {@link ByteBuf}
*/
static <T extends INetworkObject> T decodeToInstance(T obj, ByteBuf inputByteBuf)
{
obj.decode(inputByteBuf);
return obj;
}
@Contract("_, null -> false; _, !null -> true")
default boolean writeOptional(ByteBuf outputByteBuf, Object value)
{
boolean isNull = value != null;
outputByteBuf.writeBoolean(isNull);
return isNull;
}
@Nullable
default <T> T readOptional(ByteBuf inputByteBuf, Supplier<T> decoder)
{
return inputByteBuf.readBoolean()
? decoder.get()
: null;
}
default void readOptional(ByteBuf inputByteBuf, Runnable decoder)
{
if (inputByteBuf.readBoolean())
{
decoder.run();
}
}
// strings //
default void writeString(String inputString, ByteBuf outputByteBuf) { INetworkObject.writeStringStatic(inputString, outputByteBuf); }
static void writeStringStatic(String inputString, ByteBuf outputByteBuf)
{
byte[] bytes = inputString.getBytes(StandardCharsets.UTF_8);
outputByteBuf.writeShort(bytes.length);
outputByteBuf.writeBytes(bytes);
}
default String readString(ByteBuf inputByteBuf) { return INetworkObject.readStringStatic(inputByteBuf); }
static String readStringStatic(ByteBuf inputByteBuf)
{
int length = inputByteBuf.readUnsignedShort();
return inputByteBuf.readSlice(length).toString(StandardCharsets.UTF_8);
}
// collections //
default <T> void writeCollection(ByteBuf outputByteBuf, Collection<T> collection)
{
outputByteBuf.writeInt(collection.size());
this.writeFixedLengthCollection(outputByteBuf, collection);
}
default <T> void writeFixedLengthCollection(ByteBuf outputByteBuf, Collection<T> collection)
{
for (T item : collection)
{
Codec codec = Codec.getCodec(item.getClass());
codec.encode.accept(item, outputByteBuf);
}
}
default <T> void readCollection(ByteBuf inputByteBuf, Collection<T> collection, Supplier<T> innerValueConstructor)
{
int size = inputByteBuf.readInt();
Codec codec = null;
for (int i = 0; i < size; i++)
{
T item = innerValueConstructor.get();
if (codec == null)
{
codec = Codec.getCodec(item.getClass());
}
//noinspection unchecked
item = (T) codec.decode.apply(item, inputByteBuf);
collection.add(item);
}
}
default <K, V> void readMap(ByteBuf inputByteBuf, Map<K, V> map, Supplier<K> keySupplier, Supplier<V> valueSupplier)
{
ArrayList<Map.Entry<K, V>> entryList = new ArrayList<>();
this.readCollection(inputByteBuf, entryList, () -> new AbstractMap.SimpleEntry<>(keySupplier.get(), valueSupplier.get()));
for (Map.Entry<K, V> entry : entryList)
{
map.put(entry.getKey(), entry.getValue());
}
}
//================//
// helper classes //
//================//
/**
* Defines (de)serialization for different classes,
* specifically for base classes like {@link Integer}, {@link Boolean}, and {@link String}. <br><br>
*
* Should only be used for non-editable classes;
* otherwise, you may want to implement {@link INetworkObject} and use its methods where applicable.
*/
class Codec
{
private static final ConcurrentMap<Class<?>, Codec> CODEC_MAP = new ConcurrentHashMap<Class<?>, Codec>()
{{
// Primitives must be added manually here
this.put(Integer.class, new Codec((obj, outByteBuff) -> outByteBuff.writeInt((int)obj), (obj, inByteBuff) -> inByteBuff.readInt()));
this.put(Boolean.class, new Codec((obj, outByteBuff) -> outByteBuff.writeBoolean((boolean) obj), (obj, inByteBuff) -> inByteBuff.readBoolean()));
this.put(String.class, new Codec((obj, outByteBuff) -> INetworkObject.writeStringStatic((String) obj, outByteBuff), (obj, inByteBuff) -> INetworkObject.readStringStatic(inByteBuff)));
this.put(INetworkObject.class, new Codec(INetworkObject::encode, INetworkObject::decodeToInstance));
this.put(Map.Entry.class, new Codec(
(obj, outByteBuff) ->
{
Map.Entry<?, ?> entry = (Entry<?, ?>) obj;
getCodec(entry.getKey().getClass()).encode.accept(entry.getKey(), outByteBuff);
getCodec(entry.getValue().getClass()).encode.accept(entry.getValue(), outByteBuff);
},
(obj, inByteBuff) ->
{
Map.Entry<?, ?> entry = (Entry<?, ?>) obj;
return new SimpleEntry<>(
getCodec(entry.getKey().getClass()).decode.apply(entry.getKey(), inByteBuff),
getCodec(entry.getValue().getClass()).decode.apply(entry.getValue(), inByteBuff)
);
}
));
}};
public final BiConsumer<Object, ByteBuf> encode;
public final BiFunction<Object, ByteBuf, Object> decode;
//=============//
// constructor //
//=============//
@SuppressWarnings("unchecked")
public <T> Codec(BiConsumer<T, ByteBuf> encode, BiFunction<T, ByteBuf, T> decode)
{
this.encode = (BiConsumer<Object, ByteBuf>) encode;
this.decode = (BiFunction<Object, ByteBuf, Object>) decode;
}
//================//
// static methods //
//================//
public static <T> Codec getCodec(Class<T> clazz)
{
return CODEC_MAP.computeIfAbsent(clazz, ignored ->
{
// TODO when would this ever return true?
for (Map.Entry<Class<?>, Codec> entry : CODEC_MAP.entrySet())
{
if (entry.getKey().isAssignableFrom(clazz))
{
return entry.getValue();
}
}
throw new AssertionError("Class has no compatible codec: " + clazz.getSimpleName());
});
}
}
}
@@ -1,175 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.network.messages.CloseMessage;
import com.seibel.distanthorizons.core.network.messages.CloseReasonMessage;
import com.seibel.distanthorizons.core.network.messages.HelloMessage;
import com.seibel.distanthorizons.core.network.protocol.NetworkChannelInitializer;
//import io.netty.bootstrap.Bootstrap;
//import io.netty.channel.Channel;
//import io.netty.channel.ChannelFuture;
//import io.netty.channel.ChannelOption;
//import io.netty.channel.EventLoopGroup;
//import io.netty.channel.nio.NioEventLoopGroup;
//import io.netty.channel.socket.nio.NioSocketChannel;
import org.apache.logging.log4j.Logger;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
public class NetworkClient //extends NetworkEventSource implements AutoCloseable
{
// private static final Logger LOGGER = DhLoggerBuilder.getLogger();
//
// private enum EConnectionState
// {
// OPEN,
// RECONNECT,
// RECONNECT_FORCE,
// CLOSE_WAIT,
// CLOSED
// }
//
// private static final int FAILURE_RECONNECT_DELAY_SEC = 5;
// private static final int FAILURE_RECONNECT_ATTEMPTS = 3;
//
// // TODO move to payload of some sort
// private final InetSocketAddress address;
//
// private final EventLoopGroup workerGroup = new NioEventLoopGroup();
// private final Bootstrap clientBootstrap = new Bootstrap()
// .group(this.workerGroup)
// .channel(NioSocketChannel.class)
// .option(ChannelOption.SO_KEEPALIVE, true)
// .handler(new NetworkChannelInitializer(this.messageHandler));
//
// private EConnectionState connectionState;
// private Channel channel;
// private int reconnectAttempts = FAILURE_RECONNECT_ATTEMPTS;
//
//
//
// public NetworkClient(String host, int port)
// {
// this.address = new InetSocketAddress(host, port);
//
// this.registerHandlers();
// this.connect();
// }
//
// private void registerHandlers()
// {
// this.registerHandler(HelloMessage.class, (helloMessage, channelContext) ->
// {
// LOGGER.info("Connected to server: "+channelContext.channel().remoteAddress());
// });
//
// this.registerHandler(CloseReasonMessage.class, (closeReasonMessage, channelContext) ->
// {
// LOGGER.info(closeReasonMessage.reason);
// this.connectionState = EConnectionState.CLOSE_WAIT;
// });
//
// this.registerHandler(CloseMessage.class, (closeMessage, channelContext) ->
// {
// LOGGER.info("Disconnected from server: "+channelContext.channel().remoteAddress());
// if (this.connectionState == EConnectionState.CLOSE_WAIT)
// {
// this.close();
// }
// });
// }
//
// private void connect()
// {
// LOGGER.info("Connecting to server: "+this.address);
// this.connectionState = EConnectionState.OPEN;
//
// // FIXME sometimes this causes the MC connection to crash
// // this might happen if the URL can't be converted to a IP (IE UnknownHostException)
// ChannelFuture connectFuture = this.clientBootstrap.connect(this.address);
// connectFuture.addListener((ChannelFuture channelFuture) ->
// {
// if (!channelFuture.isSuccess())
// {
// LOGGER.warn("Connection failed: "+channelFuture.cause());
// return;
// }
//
// this.channel.writeAndFlush(new HelloMessage());
// });
//
// this.channel = connectFuture.channel();
// this. channel.closeFuture().addListener((ChannelFuture channelFuture) ->
// {
// switch (this.connectionState)
// {
// case OPEN:
// this.reconnectAttempts--;
// LOGGER.info("Reconnection attempts left: ["+this.reconnectAttempts+"] of ["+FAILURE_RECONNECT_ATTEMPTS+"].");
// if (this.reconnectAttempts == 0)
// {
// this.connectionState = EConnectionState.CLOSE_WAIT;
// return;
// }
//
// this.connectionState = EConnectionState.RECONNECT;
// this.workerGroup.schedule(this::connect, FAILURE_RECONNECT_DELAY_SEC, TimeUnit.SECONDS);
// break;
//
// case RECONNECT_FORCE:
// LOGGER.info("Reconnecting forcefully.");
// this.reconnectAttempts = FAILURE_RECONNECT_ATTEMPTS;
//
// this.connectionState = EConnectionState.RECONNECT;
// this.workerGroup.schedule(this::connect, 0, TimeUnit.SECONDS);
// break;
// }
// });
// }
//
// /** Kills the current connection, triggering auto-reconnection immediately. */
// public void reconnect()
// {
// this.connectionState = EConnectionState.RECONNECT_FORCE;
// this.channel.disconnect();
// }
//
// @Override
// public void close()
// {
// if (this.closeReason != null)
// {
// LOGGER.error(this.closeReason);
// }
//
// if (this.connectionState == EConnectionState.CLOSED)
// {
// return;
// }
//
// this.connectionState = EConnectionState.CLOSED;
// this.workerGroup.shutdownGracefully().syncUninterruptibly();
// this.channel.close().syncUninterruptibly();
// }
}
@@ -1,82 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.network.messages.AckMessage;
import com.seibel.distanthorizons.core.network.messages.HelloMessage;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import com.seibel.distanthorizons.core.network.protocol.MessageHandler;
import com.seibel.distanthorizons.coreapi.ModInfo;
//import io.netty.channel.ChannelHandlerContext;
import org.apache.logging.log4j.Logger;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public abstract class NetworkEventSource implements AutoCloseable
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
protected final MessageHandler messageHandler = new MessageHandler();
protected String closeReason = null;
// public NetworkEventSource()
// {
// this.registerHandler(HelloMessage.class, (helloMessage, channelContext) ->
// {
// if (helloMessage.version != ModInfo.PROTOCOL_VERSION)
// {
// try
// {
// String closeReason = "Ignoring message from channel ["+channelContext.name()+"], due to version mismatch. Expected version: ["+ModInfo.PROTOCOL_VERSION+"], received version: ["+helloMessage.version+"].";
// LOGGER.info(closeReason);
// this.close(closeReason);
// }
// catch (Exception e)
// {
// throw new RuntimeException(e);
// }
// }
// });
// }
//
// public <T extends INetworkMessage> void registerHandler(Class<T> clazz, BiConsumer<T, ChannelHandlerContext> handler) { this.messageHandler.registerHandler(clazz, handler); }
//
// public <T extends INetworkMessage> void registerAckHandler(Class<T> clazz, Consumer<ChannelHandlerContext> handler)
// {
// this.messageHandler.registerHandler(AckMessage.class, (ackMessage, channelContext) ->
// {
// if (ackMessage.messageType == clazz)
// {
// handler.accept(channelContext);
// }
// });
// }
//
// public void close(String reason) throws Exception
// {
// this.closeReason = reason;
// this.close();
// }
}
@@ -1,122 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.network.messages.CloseReasonMessage;
import com.seibel.distanthorizons.core.network.messages.CloseMessage;
import com.seibel.distanthorizons.core.network.messages.HelloMessage;
import com.seibel.distanthorizons.core.network.protocol.NetworkChannelInitializer;
//import io.netty.bootstrap.ServerBootstrap;
//import io.netty.channel.*;
//import io.netty.channel.nio.NioEventLoopGroup;
//import io.netty.channel.socket.nio.NioServerSocketChannel;
//import io.netty.handler.logging.LogLevel;
//import io.netty.handler.logging.LoggingHandler;
import org.apache.logging.log4j.Logger;
public class NetworkServer //extends NetworkEventSource implements AutoCloseable
{
// private static final Logger LOGGER = DhLoggerBuilder.getLogger();
//
// // TODO move to the config
// private final int port;
//
// private final EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// private final EventLoopGroup workerGroup = new NioEventLoopGroup();
// private Channel channel;
// private boolean isClosed = false;
//
//
//
// public NetworkServer(int port)
// {
// this.port = port;
//
// LOGGER.info("Starting server on port "+port);
// this.registerHandlers();
// this.bind();
// }
//
// private void registerHandlers()
// {
// this.registerHandler(HelloMessage.class, (helloMessage, channelContext) ->
// {
// LOGGER.info("Client connected: "+channelContext.channel().remoteAddress());
// channelContext.channel().writeAndFlush(new HelloMessage());
// });
//
// this.registerHandler(CloseMessage.class, (closeMessage, channelContext) ->
// {
// LOGGER.info("Client disconnected: "+channelContext.channel().remoteAddress());
// });
// }
//
// private void bind()
// {
// ServerBootstrap bootstrap = new ServerBootstrap()
// .group(this.bossGroup, this.workerGroup)
// .channel(NioServerSocketChannel.class)
// .handler(new LoggingHandler(LogLevel.DEBUG))
// .childHandler(new NetworkChannelInitializer(this.messageHandler));
//
// ChannelFuture bindFuture = bootstrap.bind(this.port);
// bindFuture.addListener((ChannelFuture channelFuture) ->
// {
// if (!channelFuture.isSuccess())
// {
// throw new RuntimeException("Failed to bind: " + channelFuture.cause());
// }
//
// LOGGER.info("Server is started on port "+this.port);
// });
//
// this.channel = bindFuture.channel();
// this.channel.closeFuture().addListener(future -> this.close());
// }
//
// public void disconnectClient(ChannelHandlerContext ctx, String reason)
// {
// ctx.channel().config().setAutoRead(false);
// ctx.writeAndFlush(new CloseReasonMessage(reason))
// .addListener(ChannelFutureListener.CLOSE);
// }
//
// @Override
// public void close()
// {
// if (this.closeReason != null)
// {
// LOGGER.error(this.closeReason);
// }
//
// if (this.isClosed)
// {
// return;
// }
// this.isClosed = true;
//
// LOGGER.info("Shutting down the network server.");
// this.workerGroup.shutdownGracefully().syncUninterruptibly();
// this.bossGroup.shutdownGracefully().syncUninterruptibly();
// LOGGER.info("Network server has been closed.");
// }
}
@@ -0,0 +1,235 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.event;
import com.google.common.cache.CacheBuilder;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.network.event.internal.AbstractInternalEvent;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage;
import com.seibel.distanthorizons.core.network.messages.MessageRegistry;
import com.seibel.distanthorizons.core.network.session.SessionClosedException;
import com.seibel.distanthorizons.core.network.messages.requests.CancelMessage;
import com.seibel.distanthorizons.core.network.messages.requests.ExceptionMessage;
import com.seibel.distanthorizons.coreapi.ModInfo;
import org.apache.logging.log4j.LogManager;
import java.io.InvalidClassException;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.*;
import java.util.function.Consumer;
public abstract class AbstractNetworkEventSource
{
private static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Logging.logNetworkEvent.get());
/**
* Contains all message handlers. <br>
* Grouped by: <br>
* - {@link AbstractNetworkMessage} type <br>
* - {@link AbstractNetworkEventSource} <br>
*/
private final ConcurrentHashMap<
Class<? extends AbstractNetworkMessage>,
ConcurrentMap<AbstractNetworkEventSource, Set<INetworkMessageConsumer>>
> networkHandlerSetByMessageClass = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, FutureResponseData> pendingFutureById = new ConcurrentHashMap<>();
/** automatically forgets about ID's after a set time span. */
private final Set<Long> cancelledFutureIdSet = Collections.newSetFromMap(
CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.<Long, Boolean>build()
.asMap());
//=============//
// constructor //
//=============//
protected void handleMessage(AbstractNetworkMessage message)
{
boolean handled = false;
ConcurrentMap<AbstractNetworkEventSource, Set<INetworkMessageConsumer>> handlersByEventSource = this.networkHandlerSetByMessageClass.get(message.getClass());
if (handlersByEventSource != null)
{
for (Set<INetworkMessageConsumer> handlerSet : handlersByEventSource.values())
{
for (INetworkMessageConsumer handler : handlerSet)
{
handled = true;
handler.accept(message);
}
}
}
if (message instanceof AbstractTrackableMessage)
{
AbstractTrackableMessage trackableMessage = (AbstractTrackableMessage) message;
FutureResponseData responseData = this.pendingFutureById.get(trackableMessage.futureId);
if (responseData != null)
{
handled = true;
if (message instanceof ExceptionMessage)
{
responseData.future.completeExceptionally(((ExceptionMessage) message).exception);
}
else if (message.getClass() != responseData.responseClass)
{
responseData.future.completeExceptionally(new InvalidClassException("Response with invalid type: expected " + responseData.responseClass.getSimpleName() + ", got:" + message));
}
else
{
responseData.future.complete(trackableMessage);
}
}
else if (this.cancelledFutureIdSet.remove(trackableMessage.futureId))
{
handled = true;
}
}
if (!handled && ModInfo.IS_DEV_BUILD)
{
LOGGER.warn("Unhandled message: [{}].", message);
}
}
//==================//
// abstract methods //
//==================//
public abstract <T extends AbstractNetworkMessage> void registerHandler(Class<T> handlerClass, Consumer<T> handlerImplementation);
//==================//
// message handlers //
//==================//
protected final <T extends AbstractNetworkMessage> void registerHandler(AbstractNetworkEventSource eventSource, Class<T> handlerClass, Consumer<T> handlerImplementation)
{
if (!AbstractInternalEvent.class.isAssignableFrom(handlerClass))
{
MessageRegistry.INSTANCE.getMessageId(handlerClass);
}
//noinspection unchecked
this.networkHandlerSetByMessageClass
.computeIfAbsent(handlerClass, missingHandlerClass -> new ConcurrentHashMap<>())
.computeIfAbsent(eventSource, missingEventSource -> ConcurrentHashMap.newKeySet())
.add((m) -> handlerImplementation.accept((T) m));
}
protected void removeAllHandlers(AbstractNetworkEventSource eventSource)
{
for (ConcurrentMap<AbstractNetworkEventSource, Set<INetworkMessageConsumer>> handlerMap : this.networkHandlerSetByMessageClass.values())
{
handlerMap.remove(eventSource);
}
}
//================//
// create message //
//================//
protected <TResponse extends AbstractTrackableMessage> CompletableFuture<TResponse> createRequest(AbstractTrackableMessage msg, Class<TResponse> responseClass)
{
CompletableFuture<TResponse> responseFuture = new CompletableFuture<>();
responseFuture.whenComplete((response, throwable) ->
{
if (throwable instanceof CancellationException)
{
this.cancelledFutureIdSet.add(msg.futureId);
msg.sendResponse(new CancelMessage());
}
if (!(throwable instanceof SessionClosedException))
{
this.pendingFutureById.remove(msg.futureId);
}
});
this.pendingFutureById.put(msg.futureId, new FutureResponseData(responseClass, responseFuture));
return responseFuture;
}
//==========//
// shutdown //
//==========//
public void close()
{
this.networkHandlerSetByMessageClass.clear();
this.completeAllFuturesExceptionally(new SessionClosedException(this.getClass().getSimpleName() + " is closed."));
}
private void completeAllFuturesExceptionally(Throwable cause)
{
for (FutureResponseData responseData : this.pendingFutureById.values())
{
responseData.future.completeExceptionally(cause);
}
}
//================//
// helper classes //
//================//
private static class FutureResponseData
{
public final Class<? extends AbstractTrackableMessage> responseClass;
public final CompletableFuture<AbstractTrackableMessage> future;
private <T extends AbstractTrackableMessage> FutureResponseData(Class<T> responseClass, CompletableFuture<T> future)
{
this.responseClass = responseClass;
//noinspection unchecked
this.future = (CompletableFuture<AbstractTrackableMessage>) future;
}
}
/** Simple wrapper just to make the code here a bit more readable */
@FunctionalInterface
private interface INetworkMessageConsumer
{
void accept(AbstractNetworkMessage message);
}
}
@@ -0,0 +1,81 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.event;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import java.util.function.Consumer;
/**
* Provides a way to register network message handlers which are expected to be removed later. <br><br>
*
* In other words, listeners can be added to this {@link AbstractNetworkEventSource} and when
* you no longer need any of those listeners you can {@link ScopedNetworkEventSource#close()}
* this handler to remove all of them.
*/
public final class ScopedNetworkEventSource extends AbstractNetworkEventSource
{
public final AbstractNetworkEventSource parent;
private boolean isClosed = false;
private final Consumer<AbstractNetworkMessage> actualHandleMessageStable = this::handleMessage;
//=============//
// constructor //
//=============//
public ScopedNetworkEventSource(AbstractNetworkEventSource parent) { this.parent = parent; }
//==================//
// message handlers //
//==================//
@Override
public <T extends AbstractNetworkMessage> void registerHandler(Class<T> handlerClass, Consumer<T> handlerImplementation)
{
if (this.isClosed)
{
return;
}
//noinspection unchecked
this.parent.registerHandler(this, handlerClass, (Consumer<T>) this.actualHandleMessageStable);
super.registerHandler(this, handlerClass, handlerImplementation);
}
//==========//
// shutdown //
//==========//
@Override
public void close()
{
this.isClosed = true;
this.parent.removeAllHandlers(this);
}
}
@@ -0,0 +1,17 @@
package com.seibel.distanthorizons.core.network.event.internal;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import io.netty.buffer.ByteBuf;
/** internal events are messages sent from the client/sever back to themselves. */
public abstract class AbstractInternalEvent extends AbstractNetworkMessage
{
@Override
public void encode(ByteBuf out)
{ throw new UnsupportedOperationException(this.getClass().getSimpleName() + " is an internal event, and cannot be sent."); }
@Override
public void decode(ByteBuf in)
{ throw new UnsupportedOperationException(this.getClass().getSimpleName() + " is an internal event, and cannot be received."); }
}
@@ -0,0 +1,9 @@
package com.seibel.distanthorizons.core.network.event.internal;
/**
* This event is used to indicate a disconnect.
*/
public class CloseInternalEvent extends AbstractInternalEvent
{
}
@@ -0,0 +1,15 @@
package com.seibel.distanthorizons.core.network.event.internal;
/**
* This event is received instead of a message if its protocol version is incompatible with version the mod uses.
*/
public class IncompatibleMessageInternalEvent extends AbstractInternalEvent
{
public final int protocolVersion;
public IncompatibleMessageInternalEvent(int protocolVersion)
{
this.protocolVersion = protocolVersion;
}
}
@@ -0,0 +1,23 @@
package com.seibel.distanthorizons.core.network.event.internal;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import org.jetbrains.annotations.Nullable;
/**
* This event is used to indicate that encoding or decoding of a message threw an exception.
*/
public class ProtocolErrorInternalEvent extends AbstractInternalEvent
{
public final Throwable reason;
@Nullable
public final AbstractNetworkMessage message;
public final boolean replyWithCloseReason;
public ProtocolErrorInternalEvent(Throwable reason, @Nullable AbstractNetworkMessage message, boolean replyWithCloseReason)
{
this.reason = reason;
this.message = message;
this.replyWithCloseReason = replyWithCloseReason;
}
}
@@ -17,11 +17,10 @@
* 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.network.protocol; package com.seibel.distanthorizons.core.network.exceptions;
/** For now this is only used for constraining listeners */ /** Fired if a user attempts to run an operation in a level they aren't currently in. */
public interface INetworkMessage extends INetworkObject public class InvalidLevelException extends Exception
{ {
public InvalidLevelException(String message) { super(message); }
} }
@@ -17,12 +17,11 @@
* 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.level; package com.seibel.distanthorizons.core.network.exceptions;
import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider; /** Fired if the client attempts to queue more tasks than the server is willing to handle. */
public class RateLimitedException extends Exception
public interface IDhWorldGenLevel extends IDhLevel, GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener
{ {
void doWorldGen(); public RateLimitedException(String message) { super(message); }
} }
@@ -0,0 +1,11 @@
package com.seibel.distanthorizons.core.network.exceptions;
/**
* Fired if the client attempts an operation currently forbidden by the server. <Br>
* For example attempting to request LODs when world generation is disabled on the server.
*/
public class RequestRejectedException extends Exception
{
public RequestRejectedException(String message) { super(message); }
}
@@ -0,0 +1,31 @@
package com.seibel.distanthorizons.core.network.messages;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
/** Any new implementing classes should be registered in {@link MessageRegistry} */
public abstract class AbstractNetworkMessage implements INetworkObject
{
//============//
// properties //
//============//
private NetworkSession networkSession = null;
public NetworkSession getSession() { return this.networkSession; }
public void setSession(NetworkSession networkSession) { this.networkSession = networkSession; }
public IServerPlayerWrapper serverPlayer() { return this.networkSession.serverPlayer; }
//================//
// base overrides //
//================//
@Override
public String toString() { return this.toStringHelper().toString(); }
public MoreObjects.ToStringHelper toStringHelper() { return MoreObjects.toStringHelper(this); }
}
@@ -0,0 +1,162 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.network.messages.requests.ExceptionMessage;
import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.world.EWorldEnvironment;
import io.netty.buffer.ByteBuf;
import java.util.concurrent.atomic.AtomicInteger;
public abstract class AbstractTrackableMessage extends AbstractNetworkMessage
{
/** Tracks every message we've sent */
private static final AtomicInteger LAST_MESSAGE_ID_REF = new AtomicInteger();
/**
* 32 bits - NetworkSession ID (not transmitted) <br>
* 1 bit - Requesting side (client - 0, server - 1) <br>
* 31 bits - Request/Message ID <br><br>
*
* SI = NetworkSession ID <br>
* CS = Client/Server flag <br>
* MI = Request/Message ID <br><br>
*
* <code>
* =======Bit layout======= <br>
* SI SI SI SI SI SI SI SI <-- Top bits <br>
* SI SI SI SI SI SI SI SI <br>
* SI SI SI SI SI SI SI SI <br>
* SI SI SI SI SI SI SI SI <br>
* CS MI MI MI MI MI MI MI <br>
* MI MI MI MI MI MI MI MI <br>
* MI MI MI MI MI MI MI MI <br>
* MI MI MI MI MI MI MI MI <-- Bottom bits <br>
* </code>
*/
public long futureId;
//=============//
// constructor //
//=============//
public AbstractTrackableMessage()
{
EWorldEnvironment worldEnvironment = SharedApi.getEnvironment();
LodUtil.assertTrue(worldEnvironment != null, "Message can't be created if no world is loaded.");
// message/Request ID written as the least significant bits
long id = LAST_MESSAGE_ID_REF.getAndIncrement();
// write requesting side at bit 32
id |= ((worldEnvironment == EWorldEnvironment.Server_Only) ? 1 : 0) << 31;
this.futureId = id;
}
//==================//
// abstract methods //
//==================//
protected abstract void encodeInternal(ByteBuf out) throws Exception;
protected abstract void decodeInternal(ByteBuf in) throws Exception;
//=================//
// getters/setters //
//=================//
@Override
public void setSession(NetworkSession networkSession)
{
super.setSession(networkSession);
// Session ID is written in the most significant bits
this.futureId |= (long) networkSession.id << 32;
}
//==============//
// send message //
//==============//
public void sendResponse(AbstractTrackableMessage responseMessage)
{
responseMessage.futureId = this.futureId;
this.getSession().sendMessage(responseMessage);
}
public void sendResponse(Exception e) { this.sendResponse(new ExceptionMessage(e)); }
//=============//
// serializing //
//=============//
@Override
public final void encode(ByteBuf out)
{
try
{
out.writeInt((int) this.futureId);
this.encodeInternal(out);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
@Override
public final void decode(ByteBuf in)
{
try
{
this.futureId = in.readInt();
this.decodeInternal(in);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
//================//
// base overrides //
//================//
@Override
public MoreObjects.ToStringHelper toStringHelper()
{
return super.toStringHelper()
.add("futureId", this.futureId);
}
}
@@ -1,45 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import com.seibel.distanthorizons.core.network.protocol.MessageRegistry;
//import io.netty.buffer.ByteBuf;
/**
* Simple empty response message.
* This message is not sent automatically.
*/
public class AckMessage implements INetworkMessage
{
public Class<? extends INetworkMessage> messageType;
public AckMessage() { }
public AckMessage(Class<? extends INetworkMessage> messageType) { this.messageType = messageType; }
// @Override
// public void encode(ByteBuf out) { out.writeInt(MessageRegistry.INSTANCE.getMessageId(this.messageType)); }
//
// @Override
// public void decode(ByteBuf in) { this.messageType = MessageRegistry.INSTANCE.getMessageClassById(in.readInt()); }
}
@@ -1,38 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
//import io.netty.buffer.ByteBuf;
/**
* This is not a "real" message, and only used to indicate a disconnection.
* To send a "disconnect reason" message, use {@link CloseReasonMessage}.
*/
public class CloseMessage implements INetworkMessage
{
// @Override
// public void encode(ByteBuf out) { throw new UnsupportedOperationException("CloseMessage is not a real message, and must not be sent."); }
//
// @Override
// public void decode(ByteBuf in) { throw new UnsupportedOperationException("CloseMessage is not a real message, and must not be received."); }
}
@@ -1,41 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import com.seibel.distanthorizons.core.network.protocol.INetworkObject;
//import io.netty.buffer.ByteBuf;
public class CloseReasonMessage implements INetworkMessage
{
public String reason;
public CloseReasonMessage() { }
public CloseReasonMessage(String reason) { this.reason = reason; }
// @Override
// public void encode(ByteBuf out) { INetworkObject.encodeString(this.reason, out); }
//
// @Override
// public void decode(ByteBuf in) { this.reason = INetworkObject.decodeString(in); }
}
@@ -1,38 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import com.seibel.distanthorizons.coreapi.ModInfo;
//import io.netty.buffer.ByteBuf;
public class HelloMessage implements INetworkMessage
{
public int version = ModInfo.PROTOCOL_VERSION;
// @Override
// public void encode(ByteBuf out) { out.writeInt(this.version); }
//
// @Override
// public void decode(ByteBuf in) { this.version = in.readInt(); }
}
@@ -0,0 +1,23 @@
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
/** Implemented by messages that handle level data */
public interface ILevelRelatedMessage
{
String getLevelName();
/** Checks whether the message's level matches the given level. */
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
default boolean isSameLevelAs(ILevelWrapper levelWrapper)
{
if (levelWrapper instanceof IServerLevelWrapper)
{
return this.getLevelName().equals(((IServerLevelWrapper) levelWrapper).getKeyedLevelDimensionName());
}
return this.getLevelName().equals(levelWrapper.getDimensionName());
}
}
@@ -0,0 +1,129 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.seibel.distanthorizons.core.network.messages.base.CodecCrashMessage;
import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage;
import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMessage;
import com.seibel.distanthorizons.core.network.messages.requests.CancelMessage;
import com.seibel.distanthorizons.core.network.messages.base.CloseReasonMessage;
import com.seibel.distanthorizons.core.network.messages.requests.ExceptionMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceResponseMessage;
import com.seibel.distanthorizons.coreapi.ModInfo;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
/** Keeps track of every {@link AbstractNetworkMessage} so they can be (De)serialized. */
public class MessageRegistry
{
public static final boolean DEBUG_CODEC_CRASH_MESSAGE = ModInfo.IS_DEV_BUILD;
public static final MessageRegistry INSTANCE = new MessageRegistry();
private final Map<Integer, Supplier<? extends AbstractNetworkMessage>> messageConstructorById = new HashMap<>();
private final BiMap<Class<? extends AbstractNetworkMessage>, Integer> messageClassById = HashBiMap.create();
//=============//
// constructor //
//=============//
private MessageRegistry()
{
// Note: Messages must have parameterless constructors
this.registerMessage(CloseReasonMessage.class, CloseReasonMessage::new);
// Level keys
this.registerMessage(CurrentLevelKeyMessage.class, CurrentLevelKeyMessage::new);
// Config (for full DH support)
this.registerMessage(SessionConfigMessage.class, SessionConfigMessage::new);
// Requests
this.registerMessage(CancelMessage.class, CancelMessage::new);
this.registerMessage(ExceptionMessage.class, ExceptionMessage::new);
// Full data requests & updates
this.registerMessage(FullDataSourceRequestMessage.class, FullDataSourceRequestMessage::new);
this.registerMessage(FullDataSourceResponseMessage.class, FullDataSourceResponseMessage::new);
this.registerMessage(FullDataPartialUpdateMessage.class, FullDataPartialUpdateMessage::new);
this.registerMessage(FullDataSplitMessage.class, FullDataSplitMessage::new);
// Debug messages are always last, and not included in release builds.
if (DEBUG_CODEC_CRASH_MESSAGE)
{
this.registerMessage(CodecCrashMessage.class, CodecCrashMessage::new);
}
}
//==================//
// message handling //
//==================//
public <T extends AbstractNetworkMessage> void registerMessage(Class<T> clazz, Supplier<T> supplier)
{
int id = this.messageConstructorById.size() + 1;
this.messageConstructorById.put(id, supplier);
this.messageClassById.put(clazz, id);
}
/** used when decoding messages */
public AbstractNetworkMessage createMessage(int messageId) throws IllegalArgumentException
{
try
{
return this.messageConstructorById.get(messageId).get();
}
catch (NullPointerException e)
{
throw new IllegalArgumentException("Invalid message ID: " + messageId);
}
}
//=========//
// getters //
//=========//
public int getMessageId(AbstractNetworkMessage message) { return this.getMessageId(message.getClass()); }
public int getMessageId(Class<? extends AbstractNetworkMessage> messageClass)
{
try
{
return this.messageClassById.get(messageClass);
}
catch (NullPointerException e)
{
throw new IllegalArgumentException("Message does not have ID assigned to it: [" + messageClass.getSimpleName() + "].");
}
}
}
@@ -1,46 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
//import io.netty.buffer.ByteBuf;
import java.util.UUID;
public class PlayerUUIDMessage implements INetworkMessage
{
public UUID playerUUID;
public PlayerUUIDMessage() { }
public PlayerUUIDMessage(UUID playerUUID) { this.playerUUID = playerUUID; }
// @Override
// public void encode(ByteBuf out)
// {
// out.writeLong(this.playerUUID.getMostSignificantBits());
// out.writeLong(this.playerUUID.getLeastSignificantBits());
// }
//
// @Override
// public void decode(ByteBuf in) { this.playerUUID = new UUID(in.readLong(), in.readLong()); }
}
@@ -1,42 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages;
import com.seibel.distanthorizons.core.network.protocol.INetworkObject;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage;
import com.seibel.distanthorizons.core.network.objects.RemotePlayer;
//import io.netty.buffer.ByteBuf;
public class RemotePlayerConfigMessage implements INetworkMessage
{
public RemotePlayer.Payload payload;
public RemotePlayerConfigMessage() { }
public RemotePlayerConfigMessage(RemotePlayer.Payload payload) { this.payload = payload; }
// @Override
// public void encode(ByteBuf out) { this.payload.encode(out); }
//
// @Override
// public void decode(ByteBuf in) { this.payload = INetworkObject.decode(new RemotePlayer.Payload(), in); }
}
@@ -0,0 +1,67 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages.base;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import io.netty.buffer.ByteBuf;
/**
* When the communication is about to be stopped, either side can send this message
* There may be messages after this, but they should be ignored if possible.
*/
public class CloseReasonMessage extends AbstractNetworkMessage
{
public String reason;
//==============//
// constructors //
//==============//
public CloseReasonMessage() { }
public CloseReasonMessage(String reason) { this.reason = reason; }
//===============//
// serialization //
//===============//
@Override
public void encode(ByteBuf out) { this.writeString(this.reason, out); }
@Override
public void decode(ByteBuf in) { this.reason = this.readString(in); }
//================//
// base overrides //
//================//
@Override
public MoreObjects.ToStringHelper toStringHelper()
{
return super.toStringHelper()
.add("reason", this.reason);
}
}
@@ -0,0 +1,86 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages.base;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import io.netty.buffer.ByteBuf;
public class CodecCrashMessage extends AbstractNetworkMessage
{
public ECrashPhase crashPhase;
//==============//
// constructors //
//==============//
public CodecCrashMessage() { }
public CodecCrashMessage(ECrashPhase crashPhase) { this.crashPhase = crashPhase; }
//===============//
// serialization //
//===============//
@Override
public void encode(ByteBuf out)
{
if (this.crashPhase == ECrashPhase.ENCODE)
{
throw new RuntimeException("encode force crash");
}
}
@Override
public void decode(ByteBuf in) { throw new RuntimeException("decode force crash"); }
//================//
// base overrides //
//================//
@Override
public MoreObjects.ToStringHelper toStringHelper()
{
return super.toStringHelper()
.add("crashPhase", this.crashPhase);
}
//================//
// helper classes //
//================//
/**
* ENCODE, <br>
* DECODE, <br>
*/
public enum ECrashPhase
{
ENCODE,
DECODE
}
}
@@ -0,0 +1,45 @@
package com.seibel.distanthorizons.core.network.messages.base;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import io.netty.buffer.ByteBuf;
public class CurrentLevelKeyMessage extends AbstractNetworkMessage
{
public String levelKey;
//==============//
// constructors //
//==============//
public CurrentLevelKeyMessage() { }
public CurrentLevelKeyMessage(String levelKey) { this.levelKey = levelKey; }
//===============//
// serialization //
//===============//
@Override
public void encode(ByteBuf out) { this.writeString(this.levelKey, out); }
@Override
public void decode(ByteBuf in) { this.levelKey = this.readString(in); }
//================//
// base overrides //
//================//
@Override
public MoreObjects.ToStringHelper toStringHelper()
{
return super.toStringHelper()
.add("levelKey", this.levelKey);
}
}
@@ -0,0 +1,67 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages.base;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig;
import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import io.netty.buffer.ByteBuf;
/** used for full DH support */
public class SessionConfigMessage extends AbstractNetworkMessage
{
public SessionConfig config;
//=============//
// constructor //
//=============//
public SessionConfigMessage() { }
public SessionConfigMessage(SessionConfig config) { this.config = config; }
//===============//
// serialization //
//===============//
@Override
public void encode(ByteBuf out) { this.config.encode(out); }
@Override
public void decode(ByteBuf in) { this.config = INetworkObject.decodeToInstance(new SessionConfig(), in); }
//================//
// base overrides //
//================//
@Override
public MoreObjects.ToStringHelper toStringHelper()
{
return super.toStringHelper()
.add("config", this.config);
}
}
@@ -0,0 +1,84 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages.fullData;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.network.messages.ILevelRelatedMessage;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import io.netty.buffer.ByteBuf;
public class FullDataPartialUpdateMessage extends AbstractNetworkMessage implements ILevelRelatedMessage
{
public FullDataPayload payload;
private String levelName;
@Override
public String getLevelName() { return this.levelName; }
//==============//
// constructors //
//==============//
public FullDataPartialUpdateMessage() { }
public FullDataPartialUpdateMessage(IServerLevelWrapper level, FullDataPayload payload)
{
this.levelName = level.getKeyedLevelDimensionName();
this.payload = payload;
}
//===============//
// serialization //
//===============//
@Override
public void encode(ByteBuf out)
{
this.writeString(this.levelName, out);
this.payload.encode(out);
}
@Override
public void decode(ByteBuf in)
{
this.levelName = this.readString(in);
this.payload = INetworkObject.decodeToInstance(new FullDataPayload(), in);
}
//================//
// base overrides //
//================//
@Override
public MoreObjects.ToStringHelper toStringHelper()
{
return super.toStringHelper()
.add("levelName", this.levelName)
.add("payload", this.payload);
}
}
@@ -0,0 +1,127 @@
package com.seibel.distanthorizons.core.network.messages.fullData;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.util.TimerUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
/**
* @see FullDataSplitMessage
*/
public class FullDataPayload implements INetworkObject
{
private static final AtomicInteger lastBufferId = new AtomicInteger();
// Reference counting is unreliable here for some reason so this is a "fix"
private static final Timer bufferCleanupTimer = TimerUtil.CreateTimer("FullDataBufferCleanupTimer");
public int dtoBufferId;
public ByteBuf dtoBuffer;
//==============//
// constructors //
//==============//
public FullDataPayload() { }
public FullDataPayload(@NotNull FullDataSourceV2 fullDataSource)
{
Objects.requireNonNull(fullDataSource);
this.dtoBufferId = lastBufferId.getAndIncrement();
try
{
EDhApiDataCompressionMode compressionMode = Config.Client.Advanced.LodBuilding.dataCompression.get();
FullDataSourceV2DTO dataSourceDto = FullDataSourceV2DTO.CreateFromDataSource(fullDataSource, compressionMode);
this.dtoBuffer = ByteBufAllocator.DEFAULT.buffer();
dataSourceDto.encode(this.dtoBuffer);
bufferCleanupTimer.schedule(new TimerTask()
{
@Override
public void run()
{
FullDataPayload.this.dtoBuffer.release();
}
}, 5000L);
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
//===============//
// serialization //
//===============//
@Override
public void encode(ByteBuf out)
{
out.writeInt(this.dtoBufferId);
}
@Override
public void decode(ByteBuf in)
{
this.dtoBufferId = in.readInt();
}
/**
* Used to send {@link FullDataPayload}'s since the data they contain may be larger
* than what a single packet could contain.
*
* @param payloadChunkSizeInBytes how many bytes can be sent in a single message
*/
public void splitAndSend(int payloadChunkSizeInBytes, Consumer<FullDataSplitMessage> sendMessageConsumer)
{
// chunk in this context means chunk of data, not a MC chunk
for (int payloadChunkNum = 0; ; payloadChunkNum++)
{
int offset = payloadChunkNum * payloadChunkSizeInBytes;
int actualChunkSize = Math.min(this.dtoBuffer.writerIndex() - offset, payloadChunkSizeInBytes);
if (actualChunkSize <= 0)
{
break;
}
FullDataSplitMessage chunk = new FullDataSplitMessage(this.dtoBufferId, payloadChunkNum == 0, this.dtoBuffer.slice(offset, actualChunkSize));
sendMessageConsumer.accept(chunk);
}
}
//================//
// base overrides //
//================//
@Override
public String toString()
{
return MoreObjects.toStringHelper(this)
.add("dtoBufferId", this.dtoBufferId)
.add("dtoBuffer", this.dtoBuffer)
.toString();
}
}
@@ -0,0 +1,96 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages.fullData;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.messages.ILevelRelatedMessage;
import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import io.netty.buffer.ByteBuf;
import javax.annotation.Nullable;
public class FullDataSourceRequestMessage extends AbstractTrackableMessage implements ILevelRelatedMessage
{
public long sectionPos;
/** Only present when requesting changes. */
@Nullable
public Long clientTimestamp;
private String levelName;
@Override
public String getLevelName() { return this.levelName; }
//==============//
// constructors //
//==============//
public FullDataSourceRequestMessage() {}
public FullDataSourceRequestMessage(ILevelWrapper levelWrapper, long sectionPos, @Nullable Long clientTimestamp)
{
this.levelName = levelWrapper.getDimensionName();
this.sectionPos = sectionPos;
this.clientTimestamp = clientTimestamp;
}
//===============//
// serialization //
//===============//
@Override
public void encodeInternal(ByteBuf out)
{
this.writeString(this.levelName, out);
out.writeLong(this.sectionPos);
if (this.writeOptional(out, this.clientTimestamp))
{
out.writeLong(this.clientTimestamp);
}
}
@Override
public void decodeInternal(ByteBuf in)
{
this.levelName = this.readString(in);
this.sectionPos = in.readLong();
this.clientTimestamp = this.readOptional(in, in::readLong);
}
//================//
// base overrides //
//================//
@Override
public MoreObjects.ToStringHelper toStringHelper()
{
return super.toStringHelper()
.add("levelName", this.levelName)
.add("sectionPos", this.sectionPos)
.add("clientTimestamp", this.clientTimestamp);
}
}
@@ -0,0 +1,83 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages.fullData;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage;
import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.Nullable;
/**
* Response message, containing the requested full data source,
* or null if requested in updates-only mode and the data was not updated.
*/
public class FullDataSourceResponseMessage extends AbstractTrackableMessage
{
@Nullable
public FullDataPayload payload;
//=============//
// constructor //
//=============//
public FullDataSourceResponseMessage() { }
public FullDataSourceResponseMessage(@Nullable FullDataPayload payload)
{
if (payload != null)
{
this.payload = payload;
}
}
//===============//
// serialization //
//===============//
@Override
public void encodeInternal(ByteBuf out)
{
if (this.writeOptional(out, this.payload))
{
this.payload.encode(out);
}
}
@Override
public void decodeInternal(ByteBuf in) { this.payload = this.readOptional(in, () -> INetworkObject.decodeToInstance(new FullDataPayload(), in)); }
//================//
// base overrides //
//================//
@Override
public MoreObjects.ToStringHelper toStringHelper()
{
return super.toStringHelper()
.add("payload", this.payload);
}
}
@@ -0,0 +1,94 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages.fullData;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import io.netty.buffer.ByteBuf;
/**
* Used to send part of a {@link FullDataPayload}.
*
* @see FullDataPayload
*/
public class FullDataSplitMessage extends AbstractNetworkMessage
{
public int bufferId;
public ByteBuf buffer;
public boolean isFirst;
//==============//
// constructors //
//==============//
public FullDataSplitMessage() { }
public FullDataSplitMessage(int bufferId, boolean isFirst, ByteBuf buffer)
{
this.bufferId = bufferId;
this.buffer = buffer;
this.isFirst = isFirst;
}
//===============//
// serialization //
//===============//
@Override
public void encode(ByteBuf out)
{
out.writeInt(this.bufferId);
out.writeInt(this.buffer.writerIndex());
out.writeBytes(this.buffer.readerIndex(0));
out.writeBoolean(this.isFirst);
}
@Override
public void decode(ByteBuf in)
{
this.bufferId = in.readInt();
int bufferSize = in.readInt();
this.buffer = in.readBytes(bufferSize);
this.isFirst = in.readBoolean();
}
//================//
// base overrides //
//================//
@Override
public MoreObjects.ToStringHelper toStringHelper()
{
return super.toStringHelper()
.add("bufferId", this.bufferId)
.add("buffer", this.buffer)
.add("isFirst", this.isFirst);
}
}
@@ -17,24 +17,20 @@
* 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.network.messages; package com.seibel.distanthorizons.core.network.messages.requests;
import com.seibel.distanthorizons.core.network.protocol.INetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage;
//import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
public class RequestChunksMessage implements INetworkMessage public class CancelMessage extends AbstractTrackableMessage
{ {
public CancelMessage() { }
// @Override
// public void encode(ByteBuf out)
// { @Override
// public void encodeInternal(ByteBuf out) { }
// } @Override
// public void decodeInternal(ByteBuf in) { }
// @Override
// public void decode(ByteBuf in)
// {
//
// }
} }
@@ -0,0 +1,88 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages.requests;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.exceptions.InvalidLevelException;
import com.seibel.distanthorizons.core.network.exceptions.RateLimitedException;
import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException;
import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage;
import io.netty.buffer.ByteBuf;
import java.util.ArrayList;
import java.util.List;
// TODO appears to be useless yelling at user
public class ExceptionMessage extends AbstractTrackableMessage
{
private static final List<Class<? extends Exception>> EXCEPTION_LIST = new ArrayList<Class<? extends Exception>>()
{{
// All exceptions here must include constructor: (String)
this.add(RateLimitedException.class);
this.add(InvalidLevelException.class);
this.add(RequestRejectedException.class);
}};
public Exception exception;
//==============//
// constructors //
//==============//
public ExceptionMessage() { }
public ExceptionMessage(Exception exception) { this.exception = exception; }
//===============//
// serialization //
//===============//
@Override
protected void encodeInternal(ByteBuf out)
{
out.writeInt(EXCEPTION_LIST.indexOf(this.exception.getClass()));
this.writeString(this.exception.getMessage(), out);
}
@Override
protected void decodeInternal(ByteBuf in) throws Exception
{
int id = in.readInt();
String message = this.readString(in);
this.exception = EXCEPTION_LIST.get(id).getDeclaredConstructor(String.class).newInstance(message);
}
//================//
// base overrides //
//================//
@Override
public MoreObjects.ToStringHelper toStringHelper()
{
return super.toStringHelper()
.add("exception", this.exception);
}
}
@@ -1,53 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.objects;
import com.seibel.distanthorizons.core.network.protocol.INetworkObject;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
//import io.netty.buffer.ByteBuf;
//import io.netty.channel.ChannelHandlerContext;
public class RemotePlayer
{
public IServerPlayerWrapper serverPlayer;
public Payload payload;
// public ChannelHandlerContext channelContext;
public RemotePlayer(IServerPlayerWrapper serverPlayer) { this.serverPlayer = serverPlayer; }
public static class Payload implements INetworkObject
{
// TODO Replace this example with useful fields,
// this should include any information the server needs to know about the connected client
public int renderDistance;
// @Override
// public void encode(ByteBuf out) { out.writeInt(this.renderDistance); }
//
// @Override
// public void decode(ByteBuf in) { this.renderDistance = in.readInt(); }
}
}
@@ -1,50 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2023 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.protocol;
//import io.netty.buffer.ByteBuf;
import java.nio.charset.StandardCharsets;
public interface INetworkObject
{
// void encode(ByteBuf out);
//
// void decode(ByteBuf in);
//
// static <T extends INetworkObject> T decode(T obj, ByteBuf inputByteBuf)
// {
// obj.decode(inputByteBuf);
// return obj;
// }
//
// static void encodeString(String inputString, ByteBuf outputByteBuf)
// {
// outputByteBuf.writeShort(inputString.length());
// outputByteBuf.writeBytes(inputString.getBytes(StandardCharsets.UTF_8));
// }
//
// static String decodeString(ByteBuf inputByteBuf)
// {
// int length = inputByteBuf.readShort();
// return inputByteBuf.readBytes(length).toString(StandardCharsets.UTF_8);
// }
}

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