Compare commits

..
184 Commits
Author SHA1 Message Date
Jim C K Flaten 0004539b41 Merge branch 'release/0.14.0' 2026-07-09 17:57:27 +02:00
Jim C K Flaten 9ef0f09dd2 Release 0.14.0. 2026-07-09 17:57:22 +02:00
Jim C K Flaten 79cb5aa302 Per-world material cache. 2026-07-03 17:05:48 +02:00
Jim C K Flaten 5e9760c3d1 Clean up discard logic. 2026-07-03 17:00:16 +02:00
Jim C K Flaten 637636b5e8 Pre-size ArrayLists. 2026-07-02 23:07:21 +02:00
Jim C K Flaten 0ffe620f92 More buffer defaults. 2026-07-02 22:24:35 +02:00
Jim C K Flaten 2bcf9c7ca9 Reduce buffer copies during message transmit. 2026-07-02 21:56:52 +02:00
Jim C K Flaten 222221f9dc Add missing copyright notice. 2026-07-02 16:54:50 +02:00
Jim C K Flaten d086458546 Ignore blocks outside section boundary. Assume okay. 2026-07-02 16:41:57 +02:00
Jim C K Flaten 12b1580535 Allocation reductions. 2026-07-02 16:17:35 +02:00
Matt Lavallee 81326b415d Optimize LOD compression by caching generation step and world compression type
Caches the compressed generation-step and world-compression-type blocks (which are identical constant bytes for every LOD) in static volatile fields so the LZMA2 encoder runs once for them instead of on every LOD, and it sizes the LZMA2 dictionary to the actual payload (clamped between DICT_SIZE_MIN and 1 MiB) rather than using the multi-MiB preset default, cutting per-call match-finder allocation by orders of magnitude with identical compressed results.
2026-06-27 01:16:54 +02:00
Jim C K Flaten 328684cf1d Make sure block state properties are ordered. 2026-06-24 22:56:47 +02:00
Jim C K Flaten d89cfbfec5 Reject with empty LOD. 2026-06-23 21:58:48 +02:00
Jim C K Flaten 413e2a6e70 Use the correct variables to identify chunks for unloading. 2026-06-22 12:18:43 +02:00
Jim C K Flaten ee385ea5af Iterate worlds to find by key. Fixes exception on level init requests under Spigot. 2026-06-22 00:50:18 +02:00
Jim C K Flaten 37db471c4f Improve game version detection. 2026-06-22 00:17:04 +02:00
Jim C K Flaten 3e9165b7a3 Allow real-time updates even if generation is disabled. 2026-06-21 19:40:55 +02:00
Jim C K Flaten da893ec1f0 Protocol v15 2026-06-07 00:23:06 +02:00
Jim C K Flaten 320c04aeb7 Use the requested world instead of current world. 2026-06-05 00:01:11 +02:00
Jim C K Flaten fb8f98c52f Support for DH's (currently broken) protocol v14. 2026-06-02 19:48:16 +02:00
Jim C K Flaten 99cee46ae5 Bump version numer 2026-05-10 13:51:14 +02:00
Jim C K Flaten a05ee01b8b Merge tag '0.13.1' into develop
0.13.1
2026-05-10 13:50:11 +02:00
Jim C K Flaten b6878aaaaf Merge branch 'release/0.13.1' 2026-05-10 13:50:03 +02:00
Jim C K Flaten 0fbc612eef Release 0.13.1 2026-05-10 13:49:19 +02:00
Jim C K Flaten da2dba88b1 Disable async chunk access for all platforms. 2026-05-10 13:46:08 +02:00
Jim C K Flaten 35822cb8a7 Bukkit has getVersion(), while getMinecraftVersion() is Paper only. 2026-05-10 13:16:09 +02:00
Jim C K Flaten 9625035edd Bump version number. 2026-04-19 01:31:19 +02:00
Jim C K Flaten ecda68771c Merge tag '0.13.0' into develop
0.13.0
2026-04-19 01:29:42 +02:00
Jim C K Flaten f7822bc204 Merge branch 'release/0.13.0' 2026-04-19 01:29:06 +02:00
Jim C K Flaten ca4a3a6bcc Release 0.13.0. 2026-04-19 01:28:33 +02:00
Jim C K Flaten a189487623 Shorter channel name, for compatibility with MC < 1.13. 2026-04-19 01:23:29 +02:00
Jim C K Flaten 721d4c2027 Add config option to ignore world height map. 2026-02-05 01:13:11 +01:00
Jim C K Flaten 577a45be5c Correct DataPoint.getStartY() unpacking. 2026-02-05 01:12:52 +01:00
Jim C K Flaten 8bc102a26f Add unit to config comments. 2026-01-22 20:02:06 +01:00
Jim C K Flaten 8aa9fa9c31 Be better behaved around ResultSets. 2026-01-05 21:25:49 +01:00
Jckf 3644051706 Don't warn if permission is denied. 2026-01-05 16:16:47 +01:00
Jckf 2786f4b8c7 Warn if a client has disabled Distant Generation. 2026-01-05 16:07:06 +01:00
Jim C K Flaten 930483ee0c Fix light not being sampled for top-most data point in a column. 2026-01-02 19:02:07 +01:00
Jim C K Flaten 64b24aba31 Use minY as default for 1.20.1 hack. 2026-01-02 15:43:21 +01:00
Jim C K Flaten 22447f5bd5 Fix a couple comment mistakes in the config file. 2026-01-02 03:16:44 +01:00
Jim C K Flaten 697ca18299 Workaround for 1.20.1 lighting. 2026-01-02 00:14:25 +01:00
Jim C K Flaten bef45fe732 Puase inside builder thread, and reject generation requests while paused. 2026-01-01 21:05:24 +01:00
Jim C K Flaten f60736f21e Call BStats shutdown on disable. 2026-01-01 20:51:32 +01:00
Jim C K Flaten a36102c16e Trigger updates for new chunks.
Updates are ignored if there isn't an LOD for that area already, so this will only affect server that use the "fake chunk" feature.
2025-12-21 22:59:57 +01:00
Jim C K Flaten ed98a7e0d4 Also track DH RDs. 2025-12-21 00:16:18 +01:00
Jim C K Flaten 1b4138f537 Report number of players with DH. 2025-12-18 20:40:22 +01:00
Jim C K Flaten a387c7730d Don't try to scan for beacon color above the build height limit. 2025-12-18 15:38:41 +01:00
Jim C K Flaten dbb9d8a2cf Bump version number. 2025-12-14 17:51:14 +01:00
Jim C K Flaten 3c76e8f229 Merge tag '0.12.0' into develop
0.12.0
2025-12-14 17:50:29 +01:00
Jim C K Flaten 967cd2e20d Merge branch 'release/0.12.0' 2025-12-14 17:50:19 +01:00
Jim C K Flaten 83c4a3f6a6 Release 0.12.0. 2025-12-14 17:49:41 +01:00
Jim C K Flaten c6b89e8794 Disable config console message. 2025-12-14 17:33:42 +01:00
Sooty 7725a8c0eb Use named threads
Fairly straightforward - use named threads to allow better observability when DHS is running.
2025-12-11 15:19:24 +01:00
Jim C K Flaten 79cc6e3218 Protocol v13. 2025-11-27 20:54:56 +01:00
Jim C K Flaten 15542e3a36 Hacky way to show protocol mismatch in chat. 2025-10-24 23:19:18 +02:00
Jim C K Flaten 275b495675 Server key should be empty string when not provided. 2025-10-24 23:09:49 +02:00
Jim C K Flaten dcf08ba286 Remove unused variable. 2025-10-19 19:25:56 +02:00
Jim C K Flaten 6532418b5b Implement server key feature. 2025-10-19 19:25:24 +02:00
Jim C K Flaten 16a68d802f Bump version number. 2025-10-19 19:06:47 +02:00
Jim C K Flaten f2a40d405d Merge tag '0.11.0' into develop
0.11.0
2025-10-19 19:06:19 +02:00
Jim C K Flaten c95f54cc0b Merge branch 'release/0.11.0' 2025-10-19 19:04:38 +02:00
Jim C K Flaten cdeb8f67c9 Release 0.11.0. 2025-10-19 19:04:01 +02:00
Jim C K Flaten 33f374fc41 ConcurrentHashMap disallows null values. Store -1 instead. 2025-09-12 00:28:05 +02:00
Jim C K Flaten 32f78afb39 Cast strings when setting settings. 2025-09-06 00:33:19 +02:00
Jim C K Flaten 53051b71e3 Optionally advertise DH to non-DH players. Disabled by default. 2025-09-06 00:16:51 +02:00
Jim C K Flaten 651fd09dde Implement delayed tasks. 2025-09-06 00:16:04 +02:00
Jckf a5709d96a4 Config command. 2025-08-18 22:45:16 +02:00
Jckf 6e11678a80 Benchmark command. 2025-08-18 21:17:26 +02:00
Jckf 30ea7d5438 Folia: Move LOD building out of region thread. 2025-08-16 03:06:44 +02:00
Jckf 6c2b77fe25 Simplify world event handling. 2025-08-09 22:37:38 +02:00
Jckf 24e2727bf9 Add missing import. 2025-08-09 22:37:24 +02:00
Koteinik Kerudion 54ddb357e3 Fix exception when dealing with BlockState events
With some events that use `BlockState`s and not `Block`s, and exception like this can occur:

```
[10:13:55 ERROR]: Could not pass event StructureGrowEvent to DHSupport v0.11.0-SNAPSHOT
java.lang.ClassCastException: class org.bukkit.craftbukkit.block.CapturedBlockState cannot be cast to class org.bukkit.block.Block (org.bukkit.craftbukkit.block.CapturedBlockState and org.bukkit.block.Block are in unnamed module of loader java.net.URLClassLoader @1dfe2924)
        at DistantHorizonsSupport-0.11.0-SNAPSHOT.jar/no.jckf.dhsupport.bukkit.handler.WorldHandler.handleUpdateEvent(WorldHandler.java:145) ~[DistantHorizonsSupport-0.11.0-SNAPSHOT.jar:?]
        at DistantHorizonsSupport-0.11.0-SNAPSHOT.jar/no.jckf.dhsupport.bukkit.handler.WorldHandler.lambda$registerUpdateListeners$0(WorldHandler.java:90) ~[DistantHorizonsSupport-0.11.0-SNAPSHOT.jar:?]
        at co.aikar.timings.TimedEventExecutor.execute(TimedEventExecutor.java:80) ~[folia-api-1.21.6-R0.1-SNAPSHOT.jar:?]
        at org.bukkit.plugin.RegisteredListener.callEvent(RegisteredListener.java:71) ~[folia-api-1.21.6-R0.1-SNAPSHOT.jar:?]
        at io.papermc.paper.plugin.manager.PaperEventManager.callEvent(PaperEventManager.java:54) ~[folia-1.21.6.jar:1.21.6-6-e4b0ae9]
        at io.papermc.paper.plugin.manager.PaperPluginManagerImpl.callEvent(PaperPluginManagerImpl.java:131) ~[folia-1.21.6.jar:1.21.6-6-e4b0ae9]
        at org.bukkit.plugin.SimplePluginManager.callEvent(SimplePluginManager.java:628) ~[folia-api-1.21.6-R0.1-SNAPSHOT.jar:?]
        at net.minecraft.world.level.block.SaplingBlock.advanceTree(SaplingBlock.java:74) ~[folia-1.21.6.jar:1.21.6-6-e4b0ae9]
        at net.minecraft.world.level.block.SaplingBlock.randomTick(SaplingBlock.java:49) ~[folia-1.21.6.jar:1.21.6-6-e4b0ae9]
        at net.minecraft.world.level.block.state.BlockBehaviour$BlockStateBase.randomTick(BlockBehaviour.java:883) ~[folia-1.21.6.jar:1.21.6-6-e4b0ae9]
        at net.minecraft.server.level.ServerLevel.optimiseRandomTick(ServerLevel.java:970) ~[folia-1.21.6.jar:1.21.6-6-e4b0ae9]
        at net.minecraft.server.level.ServerLevel.tickChunk(ServerLevel.java:1002) ~[folia-1.21.6.jar:1.21.6-6-e4b0ae9]
        at net.minecraft.server.level.ServerChunkCache.iterateTickingChunksFaster(ServerChunkCache.java:179) ~[folia-1.21.6.jar:1.21.6-6-e4b0ae9]
        at net.minecraft.server.level.ServerChunkCache.tickChunks(ServerChunkCache.java:632) ~[folia-1.21.6.jar:1.21.6-6-e4b0ae9]
        at net.minecraft.server.level.ServerChunkCache.tickChunks(ServerChunkCache.java:532) ~[folia-1.21.6.jar:1.21.6-6-e4b0ae9]
        at net.minecraft.server.level.ServerChunkCache.tick(ServerChunkCache.java:510) ~[folia-1.21.6.jar:1.21.6-6-e4b0ae9]
        at net.minecraft.server.level.ServerLevel.tick(ServerLevel.java:773) ~[folia-1.21.6.jar:1.21.6-6-e4b0ae9]
        at net.minecraft.server.MinecraftServer.tickChildren(MinecraftServer.java:1832) ~[folia-1.21.6.jar:1.21.6-6-e4b0ae9]
        at net.minecraft.server.MinecraftServer.tickServer(MinecraftServer.java:1667) ~[folia-1.21.6.jar:1.21.6-6-e4b0ae9]
        at io.papermc.paper.threadedregions.TickRegions$ConcreteRegionTickHandle.tickRegion(TickRegions.java:407) ~[folia-1.21.6.jar:1.21.6-6-e4b0ae9]
        at io.papermc.paper.threadedregions.TickRegionScheduler$RegionScheduleHandle.runTick(TickRegionScheduler.java:418) ~[folia-1.21.6.jar:1.21.6-6-e4b0ae9]
        at ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool$TickThreadRunner.run(SchedulerThreadPool.java:546) ~[concurrentutil-0.0.3.jar:?]
        at java.base/java.lang.Thread.run(Thread.java:1447) ~[?:?]
```

This MR fixes this.
2025-08-09 22:14:07 +02:00
Jckf 7d341959ae Improved beacon color detection. 2025-08-06 02:06:34 +02:00
Jckf ca53f19c87 Option to translate certain materials into other types of material. 2025-08-01 01:11:48 +02:00
Jckf 4f2a782254 Relax default update config. 2025-08-01 00:23:44 +02:00
Jckf e5d4ab61a0 Option to send a fake LOD instead of rejecting when chunk generation is disabled. 2025-07-31 02:16:05 +02:00
Jckf 375146229e Report success even if /dh is disabled. 2025-07-29 18:27:02 +02:00
Jckf 95839b85ce Option to disable /dh help text. 2025-07-28 23:31:28 +02:00
Jckf 0243f9bccb Run tasks sync if the we're already in the requested thread. 2025-07-24 01:36:30 +02:00
Jckf edfd1df1b9 Configurable LOD update events. 2025-07-24 00:49:12 +02:00
Jckf f068a2fa2b Fix version number. 2025-07-23 21:30:22 +02:00
Jim C K Flaten ffcfded127 Option to sample biome for every data point. 2025-07-20 02:12:49 +02:00
Jim C K Flaten 4cb0d86217 Clear query cache on error. 2025-07-19 14:55:46 +02:00
Jim C K Flaten 5ae9ae65f8 Remove left-overs from Maven. 2025-07-19 14:42:57 +02:00
Jim C K Flaten c1e0fd8de8 Biome debug command. 2025-07-19 14:35:18 +02:00
Jim C K Flaten 6e54fed39e Build a single jar using Gradle
Get rid of Maven, and build a single JAR now that we no longer need Manifold.
2025-07-19 01:59:56 +02:00
Jim C K Flaten a2239bba85 Minecraft 1.21.8. 2025-07-17 16:27:46 +02:00
Jim C K Flaten f962a0253e Weigh colors by position. 2025-07-17 15:09:58 +02:00
Jim C K Flaten ac0f4503de Beacon colors. 2025-07-17 01:55:19 +02:00
Jckf 27680c67ad Bump version number. 2025-07-13 01:09:49 +02:00
Jckf 0dacd96ab4 Merge tag '0.10.0' into develop
0.10.0
2025-07-13 01:09:15 +02:00
Jckf 50cbb129f8 Merge branch 'release/0.10.0' 2025-07-13 01:07:44 +02:00
Jckf dc47a2a5bb Relase 0.10.0. 2025-07-13 01:06:34 +02:00
Jckf 0c1db8ea56 Unload chunks after aborted load. 2025-07-06 20:56:36 +02:00
Jckf 2215709472 Improve beacon detection. 2025-07-06 20:48:57 +02:00
Jckf 659326d0d1 Remove dynamic API variable from artifact name. 2025-07-06 04:41:12 +02:00
Jckf 2a397afb16 Remove old Spigot dependency. 2025-07-06 04:35:12 +02:00
Jckf 0d96af636f Fix buffer ID increment 2025-07-06 00:46:44 +02:00
Jckf 14c2f69df5 Prune pings. 2025-07-05 23:06:13 +02:00
Jckf 4dd995f939 Improvements to pregen ETA. 2025-07-05 22:50:29 +02:00
Jckf f5d2fe1b1d Revert thread timeout. 2025-07-05 02:02:05 +02:00
Jckf 70c5b07ed2 Unbounded queue with 0 core threads does not work. 2025-07-04 00:34:33 +02:00
Jckf f7d771edb6 Update notification. 2025-07-03 17:58:43 +02:00
Jckf 48e3f4b7a2 Updates to readme. 2025-07-03 17:33:43 +02:00
Jckf fb8f10914d Improvements to Docker build process. 2025-07-03 17:24:40 +02:00
Jckf 10450a8455 DH now collapses Forge packet ID into the protocol version field. 2025-07-03 17:13:45 +02:00
Jckf ade3eeba1b Build for MC 1.21.7. 2025-07-02 23:48:09 +02:00
Jckf 040f4f034a Update notifications. Work in progress. 2025-07-02 23:46:31 +02:00
Jckf e183d8fd28 If nothing else; include seconds. 2025-07-01 22:31:29 +02:00
Jim C K Flaten 9a90ee4378 Remove border center scaling. Bump protocol version to 11. 2025-06-30 18:55:15 +02:00
Jckf 27a9549888 Shutdown idle threads. 2025-06-19 18:28:16 +02:00
Jckf c6cfc419b8 Configurable refresh interval. 2025-06-17 18:12:28 +02:00
Jim C K Flaten 4eed922a33 Disable boot time DB optimization. 2025-06-16 23:05:36 +02:00
Jim C K Flaten 60062a20f3 Migrate and optimize separately. 2025-06-08 19:34:32 +02:00
Jim C K Flaten baf8c526d8 Bump config file version for database_path, and throw if db init fails. 2025-06-08 14:12:26 +02:00
Jim C K Flaten 78e7486e33 Notify when distant generation is disabled through perms. 2025-06-08 14:06:50 +02:00
Jim C K Flaten c4b5f6c663 Status command. 2025-06-08 01:12:52 +02:00
Jim C K Flaten a5dfdf2102 Complete the last step too. 2025-06-05 23:39:11 +02:00
Jim C K Flaten 23e0708dbb Updates to pregen command. 2025-06-05 21:05:59 +02:00
Jim C K Flaten 2dac58ad27 Disable broken ystep feature. 2025-06-01 03:32:25 +02:00
Jim C K Flaten 4d62b2edb8 Reuse statements. 2025-05-31 20:02:51 +02:00
Jim C K Flaten 03516559ea Reload base config on disable to avoid empty instance of first start. 2025-05-31 18:12:49 +02:00
Jim C K Flaten 23062a7662 Configurable database file location. 2025-05-31 15:38:27 +02:00
Jim C K Flaten bc94dcff2c Trim command. 2025-05-31 02:31:11 +02:00
Jim C K Flaten 0da10a0e10 Increase default thread count to 4. 2025-05-29 17:17:47 +02:00
Jim C K Flaten a2c33442d5 Underfill for FastOverworldBuilder. 2025-05-29 14:33:35 +02:00
Jim C K Flaten 3f18fc17da Also reload base config. 2025-05-29 13:41:47 +02:00
Jim C K Flaten 11904d3b30 Clear DHS config before population. 2025-05-29 13:16:18 +02:00
Jim C K Flaten d736b7a5f0 Commands to pause/unpause all generation. 2025-05-29 01:50:37 +02:00
Jim C K Flaten b685ad5761 Make /dh feedback more helpful. 2025-05-29 01:07:41 +02:00
Jim C K Flaten 31fcb0f6ea Add comment about UID/GID. 2025-05-06 23:37:06 +02:00
Jim C K Flaten faa75beb24 Pregen should progress even when requests fail. 2025-04-30 20:38:57 +02:00
Jim C K Flaten 3596d3879f Bump version number. 2025-04-28 16:02:16 +02:00
Jim C K Flaten 757212a463 Merge tag '0.9.0' into develop
0.9.0
2025-04-28 16:01:51 +02:00
Jim C K Flaten 4b3f28ec9f Merge branch 'release/0.9.0' 2025-04-28 16:01:45 +02:00
Jim C K Flaten 3ef63a184a Release 0.9.0. 2025-04-28 16:01:40 +02:00
Jim C K Flaten ab5aab6b72 Fix data overwrite when setting mapping ID. 2025-04-27 02:02:00 +02:00
Jim C K Flaten ee8b0047e6 Fix signing bug in SectionPosition. 2025-04-27 00:51:19 +02:00
Jim C K Flaten 4dbc8f8a85 More encoding tests for DataPoint and SectionPosition. 2025-04-27 00:49:49 +02:00
Jim C K Flaten c97200db03 Tests for SectionPosition and DataPoint encode/decode. 2025-04-19 19:41:34 +02:00
Jim C K Flaten 32179bc793 Option to compile with Docker. 2025-04-18 00:29:27 +02:00
Jim C K Flaten 29c5fe311d Pack SectionPosition and DataPoint into longs. 2025-04-17 00:59:32 +02:00
Jim C K Flaten 3c6f60e8a6 Remove unused import. 2025-04-12 18:17:40 +02:00
Jim C K Flaten e93e633c81 Bind /dh too. 2025-04-12 18:17:28 +02:00
Jim C K Flaten 29f7193919 Actually let's not enable it by default. 2025-04-10 01:12:37 +02:00
Jim C K Flaten db41ee0807 Hacky fix for weird "underglow". 2025-04-10 00:54:36 +02:00
Jim C K Flaten a694885028 Replace spaces with underscores for world lookup #hack 2025-04-09 20:51:09 +02:00
Jckf c147e07168 Radius is given in blocks. 2025-04-08 15:42:05 +02:00
Jim C K Flaten e54ee9a1b1 Use artifact names that are similar to DH. 2025-04-02 22:06:57 +02:00
Jim C K Flaten e48a005c85 Better handling of string config values. 2025-04-02 21:56:46 +02:00
Jckf df3c6f5c78 This should be our world, not Bukkit's. 2025-04-01 13:26:05 +02:00
Jckf 10a64469ef Sanitize world names. 2025-04-01 13:00:51 +02:00
Jim C K Flaten 0749da55d9 Show a warning during startup if chunk generation is enabled. Can be disabled in config. 2025-03-31 19:28:24 +02:00
Jim C K Flaten dadeb77067 This is accessed from multiple threads. 2025-03-31 19:27:34 +02:00
Jim C K Flaten 2f7d159b19 This is the return of the map, not the compose. 2025-03-31 00:06:55 +02:00
Jim C K Flaten 5be954e3fb Housekeeping. 2025-03-30 23:29:11 +02:00
Jim C K Flaten 048762b63d Command to list worlds and their IDs. 2025-03-30 05:01:20 +02:00
Jim C K Flaten 5fe761d6bc Command takes block position, but generator works on section positions. 2025-03-30 04:24:43 +02:00
Jim C K Flaten c194bf35c9 Bring back option to disable chunk generation. 2025-03-30 04:23:28 +02:00
Jim C K Flaten 9b06cc0b49 Reserve the thread pool for builders. 2025-03-29 20:08:17 +01:00
Jim C K Flaten 0a53d59037 Correct coordinates for unload. 2025-03-29 02:34:21 +01:00
Jim C K Flaten 3f4ff533ee Run LOD composition and encoding in the executor pool. 2025-03-29 00:35:09 +01:00
Jim C K Flaten 8762ebbafe Math is hard. 2025-03-28 00:33:35 +01:00
Jim C K Flaten 36d884b583 More control over pregen, and more stats. 2025-03-28 00:24:48 +01:00
Jim C K Flaten 2ac627ebce Command feedback. 2025-03-27 23:22:39 +01:00
Jim C K Flaten 3c450482dd Add 1.21.5 module 2025-03-25 17:55:46 +01:00
Jim C K Flaten cc0718cf69 Build 1.21.5 on Gitlab 2025-03-25 17:53:16 +01:00
Jckf 1808fd0a92 1.21.5 probably 2025-03-25 17:42:35 +01:00
Jim C K Flaten 38ea041550 Add missing license headers 2025-03-25 03:24:36 +01:00
Jim C K Flaten 6060900428 Pregen command 2025-03-25 03:24:20 +01:00
BrainStone e999cf7ab2 Fix coordinate scaling for the client config
Implements #19
2025-03-24 22:41:36 +01:00
BrainStone 4f9e87d262 Implement using vanilla world border
Implements #19
2025-03-24 21:39:53 +01:00
Jim C K Flaten aac5e2540b Update file DhSupport.java 2025-03-19 16:24:42 +01:00
Jim C K Flaten a31fcfd8e4 Correct permission check. 2025-03-16 23:36:37 +01:00
Jim C K Flaten b94fa4b344 Fix method name collision. 2025-03-16 23:27:54 +01:00
Jim C K Flaten f962effb40 Clear config on unload. 2025-03-16 23:12:40 +01:00
Jim C K Flaten 91e2d6445e Reload command. 2025-03-16 23:05:53 +01:00
Jim C K Flaten 8faf75ff4a Option to disable DH via permissions. 2025-03-16 21:57:39 +01:00
Jim C K Flaten a58222b4f7 Update stale description. 2025-03-14 02:48:39 +01:00
Jim C K Flaten 6e18444164 Reject LOD requests when distant generation is disabled. 2025-03-14 02:44:57 +01:00
Jim C K Flaten c9c0277436 Skip update if real-time is disabled. 2025-03-14 02:40:43 +01:00
Jim C K Flaten 0443a6a400 Check that LOD exists before attempting update. 2025-03-14 02:38:57 +01:00
Jim C K Flaten f55d644144 Bump version number. 2025-03-08 16:57:04 +01:00
Jim C K Flaten 6c0b6ac300 Merge tag '0.8.1' into develop
0.8.1
2025-03-08 16:56:40 +01:00
72 changed files with 4263 additions and 1320 deletions

No files matched your search

