mirror of
https://gitlab.com/distant-horizons-team/distant-horizons-server-plugin.git
synced 2026-11-05 17:02:33 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2e71c1e9d | ||
|
|
f6b1958d56 | ||
|
|
1c6d533d8d | ||
|
|
04d8b99634 | ||
|
|
db881dbdb4 | ||
|
|
132b704e90 | ||
|
|
3ca19e814e | ||
|
|
40bd82176b | ||
|
|
25c73598bb | ||
|
|
04fc2e17f9 | ||
|
|
61ed6de27d | ||
|
|
56a0111057 | ||
|
|
61c8f6a465 | ||
|
|
443bc32613 | ||
|
|
17feb207f5 | ||
|
|
369e822607 | ||
|
|
0ad2d159b8 | ||
|
|
3cb9894e50 | ||
|
|
25c14d0893 | ||
|
|
9feff5efa5 | ||
|
|
bda0d067ee | ||
|
|
cf0fbf4854 | ||
|
|
8dbb9e773c | ||
|
|
744af7c1d3 | ||
|
|
b485b00e82 | ||
|
|
e5421ad0f6 | ||
|
|
0004539b41 | ||
|
|
9ef0f09dd2 | ||
|
|
79cb5aa302 | ||
|
|
5e9760c3d1 | ||
|
|
637636b5e8 | ||
|
|
0ffe620f92 | ||
|
|
2bcf9c7ca9 | ||
|
|
222221f9dc | ||
|
|
d086458546 | ||
|
|
12b1580535 | ||
|
|
81326b415d | ||
|
|
328684cf1d | ||
|
|
d89cfbfec5 | ||
|
|
413e2a6e70 | ||
|
|
ee385ea5af | ||
|
|
37db471c4f | ||
|
|
3e9165b7a3 | ||
|
|
da893ec1f0 | ||
|
|
320c04aeb7 | ||
|
|
fb8f98c52f | ||
|
|
99cee46ae5 | ||
|
|
a05ee01b8b | ||
|
|
b6878aaaaf | ||
|
|
0fbc612eef | ||
|
|
da2dba88b1 | ||
|
|
35822cb8a7 | ||
|
|
9625035edd | ||
|
|
ecda68771c | ||
|
|
f7822bc204 | ||
|
|
ca4a3a6bcc | ||
|
|
a189487623 | ||
|
|
721d4c2027 | ||
|
|
577a45be5c | ||
|
|
8bc102a26f | ||
|
|
8aa9fa9c31 | ||
|
|
3644051706 | ||
|
|
2786f4b8c7 | ||
|
|
930483ee0c | ||
|
|
64b24aba31 | ||
|
|
22447f5bd5 | ||
|
|
697ca18299 | ||
|
|
bef45fe732 | ||
|
|
f60736f21e | ||
|
|
a36102c16e | ||
|
|
ed98a7e0d4 | ||
|
|
1b4138f537 | ||
|
|
a387c7730d | ||
|
|
dbb9d8a2cf | ||
|
|
3c76e8f229 | ||
|
|
967cd2e20d | ||
|
|
83c4a3f6a6 | ||
|
|
c6b89e8794 | ||
|
|
7725a8c0eb | ||
|
|
79cc6e3218 | ||
|
|
15542e3a36 | ||
|
|
275b495675 | ||
|
|
dcf08ba286 | ||
|
|
6532418b5b | ||
|
|
16a68d802f | ||
|
|
f2a40d405d | ||
|
|
c95f54cc0b | ||
|
|
cdeb8f67c9 | ||
|
|
33f374fc41 | ||
|
|
32f78afb39 | ||
|
|
53051b71e3 | ||
|
|
651fd09dde | ||
|
|
a5709d96a4 | ||
|
|
6e11678a80 | ||
|
|
30ea7d5438 | ||
|
|
6c2b77fe25 | ||
|
|
24e2727bf9 | ||
|
|
54ddb357e3 | ||
|
|
7d341959ae | ||
|
|
ca53f19c87 | ||
|
|
4f2a782254 | ||
|
|
e5d4ab61a0 | ||
|
|
375146229e | ||
|
|
95839b85ce | ||
|
|
0243f9bccb | ||
|
|
edfd1df1b9 | ||
|
|
f068a2fa2b | ||
|
|
ffcfded127 | ||
|
|
4cb0d86217 | ||
|
|
5ae9ae65f8 | ||
|
|
c1e0fd8de8 | ||
|
|
6e54fed39e | ||
|
|
a2239bba85 | ||
|
|
f962a0253e | ||
|
|
ac0f4503de | ||
|
|
27680c67ad | ||
|
|
0dacd96ab4 | ||
|
|
50cbb129f8 | ||
|
|
dc47a2a5bb | ||
|
|
0c1db8ea56 | ||
|
|
2215709472 | ||
|
|
659326d0d1 | ||
|
|
2a397afb16 | ||
|
|
0d96af636f | ||
|
|
14c2f69df5 | ||
|
|
4dd995f939 | ||
|
|
f5d2fe1b1d | ||
|
|
70c5b07ed2 | ||
|
|
f7d771edb6 | ||
|
|
48e3f4b7a2 | ||
|
|
fb8f10914d | ||
|
|
10450a8455 | ||
|
|
ade3eeba1b | ||
|
|
040f4f034a | ||
|
|
e183d8fd28 | ||
|
|
9a90ee4378 | ||
|
|
27a9549888 | ||
|
|
c6cfc419b8 | ||
|
|
4eed922a33 | ||
|
|
60062a20f3 | ||
|
|
baf8c526d8 | ||
|
|
78e7486e33 | ||
|
|
c4b5f6c663 | ||
|
|
a5dfdf2102 | ||
|
|
23e0708dbb | ||
|
|
2dac58ad27 | ||
|
|
4d62b2edb8 | ||
|
|
03516559ea | ||
|
|
23062a7662 | ||
|
|
bc94dcff2c | ||
|
|
0da10a0e10 | ||
|
|
a2c33442d5 | ||
|
|
3f18fc17da | ||
|
|
11904d3b30 | ||
|
|
d736b7a5f0 | ||
|
|
b685ad5761 | ||
|
|
31fcb0f6ea | ||
|
|
faa75beb24 | ||
|
|
3596d3879f | ||
|
|
757212a463 | ||
|
|
4b3f28ec9f | ||
|
|
3ef63a184a | ||
|
|
ab5aab6b72 | ||
|
|
ee8b0047e6 | ||
|
|
4dbc8f8a85 | ||
|
|
c97200db03 | ||
|
|
32179bc793 | ||
|
|
29c5fe311d | ||
|
|
3c6f60e8a6 | ||
|
|
e93e633c81 | ||
|
|
29f7193919 | ||
|
|
db41ee0807 | ||
|
|
a694885028 | ||
|
|
c147e07168 | ||
|
|
e54ee9a1b1 | ||
|
|
e48a005c85 | ||
|
|
df3c6f5c78 | ||
|
|
10a64469ef | ||
|
|
0749da55d9 | ||
|
|
dadeb77067 | ||
|
|
2f7d159b19 | ||
|
|
5be954e3fb | ||
|
|
048762b63d | ||
|
|
5fe761d6bc | ||
|
|
c194bf35c9 | ||
|
|
9b06cc0b49 | ||
|
|
0a53d59037 | ||
|
|
3f4ff533ee | ||
|
|
8762ebbafe | ||
|
|
36d884b583 | ||
|
|
2ac627ebce | ||
|
|
3c450482dd | ||
|
|
cc0718cf69 | ||
|
|
1808fd0a92 | ||
|
|
38ea041550 | ||
|
|
6060900428 | ||
|
|
e999cf7ab2 | ||
|
|
4f9e87d262 | ||
|
|
aac5e2540b | ||
|
|
a31fcfd8e4 | ||
|
|
b94fa4b344 | ||
|
|
f962effb40 | ||
|
|
91e2d6445e | ||
|
|
8faf75ff4a | ||
|
|
a58222b4f7 | ||
|
|
6e18444164 | ||
|
|
c9c0277436 | ||
|
|
0443a6a400 | ||
|
|
f55d644144 | ||
|
|
6c0b6ac300 | ||
|
|
8a560dfbc9 | ||
|
|
2d7280440e | ||
|
|
1069b4f623 | ||
|
|
bc15977d11 | ||
|
|
1058dab9b1 | ||
|
|
c72df317c2 | ||
|
|
6d60f906a4 | ||
|
|
6fb8bfc033 | ||
|
|
82333d0e48 | ||
|
|
a5a23ba073 | ||
|
|
4e7ed414ae | ||
|
|
aed02ef99b | ||
|
|
65a66b9a97 | ||
|
|
64709347f9 | ||
|
|
153d072f39 | ||
|
|
cbff7f910b | ||
|
|
307d8795d9 | ||
|
|
41a244013b | ||
|
|
ded98f0237 | ||
|
|
975ac821e2 | ||
|
|
fcbebccd87 | ||
|
|
e26d5812a9 | ||
|
|
fa78e99f64 | ||
|
|
79aabd8b35 | ||
|
|
cdda626cfd | ||
|
|
a41040230c | ||
|
|
7624473148 | ||
|
|
10d73adf59 | ||
|
|
e626d7a6a0 | ||
|
|
61303a7ba1 | ||
|
|
e0da9a0548 | ||
|
|
2bb441d147 | ||
|
|
5ac7913b1e | ||
|
|
d86e281e84 | ||
|
|
495b891b30 | ||
|
|
01857854c8 | ||
|
|
7a8087cb1d | ||
|
|
91d7c849f6 | ||
|
|
2b91ddf437 | ||
|
|
a900b2b628 | ||
|
|
ecf310a8dc | ||
|
|
d1ae74946e | ||
|
|
aca296dbc6 | ||
|
|
d838317503 | ||
|
|
f4c20138aa | ||
|
|
23e0a23406 | ||
|
|
ed371875da | ||
|
|
aee19d166f | ||
|
|
b768f2cd74 | ||
|
|
31b3980509 |
No files matched your search
+2
-1
@@ -1 +1,2 @@
|
||||
* text=lf
|
||||
* text=auto eol=lf
|
||||
*.bat text eol=crlf
|
||||
+10
-4
@@ -27,11 +27,17 @@ replay_pid*
|
||||
/.idea
|
||||
/*.iml
|
||||
|
||||
# Maven output
|
||||
/dependency-reduced-pom.xml
|
||||
/target
|
||||
/jars
|
||||
# Visual Studio Code project config
|
||||
/.vscode
|
||||
|
||||
# Gradle output
|
||||
/.gradle
|
||||
/build
|
||||
|
||||
# Test server
|
||||
/server
|
||||
/folia
|
||||
|
||||
# Testbed downloads and runtime files
|
||||
/testbed/.cache
|
||||
/testbed/run
|
||||
+6
-26
@@ -1,49 +1,29 @@
|
||||
include:
|
||||
- template: Security/SAST.gitlab-ci.yml
|
||||
|
||||
variables:
|
||||
MAVEN_OPTS: >-
|
||||
-Dhttps.protocols=TLSv1.2
|
||||
-Dmaven.repo.local=.m2/repository
|
||||
-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN
|
||||
-Dorg.slf4j.simpleLogger.showDateTime=true
|
||||
-Djava.awt.headless=true
|
||||
|
||||
MAVEN_CLI_OPTS: >-
|
||||
--batch-mode
|
||||
--errors
|
||||
--fail-at-end
|
||||
--show-version
|
||||
-DinstallAtEnd=true
|
||||
-DdeployAtEnd=true
|
||||
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
|
||||
default:
|
||||
image: maven:3.9-eclipse-temurin-21-alpine
|
||||
image: eclipse-temurin:21
|
||||
|
||||
cache:
|
||||
key: maven-repo-${CI_COMMIT_REF_SLUG}
|
||||
key: gradle-${CI_COMMIT_REF_SLUG}
|
||||
unprotect: true
|
||||
|
||||
paths:
|
||||
- .m2/repository
|
||||
- .gradle
|
||||
|
||||
build:
|
||||
stage: build
|
||||
|
||||
parallel:
|
||||
matrix:
|
||||
- MC_VER: [ "1.16.5", "1.17.1", "1.18.2", "1.19.2", "1.19.4", "1.20.1", "1.20.2", "1.20.4", "1.20.6", "1.21.1", "1.21.3", "1.21.4", ]
|
||||
|
||||
script:
|
||||
- mvn package --projects pom-${MC_VER}.xml ${MAVEN_CLI_OPTS}
|
||||
- mv jars/*.jar ./
|
||||
- ./gradlew build
|
||||
- mv build/libs/*/*.jar ./
|
||||
|
||||
artifacts:
|
||||
name: DHS-${MC_VER}-${CI_COMMIT_REF_SLUG}-${CI_JOB_STARTED_AT}
|
||||
name: DistantHorizonsSupport-${CI_COMMIT_REF_SLUG}-${CI_JOB_STARTED_AT}
|
||||
|
||||
paths:
|
||||
- "./*.jar"
|
||||
@@ -4,17 +4,15 @@
|
||||
|
||||
DH Support is a Bukkit/Spigot/Paper/Folia server plugin that transmits Level Of Detail (LOD) data to players with Distant Horizons installed. Distant Horizons _will_ work fine without this plugin, but then each client will have to be within normal view distance of chunks to load them, and they will not receive updates for distant chunks when they change.
|
||||
|
||||
## Installation
|
||||
## Installing and configuring
|
||||
|
||||
Download the [latest release](https://gitlab.com/distant-horizons-team/distant-horizons-server-plugin/-/releases) and drop the JAR in your plugins folder, and you're done!
|
||||
|
||||
## Configuration
|
||||
|
||||
The default values should be pretty solid, but you may tweak them to better suit your specific needs. Everything you need to know should be in config.yml.
|
||||
Please see [our wiki](https://gitlab.com/distant-horizons-team/distant-horizons-server-plugin/-/wikis/home) for details.
|
||||
|
||||
## Building
|
||||
|
||||
The project uses Maven, so just run `mvn` in the project directory to compile and package a new JAR.
|
||||
This project uses Gradle.
|
||||
|
||||
`gradlew clean build`
|
||||
|
||||
## Contribution
|
||||
|
||||
|
||||
+107
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.gradle.parallel=true
|
||||
org.gradle.configureondemand=true
|
||||
@@ -0,0 +1,2 @@
|
||||
#This file is generated by updateDaemonJvm
|
||||
toolchainVersion=21
|
||||
Vendored
BIN
Binary file not shown.
+9
@@ -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
|
||||
@@ -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
@@ -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%
|
||||
@@ -1,28 +0,0 @@
|
||||
<project>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>DHSupport-${mc.api-version}${mc.api-version-patch}</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>no.jckf</groupId>
|
||||
<artifactId>DHSupport</artifactId>
|
||||
<version>${revision}</version>
|
||||
<relativePath>./pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<skip-copy>false</skip-copy>
|
||||
<mc.api-version>1.16</mc.api-version>
|
||||
<mc.api-version-patch>.5</mc.api-version-patch>
|
||||
<dhs.read-forge-byte>true</dhs.read-forge-byte>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.spigotmc</groupId>
|
||||
<artifactId>spigot-api</artifactId>
|
||||
<version>${mc.api-version}${mc.api-version-patch}-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,28 +0,0 @@
|
||||
<project>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>DHSupport-${mc.api-version}${mc.api-version-patch}</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>no.jckf</groupId>
|
||||
<artifactId>DHSupport</artifactId>
|
||||
<version>${revision}</version>
|
||||
<relativePath>./pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<skip-copy>false</skip-copy>
|
||||
<mc.api-version>1.17</mc.api-version>
|
||||
<mc.api-version-patch>.1</mc.api-version-patch>
|
||||
<dhs.read-forge-byte>true</dhs.read-forge-byte>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>${mc.api-version}${mc.api-version-patch}-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,28 +0,0 @@
|
||||
<project>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>DHSupport-${mc.api-version}${mc.api-version-patch}</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>no.jckf</groupId>
|
||||
<artifactId>DHSupport</artifactId>
|
||||
<version>${revision}</version>
|
||||
<relativePath>./pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<skip-copy>false</skip-copy>
|
||||
<mc.api-version>1.18</mc.api-version>
|
||||
<mc.api-version-patch>.2</mc.api-version-patch>
|
||||
<dhs.read-forge-byte>true</dhs.read-forge-byte>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>${mc.api-version}${mc.api-version-patch}-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,28 +0,0 @@
|
||||
<project>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>DHSupport-${mc.api-version}${mc.api-version-patch}</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>no.jckf</groupId>
|
||||
<artifactId>DHSupport</artifactId>
|
||||
<version>${revision}</version>
|
||||
<relativePath>./pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<skip-copy>false</skip-copy>
|
||||
<mc.api-version>1.19</mc.api-version>
|
||||
<mc.api-version-patch>.2</mc.api-version-patch>
|
||||
<dhs.read-forge-byte>true</dhs.read-forge-byte>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>${mc.api-version}${mc.api-version-patch}-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,28 +0,0 @@
|
||||
<project>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>DHSupport-${mc.api-version}${mc.api-version-patch}</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>no.jckf</groupId>
|
||||
<artifactId>DHSupport</artifactId>
|
||||
<version>${revision}</version>
|
||||
<relativePath>./pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<skip-copy>false</skip-copy>
|
||||
<mc.api-version>1.19</mc.api-version>
|
||||
<mc.api-version-patch>.4</mc.api-version-patch>
|
||||
<dhs.read-forge-byte>true</dhs.read-forge-byte>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>${mc.api-version}${mc.api-version-patch}-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,28 +0,0 @@
|
||||
<project>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>DHSupport-${mc.api-version}${mc.api-version-patch}</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>no.jckf</groupId>
|
||||
<artifactId>DHSupport</artifactId>
|
||||
<version>${revision}</version>
|
||||
<relativePath>./pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<skip-copy>false</skip-copy>
|
||||
<mc.api-version>1.20</mc.api-version>
|
||||
<mc.api-version-patch>.1</mc.api-version-patch>
|
||||
<dhs.read-forge-byte>true</dhs.read-forge-byte>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>${mc.api-version}${mc.api-version-patch}-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,28 +0,0 @@
|
||||
<project>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>DHSupport-${mc.api-version}${mc.api-version-patch}</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>no.jckf</groupId>
|
||||
<artifactId>DHSupport</artifactId>
|
||||
<version>${revision}</version>
|
||||
<relativePath>./pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<skip-copy>false</skip-copy>
|
||||
<mc.api-version>1.20</mc.api-version>
|
||||
<mc.api-version-patch>.2</mc.api-version-patch>
|
||||
<dhs.read-forge-byte>true</dhs.read-forge-byte>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>${mc.api-version}${mc.api-version-patch}-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,28 +0,0 @@
|
||||
<project>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>DHSupport-${mc.api-version}${mc.api-version-patch}</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>no.jckf</groupId>
|
||||
<artifactId>DHSupport</artifactId>
|
||||
<version>${revision}</version>
|
||||
<relativePath>./pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<skip-copy>false</skip-copy>
|
||||
<mc.api-version>1.20</mc.api-version>
|
||||
<mc.api-version-patch>.4</mc.api-version-patch>
|
||||
<dhs.read-forge-byte>true</dhs.read-forge-byte>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>${mc.api-version}${mc.api-version-patch}-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,26 +0,0 @@
|
||||
<project>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>DHSupport-${mc.api-version}</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>no.jckf</groupId>
|
||||
<artifactId>DHSupport</artifactId>
|
||||
<version>${revision}</version>
|
||||
<relativePath>./pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<skip-copy>false</skip-copy>
|
||||
<mc.api-version>1.20.6</mc.api-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>${mc.api-version}-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,26 +0,0 @@
|
||||
<project>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>DHSupport-${mc.api-version}</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>no.jckf</groupId>
|
||||
<artifactId>DHSupport</artifactId>
|
||||
<version>${revision}</version>
|
||||
<relativePath>./pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<skip-copy>false</skip-copy>
|
||||
<mc.api-version>1.21.1</mc.api-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>${mc.api-version}-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,26 +0,0 @@
|
||||
<project>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>DHSupport-${mc.api-version}</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>no.jckf</groupId>
|
||||
<artifactId>DHSupport</artifactId>
|
||||
<version>${revision}</version>
|
||||
<relativePath>./pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<skip-copy>false</skip-copy>
|
||||
<mc.api-version>1.21.3</mc.api-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>${mc.api-version}-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,26 +0,0 @@
|
||||
<project>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>DHSupport-${mc.api-version}</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>no.jckf</groupId>
|
||||
<artifactId>DHSupport</artifactId>
|
||||
<version>${revision}</version>
|
||||
<relativePath>./pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<skip-copy>false</skip-copy>
|
||||
<mc.api-version>1.21.4</mc.api-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>${mc.api-version}-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,236 +0,0 @@
|
||||
<project>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>no.jckf</groupId>
|
||||
<artifactId>DHSupport</artifactId>
|
||||
<version>${revision}</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<properties>
|
||||
<revision>0.8.0</revision>
|
||||
<jdk>21</jdk>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<main.class>no.jckf.dhsupport.bukkit.DhSupportBukkitPlugin</main.class>
|
||||
<mc.api-version></mc.api-version>
|
||||
<mc.api-version-patch></mc.api-version-patch>
|
||||
<dhs.read-forge-byte></dhs.read-forge-byte>
|
||||
<skip-copy>true</skip-copy>
|
||||
</properties>
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<id>jckf</id>
|
||||
<name>Jim C K Flaten</name>
|
||||
<email>jckf@jckf.no</email>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>central</id>
|
||||
<url>https://repo1.maven.org/maven2/</url>
|
||||
</repository>
|
||||
|
||||
<repository>
|
||||
<id>spigot-repo</id>
|
||||
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots</url>
|
||||
</repository>
|
||||
|
||||
<repository>
|
||||
<id>papermc</id>
|
||||
<url>https://repo.papermc.io/repository/maven-public</url>
|
||||
</repository>
|
||||
|
||||
<repository>
|
||||
<id>jitpack</id>
|
||||
<url>https://jitpack.io</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.bstats</groupId>
|
||||
<artifactId>bstats-bukkit</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>33.3.1-jre</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
<version>25.0.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.tukaani</groupId>
|
||||
<artifactId>xz</artifactId>
|
||||
<version>1.10</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.technicallycoded</groupId>
|
||||
<artifactId>FoliaLib</artifactId>
|
||||
<version>v0.4.3</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<defaultGoal>clean package</defaultGoal>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
|
||||
<configuration>
|
||||
<source>${jdk}</source>
|
||||
<target>${jdk}</target>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
|
||||
<compilerArgs>
|
||||
<arg>-Xplugin:Manifold</arg>
|
||||
<arg>-AREAD_FORGE_BYTE=${dhs.read-forge-byte}</arg>
|
||||
</compilerArgs>
|
||||
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>systems.manifold</groupId>
|
||||
<artifactId>manifold-preprocessor</artifactId>
|
||||
<version>2024.1.34</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
|
||||
<configuration>
|
||||
<shadedArtifactAttached>false</shadedArtifactAttached>
|
||||
<shadedClassifierName>all</shadedClassifierName>
|
||||
|
||||
<relocations>
|
||||
<relocation>
|
||||
<pattern>org.bstats</pattern>
|
||||
<shadedPattern>no.jckf.dhsupport.bstats</shadedPattern>
|
||||
</relocation>
|
||||
|
||||
<relocation>
|
||||
<pattern>com.tcoded.folialib</pattern>
|
||||
<shadedPattern>no.jckf.dhsupport.folialib</shadedPattern>
|
||||
</relocation>
|
||||
</relocations>
|
||||
</configuration>
|
||||
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
|
||||
<configuration>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
<artifactSet>
|
||||
<includes>
|
||||
<include>org.tukaani</include>
|
||||
<include>org.bstats</include>
|
||||
<include>com.github.technicallycoded</include>
|
||||
</includes>
|
||||
</artifactSet>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<version>3.8.0</version>
|
||||
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
|
||||
<goals>
|
||||
<goal>copy</goal>
|
||||
</goals>
|
||||
|
||||
<configuration>
|
||||
<outputDirectory>jars</outputDirectory>
|
||||
|
||||
<skip>${skip-copy}</skip>
|
||||
|
||||
<artifactItems>
|
||||
<artifactItem>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>${project.artifactId}</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>${project.packaging}</type>
|
||||
<destFileName>DHS-${project.version}_for_MC-${mc.api-version}${mc.api-version-patch}.jar</destFileName>
|
||||
</artifactItem>
|
||||
</artifactItems>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>${basedir}/src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
<targetPath>.</targetPath>
|
||||
|
||||
<includes>
|
||||
<include>plugin.yml</include>
|
||||
</includes>
|
||||
</resource>
|
||||
|
||||
<resource>
|
||||
<directory>${basedir}/src/main/resources</directory>
|
||||
<targetPath>.</targetPath>
|
||||
|
||||
<includes>
|
||||
<include>config.yml</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
</build>
|
||||
|
||||
<modules>
|
||||
<module>pom-1.16.5.xml</module>
|
||||
<module>pom-1.17.1.xml</module>
|
||||
<module>pom-1.18.2.xml</module>
|
||||
<module>pom-1.19.2.xml</module>
|
||||
<module>pom-1.19.4.xml</module>
|
||||
<module>pom-1.20.1.xml</module>
|
||||
<module>pom-1.20.2.xml</module>
|
||||
<module>pom-1.20.4.xml</module>
|
||||
<module>pom-1.20.6.xml</module>
|
||||
<module>pom-1.21.1.xml</module>
|
||||
<module>pom-1.21.3.xml</module>
|
||||
<module>pom-1.21.4.xml</module>
|
||||
</modules>
|
||||
</project>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,9 +25,7 @@ import no.jckf.dhsupport.core.scheduling.Scheduler;
|
||||
import org.bukkit.Location;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class BukkitScheduler implements Scheduler
|
||||
@@ -38,13 +36,34 @@ public class BukkitScheduler implements Scheduler
|
||||
|
||||
protected ExecutorService executor;
|
||||
|
||||
protected ScheduledExecutorService scheduledExecutor;
|
||||
|
||||
public BukkitScheduler(DhSupportBukkitPlugin plugin)
|
||||
{
|
||||
this.plugin = plugin;
|
||||
|
||||
this.foliaLib = new FoliaLib(this.plugin);
|
||||
|
||||
this.executor = Executors.newFixedThreadPool(this.plugin.getDhSupport().getConfig().getInt(DhsConfig.SCHEDULER_THREADS));
|
||||
int threadCount = this.plugin.getDhSupport().getConfig().getInt(DhsConfig.SCHEDULER_THREADS);
|
||||
|
||||
this.executor = new ThreadPoolExecutor(
|
||||
threadCount, threadCount,
|
||||
60, TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(),
|
||||
new ThreadFactory() {
|
||||
private final java.util.concurrent.atomic.AtomicInteger threadNumber = new java.util.concurrent.atomic.AtomicInteger(1);
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable r)
|
||||
{
|
||||
return new Thread(r, "DHSupport-Worker-" + threadNumber.getAndIncrement());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
((ThreadPoolExecutor) this.executor).allowCoreThreadTimeOut(true);
|
||||
|
||||
this.scheduledExecutor = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "DHSupport-Scheduler"));
|
||||
|
||||
this.plugin.getDhSupport().info("Using " + Utils.ucFirst(this.foliaLib.getImplType().name().toLowerCase().replace('_', ' ')) + " scheduler.");
|
||||
}
|
||||
@@ -52,7 +71,7 @@ public class BukkitScheduler implements Scheduler
|
||||
@Override
|
||||
public boolean canReadWorldAsync()
|
||||
{
|
||||
return !this.foliaLib.isFolia();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -60,13 +79,49 @@ public class BukkitScheduler implements Scheduler
|
||||
{
|
||||
CompletableFuture<U> future = new CompletableFuture<>();
|
||||
|
||||
this.foliaLib.getScheduler().runNextTick((task) -> {
|
||||
if (this.foliaLib.getScheduler().isGlobalTickThread()) {
|
||||
future.complete(supplier.get());
|
||||
} else {
|
||||
this.foliaLib.getScheduler().runNextTick((task) -> {
|
||||
try {
|
||||
future.complete(supplier.get());
|
||||
} catch (Exception exception) {
|
||||
future.completeExceptionally(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <U> CompletableFuture<U> runOnMainThreadDelayed(Supplier<U> supplier, long delayTicks)
|
||||
{
|
||||
CompletableFuture<U> future = new CompletableFuture<>();
|
||||
|
||||
this.foliaLib.getScheduler().runLater((task) -> {
|
||||
try {
|
||||
future.complete(supplier.get());
|
||||
} catch (Exception exception) {
|
||||
future.completeExceptionally(exception);
|
||||
}
|
||||
});
|
||||
}, delayTicks);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <U> CompletableFuture<U> runOnMainThreadDelayed(Supplier<U> supplier, long delayAmount, TimeUnit delayUnit)
|
||||
{
|
||||
CompletableFuture<U> future = new CompletableFuture<>();
|
||||
|
||||
this.foliaLib.getScheduler().runLater((task) -> {
|
||||
try {
|
||||
future.complete(supplier.get());
|
||||
} catch (Exception exception) {
|
||||
future.completeExceptionally(exception);
|
||||
}
|
||||
}, delayAmount, delayUnit);
|
||||
|
||||
return future;
|
||||
}
|
||||
@@ -83,17 +138,67 @@ public class BukkitScheduler implements Scheduler
|
||||
z
|
||||
);
|
||||
|
||||
this.foliaLib.getScheduler().runAtLocation(
|
||||
location,
|
||||
(task) -> {
|
||||
try {
|
||||
future.complete(supplier.get());
|
||||
} catch (Exception exception) {
|
||||
future.completeExceptionally(exception);
|
||||
}
|
||||
}
|
||||
if (this.foliaLib.getScheduler().isOwnedByCurrentRegion(location)) {
|
||||
future.complete(supplier.get());
|
||||
} else {
|
||||
this.foliaLib.getScheduler().runAtLocation(
|
||||
location,
|
||||
(task) -> {
|
||||
try {
|
||||
future.complete(supplier.get());
|
||||
} catch (Exception exception) {
|
||||
future.completeExceptionally(exception);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <U> CompletableFuture<U> runOnRegionThreadDelayed(UUID worldId, int x, int z, Supplier<U> supplier, long delayTicks)
|
||||
{
|
||||
CompletableFuture<U> future = new CompletableFuture<>();
|
||||
|
||||
Location location = new Location(
|
||||
this.plugin.getServer().getWorld(worldId),
|
||||
x,
|
||||
0,
|
||||
z
|
||||
);
|
||||
|
||||
this.foliaLib.getScheduler().runAtLocationLater(location, (task) -> {
|
||||
try {
|
||||
future.complete(supplier.get());
|
||||
} catch (Exception exception) {
|
||||
future.completeExceptionally(exception);
|
||||
}
|
||||
}, delayTicks);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <U> CompletableFuture<U> runOnRegionThreadDelayed(UUID worldId, int x, int z, Supplier<U> supplier, long delayAmount, TimeUnit delayUnit)
|
||||
{
|
||||
CompletableFuture<U> future = new CompletableFuture<>();
|
||||
|
||||
Location location = new Location(
|
||||
this.plugin.getServer().getWorld(worldId),
|
||||
x,
|
||||
0,
|
||||
z
|
||||
);
|
||||
|
||||
this.foliaLib.getScheduler().runAtLocationLater(location, (task) -> {
|
||||
try {
|
||||
future.complete(supplier.get());
|
||||
} catch (Exception exception) {
|
||||
future.completeExceptionally(exception);
|
||||
}
|
||||
}, delayAmount, delayUnit);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
@@ -113,13 +218,46 @@ public class BukkitScheduler implements Scheduler
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <U> CompletableFuture<U> runOnSeparateThreadDelayed(Supplier<U> supplier, long delayTicks)
|
||||
{
|
||||
return this.runOnSeparateThreadDelayed(supplier, delayTicks * 50L, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <U> CompletableFuture<U> runOnSeparateThreadDelayed(Supplier<U> supplier, long delayAmount, TimeUnit delayUnit)
|
||||
{
|
||||
CompletableFuture<U> future = new CompletableFuture<>();
|
||||
|
||||
this.scheduledExecutor.schedule(() -> {
|
||||
this.runOnSeparateThread(() -> {
|
||||
try {
|
||||
future.complete(supplier.get());
|
||||
} catch (Exception exception) {
|
||||
future.completeExceptionally(exception);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}, delayAmount, delayUnit);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelTasks()
|
||||
{
|
||||
this.executor.shutdown();
|
||||
this.scheduledExecutor.shutdownNow();
|
||||
this.executor.shutdownNow();
|
||||
this.foliaLib.getScheduler().cancelAllTasks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExecutorService getExecutor()
|
||||
{
|
||||
return this.executor;
|
||||
}
|
||||
|
||||
public void runTimer(Runnable runnable, long initialDelay, long interval)
|
||||
{
|
||||
this.foliaLib.getScheduler().runTimer(runnable, initialDelay, interval);
|
||||
|
||||
@@ -19,23 +19,26 @@
|
||||
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.WorldConfiguration;
|
||||
import no.jckf.dhsupport.core.dataobject.Beacon;
|
||||
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.*;
|
||||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.awt.Color;
|
||||
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;
|
||||
|
||||
@@ -45,28 +48,58 @@ public class BukkitWorldInterface implements WorldInterface
|
||||
|
||||
protected World world;
|
||||
|
||||
protected Configuration config;
|
||||
|
||||
protected WorldConfiguration worldConfig;
|
||||
|
||||
protected Logger logger;
|
||||
|
||||
protected Map<String, ChunkSnapshot> chunks = new HashMap<>();
|
||||
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.config = config;
|
||||
this.worldConfig = new WorldConfiguration(this, config);
|
||||
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)
|
||||
@@ -83,22 +116,33 @@ public class BukkitWorldInterface implements WorldInterface
|
||||
{
|
||||
this.unsafeValues = Bukkit.getUnsafe();
|
||||
|
||||
// Detect if we're running under Paper (or a Paper fork) that has this patch:
|
||||
// https://github.com/PaperMC/Paper/commit/5bf259115c1ce29dd96df5fcf53739c94d39f902
|
||||
// Does not exist for 1.16.5.
|
||||
try {
|
||||
Class<?> regionAccessor = Class.forName("org.bukkit.RegionAccessor");
|
||||
this.getCoordinateScale = this.world.getClass().getMethod("getCoordinateScale");
|
||||
} catch (NoSuchMethodException e) {
|
||||
|
||||
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;
|
||||
// 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("getChunkAtAsyncUrgently", int.class, int.class);
|
||||
this.getChunkAtAsync = this.world.getClass().getMethod("getChunkAtAsync", int.class, int.class, boolean.class);
|
||||
} catch (NoSuchMethodException exception) {
|
||||
if (!ASYNC_LOAD_WARNING_SENT) {
|
||||
this.getLogger().warning("Async chunk loading is not supported on this server. Performance will suffer.");
|
||||
@@ -111,7 +155,7 @@ public class BukkitWorldInterface implements WorldInterface
|
||||
@Override
|
||||
public WorldInterface newInstance()
|
||||
{
|
||||
BukkitWorldInterface newInstance = new BukkitWorldInterface(this.plugin, this.world, this.config);
|
||||
BukkitWorldInterface newInstance = new BukkitWorldInterface(this.plugin, this.world, this.config, this.blockStateStrings, this.blockProperties);
|
||||
|
||||
newInstance.setLogger(this.getLogger());
|
||||
|
||||
@@ -121,23 +165,58 @@ public class BukkitWorldInterface implements WorldInterface
|
||||
}
|
||||
|
||||
protected ChunkSnapshot getChunk(int x, int 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);
|
||||
|
||||
String key = chunkX + "x" + chunkZ;
|
||||
int sectionX = Coordinates.chunkToSection(chunkX);
|
||||
int sectionZ = Coordinates.chunkToSection(chunkZ);
|
||||
|
||||
if (this.chunks.containsKey(key)) {
|
||||
return this.chunks.get(key);
|
||||
if (this.sectionX == null || this.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");
|
||||
}
|
||||
|
||||
ChunkSnapshot chunk = this.world.getChunkAt(chunkX, chunkZ).getChunkSnapshot(true, true, false);
|
||||
int baseChunkX = Coordinates.sectionToChunk(sectionX);
|
||||
int baseChunkZ = Coordinates.sectionToChunk(sectionZ);
|
||||
|
||||
this.chunks.put(key, chunk);
|
||||
int index = (chunkX - baseChunkX) + ((chunkZ - baseChunkZ) << 2);
|
||||
|
||||
ChunkSnapshot chunk = this.chunks[index];
|
||||
|
||||
if (chunk != null) {
|
||||
return chunk;
|
||||
}
|
||||
|
||||
Scheduler scheduler = this.plugin.getDhSupport().getScheduler();
|
||||
|
||||
if (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()
|
||||
{
|
||||
@@ -145,14 +224,113 @@ public class BukkitWorldInterface implements WorldInterface
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName()
|
||||
public double getCoordinateScale()
|
||||
{
|
||||
return this.world.getName();
|
||||
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);
|
||||
|
||||
@@ -183,6 +361,10 @@ public class BukkitWorldInterface implements WorldInterface
|
||||
@Override
|
||||
public boolean isChunkLoaded(int x, int z)
|
||||
{
|
||||
if (this.isDummyMode()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int chunkX = Coordinates.blockToChunk(x);
|
||||
int chunkZ = Coordinates.blockToChunk(z);
|
||||
|
||||
@@ -192,10 +374,37 @@ public class BukkitWorldInterface implements WorldInterface
|
||||
@Override
|
||||
public boolean loadChunk(int x, int z)
|
||||
{
|
||||
if (this.isDummyMode()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int chunkX = Coordinates.blockToChunk(x);
|
||||
int chunkZ = Coordinates.blockToChunk(z);
|
||||
|
||||
this.world.loadChunk(chunkX, chunkZ);
|
||||
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;
|
||||
}
|
||||
@@ -203,6 +412,10 @@ public class BukkitWorldInterface implements WorldInterface
|
||||
@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));
|
||||
}
|
||||
@@ -211,9 +424,46 @@ public class BukkitWorldInterface implements WorldInterface
|
||||
int chunkZ = Coordinates.blockToChunk(z);
|
||||
|
||||
try {
|
||||
CompletableFuture<Chunk> chunkFuture = (CompletableFuture<Chunk>) this.getChunkAtAsync.invoke(this.world, chunkX, chunkZ);
|
||||
return ((CompletableFuture<Chunk>) this.getChunkAtAsync.invoke(this.world, chunkX, chunkZ, false))
|
||||
.thenApply((chunk) -> {
|
||||
if (chunk == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return chunkFuture.thenApply((chunk) -> true);
|
||||
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);
|
||||
}
|
||||
@@ -222,6 +472,10 @@ public class BukkitWorldInterface implements WorldInterface
|
||||
@Override
|
||||
public boolean unloadChunk(int x, int z)
|
||||
{
|
||||
if (this.isDummyMode()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int chunkX = Coordinates.blockToChunk(x);
|
||||
int chunkZ = Coordinates.blockToChunk(z);
|
||||
|
||||
@@ -231,6 +485,10 @@ public class BukkitWorldInterface implements WorldInterface
|
||||
@Override
|
||||
public boolean unloadChunkAsync(int x, int z)
|
||||
{
|
||||
if (this.isDummyMode()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int chunkX = Coordinates.blockToChunk(x);
|
||||
int chunkZ = Coordinates.blockToChunk(z);
|
||||
|
||||
@@ -240,6 +498,10 @@ public class BukkitWorldInterface implements WorldInterface
|
||||
@Override
|
||||
public boolean discardChunk(int x, int z)
|
||||
{
|
||||
if (this.isDummyMode()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int chunkX = Coordinates.blockToChunk(x);
|
||||
int chunkZ = Coordinates.blockToChunk(z);
|
||||
|
||||
@@ -261,24 +523,62 @@ public class BukkitWorldInterface implements WorldInterface
|
||||
@Override
|
||||
public int getSeaLevel()
|
||||
{
|
||||
return Math.min(Math.max(this.getMinY(), this.world.getSeaLevel()), this.getMaxY());
|
||||
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.getChunk(x, z).getHighestBlockYAt(Coordinates.blockToChunkRelative(x), Coordinates.blockToChunkRelative(z));
|
||||
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)
|
||||
{
|
||||
NamespacedKey key = this.getChunk(x, z).getBiome(Coordinates.blockToChunkRelative(x), this.getSeaLevel(), Coordinates.blockToChunkRelative(z)).getKey();
|
||||
if (this.isDummyMode()) {
|
||||
return "minecraft:plains";
|
||||
}
|
||||
|
||||
NamespacedKey key = this.getChunk(x, z).getBiome(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z)).getKey();
|
||||
|
||||
// If the server just reports "custom" and we have access to getBiomeKey, try to get the correct biome name.
|
||||
if (key.toString().equals("minecraft:custom") && this.getBiomeKey != null) {
|
||||
try {
|
||||
key = (NamespacedKey) this.getBiomeKey.invoke(this.unsafeValues, this.world, x, this.getSeaLevel(), z);
|
||||
key = (NamespacedKey) this.getBiomeKey.invoke(this.unsafeValues, this.world, x, y, z);
|
||||
} catch (IllegalAccessException | InvocationTargetException exception) {
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
@@ -290,79 +590,282 @@ public class BukkitWorldInterface implements WorldInterface
|
||||
@Override
|
||||
public String getMaterialAt(int x, int y, int z)
|
||||
{
|
||||
return this.getChunk(x, z).getBlockType(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z)).getKey().toString();
|
||||
if (this.isDummyMode()) {
|
||||
return Utils.namespaceKey(this.getDummyMaterialAt(y));
|
||||
}
|
||||
|
||||
return this.getMaterialRecordAt(x, y, z).toString();
|
||||
}
|
||||
|
||||
public MaterialRecord getMaterialRecordAt(int x, int y, int z)
|
||||
{
|
||||
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.getChunk(x, z).getBlockData(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z)).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 (byte) this.getChunk(x, z).getBlockEmittedLight(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z));
|
||||
if (this.isDummyMode()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (byte) this.getChunk(x, z).getBlockEmittedLight(Coordinates.blockToChunkRelative(x), Math.clamp(y + this.lightOffset, this.getMinY(), this.getMaxY() - 1), Coordinates.blockToChunkRelative(z));
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getSkyLightAt(int x, int y, int z)
|
||||
{
|
||||
return (byte) this.getChunk(x, z).getBlockSkyLight(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z));
|
||||
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 Collection<Beacon> getBeaconsInChunk(int x, int z)
|
||||
public boolean isBeacon(int x, int y, int z)
|
||||
{
|
||||
Collection<Beacon> beacons = new ArrayList<>();
|
||||
if (this.isDummyMode()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BlockState[] blocks = this.world.getChunkAt(Coordinates.blockToChunk(x), Coordinates.blockToChunk(z)).getTileEntities();
|
||||
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;
|
||||
}
|
||||
|
||||
for (BlockState block : blocks) {
|
||||
if (!block.getType().equals(Material.BEACON)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
beacons.add(
|
||||
new Beacon(
|
||||
block.getX(),
|
||||
block.getY(),
|
||||
block.getZ(),
|
||||
Color.WHITE.getRGB() // TODO?
|
||||
)
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return beacons;
|
||||
}
|
||||
if (n == 0) {
|
||||
return Color.WHITE.asRGB();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Configuration getConfig()
|
||||
{
|
||||
return this.worldConfig;
|
||||
return (new java.awt.Color(red, green, blue)).getRGB();
|
||||
}
|
||||
}
|
||||
@@ -18,22 +18,31 @@
|
||||
|
||||
package no.jckf.dhsupport.bukkit;
|
||||
|
||||
import no.jckf.dhsupport.bukkit.commands.DhCommand;
|
||||
import no.jckf.dhsupport.bukkit.commands.DhsCommand;
|
||||
import no.jckf.dhsupport.bukkit.handler.ConfigLoader;
|
||||
import no.jckf.dhsupport.bukkit.handler.PlayerHandler;
|
||||
import no.jckf.dhsupport.bukkit.handler.PluginMessageProxy;
|
||||
import no.jckf.dhsupport.bukkit.handler.WorldHandler;
|
||||
import no.jckf.dhsupport.core.DhSupport;
|
||||
import no.jckf.dhsupport.core.configuration.Configuration;
|
||||
import no.jckf.dhsupport.core.configuration.DhsConfig;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bstats.charts.MultiLineChart;
|
||||
import org.bstats.charts.SingleLineChart;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class DhSupportBukkitPlugin extends JavaPlugin
|
||||
{
|
||||
protected static int LOD_REFRESH_INTERVAL = 60 * 20;
|
||||
|
||||
protected DhSupport dhSupport;
|
||||
|
||||
protected Metrics metrics;
|
||||
@@ -48,50 +57,73 @@ public class DhSupportBukkitPlugin extends JavaPlugin
|
||||
public void onEnable()
|
||||
{
|
||||
String pluginVersion = "N/A";
|
||||
String mcVersion = "N/A";
|
||||
|
||||
try {
|
||||
YamlConfiguration meta = new YamlConfiguration();
|
||||
meta.load(new InputStreamReader(this.getResource("plugin.yml")));
|
||||
|
||||
pluginVersion = meta.getString("version");
|
||||
mcVersion = meta.getString("mc-version");
|
||||
} catch (Exception exception) {
|
||||
|
||||
}
|
||||
|
||||
this.getLogger().info("I am DHS " + pluginVersion + " for MC " + mcVersion + ".");
|
||||
String gameVersion = this.getGameVersion();
|
||||
|
||||
this.dhSupport = new DhSupport();
|
||||
this.getLogger().info("I am DHS " + pluginVersion + " running on " + this.getServer().getName() + " for Minecraft " + gameVersion + ".");
|
||||
|
||||
this.dhSupport = new DhSupport(pluginVersion, gameVersion);
|
||||
this.dhSupport.setLogger(this.getLogger());
|
||||
this.dhSupport.setDataDirectory(this.getDataFolder().getAbsolutePath());
|
||||
|
||||
this.metrics = new Metrics(this, 21843);
|
||||
this.metrics.addCustomChart(new SingleLineChart("dh_users", () -> this.dhSupport.getPlayerConfigurations().size()));
|
||||
this.metrics.addCustomChart(new MultiLineChart("dh_render_distance", () -> {
|
||||
Map<String, Integer> renderDistances = new HashMap<>();
|
||||
|
||||
this.configLoader = new ConfigLoader(this);
|
||||
this.configLoader.onEnable();
|
||||
for (Map<UUID, Configuration> playerConfigurations : this.dhSupport.getPlayerConfigurations().values()) {
|
||||
for (Configuration playerConfig : playerConfigurations.values()) {
|
||||
String rd = String.valueOf(playerConfig.getInt(DhsConfig.RENDER_DISTANCE));
|
||||
|
||||
renderDistances.put(rd, renderDistances.getOrDefault(rd, 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return renderDistances;
|
||||
}));
|
||||
|
||||
this.loadDhsConfig();
|
||||
|
||||
this.pluginMessageProxy = new PluginMessageProxy(this);
|
||||
this.pluginMessageProxy.onEnable();
|
||||
|
||||
this.dhSupport.onEnable();
|
||||
|
||||
this.scheduler = new BukkitScheduler(this);
|
||||
this.dhSupport.setScheduler(this.scheduler);
|
||||
|
||||
this.dhSupport.onEnable();
|
||||
|
||||
int lodRefreshInterval = this.getDhSupport().getConfig().getInt(DhsConfig.LOD_REFRESH_INTERVAL) * 20;
|
||||
|
||||
this.scheduler.runTimer(() -> {
|
||||
this.dhSupport.getGenerationTracker().prune();
|
||||
this.dhSupport.updateTouchedLods();
|
||||
}, LOD_REFRESH_INTERVAL, LOD_REFRESH_INTERVAL);
|
||||
}, lodRefreshInterval, lodRefreshInterval);
|
||||
|
||||
this.getServer().getPluginManager().registerEvents(new WorldHandler(this), this);
|
||||
this.getServer().getPluginManager().registerEvents(new PlayerHandler(this), this);
|
||||
|
||||
this.getCommand("dhs").setExecutor(new DhsCommand(this));
|
||||
this.getCommand("dh").setExecutor(new DhCommand(this));
|
||||
|
||||
this.getLogger().info("Ready!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable()
|
||||
{
|
||||
if (this.metrics != null) {
|
||||
this.metrics.shutdown();
|
||||
}
|
||||
|
||||
if (this.scheduler != null) {
|
||||
this.scheduler.cancelTasks();
|
||||
}
|
||||
@@ -114,6 +146,42 @@ public class DhSupportBukkitPlugin extends JavaPlugin
|
||||
this.getLogger().info("Lights out!");
|
||||
}
|
||||
|
||||
public void loadDhsConfig()
|
||||
{
|
||||
if (this.configLoader != null) {
|
||||
this.configLoader.onDisable();
|
||||
}
|
||||
|
||||
this.configLoader = new ConfigLoader(this);
|
||||
this.configLoader.onEnable();
|
||||
}
|
||||
|
||||
public @Nullable World getWorld(String name)
|
||||
{
|
||||
for (World world : this.getServer().getWorlds()) {
|
||||
if (world.getName().replace(' ', '_').equals(name)) {
|
||||
return world;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected String getGameVersion()
|
||||
{
|
||||
try {
|
||||
Class<? extends Server> serverClass = this.getServer().getClass();
|
||||
|
||||
serverClass.getMethod("getMinecraftVersion");
|
||||
|
||||
return this.getServer().getMinecraftVersion();
|
||||
} catch (NoSuchMethodException exception) {
|
||||
// If we're here, that probably means we're not running Paper or any Paper fork.
|
||||
}
|
||||
|
||||
return this.getServer().getBukkitVersion().split("-", 2)[0];
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public DhSupport getDhSupport()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* DH Support, server-side support for Distant Horizons.
|
||||
* Copyright (C) 2024 Jim C K Flaten
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package no.jckf.dhsupport.bukkit.commands;
|
||||
|
||||
import no.jckf.dhsupport.bukkit.DhSupportBukkitPlugin;
|
||||
import no.jckf.dhsupport.core.configuration.DhsConfig;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class DhCommand implements CommandExecutor
|
||||
{
|
||||
protected DhSupportBukkitPlugin plugin;
|
||||
|
||||
public DhCommand(DhSupportBukkitPlugin plugin)
|
||||
{
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args)
|
||||
{
|
||||
if (!this.plugin.getDhSupport().getConfig().getBool(DhsConfig.ENABLE_DH_COMMAND_HELP_TEXT, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
sender.sendMessage(ChatColor.RED + "You have issued a " + ChatColor.YELLOW + "/dh" + ChatColor.RED + " command which is meant for the Distant Horizons " + ChatColor.YELLOW + "mod" + ChatColor.RED + ", but you are using the " + ChatColor.YELLOW + "plugin" + ChatColor.RED + ". Please make sure you are reading the correct documentation.");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
/*
|
||||
* DH Support, server-side support for Distant Horizons.
|
||||
* Copyright (C) 2024 Jim C K Flaten
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package no.jckf.dhsupport.bukkit.commands;
|
||||
|
||||
import no.jckf.dhsupport.bukkit.DhSupportBukkitPlugin;
|
||||
import no.jckf.dhsupport.core.Coordinates;
|
||||
import no.jckf.dhsupport.core.PreGenerator;
|
||||
import no.jckf.dhsupport.core.Utils;
|
||||
import no.jckf.dhsupport.core.configuration.Configuration;
|
||||
import no.jckf.dhsupport.core.configuration.DhsConfig;
|
||||
import no.jckf.dhsupport.core.world.WorldInterface;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
public class DhsCommand implements CommandExecutor
|
||||
{
|
||||
protected DhSupportBukkitPlugin plugin;
|
||||
|
||||
public DhsCommand(DhSupportBukkitPlugin plugin)
|
||||
{
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args)
|
||||
{
|
||||
if (args.length < 1) {
|
||||
sender.sendMessage(ChatColor.RED + "Missing sub-command.");
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (args[0]) {
|
||||
case "status":
|
||||
return this.status(sender, Arrays.copyOfRange(args, 1, args.length));
|
||||
|
||||
case "reload":
|
||||
return this.reload(sender);
|
||||
|
||||
case "worlds":
|
||||
return this.worlds(sender);
|
||||
|
||||
case "pregen":
|
||||
return this.pregen(sender, Arrays.copyOfRange(args, 1, args.length));
|
||||
|
||||
case "pause":
|
||||
return this.pause(sender);
|
||||
|
||||
case "unpause":
|
||||
return this.unpause(sender);
|
||||
|
||||
case "trim":
|
||||
return this.trim(sender, Arrays.copyOfRange(args, 1, args.length));
|
||||
|
||||
case "bench":
|
||||
return this.bench(sender, Arrays.copyOfRange(args, 1, args.length));
|
||||
|
||||
case "config":
|
||||
return this.config(sender, Arrays.copyOfRange(args, 1, args.length));
|
||||
}
|
||||
|
||||
sender.sendMessage(ChatColor.RED + "Unknown sub-command.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean status(CommandSender sender, String[] args)
|
||||
{
|
||||
if (args.length == 0) {
|
||||
return this.statusServer(sender);
|
||||
}
|
||||
|
||||
return this.statusPlayer(sender, args[0]);
|
||||
}
|
||||
|
||||
protected boolean statusServer(CommandSender sender)
|
||||
{
|
||||
Configuration config;
|
||||
|
||||
if (sender instanceof Player) {
|
||||
World world = ((Player) sender).getWorld();
|
||||
|
||||
config = this.plugin.getDhSupport().getWorldInterface(world.getUID()).getConfig();
|
||||
|
||||
sender.sendMessage(ChatColor.BLUE + "Distant Horizons Support status for world " + ChatColor.GREEN + world.getName() + ChatColor.BLUE + ":");
|
||||
} else {
|
||||
config = this.plugin.getDhSupport().getConfig();
|
||||
|
||||
sender.sendMessage(ChatColor.BLUE + "Distant Horizons Support status for " + ChatColor.GREEN + "global context" + ChatColor.BLUE + ":");
|
||||
}
|
||||
|
||||
sender.sendMessage(ChatColor.BLUE + "Distant generation is " + (config.getBool(DhsConfig.DISTANT_GENERATION_ENABLED) ? ChatColor.GREEN + "enabled" : ChatColor.RED + "disabled") + ChatColor.BLUE + " (" + ChatColor.GREEN + config.getInt(DhsConfig.RENDER_DISTANCE) + ChatColor.BLUE + ").");
|
||||
sender.sendMessage(ChatColor.BLUE + "Real time updates is " + (config.getBool(DhsConfig.REAL_TIME_UPDATES_ENABLED) ? ChatColor.GREEN + "enabled" : ChatColor.RED + "disabled") + ChatColor.BLUE + " (" + ChatColor.GREEN + config.getInt(DhsConfig.REAL_TIME_UPDATE_RADIUS) + ChatColor.BLUE + ").");
|
||||
sender.sendMessage(ChatColor.BLUE + "Builder type is " + ChatColor.GREEN + config.getString(DhsConfig.BUILDER_TYPE) + ChatColor.BLUE + ".");
|
||||
|
||||
int playerCount = this.plugin.getDhSupport().getPlayerConfigurations().size();
|
||||
|
||||
StringBuilder playerList = new StringBuilder();
|
||||
|
||||
for (UUID playerId : this.plugin.getDhSupport().getPlayerConfigurations().keySet()) {
|
||||
playerList.append(ChatColor.GREEN + this.plugin.getServer().getPlayer(playerId).getName() + ChatColor.BLUE + ", ");
|
||||
}
|
||||
|
||||
sender.sendMessage(ChatColor.BLUE + "There " + (playerCount == 1 ? "is" : "are") + " " + ChatColor.GREEN + playerCount + ChatColor.BLUE + " " + (playerCount == 1 ? "player" : "players") + " online using Distant Horizons" + (playerCount == 0 ? "." : ": " + playerList.substring(0, playerList.length() - 2)));
|
||||
|
||||
sender.sendMessage(ChatColor.BLUE + "Current generation speed: " + ChatColor.GREEN + String.format("%.2f", this.plugin.getDhSupport().getGenerationTracker().getPingsPerSecond() * 16) + " CPS");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean statusPlayer(CommandSender sender, String playerName)
|
||||
{
|
||||
Player player = this.plugin.getServer().getPlayer(playerName);
|
||||
|
||||
if (player == null) {
|
||||
sender.sendMessage(ChatColor.RED + "Player not found.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Configuration config = this.plugin.getDhSupport().getPlayerConfiguration(player.getUniqueId(), 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;
|
||||
}
|
||||
}
|
||||
@@ -47,13 +47,13 @@ public class ConfigLoader extends Handler
|
||||
pluginConfig.setDefaults(new MemoryConfiguration());
|
||||
|
||||
// Get the config version value from the default config file.
|
||||
Integer pluginConfigVersion = defaultConfig.getInt(DhsConfig.CONFIG_VERSION);
|
||||
Integer bundledConfigVersion = defaultConfig.getInt(DhsConfig.CONFIG_VERSION);
|
||||
|
||||
// Get the config version value from the server's config file.
|
||||
Integer serverConfigVersion = pluginConfig.getInt(DhsConfig.CONFIG_VERSION);
|
||||
|
||||
// If the two values differ, load defaults for missing values.
|
||||
if (!pluginConfigVersion.equals(serverConfigVersion)) {
|
||||
if (!bundledConfigVersion.equals(serverConfigVersion)) {
|
||||
this.plugin.getLogger().warning("Your config file is for an older version of this plugin.");
|
||||
|
||||
pluginConfig.setDefaults(defaultConfig);
|
||||
@@ -63,6 +63,9 @@ public class ConfigLoader extends Handler
|
||||
// DH Support config.
|
||||
Configuration dhsConfig = this.plugin.getDhSupport().getConfig();
|
||||
|
||||
// Get rid of potentially old config values in the case of a reload.
|
||||
dhsConfig.clear();
|
||||
|
||||
// Populate DH Support config.
|
||||
pluginConfig.getKeys(true).forEach((key) -> dhsConfig.set(key, pluginConfig.get(key)));
|
||||
}
|
||||
@@ -70,6 +73,7 @@ public class ConfigLoader extends Handler
|
||||
@Override
|
||||
public void onDisable()
|
||||
{
|
||||
|
||||
// Revert any changes we did.
|
||||
this.plugin.reloadConfig();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,35 @@
|
||||
/*
|
||||
* 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;
|
||||
@@ -14,9 +39,30 @@ public class PlayerHandler implements Listener
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerConnect(PlayerJoinEvent event)
|
||||
{
|
||||
Player player = event.getPlayer();
|
||||
|
||||
DhSupport dhSupport = this.plugin.getDhSupport();
|
||||
|
||||
dhSupport.getScheduler().runOnMainThreadDelayed(() -> {
|
||||
if (dhSupport.getPlayerConfiguration(player.getUniqueId(), 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,25 +20,40 @@ package no.jckf.dhsupport.bukkit.handler;
|
||||
|
||||
import no.jckf.dhsupport.bukkit.BukkitWorldInterface;
|
||||
import no.jckf.dhsupport.bukkit.DhSupportBukkitPlugin;
|
||||
import no.jckf.dhsupport.core.configuration.DhsConfig;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.*;
|
||||
import org.bukkit.event.inventory.FurnaceBurnEvent;
|
||||
import org.bukkit.event.world.StructureGrowEvent;
|
||||
import org.bukkit.event.world.WorldLoadEvent;
|
||||
import org.bukkit.event.world.WorldUnloadEvent;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class WorldHandler implements Listener
|
||||
{
|
||||
protected static final String[] EVENT_METHOD_NAMES = { "blockList", "getBlocks", "getBlock", "getLocation", "getChunk" };
|
||||
|
||||
protected DhSupportBukkitPlugin plugin;
|
||||
|
||||
protected Map<String, Method> eventMethods = new HashMap<>();
|
||||
|
||||
public WorldHandler(DhSupportBukkitPlugin plugin)
|
||||
{
|
||||
this.plugin = plugin;
|
||||
|
||||
this.plugin.getServer().getWorlds().forEach(this::addWorldInterface);
|
||||
|
||||
this.registerUpdateListeners();
|
||||
}
|
||||
|
||||
protected void addWorldInterface(World world)
|
||||
@@ -57,9 +72,94 @@ public class WorldHandler implements Listener
|
||||
this.plugin.getDhSupport().setWorldInterface(world.getUID(), null);
|
||||
}
|
||||
|
||||
protected void touchLod(Location location)
|
||||
protected void touchLod(Location location, String reason)
|
||||
{
|
||||
this.plugin.getDhSupport().touchLod(location.getWorld().getUID(), location.getBlockX(), location.getBlockZ());
|
||||
this.plugin.getDhSupport().touchLod(location.getWorld().getUID(), location.getBlockX(), location.getBlockZ(), reason);
|
||||
}
|
||||
|
||||
protected void registerUpdateListeners()
|
||||
{
|
||||
Listener dummyListener = new Listener() {};
|
||||
|
||||
for (String eventClassName : this.plugin.getDhSupport().getConfig().getStringList(DhsConfig.UPDATE_EVENTS)) {
|
||||
try {
|
||||
Class<? extends Event> eventClass = Class.forName(eventClassName).asSubclass(Event.class);
|
||||
|
||||
this.plugin.getServer().getPluginManager().registerEvent(
|
||||
eventClass,
|
||||
dummyListener,
|
||||
EventPriority.MONITOR,
|
||||
(listener, event) -> this.handleUpdateEvent(event),
|
||||
this.plugin
|
||||
);
|
||||
|
||||
this.plugin.getDhSupport().info("Listening for " + eventClassName + ".");
|
||||
} catch (ClassNotFoundException exception) {
|
||||
this.plugin.getDhSupport().warning("Could not find event class: " + eventClassName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleUpdateEvent(Event event)
|
||||
{
|
||||
Class<? extends Event> eventClass = event.getClass();
|
||||
String eventClassName = eventClass.getName();
|
||||
Method method = null;
|
||||
|
||||
if (this.eventMethods.containsKey(eventClassName)) {
|
||||
method = this.eventMethods.get(eventClassName);
|
||||
|
||||
// We've seen this event before, but don't know how to handle it.
|
||||
if (method == null) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
for (String methodName : EVENT_METHOD_NAMES) {
|
||||
try {
|
||||
method = eventClass.getMethod(methodName);
|
||||
break;
|
||||
} catch (NoSuchMethodException exception) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Store the result, even if it is null.
|
||||
this.eventMethods.put(eventClassName, method);
|
||||
|
||||
// This is the first time we see this even, and we don't know how to handle it.
|
||||
if (method == null) {
|
||||
this.plugin.getDhSupport().warning("Unsure how to handle event: " + eventClass.getName());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
List<?> list;
|
||||
|
||||
try {
|
||||
Object result = method.invoke(event);
|
||||
|
||||
if (result instanceof Location location) {
|
||||
list = List.of(location.getBlock());
|
||||
} else if (result instanceof Chunk chunk) {
|
||||
list = List.of(chunk.getBlock(0, 0, 0));
|
||||
} else if (result instanceof List) {
|
||||
list = (List<?>) result;
|
||||
} else {
|
||||
list = List.of(result);
|
||||
}
|
||||
} catch (IllegalAccessException | InvocationTargetException exception) {
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
|
||||
for (Object entry : list) {
|
||||
if (entry instanceof Block block) {
|
||||
this.touchLod(block.getLocation(), eventClassName);
|
||||
} else if (entry instanceof BlockState state) {
|
||||
this.touchLod(state.getLocation(), eventClassName);
|
||||
} else {
|
||||
this.plugin.getDhSupport().warning("Unknown block " + entry + " from event: " + eventClassName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
@@ -73,117 +173,4 @@ public class WorldHandler implements Listener
|
||||
{
|
||||
this.removeWorldInterface(worldUnload.getWorld());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onBlockPlace(BlockPlaceEvent blockPlace)
|
||||
{
|
||||
this.touchLod(blockPlace.getBlock().getLocation());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onBlockBreak(BlockBreakEvent blockBreak)
|
||||
{
|
||||
this.touchLod(blockBreak.getBlock().getLocation());
|
||||
}
|
||||
|
||||
// FIXME messes with 1.16.5 compilation
|
||||
// @EventHandler
|
||||
// public void onTNTPrime(TNTPrimeEvent blockBreak)
|
||||
// {
|
||||
// this.touchLod(blockBreak.getBlock().getLocation());
|
||||
// }
|
||||
|
||||
@EventHandler
|
||||
public void onBlockExplode(BlockExplodeEvent blockExplode)
|
||||
{
|
||||
for (var block : blockExplode.blockList()) {
|
||||
this.touchLod(block.getLocation());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@EventHandler
|
||||
public void onBlockIgnite(BlockIgniteEvent blockIgnite)
|
||||
{
|
||||
this.touchLod(blockIgnite.getBlock().getLocation());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onBlockBurn(BlockBurnEvent blockBurn)
|
||||
{
|
||||
this.touchLod(blockBurn.getBlock().getLocation());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onLeavesDecay(LeavesDecayEvent leavesDecay)
|
||||
{
|
||||
this.touchLod(leavesDecay.getBlock().getLocation());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onBlockFade(BlockFadeEvent blockFade)
|
||||
{
|
||||
this.touchLod(blockFade.getBlock().getLocation());
|
||||
}
|
||||
|
||||
|
||||
@EventHandler
|
||||
public void onBlockGrow(BlockGrowEvent blockGrow)
|
||||
{
|
||||
this.touchLod(blockGrow.getBlock().getLocation());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onMoistureChange(MoistureChangeEvent moistureChange)
|
||||
{
|
||||
this.touchLod(moistureChange.getBlock().getLocation());
|
||||
}
|
||||
|
||||
|
||||
@EventHandler
|
||||
public void onFurnaceBurn(FurnaceBurnEvent furnaceBurn)
|
||||
{
|
||||
this.touchLod(furnaceBurn.getBlock().getLocation());
|
||||
}
|
||||
|
||||
|
||||
@EventHandler
|
||||
public void onBlockRedstone(BlockRedstoneEvent blockRedstone)
|
||||
{
|
||||
this.touchLod(blockRedstone.getBlock().getLocation());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onBlockPistonExtend(BlockPistonExtendEvent blockPistonExtend)
|
||||
{
|
||||
for (var block : blockPistonExtend.getBlocks()) {
|
||||
this.touchLod(block.getLocation());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onBlockPistonRetract(BlockPistonRetractEvent blockPistonRetract)
|
||||
{
|
||||
for (var block : blockPistonRetract.getBlocks()) {
|
||||
this.touchLod(block.getLocation());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@EventHandler
|
||||
public void onStructureGrow(StructureGrowEvent structureGrow)
|
||||
{
|
||||
for (var blockState : structureGrow.getBlocks()) {
|
||||
this.touchLod(blockState.getLocation());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onSpongeAbsorb(SpongeAbsorbEvent spongeAbsorb)
|
||||
{
|
||||
for (var blockState : spongeAbsorb.getBlocks()) {
|
||||
this.touchLod(blockState.getLocation());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
package no.jckf.dhsupport.core;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
// Block -> chunk -> section -> region
|
||||
public class Coordinates
|
||||
{
|
||||
@@ -101,4 +103,39 @@ public class Coordinates
|
||||
{
|
||||
return value & 0x0F;
|
||||
}
|
||||
|
||||
// PACK/UNPACK
|
||||
|
||||
public static long pack(int x, int z)
|
||||
{
|
||||
return ((long) x & 0xffffffffL)
|
||||
| (((long) z & 0xffffffffL) << 32);
|
||||
}
|
||||
|
||||
public static int unpackX(long packed)
|
||||
{
|
||||
return (int) packed;
|
||||
}
|
||||
|
||||
public static int unpackZ(long packed)
|
||||
{
|
||||
return (int) (packed >> 32);
|
||||
}
|
||||
|
||||
// ???
|
||||
|
||||
public static void foreachChunkInSection(int sectionX, int sectionZ, BiConsumer<Integer, Integer> consumer)
|
||||
{
|
||||
int worldX = sectionToBlock(sectionX);
|
||||
int worldZ = sectionToBlock(sectionZ);
|
||||
|
||||
for (int xMultiplier = 0; xMultiplier < 4; xMultiplier++) {
|
||||
for (int zMultiplier = 0; zMultiplier < 4; zMultiplier++) {
|
||||
consumer.accept(
|
||||
worldX + 16 * xMultiplier,
|
||||
worldZ + 16 * zMultiplier
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,12 @@ 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.Beacon;
|
||||
import no.jckf.dhsupport.core.dataobject.Lod;
|
||||
import no.jckf.dhsupport.core.dataobject.SectionPosition;
|
||||
import no.jckf.dhsupport.core.handler.LodHandler;
|
||||
@@ -35,6 +37,7 @@ 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;
|
||||
@@ -47,10 +50,15 @@ import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
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;
|
||||
@@ -63,26 +71,40 @@ public class DhSupport implements Configurable
|
||||
|
||||
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 Map<String, CompletableFuture<Lod>> queuedBuilders = new HashMap<>();
|
||||
protected Map<String, CompletableFuture<Lod>> queuedBuilders = new ConcurrentHashMap<>();
|
||||
|
||||
protected Map<String, LodModel> touchedLods = new ConcurrentHashMap<>();
|
||||
|
||||
protected Map<UUID, Configuration> playerConfigurations = new HashMap<>();
|
||||
protected Map<UUID, Map<UUID, Configuration>> playerConfigurations = new HashMap<>();
|
||||
|
||||
public DhSupport()
|
||||
protected Map<UUID, PreGenerator> preGenerators = new HashMap<>();
|
||||
|
||||
public DhSupport(String pluginVersion, String gameVersion)
|
||||
{
|
||||
this.pluginVersion = pluginVersion;
|
||||
this.gameVersion = gameVersion;
|
||||
|
||||
this.configuration = new Configuration();
|
||||
|
||||
this.database = new Database();
|
||||
this.lodRepository = new AsyncLodRepository(this.database);
|
||||
|
||||
this.pluginMessageHandler = new PluginMessageHandler(this);
|
||||
|
||||
this.updateChecker = new UpdateChecker(62013887);
|
||||
}
|
||||
|
||||
public void onEnable()
|
||||
@@ -90,20 +112,37 @@ public class DhSupport implements Configurable
|
||||
this.lodRepository.setLogger(this.getLogger());
|
||||
|
||||
try {
|
||||
this.database.open(this.getDataDirectory() + "/data.sqlite");
|
||||
this.database.open(
|
||||
this.getConfig().getString(DhsConfig.DATABASE_PATH)
|
||||
.replace("{datadir}", this.getDataDirectory())
|
||||
);
|
||||
|
||||
this.database.addMigration(CreateLodsTable.class);
|
||||
this.database.addMigration(AddLodVersionColumn.class);
|
||||
this.database.addMigration(DeleteLodsWithoutVersion.class);
|
||||
this.database.addMigration(DeleteLodsBelowVersion3.class);
|
||||
|
||||
this.database.migrate();
|
||||
} catch (Exception exception) {
|
||||
this.warning("Failed to initialize database: " + exception.getMessage());
|
||||
// TODO: Disable the plugin?
|
||||
throw new RuntimeException("Failed to initialize database!", exception);
|
||||
}
|
||||
|
||||
(new PlayerConfigHandler(this, this.pluginMessageHandler)).register();
|
||||
(new LodHandler(this, this.pluginMessageHandler)).register();
|
||||
|
||||
this.pluginMessageHandler.onEnable();
|
||||
|
||||
if (this.getConfig().getBool(DhsConfig.GENERATE_NEW_CHUNKS, true) && this.getConfig().getBool(DhsConfig.GENERATE_NEW_CHUNKS_WARNING, true)) {
|
||||
this.warning("Chunk generation is enabled. New chunks will be generated as needed to complete LOD generation. This could significantly increase the size of your world.");
|
||||
this.warning("If you understand what this means and would like to disable this warning, set " + DhsConfig.GENERATE_NEW_CHUNKS_WARNING + " to false in your config.");
|
||||
}
|
||||
|
||||
if (this.getConfig().getBool(DhsConfig.CHECK_FOR_UPDATES, true)) {
|
||||
this.getScheduler().runOnSeparateThread(() -> {
|
||||
this.checkUpdates();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void onDisable()
|
||||
@@ -133,6 +172,46 @@ public class DhSupport implements Configurable
|
||||
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) {
|
||||
@@ -140,15 +219,47 @@ public class DhSupport implements Configurable
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public Map<UUID, WorldInterface> getWorldInterfaces()
|
||||
{
|
||||
return this.worldInterfaces;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public WorldInterface getWorldInterface(UUID id)
|
||||
{
|
||||
return this.worldInterfaces.get(id);
|
||||
}
|
||||
|
||||
@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;
|
||||
@@ -170,6 +281,16 @@ public class DhSupport implements Configurable
|
||||
return this.lodRepository;
|
||||
}
|
||||
|
||||
public String getPluginVersion()
|
||||
{
|
||||
return this.pluginVersion;
|
||||
}
|
||||
|
||||
public String getGameVersion()
|
||||
{
|
||||
return this.gameVersion;
|
||||
}
|
||||
|
||||
public Configuration getConfig()
|
||||
{
|
||||
return this.configuration;
|
||||
@@ -196,14 +317,40 @@ public class DhSupport implements Configurable
|
||||
this.getLogger().warning(message);
|
||||
}
|
||||
|
||||
public void setPlayerConfiguration(UUID playerId, Configuration playerConfiguration)
|
||||
public void debug(String message)
|
||||
{
|
||||
this.playerConfigurations.put(playerId, playerConfiguration);
|
||||
// TODO: Use log levels properly?
|
||||
if (!this.getConfig().getBool(DhsConfig.DEBUG, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.getLogger().info("[DEBUG] " + message);
|
||||
}
|
||||
|
||||
public Configuration getPlayerConfiguration(UUID playerId)
|
||||
public void checkUpdates()
|
||||
{
|
||||
return this.playerConfigurations.get(playerId);
|
||||
if (this.updateChecker.isLatestVersion(this.pluginVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.warning("A newer version of the plugin is available.");
|
||||
}
|
||||
|
||||
public Map<UUID, Map<UUID, Configuration>> getPlayerConfigurations()
|
||||
{
|
||||
return this.playerConfigurations;
|
||||
}
|
||||
|
||||
public void setPlayerConfiguration(UUID playerId, Map<UUID, Configuration> playerConfigurations)
|
||||
{
|
||||
this.playerConfigurations.put(playerId, playerConfigurations);
|
||||
}
|
||||
|
||||
public Configuration getPlayerConfiguration(UUID playerId, UUID worldId)
|
||||
{
|
||||
Map<UUID, Configuration> configurations = this.playerConfigurations.get(playerId);
|
||||
|
||||
return configurations == null ? null : configurations.get(worldId);
|
||||
}
|
||||
|
||||
public void clearPlayerConfiguration(UUID playerId)
|
||||
@@ -242,25 +389,12 @@ public class DhSupport implements Configurable
|
||||
return this.queuedBuilders.get(key);
|
||||
}
|
||||
|
||||
Scheduler scheduler = this.getScheduler();
|
||||
CompletableFuture<Lod> queued = this.getScheduler().runOnSeparateThread(() -> {
|
||||
this.joinPauseState();
|
||||
|
||||
CompletableFuture<Lod> queued;
|
||||
|
||||
// TODO: Fetch chunk data on region thread, then move to builder thread.
|
||||
if (scheduler.canReadWorldAsync()) {
|
||||
queued = this.getScheduler().runOnSeparateThread(
|
||||
builder::generate
|
||||
);
|
||||
} else {
|
||||
queued = this.getScheduler().runOnRegionThread(
|
||||
worldId,
|
||||
Coordinates.sectionToBlock(position.getX()),
|
||||
Coordinates.sectionToBlock(position.getZ()),
|
||||
builder::generate
|
||||
);
|
||||
}
|
||||
|
||||
queued = queued.thenApply((lod) -> {
|
||||
return builder.generate();
|
||||
})
|
||||
.thenApply((lod) -> {
|
||||
this.queuedBuilders.remove(key);
|
||||
|
||||
return lod;
|
||||
@@ -280,92 +414,105 @@ public class DhSupport implements Configurable
|
||||
|
||||
public CompletableFuture<LodModel> getLod(UUID worldId, SectionPosition position)
|
||||
{
|
||||
int worldX = Coordinates.sectionToBlock(position.getX());
|
||||
int worldZ = Coordinates.sectionToBlock(position.getZ());
|
||||
|
||||
return this.getLodRepository()
|
||||
.loadLodAsync(worldId, position.getX(), position.getZ())
|
||||
.thenCompose((modelFromDb) -> {
|
||||
// If a LOD was found in the database, return it.
|
||||
.thenComposeAsync((modelFromDb) -> {
|
||||
// If an LOD was found in the database, return it.
|
||||
if (modelFromDb != null) {
|
||||
return CompletableFuture.completedFuture(modelFromDb);
|
||||
}
|
||||
|
||||
WorldInterface world = this.getWorldInterface(worldId).newInstance();
|
||||
|
||||
Map<String, CompletableFuture<Boolean>> loads = new HashMap<>();
|
||||
|
||||
// Load all the chunks we need for this request.
|
||||
for (int xMultiplier = 0; xMultiplier < 4; xMultiplier++) {
|
||||
for (int zMultiplier = 0; zMultiplier < 4; zMultiplier++) {
|
||||
int chunkX = worldX + 16 * xMultiplier;
|
||||
int chunkZ = worldZ + 16 * zMultiplier;
|
||||
|
||||
if (!world.isChunkLoaded(chunkX, chunkZ)) {
|
||||
loads.put(worldX + "x" + worldZ, world.loadChunkAsync(chunkX, chunkZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for chunk loads, then...
|
||||
return CompletableFuture.allOf(loads.values().toArray(new CompletableFuture[loads.size()]))
|
||||
.thenCompose((asd) -> {
|
||||
// No LOD was found. Start building a new one.
|
||||
CompletableFuture<Lod> lodFuture = this.queueBuilder(worldId, position, this.getBuilder(world, position));
|
||||
|
||||
// Find any beacons that should appear in this LOD.
|
||||
CompletableFuture<Collection<Beacon>> beaconFuture = this.getScheduler().runOnRegionThread(worldId, worldX, worldZ, () -> {
|
||||
Collection<Beacon> accumulator = new ArrayList<>();
|
||||
|
||||
boolean includeBeacons = world.getConfig().getBool(DhsConfig.INCLUDE_BEACONS, false);
|
||||
|
||||
if (includeBeacons) {
|
||||
for (int xMultiplier = 0; xMultiplier < 4; xMultiplier++) {
|
||||
for (int zMultiplier = 0; zMultiplier < 4; zMultiplier++) {
|
||||
accumulator.addAll(world.getBeaconsInChunk(worldX + 16 * xMultiplier, worldZ + 16 * zMultiplier));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
});
|
||||
|
||||
// Combine the LOD and beacons and save the result in the database.
|
||||
return lodFuture.thenCombine(beaconFuture, (lod, beacons) -> {
|
||||
// Discard the chunks we loaded.
|
||||
this.getScheduler().runOnRegionThread(worldId, worldX, worldZ, () -> {
|
||||
for (String key : loads.keySet()) {
|
||||
String[] xz = key.split("x", 2);
|
||||
|
||||
world.discardChunk(
|
||||
Coordinates.chunkToBlock(Integer.parseInt(xz[0])),
|
||||
Coordinates.chunkToBlock(Integer.parseInt(xz[1]))
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
Encoder lodEncoder = new Encoder();
|
||||
lod.encode(lodEncoder);
|
||||
|
||||
Encoder beaconEncoder = new Encoder();
|
||||
beaconEncoder.writeCollection(beacons);
|
||||
|
||||
return this.lodRepository.saveLodAsync(
|
||||
worldId,
|
||||
position.getX(),
|
||||
position.getZ(),
|
||||
lodEncoder.toByteArray(),
|
||||
beaconEncoder.toByteArray()
|
||||
);
|
||||
})
|
||||
.thenCompose((f) -> f); // Unwrap the nested future.
|
||||
});
|
||||
// Otherwise generate a new one.
|
||||
return this.generateLod(worldId, position, true);
|
||||
});
|
||||
}
|
||||
|
||||
public void touchLod(UUID worldId, int x, int z)
|
||||
protected CompletableFuture<LodModel> generateLod(UUID worldId, SectionPosition position, boolean manageChunks)
|
||||
{
|
||||
int 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;
|
||||
}))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for chunk loads, then...
|
||||
return CompletableFuture.allOf(loads.toArray(new CompletableFuture[0]))
|
||||
.thenComposeAsync((asd) -> {
|
||||
boolean loadRejected = loads.stream()
|
||||
.anyMatch((load) -> !load.join());
|
||||
|
||||
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);
|
||||
@@ -377,104 +524,241 @@ public class DhSupport implements Configurable
|
||||
|
||||
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);
|
||||
|
||||
this.getLodRepository().deleteLodAsync(lodModelToDelete.getWorldId(), lodModelToDelete.getX(), lodModelToDelete.getZ())
|
||||
.thenAccept((deleted) -> {
|
||||
if (!deleted) {
|
||||
return;
|
||||
}
|
||||
WorldInterface world = this.getWorldInterface(lodModelToDelete.getWorldId());
|
||||
|
||||
SectionPosition position = new SectionPosition();
|
||||
position.setDetailLevel(6);
|
||||
position.setX(lodModelToDelete.getX());
|
||||
position.setZ(lodModelToDelete.getZ());
|
||||
this.debug("Changes detected in " + world.getName() + " " + lodModelToDelete.getX() + " " + lodModelToDelete.getZ() + ".");
|
||||
|
||||
this.getLod(lodModelToDelete.getWorldId(), position)
|
||||
.thenAccept((newLodModel) -> {
|
||||
WorldInterface world = this.getWorldInterface(newLodModel.getWorldId());
|
||||
Configuration worldConfig = world.getConfig();
|
||||
this.getLodRepository().lodExistsAsync(lodModelToDelete.getWorldId(), lodModelToDelete.getX(), lodModelToDelete.getZ()).thenAccept((exists) -> {
|
||||
if (!exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is false, then it will be false for all players as well.
|
||||
boolean updatesEnabled = worldConfig.getBool(DhsConfig.REAL_TIME_UPDATES_ENABLED);
|
||||
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;
|
||||
}
|
||||
|
||||
if (!updatesEnabled) {
|
||||
return;
|
||||
}
|
||||
SectionPosition position = new SectionPosition();
|
||||
position.setDetailLevel(6);
|
||||
position.setX(lodModelToDelete.getX());
|
||||
position.setZ(lodModelToDelete.getZ());
|
||||
|
||||
String levelKeyPrefix = worldConfig.getString(DhsConfig.LEVEL_KEY_PREFIX);
|
||||
String levelKey = world.getName();
|
||||
this.getLod(lodModelToDelete.getWorldId(), position)
|
||||
.thenAcceptAsync((newLodModel) -> {
|
||||
Configuration worldConfig = world.getConfig();
|
||||
|
||||
if (levelKeyPrefix != null) {
|
||||
levelKey = levelKeyPrefix + levelKey;
|
||||
}
|
||||
// If this is false, then it will be false for all players as well.
|
||||
boolean updatesEnabled = worldConfig.getBool(DhsConfig.REAL_TIME_UPDATES_ENABLED);
|
||||
|
||||
int lodChunkX = Coordinates.sectionToChunk(lodModelToDelete.getX());
|
||||
int lodChunkZ = Coordinates.sectionToChunk(lodModelToDelete.getZ());
|
||||
|
||||
// TODO: Don't use Bukkit classes.
|
||||
for (Player player : Bukkit.getWorld(newLodModel.getWorldId()).getPlayers()) {
|
||||
Configuration playerConfig = this.getPlayerConfiguration(player.getUniqueId());
|
||||
|
||||
// No config for this player? Probably not using DH.
|
||||
if (playerConfig == null) {
|
||||
continue;
|
||||
if (!updatesEnabled) {
|
||||
this.debug("New LOD " + world.getName() + " " + lodModelToDelete.getX() + " " + lodModelToDelete.getZ() + " generated, but real-time updates are disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
int updatesRadius = playerConfig.getInt(DhsConfig.REAL_TIME_UPDATE_RADIUS);
|
||||
String levelKey = world.getLevelKey();
|
||||
|
||||
int playerChunkX = Coordinates.blockToChunk(player.getLocation().getBlockX());
|
||||
int playerChunkZ = Coordinates.blockToChunk(player.getLocation().getBlockZ());
|
||||
int lodChunkX = Coordinates.sectionToChunk(lodModelToDelete.getX());
|
||||
int lodChunkZ = Coordinates.sectionToChunk(lodModelToDelete.getZ());
|
||||
|
||||
int distanceX = Math.abs(Math.max(lodChunkX, playerChunkX) - Math.min(lodChunkX, playerChunkX));
|
||||
int distanceZ = Math.abs(Math.max(lodChunkZ, playerChunkZ) - Math.min(lodChunkZ, playerChunkZ));
|
||||
int playersInRangeCount = 0;
|
||||
int playersOutOfRangeCount = 0;
|
||||
int playersWithoutDhCount = 0;
|
||||
|
||||
// Update outside of player's range?
|
||||
if (distanceX > updatesRadius || distanceZ > updatesRadius) {
|
||||
continue;
|
||||
}
|
||||
// TODO: Don't use Bukkit classes.
|
||||
for (Player player : Bukkit.getWorld(newLodModel.getWorldId()).getPlayers()) {
|
||||
Configuration playerConfig = this.getPlayerConfiguration(player.getUniqueId(), world.getId());
|
||||
|
||||
int myBufferId = playerConfig.getInt("buffer-id", 0) + 1;
|
||||
playerConfig.set("buffer-id", myBufferId);
|
||||
// No config for this player? Probably not using DH.
|
||||
if (playerConfig == null) {
|
||||
playersWithoutDhCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
FullDataPartialUpdateMessage partialUpdateMessage = new FullDataPartialUpdateMessage();
|
||||
partialUpdateMessage.setLevelKey(levelKey);
|
||||
partialUpdateMessage.setBufferId(myBufferId);
|
||||
partialUpdateMessage.setBeacons(newLodModel.getBeacons());
|
||||
if (!playerConfig.getBool(DhsConfig.REAL_TIME_UPDATES_ENABLED)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] data = newLodModel.getData();
|
||||
int updatesRadius = playerConfig.getInt(DhsConfig.REAL_TIME_UPDATE_RADIUS);
|
||||
|
||||
int chunkCount = (int) Math.ceil((double) data.length / LodHandler.CHUNK_SIZE);
|
||||
int playerChunkX = Coordinates.blockToChunk(player.getLocation().getBlockX());
|
||||
int playerChunkZ = Coordinates.blockToChunk(player.getLocation().getBlockZ());
|
||||
|
||||
for (int chunkNo = 0; chunkNo < chunkCount; chunkNo++) {
|
||||
FullDataChunkMessage chunkResponse = new FullDataChunkMessage();
|
||||
chunkResponse.setBufferId(myBufferId);
|
||||
chunkResponse.setIsFirst(chunkNo == 0);
|
||||
chunkResponse.setData(Arrays.copyOfRange(
|
||||
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 * chunkNo + LodHandler.CHUNK_SIZE, data.length)
|
||||
));
|
||||
Math.min(LodHandler.CHUNK_SIZE, data.length - LodHandler.CHUNK_SIZE * chunkNo)
|
||||
);
|
||||
|
||||
this.pluginMessageHandler.sendPluginMessage(player.getUniqueId(), chunkResponse);
|
||||
this.pluginMessageHandler.sendPluginMessage(player.getUniqueId(), chunkResponse);
|
||||
}
|
||||
|
||||
this.pluginMessageHandler.sendPluginMessage(player.getUniqueId(), partialUpdateMessage);
|
||||
}
|
||||
|
||||
this.pluginMessageHandler.sendPluginMessage(player.getUniqueId(), partialUpdateMessage);
|
||||
}
|
||||
});
|
||||
|
||||
this.touchedLods.remove(key);
|
||||
});
|
||||
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;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +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;
|
||||
import java.util.Locale;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 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,16 +18,47 @@
|
||||
|
||||
package no.jckf.dhsupport.core.configuration;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.PolyNull;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class Configuration
|
||||
{
|
||||
protected Map<String, Object> variables = new HashMap<>();
|
||||
protected Map<String, Object> variables = new ConcurrentHashMap<>();
|
||||
|
||||
public void clear()
|
||||
{
|
||||
this.variables.clear();
|
||||
}
|
||||
|
||||
public 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);
|
||||
}
|
||||
|
||||
@@ -36,7 +67,7 @@ public class Configuration
|
||||
this.variables.remove(key);
|
||||
}
|
||||
|
||||
public @Nullable Object get(String key, @Nullable Object defaultValue)
|
||||
public @PolyNull Object get(String key, @PolyNull Object defaultValue)
|
||||
{
|
||||
return this.variables.getOrDefault(key, defaultValue);
|
||||
}
|
||||
@@ -46,7 +77,7 @@ public class Configuration
|
||||
return this.get(key, null);
|
||||
}
|
||||
|
||||
public @Nullable Boolean getBool(String key, @Nullable Boolean defaultValue)
|
||||
public @PolyNull Boolean getBool(String key, @PolyNull Boolean defaultValue)
|
||||
{
|
||||
return (Boolean) this.get(key, defaultValue);
|
||||
}
|
||||
@@ -56,7 +87,7 @@ public class Configuration
|
||||
return this.getBool(key, null);
|
||||
}
|
||||
|
||||
public @Nullable Integer getInt(String key, @Nullable Integer defaultValue)
|
||||
public @PolyNull Integer getInt(String key, @PolyNull Integer defaultValue)
|
||||
{
|
||||
return (Integer) this.get(key, defaultValue);
|
||||
}
|
||||
@@ -66,13 +97,35 @@ public class Configuration
|
||||
return this.getInt(key, null);
|
||||
}
|
||||
|
||||
public @Nullable String getString(String key, @Nullable String defaultValue)
|
||||
public @PolyNull String getString(String key, @PolyNull String defaultValue)
|
||||
{
|
||||
return (String) this.get(key, defaultValue);
|
||||
Object raw = this.get(key, defaultValue);
|
||||
|
||||
return raw == null ? null : String.valueOf(raw);
|
||||
}
|
||||
|
||||
public @Nullable String getString(String key)
|
||||
{
|
||||
return (String) this.get(key, null);
|
||||
return this.getString(key, null);
|
||||
}
|
||||
|
||||
public @PolyNull List<String> getStringList(String key, @PolyNull List<String> defaultValue)
|
||||
{
|
||||
return (List<String>) this.get(key, defaultValue);
|
||||
}
|
||||
|
||||
public List<String> getStringList(String key)
|
||||
{
|
||||
return this.getStringList(key, Collections.emptyList());
|
||||
}
|
||||
|
||||
public int increment(String key)
|
||||
{
|
||||
return (int) this.variables.compute(key, (k, v) -> v == null ? 1 : (int) v + 1);
|
||||
}
|
||||
|
||||
public int decrement(String key)
|
||||
{
|
||||
return (int) this.variables.compute(key, (k, v) -> v == null ? -1 : (int) v - 1);
|
||||
}
|
||||
}
|
||||
@@ -22,12 +22,25 @@ 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";
|
||||
@@ -42,6 +55,8 @@ public abstract class DhsConfig
|
||||
|
||||
public static String SCHEDULER_THREADS = "scheduler_threads";
|
||||
|
||||
public static String SERVER_KEY = "server_key";
|
||||
|
||||
public static String LEVEL_KEY_PREFIX = "level_key_prefix";
|
||||
|
||||
public static String BORDER_CENTER_X = "border_center_x";
|
||||
@@ -49,14 +64,44 @@ public abstract class DhsConfig
|
||||
public static String BORDER_CENTER_Z = "border_center_z";
|
||||
|
||||
public static String BORDER_RADIUS = "border_radius";
|
||||
|
||||
public static String USE_VANILLA_WORLD_BORDER = "use_vanilla_world_border";
|
||||
|
||||
public static String VANILLA_WORLD_BORDER_EXPANSION = "vanilla_world_border_expansion";
|
||||
|
||||
public static String 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 INCLUDE_BEACONS = "include_beacons";
|
||||
public static String PERFORM_UNDERGLOW_HACK = "perform_underglow_hack";
|
||||
|
||||
public static String SAMPLE_BIOMES_3D = "sample_biomes_3d";
|
||||
|
||||
public static String UPDATE_EVENTS = "update_events";
|
||||
|
||||
public static String ENABLE_DH_COMMAND_HELP_TEXT = "enable_dh_command_help_text";
|
||||
|
||||
public static String DUMMY_CHUNK = "dummy_chunk";
|
||||
|
||||
public static String MATERIAL_MAP = "material_map";
|
||||
|
||||
public static String AD_MESSAGE = "ad_message";
|
||||
|
||||
public static String LIGHT_OFFSET_HACK = "light_offset_hack";
|
||||
}
|
||||
@@ -1,9 +1,34 @@
|
||||
/*
|
||||
* 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.";
|
||||
@@ -18,23 +43,53 @@ public class WorldConfiguration extends Configuration
|
||||
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(WORLD_PREFIX.formatted(this.world.getName()) + key, value);
|
||||
this.config.set(this.key(key), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unset(String key)
|
||||
{
|
||||
this.config.unset(WORLD_PREFIX.formatted(this.world.getName()) + key);
|
||||
this.config.unset(this.key(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object get(String key, @Nullable Object defaultValue)
|
||||
public @PolyNull Object get(String key, @PolyNull Object defaultValue)
|
||||
{
|
||||
Object specific = this.config.get(WORLD_PREFIX.formatted(this.world.getName()) + key);
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,11 @@ package no.jckf.dhsupport.core.database;
|
||||
import no.jckf.dhsupport.core.database.migrations.Migration;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class Database
|
||||
{
|
||||
@@ -30,7 +33,11 @@ public class Database
|
||||
|
||||
protected Connection connection;
|
||||
|
||||
protected Map<String, Class<? extends Migration>> migrations = new HashMap<>();
|
||||
protected Map<String, PreparedStatement> preparedStatements = new ConcurrentHashMap<>();
|
||||
|
||||
protected List<Class<? extends Migration>> migrations = new ArrayList<>();
|
||||
|
||||
protected CompletableFuture<?> optimizing;
|
||||
|
||||
public Connection getConnection() throws SQLException
|
||||
{
|
||||
@@ -50,6 +57,8 @@ public class Database
|
||||
|
||||
public void close() throws SQLException
|
||||
{
|
||||
this.clearQueryCache();
|
||||
|
||||
if (this.connection == null || this.connection.isClosed()) {
|
||||
return;
|
||||
}
|
||||
@@ -57,6 +66,28 @@ public class Database
|
||||
this.getConnection().close();
|
||||
}
|
||||
|
||||
public PreparedStatement prepareAndReuse(String sql) throws SQLException
|
||||
{
|
||||
this.waitForOptimization();
|
||||
|
||||
if (!this.preparedStatements.containsKey(sql)) {
|
||||
this.preparedStatements.put(sql, this.getConnection().prepareStatement(sql));
|
||||
}
|
||||
|
||||
return this.preparedStatements.get(sql);
|
||||
}
|
||||
|
||||
public void clearQueryCache()
|
||||
{
|
||||
for (String key : this.preparedStatements.keySet()) {
|
||||
try {
|
||||
this.preparedStatements.remove(key).close();
|
||||
} catch (SQLException exception) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void createMigrationsTable() throws SQLException
|
||||
{
|
||||
String sql = """
|
||||
@@ -73,7 +104,7 @@ public class Database
|
||||
|
||||
public void addMigration(Class<? extends Migration> migration)
|
||||
{
|
||||
this.migrations.put(migration.getSimpleName(), migration);
|
||||
this.migrations.add(migration);
|
||||
}
|
||||
|
||||
public boolean hasMigrationRan(String name) throws SQLException
|
||||
@@ -105,16 +136,42 @@ public class Database
|
||||
{
|
||||
this.createMigrationsTable();
|
||||
|
||||
for (String name : this.migrations.keySet()) {
|
||||
for (Class<? extends Migration> migrationClass : this.migrations) {
|
||||
String name = migrationClass.getSimpleName();
|
||||
|
||||
if (this.hasMigrationRan(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Migration migration = this.migrations.get(name).getConstructor(this.getClass()).newInstance(this);
|
||||
Migration migration = migrationClass.getConstructor(this.getClass()).newInstance(this);
|
||||
|
||||
migration.up();
|
||||
|
||||
this.markMigrationAsRan(name);
|
||||
}
|
||||
}
|
||||
|
||||
public void optimize() throws Exception
|
||||
{
|
||||
this.optimizing = new CompletableFuture<>();
|
||||
|
||||
this.clearQueryCache();
|
||||
|
||||
try (Statement statement = this.getConnection().createStatement()) {
|
||||
statement.execute("PRAGMA optimize");
|
||||
statement.execute("ANALYZE");
|
||||
statement.execute("REINDEX");
|
||||
statement.execute("VACUUM");
|
||||
} finally {
|
||||
this.optimizing.complete(null);
|
||||
this.optimizing = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void waitForOptimization()
|
||||
{
|
||||
if (this.optimizing != null) {
|
||||
this.optimizing.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* DH Support, server-side support for Distant Horizons.
|
||||
* Copyright (C) 2024 Jim C K Flaten
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package no.jckf.dhsupport.core.database.migrations;
|
||||
|
||||
import no.jckf.dhsupport.core.database.Database;
|
||||
|
||||
import java.sql.Statement;
|
||||
|
||||
public class AddLodVersionColumn extends Migration
|
||||
{
|
||||
public AddLodVersionColumn(Database database)
|
||||
{
|
||||
super(database);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void up() throws Exception
|
||||
{
|
||||
try (Statement statement = this.database.getConnection().createStatement()) {
|
||||
statement.execute("""
|
||||
ALTER TABLE lods ADD version INT NULL;
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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;
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* DH Support, server-side support for Distant Horizons.
|
||||
* Copyright (C) 2024 Jim C K Flaten
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package no.jckf.dhsupport.core.database.migrations;
|
||||
|
||||
import no.jckf.dhsupport.core.database.Database;
|
||||
|
||||
import java.sql.Statement;
|
||||
|
||||
public class DeleteLodsWithoutVersion extends Migration
|
||||
{
|
||||
public DeleteLodsWithoutVersion(Database database)
|
||||
{
|
||||
super(database);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void up() throws Exception
|
||||
{
|
||||
try (Statement statement = this.database.getConnection().createStatement()) {
|
||||
statement.execute("""
|
||||
DELETE FROM lods WHERE version IS NULL;
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import java.util.UUID;
|
||||
|
||||
public class LodModel extends Model
|
||||
{
|
||||
public static int VERSION = 3;
|
||||
|
||||
public static LodModel create()
|
||||
{
|
||||
return new LodModel();
|
||||
@@ -39,6 +41,8 @@ public class LodModel extends Model
|
||||
|
||||
protected int timestamp;
|
||||
|
||||
protected int version;
|
||||
|
||||
public LodModel setWorldId(UUID worldId)
|
||||
{
|
||||
this.worldId = worldId;
|
||||
@@ -60,7 +64,7 @@ public class LodModel extends Model
|
||||
|
||||
public int getX()
|
||||
{
|
||||
return x;
|
||||
return this.x;
|
||||
}
|
||||
|
||||
public LodModel setZ(int z)
|
||||
@@ -72,7 +76,7 @@ public class LodModel extends Model
|
||||
|
||||
public int getZ()
|
||||
{
|
||||
return z;
|
||||
return this.z;
|
||||
}
|
||||
|
||||
public LodModel setData(byte[] data)
|
||||
@@ -84,7 +88,7 @@ public class LodModel extends Model
|
||||
|
||||
public byte[] getData()
|
||||
{
|
||||
return data;
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public LodModel setBeacons(byte[] beacons)
|
||||
@@ -96,7 +100,7 @@ public class LodModel extends Model
|
||||
|
||||
public byte[] getBeacons()
|
||||
{
|
||||
return beacons;
|
||||
return this.beacons;
|
||||
}
|
||||
|
||||
public LodModel setTimestamp(int timestamp)
|
||||
@@ -111,6 +115,18 @@ public class LodModel extends Model
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public LodModel setVersion(int version)
|
||||
{
|
||||
this.version = version;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getVersion()
|
||||
{
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return this.getWorldId() + "@" + this.getX() + "x" + this.getZ();
|
||||
|
||||
@@ -51,9 +51,9 @@ public class AsyncLodRepository extends LodRepository
|
||||
return future;
|
||||
}
|
||||
|
||||
public CompletableFuture<LodModel> saveLodAsync(UUID worldId, int sectionX, int sectionZ, byte[] data, byte[] beacons)
|
||||
public CompletableFuture<LodModel> saveLodAsync(UUID worldId, int sectionX, int sectionZ, byte[] data, byte[] beacons, int version)
|
||||
{
|
||||
return this.queueTask(() -> this.saveLod(worldId, sectionX, sectionZ, data, beacons));
|
||||
return this.queueTask(() -> this.saveLod(worldId, sectionX, sectionZ, data, beacons, version));
|
||||
}
|
||||
|
||||
public CompletableFuture<LodModel> loadLodAsync(UUID worldId, int sectionX, int sectionZ)
|
||||
@@ -70,4 +70,9 @@ public class AsyncLodRepository extends LodRepository
|
||||
{
|
||||
return this.queueTask(() -> this.deleteLod(worldId, sectionX, sectionZ));
|
||||
}
|
||||
|
||||
public CompletableFuture<Integer> trimLodsAsync(UUID worldId, int lowSectionX, int lowSectionZ, int highSectionX, int highSectionZ)
|
||||
{
|
||||
return this.queueTask(() -> this.trimLods(worldId, lowSectionX,lowSectionZ, highSectionX, highSectionZ));
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,16 @@ import java.util.logging.Logger;
|
||||
|
||||
public class LodRepository
|
||||
{
|
||||
protected static final String SQL_SAVE_LOD = "REPLACE INTO lods (worldId, x, z, data, beacons, timestamp, version) VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
protected static final String SQL_LOAD_LOD = "SELECT data, beacons, timestamp FROM lods WHERE worldId = ? AND x = ? AND z = ? LIMIT 1";
|
||||
|
||||
protected static final String SQL_LOD_EXISTS = "SELECT EXISTS( SELECT 1 FROM lods WHERE worldId = ? AND x = ? AND z = ? )";
|
||||
|
||||
protected static final String SQL_DELETE_LOD = "DELETE FROM lods WHERE worldId = ? AND x = ? AND z = ?";
|
||||
|
||||
protected static final String SQL_TRIM_LODS = "DELETE FROM lods WHERE worldId = ? AND (x < ? OR z < ? OR x > ? OR z > ?)";
|
||||
|
||||
protected Database database;
|
||||
|
||||
protected Logger logger;
|
||||
@@ -48,19 +58,20 @@ public class LodRepository
|
||||
return this.logger;
|
||||
}
|
||||
|
||||
public LodModel saveLod(UUID worldId, int sectionX, int sectionZ, byte[] data, byte[] beacons)
|
||||
public LodModel saveLod(UUID worldId, int sectionX, int sectionZ, byte[] data, byte[] beacons, int version)
|
||||
{
|
||||
int timestamp = (int) (System.currentTimeMillis() / 1000);
|
||||
|
||||
String sql = "REPLACE INTO lods (worldId, x, z, data, beacons, timestamp) VALUES (?, ?, ?, ?, ?, ?)";
|
||||
try {
|
||||
PreparedStatement statement = this.database.prepareAndReuse(SQL_SAVE_LOD);
|
||||
|
||||
try (PreparedStatement statement = this.database.getConnection().prepareStatement(sql)) {
|
||||
statement.setString(1, worldId.toString());
|
||||
statement.setInt(2, sectionX);
|
||||
statement.setInt(3, sectionZ);
|
||||
statement.setBytes(4, data);
|
||||
statement.setBytes(5, beacons);
|
||||
statement.setInt(6, timestamp);
|
||||
statement.setInt(7, version);
|
||||
|
||||
statement.executeUpdate();
|
||||
|
||||
@@ -70,69 +81,78 @@ public class LodRepository
|
||||
.setZ(sectionZ)
|
||||
.setData(data)
|
||||
.setBeacons(beacons)
|
||||
.setTimestamp(timestamp);
|
||||
.setTimestamp(timestamp)
|
||||
.setVersion(version);
|
||||
} catch (SQLException exception) {
|
||||
this.getLogger().warning("Could not save LOD: " + exception);
|
||||
|
||||
this.database.clearQueryCache();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public LodModel loadLod(UUID worldId, int sectionX, int sectionZ)
|
||||
{
|
||||
String sql = "SELECT data, beacons, timestamp FROM lods WHERE worldId = ? AND x = ? AND z = ? LIMIT 1";
|
||||
try {
|
||||
PreparedStatement statement = this.database.prepareAndReuse(SQL_LOAD_LOD);
|
||||
|
||||
try (PreparedStatement statement = this.database.getConnection().prepareStatement(sql)) {
|
||||
statement.setString(1, worldId.toString());
|
||||
statement.setInt(2, sectionX);
|
||||
statement.setInt(3, sectionZ);
|
||||
|
||||
ResultSet result = statement.executeQuery();
|
||||
try (ResultSet result = statement.executeQuery()) {
|
||||
if (!result.next()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] data = result.getBytes("data");
|
||||
byte[] data = result.getBytes("data");
|
||||
|
||||
if (data == null) {
|
||||
return null;
|
||||
return LodModel.create()
|
||||
.setWorldId(worldId)
|
||||
.setX(sectionX)
|
||||
.setZ(sectionZ)
|
||||
.setData(data)
|
||||
.setBeacons(result.getBytes("beacons"))
|
||||
.setTimestamp(result.getInt("timestamp"));
|
||||
}
|
||||
|
||||
return LodModel.create()
|
||||
.setWorldId(worldId)
|
||||
.setX(sectionX)
|
||||
.setZ(sectionZ)
|
||||
.setData(data)
|
||||
.setBeacons(result.getBytes("beacons"))
|
||||
.setTimestamp(result.getInt("timestamp"));
|
||||
} catch (SQLException exception) {
|
||||
this.getLogger().warning("Could not load LOD: " + exception);
|
||||
|
||||
this.database.clearQueryCache();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean lodExists(UUID worldId, int sectionX, int sectionZ)
|
||||
{
|
||||
String sql = "SELECT EXISTS( SELECT 1 FROM lods WHERE worldId = ? AND x = ? AND z = ? )";
|
||||
try {
|
||||
PreparedStatement statement = this.database.prepareAndReuse(SQL_LOD_EXISTS);
|
||||
|
||||
try (PreparedStatement statement = this.database.getConnection().prepareStatement(sql)) {
|
||||
statement.setString(1, worldId.toString());
|
||||
statement.setInt(2, sectionX);
|
||||
statement.setInt(3, sectionZ);
|
||||
|
||||
ResultSet result = statement.executeQuery();
|
||||
try (ResultSet result = statement.executeQuery()) {
|
||||
result.next();
|
||||
|
||||
return result.getInt(1) == 1;
|
||||
return result.getInt(1) == 1;
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
this.getLogger().warning("Could not check LOD existence: " + exception);
|
||||
|
||||
this.database.clearQueryCache();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean deleteLod(UUID worldId, int sectionX, int sectionZ)
|
||||
{
|
||||
String sql = "DELETE FROM lods WHERE worldId = ? AND x = ? AND z = ?";
|
||||
try {
|
||||
PreparedStatement statement = this.database.prepareAndReuse(SQL_DELETE_LOD);
|
||||
|
||||
try (PreparedStatement statement = this.database.getConnection().prepareStatement(sql)) {
|
||||
statement.setString(1, worldId.toString());
|
||||
statement.setInt(2, sectionX);
|
||||
statement.setInt(3, sectionZ);
|
||||
@@ -143,7 +163,32 @@ public class LodRepository
|
||||
} catch (SQLException exception) {
|
||||
this.getLogger().warning("Could not delete LOD: " + exception);
|
||||
|
||||
this.database.clearQueryCache();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public int trimLods(UUID worldId, int lowSectionX, int lowSectionZ, int highSectionX, int highSectionZ)
|
||||
{
|
||||
int affectedRows = 0;
|
||||
|
||||
try {
|
||||
PreparedStatement statement = this.database.prepareAndReuse(SQL_TRIM_LODS);
|
||||
|
||||
statement.setString(1, worldId.toString());
|
||||
statement.setInt(2, lowSectionX);
|
||||
statement.setInt(3, lowSectionZ);
|
||||
statement.setInt(4, highSectionX);
|
||||
statement.setInt(5, highSectionZ);
|
||||
|
||||
affectedRows = statement.executeUpdate();
|
||||
|
||||
this.database.optimize();
|
||||
} catch (Exception exception) {
|
||||
this.getLogger().warning("Could not trim LODs/optimize DB: " + exception);
|
||||
}
|
||||
|
||||
return affectedRows;
|
||||
}
|
||||
}
|
||||
@@ -18,76 +18,102 @@
|
||||
|
||||
package no.jckf.dhsupport.core.dataobject;
|
||||
|
||||
import no.jckf.dhsupport.core.bytestream.Decoder;
|
||||
import no.jckf.dhsupport.core.bytestream.Encoder;
|
||||
|
||||
public class DataPoint extends DataObject
|
||||
{
|
||||
protected byte skyLight = 0;
|
||||
protected static int MAPPING_ID_OFFSET = 0;
|
||||
protected static int MAPPING_ID_BITS = 32;
|
||||
protected static long MAPPING_ID_MASK = 0xFFFFFFFFL;
|
||||
|
||||
protected byte blockLight = 0;
|
||||
protected static int HEIGHT_OFFSET = MAPPING_ID_OFFSET + MAPPING_ID_BITS;
|
||||
protected static int HEIGHT_BITS = 12;
|
||||
protected static long HEIGHT_MASK = 0x0FFF;
|
||||
|
||||
protected int height = 1;
|
||||
protected static int START_Y_OFFSET = HEIGHT_OFFSET + HEIGHT_BITS;
|
||||
protected static int START_Y_BITS = 12;
|
||||
protected static long START_Y_MASK = 0x0FFF;
|
||||
|
||||
protected int startY = 0;
|
||||
protected static int SKY_LIGHT_OFFSET = START_Y_OFFSET + START_Y_BITS;
|
||||
protected static int SKY_LIGHT_BITS = 4;
|
||||
protected static long SKY_LIGHT_MASK = 0x0F;
|
||||
|
||||
protected int mappingId;
|
||||
protected static int BLOCK_LIGHT_OFFSET = SKY_LIGHT_OFFSET + SKY_LIGHT_BITS;
|
||||
protected static int BLOCK_LIGHT_BITS = 4;
|
||||
protected static long BLOCK_LIGHT_MASK = 0x0F;
|
||||
|
||||
public void setSkyLight(byte value)
|
||||
protected long data = 0;
|
||||
|
||||
public void setMappingId(int mappingId)
|
||||
{
|
||||
this.skyLight = value;
|
||||
}
|
||||
this.data &= ~(MAPPING_ID_MASK << MAPPING_ID_OFFSET);
|
||||
|
||||
public byte getSkyLight()
|
||||
{
|
||||
return this.skyLight;
|
||||
}
|
||||
|
||||
public void setBlockLight(byte value)
|
||||
{
|
||||
this.blockLight = value;
|
||||
}
|
||||
|
||||
public void setHeight(int value)
|
||||
{
|
||||
this.height = value;
|
||||
}
|
||||
|
||||
public int getHeight()
|
||||
{
|
||||
return this.height;
|
||||
}
|
||||
|
||||
public void setStartY(int value)
|
||||
{
|
||||
this.startY = value;
|
||||
}
|
||||
|
||||
public int getStartY()
|
||||
{
|
||||
return this.startY;
|
||||
}
|
||||
|
||||
public void setMappingId(int id)
|
||||
{
|
||||
this.mappingId = id;
|
||||
this.data |= mappingId & MAPPING_ID_MASK;
|
||||
}
|
||||
|
||||
public int getMappingId()
|
||||
{
|
||||
return this.mappingId;
|
||||
return (int) ((this.data >> MAPPING_ID_OFFSET) & MAPPING_ID_MASK);
|
||||
}
|
||||
|
||||
public void setHeight(int height)
|
||||
{
|
||||
this.data &= ~(HEIGHT_MASK << HEIGHT_OFFSET);
|
||||
|
||||
this.data |= (height & HEIGHT_MASK) << HEIGHT_OFFSET;
|
||||
}
|
||||
|
||||
public int getHeight()
|
||||
{
|
||||
return (int) ((this.data >> HEIGHT_OFFSET) & HEIGHT_MASK);
|
||||
}
|
||||
|
||||
public void setStartY(int startY)
|
||||
{
|
||||
this.data &= ~(START_Y_MASK << START_Y_OFFSET);
|
||||
|
||||
this.data |= ((long) startY & START_Y_MASK) << START_Y_OFFSET;
|
||||
}
|
||||
|
||||
public int getStartY()
|
||||
{
|
||||
return (int) ((this.data >> START_Y_OFFSET) & START_Y_MASK);
|
||||
}
|
||||
|
||||
public void setSkyLight(byte skyLight)
|
||||
{
|
||||
this.data &= ~(SKY_LIGHT_MASK << SKY_LIGHT_OFFSET);
|
||||
|
||||
this.data |= (skyLight & SKY_LIGHT_MASK) << SKY_LIGHT_OFFSET;
|
||||
}
|
||||
|
||||
public byte getSkyLight()
|
||||
{
|
||||
return (byte) ((this.data >> SKY_LIGHT_OFFSET) & SKY_LIGHT_MASK);
|
||||
}
|
||||
|
||||
public void setBlockLight(byte blockLight)
|
||||
{
|
||||
this.data &= ~(BLOCK_LIGHT_MASK << BLOCK_LIGHT_OFFSET);
|
||||
|
||||
this.data |= (blockLight & BLOCK_LIGHT_MASK) << BLOCK_LIGHT_OFFSET;
|
||||
}
|
||||
|
||||
public byte getBlockLight()
|
||||
{
|
||||
return (byte) ((this.data >> BLOCK_LIGHT_OFFSET) & BLOCK_LIGHT_MASK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(Encoder encoder)
|
||||
{
|
||||
long data = 0;
|
||||
|
||||
data |= this.mappingId;
|
||||
data |= (long) (this.height & 0x0FFF) << 32;
|
||||
data |= (long) (this.startY & 0x0FFF) << 32 + 12;
|
||||
data |= (long) (this.skyLight & 0x0F) << 32 + 12 + 12;
|
||||
data |= (long) (this.blockLight & 0x0F) << 32 + 12 + 12 + 4;
|
||||
|
||||
encoder.writeLong(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(Decoder decoder)
|
||||
{
|
||||
this.data = decoder.readLong();
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,22 @@ public class IdMapping extends DataObject
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public String getBiome()
|
||||
{
|
||||
return this.biome;
|
||||
}
|
||||
|
||||
public String getBlock()
|
||||
{
|
||||
return this.block;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Map<String, String> getProperties()
|
||||
{
|
||||
return this.properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(Encoder encoder)
|
||||
{
|
||||
|
||||
@@ -40,8 +40,14 @@ public class Lod extends DataObject
|
||||
|
||||
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);
|
||||
@@ -131,11 +161,10 @@ public class Lod extends DataObject
|
||||
|
||||
encoder.writeBoolean(true); // Apply to parent
|
||||
encoder.writeBoolean(false); // Apply to children
|
||||
encoder.writeBoolean(false); // Regenerate
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
encoder.writeLong(now);
|
||||
encoder.writeLong(now);
|
||||
encoder.writeLong(now); // Created at
|
||||
encoder.writeLong(now); // Updated at
|
||||
}
|
||||
|
||||
protected byte[] compress(byte[] uncompressedData)
|
||||
@@ -143,7 +172,17 @@ public class Lod extends DataObject
|
||||
ByteArrayOutputStream compressedStream = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
OutputStream compressorStream = new XZOutputStream(compressedStream, new LZMA2Options(3), XZ.CHECK_CRC64);
|
||||
LZMA2Options options = new LZMA2Options(3);
|
||||
|
||||
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();
|
||||
@@ -154,4 +193,9 @@ public class Lod extends DataObject
|
||||
|
||||
return compressedStream.toByteArray();
|
||||
}
|
||||
|
||||
public List<Beacon> getBeacons()
|
||||
{
|
||||
return this.beacons;
|
||||
}
|
||||
}
|
||||
@@ -23,101 +23,77 @@ import no.jckf.dhsupport.core.bytestream.Encoder;
|
||||
|
||||
public class SectionPosition extends DataObject
|
||||
{
|
||||
public static final int DETAIL_LEVEL_WIDTH = 8;
|
||||
public static final int X_POS_WIDTH = 28;
|
||||
public static final int Z_POS_WIDTH = 28;
|
||||
|
||||
public static final int DETAIL_LEVEL_OFFSET = 0;
|
||||
public static final int POS_X_OFFSET = DETAIL_LEVEL_OFFSET + DETAIL_LEVEL_WIDTH;
|
||||
public static final int POS_Z_OFFSET = POS_X_OFFSET + X_POS_WIDTH;
|
||||
public static final int DETAIL_LEVEL_BITS = 8;
|
||||
public static final long DETAIL_LEVEL_MASK = 0xFF;
|
||||
|
||||
public static final long DETAIL_LEVEL_MASK = Byte.MAX_VALUE;
|
||||
public static final int POS_X_MASK = (int) Math.pow(2, X_POS_WIDTH) - 1;
|
||||
public static final int POS_Z_MASK = (int) Math.pow(2, Z_POS_WIDTH) - 1;
|
||||
public static final int X_OFFSET = DETAIL_LEVEL_OFFSET + DETAIL_LEVEL_BITS;
|
||||
public static final int X_BITS = 28;
|
||||
public static final long X_MASK = 0x0FFFFFFF;
|
||||
|
||||
protected int detailLevel;
|
||||
public static final int Z_OFFSET = X_OFFSET + X_BITS;
|
||||
public static final int Z_BITS = 28;
|
||||
public static final long Z_MASK = 0x0FFFFFFF;
|
||||
|
||||
protected int x;
|
||||
protected long data = 0;
|
||||
|
||||
protected int z;
|
||||
|
||||
public SectionPosition()
|
||||
public void setDetailLevel(int detailLevel)
|
||||
{
|
||||
this.data &= ~(DETAIL_LEVEL_MASK << DETAIL_LEVEL_OFFSET);
|
||||
|
||||
}
|
||||
|
||||
public SectionPosition(int x, int z, int detailLevel)
|
||||
{
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
this.detailLevel = detailLevel;
|
||||
}
|
||||
|
||||
public void setDetailLevel(int level)
|
||||
{
|
||||
this.detailLevel = level;
|
||||
this.data |= (detailLevel & DETAIL_LEVEL_MASK) << DETAIL_LEVEL_OFFSET;
|
||||
}
|
||||
|
||||
public int getDetailLevel()
|
||||
{
|
||||
return this.detailLevel;
|
||||
return (int) (data & DETAIL_LEVEL_MASK);
|
||||
}
|
||||
|
||||
public void setX(int x)
|
||||
{
|
||||
this.x = x;
|
||||
this.data &= ~(X_MASK << X_OFFSET);
|
||||
|
||||
this.data |= (x & X_MASK) << X_OFFSET;
|
||||
}
|
||||
|
||||
public int getX()
|
||||
{
|
||||
return this.x;
|
||||
int raw = (int) ((data >> X_OFFSET) & X_MASK);
|
||||
|
||||
if ((raw & (1 << 27)) != 0) {
|
||||
raw |= (int) ~X_MASK;
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
public void setZ(int z)
|
||||
{
|
||||
this.z = z;
|
||||
this.data &= ~(Z_MASK << Z_OFFSET);
|
||||
|
||||
this.data |= (z & Z_MASK) << Z_OFFSET;
|
||||
}
|
||||
|
||||
public int getZ()
|
||||
{
|
||||
return this.z;
|
||||
int raw = (int) ((data >> Z_OFFSET) & Z_MASK);
|
||||
|
||||
if ((raw & (1 << 27)) != 0) {
|
||||
raw |= (int) ~Z_MASK;
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(Encoder encoder)
|
||||
{
|
||||
encoder.writeLong(this.toLong());
|
||||
encoder.writeLong(this.data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(Decoder decoder)
|
||||
{
|
||||
this.fromLong(decoder.readLong());
|
||||
}
|
||||
|
||||
public long toLong()
|
||||
{
|
||||
long data = 0;
|
||||
|
||||
data |= this.detailLevel & DETAIL_LEVEL_MASK;
|
||||
data |= (long) (this.x & POS_X_MASK) << POS_X_OFFSET;
|
||||
data |= (long) (this.z & POS_Z_MASK) << POS_Z_OFFSET;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public void fromLong(long data)
|
||||
{
|
||||
this.detailLevel = (int) (data & DETAIL_LEVEL_MASK);
|
||||
this.x = (int) ((data >> POS_X_OFFSET) & POS_X_MASK);
|
||||
this.z = (int) ((data >> POS_Z_OFFSET) & POS_Z_MASK);
|
||||
|
||||
// Adjust for potential negative values if masks do not account for sign
|
||||
if ((this.x & (1 << 23)) != 0) { // Check if the sign bit is set for 24-bit value
|
||||
this.x |= ~POS_X_MASK; // Sign extend
|
||||
}
|
||||
if ((this.z & (1 << 23)) != 0) { // Check if the sign bit is set for 24-bit value
|
||||
this.z |= ~POS_Z_MASK; // Sign extend
|
||||
}
|
||||
this.data = decoder.readLong();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -18,21 +18,18 @@
|
||||
|
||||
package no.jckf.dhsupport.core.handler;
|
||||
|
||||
import no.jckf.dhsupport.core.Coordinates;
|
||||
import no.jckf.dhsupport.core.DhSupport;
|
||||
import no.jckf.dhsupport.core.configuration.Configuration;
|
||||
import no.jckf.dhsupport.core.configuration.DhsConfig;
|
||||
import no.jckf.dhsupport.core.dataobject.Lod;
|
||||
import no.jckf.dhsupport.core.dataobject.SectionPosition;
|
||||
import no.jckf.dhsupport.core.message.plugin.ExceptionMessage;
|
||||
import no.jckf.dhsupport.core.message.plugin.FullDataChunkMessage;
|
||||
import no.jckf.dhsupport.core.message.plugin.FullDataSourceRequestMessage;
|
||||
import no.jckf.dhsupport.core.message.plugin.FullDataSourceResponseMessage;
|
||||
import no.jckf.dhsupport.core.world.WorldInterface;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class LodHandler
|
||||
{
|
||||
@@ -42,6 +39,8 @@ public class LodHandler
|
||||
|
||||
protected PluginMessageHandler pluginMessageHandler;
|
||||
|
||||
protected AtomicInteger requestsInFlight = new AtomicInteger(0);
|
||||
|
||||
public LodHandler(DhSupport dhSupport, PluginMessageHandler pluginMessageHandler)
|
||||
{
|
||||
this.dhSupport = dhSupport;
|
||||
@@ -51,7 +50,7 @@ public class LodHandler
|
||||
public void register()
|
||||
{
|
||||
this.pluginMessageHandler.getEventBus().registerHandler(FullDataSourceRequestMessage.class, (requestMessage) -> {
|
||||
//this.dhSupport.info("LOD request for " + requestMessage.getPosition().getX() + " x " + requestMessage.getPosition().getZ());
|
||||
//this.dhSupport.info("LOD request for " + requestMessage.getPosition().getX() + " " + requestMessage.getPosition().getZ());
|
||||
|
||||
if (requestMessage.getPosition().getDetailLevel() != 6) {
|
||||
ExceptionMessage exceptionMessage = new ExceptionMessage();
|
||||
@@ -62,12 +61,32 @@ public class LodHandler
|
||||
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();
|
||||
|
||||
WorldInterface world = this.dhSupport.getWorldInterface(worldUuid);
|
||||
WorldInterface world = this.dhSupport.getWorldInterfaceByLevelKey(requestMessage.getWorldName());
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -77,73 +96,75 @@ public class LodHandler
|
||||
ExceptionMessage exceptionMessage = new ExceptionMessage();
|
||||
exceptionMessage.isResponseTo(requestMessage);
|
||||
exceptionMessage.setTypeId(ExceptionMessage.TYPE_REQUEST_REJECTED);
|
||||
exceptionMessage.setMessage("Generation disabled");
|
||||
exceptionMessage.setMessage("Server has disabled LOD builder");
|
||||
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), exceptionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
int worldX = Coordinates.sectionToBlock(position.getX());
|
||||
int worldZ = Coordinates.sectionToBlock(position.getZ());
|
||||
|
||||
Integer borderCenterX = config.getInt(DhsConfig.BORDER_CENTER_X);
|
||||
Integer borderCenterZ = config.getInt(DhsConfig.BORDER_CENTER_Z);
|
||||
Integer borderRadius = config.getInt(DhsConfig.BORDER_RADIUS);
|
||||
|
||||
if (borderCenterX != null && borderCenterZ != null && borderRadius != null) {
|
||||
int minX = borderCenterX - borderRadius;
|
||||
int maxX = borderCenterX + borderRadius;
|
||||
|
||||
int minZ = borderCenterZ - borderRadius;
|
||||
int maxZ = borderCenterZ + borderRadius;
|
||||
|
||||
int higherLodX = worldX + 64;
|
||||
int higherLodZ = worldZ + 64;
|
||||
|
||||
if (higherLodX < minX || worldX > maxX || higherLodZ < minZ || worldZ > maxZ) {
|
||||
ExceptionMessage exceptionMessage = new ExceptionMessage();
|
||||
exceptionMessage.isResponseTo(requestMessage);
|
||||
exceptionMessage.setTypeId(ExceptionMessage.TYPE_REQUEST_REJECTED);
|
||||
exceptionMessage.setMessage("World border");
|
||||
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;
|
||||
}
|
||||
|
||||
/*boolean generate = config.getBool(DhsConfig.GENERATE_NEW_CHUNKS);
|
||||
int clientRateLimit = playerConfig.getInt(DhsConfig.CLIENT_LOD_REQUEST_CONCURRENCY_LIMIT, 0);
|
||||
int serverRateLimit = dhsConfig.getInt(DhsConfig.SERVER_LOD_REQUEST_CONCURRENCY_LIMIT, 0);
|
||||
|
||||
if (!generate) {
|
||||
for (int relativeChunkX = 0; relativeChunkX < Lod.width / 16; relativeChunkX++) {
|
||||
for (int relativeChunkZ = 0; relativeChunkZ < Lod.width / 16; relativeChunkZ++) {
|
||||
if (!world.chunkExists(worldX + relativeChunkX * 16, worldZ + relativeChunkZ * 16)) {
|
||||
ExceptionMessage exceptionMessage = new ExceptionMessage();
|
||||
exceptionMessage.isResponseTo(requestMessage);
|
||||
exceptionMessage.setTypeId(ExceptionMessage.TYPE_REQUEST_REJECTED);
|
||||
exceptionMessage.setMessage("Fog of war");
|
||||
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), exceptionMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
|
||||
boolean sendData = requestMessage.getTimestamp() == null || (requestMessage.getTimestamp() / 1000) < lodModel.getTimestamp();
|
||||
|
||||
if (sendData) {
|
||||
Configuration playerConfig = this.dhSupport.getPlayerConfiguration(requestMessage.getSender());
|
||||
|
||||
if (playerConfig == null) {
|
||||
// Player disconnected before delivery.
|
||||
return;
|
||||
}
|
||||
|
||||
int myBufferId = playerConfig.getInt("buffer-id", 0) + 1;
|
||||
|
||||
playerConfig.set("buffer-id", myBufferId);
|
||||
int myBufferId = playerConfig.increment("buffer-id");
|
||||
|
||||
responseMessage.setBufferId(myBufferId);
|
||||
responseMessage.setBeacons(lodModel.getBeacons());
|
||||
@@ -156,16 +177,16 @@ public class LodHandler
|
||||
FullDataChunkMessage chunkResponse = new FullDataChunkMessage();
|
||||
chunkResponse.setBufferId(myBufferId);
|
||||
chunkResponse.setIsFirst(chunkNo == 0);
|
||||
chunkResponse.setData(Arrays.copyOfRange(
|
||||
chunkResponse.setData(
|
||||
data,
|
||||
CHUNK_SIZE * chunkNo,
|
||||
Math.min(CHUNK_SIZE * chunkNo + CHUNK_SIZE, data.length)
|
||||
));
|
||||
Math.min(CHUNK_SIZE, data.length - CHUNK_SIZE * chunkNo)
|
||||
);
|
||||
|
||||
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), chunkResponse);
|
||||
}
|
||||
|
||||
//this.dhSupport.info("LOD in " + chunkCount + " parts sent for " + requestMessage.getPosition().getX() + " x " + requestMessage.getPosition().getZ());
|
||||
//this.dhSupport.info("LOD in " + chunkCount + " parts sent for " + requestMessage.getPosition().getX() + " " + requestMessage.getPosition().getZ());
|
||||
}
|
||||
|
||||
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), responseMessage);
|
||||
|
||||
@@ -22,12 +22,23 @@ import no.jckf.dhsupport.core.DhSupport;
|
||||
import no.jckf.dhsupport.core.configuration.Configuration;
|
||||
import no.jckf.dhsupport.core.configuration.DhsConfig;
|
||||
import no.jckf.dhsupport.core.message.plugin.LevelInitMessage;
|
||||
import no.jckf.dhsupport.core.message.plugin.LevelInitRequestMessage;
|
||||
import no.jckf.dhsupport.core.message.plugin.RemotePlayerConfigMessage;
|
||||
import no.jckf.dhsupport.core.world.WorldInterface;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.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;
|
||||
@@ -40,62 +51,127 @@ public class PlayerConfigHandler
|
||||
|
||||
public void register()
|
||||
{
|
||||
this.pluginMessageHandler.getEventBus().registerHandler(LevelInitRequestMessage.class, (initRequestMessage) -> {
|
||||
// TODO: Some sort of Player wrapper or interface object. Bukkit classes should not be imported here.
|
||||
Player player = Bukkit.getPlayer(initRequestMessage.getSender());
|
||||
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
Configuration dhsConfig = this.dhSupport.getWorldInterface(player.getWorld().getUID()).getConfig();
|
||||
|
||||
String levelKeyPrefix = dhsConfig.getString(DhsConfig.LEVEL_KEY_PREFIX);
|
||||
String levelKey = player.getWorld().getName();
|
||||
|
||||
if (levelKeyPrefix != null) {
|
||||
levelKey = levelKeyPrefix + levelKey;
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dhSupport.info("Received DH config for " + player.getName() + " in " + levelKey);
|
||||
Map<UUID, Configuration> playerConfigurations = new HashMap<>();
|
||||
|
||||
LevelInitMessage levelKeyResponse = new LevelInitMessage();
|
||||
levelKeyResponse.setKey(levelKey);
|
||||
levelKeyResponse.setTime(System.currentTimeMillis());
|
||||
this.pluginMessageHandler.sendPluginMessage(configMessage.getSender(), levelKeyResponse);
|
||||
for (WorldInterface world : this.dhSupport.getWorldInterfaces().values()) {
|
||||
String permission = USAGE_PERMISSION.formatted(world.getName());
|
||||
boolean dhUseIsAllowed = !player.isPermissionSet(permission) || player.hasPermission(permission);
|
||||
|
||||
Configuration clientConfig = configMessage.toConfiguration();
|
||||
Configuration dhsConfig = world.getConfig();
|
||||
|
||||
// This is not very flexible, but will do for now.
|
||||
for (String key : RemotePlayerConfigMessage.KEYS) {
|
||||
//this.dhSupport.getLogger().info("Config key " + key + ":");
|
||||
Configuration clientConfig = configMessage.toConfiguration();
|
||||
|
||||
Object dhsValue = dhsConfig.get(key);
|
||||
Object clientValue = clientConfig.get(key);
|
||||
Object keepValue = null;
|
||||
// This is not very flexible, but will do for now.
|
||||
for (String key : RemotePlayerConfigMessage.KEYS) {
|
||||
//this.dhSupport.getLogger().info("Config key " + key + ":");
|
||||
|
||||
// TODO: This is ugly. Move to comparator closures like in DH.
|
||||
if (key.equals(DhsConfig.BORDER_CENTER_X) || key.equals(DhsConfig.BORDER_CENTER_Z) || key.equals(DhsConfig.BORDER_RADIUS)) {
|
||||
keepValue = dhsValue;
|
||||
}
|
||||
Object dhsValue = dhsConfig.get(key);
|
||||
Object clientValue = clientConfig.get(key);
|
||||
Object keepValue = null;
|
||||
|
||||
if (keepValue == null) {
|
||||
if (dhsValue instanceof Boolean dhsBool && clientValue instanceof Boolean clientBool) {
|
||||
keepValue = dhsBool && clientBool;
|
||||
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.");
|
||||
|
||||
//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... 😵💫");
|
||||
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);
|
||||
}
|
||||
|
||||
clientConfig.set(key, keepValue);
|
||||
playerConfigurations.put(world.getId(), clientConfig);
|
||||
}
|
||||
|
||||
this.dhSupport.setPlayerConfiguration(configMessage.getSender(), 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,189 +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 = 10;
|
||||
|
||||
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, LevelInitMessage.class);
|
||||
this.messageTypeRegistry.registerMessageType(3, RemotePlayerConfigMessage.class);
|
||||
this.messageTypeRegistry.registerMessageType(4, CancelMessage.class);
|
||||
this.messageTypeRegistry.registerMessageType(5, ExceptionMessage.class);
|
||||
this.messageTypeRegistry.registerMessageType(6, FullDataSourceRequestMessage.class);
|
||||
this.messageTypeRegistry.registerMessageType(7, FullDataSourceResponseMessage.class);
|
||||
this.messageTypeRegistry.registerMessageType(8, FullDataPartialUpdateMessage.class);
|
||||
this.messageTypeRegistry.registerMessageType(9, FullDataChunkMessage.class);
|
||||
}
|
||||
|
||||
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(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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
#if READ_FORGE_BYTE == "true"
|
||||
decoder.readByte();
|
||||
#endif
|
||||
|
||||
// Read the client's protocol version.
|
||||
short protocolVersion = decoder.readShort();
|
||||
|
||||
if (protocolVersion != this.protocolVersion) {
|
||||
CloseReasonMessage closeReasonMessage = new CloseReasonMessage();
|
||||
closeReasonMessage.setReason((protocolVersion > this.protocolVersion ? "Server" : "Client") + " is outdated.");
|
||||
this.sendPluginMessage(senderUuid, closeReasonMessage);
|
||||
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().getSimpleName());
|
||||
exception.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
Encoder encoder = new Encoder();
|
||||
|
||||
#if READ_FORGE_BYTE == "true"
|
||||
encoder.writeByte(0);
|
||||
#endif
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,7 @@ package no.jckf.dhsupport.core.lodbuilders;
|
||||
|
||||
import no.jckf.dhsupport.core.Coordinates;
|
||||
import no.jckf.dhsupport.core.configuration.DhsConfig;
|
||||
import no.jckf.dhsupport.core.dataobject.DataPoint;
|
||||
import no.jckf.dhsupport.core.dataobject.IdMapping;
|
||||
import no.jckf.dhsupport.core.dataobject.Lod;
|
||||
import no.jckf.dhsupport.core.dataobject.SectionPosition;
|
||||
import no.jckf.dhsupport.core.dataobject.*;
|
||||
import no.jckf.dhsupport.core.world.WorldInterface;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
@@ -51,15 +48,20 @@ public class FastOverworldBuilder extends LodBuilder
|
||||
int offsetX = Coordinates.sectionToBlock(this.position.getX());
|
||||
int offsetZ = Coordinates.sectionToBlock(this.position.getZ());
|
||||
|
||||
int yStep = this.worldInterface.getConfig().getInt(DhsConfig.BUILDER_RESOLUTION);
|
||||
int yStep = 1;//this.worldInterface.getConfig().getInt(DhsConfig.BUILDER_RESOLUTION);
|
||||
int originalStep = yStep;
|
||||
|
||||
boolean scanToSeaLevel = this.worldInterface.getConfig().getBool(DhsConfig.SCAN_TO_SEA_LEVEL, false);
|
||||
boolean includeNonCollidingTopLayer = this.worldInterface.getConfig().getBool(DhsConfig.INCLUDE_NON_COLLIDING_TOP_LAYER, false);
|
||||
boolean underfill = this.worldInterface.getConfig().getBool(DhsConfig.FAST_UNDERFILL, true);
|
||||
boolean includeNonCollidingTopLayer = this.worldInterface.getConfig().getBool(DhsConfig.INCLUDE_NON_COLLIDING_TOP_LAYER, true);
|
||||
boolean sampleBiomes3d = this.worldInterface.getConfig().getBool(DhsConfig.SAMPLE_BIOMES_3D, false);
|
||||
|
||||
List<IdMapping> idMappings = new ArrayList<>();
|
||||
Map<String, Integer> mapMap = new HashMap<>();
|
||||
List<IdMapping> idMappings = new ArrayList<>(64);
|
||||
Map<String, Map<String, Integer>> mapMap = new HashMap<>();
|
||||
|
||||
List<List<DataPoint>> columns = new ArrayList<>();
|
||||
List<List<DataPoint>> columns = new ArrayList<>(Lod.width * Lod.width);
|
||||
|
||||
List<Beacon> beacons = new ArrayList<>(8);
|
||||
|
||||
for (int relativeX = 0; relativeX < Lod.width; relativeX++) {
|
||||
for (int relativeZ = 0; relativeZ < Lod.width; relativeZ++) {
|
||||
@@ -68,10 +70,7 @@ public class FastOverworldBuilder extends LodBuilder
|
||||
|
||||
// Actual Y of top-most block.
|
||||
int topLayer = this.worldInterface.getHighestYAt(worldX, worldZ);
|
||||
|
||||
// Copies of the original values.
|
||||
int hardTopLayer = topLayer;
|
||||
int originalStep = yStep;
|
||||
|
||||
if (includeNonCollidingTopLayer) {
|
||||
outer: while (topLayer + 1 < maxY) {
|
||||
@@ -95,9 +94,13 @@ public class FastOverworldBuilder extends LodBuilder
|
||||
// Distance from bottom to top-most block.
|
||||
int relativeTopLayer = topLayer - minY;
|
||||
|
||||
String biome = this.worldInterface.getBiomeAt(worldX, worldZ);
|
||||
String biome = "";
|
||||
|
||||
List<DataPoint> column = new ArrayList<>();
|
||||
if (!sampleBiomes3d) {
|
||||
biome = this.worldInterface.getBiomeAt(worldX, worldZ);
|
||||
}
|
||||
|
||||
List<DataPoint> column = new ArrayList<>(8);
|
||||
|
||||
@Nullable
|
||||
DataPoint previous = null;
|
||||
@@ -125,6 +128,19 @@ public class FastOverworldBuilder extends LodBuilder
|
||||
|
||||
String material = this.worldInterface.getMaterialAt(worldX, highWorldY, worldZ);
|
||||
|
||||
if (this.worldInterface.isBeacon(worldX, highWorldY, worldZ)) {
|
||||
beacons.add(new Beacon(
|
||||
worldX,
|
||||
highWorldY,
|
||||
worldZ,
|
||||
worldInterface.getBeaconColor(
|
||||
worldX,
|
||||
highWorldY,
|
||||
worldZ
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
if (solidGround == null && (!scanToSeaLevel || highWorldY <= seaLevel)) {
|
||||
switch (material) {
|
||||
case "minecraft:stone":
|
||||
@@ -138,15 +154,22 @@ public class FastOverworldBuilder extends LodBuilder
|
||||
}
|
||||
}
|
||||
|
||||
String compositeKey = biome + "|" + material;
|
||||
if (sampleBiomes3d) {
|
||||
biome = this.worldInterface.getBiomeAt(worldX, highWorldY, worldZ);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Integer id = mapMap.get(compositeKey);
|
||||
Map<String, Integer> biomeMap = mapMap.get(biome);
|
||||
if (biomeMap == null) {
|
||||
biomeMap = new HashMap<>();
|
||||
mapMap.put(biome, biomeMap);
|
||||
}
|
||||
|
||||
Integer id = biomeMap.get(material);
|
||||
|
||||
if (id == null) {
|
||||
idMappings.add(new IdMapping(biome, material, null));
|
||||
id = idMappings.size() - 1;
|
||||
mapMap.put(compositeKey, id);
|
||||
biomeMap.put(material, id);
|
||||
}
|
||||
|
||||
DataPoint point;
|
||||
@@ -164,10 +187,8 @@ public class FastOverworldBuilder extends LodBuilder
|
||||
point.setHeight(thisStep);
|
||||
point.setMappingId(id);
|
||||
|
||||
if (highWorldY + 1 < maxY) {
|
||||
point.setSkyLight(this.worldInterface.getSkyLightAt(worldX, highWorldY + 1, worldZ));
|
||||
point.setBlockLight(this.worldInterface.getBlockLightAt(worldX, highWorldY + 1, worldZ));
|
||||
}
|
||||
point.setSkyLight(this.worldInterface.getSkyLightAt(worldX, highWorldY + 1, worldZ));
|
||||
point.setBlockLight(this.worldInterface.getBlockLightAt(worldX, highWorldY + 1, worldZ));
|
||||
|
||||
if (material.equals("minecraft:air") || material.equals("minecraft:void_air")) {
|
||||
// Start by filling the top of the column with air, then jump down to the top layer.
|
||||
@@ -189,10 +210,15 @@ public class FastOverworldBuilder extends LodBuilder
|
||||
previous = point;
|
||||
}
|
||||
|
||||
if (underfill && previous != null) {
|
||||
previous.setHeight(previous.getStartY() + previous.getHeight());
|
||||
previous.setStartY(0);
|
||||
}
|
||||
|
||||
columns.add(column);
|
||||
}
|
||||
}
|
||||
|
||||
return new Lod(this.worldInterface, this.position, idMappings, columns);
|
||||
return new Lod(this.worldInterface, this.position, idMappings, columns, beacons);
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,7 @@ package no.jckf.dhsupport.core.lodbuilders;
|
||||
|
||||
import no.jckf.dhsupport.core.Coordinates;
|
||||
import no.jckf.dhsupport.core.configuration.DhsConfig;
|
||||
import no.jckf.dhsupport.core.dataobject.DataPoint;
|
||||
import no.jckf.dhsupport.core.dataobject.IdMapping;
|
||||
import no.jckf.dhsupport.core.dataobject.Lod;
|
||||
import no.jckf.dhsupport.core.dataobject.SectionPosition;
|
||||
import no.jckf.dhsupport.core.dataobject.*;
|
||||
import no.jckf.dhsupport.core.world.WorldInterface;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
@@ -48,14 +45,19 @@ public class FullBuilder extends LodBuilder
|
||||
int offsetX = Coordinates.sectionToBlock(this.position.getX());
|
||||
int offsetZ = Coordinates.sectionToBlock(this.position.getZ());
|
||||
|
||||
int yStep = this.worldInterface.getConfig().getInt(DhsConfig.BUILDER_RESOLUTION);
|
||||
int yStep = 1;//this.worldInterface.getConfig().getInt(DhsConfig.BUILDER_RESOLUTION);
|
||||
int originalStep = yStep;
|
||||
|
||||
boolean includeNonCollidingTopLayer = this.worldInterface.getConfig().getBool(DhsConfig.INCLUDE_NON_COLLIDING_TOP_LAYER, false);
|
||||
boolean includeNonCollidingTopLayer = this.worldInterface.getConfig().getBool(DhsConfig.INCLUDE_NON_COLLIDING_TOP_LAYER, true);
|
||||
boolean performUnderglowHack = this.worldInterface.getConfig().getBool(DhsConfig.PERFORM_UNDERGLOW_HACK, false);
|
||||
boolean sampleBiomes3d = this.worldInterface.getConfig().getBool(DhsConfig.SAMPLE_BIOMES_3D, false);
|
||||
|
||||
List<IdMapping> idMappings = new ArrayList<>();
|
||||
Map<String, Integer> mapMap = new HashMap<>();
|
||||
List<IdMapping> idMappings = new ArrayList<>(64);
|
||||
Map<String, Map<String, Map<String, Integer>>> mapMap = new HashMap<>();
|
||||
|
||||
List<List<DataPoint>> columns = new ArrayList<>();
|
||||
List<List<DataPoint>> columns = new ArrayList<>(Lod.width * Lod.width);
|
||||
|
||||
List<Beacon> beacons = new ArrayList<>(8);
|
||||
|
||||
for (int relativeX = 0; relativeX < Lod.width; relativeX++) {
|
||||
for (int relativeZ = 0; relativeZ < Lod.width; relativeZ++) {
|
||||
@@ -64,10 +66,7 @@ public class FullBuilder extends LodBuilder
|
||||
|
||||
// Actual Y of top-most block.
|
||||
int topLayer = this.worldInterface.getHighestYAt(worldX, worldZ);
|
||||
|
||||
// Copies of the original values.
|
||||
int hardTopLayer = topLayer;
|
||||
int originalStep = yStep;
|
||||
|
||||
if (includeNonCollidingTopLayer) {
|
||||
outer: while (topLayer + 1 < maxY) {
|
||||
@@ -91,9 +90,13 @@ public class FullBuilder extends LodBuilder
|
||||
// Distance from bottom to top-most block.
|
||||
int relativeTopLayer = topLayer - minY;
|
||||
|
||||
String biome = this.worldInterface.getBiomeAt(worldX, worldZ);
|
||||
String biome = "";
|
||||
|
||||
List<DataPoint> column = new ArrayList<>();
|
||||
if (!sampleBiomes3d) {
|
||||
biome = this.worldInterface.getBiomeAt(worldX, worldZ);
|
||||
}
|
||||
|
||||
List<DataPoint> column = new ArrayList<>(32);
|
||||
|
||||
@Nullable
|
||||
DataPoint previous = null;
|
||||
@@ -118,15 +121,43 @@ public class FullBuilder extends LodBuilder
|
||||
|
||||
String material = this.worldInterface.getMaterialAt(worldX, highWorldY, worldZ);
|
||||
|
||||
String compositeKey = biome + "|" + material + "|" + this.worldInterface.getBlockStateAsStringAt(worldX, highWorldY, worldZ);
|
||||
if (this.worldInterface.isBeacon(worldX, highWorldY, worldZ)) {
|
||||
beacons.add(new Beacon(
|
||||
worldX,
|
||||
highWorldY,
|
||||
worldZ,
|
||||
worldInterface.getBeaconColor(
|
||||
worldX,
|
||||
highWorldY,
|
||||
worldZ
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Integer id = mapMap.get(compositeKey);
|
||||
if (sampleBiomes3d) {
|
||||
biome = this.worldInterface.getBiomeAt(worldX, highWorldY, worldZ);
|
||||
}
|
||||
|
||||
Map<String, Map<String, Integer>> biomeMap = mapMap.get(biome);
|
||||
if (biomeMap == null) {
|
||||
biomeMap = new HashMap<>();
|
||||
mapMap.put(biome, biomeMap);
|
||||
}
|
||||
|
||||
Map<String, Integer> materialMap = biomeMap.get(material);
|
||||
if (materialMap == null) {
|
||||
materialMap = new HashMap<>();
|
||||
biomeMap.put(material, materialMap);
|
||||
}
|
||||
|
||||
String blockState = this.worldInterface.getBlockStateAsStringAt(worldX, highWorldY, worldZ);
|
||||
|
||||
Integer id = materialMap.get(blockState);
|
||||
|
||||
if (id == null) {
|
||||
idMappings.add(new IdMapping(biome, material, this.worldInterface.getBlockPropertiesAt(worldX, highWorldY, worldZ)));
|
||||
id = idMappings.size() - 1;
|
||||
mapMap.put(compositeKey, id);
|
||||
materialMap.put(blockState, id);
|
||||
}
|
||||
|
||||
DataPoint point;
|
||||
@@ -144,10 +175,8 @@ public class FullBuilder extends LodBuilder
|
||||
point.setHeight(thisStep);
|
||||
point.setMappingId(id);
|
||||
|
||||
if (highWorldY + 1 < maxY) {
|
||||
point.setSkyLight(this.worldInterface.getSkyLightAt(worldX, highWorldY + 1, worldZ));
|
||||
point.setBlockLight(this.worldInterface.getBlockLightAt(worldX, highWorldY + 1, worldZ));
|
||||
}
|
||||
point.setSkyLight(this.worldInterface.getSkyLightAt(worldX, highWorldY + 1, worldZ));
|
||||
point.setBlockLight(this.worldInterface.getBlockLightAt(worldX, highWorldY + 1, worldZ));
|
||||
|
||||
if (material.equals("minecraft:air") || material.equals("minecraft:void_air")) {
|
||||
// Start by filling the top of the column with air, then jump down to the top layer.
|
||||
@@ -159,6 +188,23 @@ public class FullBuilder extends LodBuilder
|
||||
} else {
|
||||
// Encountered air that is below a non-air block. Set yStep=1 to avoid stretching the air into the ground or sea.
|
||||
yStep = 1;
|
||||
|
||||
if (performUnderglowHack && previous != null && previous.getHeight() > 2) {
|
||||
// Retract 2 block. We need a gap between the end of the top data point and the bottom one to prevent DH from merging them.
|
||||
previous.setStartY(previous.getStartY() + 2);
|
||||
previous.setHeight(previous.getHeight() - 2);
|
||||
|
||||
DataPoint bottomBlock = new DataPoint();
|
||||
bottomBlock.setMappingId(previous.getMappingId());
|
||||
bottomBlock.setStartY(relativeY + 1);
|
||||
bottomBlock.setHeight(1);
|
||||
bottomBlock.setSkyLight(this.worldInterface.getSkyLightAt(worldX, lowWorldY, worldZ));
|
||||
bottomBlock.setBlockLight(this.worldInterface.getBlockLightAt(worldX, lowWorldY, worldZ));
|
||||
|
||||
column.add(bottomBlock);
|
||||
|
||||
previous = bottomBlock;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Entered a new material. Reset yStep in case we came from air with yStep=1.
|
||||
@@ -173,6 +219,6 @@ public class FullBuilder extends LodBuilder
|
||||
}
|
||||
}
|
||||
|
||||
return new Lod(this.worldInterface, this.position, idMappings, columns);
|
||||
return new Lod(this.worldInterface, this.position, idMappings, columns, beacons);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,11 @@ import no.jckf.dhsupport.core.bytestream.Encoder;
|
||||
|
||||
public abstract class Message implements Encodable, Decodable
|
||||
{
|
||||
public int bufferSize()
|
||||
{
|
||||
return 64;
|
||||
}
|
||||
|
||||
public void encode(Encoder encoder)
|
||||
{
|
||||
throw new UnsupportedOperationException();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,10 @@ public class FullDataChunkMessage extends PluginMessage
|
||||
|
||||
protected byte[] data;
|
||||
|
||||
protected int offset;
|
||||
|
||||
protected int length;
|
||||
|
||||
protected boolean isFirst = true;
|
||||
|
||||
public void setBufferId(int bufferId)
|
||||
@@ -38,9 +42,11 @@ public class FullDataChunkMessage extends PluginMessage
|
||||
return this.bufferId;
|
||||
}
|
||||
|
||||
public void setData(byte[] data)
|
||||
public void setData(byte[] data, int offset, int length)
|
||||
{
|
||||
this.data = data;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public byte[] getData()
|
||||
@@ -58,12 +64,18 @@ public class FullDataChunkMessage extends PluginMessage
|
||||
return this.isFirst;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int bufferSize()
|
||||
{
|
||||
return 32 + this.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(Encoder encoder)
|
||||
{
|
||||
encoder.writeInt(this.bufferId);
|
||||
encoder.writeInt(this.data.length);
|
||||
encoder.write(this.data);
|
||||
encoder.writeInt(this.length);
|
||||
encoder.write(this.data, this.offset, this.length);
|
||||
encoder.writeBoolean(this.isFirst);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* DH Support, server-side support for Distant Horizons.
|
||||
* Copyright (C) 2024 Jim C K Flaten
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package no.jckf.dhsupport.core.message.plugin;
|
||||
|
||||
import no.jckf.dhsupport.core.bytestream.Encoder;
|
||||
@@ -40,6 +58,12 @@ public class FullDataPartialUpdateMessage extends PluginMessage
|
||||
return this.beacons;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int bufferSize()
|
||||
{
|
||||
return 32 + this.beacons.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(Encoder encoder)
|
||||
{
|
||||
|
||||
@@ -48,6 +48,12 @@ public class FullDataSourceResponseMessage extends TrackablePluginMessage
|
||||
return this.beacons;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int bufferSize()
|
||||
{
|
||||
return this.bufferId == null ? super.bufferSize() : 16 + this.beacons.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(Encoder encoder)
|
||||
{
|
||||
|
||||
@@ -1,63 +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;
|
||||
|
||||
public class LevelInitMessage extends PluginMessage
|
||||
{
|
||||
protected String key;
|
||||
|
||||
protected long time;
|
||||
|
||||
public void setKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getKey()
|
||||
{
|
||||
return this.key;
|
||||
}
|
||||
|
||||
public void setTime(long time)
|
||||
{
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public long getTime()
|
||||
{
|
||||
return this.time;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(Encoder encoder)
|
||||
{
|
||||
encoder.writeShortString(this.key);
|
||||
encoder.writeLong(this.time);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(Decoder decoder)
|
||||
{
|
||||
this.key = decoder.readShortString();
|
||||
this.time = decoder.readLong();
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* DH Support, server-side support for Distant Horizons.
|
||||
* Copyright (C) 2024 Jim C K Flaten
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package no.jckf.dhsupport.core.message.plugin;
|
||||
|
||||
import no.jckf.dhsupport.core.bytestream.Decoder;
|
||||
|
||||
public class LevelInitRequestMessage extends PluginMessage
|
||||
{
|
||||
protected String worldKey;
|
||||
|
||||
public String getWorldKey()
|
||||
{
|
||||
return worldKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(Decoder decoder)
|
||||
{
|
||||
this.worldKey = decoder.readShortString();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+14
-12
@@ -22,6 +22,7 @@ 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
|
||||
{
|
||||
@@ -31,7 +32,7 @@ public class RemotePlayerConfigMessage extends PluginMessage
|
||||
DhsConfig.BORDER_CENTER_X,
|
||||
DhsConfig.BORDER_CENTER_Z,
|
||||
DhsConfig.BORDER_RADIUS,
|
||||
DhsConfig.FULL_DATA_REQUEST_CONCURRENCY_LIMIT,
|
||||
DhsConfig.CLIENT_LOD_REQUEST_CONCURRENCY_LIMIT,
|
||||
DhsConfig.REAL_TIME_UPDATES_ENABLED,
|
||||
DhsConfig.REAL_TIME_UPDATE_RADIUS,
|
||||
DhsConfig.LOGIN_DATA_SYNC_ENABLED,
|
||||
@@ -44,7 +45,7 @@ public class RemotePlayerConfigMessage extends PluginMessage
|
||||
|
||||
protected boolean distantGenerationEnabled;
|
||||
|
||||
protected int fullDataRequestConcurrencyLimit;
|
||||
protected int clientLodRequestConcurrencyLimit;
|
||||
|
||||
protected boolean realTimeUpdatesEnabled;
|
||||
|
||||
@@ -84,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 this.fullDataRequestConcurrencyLimit;
|
||||
return this.clientLodRequestConcurrencyLimit;
|
||||
}
|
||||
|
||||
public void setRealTimeUpdatesEnabled(boolean enabled)
|
||||
@@ -187,14 +188,15 @@ public class RemotePlayerConfigMessage extends PluginMessage
|
||||
@Override
|
||||
public void encode(Encoder encoder)
|
||||
{
|
||||
encoder.writeBoolean(this.distantGenerationEnabled);
|
||||
// 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.writeInt(this.borderCenterX);
|
||||
encoder.writeInt(this.borderCenterZ);
|
||||
encoder.writeInt(this.borderRadius);
|
||||
|
||||
encoder.writeInt(this.fullDataRequestConcurrencyLimit);
|
||||
encoder.writeInt(this.clientLodRequestConcurrencyLimit);
|
||||
|
||||
encoder.writeBoolean(this.realTimeUpdatesEnabled);
|
||||
encoder.writeInt(this.realTimeUpdateRadius);
|
||||
@@ -209,14 +211,14 @@ public class RemotePlayerConfigMessage extends PluginMessage
|
||||
@Override
|
||||
public void decode(Decoder decoder)
|
||||
{
|
||||
this.distantGenerationEnabled = decoder.readBoolean();
|
||||
this.distantGenerationEnabled = GeneratorPlan.byOrdinal(decoder.readByte()).generationEnabled;
|
||||
this.renderDistance = decoder.readInt();
|
||||
|
||||
this.borderCenterX = decoder.readInt();
|
||||
this.borderCenterZ = decoder.readInt();
|
||||
this.borderRadius = decoder.readInt();
|
||||
|
||||
this.fullDataRequestConcurrencyLimit = decoder.readInt();
|
||||
this.clientLodRequestConcurrencyLimit = decoder.readInt();
|
||||
|
||||
this.realTimeUpdatesEnabled = decoder.readBoolean();
|
||||
this.realTimeUpdateRadius = decoder.readInt();
|
||||
@@ -241,7 +243,7 @@ public class RemotePlayerConfigMessage extends PluginMessage
|
||||
this.borderCenterZ = borderCenterZ == null ? 0 : borderCenterZ;
|
||||
this.borderRadius = borderRadius == null ? 0 : borderRadius;
|
||||
|
||||
this.fullDataRequestConcurrencyLimit = config.getInt(DhsConfig.FULL_DATA_REQUEST_CONCURRENCY_LIMIT);
|
||||
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);
|
||||
@@ -264,7 +266,7 @@ public class RemotePlayerConfigMessage extends PluginMessage
|
||||
config.set(DhsConfig.BORDER_CENTER_Z, this.borderCenterZ);
|
||||
config.set(DhsConfig.BORDER_RADIUS, this.borderRadius);
|
||||
|
||||
config.set(DhsConfig.FULL_DATA_REQUEST_CONCURRENCY_LIMIT, this.fullDataRequestConcurrencyLimit);
|
||||
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);
|
||||
|
||||
@@ -20,6 +20,8 @@ package no.jckf.dhsupport.core.scheduling;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public interface Scheduler
|
||||
@@ -28,9 +30,23 @@ public interface Scheduler
|
||||
|
||||
<U> CompletableFuture<U> runOnMainThread(Supplier<U> supplier);
|
||||
|
||||
<U> CompletableFuture<U> runOnMainThreadDelayed(Supplier<U> supplier, long delayTicks);
|
||||
|
||||
<U> CompletableFuture<U> runOnMainThreadDelayed(Supplier<U> supplier, long delayAmount, TimeUnit delayUnit);
|
||||
|
||||
<U> CompletableFuture<U> runOnRegionThread(UUID worldId, int x, int z, Supplier<U> supplier);
|
||||
|
||||
<U> CompletableFuture<U> runOnRegionThreadDelayed(UUID worldId, int x, int z, Supplier<U> supplier, long delayTicks);
|
||||
|
||||
<U> CompletableFuture<U> runOnRegionThreadDelayed(UUID worldId, int x, int z, Supplier<U> supplier, long delayAmount, TimeUnit delayUnit);
|
||||
|
||||
<U> CompletableFuture<U> runOnSeparateThread(Supplier<U> supplier);
|
||||
|
||||
<U> CompletableFuture<U> runOnSeparateThreadDelayed(Supplier<U> supplier, long delayTicks);
|
||||
|
||||
<U> CompletableFuture<U> runOnSeparateThreadDelayed(Supplier<U> supplier, long delayAmount, TimeUnit delayUnit);
|
||||
|
||||
void cancelTasks();
|
||||
|
||||
Executor getExecutor();
|
||||
}
|
||||
@@ -0,0 +1,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;
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,8 @@
|
||||
package no.jckf.dhsupport.core.world;
|
||||
|
||||
import no.jckf.dhsupport.core.configuration.Configurable;
|
||||
import no.jckf.dhsupport.core.dataobject.Beacon;
|
||||
|
||||
import java.util.Collection;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -34,14 +33,38 @@ public interface WorldInterface extends Configurable
|
||||
|
||||
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);
|
||||
@@ -54,19 +77,37 @@ public interface WorldInterface extends Configurable
|
||||
|
||||
int getSeaLevel();
|
||||
|
||||
int getSpawnX();
|
||||
|
||||
int getSpawnY();
|
||||
|
||||
int getSpawnZ();
|
||||
|
||||
int getHighestYAt(int x, int z);
|
||||
|
||||
String getBiomeAt(int x, int z);
|
||||
|
||||
String getBiomeAt(int x, int y, int z);
|
||||
|
||||
String getMaterialAt(int x, int y, int z);
|
||||
|
||||
/**
|
||||
* Get the string representation of the block state at the given coordinates.
|
||||
* Block state properties are ordered in ascending order.
|
||||
*/
|
||||
String getBlockStateAsStringAt(int x, int y, int z);
|
||||
|
||||
/**
|
||||
* Get block properties as key/value pairs.
|
||||
* Ordered in ascending order.
|
||||
*/
|
||||
Map<String, String> getBlockPropertiesAt(int x, int y, int z);
|
||||
|
||||
byte getBlockLightAt(int x, int y, int z);
|
||||
|
||||
byte getSkyLightAt(int x, int y, int z);
|
||||
|
||||
Collection<Beacon> getBeaconsInChunk(int x, int z);
|
||||
boolean isBeacon(int x, int y, int z);
|
||||
|
||||
int getBeaconColor(int x, int y, int z);
|
||||
}
|
||||
+256
-106
@@ -1,106 +1,256 @@
|
||||
# Maximum supported render distance. 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
|
||||
|
||||
# Number of parallel LOD requests per client.
|
||||
# Required.
|
||||
# Default: 20
|
||||
full_data_request_concurrency_limit: 20
|
||||
|
||||
# Send LOD updates to players in real-time?
|
||||
# Required.
|
||||
# Default: true
|
||||
real_time_updates_enabled: true
|
||||
|
||||
# Currently unsupported.
|
||||
# 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
|
||||
|
||||
# Currently unsupported.
|
||||
# Required.
|
||||
# Default: 128
|
||||
login_data_sync_radius: 128
|
||||
|
||||
# Currently unsupported.
|
||||
# Required.
|
||||
# Default: 50
|
||||
login_data_sync_rc_limit: 50
|
||||
|
||||
# Currently unsupported.
|
||||
# Required.
|
||||
# Default: 0
|
||||
max_data_transfer_speed: 0
|
||||
|
||||
# Select which LOD builder to use.
|
||||
# Valid options are: FullBuilder, FastOverworldBuilder, and None.
|
||||
# Required.
|
||||
# Default: FullBuilder
|
||||
builder_type: FullBuilder
|
||||
|
||||
# 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.
|
||||
# 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: false
|
||||
#include_non-colliding_top_layer: true
|
||||
|
||||
# Should beacons be included in LOD data?
|
||||
# It's recommended to leave this disabled for the initial LOD generation, then enable it afterward.
|
||||
# Optional.
|
||||
# Default: false
|
||||
#include_beacons: true
|
||||
|
||||
# When running DHS on a proxied server, set this to a value that is unique to this specific server.
|
||||
# Optional.
|
||||
#level_key_prefix:
|
||||
|
||||
# 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
|
||||
|
||||
# When using the FastOverworldBuilder, should LODs always contain data down to at least sea level?
|
||||
# Enable if your world has floating islands or mountains/structures with large overhangs above sea level.
|
||||
# Optional.
|
||||
# Default: false
|
||||
#scan_to_sea_level: true
|
||||
|
||||
# Limit LOD generation to within this area.
|
||||
# Optional.
|
||||
#border_center_x: 0
|
||||
#border_center_z: 0
|
||||
#border_radius: 5000
|
||||
|
||||
# It might make sense to have a lower view distance in the nether, since visibility will be low simply because of
|
||||
# the level design anyway.
|
||||
world_nether:
|
||||
render_distance: 64
|
||||
|
||||
# Number of worker threads used to generate LODs.
|
||||
# Does not affect Folia servers.
|
||||
# Required.
|
||||
# Default: 2
|
||||
scheduler_threads: 2
|
||||
|
||||
# Don't touch :)
|
||||
config_version: 5
|
||||
# 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
|
||||
@@ -1,8 +1,16 @@
|
||||
name: DHSupport
|
||||
version: ${project.version}
|
||||
main: ${main.class}
|
||||
api-version: ${mc.api-version}
|
||||
mc-version: ${mc.api-version}${mc.api-version-patch}
|
||||
version: ${version}
|
||||
main: ${mainClass}
|
||||
api-version: ${mcApiVersion}
|
||||
prefix: DHS
|
||||
folia-supported: true
|
||||
author: Jckf
|
||||
commands:
|
||||
dhs:
|
||||
description: Manage DH Support.
|
||||
usage: /dhs
|
||||
permission: distant_horizons.admin
|
||||
dh:
|
||||
description: Not used.
|
||||
usage: /dh
|
||||
permission: distant_horizons.admin
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
Executable
+34
@@ -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}"
|
||||
@@ -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
|
||||
}
|
||||
Executable
+96
@@ -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
|
||||
Executable
+22
@@ -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."
|
||||
Executable
+433
@@ -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."
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user