Compare commits

..
417 Commits
Author SHA1 Message Date
Jim C K FlatenandClaude Opus 5.5 a2e71c1e9d Normalize line endings.
The previous ".gitattributes" setting "text=lf" is not valid, so Git ignored it
and a few files were committed with CRLF line endings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 03:46:18 +01:00
Jim C K Flaten f6b1958d56 Reduce allocations in world interface. 2026-10-26 02:42:44 +01:00
Jim C K Flaten 1c6d533d8d Improve version mismatch message behavior. 2026-10-26 01:13:25 +01:00
Jim C K Flaten 04d8b99634 Don't croak when a player disconnects with messages in flight. 2026-10-26 01:13:03 +01:00
Jim C K Flaten db881dbdb4 Validate server and level keys. 2026-10-26 00:45:50 +01:00
Jim C K FlatenandClaude Opus 5.5 132b704e90 Log plugin message handler errors on the server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 23:56:18 +01:00
Jim C K Flaten 3ca19e814e Squelch biome warning on Paper 1.21.3+ 2026-10-25 23:47:13 +01:00
Jim C K FlatenandClaude Opus 5.5 40bd82176b Add testbed with a real server and client.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:19:03 +01:00
Jim C K Flaten 25c73598bb Fix world lookup by mutated key. 2026-10-25 21:24:10 +01:00
Matt Lavallee 04fc2e17f9 Throttle login-sync and regular LOD requests independently. 2026-10-05 23:01:40 +02:00
Matt Lavallee 61ed6de27d Support DH network protocol version 16 2026-10-05 22:57:59 +02:00
Jim C K Flaten 56a0111057 Don't trigger LOD updates for events that happen during LOD generation. 2026-08-16 16:31:04 +02:00
Jim C K Flaten 61c8f6a465 Disable chunk discard by default. 2026-08-16 15:59:43 +02:00
Keith ConstableandClaude Sonnet 5 443bc32613 Fix WorldConfiguration.has() and stuck pre-generator on missing config key
Commit 9feff5e broke the pregen command by adding a `has()`-based fallback to pregen, when `has()` wasn't overridden in WorldConfiguration.

---

WorldConfiguration overrode get()/set()/unset() to redirect into the
backing global config under a worlds.<name>. prefix, but never
overrode has(), clear(), increment(), or decrement() — those kept
reading/writing the view's own local map, which nothing ever
populates. has() always returned false for every key in every world.

PreGenerator.run() used that has() to decide between the current and
deprecated concurrency-limit config keys. With has() always false, it
took the deprecated-key branch, which has no default, so getInt()
returned null and unboxing to int NPE'd on the first line of run().
The exception was swallowed by BukkitScheduler's fire-and-forget
future, leaving isPreGenerating() reporting true forever — /dhs pregen
start looked like it was running but never made progress.

Give WorldConfiguration real has()/clear()/increment()/decrement()
overrides, default the concurrency limit and floor it at 1 so a
missing config key can't NPE or spin the rate-limit loop, and log
pre-generator failures instead of discarding them silently.

Validated against a Paper 26.2 test server: pregen now advances to
completion under both the current-key and deprecated-key-only config,
with zero exceptions logged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 15:43:55 +02:00
Jim C K Flaten 17feb207f5 Correct query result check in loadLod() 2026-08-11 15:35:57 +02:00
Matt Lavallee 369e822607 Housekeeping: Gradle 9.6.1, Shadow 9.6.1, and dependency updates
## Housekeeping: Gradle 9.6.1, Shadow 9.6.1, and dependency updates

Routine build maintenance. No source files changed — build configuration only.

| Dependency | Before | After |
|------------|--------|-------|
| Gradle wrapper | 8.14.3 | 9.6.1 |
| `com.gradleup.shadow` | 8.3.8 | 9.6.1 |
| `org.gradle.toolchains.foojay-resolver-convention` | — | 1.0.0 (new) |
| `org.jetbrains:annotations` | 25.0.0 | 26.1.0 |
| `com.google.guava:guava` | 33.3.1-jre | 33.6.0-jre |
| `org.bstats:bstats-bukkit` | 3.1.0 | 3.2.1 |
| `org.json:json` | 20240303 | 20260719 |
| `org.tukaani:xz` | 1.10 | 1.12 |
| `junit:junit` | 4.11 | 4.13.2 |
| FoliaLib | `com.github.technicallycoded:FoliaLib:v0.4.3` | `com.tcoded:FoliaLib:0.5.2` |

### Notes

- **Gradle 9:** replaced the removed `$buildDir` with `layout.buildDirectory`. Added `gradle-daemon-jvm.properties` (toolchain 21) and the foojay resolver so the Java 21 toolchain auto-provisions.
- **FoliaLib** moved to upstream's own repo `https://repo.tcoded.com/releases` under the `com.tcoded` group, because JitPack's builds of 0.5.x fail on an upstream Gradle implicit-dependency error and cap out at 0.4.4. Package name is unchanged, so the existing `relocate` rule and `BukkitScheduler` needed no changes.
- Added `.vscode/settings.json` to `.gitignore`.

### Verification