+3 -4
View File
@@ -27,10 +27,9 @@ replay_pid*
/.idea
/*.iml
# Maven output
/dependency-reduced-pom.xml
/target
/jars
# Gradle output
/.gradle
/build
# Test server
/server
+6 -26
View File
@@ -1,49 +1,29 @@
include:
- template: Security/SAST.gitlab-ci.yml
variables:
MAVEN_OPTS: >-
-Dhttps.protocols=TLSv1.2
-Dmaven.repo.local=.m2/repository
-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN
-Dorg.slf4j.simpleLogger.showDateTime=true
-Djava.awt.headless=true
MAVEN_CLI_OPTS: >-
--batch-mode
--errors
--fail-at-end
--show-version
-DinstallAtEnd=true
-DdeployAtEnd=true
stages:
- build
- test
default:
image: maven:3.9-eclipse-temurin-21-alpine
image: eclipse-temurin:21
cache:
key: maven-repo-${CI_COMMIT_REF_SLUG}
key: gradle-${CI_COMMIT_REF_SLUG}
unprotect: true
paths:
- .m2/repository
- .gradle
build:
stage: build
parallel:
matrix:
- MC_VER: [ "1.16.5", "1.17.1", "1.18.2", "1.19.2", "1.19.4", "1.20.1", "1.20.2", "1.20.4", "1.20.6", "1.21.1", "1.21.3", "1.21.4", ]
script:
- mvn package --projects pom-${MC_VER}.xml ${MAVEN_CLI_OPTS}
- mv jars/*.jar ./
- ./gradlew build
- mv build/libs/*/*.jar ./
artifacts:
name: DHS-${MC_VER}-${CI_COMMIT_REF_SLUG}-${CI_JOB_STARTED_AT}
name: DistantHorizonsSupport-${CI_COMMIT_REF_SLUG}-${CI_JOB_STARTED_AT}
paths:
- "./*.jar"
+5 -7
View File
@@ -4,17 +4,15 @@
DH Support is a Bukkit/Spigot/Paper/Folia server plugin that transmits Level Of Detail (LOD) data to players with Distant Horizons installed. Distant Horizons _will_ work fine without this plugin, but then each client will have to be within normal view distance of chunks to load them, and they will not receive updates for distant chunks when they change.
## Installation
## Installing and configuring
Download the [latest release](https://gitlab.com/distant-horizons-team/distant-horizons-server-plugin/-/releases) and drop the JAR in your plugins folder, and you're done!
## Configuration
The default values should be pretty solid, but you may tweak them to better suit your specific needs. Everything you need to know should be in config.yml.
Please see [our wiki](https://gitlab.com/distant-horizons-team/distant-horizons-server-plugin/-/wikis/home) for details.
## Building
The project uses Maven, so just run `mvn` in the project directory to compile and package a new JAR.
This project uses Gradle.
`gradlew clean build`
## Contribution
+107
View File
@@ -0,0 +1,107 @@
plugins {
id 'java'
id 'com.gradleup.shadow' version '8.3.8'
}
group = 'no.jckf.dhs'
version = '0.14.0'
description = 'Distant Horizons Support'
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(21))
}
def paperVersions = [
'1.16.5',
]
repositories {
mavenCentral()
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
maven { url = 'https://jitpack.io/' }
}
configurations {
shade
implementation {
extendsFrom shade
}
}
dependencies {
compileOnly 'org.jetbrains:annotations:25.0.0'
implementation 'com.google.guava:guava:33.3.1-jre'
shade 'org.bstats:bstats-bukkit:3.1.0'
shade 'org.json:json:20240303'
shade 'org.tukaani:xz:1.10'
shade 'com.github.technicallycoded:FoliaLib:v0.4.3'
testImplementation 'junit:junit:4.11'
}
paperVersions.each { mcVersion ->
def taskSuffix = mcVersion.replace('.', '_')
def taskName = "buildJar_${taskSuffix}"
def outputDir = file("build/libs/$mcVersion")
def apiVersion = mcVersion.tokenize('.')[0..1].join('.')
def sourceSetName = "main"
// Custom processResources task per version
def processTask = tasks.register("processResources_${taskSuffix}", Copy) {
from(sourceSets[sourceSetName].resources.srcDirs)
into("$buildDir/resources-processed/$mcVersion")
include '**/*.yml'
expand(
version: project.version,
'mainClass': 'no.jckf.dhsupport.bukkit.DhSupportBukkitPlugin',
'mcApiVersion': apiVersion,
)
}
configurations {
"compileOnly${mcVersion}" {
canBeResolved = true
}
}
dependencies {
compileOnly "com.destroystokyo.paper:paper-api:${mcVersion}-R0.1-SNAPSHOT"
//compileOnly "io.papermc.paper:paper-api:${mcVersion}-R0.1-SNAPSHOT"
}
task(taskName, type: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
group = 'build'
archiveBaseName.set("DistantHorizonsSupport")
archiveVersion.set(version)
destinationDirectory.set(outputDir)
// Use processed resources as input
// Exclude raw YAML to avoid unprocessed plugin.yml in final JAR
from(sourceSets.main.output) {
exclude '**/*.yml'
}
from("$buildDir/resources-processed/$mcVersion")
configurations = [project.configurations.shade]
relocate 'org.bstats', 'no.jckf.dhsupport.bstats'
relocate 'org.json', 'no.jckf.dhsupport.json'
relocate 'org.tukaani.xz', 'no.jckf.dhsupport.xz'
relocate 'com.tcoded.folialib', 'no.jckf.dhsupport.folialib'
exclude 'org/jetbrains/**'
exclude 'org/intellij/**'
dependsOn processTask
}
build.dependsOn(taskName)
}
+2
View File
@@ -0,0 +1,2 @@
org.gradle.parallel=true
org.gradle.configureondemand=true
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+251
View File
@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
+94
View File
@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
-28
View File
@@ -1,28 +0,0 @@
<project>
<modelVersion>4.0.0</modelVersion>
<artifactId>DHSupport-${mc.api-version}${mc.api-version-patch}</artifactId>
<parent>
<groupId>no.jckf</groupId>
<artifactId>DHSupport</artifactId>
<version>${revision}</version>
<relativePath>./pom.xml</relativePath>
</parent>
<properties>
<skip-copy>false</skip-copy>
<mc.api-version>1.16</mc.api-version>
<mc.api-version-patch>.5</mc.api-version-patch>
<dhs.read-forge-byte>true</dhs.read-forge-byte>
</properties>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>${mc.api-version}${mc.api-version-patch}-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
-28
View File
@@ -1,28 +0,0 @@
<project>
<modelVersion>4.0.0</modelVersion>
<artifactId>DHSupport-${mc.api-version}${mc.api-version-patch}</artifactId>
<parent>
<groupId>no.jckf</groupId>
<artifactId>DHSupport</artifactId>
<version>${revision}</version>
<relativePath>./pom.xml</relativePath>
</parent>
<properties>
<skip-copy>false</skip-copy>
<mc.api-version>1.17</mc.api-version>
<mc.api-version-patch>.1</mc.api-version-patch>
<dhs.read-forge-byte>true</dhs.read-forge-byte>
</properties>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>${mc.api-version}${mc.api-version-patch}-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
-28
View File
@@ -1,28 +0,0 @@
<project>
<modelVersion>4.0.0</modelVersion>
<artifactId>DHSupport-${mc.api-version}${mc.api-version-patch}</artifactId>
<parent>
<groupId>no.jckf</groupId>
<artifactId>DHSupport</artifactId>
<version>${revision}</version>
<relativePath>./pom.xml</relativePath>
</parent>
<properties>
<skip-copy>false</skip-copy>
<mc.api-version>1.18</mc.api-version>
<mc.api-version-patch>.2</mc.api-version-patch>
<dhs.read-forge-byte>true</dhs.read-forge-byte>
</properties>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>${mc.api-version}${mc.api-version-patch}-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
-28
View File
@@ -1,28 +0,0 @@
<project>
<modelVersion>4.0.0</modelVersion>
<artifactId>DHSupport-${mc.api-version}${mc.api-version-patch}</artifactId>
<parent>
<groupId>no.jckf</groupId>
<artifactId>DHSupport</artifactId>
<version>${revision}</version>
<relativePath>./pom.xml</relativePath>
</parent>
<properties>
<skip-copy>false</skip-copy>
<mc.api-version>1.19</mc.api-version>
<mc.api-version-patch>.2</mc.api-version-patch>
<dhs.read-forge-byte>true</dhs.read-forge-byte>
</properties>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>${mc.api-version}${mc.api-version-patch}-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
-28
View File
@@ -1,28 +0,0 @@
<project>
<modelVersion>4.0.0</modelVersion>
<artifactId>DHSupport-${mc.api-version}${mc.api-version-patch}</artifactId>
<parent>
<groupId>no.jckf</groupId>
<artifactId>DHSupport</artifactId>
<version>${revision}</version>
<relativePath>./pom.xml</relativePath>
</parent>
<properties>
<skip-copy>false</skip-copy>
<mc.api-version>1.19</mc.api-version>
<mc.api-version-patch>.4</mc.api-version-patch>
<dhs.read-forge-byte>true</dhs.read-forge-byte>
</properties>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>${mc.api-version}${mc.api-version-patch}-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
-28
View File
@@ -1,28 +0,0 @@
<project>
<modelVersion>4.0.0</modelVersion>
<artifactId>DHSupport-${mc.api-version}${mc.api-version-patch}</artifactId>
<parent>
<groupId>no.jckf</groupId>
<artifactId>DHSupport</artifactId>
<version>${revision}</version>
<relativePath>./pom.xml</relativePath>
</parent>
<properties>
<skip-copy>false</skip-copy>
<mc.api-version>1.20</mc.api-version>
<mc.api-version-patch>.1</mc.api-version-patch>
<dhs.read-forge-byte>true</dhs.read-forge-byte>
</properties>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>${mc.api-version}${mc.api-version-patch}-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
-28
View File
@@ -1,28 +0,0 @@
<project>
<modelVersion>4.0.0</modelVersion>
<artifactId>DHSupport-${mc.api-version}${mc.api-version-patch}</artifactId>
<parent>
<groupId>no.jckf</groupId>
<artifactId>DHSupport</artifactId>
<version>${revision}</version>
<relativePath>./pom.xml</relativePath>
</parent>
<properties>
<skip-copy>false</skip-copy>
<mc.api-version>1.20</mc.api-version>
<mc.api-version-patch>.2</mc.api-version-patch>
<dhs.read-forge-byte>true</dhs.read-forge-byte>
</properties>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>${mc.api-version}${mc.api-version-patch}-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
-28
View File
@@ -1,28 +0,0 @@
<project>
<modelVersion>4.0.0</modelVersion>
<artifactId>DHSupport-${mc.api-version}${mc.api-version-patch}</artifactId>
<parent>
<groupId>no.jckf</groupId>
<artifactId>DHSupport</artifactId>
<version>${revision}</version>
<relativePath>./pom.xml</relativePath>
</parent>
<properties>
<skip-copy>false</skip-copy>
<mc.api-version>1.20</mc.api-version>
<mc.api-version-patch>.4</mc.api-version-patch>
<dhs.read-forge-byte>true</dhs.read-forge-byte>
</properties>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>${mc.api-version}${mc.api-version-patch}-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
-26
View File
@@ -1,26 +0,0 @@
<project>
<modelVersion>4.0.0</modelVersion>
<artifactId>DHSupport-${mc.api-version}</artifactId>
<parent>
<groupId>no.jckf</groupId>
<artifactId>DHSupport</artifactId>
<version>${revision}</version>
<relativePath>./pom.xml</relativePath>
</parent>
<properties>
<skip-copy>false</skip-copy>
<mc.api-version>1.20.6</mc.api-version>
</properties>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>${mc.api-version}-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
-26
View File
@@ -1,26 +0,0 @@
<project>
<modelVersion>4.0.0</modelVersion>
<artifactId>DHSupport-${mc.api-version}</artifactId>
<parent>
<groupId>no.jckf</groupId>
<artifactId>DHSupport</artifactId>
<version>${revision}</version>
<relativePath>./pom.xml</relativePath>
</parent>
<properties>
<skip-copy>false</skip-copy>
<mc.api-version>1.21.1</mc.api-version>
</properties>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>${mc.api-version}-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
-26
View File
@@ -1,26 +0,0 @@
<project>
<modelVersion>4.0.0</modelVersion>
<artifactId>DHSupport-${mc.api-version}</artifactId>
<parent>
<groupId>no.jckf</groupId>
<artifactId>DHSupport</artifactId>
<version>${revision}</version>
<relativePath>./pom.xml</relativePath>
</parent>
<properties>
<skip-copy>false</skip-copy>
<mc.api-version>1.21.3</mc.api-version>
</properties>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>${mc.api-version}-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
-26
View File
@@ -1,26 +0,0 @@
<project>
<modelVersion>4.0.0</modelVersion>
<artifactId>DHSupport-${mc.api-version}</artifactId>
<parent>
<groupId>no.jckf</groupId>
<artifactId>DHSupport</artifactId>
<version>${revision}</version>
<relativePath>./pom.xml</relativePath>
</parent>
<properties>
<skip-copy>false</skip-copy>
<mc.api-version>1.21.4</mc.api-version>
</properties>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>${mc.api-version}-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
-236
View File
@@ -1,236 +0,0 @@
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>no.jckf</groupId>
<artifactId>DHSupport</artifactId>
<version>${revision}</version>
<packaging>pom</packaging>
<properties>
<revision>0.8.1</revision>
<jdk>21</jdk>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.class>no.jckf.dhsupport.bukkit.DhSupportBukkitPlugin</main.class>
<mc.api-version></mc.api-version>
<mc.api-version-patch></mc.api-version-patch>
<dhs.read-forge-byte></dhs.read-forge-byte>
<skip-copy>true</skip-copy>
</properties>
<developers>
<developer>
<id>jckf</id>
<name>Jim C K Flaten</name>
<email>jckf@jckf.no</email>
</developer>
</developers>
<repositories>
<repository>
<id>central</id>
<url>https://repo1.maven.org/maven2/</url>
</repository>
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots</url>
</repository>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public</url>
</repository>
<repository>
<id>jitpack</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>3.1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.3.1-jre</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>25.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.tukaani</groupId>
<artifactId>xz</artifactId>
<version>1.10</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.technicallycoded</groupId>
<artifactId>FoliaLib</artifactId>
<version>v0.4.3</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<defaultGoal>clean package</defaultGoal>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${jdk}</source>
<target>${jdk}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<compilerArgs>
<arg>-Xplugin:Manifold</arg>
<arg>-AREAD_FORGE_BYTE=${dhs.read-forge-byte}</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>systems.manifold</groupId>
<artifactId>manifold-preprocessor</artifactId>
<version>2024.1.34</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<shadedArtifactAttached>false</shadedArtifactAttached>
<shadedClassifierName>all</shadedClassifierName>
<relocations>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>no.jckf.dhsupport.bstats</shadedPattern>
</relocation>
<relocation>
<pattern>com.tcoded.folialib</pattern>
<shadedPattern>no.jckf.dhsupport.folialib</shadedPattern>
</relocation>
</relocations>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*</exclude>
</excludes>
</filter>
</filters>
<artifactSet>
<includes>
<include>org.tukaani</include>
<include>org.bstats</include>
<include>com.github.technicallycoded</include>
</includes>
</artifactSet>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.8.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<outputDirectory>jars</outputDirectory>
<skip>${skip-copy}</skip>
<artifactItems>
<artifactItem>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<version>${project.version}</version>
<type>${project.packaging}</type>
<destFileName>DHS-${project.version}_for_MC-${mc.api-version}${mc.api-version-patch}.jar</destFileName>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
<filtering>true</filtering>
<targetPath>.</targetPath>
<includes>
<include>plugin.yml</include>
</includes>
</resource>
<resource>
<directory>${basedir}/src/main/resources</directory>
<targetPath>.</targetPath>
<includes>
<include>config.yml</include>
</includes>
</resource>
</resources>
</build>
<modules>
<module>pom-1.16.5.xml</module>
<module>pom-1.17.1.xml</module>
<module>pom-1.18.2.xml</module>
<module>pom-1.19.2.xml</module>
<module>pom-1.19.4.xml</module>
<module>pom-1.20.1.xml</module>
<module>pom-1.20.2.xml</module>
<module>pom-1.20.4.xml</module>
<module>pom-1.20.6.xml</module>
<module>pom-1.21.1.xml</module>
<module>pom-1.21.3.xml</module>
<module>pom-1.21.4.xml</module>
</modules>
</project>
+1
View File
@@ -0,0 +1 @@
rootProject.name = 'DistantHorizonsSupport'
@@ -38,7 +38,7 @@ public class BukkitPluginMessageSender implements PluginMessageSender
{
Player player = this.plugin.getServer().getPlayer(recipientUuid);
if (player == null) {
if (player == null || !player.isOnline()) {
return;
}
@@ -25,9 +25,7 @@ import no.jckf.dhsupport.core.scheduling.Scheduler;
import org.bukkit.Location;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.*;
import java.util.function.Supplier;
public class BukkitScheduler implements Scheduler
@@ -38,13 +36,34 @@ public class BukkitScheduler implements Scheduler
protected ExecutorService executor;
protected ScheduledExecutorService scheduledExecutor;
public BukkitScheduler(DhSupportBukkitPlugin plugin)
{
this.plugin = plugin;
this.foliaLib = new FoliaLib(this.plugin);
this.executor = Executors.newFixedThreadPool(this.plugin.getDhSupport().getConfig().getInt(DhsConfig.SCHEDULER_THREADS));
int threadCount = this.plugin.getDhSupport().getConfig().getInt(DhsConfig.SCHEDULER_THREADS);
this.executor = new ThreadPoolExecutor(
threadCount, threadCount,
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new ThreadFactory() {
private final java.util.concurrent.atomic.AtomicInteger threadNumber = new java.util.concurrent.atomic.AtomicInteger(1);
@Override
public Thread newThread(Runnable r)
{
return new Thread(r, "DHSupport-Worker-" + threadNumber.getAndIncrement());
}
}
);
((ThreadPoolExecutor) this.executor).allowCoreThreadTimeOut(true);
this.scheduledExecutor = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "DHSupport-Scheduler"));
this.plugin.getDhSupport().info("Using " + Utils.ucFirst(this.foliaLib.getImplType().name().toLowerCase().replace('_', ' ')) + " scheduler.");
}
@@ -52,7 +71,7 @@ public class BukkitScheduler implements Scheduler
@Override
public boolean canReadWorldAsync()
{
return !this.foliaLib.isFolia();
return false;
}
@Override
@@ -60,13 +79,49 @@ public class BukkitScheduler implements Scheduler
{
CompletableFuture<U> future = new CompletableFuture<>();
this.foliaLib.getScheduler().runNextTick((task) -> {
if (this.foliaLib.getScheduler().isGlobalTickThread()) {
future.complete(supplier.get());
} else {
this.foliaLib.getScheduler().runNextTick((task) -> {
try {
future.complete(supplier.get());
} catch (Exception exception) {
future.completeExceptionally(exception);
}
});
}
return future;
}
@Override
public <U> CompletableFuture<U> runOnMainThreadDelayed(Supplier<U> supplier, long delayTicks)
{
CompletableFuture<U> future = new CompletableFuture<>();
this.foliaLib.getScheduler().runLater((task) -> {
try {
future.complete(supplier.get());
} catch (Exception exception) {
future.completeExceptionally(exception);
}
});
}, delayTicks);
return future;
}
@Override
public <U> CompletableFuture<U> runOnMainThreadDelayed(Supplier<U> supplier, long delayAmount, TimeUnit delayUnit)
{
CompletableFuture<U> future = new CompletableFuture<>();
this.foliaLib.getScheduler().runLater((task) -> {
try {
future.complete(supplier.get());
} catch (Exception exception) {
future.completeExceptionally(exception);
}
}, delayAmount, delayUnit);
return future;
}
@@ -83,17 +138,67 @@ public class BukkitScheduler implements Scheduler
z
);
this.foliaLib.getScheduler().runAtLocation(
location,
(task) -> {
try {
future.complete(supplier.get());
} catch (Exception exception) {
future.completeExceptionally(exception);
}
}
if (this.foliaLib.getScheduler().isOwnedByCurrentRegion(location)) {
future.complete(supplier.get());
} else {
this.foliaLib.getScheduler().runAtLocation(
location,
(task) -> {
try {
future.complete(supplier.get());
} catch (Exception exception) {
future.completeExceptionally(exception);
}
}
);
}
return future;
}
@Override
public <U> CompletableFuture<U> runOnRegionThreadDelayed(UUID worldId, int x, int z, Supplier<U> supplier, long delayTicks)
{
CompletableFuture<U> future = new CompletableFuture<>();
Location location = new Location(
this.plugin.getServer().getWorld(worldId),
x,
0,
z
);
this.foliaLib.getScheduler().runAtLocationLater(location, (task) -> {
try {
future.complete(supplier.get());
} catch (Exception exception) {
future.completeExceptionally(exception);
}
}, delayTicks);
return future;
}
@Override
public <U> CompletableFuture<U> runOnRegionThreadDelayed(UUID worldId, int x, int z, Supplier<U> supplier, long delayAmount, TimeUnit delayUnit)
{
CompletableFuture<U> future = new CompletableFuture<>();
Location location = new Location(
this.plugin.getServer().getWorld(worldId),
x,
0,
z
);
this.foliaLib.getScheduler().runAtLocationLater(location, (task) -> {
try {
future.complete(supplier.get());
} catch (Exception exception) {
future.completeExceptionally(exception);
}
}, delayAmount, delayUnit);
return future;
}
@@ -113,13 +218,46 @@ public class BukkitScheduler implements Scheduler
return future;
}
@Override
public <U> CompletableFuture<U> runOnSeparateThreadDelayed(Supplier<U> supplier, long delayTicks)
{
return this.runOnSeparateThreadDelayed(supplier, delayTicks * 50L, TimeUnit.MILLISECONDS);
}
@Override
public <U> CompletableFuture<U> runOnSeparateThreadDelayed(Supplier<U> supplier, long delayAmount, TimeUnit delayUnit)
{
CompletableFuture<U> future = new CompletableFuture<>();
this.scheduledExecutor.schedule(() -> {
this.runOnSeparateThread(() -> {
try {
future.complete(supplier.get());
} catch (Exception exception) {
future.completeExceptionally(exception);
}
return null;
});
}, delayAmount, delayUnit);
return future;
}
@Override
public void cancelTasks()
{
this.executor.shutdown();
this.scheduledExecutor.shutdownNow();
this.executor.shutdownNow();
this.foliaLib.getScheduler().cancelAllTasks();
}
@Override
public ExecutorService getExecutor()
{
return this.executor;
}
public void runTimer(Runnable runnable, long initialDelay, long interval)
{
this.foliaLib.getScheduler().runTimer(runnable, initialDelay, interval);
@@ -19,15 +19,16 @@
package no.jckf.dhsupport.bukkit;
import no.jckf.dhsupport.core.Coordinates;
import no.jckf.dhsupport.core.Utils;
import no.jckf.dhsupport.core.configuration.Configuration;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import no.jckf.dhsupport.core.configuration.WorldConfiguration;
import no.jckf.dhsupport.core.dataobject.Beacon;
import no.jckf.dhsupport.core.scheduling.Scheduler;
import no.jckf.dhsupport.core.world.MaterialRecord;
import no.jckf.dhsupport.core.world.WorldInterface;
import org.bukkit.*;
import org.bukkit.block.BlockState;
import javax.annotation.Nullable;
import java.awt.Color;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -51,7 +52,20 @@ public class BukkitWorldInterface implements WorldInterface
protected Logger logger;
protected Map<String, ChunkSnapshot> chunks = new HashMap<>();
protected boolean dummyMode = false;
protected Map<Integer, String> dummyColumn;
protected boolean trustHeightMap = true;
private Integer sectionX;
private Integer sectionZ;
private final ChunkSnapshot[] chunks = new ChunkSnapshot[16];
protected EnumMap<Material, MaterialRecord> materialCache = new EnumMap<>(Material.class);
protected Map<String, Map<String, String>> propertyCache = new HashMap<>();
protected UnsafeValues unsafeValues;
@@ -64,12 +78,17 @@ public class BukkitWorldInterface implements WorldInterface
@Nullable
protected Method getChunkAtAsync;
protected int lightOffset = 0;
public BukkitWorldInterface(DhSupportBukkitPlugin plugin, World world, Configuration config)
{
this.plugin = plugin;
this.world = world;
this.config = config;
this.worldConfig = new WorldConfiguration(this, config);
this.lightOffset = this.getConfig().getInt(DhsConfig.LIGHT_OFFSET_HACK, this.plugin.getDhSupport().getGameVersion().equals("1.20.1") ? this.world.getMinHeight() : 0);
this.trustHeightMap = this.getConfig().getBool(DhsConfig.TRUST_HEIGHT_MAP, this.trustHeightMap);
}
public void setLogger(Logger logger)
@@ -108,7 +127,7 @@ public class BukkitWorldInterface implements WorldInterface
}
try {
this.getChunkAtAsync = this.world.getClass().getMethod("getChunkAtAsyncUrgently", int.class, int.class);
this.getChunkAtAsync = this.world.getClass().getMethod("getChunkAtAsync", int.class, int.class, boolean.class);
} catch (NoSuchMethodException exception) {
if (!ASYNC_LOAD_WARNING_SENT) {
this.getLogger().warning("Async chunk loading is not supported on this server. Performance will suffer.");
@@ -135,19 +154,47 @@ public class BukkitWorldInterface implements WorldInterface
int chunkX = Coordinates.blockToChunk(x);
int chunkZ = Coordinates.blockToChunk(z);
String key = chunkX + "x" + chunkZ;
int sectionX = Coordinates.chunkToSection(chunkX);
int sectionZ = Coordinates.chunkToSection(chunkZ);
if (this.chunks.containsKey(key)) {
return this.chunks.get(key);
if (this.sectionX == null) {
this.sectionX = sectionX;
this.sectionZ = sectionZ;
} else if (this.sectionX != sectionX || this.sectionZ != sectionZ) {
throw new IllegalStateException("We appear to be leaking world interface instances across LOD sections");
}
ChunkSnapshot chunk = this.world.getChunkAt(chunkX, chunkZ).getChunkSnapshot(true, true, false);
int baseChunkX = Coordinates.sectionToChunk(sectionX);
int baseChunkZ = Coordinates.sectionToChunk(sectionZ);
this.chunks.put(key, chunk);
int index = (chunkX - baseChunkX) + ((chunkZ - baseChunkZ) << 2);
ChunkSnapshot chunk = this.chunks[index];
if (chunk != null) {
return chunk;
}
Scheduler scheduler = this.plugin.getDhSupport().getScheduler();
if (scheduler.canReadWorldAsync()) {
chunk = this.world.getChunkAt(chunkX, chunkZ).getChunkSnapshot(true, true, false);
} else {
chunk = scheduler.runOnRegionThread(world.getUID(), x, z, () ->
this.world.getChunkAt(chunkX, chunkZ).getChunkSnapshot(true, true, false)
).join();
}
this.chunks[index] = chunk;
return chunk;
}
public boolean isInsideSection(int x, int z)
{
return Coordinates.blockToSection(x) == this.sectionX && Coordinates.blockToSection(z) == this.sectionZ;
}
@Override
public UUID getId()
{
@@ -160,6 +207,12 @@ public class BukkitWorldInterface implements WorldInterface
return this.world.getName();
}
@Override
public String getKey()
{
return this.getName().replaceAll("[^a-zA-Z0-9_-]", "");
}
@Override
public double getCoordinateScale()
{
@@ -176,9 +229,110 @@ public class BukkitWorldInterface implements WorldInterface
}
}
protected boolean useVanillaWorldBorder()
{
return this.worldConfig.getBool(DhsConfig.USE_VANILLA_WORLD_BORDER, true);
}
@Override
public void setDummyMode(boolean enable)
{
this.dummyMode = enable;
}
@Override
public boolean isDummyMode()
{
return this.dummyMode;
}
protected Map<Integer, String> buildDummyColumn()
{
int minY = this.getMinY();
Map<Integer, String> column = new HashMap<>();
for (String spec : this.getConfig().getStringList(DhsConfig.DUMMY_CHUNK)) {
String[] components = spec.toLowerCase().split(",", 2);
String namespacedKey = Utils.namespaceKey(components[1]);
if (namespacedKey.equals("minecraft:air") || namespacedKey.equals("minecraft:void_air")) {
continue;
}
String[] range = components[0].split("-", 2);
int rangeStart = Integer.parseInt(range[0]);
int rangeEnd = range.length == 1 ? rangeStart : Integer.parseInt(range[1]);
for (int i = rangeStart; i <= rangeEnd; i++) {
column.put(minY + i, namespacedKey);
}
}
return column;
}
protected Map<Integer, String> getDummyColumn()
{
if (this.dummyColumn == null) {
this.dummyColumn = this.buildDummyColumn();
}
return this.dummyColumn;
}
protected String getDummyMaterialAt(int y)
{
return this.getDummyColumn().getOrDefault(y, "minecraft:air");
}
@Override
public Integer getWorldBorderX()
{
if (this.useVanillaWorldBorder()) {
return this.world.getWorldBorder().getCenter().getBlockX();
}
return this.worldConfig.getInt(DhsConfig.BORDER_CENTER_X);
}
@Override
public Integer getWorldBorderZ()
{
if (this.useVanillaWorldBorder()) {
return this.world.getWorldBorder().getCenter().getBlockZ();
}
return this.worldConfig.getInt(DhsConfig.BORDER_CENTER_Z);
}
@Override
public Integer getWorldBorderRadius()
{
if (!this.useVanillaWorldBorder()) {
return this.worldConfig.getInt(DhsConfig.BORDER_RADIUS);
}
int borderRadius = (int) (this.world.getWorldBorder().getSize() / 2.0);
if (this.worldConfig.getString(DhsConfig.VANILLA_WORLD_BORDER_EXPANSION, "auto").equals("auto")) {
borderRadius += Coordinates.chunkToBlock(this.world.getViewDistance());
} else {
borderRadius += Coordinates.chunkToBlock(this.worldConfig.getInt(DhsConfig.VANILLA_WORLD_BORDER_EXPANSION, 0));
}
return borderRadius;
}
@Override
public boolean chunkExists(int x, int z)
{
if (this.isDummyMode()) {
return true;
}
int chunkX = Coordinates.blockToChunk(x);
int chunkZ = Coordinates.blockToChunk(z);
@@ -209,6 +363,10 @@ public class BukkitWorldInterface implements WorldInterface
@Override
public boolean isChunkLoaded(int x, int z)
{
if (this.isDummyMode()) {
return true;
}
int chunkX = Coordinates.blockToChunk(x);
int chunkZ = Coordinates.blockToChunk(z);
@@ -218,17 +376,36 @@ public class BukkitWorldInterface implements WorldInterface
@Override
public boolean loadChunk(int x, int z)
{
if (this.isDummyMode()) {
return true;
}
int chunkX = Coordinates.blockToChunk(x);
int chunkZ = Coordinates.blockToChunk(z);
this.world.loadChunk(chunkX, chunkZ);
return this.world.loadChunk(chunkX, chunkZ, false);
}
return true;
@Override
public boolean loadOrGenerateChunk(int x, int z)
{
if (this.isDummyMode()) {
return true;
}
int chunkX = Coordinates.blockToChunk(x);
int chunkZ = Coordinates.blockToChunk(z);
return this.world.loadChunk(chunkX, chunkZ, true);
}
@Override
public CompletableFuture<Boolean> loadChunkAsync(int x, int z)
{
if (this.isDummyMode()) {
return CompletableFuture.completedFuture(true);
}
if (this.getChunkAtAsync == null) {
return this.plugin.getDhSupport().getScheduler().runOnMainThread(() -> this.loadChunk(x, z));
}
@@ -237,9 +414,32 @@ public class BukkitWorldInterface implements WorldInterface
int chunkZ = Coordinates.blockToChunk(z);
try {
CompletableFuture<Chunk> chunkFuture = (CompletableFuture<Chunk>) this.getChunkAtAsync.invoke(this.world, chunkX, chunkZ);
CompletableFuture<Chunk> chunkFuture = (CompletableFuture<Chunk>) this.getChunkAtAsync.invoke(this.world, chunkX, chunkZ, false);
return chunkFuture.thenApply((chunk) -> true);
return chunkFuture.thenApply(Objects::nonNull);
} catch (InvocationTargetException | IllegalAccessException exception) {
throw new RuntimeException(exception);
}
}
@Override
public CompletableFuture<Boolean> loadOrGenerateChunkAsync(int x, int z)
{
if (this.isDummyMode()) {
return CompletableFuture.completedFuture(true);
}
if (this.getChunkAtAsync == null) {
return this.plugin.getDhSupport().getScheduler().runOnMainThread(() -> this.loadOrGenerateChunk(x, z));
}
int chunkX = Coordinates.blockToChunk(x);
int chunkZ = Coordinates.blockToChunk(z);
try {
CompletableFuture<Chunk> chunkFuture = (CompletableFuture<Chunk>) this.getChunkAtAsync.invoke(this.world, chunkX, chunkZ, true);
return chunkFuture.thenApply(Objects::nonNull);
} catch (InvocationTargetException | IllegalAccessException exception) {
throw new RuntimeException(exception);
}
@@ -248,6 +448,10 @@ public class BukkitWorldInterface implements WorldInterface
@Override
public boolean unloadChunk(int x, int z)
{
if (this.isDummyMode()) {
return true;
}
int chunkX = Coordinates.blockToChunk(x);
int chunkZ = Coordinates.blockToChunk(z);
@@ -257,6 +461,10 @@ public class BukkitWorldInterface implements WorldInterface
@Override
public boolean unloadChunkAsync(int x, int z)
{
if (this.isDummyMode()) {
return true;
}
int chunkX = Coordinates.blockToChunk(x);
int chunkZ = Coordinates.blockToChunk(z);
@@ -266,6 +474,10 @@ public class BukkitWorldInterface implements WorldInterface
@Override
public boolean discardChunk(int x, int z)
{
if (this.isDummyMode()) {
return true;
}
int chunkX = Coordinates.blockToChunk(x);
int chunkZ = Coordinates.blockToChunk(z);
@@ -290,21 +502,51 @@ public class BukkitWorldInterface implements WorldInterface
return Math.min(Math.max(this.getMinY(), this.world.getSeaLevel()), this.getMaxY());
}
@Override
public int getSpawnX()
{
return this.world.getSpawnLocation().getBlockX();
}
@Override
public int getSpawnY()
{
return this.world.getSpawnLocation().getBlockY();
}
@Override
public int getSpawnZ()
{
return this.world.getSpawnLocation().getBlockZ();
}
@Override
public int getHighestYAt(int x, int z)
{
if (!this.trustHeightMap) {
return this.getMaxY() - 1;
}
if (this.isDummyMode()) {
return this.getDummyColumn().isEmpty() ? this.getMinY() : Collections.max(this.getDummyColumn().keySet());
}
return this.getChunk(x, z).getHighestBlockYAt(Coordinates.blockToChunkRelative(x), Coordinates.blockToChunkRelative(z));
}
@Override
public String getBiomeAt(int x, int z)
public String getBiomeAt(int x, int y, int z)
{
NamespacedKey key = this.getChunk(x, z).getBiome(Coordinates.blockToChunkRelative(x), this.getSeaLevel(), Coordinates.blockToChunkRelative(z)).getKey();
if (this.isDummyMode()) {
return "minecraft:plains";
}
NamespacedKey key = this.getChunk(x, z).getBiome(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z)).getKey();
// If the server just reports "custom" and we have access to getBiomeKey, try to get the correct biome name.
if (key.toString().equals("minecraft:custom") && this.getBiomeKey != null) {
try {
key = (NamespacedKey) this.getBiomeKey.invoke(this.unsafeValues, this.world, x, this.getSeaLevel(), z);
key = (NamespacedKey) this.getBiomeKey.invoke(this.unsafeValues, this.world, x, y, z);
} catch (IllegalAccessException | InvocationTargetException exception) {
throw new RuntimeException(exception);
}
@@ -313,77 +555,129 @@ public class BukkitWorldInterface implements WorldInterface
return key.toString();
}
@Override
public String getBiomeAt(int x, int z)
{
return this.getBiomeAt(x, this.getSeaLevel(), z);
}
@Override
public String getMaterialAt(int x, int y, int z)
{
return this.getChunk(x, z).getBlockType(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z)).getKey().toString();
if (this.isDummyMode()) {
return Utils.namespaceKey(this.getDummyMaterialAt(y));
}
return this.getMaterialRecordAt(x, y, z).toString();
}
public MaterialRecord getMaterialRecordAt(int x, int y, int z)
{
return this.materialCache.computeIfAbsent(
this.getChunk(x, z).getBlockType(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z)),
(mat) -> {
String key = Utils.unNamespaceKey(mat.getKey().toString());
String mapped = this.config.getString(DhsConfig.MATERIAL_MAP + '.' + key, key);
return new MaterialRecord(
Utils.namespaceKey(mapped == null ? key : mapped),
!key.equals(mapped)
);
}
);
}
@Override
public String getBlockStateAsStringAt(int x, int y, int z)
{
return this.getChunk(x, z).getBlockData(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z)).getAsString();
MaterialRecord materialRecord = this.getMaterialRecordAt(x, y, z);
if (this.isDummyMode() || materialRecord.isMapped()) {
return materialRecord.toString();
}
Map<String, String> properties = this.getBlockPropertiesAt(x, y, z);
if (properties.isEmpty()) {
return materialRecord.toString();
}
StringBuilder builder = new StringBuilder();
builder.append(materialRecord.toString())
.append('[');
for (Map.Entry<String, String> property : properties.entrySet()) {
builder.append(property.getKey())
.append('=')
.append(property.getValue())
.append(',');
}
builder.setLength(builder.length() - 1);
builder.append(']');
return builder.toString();
}
@Override
public Map<String, String> getBlockPropertiesAt(int x, int y, int z)
{
Map<String, String> properties = new HashMap<>();
if (this.isDummyMode()) {
return Collections.emptyMap();
}
String dataString = this.getBlockStateAsStringAt(x, y, z);
String dataString = this.getChunk(x, z).getBlockData(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z)).getAsString();
int kvStart = dataString.indexOf("[");
Map<String, String> cached = this.propertyCache.get(dataString);
if (cached != null) {
return cached;
}
int kvStart = dataString.indexOf('[');
if (kvStart == -1) {
return properties;
return Collections.emptyMap();
}
String[] kvStrings = dataString.substring(kvStart + 1, dataString.length() - 1).split(",");
Arrays.sort(kvStrings);
Map<String, String> properties = new LinkedHashMap<>(kvStrings.length);
for (String kvString : kvStrings) {
String[] kv = kvString.split("=", 2);
properties.put(kv[0], kv[1]);
}
properties = Collections.unmodifiableMap(properties);
this.propertyCache.put(dataString, properties);
return properties;
}
@Override
public byte getBlockLightAt(int x, int y, int z)
{
return (byte) this.getChunk(x, z).getBlockEmittedLight(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z));
if (this.isDummyMode()) {
return 0;
}
return (byte) this.getChunk(x, z).getBlockEmittedLight(Coordinates.blockToChunkRelative(x), Math.clamp(y + this.lightOffset, this.getMinY(), this.getMaxY() - 1), Coordinates.blockToChunkRelative(z));
}
@Override
public byte getSkyLightAt(int x, int y, int z)
{
return (byte) this.getChunk(x, z).getBlockSkyLight(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z));
}
@Override
public Collection<Beacon> getBeaconsInChunk(int x, int z)
{
Collection<Beacon> beacons = new ArrayList<>();
BlockState[] blocks = this.world.getChunkAt(Coordinates.blockToChunk(x), Coordinates.blockToChunk(z)).getTileEntities();
for (BlockState block : blocks) {
if (!block.getType().equals(Material.BEACON)) {
continue;
}
beacons.add(
new Beacon(
block.getX(),
block.getY(),
block.getZ(),
Color.WHITE.getRGB() // TODO?
)
);
if (this.isDummyMode()) {
return (byte) (this.getDummyMaterialAt(y + 1).equals("minecraft:air") ? 15 : 0);
}
return beacons;
return (byte) this.getChunk(x, z).getBlockSkyLight(Coordinates.blockToChunkRelative(x), Math.clamp(y + this.lightOffset, this.getMinY(), this.getMaxY() - 1), Coordinates.blockToChunkRelative(z));
}
@Override
@@ -391,4 +685,130 @@ public class BukkitWorldInterface implements WorldInterface
{
return this.worldConfig;
}
@Override
public boolean isBeacon(int x, int y, int z)
{
if (this.isDummyMode()) {
return false;
}
if (!this.getChunk(x, z).getBlockType(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z)).equals(Material.BEACON)) {
return false;
}
return this.hasTierOneBeaconBase(x, y, z) && this.hasClearBeaconBeamPath(x, y, z);
}
public boolean hasTierOneBeaconBase(int x, int y, int z)
{
int baseY = y - 1;
if (baseY < this.getMinY()) {
return false;
}
for (int checkX = x - 1; checkX <= x + 1; checkX++) {
for (int checkZ = z - 1; checkZ <= z + 1; checkZ++) {
if (this.isInsideSection(checkX, checkZ) && !this.isBeaconBaseBlock(this.getMaterialAt(checkX, baseY, checkZ))) {
return false;
}
}
}
return true;
}
private boolean isBeaconBaseBlock(String material)
{
switch (material) {
case "minecraft:iron_block":
case "minecraft:gold_block":
case "minecraft:diamond_block":
case "minecraft:emerald_block":
case "minecraft:netherite_block":
return true;
default:
return false;
}
}
public boolean hasClearBeaconBeamPath(int x, int y, int z)
{
for (int checkY = y + 1; checkY < this.getMaxY(); checkY++) {
String material = this.getMaterialAt(x, checkY, z);
switch (material) {
case "minecraft:air":
case "minecraft:cave_air":
case "minecraft:void_air":
case "minecraft:glass":
continue;
default:
return material.endsWith("_stained_glass");
}
}
return true;
}
@Override
public int getBeaconColor(int x, int y, int z)
{
int skipped = 0;
int n = 0;
int red = 0;
int green = 0;
int blue = 0;
int maxY = this.getMaxY();
while (y + 1 < maxY) {
y++;
String material = Utils.unNamespaceKey(this.getMaterialAt(x, y, z)).toUpperCase();
if (!material.endsWith("_STAINED_GLASS") && !material.endsWith("_STAINED_GLASS_PANE")) {
skipped++;
if (skipped >= 5) {
break;
}
continue;
}
n++;
Color color = null;
for (DyeColor testColor : DyeColor.values()) {
if (material.startsWith(testColor.name())) {
color = testColor.getColor();
break;
}
}
if (color == null) {
this.getLogger().warning("Encountered unknown color: " + material);
continue;
}
red += color.getRed();
green += color.getGreen();
blue += color.getBlue();
if (n > 1) {
red /= 2;
green /= 2;
blue /= 2;
}
}
if (n == 0) {
return Color.WHITE.asRGB();
}
return (new java.awt.Color(red, green, blue)).getRGB();
}
}
@@ -18,22 +18,30 @@
package no.jckf.dhsupport.bukkit;
import no.jckf.dhsupport.bukkit.commands.DhCommand;
import no.jckf.dhsupport.bukkit.commands.DhsCommand;
import no.jckf.dhsupport.bukkit.handler.ConfigLoader;
import no.jckf.dhsupport.bukkit.handler.PlayerHandler;
import no.jckf.dhsupport.bukkit.handler.PluginMessageProxy;
import no.jckf.dhsupport.bukkit.handler.WorldHandler;
import no.jckf.dhsupport.core.DhSupport;
import no.jckf.dhsupport.core.configuration.Configuration;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import org.bstats.bukkit.Metrics;
import org.bstats.charts.MultiLineChart;
import org.bstats.charts.SingleLineChart;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import javax.annotation.Nullable;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
public class DhSupportBukkitPlugin extends JavaPlugin
{
protected static int LOD_REFRESH_INTERVAL = 5 * 20;
protected DhSupport dhSupport;
protected Metrics metrics;
@@ -48,51 +56,71 @@ public class DhSupportBukkitPlugin extends JavaPlugin
public void onEnable()
{
String pluginVersion = "N/A";
String mcVersion = "N/A";
try {
YamlConfiguration meta = new YamlConfiguration();
meta.load(new InputStreamReader(this.getResource("plugin.yml")));
pluginVersion = meta.getString("version");
mcVersion = meta.getString("mc-version");
} catch (Exception exception) {
}
this.getLogger().info("I am DHS " + pluginVersion + " for MC " + mcVersion + ".");
String gameVersion = this.getGameVersion();
this.dhSupport = new DhSupport();
this.getLogger().info("I am DHS " + pluginVersion + " running on " + this.getServer().getName() + " for Minecraft " + gameVersion + ".");
this.dhSupport = new DhSupport(pluginVersion, gameVersion);
this.dhSupport.setLogger(this.getLogger());
this.dhSupport.setDataDirectory(this.getDataFolder().getAbsolutePath());
this.metrics = new Metrics(this, 21843);
this.metrics.addCustomChart(new SingleLineChart("dh_users", () -> this.dhSupport.getPlayerConfigurations().size()));
this.metrics.addCustomChart(new MultiLineChart("dh_render_distance", () -> {
Map<String, Integer> renderDistances = new HashMap<>();
this.configLoader = new ConfigLoader(this);
this.configLoader.onEnable();
for (Configuration playerConfig : this.dhSupport.getPlayerConfigurations().values()) {
String rd = String.valueOf(playerConfig.getInt(DhsConfig.RENDER_DISTANCE));
renderDistances.put(rd, renderDistances.getOrDefault(rd, 0) + 1);
}
return renderDistances;
}));
this.loadDhsConfig();
this.pluginMessageProxy = new PluginMessageProxy(this);
this.pluginMessageProxy.onEnable();
this.dhSupport.onEnable();
this.scheduler = new BukkitScheduler(this);
this.dhSupport.setScheduler(this.scheduler);
this.dhSupport.onEnable();
int lodRefreshInterval = this.getDhSupport().getConfig().getInt(DhsConfig.LOD_REFRESH_INTERVAL) * 20;
this.scheduler.runTimer(() -> {
this.dhSupport.getGenerationTracker().prune();
this.dhSupport.updateTouchedLods();
this.dhSupport.printGenerationCount();
}, LOD_REFRESH_INTERVAL, LOD_REFRESH_INTERVAL);
}, lodRefreshInterval, lodRefreshInterval);
this.getServer().getPluginManager().registerEvents(new WorldHandler(this), this);
this.getServer().getPluginManager().registerEvents(new PlayerHandler(this), this);
this.getCommand("dhs").setExecutor(new DhsCommand(this));
this.getCommand("dh").setExecutor(new DhCommand(this));
this.getLogger().info("Ready!");
}
@Override
public void onDisable()
{
if (this.metrics != null) {
this.metrics.shutdown();
}
if (this.scheduler != null) {
this.scheduler.cancelTasks();
}
@@ -115,6 +143,42 @@ public class DhSupportBukkitPlugin extends JavaPlugin
this.getLogger().info("Lights out!");
}
public void loadDhsConfig()
{
if (this.configLoader != null) {
this.configLoader.onDisable();
}
this.configLoader = new ConfigLoader(this);
this.configLoader.onEnable();
}
public @Nullable World getWorld(String name)
{
for (World world : this.getServer().getWorlds()) {
if (world.getName().replace(' ', '_').equals(name)) {
return world;
}
}
return null;
}
protected String getGameVersion()
{
try {
Class<? extends Server> serverClass = this.getServer().getClass();
serverClass.getMethod("getMinecraftVersion");
return this.getServer().getMinecraftVersion();
} catch (NoSuchMethodException exception) {
// If we're here, that probably means we're not running Paper or any Paper fork.
}
return this.getServer().getBukkitVersion().split("-", 2)[0];
}
@Nullable
public DhSupport getDhSupport()
{
@@ -0,0 +1,49 @@
/*
* DH Support, server-side support for Distant Horizons.
* Copyright (C) 2024 Jim C K Flaten
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package no.jckf.dhsupport.bukkit.commands;
import no.jckf.dhsupport.bukkit.DhSupportBukkitPlugin;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class DhCommand implements CommandExecutor
{
protected DhSupportBukkitPlugin plugin;
public DhCommand(DhSupportBukkitPlugin plugin)
{
this.plugin = plugin;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args)
{
if (!this.plugin.getDhSupport().getConfig().getBool(DhsConfig.ENABLE_DH_COMMAND_HELP_TEXT, true)) {
return true;
}
sender.sendMessage(ChatColor.RED + "You have issued a " + ChatColor.YELLOW + "/dh" + ChatColor.RED + " command which is meant for the Distant Horizons " + ChatColor.YELLOW + "mod" + ChatColor.RED + ", but you are using the " + ChatColor.YELLOW + "plugin" + ChatColor.RED + ". Please make sure you are reading the correct documentation.");
return true;
}
}
@@ -0,0 +1,541 @@
/*
* DH Support, server-side support for Distant Horizons.
* Copyright (C) 2024 Jim C K Flaten
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package no.jckf.dhsupport.bukkit.commands;
import no.jckf.dhsupport.bukkit.DhSupportBukkitPlugin;
import no.jckf.dhsupport.core.Coordinates;
import no.jckf.dhsupport.core.PreGenerator;
import no.jckf.dhsupport.core.Utils;
import no.jckf.dhsupport.core.configuration.Configuration;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import no.jckf.dhsupport.core.world.WorldInterface;
import org.bukkit.ChatColor;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.Arrays;
import java.util.UUID;
public class DhsCommand implements CommandExecutor
{
protected DhSupportBukkitPlugin plugin;
public DhsCommand(DhSupportBukkitPlugin plugin)
{
this.plugin = plugin;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args)
{
if (args.length < 1) {
sender.sendMessage(ChatColor.RED + "Missing sub-command.");
return true;
}
switch (args[0]) {
case "status":
return this.status(sender, Arrays.copyOfRange(args, 1, args.length));
case "reload":
return this.reload(sender);
case "worlds":
return this.worlds(sender);
case "pregen":
return this.pregen(sender, Arrays.copyOfRange(args, 1, args.length));
case "pause":
return this.pause(sender);
case "unpause":
return this.unpause(sender);
case "trim":
return this.trim(sender, Arrays.copyOfRange(args, 1, args.length));
case "bench":
return this.bench(sender, Arrays.copyOfRange(args, 1, args.length));
case "config":
return this.config(sender, Arrays.copyOfRange(args, 1, args.length));
}
sender.sendMessage(ChatColor.RED + "Unknown sub-command.");
return true;
}
protected boolean status(CommandSender sender, String[] args)
{
if (args.length == 0) {
return this.statusServer(sender);
}
return this.statusPlayer(sender, args[0]);
}
protected boolean statusServer(CommandSender sender)
{
Configuration config;
if (sender instanceof Player) {
World world = ((Player) sender).getWorld();
config = this.plugin.getDhSupport().getWorldInterface(world.getUID()).getConfig();
sender.sendMessage(ChatColor.BLUE + "Distant Horizons Support status for world " + ChatColor.GREEN + world.getName() + ChatColor.BLUE + ":");
} else {
config = this.plugin.getDhSupport().getConfig();
sender.sendMessage(ChatColor.BLUE + "Distant Horizons Support status for " + ChatColor.GREEN + "global context" + ChatColor.BLUE + ":");
}
sender.sendMessage(ChatColor.BLUE + "Distant generation is " + (config.getBool(DhsConfig.DISTANT_GENERATION_ENABLED) ? ChatColor.GREEN + "enabled" : ChatColor.RED + "disabled") + ChatColor.BLUE + " (" + ChatColor.GREEN + config.getInt(DhsConfig.RENDER_DISTANCE) + ChatColor.BLUE + ").");
sender.sendMessage(ChatColor.BLUE + "Real time updates is " + (config.getBool(DhsConfig.REAL_TIME_UPDATES_ENABLED) ? ChatColor.GREEN + "enabled" : ChatColor.RED + "disabled") + ChatColor.BLUE + " (" + ChatColor.GREEN + config.getInt(DhsConfig.REAL_TIME_UPDATE_RADIUS) + ChatColor.BLUE + ").");
sender.sendMessage(ChatColor.BLUE + "Builder type is " + ChatColor.GREEN + config.getString(DhsConfig.BUILDER_TYPE) + ChatColor.BLUE + ".");
int playerCount = this.plugin.getDhSupport().getPlayerConfigurations().size();
StringBuilder playerList = new StringBuilder();
for (UUID playerId : this.plugin.getDhSupport().getPlayerConfigurations().keySet()) {
playerList.append(ChatColor.GREEN + this.plugin.getServer().getPlayer(playerId).getName() + ChatColor.BLUE + ", ");
}
sender.sendMessage(ChatColor.BLUE + "There " + (playerCount == 1 ? "is" : "are") + " " + ChatColor.GREEN + playerCount + ChatColor.BLUE + " " + (playerCount == 1 ? "player" : "players") + " online using Distant Horizons" + (playerCount == 0 ? "." : ": " + playerList.substring(0, playerList.length() - 2)));
sender.sendMessage(ChatColor.BLUE + "Current generation speed: " + ChatColor.GREEN + String.format("%.2f", this.plugin.getDhSupport().getGenerationTracker().getPingsPerSecond() * 16) + " CPS");
return true;
}
protected boolean statusPlayer(CommandSender sender, String playerName)
{
Player player = this.plugin.getServer().getPlayer(playerName);
if (player == null) {
sender.sendMessage(ChatColor.RED + "Player not found.");
return true;
}
Configuration config = this.plugin.getDhSupport().getPlayerConfiguration(player.getUniqueId());
if (config == null) {
sender.sendMessage(ChatColor.YELLOW + "The player " + ChatColor.WHITE + player.getName() + ChatColor.YELLOW + " is not using Distant Horizons.");
return true;
}
sender.sendMessage(ChatColor.BLUE + "Distant Horizons Support status for player " + ChatColor.GREEN + player.getName() + ChatColor.BLUE + ":");
sender.sendMessage(ChatColor.BLUE + "Distant generation is " + (config.getBool(DhsConfig.DISTANT_GENERATION_ENABLED) ? ChatColor.GREEN + "enabled" : ChatColor.RED + "disabled") + ChatColor.BLUE + " (" + ChatColor.GREEN + config.getInt(DhsConfig.RENDER_DISTANCE) + ChatColor.BLUE + ").");
sender.sendMessage(ChatColor.BLUE + "Real time updates is " + (config.getBool(DhsConfig.REAL_TIME_UPDATES_ENABLED) ? ChatColor.GREEN + "enabled" : ChatColor.RED + "disabled") + ChatColor.BLUE + " (" + ChatColor.GREEN + config.getInt(DhsConfig.REAL_TIME_UPDATE_RADIUS) + ChatColor.BLUE + ").");
sender.sendMessage(ChatColor.BLUE + "Server has sent " + ChatColor.GREEN + config.getInt("buffer-id", 0) + ChatColor.BLUE + " LODs to them.");
return true;
}
protected boolean reload(CommandSender sender)
{
sender.sendMessage(ChatColor.YELLOW + "Reloading config...");
this.plugin.loadDhsConfig();
sender.sendMessage(ChatColor.GREEN + "Reload complete.");
return true;
}
protected boolean worlds(CommandSender sender)
{
sender.sendMessage(ChatColor.GREEN + "World list:");
for (World bukkitWorld : this.plugin.getServer().getWorlds()) {
sender.sendMessage(ChatColor.YELLOW + " - " + ChatColor.GREEN + bukkitWorld.getName() + " " + ChatColor.GRAY + "(" + bukkitWorld.getUID() + ")");
}
return true;
}
protected boolean pregen(CommandSender sender, String[] args)
{
if (args.length < 1) {
sender.sendMessage(ChatColor.RED + "Missing action.");
return true;
}
switch (args[0]) {
case "start":
return this.pregenStart(sender, Arrays.copyOfRange(args, 1, args.length));
case "stop":
return this.pregenStop(sender, Arrays.copyOfRange(args, 1, args.length));
case "status":
return this.pregenStatus(sender, Arrays.copyOfRange(args, 1, args.length));
}
sender.sendMessage(ChatColor.RED + "Unknown action.");
return true;
}
protected boolean pregenStart(CommandSender sender, String[] args)
{
WorldInterface world;
Integer centerX;
Integer centerZ;
Integer radius;
if (args.length >= 1) {
World bukkitWorld = this.plugin.getWorld(args[0]);
if (bukkitWorld == null) {
sender.sendMessage(ChatColor.RED + "Unknown world.");
return true;
}
world = this.plugin.getDhSupport().getWorldInterface(bukkitWorld.getUID());
} else if (sender instanceof Player) {
world = this.plugin.getDhSupport().getWorldInterface(((Player) sender).getWorld().getUID());
} else {
world = null;
}
if (world == null) {
sender.sendMessage(ChatColor.RED + "No world specified.");
return true;
}
if (args.length >= 3) {
centerX = Integer.parseInt(args[1]);
centerZ = Integer.parseInt(args[2]);
} else {
centerX = world.getWorldBorderX();
centerZ = world.getWorldBorderZ();
}
if (centerX == null || centerZ == null) {
sender.sendMessage(ChatColor.RED + "No center coordinates specified.");
return true;
}
if (args.length >= 4) {
radius = Coordinates.chunkToBlock(Integer.parseInt(args[3]));
} else {
radius = world.getWorldBorderRadius();
}
if (radius == null) {
sender.sendMessage(ChatColor.RED + "No radius specified.");
return true;
}
boolean force = args.length >= 5 && args[4].equals("force");
sender.sendMessage(ChatColor.YELLOW + "Generating LODs for view distance of " + ChatColor.GREEN + Coordinates.blockToChunk(radius) + ChatColor.YELLOW + " chunks in world " + ChatColor.GREEN + world.getName() + ChatColor.YELLOW + " starting at center " + ChatColor.GREEN + centerX + " " + centerZ + ChatColor.YELLOW + "...");
if (force) {
sender.sendMessage(ChatColor.YELLOW + "All existing LODs in this area will be re-generated.");
}
this.plugin.getDhSupport().preGenerate(world, centerX, centerZ, radius, force);
return true;
}
protected boolean pregenStop(CommandSender sender, String[] args)
{
WorldInterface world;
if (args.length >= 1) {
World bukkitWorld = this.plugin.getWorld(args[0]);
if (bukkitWorld == null) {
sender.sendMessage(ChatColor.RED + "Unknown world.");
return true;
}
world = this.plugin.getDhSupport().getWorldInterface(bukkitWorld.getUID());
} else if (sender instanceof Player) {
world = this.plugin.getDhSupport().getWorldInterface(((Player) sender).getWorld().getUID());
} else {
world = null;
}
if (world == null) {
sender.sendMessage(ChatColor.RED + "No world specified.");
return true;
}
if (!this.plugin.getDhSupport().isPreGenerating(world)) {
sender.sendMessage(ChatColor.RED + "No pre-generator running in world " + ChatColor.YELLOW + world.getName() + ChatColor.RED + ".");
return true;
}
sender.sendMessage(ChatColor.YELLOW + "Stopping pre-generation in world " + ChatColor.GREEN + world.getName() + ChatColor.YELLOW + "...");
this.plugin.getDhSupport().stopPreGenerator(world);
return true;
}
protected boolean pregenStatus(CommandSender sender, String[] args)
{
WorldInterface world;
if (args.length >= 1) {
World bukkitWorld = this.plugin.getWorld(args[0]);
if (bukkitWorld == null) {
sender.sendMessage(ChatColor.RED + "Unknown world.");
return true;
}
world = this.plugin.getDhSupport().getWorldInterface(bukkitWorld.getUID());
} else if (sender instanceof Player) {
world = this.plugin.getDhSupport().getWorldInterface(((Player) sender).getWorld().getUID());
} else {
world = null;
}
if (world == null) {
sender.sendMessage(ChatColor.RED + "No world specified.");
return true;
}
PreGenerator generator = this.plugin.getDhSupport().getPreGenerator(world);
if (generator == null) {
sender.sendMessage(ChatColor.RED + "No pre-generator running in world " + ChatColor.YELLOW + world.getName() + ChatColor.RED + ".");
return true;
}
Duration elapsedTime = generator.getElapsedTime();
float momentaryLodsPerSecond = (float) this.plugin.getDhSupport().getGenerationTracker().getPingsPerSecond();
float totalLodsPerSecond = (float) generator.getCompletedRequests() / elapsedTime.toSeconds();
sender.sendMessage(ChatColor.GREEN + "Generation progress: " + ChatColor.YELLOW + String.format("%.2f", generator.getProgress() * 100f) + "%");
sender.sendMessage(ChatColor.GREEN + "Processed LODs: " + ChatColor.YELLOW + generator.getCompletedRequests() + ChatColor.GREEN + " / " + ChatColor.YELLOW + generator.getTargetRequests() + ChatColor.GREEN + " (" + ChatColor.YELLOW + String.format("%.2f", totalLodsPerSecond * 16) + ChatColor.GREEN + " CPS)");
sender.sendMessage(ChatColor.GREEN + "Time elapsed: " + ChatColor.YELLOW + Utils.humanReadableDuration(elapsedTime));
if (generator.isRunning()) {
sender.sendMessage(ChatColor.GREEN + "Time remaining: " + ChatColor.YELLOW + Utils.humanReadableDuration(Duration.ofSeconds((long) ((generator.getTargetRequests() - generator.getCompletedRequests()) / momentaryLodsPerSecond))));
} else {
sender.sendMessage(ChatColor.GREEN + "Generation is complete.");
}
return true;
}
protected boolean pause(CommandSender sender)
{
if (this.plugin.getDhSupport().isPaused()) {
sender.sendMessage(ChatColor.RED + "Already paused.");
return true;
}
if (!this.plugin.getDhSupport().pause()) {
sender.sendMessage(ChatColor.RED + "Could not pause. Check server log for error messages.");
return true;
}
sender.sendMessage(ChatColor.YELLOW + "DHS has been paused.");
return true;
}
protected boolean unpause(CommandSender sender)
{
if (!this.plugin.getDhSupport().isPaused()) {
sender.sendMessage(ChatColor.RED + "Not paused.");
return true;
}
if (!this.plugin.getDhSupport().unpause()) {
sender.sendMessage(ChatColor.RED + "Could not unpause. Check server log for error messages.");
return true;
}
sender.sendMessage(ChatColor.GREEN + "DHS has been unpaused.");
return true;
}
protected boolean trim(CommandSender sender, String[] args)
{
WorldInterface world;
Integer centerX;
Integer centerZ;
Integer radius;
if (args.length >= 1) {
World bukkitWorld = this.plugin.getWorld(args[0]);
if (bukkitWorld == null) {
sender.sendMessage(ChatColor.RED + "Unknown world.");
return true;
}
world = this.plugin.getDhSupport().getWorldInterface(bukkitWorld.getUID());
} else if (sender instanceof Player) {
world = this.plugin.getDhSupport().getWorldInterface(((Player) sender).getWorld().getUID());
} else {
world = null;
}
if (world == null) {
sender.sendMessage(ChatColor.RED + "No world specified.");
return true;
}
if (args.length >= 3) {
centerX = Integer.parseInt(args[1]);
centerZ = Integer.parseInt(args[2]);
} else {
centerX = world.getWorldBorderX();
centerZ = world.getWorldBorderZ();
}
if (centerX == null || centerZ == null) {
sender.sendMessage(ChatColor.RED + "No center coordinates specified.");
return true;
}
if (args.length >= 4) {
radius = Integer.parseInt(args[3]);
} else {
radius = world.getWorldBorderRadius();
}
if (radius == null) {
sender.sendMessage(ChatColor.RED + "No radius specified.");
return true;
}
sender.sendMessage(ChatColor.YELLOW + "Trimming LODs outside view distance of " + ChatColor.GREEN + radius + ChatColor.YELLOW + " blocks in world " + ChatColor.GREEN + world.getName() + ChatColor.YELLOW + " centered at " + ChatColor.GREEN + centerX + " " + centerZ + ChatColor.YELLOW + "...");
this.plugin.getDhSupport().trim(world, centerX, centerZ, radius)
.thenAccept((trimmedCount) -> {
sender.sendMessage(ChatColor.GREEN + "Trimming of " + ChatColor.YELLOW + trimmedCount + ChatColor.GREEN + " LODs in " + ChatColor.YELLOW + world.getName() + ChatColor.GREEN + " completed.");
});
return true;
}
protected boolean bench(CommandSender sender, String[] args)
{
int schedulerThreads = this.plugin.getDhSupport().getConfig().getInt(DhsConfig.SCHEDULER_THREADS);
WorldInterface world;
int iterations = 1000;
int concurrency = schedulerThreads;
if (args.length >= 1) {
World bukkitWorld = this.plugin.getWorld(args[0]);
if (bukkitWorld == null) {
sender.sendMessage(ChatColor.RED + "Unknown world.");
return true;
}
world = this.plugin.getDhSupport().getWorldInterface(bukkitWorld.getUID());
} else if (sender instanceof Player) {
world = this.plugin.getDhSupport().getWorldInterface(((Player) sender).getWorld().getUID());
} else {
world = null;
}
if (world == null) {
sender.sendMessage(ChatColor.RED + "No world specified.");
return true;
}
if (args.length >= 2) {
iterations = Integer.parseInt(args[1]);
}
if (args.length >= 3) {
concurrency = Integer.parseInt(args[2]);
}
if (concurrency > schedulerThreads) {
sender.sendMessage(ChatColor.RED + "Concurrency of " + ChatColor.YELLOW + concurrency + ChatColor.RED + " is more than the " + ChatColor.YELLOW + schedulerThreads + ChatColor.RED + " scheduler threads this server is running.");
return true;
}
sender.sendMessage(ChatColor.BLUE + "Starting benchmark with " + ChatColor.GREEN + iterations + ChatColor.BLUE + " iterations times " + ChatColor.GREEN + concurrency + ChatColor.BLUE + " threads in world " + ChatColor.GREEN + world.getName() + ChatColor.BLUE + "...");
this.plugin.getDhSupport().benchmark(world.getId(), iterations, concurrency)
.thenAccept((lodsPerSecond) -> {
sender.sendMessage(ChatColor.GREEN + "Benchmark completed! Result: " + ChatColor.BLUE + String.format("%.2f", lodsPerSecond * 16) + ChatColor.GREEN + " CPS.");
});
return true;
}
public boolean config(CommandSender sender, String[] args)
{
if (args.length < 1) {
sender.sendMessage(ChatColor.RED + "Missing action.");
return true;
}
if (args.length < 2) {
sender.sendMessage(ChatColor.RED + "Missing key.");
return true;
}
switch (args[0]) {
case "set":
if (args.length < 3) {
sender.sendMessage(ChatColor.RED + "Missing value.");
return true;
}
this.plugin.getDhSupport().getConfig().set(args[1], args[2]);
sender.sendMessage(ChatColor.GREEN + "Set " + ChatColor.BLUE + args[1] + ChatColor.GREEN + " to " + ChatColor.BLUE + args[2] + ChatColor.GREEN + ".");
return true;
case "get":
sender.sendMessage(ChatColor.GREEN + "Value of " + ChatColor.BLUE + args[1] + ChatColor.GREEN + " is " + ChatColor.BLUE + this.plugin.getDhSupport().getConfig().get(args[1]) + ChatColor.GREEN + ".");
return true;
case "unset":
this.plugin.getDhSupport().getConfig().unset(args[1]);
sender.sendMessage(ChatColor.GREEN + "Unset " + ChatColor.BLUE + args[1] + ChatColor.GREEN + ".");
return true;
}
sender.sendMessage(ChatColor.RED + "Unknown action.");
return true;
}
}
@@ -47,13 +47,13 @@ public class ConfigLoader extends Handler
pluginConfig.setDefaults(new MemoryConfiguration());
// Get the config version value from the default config file.
Integer pluginConfigVersion = defaultConfig.getInt(DhsConfig.CONFIG_VERSION);
Integer bundledConfigVersion = defaultConfig.getInt(DhsConfig.CONFIG_VERSION);
// Get the config version value from the server's config file.
Integer serverConfigVersion = pluginConfig.getInt(DhsConfig.CONFIG_VERSION);
// If the two values differ, load defaults for missing values.
if (!pluginConfigVersion.equals(serverConfigVersion)) {
if (!bundledConfigVersion.equals(serverConfigVersion)) {
this.plugin.getLogger().warning("Your config file is for an older version of this plugin.");
pluginConfig.setDefaults(defaultConfig);
@@ -63,6 +63,9 @@ public class ConfigLoader extends Handler
// DH Support config.
Configuration dhsConfig = this.plugin.getDhSupport().getConfig();
// Get rid of potentially old config values in the case of a reload.
dhsConfig.clear();
// Populate DH Support config.
pluginConfig.getKeys(true).forEach((key) -> dhsConfig.set(key, pluginConfig.get(key)));
}
@@ -70,6 +73,7 @@ public class ConfigLoader extends Handler
@Override
public void onDisable()
{
// Revert any changes we did.
this.plugin.reloadConfig();
}
}
@@ -1,10 +1,36 @@
/*
* DH Support, server-side support for Distant Horizons.
* Copyright (C) 2024 Jim C K Flaten
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package no.jckf.dhsupport.bukkit.handler;
import no.jckf.dhsupport.bukkit.DhSupportBukkitPlugin;
import no.jckf.dhsupport.core.DhSupport;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
public class PlayerHandler implements Listener
{
protected DhSupportBukkitPlugin plugin;
@@ -14,6 +40,26 @@ public class PlayerHandler implements Listener
this.plugin = plugin;
}
@EventHandler
public void onPlayerConnect(PlayerJoinEvent event)
{
Player player = event.getPlayer();
DhSupport dhSupport = this.plugin.getDhSupport();
dhSupport.getScheduler().runOnMainThreadDelayed(() -> {
if (dhSupport.getPlayerConfiguration(player.getUniqueId()) != null) {
return null;
}
for (String message : dhSupport.getConfig().getStringList(DhsConfig.AD_MESSAGE, Collections.emptyList())) {
player.sendMessage(message);
}
return null;
}, 10, TimeUnit.SECONDS); // Arbitrary delay, waiting for DH config message (or the lack of one).
}
@EventHandler
public void onPlayerDisconnect(PlayerQuitEvent event)
{
@@ -21,6 +21,7 @@ package no.jckf.dhsupport.bukkit.handler;
import no.jckf.dhsupport.bukkit.BukkitPluginMessageSender;
import no.jckf.dhsupport.bukkit.DhSupportBukkitPlugin;
import no.jckf.dhsupport.core.handler.PluginMessageHandler;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.PluginMessageListener;
import org.jetbrains.annotations.NotNull;
@@ -55,6 +56,10 @@ public class PluginMessageProxy extends Handler implements PluginMessageListener
@Override
public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, byte[] message)
{
this.pluginMessageHandler.onPluginMessageReceived(channel, player.getUniqueId(), message);
try {
this.pluginMessageHandler.onPluginMessageReceived(channel, player.getUniqueId(), message);
} catch (RuntimeException exception) {
player.sendMessage(ChatColor.RED + "Distant Horizons: " + exception.getMessage());
}
}
}
@@ -20,25 +20,40 @@ package no.jckf.dhsupport.bukkit.handler;
import no.jckf.dhsupport.bukkit.BukkitWorldInterface;
import no.jckf.dhsupport.bukkit.DhSupportBukkitPlugin;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.*;
import org.bukkit.event.inventory.FurnaceBurnEvent;
import org.bukkit.event.world.StructureGrowEvent;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class WorldHandler implements Listener
{
protected static final String[] EVENT_METHOD_NAMES = { "blockList", "getBlocks", "getBlock", "getLocation", "getChunk" };
protected DhSupportBukkitPlugin plugin;
protected Map<String, Method> eventMethods = new HashMap<>();
public WorldHandler(DhSupportBukkitPlugin plugin)
{
this.plugin = plugin;
this.plugin.getServer().getWorlds().forEach(this::addWorldInterface);
this.registerUpdateListeners();
}
protected void addWorldInterface(World world)
@@ -57,9 +72,94 @@ public class WorldHandler implements Listener
this.plugin.getDhSupport().setWorldInterface(world.getUID(), null);
}
protected void touchLod(Location location)
protected void touchLod(Location location, String reason)
{
this.plugin.getDhSupport().touchLod(location.getWorld().getUID(), location.getBlockX(), location.getBlockZ());
this.plugin.getDhSupport().touchLod(location.getWorld().getUID(), location.getBlockX(), location.getBlockZ(), reason);
}
protected void registerUpdateListeners()
{
Listener dummyListener = new Listener() {};
for (String eventClassName : this.plugin.getDhSupport().getConfig().getStringList(DhsConfig.UPDATE_EVENTS)) {
try {
Class<? extends Event> eventClass = Class.forName(eventClassName).asSubclass(Event.class);
this.plugin.getServer().getPluginManager().registerEvent(
eventClass,
dummyListener,
EventPriority.MONITOR,
(listener, event) -> this.handleUpdateEvent(event),
this.plugin
);
this.plugin.getDhSupport().info("Listening for " + eventClassName + ".");
} catch (ClassNotFoundException exception) {
this.plugin.getDhSupport().warning("Could not find event class: " + eventClassName);
}
}
}
protected void handleUpdateEvent(Event event)
{
Class<? extends Event> eventClass = event.getClass();
String eventClassName = eventClass.getName();
Method method = null;
if (this.eventMethods.containsKey(eventClassName)) {
method = this.eventMethods.get(eventClassName);
// We've seen this event before, but don't know how to handle it.
if (method == null) {
return;
}
} else {
for (String methodName : EVENT_METHOD_NAMES) {
try {
method = eventClass.getMethod(methodName);
break;
} catch (NoSuchMethodException exception) {
}
}
// Store the result, even if it is null.
this.eventMethods.put(eventClassName, method);
// This is the first time we see this even, and we don't know how to handle it.
if (method == null) {
this.plugin.getDhSupport().warning("Unsure how to handle event: " + eventClass.getName());
return;
}
}
List<?> list;
try {
Object result = method.invoke(event);
if (result instanceof Location location) {
list = List.of(location.getBlock());
} else if (result instanceof Chunk chunk) {
list = List.of(chunk.getBlock(0, 0, 0));
} else if (result instanceof List) {
list = (List<?>) result;
} else {
list = List.of(result);
}
} catch (IllegalAccessException | InvocationTargetException exception) {
throw new RuntimeException(exception);
}
for (Object entry : list) {
if (entry instanceof Block block) {
this.touchLod(block.getLocation(), eventClassName);
} else if (entry instanceof BlockState state) {
this.touchLod(state.getLocation(), eventClassName);
} else {
this.plugin.getDhSupport().warning("Unknown block " + entry + " from event: " + eventClassName);
}
}
}
@EventHandler
@@ -73,117 +173,4 @@ public class WorldHandler implements Listener
{
this.removeWorldInterface(worldUnload.getWorld());
}
@EventHandler
public void onBlockPlace(BlockPlaceEvent blockPlace)
{
this.touchLod(blockPlace.getBlock().getLocation());
}
@EventHandler
public void onBlockBreak(BlockBreakEvent blockBreak)
{
this.touchLod(blockBreak.getBlock().getLocation());
}
// FIXME messes with 1.16.5 compilation
// @EventHandler
// public void onTNTPrime(TNTPrimeEvent blockBreak)
// {
// this.touchLod(blockBreak.getBlock().getLocation());
// }
@EventHandler
public void onBlockExplode(BlockExplodeEvent blockExplode)
{
for (var block : blockExplode.blockList()) {
this.touchLod(block.getLocation());
}
}
@EventHandler
public void onBlockIgnite(BlockIgniteEvent blockIgnite)
{
this.touchLod(blockIgnite.getBlock().getLocation());
}
@EventHandler
public void onBlockBurn(BlockBurnEvent blockBurn)
{
this.touchLod(blockBurn.getBlock().getLocation());
}
@EventHandler
public void onLeavesDecay(LeavesDecayEvent leavesDecay)
{
this.touchLod(leavesDecay.getBlock().getLocation());
}
@EventHandler
public void onBlockFade(BlockFadeEvent blockFade)
{
this.touchLod(blockFade.getBlock().getLocation());
}
@EventHandler
public void onBlockGrow(BlockGrowEvent blockGrow)
{
this.touchLod(blockGrow.getBlock().getLocation());
}
@EventHandler
public void onMoistureChange(MoistureChangeEvent moistureChange)
{
this.touchLod(moistureChange.getBlock().getLocation());
}
@EventHandler
public void onFurnaceBurn(FurnaceBurnEvent furnaceBurn)
{
this.touchLod(furnaceBurn.getBlock().getLocation());
}
@EventHandler
public void onBlockRedstone(BlockRedstoneEvent blockRedstone)
{
this.touchLod(blockRedstone.getBlock().getLocation());
}
@EventHandler
public void onBlockPistonExtend(BlockPistonExtendEvent blockPistonExtend)
{
for (var block : blockPistonExtend.getBlocks()) {
this.touchLod(block.getLocation());
}
}
@EventHandler
public void onBlockPistonRetract(BlockPistonRetractEvent blockPistonRetract)
{
for (var block : blockPistonRetract.getBlocks()) {
this.touchLod(block.getLocation());
}
}
@EventHandler
public void onStructureGrow(StructureGrowEvent structureGrow)
{
for (var blockState : structureGrow.getBlocks()) {
this.touchLod(blockState.getLocation());
}
}
@EventHandler
public void onSpongeAbsorb(SpongeAbsorbEvent spongeAbsorb)
{
for (var blockState : spongeAbsorb.getBlocks()) {
this.touchLod(blockState.getLocation());
}
}
}
@@ -18,6 +18,8 @@
package no.jckf.dhsupport.core;
import java.util.function.BiConsumer;
// Block -> chunk -> section -> region
public class Coordinates
{
@@ -101,4 +103,39 @@ public class Coordinates
{
return value & 0x0F;
}
// PACK/UNPACK
public static long pack(int x, int z)
{
return ((long) x & 0xffffffffL)
| (((long) z & 0xffffffffL) << 32);
}
public static int unpackX(long packed)
{
return (int) packed;
}
public static int unpackZ(long packed)
{
return (int) (packed >> 32);
}
// ???
public static void foreachChunkInSection(int sectionX, int sectionZ, BiConsumer<Integer, Integer> consumer)
{
int worldX = sectionToBlock(sectionX);
int worldZ = sectionToBlock(sectionZ);
for (int xMultiplier = 0; xMultiplier < 4; xMultiplier++) {
for (int zMultiplier = 0; zMultiplier < 4; zMultiplier++) {
consumer.accept(
worldX + 16 * xMultiplier,
worldZ + 16 * zMultiplier
);
}
}
}
}
@@ -23,10 +23,11 @@ import no.jckf.dhsupport.core.configuration.Configurable;
import no.jckf.dhsupport.core.configuration.Configuration;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import no.jckf.dhsupport.core.database.Database;
import no.jckf.dhsupport.core.database.migrations.AddLodVersionColumn;
import no.jckf.dhsupport.core.database.migrations.CreateLodsTable;
import no.jckf.dhsupport.core.database.migrations.DeleteLodsWithoutVersion;
import no.jckf.dhsupport.core.database.models.LodModel;
import no.jckf.dhsupport.core.database.repositories.AsyncLodRepository;
import no.jckf.dhsupport.core.dataobject.Beacon;
import no.jckf.dhsupport.core.dataobject.Lod;
import no.jckf.dhsupport.core.dataobject.SectionPosition;
import no.jckf.dhsupport.core.handler.LodHandler;
@@ -47,10 +48,16 @@ import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.function.Predicate;
import java.util.logging.Logger;
public class DhSupport implements Configurable
{
protected String pluginVersion;
protected String gameVersion;
protected String dataDirectory;
protected Database database;
@@ -63,9 +70,12 @@ public class DhSupport implements Configurable
protected Scheduler scheduler;
protected int generationCount;
protected UpdateChecker updateChecker;
protected long generationCountStartTime;
@Nullable
protected CompletableFuture<?> pause;
protected PerformanceTracker generationTracker = new PerformanceTracker();
protected Map<UUID, WorldInterface> worldInterfaces = new HashMap<>();
@@ -73,20 +83,27 @@ public class DhSupport implements Configurable
protected PluginMessageSender pluginMessageSender;
protected Map<String, CompletableFuture<Lod>> queuedBuilders = new HashMap<>();
protected Map<String, CompletableFuture<Lod>> queuedBuilders = new ConcurrentHashMap<>();
protected Map<String, LodModel> touchedLods = new ConcurrentHashMap<>();
protected Map<UUID, Configuration> playerConfigurations = new HashMap<>();
public DhSupport()
protected Map<UUID, PreGenerator> preGenerators = new HashMap<>();
public DhSupport(String pluginVersion, String gameVersion)
{
this.pluginVersion = pluginVersion;
this.gameVersion = gameVersion;
this.configuration = new Configuration();
this.database = new Database();
this.lodRepository = new AsyncLodRepository(this.database);
this.pluginMessageHandler = new PluginMessageHandler(this);
this.updateChecker = new UpdateChecker(62013887);
}
public void onEnable()
@@ -94,20 +111,36 @@ public class DhSupport implements Configurable
this.lodRepository.setLogger(this.getLogger());
try {
this.database.open(this.getDataDirectory() + "/data.sqlite");
this.database.open(
this.getConfig().getString(DhsConfig.DATABASE_PATH)
.replace("{datadir}", this.getDataDirectory())
);
this.database.addMigration(CreateLodsTable.class);
this.database.addMigration(AddLodVersionColumn.class);
this.database.addMigration(DeleteLodsWithoutVersion.class);
this.database.migrate();
} catch (Exception exception) {
this.warning("Failed to initialize database: " + exception.getMessage());
// TODO: Disable the plugin?
throw new RuntimeException("Failed to initialize database!", exception);
}
(new PlayerConfigHandler(this, this.pluginMessageHandler)).register();
(new LodHandler(this, this.pluginMessageHandler)).register();
this.pluginMessageHandler.onEnable();
if (this.getConfig().getBool(DhsConfig.GENERATE_NEW_CHUNKS, true) && this.getConfig().getBool(DhsConfig.GENERATE_NEW_CHUNKS_WARNING, true)) {
this.warning("Chunk generation is enabled. New chunks will be generated as needed to complete LOD generation. This could significantly increase the size of your world.");
this.warning("If you understand what this means and would like to disable this warning, set " + DhsConfig.GENERATE_NEW_CHUNKS_WARNING + " to false in your config.");
}
if (this.getConfig().getBool(DhsConfig.CHECK_FOR_UPDATES, true)) {
this.getScheduler().runOnSeparateThread(() -> {
this.checkUpdates();
return null;
});
}
}
public void onDisable()
@@ -137,32 +170,44 @@ public class DhSupport implements Configurable
return this.scheduler;
}
protected void resetGenerationCount()
public boolean pause()
{
this.generationCount = 0;
this.generationCountStartTime = System.currentTimeMillis();
if (this.pause != null) {
return false;
}
this.pause = new CompletableFuture<>();
return true;
}
public void printGenerationCount()
public boolean unpause()
{
boolean showActivity = this.getConfig().getBool(DhsConfig.SHOW_BUILDER_ACTIVITY, true);
if (!showActivity || this.generationCount == 0) {
this.resetGenerationCount();
return;
if (this.pause == null) {
return false;
}
double secondsElapsed = (double) (System.currentTimeMillis() - this.generationCountStartTime) / 1000;
this.pause.complete(null);
this.pause = null;
if (secondsElapsed < 60) {
return;
return true;
}
public boolean isPaused()
{
return this.pause != null;
}
public void joinPauseState()
{
if (this.pause != null) {
this.pause.join();
}
}
double lodsPerSecond = (double) this.generationCount / secondsElapsed;
this.info("Generation in progress: " + this.generationCount + " processed, " + this.queuedBuilders.size() + " in queue, " + String.format("%.2f", lodsPerSecond) + " per second.");
this.resetGenerationCount();
public PerformanceTracker getGenerationTracker()
{
return this.generationTracker;
}
public void setWorldInterface(UUID id, @Nullable WorldInterface worldInterface)
@@ -202,6 +247,16 @@ public class DhSupport implements Configurable
return this.lodRepository;
}
public String getPluginVersion()
{
return this.pluginVersion;
}
public String getGameVersion()
{
return this.gameVersion;
}
public Configuration getConfig()
{
return this.configuration;
@@ -230,7 +285,26 @@ public class DhSupport implements Configurable
public void debug(String message)
{
//this.getLogger().info("[DEBUG] " + message);
// TODO: Use log levels properly?
if (!this.getConfig().getBool(DhsConfig.DEBUG, false)) {
return;
}
this.getLogger().info("[DEBUG] " + message);
}
public void checkUpdates()
{
if (this.updateChecker.isLatestVersion(this.pluginVersion)) {
return;
}
this.warning("A newer version of the plugin is available.");
}
public Map<UUID, Configuration> getPlayerConfigurations()
{
return this.playerConfigurations;
}
public void setPlayerConfiguration(UUID playerId, Configuration playerConfiguration)
@@ -279,25 +353,12 @@ public class DhSupport implements Configurable
return this.queuedBuilders.get(key);
}
Scheduler scheduler = this.getScheduler();
CompletableFuture<Lod> queued = this.getScheduler().runOnSeparateThread(() -> {
this.joinPauseState();
CompletableFuture<Lod> queued;
// TODO: Fetch chunk data on region thread, then move to builder thread.
if (scheduler.canReadWorldAsync()) {
queued = this.getScheduler().runOnSeparateThread(
builder::generate
);
} else {
queued = this.getScheduler().runOnRegionThread(
worldId,
Coordinates.sectionToBlock(position.getX()),
Coordinates.sectionToBlock(position.getZ()),
builder::generate
);
}
queued = queued.thenApply((lod) -> {
return builder.generate();
})
.thenApply((lod) -> {
this.queuedBuilders.remove(key);
return lod;
@@ -317,94 +378,107 @@ public class DhSupport implements Configurable
public CompletableFuture<LodModel> getLod(UUID worldId, SectionPosition position)
{
int worldX = Coordinates.sectionToBlock(position.getX());
int worldZ = Coordinates.sectionToBlock(position.getZ());
return this.getLodRepository()
.loadLodAsync(worldId, position.getX(), position.getZ())
.thenCompose((modelFromDb) -> {
// If a LOD was found in the database, return it.
.thenComposeAsync((modelFromDb) -> {
// If an LOD was found in the database, return it.
if (modelFromDb != null) {
return CompletableFuture.completedFuture(modelFromDb);
}
WorldInterface world = this.getWorldInterface(worldId).newInstance();
Map<String, CompletableFuture<Boolean>> loads = new HashMap<>();
// Load all the chunks we need for this request.
for (int xMultiplier = 0; xMultiplier < 4; xMultiplier++) {
for (int zMultiplier = 0; zMultiplier < 4; zMultiplier++) {
int chunkX = worldX + 16 * xMultiplier;
int chunkZ = worldZ + 16 * zMultiplier;
if (!world.isChunkLoaded(chunkX, chunkZ)) {
loads.put(worldX + "x" + worldZ, world.loadChunkAsync(chunkX, chunkZ));
}
}
}
// Wait for chunk loads, then...
return CompletableFuture.allOf(loads.values().toArray(new CompletableFuture[loads.size()]))
.thenCompose((asd) -> {
// No LOD was found. Start building a new one.
CompletableFuture<Lod> lodFuture = this.queueBuilder(worldId, position, this.getBuilder(world, position));
// Find any beacons that should appear in this LOD.
CompletableFuture<Collection<Beacon>> beaconFuture = this.getScheduler().runOnRegionThread(worldId, worldX, worldZ, () -> {
Collection<Beacon> accumulator = new ArrayList<>();
boolean includeBeacons = world.getConfig().getBool(DhsConfig.INCLUDE_BEACONS, false);
if (includeBeacons) {
for (int xMultiplier = 0; xMultiplier < 4; xMultiplier++) {
for (int zMultiplier = 0; zMultiplier < 4; zMultiplier++) {
accumulator.addAll(world.getBeaconsInChunk(worldX + 16 * xMultiplier, worldZ + 16 * zMultiplier));
}
}
}
return accumulator;
});
// Combine the LOD and beacons and save the result in the database.
return lodFuture.thenCombine(beaconFuture, (lod, beacons) -> {
// Discard the chunks we loaded.
this.getScheduler().runOnRegionThread(worldId, worldX, worldZ, () -> {
for (String key : loads.keySet()) {
String[] xz = key.split("x", 2);
world.discardChunk(
Coordinates.chunkToBlock(Integer.parseInt(xz[0])),
Coordinates.chunkToBlock(Integer.parseInt(xz[1]))
);
}
return null;
});
Encoder lodEncoder = new Encoder();
lod.encode(lodEncoder);
Encoder beaconEncoder = new Encoder();
beaconEncoder.writeCollection(beacons);
this.generationCount++;
return this.lodRepository.saveLodAsync(
worldId,
position.getX(),
position.getZ(),
lodEncoder.toByteArray(),
beaconEncoder.toByteArray()
);
})
.thenCompose((f) -> f); // Unwrap the nested future.
});
// Otherwise generate a new one.
return this.generateLod(worldId, position, true);
});
}
public void touchLod(UUID worldId, int x, int z)
protected CompletableFuture<LodModel> generateLod(UUID worldId, SectionPosition position, boolean manageChunks)
{
int worldX = Coordinates.sectionToBlock(position.getX());
int worldZ = Coordinates.sectionToBlock(position.getZ());
WorldInterface world = this.getWorldInterface(worldId).newInstance();
boolean generateNewChunks = world.getConfig().getBool(DhsConfig.GENERATE_NEW_CHUNKS, true);
Map<Long, CompletableFuture<Boolean>> loads = new HashMap<>();
if (manageChunks) {
// Load all the chunks we need for this request.
Coordinates.foreachChunkInSection(position.getX(), position.getZ(), (chunkX, chunkZ) -> {
if (!world.isChunkLoaded(chunkX, chunkZ)) {
if (generateNewChunks) {
loads.put(Coordinates.pack(chunkX, chunkZ), world.loadOrGenerateChunkAsync(chunkX, chunkZ));
} else {
loads.put(Coordinates.pack(chunkX, chunkZ), world.loadChunkAsync(chunkX, chunkZ));
}
}
});
}
// Wait for chunk loads, then...
return CompletableFuture.allOf(loads.values().toArray(new CompletableFuture[0]))
.thenComposeAsync((asd) -> {
boolean loadRejected = loads
.values()
.stream()
.map(loadRequest -> {
try {
return loadRequest.get();
} catch (InterruptedException | ExecutionException exception) {
return false;
}
})
.anyMatch(Predicate.isEqual(false));
if (loadRejected) {
// Use dummy chunks instead of real world data.
world.setDummyMode(true);
} else {
// Prime chunk data in world interface.
Coordinates.foreachChunkInSection(position.getX(), position.getZ(), (chunkX, chunkZ) -> {
world.getMaterialAt(chunkX, world.getSeaLevel(), chunkZ);
});
}
// Discard the chunks we loaded.
this.getScheduler().runOnRegionThread(worldId, worldX, worldZ, () -> {
for (long packed : loads.keySet()) {
world.discardChunk(
Coordinates.unpackX(packed),
Coordinates.unpackZ(packed)
);
}
return null;
});
// No LOD was found. Start building a new one.
CompletableFuture<Lod> lodFuture = this.queueBuilder(worldId, position, this.getBuilder(world, position));
// Combine the LOD and beacons and save the result in the database.
return lodFuture.thenApply((lod) -> {
Encoder lodEncoder = new Encoder(8192);
lod.encode(lodEncoder);
Encoder beaconEncoder = new Encoder(16 + lod.getBeacons().size() * 16);
beaconEncoder.writeCollection(lod.getBeacons());
this.generationTracker.ping();
return this.lodRepository.saveLodAsync(
worldId,
position.getX(),
position.getZ(),
lodEncoder.toByteArray(),
beaconEncoder.toByteArray(),
LodModel.VERSION
);
})
.thenCompose((f) -> f); // Unwrap the nested future.
});
}
public void touchLod(UUID worldId, int x, int z, @Nullable String reason)
{
int sectionX = Coordinates.blockToSection(x);
int sectionZ = Coordinates.blockToSection(z);
@@ -420,114 +494,224 @@ public class DhSupport implements Configurable
return;
}
this.debug("Touched LOD at " + this.getWorldInterface(worldId).getName() + " " + x + " " + z + (reason == null ? "." : ": " + reason));
this.touchedLods.put(key, lodModel);
}
public void touchLod(UUID worldId, int x, int z)
{
this.touchLod(worldId, x, z, null);
}
public void updateTouchedLods()
{
if (this.isPaused()) {
return;
}
for (String key : this.touchedLods.keySet()) {
LodModel lodModelToDelete = this.touchedLods.get(key);
this.touchedLods.remove(key);
WorldInterface world = this.getWorldInterface(lodModelToDelete.getWorldId());
this.debug("Changes detected in " + world.getName() + " " + lodModelToDelete.getX() + "x" + lodModelToDelete.getZ() + ".");
this.debug("Changes detected in " + world.getName() + " " + lodModelToDelete.getX() + " " + lodModelToDelete.getZ() + ".");
this.getLodRepository().deleteLodAsync(lodModelToDelete.getWorldId(), lodModelToDelete.getX(), lodModelToDelete.getZ())
.thenAccept((deleted) -> {
if (!deleted) {
this.warning("Could not delete LOD " + world.getName() + " " + lodModelToDelete.getX() + "x" + lodModelToDelete.getZ() + ".");
return;
}
this.getLodRepository().lodExistsAsync(lodModelToDelete.getWorldId(), lodModelToDelete.getX(), lodModelToDelete.getZ()).thenAccept((exists) -> {
if (!exists) {
return;
}
SectionPosition position = new SectionPosition();
position.setDetailLevel(6);
position.setX(lodModelToDelete.getX());
position.setZ(lodModelToDelete.getZ());
this.getLodRepository().deleteLodAsync(lodModelToDelete.getWorldId(), lodModelToDelete.getX(), lodModelToDelete.getZ())
.thenAccept((deleted) -> {
if (!deleted) {
this.warning("Could not delete LOD " + world.getName() + " " + lodModelToDelete.getX() + " " + lodModelToDelete.getZ() + ".");
return;
}
this.getLod(lodModelToDelete.getWorldId(), position)
.thenAccept((newLodModel) -> {
Configuration worldConfig = world.getConfig();
SectionPosition position = new SectionPosition();
position.setDetailLevel(6);
position.setX(lodModelToDelete.getX());
position.setZ(lodModelToDelete.getZ());
// If this is false, then it will be false for all players as well.
boolean updatesEnabled = worldConfig.getBool(DhsConfig.REAL_TIME_UPDATES_ENABLED);
this.getLod(lodModelToDelete.getWorldId(), position)
.thenAcceptAsync((newLodModel) -> {
Configuration worldConfig = world.getConfig();
if (!updatesEnabled) {
this.debug("New LOD " + world.getName() + " " + lodModelToDelete.getX() + "x" + lodModelToDelete.getZ() + " generated, but real-time updates are disabled.");
return;
}
// If this is false, then it will be false for all players as well.
boolean updatesEnabled = worldConfig.getBool(DhsConfig.REAL_TIME_UPDATES_ENABLED);
String levelKeyPrefix = worldConfig.getString(DhsConfig.LEVEL_KEY_PREFIX);
String levelKey = world.getName();
if (levelKeyPrefix != null) {
levelKey = levelKeyPrefix + levelKey;
}
int lodChunkX = Coordinates.sectionToChunk(lodModelToDelete.getX());
int lodChunkZ = Coordinates.sectionToChunk(lodModelToDelete.getZ());
int playersInRangeCount = 0;
int playersOutOfRangeCount = 0;
int playersWithoutDhCount = 0;
// TODO: Don't use Bukkit classes.
for (Player player : Bukkit.getWorld(newLodModel.getWorldId()).getPlayers()) {
Configuration playerConfig = this.getPlayerConfiguration(player.getUniqueId());
// No config for this player? Probably not using DH.
if (playerConfig == null) {
playersWithoutDhCount++;
continue;
if (!updatesEnabled) {
this.debug("New LOD " + world.getName() + " " + lodModelToDelete.getX() + " " + lodModelToDelete.getZ() + " generated, but real-time updates are disabled.");
return;
}
int updatesRadius = playerConfig.getInt(DhsConfig.REAL_TIME_UPDATE_RADIUS);
String levelKeyPrefix = worldConfig.getString(DhsConfig.LEVEL_KEY_PREFIX);
String levelKey = world.getKey();
int playerChunkX = Coordinates.blockToChunk(player.getLocation().getBlockX());
int playerChunkZ = Coordinates.blockToChunk(player.getLocation().getBlockZ());
int distanceX = Math.abs(Math.max(lodChunkX, playerChunkX) - Math.min(lodChunkX, playerChunkX));
int distanceZ = Math.abs(Math.max(lodChunkZ, playerChunkZ) - Math.min(lodChunkZ, playerChunkZ));
// Update outside of player's range?
if (distanceX > updatesRadius || distanceZ > updatesRadius) {
playersOutOfRangeCount++;
continue;
if (levelKeyPrefix != null) {
levelKey = levelKeyPrefix + levelKey;
}
playersInRangeCount++;
int lodChunkX = Coordinates.sectionToChunk(lodModelToDelete.getX());
int lodChunkZ = Coordinates.sectionToChunk(lodModelToDelete.getZ());
int myBufferId = playerConfig.getInt("buffer-id", 0) + 1;
playerConfig.set("buffer-id", myBufferId);
int playersInRangeCount = 0;
int playersOutOfRangeCount = 0;
int playersWithoutDhCount = 0;
FullDataPartialUpdateMessage partialUpdateMessage = new FullDataPartialUpdateMessage();
partialUpdateMessage.setLevelKey(levelKey);
partialUpdateMessage.setBufferId(myBufferId);
partialUpdateMessage.setBeacons(newLodModel.getBeacons());
// TODO: Don't use Bukkit classes.
for (Player player : Bukkit.getWorld(newLodModel.getWorldId()).getPlayers()) {
Configuration playerConfig = this.getPlayerConfiguration(player.getUniqueId());
byte[] data = newLodModel.getData();
// No config for this player? Probably not using DH.
if (playerConfig == null) {
playersWithoutDhCount++;
continue;
}
int chunkCount = (int) Math.ceil((double) data.length / LodHandler.CHUNK_SIZE);
if (!playerConfig.getBool(DhsConfig.REAL_TIME_UPDATES_ENABLED)) {
continue;
}
for (int chunkNo = 0; chunkNo < chunkCount; chunkNo++) {
FullDataChunkMessage chunkResponse = new FullDataChunkMessage();
chunkResponse.setBufferId(myBufferId);
chunkResponse.setIsFirst(chunkNo == 0);
chunkResponse.setData(Arrays.copyOfRange(
data,
LodHandler.CHUNK_SIZE * chunkNo,
Math.min(LodHandler.CHUNK_SIZE * chunkNo + LodHandler.CHUNK_SIZE, data.length)
));
int updatesRadius = playerConfig.getInt(DhsConfig.REAL_TIME_UPDATE_RADIUS);
this.pluginMessageHandler.sendPluginMessage(player.getUniqueId(), chunkResponse);
int playerChunkX = Coordinates.blockToChunk(player.getLocation().getBlockX());
int playerChunkZ = Coordinates.blockToChunk(player.getLocation().getBlockZ());
int distanceX = Math.abs(Math.max(lodChunkX, playerChunkX) - Math.min(lodChunkX, playerChunkX));
int distanceZ = Math.abs(Math.max(lodChunkZ, playerChunkZ) - Math.min(lodChunkZ, playerChunkZ));
// Update outside of player's range?
if (distanceX > updatesRadius || distanceZ > updatesRadius) {
playersOutOfRangeCount++;
continue;
}
playersInRangeCount++;
int myBufferId = playerConfig.increment("buffer-id");
FullDataPartialUpdateMessage partialUpdateMessage = new FullDataPartialUpdateMessage();
partialUpdateMessage.setLevelKey(levelKey);
partialUpdateMessage.setBufferId(myBufferId);
partialUpdateMessage.setBeacons(newLodModel.getBeacons());
byte[] data = newLodModel.getData();
int chunkCount = (int) Math.ceil((double) data.length / LodHandler.CHUNK_SIZE);
for (int chunkNo = 0; chunkNo < chunkCount; chunkNo++) {
FullDataChunkMessage chunkResponse = new FullDataChunkMessage();
chunkResponse.setBufferId(myBufferId);
chunkResponse.setIsFirst(chunkNo == 0);
chunkResponse.setData(
data,
LodHandler.CHUNK_SIZE * chunkNo,
Math.min(LodHandler.CHUNK_SIZE, data.length - LodHandler.CHUNK_SIZE * chunkNo)
);
this.pluginMessageHandler.sendPluginMessage(player.getUniqueId(), chunkResponse);
}
this.pluginMessageHandler.sendPluginMessage(player.getUniqueId(), partialUpdateMessage);
}
this.pluginMessageHandler.sendPluginMessage(player.getUniqueId(), partialUpdateMessage);
}
this.debug("Updated LOD " + world.getName() + " " + lodModelToDelete.getX() + "x" + lodModelToDelete.getZ() + " sent to " + playersInRangeCount + " players. Found " + playersOutOfRangeCount + " players out of range, and " + playersWithoutDhCount + " players without DH.");
});
});
this.debug("Updated LOD " + world.getName() + " " + lodModelToDelete.getX() + " " + lodModelToDelete.getZ() + " sent to " + playersInRangeCount + " players. Found " + playersOutOfRangeCount + " players out of range, and " + playersWithoutDhCount + " players without DH.");
});
});
});
}
}
public boolean isPreGenerating(WorldInterface world)
{
return this.preGenerators.containsKey(world.getId()) && this.getPreGenerator(world).isRunning();
}
public void preGenerate(WorldInterface world, int centerX, int centerZ, int radius, boolean force)
{
if (this.isPreGenerating(world)) {
this.info("Cannot run multiple pre-generators in the same world. Stopping current task for " + world.getName() + "...");
this.stopPreGenerator(world);
}
this.preGenerators.put(world.getId(), new PreGenerator(this, world, centerX, centerZ, radius, force));
this.getScheduler().runOnSeparateThread(() -> {
this.preGenerators.get(world.getId()).run();
return null;
});
}
public @Nullable PreGenerator getPreGenerator(WorldInterface world)
{
return this.preGenerators.get(world.getId());
}
public void stopPreGenerator(WorldInterface world)
{
if (this.isPreGenerating(world)) {
this.getPreGenerator(world).stop();
this.preGenerators.remove(world.getId());
}
}
public CompletableFuture<Integer> trim(WorldInterface world, int centerX, int centerZ, int radius)
{
return this.getLodRepository().trimLodsAsync(
world.getId(),
Coordinates.blockToSection(centerX - radius),
Coordinates.blockToSection(centerZ - radius),
Coordinates.blockToSection(centerX + radius),
Coordinates.blockToSection(centerZ + radius)
);
}
public CompletableFuture<Double> benchmark(UUID worldId, int iterations, int concurrency)
{
WorldInterface world = this.getWorldInterface(worldId);
List<SectionPosition> positions = new ArrayList<>(concurrency);
for (int i = 0; i < concurrency; i++) {
SectionPosition position = new SectionPosition();
position.setX(Coordinates.blockToSection(world.getSpawnX()) + i);
position.setZ(Coordinates.blockToSection(world.getSpawnZ()));
positions.add(position);
}
return CompletableFuture.allOf(
positions.stream()
.map((position) -> this.generateLod(world.getId(), position, false))
.toArray(CompletableFuture[]::new)
)
.thenCompose((foo) -> {
List<CompletableFuture<?>> chains = new ArrayList<>(positions.size());
long ts = System.currentTimeMillis();
for (SectionPosition position : positions) {
CompletableFuture<?> chain = CompletableFuture.completedFuture(null);
for (int i = 0; i < iterations; i++) {
chain = chain.thenCompose((bar) -> this.generateLod(world.getId(), position, false));
}
chains.add(chain);
}
return CompletableFuture.allOf(chains.toArray(CompletableFuture[]::new))
.thenApply((bar) -> {
double elapsed = (System.currentTimeMillis() - ts) / 1000d;
return iterations * concurrency / elapsed;
});
});
}
}
@@ -0,0 +1,58 @@
/*
* DH Support, server-side support for Distant Horizons.
* Copyright (C) 2024 Jim C K Flaten
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package no.jckf.dhsupport.core;
import java.util.ArrayList;
import java.util.List;
public class PerformanceTracker
{
private static final int WINDOW_SIZE_MILLIS = 60 * 3 * 1000;
private final List<Long> pings = new ArrayList<>();
public synchronized void ping()
{
this.pings.add(System.currentTimeMillis());
}
public synchronized void prune()
{
long currentTimeMillis = System.currentTimeMillis();
this.pings.removeIf((pingTimeMillis) -> currentTimeMillis - pingTimeMillis > WINDOW_SIZE_MILLIS);
}
public double getPingsPerSecond()
{
if (this.pings.isEmpty()) {
return 0;
}
// Use the earliest ping to calculate window size if we don't have data for the full period.
long windowSize = Math.min(
WINDOW_SIZE_MILLIS / 1000,
System.currentTimeMillis() - this.pings.get(0)
);
this.prune();
return (double) this.pings.size() / windowSize;
}
}
@@ -0,0 +1,165 @@
/*
* DH Support, server-side support for Distant Horizons.
* Copyright (C) 2024 Jim C K Flaten
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package no.jckf.dhsupport.core;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import no.jckf.dhsupport.core.database.models.LodModel;
import no.jckf.dhsupport.core.dataobject.SectionPosition;
import no.jckf.dhsupport.core.world.WorldInterface;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class PreGenerator implements Runnable
{
protected DhSupport dhSupport;
protected WorldInterface world;
protected int radius;
protected int centerX;
protected int centerZ;
protected boolean force;
protected int totalSteps;
protected int stepsSoFar = 0;
protected int inFlight = 0;
protected boolean run = true;
protected long startTime = 0;
protected long stopTime = 0;
public PreGenerator(DhSupport dhSupport, WorldInterface world, int centerX, int centerZ, int radius, boolean force)
{
this.dhSupport = dhSupport;
this.world = world;
this.centerX = Coordinates.blockToSection(centerX);
this.centerZ = Coordinates.blockToSection(centerZ);
this.radius = radius;
this.force = force;
this.totalSteps = (int) Math.pow((double) Coordinates.blockToChunk(this.radius) / 2, 2);
}
public void run()
{
int rateLimit = this.world.getConfig().getInt(DhsConfig.FULL_DATA_REQUEST_CONCURRENCY_LIMIT);
int currentX = this.centerX;
int currentZ = this.centerZ;
int[][] directions = {{ 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 }};
int dirIndex = 0;
List<CompletableFuture<LodModel>> requests = new ArrayList<>(rateLimit);
this.startTime = System.currentTimeMillis();
OUTER: for (int step = 1; this.stepsSoFar < totalSteps; step++) {
for (int directionChanges = 0; directionChanges < 2; directionChanges++) {
for (int stepsOnThisSide = 0; stepsOnThisSide < step; stepsOnThisSide++) {
if (!this.run) {
break OUTER;
}
this.stepsSoFar++;
if (this.stepsSoFar > this.totalSteps) {
break OUTER;
}
currentX += directions[dirIndex][0];
currentZ += directions[dirIndex][1];
if (!this.force && this.dhSupport.lodRepository.lodExists(this.world.getId(), currentX, currentZ)) {
continue;
}
SectionPosition position = new SectionPosition();
position.setX(currentX);
position.setZ(currentZ);
position.setDetailLevel(6);
CompletableFuture<LodModel> request = this.dhSupport.generateLod(this.world.getId(), position, true);
request.handle((lodModel, exception) -> this.inFlight--);
requests.add(request);
this.inFlight++;
while (this.inFlight >= rateLimit) {
CompletableFuture.anyOf(requests.toArray(new CompletableFuture[0])).join();
requests.removeIf(CompletableFuture::isDone);
}
}
dirIndex = (dirIndex + 1) % 4; // Change direction
}
}
this.stopTime = System.currentTimeMillis();
this.run = false;
}
public int getCompletedRequests()
{
return this.stepsSoFar - this.inFlight;
}
public int getTargetRequests()
{
return this.totalSteps;
}
public float getProgress()
{
return (float) this.getCompletedRequests() / this.getTargetRequests();
}
public Duration getElapsedTime()
{
if (this.startTime == 0) {
return Duration.ZERO;
}
return Duration.ofMillis((this.stopTime == 0 ? System.currentTimeMillis() : this.stopTime) - this.startTime);
}
public boolean isRunning()
{
return this.run;
}
public void stop()
{
this.run = false;
}
}
@@ -0,0 +1,110 @@
/*
* DH Support, server-side support for Distant Horizons.
* Copyright (C) 2024 Jim C K Flaten
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package no.jckf.dhsupport.core;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
public class UpdateChecker
{
protected int projectId;
public UpdateChecker(int projectId)
{
this.projectId = projectId;
}
protected JSONObject getLatestRelease() throws IOException
{
URL url = URI.create("https://gitlab.com/api/v4/projects/" + this.projectId + "/releases").toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
return null;
}
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
response.append(line);
}
in.close();
JSONArray releases = new JSONArray(response.toString());
if (releases.isEmpty()) {
return null;
}
return (JSONObject) releases.get(0);
}
protected String getLatestVersion()
{
try {
return this.getLatestRelease().getString("tag_name");
} catch (IOException exception) {
}
return null;
}
public boolean isLatestVersion(String version)
{
String latestVersionString = this.getLatestVersion();
// If the version check fails, pretend like we're up to date.
if (latestVersionString == null) {
return true;
}
String[] myVersion = version.split("[.-]");
String[] latestVersion = latestVersionString.split("\\.");
for (int i = 0; i < 3; i++) {
int mySegment = Integer.parseInt(myVersion[i]);
int latestSegment = Integer.parseInt(latestVersion[i]);
// Our version is newer than the latest release.
if (mySegment > latestSegment) {
return true;
}
// Our version is older than the latest release.
if (mySegment < latestSegment) {
return false;
}
}
// Our version is equal to the latest release.
return true;
}
}
@@ -19,23 +19,42 @@
package no.jckf.dhsupport.core;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class Utils
{
private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
private static final Map<String, String> NAMESPACE_CACHE = new ConcurrentHashMap<>();
private static final Map<String, String> UNNAMESPACE_CACHE = new ConcurrentHashMap<>();
public static String bytesToHex(byte[] bytes)
{
byte[] hexChars = new byte[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars, StandardCharsets.UTF_8);
}
public static String bytesToBin(byte[] bytes)
{
StringBuilder builder = new StringBuilder();
for (byte b : bytes) {
builder.append(Integer.toBinaryString((b & 0xFF) | 0x0100).substring(1));
}
return builder.toString().replaceAll("(.{4})", "$1 ").trim();
}
public static String ucFirst(String string, Locale locale)
{
if (string.isEmpty()) {
@@ -49,4 +68,81 @@ public class Utils
{
return ucFirst(string, Locale.getDefault());
}
public static String humanReadableDuration(final Duration duration)
{
if (duration.toDays() >= 365) {
return "infinite";
}
final StringBuilder builder = new StringBuilder();
if (duration.toDays() > 0) {
builder.append(duration.toDaysPart()).append(" day");
if (duration.toDaysPart() != 1) {
builder.append("s");
}
builder.append(", ");
}
if (duration.toHours() > 0) {
builder.append(duration.toHoursPart()).append(" hour");
if (duration.toHoursPart() != 1) {
builder.append("s");
}
builder.append(", ");
}
if (duration.toMinutes() > 0) {
builder.append(duration.toMinutesPart()).append(" minute");
if (duration.toMinutesPart() != 1) {
builder.append("s");
}
builder.append(", ");
}
if (duration.toSeconds() > 0 || builder.isEmpty()) {
builder.append(duration.toSecondsPart()).append(" second");
if (duration.toSecondsPart() != 1) {
builder.append("s");
}
builder.append(", ");
}
return builder.substring(0, builder.length() - 2);
}
/**
* @deprecated For awareness: This method caches results by key only.
*/
public static String namespaceKey(String key, String defaultNamespace)
{
return NAMESPACE_CACHE.computeIfAbsent(key, (k) -> k.contains(":") ? k : defaultNamespace + ":" + k);
}
public static String namespaceKey(String key)
{
return namespaceKey(key, "minecraft");
}
/**
* @deprecated For awareness: This method caches results by key only.
*/
public static String unNamespaceKey(String key, String namespace)
{
return UNNAMESPACE_CACHE.computeIfAbsent(key, (k) -> k.startsWith(namespace + ":") ? k.substring(namespace.length() + 1) : k);
}
public static String unNamespaceKey(String key)
{
return unNamespaceKey(key, "minecraft");
}
}
@@ -110,7 +110,7 @@ public class Decoder
{
int size = this.readInt();
List<T> items = new ArrayList<>();
List<T> items = new ArrayList<>(Math.min(size, 128));
for (int i = 0; i < size; i++) {
T item = this.readObject(className);
@@ -33,11 +33,21 @@ public class Encoder
this.output = ByteStreams.newDataOutput();
}
public Encoder(int size)
{
this.output = ByteStreams.newDataOutput(size);
}
public void write(byte[] data)
{
this.output.write(data);
}
public void write(byte[] data, int offset, int length)
{
this.output.write(data, offset, length);
}
public void writeByte(byte value)
{
this.output.writeByte(value);
@@ -18,16 +18,43 @@
package no.jckf.dhsupport.core.configuration;
import org.checkerframework.checker.nullness.qual.PolyNull;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class Configuration
{
protected Map<String, Object> variables = new HashMap<>();
protected Map<String, Object> variables = new ConcurrentHashMap<>();
public void clear()
{
this.variables.clear();
}
public boolean has(String key)
{
return this.variables.containsKey(key);
}
public void set(String key, @Nullable Object value)
{
if (value instanceof String stringValue) {
if ("false".equalsIgnoreCase(stringValue)) {
value = false;
} else if ("true".equalsIgnoreCase(stringValue)) {
value = true;
} else {
try {
value = Integer.parseInt(stringValue);
} catch (NumberFormatException exception) {}
}
}
this.variables.put(key, value);
}
@@ -36,7 +63,7 @@ public class Configuration
this.variables.remove(key);
}
public @Nullable Object get(String key, @Nullable Object defaultValue)
public @PolyNull Object get(String key, @PolyNull Object defaultValue)
{
return this.variables.getOrDefault(key, defaultValue);
}
@@ -46,7 +73,7 @@ public class Configuration
return this.get(key, null);
}
public @Nullable Boolean getBool(String key, @Nullable Boolean defaultValue)
public @PolyNull Boolean getBool(String key, @PolyNull Boolean defaultValue)
{
return (Boolean) this.get(key, defaultValue);
}
@@ -56,7 +83,7 @@ public class Configuration
return this.getBool(key, null);
}
public @Nullable Integer getInt(String key, @Nullable Integer defaultValue)
public @PolyNull Integer getInt(String key, @PolyNull Integer defaultValue)
{
return (Integer) this.get(key, defaultValue);
}
@@ -66,13 +93,30 @@ public class Configuration
return this.getInt(key, null);
}
public @Nullable String getString(String key, @Nullable String defaultValue)
public @PolyNull String getString(String key, @PolyNull String defaultValue)
{
return (String) this.get(key, defaultValue);
Object raw = this.get(key, defaultValue);
return raw == null ? null : String.valueOf(raw);
}
public @Nullable String getString(String key)
{
return (String) this.get(key, null);
return this.getString(key, null);
}
public @PolyNull List<String> getStringList(String key, @PolyNull List<String> defaultValue)
{
return (List<String>) this.get(key, defaultValue);
}
public List<String> getStringList(String key)
{
return this.getStringList(key, Collections.emptyList());
}
public int increment(String key)
{
return (int) this.variables.compute(key, (k, v) -> v == null ? 1 : (int) v + 1);
}
}
@@ -24,6 +24,10 @@ public abstract class DhsConfig
public static String DEBUG = "debug";
public static String DATABASE_PATH = "database_path";
public static String CHECK_FOR_UPDATES = "check_for_updates";
public static String RENDER_DISTANCE = "render_distance";
public static String DISTANT_GENERATION_ENABLED = "distant_generation_enabled";
@@ -44,6 +48,8 @@ public abstract class DhsConfig
public static String SCHEDULER_THREADS = "scheduler_threads";
public static String SERVER_KEY = "server_key";
public static String LEVEL_KEY_PREFIX = "level_key_prefix";
public static String BORDER_CENTER_X = "border_center_x";
@@ -51,16 +57,42 @@ public abstract class DhsConfig
public static String BORDER_CENTER_Z = "border_center_z";
public static String BORDER_RADIUS = "border_radius";
public static String USE_VANILLA_WORLD_BORDER = "use_vanilla_world_border";
public static String VANILLA_WORLD_BORDER_EXPANSION = "vanilla_world_border_expansion";
public static String SHOW_BUILDER_ACTIVITY = "show_builder_activity";
public static String LOD_REFRESH_INTERVAL = "lod_refresh_interval";
public static String GENERATE_NEW_CHUNKS = "generate_new_chunks";
public static String GENERATE_NEW_CHUNKS_WARNING = "generate_new_chunks_warning";
public static String BUILDER_TYPE = "builder_type";
public static String TRUST_HEIGHT_MAP = "trust_height_map";
public static String BUILDER_RESOLUTION = "builder_resolution";
public static String SCAN_TO_SEA_LEVEL= "scan_to_sea_level";
public static String FAST_UNDERFILL = "fast_underfill";
public static String INCLUDE_NON_COLLIDING_TOP_LAYER = "include_non-colliding_top_layer";
public static String INCLUDE_BEACONS = "include_beacons";
public static String PERFORM_UNDERGLOW_HACK = "perform_underglow_hack";
public static String SAMPLE_BIOMES_3D = "sample_biomes_3d";
public static String UPDATE_EVENTS = "update_events";
public static String ENABLE_DH_COMMAND_HELP_TEXT = "enable_dh_command_help_text";
public static String DUMMY_CHUNK = "dummy_chunk";
public static String MATERIAL_MAP = "material_map";
public static String AD_MESSAGE = "ad_message";
public static String LIGHT_OFFSET_HACK = "light_offset_hack";
}
@@ -1,6 +1,25 @@
/*
* DH Support, server-side support for Distant Horizons.
* Copyright (C) 2024 Jim C K Flaten
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package no.jckf.dhsupport.core.configuration;
import no.jckf.dhsupport.core.world.WorldInterface;
import org.checkerframework.checker.nullness.qual.PolyNull;
import javax.annotation.Nullable;
@@ -31,7 +50,7 @@ public class WorldConfiguration extends Configuration
}
@Override
public @Nullable Object get(String key, @Nullable Object defaultValue)
public @PolyNull Object get(String key, @PolyNull Object defaultValue)
{
Object specific = this.config.get(WORLD_PREFIX.formatted(this.world.getName()) + key);
@@ -21,8 +21,11 @@ package no.jckf.dhsupport.core.database;
import no.jckf.dhsupport.core.database.migrations.Migration;
import java.sql.*;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public class Database
{
@@ -30,7 +33,11 @@ public class Database
protected Connection connection;
protected Map<String, Class<? extends Migration>> migrations = new HashMap<>();
protected Map<String, PreparedStatement> preparedStatements = new ConcurrentHashMap<>();
protected List<Class<? extends Migration>> migrations = new ArrayList<>();
protected CompletableFuture<?> optimizing;
public Connection getConnection() throws SQLException
{
@@ -50,6 +57,8 @@ public class Database
public void close() throws SQLException
{
this.clearQueryCache();
if (this.connection == null || this.connection.isClosed()) {
return;
}
@@ -57,6 +66,28 @@ public class Database
this.getConnection().close();
}
public PreparedStatement prepareAndReuse(String sql) throws SQLException
{
this.waitForOptimization();
if (!this.preparedStatements.containsKey(sql)) {
this.preparedStatements.put(sql, this.getConnection().prepareStatement(sql));
}
return this.preparedStatements.get(sql);
}
public void clearQueryCache()
{
for (String key : this.preparedStatements.keySet()) {
try {
this.preparedStatements.remove(key).close();
} catch (SQLException exception) {
}
}
}
protected void createMigrationsTable() throws SQLException
{
String sql = """
@@ -73,7 +104,7 @@ public class Database
public void addMigration(Class<? extends Migration> migration)
{
this.migrations.put(migration.getSimpleName(), migration);
this.migrations.add(migration);
}
public boolean hasMigrationRan(String name) throws SQLException
@@ -105,16 +136,42 @@ public class Database
{
this.createMigrationsTable();
for (String name : this.migrations.keySet()) {
for (Class<? extends Migration> migrationClass : this.migrations) {
String name = migrationClass.getSimpleName();
if (this.hasMigrationRan(name)) {
continue;
}
Migration migration = this.migrations.get(name).getConstructor(this.getClass()).newInstance(this);
Migration migration = migrationClass.getConstructor(this.getClass()).newInstance(this);
migration.up();
this.markMigrationAsRan(name);
}
}
public void optimize() throws Exception
{
this.optimizing = new CompletableFuture<>();
this.clearQueryCache();
try (Statement statement = this.getConnection().createStatement()) {
statement.execute("PRAGMA optimize");
statement.execute("ANALYZE");
statement.execute("REINDEX");
statement.execute("VACUUM");
} finally {
this.optimizing.complete(null);
this.optimizing = null;
}
}
public void waitForOptimization()
{
if (this.optimizing != null) {
this.optimizing.join();
}
}
}
@@ -0,0 +1,41 @@
/*
* DH Support, server-side support for Distant Horizons.
* Copyright (C) 2024 Jim C K Flaten
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package no.jckf.dhsupport.core.database.migrations;
import no.jckf.dhsupport.core.database.Database;
import java.sql.Statement;
public class AddLodVersionColumn extends Migration
{
public AddLodVersionColumn(Database database)
{
super(database);
}
@Override
public void up() throws Exception
{
try (Statement statement = this.database.getConnection().createStatement()) {
statement.execute("""
ALTER TABLE lods ADD version INT NULL;
""");
}
}
}
@@ -0,0 +1,41 @@
/*
* DH Support, server-side support for Distant Horizons.
* Copyright (C) 2024 Jim C K Flaten
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package no.jckf.dhsupport.core.database.migrations;
import no.jckf.dhsupport.core.database.Database;
import java.sql.Statement;
public class DeleteLodsWithoutVersion extends Migration
{
public DeleteLodsWithoutVersion(Database database)
{
super(database);
}
@Override
public void up() throws Exception
{
try (Statement statement = this.database.getConnection().createStatement()) {
statement.execute("""
DELETE FROM lods WHERE version IS NULL;
""");
}
}
}
@@ -22,6 +22,8 @@ import java.util.UUID;
public class LodModel extends Model
{
public static int VERSION = 2;
public static LodModel create()
{
return new LodModel();
@@ -39,6 +41,8 @@ public class LodModel extends Model
protected int timestamp;
protected int version;
public LodModel setWorldId(UUID worldId)
{
this.worldId = worldId;
@@ -60,7 +64,7 @@ public class LodModel extends Model
public int getX()
{
return x;
return this.x;
}
public LodModel setZ(int z)
@@ -72,7 +76,7 @@ public class LodModel extends Model
public int getZ()
{
return z;
return this.z;
}
public LodModel setData(byte[] data)
@@ -84,7 +88,7 @@ public class LodModel extends Model
public byte[] getData()
{
return data;
return this.data;
}
public LodModel setBeacons(byte[] beacons)
@@ -96,7 +100,7 @@ public class LodModel extends Model
public byte[] getBeacons()
{
return beacons;
return this.beacons;
}
public LodModel setTimestamp(int timestamp)
@@ -111,6 +115,18 @@ public class LodModel extends Model
return this.timestamp;
}
public LodModel setVersion(int version)
{
this.version = version;
return this;
}
public int getVersion()
{
return this.version;
}
public String toString()
{
return this.getWorldId() + "@" + this.getX() + "x" + this.getZ();
@@ -51,9 +51,9 @@ public class AsyncLodRepository extends LodRepository
return future;
}
public CompletableFuture<LodModel> saveLodAsync(UUID worldId, int sectionX, int sectionZ, byte[] data, byte[] beacons)
public CompletableFuture<LodModel> saveLodAsync(UUID worldId, int sectionX, int sectionZ, byte[] data, byte[] beacons, int version)
{
return this.queueTask(() -> this.saveLod(worldId, sectionX, sectionZ, data, beacons));
return this.queueTask(() -> this.saveLod(worldId, sectionX, sectionZ, data, beacons, version));
}
public CompletableFuture<LodModel> loadLodAsync(UUID worldId, int sectionX, int sectionZ)
@@ -70,4 +70,9 @@ public class AsyncLodRepository extends LodRepository
{
return this.queueTask(() -> this.deleteLod(worldId, sectionX, sectionZ));
}
public CompletableFuture<Integer> trimLodsAsync(UUID worldId, int lowSectionX, int lowSectionZ, int highSectionX, int highSectionZ)
{
return this.queueTask(() -> this.trimLods(worldId, lowSectionX,lowSectionZ, highSectionX, highSectionZ));
}
}
@@ -29,6 +29,16 @@ import java.util.logging.Logger;
public class LodRepository
{
protected static final String SQL_SAVE_LOD = "REPLACE INTO lods (worldId, x, z, data, beacons, timestamp, version) VALUES (?, ?, ?, ?, ?, ?, ?)";
protected static final String SQL_LOAD_LOD = "SELECT data, beacons, timestamp FROM lods WHERE worldId = ? AND x = ? AND z = ? LIMIT 1";
protected static final String SQL_LOD_EXISTS = "SELECT EXISTS( SELECT 1 FROM lods WHERE worldId = ? AND x = ? AND z = ? )";
protected static final String SQL_DELETE_LOD = "DELETE FROM lods WHERE worldId = ? AND x = ? AND z = ?";
protected static final String SQL_TRIM_LODS = "DELETE FROM lods WHERE worldId = ? AND (x < ? OR z < ? OR x > ? OR z > ?)";
protected Database database;
protected Logger logger;
@@ -48,19 +58,20 @@ public class LodRepository
return this.logger;
}
public LodModel saveLod(UUID worldId, int sectionX, int sectionZ, byte[] data, byte[] beacons)
public LodModel saveLod(UUID worldId, int sectionX, int sectionZ, byte[] data, byte[] beacons, int version)
{
int timestamp = (int) (System.currentTimeMillis() / 1000);
String sql = "REPLACE INTO lods (worldId, x, z, data, beacons, timestamp) VALUES (?, ?, ?, ?, ?, ?)";
try {
PreparedStatement statement = this.database.prepareAndReuse(SQL_SAVE_LOD);
try (PreparedStatement statement = this.database.getConnection().prepareStatement(sql)) {
statement.setString(1, worldId.toString());
statement.setInt(2, sectionX);
statement.setInt(3, sectionZ);
statement.setBytes(4, data);
statement.setBytes(5, beacons);
statement.setInt(6, timestamp);
statement.setInt(7, version);
statement.executeUpdate();
@@ -70,69 +81,80 @@ public class LodRepository
.setZ(sectionZ)
.setData(data)
.setBeacons(beacons)
.setTimestamp(timestamp);
.setTimestamp(timestamp)
.setVersion(version);
} catch (SQLException exception) {
this.getLogger().warning("Could not save LOD: " + exception);
this.database.clearQueryCache();
return null;
}
}
public LodModel loadLod(UUID worldId, int sectionX, int sectionZ)
{
String sql = "SELECT data, beacons, timestamp FROM lods WHERE worldId = ? AND x = ? AND z = ? LIMIT 1";
try {
PreparedStatement statement = this.database.prepareAndReuse(SQL_LOAD_LOD);
try (PreparedStatement statement = this.database.getConnection().prepareStatement(sql)) {
statement.setString(1, worldId.toString());
statement.setInt(2, sectionX);
statement.setInt(3, sectionZ);
ResultSet result = statement.executeQuery();
try (ResultSet result = statement.executeQuery()) {
result.next();
byte[] data = result.getBytes("data");
byte[] data = result.getBytes("data");
if (data == null) {
return null;
if (data == null) {
return null;
}
return LodModel.create()
.setWorldId(worldId)
.setX(sectionX)
.setZ(sectionZ)
.setData(data)
.setBeacons(result.getBytes("beacons"))
.setTimestamp(result.getInt("timestamp"));
}
return LodModel.create()
.setWorldId(worldId)
.setX(sectionX)
.setZ(sectionZ)
.setData(data)
.setBeacons(result.getBytes("beacons"))
.setTimestamp(result.getInt("timestamp"));
} catch (SQLException exception) {
this.getLogger().warning("Could not load LOD: " + exception);
this.database.clearQueryCache();
return null;
}
}
public boolean lodExists(UUID worldId, int sectionX, int sectionZ)
{
String sql = "SELECT EXISTS( SELECT 1 FROM lods WHERE worldId = ? AND x = ? AND z = ? )";
try {
PreparedStatement statement = this.database.prepareAndReuse(SQL_LOD_EXISTS);
try (PreparedStatement statement = this.database.getConnection().prepareStatement(sql)) {
statement.setString(1, worldId.toString());
statement.setInt(2, sectionX);
statement.setInt(3, sectionZ);
ResultSet result = statement.executeQuery();
try (ResultSet result = statement.executeQuery()) {
result.next();
return result.getInt(1) == 1;
return result.getInt(1) == 1;
}
} catch (SQLException exception) {
this.getLogger().warning("Could not check LOD existence: " + exception);
this.database.clearQueryCache();
return false;
}
}
public boolean deleteLod(UUID worldId, int sectionX, int sectionZ)
{
String sql = "DELETE FROM lods WHERE worldId = ? AND x = ? AND z = ?";
try {
PreparedStatement statement = this.database.prepareAndReuse(SQL_DELETE_LOD);
try (PreparedStatement statement = this.database.getConnection().prepareStatement(sql)) {
statement.setString(1, worldId.toString());
statement.setInt(2, sectionX);
statement.setInt(3, sectionZ);
@@ -143,7 +165,32 @@ public class LodRepository
} catch (SQLException exception) {
this.getLogger().warning("Could not delete LOD: " + exception);
this.database.clearQueryCache();
return false;
}
}
public int trimLods(UUID worldId, int lowSectionX, int lowSectionZ, int highSectionX, int highSectionZ)
{
int affectedRows = 0;
try {
PreparedStatement statement = this.database.prepareAndReuse(SQL_TRIM_LODS);
statement.setString(1, worldId.toString());
statement.setInt(2, lowSectionX);
statement.setInt(3, lowSectionZ);
statement.setInt(4, highSectionX);
statement.setInt(5, highSectionZ);
affectedRows = statement.executeUpdate();
this.database.optimize();
} catch (Exception exception) {
this.getLogger().warning("Could not trim LODs/optimize DB: " + exception);
}
return affectedRows;
}
}
@@ -18,76 +18,102 @@
package no.jckf.dhsupport.core.dataobject;
import no.jckf.dhsupport.core.bytestream.Decoder;
import no.jckf.dhsupport.core.bytestream.Encoder;
public class DataPoint extends DataObject
{
protected byte skyLight = 0;
protected static int MAPPING_ID_OFFSET = 0;
protected static int MAPPING_ID_BITS = 32;
protected static long MAPPING_ID_MASK = 0xFFFFFFFFL;
protected byte blockLight = 0;
protected static int HEIGHT_OFFSET = MAPPING_ID_OFFSET + MAPPING_ID_BITS;
protected static int HEIGHT_BITS = 12;
protected static long HEIGHT_MASK = 0x0FFF;
protected int height = 1;
protected static int START_Y_OFFSET = HEIGHT_OFFSET + HEIGHT_BITS;
protected static int START_Y_BITS = 12;
protected static long START_Y_MASK = 0x0FFF;
protected int startY = 0;
protected static int SKY_LIGHT_OFFSET = START_Y_OFFSET + START_Y_BITS;
protected static int SKY_LIGHT_BITS = 4;
protected static long SKY_LIGHT_MASK = 0x0F;
protected int mappingId;
protected static int BLOCK_LIGHT_OFFSET = SKY_LIGHT_OFFSET + SKY_LIGHT_BITS;
protected static int BLOCK_LIGHT_BITS = 4;
protected static long BLOCK_LIGHT_MASK = 0x0F;
public void setSkyLight(byte value)
protected long data = 0;
public void setMappingId(int mappingId)
{
this.skyLight = value;
}
this.data &= ~(MAPPING_ID_MASK << MAPPING_ID_OFFSET);
public byte getSkyLight()
{
return this.skyLight;
}
public void setBlockLight(byte value)
{
this.blockLight = value;
}
public void setHeight(int value)
{
this.height = value;
}
public int getHeight()
{
return this.height;
}
public void setStartY(int value)
{
this.startY = value;
}
public int getStartY()
{
return this.startY;
}
public void setMappingId(int id)
{
this.mappingId = id;
this.data |= mappingId & MAPPING_ID_MASK;
}
public int getMappingId()
{
return this.mappingId;
return (int) ((this.data >> MAPPING_ID_OFFSET) & MAPPING_ID_MASK);
}
public void setHeight(int height)
{
this.data &= ~(HEIGHT_MASK << HEIGHT_OFFSET);
this.data |= (height & HEIGHT_MASK) << HEIGHT_OFFSET;
}
public int getHeight()
{
return (int) ((this.data >> HEIGHT_OFFSET) & HEIGHT_MASK);
}
public void setStartY(int startY)
{
this.data &= ~(START_Y_MASK << START_Y_OFFSET);
this.data |= ((long) startY & START_Y_MASK) << START_Y_OFFSET;
}
public int getStartY()
{
return (int) ((this.data >> START_Y_OFFSET) & START_Y_MASK);
}
public void setSkyLight(byte skyLight)
{
this.data &= ~(SKY_LIGHT_MASK << SKY_LIGHT_OFFSET);
this.data |= (skyLight & SKY_LIGHT_MASK) << SKY_LIGHT_OFFSET;
}
public byte getSkyLight()
{
return (byte) ((this.data >> SKY_LIGHT_OFFSET) & SKY_LIGHT_MASK);
}
public void setBlockLight(byte blockLight)
{
this.data &= ~(BLOCK_LIGHT_MASK << BLOCK_LIGHT_OFFSET);
this.data |= (blockLight & BLOCK_LIGHT_MASK) << BLOCK_LIGHT_OFFSET;
}
public byte getBlockLight()
{
return (byte) ((this.data >> BLOCK_LIGHT_OFFSET) & BLOCK_LIGHT_MASK);
}
@Override
public void encode(Encoder encoder)
{
long data = 0;
data |= this.mappingId;
data |= (long) (this.height & 0x0FFF) << 32;
data |= (long) (this.startY & 0x0FFF) << 32 + 12;
data |= (long) (this.skyLight & 0x0F) << 32 + 12 + 12;
data |= (long) (this.blockLight & 0x0F) << 32 + 12 + 12 + 4;
encoder.writeLong(data);
}
@Override
public void decode(Decoder decoder)
{
this.data = decoder.readLong();
}
}
@@ -43,6 +43,22 @@ public class IdMapping extends DataObject
this.properties = properties;
}
public String getBiome()
{
return this.biome;
}
public String getBlock()
{
return this.block;
}
@Nullable
public Map<String, String> getProperties()
{
return this.properties;
}
@Override
public void encode(Encoder encoder)
{
@@ -42,6 +42,10 @@ public class Lod extends DataObject
public static int width = 64;
protected static volatile byte[] compressedGenerationStep;
protected static volatile byte[] compressedWorldCompressionType;
protected WorldInterface worldInterface;
protected SectionPosition position;
@@ -50,17 +54,20 @@ public class Lod extends DataObject
protected List<List<DataPoint>> columns;
public Lod(WorldInterface worldInterface, SectionPosition position, List<IdMapping> idMappings, List<List<DataPoint>> columns)
protected List<Beacon> beacons;
public Lod(WorldInterface worldInterface, SectionPosition position, List<IdMapping> idMappings, List<List<DataPoint>> columns, List<Beacon> beacons)
{
this.worldInterface = worldInterface;
this.position = position;
this.idMappings = idMappings;
this.columns = columns;
this.beacons = beacons;
}
protected void encodeData(Encoder encoder)
{
Encoder toCompress = new Encoder();
Encoder toCompress = new Encoder(8192);
for (List<DataPoint> column : this.columns) {
toCompress.writeShort(column.size());
@@ -76,12 +83,25 @@ public class Lod extends DataObject
encoder.write(compressed);
}
protected void encodeAdjacentData(Encoder encoder)
{
encoder.writeInt(0);
encoder.writeInt(0);
encoder.writeInt(0);
encoder.writeInt(0);
}
protected void encodeColumnGenerationStep(Encoder encoder)
{
byte[] bytesToCompress = new byte[Lod.width * Lod.width];
Arrays.fill(bytesToCompress, (byte) GenerationStep.LIGHT.value);
byte[] compressed = Lod.compressedGenerationStep;
byte[] compressed = this.compress(bytesToCompress);
if (compressed == null) {
byte[] bytesToCompress = new byte[Lod.width * Lod.width];
Arrays.fill(bytesToCompress, (byte) GenerationStep.LIGHT.value);
compressed = this.compress(bytesToCompress);
Lod.compressedGenerationStep = compressed;
}
encoder.writeInt(compressed.length);
encoder.write(compressed);
@@ -89,9 +109,14 @@ public class Lod extends DataObject
protected void encodeWorldCompressionType(Encoder encoder)
{
byte[] bytesToCompress = new byte[Lod.width * Lod.width]; // All zeroes means WorldCompressionType.STRICT
byte[] compressed = Lod.compressedWorldCompressionType;
byte[] compressed = this.compress(bytesToCompress);
if (compressed == null) {
byte[] bytesToCompress = new byte[Lod.width * Lod.width]; // All zeroes means WorldCompressionType.STRICT
compressed = this.compress(bytesToCompress);
Lod.compressedWorldCompressionType = compressed;
}
encoder.writeInt(compressed.length);
encoder.write(compressed);
@@ -99,7 +124,7 @@ public class Lod extends DataObject
protected void encodeMappings(Encoder encoder)
{
Encoder toCompress = new Encoder();
Encoder toCompress = new Encoder(1024);
toCompress.writeInt(this.idMappings.size());
@@ -123,6 +148,7 @@ public class Lod extends DataObject
encoder.writeInt(0); // Checksum
this.encodeData(encoder);
this.encodeAdjacentData(encoder);
this.encodeColumnGenerationStep(encoder);
this.encodeWorldCompressionType(encoder);
this.encodeMappings(encoder);
@@ -143,7 +169,11 @@ public class Lod extends DataObject
ByteArrayOutputStream compressedStream = new ByteArrayOutputStream();
try {
OutputStream compressorStream = new XZOutputStream(compressedStream, new LZMA2Options(3), XZ.CHECK_CRC64);
LZMA2Options options = new LZMA2Options(3);
options.setDictSize(Math.max(LZMA2Options.DICT_SIZE_MIN, Math.min(uncompressedData.length, 1 << 20)));
OutputStream compressorStream = new XZOutputStream(compressedStream, options, XZ.CHECK_CRC64);
compressorStream.write(uncompressedData);
compressorStream.flush();
@@ -154,4 +184,9 @@ public class Lod extends DataObject
return compressedStream.toByteArray();
}
public List<Beacon> getBeacons()
{
return this.beacons;
}
}
@@ -23,101 +23,77 @@ import no.jckf.dhsupport.core.bytestream.Encoder;
public class SectionPosition extends DataObject
{
public static final int DETAIL_LEVEL_WIDTH = 8;
public static final int X_POS_WIDTH = 28;
public static final int Z_POS_WIDTH = 28;
public static final int DETAIL_LEVEL_OFFSET = 0;
public static final int POS_X_OFFSET = DETAIL_LEVEL_OFFSET + DETAIL_LEVEL_WIDTH;
public static final int POS_Z_OFFSET = POS_X_OFFSET + X_POS_WIDTH;
public static final int DETAIL_LEVEL_BITS = 8;
public static final long DETAIL_LEVEL_MASK = 0xFF;
public static final long DETAIL_LEVEL_MASK = Byte.MAX_VALUE;
public static final int POS_X_MASK = (int) Math.pow(2, X_POS_WIDTH) - 1;
public static final int POS_Z_MASK = (int) Math.pow(2, Z_POS_WIDTH) - 1;
public static final int X_OFFSET = DETAIL_LEVEL_OFFSET + DETAIL_LEVEL_BITS;
public static final int X_BITS = 28;
public static final long X_MASK = 0x0FFFFFFF;
protected int detailLevel;
public static final int Z_OFFSET = X_OFFSET + X_BITS;
public static final int Z_BITS = 28;
public static final long Z_MASK = 0x0FFFFFFF;
protected int x;
protected long data = 0;
protected int z;
public SectionPosition()
public void setDetailLevel(int detailLevel)
{
this.data &= ~(DETAIL_LEVEL_MASK << DETAIL_LEVEL_OFFSET);
}
public SectionPosition(int x, int z, int detailLevel)
{
this.x = x;
this.z = z;
this.detailLevel = detailLevel;
}
public void setDetailLevel(int level)
{
this.detailLevel = level;
this.data |= (detailLevel & DETAIL_LEVEL_MASK) << DETAIL_LEVEL_OFFSET;
}
public int getDetailLevel()
{
return this.detailLevel;
return (int) (data & DETAIL_LEVEL_MASK);
}
public void setX(int x)
{
this.x = x;
this.data &= ~(X_MASK << X_OFFSET);
this.data |= (x & X_MASK) << X_OFFSET;
}
public int getX()
{
return this.x;
int raw = (int) ((data >> X_OFFSET) & X_MASK);
if ((raw & (1 << 27)) != 0) {
raw |= (int) ~X_MASK;
}
return raw;
}
public void setZ(int z)
{
this.z = z;
this.data &= ~(Z_MASK << Z_OFFSET);
this.data |= (z & Z_MASK) << Z_OFFSET;
}
public int getZ()
{
return this.z;
int raw = (int) ((data >> Z_OFFSET) & Z_MASK);
if ((raw & (1 << 27)) != 0) {
raw |= (int) ~Z_MASK;
}
return raw;
}
@Override
public void encode(Encoder encoder)
{
encoder.writeLong(this.toLong());
encoder.writeLong(this.data);
}
@Override
public void decode(Decoder decoder)
{
this.fromLong(decoder.readLong());
}
public long toLong()
{
long data = 0;
data |= this.detailLevel & DETAIL_LEVEL_MASK;
data |= (long) (this.x & POS_X_MASK) << POS_X_OFFSET;
data |= (long) (this.z & POS_Z_MASK) << POS_Z_OFFSET;
return data;
}
public void fromLong(long data)
{
this.detailLevel = (int) (data & DETAIL_LEVEL_MASK);
this.x = (int) ((data >> POS_X_OFFSET) & POS_X_MASK);
this.z = (int) ((data >> POS_Z_OFFSET) & POS_Z_MASK);
// Adjust for potential negative values if masks do not account for sign
if ((this.x & (1 << 23)) != 0) { // Check if the sign bit is set for 24-bit value
this.x |= ~POS_X_MASK; // Sign extend
}
if ((this.z & (1 << 23)) != 0) { // Check if the sign bit is set for 24-bit value
this.z |= ~POS_Z_MASK; // Sign extend
}
this.data = decoder.readLong();
}
}
@@ -22,7 +22,6 @@ import no.jckf.dhsupport.core.Coordinates;
import no.jckf.dhsupport.core.DhSupport;
import no.jckf.dhsupport.core.configuration.Configuration;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import no.jckf.dhsupport.core.dataobject.Lod;
import no.jckf.dhsupport.core.dataobject.SectionPosition;
import no.jckf.dhsupport.core.message.plugin.ExceptionMessage;
import no.jckf.dhsupport.core.message.plugin.FullDataChunkMessage;
@@ -31,7 +30,6 @@ import no.jckf.dhsupport.core.message.plugin.FullDataSourceResponseMessage;
import no.jckf.dhsupport.core.world.WorldInterface;
import org.bukkit.Bukkit;
import java.util.Arrays;
import java.util.UUID;
public class LodHandler
@@ -51,7 +49,7 @@ public class LodHandler
public void register()
{
this.pluginMessageHandler.getEventBus().registerHandler(FullDataSourceRequestMessage.class, (requestMessage) -> {
//this.dhSupport.info("LOD request for " + requestMessage.getPosition().getX() + " x " + requestMessage.getPosition().getZ());
//this.dhSupport.info("LOD request for " + requestMessage.getPosition().getX() + " " + requestMessage.getPosition().getZ());
if (requestMessage.getPosition().getDetailLevel() != 6) {
ExceptionMessage exceptionMessage = new ExceptionMessage();
@@ -68,6 +66,16 @@ public class LodHandler
WorldInterface world = this.dhSupport.getWorldInterface(worldUuid);
Configuration config = world.getConfig();
Configuration playerConfig = this.dhSupport.getPlayerConfiguration(requestMessage.getSender());
if (!config.getBool(DhsConfig.DISTANT_GENERATION_ENABLED) || !playerConfig.getBool(DhsConfig.DISTANT_GENERATION_ENABLED)) {
ExceptionMessage exceptionMessage = new ExceptionMessage();
exceptionMessage.isResponseTo(requestMessage);
exceptionMessage.setTypeId(ExceptionMessage.TYPE_SECTION_REQUIRES_SPLITTING);
exceptionMessage.setMessage("Server has disabled distant generation");
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), exceptionMessage);
return;
}
SectionPosition position = requestMessage.getPosition();
@@ -77,7 +85,16 @@ public class LodHandler
ExceptionMessage exceptionMessage = new ExceptionMessage();
exceptionMessage.isResponseTo(requestMessage);
exceptionMessage.setTypeId(ExceptionMessage.TYPE_REQUEST_REJECTED);
exceptionMessage.setMessage("Generation disabled");
exceptionMessage.setMessage("Server has disabled LOD builder");
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), exceptionMessage);
return;
}
if (this.dhSupport.isPaused() && !this.dhSupport.getLodRepository().lodExists(worldUuid, position.getX(), position.getZ())) {
ExceptionMessage exceptionMessage = new ExceptionMessage();
exceptionMessage.isResponseTo(requestMessage);
exceptionMessage.setTypeId(ExceptionMessage.TYPE_REQUEST_REJECTED);
exceptionMessage.setMessage("Server has paused Distant Horizons");
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), exceptionMessage);
return;
}
@@ -85,9 +102,9 @@ public class LodHandler
int worldX = Coordinates.sectionToBlock(position.getX());
int worldZ = Coordinates.sectionToBlock(position.getZ());
Integer borderCenterX = config.getInt(DhsConfig.BORDER_CENTER_X);
Integer borderCenterZ = config.getInt(DhsConfig.BORDER_CENTER_Z);
Integer borderRadius = config.getInt(DhsConfig.BORDER_RADIUS);
Integer borderCenterX = world.getWorldBorderX();
Integer borderCenterZ = world.getWorldBorderZ();
Integer borderRadius = world.getWorldBorderRadius();
if (borderCenterX != null && borderCenterZ != null && borderRadius != null) {
int minX = borderCenterX - borderRadius;
@@ -128,22 +145,22 @@ public class LodHandler
this.dhSupport.getLod(worldUuid, position)
.thenAccept((lodModel) -> {
if (lodModel == null) {
ExceptionMessage exceptionMessage = new ExceptionMessage();
exceptionMessage.isResponseTo(requestMessage);
exceptionMessage.setTypeId(ExceptionMessage.TYPE_REQUEST_REJECTED);
exceptionMessage.setMessage("No LOD available");
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), exceptionMessage);
return;
}
FullDataSourceResponseMessage responseMessage = new FullDataSourceResponseMessage();
responseMessage.isResponseTo(requestMessage);
boolean sendData = requestMessage.getTimestamp() == null || (requestMessage.getTimestamp() / 1000) < lodModel.getTimestamp();
if (sendData) {
Configuration playerConfig = this.dhSupport.getPlayerConfiguration(requestMessage.getSender());
if (playerConfig == null) {
// Player disconnected before delivery.
return;
}
int myBufferId = playerConfig.getInt("buffer-id", 0) + 1;
playerConfig.set("buffer-id", myBufferId);
int myBufferId = playerConfig.increment("buffer-id");
responseMessage.setBufferId(myBufferId);
responseMessage.setBeacons(lodModel.getBeacons());
@@ -156,16 +173,16 @@ public class LodHandler
FullDataChunkMessage chunkResponse = new FullDataChunkMessage();
chunkResponse.setBufferId(myBufferId);
chunkResponse.setIsFirst(chunkNo == 0);
chunkResponse.setData(Arrays.copyOfRange(
chunkResponse.setData(
data,
CHUNK_SIZE * chunkNo,
Math.min(CHUNK_SIZE * chunkNo + CHUNK_SIZE, data.length)
));
Math.min(CHUNK_SIZE, data.length - CHUNK_SIZE * chunkNo)
);
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), chunkResponse);
}
//this.dhSupport.info("LOD in " + chunkCount + " parts sent for " + requestMessage.getPosition().getX() + " x " + requestMessage.getPosition().getZ());
//this.dhSupport.info("LOD in " + chunkCount + " parts sent for " + requestMessage.getPosition().getX() + " " + requestMessage.getPosition().getZ());
}
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), responseMessage);
@@ -22,13 +22,21 @@ import no.jckf.dhsupport.core.DhSupport;
import no.jckf.dhsupport.core.configuration.Configuration;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import no.jckf.dhsupport.core.message.plugin.LevelInitMessage;
import no.jckf.dhsupport.core.message.plugin.LevelInitRequestMessage;
import no.jckf.dhsupport.core.message.plugin.RemotePlayerConfigMessage;
import no.jckf.dhsupport.core.world.WorldInterface;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.util.UUID;
public class PlayerConfigHandler
{
public final static String USAGE_PERMISSION = "distant_horizons.worlds.%s";
protected DhSupport dhSupport;
protected PluginMessageHandler pluginMessageHandler;
@@ -41,30 +49,57 @@ public class PlayerConfigHandler
public void register()
{
this.pluginMessageHandler.getEventBus().registerHandler(LevelInitRequestMessage.class, (initRequestMessage) -> {
// TODO: Some sort of Player wrapper or interface object. Bukkit classes should not be imported here.
Player player = Bukkit.getPlayer(initRequestMessage.getSender());
NamespacedKey worldKey = NamespacedKey.fromString(initRequestMessage.getWorldKey());
// TODO: This too 👎
UUID worldUuid = null;
for (World world : Bukkit.getServer().getWorlds()) {
if (world.getKey().equals(worldKey)) {
worldUuid = world.getUID();
break;
}
}
if (worldUuid == null) {
this.dhSupport.warning("Player " + player.getName() + " requested init for unknown world " + worldKey + ".");
return;
}
WorldInterface world = this.dhSupport.getWorldInterface(worldUuid);
Configuration dhsConfig = world.getConfig();
String levelKeyPrefix = dhsConfig.getString(DhsConfig.LEVEL_KEY_PREFIX);
String levelKey = world.getKey();
if (levelKeyPrefix != null) {
levelKey = levelKeyPrefix + levelKey;
}
LevelInitMessage levelInitResponse = new LevelInitMessage();
levelInitResponse.setNamespacedKey(worldKey.toString());
levelInitResponse.setServerKey(dhsConfig.getString(DhsConfig.SERVER_KEY, ""));
levelInitResponse.setLevelKey(levelKey);
levelInitResponse.setTime(System.currentTimeMillis());
this.pluginMessageHandler.sendPluginMessage(initRequestMessage.getSender(), levelInitResponse);
});
this.pluginMessageHandler.getEventBus().registerHandler(RemotePlayerConfigMessage.class, (configMessage) -> {
// TODO: Some sort of Player wrapper or interface object. Bukkit classes should not be imported here.
Player player = Bukkit.getPlayer(configMessage.getSender());
WorldInterface world = this.dhSupport.getWorldInterface(player.getWorld().getUID());
double coordinateScale = world.getCoordinateScale();
String permission = USAGE_PERMISSION.formatted(world.getName());
boolean dhUseIsAllowed = !player.isPermissionSet(permission) || player.hasPermission(permission);
Configuration dhsConfig = world.getConfig();
String levelKeyPrefix = dhsConfig.getString(DhsConfig.LEVEL_KEY_PREFIX);
String levelKey = player.getWorld().getName();
if (levelKeyPrefix != null) {
levelKey = levelKeyPrefix + levelKey;
}
this.dhSupport.info("Received DH config for " + player.getName() + " in " + levelKey);
LevelInitMessage levelKeyResponse = new LevelInitMessage();
levelKeyResponse.setKey(levelKey);
levelKeyResponse.setTime(System.currentTimeMillis());
this.pluginMessageHandler.sendPluginMessage(configMessage.getSender(), levelKeyResponse);
Configuration clientConfig = configMessage.toConfiguration();
// This is not very flexible, but will do for now.
@@ -75,14 +110,29 @@ public class PlayerConfigHandler
Object clientValue = clientConfig.get(key);
Object keepValue = null;
// Hack to scale border center position.
if ((key.equals(DhsConfig.BORDER_CENTER_X) || key.equals(DhsConfig.BORDER_CENTER_Z)) && dhsValue != null) {
dhsValue = (int) (((Integer) dhsValue).doubleValue() * coordinateScale);
if (key.equals(DhsConfig.DISTANT_GENERATION_ENABLED)) {
if (dhUseIsAllowed) {
if (!(boolean) clientValue) {
player.sendMessage(ChatColor.GOLD + "This server supports Distant Horizons, but you have disabled Distant Generation in your DH settings.");
}
} else {
this.dhSupport.info("Player " + player.getName() + " has been denied the use of Distant Horizons through permissions.");
keepValue = false;
}
}
if (key.equals(DhsConfig.REAL_TIME_UPDATES_ENABLED) && !dhUseIsAllowed) {
keepValue = false;
}
// TODO: This is ugly. Move to comparator closures like in DH.
if (key.equals(DhsConfig.BORDER_CENTER_X) || key.equals(DhsConfig.BORDER_CENTER_Z) || key.equals(DhsConfig.BORDER_RADIUS)) {
keepValue = dhsValue;
if (key.equals(DhsConfig.BORDER_CENTER_X)) {
keepValue = world.getWorldBorderX();
} else if (key.equals(DhsConfig.BORDER_CENTER_Z)) {
keepValue = world.getWorldBorderZ();
} else if(key.equals(DhsConfig.BORDER_RADIUS)) {
keepValue = world.getWorldBorderRadius();
}
if (keepValue == null) {
@@ -99,7 +149,7 @@ public class PlayerConfigHandler
}
}
clientConfig.set(key, keepValue);
clientConfig.set(key, keepValue == null ? -1 : keepValue);
}
this.dhSupport.setPlayerConfiguration(configMessage.getSender(), clientConfig);
@@ -36,9 +36,9 @@ public class PluginMessageHandler
private MessageTypeRegistry messageTypeRegistry;
public final String pluginChannel = "distant_horizons:message";
public final String pluginChannel = "distant_horizons:msg";
public final int protocolVersion = 10;
public final short protocolVersion = 15;
private EventBus<PluginMessage> eventBus;
@@ -53,13 +53,14 @@ public class PluginMessageHandler
this.messageTypeRegistry.registerMessageType(0, null);
this.messageTypeRegistry.registerMessageType(1, CloseReasonMessage.class);
this.messageTypeRegistry.registerMessageType(2, LevelInitMessage.class);
this.messageTypeRegistry.registerMessageType(3, RemotePlayerConfigMessage.class);
this.messageTypeRegistry.registerMessageType(4, CancelMessage.class);
this.messageTypeRegistry.registerMessageType(5, ExceptionMessage.class);
this.messageTypeRegistry.registerMessageType(6, FullDataSourceRequestMessage.class);
this.messageTypeRegistry.registerMessageType(7, FullDataSourceResponseMessage.class);
this.messageTypeRegistry.registerMessageType(8, FullDataPartialUpdateMessage.class);
this.messageTypeRegistry.registerMessageType(9, FullDataChunkMessage.class);
this.messageTypeRegistry.registerMessageType(3, LevelInitRequestMessage.class);
this.messageTypeRegistry.registerMessageType(4, RemotePlayerConfigMessage.class);
this.messageTypeRegistry.registerMessageType(5, CancelMessage.class);
this.messageTypeRegistry.registerMessageType(6, ExceptionMessage.class);
this.messageTypeRegistry.registerMessageType(7, FullDataSourceRequestMessage.class);
this.messageTypeRegistry.registerMessageType(8, FullDataSourceResponseMessage.class);
this.messageTypeRegistry.registerMessageType(9, FullDataPartialUpdateMessage.class);
this.messageTypeRegistry.registerMessageType(10, FullDataChunkMessage.class);
}
public void onEnable()
@@ -94,6 +95,10 @@ public class PluginMessageHandler
return;
}
if (message instanceof CloseReasonMessage) {
throw new RuntimeException(((CloseReasonMessage) message).getReason());
}
message.setSender(senderUuid);
this.eventBus.dispatch(message);
@@ -105,10 +110,6 @@ public class PluginMessageHandler
Decoder decoder = new Decoder(data);
#if READ_FORGE_BYTE == "true"
decoder.readByte();
#endif
// Read the client's protocol version.
short protocolVersion = decoder.readShort();
@@ -116,7 +117,7 @@ public class PluginMessageHandler
CloseReasonMessage closeReasonMessage = new CloseReasonMessage();
closeReasonMessage.setReason((protocolVersion > this.protocolVersion ? "Server" : "Client") + " is outdated.");
this.sendPluginMessage(senderUuid, closeReasonMessage);
return null;
return closeReasonMessage;
}
// Read the message type ID.
@@ -153,23 +154,7 @@ public class PluginMessageHandler
return;
}
byte[] data;
try {
Encoder encoder = new Encoder();
message.encode(encoder);
data = encoder.toByteArray();
} catch (Exception exception) {
this.dhSupport.warning("Failed to encode " + message.getClass().getSimpleName());
exception.printStackTrace();
return;
}
Encoder encoder = new Encoder();
#if READ_FORGE_BYTE == "true"
encoder.writeByte(0);
#endif
Encoder encoder = new Encoder(message.bufferSize());
encoder.writeShort(this.protocolVersion);
encoder.writeShort(messageTypeId);
@@ -178,7 +163,13 @@ public class PluginMessageHandler
encoder.writeInt(((TrackablePluginMessage) message).getTracker());
}
encoder.write(data);
try {
message.encode(encoder);
} catch (Exception exception) {
this.dhSupport.warning("Failed to encode " + message.getClass().getSimpleName());
exception.printStackTrace();
return;
}
byte[] fullMessage = encoder.toByteArray();
@@ -20,10 +20,7 @@ package no.jckf.dhsupport.core.lodbuilders;
import no.jckf.dhsupport.core.Coordinates;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import no.jckf.dhsupport.core.dataobject.DataPoint;
import no.jckf.dhsupport.core.dataobject.IdMapping;
import no.jckf.dhsupport.core.dataobject.Lod;
import no.jckf.dhsupport.core.dataobject.SectionPosition;
import no.jckf.dhsupport.core.dataobject.*;
import no.jckf.dhsupport.core.world.WorldInterface;
import javax.annotation.Nullable;
@@ -51,15 +48,20 @@ public class FastOverworldBuilder extends LodBuilder
int offsetX = Coordinates.sectionToBlock(this.position.getX());
int offsetZ = Coordinates.sectionToBlock(this.position.getZ());
int yStep = this.worldInterface.getConfig().getInt(DhsConfig.BUILDER_RESOLUTION);
int yStep = 1;//this.worldInterface.getConfig().getInt(DhsConfig.BUILDER_RESOLUTION);
int originalStep = yStep;
boolean scanToSeaLevel = this.worldInterface.getConfig().getBool(DhsConfig.SCAN_TO_SEA_LEVEL, false);
boolean underfill = this.worldInterface.getConfig().getBool(DhsConfig.FAST_UNDERFILL, true);
boolean includeNonCollidingTopLayer = this.worldInterface.getConfig().getBool(DhsConfig.INCLUDE_NON_COLLIDING_TOP_LAYER, true);
boolean sampleBiomes3d = this.worldInterface.getConfig().getBool(DhsConfig.SAMPLE_BIOMES_3D, false);
List<IdMapping> idMappings = new ArrayList<>();
Map<String, Integer> mapMap = new HashMap<>();
List<IdMapping> idMappings = new ArrayList<>(64);
Map<String, Map<String, Integer>> mapMap = new HashMap<>();
List<List<DataPoint>> columns = new ArrayList<>();
List<List<DataPoint>> columns = new ArrayList<>(Lod.width * Lod.width);
List<Beacon> beacons = new ArrayList<>(8);
for (int relativeX = 0; relativeX < Lod.width; relativeX++) {
for (int relativeZ = 0; relativeZ < Lod.width; relativeZ++) {
@@ -68,10 +70,7 @@ public class FastOverworldBuilder extends LodBuilder
// Actual Y of top-most block.
int topLayer = this.worldInterface.getHighestYAt(worldX, worldZ);
// Copies of the original values.
int hardTopLayer = topLayer;
int originalStep = yStep;
if (includeNonCollidingTopLayer) {
outer: while (topLayer + 1 < maxY) {
@@ -95,9 +94,13 @@ public class FastOverworldBuilder extends LodBuilder
// Distance from bottom to top-most block.
int relativeTopLayer = topLayer - minY;
String biome = this.worldInterface.getBiomeAt(worldX, worldZ);
String biome = "";
List<DataPoint> column = new ArrayList<>();
if (!sampleBiomes3d) {
biome = this.worldInterface.getBiomeAt(worldX, worldZ);
}
List<DataPoint> column = new ArrayList<>(8);
@Nullable
DataPoint previous = null;
@@ -125,6 +128,19 @@ public class FastOverworldBuilder extends LodBuilder
String material = this.worldInterface.getMaterialAt(worldX, highWorldY, worldZ);
if (this.worldInterface.isBeacon(worldX, highWorldY, worldZ)) {
beacons.add(new Beacon(
worldX,
highWorldY,
worldZ,
worldInterface.getBeaconColor(
worldX,
highWorldY,
worldZ
)
));
}
if (solidGround == null && (!scanToSeaLevel || highWorldY <= seaLevel)) {
switch (material) {
case "minecraft:stone":
@@ -138,15 +154,22 @@ public class FastOverworldBuilder extends LodBuilder
}
}
String compositeKey = biome + "|" + material;
if (sampleBiomes3d) {
biome = this.worldInterface.getBiomeAt(worldX, highWorldY, worldZ);
}
@Nullable
Integer id = mapMap.get(compositeKey);
Map<String, Integer> biomeMap = mapMap.get(biome);
if (biomeMap == null) {
biomeMap = new HashMap<>();
mapMap.put(biome, biomeMap);
}
Integer id = biomeMap.get(material);
if (id == null) {
idMappings.add(new IdMapping(biome, material, null));
id = idMappings.size() - 1;
mapMap.put(compositeKey, id);
biomeMap.put(material, id);
}
DataPoint point;
@@ -164,10 +187,8 @@ public class FastOverworldBuilder extends LodBuilder
point.setHeight(thisStep);
point.setMappingId(id);
if (highWorldY + 1 < maxY) {
point.setSkyLight(this.worldInterface.getSkyLightAt(worldX, highWorldY + 1, worldZ));
point.setBlockLight(this.worldInterface.getBlockLightAt(worldX, highWorldY + 1, worldZ));
}
point.setSkyLight(this.worldInterface.getSkyLightAt(worldX, highWorldY + 1, worldZ));
point.setBlockLight(this.worldInterface.getBlockLightAt(worldX, highWorldY + 1, worldZ));
if (material.equals("minecraft:air") || material.equals("minecraft:void_air")) {
// Start by filling the top of the column with air, then jump down to the top layer.
@@ -189,10 +210,15 @@ public class FastOverworldBuilder extends LodBuilder
previous = point;
}
if (underfill && previous != null) {
previous.setHeight(previous.getStartY() + previous.getHeight());
previous.setStartY(0);
}
columns.add(column);
}
}
return new Lod(this.worldInterface, this.position, idMappings, columns);
return new Lod(this.worldInterface, this.position, idMappings, columns, beacons);
}
}
@@ -20,10 +20,7 @@ package no.jckf.dhsupport.core.lodbuilders;
import no.jckf.dhsupport.core.Coordinates;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import no.jckf.dhsupport.core.dataobject.DataPoint;
import no.jckf.dhsupport.core.dataobject.IdMapping;
import no.jckf.dhsupport.core.dataobject.Lod;
import no.jckf.dhsupport.core.dataobject.SectionPosition;
import no.jckf.dhsupport.core.dataobject.*;
import no.jckf.dhsupport.core.world.WorldInterface;
import javax.annotation.Nullable;
@@ -48,14 +45,19 @@ public class FullBuilder extends LodBuilder
int offsetX = Coordinates.sectionToBlock(this.position.getX());
int offsetZ = Coordinates.sectionToBlock(this.position.getZ());
int yStep = this.worldInterface.getConfig().getInt(DhsConfig.BUILDER_RESOLUTION);
int yStep = 1;//this.worldInterface.getConfig().getInt(DhsConfig.BUILDER_RESOLUTION);
int originalStep = yStep;
boolean includeNonCollidingTopLayer = this.worldInterface.getConfig().getBool(DhsConfig.INCLUDE_NON_COLLIDING_TOP_LAYER, true);
boolean performUnderglowHack = this.worldInterface.getConfig().getBool(DhsConfig.PERFORM_UNDERGLOW_HACK, false);
boolean sampleBiomes3d = this.worldInterface.getConfig().getBool(DhsConfig.SAMPLE_BIOMES_3D, false);
List<IdMapping> idMappings = new ArrayList<>();
Map<String, Integer> mapMap = new HashMap<>();
List<IdMapping> idMappings = new ArrayList<>(64);
Map<String, Map<String, Map<String, Integer>>> mapMap = new HashMap<>();
List<List<DataPoint>> columns = new ArrayList<>();
List<List<DataPoint>> columns = new ArrayList<>(Lod.width * Lod.width);
List<Beacon> beacons = new ArrayList<>(8);
for (int relativeX = 0; relativeX < Lod.width; relativeX++) {
for (int relativeZ = 0; relativeZ < Lod.width; relativeZ++) {
@@ -64,10 +66,7 @@ public class FullBuilder extends LodBuilder
// Actual Y of top-most block.
int topLayer = this.worldInterface.getHighestYAt(worldX, worldZ);
// Copies of the original values.
int hardTopLayer = topLayer;
int originalStep = yStep;
if (includeNonCollidingTopLayer) {
outer: while (topLayer + 1 < maxY) {
@@ -91,9 +90,13 @@ public class FullBuilder extends LodBuilder
// Distance from bottom to top-most block.
int relativeTopLayer = topLayer - minY;
String biome = this.worldInterface.getBiomeAt(worldX, worldZ);
String biome = "";
List<DataPoint> column = new ArrayList<>();
if (!sampleBiomes3d) {
biome = this.worldInterface.getBiomeAt(worldX, worldZ);
}
List<DataPoint> column = new ArrayList<>(32);
@Nullable
DataPoint previous = null;
@@ -118,15 +121,43 @@ public class FullBuilder extends LodBuilder
String material = this.worldInterface.getMaterialAt(worldX, highWorldY, worldZ);
String compositeKey = biome + "|" + material + "|" + this.worldInterface.getBlockStateAsStringAt(worldX, highWorldY, worldZ);
if (this.worldInterface.isBeacon(worldX, highWorldY, worldZ)) {
beacons.add(new Beacon(
worldX,
highWorldY,
worldZ,
worldInterface.getBeaconColor(
worldX,
highWorldY,
worldZ
)
));
}
@Nullable
Integer id = mapMap.get(compositeKey);
if (sampleBiomes3d) {
biome = this.worldInterface.getBiomeAt(worldX, highWorldY, worldZ);
}
Map<String, Map<String, Integer>> biomeMap = mapMap.get(biome);
if (biomeMap == null) {
biomeMap = new HashMap<>();
mapMap.put(biome, biomeMap);
}
Map<String, Integer> materialMap = biomeMap.get(material);
if (materialMap == null) {
materialMap = new HashMap<>();
biomeMap.put(material, materialMap);
}
String blockState = this.worldInterface.getBlockStateAsStringAt(worldX, highWorldY, worldZ);
Integer id = materialMap.get(blockState);
if (id == null) {
idMappings.add(new IdMapping(biome, material, this.worldInterface.getBlockPropertiesAt(worldX, highWorldY, worldZ)));
id = idMappings.size() - 1;
mapMap.put(compositeKey, id);
materialMap.put(blockState, id);
}
DataPoint point;
@@ -144,10 +175,8 @@ public class FullBuilder extends LodBuilder
point.setHeight(thisStep);
point.setMappingId(id);
if (highWorldY + 1 < maxY) {
point.setSkyLight(this.worldInterface.getSkyLightAt(worldX, highWorldY + 1, worldZ));
point.setBlockLight(this.worldInterface.getBlockLightAt(worldX, highWorldY + 1, worldZ));
}
point.setSkyLight(this.worldInterface.getSkyLightAt(worldX, highWorldY + 1, worldZ));
point.setBlockLight(this.worldInterface.getBlockLightAt(worldX, highWorldY + 1, worldZ));
if (material.equals("minecraft:air") || material.equals("minecraft:void_air")) {
// Start by filling the top of the column with air, then jump down to the top layer.
@@ -159,6 +188,23 @@ public class FullBuilder extends LodBuilder
} else {
// Encountered air that is below a non-air block. Set yStep=1 to avoid stretching the air into the ground or sea.
yStep = 1;
if (performUnderglowHack && previous != null && previous.getHeight() > 2) {
// Retract 2 block. We need a gap between the end of the top data point and the bottom one to prevent DH from merging them.
previous.setStartY(previous.getStartY() + 2);
previous.setHeight(previous.getHeight() - 2);
DataPoint bottomBlock = new DataPoint();
bottomBlock.setMappingId(previous.getMappingId());
bottomBlock.setStartY(relativeY + 1);
bottomBlock.setHeight(1);
bottomBlock.setSkyLight(this.worldInterface.getSkyLightAt(worldX, lowWorldY, worldZ));
bottomBlock.setBlockLight(this.worldInterface.getBlockLightAt(worldX, lowWorldY, worldZ));
column.add(bottomBlock);
previous = bottomBlock;
}
}
} else {
// Entered a new material. Reset yStep in case we came from air with yStep=1.
@@ -173,6 +219,6 @@ public class FullBuilder extends LodBuilder
}
}
return new Lod(this.worldInterface, this.position, idMappings, columns);
return new Lod(this.worldInterface, this.position, idMappings, columns, beacons);
}
}
@@ -25,6 +25,11 @@ import no.jckf.dhsupport.core.bytestream.Encoder;
public abstract class Message implements Encodable, Decodable
{
public int bufferSize()
{
return 64;
}
public void encode(Encoder encoder)
{
throw new UnsupportedOperationException();
@@ -26,6 +26,10 @@ public class FullDataChunkMessage extends PluginMessage
protected byte[] data;
protected int offset;
protected int length;
protected boolean isFirst = true;
public void setBufferId(int bufferId)
@@ -38,9 +42,11 @@ public class FullDataChunkMessage extends PluginMessage
return this.bufferId;
}
public void setData(byte[] data)
public void setData(byte[] data, int offset, int length)
{
this.data = data;
this.offset = offset;
this.length = length;
}
public byte[] getData()
@@ -58,12 +64,18 @@ public class FullDataChunkMessage extends PluginMessage
return this.isFirst;
}
@Override
public int bufferSize()
{
return 32 + this.length;
}
@Override
public void encode(Encoder encoder)
{
encoder.writeInt(this.bufferId);
encoder.writeInt(this.data.length);
encoder.write(this.data);
encoder.writeInt(this.length);
encoder.write(this.data, this.offset, this.length);
encoder.writeBoolean(this.isFirst);
}
}
@@ -1,3 +1,21 @@
/*
* DH Support, server-side support for Distant Horizons.
* Copyright (C) 2024 Jim C K Flaten
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package no.jckf.dhsupport.core.message.plugin;
import no.jckf.dhsupport.core.bytestream.Encoder;
@@ -40,6 +58,12 @@ public class FullDataPartialUpdateMessage extends PluginMessage
return this.beacons;
}
@Override
public int bufferSize()
{
return 32 + this.beacons.length;
}
@Override
public void encode(Encoder encoder)
{
@@ -48,6 +48,12 @@ public class FullDataSourceResponseMessage extends TrackablePluginMessage
return this.beacons;
}
@Override
public int bufferSize()
{
return this.bufferId == null ? super.bufferSize() : 16 + this.beacons.length;
}
@Override
public void encode(Encoder encoder)
{
@@ -21,20 +21,44 @@ package no.jckf.dhsupport.core.message.plugin;
import no.jckf.dhsupport.core.bytestream.Decoder;
import no.jckf.dhsupport.core.bytestream.Encoder;
public class LevelInitMessage extends PluginMessage
public class LevelInitMessage extends PluginMessage // TODO: Should probably eventually be a TrackablePluginMessage
{
protected String key;
protected String namespacedKey;
protected String serverKey;
protected String levelKey;
protected long time;
public void setKey(String key)
public void setNamespacedKey(String namespacedKey)
{
this.key = key;
this.namespacedKey = namespacedKey;
}
public String getKey()
public String getNamespacedKey()
{
return this.key;
return namespacedKey;
}
public void setServerKey(String serverKey)
{
this.serverKey = serverKey;
}
public String getServerKey()
{
return this.serverKey;
}
public void setLevelKey(String levelKey)
{
this.levelKey = levelKey;
}
public String getLevelKey()
{
return this.levelKey;
}
public void setTime(long time)
@@ -50,14 +74,18 @@ public class LevelInitMessage extends PluginMessage
@Override
public void encode(Encoder encoder)
{
encoder.writeShortString(this.key);
encoder.writeShortString(this.namespacedKey);
encoder.writeShortString(this.serverKey);
encoder.writeShortString(this.levelKey);
encoder.writeLong(this.time);
}
@Override
public void decode(Decoder decoder)
{
this.key = decoder.readShortString();
this.namespacedKey = decoder.readShortString();
this.serverKey = decoder.readShortString();
this.levelKey = decoder.readShortString();
this.time = decoder.readLong();
}
}
@@ -0,0 +1,37 @@
/*
* DH Support, server-side support for Distant Horizons.
* Copyright (C) 2024 Jim C K Flaten
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package no.jckf.dhsupport.core.message.plugin;
import no.jckf.dhsupport.core.bytestream.Decoder;
public class LevelInitRequestMessage extends PluginMessage
{
protected String worldKey;
public String getWorldKey()
{
return worldKey;
}
@Override
public void decode(Decoder decoder)
{
this.worldKey = decoder.readShortString();
}
}
@@ -20,6 +20,8 @@ package no.jckf.dhsupport.core.scheduling;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
public interface Scheduler
@@ -28,9 +30,23 @@ public interface Scheduler
<U> CompletableFuture<U> runOnMainThread(Supplier<U> supplier);
<U> CompletableFuture<U> runOnMainThreadDelayed(Supplier<U> supplier, long delayTicks);
<U> CompletableFuture<U> runOnMainThreadDelayed(Supplier<U> supplier, long delayAmount, TimeUnit delayUnit);
<U> CompletableFuture<U> runOnRegionThread(UUID worldId, int x, int z, Supplier<U> supplier);
<U> CompletableFuture<U> runOnRegionThreadDelayed(UUID worldId, int x, int z, Supplier<U> supplier, long delayTicks);
<U> CompletableFuture<U> runOnRegionThreadDelayed(UUID worldId, int x, int z, Supplier<U> supplier, long delayAmount, TimeUnit delayUnit);
<U> CompletableFuture<U> runOnSeparateThread(Supplier<U> supplier);
<U> CompletableFuture<U> runOnSeparateThreadDelayed(Supplier<U> supplier, long delayTicks);
<U> CompletableFuture<U> runOnSeparateThreadDelayed(Supplier<U> supplier, long delayAmount, TimeUnit delayUnit);
void cancelTasks();
Executor getExecutor();
}
@@ -0,0 +1,42 @@
/*
* DH Support, server-side support for Distant Horizons.
* Copyright (C) 2024 Jim C K Flaten
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package no.jckf.dhsupport.core.world;
public class MaterialRecord
{
protected final String key;
protected final boolean isMapped;
public MaterialRecord(String key, boolean mapped)
{
this.key = key;
this.isMapped = mapped;
}
public boolean isMapped()
{
return this.isMapped;
}
public String toString()
{
return this.key;
}
}
@@ -19,9 +19,8 @@
package no.jckf.dhsupport.core.world;
import no.jckf.dhsupport.core.configuration.Configurable;
import no.jckf.dhsupport.core.dataobject.Beacon;
import java.util.Collection;
import javax.annotation.Nullable;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -34,16 +33,32 @@ public interface WorldInterface extends Configurable
String getName();
String getKey();
double getCoordinateScale();
@Nullable Integer getWorldBorderX();
@Nullable Integer getWorldBorderZ();
@Nullable Integer getWorldBorderRadius();
void setDummyMode(boolean enable);
boolean isDummyMode();
boolean chunkExists(int x, int z);
boolean isChunkLoaded(int x, int z);
boolean loadChunk(int x, int z);
boolean loadOrGenerateChunk(int x, int z);
CompletableFuture<Boolean> loadChunkAsync(int x, int z);
CompletableFuture<Boolean> loadOrGenerateChunkAsync(int x, int z);
boolean unloadChunk(int x, int z);
boolean unloadChunkAsync(int x, int z);
@@ -56,19 +71,37 @@ public interface WorldInterface extends Configurable
int getSeaLevel();
int getSpawnX();
int getSpawnY();
int getSpawnZ();
int getHighestYAt(int x, int z);
String getBiomeAt(int x, int z);
String getBiomeAt(int x, int y, int z);
String getMaterialAt(int x, int y, int z);
/**
* Get the string representation of the block state at the given coordinates.
* Block state properties are ordered in ascending order.
*/
String getBlockStateAsStringAt(int x, int y, int z);
/**
* Get block properties as key/value pairs.
* Ordered in ascending order.
*/
Map<String, String> getBlockPropertiesAt(int x, int y, int z);
byte getBlockLightAt(int x, int y, int z);
byte getSkyLightAt(int x, int y, int z);
Collection<Beacon> getBeaconsInChunk(int x, int z);
boolean isBeacon(int x, int y, int z);
int getBeaconColor(int x, int y, int z);
}
+144 -17
View File
@@ -1,4 +1,9 @@
# Maximum supported render distance. Clients will be capped to this value.
# Where should the plugin store its database file?
# Required.
# Default: "{datadir}/data.sqlite"
database_path: "{datadir}/data.sqlite"
# Maximum supported render distance, given as a radius in number of chunks. Clients will be capped to this value.
# Required.
# Default: 1024
render_distance: 1024
@@ -19,7 +24,7 @@ full_data_request_concurrency_limit: 20
# Default: true
real_time_updates_enabled: true
# Currently unsupported.
# How close to an update players have to be in order to receive it. Given as a radius in number of chunks.
# Required.
# Default: 128
real_time_update_radius: 128
@@ -29,7 +34,7 @@ real_time_update_radius: 128
# Default: true
login_data_sync_enabled: true
# How far away can a player request updates for during login sync?
# How far away can a player request updates for during login sync? Given as a radius in number of chunks.
# Required.
# Default: 128
login_data_sync_radius: 128
@@ -44,10 +49,29 @@ login_data_sync_rc_limit: 50
# Default: 0
max_data_transfer_speed: 0
# Output builder activity to the server console.
# Should new chunks be generated to build LODs?
# If you disable this, the plugin will not be able to create LODs in areas where chunks don't already exist.
# Optional.
# Default: true
#show_builder_activity: false
#generate_new_chunks: false
# Show warning during startup if chunk generation is enabled?
# Optional.
# Default: true
#generate_new_chunks_warning: false
# Specs for a fake LOD to send when chunk generation is disabled and LOD requests otherwise would have been rejected.
# Optional.
# Default: Disabled
#dummy_chunk:
# - "0-4,bedrock"
# - "5-79,stone"
# - "80-126,water"
# How frequently (in seconds) should LODs be updated if world changes are detected?
# Required.
# Default: 15
lod_refresh_interval: 15
# Select which LOD builder to use.
# Valid options are: FullBuilder, FastOverworldBuilder, and None.
@@ -55,8 +79,16 @@ max_data_transfer_speed: 0
# Default: FullBuilder
builder_type: FullBuilder
# Trust world height map.
# Disable if you're using a world that was generated with an external tool and your LODs come out broken.
# WARNING: This will make LOD building MUCH slower!
# Optional.
# Default: true
#trust_height_map: false
# Number of blocks to advance between each sample on the Y axis when generating LODs.
# A setting of 1 will sample every block, while 2 will sample every other block, and so on.
# CURRENTLY BROKEN. THIS VALUE IS IGNORED.
# Required.
# Default: 2
builder_resolution: 2
@@ -67,16 +99,51 @@ builder_resolution: 2
# Default: true
#include_non-colliding_top_layer: false
# Should beacons be included in LOD data?
# It's recommended to leave this disabled for the initial LOD generation, then enable it afterward.
# If your server is accessible via multiple different addresses, consider setting a server key to let clients know
# that even though the address is different; it's the same server.
# This value must be GLOBALLY UNIQUE! No other server on planet Earth should have the same server key as you.
# Optional.
# Default: false
#include_beacons: true
#server_key:
# When running DHS on a proxied server, set this to a value that is unique to this specific server.
# Optional.
#level_key_prefix:
# Use the vanilla world border of the respective world.
# Optional.
# Default: true
#use_vanilla_world_border: false
# How many chunks beyond the vanilla world border can we see (if it's enabled)?. If set to auto, it uses the server's
# view distance, which effectively means that if standing at the border you can see as far as the server would generate
# chunks naturally.
# Optional.
# Default: auto
#vanilla_world_border_expansion: 12
# List of blocks to replace with other blocks during LOD building.
# Useful to hide ugly blocks or valuable resources.
# Don't use the "minecraft:" prefix for vanilla blocks.
# DO use the namespace prefix for custom blocks.
# Optional.
# Default: Hide bamboo, seagrass, kelp, and iron + diamond ores.
material_map:
bamboo: air
seagrass: water
tall_seagrass: water
kelp: water
kelp_plant: water
iron_ore: stone
diamond_ore: stone
# Vertical distance to offset light data.
# This is a hack to work around SPIGOT-7498. If you're somehow on a 1.20.1 server _with_ this patch, set the value to 0.
# Optional.
# Default:
# - On 1.20.1: Minimum build height (-64 for vanilla worlds)
# - Any other version: 0
#light_offset_hack: 0
# World specific settings.
# You can override any of the above settings on a per-world basis.
worlds:
@@ -84,28 +151,88 @@ worlds:
# Use builder that is optimized for overworld.
builder_type: FastOverworldBuilder
# Should biome be checked for every data point, or just once per column?
# Enable if your world has 3d/vertical biomes.
# Optional.
# Default: false
#sample_biomes_3d: true
# When using the FastOverworldBuilder, should LODs always contain data down to at least sea level?
# Enable if your world has floating islands or mountains/structures with large overhangs above sea level.
# Optional.
# Default: false
#scan_to_sea_level: true
# Limit LOD generation to within this area.
# When using the FastOverworldBuilder, should the bottom data point be stretched to the lowest permitted build
# height? This will make the LODs look more complete, and gives a theoretical boost to render performance.
# Optional.
# Default: true
#fast_underfill: false
# Limit LOD generation to within this area. Alle three values are in blocks.
# Optional.
#border_center_x: 0
#border_center_z: 0
#border_radius: 5000
# It might make sense to have a lower view distance in the nether, since visibility will be low simply because of
# the level design anyway.
world_nether:
# It might make sense to have a lower view distance in the nether, since visibility will be low simply because of
# the level design anyway.
render_distance: 64
# Number of worker threads used to generate LODs.
# Does not affect Folia servers.
# This is a hack to prevent light from shining through ceilings, mostly noticeable on the nether ceiling.
# Only works with the FullBuilder, should only be used with builder_resolution=1, and does have some side effects.
#perform_underglow_hack: true
#builder_resolution: 1
# List of events that should trigger LOD updates.
# Required.
# Default: 2
scheduler_threads: 2
# Default: A bunch (:
update_events:
- org.bukkit.event.block.BlockPlaceEvent
- org.bukkit.event.block.BlockBreakEvent
# - org.bukkit.event.block.TNTPrimeEvent
- org.bukkit.event.block.BlockExplodeEvent
# - org.bukkit.event.block.BlockIgniteEvent
# - org.bukkit.event.block.BlockBurnEvent
# - org.bukkit.event.block.LeavesDecayEvent
# - org.bukkit.event.block.BlockFadeEvent
# - org.bukkit.event.block.BlockGrowEvent
# - org.bukkit.event.block.MoistureChangeEvent
# - org.bukkit.event.block.BlockRedstoneEvent
- org.bukkit.event.block.BlockPistonExtendEvent
- org.bukkit.event.block.BlockPistonRetractEvent
# - org.bukkit.event.block.SpongeAbsorbEvent
# - org.bukkit.event.inventory.FurnaceBurnEvent
- org.bukkit.event.world.StructureGrowEvent
- org.bukkit.event.world.ChunkPopulateEvent
# Number of worker threads used to generate LODs.
# Required.
# Default: 4
scheduler_threads: 4
# Show a message to players who join if they don't have Distant Horizons installed?
# Optional.
# Default: Disabled
#ad_message:
# - "§aThis server supports §bDistant Horizons§a!"
# - "§aVisit §nhttp://dh-mod.com/§r§a to learn more."
# If a user uses /dh should we give them a help text?
# Optional.
# Default: true
#enable_dh_command_help_text: false
# Should we notify server admins when a new version of the plugin is available?
# Optional.
# Default: true
#check_for_updates: false
# Enable debug logging?
# Optional.
# Default: false
#debug: true
# Don't touch :)
config_version: 5
config_version: 10
+12 -4
View File
@@ -1,8 +1,16 @@
name: DHSupport
version: ${project.version}
main: ${main.class}
api-version: ${mc.api-version}
mc-version: ${mc.api-version}${mc.api-version-patch}
version: ${version}
main: ${mainClass}
api-version: ${mcApiVersion}
prefix: DHS
folia-supported: true
author: Jckf
commands:
dhs:
description: Manage DH Support.
usage: /dhs
permission: distant_horizons.admin
dh:
description: Not used.
usage: /dh
permission: distant_horizons.admin
+176
View File
@@ -0,0 +1,176 @@
/*
* DH Support, server-side support for Distant Horizons.
* Copyright (C) 2024 Jim C K Flaten
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import junit.framework.TestCase;
import no.jckf.dhsupport.core.Utils;
import no.jckf.dhsupport.core.bytestream.Decoder;
import no.jckf.dhsupport.core.bytestream.Encoder;
import no.jckf.dhsupport.core.dataobject.DataPoint;
public class DataPointTest extends TestCase
{
private DataPoint dataPoint = new DataPoint();
public void testMappingId()
{
int randomNumber = (int) Math.floor(Math.random() * 100);
this.dataPoint.setMappingId(randomNumber);
assertEquals(randomNumber, this.dataPoint.getMappingId());
}
public void testMappingIdAgain()
{
this.testMappingId();
}
public void testHeight()
{
int randomNumber = (int) Math.floor(Math.random() * 10);
this.dataPoint.setHeight(randomNumber);
assertEquals(randomNumber, this.dataPoint.getHeight());
}
public void testHeightAgain()
{
this.testHeight();
}
public void testStartY()
{
int randomNumber = (int) Math.floor(Math.random() * 100);
this.dataPoint.setStartY(randomNumber);
assertEquals(randomNumber, this.dataPoint.getStartY());
}
public void testHighStartY()
{
int randomNumber = 4000; // Chosen by fair dice roll. Guaranteed to be random.
this.dataPoint.setStartY(randomNumber);
assertEquals(randomNumber, this.dataPoint.getStartY());
}
public void testStartYAgain()
{
this.testStartY();
}
public void testSkyLight()
{
byte randomNumber = (byte) Math.floor(Math.random() * 15);
this.dataPoint.setSkyLight(randomNumber);
assertEquals(randomNumber, this.dataPoint.getSkyLight());
}
public void testSkyLightAgain()
{
this.testSkyLight();
}
public void testBlockLight()
{
byte randomNumber = (byte) Math.floor(Math.random() * 15);
this.dataPoint.setBlockLight(randomNumber);
assertEquals(randomNumber, this.dataPoint.getBlockLight());
}
public void testBlockLightAgain()
{
this.testBlockLight();
}
public void testEncodeDecode()
{
int mappingId = (int) Math.floor(Math.random() * 100);
int height = (int) Math.floor(Math.random() * 10);
int startY = (int) Math.floor(Math.random() * 100);
byte skyLight = (byte) Math.floor(Math.random() * 15);
byte blockLight = (byte) Math.floor(Math.random() * 15);
// Set them in reverse order of how they appear in the packed long. This will reveal any accidental overwrites.
this.dataPoint.setBlockLight(blockLight);
this.dataPoint.setSkyLight(skyLight);
this.dataPoint.setStartY(startY);
this.dataPoint.setHeight(height);
this.dataPoint.setMappingId(mappingId);
Encoder encoder = new Encoder();
this.dataPoint.encode(encoder);
this.dataPoint = new DataPoint();
Decoder decoder = new Decoder(encoder.toByteArray());
this.dataPoint.decode(decoder);
assertEquals("Block light", blockLight, this.dataPoint.getBlockLight());
assertEquals("Sky light", skyLight, this.dataPoint.getSkyLight());
assertEquals("Start Y", startY, this.dataPoint.getStartY());
assertEquals("Height", height, this.dataPoint.getHeight());
assertEquals("Mapping ID", mappingId, this.dataPoint.getMappingId());
}
public void testBinary()
{
this.dataPoint = new DataPoint();
this.dataPoint.setMappingId(1);
Encoder mappingIdEncoder = new Encoder();
this.dataPoint.encode(mappingIdEncoder);
this.dataPoint.setMappingId(0);
this.dataPoint.setHeight(1);
Encoder heightEncoder = new Encoder();
this.dataPoint.encode(heightEncoder);
this.dataPoint.setHeight(0);
this.dataPoint.setStartY(1);
Encoder startYEncoder = new Encoder();
this.dataPoint.encode(startYEncoder);
this.dataPoint.setStartY(0);
this.dataPoint.setSkyLight((byte) 1);
Encoder skyLightEncoder = new Encoder();
this.dataPoint.encode(skyLightEncoder);
this.dataPoint.setSkyLight((byte) 0);
this.dataPoint.setBlockLight((byte) 1);
Encoder blockLightEncoder = new Encoder();
this.dataPoint.encode(blockLightEncoder);
this.dataPoint.setBlockLight((byte) 0);
// | Blk| Sky| Start Y| Height| Mapping ID|
assertEquals("Mapping ID", "0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001", Utils.bytesToBin(mappingIdEncoder.toByteArray()));
assertEquals("Height", "0000 0000 0000 0000 0000 0000 0000 0001 0000 0000 0000 0000 0000 0000 0000 0000", Utils.bytesToBin(heightEncoder.toByteArray()));
assertEquals("StartY", "0000 0000 0000 0000 0001 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000", Utils.bytesToBin(startYEncoder.toByteArray()));
assertEquals("Sky light", "0000 0001 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000", Utils.bytesToBin(skyLightEncoder.toByteArray()));
assertEquals("Block light", "0001 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000", Utils.bytesToBin(blockLightEncoder.toByteArray()));
}
}
+155
View File
@@ -0,0 +1,155 @@
/*
* DH Support, server-side support for Distant Horizons.
* Copyright (C) 2024 Jim C K Flaten
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import junit.framework.TestCase;
import no.jckf.dhsupport.core.Utils;
import no.jckf.dhsupport.core.bytestream.Decoder;
import no.jckf.dhsupport.core.bytestream.Encoder;
import no.jckf.dhsupport.core.dataobject.SectionPosition;
public class SectionPositionTest extends TestCase
{
protected SectionPosition sectionPosition = new SectionPosition();
public void testDetailLevel()
{
int randomNumber = (int) Math.ceil(Math.random() * 6);
this.sectionPosition.setDetailLevel(randomNumber);
assertEquals(randomNumber, this.sectionPosition.getDetailLevel());
}
public void testDetailLevelAgain()
{
this.testDetailLevel();
}
public void testPositiveX()
{
int randomNumber = (int) Math.ceil(Math.random() * 100000);
this.sectionPosition.setX(randomNumber);
assertEquals(randomNumber, this.sectionPosition.getX());
}
public void testNegativeX()
{
int randomNumber = (int) Math.ceil(0 - Math.random() * 100000);
this.sectionPosition.setX(randomNumber);
assertEquals(randomNumber, this.sectionPosition.getX());
}
public void testXAgain()
{
this.testPositiveX();
}
public void testPositiveZ()
{
int randomNumber = (int) Math.ceil(Math.random() * 100000);
this.sectionPosition.setZ(randomNumber);
assertEquals(randomNumber, this.sectionPosition.getZ());
}
public void testNegativeZ()
{
int randomNumber = (int) Math.ceil(0 - Math.random() * 100000);
this.sectionPosition.setZ(randomNumber);
assertEquals(randomNumber, this.sectionPosition.getZ());
}
public void testZAgain()
{
this.testPositiveZ();
}
public void testEncodeDecode()
{
int detailLevel = (int) Math.ceil(Math.random() * 6);
int x = (int) Math.ceil(Math.random() * 100000);
int z = (int) Math.ceil(Math.random() * 100000);
this.sectionPosition.setDetailLevel(detailLevel);
this.sectionPosition.setX(x);
this.sectionPosition.setZ(z);
Encoder encoder = new Encoder();
this.sectionPosition.encode(encoder);
this.sectionPosition = new SectionPosition();
Decoder decoder = new Decoder(encoder.toByteArray());
this.sectionPosition.decode(decoder);
assertEquals("Detail level", detailLevel, this.sectionPosition.getDetailLevel());
assertEquals("X coordinate", x, this.sectionPosition.getX());
assertEquals("Z coordinate", z, this.sectionPosition.getZ());
}
public void testBinary()
{
this.sectionPosition = new SectionPosition();
this.sectionPosition.setDetailLevel(1);
Encoder detailLevelEncoder = new Encoder();
this.sectionPosition.encode(detailLevelEncoder);
byte[] detailLevelBytes = detailLevelEncoder.toByteArray();
this.sectionPosition.setDetailLevel(0);
this.sectionPosition.setX(1);
Encoder xEncoder = new Encoder();
this.sectionPosition.encode(xEncoder);
byte[] xBytes = xEncoder.toByteArray();
this.sectionPosition.setX(0);
this.sectionPosition.setX(-1);
Encoder negativeXEncoder = new Encoder();
this.sectionPosition.encode(negativeXEncoder);
byte[] negativeXBytes = negativeXEncoder.toByteArray();
this.sectionPosition.setX(0);
this.sectionPosition.setZ(1);
Encoder zEncoder = new Encoder();
this.sectionPosition.encode(zEncoder);
byte[] zBytes = zEncoder.toByteArray();
this.sectionPosition.setZ(0);
this.sectionPosition.setZ(-1);
Encoder negativeZEncoder = new Encoder();
this.sectionPosition.encode(negativeZEncoder);
byte[] negativeZBytes = negativeZEncoder.toByteArray();
this.sectionPosition.setZ(0);
// | Z| X| Detail|
assertEquals("Detail level", "0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001", Utils.bytesToBin(detailLevelBytes));
assertEquals("X coordinate", "0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0000 0000", Utils.bytesToBin(xBytes));
assertEquals("Negative X coordinate", "0000 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 1111 1111 1111 0000 0000", Utils.bytesToBin(negativeXBytes));
assertEquals("Z coordinate", "0000 0000 0000 0000 0000 0000 0001 0000 0000 0000 0000 0000 0000 0000 0000 0000", Utils.bytesToBin(zBytes));
assertEquals("Negative Z coordinate", "1111 1111 1111 1111 1111 1111 1111 0000 0000 0000 0000 0000 0000 0000 0000 0000", Utils.bytesToBin(negativeZBytes));
}
}