Compare commits

..
6 Commits
152 changed files with 1491 additions and 10033 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
.gitea/CODEOWNERS linguist-detectable=false
+3
View File
@@ -0,0 +1,3 @@
# Pull requests request review from the eligible staff team(s).
# One approval from either an Admin or Developer satisfies branch protection.
* @trueog/Admins @trueog/Developers
@@ -107,7 +107,7 @@ jobs:
files: '["${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar"]'
name: ${{ steps.release-info.outputs.tag_name }}
changelog: ${{ steps.release-artifact.outputs.body }}
game_versions: 26.1.2, 26.1.1, 26.1, 1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
game_versions: 1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
version_type: ${{ steps.parse-release-type.outputs.release_type }}
loaders: bukkit, spigot, paper, purpur
dependencies: ${{ inputs.modrinth_dependencies }}
@@ -121,7 +121,7 @@ jobs:
changelog: ${{ steps.release-artifact.outputs.body }}
changelog_type: markdown
display_name: ${{ steps.release-info.outputs.tag_name }}
game_versions: 26.1.2, 26.1.1, 26.1, 1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
game_versions: 1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
release_type: ${{ steps.parse-release-type.outputs.release_type }}
project_relations: ${{ inputs.dbo_project_relations }}
file_path: ${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar
@@ -136,5 +136,5 @@ jobs:
channel: ${{ steps.parse-release-type.outputs.release_type }}
files: '[{"path": "${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar", "platforms": ["PAPER"]}]'
description: ${{ steps.release-artifact.outputs.body }}
platform_dependencies: '{"PAPER": ["1.18.2-26.1.2"]}'
platform_dependencies: '{"PAPER": ["1.18.2-1.21.11"]}'
plugin_dependencies: ${{ inputs.hangar_plugin_dependencies }}
+4
View File
@@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready
-117
View File
@@ -1,117 +0,0 @@
import org.apache.tools.ant.filters.ReplaceTokens
plugins {
id 'org.mvplugins.multiverse-plugin' version '1.2.2'
}
group = 'org.mvplugins.multiverse.core'
description = 'Multiverse-Core'
repositories {
maven {
name = 'aikar repo'
url = uri('https://repo.aikar.co/content/groups/aikar/')
}
maven {
name = 'glaremasters repo'
url = 'https://repo.glaremasters.me/repository/towny/'
}
maven {
name = "helpchatRepoReleases"
url = uri("https://repo.helpch.at/releases/")
}
}
configure(apiDependencies) {
serverApiVersion = '1.21.11-R0.1-SNAPSHOT'
mockBukkitServerApiVersion = '1.21'
mockBukkitVersion = '4.110.0'
}
dependencies {
// Economy
externalPlugin('com.github.MilkBowl:VaultAPI:1.7.1') {
exclude group: 'org.bukkit', module: 'bukkit'
}
// PlaceholderAPI
externalPlugin 'me.clip:placeholderapi:2.11.6'
// Command Framework
shadowed 'co.aikar:acf-paper:0.5.1-SNAPSHOT'
// Config
shadowed('io.github.townyadvanced.commentedconfiguration:CommentedConfiguration:1.0.1') {
exclude group: 'org.spigotmc', module: 'spigot-api'
}
// Utils
shadowed 'io.vavr:vavr:0.10.7'
shadowed 'org.glassfish.hk2:hk2-locator:3.1.1'
shadowed('org.glassfish.hk2:hk2-inhabitant-generator:3.1.1') {
exclude group: 'org.apache.maven', module: 'maven-core'
}
shadowed('com.dumptruckman.minecraft:Logging:1.1.1') {
exclude group: 'junit', module: 'junit'
}
shadowed 'de.themoep.idconverter:mappings:1.2-SNAPSHOT'
shadowed('org.bstats:bstats-bukkit:3.1.0') {
exclude group: 'org.bukkit', module: 'bukkit'
}
shadowed 'net.minidev:json-smart:2.5.2'
shadowed 'org.jetbrains:annotations:26.0.2'
shadowed 'io.papermc:paperlib:1.0.8'
// Tests
testImplementation('com.googlecode.json-simple:json-simple:1.1.1') {
exclude group: 'junit', module: 'junit'
}
testImplementation("org.hamcrest:hamcrest:3.0")
}
ext.bitlyAccessToken = System.getenv('BITLY_ACCESS_TOKEN') ?: 'bitly-access-token'
tasks.register('prepareSource', Sync) {
inputs.property 'bitlyAccessToken', bitlyAccessToken
from sourceSets.main.java
into "$buildDir/src"
filter(ReplaceTokens, tokens: [
'bitly-access-token': bitlyAccessToken,
])
}
shadowJar {
relocate 'co.aikar', 'org.mvplugins.multiverse.external.acf'
relocate 'com.dumptruckman.minecraft.util.Logging', 'org.mvplugins.multiverse.core.utils.CoreLogging'
relocate 'com.dumptruckman.minecraft.util.DebugLog', 'org.mvplugins.multiverse.core.utils.DebugFileLogger'
relocate 'de.themoep.idconverter', 'org.mvplugins.multiverse.external.idconverter'
relocate 'io.github.townyadvanced.commentedconfiguration', 'org.mvplugins.multiverse.external.commentedconfiguration'
relocate 'org.bstats', 'org.mvplugins.multiverse.external.bstats'
relocate 'com.sun', 'org.mvplugins.multiverse.external.sun'
relocate 'net.minidev', 'org.mvplugins.multiverse.external.minidev'
relocate 'org.objectweb', 'org.mvplugins.multiverse.external.objectweb'
relocate 'io.vavr', 'org.mvplugins.multiverse.external.vavr'
relocate 'jakarta', 'org.mvplugins.multiverse.external.jakarta'
relocate 'javassist', 'org.mvplugins.multiverse.external.javassist'
relocate 'org.aopalliance', 'org.mvplugins.multiverse.external.aopalliance'
relocate 'org.glassfish', 'org.mvplugins.multiverse.external.glassfish'
relocate 'org.jvnet', 'org.mvplugins.multiverse.external.jvnet'
relocate 'org.intellij', 'org.mvplugins.multiverse.external.intellij'
relocate 'org.jetbrains', 'org.mvplugins.multiverse.external.jetbrains'
relocate 'io.papermc.lib', 'org.mvplugins.multiverse.external.paperlib'
}
publishing {
repositories {
maven {
name = "GitHubPackages"
url = "https://maven.pkg.github.com/Multiverse/Multiverse-Core"
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
}
}
+364
View File
@@ -0,0 +1,364 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import groovy.util.Node
import groovy.util.NodeList
import org.apache.tools.ant.filters.ReplaceTokens
import org.gradle.api.artifacts.Configuration
import org.gradle.api.credentials.PasswordCredentials
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.api.tasks.Sync
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.api.tasks.testing.Test
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.external.javadoc.StandardJavadocDocletOptions
import org.gradle.jvm.tasks.Jar
import org.gradle.language.jvm.tasks.ProcessResources
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
`java-library`
eclipse
`maven-publish`
id("com.gradleup.shadow") version "8.3.9"
kotlin("jvm") version "2.0.21"
}
group = "org.mvplugins.multiverse.core"
version = System.getenv("GITHUB_VERSION") ?: "5.3.4.17"
description = "Multiverse-Core"
val sourceSets = extensions.getByType<SourceSetContainer>()
val serverApi: Configuration by configurations.creating
val externalPlugin: Configuration by configurations.creating
val shadowed: Configuration by configurations.creating
repositories {
mavenLocal()
mavenCentral()
maven {
url = uri("https://hub.spigotmc.org/nexus/content/repositories/snapshots/")
}
maven {
url = uri("https://repo.onarandombox.com/content/groups/public")
}
maven {
url = uri("https://hub.spigotmc.org/nexus/content/groups/public/")
}
maven {
url = uri("https://jitpack.io")
}
maven {
url = uri("https://repo.minebench.de/")
}
maven {
url = uri("https://repo.maven.apache.org/maven2/")
}
maven {
name = "aikar repo"
url = uri("https://repo.aikar.co/content/groups/aikar/")
}
maven {
name = "glaremasters repo"
url = uri("https://repo.glaremasters.me/repository/towny/")
}
maven {
name = "helpchatRepoReleases"
url = uri("https://repo.helpch.at/releases/")
}
maven {
name = "papermc repo"
url = uri("https://repo.papermc.io/repository/maven-public/")
}
// Purpur API
maven("https://repo.purpurmc.org/snapshots")
}
// Configuration wiring like in the Groovy build
configurations["compileOnly"].extendsFrom(serverApi)
configurations["runtimeClasspath"].extendsFrom(serverApi)
configurations["implementation"].extendsFrom(externalPlugin)
configurations["compileClasspath"].extendsFrom(shadowed)
configurations["testCompileClasspath"].extendsFrom(shadowed)
configurations["testRuntimeClasspath"].extendsFrom(shadowed)
dependencies {
// Server API: Purpur 1.19.4
serverApi("org.purpurmc.purpur:purpur-api:1.19.4-R0.1-SNAPSHOT")
// Economy
externalPlugin("com.github.MilkBowl:VaultAPI:1.7.1") {
exclude(group = "org.bukkit", module = "bukkit")
}
// PlaceholderAPI
externalPlugin("me.clip:placeholderapi:2.11.6")
// Command Framework
shadowed("co.aikar:acf-paper:0.5.1-SNAPSHOT")
// Config
shadowed("io.github.townyadvanced.commentedconfiguration:CommentedConfiguration:1.0.1") {
exclude(group = "org.spigotmc", module = "spigot-api")
}
// Utils
shadowed("io.vavr:vavr:0.10.4")
shadowed("org.glassfish.hk2:hk2-locator:3.0.3")
shadowed("org.glassfish.hk2:hk2-inhabitant-generator:3.0.3") {
exclude(group = "org.apache.maven", module = "maven-core")
}
shadowed("com.dumptruckman.minecraft:Logging:1.1.1") {
exclude(group = "junit", module = "junit")
}
shadowed("de.themoep.idconverter:mappings:1.2-SNAPSHOT")
shadowed("org.bstats:bstats-bukkit:3.1.0") {
exclude(group = "org.bukkit", module = "bukkit")
}
shadowed("net.minidev:json-smart:2.4.9")
shadowed("org.jetbrains:annotations:22.0.0")
shadowed("io.papermc:paperlib:1.0.8")
// Tests
testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.0.21")
testImplementation("com.github.seeseemelk:MockBukkit-v1.19:3.1.0")
testImplementation("com.googlecode.json-simple:json-simple:1.1.1") {
exclude(group = "junit", module = "junit")
}
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("com.natpryce:hamkrest:1.8.0.1")
testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
// Annotation Processors
annotationProcessor("org.glassfish.hk2:hk2-metadata-generator:3.0.3")
testAnnotationProcessor("org.glassfish.hk2:hk2-metadata-generator:3.0.3")
}
java {
// Plugin is compiled for Java 17 (GraalVM 17-friendly)
toolchain.languageVersion.set(JavaLanguageVersion.of(17))
withSourcesJar()
withJavadocJar()
}
val bitlyAccessToken: String = System.getenv("BITLY_ACCESS_TOKEN") ?: "bitly-access-token"
// === Token-filtered source generation (prepareSource) ===
val prepareSource by tasks.registering(Sync::class) {
inputs.property("bitlyAccessToken", bitlyAccessToken)
from(sourceSets["main"].java)
// avoid deprecated buildDir getter
into(layout.buildDirectory.dir("src"))
filter<ReplaceTokens>(
"tokens" to mapOf(
"bitly-access-token" to bitlyAccessToken,
),
)
}
// Use ONLY the filtered sources for compileJava (fixes duplicate classes)
tasks.named<JavaCompile>("compileJava") {
dependsOn(prepareSource)
source = prepareSource.get().outputs.files.asFileTree
sourceCompatibility = JavaVersion.VERSION_17.toString()
targetCompatibility = JavaVersion.VERSION_17.toString()
}
tasks.named<JavaCompile>("compileTestJava") {
sourceCompatibility = JavaVersion.VERSION_17.toString()
targetCompatibility = JavaVersion.VERSION_17.toString()
}
tasks.withType<JavaCompile>().configureEach {
options.encoding = "UTF-8"
options.compilerArgs.add(
"-Aorg.glassfish.hk2.metadata.location=META-INF/hk2-locator/Multiverse-Core",
)
}
tasks.withType<Javadoc>().configureEach {
(options as StandardJavadocDocletOptions).encoding = "UTF-8"
}
tasks.named<KotlinCompile>("compileTestKotlin") {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
javaParameters.set(true)
}
}
// We're not using Kotlin in the plugin itself, just tests!
tasks.named<KotlinCompile>("compileKotlin") {
enabled = false
}
configurations
.filterNot { it.name.startsWith("test") }
.forEach {
it.exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8")
}
tasks.named<ProcessResources>("processResources") {
val props = mapOf("version" to project.version.toString())
inputs.properties(props)
filteringCharset = "UTF-8"
filesMatching("plugin.yml") {
expand(props)
}
// Never up-to-date: tests depend on fresh version expansion
outputs.upToDateWhen { false }
}
tasks.named<Javadoc>("javadoc") {
source = sourceSets["main"].allJava
classpath = configurations["compileClasspath"]
}
// Removed: configurations["api"].isCanBeResolved = true (deprecated in Gradle 8/9)
val jarTask = tasks.named<Jar>("jar")
val shadowJarTask = tasks.named<ShadowJar>("shadowJar")
listOf("apiElements", "runtimeElements").forEach { configurationName ->
configurations.named(configurationName) {
outgoing.artifacts.removeIf { artifact ->
artifact.buildDependencies.getDependencies(null).contains(jarTask.get())
}
outgoing.artifact(shadowJarTask)
}
}
tasks.named<ShadowJar>("shadowJar") {
relocate("co.aikar", "org.mvplugins.multiverse.external.acf")
relocate("com.dumptruckman.minecraft.util.Logging", "org.mvplugins.multiverse.core.utils.CoreLogging")
relocate("com.dumptruckman.minecraft.util.DebugLog", "org.mvplugins.multiverse.core.utils.DebugFileLogger")
relocate("de.themoep.idconverter", "org.mvplugins.multiverse.external.idconverter")
relocate("io.github.townyadvanced.commentedconfiguration", "org.mvplugins.multiverse.external.commentedconfiguration")
relocate("me.main__.util", "org.mvplugins.multiverse.external.serializationconfig")
relocate("org.bstats", "org.mvplugins.multiverse.external.bstats")
relocate("com.sun", "org.mvplugins.multiverse.external.sun")
relocate("net.minidev", "org.mvplugins.multiverse.external.minidev")
relocate("org.objectweb", "org.mvplugins.multiverse.external.objectweb")
relocate("io.vavr", "org.mvplugins.multiverse.external.vavr")
relocate("jakarta", "org.mvplugins.multiverse.external.jakarta")
relocate("javassist", "org.mvplugins.multiverse.external.javassist")
relocate("org.aopalliance", "org.mvplugins.multiverse.external.aopalliance")
relocate("org.glassfish", "org.mvplugins.multiverse.external.glassfish")
relocate("org.jvnet", "org.mvplugins.multiverse.external.jvnet")
relocate("org.intellij", "org.mvplugins.multiverse.external.intellij")
relocate("org.jetbrains", "org.mvplugins.multiverse.external.jetbrains")
relocate("io.papermc.lib", "org.mvplugins.multiverse.external.paperlib")
configurations = listOf(project.configurations["shadowed"])
archiveClassifier.set("")
dependencies {
exclude { dependency ->
dependency.moduleGroup == "org.jetbrains.kotlin"
}
}
}
tasks.named("build") {
dependsOn("shadowJar")
}
tasks.named<Jar>("jar") {
enabled = false
}
tasks.named<Test>("test") {
useJUnitPlatform()
testLogging {
exceptionFormat = TestExceptionFormat.FULL
}
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
pom.withXml {
val pomNode = asNode()
val dependenciesNode = (
(pomNode.get("dependencies") as? NodeList)
?.firstOrNull() as? Node
) ?: (pomNode.appendNode("dependencies") as Node)
val dependencyNodes = dependenciesNode.children().filterIsInstance<Node>().toList()
// Remove Kotlin dependency
dependencyNodes
.filter { dependencyNode ->
val groupIdNodes = dependencyNode.get("groupId") as? NodeList ?: return@filter false
val groupId = (groupIdNodes.firstOrNull() as? Node)?.text()
groupId == "org.jetbrains.kotlin"
}
.forEach { dependencyNode ->
dependenciesNode.remove(dependencyNode)
}
// Switch runtime deps to provided
dependencyNodes.forEach { dependencyNode ->
val scopeNodes = dependencyNode.get("scope") as? NodeList ?: return@forEach
val scopeNode = scopeNodes.firstOrNull() as? Node ?: return@forEach
if (scopeNode.text() == "runtime") {
scopeNode.setValue("provided")
}
}
// Add server API (Purpur) to pom as provided
configurations["serverApi"].allDependencies.forEach { dependency ->
val depNode = dependenciesNode.appendNode("dependency")
depNode.appendNode("groupId", dependency.group)
depNode.appendNode("artifactId", dependency.name)
depNode.appendNode("version", dependency.version)
depNode.appendNode("scope", "provided")
}
}
}
}
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/Multiverse/Multiverse-Core")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
maven {
name = "multiverse"
val releasesRepoUrl = uri("https://repo.dumptruckman.com/multiverse-releases")
val snapshotsRepoUrl = uri("https://repo.dumptruckman.com/multiverse-snapshots")
url = if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl
credentials(PasswordCredentials::class)
}
maven {
// todo: remove before mv5 release
name = "multiverseBeta"
url = uri("https://repo.c0ding.party/multiverse-beta")
credentials(PasswordCredentials::class)
}
}
}
Binary file not shown.
+2 -1
View File
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+21 -13
View File
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -83,10 +85,9 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# 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"'
# 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
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -133,10 +134,13 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
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.
@@ -144,7 +148,7 @@ 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=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# 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, 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" \
Vendored
+12 -10
View File
@@ -13,6 +13,8 @@
@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 ##########################################################################
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
-14
View File
@@ -1,14 +0,0 @@
/*
* This file was generated by the Gradle 'init' task.
*/
pluginManagement {
repositories {
gradlePluginPortal()
maven {
url = uri('https://repo.onarandombox.com/multiverse-releases')
}
}
}
rootProject.name = 'multiverse-core'
+11
View File
@@ -0,0 +1,11 @@
pluginManagement {
repositories {
gradlePluginPortal()
maven {
url = uri("https://repo.onarandombox.com/multiverse-releases")
}
}
}
rootProject.name = "Multiverse-Core"
@@ -126,7 +126,6 @@ public class MultiverseCore extends MultiverseModule {
MultiverseCoreApi.shutdown();
shutdownDependencyInjection();
PluginServiceLocatorFactory.get().shutdown();
Logging.info("- Disabled");
Logging.shutdown();
}
@@ -226,12 +225,11 @@ public class MultiverseCore extends MultiverseModule {
* Logs the enable message.
*/
private void logEnableMessage() {
Logging.config("\u001B[32mVersion %s (API v%s) Enabled - By %s\u001B[0m",
Logging.config("\u001B[32mVersion %s (API v%s) Enabled - By %s\u001B[39m",
this.getDescription().getVersion(), getVersionAsNumber(), StringFormatter.joinAnd(getDescription().getAuthors()));
if (configProvider.get().isShowingDonateMessage()) {
Logging.config("\u001B[32mLoving Multiverse-Core? Please consider supporting the project with a " +
"small donation: https://github.com/sponsors/Multiverse\u001B[0m");
Logging.config("\u001B[32mLoving Multiverse-Core? Please consider supporting the project with a small donation: https://github.com/sponsors/Multiverse\u001B[39m");
}
}
@@ -24,7 +24,7 @@ import java.util.function.Consumer;
/**
* Provides access to the MultiverseCore API.
*/
public final class MultiverseCoreApi {
public class MultiverseCoreApi {
private static MultiverseCoreApi instance;
private static final List<Consumer<MultiverseCoreApi>> whenLoadedCallbacks = new ArrayList<>();
@@ -1,7 +1,6 @@
package org.mvplugins.multiverse.core;
import com.google.common.collect.Lists;
import io.vavr.control.Option;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
@@ -13,13 +12,11 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.economy.MVEconomist;
import org.mvplugins.multiverse.core.utils.MinecraftTimeFormatter;
import org.mvplugins.multiverse.core.utils.REPatterns;
import org.mvplugins.multiverse.core.utils.StringFormatter;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import java.util.List;
@@ -29,17 +26,12 @@ import java.util.Locale;
final class PlaceholderExpansionHook extends PlaceholderExpansion {
private final MultiverseCore plugin;
private final CoreConfig coreConfig;
private final WorldManager worldManager;
private final MVEconomist economist;
@Inject
public PlaceholderExpansionHook(@NotNull MultiverseCore plugin,
@NotNull CoreConfig coreConfig,
@NotNull WorldManager worldManager,
@NotNull MVEconomist economist) {
public PlaceholderExpansionHook(MultiverseCore plugin, WorldManager worldManager, MVEconomist economist) {
this.plugin = plugin;
this.coreConfig = coreConfig;
this.worldManager = worldManager;
this.economist = economist;
}
@@ -50,13 +42,6 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
return super.register();
}
@Override
public void warning(String msg) {
if (coreConfig.getWarnInvalidPapiFormat()) {
super.warning(msg);
}
}
@Override
public @NotNull String getIdentifier() {
return "multiverse-core";
@@ -95,115 +80,136 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
return null;
}
final var placeholder = paramsArray.remove(0).toLowerCase(Locale.ENGLISH);
final var placeholder = paramsArray.remove(0);
return parseWorldName(offlinePlayer, paramsArray)
.flatMap(worldName -> worldManager.getWorld(worldName)
.onEmpty(() -> warning("Multiverse World not found: " + worldName)))
.flatMap(world -> world.asLoadedWorld()
.flatMap(loadedWorld -> getLoadedWorldPlaceHolderValue(placeholder, paramsArray, loadedWorld))
.orElse(() -> getWorldPlaceHolderValue(placeholder, paramsArray, world)))
.getOrElse(() -> coreConfig.getInvalidPapiFormatReturnsBlank() ? "" : null);
String worldName = parseWorldName(offlinePlayer, paramsArray);
if (worldName == null) return null;
return worldManager.getLoadedWorld(worldName)
.onEmpty(() -> warning("Multiverse World not found: " + worldName))
.map(world -> getWorldPlaceHolderValue(placeholder, paramsArray, world))
.getOrNull();
}
private @NotNull Option<String> parseWorldName(OfflinePlayer offlinePlayer, List<String> paramsArray) {
private @Nullable String parseWorldName(OfflinePlayer offlinePlayer, List<String> paramsArray) {
// No world defined, get from player
if (paramsArray.isEmpty()) {
if (offlinePlayer instanceof Player player) {
return Option.of(player.getWorld().getName());
return player.getWorld().getName();
} else {
warning("You must specify a world name for non-player placeholders");
return Option.none();
return null;
}
}
// Try get from params
String paramWorldName = paramsArray.get(paramsArray.size() - 1);
if (worldManager.isWorld(paramWorldName)) {
if (worldManager.isLoadedWorld(paramWorldName)) {
paramsArray.remove(paramsArray.size() - 1);
return Option.of(paramWorldName);
return paramWorldName;
}
// Param not a world, fallback to player
if (offlinePlayer instanceof Player player) {
return Option.of(player.getWorld().getName());
return player.getWorld().getName();
}
warning("Multiverse World not found: " + paramWorldName);
return Option.none();
return null;
}
private @NotNull Option<String> getWorldPlaceHolderValue(@NotNull String placeholder,
@NotNull List<String> placeholderParams,
@NotNull MultiverseWorld world) {
return Option.of(switch (placeholder) {
case "alias" -> world.getAliasOrName();
case "animalspawn" -> String.valueOf(world.getEntitySpawnConfig()
.getSpawnCategoryConfig(SpawnCategory.ANIMAL)
.isSpawn());
case "autoheal" -> String.valueOf(world.getAutoHeal());
case "blacklist" -> String.join(", ", world.getWorldBlacklist());
case "currency" -> String.valueOf(world.getCurrency());
case "difficulty" -> world.getDifficulty().toString();
case "entryfee" -> economist.formatPrice(world.getPrice(), world.getCurrency());
case "environment" -> world.getEnvironment().toString().toLowerCase();
case "flight" -> String.valueOf(world.isAllowFlight());
case "gamemode" -> world.getGameMode().toString().toLowerCase();
case "generator" -> world.getGenerator();
case "hunger" -> String.valueOf(world.isHunger());
case "isloaded" -> String.valueOf(world.isLoaded());
case "key" -> String.valueOf(world.getKey());
case "meta" -> {
if (placeholderParams.isEmpty()) {
warning("No meta key specified.");
yield null;
}
String metaKey = String.join("_", placeholderParams);
yield world.getMeta(metaKey)
.getOrElse(() -> {
warning("Meta key '" + metaKey + "' not found for world '" + world.getName() + "'.");
return null;
});
private @Nullable String getWorldPlaceHolderValue(@NotNull String placeholder,
@NotNull List<String> placeholderParams,
@NotNull LoadedMultiverseWorld world) {
// Switch to find what specific placeholder we want
switch (placeholder.toLowerCase(Locale.ENGLISH)) {
case "alias" -> {
return world.getAliasOrName();
}
case "monstersspawn" -> String.valueOf(world.getEntitySpawnConfig()
.getSpawnCategoryConfig(SpawnCategory.MONSTER)
.isSpawn());
case "name" -> world.getName();
case "playerlimit" -> String.valueOf(world.getPlayerLimit());
case "price" -> String.valueOf(world.getPrice());
case "pvp" -> String.valueOf(world.getPvp());
case "seed" -> String.valueOf(world.getSeed());
case "weather" -> String.valueOf(world.isAllowWeather());
case "playercount", "time", "type" -> {
warning("Placeholder '" + placeholder + "' is only available for loaded worlds.");
yield null;
case "animalspawn" -> {
return String.valueOf(world.getEntitySpawnConfig().getSpawnCategoryConfig(SpawnCategory.ANIMAL).isSpawn());
}
default -> {
warning("Unknown Placeholder: " + placeholder);
yield null;
case "autoheal" -> {
return String.valueOf(world.getAutoHeal());
}
case "blacklist" -> {
return String.join(", ", world.getWorldBlacklist());
}
case "currency" -> {
return String.valueOf(world.getCurrency());
}
case "difficulty" -> {
return world.getDifficulty().toString();
}
case "entryfee" -> {
return economist.formatPrice(world.getPrice(), world.getCurrency());
}
case "environment" -> {
return world.getEnvironment().toString().toLowerCase();
}
case "flight" -> {
return String.valueOf(world.isAllowFlight());
}
case "gamemode" -> {
return world.getGameMode().toString().toLowerCase();
}
case "generator" -> {
return world.getGenerator();
}
case "hunger" -> {
return String.valueOf(world.isHunger());
}
case "monstersspawn" -> {
return String.valueOf(world.getEntitySpawnConfig().getSpawnCategoryConfig(SpawnCategory.MONSTER).isSpawn());
}
case "name" -> {
return world.getName();
}
case "playercount" -> {
return String.valueOf(world.getBukkitWorld()
.map(World::getPlayers)
.map(List::size)
.getOrElse(-1));
}
case "playerlimit" -> {
return String.valueOf(world.getPlayerLimit());
}
case "price" -> {
return String.valueOf(world.getPrice());
}
case "pvp" -> {
return String.valueOf(world.getPvp());
}
case "seed" -> {
return String.valueOf(world.getSeed());
}
});
}
private @NotNull Option<String> getLoadedWorldPlaceHolderValue(@NotNull String placeholder,
@NotNull List<String> placeholderParams,
@NotNull LoadedMultiverseWorld world) {
return Option.of(switch (placeholder) {
case "playercount" -> String.valueOf(world.getBukkitWorld()
.map(World::getPlayers)
.map(List::size)
.getOrElse(-1));
case "time" -> {
String timeFormat = !placeholderParams.isEmpty() ? placeholderParams.get(0) : "";
long time = world.getBukkitWorld().map(World::getTime).getOrElse(0L);
yield switch (timeFormat) {
case "" -> String.valueOf(time);
case "12h" -> MinecraftTimeFormatter.format12h(time);
case "24h" -> MinecraftTimeFormatter.format24h(time);
default -> MinecraftTimeFormatter.formatTime(time, timeFormat);
};
switch (timeFormat) {
case "" -> {
return String.valueOf(time);
}
case "12h" -> {
return MinecraftTimeFormatter.format12h(time);
}
case "24h" -> {
return MinecraftTimeFormatter.format24h(time);
}
default -> {
return MinecraftTimeFormatter.formatTime(time, timeFormat);
}
}
}
case "type" -> world.getBukkitWorld().map(World::getWorldType).map(Enum::name).getOrElse("null");
default -> null;
});
case "type" -> {
return world.getBukkitWorld().map(World::getWorldType).map(Enum::name).getOrElse("null");
}
case "weather" -> {
return String.valueOf(world.isAllowWeather());
}
default -> {
warning("Unknown placeholder: " + placeholder);
return null;
}
}
}
}
@@ -1,60 +0,0 @@
package org.mvplugins.multiverse.core.command;
import co.aikar.commands.CommandCompletionContext;
import co.aikar.commands.CommandCompletionFilter;
import co.aikar.commands.apachecommonslang.ApacheCommonsLangUtil;
import org.jetbrains.annotations.ApiStatus;
import java.util.Locale;
/**
* Utility filters for command completion matching.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public final class MVCommandCompletionFilters {
/**
* Matches namespaced keys (for example, {@code minecraft:stone}) using namespace-aware checks.
* <p>
* This filter accepts completions that:
* <ul>
* <li>start with the current input,</li>
* <li>have a namespace starting with the input, or</li>
* <li>have a key value containing the input.</li>
* </ul>
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static final CommandCompletionFilter NAMESPACED_KEY = (context, completion) -> {
String[] split = completion.split(":", 2);
if (split.length < 2) {
return ApacheCommonsLangUtil.startsWithIgnoreCase(completion, context.getInput());
}
String lowerCase = context.getInput().toLowerCase(Locale.ROOT);
return ApacheCommonsLangUtil.startsWithIgnoreCase(completion, context.getInput())
|| split[0].toLowerCase(Locale.ROOT).startsWith(lowerCase)
|| split[1].toLowerCase(Locale.ROOT).contains(lowerCase);
};
/**
* Gets the namespaced-key completion filter with a generic type-safe signature.
* Use this method instead of {@link MVCommandCompletionFilters#NAMESPACED_KEY}
* to avoid raw type unchecked warnings.
*
* @param <C> completion context type
* @return the namespaced key filter
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static <C extends CommandCompletionContext> CommandCompletionFilter<C> namespacedKey() {
return NAMESPACED_KEY;
}
private MVCommandCompletionFilters() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}
@@ -11,7 +11,6 @@ import java.util.Map;
import java.util.stream.Collectors;
import co.aikar.commands.BukkitCommandCompletionContext;
import co.aikar.commands.CommandCompletionFilter;
import co.aikar.commands.CommandIssuer;
import co.aikar.commands.PaperCommandCompletions;
import co.aikar.commands.RegisteredCommand;
@@ -53,9 +52,8 @@ import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.generators.GeneratorPlugin;
import org.mvplugins.multiverse.core.world.generators.GeneratorProvider;
import org.mvplugins.multiverse.core.world.helpers.PotentialWorldFinder;
import static org.mvplugins.multiverse.core.utils.StringFormatter.addOnToCommaSeparated;
import static org.mvplugins.multiverse.core.utils.StringFormatter.addonToCommaSeperated;
@Service
public class MVCommandCompletions extends PaperCommandCompletions {
@@ -67,7 +65,6 @@ public class MVCommandCompletions extends PaperCommandCompletions {
private final CorePermissionsChecker corePermissionsChecker;
private final AnchorManager anchorManager;
private final GeneratorProvider generatorProvider;
private final PotentialWorldFinder potentialWorldFinder;
@Inject
MVCommandCompletions(
@@ -77,8 +74,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
@NotNull CoreConfig config,
@NotNull CorePermissionsChecker corePermissionsChecker,
@NotNull AnchorManager anchorManager,
@NotNull GeneratorProvider generatorProvider,
@NotNull PotentialWorldFinder potentialWorldFinder
@NotNull GeneratorProvider generatorProvider
) {
super(mvCommandManager);
this.commandManager = mvCommandManager;
@@ -88,29 +84,27 @@ public class MVCommandCompletions extends PaperCommandCompletions {
this.corePermissionsChecker = corePermissionsChecker;
this.anchorManager = anchorManager;
this.generatorProvider = generatorProvider;
this.potentialWorldFinder = potentialWorldFinder;
registerAsyncCompletion("anchornames", this::suggestAnchorNames, CommandCompletionFilter.contains());
registerAsyncCompletion("anchornames", this::suggestAnchorNames);
registerAsyncCompletion("commands", this::suggestCommands);
registerAsyncCompletion("destinations", this::suggestDestinations);
registerStaticCompletion("difficulties", suggestEnums(Difficulty.class));
registerStaticCompletion("environments", List.of("normal", "nether", "the_end")); // Don't tab complete the "custom" environment
registerAsyncCompletion("flags", this::suggestFlags);
registerStaticCompletion("gamemodes", suggestEnums(GameMode.class));
registerStaticCompletion("gamerules", this::suggestGamerules, MVCommandCompletionFilters.namespacedKey());
registerStaticCompletion("gamerules", this::suggestGamerules);
registerAsyncCompletion("gamerulesvalues", this::suggestGamerulesValues);
registerAsyncCompletion("generatorplugins", this::suggestGeneratorPlugins, CommandCompletionFilter.contains());
registerStaticCompletion("materials", suggestEnums(Material.class), CommandCompletionFilter.contains());
registerStaticCompletion("mvconfigs", config.getStringPropertyHandle().getAllPropertyNames(), CommandCompletionFilter.contains());
registerAsyncCompletion("generatorplugins", this::suggestGeneratorPlugins);
registerStaticCompletion("materials", suggestEnums(Material.class));
registerStaticCompletion("mvconfigs", config.getStringPropertyHandle().getAllPropertyNames());
registerAsyncCompletion("mvconfigvalues", this::suggestMVConfigValues);
registerAsyncCompletion("mvworlds", this::suggestMVWorlds, MVCommandCompletionFilters.namespacedKey());
registerAsyncCompletion("mvworldmetakey", this::suggestMVWorldMetaKey, CommandCompletionFilter.contains());
registerAsyncCompletion("mvworldpropsname", this::suggestMVWorldPropsName, CommandCompletionFilter.contains());
registerAsyncCompletion("mvworlds", this::suggestMVWorlds);
registerAsyncCompletion("mvworldpropsname", this::suggestMVWorldPropsName);
registerAsyncCompletion("mvworldpropsvalue", this::suggestMVWorldPropsValue);
registerCompletion("playersarray", this::suggestPlayersArray, CommandCompletionFilter.contains()); // getting online players cannot be async
registerCompletion("playersarray", this::suggestPlayersArray); // getting online players cannot be async
registerStaticCompletion("propsmodifyaction", suggestEnums(PropertyModifyAction.class));
registerStaticCompletion("spawncategories", suggestEnums(SpawnCategory.class));
registerAsyncCompletion("spawncategorypropsname", this::suggestSpawnCategoryPropsName, CommandCompletionFilter.contains());
registerAsyncCompletion("spawncategorypropsname", this::suggestSpawnCategoryPropsName);
registerAsyncCompletion("spawncategorypropsvalue", this::suggestSpawnCategoryPropsValue);
setDefaultCompletion("destinations", DestinationInstance.class);
@@ -169,7 +163,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
}
}
if (context.hasConfig("multiple")) {
return addOnToCommaSeparated(context.getInput(), handler.getCompletions(context));
return addonToCommaSeperated(context.getInput(), handler.getCompletions(context));
}
return handler.getCompletions(context);
}
@@ -303,20 +297,13 @@ public class MVCommandCompletions extends PaperCommandCompletions {
.toList();
}
case "potential" -> {
return potentialWorldFinder.findPotentialWorlds();
return worldManager.getPotentialWorlds();
}
}
Logging.severe("Invalid MVWorld scope: " + scope);
return Collections.emptyList();
}
private Collection<String> suggestMVWorldMetaKey(BukkitCommandCompletionContext context) {
return Try.of(() -> context.getContextValue(MultiverseWorld.class))
.map(MultiverseWorld::getAllMeta)
.map(Map::keySet)
.getOrElse(Collections.emptySet());
}
private Collection<String> suggestMVWorldPropsName(BukkitCommandCompletionContext context) {
return Try.of(() -> {
MultiverseWorld world = context.getContextValue(MultiverseWorldValue.class).value();
@@ -347,7 +334,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
matchedPlayers.add(name);
}
}
return addOnToCommaSeparated(context.getInput(), matchedPlayers);
return addonToCommaSeperated(context.getInput(), matchedPlayers);
}
private Collection<String> suggestSpawnCategoryPropsName(BukkitCommandCompletionContext context) {
@@ -42,7 +42,6 @@ import org.mvplugins.multiverse.core.exceptions.command.MVInvalidCommandArgument
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.utils.PlayerFinder;
import org.mvplugins.multiverse.core.utils.REPatterns;
import org.mvplugins.multiverse.core.utils.tick.TickDuration;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
@@ -94,7 +93,6 @@ public class MVCommandContexts extends PaperCommandContexts {
registerIssuerAwareContext(PlayerArrayValue.class, playerArrayContextBuilder().generateContext(PlayerArrayValue::new));
registerIssuerAwareContext(PlayerLocation.class, this::parsePlayerLocation);
registerContext(SpawnCategory[].class, this::parseSpawnCategories);
registerContext(TickDuration.class, this::parseTickDuration);
}
private MVCommandIssuer parseMVCommandIssuer(BukkitCommandExecutionContext context) {
@@ -395,13 +393,4 @@ public class MVCommandContexts extends PaperCommandContexts {
}
return categories.toArray(new SpawnCategory[0]);
}
private TickDuration parseTickDuration(BukkitCommandExecutionContext context) {
String arg = context.popFirstArg();
return TickDuration.parseString(arg)
.getOrElseThrow(failure ->
new InvalidCommandArgument("Invalid time duration format: \"" +arg + "\". Use a number " +
"followed by an optional suffix (s for seconds, d for game days) or just a number " +
"for ticks. Examples: 100, 5s, 2d"));
}
}
@@ -62,6 +62,7 @@ public class MVCommandManager extends PaperCommandManager {
this.config = config;
this.pluginLocales = new PluginLocales(this);
this.locales = this.pluginLocales;
this.pluginLocales.loadLanguages();
MVCommandConditions.load(this, worldManager, worldNameChecker);
this.enableUnstableAPI("help");
@@ -5,10 +5,8 @@ import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import java.util.function.Supplier;
import co.aikar.commands.InvalidCommandArgument;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -42,34 +40,34 @@ public class CommandValueFlag<T> extends CommandFlag {
private final Class<T> type;
private final boolean optional;
private final Supplier<? extends T> defaultValueSupplier;
private final T defaultValue;
private final Function<String, T> context;
private final Function<String, Collection<String>> completion;
/**
* Creates a new flag.
*
* @param key The key for the new flag.
* @param aliases The aliases that also refer to this flag.
* @param type The type of the value.
* @param optional Allow for flag without value.
* @param defaultValueSupplier The default value if optional is true and user does not specify a value.
* @param context Function to parse string into value type.
* @param completion Function to get completion for this flag.
* @param key The key for the new flag.
* @param aliases The aliases that also refer to this flag.
* @param type The type of the value.
* @param optional Allow for flag without value.
* @param defaultValue The default value if optional is true and user does not specify a value.
* @param context Function to parse string into value type.
* @param completion Function to get completion for this flag.
*/
protected CommandValueFlag(
@NotNull String key,
@NotNull List<String> aliases,
@NotNull Class<T> type,
boolean optional,
@Nullable Supplier<? extends T> defaultValueSupplier,
@Nullable T defaultValue,
@Nullable Function<String, T> context,
@Nullable Function<String, Collection<String>> completion
) {
super(key, aliases);
this.type = type;
this.optional = optional;
this.defaultValueSupplier = defaultValueSupplier;
this.defaultValue = defaultValue;
this.context = context;
this.completion = completion;
}
@@ -98,7 +96,7 @@ public class CommandValueFlag<T> extends CommandFlag {
* @return The default value.
*/
public @Nullable T getDefaultValue() {
return defaultValueSupplier == null ? null : defaultValueSupplier.get();
return defaultValue;
}
/**
@@ -128,7 +126,7 @@ public class CommandValueFlag<T> extends CommandFlag {
public static class Builder<T, S extends Builder<T, S>> extends CommandFlag.Builder<S> {
protected final Class<T> type;
protected boolean optional = false;
protected Supplier<? extends T> defaultValueSupplier = null;
protected T defaultValue = null;
protected Function<String, T> context = null;
protected Function<String, Collection<String>> completion = null;
@@ -160,21 +158,7 @@ public class CommandValueFlag<T> extends CommandFlag {
* @return The builder.
*/
public @NotNull S defaultValue(@NotNull T defaultValue) {
return defaultValue(() -> defaultValue);
}
/**
* Set the default value supplier. Used if optional is true and user does not specify a value.
* Supplier is only called when command is executed with the flag is present in input.
*
* @param defaultValueSupplier The default value supplier
* @return The builder
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull S defaultValue(@NotNull Supplier<? extends T> defaultValueSupplier) {
this.defaultValueSupplier = defaultValueSupplier;
this.defaultValue = defaultValue;
return (S) this;
}
@@ -210,7 +194,7 @@ public class CommandValueFlag<T> extends CommandFlag {
if (context == null && !String.class.equals(type)) {
throw new IllegalStateException("Context is required for non-string value flags");
}
return new CommandValueFlag<>(key, aliases, type, optional, defaultValueSupplier, context, completion);
return new CommandValueFlag<>(key, aliases, type, optional, defaultValue, context, completion);
}
}
@@ -223,7 +207,7 @@ public class CommandValueFlag<T> extends CommandFlag {
public static class EnumBuilder<T extends Enum<T>, S extends EnumBuilder<T, S>> extends CommandFlag.Builder<S> {
protected final Class<T> type;
protected boolean optional = false;
protected Supplier<? extends T> defaultValueSupplier = null;
protected T defaultValue = null;
protected Function<String, T> context = null;
protected Function<String, Collection<String>> completion = null;
@@ -269,21 +253,7 @@ public class CommandValueFlag<T> extends CommandFlag {
* @return The builder.
*/
public @NotNull S defaultValue(@NotNull T defaultValue) {
return defaultValue(() -> defaultValue);
}
/**
* Set the default value to supply. Used if optional is true and user does not specify a value.
* Supplier is only called when command is executed with the flag is present in input.
*
* @param defaultValueSupplier The default value supplier.
* @return The builder.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull S defaultValue(@NotNull Supplier<? extends T> defaultValueSupplier) {
this.defaultValueSupplier = defaultValueSupplier;
this.defaultValue = defaultValue;
return (S) this;
}
@@ -294,7 +264,7 @@ public class CommandValueFlag<T> extends CommandFlag {
*/
@Override
public @NotNull CommandValueFlag<T> build() {
return new CommandValueFlag<>(key, aliases, type, optional, defaultValueSupplier, context, completion);
return new CommandValueFlag<>(key, aliases, type, optional, defaultValue, context, completion);
}
}
}
@@ -1,65 +0,0 @@
package org.mvplugins.multiverse.core.command.flags;
import co.aikar.commands.InvalidCommandArgument;
import jakarta.inject.Inject;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.CommandValueFlag;
import org.mvplugins.multiverse.core.command.flag.FlagBuilder;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.destination.core.WorldDestination;
import org.mvplugins.multiverse.core.exceptions.command.MVInvalidCommandArgument;
import org.mvplugins.multiverse.core.world.WorldManager;
@ApiStatus.AvailableSince("5.7")
@Service
public class RemovePlayerDestinationFlags extends FlagBuilder {
public static final String NAME = "removeplayer";
private WorldManager worldManager;
private DestinationsProvider destinationsProvider;
private WorldDestination worldDestination;
protected RemovePlayerDestinationFlags(
@NotNull String name,
@NotNull CommandFlagsManager flagsManager,
@NotNull WorldManager worldManager,
@NotNull DestinationsProvider destinationsProvider,
@NotNull WorldDestination worldDestination
) {
super(name, flagsManager);
this.worldManager = worldManager;
this.destinationsProvider = destinationsProvider;
this.worldDestination = worldDestination;
}
@Inject
private RemovePlayerDestinationFlags(
@NotNull CommandFlagsManager flagsManager,
@NotNull WorldManager worldManager,
@NotNull DestinationsProvider destinationsProvider,
@NotNull WorldDestination worldDestination
) {
super(NAME, flagsManager);
this.destinationsProvider = destinationsProvider;
this.worldManager = worldManager;
this.worldDestination = worldDestination;
}
public final CommandValueFlag<DestinationInstance> removePlayers = flag(CommandValueFlag.builder("--remove-players", DestinationInstance.class)
.addAlias("-r")
.defaultValue(() -> worldManager.getDefaultWorld()
.map(defaultWorld -> worldDestination.fromWorld(defaultWorld))
.getOrElseThrow(() -> new InvalidCommandArgument("No default world found, so the --remove-players flag requires a destination argument."))) //TODO: locale
.completion(input -> destinationsProvider.suggestDestinationStrings(Bukkit.getConsoleSender(), input))
.context(input -> destinationsProvider.parseDestination(input)
.getOrThrow(failure ->
MVInvalidCommandArgument.of(failure.getFailureMessage())))
.optional()
.build());
}
@@ -1,19 +1,12 @@
package org.mvplugins.multiverse.core.command.flags;
import jakarta.inject.Inject;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.FlagBuilder;
/**
* @deprecated The --remove-players flag is being removed in favor of a more flexible system that allows for specifying
* a destination for players to be teleported to when a world is unloaded. See {@link RemovePlayerDestinationFlags}.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@Service
public class RemovePlayerFlags extends FlagBuilder {
@@ -20,7 +20,7 @@ import org.mvplugins.multiverse.core.command.flag.FlagBuilder;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.options.CloneWorldOptions;
@@ -48,7 +48,7 @@ class CloneCommand extends CoreCommand {
@Syntax("<world>")
@Description("{@@mv-core.clone.world.description}")
MultiverseWorld world,
LoadedMultiverseWorld world,
@Syntax("<new-world-name>")
@Description("{@@mv-core.clone.newWorld.description}")
@@ -114,7 +114,7 @@ class CloneCommand extends CoreCommand {
@Override
@CommandAlias("mvcl|mvclone")
void onCloneCommand(MVCommandIssuer issuer, MultiverseWorld world, String newWorldName, String[] flags) {
void onCloneCommand(MVCommandIssuer issuer, LoadedMultiverseWorld world, String newWorldName, String[] flags) {
super.onCloneCommand(issuer, world, newWorldName, flags);
}
}
@@ -11,7 +11,6 @@ import co.aikar.commands.annotation.Optional;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import org.bukkit.World;
import org.bukkit.WorldType;
@@ -20,16 +19,14 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.LegacyAliasCommand;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.CommandValueFlag;
import org.mvplugins.multiverse.core.command.flag.FlagBuilder;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.exceptions.command.MVInvalidCommandArgument;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.utils.StringFormatter;
import org.mvplugins.multiverse.core.utils.position.EntityPosition;
import org.mvplugins.multiverse.core.utils.result.Attempt.Failure;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
@@ -55,9 +52,8 @@ class CreateCommand extends CoreCommand {
@Subcommand("create")
@CommandPermission("multiverse.core.create")
@CommandCompletion("@empty @environments @flags:groupName=" + Flags.NAME)
@Syntax("<name> <environment> [--seed <seed> --generator <generator[:id]> --world-type <worldtype> " +
"--adjust-spawn --no-structures --generate-bonus-chest --force-spawn-position <x,y,z:pitch:yaw> " +
"--biome <biome> --generator-settings <json-settings> --properties <prop1=value1,prop2=value2,...>]")
@Syntax("<name> <environment> [--seed <seed> --generator <generator[:id]> --world-type <worldtype> --adjust-spawn "
+ "--no-structures --biome <biome>]")
@Description("{@@mv-core.create.description}")
void onCreateCommand(
MVCommandIssuer issuer,
@@ -71,9 +67,8 @@ class CreateCommand extends CoreCommand {
World.Environment environment,
@Optional
@Syntax("[--seed <seed> --generator <generator[:id]> --world-type <worldtype> --adjust-spawn " +
"--no-structures --generate-bonus-chest --force-spawn-position <x,y,z:pitch:yaw> --biome <biome> " +
"--generator-settings <json-settings> --properties <prop1=value1,prop2=value2,...>]")
@Syntax("[--seed <seed> --generator <generator[:id]> --world-type <worldtype> --adjust-spawn "
+ "--no-structures --biome <biome>]")
@Description("{@@mv-core.create.flags.description}")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
@@ -83,17 +78,14 @@ class CreateCommand extends CoreCommand {
issuer.sendInfo(MVCorei18n.CREATE_LOADING);
worldManager.createWorld(CreateWorldOptions.worldName(worldName)
.biome(parsedFlags.flagValue(flags.biome, ""))
.bonusChest(parsedFlags.hasFlag(flags.bonusChest))
.environment(environment)
.forcedSpawnPosition(parsedFlags.flagValue(flags.forceSpawnPosition))
.generator(parsedFlags.flagValue(flags.generator, ""))
.generatorSettings(parsedFlags.flagValue(flags.generatorSettings, ""))
.generateStructures(!parsedFlags.hasFlag(flags.noStructures))
.seed(parsedFlags.flagValue(flags.seed))
.useSpawnAdjust(!parsedFlags.hasFlag(flags.noAdjustSpawn))
.worldPropertyStrings(StringFormatter.parseCSVMap(parsedFlags.flagValue(flags.properties)))
.worldType(parsedFlags.flagValue(flags.worldType, WorldType.NORMAL)))
.biome(parsedFlags.flagValue(flags.biome, ""))
.environment(environment)
.seed(parsedFlags.flagValue(flags.seed))
.worldType(parsedFlags.flagValue(flags.worldType, WorldType.NORMAL))
.useSpawnAdjust(!parsedFlags.hasFlag(flags.noAdjustSpawn))
.generator(parsedFlags.flagValue(flags.generator, ""))
.generatorSettings(parsedFlags.flagValue(flags.generatorSettings, ""))
.generateStructures(!parsedFlags.hasFlag(flags.noStructures)))
.onSuccess(newWorld -> messageSuccess(issuer, newWorld))
.onFailure(failure -> messageFailure(issuer, failure));
}
@@ -188,19 +180,6 @@ class CreateCommand extends CoreCommand {
.addAlias("-b")
.completion(input -> biomeProviderFactory.suggestBiomeString(input))
.build());
private final CommandValueFlag<String> properties = flag(CommandValueFlag.builder("--properties", String.class)
.addAlias("-p")
.build());
private final CommandFlag bonusChest = flag(CommandFlag.builder("--generate-bonus-chest")
.addAlias("-c")
.build());
private final CommandValueFlag<EntityPosition> forceSpawnPosition = flag(CommandValueFlag.builder("--force-spawn-position", EntityPosition.class)
.addAlias("-f")
.context(input -> Try.of(() -> EntityPosition.fromString(input)).getOrElseThrow(MVInvalidCommandArgument::causeBy))
.build());
}
@Service
@@ -15,11 +15,11 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.LegacyAliasCommand;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.RemovePlayerDestinationFlags;
import org.mvplugins.multiverse.core.command.flags.RemovePlayerFlags;
import org.mvplugins.multiverse.core.command.queue.CommandQueueManager;
import org.mvplugins.multiverse.core.command.queue.CommandQueuePayload;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
@@ -31,8 +31,6 @@ import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter;
import org.mvplugins.multiverse.core.world.options.DeleteWorldOptions;
import java.util.Objects;
@Service
class DeleteCommand extends CoreCommand {
@@ -40,7 +38,7 @@ class DeleteCommand extends CoreCommand {
private final WorldManager worldManager;
private final PlayerWorldTeleporter playerWorldTeleporter;
private final WorldTickDeferrer worldTickDeferrer;
private final RemovePlayerDestinationFlags flags;
private final RemovePlayerFlags flags;
@Inject
DeleteCommand(
@@ -48,7 +46,7 @@ class DeleteCommand extends CoreCommand {
@NotNull WorldManager worldManager,
@NotNull PlayerWorldTeleporter playerWorldTeleporter,
@NotNull WorldTickDeferrer worldTickDeferrer,
@NotNull RemovePlayerDestinationFlags flags
@NotNull RemovePlayerFlags flags
) {
this.commandQueueManager = commandQueueManager;
this.worldManager = worldManager;
@@ -59,8 +57,8 @@ class DeleteCommand extends CoreCommand {
@Subcommand("delete")
@CommandPermission("multiverse.core.delete")
@CommandCompletion("@mvworlds:scope=both @flags:groupName=" + RemovePlayerDestinationFlags.NAME)
@Syntax("<world> [--remove-players [destination]]")
@CommandCompletion("@mvworlds:scope=both @flags:groupName=" + RemovePlayerFlags.NAME)
@Syntax("<world>")
@Description("{@@mv-core.delete.description}")
void onDeleteCommand(
MVCommandIssuer issuer,
@@ -71,7 +69,7 @@ class DeleteCommand extends CoreCommand {
MultiverseWorld world,
@Optional
@Syntax("[--remove-players [destination]]")
@Syntax("[--remove-players]")
@Description("")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
@@ -86,11 +84,10 @@ class DeleteCommand extends CoreCommand {
private void runDeleteCommand(MVCommandIssuer issuer, MultiverseWorld world, ParsedCommandFlags parsedFlags) {
issuer.sendInfo(MVCorei18n.DELETE_DELETING, Replace.WORLD.with(world.getName()));
DestinationInstance<?, ?> removeToDestination = parsedFlags.flagValue(flags.removePlayers);
var future = Objects.nonNull(removeToDestination)
var future = parsedFlags.hasFlag(flags.removePlayers)
&& world.isLoaded()
&& world instanceof LoadedMultiverseWorld loadedWorld
? playerWorldTeleporter.transferAllFromWorldToDestination(loadedWorld, removeToDestination)
? playerWorldTeleporter.removeFromWorld(loadedWorld)
: AsyncAttemptsAggregate.emptySuccess();
future.onSuccess(() -> worldTickDeferrer.deferWorldTick(() -> doWorldDeleting(issuer, world)))
@@ -116,7 +113,7 @@ class DeleteCommand extends CoreCommand {
@NotNull WorldManager worldManager,
@NotNull PlayerWorldTeleporter playerWorldTeleporter,
@NotNull WorldTickDeferrer worldTickDeferrer,
@NotNull RemovePlayerDestinationFlags flags) {
@NotNull RemovePlayerFlags flags) {
super(commandQueueManager, worldManager, playerWorldTeleporter, worldTickDeferrer, flags);
}
@@ -12,10 +12,13 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.FlagBuilder;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.commands.DumpsLogPoster.LogsType;
import org.mvplugins.multiverse.core.commands.DumpsLogPoster.UploadType;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandValueFlag;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
@Service
final class DumpsCommand extends CoreCommand {
@@ -32,17 +35,22 @@ final class DumpsCommand extends CoreCommand {
@Subcommand("dumps")
@CommandPermission("multiverse.core.dumps")
@CommandCompletion("@flags:groupName=" + Flags.NAME)
@Syntax("[--upload <pastesdev|mclogs>]")
@Syntax("[--logs <mclogs | append>] [--upload <pastesdev | pastegg>] [--paranoid]")
@Description("{@@mv-core.dumps.description}")
void onDumpsCommand(
MVCommandIssuer issuer,
@Optional
@Syntax("[--upload <pastesdev|mclogs>]")
@Syntax("[--logs <mclogs | append>] [--upload <pastesdev | pastegg>] [--paranoid]")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
// Grab all our flags
boolean paranoid = parsedFlags.hasFlag(flags.paranoid);
LogsType logsType = parsedFlags.flagValue(flags.logs, LogsType.MCLOGS);
UploadType servicesType = parsedFlags.flagValue(flags.upload, UploadType.PASTESDEV);
dumpsService.postLogs(issuer, servicesType);
dumpsService.postLogs(issuer, logsType, servicesType, paranoid);
}
@Service
@@ -55,9 +63,19 @@ final class DumpsCommand extends CoreCommand {
super(NAME, flagsManager);
}
private final CommandValueFlag<LogsType> logs = flag(CommandValueFlag
.enumBuilder("--logs", LogsType.class)
.addAlias("-l")
.build());
private final CommandValueFlag<UploadType> upload = flag(CommandValueFlag
.enumBuilder("--upload", UploadType.class)
.addAlias("-u")
.build());
// Does not upload logs or plugin list (except if --logs mclogs is there)
private final CommandFlag paranoid = flag(CommandFlag.builder("--paranoid")
.addAlias("-p")
.build());
}
}
@@ -1,19 +1,11 @@
package org.mvplugins.multiverse.core.commands;
import java.lang.management.ManagementFactory;
import java.time.Instant;
import java.util.Date;
import java.util.Map;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import net.minidev.json.JSONArray;
import net.minidev.json.JSONObject;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.event.MVDumpsDebugInfoEvent;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
@@ -26,10 +18,8 @@ final class DumpsLogPoster extends BukkitRunnable {
enum UploadType {
// BEGIN CHECKSTYLE-SUPPRESSION: JavadocVariable
// PASTEBIN(PasteServiceType.PASTEBIN, "pastebin.com"),
PASTESDEV(PasteServiceType.PASTESDEV, "pastes.dev"),
MCLOGS(PasteServiceType.MCLOGS, "mclo.gs"),
;
PASTEGG(PasteServiceType.PASTEGG, "paste.gg"),
PASTESDEV(PasteServiceType.PASTESDEV, "pastes.dev");
// END CHECKSTYLE-SUPPRESSION: JavadocVariable
private final PasteServiceType pasteServiceType;
@@ -41,21 +31,34 @@ final class DumpsLogPoster extends BukkitRunnable {
}
}
@NotNull
private final MultiverseCore plugin;
enum LogsType {
/**
* Appends the log file to the debug info.
*/
APPEND,
/**
* Uploads the log file to a paste service.
*/
MCLOGS
}
private final MVCommandIssuer issuer;
private final LogsType logsType;
private final UploadType uploadType;
private final boolean paranoid;
private final String logs;
private final MVDumpsDebugInfoEvent versionEvent;
DumpsLogPoster(@NotNull MultiverseCore core,
@NotNull MVCommandIssuer issuer,
DumpsLogPoster(@NotNull MVCommandIssuer issuer,
@NotNull DumpsLogPoster.LogsType logsType,
@NotNull DumpsLogPoster.UploadType uploadType,
boolean paranoid,
@NotNull String logs,
@NotNull MVDumpsDebugInfoEvent versionEvent) {
this.plugin = core;
this.issuer = issuer;
this.logsType = logsType;
this.uploadType = uploadType;
this.paranoid = paranoid;
this.logs = logs;
this.versionEvent = versionEvent;
}
@@ -66,12 +69,24 @@ final class DumpsLogPoster extends BukkitRunnable {
handleVersionEvent();
}
@SuppressWarnings("checkstyle:MissingSwitchDefault")
private void handleLogs() {
versionEvent.putDetailedDebugInfo("latest.log", logs);
Logging.finer("Logs type is: " + logsType);
if (!paranoid) {
switch (logsType) {
case MCLOGS -> sendDumpsUrl("Logs", postRawDataToMcLogs(logs));
case APPEND -> versionEvent.putDetailedDebugInfo("latest.log", logs);
default -> Logging.finer("Not uploading logs.");
}
} else {
Logging.finer("Paranoid mode is on, not uploading logs.");
}
}
private void handleVersionEvent() {
Logging.finer("Upload service is: " + uploadType);
final Map<String, String> files = versionEvent.getDetailedDebugInfo();
sendDumpsUrl(uploadType.serviceName, postFilesToService(files));
}
@@ -80,77 +95,37 @@ final class DumpsLogPoster extends BukkitRunnable {
issuer.sendInfo(MVCorei18n.DUMPS_URL_LIST, "{service}", service, "{link}", url);
}
private String postRawDataToMcLogs(@NotNull String rawPasteData) {
PasteService pasteService = PasteServiceFactory.getService(PasteServiceType.MCLOGS, true);
return pasteToService(() -> pasteService.postData(rawPasteData));
}
private String postFilesToService(@NotNull Map<String, String> pasteFiles) {
PasteService pasteService = PasteServiceFactory.getService(uploadType.pasteServiceType, false);
return pasteToService(() -> pasteService.postData(encodePasteAsJson(pasteFiles)));
}
PasteService pasteService = PasteServiceFactory.getService(uploadType.pasteServiceType, true);
private String encodePasteAsJson(Map<String, String> files) {
JSONObject rootObject = new JSONObject();
JSONArray plugins = new JSONArray();
JSONArray filesObject = new JSONArray();
for (Plugin plugin : plugin.getServer().getPluginManager().getPlugins()) {
plugins.add(new JSONObject(Map.of(
"name", plugin.getName(),
"version", plugin.getDescription().getVersion(),
"description", Option.of(plugin.getDescription().getDescription()).getOrElse(""),
"authors", plugin.getDescription().getAuthors(),
"website", Option.of(plugin.getDescription().getWebsite()).getOrElse(""),
"enabled", plugin.isEnabled()
)));
}
for (Map.Entry<String, String> entry : files.entrySet()) {
String fileType = "text";
if (entry.getKey().endsWith(".yml") || entry.getKey().endsWith(".yaml")) {
fileType = "yaml";
} else if (entry.getKey().endsWith(".json")) {
fileType = "json";
} else if (entry.getKey().endsWith(".md")) {
fileType = "markdown";
} else if (entry.getKey().endsWith(".log")) {
fileType = "accesslog";
return pasteToService(() -> {
if (pasteService.supportsMultiFile()) {
return pasteService.postData(pasteFiles);
} else {
return pasteService.postData(this.encodeAsString(pasteFiles));
}
});
}
filesObject.add(new JSONObject(Map.of(
"name", entry.getKey(),
"data", entry.getValue(),
"type", fileType
)));
private String encodeAsString(Map<String, String> files) {
StringBuilder uploadData = new StringBuilder();
for (Map.Entry<String, String> entry : files.entrySet()) {
String file = entry.getKey();
String data = entry.getValue();
uploadData.append("# ---------- ")
.append(file)
.append(" ----------\n\n")
.append(data)
.append("\n\n");
}
rootObject.put("server", new JSONObject(Map.of(
"name", plugin.getServer().getName(),
"version", plugin.getServer().getVersion(),
"bukkitVersion", plugin.getServer().getBukkitVersion(),
"onlineMode", plugin.getServer().getOnlineMode(),
"javaVersion", Runtime.version().toString(),
"operatingSystem", System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch"),
"uptime", calculateUptimeString(),
"memory", "%d free / %d total / %d max".formatted(
toMb(Runtime.getRuntime().freeMemory()),
toMb(Runtime.getRuntime().totalMemory()),
toMb(Runtime.getRuntime().maxMemory()))
)));
rootObject.put("plugins", plugins);
rootObject.put("files", filesObject);
rootObject.put("createdAt", Instant.now().toString());
return rootObject.toJSONString();
}
private long toMb(long value) {
return value / 1024 / 1024;
}
private String calculateUptimeString() {
long uptimeMs = System.currentTimeMillis() - ManagementFactory.getRuntimeMXBean().getStartTime();
long totalSeconds = uptimeMs / 1000;
long hours = totalSeconds / 3600;
long minutes = (totalSeconds % 3600) / 60;
long seconds = totalSeconds % 60;
return String.format("%d hrs, %d mins, %d sec", hours, minutes, seconds);
return uploadData.toString();
}
private String pasteToService(LogPaster paster) {
@@ -5,15 +5,17 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.module.MultiverseModulesRegistry;
import org.mvplugins.multiverse.core.commands.DumpsLogPoster.LogsType;
import org.mvplugins.multiverse.core.commands.DumpsLogPoster.UploadType;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.event.MVDumpsDebugInfoEvent;
@@ -37,12 +39,16 @@ final class DumpsService {
this.fileUtils = fileUtils;
}
void postLogs(MVCommandIssuer issuer, UploadType servicesType) {
issuer.sendInfo(MVCorei18n.DUMPS_STARTING);
void postLogs(MVCommandIssuer issuer, LogsType logsType, UploadType servicesType, boolean paranoid) {
// Initialise and add info to the debug event
MVDumpsDebugInfoEvent versionEvent = createAndCallDebugInfoEvent();
new DumpsLogPoster(plugin, issuer, servicesType, getLogs(), versionEvent)
// Add plugin list if user isn't paranoid
if (!paranoid) {
versionEvent.putDetailedDebugInfo("plugins.md", "# Plugins\n\n" + getPluginList());
}
new DumpsLogPoster(issuer, logsType, servicesType, paranoid, getLogs(), versionEvent)
.runTaskAsynchronously(plugin);
}
@@ -67,18 +73,18 @@ final class DumpsService {
private @NotNull String readLogsFromFile(Path logsPath) {
String logs = "Could not read log";
// Try reading as UTF-8 first
// Try reading as ANSI encoded
try {
logs = Files.readString(logsPath, StandardCharsets.UTF_8);
} catch (IOException e) {
Logging.severe("Could not read logs/latest.log as UTF-8. Trying ISO-8859-1. See below for stack trace");
Logging.severe("Could not read logs/latest.log. See below for stack trace");
e.printStackTrace();
try {
logs = Files.readString(logsPath, StandardCharsets.ISO_8859_1);
logs = Files.readString(logsPath, StandardCharsets.UTF_8);
} catch (IOException ex) {
// It is some other strange encoding
Logging.severe("Could not read ./logs/latest.log as ISO-8859-1. See below for stack trace");
Logging.severe("Could not read ./logs/latest.log. See below for stack trace");
ex.printStackTrace();
}
}
@@ -86,6 +92,15 @@ final class DumpsService {
return logs;
}
private String getDebugInfoString() {
return "# Multiverse-Core Version info" + "\n\n"
+ " - Multiverse-Core Version: " + this.plugin.getDescription().getVersion() + '\n'
+ " - Bukkit Version: " + this.plugin.getServer().getVersion() + '\n'
+ " - Loaded Worlds: " + worldManager.getLoadedWorlds() + '\n'
+ " - Multiverse Plugins Loaded: " + StringFormatter.joinAnd(MultiverseModulesRegistry.get().getRegisteredPlugins()) + '\n'
+ " - Multiverse Plugins Count: " + MultiverseModulesRegistry.get().getPluginCount() + '\n';
}
private MVDumpsDebugInfoEvent createAndCallDebugInfoEvent() {
MVDumpsDebugInfoEvent event = new MVDumpsDebugInfoEvent();
addDebugInfoToEvent(event);
@@ -97,63 +112,34 @@ final class DumpsService {
// Add the legacy file, but as markdown, so it's readable
event.putDetailedDebugInfo("version.md", this.getDebugInfoString());
// Add server.properties if we found it
if (fileUtils.getServerProperties() != null) {
event.putDetailedDebugInfo(fileUtils.getServerProperties().getName(), fileUtils.getServerProperties());
} else {
Logging.warning("/mv dumps could not find server.properties. Not including file");
}
// add config.yml
File configFile = new File(plugin.getDataFolder(), "config.yml");
event.putDetailedDebugInfo("Multiverse-Core/config.yml", configFile);
// add worlds.yml
File worldsFile = new File(plugin.getDataFolder(), "worlds.yml");
event.putDetailedDebugInfo("Multiverse-Core/worlds.yml", worldsFile);
// Add bukkit.yml if we found it
if (fileUtils.getBukkitConfig() != null) {
event.putDetailedDebugInfo(fileUtils.getBukkitConfig().getName(), fileUtils.getBukkitConfig());
event.putDetailedDebugInfo(fileUtils.getBukkitConfig().getPath(), fileUtils.getBukkitConfig());
} else {
Logging.warning("/mv dumps could not find bukkit.yml. Not including file");
}
// Add spigot.yml
File spigotYml = fileUtils.getServerFolder().toPath().resolve("spigot.yml").toFile();
if (spigotYml.isFile()) {
event.putDetailedDebugInfo(spigotYml.getName(), spigotYml);
// Add server.properties if we found it
if (fileUtils.getServerProperties() != null) {
event.putDetailedDebugInfo(fileUtils.getServerProperties().getPath(), fileUtils.getServerProperties());
} else {
Logging.warning("/mv dumps could not find spigot.yml. Not including file");
Logging.warning("/mv dumps could not find server.properties. Not including file");
}
// Add paper-global.yml
File paperGlobalYml = fileUtils.getServerFolder().toPath().resolve("config/paper-global.yml").toFile();
if (paperGlobalYml.isFile()) {
event.putDetailedDebugInfo(paperGlobalYml.getName(), paperGlobalYml);
} else {
Logging.warning("/mv dumps could not find paper-global.yml. Not including file");
}
// add paper-world-defaults.yml
File paperWorldDefaultsYml = fileUtils.getServerFolder().toPath().resolve("config/paper-world-defaults.yml").toFile();
if (paperWorldDefaultsYml.isFile()) {
event.putDetailedDebugInfo(paperWorldDefaultsYml.getName(), paperWorldDefaultsYml);
} else {
Logging.warning("/mv dumps could not find paper-global.yml. Not including file");
}
// add multiverse-core config.yml
File configFile = new File(plugin.getDataFolder(), "config.yml");
event.putDetailedDebugInfo("multiverse-core/config.yml", configFile);
// add multiverse-core worlds.yml
File worldsFile = new File(plugin.getDataFolder(), "worlds.yml");
event.putDetailedDebugInfo("multiverse-core/worlds.yml", worldsFile);
// add multiverse-core anchors.yml
File anchorsFile = new File(plugin.getDataFolder(), "anchors.yml");
event.putDetailedDebugInfo("multiverse-core/anchors.yml", anchorsFile);
}
private String getDebugInfoString() {
return "# Multiverse-Core Version info" + "\n\n"
+ " - Multiverse-Core Version: " + this.plugin.getDescription().getVersion() + '\n'
+ " - Bukkit Version: " + this.plugin.getServer().getVersion() + '\n'
+ " - Loaded Worlds: " + worldManager.getLoadedWorlds() + '\n'
+ " - Multiverse Plugins Loaded: " + StringFormatter.joinAnd(MultiverseModulesRegistry.get().getRegisteredPlugins()) + '\n'
+ " - Multiverse Plugins Count: " + MultiverseModulesRegistry.get().getPluginCount() + '\n';
private String getPluginList() {
return " - " + StringUtils.join(Arrays.stream(plugin.getServer().getPluginManager().getPlugins())
.map(Object::toString)
.sorted()
.toList(), "\n - ");
}
}
@@ -48,7 +48,7 @@ class ImportCommand extends CoreCommand {
void onImportCommand(
MVCommandIssuer issuer,
// @Conditions("worldname:scope=new")
@Conditions("worldname:scope=new")
@Syntax("<name>")
@Description("{@@mv-core.import.name.description}")
String worldName,
@@ -67,8 +67,7 @@ class ImportCommand extends CoreCommand {
worldManager.importWorld(ImportWorldOptions.worldName(worldName)
.biome(parsedFlags.flagValue(flags.biome, ""))
.environment(environment)
.generator(parsedFlags.flagValue(flags.generator))
.generatorSettings(parsedFlags.flagValue(flags.generatorSettings, ""))
.generator(parsedFlags.flagValue(flags.generator, String.class))
.useSpawnAdjust(!parsedFlags.hasFlag(flags.noAdjustSpawn))
.doFolderCheck(!parsedFlags.hasFlag(flags.skipFolderCheck)))
.onSuccess(newWorld -> {
@@ -111,11 +110,6 @@ class ImportCommand extends CoreCommand {
private final CommandFlag skipFolderCheck = flag(CommandFlag.builder("--skip-folder-check")
.addAlias("-f")
.build());
private final CommandValueFlag<String> generatorSettings = flag(CommandValueFlag
.builder("--generator-settings", String.class)
.addAlias("-gs")
.build());
}
@Service
@@ -87,7 +87,6 @@ class InfoCommand extends CoreCommand {
private Map<String, String> getInfo(LoadedMultiverseWorld world) {
Map<String, String> outMap = new LinkedHashMap<>();
outMap.put("World Key", world.getKey().toString());
outMap.put("World Name", world.getName());
outMap.put("World Alias", world.getAlias());
outMap.put("World UID", world.getUID().toString());
@@ -104,7 +103,6 @@ class InfoCommand extends CoreCommand {
outMap.put("World Type", world.getWorldType().map(WorldType::getName).getOrNull());
outMap.put("Biome", world.getBiome());
outMap.put("Generator", world.getGenerator());
outMap.put("Generator Settings", world.getGeneratorSettings());
outMap.put("Generate Structures", world.canGenerateStructures().map(String::valueOf).getOrNull());
outMap.put("Auto Load", String.valueOf(world.isAutoLoad()));
@@ -114,12 +112,13 @@ class InfoCommand extends CoreCommand {
outMap.put("World Scale", String.valueOf(world.getScale()));
outMap.put("Weather Enabled", String.valueOf(world.isAllowWeather()));
outMap.put("Allow Flight", String.valueOf(world.isAllowFlight()));
outMap.put("Grant Advancements", String.valueOf(world.isAllowAdvancementGrant()));
outMap.put("Auto Heal", String.valueOf(world.getAutoHeal()));
outMap.put("Hunger Depletes", String.valueOf(world.isHunger()));
outMap.put("Keep Spawn In Memory", String.valueOf(world.isKeepSpawnInMemory()));
outMap.put("PVP Enabled", String.valueOf(world.getPvp()));
outMap.put("Portal Form", String.valueOf(world.getPortalForm()));
outMap.put("Player Limit", String.valueOf(world.getPlayerLimit()));
// getAnimalSpawningInfo(outMap, world);
// getMonsterSpawningInfo(outMap, world);
outMap.put("World Blacklist", String.join(", ", world.getWorldBlacklist()));
return outMap;
@@ -136,6 +135,30 @@ class InfoCommand extends CoreCommand {
}
}
// private void getAnimalSpawningInfo(Map<String, String> outMap, MultiverseWorld world) {
// if (world.isSpawningAnimals()) {
// outMap.put("Spawning Animals", "ALL");
// } else {
// if (!world.getSpawningAnimalsExceptions().isEmpty()) {
// outMap.put("Spawning Animals", world.getSpawningAnimalsExceptions().toString());
// } else {
// outMap.put("Spawning Animals", "NONE");
// }
// }
// }
//
// private void getMonsterSpawningInfo(Map<String, String> outMap, MultiverseWorld world) {
// if (world.isSpawningMonsters()) {
// outMap.put("Spawning Monsters", "ALL");
// } else {
// if (!world.getSpawningMonstersExceptions().isEmpty()) {
// outMap.put("Spawning Monsters", world.getSpawningMonstersExceptions().toString());
// } else {
// outMap.put("Spawning Monsters", "NONE");
// }
// }
// }
@Service
private static final class LegacyAlias extends InfoCommand implements LegacyAliasCommand {
@Inject
@@ -16,8 +16,8 @@ import org.mvplugins.multiverse.core.command.LegacyAliasCommand;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.FlagBuilder;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.RemovePlayerFlags;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
@@ -38,7 +38,7 @@ class LoadCommand extends CoreCommand {
@Subcommand("load")
@CommandPermission("multiverse.core.load")
@CommandCompletion("@mvworlds:scope=unloaded @flags:groupName=" + Flags.NAME)
@CommandCompletion("@mvworlds:scope=unloaded")
@Syntax("<world>")
@Description("{@@mv-core.load.description}")
void onLoadCommand(
@@ -49,7 +49,7 @@ class LoadCommand extends CoreCommand {
MultiverseWorld world,
@Optional
@Syntax("[--skip-folder-check]")
@Syntax("[--remove-players]")
@Description("")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
@@ -67,9 +67,9 @@ class LoadCommand extends CoreCommand {
}
@Service
private static final class Flags extends FlagBuilder {
private static final class Flags extends RemovePlayerFlags {
private static final String NAME = "mvload";
private static final String NAME = "mvunload";
@Inject
private Flags(@NotNull CommandFlagsManager flagsManager) {
@@ -1,121 +0,0 @@
package org.mvplugins.multiverse.core.commands;
import co.aikar.commands.annotation.CommandCompletion;
import co.aikar.commands.annotation.CommandPermission;
import co.aikar.commands.annotation.Description;
import co.aikar.commands.annotation.Flags;
import co.aikar.commands.annotation.Optional;
import co.aikar.commands.annotation.Single;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import jakarta.inject.Inject;
import org.bukkit.ChatColor;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.PageFilterFlags;
import org.mvplugins.multiverse.core.display.ContentDisplay;
import org.mvplugins.multiverse.core.display.filters.DefaultContentFilter;
import org.mvplugins.multiverse.core.display.handlers.PagedSendHandler;
import org.mvplugins.multiverse.core.display.parsers.MapContentProvider;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import java.util.Locale;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
@Service
@Subcommand("meta")
final class MetaCommand extends CoreCommand {
private final WorldManager worldManager;
private final PageFilterFlags flags;
@Inject
MetaCommand(@NotNull WorldManager worldManager, @NotNull PageFilterFlags flags) {
this.worldManager = worldManager;
this.flags = flags;
}
@Subcommand("info")
@CommandPermission("multiverse.core.meta.info")
@CommandCompletion("@mvworlds:scope=both|@flags:resolveUntil=arg1,groupName=" + PageFilterFlags.NAME + " " +
"@flags:groupName=" + PageFilterFlags.NAME)
@Syntax("[world] [--page <page>] [--filter <filter>]")
@Description("{@@mv-core.meta.info.description}")
void infoCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
@Description("{@@mv-core.meta.info.world.description}")
MultiverseWorld world,
@Optional
@Syntax("[--page <page>] [--filter <filter>]")
String[] flagArray
) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
ContentDisplay.create()
.addContent(MapContentProvider.forContent(world.getAllMeta())
.withKeyColor(ChatColor.AQUA)
.withValueColor(ChatColor.WHITE))
.withSendHandler(PagedSendHandler.create()
.withHeader(Message.of(MVCorei18n.META_INFO_HEADER, replace("{world}").with(world.getName())))
.noContentMessage(Message.of(MVCorei18n.META_INFO_NOCONTENT, replace("{world}").with(world.getName())))
.withTargetPage(parsedFlags.flagValue(flags.page, 1))
.withFilter(parsedFlags.flagValue(flags.filter, DefaultContentFilter.get())))
.send(issuer);
}
@Subcommand("modify")
@CommandPermission("multiverse.core.meta.modify")
@CommandCompletion("@mvworlds:scope=both set|remove @mvworldmetakey @empty")
@Syntax("[world] <set|remove> <key> [value]")
void modifyCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware,maxArgForAware=4")
@Syntax("[world]")
MultiverseWorld world,
@Syntax("<set|remove>")
String action,
@Syntax("<key>")
String key,
@Optional
@Single
@Syntax("[value]")
String value
) {
switch (action.toLowerCase(Locale.ROOT)) {
case "set" -> world.setMeta(key, value)
.onSuccess(ignore -> issuer.sendInfo(MVCorei18n.META_MODIFY_SET_SUCCESS,
replace("{key}").with(key),
replace("{value}").with(value),
Replace.WORLD.with(world.getName())))
.onFailure(throwable -> issuer.sendError(MVCorei18n.META_MODIFY_SET_FAILURE,
replace("{key}").with(key),
replace("{value}").with(value),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(throwable)));
case "remove" -> world.removeMeta(key)
.onSuccess(ignore -> issuer.sendInfo(MVCorei18n.META_MODIFY_REMOVE_SUCCESS,
replace("{key}").with(key),
Replace.WORLD.with(world.getName())))
.onFailure(throwable -> issuer.sendError(MVCorei18n.META_MODIFY_REMOVE_FAILURE,
replace("{key}").with(key),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(throwable)));
default -> issuer.sendError(MVCorei18n.META_MODIFY_INVALIDACTION, replace("{action}").with(action));
}
worldManager.saveWorldsConfig();
}
}
@@ -2,7 +2,6 @@ package org.mvplugins.multiverse.core.commands;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import co.aikar.commands.ACFUtil;
import co.aikar.commands.annotation.CommandAlias;
@@ -24,12 +23,9 @@ import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.CommandValueFlag;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.RemovePlayerDestinationFlags;
import org.mvplugins.multiverse.core.command.flags.RemovePlayerFlags;
import org.mvplugins.multiverse.core.command.queue.CommandQueueManager;
import org.mvplugins.multiverse.core.command.queue.CommandQueuePayload;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.destination.core.WorldDestination;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
@@ -69,8 +65,7 @@ class RegenCommand extends CoreCommand {
@Subcommand("regen")
@CommandPermission("multiverse.core.regen")
@CommandCompletion("@mvworlds:scope=loaded @flags:groupName=" + Flags.NAME)
@Syntax("<world> [--seed [seed]] [--reset-world-config] [--reset-gamerules] [--reset-world-border] " +
"[--remove-players [destination]]")
@Syntax("<world> [--seed [seed] --reset-world-config --reset-gamerules --reset-world-border --remove-players]")
@Description("{@@mv-core.regen.description}")
void onRegenCommand(
MVCommandIssuer issuer,
@@ -80,8 +75,7 @@ class RegenCommand extends CoreCommand {
LoadedMultiverseWorld world,
@Optional
@Syntax("[--seed [seed]] [--reset-world-config] [--reset-gamerules] [--reset-world-border] " +
"[--remove-players [destination]]")
@Syntax("[--seed [seed] --reset-world-config --reset-gamerules --reset-world-border --remove-players]")
@Description("{@@mv-core.regen.other.description}")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
@@ -97,9 +91,8 @@ class RegenCommand extends CoreCommand {
issuer.sendInfo(MVCorei18n.REGEN_REGENERATING, Replace.WORLD.with(world.getName()));
List<Player> worldPlayers = world.getPlayers().getOrElse(Collections.emptyList());
DestinationInstance<?, ?> removeToDestination = parsedFlags.flagValue(flags.removePlayers);
var future = Objects.nonNull(removeToDestination)
? playerWorldTeleporter.transferAllFromWorldToDestination(world, removeToDestination)
var future = parsedFlags.hasFlag(flags.removePlayers)
? playerWorldTeleporter.removeFromWorld(world)
: AsyncAttemptsAggregate.emptySuccess();
// todo: using future will hide stacktrace
@@ -135,18 +128,13 @@ class RegenCommand extends CoreCommand {
}
@Service
private static final class Flags extends RemovePlayerDestinationFlags {
private static final class Flags extends RemovePlayerFlags {
private static final String NAME = "mvregen";
@Inject
private Flags(
@NotNull CommandFlagsManager flagsManager,
@NotNull WorldManager worldManager,
@NotNull DestinationsProvider destinationsProvider,
@NotNull WorldDestination worldDestination
) {
super(NAME, flagsManager, worldManager, destinationsProvider, worldDestination);
private Flags(@NotNull CommandFlagsManager flagsManager) {
super(NAME, flagsManager);
}
private final CommandValueFlag<String> seed = flag(CommandValueFlag.builder("--seed", String.class)
@@ -14,13 +14,11 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.LegacyAliasCommand;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.RemovePlayerDestinationFlags;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.destination.core.WorldDestination;
import org.mvplugins.multiverse.core.command.flags.RemovePlayerFlags;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.utils.result.AsyncAttemptsAggregate;
@@ -29,8 +27,6 @@ import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter;
import org.mvplugins.multiverse.core.world.options.RemoveWorldOptions;
import java.util.Objects;
@Service
class RemoveCommand extends CoreCommand {
@@ -52,7 +48,7 @@ class RemoveCommand extends CoreCommand {
@Subcommand("remove")
@CommandPermission("multiverse.core.remove")
@CommandCompletion("@mvworlds:scope=both @flags:groupName=" + Flags.NAME)
@Syntax("<world> [--remove-players [destination]] [--no-unload-bukkit-world] [--no-save]")
@Syntax("<world>")
@Description("{@@mv-core.remove.description}")
void onRemoveCommand(
MVCommandIssuer issuer,
@@ -62,16 +58,13 @@ class RemoveCommand extends CoreCommand {
MultiverseWorld world,
@Optional
@Syntax("[--remove-players [destination]] [--no-unload-bukkit-world] [--no-save]")
@Syntax("[--remove-players]")
@Description("")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
DestinationInstance<?, ?> removeToDestination = parsedFlags.flagValue(flags.removePlayers);
var future = Objects.nonNull(removeToDestination)
? world.asLoadedWorld()
.map(loadedWorld -> playerWorldTeleporter.transferAllFromWorldToDestination(loadedWorld, removeToDestination))
.getOrElse(AsyncAttemptsAggregate::emptySuccess)
var future = parsedFlags.hasFlag(flags.removePlayers)
? worldManager.getLoadedWorld(world).map(playerWorldTeleporter::removeFromWorld).getOrElse(AsyncAttemptsAggregate::emptySuccess)
: AsyncAttemptsAggregate.emptySuccess();
future.onSuccess(() -> doWorldRemoving(issuer, world, parsedFlags))
@@ -92,18 +85,13 @@ class RemoveCommand extends CoreCommand {
}
@Service
private static final class Flags extends RemovePlayerDestinationFlags {
private static final class Flags extends RemovePlayerFlags {
private static final String NAME = "mvremove";
@Inject
private Flags(
@NotNull CommandFlagsManager flagsManager,
@NotNull WorldManager worldManager,
@NotNull DestinationsProvider destinationsProvider,
@NotNull WorldDestination worldDestination
) {
super(NAME, flagsManager, worldManager, destinationsProvider, worldDestination);
private Flags(@NotNull CommandFlagsManager flagsManager) {
super(NAME, flagsManager);
}
private final CommandFlag noUnloadBukkitWorld = flag(CommandFlag.builder("--no-unload-bukkit-world")
@@ -14,13 +14,11 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.LegacyAliasCommand;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.RemovePlayerDestinationFlags;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.destination.core.WorldDestination;
import org.mvplugins.multiverse.core.command.flags.RemovePlayerFlags;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.utils.result.AsyncAttemptsAggregate;
@@ -29,8 +27,6 @@ import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter;
import org.mvplugins.multiverse.core.world.options.UnloadWorldOptions;
import java.util.Objects;
@Service
class UnloadCommand extends CoreCommand {
@@ -52,7 +48,7 @@ class UnloadCommand extends CoreCommand {
@Subcommand("unload")
@CommandPermission("multiverse.core.unload")
@CommandCompletion("@mvworlds @flags:groupName=" + Flags.NAME)
@Syntax("<world> [--remove-players [destination]] [--no-save]")
@Syntax("<world>")
@Description("{@@mv-core.unload.description}")
void onUnloadCommand(
MVCommandIssuer issuer,
@@ -62,16 +58,15 @@ class UnloadCommand extends CoreCommand {
LoadedMultiverseWorld world,
@Optional
@Syntax("[--remove-players [destination]] [--no-save]")
@Syntax("[--remove-players] [--no-save]")
@Description("{@@mv-core.gamerules.description.page}")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
issuer.sendInfo(MVCorei18n.UNLOAD_UNLOADING, Replace.WORLD.with(world.getAliasOrName()));
DestinationInstance<?, ?> removeToDestination = parsedFlags.flagValue(flags.removePlayers);
var future = Objects.nonNull(removeToDestination)
? playerWorldTeleporter.transferAllFromWorldToDestination(world, removeToDestination)
var future = parsedFlags.hasFlag(flags.removePlayers)
? playerWorldTeleporter.removeFromWorld(world)
: AsyncAttemptsAggregate.emptySuccess();
future.onSuccess(() -> doWorldUnloading(issuer, world, parsedFlags))
@@ -93,18 +88,13 @@ class UnloadCommand extends CoreCommand {
}
@Service
private static final class Flags extends RemovePlayerDestinationFlags {
private static final class Flags extends RemovePlayerFlags {
private static final String NAME = "mvunload";
@Inject
private Flags(
@NotNull CommandFlagsManager flagsManager,
@NotNull WorldManager worldManager,
@NotNull DestinationsProvider destinationsProvider,
@NotNull WorldDestination worldDestination
) {
super(NAME, flagsManager, worldManager, destinationsProvider, worldDestination);
private Flags(@NotNull CommandFlagsManager flagsManager) {
super(NAME, flagsManager);
}
private final CommandFlag noUnloadBukkitWorld = flag(CommandFlag.builder("--no-unload-bukkit-world")
@@ -12,11 +12,8 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.utils.compatibility.WorldBorderCompatibility;
import org.mvplugins.multiverse.core.utils.tick.TickDuration;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import java.time.temporal.ChronoUnit;
import java.util.function.Consumer;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
@@ -36,14 +33,15 @@ final class WorldBorderCommand extends CoreCommand {
@Optional
@Default("0")
@Syntax("[time]")
TickDuration duration,
int time,
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
worldBorderAction(issuer, world, worldBorder ->
onWorldBorderSet(issuer, worldBorder.getSize() + size, duration, world));
worldBorderAction(issuer, world, worldBorder -> {
onWorldBorderSet(issuer, worldBorder.getSize() + size, time, world);
});
}
@Subcommand("center")
@@ -147,7 +145,7 @@ final class WorldBorderCommand extends CoreCommand {
@Optional
@Default("0")
@Syntax("[time]")
TickDuration duration,
int time,
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
@@ -160,18 +158,15 @@ final class WorldBorderCommand extends CoreCommand {
Replace.WORLD.with(world.getAliasOrName()));
return;
}
if (!WorldBorderCompatibility.supportsChangeSizeInTicks() && !duration.isExactTo(ChronoUnit.SECONDS)) {
setRoundOffError(issuer, duration);
}
WorldBorderCompatibility.changeSizeDuration(worldBorder, size, duration);
if (duration.toTicks() <= 0) {
worldBorder.setSize(size, time);
if (time <= 0) {
issuer.sendMessage(MVCorei18n.WORLDBORDER_SET_IMMEDIATE,
replace("{size}").with(worldBorder.getSize()),
Replace.WORLD.with(world.getAliasOrName()));
} else {
issuer.sendMessage(previousSize > size ? MVCorei18n.WORLDBORDER_SET_GROWING : MVCorei18n.WORLDBORDER_SET_SHRINKING,
replace("{size}").with(size),
replace("{time}").with(String.format("%.2f", duration.toSeconds())),
replace("{time}").with(time),
Replace.WORLD.with(world.getAliasOrName()));
}
});
@@ -206,34 +201,25 @@ final class WorldBorderCommand extends CoreCommand {
MVCommandIssuer issuer,
@Syntax("<time>")
TickDuration duration,
int time,
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
worldBorderAction(issuer, world, worldBorder -> {
if (WorldBorderCompatibility.getWarningTimeTicks(worldBorder) == duration.toTicks()) {
if (worldBorder.getWarningTime() == time) {
issuer.sendMessage(MVCorei18n.WORLDBORDER_WARNINGTIME_NOTHINGCHANGED,
Replace.WORLD.with(world.getAliasOrName()));
return;
}
if (!WorldBorderCompatibility.supportsSetWarningTimeInTicks() && !duration.isExactTo(ChronoUnit.SECONDS)) {
setRoundOffError(issuer, duration);
}
WorldBorderCompatibility.setWarningTimeDuration(worldBorder, duration);
worldBorder.setWarningTime(time);
issuer.sendMessage(MVCorei18n.WORLDBORDER_WARNINGTIME_SUCCESS,
replace("{time}").with(String.format("%.2f", WorldBorderCompatibility.getWarningTime(worldBorder))),
replace("{time}").with(worldBorder.getWarningTime()),
Replace.WORLD.with(world.getAliasOrName()));
});
}
private void setRoundOffError(MVCommandIssuer issuer, TickDuration duration) {
issuer.sendError(MVCorei18n.WORLDBORDER_ROUNDOFF_WARNING,
replace("{fromTime}").with(String.format("%.2f", duration.toSeconds())),
replace("{toTime}").with(Math.round(duration.toSeconds())));
}
private void worldBorderAction(MVCommandIssuer issuer, LoadedMultiverseWorld world, Consumer<WorldBorder> worldBorderAction) {
Try.run(() -> world.getWorldBorder().peek(worldBorderAction))
.onFailure(error -> issuer.sendError(error.getLocalizedMessage()));
@@ -186,16 +186,6 @@ public final class CoreConfig {
return configHandle.get(configNodes.enforceFlight);
}
@ApiStatus.AvailableSince("5.5")
public Try<Void> setGamemodeAndFlightEnforceDelay(int delayTicks) {
return configHandle.set(configNodes.gamemodeAndFlightEnforceDelay, delayTicks);
}
@ApiStatus.AvailableSince("5.5")
public int getGamemodeAndFlightEnforceDelay() {
return configHandle.get(configNodes.gamemodeAndFlightEnforceDelay);
}
@ApiStatus.AvailableSince("5.3")
public Try<Void> setApplyEntitySpawnRate(boolean applyEntitySpawnRate) {
return configHandle.set(configNodes.applyEntitySpawnRate, applyEntitySpawnRate);
@@ -503,26 +493,6 @@ public final class CoreConfig {
return configHandle.get(configNodes.registerPapiHook);
}
@ApiStatus.AvailableSince("5.5")
public Try<Void> setWarnInvalidPapiFormat(boolean warnInvalidPapiFormat) {
return configHandle.set(configNodes.warnInvalidPapiFormat, warnInvalidPapiFormat);
}
@ApiStatus.AvailableSince("5.5")
public boolean getWarnInvalidPapiFormat() {
return configHandle.get(configNodes.warnInvalidPapiFormat);
}
@ApiStatus.AvailableSince("5.5")
public Try<Void> setInvalidPapiFormatReturnsBlank(boolean invalidPapiFormatReturnsBlank) {
return configHandle.set(configNodes.invalidPapiFormatReturnsBlank, invalidPapiFormatReturnsBlank);
}
@ApiStatus.AvailableSince("5.5")
public boolean getInvalidPapiFormatReturnsBlank() {
return configHandle.get(configNodes.invalidPapiFormatReturnsBlank);
}
/**
* {@inheritDoc}
*/
@@ -651,16 +621,6 @@ public final class CoreConfig {
return configHandle.get(configNodes.eventPriorityPlayerTeleport);
}
@ApiStatus.AvailableSince("5.5")
public Try<Void> setEventPriorityPlayerWorldChange(EventPriority eventPriorityPlayerWorldChange) {
return configHandle.set(configNodes.eventPriorityPlayerWorldChange, eventPriorityPlayerWorldChange);
}
@ApiStatus.AvailableSince("5.5")
public EventPriority getEventPriorityPlayerWorldChange() {
return configHandle.get(configNodes.eventPriorityPlayerWorldChange);
}
public Try<Void> setBukkitYmlPath(String bukkitYmlPath) {
return configHandle.set(configNodes.bukkitYmlPath, bukkitYmlPath);
}
@@ -135,16 +135,6 @@ final class CoreConfigNodes {
.name("enforce-flight")
.build());
final ConfigNode<Integer> gamemodeAndFlightEnforceDelay = node(ConfigNode.builder("world.gamemode-and-flight-enforce-delay", Integer.class)
.comment("")
.comment("Sets the delay in ticks before Multiverse enforces gamemode and flight ability on world change.")
.comment("Increase this value if you are experiencing issues with other plugins overriding gamemode or flight ability.")
.comment("Or set to 0 to enforce immediately during world change event.")
.defaultValue(1)
.name("gamemode-and-flight-enforce-delay")
.suggester((sender -> List.of("0", "1", "2", "5", "10")))
.build());
final ConfigNode<Boolean> applyEntitySpawnRate = node(ConfigNode.builder("world.apply-entity-spawn-rate", Boolean.class)
.comment("")
.comment("Sets whether Multiverse will apply the world's entity `tick-rate` config in worlds.yml.")
@@ -220,10 +210,7 @@ final class CoreConfigNodes {
final ConfigNode<PassengerModes> passengerMode = node(ConfigNode.builder("teleport.passenger-mode", PassengerModes.class)
.comment("")
.comment("Configures how passengers and vehicles are handled when an entity is teleported. This config is")
.comment("only applied to /mvtp, join-destination and first-spawn-override. It will not touch any other")
.comment("teleport by other plugins or commands.")
.comment("========= Available options: =========")
.comment("Configures how passengers and vehicles are handled when an entity is teleported.")
.comment(" default: Server will handle passengers and vehicles. On papermc 1.21.10+, all will be retained by default.")
.comment(" On older versions, all will be dismounted if world changes.")
.comment(" dismount_passengers: Passengers will be removed from the parent entity before the teleport.")
@@ -403,23 +390,6 @@ final class CoreConfigNodes {
.name("register-papi-hook")
.build());
final ConfigNode<Boolean> warnInvalidPapiFormat = node(ConfigNode.builder("messaging.warn-invalid-papi-format", Boolean.class)
.comment("")
.comment("This config option defines whether or not Multiverse should warn about invalid PlaceholderAPI formats.")
.comment("Only applies if PlaceholderAPI is installed, and only to multiverse placeholders.")
.defaultValue(true)
.name("warn-invalid-papi-format")
.build());
final ConfigNode<Boolean> invalidPapiFormatReturnsBlank = node(ConfigNode.builder("messaging.invalid-papi-format-returns-blank", Boolean.class)
.comment("")
.comment("By default, invalid formats will return the original placeholder string.")
.comment("This config option defines whether or not Multiverse should show invalid PlaceholderAPI formats as blank.")
.comment("Only applies if PlaceholderAPI is installed, and only to multiverse placeholders.")
.defaultValue(false)
.name("invalid-papi-format-returns-blank")
.build());
final ConfigNode<Locale> defaultLocale = node(ConfigNode.builder("messaging.default-locale", Locale.class)
.comment("")
.comment("This config option defines the default language Multiverse should use.")
@@ -558,18 +528,6 @@ final class CoreConfigNodes {
"full effect after a server restart."))
.build());
final ConfigNode<EventPriority> eventPriorityPlayerWorldChange = node(ConfigNode.builder("event-priority.player-changed-world", EventPriority.class)
.defaultValue(EventPriority.NORMAL)
.name("event-priority-player-changed-world")
.comment("")
.comment("This config option defines the priority for the PlayerChangedWorldEvent.")
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-changed-world", newValue))
.onChange((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-changed-world' config option will only take " +
"full effect after a server restart."))
.build());
private final ConfigHeaderNode miscHeader = node(ConfigHeaderNode.builder("misc")
.comment("")
.comment("")
@@ -3,9 +3,11 @@ package org.mvplugins.multiverse.core.config.handle;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Logger;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
@@ -17,7 +19,6 @@ import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.migration.ConfigMigrator;
import org.mvplugins.multiverse.core.config.node.ListValueNode;
import org.mvplugins.multiverse.core.config.node.MapValueNode;
import org.mvplugins.multiverse.core.config.node.NodeGroup;
import org.mvplugins.multiverse.core.config.node.ValueNode;
@@ -92,13 +93,17 @@ public abstract class BaseConfigurationHandle<C extends ConfigurationSection> {
}
protected <T> T deserializeNodeFromConfig(ValueNode<T> node) {
if (node.getSerializer() == null) {
return Option.of(config.getObject(node.getPath(), node.getType())).getOrElse(node::getDefaultValue);
}
return Try.of(() -> {
var value = config.get(node.getPath());
return value == null ? node.getDefaultValue() : node.deserialize(value);
})
.flatMap(value -> node.validate(value).map(ignore -> value))
.onFailure(e -> Logging.warning("Failed to deserialize node %s: %s",
node.getPath(), e.getMessage()))
if (value == null) {
return node.getDefaultValue();
}
return node.getSerializer().deserialize(value, node.getType());
}).flatMap(value -> node.validate(value).map(ignore -> value))
.onFailure(e -> Logging.warning("Failed to deserialize node %s: %s", node.getPath(), e.getMessage()))
.getOrElse(node::getDefaultValue);
}
@@ -119,7 +124,12 @@ public abstract class BaseConfigurationHandle<C extends ConfigurationSection> {
if (value == null) {
value = node.getDefaultValue();
}
config.set(node.getPath(), node.serialize(value));
if (node.getSerializer() != null) {
var serialized = node.getSerializer().serialize(value, node.getType());
config.set(node.getPath(), serialized);
} else {
config.set(node.getPath(), value);
}
}
/**
@@ -153,15 +163,6 @@ public abstract class BaseConfigurationHandle<C extends ConfigurationSection> {
return set(Bukkit.getConsoleSender(), node, value);
}
public <K, V> Try<Void> set(@NotNull MapValueNode<K, V> node, K key, V value) {
return node.validateEntry(key, value).map(ignore -> {
Map<K,V> map = get(node);
map.put(key, value);
// node.onSetEntryValue(null, key, null, value);
return null;
});
}
/**
* Sets the value of a node, if the validator is not null, it will be tested first.
*
@@ -220,18 +221,6 @@ public abstract class BaseConfigurationHandle<C extends ConfigurationSection> {
});
}
public <K, V> Try<Void> remove(@NotNull MapValueNode<K, V> node, K key) {
return node.validateKey(key).map(ignore -> {
Map<K,V> map = get(node);
V value = map.remove(key);
if (value == null) {
throw new IllegalArgumentException("Cannot remove entry as it is already not in the map!");
}
// node.onSetEntryValue(key, value, null);
return null;
});
}
/**
* Sets the default value of a node.
*
@@ -112,5 +112,10 @@ public abstract class FileConfigurationHandle<C extends FileConfiguration> exten
* @return The configuration handle.
*/
public abstract @NotNull FileConfigurationHandle<C> build();
@SuppressWarnings("unchecked")
protected B self() {
return (B) this;
}
}
}
@@ -180,24 +180,6 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
return serializer;
}
/**
* {@inheritDoc}
*/
@Override
public T deserialize(@Nullable Object object) {
return serializer == null
? (type.isInstance(object)) ? type.cast(object) : getDefaultValue()
: serializer.deserialize(object, type);
}
/**
* {@inheritDoc}
*/
@Override
public Object serialize(T value) {
return serializer == null ? value : serializer.serialize(value, type);
}
/**
* {@inheritDoc}
*/
@@ -120,9 +120,9 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
private void setDefaultSuggester() {
if (itemSuggester instanceof SenderNodeSuggester senderItemSuggester) {
this.suggester = (SenderNodeSuggester)(sender, input) ->
StringFormatter.addOnToCommaSeparated(input, senderItemSuggester.suggest(sender, input));
StringFormatter.addonToCommaSeperated(input, senderItemSuggester.suggest(sender, input));
} else {
this.suggester = input -> StringFormatter.addOnToCommaSeparated(input, itemSuggester.suggest(input));
this.suggester = input -> StringFormatter.addonToCommaSeperated(input, itemSuggester.suggest(input));
}
}
@@ -283,7 +283,7 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
//noinspection unchecked
super(path, (Class<List<I>>) (Object) List.class);
this.itemType = itemType;
this.defaultValue = ArrayList::new;
this.defaultValue = () -> (List<I>) new ArrayList<>();
}
/**
@@ -1,603 +0,0 @@
package org.mvplugins.multiverse.core.config.node;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.ConfigurationSection;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.node.functions.DefaultStringParserProvider;
import org.mvplugins.multiverse.core.config.node.functions.DefaultSuggesterProvider;
import org.mvplugins.multiverse.core.config.node.functions.NodeChangeCallback;
import org.mvplugins.multiverse.core.config.node.functions.NodeStringParser;
import org.mvplugins.multiverse.core.config.node.functions.NodeSuggester;
import org.mvplugins.multiverse.core.config.node.functions.NodeValueCallback;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeStringParser;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeSuggester;
import org.mvplugins.multiverse.core.config.node.serializer.DefaultSerializerProvider;
import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer;
import org.mvplugins.multiverse.core.exceptions.MultiverseException;
import org.mvplugins.multiverse.core.utils.REPatterns;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* A config node that contains key-value mappings.
*
* @param <K> The key type.
* @param <V> The value type.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public class MapConfigNode<K, V> extends ConfigNode<Map<K, V>> implements MapValueNode<K, V> {
/**
* Creates a new builder for a {@link MapConfigNode}.
*
* @param path The path of the node.
* @param keyType The map key type.
* @param valueType The map value type.
* @param <K> The key type.
* @param <V> The value type.
* @return The new builder.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static @NotNull <K, V> Builder<K, V> mapBuilder(
@NotNull String path,
@NotNull Class<K> keyType,
@NotNull Class<V> valueType) {
return new Builder<>(path, keyType, valueType);
}
protected final @NotNull Class<K> keyType;
protected final @NotNull Class<V> valueType;
protected final @Nullable NodeSuggester keySuggester;
protected final @Nullable NodeSuggester valueSuggester;
protected final @Nullable NodeStringParser<K> keyStringParser;
protected final @Nullable NodeStringParser<V> valueStringParser;
protected final @Nullable NodeSerializer<K> keySerializer;
protected final @Nullable NodeSerializer<V> valueSerializer;
protected final @Nullable Function<K, Try<Void>> keyValidator;
protected final @Nullable Function<V, Try<Void>> valueValidator;
protected MapConfigNode(
@NotNull String path,
@NotNull String[] comments,
@Nullable String name,
@NotNull Class<Map<K, V>> type,
@NotNull String[] aliases,
@Nullable Supplier<Map<K, V>> defaultValueSupplier,
@Nullable NodeSuggester suggester,
@Nullable NodeStringParser<Map<K, V>> stringParser,
@Nullable NodeSerializer<Map<K, V>> serializer,
@Nullable Function<Map<K, V>, Try<Void>> validator,
@Nullable NodeValueCallback<Map<K, V>> onLoad,
@Nullable NodeChangeCallback<Map<K, V>> onLoadAndChange,
@Nullable NodeChangeCallback<Map<K, V>> onChange,
@NotNull Class<K> keyType,
@NotNull Class<V> valueType,
@Nullable NodeSuggester keySuggester,
@Nullable NodeSuggester valueSuggester,
@Nullable NodeStringParser<K> keyStringParser,
@Nullable NodeStringParser<V> valueStringParser,
@Nullable NodeSerializer<K> keySerializer,
@Nullable NodeSerializer<V> valueSerializer,
@Nullable Function<K, Try<Void>> keyValidator,
@Nullable Function<V, Try<Void>> valueValidator) {
super(path, comments, name, type, aliases, defaultValueSupplier, suggester, stringParser, serializer,
validator, onLoad, onLoadAndChange, onChange);
this.keyType = keyType;
this.valueType = valueType;
this.keySuggester = keySuggester != null
? keySuggester
: DefaultSuggesterProvider.getDefaultSuggester(keyType);
this.valueSuggester = valueSuggester != null
? valueSuggester
: DefaultSuggesterProvider.getDefaultSuggester(valueType);
this.keyStringParser = keyStringParser != null
? keyStringParser
: DefaultStringParserProvider.getDefaultStringParser(keyType);
this.valueStringParser = valueStringParser != null
? valueStringParser
: DefaultStringParserProvider.getDefaultStringParser(valueType);
this.keySerializer = keySerializer != null
? keySerializer
: DefaultSerializerProvider.getDefaultSerializer(keyType);
this.valueSerializer = valueSerializer != null
? valueSerializer
: DefaultSerializerProvider.getDefaultSerializer(valueType);
this.keyValidator = keyValidator != null
? keyValidator
: defaultYamlKeyValidator();
this.valueValidator = valueValidator;
setDefaults();
}
private Function<K, Try<Void>> defaultYamlKeyValidator() {
return key -> Try.of(() -> {
if (!REPatterns.YAML_KEY.matcher(String.valueOf(serializeKey(key))).matches()) {
throw new MultiverseException("Invalid yaml key: '" + key + "'. Keys can only " +
"contain alphanumeric characters, underscores and hyphens.");
}
return null;
});
}
private void setDefaults() {
if ((this.keySuggester != null || this.valueSuggester != null) && this.suggester == null) {
setDefaultSuggester();
}
if (this.keyStringParser != null && this.valueStringParser != null && this.stringParser == null) {
setDefaultStringParser();
}
if ((this.keyValidator != null || this.valueValidator != null) && this.validator == null) {
setDefaultValidator();
}
if ((this.keySerializer != null || this.valueSerializer != null) && this.serializer == null) {
setDefaultSerializer();
}
if (this.defaultValue == null) {
this.defaultValue = LinkedHashMap::new;
}
}
private void setDefaultSuggester() {
if (this.keySuggester instanceof SenderNodeSuggester || this.valueSuggester instanceof SenderNodeSuggester) {
this.suggester = (SenderNodeSuggester) this::suggestEntries;
} else {
this.suggester = input -> suggestEntries(null, input);
}
}
private @NotNull Collection<String> suggestEntries(@Nullable CommandSender sender, @Nullable String input) {
String content = input == null ? "" : input;
int lastComma = content.lastIndexOf(',');
String prefix = lastComma == -1 ? "" : content.substring(0, lastComma + 1);
String currentEntry = lastComma == -1 ? content : content.substring(lastComma + 1);
String[] keyValue = REPatterns.EQUALS.split(currentEntry, 2);
if (keyValue.length == 2 && valueSuggester != null) {
String keyPart = keyValue[0];
String valuePart = keyValue[1];
return suggestFrom(valueSuggester, sender, valuePart).stream()
.map(suggestedValue -> prefix + keyPart + "=" + suggestedValue)
.toList();
}
if (keySuggester == null) {
return Collections.emptyList();
}
return suggestFrom(keySuggester, sender, currentEntry).stream()
.map(suggestedKey -> prefix + suggestedKey + "=")
.toList();
}
private @NotNull Collection<String> suggestFrom(
@NotNull NodeSuggester suggester,
@Nullable CommandSender sender,
@Nullable String input) {
if (sender != null && suggester instanceof SenderNodeSuggester senderSuggester) {
return senderSuggester.suggest(sender, input);
}
return suggester.suggest(input);
}
private void setDefaultStringParser() {
this.stringParser = (input, type) -> Try.of(() -> {
Map<K, V> parsed = new LinkedHashMap<>();
if (input == null || input.isBlank()) {
return parsed;
}
for (String entry : REPatterns.SEMICOLON.split(input)) {
String[] keyValue = REPatterns.EQUALS.split(entry, 2);
if (keyValue.length != 2) {
throw new IllegalArgumentException("Invalid map entry '" + entry + "'. Expected format key=value");
}
NodeStringParser<K> keyParser = Objects.requireNonNull(keyStringParser, "keyStringParser");
NodeStringParser<V> valueParser = Objects.requireNonNull(valueStringParser, "valueStringParser");
K parsedKey = keyParser.parse(keyValue[0].trim(), keyType).get();
V parsedValue = valueParser.parse(keyValue[1].trim(), valueType).get();
parsed.put(parsedKey, parsedValue);
}
return parsed;
});
}
private void setDefaultValidator() {
this.validator = value -> Try.of(() -> {
if (value == null) {
return null;
}
for (Map.Entry<K, V> entry : value.entrySet()) {
if (keyValidator != null) {
keyValidator.apply(entry.getKey()).get();
}
if (valueValidator != null) {
valueValidator.apply(entry.getValue()).get();
}
}
return null;
});
}
private void setDefaultSerializer() {
this.serializer = new NodeSerializer<>() {
@Override
public Map<K, V> deserialize(Object object, Class<Map<K, V>> type) {
if (object == null) {
return new LinkedHashMap<>();
}
if (object instanceof Map<?, ?> rawMap) {
Map<K, V> result = new LinkedHashMap<>();
rawMap.forEach((key, value) -> {
Map.Entry<K, V> entry = deserializeEntry(key, value);
result.put(entry.getKey(), entry.getValue());
});
return result;
}
if (object instanceof ConfigurationSection section) {
Map<K, V> result = new LinkedHashMap<>();
for (String key : section.getKeys(false)) {
Object rawValue = section.get(key);
Map.Entry<K, V> entry = deserializeEntry(key, rawValue);
result.put(entry.getKey(), entry.getValue());
}
return result;
}
throw new IllegalArgumentException("Invalid map object '" + object + "'. Expected map or section.");
}
@Override
public Object serialize(Map<K, V> object, Class<Map<K, V>> type) {
Map<Object, Object> serialized = new LinkedHashMap<>();
if (object == null) {
return serialized;
}
object.forEach((key, value) -> {
Map.Entry<Object, Object> entry = serializeEntry(key, value);
serialized.put(entry.getKey(), entry.getValue());
});
return serialized;
}
};
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Try<Void> validateKey(@Nullable K key) {
return keyValidator == null ? Try.success(null) : keyValidator.apply(key);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Try<Void> validateValue(@Nullable V value) {
return valueValidator == null ? Try.success(null) : valueValidator.apply(value);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Try<Void> validateEntry(@Nullable K key, @Nullable V value) {
return validateKey(key).flatMap(ignored -> validateValue(value));
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Map.Entry<K, V> deserializeEntry(@Nullable Object key, @Nullable Object value) {
K deserializedKey = keySerializer == null
? keyType.isInstance(key) ? keyType.cast(key) : null
: keySerializer.deserialize(key, keyType);
V deserializedValue = valueSerializer == null
? keyType.isInstance(value) ? valueType.cast(value) : null
: valueSerializer.deserialize(value, valueType);
return new SimpleEntry<>(deserializedKey, deserializedValue);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Map.Entry<Object, Object> serializeEntry(@Nullable K key, @Nullable V value) {
return new SimpleEntry<>(serializeKey(key), serializeValue(value));
}
private @Nullable Object serializeKey(@Nullable K key) {
return keySerializer == null ? key : keySerializer.serialize(key, keyType);
}
private @Nullable Object serializeValue(@Nullable V value) {
return valueSerializer == null ? value : valueSerializer.serialize(value, valueType);
}
private record SimpleEntry<K, V>(K key, V value) implements Map.Entry<K, V> {
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
@Override
public V setValue(V value) {
throw new UnsupportedOperationException();
}
}
/**
* Builder for {@link MapConfigNode} instances.
*
* <p>This builder extends {@link ConfigNode.Builder} with additional configuration specific to map entries:
* entry-level suggesters, string parsers, serializers and validators for both keys and values. When any
* entry-level component is omitted the builder provides a sensible default (for example default parsers,
* suggesters, serializers or an empty map default value).
*
* @param <K> The map key type.
* @param <V> The map value type.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static class Builder<K, V> extends ConfigNode.Builder<Map<K, V>, Builder<K, V>> {
protected final @NotNull Class<K> keyType;
protected final @NotNull Class<V> valueType;
protected @Nullable NodeSuggester keySuggester;
protected @Nullable NodeSuggester valueSuggester;
protected @Nullable NodeStringParser<K> keyStringParser;
protected @Nullable NodeStringParser<V> valueStringParser;
protected @Nullable NodeSerializer<K> keySerializer;
protected @Nullable NodeSerializer<V> valueSerializer;
protected @Nullable Function<K, Try<Void>> keyValidator;
protected @Nullable Function<V, Try<Void>> valueValidator;
/**
* Creates a new builder for a map config node.
*
* @param path The configuration path for the node.
* @param keyType The runtime type of the map keys.
* @param valueType The runtime type of the map values.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
protected Builder(@NotNull String path, @NotNull Class<K> keyType, @NotNull Class<V> valueType) {
//noinspection unchecked
super(path, (Class<Map<K, V>>) (Object) Map.class);
this.keyType = keyType;
this.valueType = valueType;
this.defaultValue = LinkedHashMap::new;
}
/**
* Sets a suggester for map keys. The suggester is used when computing tab-completion suggestions for the
* key part of an entry.
*
* @param keySuggester The suggester to use for keys.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> keySuggester(@NotNull NodeSuggester keySuggester) {
this.keySuggester = keySuggester;
return self();
}
/**
* Sets a sender-aware suggester for map keys. Use this when suggestions depend on the command sender
* (permissions, location, etc.).
*
* @param keySuggester The sender-aware suggester.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> keySuggester(@NotNull SenderNodeSuggester keySuggester) {
this.keySuggester = keySuggester;
return self();
}
/**
* Sets a suggester for map values. The suggester is used when computing tab-completion suggestions for the
* value part of an entry.
*
* @param valueSuggester The suggester to use for values.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> valueSuggester(@NotNull NodeSuggester valueSuggester) {
this.valueSuggester = valueSuggester;
return self();
}
/**
* Sets a sender-aware suggester for map values. Use this when value suggestions depend on the
* command sender.
*
* @param valueSuggester The sender-aware suggester.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> valueSuggester(@NotNull SenderNodeSuggester valueSuggester) {
this.valueSuggester = valueSuggester;
return self();
}
/**
* Sets the string parser for keys. The parser is used when parsing user-provided key strings into key
* instances (e.g. when parsing a map from a single-line string representation).
*
* @param keyStringParser The parser to use for keys.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> keyStringParser(@NotNull NodeStringParser<K> keyStringParser) {
this.keyStringParser = keyStringParser;
return self();
}
/**
* Sets a sender-aware string parser for keys. Use this when parsing depends on the sender context.
*
* @param keyStringParser The sender-aware key parser.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> keyStringParser(@NotNull SenderNodeStringParser<K> keyStringParser) {
this.keyStringParser = keyStringParser;
return self();
}
/**
* Sets the string parser for values. The parser converts the textual part after '=' in an entry into the
* value type.
*
* @param valueStringParser The parser to use for values.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> valueStringParser(@NotNull NodeStringParser<V> valueStringParser) {
this.valueStringParser = valueStringParser;
return self();
}
/**
* Sets a sender-aware string parser for values.
*
* @param valueStringParser The sender-aware value parser.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> valueStringParser(@NotNull SenderNodeStringParser<V> valueStringParser) {
this.valueStringParser = valueStringParser;
return self();
}
/**
* Sets the serializer for keys. Serializers convert keys to/from persisted forms stored in the configuration
* backend.
*
* @param keySerializer The key serializer.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> keySerializer(@NotNull NodeSerializer<K> keySerializer) {
this.keySerializer = keySerializer;
return self();
}
/**
* Sets the serializer for values.
*
* @param valueSerializer The value serializer.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> valueSerializer(@NotNull NodeSerializer<V> valueSerializer) {
this.valueSerializer = valueSerializer;
return self();
}
/**
* Sets a validator for keys. The validator should return a successful {@link Try} for valid keys or a failed
* {@link Try} describing the validation error for invalid keys.
*
* @param keyValidator The key validator.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> keyValidator(@NotNull Function<K, Try<Void>> keyValidator) {
this.keyValidator = keyValidator;
return self();
}
/**
* Sets a validator for values.
*
* @param valueValidator The value validator.
* @return This builder for chaining.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Builder<K, V> valueValidator(@NotNull Function<V, Try<Void>> valueValidator) {
this.valueValidator = valueValidator;
return self();
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull MapConfigNode<K, V> build() {
return new MapConfigNode<>(
path,
comments.toArray(new String[0]),
name,
type,
aliases,
defaultValue,
suggester,
stringParser,
serializer,
validator,
onLoad,
onLoadAndChange,
onChange,
keyType,
valueType,
keySuggester,
valueSuggester,
keyStringParser,
valueStringParser,
keySerializer,
valueSerializer,
keyValidator,
valueValidator);
}
}
}
@@ -1,93 +0,0 @@
package org.mvplugins.multiverse.core.config.node;
import io.vavr.control.Try;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
/**
* A {@link ValueNode} specialization that stores a {@link Map} of key-value pairs.
*
* @param <K> The key type.
* @param <V> The value type.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public interface MapValueNode<K, V> extends ValueNode<Map<K, V>> {
/**
* Validates a single map key.
* <br />
* Implementations should return a successful {@link Try} when the key is acceptable, or a failed
* {@link Try} when the key is invalid.
*
* @param key The key to validate, or {@code null} if the key is absent.
* @return A successful {@link Try} if the key is valid, or a failed {@link Try} if it is not.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull Try<Void> validateKey(@Nullable K key);
/**
* Validates a single map value.
* <br />
* Implementations should return a successful {@link Try} when the value is acceptable, or a failed
* {@link Try} when the value is invalid.
*
* @param value The value to validate, or {@code null} if the value is absent.
* @return A successful {@link Try} if the value is valid, or a failed {@link Try} if it is not.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull Try<Void> validateValue(@Nullable V value);
/**
* Validates a key-value entry.
* <br />
* This is typically implemented by composing {@link #validateKey(Object)} and {@link #validateValue(Object)}.
* If either part fails, the entry is considered invalid.
*
* @param key The entry key, or {@code null} if the key is absent.
* @param value The entry value, or {@code null} if the value is absent.
* @return A successful {@link Try} if both parts are valid, or a failed {@link Try} if either part is invalid.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull Try<Void> validateEntry(@Nullable K key, @Nullable V value);
/**
* Deserializes a raw key-value pair into a map entry.
* <br />
* This method converts a single persisted entry, not the full map. If either component cannot be converted,
* implementations may return {@code null} for that side of the entry.
*
* @param key The raw key object, or {@code null} if no key is present.
* @param value The raw value object, or {@code null} if no value is present.
* @return A map entry containing the deserialized key and value.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull Map.Entry<K, V> deserializeEntry(@Nullable Object key, @Nullable Object value);
/**
* Serializes a map entry into raw configuration objects.
* <br />
* This method converts a single entry to a persisted representation, not the full map. The returned entry
* contains the serialized key and serialized value.
*
* @param key The entry key, or {@code null} if the key is absent.
* @param value The entry value, or {@code null} if the value is absent.
* @return A map entry containing the serialized key and value.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull Map.Entry<Object, Object> serializeEntry(@Nullable K key, @Nullable V value);
}
@@ -34,7 +34,7 @@ public interface ValueNode<T> extends Node {
* Gets the aliases of this node. Serves as shorter or legacy alternatives the {@link #getName()} and must be
* unique within a node group.
*
* @return The aliases of this node, or an empty array if the node has no aliases.
* @return The aliases of this node.
*
* @since 5.1
*/
@@ -44,36 +44,27 @@ public interface ValueNode<T> extends Node {
}
/**
* Gets the default value for this node.
* Gets the default value with type {@link T} of the node.
*
* <p>The returned value is used when no explicit value is present or when deserialization falls back to the
* node's default. Implementations may return {@code null} to indicate that no default value is available.
*
* @return The default value of the node, or {@code null} if none is configured.
* @return The default value of the node.
*/
@Nullable T getDefaultValue();
/**
* Suggests possible string values for this node.
* Suggests possible string values for this node. Generated based on the current user input.
*
* <p>The returned values are based on the current partial user input and should be suitable for command-line
* tab completion or other interactive hints.
*
* @param input The current partial user input, or {@code null} if no input has been typed yet.
* @param input The current partial user input
* @return A collection of possible string values.
*/
@NotNull Collection<String> suggest(@Nullable String input);
/**
* Suggests possible string values for this node using sender context.
* Suggests possible string values for this node. Use contextural information from the sender such as
* sender name, permissions, or player location for better suggestions.
*
* <p>Implementations may use the sender's name, permissions, or player location to tailor the returned
* suggestions. If no sender-specific logic is available, implementations should fall back to
* {@link #suggest(String)}.
*
* @param sender The sender context.
* @param input The current partial user input, or {@code null} if no input has been typed yet.
* @return A collection of possible string values.
* @param sender The sender context.
* @param input The input string.
* @return A collection of possible string values
*
* @since 5.1
*/
@@ -81,25 +72,20 @@ public interface ValueNode<T> extends Node {
@NotNull Collection<String> suggest(@NotNull CommandSender sender, @Nullable String input);
/**
* Parses the given string into a value of type {@link T}.
* Parses the given string into a value of type {@link T}. Used for property set by user input.
*
* <p>This is typically used for values entered by a user or read from configuration text.
*
* @param input The string to parse, or {@code null} if the source value is absent.
* @return The parsed value, or a failed {@link Try} containing the parsing error.
* @param input The string to parse.
* @return The parsed value, or given exception if parsing failed.
*/
@NotNull Try<T> parseFromString(@Nullable String input);
/**
* Parses the given string into a value of type {@link T} with context from the sender.
* Used for property set by user input.
*
* <p>Sender-aware implementations may use permissions or other sender-specific information when converting the
* input. If no sender-aware parser is available, implementations should fall back to
* {@link #parseFromString(String)}.
*
* @param sender The sender context.
* @param input The string to parse, or {@code null} if the source value is absent.
* @return The parsed value, or a failed {@link Try} containing the parsing error.
* @param sender The sender context.
* @param input The string to parse.
* @return The parsed value, or given exception if parsing failed.
*
* @since 5.1
*/
@@ -111,59 +97,23 @@ public interface ValueNode<T> extends Node {
/**
* Gets the serializer for this node.
*
* <p>This method exists for backward compatibility while the preferred API is
* {@link #serialize(Object)} and {@link #deserialize(Object)}.
*
* @return The serializer for this node, or {@code null} if none is configured.
*
* @deprecated Use {@link #serialize(Object)} and {@link #deserialize(Object)} instead.
* @return The serializer for this node.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@Nullable NodeSerializer<T> getSerializer();
/**
* Deserializes a raw configuration object into a value of type {@link T}.
*
* <p>The input is typically a value read from YAML or another configuration source. Implementations should
* return a sensible fallback, such as {@link #getDefaultValue()}, when the object cannot be converted.
*
* @param object The raw object to deserialize.
* @return The deserialized value, or {@code null} if the object cannot be converted and no fallback exists.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Nullable T deserialize(@Nullable Object object);
/**
* Serializes a value of type {@link T} into a configuration-friendly object.
*
* <p>The returned object should be suitable for persistence in a configuration file.
*
* @param value The value to serialize.
* @return The serialized representation, or {@code null} if the value cannot be serialized.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Nullable Object serialize(@Nullable T value);
/**
* Validates the value of this node.
*
* @param value The value to validate.
* @return A successful {@link Try} if the value is valid, or a failed {@link Try} containing the validation
* exception if the value is invalid.
* @return An empty {@link Try} if the value is valid, or a {@link Try} containing an exception if the value is
* invalid.
*/
Try<Void> validate(@Nullable T value);
/**
* Called when the value of this node is loaded by {@link BaseConfigurationHandle#load()}.
*
* <p>This callback is invoked after the value has been deserialized and validated during a configuration load.
*
* @param value The loaded value, or {@code null} if the configured value could not be resolved.
* @param value The loaded value.
*
* @since 5.4
*/
@@ -173,12 +123,9 @@ public interface ValueNode<T> extends Node {
/**
* Called when the value of this node is loaded or changed.
*
* <p>This callback is shared by both initial load and subsequent updates. When invoked during a load, the
* {@code oldValue} parameter will be {@code null} and the sender will be the console sender.
*
* @param sender The sender who triggered the change. If triggered by loading or no target sender is specified,
* it will be the console sender.
* @param oldValue The previous value, or {@code null} when called during load.
* @param sender The sender who triggered the change. If triggered by loading or no target sender is specified,
* it will be the console sender.
* @param oldValue The old value, will always be null on load.
* @param newValue The new value.
*
* @since 5.4
@@ -189,11 +136,9 @@ public interface ValueNode<T> extends Node {
/**
* Called when the value of this node is changed by {@link BaseConfigurationHandle#set(ValueNode, Object)}.
*
* <p>This callback is only invoked for explicit updates after the value already exists.
*
* @param sender The sender who changed the value, or the console sender if no target sender was specified.
* @param oldValue The old value.
* @param newValue The new value.
* @param sender The sender who changed the value, or console sender if no target sender specified.
* @param oldValue The old value.
* @param newValue The new value.
*
* @since 5.4
*/
@@ -203,15 +148,12 @@ public interface ValueNode<T> extends Node {
/**
* Called when the value of this node is set.
*
* <p>This is the legacy, senderless form of {@link #onLoadAndChange(CommandSender, Object, Object)}.
*
* @param oldValue The old value.
* @param newValue The new value.
*
* @deprecated Use {@link #onLoadAndChange(CommandSender, Object, Object)} instead.
*/
@Deprecated(since = "5.4", forRemoval = true)
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
default void onSetValue(@Nullable T oldValue, @Nullable T newValue) {
onLoadAndChange(Bukkit.getConsoleSender(), oldValue, newValue);
}
@@ -6,7 +6,6 @@ import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import jakarta.inject.Inject;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
@@ -15,11 +14,13 @@ import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.destination.Destination;
import org.mvplugins.multiverse.core.destination.DestinationSuggestionPacket;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.teleportation.LocationManipulation;
import org.mvplugins.multiverse.core.utils.REPatterns;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.entrycheck.WorldEntryCheckerProvider;
@@ -55,19 +56,6 @@ public final class WorldDestination implements Destination<WorldDestination, Wor
return "w";
}
/**
* Directly create a world destination instance for a Multiverse World without any string parsing validation needed.
*
* @param multiverseWorld The destination's target world.
* @return The destination instance with the target world.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull WorldDestinationInstance fromWorld(MultiverseWorld multiverseWorld) {
return new WorldDestinationInstance(this, multiverseWorld, null, -1.0f);
}
/**
* {@inheritDoc}
*/
@@ -35,7 +35,7 @@ public final class DynamicListenerRegistration {
private static final boolean hasEventExecutorCreate;
static {
hasEventExecutorCreate = ReflectHelper.hasMethod(EventExecutor.class, "create", Method.class, Class.class);
hasEventExecutorCreate = ReflectHelper.getMethod(EventExecutor.class, "create", Method.class, Class.class) != null;
}
private final EventPriorityMapper eventPriorityMapper;
@@ -98,22 +98,18 @@ public final class DynamicListenerRegistration {
}
method.setAccessible(true);
Object methodOutput = ReflectHelper.invokeMethod(listener, method);
if (!(methodOutput instanceof EventRunnable eventRunnable)) {
Logging.warning("Event method %s in %s did not return a SingleEventListener",
method.getName(), listener.getClass().getName());
return;
}
EventExecutor executor = new EventRunnableExecutor<>(eventClass, eventRunnable);
EventPriority priority = getDynamicEventPriority(method);
boolean ignoreCancelled = isIgnoreIfCancelled(method);
ReflectHelper.tryInvokeMethod(listener, method)
.onFailure(e -> Logging.warning("Failed to invoke event method %s in %s: $s",
method.getName(), listener.getClass().getName(), e.getMessage()))
.filter(EventRunnable.class::isInstance)
.onFailure(e -> Logging.warning("Event method %s in %s did not return an EventRunnable instance!",
method.getName(), listener.getClass().getName()))
.map(EventRunnable.class::cast)
.map(eventRunnable -> new EventRunnableExecutor<>(eventClass, eventRunnable))
.peek(executor -> {
EventPriority priority = getDynamicEventPriority(method);
boolean ignoreCancelled = isIgnoreIfCancelled(method);
Bukkit.getPluginManager().registerEvent(eventClass, listener, priority, executor, plugin, ignoreCancelled);
Logging.finest("Registered dynamic event: %s, priority: %s, ignore if cancelled: %s",
eventClass.getName(), priority, ignoreCancelled);
});
Logging.finest("Registering dynamic event for %s with priority %s", eventClass.getName(), priority);
Bukkit.getPluginManager().registerEvent(eventClass, listener, priority, executor, plugin, ignoreCancelled);
}
private Class<? extends Event> getEventClass(Method method) {
@@ -133,12 +129,13 @@ public final class DynamicListenerRegistration {
}
private Class<? extends Event> getEventClassFromString(String className) {
return ReflectHelper.tryGetClass(className)
.onFailure(throwable -> Logging.fine("Failed to find event class: %s", className))
.filter(Event.class::isAssignableFrom)
.onFailure(throwable -> Logging.warning("Class is not an Event: %s", className))
.map(clazz -> (Class<? extends Event>) clazz.asSubclass(Event.class))
.getOrNull();
Class<?> annotatedClass = ReflectHelper.getClass(className);
if (annotatedClass == null || !Event.class.isAssignableFrom(annotatedClass)) {
// Usually means the server software used did not implement the event
Logging.fine("Event class does not exist: %s", className);
return null;
}
return annotatedClass.asSubclass(Event.class);
}
private EventPriority getDynamicEventPriority(Method method) {
@@ -7,7 +7,6 @@ import java.io.FileReader;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import com.dumptruckman.minecraft.util.Logging;
@@ -25,7 +24,7 @@ public final class MVDumpsDebugInfoEvent extends Event {
public MVDumpsDebugInfoEvent() {
debugInfoBuilder = new StringBuilder();
detailedDebugInfo = new LinkedHashMap<>();
detailedDebugInfo = new HashMap<>();
}
private static final HandlerList HANDLERS = new HandlerList();
@@ -1,11 +1,8 @@
package org.mvplugins.multiverse.core.event.world;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
/**
* Called when a world has been successfully cloned.
@@ -13,24 +10,14 @@ import org.mvplugins.multiverse.core.world.MultiverseWorld;
public final class MVWorldClonedEvent extends MultiverseWorldEvent<LoadedMultiverseWorld> {
private static final HandlerList HANDLERS = new HandlerList();
private final MultiverseWorld fromWorld;
private final LoadedMultiverseWorld fromWorld;
public MVWorldClonedEvent(LoadedMultiverseWorld world, MultiverseWorld fromWorld) {
public MVWorldClonedEvent(LoadedMultiverseWorld world, LoadedMultiverseWorld fromWorld) {
super(world);
this.fromWorld = fromWorld;
}
/**
* @deprecated Cloning can be done from unloaded worlds as well. Use {@link #getSourceWorld()} instead.
*/
@Deprecated(forRemoval = true, since = "5.6")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public @Nullable LoadedMultiverseWorld getFromWorld() {
return fromWorld.asLoadedWorld().getOrNull();
}
@ApiStatus.AvailableSince("5.6")
public @NotNull MultiverseWorld getSourceWorld() {
public LoadedMultiverseWorld getFromWorld() {
return fromWorld;
}
@@ -50,4 +37,5 @@ public final class MVWorldClonedEvent extends MultiverseWorldEvent<LoadedMultive
public static HandlerList getHandlerList() {
return HANDLERS;
}
}
@@ -1,8 +1,6 @@
package org.mvplugins.multiverse.core.exceptions.command;
import co.aikar.commands.InvalidCommandArgument;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.locale.message.LocalizableMessage;
import org.mvplugins.multiverse.core.locale.message.LocalizedMessage;
import org.mvplugins.multiverse.core.locale.message.Message;
@@ -11,18 +9,6 @@ import org.mvplugins.multiverse.core.locale.message.Message;
*/
public class MVInvalidCommandArgument extends InvalidCommandArgument {
@ApiStatus.AvailableSince("5.7")
public static MVInvalidCommandArgument causeBy(Throwable throwable) {
return causeBy(throwable, true);
}
@ApiStatus.AvailableSince("5.7")
public static MVInvalidCommandArgument causeBy(Throwable throwable, boolean showSyntax) {
return (throwable instanceof LocalizableMessage localizableMessage)
? of(localizableMessage.getLocalizableMessage(), showSyntax)
: new MVInvalidCommandArgument(throwable.getLocalizedMessage(), showSyntax);
}
public static MVInvalidCommandArgument of(Message message) {
return of(message, true);
}
@@ -33,10 +19,6 @@ public class MVInvalidCommandArgument extends InvalidCommandArgument {
: new MVInvalidCommandArgument(message, showSyntax);
}
private MVInvalidCommandArgument(String message, boolean showSyntax) {
super(message, showSyntax);
}
private MVInvalidCommandArgument(Message message, boolean showSyntax) {
super(message.formatted(), showSyntax);
}
@@ -7,7 +7,6 @@
package org.mvplugins.multiverse.core.listeners;
import com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent;
import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
import org.bukkit.entity.LivingEntity;
@@ -22,8 +21,6 @@ import org.bukkit.event.entity.FoodLevelChangeEvent;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.dynamiclistener.EventRunnable;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventClass;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod;
import org.mvplugins.multiverse.core.world.WorldManager;
@@ -80,37 +77,6 @@ final class MVEntityListener implements CoreListener {
});
}
/**
* Fired before other spawn checks is done, helps in performance by cancelling early and preventing unnecessary
* checks for spawn reasons that are not allowed in the world.
*
* @return Event wrapper
*/
@EventClass("com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent")
EventRunnable<?> preCreatureSpawn() {
return new EventRunnable<PreCreatureSpawnEvent>() {
@Override
public void onEvent(PreCreatureSpawnEvent event) {
// Always allow custom command and plugins to spawn creatures
if (event.getReason() == SpawnReason.CUSTOM
|| event.getReason() == SpawnReason.COMMAND
|| event.getReason() == SpawnReason.BREEDING
|| event.getReason() == SpawnReason.SPAWNER_EGG) {
return;
}
worldManager.getLoadedWorld(event.getSpawnLocation().getWorld())
.peek(world -> {
if (!world.getEntitySpawnConfig().shouldAllowSpawn(event.getType())) {
Logging.finest("Cancelling Pre Creature Spawn Event for: " + event.getType());
event.setCancelled(true);
event.setShouldAbortSpawn(true);
}
});
}
};
}
/**
* Handle Spawn Category settings.
*
@@ -7,7 +7,9 @@
package org.mvplugins.multiverse.core.listeners;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
@@ -36,7 +38,6 @@ import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.DefaultEventPriority;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventPriorityKey;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.IgnoreIfCancelled;
import org.mvplugins.multiverse.core.economy.MVEconomist;
import org.mvplugins.multiverse.core.event.MVRespawnEvent;
import org.mvplugins.multiverse.core.locale.PluginLocales;
@@ -244,7 +245,7 @@ final class MVPlayerListener implements CoreListener {
}
private void teleportToDestinationOnJoin(Player player, DestinationInstance<?, ?> destination) {
asyncSafetyTeleporter.to(destination).passengerMode(config.getPassengerMode()).teleportSingle(player)
asyncSafetyTeleporter.to(destination).teleportSingle(player)
.onSuccess(result -> Logging.fine("Player %s has been teleported on join",
player.getName()))
.onFailure(failure -> Logging.warning("Failed to teleport player %s on join: %s",
@@ -256,9 +257,9 @@ final class MVPlayerListener implements CoreListener {
* @param event The Event that was fired.
*/
@EventMethod
@EventPriorityKey("mvcore-player-changed-world")
@DefaultEventPriority(EventPriority.NORMAL)
@DefaultEventPriority(EventPriority.MONITOR)
void playerChangedWorld(PlayerChangedWorldEvent event) {
// Permissions now determine whether or not to handle a gamemode.
this.handleGameModeAndFlight(event.getPlayer(), event.getPlayer().getWorld());
}
@@ -270,28 +271,12 @@ final class MVPlayerListener implements CoreListener {
@EventPriorityKey("mvcore-player-teleport")
@DefaultEventPriority(EventPriority.HIGHEST)
void playerTeleport(PlayerTeleportEvent event) {
Player teleportee = event.getPlayer();
if (event.isCancelled()) {
Logging.finer("Teleport event for player '" + teleportee.getName() + "' was already cancelled.");
return;
}
Logging.finer("Got teleport event for player '"
+ teleportee.getName() + "' with cause '" + event.getCause() + "'");
Location fromLocation = event.getFrom();
if (fromLocation == null || fromLocation.getWorld() == null) { // should never be null, but just in case
Logging.finer("Teleport event for player '" + teleportee.getName() + "' has null from-location or world.");
+ event.getPlayer().getName() + "' with cause '" + event.getCause() + "'");
if (event.isCancelled()) {
return;
}
Location toLocation = event.getTo();
if (toLocation == null || toLocation.getWorld() == null) { // may be null on spigot
Logging.finer("Teleport event for player '" + teleportee.getName() + "' has null to-location or world.");
return;
}
Player teleportee = event.getPlayer();
Option<String> teleporterName = teleportQueue.popFromQueue(teleportee.getName());
CommandSender teleporter = teleporterName.map(name -> {
if (name.equalsIgnoreCase("CONSOLE")) {
@@ -312,17 +297,17 @@ final class MVPlayerListener implements CoreListener {
}
Logging.finer("Teleporter %s is teleporting %s from %s to %s", teleporter.getName(), teleportee.getName(),
fromLocation, toLocation);
event.getFrom(), event.getTo());
MultiverseWorld fromWorld = getWorldManager().getLoadedWorld(fromLocation.getWorld()).getOrNull();
LoadedMultiverseWorld toWorld = getWorldManager().getLoadedWorld(toLocation.getWorld()).getOrNull();
MultiverseWorld fromWorld = getWorldManager().getLoadedWorld(event.getFrom().getWorld()).getOrNull();
LoadedMultiverseWorld toWorld = getWorldManager().getLoadedWorld(event.getTo().getWorld()).getOrNull();
if (toWorld == null) {
Logging.fine("Player '" + teleportee.getName() + "' is teleporting to world '"
+ toLocation.getWorld().getName() + "' which is not managed by Multiverse-Core. No further "
+ event.getTo().getWorld().getName() + "' which is not managed by Multiverse-Core. No further "
+ "actions will be taken by Multiverse-Core.");
return;
}
if (fromLocation.getWorld().equals(toLocation.getWorld())) {
if (event.getFrom().getWorld().equals(event.getTo().getWorld())) {
// The player is Teleporting to the same world.
Logging.finer("Player '" + teleportee.getName() + "' is teleporting to the same world.");
this.stateSuccess(teleportee.getName(), toWorld.getName());
@@ -356,19 +341,20 @@ final class MVPlayerListener implements CoreListener {
*/
@EventMethod
@DefaultEventPriority(EventPriority.LOWEST)
@IgnoreIfCancelled
void playerPortalCheck(PlayerPortalEvent event) {
Location fromLocation = event.getFrom();
if (fromLocation == null || fromLocation.getWorld() == null) {
if (event.isCancelled()) {
return;
}
if (event.getFrom().getWorld() == null) {
Logging.warning("PlayerPortalEvent's from world is null!");
return;
}
if (fromLocation.getWorld().getBlockAt(fromLocation).getType() == Material.NETHER_PORTAL) {
if (event.getFrom().getWorld().getBlockAt(event.getFrom()).getType() == Material.NETHER_PORTAL) {
return;
}
// Player was actually outside of the portal, adjust the from location
Location newLocation = blockSafety.findPortalBlockNextTo(fromLocation);
Location newLocation = blockSafety.findPortalBlockNextTo(event.getFrom());
if (newLocation != null) {
event.setFrom(newLocation);
}
@@ -381,33 +367,28 @@ final class MVPlayerListener implements CoreListener {
@EventMethod
@EventPriorityKey("mvcore-player-portal")
@DefaultEventPriority(EventPriority.HIGH)
@IgnoreIfCancelled
void playerPortal(PlayerPortalEvent event) {
Location fromLocation = event.getFrom();
if (fromLocation == null || fromLocation.getWorld() == null) { // should never be null, but just in case
Logging.finer("PlayerPortalEvent's from world is null!");
if (event.isCancelled()) {
return;
}
Location toLocation = event.getTo();
if (toLocation == null || toLocation.getWorld() == null) { // may be null on spigot
if (event.getTo() == null || event.getTo().getWorld() == null) {
Logging.finer("PlayerPortalEvent's to world is null!");
return;
}
if (config.isUsingCustomPortalSearch()) {
event.setSearchRadius(config.getCustomPortalSearchRadius());
}
if (Objects.equals(fromLocation.getWorld(), toLocation.getWorld())) {
if (Objects.equals(event.getFrom().getWorld(), event.getTo().getWorld())) {
// The player is Portaling to the same world.
Logging.finer("Player '" + event.getPlayer().getName() + "' is portaling to the same world.");
return;
}
MultiverseWorld fromWorld = getWorldManager().getLoadedWorld(fromLocation.getWorld()).getOrNull();
LoadedMultiverseWorld toWorld = getWorldManager().getLoadedWorld(toLocation.getWorld()).getOrNull();
MultiverseWorld fromWorld = getWorldManager().getLoadedWorld(event.getFrom().getWorld()).getOrNull();
LoadedMultiverseWorld toWorld = getWorldManager().getLoadedWorld(event.getTo().getWorld()).getOrNull();
if (toWorld == null) {
Logging.fine("Player '" + event.getPlayer().getName() + "' is portaling to world '"
+ toLocation.getWorld().getName() + "' which is not managed by Multiverse-Core. No further "
+ event.getTo().getWorld().getName() + "' which is not managed by Multiverse-Core. No further "
+ "actions will be taken by Multiverse-Core.");
return;
}
@@ -421,31 +402,21 @@ final class MVPlayerListener implements CoreListener {
}
/**
* Handles the gamemode for the specified {@link Player}. Delays the enforcement if configured to do so
* to ensure multiverse has final say over other plugins changing gamemodes.
* Handles the gamemode for the specified {@link Player}.
*
* @param player The {@link Player}.
* @param world The {@link World} the player is supposed to be in.
*/
private void handleGameModeAndFlight(final Player player, World world) {
if (config.getGamemodeAndFlightEnforceDelay() <= 0) {
doGameModeAndFlightEnforcement(player, world);
return;
}
server.getScheduler().runTaskLater(
this.plugin,
() -> doGameModeAndFlightEnforcement(player, world),
config.getGamemodeAndFlightEnforceDelay()
);
}
private void doGameModeAndFlightEnforcement(Player player, World world) {
if (!player.isOnline() || !player.getWorld().equals(world)) {
Logging.finer("Player %s is no longer online or not in the expected world '%s'", player.getName(), world.getName());
return;
}
Logging.finer("Handling gamemode and flight for player %s in world '%s'", player.getName(), world.getName());
enforcementHandler.handleFlightEnforcement(player);
enforcementHandler.handleGameModeEnforcement(player);
// We perform this task one tick later to MAKE SURE that the player actually reaches the
// destination world, otherwise we'd be changing the player mode if they havent moved anywhere.
this.server.getScheduler().runTaskLater(this.plugin, () -> {
if (!player.isOnline() || !player.getWorld().equals(world)) {
return;
}
Logging.finer("Handling gamemode and flight for player %s in world '%s'", player.getName(), world.getName());
enforcementHandler.handleFlightEnforcement(player);
enforcementHandler.handleGameModeEnforcement(player);
}, 1L);
}
}
@@ -18,11 +18,9 @@ import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.world.PortalCreateEvent;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.IgnoreIfCancelled;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.world.WorldManager;
import static org.bukkit.PortalType.CUSTOM;
@@ -35,15 +33,11 @@ final class MVPortalListener implements CoreListener {
private final CoreConfig config;
private final WorldManager worldManager;
private final MVCommandManager commandManager;
@Inject
MVPortalListener(@NotNull CoreConfig config,
@NotNull WorldManager worldManager,
@NotNull MVCommandManager commandManager) {
MVPortalListener(@NotNull CoreConfig config, @NotNull WorldManager worldManager) {
this.config = config;
this.worldManager = worldManager;
this.commandManager = commandManager;
}
/**
@@ -65,7 +59,6 @@ final class MVPortalListener implements CoreListener {
if (!world.getPortalForm().isPortalAllowed(targetType)) {
Logging.fine("Cancelling creation of %s portal because portalForm disallows.", targetType);
event.setCancelled(true);
commandManager.getCommandIssuer(event.getEntity()).sendError(MVCorei18n.PORTALFORM_DISABLED_NETHER);
}
}).onEmpty(() ->
Logging.fine("World '%s' is not managed by Multiverse! Ignoring at PortalCreateEvent.",
@@ -108,7 +101,6 @@ final class MVPortalListener implements CoreListener {
if (!world.getPortalForm().isPortalAllowed(PortalType.ENDER)) {
Logging.fine("Cancelling creation of ENDER portal because portalForm disallows.");
event.setCancelled(true);
commandManager.getCommandIssuer(event.getPlayer()).sendError(MVCorei18n.PORTALFORM_DISABLED_END);
}
}).onEmpty(() ->
Logging.fine("World '%s' is not managed by Multiverse! Ignoring at PlayerInteractEvent.",
@@ -91,7 +91,6 @@ public enum MVCorei18n implements MessageKeyProvider {
// /mv dumps
DUMPS_DESCRIPTION,
DUMPS_STARTING,
DUMPS_URL_LIST,
// /mv gamerule set
@@ -147,19 +146,6 @@ public enum MVCorei18n implements MessageKeyProvider {
LOAD_LOADING,
LOAD_SUCCESS,
// /mv meta info
META_INFO_DESCRIPTION,
META_INFO_WORLD,
META_INFO_HEADER,
META_INFO_NOCONTENT,
// /mv meta modify
META_MODIFY_SET_SUCCESS,
META_MODIFY_SET_FAILURE,
META_MODIFY_REMOVE_SUCCESS,
META_MODIFY_REMOVE_FAILURE,
META_MODIFY_INVALIDACTION,
// /mv modify
MODIFY_DESCRIPTION,
MODIFY_WORLD_DESCRIPTION,
@@ -264,8 +250,6 @@ public enum MVCorei18n implements MessageKeyProvider {
WORLDBORDER_WARNINGTIME_NOTHINGCHANGED,
WORLDBORDER_WARNINGTIME_SUCCESS,
WORLDBORDER_ROUNDOFF_WARNING,
// commands error
COMMANDS_ERROR_PLAYERSONLY,
COMMANDS_ERROR_MULTIVERSEWORLDONLY,
@@ -294,10 +278,6 @@ public enum MVCorei18n implements MessageKeyProvider {
DESTINATION_SHARED_FAILUREREASON_WORLDNOTFOUND,
DESTINATION_PARSE_FAILUREREASON_INVALIDDESTINATIONID,
// portal form disabled
PORTALFORM_DISABLED_END,
PORTALFORM_DISABLED_NETHER,
// teleport failure reason
TELEPORTFAILUREREASON_NULL_DESTINATION,
TELEPORTFAILUREREASON_NULL_LOCATION,
@@ -312,7 +292,6 @@ public enum MVCorei18n implements MessageKeyProvider {
CLONEWORLD_WORLDEXISTFOLDER,
CLONEWORLD_WORLDEXISTUNLOADED,
CLONEWORLD_WORLDEXISTLOADED,
CLONEWORLD_FROMWORLDFOLDERINVALID,
CLONEWORLD_COPYFAILED,
CREATEWORLD_INVALIDWORLDNAME,
@@ -330,14 +309,12 @@ public enum MVCorei18n implements MessageKeyProvider {
IMPORTWORLD_WORLDEXISTLOADED,
IMPORTWORLD_WORLDFOLDERINVALID,
IMPORTWORLD_BUKKITENVIRONMENTMISMATCH,
IMPORTWORLD_BUKKITNAMESPACEDKEYMISMATCH,
LOADWORLD_WORLDALREADYLOADING,
LOADWORLD_WORLDNONEXISTENT,
LOADWORLD_WORLDEXISTFOLDER,
LOADWORLD_WORLDEXISTLOADED,
LOADWORLD_BUKKITENVIRONMENTMISMATCH,
LOADWORLD_BUKKITNAMESPACEDKEYMISMATCH,
REMOVEWORLD_WORLDNONEXISTENT,
@@ -374,12 +351,6 @@ public enum MVCorei18n implements MessageKeyProvider {
EXCEPTION_POSITIONPARSE_INVALIDCOORDINATES,
EXCEPTION_POSITIONPARSE_INVALIDNUMBER,
// world key parse failures
WORLDKEYPARSE_EMPTY,
WORLDKEYPARSE_INVALIDWORLDNAME,
WORLDKEYPARSE_INVALIDNAMESPACEDKEY,
WORLDKEYPARSE_NAMESPACEDKEYUNSUPPORTED,
// generic
GENERIC_SUCCESS,
GENERIC_FAILURE,
@@ -1,7 +1,6 @@
package org.mvplugins.multiverse.core.locale.message;
import io.vavr.control.Either;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -108,7 +107,6 @@ public final class MessageReplacement {
GAMERULE(replace("{gamerule}")),
LOCATION(replace("{location}")),
NAME(replace("{name}")),
NAMESPACE(replace("{namespace}")),
PLAYER(replace("{player}")),
REASON(replace("{reason}")),
VALUE(replace("{value}")),
@@ -147,10 +147,9 @@ public abstract class MultiverseModule extends JavaPlugin {
Try.of(() -> commandManagerProvider.get())
.mapTry(MVCommandManager::getLocales)
.andThen(pluginLocales -> {
pluginLocales.addBundleClassLoader(this.getClassLoader());
pluginLocales.addFileResClassLoader(this);
pluginLocales.addBundleClassLoader(this.getClassLoader());
pluginLocales.addMessageBundles(this.getName().toLowerCase(Locale.ENGLISH));
pluginLocales.loadLanguages();
})
.onFailure(e -> {
Logging.severe("Failed to register locales");
@@ -238,12 +238,15 @@ public final class AsyncSafetyTeleporterAction {
@NotNull Location location,
@NotNull List<Entity> passengers
) {
List<Entity> toTeleport = new ArrayList<>(passengers);
toTeleport.addFirst(teleportee);
List<Entity> toTeleport = new ArrayList<>(passengers.size() + 1);
toTeleport.add(teleportee);
toTeleport.addAll(passengers);
return AsyncAttemptsAggregate.allOfAggregate(toTeleport.stream()
.map(passenger -> doAsyncTeleport(passenger, location))
.toList())
return AsyncAttemptsAggregate.allOfAggregate(
toTeleport.stream()
.map(passenger -> doAsyncTeleport(passenger, location))
.toList()
)
.onSuccess(() -> Bukkit.getScheduler().runTask(multiverseCore, () -> {
passengers.forEach(teleportee::addPassenger);
Logging.finer("Mounted %d passengers to %s", passengers.size(), teleportee.getName());
@@ -132,10 +132,6 @@ public final class BlockSafety {
*/
public boolean canSpawnAtBlockSafely(@NotNull Block block) {
Logging.finest("Checking spawn safety for location: %s, %s, %s", block.getX(), block.getY(), block.getZ());
if (!block.getWorld().getWorldBorder().isInside(block.getLocation())) {
Logging.finest("Location is outside world border.");
return false;
}
if (isUnsafeSpawnBody(block)) {
// Player body will be stuck in solid
Logging.finest("Unsafe location for player's body: " + block);
@@ -1,92 +0,0 @@
package org.mvplugins.multiverse.core.utils;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* A map with case-insensitive String keys. All keys are stored in lower-case form.
*
* @param <T> the type of mapped values
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public class CaseInsensitiveStringMap<T> implements Map<String, T> {
private final Map<String, T> map;
public CaseInsensitiveStringMap() {
map = new HashMap<>();
}
@Override
public int size() {
return map.size();
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return map.containsKey(normalizeKey(key));
}
@Override
public boolean containsValue(Object value) {
return map.containsValue(value);
}
@Override
public T get(Object key) {
return map.get(normalizeKey(key));
}
@Override
public @Nullable T put(String key, T value) {
return map.put(normalizeKey(key), value);
}
@Override
public T remove(Object key) {
return map.remove(normalizeKey(key));
}
@Override
public void putAll(@NotNull Map<? extends String, ? extends T> m) {
m.forEach((key, value) -> map.put(normalizeKey(key), value));
}
@Override
public void clear() {
map.clear();
}
@Override
public @NotNull Set<String> keySet() {
return map.keySet();
}
@Override
public @NotNull Collection<T> values() {
return map.values();
}
@Override
public @NotNull Set<Entry<String, T>> entrySet() {
return map.entrySet();
}
private String normalizeKey(Object key) {
return String.valueOf(key).toLowerCase(Locale.ROOT);
}
}
@@ -18,7 +18,6 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.dumptruckman.minecraft.util.Logging;
@@ -211,31 +210,28 @@ public final class FileUtils {
private final Path sourceDir;
private final Path targetDir;
private final List<Path> excludeFiles;
private final List<String> excludeFiles;
private CopyDirFileVisitor(@NotNull Path sourceDir, @NotNull Path targetDir, @NotNull List<String> excludeFiles) {
this.sourceDir = sourceDir;
this.targetDir = targetDir;
this.excludeFiles = excludeFiles.stream()
.map(sourceDir::resolve)
.toList();
Logging.finest(this.excludeFiles.stream().map(Path::toString).collect(Collectors.joining(", ", "Exclude files: [", "]")));
this.excludeFiles = excludeFiles;
}
@Override
public @NotNull FileVisitResult preVisitDirectory(@NotNull Path dir, @NotNull BasicFileAttributes attrs) throws IOException {
public @NotNull FileVisitResult preVisitDirectory(Path dir, @NotNull BasicFileAttributes attrs) throws IOException {
Path newDir = targetDir.resolve(sourceDir.relativize(dir));
if (!Files.isDirectory(newDir)) {
Files.createDirectories(newDir);
Files.createDirectory(newDir);
}
return FileVisitResult.CONTINUE;
}
@Override
public @NotNull FileVisitResult visitFile(@NotNull Path file, @NotNull BasicFileAttributes attrs) throws IOException {
public @NotNull FileVisitResult visitFile(Path file, @NotNull BasicFileAttributes attrs) throws IOException {
// Pass files that are set to ignore
if (excludeFiles.contains(file)) {
Logging.finest("Ignoring file: " + file);
if (excludeFiles.contains(file.getFileName().toString())) {
Logging.finest("Ignoring file: " + file.getFileName());
return FileVisitResult.CONTINUE;
}
// Copy the files
@@ -1,8 +1,8 @@
package org.mvplugins.multiverse.core.utils;
import io.vavr.control.Try;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.utils.tick.TickDuration;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
@@ -15,6 +15,10 @@ import java.time.format.DateTimeFormatter;
@ApiStatus.AvailableSince("5.1")
public final class MinecraftTimeFormatter {
private static final double TIME_MULTIPLIER = 3.6;
private static final long DAY_SECONDS = 24 * 60 * 60;
private static final long START_OFFSET = 6 * 60 * 60;
/**
* Formats Minecraft time to 12-hour format.
*
@@ -44,14 +48,19 @@ public final class MinecraftTimeFormatter {
* See <a href="https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html">DateTimeFormatter documentation</a>
* for available patterns.
*
* @param ticks The Minecraft time to format.
* @param format The format string for the time.
* @param time The Minecraft time to format.
* @param format The format string for the time.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
public static String formatTime(long ticks, String format) {
LocalTime localTime = TickDuration.ofTicks(ticks).toLocalTime();
public static String formatTime(long time, String format) {
// Convert Minecraft time to real-world time
long realTime = (long) ((time * TIME_MULTIPLIER) + START_OFFSET) % DAY_SECONDS; // Minecraft ticks to seconds
// Convert seconds to LocalTime
LocalTime localTime = LocalTime.ofSecondOfDay(realTime);
return Try.of(() -> DateTimeFormatter.ofPattern(format))
.map(localTime::format)
.getOrElse("invalid time format: " + format);
@@ -3,8 +3,6 @@ package org.mvplugins.multiverse.core.utils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import io.vavr.control.Try;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -13,182 +11,12 @@ import org.jetbrains.annotations.Nullable;
*/
public final class ReflectHelper {
/**
* Try to get the {@link Class} based on its classpath.
*
* @param classPath The target classpath.
* @return A {@link Try} containing the {@link Class} if found, else a failure.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static Try<Class<?>> tryGetClass(@NotNull String classPath) {
return Try.of(() -> Class.forName(classPath));
}
/**
* Check if the {@link Class} for a give classpath is present/valid.
*
* @param classPath Target classpath.
* @return True if class path is a valid class, else false.
*/
public static boolean hasClass(String classPath) {
return tryGetClass(classPath).isSuccess();
}
/**
* Try to get a {@link Method} from a given class.
*
* @param clazz The class to search the method on.
* @param methodName Name of the method to get.
* @param parameterTypes Parameters present for that method.
* @param <C> The class type.
* @return A {@link Try} containing the {@link Method} if found, else a failure.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static <C> Try<Method> tryGetMethod(@NotNull Class<C> clazz, @NotNull String methodName, Class<?>... parameterTypes) {
return Try.of(() -> clazz.getMethod(methodName, parameterTypes))
.orElse(Try.of(() -> {
Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
return method;
}));
}
/**
* Check if a {@link Method} exists on a given class.
*
* @param clazz The class to search the method on.
* @param methodName Name of the method to check.
* @param parameterTypes Parameters present for that method.
* @return True if method exists, else false.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static boolean hasMethod(@NotNull Class<?> clazz, @NotNull String methodName, Class<?>... parameterTypes) {
return tryGetMethod(clazz, methodName, parameterTypes).isSuccess();
}
/**
* Try to invoke a {@link Method} on a class instance.
*
* @param classInstance Instance of the class responsible for the method.
* @param method The method to invoke.
* @param parameters Parameters needed when invoking the method.
* @param <C> The class type.
* @param <R> The return type of the method.
* @return A {@link Try} containing the return value of the method if found, else a failure.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
@SuppressWarnings("unchecked")
public static <C, R> Try<R> tryInvokeMethod(@NotNull C classInstance, @NotNull Method method, Object...parameters) {
return Try.of(() -> (R) method.invoke(classInstance, parameters));
}
/**
* Try to invoke a static {@link Method}.
*
* @param method The static method to invoke.
* @param parameters Parameters needed when invoking the method.
* @param <R> The return type of the method.
* @return A {@link Try} containing the return value of the method if found, else a failure.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
@SuppressWarnings("unchecked")
public static <R> Try<R> tryInvokeStaticMethod(@NotNull Method method, Object...parameters) {
return Try.of(() -> (R) method.invoke(null, parameters));
}
/**
* Try to get a {@link Field} from a given class.
*
* @param clazz The class to search the field on.
* @param fieldName Name of the field to get.
* @param <C> The class type.
* @return A {@link Try} containing the {@link Field} if found, else a failure.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static <C> Try<Field> tryGetField(@NotNull Class<C> clazz, @NotNull String fieldName) {
return Try.of(() -> clazz.getField(fieldName))
.orElse(Try.of(() -> {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
}));
}
/**
* Check if a {@link Field} exists on a given class.
*
* @param clazz The class to search the field on.
* @param fieldName Name of the field to check.
* @return True if field exists, else false.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static boolean hasField(@NotNull Class<?> clazz, @NotNull String fieldName) {
return tryGetField(clazz, fieldName).isSuccess();
}
/**
* Try to get the value of a {@link Field} from an instance of the class responsible.
*
* @param classInstance Instance of the class to get the field value from.
* @param field The field to get the value from.
* @param fieldType Type of the field.
* @param <C> The class type.
* @param <V> The field value type.
* @return A {@link Try} containing the field value if found, else a failure.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static <C, V> Try<V> tryGetFieldValue(@NotNull C classInstance, @NotNull Field field, @NotNull Class<V> fieldType) {
return Try.of(() -> fieldType.cast(field.get(classInstance)));
}
/**
* Try to get the value of a static {@link Field}.
*
* @param field The static field to get the value from.
* @param fieldType Type of the field.
* @param <V> The field value type.
* @return A {@link Try} containing the field value if found, else a failure.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static <V> Try<V> tryGetStaticFieldValue(@NotNull Field field, @NotNull Class<V> fieldType) {
return Try.of(() -> fieldType.cast(field.get(null)));
}
/**
* Try to get the {@link Class} based on its classpath.
*
* @param classPath The target classpath.
* @return A {@link Class} if found, else null.
*
* @deprecated Use {@link #tryGetClass(String)} instead, which returns a {@link Try} that can be used to handle
* the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@Nullable
public static Class<?> getClass(String classPath) {
try {
@@ -198,6 +26,16 @@ public final class ReflectHelper {
}
}
/**
* Check if the {@link Class} for a give classpath is present/valid.
*
* @param classPath Target classpath.
* @return True if class path is a valid class, else false.
*/
public static boolean hasClass(String classPath) {
return getClass(classPath) != null;
}
/**
* Try to get a {@link Method} from a given class.
*
@@ -206,11 +44,7 @@ public final class ReflectHelper {
* @param parameterTypes Parameters present for that method.
* @param <C> The class type.
* @return A {@link Method} if found, else null.
*
* @deprecated Use {@link #tryGetMethod(Class, String, Class[])} instead, which returns a {@link Try} that can be
* used to handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@Nullable
public static <C> Method getMethod(Class<C> clazz, String methodName, Class<?>... parameterTypes) {
try {
@@ -230,11 +64,7 @@ public final class ReflectHelper {
* @param parameterTypes Parameters present for that method.
* @param <C> The class type.
* @return A {@link Method} if found, else null.
*
* @deprecated Use {@link #tryGetMethod(Class, String, Class[])} instead, which returns a {@link Try} that can be
* used to handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@Nullable
public static <C> Method getMethod(C classInstance, String methodName, Class<?>... parameterTypes) {
return getMethod(classInstance.getClass(), methodName, parameterTypes);
@@ -249,13 +79,8 @@ public final class ReflectHelper {
* @param <C> The class type.
* @param <R> The return type.
* @return Return value of the method call if any, else null.
*
* @deprecated Use {@link #tryInvokeMethod(Object, Method, Object...)} instead, which returns a {@link Try} that can
* be used to handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@Nullable
@SuppressWarnings("unchecked")
public static <C, R> R invokeMethod(C classInstance, Method method, Object...parameters) {
try {
return (R) method.invoke(classInstance, parameters);
@@ -271,11 +96,7 @@ public final class ReflectHelper {
* @param fieldName Name of the field to get.
* @param <C> The class type.
* @return A {@link Field} if found, else null.
*
* @deprecated Use {@link #tryGetField(Class, String)} instead, which returns a {@link Try} that can be used to
* handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@Nullable
public static <C> Field getField(Class<C> clazz, String fieldName) {
try {
@@ -294,11 +115,7 @@ public final class ReflectHelper {
* @param fieldName Name of the field to get.
* @param <C> The class type.
* @return A {@link Field} if found, else null.
*
* @deprecated Use {@link #tryGetField(Class, String)} instead, which returns a {@link Try} that can be used to
* handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@Nullable
public static <C> Field getField(C classInstance, String fieldName) {
return getField(classInstance.getClass(), fieldName);
@@ -313,11 +130,7 @@ public final class ReflectHelper {
* @param <C> The class type.
* @param <V> The field value type.
* @return The field value if any, else null.
*
* @deprecated Use {@link #tryGetFieldValue(Object, Field, Class)} instead, which returns a {@link Try} that can be
* used to handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@Nullable
public static <C, V> V getFieldValue(C classInstance, @Nullable Field field, @NotNull Class<V> fieldType) {
try {
@@ -340,11 +153,7 @@ public final class ReflectHelper {
* @param <C> The class type.
* @param <V> The field value type.
* @return The field value if any, else null.
*
* @deprecated Use {@link #tryGetField(Class, String)} then map to {@link #tryGetFieldValue(Object, Field, Class)} instead,
* which returns a {@link Try} that can be used to handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@Nullable
public static <C, V> V getFieldValue(C classInstance, @Nullable String fieldName, @NotNull Class<V> fieldType) {
return getFieldValue(classInstance, getField(classInstance, fieldName), fieldType);
@@ -3,7 +3,6 @@ package org.mvplugins.multiverse.core.utils;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
@@ -15,21 +14,6 @@ import java.util.Map;
@Service
public final class ServerProperties {
private static ServerProperties instance;
/**
* A very bad static way of obtaining the level name from server.properties of older mc versions that
* don't have any way of getting main level name.
* <br />
* For internal use ONLY!
*
* @return The level name if it can be obtained, else none.
*/
@ApiStatus.Internal
public static Option<String> getStaticLevelName() {
return Option.of(instance).flatMap(ServerProperties::getLevelName);
}
private final Map<String, String> properties;
private final FileUtils fileUtils;
@@ -38,8 +22,6 @@ public final class ServerProperties {
this.fileUtils = fileUtils;
properties = new HashMap<>();
parseServerPropertiesFile();
instance = this;
}
private void parseServerPropertiesFile() {
@@ -2,17 +2,13 @@ package org.mvplugins.multiverse.core.utils;
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@@ -34,15 +30,15 @@ public final class StringFormatter {
* @param list the list of strings to join. If the list is empty, an empty string is returned.
* @return the concatenated string
*/
public static @NotNull String joinAnd(@Nullable List<String> list) {
public static @NotNull String joinAnd(List<String> list) {
return join(list, ", ", " and ");
}
public static @NotNull String join(@Nullable Collection<?> list, @NotNull String separator) {
public static @NotNull String join(Collection list, String separator) {
if (list == null || list.isEmpty()) {
return "";
}
return list.stream().map(String::valueOf).collect(Collectors.joining(separator));
return list.stream().map(String::valueOf).collect(Collectors.joining(separator)).toString();
}
/**
@@ -54,7 +50,7 @@ public final class StringFormatter {
* @param lastSeparator the separator to use before the last element. For example, " and ".
* @return the concatenated string
*/
public static @NotNull String join(@Nullable List<String> list, @NotNull String separator, @NotNull String lastSeparator) {
public static @NotNull String join(List<String> list, String separator, String lastSeparator) {
if (list == null || list.isEmpty()) {
return "";
}
@@ -75,30 +71,11 @@ public final class StringFormatter {
/**
* Appends a list of suggestions to the end of the input string, separated by commas.
*
* @param input The current input
* @param addons The autocomplete suggestions
* @return A collection of suggestions with the next suggestion appended
*
* @deprecated Method name has a spelling error. Use {@link #addOnToCommaSeparated(String, Collection)} instead.
*/
@Deprecated(forRemoval = true, since = "5.5")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public static Collection<String> addonToCommaSeperated(@Nullable String input, @NotNull Collection<String> addons) {
return addOnToCommaSeparated(input, addons);
}
/**
* Appends a list of suggestions to the end of the input string, separated by commas.
*
* @param input The current input
* @param addons The autocomplete suggestions
* @return A collection of suggestions with the next suggestion appended
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public static Collection<String> addOnToCommaSeparated(@Nullable String input, @NotNull Collection<String> addons) {
if (Strings.isNullOrEmpty(input)) {
return addons;
}
@@ -117,7 +94,7 @@ public final class StringFormatter {
* @param args The args to parse
* @return The parsed args
*/
public static @NotNull Collection<String> parseQuotesInArgs(@NotNull String[] args) {
public static Collection<String> parseQuotesInArgs(String[] args) {
List<String> result = new ArrayList<>(args.length);
StringBuilder current = new StringBuilder();
boolean inQuotes = false;
@@ -162,28 +139,7 @@ public final class StringFormatter {
* @param input The string to add quotes to
* @return The quoted string
*/
@Contract("null -> null")
public static @Nullable String quoteMultiWordString(@Nullable String input) {
return input != null && input.contains(" ") ? "\"" + input + "\"" : input;
}
/**
* Parses a CSV string of key=value pairs into a map.
* E.g. "key1=value1,key2=value2" -> {key1=value1, key2=value2}
*
* @param input The CSV string to parse
* @return The parsed map
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public static @Unmodifiable Map<String, String> parseCSVMap(@Nullable String input) {
if (Strings.isNullOrEmpty(input)) {
return Map.of();
}
return REPatterns.COMMA.splitAsStream(input)
.map(s -> REPatterns.EQUALS.split(s, 2))
.filter(parts -> parts.length == 2)
.collect(Collectors.toUnmodifiableMap(parts -> parts[0], parts -> parts[1]));
public static String quoteMultiWordString(String input) {
return input.contains(" ") ? "\"" + input + "\"" : input;
}
}
@@ -1,7 +1,7 @@
package org.mvplugins.multiverse.core.utils;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import org.bukkit.Server;
import org.bukkit.scheduler.BukkitRunnable;
@@ -10,6 +10,8 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Objects;
/**
* Defers action that cannot be done during world tick.
@@ -19,22 +21,26 @@ public final class WorldTickDeferrer {
private final MultiverseCore plugin;
private final Option<Object> console;
private final Option<Field> isIteratingOverLevelsMethod;
private Object console = null;
private Field isIteratingOverLevelsMethod = null;
@Inject
WorldTickDeferrer(@NotNull MultiverseCore plugin, @NotNull Server server) {
this.plugin = plugin;
this.console = ReflectHelper.tryGetMethod(server.getClass(), "getServer")
.onFailure(throwable -> Logging.fine("Unable to find getServer method."))
.flatMap(getServerMethod -> ReflectHelper.tryInvokeMethod(server, getServerMethod))
.onFailure(throwable -> Logging.fine("Unable to find console."))
.toOption();
this.isIteratingOverLevelsMethod = console.toTry()
.map(Object::getClass)
.flatMap(consoleClazz -> ReflectHelper.tryGetField(consoleClazz, "isIteratingOverLevels"))
.onFailure(throwable -> Logging.fine("Unable to find isIteratingOverLevels field."))
.toOption();
Method getServerMethod = ReflectHelper.getMethod(server.getClass(), "getServer");
if (getServerMethod == null) {
Logging.fine("Unable to find getServer method.");
return;
}
this.console = ReflectHelper.invokeMethod(server, getServerMethod);
if (console == null) {
Logging.fine("Unable to find console.");
return;
}
this.isIteratingOverLevelsMethod = Try.of(() -> console.getClass().getField("isIteratingOverLevels")).getOrNull();
if (isIteratingOverLevelsMethod == null) {
Logging.fine("Unable to find isIteratingOverLevels field.");
}
}
/**
@@ -57,14 +63,16 @@ public final class WorldTickDeferrer {
}
/**
* Check if the server is currently doing a world tick.
* Check if the the server is currently doing a world tick.
*
* @return True if the server is currently doing a world tick
*/
private boolean isIteratingOverLevels() {
return isIteratingOverLevelsMethod
.flatMap(field -> console
.flatMap(c -> ReflectHelper.tryGetFieldValue(c, field, Boolean.class).toOption()))
.getOrElse(false);
if (console == null || isIteratingOverLevelsMethod == null) {
return false;
}
return Objects.requireNonNullElse(
ReflectHelper.getFieldValue(console, isIteratingOverLevelsMethod, Boolean.class),
false);
}
}
@@ -1,123 +0,0 @@
package org.mvplugins.multiverse.core.utils.compatibility;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.Server;
import org.bukkit.World;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import java.lang.reflect.Method;
import java.nio.file.Path;
/**
* Compatibility class used to handle API changes in {@link Bukkit} class.
*
* @since 5.6
*/
@ApiStatus.AvailableSince("5.6")
public final class BukkitCompatibility {
private static final Try<Method> GET_LEVEL_DIRECTORY_METHOD;
private static final Try<Method> GET_WORLD_NAMESPACED_KEY_METHOD;
static {
GET_LEVEL_DIRECTORY_METHOD = ReflectHelper.tryGetMethod(Server.class, "getLevelDirectory");
GET_WORLD_NAMESPACED_KEY_METHOD = ReflectHelper.tryGetMethod(Bukkit.class, "getWorld", NamespacedKey.class);
}
/**
* Check whether the server is using the new dimension storage system introduced in PaperMC 26.1.
* <br />
* This is based of whether getLevelDirectory method exists in the server class,which is the main API change for
* the new dimension storage system.
*
* @return True if the server is using the new dimension storage system, else false.
*
* @since 5.6
*/
@ApiStatus.AvailableSince("5.6")
public static boolean isUsingNewDimensionStorage() {
return GET_LEVEL_DIRECTORY_METHOD
.flatMap(method -> ReflectHelper.tryInvokeMethod(Bukkit.getServer(), method))
.isSuccess();
}
/**
* Gets the folder where all the worlds will be store. Before 26.1, all worlds are stored in the root directory
* of the server, which can be obtained by {@link Server#getWorldContainer()}.
* <br />
* After 26.1, PaperMC changed all worlds are stored in the "[level]/dimensions" folder under the world
* level directory, which needs to be manually parsed.
*
* @return The location where all the worlds folders should be, depending on server's mc version.
*
* @since 5.6
*/
@ApiStatus.AvailableSince("5.6")
@NotNull
public static Path getWorldFoldersDirectory() {
Server server = Bukkit.getServer();
return GET_LEVEL_DIRECTORY_METHOD.flatMap(method -> ReflectHelper.tryInvokeMethod(server, method))
.filter(Path.class::isInstance)
.map(Path.class::cast)
.map(path -> path.resolve("dimensions"))
.getOrElse(() -> server.getWorldContainer().toPath());
}
/**
* Check if the world with the given name or namespaced key (e.g. minecraft:overworld) exists,
* and return it if it does.
* <br />
* Note that some default world names have different namespaced key matched with them.
* E.g.: world -> minecraft:overworld, world_nether -> minecraft:the_nether, world_the_end -> minecraft:the_end.
*
* @param nameOrKey Either a name or namespaced key string representation.
* @return The world if it exists
*
* @since 5.6
*/
@ApiStatus.AvailableSince("5.6")
@NotNull
public static Option<World> getWorldByNameOrKey(@NotNull String nameOrKey) {
return Option.of(Bukkit.getWorld(nameOrKey))
.orElse(() -> GET_WORLD_NAMESPACED_KEY_METHOD
.flatMap(method -> ReflectHelper.tryInvokeStaticMethod(method, NamespacedKey.fromString(nameOrKey)))
.filter(World.class::isInstance)
.map(World.class::cast)
.toOption());
}
/**
* Check if the world represented by the given {@link WorldKeyOrName} exists and return it if present.
*
* <p>This first attempts to resolve the world by name using {@link WorldKeyOrName#usableName()} (which
* delegates to the traditional Bukkit world name lookup). If that fails it will attempt to resolve a
* {@link NamespacedKey} using {@link WorldKeyOrName#usableKey()} (which mirrors the newer Bukkit API
* that accepts namespaced keys, e.g. "minecraft:overworld").
*
* @param keyOrName The key-or-name wrapper providing either a world name or a {@link NamespacedKey}.
* @return The world if it exists.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static Option<World> getWorldByNameOrKey(@NotNull WorldKeyOrName keyOrName) {
return Option.of(Bukkit.getWorld(keyOrName.usableName()))
.orElse(() -> GET_WORLD_NAMESPACED_KEY_METHOD
.flatMap(method -> ReflectHelper.tryInvokeStaticMethod(method, keyOrName.usableKey()))
.filter(World.class::isInstance)
.map(World.class::cast)
.toOption());
}
private BukkitCompatibility() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}
@@ -1,45 +0,0 @@
package org.mvplugins.multiverse.core.utils.compatibility;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.UnsafeValues;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
import java.lang.reflect.Method;
/**
* Compatibility class used to handle API changes in {@link UnsafeValues} class.
*
* @since 5.6
*/
@ApiStatus.AvailableSince("5.7")
public final class UnsafeValuesCompatibility {
private static final Try<Method> GET_MAIN_LEVEL_NAME_METHOD;
static {
GET_MAIN_LEVEL_NAME_METHOD = ReflectHelper.tryGetMethod(UnsafeValues.class, "getMainLevelName");
}
/**
* Try to call the getMainLevelName method in UnsafeValues, which is used to get level-name configured in
* server.properties file.
*
* @return The level-name configured in server.properties file, or empty if the method is not available.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static Option<String> getMainLevelName() {
return GET_MAIN_LEVEL_NAME_METHOD
.flatMap(method -> ReflectHelper.tryInvokeMethod(Bukkit.getUnsafe(), method))
.map(String.class::cast)
.toOption();
}
private UnsafeValuesCompatibility() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}
@@ -1,170 +0,0 @@
package org.mvplugins.multiverse.core.utils.compatibility;
import org.bukkit.WorldBorder;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
import org.mvplugins.multiverse.core.utils.tick.TickDuration;
import java.util.concurrent.TimeUnit;
/**
* Compatibility class used to handle API changes in {@link WorldBorder} class.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@SuppressWarnings("removal")
public final class WorldBorderCompatibility {
private static final boolean HAS_GET_WARNING_TIME_TICKS_METHOD;
private static final boolean HAS_SET_WARNING_TIME_TICKS_METHOD;
private static final boolean HAS_SET_WARNING_TIME_METHOD;
private static final boolean HAS_GET_WARNING_TIME_METHOD;
private static final boolean HAS_CHANGE_SIZE_METHOD;
private static final boolean HAS_SET_SIZE_METHOD;
static {
HAS_GET_WARNING_TIME_TICKS_METHOD = ReflectHelper.hasMethod(WorldBorder.class, "getWarningTimeTicks");
HAS_SET_WARNING_TIME_TICKS_METHOD = ReflectHelper.hasMethod(WorldBorder.class, "setWarningTimeTicks", int.class);
HAS_GET_WARNING_TIME_METHOD = ReflectHelper.hasMethod(WorldBorder.class, "getWarningTime");
HAS_SET_WARNING_TIME_METHOD = ReflectHelper.hasMethod(WorldBorder.class, "setWarningTime", int.class);
HAS_CHANGE_SIZE_METHOD = ReflectHelper.hasMethod(WorldBorder.class, "changeSize", double.class, long.class);
HAS_SET_SIZE_METHOD = ReflectHelper.hasMethod(WorldBorder.class, "setSize", double.class, long.class);
}
/**
* Gets the world border warning time in ticks, using the most precise API available.
* <br />
* If the server exposes {@code getWarningTimeTicks()}, that value is returned directly. Otherwise, the older
* {@code getWarningTime()} API is used and converted from seconds to ticks.
*
* @param worldBorder The world border to query.
* @return The warning time in ticks.
*
* @since 5.7
* @throws IllegalStateException If neither warning-time getter is available.
*/
@ApiStatus.AvailableSince("5.7")
public static int getWarningTimeTicks(WorldBorder worldBorder) {
if (HAS_GET_WARNING_TIME_TICKS_METHOD) {
return worldBorder.getWarningTimeTicks();
} else if (HAS_GET_WARNING_TIME_METHOD) {
return worldBorder.getWarningTime() * 20;
}
throw new IllegalStateException("Neither getWarningTimeTicks nor getWarningTime method is available in WorldBorder class.");
}
/**
* Gets the world border warning time in seconds, using the most appropriate API available.
* <br />
* If the server exposes {@code getWarningTimeTicks()}, that value is converted from ticks to seconds. Otherwise,
* the older {@code getWarningTime()} API is used directly.
*
* @param worldBorder The world border to query.
* @return The warning time in seconds.
*
* @since 5.7
* @throws IllegalStateException If neither warning-time getter is available.
*/
@ApiStatus.AvailableSince("5.7")
public static double getWarningTime(WorldBorder worldBorder) {
if (HAS_GET_WARNING_TIME_TICKS_METHOD) {
return (double) worldBorder.getWarningTimeTicks() / 20.0;
} else if (HAS_GET_WARNING_TIME_METHOD) {
return worldBorder.getWarningTime();
}
throw new IllegalStateException("Neither getWarningTimeTicks nor getWarningTime method is available in WorldBorder class.");
}
/**
* Checks whether the current server supports setting the world border warning time in ticks.
*
* @return True if {@code setWarningTimeTicks(int)} is available, else false.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static boolean supportsSetWarningTimeInTicks() {
return HAS_SET_WARNING_TIME_TICKS_METHOD;
}
/**
* Sets the world border warning time in ticks.
* <br />
* This is a convenience wrapper around {@link #setWarningTimeDuration(WorldBorder, TickDuration)} that converts
* the given tick value into a {@link TickDuration} first.
*
* @param worldBorder The world border to update.
* @param ticks The warning time in ticks.
*
* @since 5.7
*/
public static void setWarningTimeTicks(WorldBorder worldBorder, int ticks) {
setWarningTimeDuration(worldBorder, TickDuration.ofTicks(ticks));
}
/**
* Sets the world border warning time, preferring tick-based APIs when available.
* <br />
* If the server supports {@code setWarningTimeTicks(int)}, the provided duration is applied in ticks. Otherwise,
* the older {@code setWarningTime(int)} API is used and the duration is converted to seconds.
*
* @param worldBorder The world border to update.
* @param duration The warning duration to apply.
*
* @since 5.7
* @throws IllegalStateException If neither warning-time setter is available.
*/
@ApiStatus.AvailableSince("5.7")
public static void setWarningTimeDuration(WorldBorder worldBorder, TickDuration duration) {
if (HAS_SET_WARNING_TIME_TICKS_METHOD) {
worldBorder.setWarningTimeTicks((int) duration.toTicks());
} else if (HAS_SET_WARNING_TIME_METHOD) {
worldBorder.setWarningTime((int) Math.round(duration.to(TimeUnit.SECONDS)));
} else {
throw new IllegalStateException("Neither setWarningTimeTicks nor setWarningTime method is available in WorldBorder class.");
}
}
/**
* Checks whether the current server supports changing world border size over a duration in ticks.
*
* @return True if {@code changeSize(double, long)} is available, else false.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static boolean supportsChangeSizeInTicks() {
return HAS_CHANGE_SIZE_METHOD;
}
/**
* Changes the world border size, preferring the tick-based API when available.
* <br />
* If the server supports {@code changeSize(double, long)}, the size change is applied in ticks. Otherwise, the
* older {@code setSize(double, long)} API is used and the duration is converted to seconds.
*
* @param worldBorder The world border to update.
* @param newSize The new border size.
* @param duration The transition duration to apply.
*
* @since 5.7
* @throws IllegalStateException If neither size-changing API is available.
*/
@ApiStatus.AvailableSince("5.7")
public static void changeSizeDuration(WorldBorder worldBorder, double newSize, TickDuration duration) {
if (HAS_CHANGE_SIZE_METHOD) {
worldBorder.changeSize(newSize, duration.toTicks());
} else if (HAS_SET_SIZE_METHOD) {
worldBorder.setSize(newSize, Math.round(duration.to(TimeUnit.SECONDS)));
} else {
throw new IllegalStateException("Neither changeSize nor setSize method is available in WorldBorder class.");
}
}
private WorldBorderCompatibility() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}
@@ -1,92 +0,0 @@
package org.mvplugins.multiverse.core.utils.compatibility;
import io.papermc.lib.PaperLib;
import io.vavr.control.Try;
import org.bukkit.World;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
import java.lang.reflect.Method;
/**
* Compatibility class used to handle API changes in {@link World} class.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public final class WorldCompatibility {
private static final Try<Method> SAVE_WITH_FLUSH_METHOD;
private static final boolean HAS_GET_COORDINATE_SCALE_METHOD;
private static final boolean HAS_HAS_BONUS_CHEST_METHOD;
static {
SAVE_WITH_FLUSH_METHOD = ReflectHelper.tryGetMethod(World.class, "save", boolean.class);
HAS_GET_COORDINATE_SCALE_METHOD = ReflectHelper.hasMethod(World.class, "getCoordinateScale");
HAS_HAS_BONUS_CHEST_METHOD = ReflectHelper.hasMethod(World.class, "hasBonusChest");
}
/**
* Saves the world, with an option to wait for chunk writers to finish if the method is available. Saving with
* flush is only supported on PaperMC 1.21+.
* <br />
* If the method is not available, it will fall back to the normal save method, which may not wait for chunk writers
* to finish, but is the best we can do for older versions of Minecraft.
*
* @param world The world to save
* @param flush Waits for chunk writers to finish
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static void saveWithFlush(World world, boolean flush) {
SAVE_WITH_FLUSH_METHOD
.flatMap(method -> ReflectHelper.tryInvokeMethod(world, method, flush))
.orElseRun(ignore -> world.save());
}
/**
* Gets the coordinate scale for the world, which is used to convert between overworld and nether coordinates.
* On PaperMC, this can be obtained directly from the API. On older versions, it is manually determined based on the
* world environment (overworld = 1, nether = 8, end = 1).
*
* @param world The world to get the coordinate scale for
* @return The scale
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static double getCoordinateScale(World world) {
if (HAS_GET_COORDINATE_SCALE_METHOD) {
return world.getCoordinateScale();
}
return switch (world.getEnvironment()) {
case NORMAL -> 1;
case NETHER -> 8;
default -> 1;
};
}
/**
* Checks if the world has a bonus chest.
* <br />
* On PaperMC 1.21.5+, this can be obtained directly from the API. On older versions, it always returns false as
* bonus chest is not supported.
*
* @param world The world to check
* @return True if the world has a bonus chest, false otherwise
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static boolean hasBonusChest(World world) {
if (HAS_HAS_BONUS_CHEST_METHOD) {
return world.hasBonusChest();
}
return false;
}
private WorldCompatibility() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}
@@ -1,188 +0,0 @@
package org.mvplugins.multiverse.core.utils.compatibility;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import org.bukkit.NamespacedKey;
import org.bukkit.WorldCreator;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
import org.mvplugins.multiverse.core.utils.position.EntityPosition;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import java.lang.reflect.Method;
import java.util.Locale;
/**
* Compatibility class used to handle {@link WorldCreator} API changes across different server versions.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public final class WorldCreatorCompatibility {
private static final Try<Class<?>> POSITION_CLASS;
private static final Try<Method> FORCED_SPAWN_POSITION_METHOD;
private static final Try<Method> OF_KEY_METHOD;
private static final Try<Method> OF_NAME_AND_KEY_METHOD;
private static final Try<Method> BONUS_CHEST_METHOD;
static {
POSITION_CLASS = ReflectHelper.tryGetClass("io.papermc.paper.math.Position");
FORCED_SPAWN_POSITION_METHOD = POSITION_CLASS.flatMap(positionClass ->
ReflectHelper.tryGetMethod(WorldCreator.class, "forcedSpawnPosition", positionClass, float.class, float.class));
BONUS_CHEST_METHOD = ReflectHelper.tryGetMethod(WorldCreator.class, "bonusChest", boolean.class);
OF_KEY_METHOD = ReflectHelper.tryGetMethod(WorldCreator.class, "ofKey", NamespacedKey.class);
OF_NAME_AND_KEY_METHOD = ReflectHelper.tryGetMethod(WorldCreator.class, "ofNameAndKey", String.class, NamespacedKey.class);
}
/**
* Check whether the current server version supports creating worlds with a {@link NamespacedKey}.
* <br />
* This is based on whether the {@code ofKey(NamespacedKey)} method exists in the {@link WorldCreator} class,
* which was introduced in later server versions.
*
* @return True if the server supports world creation with NamespacedKey, else false.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static boolean canCreateWorldWithKey() {
return OF_KEY_METHOD.isSuccess();
}
/**
* Creates a {@link WorldCreator} from a {@link WorldKeyOrName}, using the most appropriate method
* based on the server version and the key/name state.
* <br />
* If the server supports NamespacedKey-based creation and the provided keyOrName is a key,
* this method will use {@code ofKey()}. Otherwise, it falls back to {@code name()}.
*
* @param keyOrName Either a world key or name to create from.
* @return The {@link WorldCreator} initialized for the given key or name.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static WorldCreator ofKeyOrName(@NotNull WorldKeyOrName keyOrName) {
return ofNameAndKey(keyOrName.usableKey(), keyOrName.usableName());
}
/**
* Creates a {@link WorldCreator} from a {@link NamespacedKey} and world name, using the most
* appropriate method based on the server version and dimension storage configuration.
* <br />
* This method attempts to use the most feature-rich creation method available:
* if {@code ofNameAndKey()} is available and the parameters are compatible, use it;
* otherwise, if {@code ofKey()} is available, use it; as a fallback, use {@code name()}.
*
* @param worldKey The namespaced key for the world.
* @param worldName The display name for the world.
* @return The {@link WorldCreator} initialized with the given key and name.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static WorldCreator ofNameAndKey(@NotNull NamespacedKey worldKey, String worldName) {
if (OF_NAME_AND_KEY_METHOD.isSuccess() && canPassIntoNameAndKey(worldKey)) {
return WorldCreator.ofNameAndKey(worldName, worldKey);
}
if (OF_KEY_METHOD.isSuccess()) {
return WorldCreator.ofKey(worldKey);
}
return WorldCreator.name(worldName);
}
/**
* Check if the given world key and name can be passed into the {@code ofNameAndKey()} method.
* <br />
* The parameters are compatible only if the server is not using new dimension storage,
* or if the namespace is the Minecraft namespace and the world name (lowercased) matches the key.
*
* @param worldKey The namespaced key for the world.
* @return True if the parameters can be safely passed to {@code ofNameAndKey()}, else false.
*/
private static boolean canPassIntoNameAndKey(@NotNull NamespacedKey worldKey) {
return !BukkitCompatibility.isUsingNewDimensionStorage()
|| worldKey.getNamespace().equals(NamespacedKey.MINECRAFT);
}
/**
* Checks if the server supports configuring a forced spawn position via the WorldCreator API.
* <br />
* The force spawn position API is generally only available on PaperMC 26.1+
*
* @return Whether force spawn position API is supported on the current server version.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static boolean supportsForcedSpawnPosition() {
return FORCED_SPAWN_POSITION_METHOD.isSuccess();
}
/**
* Tries to set the forced spawn position if the server implements the API. This call will do nothing if server
* software does not implement the required APIs.
*
* @param worldCreator Target creator instance to set on.
* @param position The position to set as the forced spawn point for the world.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static void setForcedSpawnPosition(WorldCreator worldCreator, EntityPosition position) {
if (!supportsForcedSpawnPosition()) {
Logging.fine("Server does not support forced spawn position configuration via WorldCreator API.");
return;
}
ReflectHelper.tryInvokeMethod(
worldCreator,
FORCED_SPAWN_POSITION_METHOD.get(),
io.papermc.paper.math.Position.fine(
position.getVector().getX().getRawValue(),
position.getVector().getY().getRawValue(),
position.getVector().getZ().getRawValue()
),
(float) position.getDirection().getYaw().getRawValue(),
(float) position.getDirection().getPitch().getRawValue()
).onFailure(ex ->
Logging.warning("Failed to set forced spawn position on WorldCreator: %s", ex.getMessage()));
}
/**
* Checks if the server supports configuring bonus chest generation via the WorldCreator API.
* <br />
* The bonus chest API is generally only available on PaperMC 1.21.5+
*
* @return Whether bonus chest API is supported on the current server version.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static boolean supportsBonusChest() {
return BONUS_CHEST_METHOD.isSuccess();
}
/**
* Tries to set bonus chest if the server implements the API. This call will do nothing if server software does not
* implement the required APIs.
*
* @param worldCreator Target creator instance to set on.
* @param generateBonusChest Whether to generate a bonus chest at the world spawn point.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static void setBonusChest(WorldCreator worldCreator, boolean generateBonusChest) {
if (!supportsBonusChest()) {
Logging.fine("Server does not support bonus chest generation via WorldCreator API.");
return;
}
worldCreator.bonusChest(generateBonusChest);
}
private WorldCreatorCompatibility() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}
@@ -1,5 +0,0 @@
/**
* Contains classes to handle compatibility due to differing API methods across different server versions and the
* API gaps between PaperMC and Spigot.
*/
package org.mvplugins.multiverse.core.utils.compatibility;
@@ -2,7 +2,6 @@ package org.mvplugins.multiverse.core.utils.result;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import io.vavr.control.Either;
@@ -20,38 +19,6 @@ import org.mvplugins.multiverse.core.locale.message.MessageReplacement;
*/
public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Success, Attempt.Failure {
/**
* Creates a new success attempt.
*
* @param value The value.
* @param <T> The type of the value.
* @param <F> The type of failure reason.
* @return The new success attempt.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
static <T, F extends FailureReason> Attempt.Success<T, F> successRef(T value) {
return new Success<>(value);
}
/**
* Creates a new failure attempt.
*
* @param failureReason The reason for failure.
* @param messageReplacements The replacements for the failure message.
* @param <T> The type of the value.
* @param <F> The type of failure reason.
* @return The new failure attempt.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
static <T, F extends FailureReason> Attempt.Failure<T, F> failureRef(
F failureReason, MessageReplacement... messageReplacements) {
return new Failure<>(failureReason, Message.of(failureReason, "Failed!", messageReplacements));
}
/**
* Creates a new success attempt.
*
@@ -61,7 +28,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @return The new success attempt.
*/
static <T, F extends FailureReason> Attempt<T, F> success(T value) {
return successRef(value);
return new Success<>(value);
}
/**
@@ -75,7 +42,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
*/
static <T, F extends FailureReason> Attempt<T, F> failure(
F failureReason, MessageReplacement... messageReplacements) {
return failureRef(failureReason, messageReplacements);
return new Failure<>(failureReason, Message.of(failureReason, "Failed!", messageReplacements));
}
/**
@@ -236,33 +203,6 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
*/
<U> Attempt<U, F> mapAttempt(Supplier<Attempt<U, F>> mapper);
/**
* Fails this attempt with the given failure attempt if the value matches the given predicate.This will do nothing
* if this is already a failure attempt.
*
* @param predicate The predicate to test the value.
* @param failureAttempt The failure attempt to return if the predicate matches.
* @return The new attempt
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
Attempt<T, F> failIf(Predicate<? super T> predicate, Supplier<Attempt.Failure<T, F>> failureAttempt);
/**
* Fails this attempt with the given failure attempt if the value matches the given predicate.This will do nothing
* if this is already a failure attempt.
*
* @param predicate The predicate to test the value.
* @param failureAttempt The failure attempt to return if the predicate matches, which can be generated based
* on the value.
* @return The new attempt
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
Attempt<T, F> failIf(Predicate<? super T> predicate, Function<? super T, Attempt.Failure<T, F>> failureAttempt);
/**
* Maps to another attempt with a different fail reason.
*
@@ -430,16 +370,6 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
return mapper.get();
}
@Override
public Attempt<T, F> failIf(Predicate<? super T> predicate, Supplier<Attempt.Failure<T, F>> failureAttempt) {
return predicate.test(value) ? failureAttempt.get() : this;
}
@Override
public Attempt<T, F> failIf(Predicate<? super T> predicate, Function<? super T, Failure<T, F>> failureAttempt) {
return predicate.test(value) ? failureAttempt.apply(value) : this;
}
@Override
public <UF extends FailureReason> Attempt<T, UF> transform(UF failureReason) {
return changeFailureType();
@@ -602,16 +532,6 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
return changeValueType();
}
@Override
public Attempt<T, F> failIf(Predicate<? super T> predicate, Supplier<Attempt.Failure<T, F>> failureAttempt) {
return this;
}
@Override
public Attempt<T, F> failIf(Predicate<? super T> predicate, Function<? super T, Failure<T, F>> failureAttempt) {
return this;
}
@Override
public <UF extends FailureReason> Attempt<T, UF> transform(UF failureReason) {
return new Failure<>(failureReason, getFailureMessage(), this);
@@ -1,274 +0,0 @@
package org.mvplugins.multiverse.core.utils.tick;
import io.vavr.control.Try;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.time.LocalTime;
import java.time.temporal.TemporalUnit;
import java.util.concurrent.TimeUnit;
/**
* Represents a duration in Minecraft game ticks.
* <p>
* This class wraps a {@link Duration} and provides convenient conversion methods
* between game ticks (50ms per tick), game days (24,000 ticks), and other time units.
* Game time is mapped to real-world seconds via: realSeconds = (ticks * 3.6) + 6*3600 (mod 24h),
* which means 00:00 maps to 06:00 in-game time.
* </p>
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public final class TickDuration {
/**
* Parses a time string into a {@link TickDuration}.
* <br />
* Formats supported:
* <ul>
* <li>{@code "123"} - raw tick count</li>
* <li>{@code "10s"} - seconds</li>
* <li>{@code "5d"} - game days</li>
* </ul>
*
* @param timeString the time string to parse
* @return a {@link Try} containing the parsed {@link TickDuration}, or a failure if parsing fails
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static Try<TickDuration> parseString(@NotNull String timeString) {
return Try.of(() -> {
if (timeString.endsWith("s")) {
String subString = timeString.substring(0, timeString.length() - 1);
return of(Long.parseLong(subString), TimeUnit.SECONDS);
} else if (timeString.endsWith("d")) {
String subString = timeString.substring(0, timeString.length() - 1);
return ofGameDays(Long.parseLong(subString));
}
return ofTicks(Long.parseLong(timeString));
});
}
/**
* Creates a {@link TickDuration} from a {@link Duration}.
*
* @param duration the duration
* @return a new {@link TickDuration}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Contract(value = "_ -> new", pure = true)
public static @NotNull TickDuration ofDuration(@NotNull Duration duration) {
return new TickDuration(duration);
}
/**
* Creates a {@link TickDuration} from seconds (as a {@link Duration}).
*
* @param duration the duration representing seconds
* @return a new {@link TickDuration}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Contract(value = "_ -> new", pure = true)
public static @NotNull TickDuration ofSeconds(@NotNull Duration duration) {
return new TickDuration(duration);
}
/**
* Creates a {@link TickDuration} from a tick count.
*
* @param ticks the number of game ticks
* @return a new {@link TickDuration}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Contract(value = "_ -> new", pure = true)
public static @NotNull TickDuration ofTicks(long ticks) {
return of(ticks, TickUnit.TICK);
}
/**
* Creates a {@link TickDuration} from a game day count.
*
* @param gameDays the number of game days (24,000 ticks each)
* @return a new {@link TickDuration}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Contract(value = "_ -> new", pure = true)
public static @NotNull TickDuration ofGameDays(long gameDays) {
return of(gameDays, TickUnit.GAME_DAY);
}
/**
* Creates a {@link TickDuration} from a time value and {@link TimeUnit}.
*
* @param time the time value
* @param unit the time unit
* @return a new {@link TickDuration}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Contract(value = "_, _ -> new", pure = true)
public static @NotNull TickDuration of(long time, @NotNull TimeUnit unit) {
return of(time, unit.toChronoUnit());
}
/**
* Creates a {@link TickDuration} from a time value and {@link TemporalUnit}.
*
* @param time the time value
* @param unit the temporal unit
* @return a new {@link TickDuration}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Contract(value = "_, _ -> new", pure = true)
public static @NotNull TickDuration of(long time, @NotNull TemporalUnit unit) {
return new TickDuration(Duration.of(time, unit));
}
private static final double TIME_MULTIPLIER = 3.6;
private static final long DAY_SECONDS = 24 * 60 * 60;
private static final long START_OFFSET = 6 * 60 * 60;
private final Duration duration;
/**
* Creates a {@link TickDuration} from a {@link Duration}.
*
* @param duration the underlying duration
*/
private TickDuration(@NotNull Duration duration) {
this.duration = duration;
}
/**
* Converts this duration to Minecraft local time.
* <p>
* Maps the duration to real-world seconds using the formula:
* realSeconds = (ticks * 3.6) + 6*3600 (mod 24h)
* </p>
*
* @return the {@link LocalTime} in Minecraft time (where 00:00 = 06:00 in-game)
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull LocalTime toLocalTime() {
// Minecraft maps ticks -> seconds by: realSeconds = (ticks * 3.6) + 6*3600 (mod 24h)
long realSeconds = (long) ((toTicks() * TIME_MULTIPLIER) + START_OFFSET) % DAY_SECONDS;
return LocalTime.ofSecondOfDay(realSeconds);
}
/**
* Converts this duration to game ticks.
*
* @return the number of game ticks (1 tick = 50ms)
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public long toTicks() {
return duration.toMillis() / TickUnit.TICK.inMillis();
}
/**
* Converts this duration to game days.
*
* @return the number of game days (1 day = 24,000 ticks)
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public double toGameDays() {
return (double) duration.toMillis() / (double) TickUnit.GAME_DAY.inMillis();
}
/**
* Converts this duration to seconds.
*
* @return the duration in seconds
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public double toSeconds() {
return to(TimeUnit.SECONDS);
}
/**
* Converts this duration to a specified {@link TimeUnit}.
*
* @param unit the target time unit
* @return the duration in the specified unit
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public double to(@NotNull TimeUnit unit) {
return to(unit.toChronoUnit());
}
/**
* Converts this duration to a specified {@link TemporalUnit}.
*
* @param unit the target temporal unit
* @return the duration in the specified unit
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public double to(@NotNull TemporalUnit unit) {
return (double) duration.toMillis() / (double) unit.getDuration().toMillis();
}
/**
* Checks whether this duration is an exact multiple of the given {@link TimeUnit}.
*
* @param unit the time unit to test against
* @return {@code true} if this duration divides evenly by the unit, otherwise {@code false}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public boolean isExactTo(@NotNull TimeUnit unit) {
return isExactTo(unit.toChronoUnit());
}
/**
* Checks whether this duration is an exact multiple of the given {@link TemporalUnit}.
*
* @param unit the temporal unit to test against
* @return {@code true} if this duration divides evenly by the unit, otherwise {@code false}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public boolean isExactTo(@NotNull TemporalUnit unit) {
return duration.toMillis() % unit.getDuration().toMillis() == 0;
}
/**
* Returns the underlying {@link Duration}.
*
* @return the duration
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public Duration getDuration() {
return duration;
}
}
@@ -1,112 +0,0 @@
package org.mvplugins.multiverse.core.utils.tick;
import org.jetbrains.annotations.ApiStatus;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalUnit;
/**
* A {@link TemporalUnit} implementation for Minecraft game ticks.
* <br />
* Provides two standard units:
* <ul>
* <li>{@link #TICK} - A single game tick (50 milliseconds)</li>
* <li>{@link #GAME_DAY} - A full game day (24,000 ticks)</li>
* </ul>
* This unit is not date-based and is precisely defined without estimation.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public final class TickUnit implements TemporalUnit {
private static final long SINGLE_TICK_DURATION_MS = 50L;
private static final long TICKS_PER_GAME_DAY = 24_000L;
/**
* A single game tick (50 milliseconds).
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static final TickUnit TICK = new TickUnit(SINGLE_TICK_DURATION_MS);
/**
* A full game day (24,000 ticks = 20 minutes in real time).
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static final TickUnit GAME_DAY = new TickUnit(SINGLE_TICK_DURATION_MS * TICKS_PER_GAME_DAY);
private final long milliseconds;
private final Duration duration;
private TickUnit(long milliseconds) {
this.milliseconds = milliseconds;
this.duration = Duration.ofMillis(milliseconds);
}
/**
* Gets the duration of this unit in milliseconds.
*
* @return the duration in milliseconds
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public long inMillis() {
return milliseconds;
}
/**
* {@inheritDoc}
*/
@Override
public Duration getDuration() {
return duration;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isDurationEstimated() {
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isDateBased() {
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isTimeBased() {
return true;
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked") // following ChronoUnit#addTo
@Override
public <R extends Temporal> R addTo(R temporal, long amount) {
return (R) temporal.plus(duration.multipliedBy(amount));
}
/**
* {@inheritDoc}
*/
@Override
public long between(final Temporal start, final Temporal end) {
return start.until(end, ChronoUnit.MILLIS) / milliseconds;
}
}
@@ -16,9 +16,6 @@ import javax.net.ssl.HttpsURLConnection;
* HTTP API-client.
*/
abstract sealed class HttpAPIClient permits PasteService, URLShortener {
static final String DUMPS_VIEWER_URL = "https://mvdumps.c0ding.party/";
/**
* The URL for this API-request, and if necessary, the access token.
* If an access token is not necessary, it should be set to null.
@@ -34,9 +34,7 @@ final class McloGsPasteService extends PasteService {
public String postData(String data) throws PasteFailedException {
try {
String stringJSON = this.exec(encodeData(data), ContentType.URLENCODED); // Execute request
return DUMPS_VIEWER_URL +
((JSONObject) new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE).parse(stringJSON)).get("id") +
"?service=mclogs";
return String.valueOf(((JSONObject) new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE).parse(stringJSON)).get("url")); // Interpret result
} catch (IOException | ParseException e) {
throw new PasteFailedException(e);
}
@@ -46,9 +44,7 @@ final class McloGsPasteService extends PasteService {
public String postData(Map<String, String> data) throws PasteFailedException {
try {
String stringJSON = this.exec(encodeData(data), ContentType.JSON); // Execute request
return DUMPS_VIEWER_URL +
((JSONObject) new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE).parse(stringJSON)).get("id") +
"?service=mclogs";
return String.valueOf(((JSONObject) new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE).parse(stringJSON)).get("url")); // Interpret result
} catch (IOException | ParseException e) {
throw new PasteFailedException(e);
}
@@ -26,7 +26,7 @@ final class PastebinPasteService extends PasteService {
"&" + URLEncoder.encode("api_option", StandardCharsets.UTF_8) + "=" + URLEncoder.encode("paste", StandardCharsets.UTF_8) +
"&" + URLEncoder.encode("api_paste_code", StandardCharsets.UTF_8) + "=" + URLEncoder.encode(data, StandardCharsets.UTF_8) +
"&" + URLEncoder.encode("api_paste_private", StandardCharsets.UTF_8) + "=" + URLEncoder.encode(this.isPrivate ? "1" : "0", StandardCharsets.UTF_8) +
"&" + URLEncoder.encode("api_paste_format", StandardCharsets.UTF_8) + "=" + URLEncoder.encode("json", StandardCharsets.UTF_8) +
"&" + URLEncoder.encode("api_paste_format", StandardCharsets.UTF_8) + "=" + URLEncoder.encode("yaml", StandardCharsets.UTF_8) +
"&" + URLEncoder.encode("api_paste_name", StandardCharsets.UTF_8) + "=" + URLEncoder.encode("Multiverse-Core Debug Info", StandardCharsets.UTF_8);
}
@@ -39,9 +39,8 @@ final class PastesDevPasteService extends PasteService {
@Override
public String postData(String data) throws PasteFailedException {
try {
String stringJSON = this.exec(encodeData(data), ContentType.JSON);
return DUMPS_VIEWER_URL +
((JSONObject) new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE).parse(stringJSON)).get("key");
String stringJSON = this.exec(encodeData(data), ContentType.PLAINTEXT_YAML);
return "https://pastes.dev/" + ((JSONObject) new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE).parse(stringJSON)).get("key");
} catch (IOException | ParseException e) {
throw new PasteFailedException(e);
}
@@ -39,7 +39,7 @@ public final class LoadedMultiverseWorld extends MultiverseWorld {
@NotNull LocationManipulation locationManipulation,
@NotNull EntityPurger entityPurger
) {
super(worldConfig, config);
super(world.getName(), worldConfig, config);
this.worldUid = world.getUID();
this.blockSafety = blockSafety;
this.locationManipulation = locationManipulation;
@@ -77,7 +77,7 @@ public final class LoadedMultiverseWorld extends MultiverseWorld {
Logging.fine("Spawn location from world.dat file was unsafe!!");
Logging.fine("NOT adjusting spawn for '" + this.getAliasOrName() + "' because you told me not to.");
Logging.fine("To turn on spawn adjustment for this world simply type:");
Logging.fine("/mv modify %s set adjust-spawn true", getName());
Logging.fine("/mvm set adjustspawn true " + this.getAliasOrName());
return location;
}
@@ -173,7 +173,7 @@ public final class LoadedMultiverseWorld extends MultiverseWorld {
* {@inheritDoc}
*/
@Override
void setWorldConfig(@NotNull WorldConfig worldConfig) {
void setWorldConfig(WorldConfig worldConfig) {
super.setWorldConfig(worldConfig);
setupWorldConfig(getBukkitWorld().get());
}
@@ -184,8 +184,7 @@ public final class LoadedMultiverseWorld extends MultiverseWorld {
@Override
public String toString() {
return "LoadedMultiverseWorld{"
+ "key='" + getKey() + "', "
+ "name='" + getName() + "', "
+ "name='" + worldName + "', "
+ "env='" + getEnvironment() + "', "
+ "type='" + getWorldType().getOrNull() + "', "
+ "gen='" + getGenerator() + "'"
@@ -1,29 +1,21 @@
package org.mvplugins.multiverse.core.world;
import java.io.File;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.common.base.Strings;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.Difficulty;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnmodifiableView;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle;
import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter;
import org.mvplugins.multiverse.core.world.helpers.WorldFolderResolver;
import org.mvplugins.multiverse.core.world.location.SpawnLocation;
import org.mvplugins.multiverse.core.world.entity.EntitySpawnConfig;
@@ -31,6 +23,11 @@ import org.mvplugins.multiverse.core.world.entity.EntitySpawnConfig;
* Represents a world handled by Multiverse which has all the custom properties provided by Multiverse.
*/
public sealed class MultiverseWorld permits LoadedMultiverseWorld {
/**
* This world's name.
*/
protected final String worldName;
/**
* This world's configuration.
*/
@@ -39,7 +36,8 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
protected final CoreConfig config;
private String colourlessAlias = "";
MultiverseWorld(WorldConfig worldConfig, CoreConfig config) {
MultiverseWorld(String worldName, WorldConfig worldConfig, CoreConfig config) {
this.worldName = worldName;
this.worldConfig = worldConfig;
this.config = config;
this.worldConfig.setMVWorld(this);
@@ -47,46 +45,15 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
}
/**
* The key that represents this world. This should now be used as the unique key for the world instead
* of the world name. Also note that the key cannot be modified.
*
* @return The world key
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public NamespacedKey getKey() {
return worldConfig.getWorldKeyOrName().usableKey();
}
/**
* Gets the name of this world. Prefer to use {@link #getKey()} as unique id instead of this name.
* Also note that the name cannot be modified.
* Gets the name of this world. The name cannot be changed.
* <br/>
* Note for plugin developers: Usually {@link #getAliasOrName()}is what you want to use instead of this method.
* Note for plugin developers: Usually {@link #getAliasOrName()}
* is what you want to use instead of this method.
*
* @return The name of the world as a String.
*/
@NotNull
public String getName() {
return worldConfig.getLegacyWorldName();
}
/**
* Gets the folder where all the world contents are stored in. Generally, the location of the folder is at the server
* roots directory for pre-26.1 servers, and at "[level]/dimensions" folder under the world level directory for
* 26.1+ PaperMC servers.
* <br />
* Note this folder location is based on Multiverse's understanding of Paper and Spigot's folder structure. If the
* server software does something weird, this folder will not reflect the actual world folder location.
* <br />
* If the world is loaded, you should use {@link World#getWorldFolder()} instead as it is more accurate.
* This method is more of a fallback for when the world is not loaded.
*
* @return The world folder.
*/
@ApiStatus.AvailableSince("5.7")
public File getOfflineWorldFolder() {
return WorldFolderResolver.resolve(this);
return worldName;
}
/**
@@ -99,8 +66,7 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
}
/**
* Gets whether this world is loaded. Note that this may return false even if this is a stored instance of
* {@link LoadedMultiverseWorld} as worlds can be unloaded anytime.
* Gets whether this world is loaded.
*
* @return True if the world is loaded, else false.
*/
@@ -108,23 +74,6 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
return worldConfig.isLoadedWorld();
}
/**
* Attempts to get loaded world instance. Only returns a value if the world is loaded at the time of calling, even
* if this is a stored instance of {@link LoadedMultiverseWorld}.
* <br/>
* You may use {@link #isLoaded()} to check if the world is loaded first.
*
* @return Loaded world instance wrapped in an Option, or None if the world is not loaded.
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public Option<LoadedMultiverseWorld> asLoadedWorld() {
return Option.of(worldConfig.getMVWorld())
.filter(world -> world instanceof LoadedMultiverseWorld)
.map(world -> (LoadedMultiverseWorld) world);
}
/**
* Gets the properties handler of this world.
*
@@ -175,7 +124,7 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
* @return The alias of the world as a String.
*/
public String getAliasOrName() {
return Strings.isNullOrEmpty(worldConfig.getAlias()) ? getName() : worldConfig.getAlias();
return Strings.isNullOrEmpty(worldConfig.getAlias()) ? worldName : worldConfig.getAlias();
}
/**
@@ -424,19 +373,6 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
return worldConfig.getGenerator();
}
/**
* Gets the generator settings used to create this world. You cannot change this after world creation.
* Changing this manually in config on existing world will have no effect on the world generation.
*
* @return The generator settings string.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public String getGeneratorSettings() {
return worldConfig.getGeneratorSettings();
}
/**
* Gets whether or not this world will display in chat, mvw and mvl regardless if a user has the
* access permissions to go to this world.
@@ -498,59 +434,6 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
return worldConfig.setKeepSpawnInMemory(keepSpawnInMemory);
}
/**
* Get all meta key-value pairs for this world. This is an unmodifiable view of the internal map, so changes to
* the world meta will be reflected in this map, but attempts to modify this map will throw an exception.
*
* @return An unmodifiable view of all meta key-value pairs for this world.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @UnmodifiableView Map<String, String> getAllMeta() {
return Collections.unmodifiableMap(worldConfig.getMeta());
}
/**
* Gets the meta value for the given key if it exists.
*
* @param key The meta key to look up, may be {@code null} in which case an empty result is returned.
* @return An {@link Option} containing the meta value if present, otherwise an empty {@link Option}.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Option<String> getMeta(@Nullable String key) {
return Option.of(worldConfig.getMeta().get(key));
}
/**
* Sets a meta key-value pair for this world. Existing value for the key will be replaced.
*
* @param key The meta key to set, must not be {@code null}.
* @param value The meta value to set.
* @return Result of setting the property.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Try<Void> setMeta(@NotNull String key, @Nullable String value) {
return worldConfig.setMeta(key, value);
}
/**
* Removes the meta entry associated with the given key.
*
* @param key The meta key to remove.
* @return Result of removing the property.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Try<Void> removeMeta(@NotNull String key) {
return worldConfig.removeMeta(key);
}
/**
* Gets the player limit for this world after which players without an override
* permission node will not be allowed in. A value of -1 or less signifies no limit
@@ -786,7 +669,7 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
*
* @return The world config.
*/
@NotNull WorldConfig getWorldConfig() {
WorldConfig getWorldConfig() {
return worldConfig;
}
@@ -795,7 +678,7 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
*
* @param worldConfig The world config.
*/
void setWorldConfig(@NotNull WorldConfig worldConfig) {
void setWorldConfig(WorldConfig worldConfig) {
this.worldConfig = worldConfig;
}
@@ -805,8 +688,7 @@ public sealed class MultiverseWorld permits LoadedMultiverseWorld {
@Override
public String toString() {
return "MultiverseWorld{"
+ "key='" + getKey() + "', "
+ "name='" + getName() + "', "
+ "name='" + worldName + "', "
+ "env='" + getEnvironment() + "', "
+ "gen='" + getGenerator() + "'"
+ '}';
@@ -1,7 +1,5 @@
package org.mvplugins.multiverse.core.world;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import java.util.List;
/**
@@ -10,5 +8,5 @@ import java.util.List;
* @param newWorlds List of the new WorldConfigs added
* @param removedWorlds List of the worlds removed from the config
*/
record NewAndRemovedWorlds(List<WorldConfig> newWorlds, List<WorldKeyOrName> removedWorlds) {
record NewAndRemovedWorlds(List<WorldConfig> newWorlds, List<String> removedWorlds) {
}
@@ -2,7 +2,6 @@ package org.mvplugins.multiverse.core.world;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import com.dumptruckman.minecraft.util.Logging;
@@ -30,7 +29,6 @@ import org.mvplugins.multiverse.core.config.migration.action.MoveMigratorAction;
import org.mvplugins.multiverse.core.config.migration.action.NullStringMigratorAction;
import org.mvplugins.multiverse.core.config.migration.VersionMigrator;
import org.mvplugins.multiverse.core.economy.MVEconomist;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import org.mvplugins.multiverse.core.world.location.SpawnLocation;
import org.mvplugins.multiverse.core.world.entity.EntitySpawnConfig;
@@ -39,16 +37,16 @@ import org.mvplugins.multiverse.core.world.entity.EntitySpawnConfig;
*/
final class WorldConfig {
private final WorldKeyOrName keyOrName;
private final String worldName;
private final WorldConfigNodes configNodes;
private final MemoryConfigurationHandle configHandle;
private final StringPropertyHandle stringPropertyHandle;
WorldConfig(
@NotNull WorldKeyOrName keyOrName,
@NotNull String worldName,
@NotNull ConfigurationSection configSection,
@NotNull MultiverseCore multiverseCore) {
this.keyOrName = keyOrName;
this.worldName = worldName;
this.configNodes = new WorldConfigNodes(multiverseCore);
this.configHandle = MemoryConfigurationHandle.builder(configSection, configNodes.getNodes())
.logger(Logging.getLogger())
@@ -120,11 +118,6 @@ final class WorldConfig {
.addAction(MoveMigratorAction.of("spawning.animals", "spawning.animal"))
.addAction(MoveMigratorAction.of("spawning.monsters", "spawning.monster"))
.build())
.addVersionMigrator(VersionMigrator.builder(1.3)
.addAction(config -> config.set("read-only.legacy-world-name", keyOrName.usableName()))
.addAction(MoveMigratorAction.of("environment", "read-only.environment"))
.addAction(MoveMigratorAction.of("seed", "read-only.seed"))
.build())
.build();
}
@@ -139,7 +132,7 @@ final class WorldConfig {
Try<Void> save() {
return configHandle.save().onFailure(ex ->
Logging.warning("Failed to save world config for world '%s'. %s",
keyOrName, ex.getLocalizedMessage()));
worldName, ex.getLocalizedMessage()));
}
ConfigurationSection getConfigurationSection() {
@@ -150,8 +143,8 @@ final class WorldConfig {
return stringPropertyHandle;
}
WorldKeyOrName getWorldKeyOrName() {
return keyOrName;
String getWorldName() {
return worldName;
}
boolean getAdjustSpawn() {
@@ -265,6 +258,14 @@ final class WorldConfig {
return configHandle.set(configNodes.entryFeeCurrency, entryFeeCurrency);
}
World.Environment getEnvironment() {
return configHandle.get(configNodes.environment);
}
Try<Void> setEnvironment(World.Environment environment) {
return configHandle.set(configNodes.environment, environment);
}
GameMode getGameMode() {
return configHandle.get(configNodes.gamemode);
}
@@ -304,18 +305,6 @@ final class WorldConfig {
return configHandle.set(configNodes.keepSpawnInMemory, keepSpawnInMemory);
}
Map<String, String> getMeta() {
return configHandle.get(configNodes.meta);
}
Try<Void> setMeta(String key, String value) {
return configHandle.set(configNodes.meta, key, value);
}
Try<Void> removeMeta(String key) {
return configHandle.remove(configNodes.meta, key);
}
int getPlayerLimit() {
return configHandle.get(configNodes.playerLimit);
}
@@ -356,6 +345,14 @@ final class WorldConfig {
return configHandle.set(configNodes.scale, scale);
}
long getSeed() {
return configHandle.get(configNodes.seed);
}
Try<Void> setSeed(long seed) {
return configHandle.set(configNodes.seed, seed);
}
SpawnLocation getSpawnLocation() {
return configHandle.get(configNodes.spawnLocation);
}
@@ -380,46 +377,10 @@ final class WorldConfig {
return configHandle.set(configNodes.worldBlacklist, worldBlacklist);
}
World.Environment getEnvironment() {
return configHandle.get(configNodes.environment);
}
Try<Void> setEnvironment(World.Environment environment) {
return configHandle.set(configNodes.environment, environment);
}
String getGeneratorSettings() {
return configHandle.get(configNodes.generatorSettings);
}
Try<Void> setGeneratorSettings(String generatorSettings) {
return configHandle.set(configNodes.generatorSettings, generatorSettings);
}
String getLegacyWorldName() {
return configHandle.get(configNodes.legacyWorldName);
}
Try<Void> setLegacyWorldName(String legacyWorldName) {
return configHandle.set(configNodes.legacyWorldName, legacyWorldName);
}
long getSeed() {
return configHandle.get(configNodes.seed);
}
Try<Void> setSeed(long seed) {
return configHandle.set(configNodes.seed, seed);
}
void setMVWorld(@NotNull MultiverseWorld world) {
configNodes.setWorld(world);
}
MultiverseWorld getMVWorld() {
return configNodes.getWorld();
}
boolean isLoadedWorld() {
return configNodes.getWorld() instanceof LoadedMultiverseWorld;
}
@@ -431,7 +392,7 @@ final class WorldConfig {
/**
* Migrates the entry fee settings. Assumes entry fee is disabled if currency is not set.
*/
private static final class EntryFeeMigrator implements MigratorAction {
static final class EntryFeeMigrator implements MigratorAction {
@Override
public void migrate(ConfigurationSection config) {
String currency = config.getString("entry-fee.currency", "");
@@ -7,7 +7,6 @@ import java.util.Objects;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Difficulty;
import org.bukkit.GameMode;
import org.bukkit.Material;
@@ -18,7 +17,6 @@ import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.config.node.MapConfigNode;
import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer;
import org.mvplugins.multiverse.core.event.world.MVWorldPropertyChangedEvent;
import org.mvplugins.multiverse.core.config.node.ConfigNode;
@@ -26,7 +24,6 @@ import org.mvplugins.multiverse.core.config.node.ListConfigNode;
import org.mvplugins.multiverse.core.config.node.NodeGroup;
import org.mvplugins.multiverse.core.economy.MVEconomist;
import org.mvplugins.multiverse.core.utils.MaterialConverter;
import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter;
import org.mvplugins.multiverse.core.world.helpers.EnforcementHandler;
import org.mvplugins.multiverse.core.world.location.NullSpawnLocation;
import org.mvplugins.multiverse.core.world.location.SpawnLocation;
@@ -93,8 +90,6 @@ final class WorldConfigNodes {
.onLoadAndChange((oldValue, newValue) -> {
if (world == null) return;
world.updateColourlessAlias();
worldManager.getWorldStore().changeAlias(
ChatTextFormatter.removeColor(oldValue), ChatTextFormatter.removeColor(newValue), world);
}));
final ConfigNode<Boolean> allowAdvancementGrant = node(ConfigNode.builder("allow-advancement-grant", Boolean.class)
@@ -177,6 +172,11 @@ final class WorldConfigNodes {
}
}));
final ConfigNode<World.Environment> environment = node(ConfigNode
.builder("environment", World.Environment.class)
.defaultValue(World.Environment.NORMAL)
.hidden());
final ConfigNode<GameMode> gamemode = node(ConfigNode.builder("gamemode", GameMode.class)
.defaultValue(GameMode.SURVIVAL)
.onLoadAndChange((oldValue, newValue) -> {
@@ -198,21 +198,11 @@ final class WorldConfigNodes {
final ConfigNode<Boolean> keepSpawnInMemory = node(ConfigNode
.builder("keep-spawn-in-memory", Boolean.class)
.defaultValue(true)
.onLoadAndChange((sender, oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
loadedWorld.getBukkitWorld().peek(bukkitWorld -> {
bukkitWorld.setKeepSpawnInMemory(newValue);
if (bukkitWorld.getKeepSpawnInMemory() != newValue) {
sender.sendMessage(ChatColor.RED + "Keep spawn in memory feature has been removed by " +
"Minecraft in 1.21.9+ and will no longer have any effect when set to true.");
}
});
loadedWorld.getBukkitWorld().peek(bukkitWorld -> bukkitWorld.setKeepSpawnInMemory(newValue));
}));
final MapConfigNode<String, String> meta = (MapConfigNode<String, String>) node(MapConfigNode
.mapBuilder("meta", String.class, String.class)
.hidden());
final ConfigNode<Integer> playerLimit = node(ConfigNode.builder("player-limit", Integer.class)
.defaultValue(-1));
@@ -244,6 +234,10 @@ final class WorldConfigNodes {
};
}));
final ConfigNode<Long> seed = node(ConfigNode.builder("seed", Long.class)
.defaultValue(Long.MIN_VALUE)
.hidden());
final ConfigNode<SpawnLocation> spawnLocation = node(ConfigNode.builder("spawn-location", SpawnLocation.class)
.defaultValue(NullSpawnLocation.get())
.hidden()
@@ -280,22 +274,6 @@ final class WorldConfigNodes {
final ConfigNode<List<String>> worldBlacklist = node(ListConfigNode.listBuilder("world-blacklist", String.class));
final ConfigNode<World.Environment> environment = node(ConfigNode
.builder("read-only.environment", World.Environment.class)
.defaultValue(World.Environment.NORMAL)
.hidden());
final ConfigNode<String> generatorSettings = node(ConfigNode.builder("read-only.generator-settings", String.class)
.defaultValue("")
.hidden());
final ConfigNode<String> legacyWorldName = node(ConfigNode.builder("read-only.legacy-world-name", String.class)
.hidden());
final ConfigNode<Long> seed = node(ConfigNode.builder("read-only.seed", Long.class)
.defaultValue(Long.MIN_VALUE)
.hidden());
final ConfigNode<Double> version = node(ConfigNode.builder("version", Double.class)
.defaultValue(CONFIG_VERSION)
.hidden());
@@ -1,10 +1,14 @@
package org.mvplugins.multiverse.core.world;
import java.io.File;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
@@ -13,22 +17,19 @@ import java.util.stream.Collectors;
import com.dumptruckman.minecraft.util.Logging;
import com.google.common.base.Strings;
import io.papermc.lib.PaperLib;
import io.vavr.control.Option;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import org.bukkit.Bukkit;
import org.bukkit.GameRule;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.WorldBorder;
import org.bukkit.WorldCreator;
import org.bukkit.WorldType;
import org.bukkit.plugin.PluginManager;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.CoreConfig;
@@ -47,23 +48,19 @@ import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.permissions.CorePermissions;
import org.mvplugins.multiverse.core.teleportation.BlockSafety;
import org.mvplugins.multiverse.core.teleportation.LocationManipulation;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
import org.mvplugins.multiverse.core.utils.ServerProperties;
import org.mvplugins.multiverse.core.utils.compatibility.BukkitCompatibility;
import org.mvplugins.multiverse.core.utils.compatibility.WorldCompatibility;
import org.mvplugins.multiverse.core.utils.compatibility.WorldCreatorCompatibility;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.utils.FileUtils;
import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter;
import org.mvplugins.multiverse.core.world.biomeprovider.BiomeProviderFactory;
import org.mvplugins.multiverse.core.world.entity.EntityPurger;
import org.mvplugins.multiverse.core.world.generators.GeneratorProvider;
import org.mvplugins.multiverse.core.world.helpers.DataStore.GameRulesStore;
import org.mvplugins.multiverse.core.world.helpers.DataTransfer;
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder;
import org.mvplugins.multiverse.core.world.helpers.PotentialWorldFinder;
import org.mvplugins.multiverse.core.world.helpers.WorldFolderResolver;
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder.DimensionFormat;
import org.mvplugins.multiverse.core.world.helpers.WorldNameChecker;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import org.mvplugins.multiverse.core.world.options.CloneWorldOptions;
import org.mvplugins.multiverse.core.world.options.CreateWorldOptions;
import org.mvplugins.multiverse.core.world.options.DeleteWorldOptions;
@@ -90,12 +87,12 @@ import static org.mvplugins.multiverse.core.world.helpers.DataStore.WorldConfigS
@Service // SUPPRESS CHECKSTYLE: ClassFanOutComplexity This is the world manager, it's going to be complex.
public final class WorldManager {
private static final List<String> CLONE_IGNORE_FILES = List.of(
"uid.dat", "session.lock", // All pre 26.1 format
"data/paper/metadata.dat" // New papermc format for 26.1+
);
private static final List<String> CLONE_IGNORE_FILES = Arrays.asList("uid.dat", "session.lock");
private static final DimensionFormat DEFAULT_NETHER_FORMAT = new DimensionFormat("%overworld%_nether");
private static final DimensionFormat DEFAULT_END_FORMAT = new DimensionFormat("%overworld%_the_end");
private final WorldStore worldStore;
private final Map<String, MultiverseWorld> worldsMap;
private final Map<String, LoadedMultiverseWorld> loadedWorldsMap;
private final List<String> unloadTracker;
private final List<String> loadTracker;
private final WorldsConfigManager worldsConfigManager;
@@ -110,11 +107,9 @@ public final class WorldManager {
private final ServerProperties serverProperties;
private final CoreConfig config;
private final EntityPurger entityPurger;
private final Provider<PotentialWorldFinder> potentialWorldFinder;
@Inject
WorldManager(
@NotNull WorldStore worldStore,
@NotNull WorldsConfigManager worldsConfigManager,
@NotNull WorldNameChecker worldNameChecker,
@NotNull BiomeProviderFactory biomeProviderFactory,
@@ -126,9 +121,7 @@ public final class WorldManager {
@NotNull CorePermissions corePermissions,
@NotNull ServerProperties serverProperties,
@NotNull CoreConfig config,
@NotNull EntityPurger entityPurger,
@NotNull Provider<PotentialWorldFinder> potentialWorldFinder) {
this.worldStore = worldStore;
@NotNull EntityPurger entityPurger) {
this.worldsConfigManager = worldsConfigManager;
this.worldNameChecker = worldNameChecker;
this.biomeProviderFactory = biomeProviderFactory;
@@ -141,8 +134,9 @@ public final class WorldManager {
this.serverProperties = serverProperties;
this.config = config;
this.entityPurger = entityPurger;
this.potentialWorldFinder = potentialWorldFinder;
this.worldsMap = new HashMap<>();
this.loadedWorldsMap = new HashMap<>();
this.unloadTracker = new ArrayList<>();
this.loadTracker = new ArrayList<>();
}
@@ -174,23 +168,19 @@ public final class WorldManager {
}
private void loadNewWorldConfigs(Collection<WorldConfig> newWorldConfigs) {
newWorldConfigs.forEach(worldConfig -> {
worldStore.getUnloadedWorldRef(worldConfig.getWorldKeyOrName().usableName())
.peek(unloadedWorld -> unloadedWorld.setWorldConfig(worldConfig))
.onEmpty(() -> newMultiverseWorld(worldConfig));
worldStore.getLoadedWorld(worldConfig.getWorldKeyOrName().usableName())
.peek(loadedWorld -> loadedWorld.setWorldConfig(worldConfig));
});
newWorldConfigs.forEach(worldConfig -> Option.of(worldsMap.get(worldConfig.getWorldName()))
.peek(unloadedWorld -> unloadedWorld.setWorldConfig(worldConfig))
.onEmpty(() -> newMultiverseWorld(worldConfig.getWorldName(), worldConfig)));
}
private void removeWorldsNotInConfigs(Collection<WorldKeyOrName> removedWorlds) {
removedWorlds.forEach(keyOrName -> getWorld(keyOrName.usableName())
private void removeWorldsNotInConfigs(Collection<String> removedWorlds) {
removedWorlds.forEach(worldName -> getWorld(worldName)
.map(world -> removeWorld(RemoveWorldOptions.world(world)))
.getOrElse(() -> worldActionResult(RemoveFailureReason.WORLD_NON_EXISTENT, keyOrName.toString()))
.getOrElse(() -> worldActionResult(RemoveFailureReason.UNLOAD_FAILED, worldName))
.onFailure(failure ->
Logging.severe("Failed to unload world %s: %s", keyOrName, failure))
Logging.severe("Failed to unload world %s: %s", worldName, failure))
.onSuccess(success ->
Logging.fine("Unloaded world %s as it was removed from config", keyOrName)));
Logging.fine("Unloaded world %s as it was removed from config", worldName)));
}
/**
@@ -202,10 +192,9 @@ public final class WorldManager {
.collect(Collectors.toMap(World::getName, Function.identity()));
serverProperties.getLevelName().peek(overworldName -> {
//TODO: check by namespaced key instead
World overworld = bukkitWorlds.remove(overworldName);
World nether = bukkitWorlds.remove(DimensionFinder.DEFAULT_NETHER_FORMAT.replaceOverworld(overworldName));
World end = bukkitWorlds.remove(DimensionFinder.DEFAULT_END_FORMAT.replaceOverworld(overworldName));
World nether = bukkitWorlds.remove(DEFAULT_NETHER_FORMAT.replaceOverworld(overworldName));
World end = bukkitWorlds.remove(DEFAULT_END_FORMAT.replaceOverworld(overworldName));
if (config.getAutoImportDefaultWorlds()) {
importExistingBukkitWorld(overworld);
@@ -252,60 +241,43 @@ public final class WorldManager {
* @return The result of the creation.
*/
public Attempt<LoadedMultiverseWorld, CreateFailureReason> createWorld(CreateWorldOptions options) {
return parseCreateWorldOptionsKeyOrName(options).mapAttempt(this::doCreateWorld);
return validateCreateWorldOptions(options).mapAttempt(this::doCreateWorld);
}
private Attempt<KeyOrNameWithOptions<CreateWorldOptions>, CreateFailureReason> parseCreateWorldOptionsKeyOrName(
private Attempt<CreateWorldOptions, CreateFailureReason> validateCreateWorldOptions(
CreateWorldOptions options) {
return options.keyOrName()
.fold(WorldKeyOrName::parse, Attempt::success)
.transform(CreateFailureReason.INVALID_WORLDNAME)
.failIf(keyOrName -> !worldNameChecker.isValidWorldName(keyOrName.usableName()),
keyOrName -> worldActionResult(CreateFailureReason.INVALID_WORLDNAME, keyOrName))
.failIf(keyOrName -> keyOrName.isKey() && !WorldCreatorCompatibility.canCreateWorldWithKey(),
keyOrName -> worldActionResult(CreateFailureReason.NAMESPACEDKEY_UNSUPPORTED, keyOrName))
.failIf(worldStore::isLoadedWorld,
keyOrName -> worldActionResult(CreateFailureReason.WORLD_EXIST_LOADED, keyOrName))
.failIf(worldStore::isUnloadedWorld,
keyOrName -> worldActionResult(CreateFailureReason.WORLD_EXIST_UNLOADED, keyOrName))
.failIf(keyOrName -> options.doFolderCheck() && worldNameChecker.hasWorldFolder(keyOrName),
keyOrName -> worldActionResult(CreateFailureReason.WORLD_EXIST_FOLDER, keyOrName))
.map(keyOrName -> new KeyOrNameWithOptions<>(keyOrName, options));
if (!worldNameChecker.isValidWorldName(options.worldName())) {
return worldActionResult(CreateFailureReason.INVALID_WORLDNAME, options.worldName());
} else if (getLoadedWorld(options.worldName()).isDefined()) {
return worldActionResult(CreateFailureReason.WORLD_EXIST_LOADED, options.worldName());
} else if (getWorld(options.worldName()).isDefined()) {
return worldActionResult(CreateFailureReason.WORLD_EXIST_UNLOADED, options.worldName());
} else if (options.doFolderCheck() && worldNameChecker.hasWorldFolder(options.worldName())) {
return worldActionResult(CreateFailureReason.WORLD_EXIST_FOLDER, options.worldName());
}
return worldActionResult(options);
}
private Attempt<LoadedMultiverseWorld, CreateFailureReason> doCreateWorld(
KeyOrNameWithOptions<CreateWorldOptions> keyOrNameWithOptions) {
WorldKeyOrName keyOrName = keyOrNameWithOptions.keyOrName();
CreateWorldOptions options = keyOrNameWithOptions.options();
String generatorString = generatorProvider.parseGeneratorString(keyOrName.usableName(), options.generator());
WorldCreator worldCreator = WorldCreatorCompatibility.ofKeyOrName(keyOrName)
CreateWorldOptions options) {
String generatorString = generatorProvider.parseGeneratorString(options.worldName(), options.generator());
WorldCreator worldCreator = WorldCreator.name(options.worldName())
.environment(options.environment())
.generateStructures(options.generateStructures())
.generatorSettings(options.generatorSettings())
.seed(options.seed())
.type(options.worldType());
WorldCreatorCompatibility.setBonusChest(worldCreator, options.bonusChest());
options.forcedSpawnPosition().peek(position -> WorldCreatorCompatibility.setForcedSpawnPosition(worldCreator, position));
return addBiomeProviderToCreator(worldCreator, keyOrName.usableName(), options.biome())
return addBiomeProviderToCreator(worldCreator, options.worldName(), options.biome())
.mapAttempt(creator -> addGeneratorToCreator(creator, generatorString))
.mapAttempt(this::createBukkitWorld)
.transform(CreateFailureReason.WORLD_CREATOR_FAILED)
.map(bukkitWorld -> newLoadedMultiverseWorld(
bukkitWorld,
generatorString,
options.biome(),
options.generatorSettings(),
options.useSpawnAdjust()))
.peek(loadedWorld -> postCreateWorld(loadedWorld, options));
}
private void postCreateWorld(LoadedMultiverseWorld loadedWorld, CreateWorldOptions options) {
options.worldPropertyStrings().forEach((key, value) -> loadedWorld.getStringPropertyHandle()
.setPropertyString(key, value)
.onFailure(failure -> Logging.warning("Failed to set property '%s' to '%s' for world %s: %s",
key, value, loadedWorld.getName(), failure.getMessage())));
pluginManager.callEvent(new MVWorldCreatedEvent(loadedWorld));
saveWorldsConfig();
bukkitWorld,
generatorString,
options.biome(),
options.useSpawnAdjust()))
.peek(loadedWorld -> pluginManager.callEvent(new MVWorldCreatedEvent(loadedWorld)));
}
/**
@@ -315,51 +287,34 @@ public final class WorldManager {
* @return The result of the import.
*/
public Attempt<LoadedMultiverseWorld, ImportFailureReason> importWorld(ImportWorldOptions options) {
return options.keyOrName()
.fold(WorldKeyOrName::parse, Attempt::success)
.transform(ImportFailureReason.INVALID_WORLDNAME)
.failIf(keyOrName -> !worldNameChecker.isValidWorldName(keyOrName.usableName()),
keyOrName -> worldActionResult(ImportFailureReason.INVALID_WORLDNAME, keyOrName))
.failIf(keyOrName -> keyOrName.isKey() && !WorldCreatorCompatibility.canCreateWorldWithKey(),
keyOrName -> worldActionResult(ImportFailureReason.NAMESPACEDKEY_UNSUPPORTED, keyOrName))
.failIf(worldStore::isLoadedWorld,
keyOrName -> worldActionResult(ImportFailureReason.WORLD_EXIST_LOADED, keyOrName))
.failIf(worldStore::isUnloadedWorld,
keyOrName -> worldActionResult(ImportFailureReason.WORLD_EXIST_UNLOADED, keyOrName))
.map(keyOrName -> new KeyOrNameWithOptions<>(keyOrName, options))
.mapAttempt(pair -> BukkitCompatibility.getWorldByNameOrKey(pair.keyOrName())
.map(bukkitWorld -> doImportBukkitWorld(pair, bukkitWorld))
.getOrElse(() -> validateImportWorldOptions(pair).mapAttempt(this::doImportWorld)));
String worldName = options.worldName();
if (isLoadedWorld(worldName)) {
return worldActionResult(ImportFailureReason.WORLD_EXIST_LOADED, worldName);
} else if (isWorld(worldName)) {
return worldActionResult(ImportFailureReason.WORLD_EXIST_UNLOADED, worldName);
}
return Option.of(Bukkit.getWorld(worldName))
.map(bukkitWorld -> doImportBukkitWorld(options, bukkitWorld))
.getOrElse(() -> validateImportWorldOptions(options).mapAttempt(this::doImportWorld));
}
private Attempt<KeyOrNameWithOptions<ImportWorldOptions>, ImportFailureReason> validateImportWorldOptions(
KeyOrNameWithOptions<ImportWorldOptions> keyOrNameWithOptions) {
WorldKeyOrName keyOrName = keyOrNameWithOptions.keyOrName();
if (keyOrNameWithOptions.options().doFolderCheck()) {
//todo This is a duplicate of folder check in load world
WorldNameChecker.FolderStatus folderStatus = worldNameChecker.checkFolder(keyOrName);
if (!folderStatus.isLoadable()) {
return worldActionResult(ImportFailureReason.WORLD_FOLDER_INVALID, keyOrName);
}
if (folderStatus == WorldNameChecker.FolderStatus.REQUIRES_MIGRATION) {
Logging.info("World '%s' will be automatically migrated by PaperMC to the new dimension " +
"location. If you face any issue with migration, please contact PaperMC support!",
keyOrName);
}
private Attempt<ImportWorldOptions, ImportFailureReason> validateImportWorldOptions(ImportWorldOptions options) {
String worldName = options.worldName();
if (!worldNameChecker.isValidWorldName(worldName)) {
return worldActionResult(ImportFailureReason.INVALID_WORLDNAME, worldName);
} else if (options.doFolderCheck() && !worldNameChecker.isValidWorldFolder(worldName)) {
return worldActionResult(ImportFailureReason.WORLD_FOLDER_INVALID, worldName);
}
return worldActionResult(keyOrNameWithOptions);
return worldActionResult(options);
}
private Attempt<LoadedMultiverseWorld, ImportFailureReason> doImportWorld(
KeyOrNameWithOptions<ImportWorldOptions> keyOrNameWithOptions) {
WorldKeyOrName keyOrName = keyOrNameWithOptions.keyOrName();
ImportWorldOptions options = keyOrNameWithOptions.options();
String generatorString = generatorProvider.parseGeneratorString(keyOrName.usableName(), options.generator());
WorldCreator worldCreator = WorldCreatorCompatibility.ofKeyOrName(keyOrName)
.environment(options.environment())
.generatorSettings(options.generatorSettings());
ImportWorldOptions options) {
String generatorString = generatorProvider.parseGeneratorString(options.worldName(), options.generator());
WorldCreator worldCreator = WorldCreator.name(options.worldName())
.environment(options.environment());
return addBiomeProviderToCreator(worldCreator, keyOrName.usableName(), options.biome())
return addBiomeProviderToCreator(worldCreator, options.worldName(), options.biome())
.mapAttempt(creator -> addGeneratorToCreator(creator, generatorString))
.mapAttempt(this::createBukkitWorld)
.transform(ImportFailureReason.WORLD_CREATOR_FAILED)
@@ -367,15 +322,11 @@ public final class WorldManager {
bukkitWorld,
generatorString,
options.biome(),
options.generatorSettings(),
options.useSpawnAdjust()))
.peek(loadedWorld -> pluginManager.callEvent(new MVWorldImportedEvent(loadedWorld)));
}
private Attempt<LoadedMultiverseWorld, ImportFailureReason> doImportBukkitWorld(
KeyOrNameWithOptions<ImportWorldOptions> keyOrNameWithOptions, World bukkitWorld) {
WorldKeyOrName keyOrName = keyOrNameWithOptions.keyOrName();
ImportWorldOptions options = keyOrNameWithOptions.options();
private Attempt<LoadedMultiverseWorld, ImportFailureReason> doImportBukkitWorld(ImportWorldOptions options, World bukkitWorld) {
if (options.environment() != bukkitWorld.getEnvironment()) {
return Attempt.failure(ImportFailureReason.BUKKIT_ENVIRONMENT_MISMATCH,
Replace.WORLD.with(bukkitWorld.getName()),
@@ -383,21 +334,18 @@ public final class WorldManager {
replace("{mvEnvironment}").with(options.environment().name()));
}
//todo: check for key mismatch?
LoadedMultiverseWorld loadedWorld = newLoadedMultiverseWorld(
bukkitWorld,
generatorProvider.parseGeneratorString(keyOrName.usableName(), options.generator()),
generatorProvider.parseGeneratorString(options.worldName(), options.generator()),
options.biome(),
options.generatorSettings(),
options.useSpawnAdjust());
pluginManager.callEvent(new MVWorldImportedEvent(loadedWorld));
return Attempt.success(loadedWorld);
}
private MultiverseWorld newMultiverseWorld(WorldConfig worldConfig) {
MultiverseWorld mvWorld = new MultiverseWorld(worldConfig, config);
worldStore.putUnloadedWorld(mvWorld);
private MultiverseWorld newMultiverseWorld(String worldName, WorldConfig worldConfig) {
MultiverseWorld mvWorld = new MultiverseWorld(worldName, worldConfig, config);
worldsMap.put(mvWorld.getName().toLowerCase(Locale.ENGLISH), mvWorld);
corePermissions.addWorldPermissions(mvWorld);
return mvWorld;
}
@@ -410,28 +358,22 @@ public final class WorldManager {
* @param adjustSpawn Whether to adjust spawn.
*/
private LoadedMultiverseWorld newLoadedMultiverseWorld(
@NotNull World world,
@Nullable String generator,
@Nullable String biome,
@Nullable String generatorSettings,
boolean adjustSpawn) {
WorldConfig worldConfig = worldsConfigManager.addWorldConfig(world.getKey());
@NotNull World world, @Nullable String generator, @Nullable String biome, boolean adjustSpawn) {
WorldConfig worldConfig = worldsConfigManager.addWorldConfig(world.getName());
// Properties from multiverse input
worldConfig.setAdjustSpawn(adjustSpawn);
worldConfig.setGenerator(generator == null ? "" : generator);
worldConfig.setBiome(biome == null ? "" : biome);
worldConfig.setGeneratorSettings(generatorSettings == null ? "" : generatorSettings);
// Properties from the bukkit world
worldConfig.setLegacyWorldName(world.getName());
worldConfig.setDifficulty(world.getDifficulty());
worldConfig.setKeepSpawnInMemory(world.getKeepSpawnInMemory());
worldConfig.setScale(WorldCompatibility.getCoordinateScale(world));
worldConfig.setScale(getCoordinateScale(world));
worldConfig.save();
newMultiverseWorld(worldConfig);
newMultiverseWorld(world.getName(), worldConfig);
LoadedMultiverseWorld loadedWorld = new LoadedMultiverseWorld(
world,
worldConfig,
@@ -440,12 +382,23 @@ public final class WorldManager {
locationManipulation,
entityPurger
);
worldStore.putLoadedWorld(loadedWorld);
loadedWorldsMap.put(loadedWorld.getName().toLowerCase(Locale.ENGLISH), loadedWorld);
saveWorldsConfig();
pluginManager.callEvent(new MVWorldLoadedEvent(loadedWorld));
return loadedWorld;
}
private double getCoordinateScale(World world) {
if (PaperLib.isPaper()) {
return world.getCoordinateScale();
}
return switch (world.getEnvironment()) {
case NORMAL -> 1;
case NETHER -> 8;
default -> 1;
};
}
/**
* Loads an existing world in config.
*
@@ -512,19 +465,11 @@ public final class WorldManager {
return doLoadBukkitWorld(bukkitWorld, mvWorld);
}
if (options.doFolderCheck()) {
WorldNameChecker.FolderStatus folderStatus = worldNameChecker.checkFolder(mvWorld.getOfflineWorldFolder());
if (!folderStatus.isLoadable()) {
return worldActionResult(LoadFailureReason.WORLD_FOLDER_INVALID, mvWorld.getName());
}
if (folderStatus == WorldNameChecker.FolderStatus.REQUIRES_MIGRATION) {
Logging.info("World '%s' will be automatically migrated by PaperMC to the new dimension " +
"location. If you face any issue with migration, please contact PaperMC support!",
mvWorld.getName());
}
if (options.doFolderCheck() && !worldNameChecker.isValidWorldFolder(mvWorld.getName())) {
return worldActionResult(LoadFailureReason.WORLD_FOLDER_INVALID, mvWorld.getName());
}
WorldCreator worldCreator = WorldCreatorCompatibility.ofNameAndKey(mvWorld.getKey(), mvWorld.getName())
WorldCreator worldCreator = WorldCreator.name(mvWorld.getName())
.environment(mvWorld.getEnvironment())
.seed(mvWorld.getSeed());
return addBiomeProviderToCreator(worldCreator, mvWorld.getName(), mvWorld.getBiome())
@@ -547,23 +492,7 @@ public final class WorldManager {
}
private Attempt<LoadedMultiverseWorld, LoadFailureReason> newLoadedMultiverseWorld(MultiverseWorld mvWorld, World bukkitWorld) {
WorldConfig worldConfig = mvWorld.getWorldConfig();
if (worldConfig.getWorldKeyOrName().isName() && mvWorld.getName().equalsIgnoreCase(bukkitWorld.getName())) {
// do migration of namespaced key
Logging.info("Migrating world config for '%s' to use namespaced key '%s'...",
mvWorld.getName(), bukkitWorld.getKey());
worldConfig = worldsConfigManager.migrateWorldConfigKey(worldConfig, bukkitWorld.getKey());
mvWorld.setWorldConfig(worldConfig);
}
if (!bukkitWorld.getKey().equals(mvWorld.getKey())) {
return Attempt.failure(LoadFailureReason.BUKKIT_NAMESPACED_KEY_MISMATCH,
Replace.WORLD.with(mvWorld.getName()),
replace("{bukkitNamespace}").with(bukkitWorld.getKey()),
replace("{mvNamespace}").with(mvWorld.getKey()));
}
WorldConfig worldConfig = worldsConfigManager.getWorldConfig(mvWorld.getName()).get(); //TODO: null check here, but logically it should never be null.
LoadedMultiverseWorld loadedWorld = new LoadedMultiverseWorld(
bukkitWorld,
worldConfig,
@@ -572,7 +501,7 @@ public final class WorldManager {
locationManipulation,
entityPurger
);
worldStore.putLoadedWorld(loadedWorld);
loadedWorldsMap.put(loadedWorld.getName().toLowerCase(Locale.ENGLISH), loadedWorld);
saveWorldsConfig();
pluginManager.callEvent(new MVWorldLoadedEvent(loadedWorld));
return Attempt.success(loadedWorld);
@@ -606,10 +535,12 @@ public final class WorldManager {
success -> removeLoadedMultiverseWorld(world));
}
private Attempt<MultiverseWorld, UnloadFailureReason> removeLoadedMultiverseWorld(@NotNull LoadedMultiverseWorld mvWorld) {
MultiverseWorld unloadedWorld = worldStore.getUnloadedWorldRef(mvWorld.getKey().toString()).getOrElseThrow(
() -> new IllegalStateException("Unloaded ref of world not found: " + mvWorld));
worldStore.removeLoadedWorld(mvWorld);
private Attempt<MultiverseWorld, UnloadFailureReason> removeLoadedMultiverseWorld(@NotNull LoadedMultiverseWorld world) {
MultiverseWorld mvWorld = Objects.requireNonNull(loadedWorldsMap.remove(world.getName()),
"For some reason, the loaded world isn't in the map... BUGGG");
Logging.fine("Removed MultiverseWorld from map: " + world.getName());
var unloadedWorld = Objects.requireNonNull(worldsMap.get(world.getName()),
"For some reason, the unloaded world isn't in the map... BUGGG");
mvWorld.getWorldConfig().setMVWorld(unloadedWorld);
pluginManager.callEvent(new MVWorldUnloadedEvent(mvWorld));
return worldActionResult(unloadedWorld);
@@ -699,15 +630,11 @@ public final class WorldManager {
*/
private Attempt<String, RemoveFailureReason> removeWorldFromConfig(@NotNull MultiverseWorld world) {
// Remove world from config
worldStore.removeWorld(world);
WorldConfig worldConfig = world.getWorldConfig();
worldConfig.deferenceMVWorld();
worldsConfigManager.deleteWorldConfig(worldConfig);
worldsMap.remove(world.getName());
world.getWorldConfig().deferenceMVWorld();
worldsConfigManager.deleteWorldConfig(world.getName());
saveWorldsConfig();
corePermissions.removeWorldPermissions(world);
pluginManager.callEvent(new MVWorldRemovedEvent(world));
return worldActionResult(world.getName());
}
@@ -764,7 +691,6 @@ public final class WorldManager {
private Attempt<File, DeleteFailureReason> validateWorldToDelete(
@NotNull LoadedMultiverseWorld world) {
return world.getBukkitWorld().map(World::getWorldFolder)
.peek(folder -> Logging.finer("World folder for world %s is at: %s", world.getName(), folder.getPath()))
.filter(worldNameChecker::isValidWorldFolder)
.map(this::<File, DeleteFailureReason>worldActionResult)
.getOrElse(() -> {
@@ -780,91 +706,91 @@ public final class WorldManager {
* @return The result of the clone.
*/
public Attempt<LoadedMultiverseWorld, CloneFailureReason> cloneWorld(@NotNull CloneWorldOptions options) {
return parseCloneWorldOptionsNewWorld(options)
return cloneWorldValidateWorld(options)
.mapAttempt(this::cloneWorldCopyFolder)
.mapAttempt(keyOrNameWithOptions -> {
.mapAttempt(validatedOptions -> {
ImportWorldOptions importWorldOptions = ImportWorldOptions
.worldKeyOrName(keyOrNameWithOptions.keyOrName())
.biome(options.fromWorld().getBiome())
.environment(options.fromWorld().getEnvironment())
.generator(options.fromWorld().getGenerator());
.worldName(validatedOptions.newWorldName())
.biome(validatedOptions.world().getBiome())
.environment(validatedOptions.world().getEnvironment())
.generator(validatedOptions.world().getGenerator());
return importWorld(importWorldOptions).transform(CloneFailureReason.IMPORT_FAILED);
})
.onSuccess(newWorld -> {
cloneWorldTransferData(options, newWorld);
if (options.keepWorldConfig()) {
// Special case for spawn location as it's considered a hidden config
newWorld.setSpawnLocation(options.fromWorld().getSpawnLocation());
newWorld.setSpawnLocation(options.world().getSpawnLocation());
}
saveWorldsConfig();
pluginManager.callEvent(new MVWorldClonedEvent(newWorld, options.fromWorld()));
pluginManager.callEvent(new MVWorldClonedEvent(newWorld, options.world()));
});
}
private Attempt<KeyOrNameWithOptions<CloneWorldOptions>, CloneFailureReason> parseCloneWorldOptionsNewWorld(
private Attempt<CloneWorldOptions, CloneFailureReason> cloneWorldValidateWorld(
@NotNull CloneWorldOptions options) {
return options.newWorldKeyOrName()
.fold(WorldKeyOrName::parse, Attempt::success)
.transform(CloneFailureReason.INVALID_WORLDNAME)
.failIf(keyOrName -> !worldNameChecker.isValidWorldName(keyOrName.usableName()),
keyOrName -> worldActionResult(CloneFailureReason.INVALID_WORLDNAME, keyOrName))
.failIf(keyOrName -> keyOrName.isKey() && !WorldCreatorCompatibility.canCreateWorldWithKey(),
keyOrName -> worldActionResult(CloneFailureReason.NAMESPACEDKEY_UNSUPPORTED, keyOrName))
.failIf(worldStore::isLoadedWorld,
keyOrName -> worldActionResult(CloneFailureReason.WORLD_EXIST_LOADED, keyOrName))
.failIf(worldStore::isWorld,
keyOrName -> worldActionResult(CloneFailureReason.WORLD_EXIST_UNLOADED, keyOrName))
.failIf(worldNameChecker::hasWorldFolder,
keyOrName -> worldActionResult(CloneFailureReason.WORLD_EXIST_FOLDER, keyOrName))
.failIf(keyOrName -> !worldNameChecker.isValidWorldFolder(options.fromWorld().getOfflineWorldFolder()),
keyOrName -> worldActionResult(CloneFailureReason.FROM_WORLD_FOLDER_INVALID,
options.fromWorld().getName()))
.map(keyOrName -> new KeyOrNameWithOptions<>(keyOrName, options));
String newWorldName = options.newWorldName();
if (!worldNameChecker.isValidWorldName(newWorldName)) {
Logging.severe("Invalid world name: " + newWorldName);
return worldActionResult(CloneFailureReason.INVALID_WORLDNAME, newWorldName);
}
if (isLoadedWorld(newWorldName)) {
Logging.severe("World already loaded when attempting to clone: " + newWorldName);
return worldActionResult(CloneFailureReason.WORLD_EXIST_LOADED, newWorldName);
}
if (isWorld(newWorldName)) {
Logging.severe("World already exist unloaded: " + newWorldName);
return worldActionResult(CloneFailureReason.WORLD_EXIST_UNLOADED, newWorldName);
}
if (worldNameChecker.hasWorldFolder(newWorldName)) {
return worldActionResult(CloneFailureReason.WORLD_EXIST_FOLDER, newWorldName);
}
return worldActionResult(options);
}
private Attempt<KeyOrNameWithOptions<CloneWorldOptions>, CloneFailureReason> cloneWorldCopyFolder(
@NotNull KeyOrNameWithOptions<CloneWorldOptions> keyOrNameWithOptions) {
WorldKeyOrName newWorldkeyOrName = keyOrNameWithOptions.keyOrName();
CloneWorldOptions options = keyOrNameWithOptions.options();
private Attempt<CloneWorldOptions, CloneFailureReason> cloneWorldCopyFolder(@NotNull CloneWorldOptions options) {
if (options.saveBukkitWorld()) {
options.fromWorld().asLoadedWorld().peek(loadedWorld -> {
Logging.finer("Saving world before cloning: " + loadedWorld.getName());
loadedWorld.getBukkitWorld().peek(bukkitWorld -> WorldCompatibility.saveWithFlush(bukkitWorld, true));
});
Logging.finer("Saving bukkit world before cloning: " + options.world().getName());
options.world().getBukkitWorld().peek(this::saveWorldWithFlush);
}
File worldFolder = WorldFolderResolver.resolve(options.fromWorld());
File newWorldFolder = WorldFolderResolver.resolve(newWorldkeyOrName);
File worldFolder = options.world().getBukkitWorld().map(World::getWorldFolder).get();
File newWorldFolder = new File(Bukkit.getWorldContainer(), options.newWorldName());
return fileUtils.copyFolder(worldFolder, newWorldFolder, CLONE_IGNORE_FILES).fold(
exception -> worldActionResult(CloneFailureReason.COPY_FAILED,
options.fromWorld().getName(), exception),
success -> worldActionResult(keyOrNameWithOptions));
options.world().getName(), exception),
success -> worldActionResult(options));
}
// This method is only available since 1.21
private final Method saveWithFlush = ReflectHelper.getMethod(World.class, "save", boolean.class);
private void saveWorldWithFlush(World world) {
if (saveWithFlush != null) {
Logging.fine("Using world save method with flush...");
ReflectHelper.invokeMethod(world, saveWithFlush, true);
} else {
world.save();
}
}
private void cloneWorldTransferData(@NotNull CloneWorldOptions options, @NotNull LoadedMultiverseWorld newWorld) {
DataTransfer<LoadedMultiverseWorld> dataTransfer = transferData(options, options.world());
dataTransfer.pasteAllTo(newWorld);
}
private DataTransfer<LoadedMultiverseWorld> transferData(
@NotNull KeepWorldSettingsOptions options, @NotNull LoadedMultiverseWorld world) {
DataTransfer<LoadedMultiverseWorld> dataTransfer = new DataTransfer<>();
if (options.keepWorldConfig()) {
new DataTransfer<MultiverseWorld>()
.addDataStore(new WorldConfigStore<>(), options.fromWorld())
.pasteAllTo(newWorld);
dataTransfer.addDataStore(new WorldConfigStore(), world);
}
if (options.keepGameRule()) {
dataTransfer.addDataStore(new GameRulesStore(), world);
}
if (options.keepWorldBorder()) {
dataTransfer.addDataStore(new WorldBorderStore(), world);
}
newWorld.getBukkitWorld().peek(bukkitWorld -> {
if (!options.keepWorldBorder()) {
WorldBorder worldBorder = bukkitWorld.getWorldBorder();
worldBorder.reset();
}
if (!options.keepGameRule()) {
Arrays.stream(bukkitWorld.getGameRules())
.map(gameRuleName -> GameRule.getByName(gameRuleName))
.filter(Objects::nonNull)
.forEach(gameRule -> {
Object gameRuleDefault = bukkitWorld.getGameRuleDefault(gameRule);
if (gameRuleDefault != null) {
bukkitWorld.setGameRule(gameRule, gameRuleDefault);
}
});
}
});
return dataTransfer;
}
/**
@@ -875,16 +801,14 @@ public final class WorldManager {
*/
public Attempt<LoadedMultiverseWorld, RegenFailureReason> regenWorld(@NotNull RegenWorldOptions options) {
LoadedMultiverseWorld world = options.world();
DataTransfer<LoadedMultiverseWorld> dataTransfer = regenWorldTransferData(options, world);
DataTransfer<LoadedMultiverseWorld> dataTransfer = transferData(options, world);
boolean shouldKeepSpawnLocation = options.keepWorldConfig() && options.seed() == world.getSeed();
Location spawnLocation = world.getSpawnLocation();
CreateWorldOptions createWorldOptions = CreateWorldOptions.worldKeyOrName(world.worldConfig.getWorldKeyOrName())
.biome(world.getBiome())
.bonusChest(world.getBukkitWorld().map(WorldCompatibility::hasBonusChest).getOrElse(false))
CreateWorldOptions createWorldOptions = CreateWorldOptions.worldName(world.getName())
.environment(world.getEnvironment())
.generateStructures(world.canGenerateStructures().getOrElse(true))
.generator(world.getGenerator())
.generatorSettings(world.getGeneratorSettings())
.seed(options.seed())
.useSpawnAdjust(!shouldKeepSpawnLocation && world.getAdjustSpawn())
.worldType(world.getWorldType().getOrElse(WorldType.NORMAL))
@@ -905,35 +829,18 @@ public final class WorldManager {
});
}
private DataTransfer<LoadedMultiverseWorld> regenWorldTransferData(
@NotNull KeepWorldSettingsOptions options, @NotNull LoadedMultiverseWorld world) {
DataTransfer<LoadedMultiverseWorld> dataTransfer = new DataTransfer<>();
if (options.keepWorldConfig()) {
dataTransfer.addDataStore(new WorldConfigStore<>(), world);
}
if (options.keepGameRule()) {
dataTransfer.addDataStore(new GameRulesStore(), world);
}
if (options.keepWorldBorder()) {
dataTransfer.addDataStore(new WorldBorderStore(), world);
}
return dataTransfer;
}
private <T, F extends FailureReason> Attempt<T, F> worldActionResult(@NotNull T value) {
return Attempt.success(value);
}
private <T, F extends FailureReason> Attempt.Failure<T, F> worldActionResult(
@NotNull F failureReason, @NotNull Object worldName) {
return Attempt.failureRef(failureReason, Replace.WORLD.with(worldName));
private <T, F extends FailureReason> Attempt<T, F> worldActionResult(
@NotNull F failureReason, @NotNull String worldName) {
return Attempt.failure(failureReason, Replace.WORLD.with(worldName));
}
private <T, F extends FailureReason> Attempt.Failure<T, F> worldActionResult(
private <T, F extends FailureReason> Attempt<T, F> worldActionResult(
@NotNull F failureReason, @NotNull String worldName, @NotNull Throwable error) {
return Attempt.failureRef(failureReason, Replace.WORLD.with(worldName), Replace.ERROR.with(error));
return Attempt.failure(failureReason, Replace.WORLD.with(worldName), Replace.ERROR.with(error));
}
private Attempt<WorldCreator, WorldCreatorFailureReason> addBiomeProviderToCreator(
@@ -984,9 +891,7 @@ public final class WorldManager {
}).onFailure(exception -> {
Logging.severe("Failed to create bukkit world: " + worldCreator.name());
exception.printStackTrace();
}).andFinally(() -> {
this.loadTracker.remove(worldCreator.name());
}).fold(throwable -> Attempt.failure(WorldCreatorFailureReason.BUKKIT_CREATION_FAILED,
}).andFinally(() -> this.loadTracker.remove(worldCreator.name())).fold(throwable -> Attempt.failure(WorldCreatorFailureReason.BUKKIT_CREATION_FAILED,
Replace.WORLD.with(worldCreator.name()),
Replace.ERROR.with(throwable)),
Attempt::success);
@@ -1029,13 +934,17 @@ public final class WorldManager {
* Checks based on folder contents and name.
*
* @return A list of all potential worlds.
*
* @deprecated Use {@link PotentialWorldFinder#findPotentialWorlds()} instead.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public List<String> getPotentialWorlds() {
return potentialWorldFinder.get().findPotentialWorlds();
File[] files = Bukkit.getWorldContainer().listFiles();
if (files == null) {
return Collections.emptyList();
}
return Arrays.stream(files)
.filter(file -> !isWorld(file.getName()))
.filter(worldNameChecker::isValidWorldFolder)
.map(File::getName)
.toList();
}
/**
@@ -1057,7 +966,9 @@ public final class WorldManager {
* @return The world if it exists.
*/
public Option<MultiverseWorld> getWorld(@Nullable String worldName) {
return worldStore.getWorld(worldName);
return getLoadedWorld(worldName)
.map(world -> (MultiverseWorld) world)
.orElse(() -> getUnloadedWorld(worldName));
}
/**
@@ -1068,7 +979,9 @@ public final class WorldManager {
* @return The world if it exists.
*/
public Option<MultiverseWorld> getWorldByNameOrAlias(@Nullable String worldNameOrAlias) {
return getWorld(worldStore.translateAlias(worldNameOrAlias));
return getLoadedWorldByNameOrAlias(worldNameOrAlias)
.map(world -> (MultiverseWorld) world)
.orElse(() -> getUnloadedWorldByNameOrAlias(worldNameOrAlias));
}
/**
@@ -1078,15 +991,14 @@ public final class WorldManager {
* <p>If you want only unloaded worlds, use {@link #getUnloadedWorlds()}. If you want only loaded worlds, use
* {@link #getLoadedWorlds()}.</p>
*
* <p>Note that this is an unmodifiable copy of the current worlds. It will not update as worlds are added/removed.
* Call it everytime you need the most updated list of worlds.</p>
*
* @return A list of all worlds that may or may not be loaded.
*/
@Unmodifiable
@NotNull
public Collection<MultiverseWorld> getWorlds() {
return worldStore.getWorlds();
return worldsMap.values().stream()
.map(world -> getLoadedWorld(world)
.map(loadedWorld -> (MultiverseWorld) loadedWorld)
.getOrElse(world))
.toList();
}
/**
@@ -1096,7 +1008,7 @@ public final class WorldManager {
* @return True if the world is a world is known to multiverse, but may or may not be loaded.
*/
public boolean isWorld(@Nullable String worldName) {
return worldName != null && worldStore.getWorld(worldName).isDefined();
return worldName != null && worldsMap.containsKey(worldName.toLowerCase(Locale.ENGLISH));
}
/**
@@ -1106,7 +1018,9 @@ public final class WorldManager {
* @return The world if it exists.
*/
public Option<MultiverseWorld> getUnloadedWorld(@Nullable String worldName) {
return worldStore.getUnloadedWorld(worldName);
return isLoadedWorld(worldName)
? Option.none()
: Option.of(worldName).flatMap(name -> Option.of(worldsMap.get(name.toLowerCase(Locale.ENGLISH))));
}
/**
@@ -1116,7 +1030,18 @@ public final class WorldManager {
* @return The world if it exists.
*/
public Option<MultiverseWorld> getUnloadedWorldByNameOrAlias(@Nullable String worldNameOrAlias) {
return getUnloadedWorld(worldStore.translateAlias(worldNameOrAlias));
return getUnloadedWorld(worldNameOrAlias).orElse(() -> getUnloadedWorldByAlias(worldNameOrAlias));
}
private Option<MultiverseWorld> getUnloadedWorldByAlias(@Nullable String alias) {
if (alias == null || alias.isEmpty()) {
return Option.none();
}
String colourlessAlias = ChatTextFormatter.removeColor(alias);
return Option.ofOptional(worldsMap.values().stream()
.filter(world -> !world.isLoaded())
.filter(world -> world.getColourlessAlias().equalsIgnoreCase(colourlessAlias))
.findFirst());
}
/**
@@ -1124,10 +1049,10 @@ public final class WorldManager {
*
* @return A list of all worlds that are not loaded.
*/
@Unmodifiable
@NotNull
public Collection<MultiverseWorld> getUnloadedWorlds() {
return worldStore.getUnloadedWorlds();
return worldsMap.values().stream()
.filter(world -> !world.isLoaded())
.toList();
}
/**
@@ -1167,7 +1092,8 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable String worldName) {
return Option.of(worldName).flatMap(worldStore::getLoadedWorld);
return Option.of(worldName)
.flatMap(name -> Option.of(loadedWorldsMap.get(name.toLowerCase(Locale.ENGLISH))));
}
/**
@@ -1177,21 +1103,28 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorldByNameOrAlias(@Nullable String worldNameOrAlias) {
return getLoadedWorld(worldStore.translateAlias(worldNameOrAlias));
return getLoadedWorld(worldNameOrAlias)
.orElse(() -> getLoadedWorldByAlias(worldNameOrAlias));
}
private Option<LoadedMultiverseWorld> getLoadedWorldByAlias(@Nullable String alias) {
if (alias == null || alias.isEmpty()) {
return Option.none();
}
return Option.ofOptional(loadedWorldsMap.values().stream()
.filter(world -> world.getColourlessAlias()
.equalsIgnoreCase(ChatTextFormatter.removeColor(alias)))
.findFirst());
}
/**
* Get a read-only list of all multiverse worlds that are loaded.
*
* <p>Note that this is an unmodifiable copy of the current worlds. It will not update as worlds are added/removed.
* Call it everytime you need the most updated list of worlds.</p>
*
* @return A list of all multiverse worlds that are loaded.
*/
@Unmodifiable
@NotNull
public Collection<LoadedMultiverseWorld> getLoadedWorlds() {
return worldStore.getLoadedWorlds();
return loadedWorldsMap.values().stream()
.toList();
}
/**
@@ -1221,7 +1154,7 @@ public final class WorldManager {
* @return True if the world is a multiverse world that is loaded.
*/
public boolean isLoadedWorld(@Nullable String worldName) {
return worldName != null && worldStore.getLoadedWorld(worldName).isDefined();
return worldName != null && loadedWorldsMap.containsKey(worldName.toLowerCase(Locale.ENGLISH));
}
/**
@@ -1249,18 +1182,4 @@ public final class WorldManager {
failure.printStackTrace();
});
}
WorldStore getWorldStore() {
return worldStore;
}
/**
* A simple pair wrapper for convenience to pass both the world key or name and the options together between methods
* when parsing world options.
*
* @param keyOrName The world key or name.
* @param options The world options.
* @param <T> The world options type.
*/
private record KeyOrNameWithOptions<T>(WorldKeyOrName keyOrName, T options) { }
}
@@ -1,270 +0,0 @@
package org.mvplugins.multiverse.core.world;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.bukkit.NamespacedKey;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.utils.CaseInsensitiveStringMap;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@Service
final class WorldStore {
/**
* Only loaded worlds, i.e. will be empty if all worlds are unloaded.
*/
private final List<LoadedMultiverseWorld> loadedList;
/**
* Only unloaded worlds, i.e. will be empty if all worlds are loaded.
*/
private final List<MultiverseWorld> unloadedList;
/**
* Contains single reference to all worlds, either loaded or unloaded ref depending on the world's current status.
*/
private final List<MultiverseWorld> worldList;
/**
* Maps all key and key to loaded ref.
*/
private final Map<String, LoadedMultiverseWorld> loadedMap;
/**
* Maps all key and key to unloaded ref.
*/
private final Map<String, MultiverseWorld> unloadedMap;
/**
* Maps colorless alias to world key. As alias may not be unique, this is a multimap.
*/
private final Multimap<String, String> aliasMap;
@Inject
private WorldStore() {
this.loadedList = new ArrayList<>();
this.unloadedList = new ArrayList<>();
this.worldList = new ArrayList<>();
this.loadedMap = new CaseInsensitiveStringMap<>();
this.unloadedMap = new CaseInsensitiveStringMap<>();
this.aliasMap = HashMultimap.create();
}
void putUnloadedWorld(MultiverseWorld world) {
if (world instanceof LoadedMultiverseWorld) {
throw new IllegalArgumentException("Loaded world cannot be put in unloaded map");
}
unloadedMap.put(world.getKey().toString(), world);
unloadedMap.put(world.getName(), world);
unloadedList.add(world);
worldList.add(world);
}
void putLoadedWorld(LoadedMultiverseWorld world) {
loadedMap.put(world.getKey().toString(), world);
loadedMap.put(world.getName(), world);
unloadedList.remove(unloadedMap.get(world.getKey().toString()));
worldList.remove(unloadedMap.get(world.getKey().toString()));
loadedList.add(world);
worldList.add(world);
}
void removeWorld(MultiverseWorld world) {
LoadedMultiverseWorld loadedRef = loadedMap.get(world.getKey().toString());
MultiverseWorld unloadedRef = unloadedMap.get(world.getKey().toString());
// remove from list
unloadedList.remove(unloadedRef);
loadedList.remove(loadedRef);
worldList.remove(unloadedRef);
worldList.remove(loadedRef);
// remove from maps
loadedMap.remove(world.getKey().toString());
unloadedMap.remove(world.getName());
unloadedMap.remove(world.getKey().toString());
unloadedMap.remove(world.getName());
// remove alias
aliasMap.remove(world.getColourlessAlias(), world.getKey().toString());
}
void removeLoadedWorld(LoadedMultiverseWorld world) {
// remove from list
loadedList.remove(world);
worldList.remove(world);
// remove from maps
loadedMap.remove(world.getKey().toString());
loadedMap.remove(world.getName());
// Add back unloaded
unloadedList.add(unloadedMap.get(world.getKey().toString()));
}
void changeAlias(@Nullable String oldAlias, @Nullable String newAlias, @NotNull MultiverseWorld world) {
if (Objects.equals(oldAlias, newAlias)) {
// nothing changed, ignore
return;
}
if (!Strings.isNullOrEmpty(oldAlias)) {
aliasMap.remove(oldAlias, world.getKey().toString());
}
if (!Strings.isNullOrEmpty(newAlias)) {
aliasMap.put(newAlias, world.getKey().toString());
}
}
@Unmodifiable
@NotNull
List<MultiverseWorld> getWorlds() {
return List.copyOf(worldList);
}
@Unmodifiable
@NotNull
List<LoadedMultiverseWorld> getLoadedWorlds() {
return List.copyOf(loadedList);
}
@Unmodifiable
@NotNull
List<MultiverseWorld> getUnloadedWorlds() {
return List.copyOf(unloadedList);
}
@NotNull
Option<MultiverseWorld> getWorld(@Nullable WorldKeyOrName keyOrName) {
return keyOrName == null
? Option.none()
: getWorld(keyOrName.usableKey()).orElse(() -> getWorld(keyOrName.usableName()));
}
@NotNull
Option<MultiverseWorld> getWorld(@Nullable NamespacedKey worldKey) {
return Option.of(worldKey).map(NamespacedKey::toString).flatMap(this::getWorld);
}
@NotNull
Option<MultiverseWorld> getWorld(@Nullable String worldKeyString) {
return Option.of((MultiverseWorld) loadedMap.get(worldKeyString))
.orElse(() -> Option.of(unloadedMap.get(worldKeyString)));
}
boolean isWorld(@Nullable WorldKeyOrName keyOrName) {
return keyOrName != null && (isWorld(keyOrName.usableKey()) || isWorld(keyOrName.usableName()));
}
boolean isWorld(@Nullable NamespacedKey worldKey) {
return getWorld(worldKey).isDefined();
}
boolean isWorld(@Nullable String worldKeyString) {
return getWorld(worldKeyString).isDefined();
}
@NotNull
Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable WorldKeyOrName keyOrName) {
return keyOrName == null
? Option.none()
: getLoadedWorld(keyOrName.usableKey()).orElse(() -> getLoadedWorld(keyOrName.usableName()));
}
@NotNull
Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable NamespacedKey worldKey) {
return Option.of(worldKey).map(NamespacedKey::toString).flatMap(this::getLoadedWorld);
}
@NotNull
Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable String worldKeyString) {
return Option.of(loadedMap.get(worldKeyString))
.filter(MultiverseWorld::isLoaded);
}
boolean isLoadedWorld(@Nullable WorldKeyOrName keyOrName) {
return keyOrName != null && (isLoadedWorld(keyOrName.usableKey()) || isLoadedWorld(keyOrName.usableName()));
}
boolean isLoadedWorld(@Nullable NamespacedKey worldKey) {
return getLoadedWorld(worldKey).isDefined();
}
boolean isLoadedWorld(@Nullable String worldKeyString) {
return getLoadedWorld(worldKeyString).isDefined();
}
@NotNull
Option<MultiverseWorld> getUnloadedWorld(@Nullable WorldKeyOrName keyOrName) {
return keyOrName == null
? Option.none()
: getUnloadedWorld(keyOrName.usableKey()).orElse(() -> getUnloadedWorld(keyOrName.usableName()));
}
@NotNull
Option<MultiverseWorld> getUnloadedWorld(@Nullable NamespacedKey worldKey) {
return Option.of(worldKey).map(NamespacedKey::toString).flatMap(this::getUnloadedWorld);
}
@NotNull
Option<MultiverseWorld> getUnloadedWorld(@Nullable String worldKeyString) {
return Option.of(unloadedMap.get(worldKeyString))
.filter(world -> !world.isLoaded());
}
boolean isUnloadedWorld(@Nullable WorldKeyOrName keyOrName) {
return keyOrName != null && (isUnloadedWorld(keyOrName.usableKey()) || isUnloadedWorld(keyOrName.usableName()));
}
boolean isUnloadedWorld(@Nullable NamespacedKey worldKey) {
return getUnloadedWorld(worldKey).isDefined();
}
boolean isUnloadedWorld(@Nullable String worldKeyString) {
return getUnloadedWorld(worldKeyString).isDefined();
}
@NotNull
Option<MultiverseWorld> getUnloadedWorldRef(@Nullable WorldKeyOrName keyOrName) {
return keyOrName == null
? Option.none()
: getUnloadedWorldRef(keyOrName.usableKey()).orElse(() -> getUnloadedWorldRef(keyOrName.usableName()));
}
@NotNull
Option<MultiverseWorld> getUnloadedWorldRef(@Nullable NamespacedKey worldKey) {
return Option.of(worldKey).map(NamespacedKey::toString).flatMap(this::getUnloadedWorldRef);
}
@NotNull
Option<MultiverseWorld> getUnloadedWorldRef(@Nullable String worldKeyString) {
return Option.of(unloadedMap.get(worldKeyString));
}
@Nullable
String translateAlias(@Nullable String worldNameOrAlias) {
if (Strings.isNullOrEmpty(worldNameOrAlias)) {
return null;
}
//TODO: Not sure if we should fail if there is multiple alias of the same name, but for now just return the first one
return aliasMap.get(worldNameOrAlias).stream()
.findFirst()
.orElse(worldNameOrAlias);
}
}
@@ -10,7 +10,6 @@ import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import org.bukkit.NamespacedKey;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
@@ -18,9 +17,6 @@ import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import org.mvplugins.multiverse.core.world.key.WorldKeyParseFailReason;
import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
@@ -32,7 +28,7 @@ import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
final class WorldsConfigManager {
private static final String CONFIG_FILENAME = "worlds.yml";
private final SortedMap<WorldKeyOrName, WorldConfig> worldConfigMap;
private final SortedMap<String, WorldConfig> worldConfigMap;
private final File worldConfigFile;
private YamlConfiguration worldsConfig;
@@ -51,7 +47,7 @@ final class WorldsConfigManager {
*
* @return A {@link NewAndRemovedWorlds} record.
*/
Try<NewAndRemovedWorlds> load() {
public Try<NewAndRemovedWorlds> load() {
return Try.of(() -> {
loadWorldYmlFile();
return parseNewAndRemovedWorlds();
@@ -117,7 +113,7 @@ final class WorldsConfigManager {
config.set("worlds", null);
for (Map.Entry<String, ConfigurationSection> entry : worldConfigMap.entrySet()) {
config.set(encodeConfigKey(entry.getKey()), entry.getValue());
config.set(encodeWorldName(entry.getKey()), entry.getValue());
}
config.save(worldConfigFile);
})
@@ -166,39 +162,33 @@ final class WorldsConfigManager {
* @return A tuple containing a list of the new WorldConfigs added and a list of the worlds removed from the config.
*/
private NewAndRemovedWorlds parseNewAndRemovedWorlds() {
List<WorldKeyOrName> allWorldsInConfig = worldsConfig.getKeys(false)
List<String> allWorldsInConfig = worldsConfig.getKeys(false)
.stream()
.map(keyStr -> decodeConfigKey(keyStr)
.onFailure(reason ->
Logging.warning("Failed to parse world key '%s' in config: %s", keyStr, reason))
.getOrNull())
.filter(Objects::nonNull)
.map(this::decodeWorldName)
.toList();
List<WorldConfig> newWorldsAdded = new ArrayList<>();
for (WorldKeyOrName worldKeyStr : allWorldsInConfig) {
getWorldConfig(worldKeyStr)
.peek(config -> config.load(getWorldConfigSection(worldKeyStr))
.onFailure(e -> Logging.warning("Failed to load world config for world '%s': %s",
worldKeyStr, e.getMessage())))
for (String worldName : allWorldsInConfig) {
getWorldConfig(worldName)
.peek(config -> config.load(getWorldConfigSection(worldName)))
.orElse(() -> {
WorldConfig newWorldConfig = new WorldConfig(
worldKeyStr,
getWorldConfigSection(worldKeyStr),
worldName,
getWorldConfigSection(worldName),
multiverseCore);
worldConfigMap.put(worldKeyStr, newWorldConfig);
worldConfigMap.put(worldName, newWorldConfig);
newWorldsAdded.add(newWorldConfig);
return Option.of(newWorldConfig);
})
.peek(WorldConfig::save);
}
List<WorldKeyOrName> worldsRemoved = worldConfigMap.keySet().stream()
List<String> worldsRemoved = worldConfigMap.keySet().stream()
.filter(worldName -> !allWorldsInConfig.contains(worldName))
.toList();
for (WorldKeyOrName s : worldsRemoved) {
for (String s : worldsRemoved) {
worldConfigMap.remove(s);
}
@@ -210,7 +200,7 @@ final class WorldsConfigManager {
*
* @return Whether the worlds.yml file has been loaded.
*/
boolean isLoaded() {
public boolean isLoaded() {
return worldsConfig != null;
}
@@ -219,17 +209,17 @@ final class WorldsConfigManager {
*
* @return Whether the save was successful or the error that occurred.
*/
Try<Void> save() {
public Try<Void> save() {
return Try.run(() -> {
if (!isLoaded()) {
throw new IllegalStateException("WorldsConfigManager is not loaded!");
}
worldsConfig = new YamlConfiguration();
worldConfigMap.forEach((keyOrName, worldConfig) -> {
worldConfigMap.forEach((worldName, worldConfig) -> {
worldConfig.save().onFailure(e -> {
throw new RuntimeException("Failed to save world %s in config: " + keyOrName, e);
throw new RuntimeException("Failed to save world config: " + worldName, e);
});
worldsConfig.set(encodeConfigKey(keyOrName), worldConfig.getConfigurationSection());
worldsConfig.set(encodeWorldName(worldName), worldConfig.getConfigurationSection());
});
worldsConfig.save(worldConfigFile);
}).onFailure(e -> {
@@ -238,81 +228,63 @@ final class WorldsConfigManager {
}
/**
* Gets the {@link WorldConfig} instance of a world in the worlds.yml file.
* Gets the {@link WorldConfig} instance of all worlds in the worlds.yml file.
*
* @param keyOrName The target key to get
* @param worldName The name of the world to check.
* @return The {@link WorldConfig} instance of the world, or empty option if it doesn't exist.
*/
@NotNull Option<WorldConfig> getWorldConfig(@NotNull WorldKeyOrName keyOrName) {
return Option.of(worldConfigMap.get(keyOrName));
}
@NotNull WorldConfig migrateWorldConfigKey(@NotNull WorldConfig worldConfig, @NotNull NamespacedKey toKey) {
worldConfig.save();
WorldKeyOrName newKeyOrName = WorldKeyOrName.parseKey(toKey);
WorldConfig migratedWorldConfig = new WorldConfig(
newKeyOrName,
worldConfig.getConfigurationSection(),
multiverseCore
);
deleteWorldConfig(worldConfig);
worldConfigMap.put(newKeyOrName, migratedWorldConfig);
return migratedWorldConfig;
public @NotNull Option<WorldConfig> getWorldConfig(@NotNull String worldName) {
return Option.of(worldConfigMap.get(worldName));
}
/**
* Add a new world to the worlds.yml file. If a world with the given name already exists, an exception is thrown.
*
* @param namespacedKey The target key to add
* @param worldName The name of the world to add.
* @return The newly created {@link WorldConfig} instance.
*/
@NotNull WorldConfig addWorldConfig(@NotNull NamespacedKey namespacedKey) {
WorldKeyOrName keyOrName = WorldKeyOrName.parseKey(namespacedKey);
if (worldConfigMap.containsKey(keyOrName)) {
throw new IllegalStateException("WorldConfig for world " + namespacedKey + " already exists.");
public @NotNull WorldConfig addWorldConfig(@NotNull String worldName) {
if (worldConfigMap.containsKey(worldName)) {
throw new IllegalArgumentException("WorldConfig for world " + worldName + " already exists.");
}
WorldConfig worldConfig = new WorldConfig(keyOrName, getWorldConfigSection(keyOrName), multiverseCore);
worldConfigMap.put(keyOrName, worldConfig);
WorldConfig worldConfig = new WorldConfig(worldName, getWorldConfigSection(worldName), multiverseCore);
worldConfigMap.put(worldName, worldConfig);
return worldConfig;
}
/**
* Deletes the world config for the given world.
*
* @param worldConfig The target world config to delete
* @param worldName The name of the world to delete.
*/
void deleteWorldConfig(@NotNull WorldConfig worldConfig) {
worldConfigMap.remove(worldConfig.getWorldKeyOrName());
worldsConfig.set(encodeConfigKey(worldConfig.getWorldKeyOrName()), null);
public void deleteWorldConfig(@NotNull String worldName) {
worldConfigMap.remove(worldName);
worldsConfig.set(encodeWorldName(worldName), null);
}
/**
* Gets the {@link ConfigurationSection} for the given world in the worlds.yml file. If the section doesn't exist,
* it is created.
*
* @param keyOrName the config key of the world to get the configuration section for.
* @param worldName The name of the world.
* @return The {@link ConfigurationSection} for the given world.
*/
@NotNull
private ConfigurationSection getWorldConfigSection(@NotNull WorldKeyOrName keyOrName) {
String encodeWorldKey = encodeConfigKey(keyOrName);
ConfigurationSection section = worldsConfig.getConfigurationSection(encodeWorldKey);
return section == null ? worldsConfig.createSection(encodeWorldKey) : section;
}
private String encodeConfigKey(@NotNull WorldKeyOrName worldKeyOrName) {
return encodeConfigKey(worldKeyOrName.serialise());
private ConfigurationSection getWorldConfigSection(String worldName) {
worldName = encodeWorldName(worldName);
return worldsConfig.isConfigurationSection(worldName)
? worldsConfig.getConfigurationSection(worldName)
: worldsConfig.createSection(worldName);
}
/**
* Remove dot . with [dot] as it is a special character in YAML that causes sub-path issues.
* Dot is a special character in YAML that causes sub-path issues.
*/
private String encodeConfigKey(@NotNull String worldName) {
private String encodeWorldName(String worldName) {
return worldName.replace(".", "[dot]");
}
private Attempt<WorldKeyOrName, WorldKeyParseFailReason> decodeConfigKey(@NotNull String worldKeyStr) {
return WorldKeyOrName.parse(worldKeyStr.replace("[dot]", "."));
private String decodeWorldName(String worldName) {
return worldName.replace("[dot]", ".");
}
private static final class ConfigMigratedException extends RuntimeException {
@@ -1,36 +1,40 @@
package org.mvplugins.multiverse.core.world.biomeprovider;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.block.Biome;
import org.bukkit.generator.BiomeProvider;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
/**
* A parser for {@link SingleBiomeProvider}
*/
final class SingleBiomeProviderParser implements BiomeProviderParser {
private List<String> biomes;
private List<String> biomes;
@Override
public BiomeProvider parseBiomeProvider(@NotNull String worldName, @NotNull String params) {
NamespacedKey biomeKey = NamespacedKey.fromString(params.toLowerCase(Locale.ENGLISH));
return new SingleBiomeProvider(Registry.BIOME.get(biomeKey));
}
@Override
public BiomeProvider parseBiomeProvider(@NotNull String worldName, @NotNull String params) {
final NamespacedKey biomeKey = NamespacedKey.fromString(StringUtils.lowerCase(params, Locale.ENGLISH));
return new SingleBiomeProvider(Registry.BIOME.get(biomeKey));
}
@Override
public Collection<String> suggestParams(@NotNull String currentInput) {
if (biomes == null) {
biomes = Registry.BIOME.stream()
.map(biome -> biome.getKey().getKey().toLowerCase(Locale.ENGLISH))
.toList();
}
return biomes;
}
}
@Override
public Collection<String> suggestParams(@NotNull String currentInput) {
if (biomes == null) {
final List<String> result = new ArrayList<>();
for (Biome biome : Registry.BIOME) {
result.add(StringUtils.lowerCase(biome.getKey().getKey(), Locale.ENGLISH));
}
biomes = result;
}
return biomes;
}
}
@@ -1,7 +1,6 @@
package org.mvplugins.multiverse.core.world.entity;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.SpawnCategory;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
@@ -41,24 +40,25 @@ public final class EntityPurger {
public int purgeEntities(LoadedMultiverseWorld world, SpawnCategory... spawnCategories) {
Set<SpawnCategory> spawnCategoriesSet = Set.of(spawnCategories);
AtomicInteger purgeCount = new AtomicInteger(0);
world.getBukkitWorld().peek(bukkitWorld -> bukkitWorld.getEntities().stream()
.filter(entity -> !(entity instanceof Player))
.filter(entity -> spawnCategoriesSet.contains(entity.getSpawnCategory()))
.forEach(entity -> {
world.getBukkitWorld().peek(bukkitWorld -> {
for (Entity entity : bukkitWorld.getEntities()) {
if (spawnCategoriesSet.contains(entity.getSpawnCategory())) {
entity.remove();
purgeCount.incrementAndGet();
}));
}
}
});
return purgeCount.get();
}
public int purgeAllEntities(LoadedMultiverseWorld world) {
AtomicInteger purgeCount = new AtomicInteger(0);
world.getBukkitWorld().peek(bukkitWorld -> bukkitWorld.getEntities().stream()
.filter(entity -> !(entity instanceof Player))
.forEach(entity -> {
entity.remove();
purgeCount.incrementAndGet();
}));
world.getBukkitWorld().peek(bukkitWorld -> {
for (Entity entity : bukkitWorld.getEntities()) {
entity.remove();
purgeCount.incrementAndGet();
}
});
return purgeCount.get();
}
}
@@ -4,7 +4,6 @@ import com.dumptruckman.minecraft.util.Logging;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.MemoryConfiguration;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.SpawnCategory;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.config.CoreConfig;
@@ -34,11 +33,6 @@ public final class EntitySpawnConfig {
));
}
@ApiStatus.AvailableSince("5.7")
public boolean shouldAllowSpawn(EntityType entityType) {
return getSpawnCategoryConfig(SpawnCategoryMapper.getSpawnCategory(entityType)).shouldAllowSpawn(entityType);
}
public boolean shouldAllowSpawn(Entity entity) {
return getSpawnCategoryConfig(entity.getSpawnCategory()).shouldAllowSpawn(entity);
}
@@ -4,8 +4,10 @@ import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.SpawnCategory;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
@@ -19,27 +21,26 @@ import java.util.Map;
*/
final class SpawnCategoryMapper {
private static final Map<EntityType, SpawnCategory> entityTypeToSpawnCategoryMap;
private static final Map<SpawnCategory, List<EntityType>> spawnCategoryMap;
static {
entityTypeToSpawnCategoryMap = new HashMap<>();
spawnCategoryMap = new HashMap<>();
buildSpawnCategoryMap();
}
private static Map<SpawnCategory, List<EntityType>> spawnCategoryMap;
private static void buildSpawnCategoryMap() {
Class<?> entityTypeClass = ReflectHelper.tryGetClass("net.minecraft.world.entity.EntityType").getOrNull();
spawnCategoryMap = new HashMap<>();
Class<?> entityTypeClass = ReflectHelper.getClass("net.minecraft.world.entity.EntityType");
if (entityTypeClass == null) {
Logging.warning("Failed to find EntityType class. SpawnCategoryMapper will not work.");
return;
}
Method getCategoryMethod = ReflectHelper.tryGetMethod(entityTypeClass, "getCategory").getOrNull();
Method getCategoryMethod = ReflectHelper.getMethod(entityTypeClass, "getCategory");
if (getCategoryMethod == null) {
Logging.warning("Failed to find getCategory method. SpawnCategoryMapper will not work.");
return;
}
Class<?> craftSpawnCategoryClass = ReflectHelper.tryGetClass("org.bukkit.craftbukkit.util.CraftSpawnCategory").getOrNull();
Class<?> craftSpawnCategoryClass = ReflectHelper.getClass("org.bukkit.craftbukkit.util.CraftSpawnCategory");
if (craftSpawnCategoryClass == null) {
Logging.warning("Failed to find CraftSpawnCategory class. SpawnCategoryMapper will not work.");
return;
@@ -52,26 +53,25 @@ final class SpawnCategoryMapper {
Logging.warning("Failed to find toBukkit method. SpawnCategoryMapper will not work.");
return;
}
Arrays.stream(entityTypeClass.getFields()).forEach(entityTypeField -> Try.of(() -> EntityType.valueOf(entityTypeField.getName()))
.peek(entityType -> ReflectHelper.tryGetStaticFieldValue(entityTypeField, entityTypeClass)
.flatMap(nmsEntityType -> ReflectHelper.tryInvokeMethod(nmsEntityType, getCategoryMethod))
.flatMap(nsmMobCategory -> ReflectHelper.tryInvokeStaticMethod(toBukkitMethod, nsmMobCategory))
.filter(bukkitSpawnCategory -> bukkitSpawnCategory instanceof SpawnCategory)
.map(bukkitSpawnCategory -> (SpawnCategory) bukkitSpawnCategory)
.peek(bukkitSpawnCategory -> entityTypeToSpawnCategoryMap.put(entityType, bukkitSpawnCategory))
.peek(bukkitSpawnCategory -> spawnCategoryMap
.computeIfAbsent(bukkitSpawnCategory, ignore -> new ArrayList<>())
.add(entityType))));
}
/**
* Gets the spawn category that the given entity type is in.
*
* @param entityType The entity type
* @return The spawn category that the given entity type is in, or null if it cannot be determined
*/
static SpawnCategory getSpawnCategory(EntityType entityType) {
return entityTypeToSpawnCategoryMap.get(entityType);
Field[] entityTypeFields = entityTypeClass.getFields();
for (Field entityTypeField : entityTypeFields) {
String entityName = entityTypeField.getName();
EntityType entityType = Try.of(() -> EntityType.valueOf(entityName)).getOrNull();
if (entityType == null) {
continue;
}
Object nmsEntityType = ReflectHelper.getFieldValue(null, entityTypeField, entityTypeClass);
Object nsmMobCategory = ReflectHelper.invokeMethod(nmsEntityType, getCategoryMethod);
if (nsmMobCategory == null) {
continue;
}
Object bukkitSpawnCategory = ReflectHelper.invokeMethod(null, toBukkitMethod, nsmMobCategory);
if (!(bukkitSpawnCategory instanceof SpawnCategory spawnCategory)) {
continue;
}
spawnCategoryMap.computeIfAbsent(spawnCategory, ignore -> new ArrayList<>())
.add(entityType);
}
}
/**
@@ -9,9 +9,7 @@ import org.bukkit.GameRule;
import org.bukkit.World;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle;
import org.mvplugins.multiverse.core.utils.compatibility.WorldBorderCompatibility;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
/**
* A data store for storing and restoring data from an object.
@@ -92,14 +90,14 @@ public interface DataStore<T> {
/**
* A {@link DataStore} for storing and restoring world properties for a multiverse world.
*/
class WorldConfigStore<T extends MultiverseWorld> implements DataStore<T> {
class WorldConfigStore implements DataStore<LoadedMultiverseWorld> {
private Map<String, Object> configMap;
/**
* {@inheritDoc}
*/
@Override
public WorldConfigStore<T> copyFrom(T world) {
public WorldConfigStore copyFrom(LoadedMultiverseWorld world) {
this.configMap = new HashMap<>();
StringPropertyHandle worldPropertyHandler = world.getStringPropertyHandle();
worldPropertyHandler.getAllPropertyNames().forEach(name -> worldPropertyHandler.getProperty(name)
@@ -113,7 +111,7 @@ public interface DataStore<T> {
* {@inheritDoc}
*/
@Override
public WorldConfigStore<T> pasteTo(T world) {
public WorldConfigStore pasteTo(LoadedMultiverseWorld world) {
if (configMap == null) {
return this;
}
@@ -141,16 +139,14 @@ public interface DataStore<T> {
*/
@Override
public WorldBorderStore copyFrom(LoadedMultiverseWorld world) {
world.getBukkitWorld()
.map(World::getWorldBorder)
.peek(worldBorder -> {
borderCenterX = worldBorder.getCenter().getX();
borderCenterZ = worldBorder.getCenter().getZ();
borderDamageAmount = worldBorder.getDamageAmount();
borderDamageBuffer = worldBorder.getDamageBuffer();
borderSize = worldBorder.getSize();
borderTimeRemaining = WorldBorderCompatibility.getWarningTimeTicks(worldBorder);
});
world.getBukkitWorld().peek(bukkitWorld -> {
borderCenterX = bukkitWorld.getWorldBorder().getCenter().getX();
borderCenterZ = bukkitWorld.getWorldBorder().getCenter().getZ();
borderDamageAmount = bukkitWorld.getWorldBorder().getDamageAmount();
borderDamageBuffer = bukkitWorld.getWorldBorder().getDamageBuffer();
borderSize = bukkitWorld.getWorldBorder().getSize();
borderTimeRemaining = bukkitWorld.getWorldBorder().getWarningTime();
});
return this;
}
@@ -159,15 +155,13 @@ public interface DataStore<T> {
*/
@Override
public WorldBorderStore pasteTo(LoadedMultiverseWorld world) {
world.getBukkitWorld()
.map(World::getWorldBorder)
.peek(worldBorder -> {
worldBorder.setCenter(borderCenterX, borderCenterZ);
worldBorder.setDamageAmount(borderDamageAmount);
worldBorder.setDamageBuffer(borderDamageBuffer);
worldBorder.setSize(borderSize);
WorldBorderCompatibility.setWarningTimeTicks(worldBorder, borderTimeRemaining);
});
world.getBukkitWorld().peek(bukkitWorld -> {
bukkitWorld.getWorldBorder().setCenter(borderCenterX, borderCenterZ);
bukkitWorld.getWorldBorder().setDamageAmount(borderDamageAmount);
bukkitWorld.getWorldBorder().setDamageBuffer(borderDamageBuffer);
bukkitWorld.getWorldBorder().setSize(borderSize);
bukkitWorld.getWorldBorder().setWarningTime(borderTimeRemaining);
});
return this;
}
}
@@ -4,7 +4,6 @@ import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.bukkit.World.Environment;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.config.CoreConfig;
@@ -20,24 +19,6 @@ import java.util.regex.Pattern;
@Service
public final class DimensionFinder {
/**
* Default world for nether worlds, The default is "%overworld_nether%", which means that the nether world of
* "world" will be named "world_nether".
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static final DimensionFormat DEFAULT_NETHER_FORMAT = new DimensionFormat("%overworld%_nether");
/**
* Default world name format for end worlds. The default is "%overworld%_the_end", which means that the end world
* of "world" will be named "world_the_end".
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static final DimensionFormat DEFAULT_END_FORMAT = new DimensionFormat("%overworld%_the_end");
private final CoreConfig config;
private final WorldManager worldManager;
@@ -7,11 +7,9 @@ import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter;
import org.mvplugins.multiverse.core.teleportation.TeleportFailureReason;
import org.mvplugins.multiverse.core.utils.result.AsyncAttemptsAggregate;
@@ -86,24 +84,6 @@ public final class PlayerWorldTeleporter {
.getOrElse(AsyncAttemptsAggregate::emptySuccess);
}
/**
* Transfers all players from the given world to the given destination.
*
* @param world The world to transfer players from.
* @param destinationInstance The destination instance to transfer players to.
* @return A list of async futures that represent the teleportation result of each player.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public AsyncAttemptsAggregate<Void, TeleportFailureReason> transferAllFromWorldToDestination(
@NotNull LoadedMultiverseWorld world,
@NotNull DestinationInstance<?, ?> destinationInstance) {
return world.getPlayers()
.map(players -> safetyTeleporter.to(destinationInstance).teleport(players))
.getOrElse(AsyncAttemptsAggregate::emptySuccess);
}
/**
* Teleports all players to the given world's spawn location.
*
@@ -1,81 +0,0 @@
package org.mvplugins.multiverse.core.world.helpers;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.utils.compatibility.BukkitCompatibility;
import org.mvplugins.multiverse.core.world.WorldManager;
import java.io.File;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
/**
* Tool that traversals the server folders to find all potential worlds that can be imported, based on folder contents
* and name.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Service
public final class PotentialWorldFinder {
private final WorldManager worldManager;
private final WorldNameChecker worldNameChecker;
@Inject
private PotentialWorldFinder(@NotNull WorldManager worldManager, @NotNull WorldNameChecker worldNameChecker) {
this.worldManager = worldManager;
this.worldNameChecker = worldNameChecker;
}
/**
* Gets a list of all potential worlds that can be loaded from the server folders.
* Checks based on folder contents and name.
*
* @return A list of all potential worlds.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@Unmodifiable
@NotNull
public List<String> findPotentialWorlds() {
return BukkitCompatibility.isUsingNewDimensionStorage()
? Stream.concat(findFromDimensionsFolder(), findFromRootFolder()).toList()
: findFromRootFolder().toList();
}
private Stream<String> findFromRootFolder() {
Path worldContainer = Bukkit.getWorldContainer().toPath();
return Arrays.stream(listFolders(worldContainer))
.filter(worldNameChecker::isValidWorldFolder)
.map(File::getName)
.filter(worldNameChecker::isValidWorldName)
.filter(worldName -> !worldManager.isWorld(worldName));
}
private Stream<String> findFromDimensionsFolder() {
Path worldContainer = BukkitCompatibility.getWorldFoldersDirectory();
return Arrays.stream(listFolders(worldContainer))
.map(File::getName)
.flatMap(namespace -> Arrays.stream(listFolders(worldContainer.resolve(namespace)))
.filter(worldNameChecker::isValidWorldFolder)
.map(File::getName)
.map(key -> namespace + ":" + key))
.filter(namespacedKey -> NamespacedKey.fromString(namespacedKey) != null)
.filter(namespacedKey -> !worldManager.isWorld(namespacedKey));
}
private @NotNull File[] listFolders(Path folder) {
return Option.of(folder.toFile().listFiles(File::isDirectory))
.getOrElse(() -> new File[0]);
}
}
@@ -1,108 +0,0 @@
package org.mvplugins.multiverse.core.world.helpers;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.utils.compatibility.BukkitCompatibility;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import java.io.File;
/**
* <p>Utility class for resolving the file system location of a world folder.</p>
*
* <p>This class handles the differences in world folder locations between server versions,
* particularly the new dimension storage system introduced in PaperMC 26.1 which stores worlds
* in a dimensions folder under the world level directory.</p>
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public final class WorldFolderResolver {
/**
* Resolves the world folder file path for the given world key or name.
* <br />
* This method automatically selects the appropriate resolution method based on whether the server
* is using the new dimension storage system introduced in PaperMC 26.1.
*
* @param keyOrName The world key or name to resolve.
* @return The resolved world folder file.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static File resolve(@NotNull WorldKeyOrName keyOrName) {
return BukkitCompatibility.isUsingNewDimensionStorage()
? resolveAsDimensionKey(keyOrName.usableKey())
: resolveAsLegacyWorldName(keyOrName.usableName());
}
/**
* Resolves the world folder file path for the given Multiverse world.
* <br />
* This method automatically selects the appropriate resolution method based on whether the server
* is using the new dimension storage system introduced in PaperMC 26.1.
*
* @param multiverseWorld The Multiverse world to resolve.
* @return The resolved world folder file.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static File resolve(@NotNull MultiverseWorld multiverseWorld) {
File dimensionKey = resolveAsDimensionKey(multiverseWorld.getKey());
File legacy = resolveAsLegacyWorldName(multiverseWorld.getName());
if (BukkitCompatibility.isUsingNewDimensionStorage()) {
return !dimensionKey.isDirectory() && legacy.isDirectory() ? legacy : dimensionKey;
}
return legacy;
}
/**
* Gets the world folder using the new dimension storage path format of {@code [namespace]/[key]}.
* <br />
* This method is only valid for PaperMC 26.1 and above. For earlier server versions,
* use {@link #resolveAsLegacyWorldName(String)} instead.
*
* @param namespacedKey The namespaced key of the world.
* @return The resolved world folder file using the new dimension storage format.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static File resolveAsDimensionKey(@NotNull NamespacedKey namespacedKey) {
return BukkitCompatibility.getWorldFoldersDirectory()
.resolve(namespacedKey.getNamespace())
.resolve(namespacedKey.getKey())
.toFile();
}
/**
* Gets the world folder using the legacy world name format from the server root directory.
* <br />
* This method is the default for server versions before PaperMC 26.1.
*
* @param worldName The name of the world.
* @return The resolved world folder file using the legacy world name format.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public static File resolveAsLegacyWorldName(@NotNull String worldName) {
return Bukkit.getWorldContainer()
.toPath()
.resolve(worldName)
.toFile();
}
private WorldFolderResolver() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}
@@ -1,18 +1,15 @@
package org.mvplugins.multiverse.core.world.helpers;
import java.io.File;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import io.vavr.control.Option;
import org.jetbrains.annotations.ApiStatus;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.utils.REPatterns;
import org.mvplugins.multiverse.core.utils.compatibility.BukkitCompatibility;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
/**
* <p>Utility class in helping to check the status of a world name and it's associated world folder.</p>
@@ -32,18 +29,6 @@ public final class WorldNameChecker {
"plugins",
"versions");
private static final List<WorldFolderSchema> WORLD_FOLDER_SCHEMA = List.of(
// OLD
WorldFolderSchema.file("level.dat"),
WorldFolderSchema.folder("DIM1"),
WorldFolderSchema.folder("DIM-1"),
// NEW
WorldFolderSchema.file("paper-world.yml"),
WorldFolderSchema.folder("data"),
WorldFolderSchema.folder("entities"),
WorldFolderSchema.folder("poi"),
WorldFolderSchema.folder("region"));
/**
* Checks if a world name is valid.
*
@@ -68,7 +53,7 @@ public final class WorldNameChecker {
if (name.isEmpty()) {
return NameStatus.EMPTY;
}
if (!BukkitCompatibility.isUsingNewDimensionStorage() && BLACKLIST_NAMES.contains(name)) {
if (BLACKLIST_NAMES.contains(name)) {
return NameStatus.BLACKLISTED;
}
if (!REPatterns.NAMESPACE_KEY.matcher(name).matches()) {
@@ -84,54 +69,19 @@ public final class WorldNameChecker {
*
* @param worldName The world name to check on.
* @return True if the folder exists, else false.
*
* @deprecated Use {@link #hasWorldFolder(WorldKeyOrName)} instead, which is more robust and supports namespaced keys.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public boolean hasWorldFolder(@Nullable String worldName) {
return checkFolder(worldName) != FolderStatus.DOES_NOT_EXIST;
}
/**
* Check if a world name has a world folder directory. It may not contain valid world data.
*
* @param worldKey The world key to check on.
* @return True if the folder exists, else false.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public boolean hasWorldFolder(@Nullable WorldKeyOrName worldKey) {
return checkFolder(worldKey) != FolderStatus.DOES_NOT_EXIST;
}
/**
* Checks if a world name has a valid world folder with basic world data.
*
* @param worldName The world name to check on.
* @return True if check result is valid, else false.
*
* @deprecated Use {@link #isValidWorldFolder(WorldKeyOrName)} instead, which is more robust and supports
* namespaced keys.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public boolean isValidWorldFolder(@Nullable String worldName) {
return checkFolder(worldName).loadable;
}
/**
* Checks if a world name has a valid world folder with basic world data.
*
* @param nameOrKey The world key to check on.
* @return True if check result is valid, else false.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public boolean isValidWorldFolder(@Nullable WorldKeyOrName nameOrKey) {
return checkFolder(nameOrKey).loadable;
return checkFolder(worldName) == FolderStatus.VALID;
}
/**
@@ -141,7 +91,7 @@ public final class WorldNameChecker {
* @return True if check result is valid, else false.
*/
public boolean isValidWorldFolder(@Nullable File worldFolder) {
return checkFolder(worldFolder).loadable;
return checkFolder(worldFolder) == FolderStatus.VALID;
}
/**
@@ -149,45 +99,14 @@ public final class WorldNameChecker {
*
* @param worldName The world name to check on.
* @return The resulting folder status.
*
* @deprecated Use {@link #checkFolder(WorldKeyOrName)} instead, which is more robust and supports namespaced keys.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@NotNull
public FolderStatus checkFolder(@Nullable String worldName) {
if (worldName == null) {
return FolderStatus.DOES_NOT_EXIST;
}
return WorldKeyOrName.parse(worldName)
.map(this::checkFolder)
.getOrElse(FolderStatus.NOT_A_WORLD);
}
/**
* Checks the current folder status for a world key or name.
*
* @param keyOrName The world key or name to check on.
* @return The resulting folder status.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
@NotNull
public FolderStatus checkFolder(@Nullable WorldKeyOrName keyOrName) {
if (keyOrName == null) {
return FolderStatus.DOES_NOT_EXIST;
}
if (BukkitCompatibility.isUsingNewDimensionStorage()) {
File oldWorldFolder = WorldFolderResolver.resolveAsLegacyWorldName(keyOrName.usableName());
if (checkFolder(oldWorldFolder) == FolderStatus.VALID) {
return FolderStatus.REQUIRES_MIGRATION;
}
}
return checkFolder(WorldFolderResolver.resolve(keyOrName));
File worldFolder = new File(Bukkit.getWorldContainer(), worldName);
return checkFolder(worldFolder);
}
/**
@@ -201,7 +120,7 @@ public final class WorldNameChecker {
if (worldFolder == null || !worldFolder.exists() || !worldFolder.isDirectory()) {
return FolderStatus.DOES_NOT_EXIST;
}
if (!folderWorldSchemaCheck(worldFolder)) {
if (!folderHasDat(worldFolder)) {
return FolderStatus.NOT_A_WORLD;
}
return FolderStatus.VALID;
@@ -214,54 +133,9 @@ public final class WorldNameChecker {
* @param worldFolder The File that may be a world.
* @return True if it looks like a world, else false.
*/
private boolean folderWorldSchemaCheck(@NotNull File worldFolder) {
return WORLD_FOLDER_SCHEMA.stream()
.filter(schema -> schema.check(worldFolder))
.count() >= 2;
}
/**
* Helper class to check if a file or folder exist.
*/
private interface WorldFolderSchema {
static WorldFolderSchema file(String path) {
return new WorldFile(path);
}
static WorldFolderSchema folder(String path) {
return new WorldFolder(path);
}
boolean check(File worldFolder);
final class WorldFile implements WorldFolderSchema {
private final String path;
private WorldFile(String path) {
this.path = path;
}
@Override
public boolean check(File worldFolder) {
File thisFolder = worldFolder.toPath().resolve(path).toFile();
return thisFolder.exists() && thisFolder.isFile();
}
}
final class WorldFolder implements WorldFolderSchema {
private final String path;
private WorldFolder(String path) {
this.path = path;
}
@Override
public boolean check(File worldFolder) {
File thisFolder = worldFolder.toPath().resolve(path).toFile();
return thisFolder.exists() && thisFolder.isDirectory();
}
}
private boolean folderHasDat(@NotNull File worldFolder) {
File[] files = worldFolder.listFiles((file, name) -> name.toLowerCase(Locale.ENGLISH).endsWith(".dat"));
return files != null && files.length > 0;
}
/**
@@ -296,39 +170,16 @@ public final class WorldNameChecker {
/**
* Folder is valid.
*/
VALID(true),
/**
* This folder will cause PaperMC to migrate to new dimension world folder in 26.1+
*/
REQUIRES_MIGRATION(true),
VALID,
/**
* Folder exist, but contents in it doesnt look like a world.
*/
NOT_A_WORLD(false),
NOT_A_WORLD,
/**
* Folder does not exist.
*/
DOES_NOT_EXIST(false),
;
private final boolean loadable;
FolderStatus(boolean loadable) {
this.loadable = loadable;
}
/**
* Whether this folder status is loadable, meaning it has the basic world data and can be loaded as a world.
* Note that this does not guarantee the server will definitely load the world with no errors.
*
* @return True if folder probably is loadable by the server, else false.
*/
@ApiStatus.AvailableSince("5.6")
public boolean isLoadable() {
return loadable;
}
DOES_NOT_EXIST
}
}
@@ -1,394 +0,0 @@
package org.mvplugins.multiverse.core.world.key;
import io.vavr.control.Either;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.NamespacedKey;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.locale.message.LocalizableMessage;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement;
import org.mvplugins.multiverse.core.utils.ServerProperties;
import org.mvplugins.multiverse.core.utils.compatibility.UnsafeValuesCompatibility;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder;
import java.util.Locale;
import java.util.Objects;
/**
* Represents either a world key or a world name, with methods to parse from strings and retrieve the key or name.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public sealed abstract class WorldKeyOrName implements Comparable<WorldKeyOrName>, LocalizableMessage permits WorldKeyOrName.Key, WorldKeyOrName.Name {
private static final String DEFAULT_OVERWORLD_KEY = "overworld";
private static final String DEFAULT_NETHER_KEY = "the_nether";
private static final String DEFAULT_END_KEY = "the_end";
/**
* Parse a string into a {@link WorldKeyOrName} instance.
* <p>
* The provided input will be interpreted as a namespaced key if it contains a ':' character,
* otherwise it will be treated as a world name.
*
* @param nameOrKey The input string to parse, may be a world name or a namespaced key string
* @return An {@link Attempt} containing either a parsed {@link WorldKeyOrName} on success or a
* {@link WorldKeyParseFailReason} describing the failure
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static Attempt<WorldKeyOrName, WorldKeyParseFailReason> parse(@Nullable String nameOrKey) {
if (nameOrKey == null || nameOrKey.isEmpty()) {
return Attempt.failure(WorldKeyParseFailReason.EMPTY);
}
return nameOrKey.contains(":") ? parseKey(nameOrKey) : parseName(nameOrKey);
}
/**
* Parse a world name into a {@link WorldKeyOrName} instance.
* <p>
* This will attempt to create a usable {@link NamespacedKey} from the given name using
* {@link NamespacedKey#minecraft(String)}. If parsing fails, a failure {@link Attempt} will be
* returned describing an invalid world name.
*
* @param name The world name to parse
* @return An {@link Attempt} containing a {@link WorldKeyOrName.Name} on success or a
* {@link WorldKeyParseFailReason#INVALID_WORLD_NAME} failure on error
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static Attempt<WorldKeyOrName, WorldKeyParseFailReason> parseName(@NotNull String name) {
return Try.of(() -> NamespacedKey.minecraft(mapWorldNameToMinecraftKey(name)))
.map(usableKey -> Attempt.<WorldKeyOrName, WorldKeyParseFailReason>success(new Name(name, usableKey)))
.recover(throwable -> Attempt.failure(WorldKeyParseFailReason.INVALID_WORLD_NAME,
MessageReplacement.Replace.WORLD.with(name)))
.getOrElse(() -> Attempt.failure(WorldKeyParseFailReason.INVALID_WORLD_NAME,
MessageReplacement.Replace.WORLD.with(name)));
}
private static String mapWorldNameToMinecraftKey(@NotNull String nameOrKey) {
String defaultLevelName = getMostAccurateLevelName();
String lowerCaseName = nameOrKey.toLowerCase(Locale.ROOT);
if (defaultLevelName.equalsIgnoreCase(lowerCaseName)) {
lowerCaseName = DEFAULT_OVERWORLD_KEY;
} else if (DimensionFinder.DEFAULT_NETHER_FORMAT.replaceOverworld(defaultLevelName).equalsIgnoreCase(lowerCaseName)) {
lowerCaseName = DEFAULT_NETHER_KEY;
} else if (DimensionFinder.DEFAULT_END_FORMAT.replaceOverworld(defaultLevelName).equalsIgnoreCase(lowerCaseName)) {
lowerCaseName = DEFAULT_END_KEY;
}
return lowerCaseName;
}
/**
* Parse a namespaced key string into a {@link WorldKeyOrName} instance.
*
* @param nameOrKey The namespaced key string to parse (e.g. "minecraft:world")
* @return An {@link Attempt} containing a {@link WorldKeyOrName.Key} on success or a
* {@link WorldKeyParseFailReason#INVALID_NAMESPACED_KEY} failure on error
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static Attempt<WorldKeyOrName, WorldKeyParseFailReason> parseKey(@NotNull String nameOrKey) {
return Option.of(NamespacedKey.fromString(nameOrKey))
.filter(Objects::nonNull)
.map(key -> Attempt.<WorldKeyOrName, WorldKeyParseFailReason>success(new Key(key, usableNameFromKey(key))))
.getOrElse(() -> Attempt.failure(WorldKeyParseFailReason.INVALID_NAMESPACED_KEY,
MessageReplacement.Replace.NAMESPACE.with(nameOrKey)));
}
/**
* Create a {@link WorldKeyOrName} representing a namespaced key.
*
* @param key The {@link NamespacedKey} to wrap
* @return A {@link WorldKeyOrName} containing the provided key
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static WorldKeyOrName parseKey(@NotNull NamespacedKey key) {
return new Key(key, usableNameFromKey(key));
}
private static String usableNameFromKey(@NotNull NamespacedKey key) {
return key.getNamespace().equals(NamespacedKey.MINECRAFT)
? mapMinecraftKeyToWorldName(key)
: mapCustomKeyToWorldName(key);
}
private static String mapMinecraftKeyToWorldName(@NotNull NamespacedKey key) {
String defaultLevelName = getMostAccurateLevelName();
return switch (key.getKey()) {
case DEFAULT_OVERWORLD_KEY -> defaultLevelName;
case DEFAULT_NETHER_KEY -> DimensionFinder.DEFAULT_NETHER_FORMAT.replaceOverworld(defaultLevelName);
case DEFAULT_END_KEY -> DimensionFinder.DEFAULT_END_FORMAT.replaceOverworld(defaultLevelName);
default -> key.getKey();
};
}
private static String mapCustomKeyToWorldName(@NotNull NamespacedKey key) {
return key.getNamespace() + "_" + key.getKey();
}
private static String getMostAccurateLevelName() {
return UnsafeValuesCompatibility.getMainLevelName()
.orElse(ServerProperties::getStaticLevelName)
.getOrElse("world"); // Worse case assume its world
}
/**
* Returns true if this instance represents a world name (not a namespaced key).
*
* @return true if this is a name variant
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public abstract boolean isName();
/**
* Returns true if this instance represents a namespaced key (not a world name).
*
* @return true if this is a key variant
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public abstract boolean isKey();
/**
* Get the underlying {@link NamespacedKey} if this instance is a key variant.
*
* @return An {@link Option} containing the key when present, otherwise {@link Option#none()}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public abstract @NotNull Option<NamespacedKey> getKey();
/**
* Get the underlying world name if this instance is a name variant.
*
* @return An {@link Option} containing the name when present, otherwise {@link Option#none()}
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public abstract @NotNull Option<String> getName();
/**
* Represent this value as an {@link Either} where the left side is the {@link NamespacedKey}
* and the right side is the world name.
*
* @return An {@link Either} containing either the key (left) or the name (right)
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public abstract @NotNull Either<NamespacedKey, String> asEither();
/**
* Get a usable {@link NamespacedKey} for this instance.
* <p>
* If this instance is a key variant, the underlying key is returned. If it is a name variant,
* an implementation-provided usable key (derived from the name) is returned.
*
* @return A non-null {@link NamespacedKey} that can be used where a key is required
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public abstract @NotNull NamespacedKey usableKey();
/**
* Get a usable name for this instance.
* <p>
* If this instance is a name variant, the underlying name is returned. If it is a key variant,
* a reasonable string representation derived from the key is returned.
*
* @return A non-null {@link String} representing a usable name
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public abstract @NotNull String usableName();
/**
* Serialize this instance to a string form suitable for storage or comparison.
*
* @return The serialized representation of this value
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public abstract @NotNull String serialise();
/**
* Sorts the instances based on their serialized form, which ensures that keys and names are sorted in an
* alphabetical manner by default.
*
* @param o the object to be compared.
* @return a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.
*/
@Override
public int compareTo(WorldKeyOrName o) {
return serialise().compareTo(o.serialise());
}
/**
* {@inheritDoc}
*/
@Override
public @Nullable Message getLocalizableMessage() {
return Message.of(serialise());
}
public static final class Key extends WorldKeyOrName {
private final NamespacedKey key;
private final String usableName;
private Key(@NotNull NamespacedKey key, @NotNull String usableName) {
this.key = key;
this.usableName = usableName;
}
@Override
public boolean isName() {
return false;
}
@Override
public boolean isKey() {
return true;
}
@Override
public @NotNull Option<NamespacedKey> getKey() {
return Option.of(key);
}
@Override
public @NotNull Option<String> getName() {
return Option.none();
}
@Override
public @NotNull Either<NamespacedKey, String> asEither() {
return Either.left(key);
}
@Override
public @NotNull NamespacedKey usableKey() {
return key;
}
@Override
public @NotNull String usableName() {
return usableName;
}
@Override
public @NotNull String serialise() {
return key.toString();
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
Key key1 = (Key) o;
return Objects.equals(key, key1.key);
}
@Override
public int hashCode() {
return Objects.hashCode(key);
}
@Override
public String toString() {
return "Key{" +
"key=" + key +
", usableName='" + usableName + '\'' +
'}';
}
}
public static final class Name extends WorldKeyOrName {
private final String name;
private final NamespacedKey usableKey;
private Name(String name, NamespacedKey usableKey) {
this.name = name;
this.usableKey = usableKey;
}
@Override
public boolean isName() {
return true;
}
@Override
public boolean isKey() {
return false;
}
@Override
public @NotNull Option<NamespacedKey> getKey() {
return Option.none();
}
@Override
public @NotNull Option<String> getName() {
return Option.of(name);
}
@Override
public @NotNull Either<NamespacedKey, String> asEither() {
return Either.right(name);
}
@Override
public @NotNull NamespacedKey usableKey() {
return usableKey;
}
@Override
public @NotNull String usableName() {
return name;
}
@Override
public @NotNull String serialise() {
return name;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
Name name1 = (Name) o;
return Objects.equals(name, name1.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
@Override
public String toString() {
return "Name{" +
"name='" + name + '\'' +
", usableKey=" + usableKey +
'}';
}
}
}
@@ -1,64 +0,0 @@
package org.mvplugins.multiverse.core.world.key;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.jetbrains.annotations.ApiStatus;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
/**
* Reasons why parsing a world key or name failed.
* <br />
* These values are returned by parsing utilities such as {@link org.mvplugins.multiverse.core.world.key.WorldKeyOrName#parse}
* to indicate the specific cause of failure so callers can present an appropriate localized message.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public enum WorldKeyParseFailReason implements FailureReason {
/**
* The provided input was null or empty.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
EMPTY(MVCorei18n.WORLDKEYPARSE_EMPTY),
/**
* The provided world name contains invalid characters or format.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
INVALID_WORLD_NAME(MVCorei18n.WORLDKEYPARSE_INVALIDWORLDNAME),
/**
* The provided string was intended to be a namespaced key but did not parse as a valid
* {@link org.bukkit.NamespacedKey} (malformed namespace/key or missing parts).
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
INVALID_NAMESPACED_KEY(MVCorei18n.WORLDKEYPARSE_INVALIDNAMESPACEDKEY),
;
private final MessageKeyProvider message;
WorldKeyParseFailReason(MessageKeyProvider message) {
this.message = message;
}
/**
* Return the localized message key associated with this failure reason.
* <p>
* Implementations of {@link FailureReason} can use this to provide a user-facing localized
* message for the failure.
*
* @return The {@link MessageKey} that corresponds to this failure reason
* @since 5.7
*/
@Override
public MessageKey getMessageKey() {
return message.getMessageKey();
}
}
@@ -1,125 +1,44 @@
package org.mvplugins.multiverse.core.world.options;
import io.vavr.control.Either;
import org.bukkit.NamespacedKey;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
/**
* Options for customizing the cloning of a world.
*/
public final class CloneWorldOptions implements KeepWorldSettingsOptions {
/**
* Creates a new {@link CloneWorldOptions} instance with the given loaded world.
*
* @param fromWorld The loaded world to clone.
* @param newWorldName The name of the new world.
* @return A new {@link CloneWorldOptions} instance.
*
* @deprecated Cloning can be done from unloaded worlds as well. Use {@link #fromTo(MultiverseWorld, String)} instead.
* To ensure you use the non-deprecated method, you need to downcast the loaded world to a {@link MultiverseWorld}
* before passing it in, for example: {@code CloneWorldOptions.fromTo((MultiverseWorld) loadedWorld, newWorldName)}
*/
@Deprecated(forRemoval = true, since = "5.6")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public static @NotNull CloneWorldOptions fromTo(@NotNull LoadedMultiverseWorld fromWorld, @NotNull String newWorldName) {
return fromTo((MultiverseWorld) fromWorld, newWorldName);
}
/**
* Creates a new {@link CloneWorldOptions} instance with the given world.
*
* @param fromWorld The world to clone.
* @param world The world to clone.
* @param newWorldName The name of the new world.
* @return A new {@link CloneWorldOptions} instance.
*
* @since 5.6
*/
@ApiStatus.AvailableSince("5.6")
public static @NotNull CloneWorldOptions fromTo(@NotNull MultiverseWorld fromWorld, @NotNull String newWorldName) {
return new CloneWorldOptions(fromWorld, Either.left(newWorldName));
public static CloneWorldOptions fromTo(LoadedMultiverseWorld world, String newWorldName) {
return new CloneWorldOptions(world, newWorldName);
}
/**
* Creates a new {@link CloneWorldOptions} instance with the given world and namespaced key.
*
* @param fromWorld The world to clone.
* @param key The namespaced key for the new world.
* @return A new {@link CloneWorldOptions} instance.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static @NotNull CloneWorldOptions fromTo(@NotNull MultiverseWorld fromWorld, @NotNull NamespacedKey key) {
return new CloneWorldOptions(fromWorld, Either.right(WorldKeyOrName.parseKey(key)));
}
/**
* Creates a new {@link CloneWorldOptions} instance with the given world and world key or name.
*
* @param fromWorld The world to clone.
* @param keyOrName The key or name for the new world.
* @return A new {@link CloneWorldOptions} instance.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static @NotNull CloneWorldOptions fromTo(@NotNull MultiverseWorld fromWorld, @NotNull WorldKeyOrName keyOrName) {
return new CloneWorldOptions(fromWorld, Either.right(keyOrName));
}
private final MultiverseWorld fromWorld;
private final Either<String, WorldKeyOrName> newWorldKeyOrName;
private final LoadedMultiverseWorld world;
private final String newWorldName;
private boolean keepGameRule = true;
private boolean keepWorldConfig = true;
private boolean saveBukkitWorld = true;
private boolean keepWorldBorder = true;
CloneWorldOptions(MultiverseWorld fromWorld, Either<String, WorldKeyOrName> newWorldKeyOrName) {
this.fromWorld = fromWorld;
this.newWorldKeyOrName = newWorldKeyOrName;
CloneWorldOptions(LoadedMultiverseWorld world, String newWorldName) {
this.world = world;
this.newWorldName = newWorldName;
}
/**
* Gets the loaded world to clone.
* Gets the world to clone.
*
* @return The world to clone.
*
* @deprecated Cloning can be done from unloaded worlds as well. Use {@link #fromWorld()} instead.
*/
@Deprecated(forRemoval = true, since = "5.6")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public LoadedMultiverseWorld world() {
return fromWorld.asLoadedWorld().getOrNull();
}
/**
* Gets the loaded world to clone.
*
* @return The world to clone.
*
* @since 5.6
*/
@ApiStatus.AvailableSince("5.6")
public @NotNull MultiverseWorld fromWorld() {
return fromWorld;
}
/**
* Gets the new world key or name, either unparsed as string or the {@link WorldKeyOrName} instance.
*
* @return The new world key or name.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Either<String, WorldKeyOrName> newWorldKeyOrName() {
return newWorldKeyOrName;
return world;
}
/**
@@ -127,10 +46,8 @@ public final class CloneWorldOptions implements KeepWorldSettingsOptions {
*
* @return The name of the new world.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public @NotNull String newWorldName() {
return newWorldKeyOrName.fold(name -> name, WorldKeyOrName::usableName);
public String newWorldName() {
return newWorldName;
}
/**
@@ -1,21 +1,10 @@
package org.mvplugins.multiverse.core.world.options;
import co.aikar.commands.ACFUtil;
import io.vavr.control.Either;
import io.vavr.control.Option;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.WorldType;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnmodifiableView;
import org.mvplugins.multiverse.core.utils.position.EntityPosition;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Options for customizing the creation of a new world.
@@ -29,43 +18,12 @@ public final class CreateWorldOptions {
* @return A new {@link CreateWorldOptions} instance.
*/
public static @NotNull CreateWorldOptions worldName(@NotNull String worldName) {
return new CreateWorldOptions(Either.left(worldName));
return new CreateWorldOptions(worldName);
}
/**
* Creates a new {@link CreateWorldOptions} instance with the given namespaced key. Note that creating world with
* namespace requires PaperMC. This will not work on Spigot.
*
* @param key The namespaced key for the world to create.
* @return A new {@link CreateWorldOptions} instance.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static @NotNull CreateWorldOptions worldKey(@NotNull NamespacedKey key) {
return new CreateWorldOptions(Either.right(WorldKeyOrName.parseKey(key)));
}
/**
* Creates a new {@link CreateWorldOptions} instance with the given world key or name. Note that creating world with
* namespace requires PaperMC. WorldKeyOrName parsed as namespaced key (i.e. {@link WorldKeyOrName#isKey()} is true)
* will not work on Spigot.
*
* @param keyOrName The key or name for the world to create.
* @return A new {@link CreateWorldOptions} instance.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public static @NotNull CreateWorldOptions worldKeyOrName(@NotNull WorldKeyOrName keyOrName) {
return new CreateWorldOptions(Either.right(keyOrName));
}
private final Either<String, WorldKeyOrName> keyOrName;
private final String worldName;
private String biome = "";
private boolean bonusChest = false;
private World.Environment environment = World.Environment.NORMAL;
private EntityPosition forcedSpawnPosition = null;
private boolean generateStructures = true;
private String generator = null;
private String generatorSettings = "";
@@ -73,38 +31,24 @@ public final class CreateWorldOptions {
private boolean useSpawnAdjust = true;
private WorldType worldType = WorldType.NORMAL;
private boolean doFolderCheck = true;
private final Map<String, String> worldPropertyStrings = new HashMap<>();
/**
* Creates a new {@link CreateWorldOptions} instance with either a world name or world key or name.
* Creates a new {@link CreateWorldOptions} instance with the given world name.
*
* @param keyOrName Either the world name or the world key/name instance.
* @param worldName The name of the world to create.
*/
CreateWorldOptions(@NotNull Either<String, WorldKeyOrName> keyOrName) {
this.keyOrName = keyOrName;
CreateWorldOptions(@NotNull String worldName) {
this.worldName = worldName;
this.seed = ACFUtil.RANDOM.nextLong();
}
/**
* Gets the new world key or name, either unparsed as string or the {@link WorldKeyOrName} instance.
*
* @return The new world key or name.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Either<String, WorldKeyOrName> keyOrName() {
return keyOrName;
}
/**
* Gets the name of the world to create.
*
* @return The name of the world to create.
*/
@Deprecated(forRemoval = true, since = "5.7")
public @NotNull String worldName() {
return keyOrName.fold(name -> name ,WorldKeyOrName::usableName);
return worldName;
}
/**
@@ -129,32 +73,6 @@ public final class CreateWorldOptions {
return biome;
}
/**
* Sets whether bonus chest should generate at spawn upon world creation.
* <br />
* This feature only works on PaperMC 1.21.5+
*
* @param bonusChest Whether bonus chest should generate at spawn upon world creation.
* @return This {@link CreateWorldOptions} instance.
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull CreateWorldOptions bonusChest(boolean bonusChest) {
this.bonusChest = bonusChest;
return this;
}
/**
* Gets whether bonus chest should generate at spawn upon world creation.
* <br />
* This feature only works on PaperMC 1.21.5+
*
* @return true if bonus chest should generate, else false.
*/
@ApiStatus.AvailableSince("5.7")
public boolean bonusChest() {
return bonusChest;
}
/**
* Sets the environment of the world to create.
*
@@ -175,37 +93,6 @@ public final class CreateWorldOptions {
return environment;
}
/**
* Sets the forced spawn position of the world to apply. This may be null, in which case the spawn position will
* be determined by the default generator. Setting spawn position and {@link #useSpawnAdjust(boolean)} to false
* will improve world creation speed on PaperMC as chunks will not be loaded to search for spawn point.
* <br />
* This feature only works on PaperMC 26.1+
*
* @param forcedSpawnPosition The forced spawn position of the world to create.
* @return This {@link CreateWorldOptions} instance.
*
* @since 5.7
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull CreateWorldOptions forcedSpawnPosition(@Nullable EntityPosition forcedSpawnPosition) {
this.forcedSpawnPosition = forcedSpawnPosition;
return this;
}
/**
* Sets the forced spawn position of the world to apply. This may be null, in which case the spawn position will
* be determined by the default generator.
* <br />
* This feature only works on PaperMC 26.1+
*
* @return The force spawn position to apply if available.
*/
@ApiStatus.AvailableSince("5.7")
public @NotNull Option<EntityPosition> forcedSpawnPosition() {
return Option.of(this.forcedSpawnPosition);
}
/**
* Sets whether structures such as NPC villages should be generated.
*
@@ -373,46 +260,4 @@ public final class CreateWorldOptions {
public boolean doFolderCheck() {
return doFolderCheck;
}
/**
* Sets a world property string key-value pair. Overwrites any existing key.
*
* @param key The key of the world property string.
* @param value The value of the world property string.
* @return This {@link CreateWorldOptions} instance.
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public @NotNull CreateWorldOptions worldPropertyString(@NotNull String key, @Nullable String value) {
this.worldPropertyStrings.put(key, value);
return this;
}
/**
* Sets multiple world property string key-value pairs. Overwrites any existing keys.
*
* @param worldProperties A map of world property string key-value pairs.
* @return This {@link CreateWorldOptions} instance.
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public @NotNull CreateWorldOptions worldPropertyStrings(@NotNull Map<@NotNull String, @Nullable String> worldProperties) {
this.worldPropertyStrings.putAll(worldProperties);
return this;
}
/**
* Gets an unmodifiable view of the world property strings. Use {@link #worldPropertyString(String, String)} and
* {@link #worldPropertyStrings(Map)} to modify the world property strings.
*
* @return An unmodifiable view of the world property strings.
*
* @since 5.5
*/
@ApiStatus.AvailableSince("5.5")
public @UnmodifiableView @NotNull Map<String, String> worldPropertyStrings() {
return Collections.unmodifiableMap(worldPropertyStrings);
}
}
Loaded 100 of 152 files, more files were not shown because too many files have changed in this diff. Show more