`./gradlew clean build` passes with tests and zero Gradle deprecation warnings under `--warning-mode all`. Shadow 9 needed no build-script migration; the shaded jar is equivalent to the Shadow 8 output aside from dropping stale empty `META-INF/versions/9/org/tukaani/...` directory entries.
2026-08-04 21:28:07 +02:00
Jim C K Flaten 0ad2d159b8 Clamp heightmap to min/max heights. 2026-08-04 20:15:48 +02:00
terbin 3cb9894e50 Fix chunk access in getMaterialRecordAt() when "dummy mode" is enabled. 2026-08-04 19:54:18 +02:00
Jim C K Flaten 25c14d0893 Ignore events for unloaded worlds. 2026-07-29 20:45:53 +02:00
Jim C K Flaten 9feff5efa5 Enforce LOD request concurrency limit. 2026-07-22 22:50:35 +02:00
Jim C K Flaten bda0d067ee Restore chunk cache priming for benchmark runs. 2026-07-21 23:02:14 +02:00
Jim C K Flaten cf0fbf4854 Shorten chunk lifecycle. 2026-07-21 21:01:08 +02:00
Jim C K Flaten 8dbb9e773c Use dummy chunks for world border rejections. 2026-07-17 20:59:02 +02:00
Jim C K Flaten 744af7c1d3 Support cross-world LOD requests. 2026-07-12 18:55:55 +02:00
Jim C K Flaten b485b00e82 Bump version number. 2026-07-09 17:57:54 +02:00
Jim C K Flaten e5421ad0f6 Merge tag '0.14.0' into develop
0.14.0
2026-07-09 17:57:33 +02:00
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
Jim C K Flaten 8a560dfbc9 Merge branch 'release/0.8.1' 2025-03-08 16:56:31 +01:00
Jim C K Flaten 2d7280440e Release 0.8.1. 2025-03-08 16:55:59 +01:00
Jim C K Flaten 1069b4f623 Clean this up a bit. 2025-03-08 16:52:26 +01:00
Jim C K Flaten bc15977d11 Scale as double, but return int. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 1058dab9b1 Show generation progress in the console. 2025-03-08 16:52:26 +01:00
Jim C K Flaten c72df317c2 These aren't enforced, but we can provide an explanation. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 6d60f906a4 Coordinate scale for border center. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 6fb8bfc033 Update every 5 seconds. 2025-03-08 16:52:26 +01:00
s809 82333d0e48 Replace integer division with floating point in debug message 2025-03-08 16:52:26 +01:00
s809 a5a23ba073 Use LIGHT generation step for LOD encoding 2025-03-08 16:52:26 +01:00
Jim C K Flaten 4e7ed414ae Include non-collidion top layer by default. Snow looks dumb without it. 2025-03-08 16:52:26 +01:00
Jim C K Flaten aed02ef99b Better logging. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 65a66b9a97 Bump version number. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 64709347f9 Release 0.8.0. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 153d072f39 Correct condition for async chunk load. 2025-03-08 16:52:26 +01:00
Jim C K Flaten cbff7f910b No need to warn about this. It's expected. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 307d8795d9 There's no time like now. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 41a244013b Report if no rows deleted. 2025-03-08 16:52:26 +01:00
Jim C K Flaten ded98f0237 Typo. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 975ac821e2 Enable real-time by default. 2025-03-08 16:52:26 +01:00
s809 fcbebccd87 Add other important events, works but not very consistent 2025-03-08 16:52:26 +01:00
s809 e26d5812a9 Real time updates now work. 2025-03-08 16:52:26 +01:00
Jim C K Flaten fa78e99f64 Wrong var. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 79aabd8b35 Distance. 2025-03-08 16:52:26 +01:00
Jim C K Flaten cdda626cfd Ugly implementation of real time updates maybe. 2025-03-08 16:52:26 +01:00
Jim C K Flaten a41040230c Shutdown executor on disable. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 7624473148 Real-time updates go here. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 10d73adf59 Track player configs. 2025-03-08 16:52:26 +01:00
Jim C K Flaten e626d7a6a0 Send sane close reason on unsupported protocol version. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 61303a7ba1 Cancel tasks on disable. 2025-03-08 16:52:26 +01:00
Jim C K Flaten e0da9a0548 Protocol v10. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 2bb441d147 "None" builder. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 5ac7913b1e Remove debug message. 2025-03-08 16:52:26 +01:00
Jim C K Flaten d86e281e84 Disable beacon rendering by default. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 495b891b30 Request chunk unloading without saving. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 01857854c8 Protocol v9 2025-03-08 16:52:26 +01:00
Jim C K Flaten 7a8087cb1d Clamp sea level to min/max Y. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 91d7c849f6 Bump version number. 2025-03-08 16:52:26 +01:00
Jim C K Flaten 2b91ddf437 Clean this up a bit. 2025-03-08 16:41:11 +01:00
Jim C K Flaten a900b2b628 Scale as double, but return int. 2025-03-08 16:16:21 +01:00
Jim C K Flaten ecf310a8dc Show generation progress in the console. 2025-03-08 16:07:40 +01:00
Jim C K Flaten d1ae74946e These aren't enforced, but we can provide an explanation. 2025-03-08 15:50:25 +01:00
Jim C K Flaten aca296dbc6 Coordinate scale for border center. 2025-03-08 15:39:02 +01:00
Jim C K Flaten d838317503 Update every 5 seconds. 2025-03-08 14:10:58 +01:00
s809 f4c20138aa Replace integer division with floating point in debug message 2025-03-08 17:54:27 +05:00
s809 23e0a23406 Use LIGHT generation step for LOD encoding 2025-03-08 15:31:45 +05:00
Jim C K Flaten ed371875da Include non-collidion top layer by default. Snow looks dumb without it. 2025-03-07 23:47:38 +01:00
Jim C K Flaten aee19d166f Better logging. 2025-03-05 21:21:56 +01:00
Jim C K Flaten b768f2cd74 Bump version number. 2025-03-03 02:04:01 +01:00
Jim C K Flaten 31b3980509 Merge tag '0.8.0' into develop
0.8.0
2025-03-03 02:03:39 +01:00
Jim C K Flaten f2421057c2 Merge branch 'release/0.8.0' 2025-03-03 02:03:35 +01:00
Jim C K Flaten 1a2228b230 Release 0.8.0. 2025-03-03 02:03:31 +01:00
Jim C K Flaten fe7c9b8160 Correct condition for async chunk load. 2025-03-03 02:00:49 +01:00
Jim C K Flaten dc8bcfd388 No need to warn about this. It's expected. 2025-03-03 01:56:58 +01:00
Jim C K Flaten c096f4b4c0 There's no time like now. 2025-03-03 01:56:27 +01:00
Jim C K Flaten 0c89f759d7 Report if no rows deleted. 2025-03-03 01:55:33 +01:00
Jim C K Flaten 99e8b8520f Typo. 2025-03-03 01:54:43 +01:00
Jim C K Flaten f74bb49260 Enable real-time by default. 2025-03-02 12:11:42 +01:00
s809 31ceb3db51 Add other important events, works but not very consistent 2025-03-02 00:12:23 +05:00
s809 aee28bc193 Real time updates now work. 2025-03-01 21:54:57 +05:00
Jim C K Flaten e8c24add0b Wrong var. 2025-03-01 17:18:01 +01:00
Jim C K Flaten 11c3cedd03 Distance. 2025-03-01 15:58:00 +01:00
Jim C K Flaten ca0c386917 Ugly implementation of real time updates maybe. 2025-03-01 00:30:37 +01:00
Jim C K Flaten 16b993cf91 Shutdown executor on disable. 2025-03-01 00:29:54 +01:00
Jim C K Flaten d7385e3268 Real-time updates go here. 2025-02-27 22:41:19 +01:00
Jim C K Flaten f40cf3ae9f Track player configs. 2025-02-27 20:42:39 +01:00
Jim C K Flaten 3d94ae788a Send sane close reason on unsupported protocol version. 2025-02-27 20:15:11 +01:00
Jim C K Flaten 7718be6fa7 Cancel tasks on disable. 2025-02-17 22:38:43 +01:00
Jim C K Flaten 5d164c4ce6 Protocol v10. 2025-02-07 23:55:27 +01:00
Jim C K Flaten d81cab701d "None" builder. 2025-01-26 21:24:46 +01:00
Jim C K Flaten 3bac9955e2 Remove debug message. 2025-01-25 16:39:44 +01:00
Jim C K Flaten 179d0dad17 Disable beacon rendering by default. 2025-01-25 16:27:08 +01:00
Jim C K Flaten d503e18731 Request chunk unloading without saving. 2025-01-25 16:14:12 +01:00
Jim C K Flaten 4bdcdd4736 Protocol v9 2025-01-19 19:23:33 +00:00
Jim C K Flaten d1bb17d126 Clamp sea level to min/max Y. 2025-01-18 16:55:21 +01:00
Jim C K Flaten 3677655ec8 Bump version number. 2025-01-12 04:39:42 +01:00
Jim C K Flaten a82fc18cb5 Merge tag '0.7.0' into develop
0.7.0
2025-01-12 04:39:07 +01:00
Jim C K Flaten 73da993dc3 Merge branch 'release/0.7.0' 2025-01-12 04:39:00 +01:00
Jim C K Flaten e3962a6965 Release 0.7.0. 2025-01-12 04:38:55 +01:00
Jim C K Flaten 709a7f20a1 Add missing return. 2024-12-28 01:42:56 +01:00
Jim C K Flaten fe4764042a Run chunk loads on main thread when async is not an option. 2024-12-28 01:34:45 +01:00
Jim C K Flaten c67e4b858c Remove generate_new_chunks config option. 2024-12-28 01:15:39 +01:00
Jim C K Flaten 0831e8dd0b Reject requests for down-sampled LODs. 2024-12-27 18:50:07 +01:00
Jim C K Flaten 4e949b9547 Build for 1.21.3 and 1.21.4. 2024-12-25 21:34:11 +01:00
Jim C K Flaten 2e069a7f14 Protocol v8. 2024-12-25 21:24:04 +01:00
Jim C K Flaten f79f06b90e Show targeted MC version in log on startup. 2024-12-25 18:43:29 +01:00
Jim C K Flaten 125fac4c39 Add logo 🤩 2024-10-24 19:48:55 +00:00
Jim C K Flaten e4988053d8 Bump version number. 2024-10-21 23:26:32 +02:00
Jim C K Flaten e6c1edd26e Merge tag '0.6.1' into develop
0.6.1
2024-10-21 23:26:07 +02:00
Jim C K Flaten be8da0fd48 Merge branch 'release/0.6.1' 2024-10-21 23:25:59 +02:00
Jim C K Flaten 3688ef80c1 Release 0.6.1. 2024-10-21 23:25:34 +02:00
Jim C K Flaten e3fe077f02 Do unsafe things after logger is set. 2024-10-21 23:23:01 +02:00
Jim C K Flaten 888fc62aa3 Bump version number. 2024-10-20 01:41:33 +02:00
Jim C K Flaten 96de5a5e08 Merge tag '0.6.0' into develop
0.6.0
2024-10-20 01:40:41 +02:00
Jim C K Flaten e73441d1a9 Merge branch 'release/0.6.0' 2024-10-20 01:40:32 +02:00
Jim C K Flaten 0d58b7abc7 Release 0.6.0. 2024-10-20 01:40:28 +02:00
Jim C K Flaten 4b9d21191e Bump config version. 2024-10-20 01:40:11 +02:00
Jim C K Flaten 96323adf03 Always use this when referencing members. 2024-10-20 01:09:43 +02:00
Jim C K Flaten c5546107ca Protocol v6. 2024-10-20 01:09:08 +02:00
Jim C K Flaten d84977998f Optional. 2024-10-14 04:27:28 +02:00
Jim C K Flaten b1b576f8e4 Make non-colliding top layer detection optional. 2024-10-14 04:20:04 +02:00
Jim C K Flaten 45612e36eb Revert to yStep=1 when sampling air that's under someting else. 2024-10-14 04:00:16 +02:00
Jim C K Flaten f9d327b6b9 Force yStep=1 for non-colliding top layer. 2024-10-14 01:27:29 +02:00
Jim C K Flaten 30b82dbec4 Don't copy output for parent module. 2024-10-14 00:52:04 +02:00
Jim C K Flaten 246b37ee82 SAST 2024-10-14 00:18:41 +02:00
Jim C K Flaten b9b0feb0f0 Update readme. 2024-10-12 05:53:21 +02:00
Jim C K Flaten 1530edf221 Remove more Github stuff. 2024-10-12 04:40:06 +02:00
Jim C K Flaten 6004d56140 Simplify pipeline. 2024-10-12 02:36:46 +00:00
Jim C K Flaten 6b454927cb Build pipeline. 2024-10-12 03:21:52 +02:00
Jim C K Flaten ce5ac6224a Remove Github config. 2024-10-12 01:43:16 +02:00
Jim C K Flaten e39bec20e0 Option to always scan to sea level. 2024-10-10 23:10:31 +02:00
Jim C K Flaten 0825358a32 Fixed non-colliding top layer detection. 2024-10-08 01:57:00 +02:00
Jim C K Flaten 0ec9733384 Fixed world border center coordinates parsing. 2024-10-07 23:35:42 +02:00
Jim C K Flaten 9b5d68a120 Bump version number. 2024-10-06 23:44:06 +02:00
Jim C K Flaten b47cde87dd Merge tag '0.5.0' into develop
0.5.0
2024-10-06 23:43:36 +02:00
Jim C K Flaten 667694e5e7 Merge branch 'release/0.5.0' 2024-10-06 23:43:33 +02:00
Jim C K Flaten c22665da86 Release 0.5.0. 2024-10-06 23:43:29 +02:00
Jim C K Flaten 7bcc35bf0a Don't walk backwards into the void. 2024-10-06 23:02:00 +02:00
Jim C K Flaten 68a17c4e99 Better handling of non-colliding top layers. 2024-10-06 21:25:02 +02:00
Jim C K Flaten 517bbe2ff2 Pre-load chunk async when possible. 2024-10-06 20:38:09 +02:00
Jim C K Flaten a81b36960d Better Y loop. 2024-10-06 18:11:57 +02:00
Jim C K Flaten 456fdcdf91 Add author. 2024-10-06 17:19:14 +02:00
Jim C K Flaten 24b8f65abf Updates to make Folia work again (#86) 2024-10-06 17:08:40 +02:00
Jim C K Flaten 72a2758709 Support for custom biomes (#85) 2024-10-06 13:44:26 +02:00
Jim C K Flaten ce2837ebe6 Remove unused imports. 2024-10-06 00:52:28 +02:00
Jim C K Flaten aba5dc659c Use chunk snapshots (#84)
Use chunk snapshots instead of accessing live world.

TODO: Fetch the snapshots using region scheduler before passing them to
a builder thread. Will give a _massive_ performance boost to Folia
servers.
2024-10-06 00:51:54 +02:00
Jim C K Flaten 2a8cc8f2d3 Use different threads for Folia. 2024-10-05 21:40:58 +02:00
Jim C K Flaten 107514dc20 This is just a Supplier. 2024-10-05 20:43:15 +02:00
Jim C K Flaten e142d5b85d Beacons (#83) 2024-10-05 20:40:51 +02:00
Jim C K Flaten a348f2fb9b Protocol v5. 2024-10-05 16:10:04 +02:00
Jim C K Flaten 8410ea41b2 Early load is no longer necessary. 2024-10-05 15:57:25 +02:00
Jim C K Flaten 9e4f81d65a Config version (#82)
Closes #78
2024-10-05 15:55:56 +02:00
Jim C K Flaten 932908e6d4 Async DB (#81)
#80
2024-10-05 04:10:54 +02:00
Jim C K Flaten 7d5ff19bf2 Async DB load. 2024-10-03 22:29:45 +02:00
dependabot[bot] b289b7e94f Bump systems.manifold:manifold-preprocessor from 2024.1.33 to 2024.1.34 (#77)
Bumps systems.manifold:manifold-preprocessor from 2024.1.33 to
2024.1.34.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=systems.manifold:manifold-preprocessor&package-manager=maven&previous-version=2024.1.33&new-version=2024.1.34)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 23:28:51 +02:00
dependabot[bot] a79b5f3322 Bump org.jetbrains:annotations from 24.1.0 to 25.0.0 (#76)
Bumps
[org.jetbrains:annotations](https://github.com/JetBrains/java-annotations)
from 24.1.0 to 25.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/JetBrains/java-annotations/releases">org.jetbrains:annotations's
releases</a>.</em></p>
<blockquote>
<h2>25.0.0</h2>
<ul>
<li>Added Kotlin Multiplatform artifact
(multiplatform-annotations).</li>
<li>Removed Java 5 artifact.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/JetBrains/java-annotations/blob/master/CHANGELOG.md">org.jetbrains:annotations's
changelog</a>.</em></p>
<blockquote>
<h2>Version 25.0.0</h2>
<ul>
<li>Added Kotlin Multiplatform artifact
(multiplatform-annotations).</li>
<li>Removed Java 5 artifact.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/JetBrains/java-annotations/commit/daa47891c4b233820ac528947a1bd4a191de1822"><code>daa4789</code></a>
Merge pull request <a
href="https://redirect.github.com/JetBrains/java-annotations/issues/112">#112</a>
from serjsysoev/signall</li>
<li><a
href="https://github.com/JetBrains/java-annotations/commit/b1d593d73430e540b1fed0a3017a8819a19f0613"><code>b1d593d</code></a>
Add signAll task that signs all artifacts</li>
<li><a
href="https://github.com/JetBrains/java-annotations/commit/e1ad90fe468c1d46fc15c0fd435b068a9a8a11a4"><code>e1ad90f</code></a>
Merge pull request <a
href="https://redirect.github.com/JetBrains/java-annotations/issues/111">#111</a>
from serjsysoev/multiplatform-publication</li>
<li><a
href="https://github.com/JetBrains/java-annotations/commit/229e51a058b3672eaccb979f4dd0704d37e3db4f"><code>229e51a</code></a>
Add javadoc and common sources publishing</li>
<li><a
href="https://github.com/JetBrains/java-annotations/commit/5d7e0230b12d4f5f59e95fbfb1fd961febfc6ba7"><code>5d7e023</code></a>
Set up multiplatform publishing maintaining jvm artifact name</li>
<li><a
href="https://github.com/JetBrains/java-annotations/commit/bc94c2ffbb0ba073b3f3ae5bc877844b23f3a081"><code>bc94c2f</code></a>
Move the multiplatform project to the root</li>
<li><a
href="https://github.com/JetBrains/java-annotations/commit/7bc931ab0d2cb10a5fa65eba869e049b269c1957"><code>7bc931a</code></a>
Make multiplatform expect/actual implicitly actualizing declarations
with jav...</li>
<li><a
href="https://github.com/JetBrains/java-annotations/commit/42bb869f44c3a3ea12666dc09d1ed1db7857d592"><code>42bb869</code></a>
Fix multiplatform artifact module-info.java</li>
<li><a
href="https://github.com/JetBrains/java-annotations/commit/243b2f7a3c197bed7d8a6510f742defd745c9f94"><code>243b2f7</code></a>
Remove Java artifact</li>
<li><a
href="https://github.com/JetBrains/java-annotations/commit/a1060cdc5b9a5665768b3f32a49343efef10681c"><code>a1060cd</code></a>
Move java annotations to jvm part of multiplatform artifact</li>
<li>Additional commits viewable in <a
href="https://github.com/JetBrains/java-annotations/compare/24.1.0...25.0.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.jetbrains:annotations&package-manager=maven&previous-version=24.1.0&new-version=25.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 23:28:34 +02:00
dependabot[bot] dab76ae2b7 Bump com.google.guava:guava from 33.3.0-jre to 33.3.1-jre (#75)
Bumps [com.google.guava:guava](https://github.com/google/guava) from
33.3.0-jre to 33.3.1-jre.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/google/guava/releases">com.google.guava:guava's
releases</a>.</em></p>
<blockquote>
<h2>33.3.1</h2>
<h3>Maven</h3>
<pre lang="xml"><code>&lt;dependency&gt;
  &lt;groupId&gt;com.google.guava&lt;/groupId&gt;
  &lt;artifactId&gt;guava&lt;/artifactId&gt;
  &lt;version&gt;33.3.1-jre&lt;/version&gt;
  &lt;!-- or, for Android: --&gt;
  &lt;version&gt;33.3.1-android&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<h3>Jar files</h3>
<ul>
<li><a
href="https://repo1.maven.org/maven2/com/google/guava/guava/33.3.1-jre/guava-33.3.1-jre.jar">33.3.1-jre.jar</a></li>
<li><a
href="https://repo1.maven.org/maven2/com/google/guava/guava/33.3.1-android/guava-33.3.1-android.jar">33.3.1-android.jar</a></li>
</ul>
<p>Guava requires <a
href="https://github.com/google/guava/wiki/UseGuavaInYourBuild#what-about-guavas-own-dependencies">one
runtime dependency</a>, which you can download here:</p>
<ul>
<li><a
href="https://repo1.maven.org/maven2/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar">failureaccess-1.0.1.jar</a></li>
</ul>
<h3>Javadoc</h3>
<ul>
<li><a
href="https://guava.dev/releases/33.3.1-jre/api/docs/">33.3.1-jre</a></li>
<li><a
href="https://guava.dev/releases/33.3.1-android/api/docs/">33.3.1-android</a></li>
</ul>
<h3>JDiff</h3>
<ul>
<li><a
href="https://guava.dev/releases/33.3.1-jre/api/diffs/">33.3.1-jre vs.
33.3.0-jre</a></li>
<li><a
href="https://guava.dev/releases/33.3.1-android/api/diffs/">33.3.1-android
vs. 33.3.0-android</a></li>
<li><a
href="https://guava.dev/releases/33.3.1-android/api/androiddiffs/">33.3.1-android
vs. 33.3.1-jre</a></li>
</ul>
<h3>Changelog</h3>
<ul>
<li>Added <code>j2objc-annotations</code> to the Gradle runtime
classpath to stop producing <a
href="https://redirect.github.com/google/guava/issues/7397">an Android
Gradle Plugin error</a>. (a3b51888c2)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/google/guava/commits">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.google.guava:guava&package-manager=maven&previous-version=33.3.0-jre&new-version=33.3.1-jre)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 23:28:10 +02:00
Jim C K Flaten c2b7a49492 Login sync (#73) 2024-09-24 23:00:44 +02:00
Jim C K Flaten c3c5362cfe These can be NOT NULL 2024-09-24 22:58:21 +02:00
Jim C K Flaten 061992d5b9 Pass the full model. 2024-09-24 17:53:17 +02:00
Jim C K Flaten dcf2213625 Pass logger to repo. 2024-09-24 17:00:31 +02:00
Jim C K Flaten 5f1d243022 Check and update the write queue as well. 2024-09-24 16:52:36 +02:00
Jim C K Flaten b00efd0798 Update LODs when the world changes (#72) 2024-09-24 16:25:58 +02:00
Jim C K Flaten 44e4eff851 Coordinate utility class, LOD repository class, and general cleanup (#71) 2024-09-23 18:16:29 +02:00
dependabot[bot] 5265438b1c Bump org.tukaani:xz from 1.9 to 1.10 (#70)
Bumps [org.tukaani:xz](https://github.com/tukaani-project/xz-java) from
1.9 to 1.10.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/tukaani-project/xz-java/blob/master/NEWS.md">org.tukaani:xz's
changelog</a>.</em></p>
<blockquote>
<h2>1.10 (2024-07-29)</h2>
<ul>
<li>
<p>Licensing change: From version 1.10 onwards, XZ for Java is under
the BSD Zero Clause License (0BSD). 1.9 and older are in the
public domain and obviously remain so; the change only affects
the new releases.</p>
<p>0BSD is an extremely permissive license which doesn't require
retaining or reproducing copyright or license notices when
distributing the code, thus in practice there is extremely
little difference to public domain.</p>
</li>
<li>
<p>Mark copyright and license information in the source package so
that it is compliant to the <a
href="https://reuse.software/spec-3.2/">REUSE Specification version
3.2</a>.</p>
</li>
<li>
<p>Improve LZMAInputStream.enableRelaxedEndCondition():</p>
<ul>
<li>
<p>Error detection is slightly better.</p>
</li>
<li>
<p>The input position will always be at the end of the stream
after successful decompression.</p>
</li>
</ul>
</li>
<li>
<p>Support .lzma files that have both a known uncompressed size and
the end marker. Such files are uncommon but valid. The same issue
was fixed in XZ Utils 5.2.6 in 2022.</p>
</li>
<li>
<p>Add ARM64 and RISC-V BCJ filters.</p>
</li>
<li>
<p>Speed optimizations:</p>
<ul>
<li>Delta filter</li>
<li>LZMA/LZMA2 decoder</li>
<li>LZMA/LZMA2 encoder (partially Java &gt;= 9 only)</li>
<li>CRC64 (Java &gt;= 9 only)</li>
</ul>
</li>
<li>
<p>Changes that affect API/ABI compatibility:</p>
<ul>
<li>
<p>Change XZOutputStream constructors to not call the method
<code>public void updateFilters(FilterOptions[]
filterOptions)</code>.</p>
</li>
<li>
<p>In SeekableXZInputStream, change the method
<code>public void seekToBlock(int blockNumber)</code> to not call
the method <code>public long getBlockPos(int blockNumber)</code>.</p>
</li>
<li>
<p>Make the filter options classes <code>final</code>:</p>
<ul>
<li>ARM64Options</li>
<li>ARMOptions</li>
<li>ARMThumbOptions</li>
<li>DeltaOptions</li>
</ul>
</li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/tukaani-project/xz-java/commit/0f5ee02ca950b5e7cdadf89f8c314388092ad64c"><code>0f5ee02</code></a>
Bump the version number to 1.10</li>
<li><a
href="https://github.com/tukaani-project/xz-java/commit/4cd5a8b4ee9240e5b29cbbe4d002703e7c59880b"><code>4cd5a8b</code></a>
Update NEWS.md for 1.10</li>
<li><a
href="https://github.com/tukaani-project/xz-java/commit/21edbdf8838d0b208f069540d9a2f595d9f8a016"><code>21edbdf</code></a>
README.md: Add a section about reproducible builds</li>
<li><a
href="https://github.com/tukaani-project/xz-java/commit/372dc487e817d95889229e0a03a8ac8d451e4b61"><code>372dc48</code></a>
README.md: Use a subheading for old build environments</li>
<li><a
href="https://github.com/tukaani-project/xz-java/commit/5ec1e8e3ad1cc20c1014adfec89245ef79fa24e7"><code>5ec1e8e</code></a>
build.xml: Add Build-Jdk-Spec to MANIFEST.MF</li>
<li><a
href="https://github.com/tukaani-project/xz-java/commit/2103742fb418d6e5f5231a02382a0e2b8bf664e8"><code>2103742</code></a>
build.xml: Make MANIFEST.MF friendlier to reproducible builds</li>
<li><a
href="https://github.com/tukaani-project/xz-java/commit/d18a12a4336f51801fa5d66e53651059affa3ab9"><code>d18a12a</code></a>
build.xml: Use &lt;macrodef&gt; to make JARs of the demo programs</li>
<li><a
href="https://github.com/tukaani-project/xz-java/commit/39b7371c45adeb858c5b34d46f20de95d2abdff0"><code>39b7371</code></a>
build.xml: Add SOURCE_DATE_EPOCH support</li>
<li><a
href="https://github.com/tukaani-project/xz-java/commit/5689e647209e5f3201f0b083ca93d584017e61ae"><code>5689e64</code></a>
build.xml: Change the &quot;pom&quot; target to use unversioned
filename</li>
<li><a
href="https://github.com/tukaani-project/xz-java/commit/7f4ccd64e588c8f026d0b4030bbb096bf82aa6f5"><code>7f4ccd6</code></a>
Simplify building with OpenJDK 8</li>
<li>Additional commits viewable in <a
href="https://github.com/tukaani-project/xz-java/compare/v1.9...v1.10">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.tukaani:xz&package-manager=maven&previous-version=1.9&new-version=1.10)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-23 14:22:24 +02:00
dependabot[bot] 9baf68fb9e Bump org.bstats:bstats-bukkit from 3.0.3 to 3.1.0 (#69)
Bumps
[org.bstats:bstats-bukkit](https://github.com/Bastian/bStats-Metrics)
from 3.0.3 to 3.1.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/Bastian/bStats-Metrics/releases">org.bstats:bstats-bukkit's
releases</a>.</em></p>
<blockquote>
<h2>Release 3.1.0</h2>
<p>Version 3.1.0</p>
<h2>What's Changed</h2>
<ul>
<li>Support for Folia by <a
href="https://github.com/ricsal1"><code>@​ricsal1</code></a> in <a
href="https://redirect.github.com/Bastian/bstats-metrics/pull/126">Bastian/bstats-metrics#126</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Bastian/bstats-metrics/compare/v3.0.3...v3.1.0">https://github.com/Bastian/bstats-metrics/compare/v3.0.3...v3.1.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/Bastian/bstats-metrics/commit/3e8319112fa82d2ae90e1bffa3afab208587fff8"><code>3e83191</code></a>
Release version 3.1.0</li>
<li><a
href="https://github.com/Bastian/bstats-metrics/commit/388d72aedbb7ab95241226a69780cb02170e13d0"><code>388d72a</code></a>
Add support for Folia (<a
href="https://redirect.github.com/Bastian/bStats-Metrics/issues/126">#126</a>)</li>
<li><a
href="https://github.com/Bastian/bstats-metrics/commit/6f97853f9033067f06fa9fc317abf55dc89e22d9"><code>6f97853</code></a>
Increment patch version and append -SNAPSHOT suffix</li>
<li>See full diff in <a
href="https://github.com/Bastian/bStats-Metrics/compare/v3.0.3...v3.1.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.bstats:bstats-bukkit&package-manager=maven&previous-version=3.0.3&new-version=3.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-23 14:20:26 +02:00
Jim C K Flaten 3c2abdcfc1 Clean up biome var. 2024-09-22 23:45:14 +02:00
Jim C K Flaten 595e439934 Do a cheaper region file check before asking for a chunk load. 2024-09-22 22:47:59 +02:00
Jim C K Flaten 3b5eb199b1 Batch database inserts. 2024-09-22 21:16:47 +02:00
Jim C K Flaten 81f725267a Quick mafs. 2024-09-22 19:38:15 +02:00
Jim C K Flaten 1845c94f47 Remove leftovers in POM. 2024-09-21 22:22:48 +02:00
Jim C K Flaten 6e307ca594 Bottom layer should not be 0 blocks high. 2024-09-21 17:45:03 +02:00
Jim C K Flaten a8a1bd12f5 Less is more. 2024-09-21 16:40:17 +02:00
Jim C K Flaten eee2993010 Don't copy defaults. 2024-09-21 16:40:00 +02:00
Jim C K Flaten e68e252c10 Target 1.21.1. 2024-09-21 14:43:10 +02:00
Jim C K Flaten 6d09fe674b Vertical resolution (#68)
Closes #33
2024-09-21 01:24:37 +02:00
Jim C K Flaten 0ea287332b Up default max view distance to 1024 chunks. 2024-09-21 01:24:23 +02:00
Jim C K Flaten c603e339b2 Load full config. 2024-09-21 01:18:05 +02:00
Jim C K Flaten b3c98babcb LZMA (#67)
Closes #66
2024-09-20 21:41:21 +02:00
Jim C K Flaten 248fa5be73 Remove unused method. 2024-09-20 21:05:44 +02:00
Jim C K Flaten e7c30b7732 Add missing file headers. 2024-09-20 21:05:31 +02:00
Jim C K Flaten 2ae0213bc8 LOD storage (#65)
This is a bit of a mess, but works. Ideally I'd want to abstract away
everything from the `java.sql` namespace to keep the implementation
specifics within the database classes, but the amount of work that would
take feels a bit unnecessary.

Closes #32
2024-09-20 14:53:33 +02:00
Jim C K Flaten 191531d28e Add back world interface cloning. 2024-09-20 01:57:50 +02:00
Jim C K Flaten 8e36adce9d Remove emojis. 2024-09-19 19:23:48 +02:00
Jim C K Flaten 79aac2a4b5 Fast builder for overworld, full builder for netherworld. 2024-09-19 19:22:19 +02:00
Jim C K Flaten d102330432 Bump version number. 2024-09-18 18:40:27 +02:00
Jim C K Flaten 48f0ded08f Merge tag '0.4.0' into develop
0.4.0
2024-09-18 18:40:04 +02:00
Jim C K Flaten 2eedc4a588 Merge branch 'release/0.4.0' 2024-09-18 18:39:55 +02:00
Jim C K Flaten 9accd512d0 Release 0.4.0. 2024-09-18 18:39:48 +02:00
Jim C K Flaten 96ef181f7b Update to protocol v4 (#61) 2024-09-18 18:37:51 +02:00
dependabot[bot] c6d69bd52a Bump systems.manifold:manifold-preprocessor from 2024.1.32 to 2024.1.33 (#60)
Bumps systems.manifold:manifold-preprocessor from 2024.1.32 to
2024.1.33.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=systems.manifold:manifold-preprocessor&package-manager=maven&previous-version=2024.1.32&new-version=2024.1.33)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 13:10:23 +02:00
dependabot[bot] 480f4ca0a3 Bump systems.manifold:manifold-preprocessor from 2024.1.30 to 2024.1.32 (#59)
Bumps systems.manifold:manifold-preprocessor from 2024.1.30 to
2024.1.32.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=systems.manifold:manifold-preprocessor&package-manager=maven&previous-version=2024.1.30&new-version=2024.1.32)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-09 05:58:16 +02:00
dependabot[bot] 6b45e447cc Bump org.bstats:bstats-bukkit from 3.0.2 to 3.0.3 (#57)
Bumps
[org.bstats:bstats-bukkit](https://github.com/Bastian/bStats-Metrics)
from 3.0.2 to 3.0.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/Bastian/bStats-Metrics/releases">org.bstats:bstats-bukkit's
releases</a>.</em></p>
<blockquote>
<h2>Release 3.0.3</h2>
<p>Version 3.0.3</p>
<h2>Changes</h2>
<ul>
<li>Use Plugin instead of JavaPlugin in Bukkit Metrics constructor (<a
href="https://redirect.github.com/Bastian/bstats-metrics/pull/121">Bastian/bstats-metrics#121</a>)</li>
<li>Mark bStats threads as daemon (<a
href="https://redirect.github.com/Bastian/bstats-metrics/issues/123">Bastian/bstats-metrics#123</a>)</li>
<li>Add option to <code>MetricsBase</code> constructor to skip
relocation check (only for server implementations - not available to
regular Metrics classes)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/Bastian/bstats-metrics/commit/041c82ec5d96921a109ec131589d55c26146f947"><code>041c82e</code></a>
Release version 3.0.3</li>
<li><a
href="https://github.com/Bastian/bstats-metrics/commit/a4eaa75746462d12af3a3de7a01372b90bc5c5c3"><code>a4eaa75</code></a>
Use Plugin instead of JavaPlugin in Bukkit Metrics constructor</li>
<li><a
href="https://github.com/Bastian/bstats-metrics/commit/184ceb943f989ad74eaac8783678abfb54e729ed"><code>184ceb9</code></a>
Add option in MetricsBase to disable relocate check</li>
<li><a
href="https://github.com/Bastian/bstats-metrics/commit/e8d3b1c2b74f3ebcaa77356c4384c495d0b9046e"><code>e8d3b1c</code></a>
Mark bStats threads as daemon</li>
<li><a
href="https://github.com/Bastian/bstats-metrics/commit/6660b2b308c768df622088d2aea029ca6e2cf2c1"><code>6660b2b</code></a>
Increment patch version and append -SNAPSHOT suffix</li>
<li>See full diff in <a
href="https://github.com/Bastian/bStats-Metrics/compare/v3.0.2...v3.0.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.bstats:bstats-bukkit&package-manager=maven&previous-version=3.0.2&new-version=3.0.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-27 02:01:41 +02:00
dependabot[bot] 1bad8ceee8 Bump org.apache.maven.plugins:maven-dependency-plugin from 2.8 to 3.8.0 (#56)
Bumps
[org.apache.maven.plugins:maven-dependency-plugin](https://github.com/apache/maven-dependency-plugin)
from 2.8 to 3.8.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/apache/maven-dependency-plugin/releases">org.apache.maven.plugins:maven-dependency-plugin's
releases</a>.</em></p>
<blockquote>
<h2>3.7.1</h2>
<!-- raw HTML omitted -->
<h2>🐛 Bug Fixes</h2>
<ul>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-943">[MDEP-943]</a> -
Fix appendOutput option (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/414">#414</a>)
<a
href="https://github.com/slawekjaranowski"><code>@​slawekjaranowski</code></a></li>
</ul>
<h2>📦 Dependency updates</h2>
<ul>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-944">[MDEP-944]</a> -
Bump org.apache.maven.shared:maven-common-artifact-filters from 3.3.2 to
3.4.0 (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/413">#413</a>)
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
</ul>
<h2>👻 Maintenance</h2>
<ul>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-945">[MDEP-945]</a> -
Fix documentation about get goal (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/415">#415</a>)
<a
href="https://github.com/slawekjaranowski"><code>@​slawekjaranowski</code></a></li>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-940">[MDEP-940]</a> -
Use Resolver API instead of m-a-t for resolving artifacts (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/410">#410</a>)
<a
href="https://github.com/slawekjaranowski"><code>@​slawekjaranowski</code></a></li>
</ul>
<h2>3.7.0</h2>
<!-- raw HTML omitted -->
<ul>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-941">[MDEP-941]</a> -
Deprecate dependency:sources in favor of dependency:resolv… (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/411">#411</a>)
<a href="https://github.com/michael-o"><code>@​michael-o</code></a></li>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-838">[MDEP-838]</a> -
&quot;Artifact has not been packaged yet&quot; error message is not …
(<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/412">#412</a>)
<a href="https://github.com/michael-o"><code>@​michael-o</code></a></li>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-939">[MDEP-939]</a> -
Lock down classifier in dependency:sources goal (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/409">#409</a>)
<a href="https://github.com/michael-o"><code>@​michael-o</code></a></li>
<li>Revert &quot;Fix sources goal for multimodule projects&quot; (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/408">#408</a>)
<a href="https://github.com/michael-o"><code>@​michael-o</code></a></li>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-923">[MDEP-923]</a> -
Move methods in place where are used (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/407">#407</a>)
<a
href="https://github.com/slawekjaranowski"><code>@​slawekjaranowski</code></a></li>
<li>MDEP-938] Correct invalid property name and add deprecated old one
(<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/404">#404</a>)
<a href="https://github.com/hazendaz"><code>@​hazendaz</code></a></li>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-835">[MDEP-835]</a> -
Add optional dependency for tree IT (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/403">#403</a>)
<a
href="https://github.com/slawekjaranowski"><code>@​slawekjaranowski</code></a></li>
<li>Add missing dependency (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/401">#401</a>)
<a href="https://github.com/michael-o"><code>@​michael-o</code></a></li>
</ul>
<h2>🚀 New features and improvements</h2>
<ul>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-799">[MDEP-799]</a> -
tree: add optional output type json (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/391">#391</a>)
<a href="https://github.com/LogFlames"><code>@​LogFlames</code></a></li>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-928">[MDEP-928]</a> -
Allow excluding classes from dependency:analyze (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/393">#393</a>)
<a
href="https://github.com/slawekjaranowski"><code>@​slawekjaranowski</code></a></li>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-924">[MDEP-924]</a> -
Get rid of maven-artifact-transfer from list-classes goal (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/382">#382</a>)
<a
href="https://github.com/slawekjaranowski"><code>@​slawekjaranowski</code></a></li>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-925">[MDEP-925]</a> -
Require Maven 3.6.3 (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/381">#381</a>)
<a
href="https://github.com/slawekjaranowski"><code>@​slawekjaranowski</code></a></li>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-922">[MDEP-922]</a> -
dependency:analyze-exclusions - should report issue only in current
project (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/378">#378</a>)
<a
href="https://github.com/slawekjaranowski"><code>@​slawekjaranowski</code></a></li>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-917">[MDEP-917]</a> -
dependency:analyze-exclusions - use Resolver API instead of
ProjectBuilder (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/374">#374</a>)
<a
href="https://github.com/slawekjaranowski"><code>@​slawekjaranowski</code></a></li>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-317">[MDEP-317]</a> -
add mojo to analyze invalid exclusions (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/362">#362</a>)
<a href="https://github.com/vbreivik"><code>@​vbreivik</code></a></li>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-894">[MDEP-894]</a> -
Use <code>@Component</code> only - fix tests (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/360">#360</a>)
<a href="https://github.com/michael-o"><code>@​michael-o</code></a></li>
</ul>
<h2>🐛 Bug Fixes</h2>
<ul>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-914">[MDEP-914]</a> -
Fix link in collect goal description (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/380">#380</a>)
<a
href="https://github.com/slawekjaranowski"><code>@​slawekjaranowski</code></a></li>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-895">[MDEP-895]</a> -
dependency:sources fails for multi-module project (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/349">#349</a>)
<a href="https://github.com/jmle"><code>@​jmle</code></a></li>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-771">[MDEP-771]</a> -
Remove broken 404 link (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/344">#344</a>)
<a href="https://github.com/elharo"><code>@​elharo</code></a></li>
</ul>
<h2>📦 Dependency updates</h2>
<ul>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-936">[MDEP-936]</a> -
Bump org.apache.maven.shared:maven-dependency-tree from 3.2.1 to 3.3.0
(<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/397">#397</a>)
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-920">[MDEP-920]</a> -
Bump org.assertj:assertj-core from 3.25.3 to 3.26.0 (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/395">#395</a>)
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-929">[MDEP-929]</a> -
Bump org.apache.maven.shared:maven-dependency-analyzer from 1.14.0 to
1.14.1 (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/394">#394</a>)
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
<li>Bump org.apache.maven.shared:maven-dependency-analyzer from 1.13.2
to 1.14.0 (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/392">#392</a>)
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
<li><a
href="https://issues.apache.org/jira/browse/MDEP-921">[MDEP-921]</a> -
Bump org.apache.commons:commons-text from 1.11.0 to 1.12.0 (<a
href="https://redirect.github.com/apache/maven-dependency-plugin/pull/376">#376</a>)
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/apache/maven-dependency-plugin/commit/75814c79197672264078eb6e181aa2b343150dea"><code>75814c7</code></a>
[maven-release-plugin] prepare release
maven-dependency-plugin-3.8.0</li>
<li><a
href="https://github.com/apache/maven-dependency-plugin/commit/50397c42b9f4ede4ed82c055caaf3ef0733f3aba"><code>50397c4</code></a>
[MDEP-903] Upgrade to Doxia 2.0.0 Milestone Stack</li>
<li><a
href="https://github.com/apache/maven-dependency-plugin/commit/1115ecb4fa11d08f15435b9d094708667e123d18"><code>1115ecb</code></a>
Bump org.codehaus.plexus:plexus-archiver from 4.9.2 to 4.10.0</li>
<li><a
href="https://github.com/apache/maven-dependency-plugin/commit/5288cece722923651fbcf5e13741d3f94e910dbf"><code>5288cec</code></a>
Bump org.apache.maven.plugins:maven-plugins from 42 to 43</li>
<li><a
href="https://github.com/apache/maven-dependency-plugin/commit/ea4d8e249dc7d7c134c128f25e2a62c6772be1fd"><code>ea4d8e2</code></a>
Bump jettyVersion from 9.4.54.v20240208 to 9.4.55.v20240627</li>
<li><a
href="https://github.com/apache/maven-dependency-plugin/commit/2d0b82a865441a683897f5429b9986ed8c75a835"><code>2d0b82a</code></a>
Bump org.codehaus.plexus:plexus-io from 3.4.2 to 3.5.0</li>
<li><a
href="https://github.com/apache/maven-dependency-plugin/commit/86b7772498216280c948f8887f20ce2d03269e8a"><code>86b7772</code></a>
(doc) Remove repeated word</li>
<li><a
href="https://github.com/apache/maven-dependency-plugin/commit/94e1caf57c84a9da81317b19b3611299d6db700d"><code>94e1caf</code></a>
Bump org.jsoup:jsoup from 1.17.2 to 1.18.1</li>
<li><a
href="https://github.com/apache/maven-dependency-plugin/commit/06b427327bfb28a26c9be78ff840948d26eba197"><code>06b4273</code></a>
Bump org.assertj:assertj-core from 3.26.0 to 3.26.3</li>
<li><a
href="https://github.com/apache/maven-dependency-plugin/commit/71cee33151c85d11b4d4ca2f63b5be72f1c482f3"><code>71cee33</code></a>
Remove outdated invoker conditions</li>
<li>Additional commits viewable in <a
href="https://github.com/apache/maven-dependency-plugin/compare/maven-dependency-plugin-2.8...maven-dependency-plugin-3.8.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.apache.maven.plugins:maven-dependency-plugin&package-manager=maven&previous-version=2.8&new-version=3.8.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-27 02:01:32 +02:00
Jim C K Flaten b249c1ce90 Bump version number. 2024-08-24 01:51:06 +02:00
Jim C K Flaten dd1fc52fbe Merge tag '0.3.1' into develop
0.3.1
2024-08-24 01:50:23 +02:00
Jim C K Flaten 5591c5b7ec Merge branch 'release/0.3.1' 2024-08-24 01:50:19 +02:00
Jim C K Flaten 1f2f393803 Release 0.3.1. 2024-08-24 01:50:15 +02:00
Jim C K Flaten c404cb3322 Disable these log messages again. 2024-08-24 01:45:41 +02:00
Jim C K Flaten cb7ba37356 Send null byte as well. 2024-08-24 01:36:18 +02:00
Jim C K Flaten ce9bb1060c Bump version. 2024-08-23 23:12:08 +02:00
Jim C K Flaten 9402e6dee9 Merge tag '0.3.0' into develop
0.3.0
2024-08-23 23:11:42 +02:00
Jim C K Flaten 3b04ae153e Merge branch 'release/0.3.0' 2024-08-23 23:11:35 +02:00
Jim C K Flaten f7f723b7c6 Release 0.3.0. 2024-08-23 23:11:30 +02:00
Jim C K Flaten 4e8cb7fdf2 World border (#55)
Introduces per-world configuration and world borders.
2024-08-23 23:02:53 +02:00
Jim C K Flaten 3e49275c63 Drop Forge packet ID on 1.20.4 and below. 2024-08-23 22:58:23 +02:00
Jim C K Flaten d349cd038e Property parsing appears to work client-side now. 2024-08-19 22:27:03 +02:00
Jim C K Flaten 5452cab99a Update config comments. 2024-08-19 21:13:28 +02:00
Jim C K Flaten f22fb514b2 Initialize early. 2024-08-19 20:43:41 +02:00
Jim C K Flaten 3e5fa994dc Log prefix. 2024-08-19 20:43:25 +02:00
Jim C K Flaten e5989134cc Only include patch level from 1.20.5 and up. 2024-08-19 20:39:14 +02:00
Jim C K Flaten 8c830e30ae Only use Paper scheduler for Folia. 2024-08-19 20:22:41 +02:00
dependabot[bot] aca9769658 Bump com.github.technicallycoded:FoliaLib from main-SNAPSHOT to v0.4.3 (#52)
Bumps
[com.github.technicallycoded:FoliaLib](https://github.com/TechnicallyCoded/FoliaLib)
from main-SNAPSHOT to v0.4.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/TechnicallyCoded/FoliaLib/releases">com.github.technicallycoded:FoliaLib's
releases</a>.</em></p>
<blockquote>
<h2>BETA 0.4.3 - Chain Futures, New Methods, and Bug Patches</h2>
<h2>What's Changed</h2>
<ul>
<li>Chain by <a
href="https://github.com/CJCrafter"><code>@​CJCrafter</code></a> in <a
href="https://redirect.github.com/TechnicallyCoded/FoliaLib/pull/16">TechnicallyCoded/FoliaLib#16</a></li>
<li>Thread check by <a
href="https://github.com/CJCrafter"><code>@​CJCrafter</code></a> in <a
href="https://redirect.github.com/TechnicallyCoded/FoliaLib/pull/18">TechnicallyCoded/FoliaLib#18</a></li>
<li>Check projectile death state, fixes <a
href="https://redirect.github.com/TechnicallyCoded/FoliaLib/issues/14">#14</a>
by <a href="https://github.com/PikaMug"><code>@​PikaMug</code></a> in <a
href="https://redirect.github.com/TechnicallyCoded/FoliaLib/pull/24">TechnicallyCoded/FoliaLib#24</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/CJCrafter"><code>@​CJCrafter</code></a>
made their first contribution in <a
href="https://redirect.github.com/TechnicallyCoded/FoliaLib/pull/16">TechnicallyCoded/FoliaLib#16</a></li>
<li><a href="https://github.com/PikaMug"><code>@​PikaMug</code></a> made
their first contribution in <a
href="https://redirect.github.com/TechnicallyCoded/FoliaLib/pull/24">TechnicallyCoded/FoliaLib#24</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/TechnicallyCoded/FoliaLib/compare/v0.3.4...v0.4.3">https://github.com/TechnicallyCoded/FoliaLib/compare/v0.3.4...v0.4.3</a></p>
<h2>BETA 0.3.4 - Add runAtEntityLater and runAtEntityLater &amp;
Fixes</h2>
<p>Changes</p>
<ul>
<li>Add runAtEntityLater and runAtEntityLater. Resolves <a
href="https://redirect.github.com/TechnicallyCoded/FoliaLib/issues/12">#12</a></li>
<li>Fix a few NPEs</li>
</ul>
<h2>BETA 0.3.1 - Relocation Checks, Task Consumers &amp; Tick Duration
Corrections</h2>
<p>Changes:</p>
<ul>
<li>Added relocation (shade plugin) check at startup</li>
<li>Added task consumers</li>
<li>Added better platform detection</li>
<li>Automatically rewrite tick values to be &gt; 0 (Will show a warning
in console unless you call
FoliaLib#disableInvalidTickValueWarning())</li>
<li>Update README with new examples and information</li>
<li>Minor bug fixes</li>
</ul>
<h2>BETA 0.2.5 - Java 8 support, Support ticks as a time unit &amp;
Fixes</h2>
<p>Changes:</p>
<ul>
<li>Migrate the project to being a multi-module project</li>
<li>Move non-Folia classes to be compiled in Java 8</li>
<li>Add support for legacy Minecraft versions (don't load Java 16
classes at runtime if not running on Folia)</li>
<li>Add ability to check is a task is cancelled</li>
<li>Add isCancelled() compatibility with legacy Bukkit versions using
isCurrentlyRunning() and isQueued()</li>
<li>Minor fixes &amp; documentation updates</li>
</ul>
<h2>BETA 0.2.0 - Cancellable tasks, entity tasks &amp; fixes</h2>
<p>Changes:</p>
<ul>
<li>Added cancellable tasks,</li>
<li>Added entity tasks,</li>
<li>Removed scopes and use separate method names instead,</li>
<li>Changed server version checks to use class names instead of version
name</li>
</ul>
<h2>BETA 0.1.1 - Release</h2>
<p>Initial release + minor updates and fixes</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/TechnicallyCoded/FoliaLib/commits/v0.4.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.github.technicallycoded:FoliaLib&package-manager=maven&previous-version=main-SNAPSHOT&new-version=v0.4.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 14:33:50 +02:00
dependabot[bot] 6aa41955fe Bump com.google.guava:guava from 33.2.1-jre to 33.3.0-jre (#53)
Bumps [com.google.guava:guava](https://github.com/google/guava) from
33.2.1-jre to 33.3.0-jre.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/google/guava/releases">com.google.guava:guava's
releases</a>.</em></p>
<blockquote>
<h2>33.3.0</h2>
<h3>Maven</h3>
<pre lang="xml"><code>&lt;dependency&gt;
  &lt;groupId&gt;com.google.guava&lt;/groupId&gt;
  &lt;artifactId&gt;guava&lt;/artifactId&gt;
  &lt;version&gt;33.3.0-jre&lt;/version&gt;
  &lt;!-- or, for Android: --&gt;
  &lt;version&gt;33.3.0-android&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<h3>Jar files</h3>
<ul>
<li><a
href="https://repo1.maven.org/maven2/com/google/guava/guava/33.3.0-jre/guava-33.3.0-jre.jar">33.3.0-jre.jar</a></li>
<li><a
href="https://repo1.maven.org/maven2/com/google/guava/guava/33.3.0-android/guava-33.3.0-android.jar">33.3.0-android.jar</a></li>
</ul>
<p>Guava requires <a
href="https://github.com/google/guava/wiki/UseGuavaInYourBuild#what-about-guavas-own-dependencies">one
runtime dependency</a>, which you can download here:</p>
<ul>
<li><a
href="https://repo1.maven.org/maven2/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar">failureaccess-1.0.1.jar</a></li>
</ul>
<h3>Javadoc</h3>
<ul>
<li><a
href="https://guava.dev/releases/33.3.0-jre/api/docs/">33.3.0-jre</a></li>
<li><a
href="https://guava.dev/releases/33.3.0-android/api/docs/">33.3.0-android</a></li>
</ul>
<h3>JDiff</h3>
<ul>
<li><a
href="https://guava.dev/releases/33.3.0-jre/api/diffs/">33.3.0-jre vs.
33.2.1-jre</a></li>
<li><a
href="https://guava.dev/releases/33.3.0-android/api/diffs/">33.3.0-android
vs. 33.2.1-android</a></li>
<li><a
href="https://guava.dev/releases/33.3.0-android/api/androiddiffs/">33.3.0-android
vs. 33.3.0-jre</a></li>
</ul>
<h3>Changelog</h3>
<ul>
<li><code>base</code>: Removed <code>@Beta</code> from the
<code>Duration</code> overload of
<code>Suppliers.memoizeWithExpiration</code>.
(76fca99db95ce9c8e55bb9c37fd0e44ef0451a80)</li>
<li><code>cache</code>: Added <code>CacheBuilder</code>
<code>Duration</code> overloads to <code>guava-android</code>.
(a5f9bcafd6)</li>
<li><code>collect</code>: Removed <code>@Beta</code> from the
<code>guava-android</code> <code>Collector</code> APIs.
(c86c09dc3d)</li>
<li><code>collect</code>: Added
<code>ImmutableMultimap.builderWithExpectedKeys</code> and
<code>ImmutableMultimap.Builder.expectedValuesPerKey</code>.
(c3d5b17dc2)</li>
<li><code>graph</code>: Improved <code>Graphs.hasCycle</code> to avoid
causing <code>StackOverflowError</code> for long paths.
(63734b9dfc)</li>
<li><code>net</code>: Added <code>text/markdown</code> to
<code>MediaType</code>. (2466a099ae)</li>
<li><code>net</code>: Deprecated <code>HttpHeaders</code> constant for
<code>Sec-Ch-UA-Form-Factor</code> in favor of
<code>Sec-Ch-UA-Form-Factors</code> to follow the latest spec.
(b310b7e1ee)</li>
<li><code>testing</code>: Changed some test libraries to throw
<code>AssertionError</code> (instead of the more specific
<code>AssertionFailedError</code>) in some cases. (fdfbed1985)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/google/guava/commits">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.google.guava:guava&package-manager=maven&previous-version=33.2.1-jre&new-version=33.3.0-jre)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 14:33:20 +02:00
Jim C K Flaten 50730d0f11 Option to disable chunk generation when building LODs (#51)
Closes #49
2024-08-18 02:03:49 +02:00
Jim C K Flaten 2f4a070be8 Regional scheduling takes block coordinates, not chunk coordinates. 2024-08-17 22:57:23 +02:00
Jim C K Flaten 0c76ae7748 Fix Folialib shading. 2024-08-17 22:34:23 +02:00
Jim C K Flaten b8b6bd1b84 Build for multiple versions of Minecraft (#48)
![image](https://github.com/user-attachments/assets/36369286-76a5-45cc-88a6-92187c24b38a)

Closes #47
2024-08-14 16:30:49 +02:00
Jim C K Flaten 49beaea384 Update README.md 2024-08-12 03:06:43 +02:00
Jim C K Flaten 1b71004a64 Create FUNDING.yml 2024-08-12 02:55:06 +02:00
Jim C K Flaten 3e530589f4 Add Folia to readme. 2024-08-12 01:01:05 +02:00
Jim C K Flaten 43695a4c22 Folia (#46)
Closes #44
2024-08-12 00:44:39 +02:00
Jim C K Flaten b57d40742f Add level_key_prefix config option. (#45) 2024-08-07 12:13:09 +02:00
Jim C K Flaten 6e78561671 Show DH players in console/log. 2024-08-06 00:54:22 +02:00
Jim C K Flaten d48fe0934f Merge tag '0.2.2' into develop
0.2.2
2024-07-19 00:17:47 +02:00
Jim C K Flaten 7a2992d47c Bump version number. 2024-07-18 23:40:41 +02:00
Jim C K Flaten de03cad4f0 Merge tag '0.2.1' into develop
0.2.1
2024-07-18 23:40:03 +02:00
95 changed files with 9041 additions and 1106 deletions

No files matched your search

+2 -1
View File
@@ -1 +1,2 @@
* text=lf
* text=auto eol=lf
*.bat text eol=crlf
-13
View File
@@ -1,13 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "maven" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
ignore:
- dependency-name: "io.netty:*"
-35
View File
@@ -1,35 +0,0 @@
# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Java CI with Maven
on:
push:
branches: [ "develop" ]
pull_request:
branches: [ "develop" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn -B package --file pom.xml
# Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive
- name: Update dependency graph
uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6
+12 -3
View File
@@ -25,10 +25,19 @@ replay_pid*
# IntelliJ IDEA project config
/.idea
/*.iml
# Maven output
/dependency-reduced-pom.xml
/target
# Visual Studio Code project config
/.vscode
# Gradle output
/.gradle
/build
# Test server
/server
/folia
# Testbed downloads and runtime files
/testbed/.cache
/testbed/run
+29
View File
@@ -0,0 +1,29 @@
include:
- template: Security/SAST.gitlab-ci.yml
stages:
- build
- test
default:
image: eclipse-temurin:21
cache:
key: gradle-${CI_COMMIT_REF_SLUG}
unprotect: true
paths:
- .gradle
build:
stage: build
script:
- ./gradlew build
- mv build/libs/*/*.jar ./
artifacts:
name: DistantHorizonsSupport-${CI_COMMIT_REF_SLUG}-${CI_JOB_STARTED_AT}
paths:
- "./*.jar"
-1
View File
@@ -1 +0,0 @@
dhs.jckf.no
+11 -9
View File
@@ -1,19 +1,21 @@
# Server-side support for [Distant Horizons](https://gitlab.com/jeseibel/distant-horizons)
<img src="https://gitlab.com/distant-horizons-team/distant-horizons-core/-/raw/main/_Misc%20Files/logo%20files/new/SVG/Distant-Horizons-Plugin.svg?ref_type=heads" height="128" alt="Plugin logo">
DH Support is a Bukkit/Spigot/Paper server plugin that transmits Level Of Detail (LOD) data to connected clients in order to provide a much higher view distance than Minecraft normally would allow. 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.
# Distant Horizons Server Plugin
## Installation
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.
Download the [latest release](https://github.com/Jckf/DH-Support/releases) and drop the JAR in your plugins folder, and you're done!
## Installing and configuring
## Configuration
Please see [our wiki](https://gitlab.com/distant-horizons-team/distant-horizons-server-plugin/-/wikis/home) for details.
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.
## Building
## Compilation
This project uses Gradle.
The project uses Maven, so just run `mvn` in the project directory to compile and package a new JAR.
`gradlew clean build`
## Contribution
There are several ways to contribute to this project. You can offer your feedback on [the DH Discord](https://discord.gg/xAB8G4cENx) in [this thread](https://discord.com/channels/881614130614767666/1154490009735417989), report any issues or bugs you find, or attack an open issue and submit a pull request.
There are several ways to contribute to this project. You can offer your feedback on [the DH Discord](https://discord.gg/xAB8G4cENx), report any issues or bugs you find, or attack an open issue and submit a pull request ❤️
![Plugin usage statistics](https://bstats.org/signatures/bukkit/DH%20Support.svg)
+107
View File
@@ -0,0 +1,107 @@
plugins {
id 'java'
id 'com.gradleup.shadow' version '9.6.1'
}
group = 'no.jckf.dhs'
version = '0.15.0-SNAPSHOT'
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://repo.tcoded.com/releases' }
}
configurations {
shade
implementation {
extendsFrom shade
}
}
dependencies {
compileOnly 'org.jetbrains:annotations:26.1.0'
implementation 'com.google.guava:guava:33.6.0-jre'
shade 'org.bstats:bstats-bukkit:3.2.1'
shade 'org.json:json:20260719'
shade 'org.tukaani:xz:1.12'
shade 'com.tcoded:FoliaLib:0.5.2'
testImplementation 'junit:junit:4.13.2'
}
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(layout.buildDirectory.dir("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(layout.buildDirectory.dir("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
+2
View File
@@ -0,0 +1,2 @@
#This file is generated by updateDaemonJvm
toolchainVersion=21
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+248
View File
@@ -0,0 +1,248 @@
#!/bin/sh
#
# Copyright © 2015 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
#
##############################################################################
#
# gradlew 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 gradlew
#
# 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/3d91ce3b8caaf77ad09f381f43615b715b53f72c/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
# 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" )
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" \
-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
+82
View File
@@ -0,0 +1,82 @@
@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 gradlew startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
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
"%COMSPEC%" /c exit 1
: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
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
-153
View File
@@ -1,153 +0,0 @@
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>no.jckf</groupId>
<name>dh-support</name>
<version>0.2.2</version>
<artifactId>DHSupport</artifactId>
<properties>
<jdk>17</jdk>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.class>no.jckf.dhsupport.bukkit.DhSupportBukkitPlugin</main.class>
</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>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.21-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>3.0.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.2.1-jre</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>24.1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.lz4</groupId>
<artifactId>lz4-java</artifactId>
<version>1.8.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<defaultGoal>clean install</defaultGoal>
<finalName>${project.artifactId}</finalName>
<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>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>all</shadedClassifierName>
<relocations>
<relocation>
<pattern>net.jpountz</pattern>
<shadedPattern>no.jckf.dhsupport.lz4</shadedPattern>
</relocation>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>no.jckf.dhsupport.bstats</shadedPattern>
</relocation>
</relocations>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<artifactSet>
<includes>
<include>org.lz4</include>
<include>org.bstats</include>
</includes>
</artifactSet>
</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>
</project>
+5
View File
@@ -0,0 +1,5 @@
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
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;
}
@@ -0,0 +1,265 @@
/*
* 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;
import com.tcoded.folialib.FoliaLib;
import no.jckf.dhsupport.core.Utils;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import no.jckf.dhsupport.core.scheduling.Scheduler;
import org.bukkit.Location;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.function.Supplier;
public class BukkitScheduler implements Scheduler
{
protected DhSupportBukkitPlugin plugin;
protected FoliaLib foliaLib;
protected ExecutorService executor;
protected ScheduledExecutorService scheduledExecutor;
public BukkitScheduler(DhSupportBukkitPlugin plugin)
{
this.plugin = plugin;
this.foliaLib = new FoliaLib(this.plugin);
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.");
}
@Override
public boolean canReadWorldAsync()
{
return false;
}
@Override
public <U> CompletableFuture<U> runOnMainThread(Supplier<U> supplier)
{
CompletableFuture<U> future = new CompletableFuture<>();
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;
}
@Override
public <U> CompletableFuture<U> runOnRegionThread(UUID worldId, int x, int z, Supplier<U> supplier)
{
CompletableFuture<U> future = new CompletableFuture<>();
Location location = new Location(
this.plugin.getServer().getWorld(worldId),
x,
0,
z
);
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;
}
@Override
public <U> CompletableFuture<U> runOnSeparateThread(Supplier<U> supplier)
{
CompletableFuture<U> future = new CompletableFuture<>();
this.executor.execute(() -> {
try {
future.complete(supplier.get());
} catch (Exception exception) {
future.completeExceptionally(exception);
}
});
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.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);
}
}
@@ -18,38 +18,494 @@
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.scheduling.Scheduler;
import no.jckf.dhsupport.core.world.AbstractWorldInterface;
import no.jckf.dhsupport.core.world.MaterialRecord;
import no.jckf.dhsupport.core.world.WorldInterface;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.*;
import org.bukkit.block.data.BlockData;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
public class BukkitWorldInterface implements WorldInterface
public class BukkitWorldInterface extends AbstractWorldInterface
{
protected static boolean BIOME_KEY_WARNING_SENT = false;
protected static boolean ASYNC_LOAD_WARNING_SENT = false;
protected DhSupportBukkitPlugin plugin;
protected World world;
protected Block block;
protected Logger logger;
public BukkitWorldInterface(World world)
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);
// Building these is expensive, and a world only has so many block states. Every LOD gets its own instance through
// newInstance(), so these are shared between all instances for the same world.
protected final Map<BlockData, String> blockStateStrings;
protected final Map<BlockData, Map<String, String>> blockProperties;
protected UnsafeValues unsafeValues;
@Nullable
protected Method getCoordinateScale;
@Nullable
protected Method getBiomeKey;
@Nullable
protected Method getChunkAtAsync;
protected int lightOffset = 0;
protected Integer seaLevel;
protected Integer dummyMaxY;
public BukkitWorldInterface(DhSupportBukkitPlugin plugin, World world, Configuration config)
{
this(plugin, world, config, new ConcurrentHashMap<>(), new ConcurrentHashMap<>());
}
protected BukkitWorldInterface(DhSupportBukkitPlugin plugin, World world, Configuration config, Map<BlockData, String> blockStateStrings, Map<BlockData, Map<String, String>> blockProperties)
{
super(world.getName(), config);
this.plugin = plugin;
this.world = world;
this.blockStateStrings = blockStateStrings;
this.blockProperties = blockProperties;
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)
{
this.logger = logger;
}
public Logger getLogger()
{
return this.logger;
}
public void doUnsafeThings()
{
this.unsafeValues = Bukkit.getUnsafe();
// Does not exist for 1.16.5.
try {
this.getCoordinateScale = this.world.getClass().getMethod("getCoordinateScale");
} catch (NoSuchMethodException e) {
}
// Since 1.21.3, biomes come from the registry and report their real keys, custom ones included.
// Before that, Biome was an enum and every custom biome was reported as "minecraft:custom".
if (org.bukkit.block.Biome.class.isEnum()) {
// Detect if we're running under Paper (or a Paper fork) that has this patch:
// https://github.com/PaperMC/Paper/commit/5bf259115c1ce29dd96df5fcf53739c94d39f902
try {
Class<?> regionAccessor = Class.forName("org.bukkit.RegionAccessor");
this.getBiomeKey = this.unsafeValues.getClass().getMethod("getBiomeKey", regionAccessor, int.class, int.class, int.class);
} catch (ClassNotFoundException | NoSuchMethodException e) {
if (!BIOME_KEY_WARNING_SENT) {
this.getLogger().warning("Custom biomes are not supported on this server.");
BIOME_KEY_WARNING_SENT = true;
}
}
}
try {
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.");
ASYNC_LOAD_WARNING_SENT = true;
}
}
}
@Override
public WorldInterface newInstance()
{
return new BukkitWorldInterface(this.world);
BukkitWorldInterface newInstance = new BukkitWorldInterface(this.plugin, this.world, this.config, this.blockStateStrings, this.blockProperties);
newInstance.setLogger(this.getLogger());
newInstance.doUnsafeThings();
return newInstance;
}
// Shitty cache :)
protected Block getBlock(int x, int y, int z)
protected ChunkSnapshot getChunk(int x, int z)
{
if (this.block == null || this.block.getY() != y || this.block.getX() != x || this.block.getZ() != z) {
this.block = this.world.getBlockAt(x, y, z);
return this.getChunk(x, z, null);
}
protected ChunkSnapshot getChunk(int x, int z, @Nullable ChunkSnapshot alreadyLoaded)
{
int chunkX = Coordinates.blockToChunk(x);
int chunkZ = Coordinates.blockToChunk(z);
int sectionX = Coordinates.chunkToSection(chunkX);
int sectionZ = Coordinates.chunkToSection(chunkZ);
if (this.sectionX == null || this.sectionZ == 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");
}
return this.block;
int baseChunkX = Coordinates.sectionToChunk(sectionX);
int baseChunkZ = Coordinates.sectionToChunk(sectionZ);
int index = (chunkX - baseChunkX) + ((chunkZ - baseChunkZ) << 2);
ChunkSnapshot chunk = this.chunks[index];
if (chunk != null) {
return chunk;
}
Scheduler scheduler = this.plugin.getDhSupport().getScheduler();
if (alreadyLoaded != null) {
chunk = alreadyLoaded;
} else 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()
{
return this.world.getUID();
}
@Override
public double getCoordinateScale()
{
if (this.getCoordinateScale == null) {
boolean isNether = this.world.getEnvironment().equals(World.Environment.NETHER);
return isNether ? 8 : 1;
}
try {
return (double) this.getCoordinateScale.invoke(this.world);
} catch (InvocationTargetException | IllegalAccessException exception) {
throw new RuntimeException(exception);
}
}
protected boolean useVanillaWorldBorder()
{
return this.worldConfig.getBool(DhsConfig.USE_VANILLA_WORLD_BORDER, true);
}
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);
boolean alreadyLoaded = this.world.isChunkLoaded(chunkX, chunkZ);
if (alreadyLoaded) {
return true;
}
int regionX = Coordinates.chunkToRegion(chunkX);
int regionZ = Coordinates.chunkToRegion(chunkZ);
File regionFile = new File(world.getWorldFolder() + "/region/r." + regionX + "." + regionZ + ".mca");
if (!regionFile.exists()) {
return false;
}
boolean exists = this.world.loadChunk(chunkX, chunkZ, false);
if (exists) {
this.world.unloadChunk(chunkX, chunkZ, false);
}
return exists;
}
@Override
public boolean isChunkLoaded(int x, int z)
{
if (this.isDummyMode()) {
return true;
}
int chunkX = Coordinates.blockToChunk(x);
int chunkZ = Coordinates.blockToChunk(z);
return this.world.isChunkLoaded(chunkX, chunkZ);
}
@Override
public boolean loadChunk(int x, int z)
{
if (this.isDummyMode()) {
return true;
}
int chunkX = Coordinates.blockToChunk(x);
int chunkZ = Coordinates.blockToChunk(z);
if (!this.world.loadChunk(chunkX, chunkZ, false)) {
return false;
}
this.getChunk(x, z);
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);
if (!this.world.loadChunk(chunkX, chunkZ, true)) {
return false;
}
this.getChunk(x, z);
return 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));
}
int chunkX = Coordinates.blockToChunk(x);
int chunkZ = Coordinates.blockToChunk(z);
try {
return ((CompletableFuture<Chunk>) this.getChunkAtAsync.invoke(this.world, chunkX, chunkZ, false))
.thenApply((chunk) -> {
if (chunk == null) {
return false;
}
this.getChunk(x, z, chunk.getChunkSnapshot(true, true, false));
return true;
});
} 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 {
return ((CompletableFuture<Chunk>) this.getChunkAtAsync.invoke(this.world, chunkX, chunkZ, true))
.thenApply((chunk) -> {
if (chunk == null) {
return false;
}
this.getChunk(x, z, chunk.getChunkSnapshot(true, true, false));
return true;
});
} catch (InvocationTargetException | IllegalAccessException exception) {
throw new RuntimeException(exception);
}
}
@Override
public boolean unloadChunk(int x, int z)
{
if (this.isDummyMode()) {
return true;
}
int chunkX = Coordinates.blockToChunk(x);
int chunkZ = Coordinates.blockToChunk(z);
return this.world.unloadChunk(chunkX, chunkZ);
}
@Override
public boolean unloadChunkAsync(int x, int z)
{
if (this.isDummyMode()) {
return true;
}
int chunkX = Coordinates.blockToChunk(x);
int chunkZ = Coordinates.blockToChunk(z);
return this.world.unloadChunkRequest(chunkX, chunkZ);
}
@Override
public boolean discardChunk(int x, int z)
{
if (this.isDummyMode()) {
return true;
}
int chunkX = Coordinates.blockToChunk(x);
int chunkZ = Coordinates.blockToChunk(z);
return this.world.unloadChunk(chunkX, chunkZ, false);
}
@Override
@@ -67,72 +523,349 @@ public class BukkitWorldInterface implements WorldInterface
@Override
public int getSeaLevel()
{
return this.world.getSeaLevel();
if (this.seaLevel == null) {
this.seaLevel = Math.clamp(this.world.getSeaLevel(), this.getMinY(), this.getMaxY() - 1);
}
return this.seaLevel;
}
@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)
{
return this.world.getHighestBlockAt(x, z).getY();
if (!this.trustHeightMap) {
return this.getMaxY() - 1;
}
if (this.isDummyMode()) {
if (this.dummyMaxY == null) {
this.dummyMaxY = this.getDummyColumn().isEmpty() ? this.getMinY() : Collections.max(this.getDummyColumn().keySet());
}
return this.dummyMaxY;
}
return Math.clamp(this.getChunk(x, z).getHighestBlockYAt(Coordinates.blockToChunkRelative(x), Coordinates.blockToChunkRelative(z)), this.getMinY(), this.getMaxY() - 1);
}
@Override
public String getBiomeAt(int x, int z)
public String getBiomeAt(int x, int y, int z)
{
return this.getBlock(x, this.world.getSeaLevel(), z).getBiome().getKey().toString();
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, y, z);
} catch (IllegalAccessException | InvocationTargetException exception) {
throw new RuntimeException(exception);
}
}
return key.toString();
}
@Override
public String getMaterialAt(int x, int y, int z)
{
return this.getBlock(x, y, z).getBlockData().getMaterial().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)
{
if (this.isDummyMode()) {
return new MaterialRecord(Utils.namespaceKey(this.getDummyMaterialAt(y)), false);
}
return this.getMaterialRecord(this.getChunk(x, z).getBlockType(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z)));
}
protected MaterialRecord getMaterialRecord(Material material)
{
return this.materialCache.computeIfAbsent(
material,
(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)
);
}
);
}
protected BlockData getBlockDataAt(int x, int y, int z)
{
return this.getChunk(x, z).getBlockData(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z));
}
@Override
public String getBlockStateAsStringAt(int x, int y, int z)
{
return this.getBlock(x, y, z).getBlockData().getAsString();
if (this.isDummyMode()) {
return this.getMaterialRecordAt(x, y, z).toString();
}
BlockData blockData = this.getBlockDataAt(x, y, z);
String blockStateString = this.blockStateStrings.get(blockData);
// Check before calling computeIfAbsent(), as the method reference would allocate on every call, even on hits.
if (blockStateString == null) {
blockStateString = this.blockStateStrings.computeIfAbsent(blockData, this::createBlockStateString);
}
return blockStateString;
}
protected String createBlockStateString(BlockData blockData)
{
MaterialRecord materialRecord = this.getMaterialRecord(blockData.getMaterial());
if (materialRecord.isMapped()) {
return materialRecord.toString();
}
Map<String, String> properties = this.getBlockProperties(blockData);
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);
return this.getBlockProperties(this.getBlockDataAt(x, y, z));
}
int kvStart = dataString.indexOf("[");
protected Map<String, String> getBlockProperties(BlockData blockData)
{
Map<String, String> properties = this.blockProperties.get(blockData);
if (properties == null) {
properties = this.blockProperties.computeIfAbsent(blockData, this::parseBlockProperties);
}
return properties;
}
protected Map<String, String> parseBlockProperties(BlockData blockData)
{
String dataString = blockData.getAsString();
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]);
}
return properties;
return Collections.unmodifiableMap(properties);
}
@Override
public byte getBlockLightAt(int x, int y, int z)
{
return this.getBlock(x, y, z).getLightFromBlocks();
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 this.getBlock(x, y, z).getLightFromSky();
if (this.isDummyMode()) {
return (byte) (this.getDummyMaterialAt(y + 1).equals("minecraft:air") ? 15 : 0);
}
return (byte) this.getChunk(x, z).getBlockSkyLight(Coordinates.blockToChunkRelative(x), Math.clamp(y + this.lightOffset, this.getMinY(), this.getMaxY() - 1), Coordinates.blockToChunkRelative(z));
}
@Override
public boolean isTransparent(int x, int y, int z)
public boolean isBeacon(int x, int y, int z)
{
return this.getBlock(x, y, z).getBlockData().getMaterial().isTransparent();
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,17 +18,28 @@
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.Handler;
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;
import java.util.UUID;
public class DhSupportBukkitPlugin extends JavaPlugin
{
@@ -36,68 +47,139 @@ public class DhSupportBukkitPlugin extends JavaPlugin
protected Metrics metrics;
protected final Map<Class<? extends Handler>, Handler> handlers = new HashMap<>()
{{
this.put(ConfigLoader.class, null);
this.put(PluginMessageProxy.class, null);
}};
protected ConfigLoader configLoader;
protected PluginMessageProxy pluginMessageProxy;
protected BukkitScheduler scheduler;
@Override
public void onEnable()
{
this.dhSupport = new DhSupport();
String pluginVersion = "N/A";
try {
YamlConfiguration meta = new YamlConfiguration();
meta.load(new InputStreamReader(this.getResource("plugin.yml")));
pluginVersion = meta.getString("version");
} catch (Exception exception) {
}
String gameVersion = this.getGameVersion();
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<>();
for (Class<? extends Handler> className : this.handlers.keySet()) {
try {
Handler instance = className
.getConstructor(DhSupportBukkitPlugin.class)
.newInstance(this);
for (Map<UUID, Configuration> playerConfigurations : this.dhSupport.getPlayerConfigurations().values()) {
for (Configuration playerConfig : playerConfigurations.values()) {
String rd = String.valueOf(playerConfig.getInt(DhsConfig.RENDER_DISTANCE));
instance.onEnable();
this.handlers.replace(className, instance);
} catch (Exception exception) {
this.getLogger().warning("Failed to enable handler: " + className);
this.getLogger().warning(exception.getClass().getSimpleName() + " - " + exception.getMessage());
this.getServer().getPluginManager().disablePlugin(this);
return;
renderDistances.put(rd, renderDistances.getOrDefault(rd, 0) + 1);
}
}
}
return renderDistances;
}));
this.loadDhsConfig();
this.pluginMessageProxy = new PluginMessageProxy(this);
this.pluginMessageProxy.onEnable();
this.scheduler = new BukkitScheduler(this);
this.dhSupport.setScheduler(this.scheduler);
this.dhSupport.onEnable();
this.getServer().getPluginManager().registerEvents(new WorldHandler(this), this);
int lodRefreshInterval = this.getDhSupport().getConfig().getInt(DhsConfig.LOD_REFRESH_INTERVAL) * 20;
this.getLogger().info("Ready 😀");
this.scheduler.runTimer(() -> {
this.dhSupport.getGenerationTracker().prune();
this.dhSupport.updateTouchedLods();
}, 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();
}
if (this.pluginMessageProxy != null) {
this.pluginMessageProxy.onDisable();
this.pluginMessageProxy = null;
}
if (this.configLoader != null) {
this.configLoader.onDisable();
this.configLoader = null;
}
if (this.dhSupport != null) {
this.dhSupport.onDisable();
this.dhSupport = null;
}
for (Class<? extends Handler> className : this.handlers.keySet()) {
Handler instance = this.handlers.get(className);
this.getLogger().info("Lights out!");
}
try {
if (instance != null) {
instance.onDisable();
}
} catch (Exception exception) {
this.getLogger().warning("Failed to disable handler: " + className);
this.getLogger().warning(exception.getClass().getSimpleName() + " - " + exception.getMessage());
}
this.handlers.replace(className, null);
public void loadDhsConfig()
{
if (this.configLoader != null) {
this.configLoader.onDisable();
}
this.getLogger().info("👋😭");
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
@@ -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(), player.getWorld().getUID());
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;
}
}
@@ -20,6 +20,8 @@ package no.jckf.dhsupport.bukkit.handler;
import no.jckf.dhsupport.bukkit.DhSupportBukkitPlugin;
import no.jckf.dhsupport.core.configuration.Configuration;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import org.bukkit.configuration.MemoryConfiguration;
import org.bukkit.configuration.file.FileConfiguration;
public class ConfigLoader extends Handler
@@ -35,22 +37,43 @@ public class ConfigLoader extends Handler
// Create config file if none is present.
this.plugin.saveDefaultConfig();
// Bukkit plugin config.
// The server's current config.
FileConfiguration pluginConfig = this.plugin.getConfig();
// Load missing values from default file.
pluginConfig.options().copyDefaults(true);
// The plugin's default config.
org.bukkit.configuration.Configuration defaultConfig = pluginConfig.getDefaults();
// Make sure nothing is returned for missing keys.
pluginConfig.setDefaults(new MemoryConfiguration());
// Get the config version value from the default config file.
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 (!bundledConfigVersion.equals(serverConfigVersion)) {
this.plugin.getLogger().warning("Your config file is for an older version of this plugin.");
pluginConfig.setDefaults(defaultConfig);
pluginConfig.options().copyDefaults(true);
}
// 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(false).forEach((key) -> dhsConfig.set(key, pluginConfig.get(key)));
pluginConfig.getKeys(true).forEach((key) -> dhsConfig.set(key, pluginConfig.get(key)));
}
@Override
public void onDisable()
{
// Revert any changes we did.
this.plugin.reloadConfig();
}
}
@@ -0,0 +1,68 @@
/*
* 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.Collections;
import java.util.concurrent.TimeUnit;
public class PlayerHandler implements Listener
{
protected DhSupportBukkitPlugin plugin;
public PlayerHandler(DhSupportBukkitPlugin plugin)
{
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(), player.getWorld().getUID()) != 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)
{
this.plugin.getDhSupport().clearPlayerConfiguration(event.getPlayer().getUniqueId());
this.plugin.getDhSupport().getPluginMessageHandler().onPlayerDisconnect(event.getPlayer().getUniqueId());
}
}
@@ -20,11 +20,15 @@ package no.jckf.dhsupport.bukkit.handler;
import no.jckf.dhsupport.bukkit.BukkitPluginMessageSender;
import no.jckf.dhsupport.bukkit.DhSupportBukkitPlugin;
import no.jckf.dhsupport.core.handler.ConnectionClosedException;
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;
import java.util.logging.Level;
public class PluginMessageProxy extends Handler implements PluginMessageListener
{
protected PluginMessageHandler pluginMessageHandler;
@@ -55,6 +59,14 @@ 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 (ConnectionClosedException exception) {
player.sendMessage(ChatColor.RED + "Distant Horizons: " + exception.getMessage());
} catch (RuntimeException exception) {
this.plugin.getDhSupport().getLogger().log(Level.WARNING, "Error while handling plugin message from " + player.getName() + ".", exception);
player.sendMessage(ChatColor.RED + "Distant Horizons: " + exception.getMessage());
}
}
}
@@ -20,28 +20,51 @@ 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.world.ChunkLoadEvent;
import org.bukkit.event.world.ChunkUnloadEvent;
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)
{
this.plugin.getDhSupport().setWorldInterface(world.getUID(), new BukkitWorldInterface(world));
BukkitWorldInterface worldInterface = new BukkitWorldInterface(this.plugin, world, this.plugin.getDhSupport().getConfig());
worldInterface.setLogger(this.plugin.getDhSupport().getLogger());
worldInterface.doUnsafeThings();
this.plugin.getDhSupport().setWorldInterface(world.getUID(), worldInterface);
}
protected void removeWorldInterface(World world)
@@ -49,6 +72,96 @@ public class WorldHandler implements Listener
this.plugin.getDhSupport().setWorldInterface(world.getUID(), null);
}
protected void touchLod(Location location, String reason)
{
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
public void onWorldLoad(WorldLoadEvent worldLoad)
{
@@ -60,16 +173,4 @@ public class WorldHandler implements Listener
{
this.removeWorldInterface(worldUnload.getWorld());
}
@EventHandler
public void onChunkLoad(ChunkLoadEvent chunkLoad)
{
}
@EventHandler
public void onChunkUnload(ChunkUnloadEvent chunkUnload)
{
}
}
@@ -0,0 +1,141 @@
/*
* 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.function.BiConsumer;
// Block -> chunk -> section -> region
public class Coordinates
{
protected static final int BLOCK_TO_CHUNK_POWER = 4;
protected static final int CHUNK_TO_SECTION_POWER = 2;
protected static final int SECTION_TO_REGION_POWER = 4;
// UP
public static int blockToChunk(int value)
{
return value >> BLOCK_TO_CHUNK_POWER;
}
public static int chunkToSection(int value)
{
return value >> CHUNK_TO_SECTION_POWER;
}
public static int sectionToRegion(int value)
{
return value >> SECTION_TO_REGION_POWER;
}
// DOWN
public static int regionToSection(int value)
{
return value << SECTION_TO_REGION_POWER;
}
public static int sectionToChunk(int value)
{
return value << CHUNK_TO_SECTION_POWER;
}
public static int chunkToBlock(int value)
{
return value << BLOCK_TO_CHUNK_POWER;
}
// UP VIA
public static int blockToSection(int value)
{
return value >> (BLOCK_TO_CHUNK_POWER + CHUNK_TO_SECTION_POWER);
}
public static int blockToRegion(int value)
{
return value >> (BLOCK_TO_CHUNK_POWER + CHUNK_TO_SECTION_POWER + SECTION_TO_REGION_POWER);
}
public static int chunkToRegion(int value)
{
return value >> (CHUNK_TO_SECTION_POWER + SECTION_TO_REGION_POWER);
}
// DOWN VIA
public static int regionToChunk(int value)
{
return value << (CHUNK_TO_SECTION_POWER + SECTION_TO_REGION_POWER);
}
public static int regionToBlock(int value)
{
return value << (BLOCK_TO_CHUNK_POWER + CHUNK_TO_SECTION_POWER + SECTION_TO_REGION_POWER);
}
public static int sectionToBlock(int value)
{
return value << (BLOCK_TO_CHUNK_POWER + CHUNK_TO_SECTION_POWER);
}
// RELATIVE
public static int blockToChunkRelative(int value)
{
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
);
}
}
}
}
@@ -21,53 +21,128 @@ package no.jckf.dhsupport.core;
import no.jckf.dhsupport.core.bytestream.Encoder;
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.DeleteLodsBelowVersion3;
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.Lod;
import no.jckf.dhsupport.core.dataobject.SectionPosition;
import no.jckf.dhsupport.core.handler.LodHandler;
import no.jckf.dhsupport.core.handler.PlayerConfigHandler;
import no.jckf.dhsupport.core.handler.PluginMessageHandler;
import no.jckf.dhsupport.core.lodbuilders.LodBuilder;
import no.jckf.dhsupport.core.message.plugin.FullDataChunkMessage;
import no.jckf.dhsupport.core.message.plugin.FullDataPartialUpdateMessage;
import no.jckf.dhsupport.core.message.plugin.LevelInitMessage;
import no.jckf.dhsupport.core.message.plugin.PluginMessageSender;
import no.jckf.dhsupport.core.scheduling.Scheduler;
import no.jckf.dhsupport.core.world.WorldInterface;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
public class DhSupport implements Configurable
{
protected String pluginVersion;
protected String gameVersion;
protected String dataDirectory;
protected Database database;
protected AsyncLodRepository lodRepository;
protected Configuration configuration;
protected Logger logger;
protected Scheduler scheduler;
protected UpdateChecker updateChecker;
@Nullable
protected CompletableFuture<?> pause;
protected PerformanceTracker generationTracker = new PerformanceTracker();
protected Map<UUID, WorldInterface> worldInterfaces = new HashMap<>();
protected PluginMessageHandler pluginMessageHandler;
protected PluginMessageSender pluginMessageSender;
protected ExecutorService executor;
protected Map<String, CompletableFuture<Lod>> queuedBuilders = new ConcurrentHashMap<>();
protected Map<UUID, Map<String, byte[]>> lodCache = new HashMap<>();
protected Map<String, LodModel> touchedLods = new ConcurrentHashMap<>();
public DhSupport()
protected Map<UUID, Map<UUID, Configuration>> playerConfigurations = new HashMap<>();
protected Map<UUID, PreGenerator> preGenerators = new HashMap<>();
public DhSupport(String pluginVersion, String gameVersion)
{
// Mumble mumble. Something about passing references to an incomplete "this".
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()
{
this.executor = Executors.newFixedThreadPool(4);
this.lodRepository.setLogger(this.getLogger());
try {
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.addMigration(DeleteLodsBelowVersion3.class);
this.database.migrate();
} catch (Exception exception) {
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()
@@ -77,20 +152,94 @@ public class DhSupport implements Configurable
}
}
public void setDataDirectory(String dataDirectory)
{
this.dataDirectory = dataDirectory;
}
public String getDataDirectory()
{
return this.dataDirectory;
}
public void setScheduler(Scheduler scheduler)
{
this.scheduler = scheduler;
}
public Scheduler getScheduler()
{
return this.scheduler;
}
public boolean pause()
{
if (this.pause != null) {
return false;
}
this.pause = new CompletableFuture<>();
return true;
}
public boolean unpause()
{
if (this.pause == null) {
return false;
}
this.pause.complete(null);
this.pause = null;
return true;
}
public boolean isPaused()
{
return this.pause != null;
}
public void joinPauseState()
{
if (this.pause != null) {
this.pause.join();
}
}
public PerformanceTracker getGenerationTracker()
{
return this.generationTracker;
}
public void setWorldInterface(UUID id, @Nullable WorldInterface worldInterface)
{
if (worldInterface == null) {
//this.info("Removing world interface for " + id);
this.worldInterfaces.remove(id);
this.lodCache.remove(id);
return;
}
//this.info("Adding world interface for " + id);
if (!LevelInitMessage.isValidLevelKey(worldInterface.getLevelKey())) {
this.warning("World \"" + worldInterface.getName() + "\" has the level key \"" + worldInterface.getLevelKey() + "\", which Distant Horizons clients don't accept. Distant Horizons is disabled in this world. Level keys can be up to 150 characters of a-z, A-Z, 0-9, _ and -. Rename the world, or change level_key_prefix.");
}
if (!LevelInitMessage.isValidServerKey(worldInterface.getServerKey())) {
this.warning("World \"" + worldInterface.getName() + "\" has the server key \"" + worldInterface.getServerKey() + "\", which Distant Horizons clients don't accept. It will not be used. Server keys can be up to 150 characters of a-z, A-Z, 0-9, _ and -.");
}
// Clients tell levels apart by their level key, so two worlds sharing one will get mixed up.
for (WorldInterface other : this.worldInterfaces.values()) {
if (!other.getId().equals(id) && other.getLevelKey().equals(worldInterface.getLevelKey())) {
this.warning("Worlds \"" + other.getName() + "\" and \"" + worldInterface.getName() + "\" have the same level key \"" + worldInterface.getLevelKey() + "\". Distant Horizons can't tell them apart. Rename one of them, or give one a different level_key_prefix.");
}
}
this.worldInterfaces.put(id, worldInterface);
this.lodCache.put(id, new HashMap<>());
}
public Map<UUID, WorldInterface> getWorldInterfaces()
{
return this.worldInterfaces;
}
@Nullable
@@ -100,6 +249,17 @@ public class DhSupport implements Configurable
}
@Nullable
public WorldInterface getWorldInterfaceByLevelKey(String levelKey)
{
for (WorldInterface world : this.worldInterfaces.values()) {
if (world.getLevelKey().equals(levelKey)) {
return world;
}
}
return null;
}
public PluginMessageHandler getPluginMessageHandler()
{
return this.pluginMessageHandler;
@@ -116,13 +276,23 @@ public class DhSupport implements Configurable
return this.pluginMessageSender;
}
@Override
public AsyncLodRepository getLodRepository()
{
return this.lodRepository;
}
public String getPluginVersion()
{
return this.pluginVersion;
}
public String getGameVersion()
{
return this.gameVersion;
}
public Configuration getConfig()
{
if (this.configuration == null) {
this.configuration = new Configuration();
}
return this.configuration;
}
@@ -147,33 +317,448 @@ public class DhSupport implements Configurable
this.getLogger().warning(message);
}
public CompletableFuture<Lod> queueBuilder(LodBuilder builder)
public void debug(String message)
{
return CompletableFuture.supplyAsync(builder::generate, this.executor);
// TODO: Use log levels properly?
if (!this.getConfig().getBool(DhsConfig.DEBUG, false)) {
return;
}
this.getLogger().info("[DEBUG] " + message);
}
public CompletableFuture<byte[]> getLodData(UUID worldId, SectionPosition position)
public void checkUpdates()
{
String key = position.getX() + "x" + position.getZ();
if (this.updateChecker.isLatestVersion(this.pluginVersion)) {
return;
}
if (!this.lodCache.get(worldId).containsKey(key)) {
//this.info("Cache miss: " + worldId + " " + key);
this.warning("A newer version of the plugin is available.");
}
LodBuilder builder = new LodBuilder(this.getWorldInterface(worldId).newInstance(), position);
public Map<UUID, Map<UUID, Configuration>> getPlayerConfigurations()
{
return this.playerConfigurations;
}
return this.queueBuilder(builder).thenApply((lod) -> {
Encoder encoder = new Encoder();
lod.encode(encoder);
byte[] data = encoder.toByteArray();
public void setPlayerConfiguration(UUID playerId, Map<UUID, Configuration> playerConfigurations)
{
this.playerConfigurations.put(playerId, playerConfigurations);
}
this.lodCache.get(worldId).put(key, data);
public Configuration getPlayerConfiguration(UUID playerId, UUID worldId)
{
Map<UUID, Configuration> configurations = this.playerConfigurations.get(playerId);
return data;
return configurations == null ? null : configurations.get(worldId);
}
public void clearPlayerConfiguration(UUID playerId)
{
this.playerConfigurations.remove(playerId);
}
public LodBuilder getBuilder(WorldInterface world, SectionPosition position)
{
String builderType = world.getConfig().getString(DhsConfig.BUILDER_TYPE);
LodBuilder builder;
try {
Class<? extends LodBuilder> builderClass = Class.forName(LodBuilder.class.getPackageName() + "." + builderType).asSubclass(LodBuilder.class);
Constructor<?> constructor = builderClass.getConstructor(WorldInterface.class, SectionPosition.class);
builder = (LodBuilder) constructor.newInstance(world, position);
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException | ClassNotFoundException exception) {
return null;
}
return builder;
}
public CompletableFuture<Lod> queueBuilder(UUID worldId, SectionPosition position, LodBuilder builder)
{
String key = LodModel.create()
.setWorldId(worldId)
.setX(position.getX())
.setZ(position.getZ())
.toString();
if (this.queuedBuilders.containsKey(key)) {
return this.queuedBuilders.get(key);
}
CompletableFuture<Lod> queued = this.getScheduler().runOnSeparateThread(() -> {
this.joinPauseState();
return builder.generate();
})
.thenApply((lod) -> {
this.queuedBuilders.remove(key);
return lod;
})
.exceptionally((exception) -> {
exception.printStackTrace();
this.queuedBuilders.remove(key);
return null;
});
this.queuedBuilders.put(key, queued);
return queued;
}
public CompletableFuture<LodModel> getLod(UUID worldId, SectionPosition position)
{
return this.getLodRepository()
.loadLodAsync(worldId, position.getX(), position.getZ())
.thenComposeAsync((modelFromDb) -> {
// If an LOD was found in the database, return it.
if (modelFromDb != null) {
return CompletableFuture.completedFuture(modelFromDb);
}
// Otherwise generate a new one.
return this.generateLod(worldId, position, true);
});
}
protected CompletableFuture<LodModel> generateLod(UUID worldId, SectionPosition position, boolean manageChunks)
{
int sectionWorldX = Coordinates.sectionToBlock(position.getX());
int sectionWorldZ = Coordinates.sectionToBlock(position.getZ());
WorldInterface world = this.getWorldInterface(worldId).newInstance();
boolean generateNewChunks = world.getConfig().getBool(DhsConfig.GENERATE_NEW_CHUNKS, true) && world.isInsideWorldBorder(sectionWorldX, sectionWorldZ, Lod.width);
List<CompletableFuture<Boolean>> loads = new ArrayList<>();
if (manageChunks) {
boolean discardNewChunks = world.getConfig().getBool(DhsConfig.DISCARD_NEW_CHUNKS, false);
// Load all the chunks we need for this request.
Coordinates.foreachChunkInSection(position.getX(), position.getZ(), (chunkWorldX, chunkWorldZ) -> {
final boolean alreadyLoaded = world.isChunkLoaded(chunkWorldX, chunkWorldZ);
loads.add(
(generateNewChunks ? world.loadOrGenerateChunkAsync(chunkWorldX, chunkWorldZ) : world.loadChunkAsync(chunkWorldX, chunkWorldZ))
.whenComplete((loaded, throwable) -> {
if (!alreadyLoaded) {
if (discardNewChunks) {
world.discardChunk(chunkWorldX, chunkWorldZ);
} else {
world.unloadChunkAsync(chunkWorldX, chunkWorldZ);
}
}
})
.exceptionally((throwable -> {
this.warning(throwable.getMessage());
return false;
}))
);
});
}
//this.info("Cache hit: " + worldId + " " + key);
// Wait for chunk loads, then...
return CompletableFuture.allOf(loads.toArray(new CompletableFuture[0]))
.thenComposeAsync((asd) -> {
boolean loadRejected = loads.stream()
.anyMatch((load) -> !load.join());
return CompletableFuture.completedFuture(this.lodCache.get(worldId).get(key));
if (loadRejected) {
// Use dummy chunks instead of real world data.
world.setDummyMode(true);
} else if (!manageChunks) {
// This only executes for benchmark runs, and ensures that LOD builders receive a world interface that has already cached the necessary chunks.
// The normal execution path, with manageChunks=true, performs caching during chunk loading.
final int seaLevel = world.getSeaLevel();
Coordinates.foreachChunkInSection(
position.getX(),
position.getZ(),
(chunkWorldX, chunkWorldZ) -> world.getMaterialAt(chunkWorldX, seaLevel, chunkWorldZ)
);
}
// 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);
LodModel lodModel = LodModel.create()
.setWorldId(worldId)
.setX(sectionX)
.setZ(sectionZ);
String key = lodModel.toString();
// Ignore events triggered during LOD generation.
if (this.queuedBuilders.containsKey(key)) {
return;
}
if (this.touchedLods.containsKey(key)) {
return;
}
WorldInterface world = this.getWorldInterface(worldId);
if (world == null) {
this.warning("Received a touch for unloaded world " + worldId + ". Ignoring.");
return;
}
this.debug("Touched LOD at " + world.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() + " " + lodModelToDelete.getZ() + ".");
this.getLodRepository().lodExistsAsync(lodModelToDelete.getWorldId(), lodModelToDelete.getX(), lodModelToDelete.getZ()).thenAccept((exists) -> {
if (!exists) {
return;
}
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;
}
SectionPosition position = new SectionPosition();
position.setDetailLevel(6);
position.setX(lodModelToDelete.getX());
position.setZ(lodModelToDelete.getZ());
this.getLod(lodModelToDelete.getWorldId(), position)
.thenAcceptAsync((newLodModel) -> {
Configuration worldConfig = world.getConfig();
// If this is false, then it will be false for all players as well.
boolean updatesEnabled = worldConfig.getBool(DhsConfig.REAL_TIME_UPDATES_ENABLED);
if (!updatesEnabled) {
this.debug("New LOD " + world.getName() + " " + lodModelToDelete.getX() + " " + lodModelToDelete.getZ() + " generated, but real-time updates are disabled.");
return;
}
String levelKey = world.getLevelKey();
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(), world.getId());
// No config for this player? Probably not using DH.
if (playerConfig == null) {
playersWithoutDhCount++;
continue;
}
if (!playerConfig.getBool(DhsConfig.REAL_TIME_UPDATES_ENABLED)) {
continue;
}
int updatesRadius = playerConfig.getInt(DhsConfig.REAL_TIME_UPDATE_RADIUS);
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.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;
}).exceptionally((exception) -> {
this.getLogger().log(Level.WARNING, "Pre-generation of " + world.getName() + " failed.", exception);
this.stopPreGenerator(world);
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;
});
});
}
}
@@ -1,149 +0,0 @@
/*
* 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.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.world.WorldInterface;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class LodBuilder
{
protected WorldInterface worldInterface;
protected SectionPosition position;
public LodBuilder(WorldInterface worldInterface, SectionPosition position)
{
this.worldInterface = worldInterface;
this.position = position;
}
public Lod generate()
{
int minY = this.worldInterface.getMinY();
int maxY = this.worldInterface.getMaxY();
int height = maxY - minY;
int seaLevel = this.worldInterface.getSeaLevel();
int relativeSeaLevel = seaLevel - minY;
int offsetX = this.position.getX() * 64;
int offsetZ = this.position.getZ() * 64;
List<IdMapping> idMappings = new ArrayList<>();
Map<String, Integer> mapMap = new HashMap<>();
List<List<DataPoint>> columns = new ArrayList<>();
String biome = ""; // Initialize to squelch warnings.
for (int relativeX = 0; relativeX < Lod.width; relativeX++) {
for (int relativeZ = 0; relativeZ < Lod.width; relativeZ++) {
int worldX = offsetX + relativeX;
int worldZ = offsetZ + relativeZ;
// Actual Y of top-most block.
int topLayer = this.worldInterface.getHighestYAt(worldX, worldZ);
// Distance from bottom to top-most block.
int relativeTopLayer = topLayer - minY;
biome = this.worldInterface.getBiomeAt(offsetX + relativeX, offsetZ + relativeZ);
List<DataPoint> column = new ArrayList<>();
@Nullable
DataPoint previous = null;
@Nullable
Integer solidGround = null;
for (int relativeY = height; (solidGround == null || relativeY >= solidGround) && relativeY >= 0; relativeY--) {
int worldY = minY + relativeY;
String material = this.worldInterface.getMaterialAt(worldX, worldY, worldZ);
if (solidGround == null) {
switch (material) {
case "minecraft:stone":
case "minecraft:grass":
case "minecraft:dirt":
case "minecraft:sand":
case "minecraft:sandstone":
case "minecraft:mycelium":
solidGround = Math.min(relativeY - 10, relativeSeaLevel - 10);
}
}
String compositeKey = biome + "|" + material;
//String compositeKey = biome + "|" + material + "|" + this.worldInterface.getBlockStateAsStringAt(worldX, worldY, worldZ);
@Nullable
Integer id = mapMap.get(compositeKey);
if (id == null) {
idMappings.add(new IdMapping(biome, material, null));
//idMappings.add(new IdMapping(biome, material, this.worldInterface.getBlockPropertiesAt(worldX, worldY, worldZ)));
id = idMappings.size() - 1;
mapMap.put(compositeKey, id);
}
DataPoint point;
if (previous != null && previous.getMappingId() == id) {
point = previous;
point.setStartY(point.getStartY() - 1);
point.setHeight(point.getHeight() + 1);
} else {
point = new DataPoint();
column.add(point);
point.setStartY(relativeY);
point.setMappingId(id);
point.setSkyLight(this.worldInterface.getSkyLightAt(worldX, worldY + 1, worldZ));
point.setBlockLight(this.worldInterface.getBlockLightAt(worldX, worldY + 1, worldZ));
if (relativeY == height && (material.equals("minecraft:air") || material.equals("minecraft:void_air"))) {
point.setStartY(relativeTopLayer + 1);
point.setHeight(height - relativeTopLayer);
relativeY = point.getStartY();
}
}
previous = point;
}
columns.add(column);
}
}
return new Lod(this.worldInterface, this.position, idMappings, columns);
}
}
@@ -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,171 @@
/*
* 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.Configuration;
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()
{
Configuration worldConfig = this.world.getConfig();
// Fall back to the old key in case the server's config file is for an older plugin version.
int rateLimit = Math.max(1, worldConfig.has(DhsConfig.CLIENT_LOD_REQUEST_CONCURRENCY_LIMIT)
? worldConfig.getInt(DhsConfig.CLIENT_LOD_REQUEST_CONCURRENCY_LIMIT)
: worldConfig.getInt(DhsConfig.FULL_DATA_REQUEST_CONCURRENCY_LIMIT, 20));
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;
}
}
+148 -37
View File
@@ -1,37 +1,148 @@
/*
* 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.nio.charset.StandardCharsets;
public class Utils
{
private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
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);
}
}
/*
* 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.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()) {
return "";
}
return string.substring(0, 1).toUpperCase(locale) + string.substring(1);
}
public static String ucFirst(String string)
{
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,35 +18,114 @@
package no.jckf.dhsupport.core.configuration;
import java.util.HashMap;
import org.checkerframework.checker.nullness.qual.PolyNull;
import javax.annotation.Nullable;
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 set(String key, Object value)
public void clear()
{
this.variables.clear();
}
public void clear(String prefix)
{
this.variables.keySet().removeIf((key) -> key.startsWith(prefix));
}
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);
}
public Object get(String key)
public void unset(String key)
{
return this.variables.get(key);
this.variables.remove(key);
}
public boolean getBool(String key)
public @PolyNull Object get(String key, @PolyNull Object defaultValue)
{
return (boolean) this.get(key);
return this.variables.getOrDefault(key, defaultValue);
}
public Integer getInt(String key)
public @Nullable Object get(String key)
{
return (Integer) this.get(key);
return this.get(key, null);
}
public String getString(String key)
public @PolyNull Boolean getBool(String key, @PolyNull Boolean defaultValue)
{
return (String) this.get(key);
return (Boolean) this.get(key, defaultValue);
}
public @Nullable Boolean getBool(String key)
{
return this.getBool(key, null);
}
public @PolyNull Integer getInt(String key, @PolyNull Integer defaultValue)
{
return (Integer) this.get(key, defaultValue);
}
public @Nullable Integer getInt(String key)
{
return this.getInt(key, null);
}
public @PolyNull String getString(String key, @PolyNull String defaultValue)
{
Object raw = this.get(key, defaultValue);
return raw == null ? null : String.valueOf(raw);
}
public @Nullable String getString(String key)
{
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);
}
public int decrement(String key)
{
return (int) this.variables.compute(key, (k, v) -> v == null ? -1 : (int) v - 1);
}
}
@@ -18,40 +18,90 @@
package no.jckf.dhsupport.core.configuration;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
public abstract class DhsConfig
{
public static String CONFIG_VERSION = "config_version";
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";
/**
* @deprecated Replaced by CLIENT_LOD_REQUEST_CONCURRENCY_LIMIT.
*/
public static String FULL_DATA_REQUEST_CONCURRENCY_LIMIT = "full_data_request_concurrency_limit";
public static String CLIENT_LOD_REQUEST_CONCURRENCY_LIMIT = "client_lod_request_concurrency_limit";
public static String SERVER_LOD_REQUEST_CONCURRENCY_LIMIT = "server_lod_request_concurrency_limit";
public static String REAL_TIME_UPDATES_ENABLED = "real_time_updates_enabled";
public static String REAL_TIME_UPDATE_RADIUS = "real_time_update_radius";
public static String LOGIN_DATA_SYNC_ENABLED = "login_data_sync_enabled";
public static String LOGIN_DATA_SYNC_RADIUS = "login_data_sync_radius";
public static String LOGIN_DATA_SYNC_RC_LIMIT = "login_data_sync_rc_limit";
public static Collection<String> getKeys()
{
List<String> keys = new ArrayList<>();
public static String MAX_DATA_TRANSFER_SPEED = "max_data_transfer_speed";
Arrays.stream(DhsConfig.class.getDeclaredFields())
.filter((field) -> Modifier.isStatic(field.getModifiers()))
.forEach((field) -> {
try {
keys.add((String) field.get(DhsConfig.class));
} catch (IllegalAccessException exception) {
throw new RuntimeException(exception);
}
});
public static String SCHEDULER_THREADS = "scheduler_threads";
return keys;
}
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";
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 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 DISCARD_NEW_CHUNKS = "discard_new_chunks";
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 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";
}
@@ -0,0 +1,95 @@
/*
* 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;
/**
* A view over a backing {@link Configuration}, scoped to a single world via a
* {@code worlds.<name>.} key prefix. Holds no state of its own — every method reads or writes
* through to the backing config, falling back to the unprefixed (global) key when a
* world-specific value isn't set.
*/
public class WorldConfiguration extends Configuration
{
protected static String WORLD_PREFIX = "worlds.%s.";
protected WorldInterface world;
protected Configuration config;
public WorldConfiguration(WorldInterface world, Configuration config)
{
this.world = world;
this.config = config;
}
protected String key(String key)
{
return WORLD_PREFIX.formatted(this.world.getName()) + key;
}
@Override
public void set(String key, @Nullable Object value)
{
this.config.set(this.key(key), value);
}
@Override
public void unset(String key)
{
this.config.unset(this.key(key));
}
@Override
public @PolyNull Object get(String key, @PolyNull Object defaultValue)
{
Object specific = this.config.get(this.key(key));
return specific == null ? this.config.get(key, defaultValue) : specific;
}
@Override
public boolean has(String key)
{
// Mirror get()'s resolution order: world-specific first, then the global key.
return this.config.has(this.key(key)) || this.config.has(key);
}
@Override
public void clear()
{
this.config.clear(WORLD_PREFIX.formatted(this.world.getName()));
}
@Override
public int increment(String key)
{
return this.config.increment(this.key(key));
}
@Override
public int decrement(String key)
{
return this.config.decrement(this.key(key));
}
}
@@ -0,0 +1,177 @@
/*
* 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;
import no.jckf.dhsupport.core.database.migrations.Migration;
import java.sql.*;
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
{
protected String path;
protected Connection connection;
protected Map<String, PreparedStatement> preparedStatements = new ConcurrentHashMap<>();
protected List<Class<? extends Migration>> migrations = new ArrayList<>();
protected CompletableFuture<?> optimizing;
public Connection getConnection() throws SQLException
{
if (this.connection == null || this.connection.isClosed()) {
this.connection = DriverManager.getConnection("jdbc:sqlite:" + this.path);
}
return this.connection;
}
public void open(String path) throws SQLException
{
this.path = path;
this.getConnection();
}
public void close() throws SQLException
{
this.clearQueryCache();
if (this.connection == null || this.connection.isClosed()) {
return;
}
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 = """
CREATE TABLE IF NOT EXISTS migrations (
name STRING PRIMARY KEY,
timestamp INTEGER
);
""";
try (Statement statement = this.getConnection().createStatement()) {
statement.execute(sql);
}
}
public void addMigration(Class<? extends Migration> migration)
{
this.migrations.add(migration);
}
public boolean hasMigrationRan(String name) throws SQLException
{
String sql = "SELECT 1 FROM migrations WHERE name = ?;";
try (PreparedStatement statement = this.getConnection().prepareStatement(sql)) {
statement.setString(1, name);
ResultSet result = statement.executeQuery();
return result.next();
}
}
public void markMigrationAsRan(String name) throws SQLException
{
String sql = "INSERT INTO migrations (name, timestamp) VALUES (?, ?);";
try (PreparedStatement statement = this.getConnection().prepareStatement(sql)) {
statement.setString(1, name);
statement.setInt(2, (int) (System.currentTimeMillis() / 1000));
statement.executeUpdate();
}
}
public void migrate() throws Exception
{
this.createMigrationsTable();
for (Class<? extends Migration> migrationClass : this.migrations) {
String name = migrationClass.getSimpleName();
if (this.hasMigrationRan(name)) {
continue;
}
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,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.core.database.migrations;
import no.jckf.dhsupport.core.database.Database;
import java.sql.Statement;
public class CreateLodsTable extends Migration
{
public CreateLodsTable(Database database)
{
super(database);
}
@Override
public void up() throws Exception
{
try (Statement statement = this.database.getConnection().createStatement()) {
statement.execute("""
CREATE TABLE IF NOT EXISTS lods (
worldId STRING NOT NULL,
x INTEGER NOT NULL,
z INTEGER NOT NULL,
data BLOB NOT NULL,
beacons BLOB NOT NULL,
timestamp INTEGER NOT NULL,
PRIMARY KEY (worldId, x, z)
);
""");
}
}
}
@@ -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.database.migrations;
import no.jckf.dhsupport.core.database.Database;
import java.sql.Statement;
/** Version 2 LODs lack the "regenerate" flag added by DH network protocol 16. */
public class DeleteLodsBelowVersion3 extends Migration
{
public DeleteLodsBelowVersion3(Database database)
{
super(database);
}
@Override
public void up() throws Exception
{
try (Statement statement = this.database.getConnection().createStatement()) {
statement.execute("""
DELETE FROM lods WHERE version < 3;
""");
}
}
}
@@ -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;
""");
}
}
}
@@ -0,0 +1,33 @@
/*
* 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;
public abstract class Migration
{
protected Database database;
public Migration(Database database)
{
this.database = database;
}
public abstract void up() throws Exception;
}
@@ -0,0 +1,134 @@
/*
* 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.models;
import java.util.UUID;
public class LodModel extends Model
{
public static int VERSION = 3;
public static LodModel create()
{
return new LodModel();
}
protected UUID worldId;
protected int x;
protected int z;
protected byte[] data;
protected byte[] beacons;
protected int timestamp;
protected int version;
public LodModel setWorldId(UUID worldId)
{
this.worldId = worldId;
return this;
}
public UUID getWorldId()
{
return this.worldId;
}
public LodModel setX(int x)
{
this.x = x;
return this;
}
public int getX()
{
return this.x;
}
public LodModel setZ(int z)
{
this.z = z;
return this;
}
public int getZ()
{
return this.z;
}
public LodModel setData(byte[] data)
{
this.data = data;
return this;
}
public byte[] getData()
{
return this.data;
}
public LodModel setBeacons(byte[] beacons)
{
this.beacons = beacons;
return this;
}
public byte[] getBeacons()
{
return this.beacons;
}
public LodModel setTimestamp(int timestamp)
{
this.timestamp = timestamp;
return this;
}
public int getTimestamp()
{
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();
}
}
@@ -0,0 +1,24 @@
/*
* 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.models;
public abstract class Model
{
public abstract String toString();
}
@@ -0,0 +1,78 @@
/*
* 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.repositories;
import no.jckf.dhsupport.core.database.Database;
import no.jckf.dhsupport.core.database.models.LodModel;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.function.Supplier;
public class AsyncLodRepository extends LodRepository
{
protected Executor executor = Executors.newSingleThreadExecutor();
public AsyncLodRepository(Database database)
{
super(database);
}
protected <T> CompletableFuture<T> queueTask(Supplier<T> task)
{
CompletableFuture<T> future = new CompletableFuture<>();
this.executor.execute(() -> {
try {
future.complete(task.get());
} catch (Exception exception) {
future.completeExceptionally(exception);
}
});
return future;
}
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, version));
}
public CompletableFuture<LodModel> loadLodAsync(UUID worldId, int sectionX, int sectionZ)
{
return this.queueTask(() -> this.loadLod(worldId, sectionX, sectionZ));
}
public CompletableFuture<Boolean> lodExistsAsync(UUID worldId, int sectionX, int sectionZ)
{
return this.queueTask(() -> this.lodExists(worldId, sectionX, sectionZ));
}
public CompletableFuture<Boolean> deleteLodAsync(UUID worldId, int sectionX, int sectionZ)
{
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));
}
}
@@ -0,0 +1,194 @@
/*
* 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.repositories;
import no.jckf.dhsupport.core.database.Database;
import no.jckf.dhsupport.core.database.models.LodModel;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.UUID;
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;
public LodRepository(Database database)
{
this.database = database;
}
public void setLogger(Logger logger)
{
this.logger = logger;
}
public Logger getLogger()
{
return this.logger;
}
public LodModel saveLod(UUID worldId, int sectionX, int sectionZ, byte[] data, byte[] beacons, int version)
{
int timestamp = (int) (System.currentTimeMillis() / 1000);
try {
PreparedStatement statement = this.database.prepareAndReuse(SQL_SAVE_LOD);
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();
return LodModel.create()
.setWorldId(worldId)
.setX(sectionX)
.setZ(sectionZ)
.setData(data)
.setBeacons(beacons)
.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)
{
try {
PreparedStatement statement = this.database.prepareAndReuse(SQL_LOAD_LOD);
statement.setString(1, worldId.toString());
statement.setInt(2, sectionX);
statement.setInt(3, sectionZ);
try (ResultSet result = statement.executeQuery()) {
if (!result.next()) {
return null;
}
byte[] data = result.getBytes("data");
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)
{
try {
PreparedStatement statement = this.database.prepareAndReuse(SQL_LOD_EXISTS);
statement.setString(1, worldId.toString());
statement.setInt(2, sectionX);
statement.setInt(3, sectionZ);
try (ResultSet result = statement.executeQuery()) {
result.next();
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)
{
try {
PreparedStatement statement = this.database.prepareAndReuse(SQL_DELETE_LOD);
statement.setString(1, worldId.toString());
statement.setInt(2, sectionX);
statement.setInt(3, sectionZ);
int affectedRows = statement.executeUpdate();
return affectedRows > 0;
} 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;
}
}
@@ -0,0 +1,52 @@
/*
* 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.dataobject;
import no.jckf.dhsupport.core.bytestream.Encoder;
public class Beacon extends DataObject
{
protected BlockPosition position;
protected int color;
public Beacon()
{
}
public Beacon(BlockPosition position, int color)
{
this.position = position;
this.color = color;
}
public Beacon(int x, int y, int z, int color)
{
this(new BlockPosition(x, y, z), color);
}
@Override
public void encode(Encoder encoder)
{
this.position.encode(encoder);
encoder.writeInt(this.color);
}
}
@@ -0,0 +1,80 @@
/*
* 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.dataobject;
import no.jckf.dhsupport.core.bytestream.Encoder;
public class BlockPosition extends DataObject
{
protected int x;
protected int y;
protected int z;
public BlockPosition()
{
}
public BlockPosition(int x, int y, int z)
{
this.x = x;
this.y = y;
this.z = z;
}
public void setX(int x)
{
this.x = x;
}
public int getX()
{
return this.x;
}
public void setY(int y)
{
this.y = y;
}
public int getY()
{
return this.y;
}
public void setZ(int z)
{
this.z = z;
}
public int getZ()
{
return this.z;
}
@Override
public void encode(Encoder encoder)
{
encoder.writeInt(this.x);
encoder.writeInt(this.y);
encoder.writeInt(this.z);
}
}
@@ -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)
{
@@ -18,16 +18,16 @@
package no.jckf.dhsupport.core.dataobject;
import net.jpountz.lz4.LZ4Factory;
import net.jpountz.lz4.LZ4FrameOutputStream;
import net.jpountz.xxhash.XXHashFactory;
import no.jckf.dhsupport.core.bytestream.Encoder;
import no.jckf.dhsupport.core.enums.CompressionType;
import no.jckf.dhsupport.core.enums.GenerationStep;
import no.jckf.dhsupport.core.world.WorldInterface;
import org.tukaani.xz.LZMA2Options;
import org.tukaani.xz.XZ;
import org.tukaani.xz.XZOutputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;
@@ -38,10 +38,16 @@ public class Lod extends DataObject
{
public static int dataFormatVersion = 1;
public static CompressionType compressionType = CompressionType.LZ4;
public static CompressionType compressionType = CompressionType.LZMA2;
protected static final int MAX_DICT_SIZE = 1 << 20;
public static int width = 64;
protected static volatile byte[] compressedGenerationStep;
protected static volatile byte[] compressedWorldCompressionType;
protected WorldInterface worldInterface;
protected SectionPosition position;
@@ -50,17 +56,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 +85,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.STRUCTURE_START.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 +111,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 +126,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());
@@ -118,9 +145,12 @@ public class Lod extends DataObject
{
this.position.encode(encoder);
encoder.writeInt(0); // TODO: Checksum.
long now = System.currentTimeMillis();
encoder.writeInt(0); // Checksum
this.encodeData(encoder);
this.encodeAdjacentData(encoder);
this.encodeColumnGenerationStep(encoder);
this.encodeWorldCompressionType(encoder);
this.encodeMappings(encoder);
@@ -130,9 +160,11 @@ public class Lod extends DataObject
encoder.writeByte(Lod.compressionType.value);
encoder.writeBoolean(true); // Apply to parent
encoder.writeBoolean(false); // Apply to children
encoder.writeBoolean(false); // Regenerate
encoder.writeLong(0); // TODO: Last modified TS.
encoder.writeLong(0); // TODO: Created TS.
encoder.writeLong(now); // Created at
encoder.writeLong(now); // Updated at
}
protected byte[] compress(byte[] uncompressedData)
@@ -140,14 +172,17 @@ public class Lod extends DataObject
ByteArrayOutputStream compressedStream = new ByteArrayOutputStream();
try {
LZ4FrameOutputStream compressorStream = new LZ4FrameOutputStream(
new BufferedOutputStream(compressedStream),
LZ4FrameOutputStream.BLOCKSIZE.SIZE_4MB,
uncompressedData.length,
LZ4Factory.fastestInstance().highCompressor(17),
XXHashFactory.fastestInstance().hash32(),
LZ4FrameOutputStream.FLG.Bits.BLOCK_INDEPENDENCE
);
LZMA2Options options = new LZMA2Options(3);
int dictSize = LZMA2Options.DICT_SIZE_MIN;
while (dictSize < uncompressedData.length && dictSize < MAX_DICT_SIZE) {
dictSize <<= 1;
}
options.setDictSize(dictSize);
OutputStream compressorStream = new XZOutputStream(compressedStream, options, XZ.CHECK_CRC64);
compressorStream.write(uncompressedData);
compressorStream.flush();
@@ -158,4 +193,9 @@ public class Lod extends DataObject
return compressedStream.toByteArray();
}
public List<Beacon> getBeacons()
{
return this.beacons;
}
}
@@ -23,89 +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 void setDetailLevel(int level)
public void setDetailLevel(int detailLevel)
{
this.detailLevel = level;
this.data &= ~(DETAIL_LEVEL_MASK << DETAIL_LEVEL_OFFSET);
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();
}
}
@@ -0,0 +1,47 @@
/*
* 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.enums;
/**
* Sent as its ordinal. Declaration order must match EDhApiGeneratorPlan.
*
* @see EDhApiGeneratorPlan
*/
public enum GeneratorPlan
{
SURFACE_THEN_CHUNKS(true),
SURFACE_ONLY(true),
CHUNKS_ONLY(true),
DISABLED(false)
;
public final boolean generationEnabled;
GeneratorPlan(boolean generationEnabled)
{
this.generationEnabled = generationEnabled;
}
public static GeneratorPlan byOrdinal(int ordinal)
{
GeneratorPlan[] values = GeneratorPlan.values();
return ordinal < 0 || ordinal >= values.length ? GeneratorPlan.DISABLED : values[ordinal];
}
}
@@ -0,0 +1,31 @@
/*
* 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.handler;
/**
* Thrown when a DH connection is closed with a reason meant for the player, such as a protocol version mismatch.
* Unlike other exceptions from message handling, this is expected and not a server error.
*/
public class ConnectionClosedException extends RuntimeException
{
public ConnectionClosedException(String reason)
{
super(reason);
}
}
@@ -19,23 +19,27 @@
package no.jckf.dhsupport.core.handler;
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.SectionPosition;
import no.jckf.dhsupport.core.message.plugin.ExceptionMessage;
import no.jckf.dhsupport.core.message.plugin.FullDataChunkMessage;
import no.jckf.dhsupport.core.message.plugin.FullDataSourceRequestMessage;
import no.jckf.dhsupport.core.message.plugin.FullDataSourceResponseMessage;
import org.bukkit.Bukkit;
import no.jckf.dhsupport.core.world.WorldInterface;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
public class LodHandler
{
protected static int CHUNK_SIZE = 1024 * 16; // TODO: Configurable?
public static int CHUNK_SIZE = 1024 * 16; // TODO: Configurable?
protected DhSupport dhSupport;
protected PluginMessageHandler pluginMessageHandler;
protected int bufferId = 0; // TODO: Should be tracked per player instead of globally, and reset when a player disconnects.
protected AtomicInteger requestsInFlight = new AtomicInteger(0);
public LodHandler(DhSupport dhSupport, PluginMessageHandler pluginMessageHandler)
{
@@ -46,40 +50,156 @@ 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());
int myBufferId = this.bufferId++;
if (requestMessage.getPosition().getDetailLevel() != 6) {
ExceptionMessage exceptionMessage = new ExceptionMessage();
exceptionMessage.isResponseTo(requestMessage);
exceptionMessage.setTypeId(ExceptionMessage.TYPE_SECTION_REQUIRES_SPLITTING);
exceptionMessage.setMessage("Only detail level 6 is supported");
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), exceptionMessage);
return;
}
// TODO: Some sort of Player wrapper or interface object. Bukkit classes should not be imported here.
UUID worldUuid = Bukkit.getPlayer(requestMessage.getSender()).getWorld().getUID();
Configuration dhsConfig = this.dhSupport.getConfig();
this.dhSupport.getLodData(worldUuid, requestMessage.getPosition())
.thenAccept((lod) -> {
int chunkCount = (int) Math.ceil((double) lod.length / CHUNK_SIZE);
WorldInterface world = this.dhSupport.getWorldInterfaceByLevelKey(requestMessage.getWorldName());
for (int chunkNo = 0; chunkNo < chunkCount; chunkNo++) {
FullDataChunkMessage chunkResponse = new FullDataChunkMessage();
chunkResponse.setBufferId(myBufferId);
chunkResponse.setIsFirst(chunkNo == 0);
chunkResponse.setData(Arrays.copyOfRange(
lod,
CHUNK_SIZE * chunkNo,
Math.min(CHUNK_SIZE * chunkNo + CHUNK_SIZE, lod.length)
));
if (world == null) {
ExceptionMessage exceptionMessage = new ExceptionMessage();
exceptionMessage.isResponseTo(requestMessage);
exceptionMessage.setTypeId(ExceptionMessage.TYPE_REQUEST_REJECTED);
exceptionMessage.setMessage("Unknown level " + requestMessage.getWorldName());
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), exceptionMessage);
return;
}
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), chunkResponse);
UUID worldUuid = world.getId();
Configuration config = world.getConfig();
Configuration playerConfig = this.dhSupport.getPlayerConfiguration(requestMessage.getSender(), worldUuid);
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();
String builderType = config.getString(DhsConfig.BUILDER_TYPE);
if (builderType.equalsIgnoreCase("none") && !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 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;
}
int clientRateLimit = playerConfig.getInt(DhsConfig.CLIENT_LOD_REQUEST_CONCURRENCY_LIMIT, 0);
int serverRateLimit = dhsConfig.getInt(DhsConfig.SERVER_LOD_REQUEST_CONCURRENCY_LIMIT, 0);
// The client runs its sync-on-load and generation queues independently, each against its own limit.
boolean isSyncRequest = requestMessage.getTimestamp() != null;
if (isSyncRequest) {
clientRateLimit = playerConfig.getInt(DhsConfig.LOGIN_DATA_SYNC_RC_LIMIT, 0);
}
String activeRequestCounter = isSyncRequest ? "active-sync-requests" : "active-requests";
boolean exceedsClientRateLimit = playerConfig.increment(activeRequestCounter) > clientRateLimit;
boolean exceedsServerRateLimit = serverRateLimit > 0 && this.requestsInFlight.incrementAndGet() > serverRateLimit;
if (exceedsClientRateLimit || exceedsServerRateLimit) {
if (serverRateLimit > 0) {
this.requestsInFlight.decrementAndGet();
}
playerConfig.decrement(activeRequestCounter);
ExceptionMessage exceptionMessage = new ExceptionMessage();
exceptionMessage.isResponseTo(requestMessage);
exceptionMessage.setTypeId(ExceptionMessage.TYPE_RATE_LIMITED);
exceptionMessage.setMessage("Rate limited by the server");
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), exceptionMessage);
return;
}
this.dhSupport.getLod(worldUuid, position)
.whenComplete((lodModel, exception) -> {
if (serverRateLimit > 0) {
this.requestsInFlight.decrementAndGet();
}
playerConfig.decrement(activeRequestCounter);
})
.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);
responseMessage.setBufferId(myBufferId);
boolean sendData = requestMessage.getTimestamp() == null || (requestMessage.getTimestamp() / 1000) < lodModel.getTimestamp();
if (sendData) {
int myBufferId = playerConfig.increment("buffer-id");
responseMessage.setBufferId(myBufferId);
responseMessage.setBeacons(lodModel.getBeacons());
byte[] data = lodModel.getData();
int chunkCount = (int) Math.ceil((double) data.length / CHUNK_SIZE);
for (int chunkNo = 0; chunkNo < chunkCount; chunkNo++) {
FullDataChunkMessage chunkResponse = new FullDataChunkMessage();
chunkResponse.setBufferId(myBufferId);
chunkResponse.setIsFirst(chunkNo == 0);
chunkResponse.setData(
data,
CHUNK_SIZE * chunkNo,
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() + " " + requestMessage.getPosition().getZ());
}
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), responseMessage);
//this.dhSupport.info("LOD in " + chunkCount + " parts sent for " + requestMessage.getPosition().getX() + " x " + requestMessage.getPosition().getZ());
})
.exceptionally((exception) -> {
this.dhSupport.warning(exception.getMessage());
exception.printStackTrace();
ExceptionMessage exceptionMessage = new ExceptionMessage();
exceptionMessage.isResponseTo(requestMessage);
exceptionMessage.setTypeId(ExceptionMessage.TYPE_REQUEST_REJECTED);
exceptionMessage.setMessage("Internal error");
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), exceptionMessage);
return null;
});
});
@@ -21,12 +21,24 @@ package no.jckf.dhsupport.core.handler;
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.CurrentLevelKeyMessage;
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.HashMap;
import java.util.Map;
import java.util.UUID;
public class PlayerConfigHandler
{
public final static String USAGE_PERMISSION = "distant_horizons.worlds.%s";
protected DhSupport dhSupport;
protected PluginMessageHandler pluginMessageHandler;
@@ -39,47 +51,127 @@ public class PlayerConfigHandler
public void register()
{
this.pluginMessageHandler.getEventBus().registerHandler(RemotePlayerConfigMessage.class, (configMessage) -> {
this.pluginMessageHandler.getEventBus().registerHandler(LevelInitRequestMessage.class, (initRequestMessage) -> {
// TODO: Some sort of Player wrapper or interface object. Bukkit classes should not be imported here.
// TODO: Should this be unique?
String levelKey = Bukkit.getPlayer(configMessage.getSender()).getWorld().getName();
Player player = Bukkit.getPlayer(initRequestMessage.getSender());
CurrentLevelKeyMessage levelKeyResponse = new CurrentLevelKeyMessage();
levelKeyResponse.setKey(levelKey);
this.pluginMessageHandler.sendPluginMessage(configMessage.getSender(), levelKeyResponse);
Configuration dhsConfig = this.dhSupport.getConfig();
Configuration clientConfig = configMessage.toConfiguration();
// This is not very flexible, but will do for now.
for (String key : DhsConfig.getKeys()) {
//this.dhSupport.getLogger().info("Config key " + key + ":");
Object dhsValue = dhsConfig.get(key);
Object clientValue = clientConfig.get(key);
Object keepValue;
if (dhsValue instanceof Boolean dhsBool && clientValue instanceof Boolean clientBool) {
keepValue = dhsBool && clientBool;
//this.dhSupport.getLogger().info(" Server " + (dhsBool ? "Y" : "N") + " or client " + (clientBool ? "Y" : "N") + " = " + ((boolean) keepValue ? "Y" : "N"));
} else if (dhsValue instanceof Integer dhsInt && clientValue instanceof Integer clientInt) {
keepValue = dhsInt < clientInt ? dhsInt : clientInt;
//this.dhSupport.getLogger().info(" Server " + dhsInt + " or client " + clientInt + " = " + keepValue);
} else {
keepValue = null;
//this.dhSupport.getLogger().info(" Uhh... 😵‍💫");
}
clientConfig.set(key, keepValue);
if (player == null) {
return;
}
// TODO: Store the resulting config in some sort of context object for this player.
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);
// Clients would refuse this level init. A warning was logged when the world was loaded.
if (!LevelInitMessage.isValidLevelKey(world.getLevelKey())) {
this.dhSupport.debug("Not sending level init for world " + world.getName() + " to " + player.getName() + ", as its level key is invalid.");
return;
}
LevelInitMessage levelInitResponse = new LevelInitMessage();
levelInitResponse.setNamespacedKey(worldKey.toString());
levelInitResponse.setServerKey(LevelInitMessage.isValidServerKey(world.getServerKey()) ? world.getServerKey() : "");
levelInitResponse.setLevelKey(world.getLevelKey());
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());
if (player == null) {
return;
}
Map<UUID, Configuration> playerConfigurations = new HashMap<>();
for (WorldInterface world : this.dhSupport.getWorldInterfaces().values()) {
String permission = USAGE_PERMISSION.formatted(world.getName());
boolean dhUseIsAllowed = !player.isPermissionSet(permission) || player.hasPermission(permission);
Configuration dhsConfig = world.getConfig();
Configuration clientConfig = configMessage.toConfiguration();
// This is not very flexible, but will do for now.
for (String key : RemotePlayerConfigMessage.KEYS) {
//this.dhSupport.getLogger().info("Config key " + key + ":");
Object dhsValue = dhsConfig.get(key);
Object clientValue = clientConfig.get(key);
Object keepValue = null;
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;
}
// If no value was found under the new key, read the old key to see if the server's config file is for an older plugin version.
if (key.equals(DhsConfig.CLIENT_LOD_REQUEST_CONCURRENCY_LIMIT) && dhsValue == null) {
dhsValue = dhsConfig.get(DhsConfig.FULL_DATA_REQUEST_CONCURRENCY_LIMIT);
}
// TODO: This is ugly. Move to comparator closures like in DH.
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) {
if (dhsValue instanceof Boolean dhsBool && clientValue instanceof Boolean clientBool) {
keepValue = dhsBool && clientBool;
//this.dhSupport.getLogger().info(" Server " + (dhsBool ? "Y" : "N") + " or client " + (clientBool ? "Y" : "N") + " = " + ((boolean) keepValue ? "Y" : "N"));
} else if (dhsValue instanceof Integer dhsInt && clientValue instanceof Integer clientInt) {
keepValue = dhsInt < clientInt ? dhsInt : clientInt;
//this.dhSupport.getLogger().info(" Server " + dhsInt + " or client " + clientInt + " = " + keepValue);
} else {
//this.dhSupport.getLogger().info(" Uhh... 😵‍💫");
}
}
clientConfig.set(key, keepValue == null ? -1 : keepValue);
}
playerConfigurations.put(world.getId(), clientConfig);
}
this.dhSupport.setPlayerConfiguration(configMessage.getSender(), playerConfigurations);
RemotePlayerConfigMessage configResponse = new RemotePlayerConfigMessage();
configResponse.fromConfiguration(clientConfig);
configResponse.fromConfiguration(playerConfigurations.get(player.getWorld().getUID()));
this.pluginMessageHandler.sendPluginMessage(configMessage.getSender(), configResponse);
});
@@ -1,178 +1,202 @@
/*
* 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.handler;
import no.jckf.dhsupport.core.DhSupport;
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.event.EventBus;
import no.jckf.dhsupport.core.message.MessageTypeRegistry;
import no.jckf.dhsupport.core.message.plugin.*;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import java.util.UUID;
public class PluginMessageHandler
{
protected DhSupport dhSupport;
private MessageTypeRegistry messageTypeRegistry;
public final String pluginChannel = "distant_horizons:message";
public final int protocolVersion = 3;
private EventBus<PluginMessage> eventBus;
public PluginMessageHandler(DhSupport dhSupport)
{
this.dhSupport = dhSupport;
this.eventBus = new EventBus<>();
// Define plugin channel message types.
this.messageTypeRegistry = new MessageTypeRegistry();
this.messageTypeRegistry.registerMessageType(0, null);
this.messageTypeRegistry.registerMessageType(1, CloseReasonMessage.class);
this.messageTypeRegistry.registerMessageType(2, CurrentLevelKeyMessage.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*/ null);
this.messageTypeRegistry.registerMessageType(9, FullDataChunkMessage.class);
}
public void onEnable()
{
}
public void onDisable()
{
}
@Nullable
public EventBus<PluginMessage> getEventBus()
{
return this.eventBus;
}
public void onPluginMessageReceived(@NotNull String channel, @NotNull UUID senderUuid, byte[] data)
{
PluginMessage message;
try {
message = this.readPluginMessage(data);
} catch (Exception exception) {
this.dhSupport.warning("Error while parsing incoming plugin message: " + exception.getClass() + " - " + exception.getMessage());
this.dhSupport.warning("Data was: " + Utils.bytesToHex(data));
return;
}
if (message == null) {
return;
}
message.setSender(senderUuid);
this.eventBus.dispatch(message);
}
protected PluginMessage readPluginMessage(byte[] data)
{
//this.dhSupport.info("Plugin message received. Length: " + data.length);
Decoder decoder = new Decoder(data);
// Read the client's protocol version.
short protocolVersion = decoder.readShort();
if (protocolVersion != this.protocolVersion) {
this.dhSupport.warning("Unsupported protocol version: " + protocolVersion);
return null;
}
// Read the message type ID.
short messageTypeId = decoder.readShort();
Class<? extends PluginMessage> messageClass = (Class<? extends PluginMessage>) this.messageTypeRegistry.getMessageClass(messageTypeId);
//this.dhSupport.info("Looks like a " + messageClass.getSimpleName());
PluginMessage message;
try {
message = messageClass.getConstructor().newInstance();
if (message instanceof TrackablePluginMessage) {
((TrackablePluginMessage) message).setTracker(decoder.readInt());
}
message.decode(decoder);
} catch (Exception exception) {
this.dhSupport.warning("Failed to init message class: " + exception.getClass() + " - " + exception.getMessage());
return null;
}
return message;
}
public void sendPluginMessage(UUID recipientUuid, PluginMessage message)
{
int messageTypeId = this.messageTypeRegistry.getMessageTypeId(message.getClass());
if (messageTypeId == -1) {
this.dhSupport.warning("Trying to send unknown message type: " + message.getClass());
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() + ": " + exception.getClass() + " - " + exception.getMessage());
return;
}
Encoder encoder = new Encoder();
encoder.writeShort(this.protocolVersion);
encoder.writeShort(messageTypeId);
if (message instanceof TrackablePluginMessage) {
encoder.writeInt(((TrackablePluginMessage) message).getTracker());
}
encoder.write(data);
byte[] fullMessage = encoder.toByteArray();
//this.dhSupport.info("Sending: " + Utils.bytesToHex(fullMessage));
this.dhSupport.getPluginMessageSender().sendPluginMessage(recipientUuid, this.pluginChannel, fullMessage);
}
}
/*
* 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.handler;
import no.jckf.dhsupport.core.DhSupport;
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.event.EventBus;
import no.jckf.dhsupport.core.message.MessageTypeRegistry;
import no.jckf.dhsupport.core.message.plugin.*;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class PluginMessageHandler
{
protected DhSupport dhSupport;
private MessageTypeRegistry messageTypeRegistry;
public final String pluginChannel = "distant_horizons:msg";
public final short protocolVersion = 16;
private EventBus<PluginMessage> eventBus;
// Players whose DH connection has been closed. Their messages are ignored until they reconnect.
protected Set<UUID> closedConnections = ConcurrentHashMap.newKeySet();
public PluginMessageHandler(DhSupport dhSupport)
{
this.dhSupport = dhSupport;
this.eventBus = new EventBus<>();
// Define plugin channel message types.
this.messageTypeRegistry = new MessageTypeRegistry();
this.messageTypeRegistry.registerMessageType(0, null);
this.messageTypeRegistry.registerMessageType(1, CloseReasonMessage.class);
this.messageTypeRegistry.registerMessageType(2, LevelInitMessage.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()
{
}
public void onDisable()
{
}
public void onPlayerDisconnect(UUID playerId)
{
this.closedConnections.remove(playerId);
}
@Nullable
public EventBus<PluginMessage> getEventBus()
{
return this.eventBus;
}
public void onPluginMessageReceived(@NotNull String channel, @NotNull UUID senderUuid, byte[] data)
{
if (this.closedConnections.contains(senderUuid)) {
return;
}
PluginMessage message;
try {
message = this.readPluginMessage(senderUuid, data);
} catch (Exception exception) {
this.dhSupport.warning("Error while parsing incoming plugin message: " + exception.getClass() + " - " + exception.getMessage());
this.dhSupport.warning("Data was: " + Utils.bytesToHex(data));
return;
}
if (message == null) {
return;
}
if (message instanceof CloseReasonMessage) {
this.closedConnections.add(senderUuid);
throw new ConnectionClosedException(((CloseReasonMessage) message).getReason());
}
message.setSender(senderUuid);
this.eventBus.dispatch(message);
}
protected PluginMessage readPluginMessage(UUID senderUuid, byte[] data)
{
//this.dhSupport.info("Plugin message received. Length: " + data.length);
Decoder decoder = new Decoder(data);
// Read the client's protocol version.
short protocolVersion = decoder.readShort();
if (protocolVersion != this.protocolVersion) {
CloseReasonMessage closeReasonMessage = new CloseReasonMessage();
if (protocolVersion > this.protocolVersion) {
closeReasonMessage.setReason("This server's DH plugin is outdated. You will not receive any LOD data.");
} else {
closeReasonMessage.setReason("Your client's version of DH is outdated. You will not receive any LOD data.");
}
this.sendPluginMessage(senderUuid, closeReasonMessage);
return closeReasonMessage;
}
// Read the message type ID.
short messageTypeId = decoder.readShort();
Class<? extends PluginMessage> messageClass = (Class<? extends PluginMessage>) this.messageTypeRegistry.getMessageClass(messageTypeId);
//this.dhSupport.info("Looks like a " + messageClass.getSimpleName());
PluginMessage message;
try {
message = messageClass.getConstructor().newInstance();
if (message instanceof TrackablePluginMessage) {
((TrackablePluginMessage) message).setTracker(decoder.readInt());
}
message.decode(decoder);
} catch (Exception exception) {
this.dhSupport.warning("Failed to init message class: " + exception.getClass() + " - " + exception.getMessage());
return null;
}
return message;
}
public void sendPluginMessage(UUID recipientUuid, PluginMessage message)
{
int messageTypeId = this.messageTypeRegistry.getMessageTypeId(message.getClass());
if (messageTypeId == -1) {
this.dhSupport.warning("Trying to send unknown message type: " + message.getClass());
return;
}
Encoder encoder = new Encoder(message.bufferSize());
encoder.writeShort(this.protocolVersion);
encoder.writeShort(messageTypeId);
if (message instanceof TrackablePluginMessage) {
encoder.writeInt(((TrackablePluginMessage) message).getTracker());
}
try {
message.encode(encoder);
} catch (Exception exception) {
this.dhSupport.warning("Failed to encode " + message.getClass().getSimpleName());
exception.printStackTrace();
return;
}
byte[] fullMessage = encoder.toByteArray();
//this.dhSupport.info("Sending: " + Utils.bytesToHex(fullMessage));
this.dhSupport.getPluginMessageSender().sendPluginMessage(recipientUuid, this.pluginChannel, fullMessage);
}
}
@@ -0,0 +1,224 @@
/*
* 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.lodbuilders;
import no.jckf.dhsupport.core.Coordinates;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import no.jckf.dhsupport.core.dataobject.*;
import no.jckf.dhsupport.core.world.WorldInterface;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class FastOverworldBuilder extends LodBuilder
{
public FastOverworldBuilder(WorldInterface worldInterface, SectionPosition position)
{
super(worldInterface, position);
}
public Lod generate()
{
int minY = this.worldInterface.getMinY();
int maxY = this.worldInterface.getMaxY();
int height = maxY - minY;
int seaLevel = this.worldInterface.getSeaLevel();
int relativeSeaLevel = seaLevel - minY;
int offsetX = Coordinates.sectionToBlock(this.position.getX());
int offsetZ = Coordinates.sectionToBlock(this.position.getZ());
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<>(64);
Map<String, Map<String, Integer>> mapMap = new HashMap<>();
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++) {
int worldX = offsetX + relativeX;
int worldZ = offsetZ + relativeZ;
// Actual Y of top-most block.
int topLayer = this.worldInterface.getHighestYAt(worldX, worldZ);
int hardTopLayer = topLayer;
if (includeNonCollidingTopLayer) {
outer: while (topLayer + 1 < maxY) {
String topSample = this.worldInterface.getMaterialAt(worldX, topLayer + 1, worldZ);
switch (topSample) {
case "minecraft:air":
case "minecraft:void_air":
break outer;
}
topLayer++;
}
}
// If these differ, the top layer is non-colliding and likely requires yStep=1.
if (topLayer != hardTopLayer) {
yStep = 1;
}
// Distance from bottom to top-most block.
int relativeTopLayer = topLayer - minY;
String biome = "";
if (!sampleBiomes3d) {
biome = this.worldInterface.getBiomeAt(worldX, worldZ);
}
List<DataPoint> column = new ArrayList<>(8);
@Nullable
DataPoint previous = null;
@Nullable
Integer solidGround = null;
int firstY = height - yStep;
for (int relativeY = firstY; (solidGround == null || relativeY >= solidGround) && relativeY >= 1 - yStep; relativeY -= yStep) {
int thisStep = yStep;
if (relativeY < 0) {
thisStep -= -relativeY;
relativeY = 0;
}
int lowWorldY = minY + relativeY;
int highWorldY = lowWorldY + thisStep - 1;
// We've reached the top-most colliding block. Restore yStep.
if (highWorldY == hardTopLayer) {
yStep = originalStep;
}
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":
case "minecraft:grass":
case "minecraft:dirt":
case "minecraft:gravel":
case "minecraft:sand":
case "minecraft:sandstone":
case "minecraft:mycelium":
solidGround = Math.min(relativeY - 10, relativeSeaLevel - 10);
}
}
if (sampleBiomes3d) {
biome = this.worldInterface.getBiomeAt(worldX, highWorldY, worldZ);
}
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;
biomeMap.put(material, id);
}
DataPoint point;
if (previous != null && previous.getMappingId() == id) {
point = previous;
point.setStartY(point.getStartY() - thisStep);
point.setHeight(point.getHeight() + thisStep);
} else {
point = new DataPoint();
column.add(point);
point.setStartY(relativeY);
point.setHeight(thisStep);
point.setMappingId(id);
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.
if (relativeY == firstY) {
point.setStartY(relativeTopLayer + 1);
point.setHeight(height - relativeTopLayer);
relativeY = point.getStartY();
} 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;
}
} else {
// Entered a new material. Reset yStep in case we came from air with yStep=1.
yStep = originalStep;
}
}
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, beacons);
}
}
@@ -0,0 +1,224 @@
/*
* 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.lodbuilders;
import no.jckf.dhsupport.core.Coordinates;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import no.jckf.dhsupport.core.dataobject.*;
import no.jckf.dhsupport.core.world.WorldInterface;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class FullBuilder extends LodBuilder
{
public FullBuilder(WorldInterface worldInterface, SectionPosition position)
{
super(worldInterface, position);
}
public Lod generate()
{
int minY = this.worldInterface.getMinY();
int maxY = this.worldInterface.getMaxY();
int height = maxY - minY;
int offsetX = Coordinates.sectionToBlock(this.position.getX());
int offsetZ = Coordinates.sectionToBlock(this.position.getZ());
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<>(64);
Map<String, Map<String, Map<String, Integer>>> mapMap = new HashMap<>();
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++) {
int worldX = offsetX + relativeX;
int worldZ = offsetZ + relativeZ;
// Actual Y of top-most block.
int topLayer = this.worldInterface.getHighestYAt(worldX, worldZ);
int hardTopLayer = topLayer;
if (includeNonCollidingTopLayer) {
outer: while (topLayer + 1 < maxY) {
String topSample = this.worldInterface.getMaterialAt(worldX, topLayer + 1, worldZ);
switch (topSample) {
case "minecraft:air":
case "minecraft:void_air":
break outer;
}
topLayer++;
}
}
// If these differ, the top layer is non-colliding and likely requires yStep=1.
if (topLayer != hardTopLayer) {
yStep = 1;
}
// Distance from bottom to top-most block.
int relativeTopLayer = topLayer - minY;
String biome = "";
if (!sampleBiomes3d) {
biome = this.worldInterface.getBiomeAt(worldX, worldZ);
}
List<DataPoint> column = new ArrayList<>(32);
@Nullable
DataPoint previous = null;
int firstY = height - yStep;
for (int relativeY = firstY; relativeY >= 1 - yStep; relativeY -= yStep) {
int thisStep = yStep;
if (relativeY < 0) {
thisStep -= -relativeY;
relativeY = 0;
}
int lowWorldY = minY + relativeY;
int highWorldY = lowWorldY + thisStep - 1;
// We've reached the top-most colliding block. Restore yStep.
if (highWorldY == hardTopLayer) {
yStep = originalStep;
}
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 (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;
materialMap.put(blockState, id);
}
DataPoint point;
if (previous != null && previous.getMappingId() == id) {
point = previous;
point.setStartY(point.getStartY() - thisStep);
point.setHeight(point.getHeight() + thisStep);
} else {
point = new DataPoint();
column.add(point);
point.setStartY(relativeY);
point.setHeight(thisStep);
point.setMappingId(id);
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.
if (relativeY == firstY) {
point.setStartY(relativeTopLayer + 1);
point.setHeight(height - relativeTopLayer);
relativeY = point.getStartY();
} 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.
yStep = originalStep;
}
}
previous = point;
}
columns.add(column);
}
}
return new Lod(this.worldInterface, this.position, idMappings, columns, beacons);
}
}
@@ -0,0 +1,38 @@
/*
* 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.lodbuilders;
import no.jckf.dhsupport.core.dataobject.Lod;
import no.jckf.dhsupport.core.dataobject.SectionPosition;
import no.jckf.dhsupport.core.world.WorldInterface;
public abstract class LodBuilder
{
protected WorldInterface worldInterface;
protected SectionPosition position;
public LodBuilder(WorldInterface worldInterface, SectionPosition position)
{
this.worldInterface = worldInterface;
this.position = position;
}
public abstract Lod generate();
}
@@ -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();
@@ -1,42 +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.message;
import java.util.ArrayList;
import java.util.List;
public class MessageTypeRegistry
{
protected List<Class<? extends Message>> messageTypes = new ArrayList<>();
public void registerMessageType(int id, Class<? extends Message> messageClass)
{
this.messageTypes.add(id, messageClass);
}
public Class<? extends Message> getMessageClass(int id)
{
return this.messageTypes.size() - 1 >= id ? this.messageTypes.get(id) : null;
}
public int getMessageTypeId(Class<? extends Message> messageClass)
{
return this.messageTypes.indexOf(messageClass);
}
}
/*
* 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;
import java.util.ArrayList;
import java.util.List;
public class MessageTypeRegistry
{
protected List<Class<? extends Message>> messageTypes = new ArrayList<>();
public void registerMessageType(int id, Class<? extends Message> messageClass)
{
this.messageTypes.add(id, messageClass);
}
public Class<? extends Message> getMessageClass(int id)
{
return this.messageTypes.size() - 1 >= id ? this.messageTypes.get(id) : null;
}
public int getMessageTypeId(Class<? extends Message> messageClass)
{
return this.messageTypes.indexOf(messageClass);
}
}
@@ -32,7 +32,7 @@ public class CloseReasonMessage extends PluginMessage
public String getReason()
{
return reason;
return this.reason;
}
@Override
@@ -24,9 +24,9 @@ import no.jckf.dhsupport.core.bytestream.Encoder;
public class ExceptionMessage extends TrackablePluginMessage
{
public static int TYPE_RATE_LIMITED = 0;
public static int TYPE_INVALID_LEVEL = 1;
public static int TYPE_INVALID_POSITION = 2;
public static int TYPE_REQUEST_REJECTED = 3;
public static int TYPE_REQUEST_OUT_OF_RANGE = 1;
public static int TYPE_REQUEST_REJECTED = 2;
public static int TYPE_SECTION_REQUIRES_SPLITTING = 3;
protected int typeId;
@@ -39,7 +39,7 @@ public class ExceptionMessage extends TrackablePluginMessage
public int getTypeId()
{
return typeId;
return this.typeId;
}
public void setMessage(String message)
@@ -49,7 +49,7 @@ public class ExceptionMessage extends TrackablePluginMessage
public String getMessage()
{
return message;
return this.message;
}
@Override
@@ -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)
@@ -35,17 +39,19 @@ public class FullDataChunkMessage extends PluginMessage
public int getBufferId()
{
return bufferId;
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()
{
return data;
return this.data;
}
public void setIsFirst(boolean isFirst)
@@ -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);
}
}
@@ -0,0 +1,74 @@
/*
* 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;
public class FullDataPartialUpdateMessage extends PluginMessage
{
protected String levelKey;
protected int bufferId;
protected byte[] beacons;
public void setLevelKey(String levelKey)
{
this.levelKey = levelKey;
}
public String getLevelKey()
{
return levelKey;
}
public void setBufferId(int bufferId)
{
this.bufferId = bufferId;
}
public int getBufferId()
{
return bufferId;
}
public void setBeacons(byte[] beacons)
{
this.beacons = beacons;
}
public byte[] getBeacons()
{
return this.beacons;
}
@Override
public int bufferSize()
{
return 32 + this.beacons.length;
}
@Override
public void encode(Encoder encoder)
{
encoder.writeShortString(this.levelKey);
encoder.writeInt(this.bufferId);
encoder.write(this.beacons);
}
}
@@ -36,7 +36,7 @@ public class FullDataSourceRequestMessage extends TrackablePluginMessage
public String getWorldName()
{
return worldName;
return this.worldName;
}
public void setPosition(SectionPosition position)
@@ -46,7 +46,7 @@ public class FullDataSourceRequestMessage extends TrackablePluginMessage
public SectionPosition getPosition()
{
return position;
return this.position;
}
public void setTimestamp(Long timestamp)
@@ -56,7 +56,7 @@ public class FullDataSourceRequestMessage extends TrackablePluginMessage
public Long getTimestamp()
{
return timestamp;
return this.timestamp;
}
@Override
@@ -20,24 +20,46 @@ package no.jckf.dhsupport.core.message.plugin;
import no.jckf.dhsupport.core.bytestream.Encoder;
import javax.annotation.Nullable;
public class FullDataSourceResponseMessage extends TrackablePluginMessage
{
protected int bufferId;
protected Integer bufferId;
public void setBufferId(int bufferId)
protected byte[] beacons;
public void setBufferId(@Nullable Integer bufferId)
{
this.bufferId = bufferId;
}
public int getBufferId()
{
return bufferId;
return this.bufferId;
}
public void setBeacons(byte[] beacons)
{
this.beacons = beacons;
}
public byte[] getBeacons()
{
return this.beacons;
}
@Override
public int bufferSize()
{
return this.bufferId == null ? super.bufferSize() : 16 + this.beacons.length;
}
@Override
public void encode(Encoder encoder)
{
encoder.writeBoolean(true);
encoder.writeInt(this.bufferId);
if (encoder.writeOptional(this.bufferId)) {
encoder.writeInt(this.bufferId);
encoder.write(this.beacons);
}
}
}
@@ -0,0 +1,111 @@
/*
* 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;
import no.jckf.dhsupport.core.bytestream.Encoder;
import java.util.regex.Pattern;
public class LevelInitMessage extends PluginMessage // TODO: Should probably eventually be a TrackablePluginMessage
{
// Distant Horizons clients refuse level init messages with keys that don't match these.
public static final Pattern SERVER_KEY_PATTERN = Pattern.compile("^(?=.{1,150}$)[a-zA-Z0-9_-]+$");
public static final Pattern LEVEL_KEY_PATTERN = Pattern.compile("^(?=.{1,150}$)([a-zA-Z0-9_-]+@)?[a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)?$");
protected String namespacedKey;
protected String serverKey;
protected String levelKey;
protected long time;
/**
* The server key is optional, so empty keys are valid too.
*/
public static boolean isValidServerKey(String serverKey)
{
return serverKey.isEmpty() || SERVER_KEY_PATTERN.matcher(serverKey).matches();
}
public static boolean isValidLevelKey(String levelKey)
{
return LEVEL_KEY_PATTERN.matcher(levelKey).matches();
}
public void setNamespacedKey(String namespacedKey)
{
this.namespacedKey = namespacedKey;
}
public String getNamespacedKey()
{
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)
{
this.time = time;
}
public long getTime()
{
return this.time;
}
@Override
public void encode(Encoder encoder)
{
encoder.writeShortString(this.namespacedKey);
encoder.writeShortString(this.serverKey);
encoder.writeShortString(this.levelKey);
encoder.writeLong(this.time);
}
@Override
public void decode(Decoder decoder)
{
this.namespacedKey = decoder.readShortString();
this.serverKey = decoder.readShortString();
this.levelKey = decoder.readShortString();
this.time = decoder.readLong();
}
}
@@ -1,49 +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;
import no.jckf.dhsupport.core.bytestream.Encoder;
public class CurrentLevelKeyMessage extends PluginMessage
{
protected String key;
public void setKey(String key)
{
this.key = key;
}
public String getKey()
{
return this.key;
}
@Override
public void encode(Encoder encoder)
{
encoder.writeShortString(this.key);
}
@Override
public void decode(Decoder decoder)
{
this.key = decoder.readShortString();
}
}
/*
* 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();
}
}
@@ -1,40 +1,40 @@
/*
* 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.message.Message;
import javax.annotation.Nullable;
import java.util.UUID;
public abstract class PluginMessage extends Message
{
protected UUID sender;
public void setSender(UUID uuid)
{
this.sender = uuid;
}
@Nullable
public UUID getSender()
{
return this.sender;
}
}
/*
* 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.message.Message;
import javax.annotation.Nullable;
import java.util.UUID;
public abstract class PluginMessage extends Message
{
protected UUID sender;
public void setSender(UUID uuid)
{
this.sender = uuid;
}
@Nullable
public UUID getSender()
{
return this.sender;
}
}
@@ -22,21 +22,49 @@ import no.jckf.dhsupport.core.bytestream.Decoder;
import no.jckf.dhsupport.core.bytestream.Encoder;
import no.jckf.dhsupport.core.configuration.Configuration;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import no.jckf.dhsupport.core.enums.GeneratorPlan;
public class RemotePlayerConfigMessage extends PluginMessage
{
public static String[] KEYS = {
DhsConfig.DISTANT_GENERATION_ENABLED,
DhsConfig.RENDER_DISTANCE,
DhsConfig.BORDER_CENTER_X,
DhsConfig.BORDER_CENTER_Z,
DhsConfig.BORDER_RADIUS,
DhsConfig.CLIENT_LOD_REQUEST_CONCURRENCY_LIMIT,
DhsConfig.REAL_TIME_UPDATES_ENABLED,
DhsConfig.REAL_TIME_UPDATE_RADIUS,
DhsConfig.LOGIN_DATA_SYNC_ENABLED,
DhsConfig.LOGIN_DATA_SYNC_RADIUS,
DhsConfig.LOGIN_DATA_SYNC_RC_LIMIT,
DhsConfig.MAX_DATA_TRANSFER_SPEED,
};
protected int renderDistance;
protected boolean distantGenerationEnabled;
protected int fullDataRequestConcurrencyLimit;
protected int clientLodRequestConcurrencyLimit;
protected boolean realTimeUpdatesEnabled;
protected int realTimeUpdateRadius;
protected boolean loginDataSyncEnabled;
protected int loginDataSyncRadius;
protected int loginDataSyncRcLimit;
protected int maxDataTransferSpeed;
protected int borderCenterX;
protected int borderCenterZ;
protected int borderRadius;
public void setRenderDistance(int distance)
{
this.renderDistance = distance;
@@ -57,14 +85,14 @@ public class RemotePlayerConfigMessage extends PluginMessage
return this.distantGenerationEnabled;
}
public void setFullDataRequestConcurrencyLimit(int limit)
public void setClientLodRequestConcurrencyLimit(int limit)
{
this.fullDataRequestConcurrencyLimit = limit;
this.clientLodRequestConcurrencyLimit = limit;
}
public int getFullDataRequestConcurrencyLimit()
public int getClientLodRequestConcurrencyLimit()
{
return fullDataRequestConcurrencyLimit;
return this.clientLodRequestConcurrencyLimit;
}
public void setRealTimeUpdatesEnabled(boolean enabled)
@@ -74,7 +102,17 @@ public class RemotePlayerConfigMessage extends PluginMessage
public boolean isRealTimeUpdatesEnabled()
{
return realTimeUpdatesEnabled;
return this.realTimeUpdatesEnabled;
}
public void setRealTimeUpdateRadius(int radius)
{
this.realTimeUpdateRadius = radius;
}
public int getRealTimeUpdateRadius()
{
return this.realTimeUpdateRadius;
}
public void setLoginDataSyncEnabled(boolean enabled)
@@ -84,7 +122,17 @@ public class RemotePlayerConfigMessage extends PluginMessage
public boolean getLoginDataSyncEnabled()
{
return loginDataSyncEnabled;
return this.loginDataSyncEnabled;
}
public void setLoginDataSyncRadius(int radius)
{
this.loginDataSyncRadius = radius;
}
public int getLoginDataSyncRadius()
{
return this.loginDataSyncRadius;
}
public void setLoginDataSyncRcLimit(int limit)
@@ -94,52 +142,141 @@ public class RemotePlayerConfigMessage extends PluginMessage
public int getLoginDataSyncRcLimit()
{
return loginDataSyncRcLimit;
return this.loginDataSyncRcLimit;
}
public void setMaxDataTransferSpeed(int speed)
{
this.maxDataTransferSpeed = speed;
}
public int getMaxDataTransferSpeed()
{
return this.maxDataTransferSpeed;
}
public void setBorderCenterX(int borderCenterX)
{
this.borderCenterX = borderCenterX;
}
public void setBorderCenterZ(int borderCenterZ)
{
this.borderCenterZ = borderCenterZ;
}
public void setBorderRadius(int borderRadius)
{
this.borderRadius = borderRadius;
}
public int getBorderCenterX()
{
return borderCenterX;
}
public int getBorderCenterZ()
{
return borderCenterZ;
}
public int getBorderRadius()
{
return borderRadius;
}
@Override
public void encode(Encoder encoder)
{
// Sent as the most permissive plan, so the client's own plan decides the generation mode.
encoder.writeByte((this.distantGenerationEnabled ? GeneratorPlan.SURFACE_THEN_CHUNKS : GeneratorPlan.DISABLED).ordinal());
encoder.writeInt(this.renderDistance);
encoder.writeBoolean(this.distantGenerationEnabled);
encoder.writeInt(this.fullDataRequestConcurrencyLimit);
encoder.writeInt(this.borderCenterX);
encoder.writeInt(this.borderCenterZ);
encoder.writeInt(this.borderRadius);
encoder.writeInt(this.clientLodRequestConcurrencyLimit);
encoder.writeBoolean(this.realTimeUpdatesEnabled);
encoder.writeInt(this.realTimeUpdateRadius);
encoder.writeBoolean(this.loginDataSyncEnabled);
encoder.writeInt(this.loginDataSyncRadius);
encoder.writeInt(this.loginDataSyncRcLimit);
encoder.writeInt(this.maxDataTransferSpeed);
}
@Override
public void decode(Decoder decoder)
{
this.distantGenerationEnabled = GeneratorPlan.byOrdinal(decoder.readByte()).generationEnabled;
this.renderDistance = decoder.readInt();
this.distantGenerationEnabled = decoder.readBoolean();
this.fullDataRequestConcurrencyLimit = decoder.readInt();
this.borderCenterX = decoder.readInt();
this.borderCenterZ = decoder.readInt();
this.borderRadius = decoder.readInt();
this.clientLodRequestConcurrencyLimit = decoder.readInt();
this.realTimeUpdatesEnabled = decoder.readBoolean();
this.realTimeUpdateRadius = decoder.readInt();
this.loginDataSyncEnabled = decoder.readBoolean();
this.loginDataSyncRadius = decoder.readInt();
this.loginDataSyncRcLimit = decoder.readInt();
this.maxDataTransferSpeed = decoder.readInt();
}
public void fromConfiguration(Configuration config)
{
this.renderDistance = config.getInt(DhsConfig.RENDER_DISTANCE);
this.distantGenerationEnabled = config.getBool(DhsConfig.DISTANT_GENERATION_ENABLED);
this.fullDataRequestConcurrencyLimit = config.getInt(DhsConfig.FULL_DATA_REQUEST_CONCURRENCY_LIMIT);
this.renderDistance = config.getInt(DhsConfig.RENDER_DISTANCE);
Integer borderCenterX = config.getInt(DhsConfig.BORDER_CENTER_X);
Integer borderCenterZ = config.getInt(DhsConfig.BORDER_CENTER_Z);
Integer borderRadius = config.getInt(DhsConfig.BORDER_RADIUS);
this.borderCenterX = borderCenterX == null ? 0 : borderCenterX;
this.borderCenterZ = borderCenterZ == null ? 0 : borderCenterZ;
this.borderRadius = borderRadius == null ? 0 : borderRadius;
this.clientLodRequestConcurrencyLimit = config.getInt(DhsConfig.CLIENT_LOD_REQUEST_CONCURRENCY_LIMIT);
this.realTimeUpdatesEnabled = config.getBool(DhsConfig.REAL_TIME_UPDATES_ENABLED);
this.realTimeUpdateRadius = config.getInt(DhsConfig.REAL_TIME_UPDATE_RADIUS);
this.loginDataSyncEnabled = config.getBool(DhsConfig.LOGIN_DATA_SYNC_ENABLED);
this.loginDataSyncRadius = config.getInt(DhsConfig.LOGIN_DATA_SYNC_RADIUS);
this.loginDataSyncRcLimit = config.getInt(DhsConfig.LOGIN_DATA_SYNC_RC_LIMIT);
this.maxDataTransferSpeed = config.getInt(DhsConfig.MAX_DATA_TRANSFER_SPEED);
}
public Configuration toConfiguration()
{
Configuration config = new Configuration();
config.set(DhsConfig.RENDER_DISTANCE, this.renderDistance);
config.set(DhsConfig.DISTANT_GENERATION_ENABLED, this.distantGenerationEnabled);
config.set(DhsConfig.FULL_DATA_REQUEST_CONCURRENCY_LIMIT, this.fullDataRequestConcurrencyLimit);
config.set(DhsConfig.RENDER_DISTANCE, this.renderDistance);
config.set(DhsConfig.BORDER_CENTER_X, this.borderCenterX);
config.set(DhsConfig.BORDER_CENTER_Z, this.borderCenterZ);
config.set(DhsConfig.BORDER_RADIUS, this.borderRadius);
config.set(DhsConfig.CLIENT_LOD_REQUEST_CONCURRENCY_LIMIT, this.clientLodRequestConcurrencyLimit);
config.set(DhsConfig.REAL_TIME_UPDATES_ENABLED, this.realTimeUpdatesEnabled);
config.set(DhsConfig.REAL_TIME_UPDATE_RADIUS, this.realTimeUpdateRadius);
config.set(DhsConfig.LOGIN_DATA_SYNC_ENABLED, this.loginDataSyncEnabled);
config.set(DhsConfig.LOGIN_DATA_SYNC_RADIUS, this.loginDataSyncRadius);
config.set(DhsConfig.LOGIN_DATA_SYNC_RC_LIMIT, this.loginDataSyncRcLimit);
config.set(DhsConfig.MAX_DATA_TRANSFER_SPEED, this.maxDataTransferSpeed);
return config;
}
}
@@ -29,7 +29,7 @@ public class TrackablePluginMessage extends PluginMessage
public int getTracker()
{
return tracker;
return this.tracker;
}
public void isResponseTo(TrackablePluginMessage other)
@@ -0,0 +1,52 @@
/*
* 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.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
{
boolean canReadWorldAsync();
<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,125 @@
/*
* 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;
import no.jckf.dhsupport.core.configuration.Configuration;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import no.jckf.dhsupport.core.configuration.WorldConfiguration;
public abstract class AbstractWorldInterface implements WorldInterface
{
protected String name;
protected String key;
protected String levelKey;
protected String serverKey;
protected Configuration config;
protected WorldConfiguration worldConfig;
protected boolean dummyMode = false;
public AbstractWorldInterface(String name, Configuration config)
{
this.name = name;
this.key = name.replaceAll("[^a-zA-Z0-9_-]", "");
this.config = config;
this.worldConfig = new WorldConfiguration(this, config);
// Not updated if the config changes at runtime.
this.levelKey = this.worldConfig.getString(DhsConfig.LEVEL_KEY_PREFIX, "") + this.key;
this.serverKey = this.worldConfig.getString(DhsConfig.SERVER_KEY, "");
}
@Override
public String getName()
{
return this.name;
}
@Override
public String getKey()
{
return this.key;
}
@Override
public String getLevelKey()
{
return this.levelKey;
}
@Override
public String getServerKey()
{
return this.serverKey;
}
@Override
public Configuration getConfig()
{
return this.worldConfig;
}
@Override
public void setDummyMode(boolean enable)
{
this.dummyMode = enable;
}
@Override
public boolean isDummyMode()
{
return this.dummyMode;
}
@Override
public boolean isInsideWorldBorder(int x, int z, int areaWidth)
{
Integer borderCenterX = this.getWorldBorderX();
Integer borderCenterZ = this.getWorldBorderZ();
Integer borderRadius = this.getWorldBorderRadius();
if (borderCenterX == null || borderCenterZ == null || borderRadius == null) {
return true;
}
int borderMinX = borderCenterX - borderRadius;
int borderMaxX = borderCenterX + borderRadius;
int borderMinZ = borderCenterZ - borderRadius;
int borderMaxZ = borderCenterZ + borderRadius;
int sectionMaxX = x + (areaWidth - 1);
int sectionMaxZ = z + (areaWidth - 1);
return x <= borderMaxX
&& sectionMaxX >= borderMinX
&& z <= borderMaxZ
&& sectionMaxZ >= borderMinZ;
}
@Override
public String getBiomeAt(int x, int z)
{
return this.getBiomeAt(x, this.getSeaLevel(), z);
}
}
@@ -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;
}
}
@@ -18,31 +18,96 @@
package no.jckf.dhsupport.core.world;
import java.util.Map;
import no.jckf.dhsupport.core.configuration.Configurable;
public interface WorldInterface
import javax.annotation.Nullable;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public interface WorldInterface extends Configurable
{
WorldInterface newInstance();
UUID getId();
String getName();
String getKey();
String getLevelKey();
String getServerKey();
double getCoordinateScale();
@Nullable Integer getWorldBorderX();
@Nullable Integer getWorldBorderZ();
@Nullable Integer getWorldBorderRadius();
boolean isInsideWorldBorder(int x, int z, int area);
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);
boolean discardChunk(int x, int z);
int getMinY();
int getMaxY();
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);
boolean isTransparent(int x, int y, int z);
boolean isBeacon(int x, int y, int z);
int getBeaconColor(int x, int y, int z);
}
+256 -18
View File
@@ -1,18 +1,256 @@
# Maximum supported render distance. Clients will be capped to this value.
# Required.
render_distance: 128
# TODO
distant_generation_enabled: true
# TODO
full_data_request_concurrency_limit: 20
# TODO
real_time_updates_enabled: true
# TODO
login_data_sync_enabled: true
# TODO
login_data_sync_rc_limit: 50
# 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
# Are LOD features enabled at all?
# Disable if you only want to use the plugin for level key info in a proxied setup.
# Required.
# Default: true
distant_generation_enabled: true
# Maximum number of concurrent LOD requests per client.
# Required.
# Default: 20
client_lod_request_concurrency_limit: 20
# Maximum concurrent LOD requests server-wide, across all players. 0 = unlimited.
# Optional.
# Default: 0
#server_lod_request_concurrency_limit: 100
# Send LOD updates to players in real-time?
# Required.
# Default: true
real_time_updates_enabled: true
# 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
# Let clients ask for updates to LODs they've previously downloaded.
# Required.
# Default: true
login_data_sync_enabled: true
# 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
# How many login sync requests can a client send at once?
# Required.
# Default: 50
login_data_sync_rc_limit: 50
# Currently unsupported.
# Required.
# Default: 0
max_data_transfer_speed: 0
# 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
#generate_new_chunks: false
# Show warning during startup if chunk generation is enabled?
# Optional.
# Default: true
#generate_new_chunks_warning: false
# Should new chunks be saved to disk, or should the plugin attempt to discard them?
# There is no guarantee that the server will respect every discard request, so chunks may still end up saved to disk.
# Before you consider enabling this feature, please see https://github.com/PaperMC/Paper/issues/14188
# Optional.
# Default: false
#discard_new_chunks: true
# 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.
# Required.
# 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
# Whether to include non-colliding blocks when they are the top-most block.
# True means tall grass, flowers, and snow will color the block below them.
# Optional.
# Default: true
#include_non-colliding_top_layer: false
# 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.
# Can only contain letters (a-z, A-Z), digits (0-9), underscores and dashes, and be at most 150 characters long.
# Optional.
# Default: empty
#server_key:
# When running DHS on a proxied server, set this to a value that is unique to this specific server.
# Each world's level key is this prefix followed by the world name, with all characters except letters (a-z, A-Z),
# digits (0-9), underscores and dashes removed. The prefix should only use those characters as well, but may end with @.
# Level keys must be between 1 and 150 characters long, and unique for each world.
# Optional.
# Default: empty
#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:
world:
# 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
# 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
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
# 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: 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: 12
+15 -3
View File
@@ -1,4 +1,16 @@
name: DHSupport
version: 0.2.2
main: no.jckf.dhsupport.bukkit.DhSupportBukkitPlugin
api-version: 1.21
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));
}
}
@@ -0,0 +1,179 @@
/*
* 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 junit.framework.TestCase;
import no.jckf.dhsupport.core.configuration.Configuration;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import no.jckf.dhsupport.core.world.FakeWorldInterface;
import no.jckf.dhsupport.core.world.WorldInterface;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Handler;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
public class WorldLookupTest extends TestCase
{
protected DhSupport dhSupport;
protected WorldInterface overworld;
protected WorldInterface nether;
protected WorldInterface spacedWorld;
protected List<String> warnings = new ArrayList<>();
@Override
protected void setUp()
{
this.dhSupport = new DhSupport("test", "1.21");
this.dhSupport.setLogger(capturingLogger(this.warnings));
Configuration config = this.dhSupport.getConfig();
config.set(DhsConfig.LEVEL_KEY_PREFIX, "prefix_");
config.set("worlds.My World.v2." + DhsConfig.LEVEL_KEY_PREFIX, "");
this.overworld = new FakeWorldInterface("world", config);
this.nether = new FakeWorldInterface("world_nether", config);
// Level keys are built from sanitized world names, so they can't be fed back into Bukkit.getWorld().
this.spacedWorld = new FakeWorldInterface("My World.v2", config);
for (WorldInterface world : new WorldInterface[]{ this.overworld, this.nether, this.spacedWorld }) {
this.dhSupport.setWorldInterface(world.getId(), world);
}
}
protected static Logger capturingLogger(List<String> messages)
{
Logger logger = Logger.getAnonymousLogger();
logger.setUseParentHandlers(false);
logger.addHandler(new Handler()
{
@Override
public void publish(LogRecord record)
{
messages.add(record.getMessage());
}
@Override
public void flush()
{
}
@Override
public void close()
{
}
});
return logger;
}
public void testForwardLookupResolvesBackToSameWorld()
{
for (WorldInterface world : this.dhSupport.getWorldInterfaces().values()) {
assertSame(world, this.dhSupport.getWorldInterfaceByLevelKey(world.getLevelKey()));
}
}
public void testBackwardLookupResolvesToMatchingWorld()
{
assertSame(this.overworld, this.dhSupport.getWorldInterfaceByLevelKey("prefix_world"));
assertSame(this.nether, this.dhSupport.getWorldInterfaceByLevelKey("prefix_world_nether"));
assertSame(this.spacedWorld, this.dhSupport.getWorldInterfaceByLevelKey("MyWorldv2"));
}
public void testBackwardLookupRequiresExactMatch()
{
// Neither the unprefixed key nor a shared prefix should match.
assertNull(this.dhSupport.getWorldInterfaceByLevelKey("world"));
assertNull(this.dhSupport.getWorldInterfaceByLevelKey("prefix_"));
}
public void testBackwardLookupDoesNotMatchRawWorldName()
{
assertNull(this.dhSupport.getWorldInterfaceByLevelKey("My World.v2"));
}
public void testBackwardLookupReturnsNullForUnknownKey()
{
assertNull(this.dhSupport.getWorldInterfaceByLevelKey("prefix_world_the_end"));
assertNull(this.dhSupport.getWorldInterfaceByLevelKey(""));
}
public void testRemovedWorldIsNoLongerFound()
{
this.dhSupport.setWorldInterface(this.nether.getId(), null);
assertNull(this.dhSupport.getWorldInterfaceByLevelKey("prefix_world_nether"));
}
public void testNoWarningForUniqueLevelKeys()
{
assertTrue(this.warnings.toString(), this.warnings.isEmpty());
}
public void testWarningForDuplicateLevelKey()
{
// "My World.v2" and "MyWorld.v2" both sanitize to "MyWorldv2", and have the same empty prefix.
this.dhSupport.getConfig().set("worlds.MyWorld.v2." + DhsConfig.LEVEL_KEY_PREFIX, "");
WorldInterface duplicate = new FakeWorldInterface("MyWorld.v2", this.dhSupport.getConfig());
this.dhSupport.setWorldInterface(duplicate.getId(), duplicate);
assertEquals(1, this.warnings.size());
assertTrue(this.warnings.get(0), this.warnings.get(0).contains("\"MyWorldv2\""));
}
public void testNoWarningWhenReplacingSameWorld()
{
this.dhSupport.setWorldInterface(this.overworld.getId(), this.overworld);
assertTrue(this.warnings.toString(), this.warnings.isEmpty());
}
public void testWarningForInvalidLevelKey()
{
// Without a prefix, nothing is left of the name once it is sanitized.
this.dhSupport.getConfig().set("worlds.世界." + DhsConfig.LEVEL_KEY_PREFIX, "");
WorldInterface world = new FakeWorldInterface("世界", this.dhSupport.getConfig());
this.dhSupport.setWorldInterface(world.getId(), world);
assertEquals(1, this.warnings.size());
assertTrue(this.warnings.get(0), this.warnings.get(0).contains("\"世界\" has the level key \"\""));
}
public void testWarningForInvalidServerKey()
{
this.dhSupport.getConfig().set(DhsConfig.SERVER_KEY, "my server!");
WorldInterface world = new FakeWorldInterface("other", this.dhSupport.getConfig());
this.dhSupport.setWorldInterface(world.getId(), world);
assertEquals(1, this.warnings.size());
assertTrue(this.warnings.get(0), this.warnings.get(0).contains("server key \"my server!\""));
}
}
@@ -0,0 +1,122 @@
/*
* 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 junit.framework.TestCase;
import no.jckf.dhsupport.core.world.WorldInterface;
import java.lang.reflect.Proxy;
public class WorldConfigurationTest extends TestCase
{
protected Configuration globalConfig;
protected WorldConfiguration worldConfig;
protected WorldConfiguration otherWorldConfig;
@Override
protected void setUp()
{
this.globalConfig = new Configuration();
this.worldConfig = new WorldConfiguration(fakeWorld("world"), this.globalConfig);
this.otherWorldConfig = new WorldConfiguration(fakeWorld("world_nether"), this.globalConfig);
}
/**
* WorldConfiguration only ever calls getName() on the world it's given, so a minimal proxy
* stands in for the ~40 other methods WorldInterface declares.
*/
protected static WorldInterface fakeWorld(String name)
{
return (WorldInterface) Proxy.newProxyInstance(
WorldInterface.class.getClassLoader(),
new Class[]{ WorldInterface.class },
(proxy, method, args) -> method.getName().equals("getName") ? name : null
);
}
public void testHasIsTrueForGlobalOnlyKey()
{
this.globalConfig.set("client_lod_request_concurrency_limit", 20);
assertTrue(this.worldConfig.has("client_lod_request_concurrency_limit"));
}
public void testHasIsTrueForWorldSpecificKey()
{
this.worldConfig.set("client_lod_request_concurrency_limit", 4);
assertTrue(this.worldConfig.has("client_lod_request_concurrency_limit"));
}
public void testHasIsFalseForUnsetKey()
{
assertFalse(this.worldConfig.has("nonexistent_key"));
}
public void testHasAgreesWithGetForGlobalKey()
{
this.globalConfig.set("some_key", "value");
assertEquals(this.worldConfig.has("some_key"), this.worldConfig.get("some_key") != null);
}
public void testHasAgreesWithGetForWorldSpecificKey()
{
this.worldConfig.set("some_key", "value");
assertEquals(this.worldConfig.has("some_key"), this.worldConfig.get("some_key") != null);
}
public void testHasAgreesWithGetForUnsetKey()
{
assertEquals(this.worldConfig.has("some_key"), this.worldConfig.get("some_key") != null);
}
public void testSetDoesNotShadowOtherWorlds()
{
this.worldConfig.set("some_key", "world-value");
assertFalse(this.otherWorldConfig.has("some_key"));
assertNull(this.otherWorldConfig.get("some_key"));
}
public void testIncrementIsVisibleThroughGet()
{
this.worldConfig.increment("counter");
this.worldConfig.increment("counter");
assertEquals(2, (int) this.worldConfig.getInt("counter"));
}
public void testClearDropsOnlyThisWorldsKeys()
{
this.globalConfig.set("global_key", "global-value");
this.worldConfig.set("world_key", "world-value");
this.otherWorldConfig.set("other_world_key", "other-world-value");
this.worldConfig.clear();
assertFalse(this.worldConfig.has("world_key"));
assertTrue(this.globalConfig.has("global_key"));
assertTrue(this.otherWorldConfig.has("other_world_key"));
}
}
@@ -0,0 +1,52 @@
/*
* 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 junit.framework.TestCase;
public class LevelInitMessageTest extends TestCase
{
public void testValidLevelKeys()
{
for (String levelKey : new String[]{ "world", "DHTestWorld", "world_nether", "a-b_C9", "server1_world", "lobby@world", "lobby@minecraft:overworld", "a".repeat(150) }) {
assertTrue(levelKey, LevelInitMessage.isValidLevelKey(levelKey));
}
}
public void testInvalidLevelKeys()
{
for (String levelKey : new String[]{ "", "my world", "world.v2", "verdén", "a@b@c", "a:b:c", "@world", "world:", "a".repeat(151) }) {
assertFalse(levelKey, LevelInitMessage.isValidLevelKey(levelKey));
}
}
public void testValidServerKeys()
{
for (String serverKey : new String[]{ "", "abc", "my-server_1", "a".repeat(150) }) {
assertTrue(serverKey, LevelInitMessage.isValidServerKey(serverKey));
}
}
public void testInvalidServerKeys()
{
for (String serverKey : new String[]{ "my server", "server!", "lobby@net", "a:b", "サーバー", "a".repeat(151) }) {
assertFalse(serverKey, LevelInitMessage.isValidServerKey(serverKey));
}
}
}
@@ -0,0 +1,196 @@
/*
* 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;
import junit.framework.TestCase;
import no.jckf.dhsupport.core.configuration.Configuration;
import no.jckf.dhsupport.core.configuration.DhsConfig;
public class AbstractWorldInterfaceTest extends TestCase
{
protected Configuration globalConfig;
@Override
protected void setUp()
{
this.globalConfig = new Configuration();
}
public void testNameIsKeptAsIs()
{
assertEquals("My World.v2", new FakeWorldInterface("My World.v2", this.globalConfig).getName());
}
public void testKeyKeepsAllowedCharacters()
{
assertEquals("world_nether-2", new FakeWorldInterface("world_nether-2", this.globalConfig).getKey());
}
public void testKeyStripsDisallowedCharacters()
{
assertEquals("MyWorldv2", new FakeWorldInterface("My World.v2", this.globalConfig).getKey());
assertEquals("worldsnether", new FakeWorldInterface("worlds/nether", this.globalConfig).getKey());
assertEquals("verdn", new FakeWorldInterface("verdén", this.globalConfig).getKey());
}
public void testLevelKeyWithoutPrefixIsKey()
{
WorldInterface world = new FakeWorldInterface("My World", this.globalConfig);
assertEquals("MyWorld", world.getLevelKey());
}
public void testLevelKeyUsesGlobalPrefix()
{
this.globalConfig.set(DhsConfig.LEVEL_KEY_PREFIX, "server1_");
WorldInterface world = new FakeWorldInterface("My World", this.globalConfig);
assertEquals("server1_MyWorld", world.getLevelKey());
}
public void testLevelKeyPrefersWorldSpecificPrefix()
{
this.globalConfig.set(DhsConfig.LEVEL_KEY_PREFIX, "server1_");
this.globalConfig.set("worlds.world_nether." + DhsConfig.LEVEL_KEY_PREFIX, "nether_");
WorldInterface overworld = new FakeWorldInterface("world", this.globalConfig);
WorldInterface nether = new FakeWorldInterface("world_nether", this.globalConfig);
assertEquals("server1_world", overworld.getLevelKey());
assertEquals("nether_world_nether", nether.getLevelKey());
}
public void testValidPrefixIsKept()
{
this.globalConfig.set(DhsConfig.LEVEL_KEY_PREFIX, "lobby@");
assertEquals("lobby@world", new FakeWorldInterface("world", this.globalConfig).getLevelKey());
}
public void testPrefixIsUsedAsIs()
{
// Invalid prefixes are not fixed up. DhSupport warns about the resulting level key instead.
this.globalConfig.set(DhsConfig.LEVEL_KEY_PREFIX, "my server.");
assertEquals("my server.world", new FakeWorldInterface("world", this.globalConfig).getLevelKey());
}
public void testServerKeyIsEmptyByDefault()
{
assertEquals("", new FakeWorldInterface("world", this.globalConfig).getServerKey());
}
public void testValidServerKeyIsKept()
{
this.globalConfig.set(DhsConfig.SERVER_KEY, "my-server_1");
assertEquals("my-server_1", new FakeWorldInterface("world", this.globalConfig).getServerKey());
}
public void testLevelKeyIsFixedAtConstruction()
{
WorldInterface world = new FakeWorldInterface("world", this.globalConfig);
this.globalConfig.set(DhsConfig.LEVEL_KEY_PREFIX, "server1_");
assertEquals("world", world.getLevelKey());
}
public void testDummyModeIsOffByDefault()
{
WorldInterface world = new FakeWorldInterface("world", this.globalConfig);
assertFalse(world.isDummyMode());
world.setDummyMode(true);
assertTrue(world.isDummyMode());
}
public void testEverythingIsInsideMissingWorldBorder()
{
FakeWorldInterface world = new FakeWorldInterface("world", this.globalConfig);
assertTrue(world.isInsideWorldBorder(1_000_000, -1_000_000, 64));
// A partial border counts as no border.
world.borderX = 0;
world.borderZ = 0;
assertTrue(world.isInsideWorldBorder(1_000_000, -1_000_000, 64));
}
public void testAreaInsideWorldBorder()
{
FakeWorldInterface world = borderedWorld(0, 0, 100);
assertTrue(world.isInsideWorldBorder(0, 0, 64));
assertTrue(world.isInsideWorldBorder(-64, -64, 64));
}
public void testAreaOverlappingWorldBorderEdgeIsInside()
{
FakeWorldInterface world = borderedWorld(0, 0, 100);
// Starts exactly on the max edge.
assertTrue(world.isInsideWorldBorder(100, 0, 64));
assertTrue(world.isInsideWorldBorder(0, 100, 64));
// Ends exactly on the min edge.
assertTrue(world.isInsideWorldBorder(-163, 0, 64));
assertTrue(world.isInsideWorldBorder(0, -163, 64));
}
public void testAreaOutsideWorldBorder()
{
FakeWorldInterface world = borderedWorld(0, 0, 100);
assertFalse(world.isInsideWorldBorder(101, 0, 64));
assertFalse(world.isInsideWorldBorder(0, 101, 64));
assertFalse(world.isInsideWorldBorder(-164, 0, 64));
assertFalse(world.isInsideWorldBorder(0, -164, 64));
}
public void testWorldBorderWithOffsetCenter()
{
FakeWorldInterface world = borderedWorld(1000, -500, 50);
assertTrue(world.isInsideWorldBorder(1000, -500, 1));
assertFalse(world.isInsideWorldBorder(0, 0, 64));
}
public void testBiomeWithoutYUsesSeaLevel()
{
FakeWorldInterface world = new FakeWorldInterface("world", this.globalConfig);
world.seaLevel = 70;
assertEquals("biome@5,70,7", world.getBiomeAt(5, 7));
}
protected FakeWorldInterface borderedWorld(int centerX, int centerZ, int radius)
{
FakeWorldInterface world = new FakeWorldInterface("world", this.globalConfig);
world.borderX = centerX;
world.borderZ = centerZ;
world.borderRadius = radius;
return world;
}
}
@@ -0,0 +1,227 @@
/*
* 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;
import no.jckf.dhsupport.core.configuration.Configuration;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
* Keeps everything AbstractWorldInterface implements, and stubs out the rest.
* Sea level and border can be set to drive the base class logic.
*/
public class FakeWorldInterface extends AbstractWorldInterface
{
protected UUID id = UUID.randomUUID();
public int seaLevel = 63;
public Integer borderX;
public Integer borderZ;
public Integer borderRadius;
public FakeWorldInterface(String name, Configuration config)
{
super(name, config);
}
@Override
public WorldInterface newInstance()
{
return new FakeWorldInterface(this.name, this.config);
}
@Override
public UUID getId()
{
return this.id;
}
@Override
public double getCoordinateScale()
{
return 1;
}
@Override
public Integer getWorldBorderX()
{
return this.borderX;
}
@Override
public Integer getWorldBorderZ()
{
return this.borderZ;
}
@Override
public Integer getWorldBorderRadius()
{
return this.borderRadius;
}
@Override
public boolean chunkExists(int x, int z)
{
return false;
}
@Override
public boolean isChunkLoaded(int x, int z)
{
return false;
}
@Override
public boolean loadChunk(int x, int z)
{
return false;
}
@Override
public boolean loadOrGenerateChunk(int x, int z)
{
return false;
}
@Override
public CompletableFuture<Boolean> loadChunkAsync(int x, int z)
{
return CompletableFuture.completedFuture(false);
}
@Override
public CompletableFuture<Boolean> loadOrGenerateChunkAsync(int x, int z)
{
return CompletableFuture.completedFuture(false);
}
@Override
public boolean unloadChunk(int x, int z)
{
return false;
}
@Override
public boolean unloadChunkAsync(int x, int z)
{
return false;
}
@Override
public boolean discardChunk(int x, int z)
{
return false;
}
@Override
public int getMinY()
{
return 0;
}
@Override
public int getMaxY()
{
return 0;
}
@Override
public int getSeaLevel()
{
return this.seaLevel;
}
@Override
public int getSpawnX()
{
return 0;
}
@Override
public int getSpawnY()
{
return 0;
}
@Override
public int getSpawnZ()
{
return 0;
}
@Override
public int getHighestYAt(int x, int z)
{
return 0;
}
@Override
public String getBiomeAt(int x, int y, int z)
{
return "biome@" + x + "," + y + "," + z;
}
@Override
public String getMaterialAt(int x, int y, int z)
{
return null;
}
@Override
public String getBlockStateAsStringAt(int x, int y, int z)
{
return null;
}
@Override
public Map<String, String> getBlockPropertiesAt(int x, int y, int z)
{
return Map.of();
}
@Override
public byte getBlockLightAt(int x, int y, int z)
{
return 0;
}
@Override
public byte getSkyLightAt(int x, int y, int z)
{
return 0;
}
@Override
public boolean isBeacon(int x, int y, int z)
{
return false;
}
@Override
public int getBeaconColor(int x, int y, int z)
{
return 0;
}
}
+50
View File
@@ -0,0 +1,50 @@
# Testbed
A real Paper server running the plugin, and a real Fabric client running Distant Horizons, for manual and end-to-end testing.
Pinned versions live in `versions.env`.
## Usage
```sh
testbed/setup.sh # Once, and after bumping versions.env. Downloads into testbed/.cache.
testbed/server.sh # Builds the plugin and runs the server in the foreground. Type "stop" to quit.
testbed/client.sh # In a second terminal. Opens a game window and joins the server.
```
The server runs in offline mode with the whitelist off, and the client logs in offline as `DhTester`, who is made operator so `/dhs` works in game. Set `PLAYER_NAME` for both scripts to use another name.
By default the main world is called `DH Test World`. The space in the name tests level key sanitizing. Set `LEVEL_NAME` before the first `server.sh` run to use another name. `server.properties` is only written when it doesn't exist, so to change it later, edit the file or delete `testbed/run`.
Set `SKIP_BUILD=1` to start the server without rebuilding the plugin.
## Automated test
```sh
testbed/test.sh
```
Starts a server and client in a fresh world under `testbed/run/test`, runs four checks, shuts both down and exits non-zero if any check fails. A run takes about two minutes, and needs a desktop session since the client opens a window.
1. **Login**: the client gets a level init for the overworld with the expected level key.
2. **LOD generation**: `dhs status` shows the player using DH, with a generation speed above zero.
3. **Custom biome**: the world gets a datapack that makes `dhtest:test_biome`, a copy of plains, the only overworld biome. The plugin's stored LODs must contain that key and not `minecraft:custom`, and the server must not warn that custom biomes are unsupported.
4. **Dimension switch**: teleporting to the nether gives a level init with the nether's level key.
The plugin config gets the invalid server key `DH testbed server!`. The plugin must warn about it and send clients an empty server key instead. Set `SERVER_KEY` to test another one.
Each check also fails if the server logged exceptions, or if the plugin sent the player an error message in chat. Handler exceptions only show up in chat, not in the server log.
The run directory is deleted after a passing run, and kept after a failure for its `server.out` and `client.out` logs. Set `KEEP_RUN=1` to always keep it, and `PLUGIN_JAR` to test a prebuilt jar, such as an older version.
## Files
- `testbed/.cache`: launcher, game files, Java runtime, Paper and DH jars. Shared between runs.
- `testbed/run/server`, `testbed/run/client`: server and client game directories, including worlds, configs and logs.
Both are gitignored. Delete `testbed/run` for a fresh world and fresh configs.
## Useful log lines
- Client, `testbed/run/client/logs/latest.log`: `Level init received for [...]: server key [...], level key [...]`
- Server console: `dhs status` shows DH players and generation speed.
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Starts a Fabric client with Distant Horizons and joins the testbed server.
#
# Environment:
# PLAYER_NAME Offline-mode username. Defaults to DhTester. Use the same value for server.sh.
# SERVER_HOST Defaults to localhost.
# SERVER_PORT Defaults to 25565.
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
require_setup
mkdir -p "$CLIENT_DIR/mods"
rm -f "$CLIENT_DIR"/mods/DistantHorizons-*.jar
cp "$DH_JAR" "$CLIENT_DIR/mods/"
# Skip first-launch screens and keep running when the window loses focus.
if [[ ! -f "$CLIENT_DIR/options.txt" ]]; then
cat > "$CLIENT_DIR/options.txt" <<EOF
onboardAccessibility:false
pauseOnLostFocus:false
skipMultiplayerWarning:true
joinedFirstServer:true
EOF
fi
exec "$PORTABLEMC" --main-dir "$MC_MAIN_DIR" start "fabric:$MC_VERSION" \
--mc-dir "$CLIENT_DIR" \
--username "$PLAYER_NAME" \
--join-server "${SERVER_HOST:-localhost}" \
--join-server-port "${SERVER_PORT:-25565}"
+53
View File
@@ -0,0 +1,53 @@
# Shared paths and helpers for the testbed scripts. Sourced, not executed.
TESTBED_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$TESTBED_DIR")"
source "$TESTBED_DIR/versions.env"
CACHE_DIR="$TESTBED_DIR/.cache"
RUN_DIR="${RUN_DIR:-$TESTBED_DIR/run}"
SERVER_DIR="$RUN_DIR/server"
CLIENT_DIR="$RUN_DIR/client"
MC_MAIN_DIR="$CACHE_DIR/minecraft"
PORTABLEMC="$CACHE_DIR/venv/bin/portablemc"
# Mojang's Java runtime, installed by portablemc along with the client. The server uses it too.
JAVA="$MC_MAIN_DIR/jvm/java-runtime-epsilon/bin/java"
# Offline-mode username, shared by the client and the server's op list.
PLAYER_NAME="${PLAYER_NAME:-DhTester}"
PAPER_JAR="$CACHE_DIR/paper-$MC_VERSION-$PAPER_BUILD.jar"
DH_JAR="$CACHE_DIR/DistantHorizons-$DH_VERSION.jar"
# download <url> <destination> <sha256|sha512> <hash>
download()
{
local url="$1" dest="$2" algorithm="$3" expected="$4"
if [[ -f "$dest" ]] && echo "$expected $dest" | "${algorithm}sum" --check --status; then
return
fi
echo "Downloading $(basename "$dest")..."
curl --fail --location --silent --show-error --output "$dest.part" "$url"
if ! echo "$expected $dest.part" | "${algorithm}sum" --check --status; then
rm -f "$dest.part"
echo "Checksum mismatch for $url" >&2
exit 1
fi
mv "$dest.part" "$dest"
}
require_setup()
{
if [[ ! -x "$JAVA" || ! -f "$PAPER_JAR" || ! -f "$DH_JAR" || ! -x "$PORTABLEMC" ]]; then
echo "Testbed is not set up. Run testbed/setup.sh first." >&2
exit 1
fi
}
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Builds the plugin and runs a Paper server with it in the foreground. Type "stop" to shut down.
#
# Environment:
# LEVEL_NAME Name of the main world. Defaults to a name with a space in it,
# since world names like that have broken level key lookups before.
# SKIP_BUILD Set to 1 to reuse the last built plugin jar.
# PLUGIN_JAR Use this plugin jar instead of building one. Implies SKIP_BUILD.
# PLAYER_NAME Player to make operator, so /dhs works in game. Defaults to DhTester.
# SERVER_KEY Sets server_key in the plugin config when it is first created.
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
require_setup
LEVEL_NAME="${LEVEL_NAME:-DH Test World}"
if [[ -z "${PLUGIN_JAR:-}" ]]; then
if [[ "${SKIP_BUILD:-0}" != "1" ]]; then
(cd "$PROJECT_DIR" && ./gradlew --quiet build -x test)
fi
PLUGIN_JAR="$(ls -t "$PROJECT_DIR"/build/libs/1.16.5/DistantHorizonsSupport-*.jar | head -n 1)"
fi
mkdir -p "$SERVER_DIR/plugins"
rm -f "$SERVER_DIR"/plugins/DistantHorizonsSupport-*.jar
cp "$PLUGIN_JAR" "$SERVER_DIR/plugins/"
# Start from the plugin's default config, so settings that aren't overridden keep their defaults.
if [[ -n "${SERVER_KEY:-}" && ! -f "$SERVER_DIR/plugins/DHSupport/config.yml" ]]; then
mkdir -p "$SERVER_DIR/plugins/DHSupport"
python3 -I - "$PLUGIN_JAR" "$SERVER_DIR/plugins/DHSupport/config.yml" "$SERVER_KEY" <<'EOF'
import json, sys, zipfile
plugin_jar, config_path, server_key = sys.argv[1:]
with zipfile.ZipFile(plugin_jar) as jar:
config = jar.read('config.yml').decode()
# A JSON string is also a valid YAML string, with any quotes escaped.
with open(config_path, 'w') as file:
file.write(config.rstrip('\n') + '\n\nserver_key: ' + json.dumps(server_key) + '\n')
EOF
fi
echo "eula=true" > "$SERVER_DIR/eula.txt"
# Only written once, so manual changes to server.properties are kept between runs.
if [[ ! -f "$SERVER_DIR/server.properties" ]]; then
# Non-ASCII characters are written as \uXXXX escapes, which Java's properties format always understands.
ESCAPED_LEVEL_NAME="$(python3 -I -c 'import sys; print("".join(c if ord(c) < 128 else "\\u%04x" % ord(c) for c in sys.argv[1]))' "$LEVEL_NAME")"
cat > "$SERVER_DIR/server.properties" <<EOF
level-name=$ESCAPED_LEVEL_NAME
online-mode=false
white-list=false
motd=DH Support testbed
spawn-protection=0
view-distance=8
EOF
fi
# Commands like /dhs need operator. In offline mode the UUID is derived from the name, like Java's UUID.nameUUIDFromBytes().
if [[ ! -f "$SERVER_DIR/ops.json" ]] || ! grep -q "\"$PLAYER_NAME\"" "$SERVER_DIR/ops.json"; then
PLAYER_UUID="$(python3 -c '
import hashlib, sys, uuid
digest = bytearray(hashlib.md5(("OfflinePlayer:" + sys.argv[1]).encode()).digest())
digest[6] = digest[6] & 0x0f | 0x30
digest[8] = digest[8] & 0x3f | 0x80
print(uuid.UUID(bytes=bytes(digest)))
' "$PLAYER_NAME")"
cat > "$SERVER_DIR/ops.json" <<EOF
[
{
"uuid": "$PLAYER_UUID",
"name": "$PLAYER_NAME",
"level": 4,
"bypassesPlayerLimit": false
}
]
EOF
fi
echo "Starting Paper $MC_VERSION with $(basename "$PLUGIN_JAR")..."
# Java encodes file paths using the locale. Without UTF-8, it can't create worlds with names like "世界".
export LC_ALL=C.UTF-8
cd "$SERVER_DIR"
exec "$JAVA" -Xms2G -Xmx4G -jar "$PAPER_JAR" --nogui
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Downloads everything the testbed needs into testbed/.cache. Safe to re-run.
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
mkdir -p "$CACHE_DIR"
if [[ ! -x "$PORTABLEMC" ]] || ! "$PORTABLEMC" --version 2>/dev/null | grep -q "$PORTABLEMC_VERSION"; then
echo "Installing portablemc $PORTABLEMC_VERSION..."
python3 -m venv "$CACHE_DIR/venv"
"$CACHE_DIR/venv/bin/pip" install --quiet --disable-pip-version-check "portablemc==$PORTABLEMC_VERSION"
fi
echo "Installing Minecraft $MC_VERSION with Fabric, and its Java runtime..."
"$PORTABLEMC" --main-dir "$MC_MAIN_DIR" start "fabric:$MC_VERSION" --dry > /dev/null
download "$PAPER_URL" "$PAPER_JAR" sha256 "$PAPER_SHA256"
download "$DH_URL" "$DH_JAR" sha512 "$DH_SHA512"
echo "Testbed is ready."
+433
View File
@@ -0,0 +1,433 @@
#!/usr/bin/env bash
# Runs a real server and client in a fresh world, and checks that:
# 1. Login: the client gets a level init with the expected level key.
# 2. LOD generation: the server sees the player using DH and generates LODs.
# 3. Custom biome: LODs from a datapack biome store its real key, not "minecraft:custom".
# 4. Dimension switch: moving to the nether gives a new level init, without errors.
# After each step, the server log is checked for exceptions, and the client chat for errors the
# plugin reports to the player.
#
# Needs a desktop session, as the client opens a window. Exits non-zero if any check fails.
#
# Environment:
# LEVEL_NAME Name of the main world. Defaults to "DH Test World".
# SERVER_KEY Server key in the plugin config. Defaults to an invalid one, which the plugin must not send.
# SKIP_BUILD Set to 1 to reuse the last built plugin jar.
# PLUGIN_JAR Test this plugin jar instead of building one.
# KEEP_RUN Set to 1 to keep testbed/run/test after a passing run. It is always kept after a failure.
set -uo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
require_setup
export RUN_DIR="$TESTBED_DIR/run/test"
export LEVEL_NAME="${LEVEL_NAME:-DH Test World}"
export SERVER_KEY="${SERVER_KEY:-DH testbed server!}"
export PLAYER_NAME
SERVER_DIR="$RUN_DIR/server"
CLIENT_DIR="$RUN_DIR/client"
SERVER_OUT="$RUN_DIR/server.out"
CLIENT_OUT="$RUN_DIR/client.out"
CONSOLE="$RUN_DIR/console"
# Expected level key for a world name, without a prefix. Mirrors AbstractWorldInterface.
expected_level_key()
{
echo "$1" | sed 's/[^a-zA-Z0-9_-]//g'
}
# Expected server key sent to clients. Invalid keys are not sent, and the server logs a warning instead.
expected_server_key()
{
if [[ -z "$1" || "$1" =~ ^[a-zA-Z0-9_-]{1,150}$ ]]; then
echo "$1"
fi
}
LEVEL_KEY="$(expected_level_key "$LEVEL_NAME")"
NETHER_LEVEL_KEY="$(expected_level_key "${LEVEL_NAME}_nether")"
SERVER_KEY_EXPECTED="$(expected_server_key "$SERVER_KEY")"
# Datapack biome that replaces every biome in the overworld.
CUSTOM_BIOME="dhtest:test_biome"
SERVER_PID=""
CLIENT_PID=""
FAILURES=0
#===========#
# reporting #
#===========#
pass()
{
echo "PASS $1"
}
fail()
{
echo "FAIL $1"
[[ $# -gt 1 ]] && echo " $2"
FAILURES=$((FAILURES + 1))
}
abort()
{
fail "$@"
exit 1
}
#=========#
# helpers #
#=========#
# Log text without ANSI colour codes, from an optional line number onwards.
log_text()
{
local file="$1" from="${2:-1}"
tail -n "+$from" "$file" 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g'
}
line_count()
{
wc -l < "$1" 2>/dev/null || echo 0
}
# wait_for <file> <regex> <timeout seconds> [from line]
wait_for()
{
local file="$1" pattern="$2" timeout="$3" from="${4:-1}"
local deadline=$((SECONDS + timeout))
while ((SECONDS < deadline)); do
if log_text "$file" "$from" | grep -qE "$pattern"; then
return 0
fi
if [[ -n "$SERVER_PID" ]] && ! kill -0 "$SERVER_PID" 2>/dev/null; then
abort "Server exited unexpectedly" "See $SERVER_OUT"
fi
if [[ -n "$CLIENT_PID" ]] && ! kill -0 "$CLIENT_PID" 2>/dev/null; then
abort "Client exited unexpectedly" "See $CLIENT_OUT"
fi
sleep 1
done
return 1
}
console()
{
echo "$1" >&3
}
# Fails the given check if the server has logged exceptions or errors since the given line.
check_server_errors()
{
local check="$1" from="$2"
local errors
errors="$(log_text "$SERVER_OUT" "$from" | grep -E "Exception|ERROR\]|Cannot invoke" | head -n 5)"
if [[ -n "$errors" ]]; then
fail "$check: server logged errors" "$(echo "$errors" | head -n 1)"
return 1
fi
return 0
}
# Fails the given check if the plugin has sent the player error messages since the given line.
# PluginMessageProxy catches exceptions from message handlers and sends them to the player's chat,
# so they never reach the server log. DH's own client messages have a colour code and don't match.
check_client_errors()
{
local check="$1" from="$2"
local errors
errors="$(log_text "$CLIENT_OUT" "$from" | grep -aE "\[CHAT\] (§c)?Distant Horizons: " | head -n 5)"
if [[ -n "$errors" ]]; then
fail "$check: plugin sent errors to the player" "$(echo "$errors" | head -n 1 | sed -E 's/.*\[CHAT\] //')"
return 1
fi
return 0
}
# Writes a datapack that makes $CUSTOM_BIOME, a copy of vanilla plains, the only overworld biome.
# The biome definition and data pack format come from the client jar, so they match the game version.
create_biome_datapack()
{
local pack="$SERVER_DIR/$LEVEL_NAME/datapacks/dhtest"
local client_jar
client_jar="$(ls "$MC_MAIN_DIR"/versions/fabric-"$MC_VERSION"-*/fabric-"$MC_VERSION"-*.jar | head -n 1)"
mkdir -p "$pack/data/dhtest/worldgen/biome" "$pack/data/minecraft/dimension"
python3 -I - "$client_jar" "$pack" <<'EOF'
import json, sys, zipfile
client_jar, pack = sys.argv[1], sys.argv[2]
with zipfile.ZipFile(client_jar) as jar:
pack_format = json.loads(jar.read('version.json'))['pack_version']['data_major']
plains = jar.read('data/minecraft/worldgen/biome/plains.json')
with open(pack + '/pack.mcmeta', 'w') as file:
json.dump({'pack': {
'description': 'DH Support testbed biome',
'pack_format': pack_format,
'min_format': pack_format,
'max_format': pack_format,
}}, file)
with open(pack + '/data/dhtest/worldgen/biome/test_biome.json', 'wb') as file:
file.write(plains)
with open(pack + '/data/minecraft/dimension/overworld.json', 'w') as file:
json.dump({
'type': 'minecraft:overworld',
'generator': {
'type': 'minecraft:noise',
'settings': 'minecraft:overworld',
'biome_source': {'type': 'minecraft:fixed', 'biome': 'dhtest:test_biome'},
},
}, file)
EOF
}
# Prints how often each given biome key appears in the plugin's stored LODs, one count per line.
# LOD data holds several XZ-compressed sections, so every XZ stream in each row is decompressed.
count_lod_biomes()
{
python3 -I - "$SERVER_DIR/plugins/DHSupport/data.sqlite" "$@" <<'EOF'
import lzma, sqlite3, sys
database, keys = sys.argv[1], [key.encode() for key in sys.argv[2:]]
counts = [0] * len(keys)
try:
connection = sqlite3.connect('file:' + database + '?mode=ro', uri=True, timeout=10)
rows = connection.execute('SELECT data FROM lods').fetchall()
except sqlite3.Error:
rows = []
for (data,) in rows:
start = 0
while (start := data.find(b'\xfd7zXZ\x00', start)) >= 0:
try:
raw = lzma.LZMADecompressor(format=lzma.FORMAT_XZ).decompress(data[start:])
counts = [count + raw.count(key) for count, key in zip(counts, keys)]
except lzma.LZMAError:
pass
start += 6
print('\n'.join(str(count) for count in counts))
EOF
}
stop_all()
{
if [[ -n "$CLIENT_PID" ]] && kill -0 "$CLIENT_PID" 2>/dev/null; then
kill -- "-$CLIENT_PID" 2>/dev/null
fi
if [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then
console "stop" 2>/dev/null
for _ in $(seq 1 60); do
kill -0 "$SERVER_PID" 2>/dev/null || break
sleep 1
done
kill -- "-$SERVER_PID" 2>/dev/null
fi
exec 3>&- 2>/dev/null
if ((FAILURES == 0)) && [[ "${KEEP_RUN:-0}" != "1" ]]; then
rm -rf "$RUN_DIR"
else
echo "Logs kept in $RUN_DIR"
fi
}
#=======#
# setup #
#=======#
rm -rf "$RUN_DIR"
mkdir -p "$RUN_DIR"
mkfifo "$CONSOLE"
create_biome_datapack
trap stop_all EXIT
trap 'exit 130' INT TERM
echo "Starting server with world \"$LEVEL_NAME\" (expected level key: $LEVEL_KEY) and server key \"$SERVER_KEY\" (expected: $SERVER_KEY_EXPECTED)..."
# setsid puts each side in its own process group, so cleanup can stop everything they started.
setsid "$TESTBED_DIR/server.sh" < "$CONSOLE" > "$SERVER_OUT" 2>&1 &
SERVER_PID=$!
# Hold the console pipe open for the whole run. Closing it would end the server's input.
exec 3> "$CONSOLE"
wait_for "$SERVER_OUT" 'Done \(' 300 || abort "Server did not start within 300 seconds" "See $SERVER_OUT"
check_server_errors "Server startup" 1 || exit 1
#==========#
# 1. login #
#==========#
echo "Starting client..."
SERVER_MARK=$(line_count "$SERVER_OUT")
setsid "$TESTBED_DIR/client.sh" > "$CLIENT_OUT" 2>&1 &
CLIENT_PID=$!
if ! wait_for "$SERVER_OUT" "$PLAYER_NAME.*joined the game" 300 "$SERVER_MARK"; then
abort "Login: client did not join within 300 seconds" "$(log_text "$SERVER_OUT" "$SERVER_MARK" | grep -E "lost connection" | head -n 1)"
fi
if ! wait_for "$CLIENT_OUT" "Level init received for \[minecraft:overworld\]" 60; then
abort "Login: client got no level init for the overworld within 60 seconds" "$(log_text "$CLIENT_OUT" | grep -oE "Server sent invalid (level|server) key" | head -n 1)"
fi
# Give the client time to send its first LOD requests.
sleep 10
LEVEL_INIT="$(log_text "$CLIENT_OUT" | grep -oE "Level init received for \[minecraft:overworld\]: server key \[[^]]*\], level key \[[^]]*\]" | tail -n 1)"
ACTUAL_KEY="$(echo "$LEVEL_INIT" | sed -E 's/.*level key \[([^]]*)\]/\1/')"
ACTUAL_SERVER_KEY="$(echo "$LEVEL_INIT" | sed -E 's/.*server key \[([^]]*)\].*/\1/')"
if [[ "$ACTUAL_KEY" != "$LEVEL_KEY" ]]; then
fail "Login: wrong level key" "Expected $LEVEL_KEY, got $ACTUAL_KEY"
elif [[ "$ACTUAL_SERVER_KEY" != "$SERVER_KEY_EXPECTED" ]]; then
fail "Login: wrong server key" "Expected $SERVER_KEY_EXPECTED, got $ACTUAL_SERVER_KEY"
elif [[ "$SERVER_KEY_EXPECTED" != "$SERVER_KEY" ]] && ! log_text "$SERVER_OUT" | grep -q "has the server key"; then
fail "Login: server did not warn about the invalid server key"
elif log_text "$CLIENT_OUT" | grep -qE "invalid (level|server) key"; then
fail "Login: client rejected a key" "$(log_text "$CLIENT_OUT" | grep -oE "Server sent invalid (level|server) key" | head -n 1)"
elif check_server_errors "Login" "$SERVER_MARK" && check_client_errors "Login" 1; then
pass "Login: level key is $ACTUAL_KEY, server key is \"$ACTUAL_SERVER_KEY\""
fi
#===================#
# 2. LOD generation #
#===================#
echo "Waiting for LOD generation..."
SERVER_MARK=$(line_count "$SERVER_OUT")
CLIENT_MARK=$(line_count "$CLIENT_OUT")
LOD_OK=0
DEADLINE=$((SECONDS + 120))
while ((SECONDS < DEADLINE)); do
STATUS_MARK=$(line_count "$SERVER_OUT")
console "dhs status"
sleep 5
STATUS="$(log_text "$SERVER_OUT" "$((STATUS_MARK + 1))")"
SPEED="$(echo "$STATUS" | grep -oE "Current generation speed: [0-9.]+" | tail -n 1 | grep -oE "[0-9.]+$")"
if echo "$STATUS" | grep -qE "using Distant Horizons: .*$PLAYER_NAME" \
&& [[ -n "$SPEED" ]] && awk "BEGIN { exit !($SPEED > 0) }"; then
LOD_OK=1
break
fi
done
if ((LOD_OK)); then
check_server_errors "LOD generation" "$SERVER_MARK" \
&& check_client_errors "LOD generation" "$CLIENT_MARK" \
&& pass "LOD generation: $SPEED chunks per second"
else
fail "LOD generation: no generation for $PLAYER_NAME within 120 seconds" "Last speed: ${SPEED:-none}"
check_client_errors "LOD generation" "$CLIENT_MARK"
fi
#=================#
# 3. custom biome #
#=================#
echo "Checking custom biome in LODs..."
BIOME_OK=0
DEADLINE=$((SECONDS + 60))
while ((SECONDS < DEADLINE)); do
mapfile -t BIOME_COUNTS < <(count_lod_biomes "$CUSTOM_BIOME" "minecraft:custom")
if ((${BIOME_COUNTS[0]:-0} > 0)); then
BIOME_OK=1
break
fi
sleep 5
done
if log_text "$SERVER_OUT" | grep -q "Custom biomes are not supported"; then
fail "Custom biome: server warned that custom biomes are not supported"
elif ((!BIOME_OK)); then
fail "Custom biome: $CUSTOM_BIOME not found in stored LODs within 60 seconds" "minecraft:custom found ${BIOME_COUNTS[1]:-0} times"
elif ((${BIOME_COUNTS[1]:-0} > 0)); then
fail "Custom biome: stored LODs also contain minecraft:custom" "${BIOME_COUNTS[1]} times"
else
pass "Custom biome: $CUSTOM_BIOME found ${BIOME_COUNTS[0]} times in stored LODs"
fi
#=====================#
# 4. dimension switch #
#=====================#
echo "Switching to the nether..."
SERVER_MARK=$(line_count "$SERVER_OUT")
CLIENT_MARK=$(line_count "$CLIENT_OUT")
# Creative, so the player can't suffocate if they land inside netherrack.
console "gamemode creative $PLAYER_NAME"
console "execute in minecraft:the_nether run tp $PLAYER_NAME 0 100 0"
if wait_for "$CLIENT_OUT" "Level init received for \[minecraft:the_nether\]" 60 "$CLIENT_MARK"; then
NETHER_KEY="$(log_text "$CLIENT_OUT" "$CLIENT_MARK" | grep -oE "Level init received for \[minecraft:the_nether\]: .*level key \[[^]]*\]" | tail -n 1 | sed -E 's/.*level key \[([^]]*)\]/\1/')"
# Give the client time to send LOD requests for the new level.
sleep 20
if [[ "$NETHER_KEY" != "$NETHER_LEVEL_KEY" ]]; then
fail "Dimension switch: wrong nether level key" "Expected $NETHER_LEVEL_KEY, got $NETHER_KEY"
elif log_text "$CLIENT_OUT" "$CLIENT_MARK" | grep -q "invalid level key"; then
fail "Dimension switch: client rejected the level key"
elif check_server_errors "Dimension switch" "$SERVER_MARK" && check_client_errors "Dimension switch" "$CLIENT_MARK"; then
pass "Dimension switch: nether level key is $NETHER_KEY"
fi
else
fail "Dimension switch: client got no level init for the nether within 60 seconds"
fi
#=========#
# summary #
#=========#
if ((FAILURES > 0)); then
echo "$FAILURES check(s) failed."
exit 1
fi
echo "All checks passed."
+13
View File
@@ -0,0 +1,13 @@
# Pinned versions for the testbed. Bump these together.
MC_VERSION="26.3"
PAPER_BUILD="223"
PAPER_URL="https://fill-data.papermc.io/v1/objects/f8e684a4fef2a8b46d03aac049b49d59275e9ce5367dba7c6075cb5780f38470/paper-26.3-223.jar"
PAPER_SHA256="f8e684a4fef2a8b46d03aac049b49d59275e9ce5367dba7c6075cb5780f38470"
DH_VERSION="3.3.4-26.3"
DH_URL="https://cdn.modrinth.com/data/uCdwusMi/versions/TGgEbP9A/DistantHorizons-3.3.4-26.3-fabric-neoforge.jar"
DH_SHA512="0b90b1b08d99974bfc3c8e86b2ceb252e2f91715ed66b925004c28335fb44e1bddbbb6b8043e4d4d53c87ed247789cd1788b1ba4e3f4394efe154e0c59cff236"
PORTABLEMC_VERSION="5.0.6"