Compare commits

..
6 Commits
186 changed files with 1739 additions and 12310 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.2, 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.2, 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.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 = 'jitpack repo'
url = uri('https://jitpack.io')
}
maven {
name = 'aikar repo'
url = uri('https://repo.aikar.co/content/groups/aikar/')
}
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.12.3'
// Command Framework
shadowed 'co.aikar:acf-paper:0.5.1-SNAPSHOT'
// Config
shadowed('com.github.TownyAdvanced:CommentedConfiguration:1.0.3') {
exclude group: 'org.spigotmc', module: 'spigot-api'
}
// Utils
shadowed 'io.vavr:vavr:1.0.1'
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.2.1') {
exclude group: 'org.bukkit', module: 'bukkit'
}
shadowed 'net.minidev:json-smart:2.6.0'
shadowed 'org.jetbrains:annotations:26.1.0'
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"
@@ -30,7 +30,6 @@ import org.mvplugins.multiverse.core.inject.PluginServiceLocatorFactory;
import org.mvplugins.multiverse.core.module.MultiverseModule;
import org.mvplugins.multiverse.core.utils.StringFormatter;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.entity.SpawnCategoryMapper;
import org.mvplugins.multiverse.core.world.location.NullSpawnLocation;
import org.mvplugins.multiverse.core.world.location.SpawnLocation;
import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation;
@@ -98,16 +97,12 @@ public class MultiverseCore extends MultiverseModule {
}
Logging.setShowingConfig(shouldShowConfig());
// Build it here so our logger can work, and failure messages will be logged
SpawnCategoryMapper.buildSpawnCategoryMap();
// Initialize the worlds
Try.run(() -> {
setUpLocales();
worldManagerProvider.get().initAllWorlds();
worldManagerProvider.get().initAllWorlds().andThenTry(() -> {
loadEconomist(); // Setup economy here so vault is loaded
loadAnchors();
registerDynamicListeners(CoreListener.class);
setUpLocales();
registerCommands(CoreCommand.class);
registerDestinations();
setupMetrics();
@@ -131,7 +126,6 @@ public class MultiverseCore extends MultiverseModule {
MultiverseCoreApi.shutdown();
shutdownDependencyInjection();
PluginServiceLocatorFactory.get().shutdown();
Logging.info("- Disabled");
Logging.shutdown();
}
@@ -231,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");
}
}
@@ -1,17 +1,9 @@
package org.mvplugins.multiverse.core;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.plugin.ServicePriority;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.anchor.AnchorManager;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
@@ -24,13 +16,18 @@ import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.biomeprovider.BiomeProviderFactory;
import org.mvplugins.multiverse.core.world.generators.GeneratorProvider;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
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>> WHEN_LOADED_CALLBACKS = new ArrayList<>();
private static final List<Consumer<MultiverseCoreApi>> whenLoadedCallbacks = new ArrayList<>();
static void init(@NotNull MultiverseCore multiverseCore) {
if (instance != null) {
@@ -39,24 +36,11 @@ public final class MultiverseCoreApi {
instance = new MultiverseCoreApi(multiverseCore.getServiceLocator());
Bukkit.getServicesManager().register(MultiverseCoreApi.class, instance, multiverseCore, ServicePriority.Normal);
List<Consumer<MultiverseCoreApi>> callbacks = List.copyOf(WHEN_LOADED_CALLBACKS);
WHEN_LOADED_CALLBACKS.clear();
MultiverseCoreApi loadedApi = instance;
callbacks.forEach(callback -> runLoadCallback(callback, loadedApi));
}
private static void runLoadCallback(
@NotNull Consumer<MultiverseCoreApi> callback,
@NotNull MultiverseCoreApi loadedApi) {
Try.run(() -> callback.accept(loadedApi))
.onFailure(exception -> Logging.warning(
"A Multiverse-Core API load callback failed: %s", exception.getMessage()));
whenLoadedCallbacks.forEach(c -> c.accept(instance));
whenLoadedCallbacks.clear();
}
static void shutdown() {
if (instance == null) {
return;
}
Bukkit.getServicesManager().unregister(instance);
instance = null;
}
@@ -78,7 +62,7 @@ public final class MultiverseCoreApi {
if (instance != null) {
consumer.accept(instance);
} else {
WHEN_LOADED_CALLBACKS.add(consumer);
whenLoadedCallbacks.add(consumer);
}
}
@@ -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";
@@ -87,8 +72,7 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
@Override
public @Nullable String onRequest(OfflinePlayer offlinePlayer, @NotNull String params) {
// Split string in to an Array with underscores
List<String> paramsArray = Lists.newArrayList(
StringFormatter.parseQuotesInArgs(REPatterns.UNDERSCORE.split(params), "_"));
List<String> paramsArray = Lists.newArrayList(REPatterns.UNDERSCORE.split(params));
// No placeholder defined
if (paramsArray.isEmpty()) {
@@ -96,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) {
@@ -39,12 +39,9 @@ import org.mvplugins.multiverse.core.display.filters.ContentFilter;
import org.mvplugins.multiverse.core.display.filters.DefaultContentFilter;
import org.mvplugins.multiverse.core.display.filters.RegexContentFilter;
import org.mvplugins.multiverse.core.exceptions.command.MVInvalidCommandArgument;
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.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;
@@ -96,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) {
@@ -170,67 +166,15 @@ public class MVCommandContexts extends PaperCommandContexts {
return generatorPlugin;
}
private Message loadedMultiverseWorldPlayerOnlyMessage() {
return Message.of(MVCorei18n.COMMANDS_ERROR_LOADEDMULTIVERSEWORLD_PLAYERSONLY);
}
private Message loadedMultiverseWorldIssuerMessage() {
return Message.of(MVCorei18n.COMMANDS_ERROR_LOADEDMULTIVERSEWORLD_ISSUER);
}
private Message loadedMultiverseWorldInputConsoleMessage(String worldName) {
return Message.of(MVCorei18n.COMMANDS_ERROR_LOADEDMULTIVERSEWORLD_INPUTCONSOLE,
Replace.WORLD.with(worldName));
}
private Message loadedMultiverseWorldInputMessage(String worldName) {
return Message.of(MVCorei18n.COMMANDS_ERROR_LOADEDMULTIVERSEWORLD_INPUT, Replace.WORLD.with(worldName));
}
private Message multiverseWorldPlayerOnlyMessage() {
return Message.of(MVCorei18n.COMMANDS_ERROR_MULTIVERSEWORLD_PLAYERSONLY);
}
private Message multiverseWorldIssuerMessage() {
return Message.of(MVCorei18n.COMMANDS_ERROR_MULTIVERSEWORLD_ISSUER);
}
private Message multiverseWorldInputConsoleMessage(String worldName) {
return Message.of(MVCorei18n.COMMANDS_ERROR_MULTIVERSEWORLD_INPUTCONSOLE, Replace.WORLD.with(worldName));
}
private Message multiverseWorldInputMessage(String worldName) {
return Message.of(MVCorei18n.COMMANDS_ERROR_MULTIVERSEWORLD_INPUT, Replace.WORLD.with(worldName));
}
private Message playerOnlyMessage() {
return Message.of(MVCorei18n.COMMANDS_ERROR_PLAYERSONLY);
}
private Message playerInputIssuerMessage(String playerName) {
return Message.of(MVCorei18n.COMMANDS_ERROR_PLAYER_ISSUERINPUT, Replace.PLAYER.with(playerName));
}
private Message playerInputMessage(String playerName) {
return Message.of(MVCorei18n.COMMANDS_ERROR_PLAYER_INPUT, Replace.PLAYER.with(playerName));
}
private Message playerSelectorMessage(String selector) {
return Message.of(MVCorei18n.COMMANDS_ERROR_PLAYER_SELECTOR, Replace.PLAYER.with(selector));
}
private Message playersInputMessage(String playerName) {
return Message.of(MVCorei18n.COMMANDS_ERROR_PLAYERS_INPUT, Replace.PLAYER.with(playerName));
}
private IssuerAwareContextBuilder<LoadedMultiverseWorld> loadedMultiverseWorldContextBuilder() {
return new IssuerAwareContextBuilder<LoadedMultiverseWorld>()
.fromPlayer((context, player) -> worldManager.getLoadedWorld(player.getWorld()).getOrNull())
.fromInput((context, input) -> getLoadedMultiverseWorld(input))
.issuerOnlyFailMessage((context) -> loadedMultiverseWorldPlayerOnlyMessage())
.issuerAwarePlayerFailMessage((context, player) -> loadedMultiverseWorldIssuerMessage())
.issuerAwareInputFailMessage((context, input) -> loadedMultiverseWorldInputConsoleMessage(input))
.inputOnlyFailMessage((context, input) -> loadedMultiverseWorldInputMessage(input));
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player in a loaded Multiverse World."))
.issuerAwarePlayerFailMessage((context, player) -> Message.of("You are not in a loaded multiverse world. Either specify a multiverse world name or use this command in a loaded multiverse world."))
.issuerAwareInputFailMessage((context, input) -> Message.of("World '" + input + "' is not a loaded multiverse world. Remember to specify the world name when using this command in console."))
.inputOnlyFailMessage((context, input) -> Message.of("World " + input + " is not a loaded multiverse world."));
}
private IssuerAwareContextBuilder<LoadedMultiverseWorld[]> loadedMultiverseWorldArrayContextBuilder() {
@@ -248,17 +192,16 @@ public class MVCommandContexts extends PaperCommandContexts {
}
LoadedMultiverseWorld world = getLoadedMultiverseWorld(worldName);
if (world == null) {
throw new InvalidCommandArgument(
loadedMultiverseWorldInputMessage(worldName).formatted(context.getIssuer()));
throw new InvalidCommandArgument("World " + worldName + " is not a loaded multiverse world.");
}
worlds.add(world);
}
return worlds.isEmpty() ? null : worlds.toArray(new LoadedMultiverseWorld[0]);
})
.issuerOnlyFailMessage((context) -> loadedMultiverseWorldPlayerOnlyMessage())
.issuerAwarePlayerFailMessage((context, player) -> loadedMultiverseWorldIssuerMessage())
.issuerAwareInputFailMessage((context, input) -> loadedMultiverseWorldInputConsoleMessage(input))
.inputOnlyFailMessage((context, input) -> loadedMultiverseWorldInputMessage(input));
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player in a loaded Multiverse World."))
.issuerAwarePlayerFailMessage((context, player) -> Message.of("You are not in a loaded multiverse world. Either specify a multiverse world name or use this command in a loaded multiverse world."))
.issuerAwareInputFailMessage((context, input) -> Message.of("World '" + input + "' is not a loaded multiverse world. Remember to specify the world name when using this command in console."))
.inputOnlyFailMessage((context, input) -> Message.of("World " + input + " is not a loaded multiverse world."));
}
@Nullable
@@ -272,10 +215,10 @@ public class MVCommandContexts extends PaperCommandContexts {
return new IssuerAwareContextBuilder<MultiverseWorld>()
.fromPlayer((context, player) -> worldManager.getWorld(player.getWorld()).getOrNull())
.fromInput((context, input) -> getMultiverseWorld(input))
.issuerOnlyFailMessage((context) -> multiverseWorldPlayerOnlyMessage())
.issuerAwarePlayerFailMessage((context, player) -> multiverseWorldIssuerMessage())
.issuerAwareInputFailMessage((context, input) -> multiverseWorldInputConsoleMessage(input))
.inputOnlyFailMessage((context, input) -> multiverseWorldInputMessage(input));
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player in a Multiverse World."))
.issuerAwarePlayerFailMessage((context, player) -> Message.of("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world."))
.issuerAwareInputFailMessage((context, input) -> Message.of("World '" + input + "' is not a multiverse world. Remember to specify the world name when using this command in console."))
.inputOnlyFailMessage((context, input) -> Message.of("World " + input + " is not a multiverse world."));
}
private IssuerAwareContextBuilder<MultiverseWorld[]> multiverseWorldArrayContextBuilder() {
@@ -293,17 +236,16 @@ public class MVCommandContexts extends PaperCommandContexts {
}
MultiverseWorld world = getMultiverseWorld(worldName);
if (world == null) {
throw new InvalidCommandArgument(
multiverseWorldInputMessage(worldName).formatted(context.getIssuer()));
throw new InvalidCommandArgument("World " + worldName + " is not a multiverse world.");
}
worlds.add(world);
}
return worlds.isEmpty() ? null : worlds.toArray(new MultiverseWorld[0]);
})
.issuerOnlyFailMessage((context) -> multiverseWorldPlayerOnlyMessage())
.issuerAwarePlayerFailMessage((context, player) -> multiverseWorldIssuerMessage())
.issuerAwareInputFailMessage((context, input) -> multiverseWorldInputConsoleMessage(input))
.inputOnlyFailMessage((context, input) -> multiverseWorldInputMessage(input));
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player in a Multiverse World."))
.issuerAwarePlayerFailMessage((context, player) -> Message.of("You are not in a multiverse world. Either specify a multiverse world name or use this command in a multiverse world."))
.issuerAwareInputFailMessage((context, input) -> Message.of("World '" + input + "' is not a multiverse world. Remember to specify the world name when using this command in console."))
.inputOnlyFailMessage((context, input) -> Message.of("World " + input + " is not a multiverse world."));
}
@Nullable
@@ -323,9 +265,9 @@ public class MVCommandContexts extends PaperCommandContexts {
return new IssuerAwareContextBuilder<Player>()
.fromPlayer((context, player) -> player)
.fromInput((context, input) -> PlayerFinder.get(input, context.getSender()))
.issuerOnlyFailMessage((context) -> playerOnlyMessage())
.issuerAwareInputFailMessage((context, input) -> playerInputIssuerMessage(input))
.inputOnlyFailMessage((context, input) -> playerInputMessage(input));
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player."))
.issuerAwareInputFailMessage((context, input) -> Message.of("Invalid player: " + input + ". Either specify an online player or use this command as a player."))
.inputOnlyFailMessage((context, input) -> Message.of("Player " + input + " not found."));
}
private IssuerAwareContextBuilder<Player[]> playerArrayContextBuilder() {
@@ -342,13 +284,13 @@ public class MVCommandContexts extends PaperCommandContexts {
throw new InvalidCommandArgument(failure.getLocalizedMessage() + " "
+ Option.of(failure.getCause()).map(Throwable::getLocalizedMessage).getOrElse(""));
}))
.issuerOnlyFailMessage((context) -> playerOnlyMessage())
.issuerAwareInputFailMessage((context, input) -> playerInputIssuerMessage(input))
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player."))
.issuerAwareInputFailMessage((context, input) -> Message.of("Invalid player: " + input + ". Either specify an online player or use this command as a player."))
.inputOnlyFailMessage((context, input) -> {
if (PlayerFinder.isSelector(input)) {
return playerSelectorMessage(input);
return Message.of("No player(s) matched selector: " + input + ".");
}
return playersInputMessage(input);
return Message.of("Player(s) " + input + " not found.");
});
}
@@ -451,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"));
}
}
@@ -12,8 +12,6 @@ import co.aikar.commands.LogLevel;
import co.aikar.commands.PaperCommandManager;
import co.aikar.commands.RootCommand;
import com.dumptruckman.minecraft.util.Logging;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import org.bukkit.Bukkit;
@@ -26,7 +24,6 @@ import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.queue.CommandQueueManager;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.locale.PluginLocales;
import org.mvplugins.multiverse.core.locale.message.LocalizedMessage;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.WorldNameChecker;
@@ -65,22 +62,13 @@ 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");
this.setDefaultExceptionHandler(new MVDefaultExceptionHandler());
}
@PostConstruct
private void postConstruct() {
LocalizedMessage.setDefaultLocalesManager(getLocales());
}
@PreDestroy
private void preDestroy() {
LocalizedMessage.setDefaultLocalesManager(null);
}
/**
* Registers a list of commands and handles {@link LegacyAliasCommand} based on config option.
* @param commands The commands to register
@@ -6,16 +6,12 @@ import co.aikar.commands.InvalidCommandArgument;
import co.aikar.commands.contexts.IssuerAwareContextResolver;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.ApiStatus;
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 java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* Reusable logic of issuer only and issuer aware context resolvers
*
@@ -26,17 +22,10 @@ public final class IssuerAwareContextBuilder<T> {
private BiFunction<BukkitCommandExecutionContext, Player, T> fromPlayer;
private BiFunction<BukkitCommandExecutionContext, String, T> fromInput;
private Function<BukkitCommandExecutionContext, Message> issuerOnlyFailMessage =
context -> Message.of(MVCorei18n.COMMANDS_ERROR_PLAYERSONLY);
private BiFunction<BukkitCommandExecutionContext, Player, Message> issuerAwarePlayerFailMessage =
(context, player) -> Message.of(MVCorei18n.COMMANDS_ERROR_RESOLVE_PLAYER,
Replace.PLAYER.with(player.getName()));
private BiFunction<BukkitCommandExecutionContext, String, Message> issuerAwareInputFailMessage =
(context, input) -> Message.of(MVCorei18n.COMMANDS_ERROR_RESOLVE_INPUT,
replace("{input}").with(input));
private BiFunction<BukkitCommandExecutionContext, String, Message> inputOnlyFailMessage =
(context, input) -> Message.of(MVCorei18n.COMMANDS_ERROR_RESOLVE_INPUT,
replace("{input}").with(input));
private Function<BukkitCommandExecutionContext, Message> issuerOnlyFailMessage = context -> Message.of("This command can only be used by a player.");
private BiFunction<BukkitCommandExecutionContext, Player, Message> issuerAwarePlayerFailMessage = (context, player) -> Message.of("Unable to resolve context for player '" + player.getName() + "'.");
private BiFunction<BukkitCommandExecutionContext, String, Message> issuerAwareInputFailMessage = (context, input) -> Message.of("Unable to resolve context for input '" + input + "'.");
private BiFunction<BukkitCommandExecutionContext, String, Message> inputOnlyFailMessage = (context, input) -> Message.of("Unable to resolve context for input '" + input + "'.");
public IssuerAwareContextBuilder() {
}
@@ -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,67 +0,0 @@
package org.mvplugins.multiverse.core.command.flags;
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.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
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(() -> MVInvalidCommandArgument.of(
Message.of(MVCorei18n.COMMANDS_ERROR_REMOVEPLAYERS_NODEFAULT))))
.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 {
@@ -15,6 +15,7 @@ import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.block.data.type.CommandBlock;
import org.bukkit.command.BlockCommandSender;
import org.bukkit.command.CommandSender;
@@ -27,7 +28,6 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.*;
@@ -79,9 +79,8 @@ public class CommandQueueManager {
if (config.getUseConfirmOtp()) {
confirmCommand += " " + payload.otp();
}
payload.issuer().sendMessage(MVCorei18n.QUEUECOMMAND_PROMPT,
replace("{command}").with(confirmCommand),
replace("{timeout}").with(config.getConfirmTimeout()));
payload.issuer().sendMessage(String.format("Run %s%s %sto continue. This will expire in %s seconds.",
ChatColor.GREEN, confirmCommand, ChatColor.WHITE, config.getConfirmTimeout()));
}
/**
@@ -128,7 +127,7 @@ public class CommandQueueManager {
// Payload already removed
return;
}
payload.issuer().sendMessage(MVCorei18n.QUEUECOMMAND_EXPIRED);
payload.issuer().sendMessage("Your queued command has expired.");
};
}
@@ -5,7 +5,6 @@ import org.bukkit.scheduler.BukkitTask;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
/**
@@ -24,10 +23,12 @@ public class CommandQueuePayload {
return new CommandQueuePayload(issuer);
}
private static final String DEFAULT_PROMPT_MESSAGE = "The command you are trying to run is deemed dangerous."; // todo: localize
private final MVCommandIssuer issuer;
private String otp;
private Runnable action = () -> {};
private Message prompt = Message.of(MVCorei18n.QUEUECOMMAND_DEFAULTPROMPT);
private Message prompt = Message.of(DEFAULT_PROMPT_MESSAGE);
private BukkitTask expireTask;
protected CommandQueuePayload(@NotNull MVCommandIssuer issuer) {
@@ -12,9 +12,6 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.anchor.AnchorManager;
import org.mvplugins.multiverse.core.anchor.MultiverseAnchor;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
@Service
final class AnchorDeleteCommand extends CoreCommand {
@@ -38,9 +35,9 @@ final class AnchorDeleteCommand extends CoreCommand {
@Description("")
MultiverseAnchor anchor) {
if (anchorManager.deleteAnchor(anchor).isSuccess()) {
issuer.sendMessage(MVCorei18n.ANCHOR_DELETE_SUCCESS, replace("{anchor}").with(anchor.getName()));
issuer.sendMessage("&aAnchor &f" + anchor.getName() + "&a deleted.");
} else {
issuer.sendError(MVCorei18n.ANCHOR_DELETE_FAILURE);
issuer.sendMessage("&cFailed to delete anchor.");
}
}
}
@@ -15,18 +15,15 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.anchor.AnchorManager;
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.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.ListContentProvider;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.teleportation.LocationManipulation;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
@Service
final class AnchorListCommand extends CoreCommand {
@@ -61,19 +58,18 @@ final class AnchorListCommand extends CoreCommand {
ContentDisplay.create()
.addContent(ListContentProvider.forContent(getAnchors(issuer.getPlayer())))
.withSendHandler(PagedSendHandler.create()
.withHeader(Message.of(MVCorei18n.ANCHOR_LIST_HEADER))
.withHeader("&3==== [ Multiverse Anchors ] ====")
.doPagination(true)
.withTargetPage(parsedFlags.flagValue(flags.page, 1))
.withFilter(parsedFlags.flagValue(flags.filter, DefaultContentFilter.get())))
.send(issuer);
}
private List<? extends Message> getAnchors(Player player) {
private List<String> getAnchors(Player player) {
return anchorManager.getAnchors(player).stream()
.map(anchor ->
Message.of(MVCorei18n.ANCHOR_LIST_ENTRY,
replace("{anchor}").with(anchor.getName()),
replace("{location}").with(locationManipulation.locationToString(anchor.getLocation()))))
"&a%s&7 - &f%s".formatted(
anchor.getName(), locationManipulation.locationToString(anchor.getLocation())))
.toList();
}
}
@@ -15,11 +15,8 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.anchor.AnchorManager;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.teleportation.LocationManipulation;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
@Service
final class AnchorSetCommand extends CoreCommand {
@@ -83,12 +80,10 @@ final class AnchorSetCommand extends CoreCommand {
}
private void sendAnchorSetSuccessMessage(MVCommandIssuer issuer, String anchorName, String locationString) {
issuer.sendMessage(MVCorei18n.ANCHOR_SET_SUCCESS,
replace("{anchor}").with(anchorName),
replace("{location}").with(locationString));
issuer.sendMessage("&aAnchor &f" + anchorName + "&a set to &f" + locationString);
}
private void sendAnchorSetFailedMessage(MVCommandIssuer issuer, String anchorName) {
issuer.sendMessage(MVCorei18n.ANCHOR_SET_FAILURE, replace("{anchor}").with(anchorName));
issuer.sendMessage("&cFailed to set anchor &f" + anchorName + ".");
}
}
@@ -61,7 +61,7 @@ class CheckCommand extends CoreCommand {
replace("{location}").with(destination.getLocation(player)
.map(locationManipulation::locationToString)
.map(Message::of)
.getOrElse(() -> Message.of(MVCorei18n.GENERIC_NULL))));
.getOrElse(() -> Message.of(MVCorei18n.GENERIC_NULL, "Null!"))));
// TODO: Show permission required for this particular destination
}
@@ -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;
@@ -41,21 +41,21 @@ class CloneCommand extends CoreCommand {
@Subcommand("clone")
@CommandPermission("multiverse.core.clone")
@CommandCompletion("@mvworlds:scope=loaded @empty @flags:groupName=" + Flags.NAME)
@Syntax("<world> <new-world-name> [--reset-world-config --reset-gamerules --reset-world-border --no-save]")
@Syntax("<world> <new-world-name> [--reset-world-config --reset-gamerules --reset-world-border]")
@Description("{@@mv-core.clone.description}")
void onCloneCommand(
MVCommandIssuer issuer,
@Syntax("<world>")
@Description("{@@mv-core.clone.world.description}")
MultiverseWorld world,
LoadedMultiverseWorld world,
@Syntax("<new-world-name>")
@Description("{@@mv-core.clone.newworld.description}")
@Description("{@@mv-core.clone.newWorld.description}")
String newWorldName,
@Optional
@Syntax("[--reset-world-config --reset-gamerules --reset-world-border --no-save]")
@Syntax("[--reset-world-config --reset-gamerules --reset-world-border]")
@Description("{@@mv-core.regen.other.description}")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
@@ -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> " +
"--no-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> --no-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);
@@ -79,22 +77,21 @@ class DeleteCommand extends CoreCommand {
commandQueueManager.addToQueue(CommandQueuePayload
.issuer(issuer)
.action(() -> runDeleteCommand(issuer, world, parsedFlags))
.prompt(Message.of(MVCorei18n.DELETE_PROMPT,
.prompt(Message.of(MVCorei18n.DELETE_PROMPT, "",
Replace.WORLD.with(world.getName()))));
}
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)))
.onFailure(() -> issuer.sendError(MVCorei18n.GENERIC_TELEPORTPLAYERS_FAILED));
.onFailure(() -> issuer.sendError("Failed to teleport one or more players out of the world!"));
}
private void doWorldDeleting(MVCommandIssuer issuer, MultiverseWorld 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 - ");
}
}
@@ -21,9 +21,7 @@ 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.ListContentProvider;
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.utils.StringFormatter;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
@@ -33,8 +31,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
@Service
@Subcommand("entity-spawn-config")
final class EntitySpawnConfigCommand extends CoreCommand {
@@ -68,26 +64,21 @@ final class EntitySpawnConfigCommand extends CoreCommand {
ContentDisplay.create()
.addContent(ListContentProvider.forContent(getEntitySpawnConfigList(world)))
.withSendHandler(PagedSendHandler.create()
.withHeader(Message.of(MVCorei18n.ENTITYSPAWNCONFIG_INFO_HEADER,
Replace.WORLD.with(world.getName())))
.withHeader(Message.of("==== [ Entity Spawn Config '" + world.getName() + "' ] ===="))
.withLinesPerPage(8)
.withTargetPage(parsedFlags.flagValue(flags.page, 1))
.withFilter(parsedFlags.flagValue(flags.filter, DefaultContentFilter.get())))
.send(issuer);
}
private List<Message> getEntitySpawnConfigList(MultiverseWorld world) {
List<Message> list = new ArrayList<>();
private List<String> getEntitySpawnConfigList(MultiverseWorld world) {
List<String> list = new ArrayList<>();
Arrays.stream(SpawnCategory.values()).forEach(spawnCategory -> {
list.add(Message.of(MVCorei18n.ENTITYSPAWNCONFIG_INFO_CATEGORY,
replace("{category}").with(spawnCategory.name())));
list.add(spawnCategory.name() + ": ");
SpawnCategoryConfig spawnCategoryConfig = world.getEntitySpawnConfig().getSpawnCategoryConfig(spawnCategory);
list.add(Message.of(MVCorei18n.ENTITYSPAWNCONFIG_INFO_SPAWN,
replace("{spawn}").with(spawnCategoryConfig.isSpawn())));
list.add(Message.of(MVCorei18n.ENTITYSPAWNCONFIG_INFO_TICKRATE,
replace("{tickRate}").with(spawnCategoryConfig.getTickRate())));
list.add(Message.of(MVCorei18n.ENTITYSPAWNCONFIG_INFO_EXCEPTIONS,
replace("{exceptions}").with(StringFormatter.join(spawnCategoryConfig.getExceptions(), ", "))));
list.add(" spawn: " + spawnCategoryConfig.isSpawn());
list.add(" tick-rate: " + spawnCategoryConfig.getTickRate());
list.add(" exceptions: " + StringFormatter.join(spawnCategoryConfig.getExceptions(), ", "));
});
return list;
}
@@ -123,16 +114,9 @@ final class EntitySpawnConfigCommand extends CoreCommand {
.getStringPropertyHandle()
.modifyPropertyString(property, value, action)
.andThenTry(worldManager::saveWorldsConfig)
.onSuccess(ignore -> issuer.sendInfo(MVCorei18n.ENTITYSPAWNCONFIG_MODIFY_SUCCESS,
replace("{property}").with(property),
Replace.VALUE.with(value),
replace("{category}").with(spawnCategory.name()),
Replace.WORLD.with(world.getName())))
.onFailure(e -> issuer.sendError(MVCorei18n.ENTITYSPAWNCONFIG_MODIFY_FAILURE,
replace("{property}").with(property),
Replace.VALUE.with(value),
replace("{category}").with(spawnCategory.name()),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(e)));
.onSuccess(ignore -> issuer.sendInfo("Successfully set " + property + " to " + value
+ " for " + spawnCategory.name() + " in " + world.getName()))
.onFailure(e -> issuer.sendError("Unable to set " + property + " to " + value
+ " for " + spawnCategory.name() + " in " + world.getName() + ": " + e.getMessage()));
}
}
@@ -54,21 +54,21 @@ class GameruleCommand extends CoreCommand {
@Subcommand("gamerule|rule set")
@CommandPermission("multiverse.core.gamerule.set")
@CommandCompletion("@gamerules @gamerulesvalues @mvworlds:multiple|*")
@Syntax("<gamerule> <value> [worlds|*]")
@Syntax("<Gamerule> <Gamerule value> [World or *]")
@Description("{@@mv-core.gamerule.set.description}")
void onGameruleSetCommand(
MVCommandIssuer issuer,
@Syntax("<gamerule>")
@Syntax("<Gamerule>")
@Description("{@@mv-core.gamerule.set.gamerule.description}")
GameRule gamerule,
@Syntax("<value>")
@Syntax("<Value>")
@Description("{@@mv-core.gamerule.set.value.description}")
GameRuleValue gameRuleValue,
@Flags("resolve=issuerAware,maxArgForAware=2")
@Syntax("[worlds|*]")
@Syntax("[World or *]")
@Description("{@@mv-core.gamerule.set.world.description}")
LoadedMultiverseWorld[] worlds) {
Object value = gameRuleValue.value();
@@ -105,17 +105,17 @@ class GameruleCommand extends CoreCommand {
@Subcommand("gamerule|rule reset")
@CommandPermission("multiverse.core.gamerule.set")
@CommandCompletion("@gamerules @mvworlds:multiple|*")
@Syntax("<gamerule> [worlds|*]")
@Syntax("<Gamerule> [World or *]")
@Description("{@@mv-core.gamerule.reset.description}")
void onGameruleResetCommand(
MVCommandIssuer issuer,
@Syntax("<gamerule>")
@Syntax("<Gamerule>")
@Description("{@@mv-core.gamerule.reset.gamerule.description}")
GameRule gamerule,
@Flags("resolve=issuerAware,maxArgForAware=1")
@Syntax("[worlds|*]")
@Syntax("[World or *]")
@Description("{@@mv-core.gamerule.reset.world.description}")
LoadedMultiverseWorld[] worlds) {
AtomicBoolean success = new AtomicBoolean(true);
@@ -153,7 +153,7 @@ class GameruleCommand extends CoreCommand {
MVCommandIssuer issuer,
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
@Syntax("<world>")
@Description("{@@mv-core.gamerule.list.description.world}")
LoadedMultiverseWorld world,
@@ -10,6 +10,7 @@ import co.aikar.commands.annotation.Optional;
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;
@@ -22,13 +23,10 @@ import org.mvplugins.multiverse.core.display.filters.DefaultContentFilter;
import org.mvplugins.multiverse.core.display.handlers.PagedSendHandler;
import org.mvplugins.multiverse.core.display.parsers.ListContentProvider;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.utils.StringFormatter;
import org.mvplugins.multiverse.core.world.generators.GeneratorPlugin;
import org.mvplugins.multiverse.core.world.generators.GeneratorProvider;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* List all gamerules in your current or specified world.
*/
@@ -70,7 +68,7 @@ class GeneratorsCommand extends CoreCommand {
ContentDisplay.create()
.addContent(ListContentProvider.forContent(generators))
.withSendHandler(PagedSendHandler.create()
.withHeader(Message.of(MVCorei18n.GENERATORS_HEADER))
.withHeader("%s====[ Multiverse Generator List ]====", ChatColor.AQUA)
.doPagination(true)
.withTargetPage(parsedFlags.flagValue(flags.page, 1))
.withFilter(parsedFlags.flagValue(flags.filter, DefaultContentFilter.get())))
@@ -87,13 +85,10 @@ class GeneratorsCommand extends CoreCommand {
@Syntax("<generator>")
GeneratorPlugin generatorPlugin
) {
issuer.sendMessage(MVCorei18n.GENERATORS_INFO_PLUGIN,
replace("{plugin}").with(generatorPlugin.getPluginName()));
issuer.sendMessage(MVCorei18n.GENERATORS_INFO_EXAMPLEUSAGES);
issuer.sendMessage(MVCorei18n.GENERATORS_INFO_USAGES,
replace("{usages}").with(StringFormatter.join(generatorPlugin.getExampleUsages(), "\n")));
issuer.sendMessage(MVCorei18n.GENERATORS_INFO_INFOLINK,
replace("{link}").with(generatorPlugin.getInfoLink()));
issuer.sendMessage(ChatColor.RESET + "Generator Plugin: " + generatorPlugin.getPluginName());
issuer.sendMessage(ChatColor.RESET + "Example usages: ");
issuer.sendMessage(ChatColor.RESET + StringFormatter.join(generatorPlugin.getExampleUsages(), "\n"));
issuer.sendMessage(ChatColor.RESET + "Link to more info: " + generatorPlugin.getInfoLink());
}
@Service
@@ -43,24 +43,22 @@ class ImportCommand extends CoreCommand {
@Subcommand("import")
@CommandPermission("multiverse.core.import")
@CommandCompletion("@mvworlds:scope=potential @environments @flags:groupName=" + Flags.NAME)
@Syntax("<name> <environment> [--generator <generator[:id]> --no-adjust-spawn --biome <biome> " +
"--skip-folder-check --generator-settings <json-settings>]")
@Syntax("<name> <environment> [--generator <generator[:id]> --adjust-spawn --biome <biome>]")
@Description("{@@mv-core.import.description}")
void onImportCommand(
MVCommandIssuer issuer,
// @Conditions("worldname:scope=new")
@Conditions("worldname:scope=new")
@Syntax("<name>")
@Description("{@@mv-core.import.name.description}")
String worldName,
@Syntax("<environment>")
@Syntax("<env>")
@Description("{@@mv-core.import.env.description}")
World.Environment environment,
@Optional
@Syntax("[--generator <generator[:id]> --no-adjust-spawn --biome <biome> " +
"--skip-folder-check --generator-settings <json-settings>]")
@Syntax("[--generator <generator[:id]> --adjust-spawn --biome <biome>]")
@Description("{@@mv-core.import.other.description}")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
@@ -69,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 -> {
@@ -113,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
@@ -54,12 +54,12 @@ class ListCommand extends CoreCommand {
@Subcommand("list")
@CommandPermission("multiverse.core.list.worlds")
@CommandCompletion("@flags:groupName=" + Flags.NAME)
@Syntax("[--filter <filter>] [--page <page>] [--raw]")
@Syntax("--filter [filter] --page [page] --raw")
@Description("{{@mv-core.list.description}}")
public void onListCommand(
MVCommandIssuer issuer,
@Syntax("[--filter <filter>] [--page <page>] [--raw]")
@Syntax("[--filter <filter>] [--page <page>]")
@Description("Filters the list of worlds by the given regex and displays the given page.")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
@@ -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,8 +38,8 @@ class LoadCommand extends CoreCommand {
@Subcommand("load")
@CommandPermission("multiverse.core.load")
@CommandCompletion("@mvworlds:scope=unloaded @flags:groupName=" + Flags.NAME)
@Syntax("<world> [--skip-folder-check]")
@CommandCompletion("@mvworlds:scope=unloaded")
@Syntax("<world>")
@Description("{@@mv-core.load.description}")
void onLoadCommand(
MVCommandIssuer issuer,
@@ -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();
}
}
@@ -107,14 +107,14 @@ class ModifyCommand extends CoreCommand {
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception));
Replace.ERROR.with(exception.getMessage()));
} else {
issuer.sendMessage(MVCorei18n.MODIFY_FAILURE,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.VALUE.with(propertyValue),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception));
Replace.ERROR.with(exception.getMessage()));
}
});
}
@@ -10,16 +10,12 @@ import jakarta.inject.Inject;
import org.bukkit.entity.SpawnCategory;
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.StringFormatter;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.entity.EntityPurger;
import java.util.List;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
@Service
final class PurgeAllEntitiesCommand extends CoreCommand {
@@ -47,16 +43,12 @@ final class PurgeAllEntitiesCommand extends CoreCommand {
) {
if (spawnCategories == null || spawnCategories.length == 0) {
int purgeCount = entityPurger.purgeAllEntities(world);
issuer.sendMessage(MVCorei18n.PURGEALLENTITIES_SUCCESS,
Replace.COUNT.with(purgeCount),
Replace.WORLD.with(world.getName()));
issuer.sendMessage("Successfully purged " + purgeCount + " entities in world " + world.getName() + ".");
return;
}
int purgeCount = entityPurger.purgeEntities(world, spawnCategories);
issuer.sendMessage(MVCorei18n.PURGEALLENTITIES_SUCCESS_CATEGORIES,
Replace.COUNT.with(purgeCount),
Replace.WORLD.with(world.getName()),
replace("{categories}").with(StringFormatter.join(List.of(spawnCategories), ", ")));
issuer.sendMessage("Successfully purged " + purgeCount + " entities in world " + world.getName() +
" for spawn categories " + StringFormatter.join(List.of(spawnCategories), ", ") + ".");
}
}
@@ -8,8 +8,6 @@ import co.aikar.commands.annotation.Syntax;
import jakarta.inject.Inject;
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.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.entity.EntityPurger;
@@ -35,8 +33,6 @@ final class PurgeEntitiesCommand extends CoreCommand {
LoadedMultiverseWorld world
) {
int purgeCount = entityPurger.purgeEntities(world);
issuer.sendMessage(MVCorei18n.PURGEENTITIES_SUCCESS,
Replace.COUNT.with(purgeCount),
Replace.WORLD.with(world.getName()));
issuer.sendMessage("Successfully purged " + purgeCount + " entities in world " + world.getName() + ".");
}
}
@@ -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] " +
"[--keep-files <files>] [--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] " +
"[--keep-files <files>] [--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);
@@ -89,7 +83,7 @@ class RegenCommand extends CoreCommand {
commandQueueManager.addToQueue(CommandQueuePayload
.issuer(issuer)
.action(() -> runRegenCommand(issuer, world, parsedFlags))
.prompt(Message.of(MVCorei18n.REGEN_PROMPT,
.prompt(Message.of(MVCorei18n.REGEN_PROMPT, "",
Replace.WORLD.with(world.getName()))));
}
@@ -97,15 +91,14 @@ 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
future.onSuccess(() -> worldTickDeferrer
.deferWorldTick(() -> doWorldRegening(issuer, world, parsedFlags, worldPlayers)))
.onFailure(() -> issuer.sendError(MVCorei18n.GENERIC_TELEPORTPLAYERS_FAILED));
.onFailure(() -> issuer.sendError("Failed to teleport one or more players out of the world!"));
}
private void doWorldRegening(
@@ -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,20 +58,17 @@ 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))
.onFailure(() -> issuer.sendError(MVCorei18n.GENERIC_TELEPORTPLAYERS_FAILED));
.onFailure(() -> issuer.sendError("Failed to teleport one or more players out of the world!"));
}
private void doWorldRemoving(MVCommandIssuer issuer, MultiverseWorld world, ParsedCommandFlags 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")
@@ -40,12 +40,12 @@ class SetSpawnCommand extends CoreCommand {
@Subcommand("setspawn")
@CommandPermission("multiverse.core.spawn.set")
@CommandCompletion("@flags:groupName=" + UnsafeFlags.NAME + " @flags:resolveUntil=arg1,groupName=" + UnsafeFlags.NAME)
@Syntax("[worldname:x,y,z[,yaw,pitch]] [--unsafe]")
@Syntax("[worldname:x,y,z[,pitch,yaw]] [--unsafe]")
@Description("{@@mv-core.setspawn.description}")
void onSetSpawnCommand(
MVCommandIssuer issuer,
@Syntax("[worldname:x,y,z[,yaw,pitch]]")
@Syntax("[worldname:x,y,z[,pitch,yaw]]")
@Description("{@@mv-core.setspawn.location.description}")
PlayerLocation playerLocation,
@@ -62,7 +62,7 @@ final class SpawnCommand extends CoreCommand {
@CommandPermission("@mvspawn")
@CommandCompletion("@playersarray:checkPermissions=@mvspawnother|@flags:resolveUntil=arg1,groupName=" + UnsafeFlags.NAME
+ " @flags:groupName=" + UnsafeFlags.NAME)
@Syntax("[player] [--unsafe]")
@Syntax("[player]")
@Description("{@@mv-core.spawn.description}")
void onSpawnTpCommand(
MVCommandIssuer issuer,
@@ -88,11 +88,11 @@ final class SpawnCommand extends CoreCommand {
List<Entity> entities, boolean checkSafety) {
LoadedMultiverseWorld mvWorld = worldManager.getLoadedWorld(world).getOrNull();
if (mvWorld == null) {
issuer.sendMessage(MVCorei18n.SPAWN_NOTMVWORLD, Replace.WORLD.with(world.getName()));
issuer.sendMessage("The world '" + world.getName() + "' is not a multiverse world!");
return;
}
if (!permissionsChecker.checkSpawnPermission(issuer.getIssuer(), entities, mvWorld)) {
issuer.sendMessage(MVCorei18n.SPAWN_NOPERMISSION);
issuer.sendMessage("You do not have permission to use this command in this world!");
return;
}
@@ -129,18 +129,16 @@ final class SpawnCommand extends CoreCommand {
.checkSafety(checkSafety)
.teleport(entities)
.onSuccessCount(successCount -> issuer.sendMessage(MVCorei18n.SPAWN_SUCCESS,
Replace.PLAYER.with(Message.of(MVCorei18n.GENERIC_PLAYERCOUNT,
Replace.COUNT.with(successCount))),
Replace.PLAYER.with(successCount + " players"), //todo: replace this with localised "{count} players"
Replace.WORLD.with(mvWorld.getName())))
.onFailureCount(reasonsCountMap -> {
for (var entry : reasonsCountMap.entrySet()) {
Logging.finer("Failed to teleport %s players to %s: %s",
entry.getValue(), mvWorld.getName(), entry.getKey());
issuer.sendError(MVCorei18n.SPAWN_FAILED,
Replace.PLAYER.with(Message.of(MVCorei18n.GENERIC_PLAYERCOUNT,
Replace.COUNT.with(entry.getValue()))),
Replace.PLAYER.with(entry.getValue() + " players"),
Replace.WORLD.with(mvWorld.getName()),
Replace.REASON.with(Message.of(entry.getKey())));
Replace.REASON.with(entry.getKey().getMessageKey()));
}
});
}
@@ -59,7 +59,7 @@ final class TeleportCommand extends CoreCommand {
@CommandCompletion("@playersarray:checkPermissions=@mvteleportother|@destinations:byIssuerForArg=arg1 "
+ "@destinations:notByIssuerForArg=arg1|@flags:byIssuerForArg=arg1,groupName=" + Flags.NAME + " "
+ "@flags:notByIssuerForArg=arg1,groupName=" + Flags.NAME)
@Syntax("[player] <destination> [--unsafe --silent]")
@Syntax("[player] <destination> [--unsafe]")
@Description("{@@mv-core.teleport.description}")
void onTeleportCommand(
MVCommandIssuer issuer,
@@ -74,7 +74,7 @@ final class TeleportCommand extends CoreCommand {
DestinationInstance<?, ?> destination,
@Optional
@Syntax("[--unsafe --silent]")
@Syntax("[--unsafe]")
@Description("")
String[] flagArray) {
Player[] players = playersValue.value();
@@ -94,9 +94,10 @@ final class TeleportCommand extends CoreCommand {
DestinationInstance<?, ?> destination,
ParsedCommandFlags parsedFlags) {
if (!permissionsChecker.checkTeleportPermission(issuer.getIssuer(), player, destination)) {
issuer.sendError(player == issuer.getPlayer()
? MVCorei18n.TELEPORT_NOPERMISSION_SELF
: MVCorei18n.TELEPORT_NOPERMISSION_OTHER);
// TODO localize
issuer.sendMessage(player == issuer.getPlayer()
? "You do not have permission to teleport yourself!"
: "You do not have permission to teleport other players!");
return;
}
@@ -133,7 +134,8 @@ final class TeleportCommand extends CoreCommand {
DestinationInstance<?, ?> destination,
ParsedCommandFlags parsedFlags) {
if (!permissionsChecker.checkTeleportPermission(issuer.getIssuer(), Arrays.asList(players), destination)) {
issuer.sendError(MVCorei18n.TELEPORT_NOPERMISSION_ALL);
// TODO localize
issuer.sendMessage("You do not have permission to teleport all these players!");
return;
}
@@ -147,8 +149,7 @@ final class TeleportCommand extends CoreCommand {
return;
}
issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(Message.of(MVCorei18n.GENERIC_PLAYERCOUNT,
Replace.COUNT.with(successCount))),
Replace.PLAYER.with(successCount + " players"),
Replace.DESTINATION.with(destination.getDisplayMessage()));
})
.onFailureCount(reasonsCountMap -> {
@@ -156,8 +157,7 @@ final class TeleportCommand extends CoreCommand {
Logging.finer("Failed to teleport %s players to %s: %s",
entry.getValue(), destination, entry.getKey());
issuer.sendError(MVCorei18n.TELEPORT_FAILED,
Replace.PLAYER.with(Message.of(MVCorei18n.GENERIC_PLAYERCOUNT,
Replace.COUNT.with(entry.getValue()))),
Replace.PLAYER.with(entry.getValue() + " players"),
Replace.DESTINATION.with(destination.getDisplayMessage()),
Replace.REASON.with(Message.of(entry.getKey())));
}
@@ -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-unload-bukkit-world] [--no-save]")
@Syntax("<world>")
@Description("{@@mv-core.unload.description}")
void onUnloadCommand(
MVCommandIssuer issuer,
@@ -62,20 +58,19 @@ class UnloadCommand extends CoreCommand {
LoadedMultiverseWorld world,
@Optional
@Syntax("[--remove-players [destination]] [--no-unload-bukkit-world] [--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))
.onFailure(() -> issuer.sendError(MVCorei18n.GENERIC_TELEPORTPLAYERS_FAILED));
.onFailure(() -> issuer.sendError("Failed to teleport one or more players out of the world!"));
}
private void doWorldUnloading(MVCommandIssuer issuer, LoadedMultiverseWorld world, ParsedCommandFlags 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,24 +33,25 @@ 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")
void onWorldBorderCenter(
MVCommandIssuer issuer,
@Syntax("<x>")
@Syntax("[x]")
double x,
@Syntax("<z>")
@Syntax("[z]")
double z,
@Flags("resolve=issuerAware,maxArgForAware=0")
@@ -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,36 +201,27 @@ 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(MVCorei18n.GENERIC_ERROR_DETAILS, Replace.ERROR.with(error)));
.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);
@@ -246,16 +236,6 @@ public final class CoreConfig {
return configHandle.get(configNodes.endWorldNameFormat);
}
@ApiStatus.AvailableSince("5.8")
public Try<Void> setWarnAliasConflicts(boolean warnAliasConflicts) {
return configHandle.set(configNodes.warnAliasConflicts, warnAliasConflicts);
}
@ApiStatus.AvailableSince("5.8")
public boolean getWarnAliasConflicts() {
return configHandle.get(configNodes.warnAliasConflicts);
}
/**
* {@inheritDoc}
*/
@@ -513,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}
*/
@@ -661,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);
}
@@ -25,8 +25,6 @@ import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.dynamiclistener.EventPriorityMapper;
import org.mvplugins.multiverse.core.event.MVDebugModeEvent;
import org.mvplugins.multiverse.core.exceptions.MultiverseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.permissions.PermissionUtils;
import org.mvplugins.multiverse.core.teleportation.PassengerModes;
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder.DimensionFormat;
@@ -137,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.")
@@ -200,15 +188,6 @@ final class CoreConfigNodes {
.stringParser(DimensionFormatNodeStringParser.INSTANCE)
.build());
final ConfigNode<Boolean> warnAliasConflicts = node(ConfigNode.builder("world.warn-alias-conflicts", Boolean.class)
.comment("")
.comment("Sets whether Multiverse will warn about alias duplicates when adding worlds or modifying aliases.")
.comment("Although not enforced, it is highly recommended to not have multiple worlds with the same alias or")
.comment("alias matching another world's name as it can cause confusion during world listing and selection in commands.")
.defaultValue(true)
.name("warn-alias-conflicts")
.build());
private final ConfigHeaderNode teleportHeader = node(ConfigHeaderNode.builder("teleport")
.comment("")
.comment("")
@@ -231,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.")
@@ -382,8 +358,7 @@ final class CoreConfigNodes {
.defaultValue(128)
.name("custom-portal-search-radius")
.validator(value -> value < 0
? Try.failure(new MultiverseException(
Message.of(MVCorei18n.CORE_CONFIG_CUSTOMPORTALSEARCHRADIUS_NONNEGATIVE), null))
? Try.failure(new MultiverseException("The value must be greater than or equal to 0.", null))
: Try.success(null))
.build());
@@ -415,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.")
@@ -499,8 +457,7 @@ final class CoreConfigNodes {
.defaultValue(30)
.name("confirm-timeout")
.validator(value -> (value <= 0)
? Try.failure(new MultiverseException(
Message.of(MVCorei18n.CORE_CONFIG_CONFIRMTIMEOUT_POSITIVE)))
? Try.failure(new MultiverseException("Confirm timeout must be a positive number!"))
: Try.success(null))
.build());
@@ -571,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("")
@@ -631,8 +576,7 @@ final class CoreConfigNodes {
.name("global-debug")
.suggester(input -> List.of("0", "1", "2", "3"))
.validator(value -> (value < 0 || value > 3)
? Try.failure(new MultiverseException(
Message.of(MVCorei18n.CORE_CONFIG_DEBUGLEVEL_RANGE)))
? Try.failure(new MultiverseException("Debug level must be between 0 and 3."))
: Try.success(null))
.onLoadAndChange((oldValue, newValue) -> {
if (newValue != Logging.getDebugLevel()) {
@@ -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}
*/
@@ -492,7 +474,6 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
* @deprecated Use {@link #onLoadAndChange(NodeChangeCallback)} instead.
*/
@Deprecated(since = "5.4", forRemoval = true)
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public @NotNull B onSetValue(@NotNull BiConsumer<T, T> onSetValue) {
return onLoadAndChange(onSetValue::accept);
}
@@ -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,607 +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.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
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;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* 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(Message.of(MVCorei18n.CONFIG_NODE_INVALIDYAMLKEY,
replace("{key}").with(key)));
}
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);
}
@@ -8,10 +8,6 @@ import co.aikar.commands.ACFUtil;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.mvplugins.multiverse.core.exceptions.MultiverseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* Provides default string parsers for common types.
@@ -55,33 +51,24 @@ public final class DefaultStringParserProvider {
() -> switch (String.valueOf(input).toLowerCase(Locale.ENGLISH)) {
case "t", "true", "on", "y", "yes", "1", "allow" -> true;
case "f", "false", "off", "n", "no", "0", "deny" -> false;
default -> throw new MultiverseException(Message.of(MVCorei18n.CONFIG_STRING_PARSER_INVALIDBOOLEAN,
replace("{input}").with(input)));
default -> throw new MultiverseException("Unable to convert '" + input + "' to boolean. Please use 'true' or 'false'");
});
private static final NodeStringParser<Integer> INTEGER_STRING_PARSER = (input, type) -> Try.of(
() -> ACFUtil.parseInt(input))
.flatMap(number -> Option.of(number).toTry(() -> new MultiverseException(Message.of(
MVCorei18n.CONFIG_STRING_PARSER_INVALIDINTEGER,
replace("{input}").with(input)))));
.flatMap(number -> Option.of(number).toTry(() -> new MultiverseException("Unable to convert '" + input + "' to number. (integer)")));
private static final NodeStringParser<Double> DOUBLE_STRING_PARSER = (input, type) -> Try.of(
() -> ACFUtil.parseDouble(input))
.flatMap(number -> Option.of(number).toTry(() -> new MultiverseException(Message.of(
MVCorei18n.CONFIG_STRING_PARSER_INVALIDDOUBLE,
replace("{input}").with(input)))));
.flatMap(number -> Option.of(number).toTry(() -> new MultiverseException("Unable to convert '" + input + "' to number. (double)")));
private static final NodeStringParser<Float> FLOAT_STRING_PARSER = (input, type) -> Try.of(
() -> ACFUtil.parseFloat(input))
.flatMap(number -> Option.of(number).toTry(() -> new MultiverseException(Message.of(
MVCorei18n.CONFIG_STRING_PARSER_INVALIDFLOAT,
replace("{input}").with(input)))));
.flatMap(number -> Option.of(number).toTry(() -> new MultiverseException("Unable to convert '" + input + "' to number. (float)")));
private static final NodeStringParser<Long> LONG_STRING_PARSER = (input, type) -> Try.of(
() -> ACFUtil.parseLong(input))
.flatMap(number -> Option.of(number).toTry(() -> new MultiverseException(Message.of(
MVCorei18n.CONFIG_STRING_PARSER_INVALIDLONG,
replace("{input}").with(input)))));
.flatMap(number -> Option.of(number).toTry(() -> new MultiverseException("Unable to convert '" + input + "' to number. (long)")));
static {
addDefaultStringParser(String.class, STRING_STRING_PARSER);
@@ -7,11 +7,8 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* Destination instance implementation for the {@link AnchorDestination}.
*/
@@ -72,7 +69,8 @@ public final class AnchorDestinationInstance extends DestinationInstance<AnchorD
*/
@Override
public @NotNull Message getDisplayMessage() {
return Message.of(MVCorei18n.DESTINATION_ANCHOR_DISPLAY, replace("{anchor}").with(anchorName));
//TODO Localize
return Message.of("anchor '" + anchorName + "'");
}
/**
@@ -9,9 +9,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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;
/**
* Destination instance implementation for the {@link BedDestination}.
@@ -72,9 +70,8 @@ public final class BedDestinationInstance extends DestinationInstance<BedDestina
*/
@Override
public @NotNull Message getDisplayMessage() {
return player == null
? Message.of(MVCorei18n.DESTINATION_BED_DISPLAY_OWN)
: Message.of(MVCorei18n.DESTINATION_BED_DISPLAY_OTHER, Replace.PLAYER.with(player.getName()));
//TODO Localize
return Message.of(player == null ? "your bed/respawn point" : player.getName() + "'s bed/respawn point");
}
/**
@@ -96,14 +96,12 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
return Attempt.failure(InstanceFailureReason.INVALID_FORMAT);
}
String worldNameOrAlias = items[0];
String worldName = items[0];
String positionStr = items[1];
World world = getLoadedMultiverseWorld(worldNameOrAlias)
.flatMap(LoadedMultiverseWorld::getBukkitWorld)
.getOrNull();
World world = getLoadedMultiverseWorld(worldName).flatMap(LoadedMultiverseWorld::getBukkitWorld).getOrNull();
if (world == null) {
return Attempt.failure(InstanceFailureReason.WORLD_NOT_FOUND, Replace.WORLD.with(worldNameOrAlias));
return Attempt.failure(InstanceFailureReason.WORLD_NOT_FOUND, Replace.WORLD.with(worldName));
}
EntityPosition position;
@@ -113,7 +111,7 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
return Attempt.failure(InstanceFailureReason.INVALID_NUMBER_FORMAT, Replace.ERROR.with(e));
}
return Attempt.success(new ExactDestinationInstance(this, worldManager, world.getName(), position));
return Attempt.success(new ExactDestinationInstance(this, worldManager, worldName, position));
}
//TODO: Extract to a world finder class
@@ -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}
*/
@@ -2,6 +2,8 @@ package org.mvplugins.multiverse.core.display.handlers;
import java.util.List;
import co.aikar.commands.BukkitCommandIssuer;
import org.bukkit.ChatColor;
import org.bukkit.command.ConsoleCommandSender;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
@@ -67,12 +69,12 @@ public class PagedSendHandler extends BaseSendHandler<PagedSendHandler> {
private void sendPaged(@NotNull MVCommandIssuer issuer, @NotNull List<String> content) {
int totalPages = (content.size() + linesPerPage - 1) / linesPerPage; // Basically just divide round up
if (targetPage < 1 || targetPage > totalPages) {
issuer.sendMessage(MVCorei18n.CONTENTDISPLAY_INVALIDPAGE, replace("{total}").with(totalPages));
issuer.sendMessage(String.format("%sInvalid page number. Please enter a page number between 1 and %s", ChatColor.RED, totalPages));
return;
}
if (filter.needToFilter()) {
issuer.sendMessage(MVCorei18n.CONTENTDISPLAY_PAGEFILTER,
issuer.sendMessage("{page} {filter}",
replace("{page}").with(Message.of(MVCorei18n.CONTENTDISPLAY_PAGE,
replace("{current}").with(targetPage),
replace("{total}").with(totalPages))),
@@ -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,5 +1,6 @@
package org.mvplugins.multiverse.core.exceptions;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.locale.message.LocalizableMessage;
@@ -1,9 +1,6 @@
package org.mvplugins.multiverse.core.exceptions.command;
import co.aikar.commands.InvalidCommandArgument;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.locale.message.LocalizableMessage;
import org.mvplugins.multiverse.core.locale.message.LocalizedMessage;
import org.mvplugins.multiverse.core.locale.message.Message;
@@ -12,41 +9,21 @@ 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) {
if (throwable instanceof LocalizableMessage localizableMessage) {
Message message = localizableMessage.getLocalizableMessage();
return message == null
? new MVInvalidCommandArgument(throwable.getLocalizedMessage(), showSyntax)
: of(message, showSyntax);
}
return new MVInvalidCommandArgument(throwable.getLocalizedMessage(), showSyntax);
}
public static MVInvalidCommandArgument of(@NotNull Message message) {
public static MVInvalidCommandArgument of(Message message) {
return of(message, true);
}
public static MVInvalidCommandArgument of(@NotNull Message message, boolean showSyntax) {
public static MVInvalidCommandArgument of(Message message, boolean showSyntax) {
return message instanceof LocalizedMessage
? new MVInvalidCommandArgument((LocalizedMessage) message, showSyntax)
: new MVInvalidCommandArgument(message, showSyntax);
}
private MVInvalidCommandArgument(String message, boolean showSyntax) {
super(message, showSyntax);
}
private MVInvalidCommandArgument(Message message, boolean showSyntax) {
super(message.formatted(), showSyntax);
}
private MVInvalidCommandArgument(LocalizedMessage message, boolean showSyntax) {
super(message.getMessageKey(), showSyntax, message.getRawReplacements());
super(message.getMessageKey(), showSyntax, message.getReplacements());
}
}
@@ -7,10 +7,8 @@
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.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
@@ -20,19 +18,12 @@ import org.bukkit.event.entity.EntityRegainHealthEvent;
import org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason;
import org.bukkit.event.entity.EntitySpawnEvent;
import org.bukkit.event.entity.FoodLevelChangeEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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.utils.compatibility.EntityCompatibility;
import org.mvplugins.multiverse.core.world.WorldManager;
import java.util.Arrays;
/**
* Multiverse's Entity {@link Listener}.
*/
@@ -86,34 +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 (isAlwaysAllowedSpawnReason(event.getReason())) {
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.
*
@@ -126,13 +89,16 @@ final class MVEntityListener implements CoreListener {
}
// Always allow custom command and plugins to spawn creatures
if (isAlwaysAllowedSpawnReason(event.getSpawnReason())) {
if (event.getSpawnReason() == SpawnReason.CUSTOM
|| event.getSpawnReason() == SpawnReason.COMMAND
|| event.getSpawnReason() == SpawnReason.BREEDING
|| event.getSpawnReason() == SpawnReason.SPAWNER_EGG) {
return;
}
worldManager.getLoadedWorld(event.getEntity().getWorld())
.peek(world -> {
if (!world.getEntitySpawnConfig().shouldAllowSpawn(event.getEntityType())) {
if (!world.getEntitySpawnConfig().shouldAllowSpawn(event.getEntity())) {
Logging.finest("Cancelling Creature Spawn Event for: " + event.getEntity());
event.setCancelled(true);
}
@@ -151,34 +117,10 @@ final class MVEntityListener implements CoreListener {
worldManager.getLoadedWorld(event.getEntity().getWorld())
.peek(world -> {
if (!world.getEntitySpawnConfig().shouldAllowSpawn(event.getEntityType())) {
if (!world.getEntitySpawnConfig().shouldAllowSpawn(event.getEntity())) {
Logging.finest("Cancelling Entity Spawn Event for: " + event.getEntity());
event.setCancelled(true);
}
});
}
@EventMethod
void chunkLoad(ChunkLoadEvent event) {
worldManager.getLoadedWorld(event.getWorld())
.peek(world -> {
long count = Arrays.stream(event.getChunk().getEntities())
.filter(entity -> !(entity instanceof Player))
.filter(entity -> !isAlwaysAllowedSpawnReason(EntityCompatibility.getEntitySpawnReason(entity)))
.filter(entity -> !world.getEntitySpawnConfig().shouldAllowSpawn(entity))
.peek(Entity::remove)
.count();
if (count > 0) {
Logging.finest("Removed %d entities from chunk %d, %d in world %s due to spawn category settings.",
count, event.getChunk().getX(), event.getChunk().getZ(), event.getWorld().getName());
}
});
}
private boolean isAlwaysAllowedSpawnReason(@Nullable SpawnReason reason) {
return reason == SpawnReason.CUSTOM
|| reason == SpawnReason.COMMAND
|| reason == SpawnReason.BREEDING
|| reason == SpawnReason.SPAWNER_EGG;
}
}
@@ -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.",
@@ -20,25 +20,6 @@ public enum MVCorei18n implements MessageKeyProvider {
// configuration
CONFIG_SAVE_FAILED,
CONFIG_NODE_NOTFOUND,
CONFIG_NODE_INVALIDYAMLKEY,
CONFIG_STRING_PARSER_INVALIDBOOLEAN,
CONFIG_STRING_PARSER_INVALIDINTEGER,
CONFIG_STRING_PARSER_INVALIDDOUBLE,
CONFIG_STRING_PARSER_INVALIDFLOAT,
CONFIG_STRING_PARSER_INVALIDLONG,
// core config
CORE_CONFIG_CUSTOMPORTALSEARCHRADIUS_NONNEGATIVE,
CORE_CONFIG_CONFIRMTIMEOUT_POSITIVE,
CORE_CONFIG_DEBUGLEVEL_RANGE,
// /mv anchor
ANCHOR_DELETE_SUCCESS,
ANCHOR_DELETE_FAILURE,
ANCHOR_LIST_HEADER,
ANCHOR_LIST_ENTRY,
ANCHOR_SET_SUCCESS,
ANCHOR_SET_FAILURE,
// /mv check
CHECK_DESCRIPTION,
@@ -110,18 +91,8 @@ public enum MVCorei18n implements MessageKeyProvider {
// /mv dumps
DUMPS_DESCRIPTION,
DUMPS_STARTING,
DUMPS_URL_LIST,
// /mv entity-spawn-config
ENTITYSPAWNCONFIG_INFO_HEADER,
ENTITYSPAWNCONFIG_INFO_CATEGORY,
ENTITYSPAWNCONFIG_INFO_SPAWN,
ENTITYSPAWNCONFIG_INFO_TICKRATE,
ENTITYSPAWNCONFIG_INFO_EXCEPTIONS,
ENTITYSPAWNCONFIG_MODIFY_SUCCESS,
ENTITYSPAWNCONFIG_MODIFY_FAILURE,
// /mv gamerule set
GAMERULE_SET_DESCRIPTION,
GAMERULE_SET_GAMERULE_DESCRIPTION,
@@ -148,12 +119,7 @@ public enum MVCorei18n implements MessageKeyProvider {
// /mv generators
GENERATORS_DESCRIPTION,
GENERATORS_DESCRIPTION_FLAGS,
GENERATORS_HEADER,
GENERATORS_EMPTY,
GENERATORS_INFO_PLUGIN,
GENERATORS_INFO_EXAMPLEUSAGES,
GENERATORS_INFO_USAGES,
GENERATORS_INFO_INFOLINK,
// /mv import
IMPORT_DESCRIPTION,
@@ -180,19 +146,6 @@ public enum MVCorei18n implements MessageKeyProvider {
LOAD_LOADING,
LOAD_SUCCESS,
// /mv meta info
META_INFO_DESCRIPTION,
META_INFO_WORLD_DESCRIPTION,
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,
@@ -205,13 +158,6 @@ public enum MVCorei18n implements MessageKeyProvider {
MODIFY_FAILURE,
MODIFY_FAILURE_NOVALUE,
// /mv purge-all-entities
PURGEALLENTITIES_SUCCESS,
PURGEALLENTITIES_SUCCESS_CATEGORIES,
// /mv purge-entities
PURGEENTITIES_SUCCESS,
// /mv regen
REGEN_DESCRIPTION,
REGEN_WORLD_DESCRIPTION,
@@ -246,8 +192,6 @@ public enum MVCorei18n implements MessageKeyProvider {
// /mv spawn
SPAWN_DESCRIPTION,
SPAWN_PLAYER_DESCRIPTION,
SPAWN_NOTMVWORLD,
SPAWN_NOPERMISSION,
SPAWN_SUCCESS,
SPAWN_FAILED,
@@ -256,9 +200,6 @@ public enum MVCorei18n implements MessageKeyProvider {
TELEPORT_PLAYER_DESCRIPTION,
TELEPORT_DESTINATION_DESCRIPTION,
TELEPORT_TOOMANYPLAYERS,
TELEPORT_NOPERMISSION_SELF,
TELEPORT_NOPERMISSION_OTHER,
TELEPORT_NOPERMISSION_ALL,
TELEPORT_SUCCESS,
TELEPORT_FAILED,
@@ -309,26 +250,9 @@ public enum MVCorei18n implements MessageKeyProvider {
WORLDBORDER_WARNINGTIME_NOTHINGCHANGED,
WORLDBORDER_WARNINGTIME_SUCCESS,
WORLDBORDER_ROUNDOFF_WARNING,
// commands error
COMMANDS_ERROR_PLAYERSONLY,
COMMANDS_ERROR_MULTIVERSEWORLDONLY,
COMMANDS_ERROR_LOADEDMULTIVERSEWORLD_PLAYERSONLY,
COMMANDS_ERROR_LOADEDMULTIVERSEWORLD_ISSUER,
COMMANDS_ERROR_LOADEDMULTIVERSEWORLD_INPUTCONSOLE,
COMMANDS_ERROR_LOADEDMULTIVERSEWORLD_INPUT,
COMMANDS_ERROR_MULTIVERSEWORLD_PLAYERSONLY,
COMMANDS_ERROR_MULTIVERSEWORLD_ISSUER,
COMMANDS_ERROR_MULTIVERSEWORLD_INPUTCONSOLE,
COMMANDS_ERROR_MULTIVERSEWORLD_INPUT,
COMMANDS_ERROR_PLAYER_ISSUERINPUT,
COMMANDS_ERROR_PLAYER_INPUT,
COMMANDS_ERROR_PLAYER_SELECTOR,
COMMANDS_ERROR_PLAYERS_INPUT,
COMMANDS_ERROR_RESOLVE_PLAYER,
COMMANDS_ERROR_RESOLVE_INPUT,
COMMANDS_ERROR_REMOVEPLAYERS_NODEFAULT,
// entry check
ENTRYCHECK_BLACKLISTED,
@@ -345,10 +269,7 @@ public enum MVCorei18n implements MessageKeyProvider {
// multiverse parse destination failure reason
DESTINATION_ANCHOR_FAILUREREASON_ANCHORNOTFOUND,
DESTINATION_ANCHOR_DISPLAY,
DESTINATION_BED_FAILUREREASON_PLAYERNOTFOUND,
DESTINATION_BED_DISPLAY_OWN,
DESTINATION_BED_DISPLAY_OTHER,
DESTINATION_CANNON_FAILUREREASON_INVALIDFORMAT,
DESTINATION_EXACT_FAILUREREASON_INVALIDFORMAT,
DESTINATION_PLAYER_FAILUREREASON_PLAYERNOTFOUND,
@@ -357,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,
@@ -370,17 +287,11 @@ public enum MVCorei18n implements MessageKeyProvider {
TELEPORTFAILUREREASON_TELEPORT_FAILED_EXCEPTION,
TELEPORTFAILUREREASON_EVENT_CANCELLED,
// alias name conflict
ALIASNAMECONFLICT_DETECTED,
ALIASNAMECONFLICT_DUPLICATEALIAS,
ALIASNAMECONFLICT_DUPLICATEWORLDNAME,
// world manager result
CLONEWORLD_INVALIDWORLDNAME,
CLONEWORLD_WORLDEXISTFOLDER,
CLONEWORLD_WORLDEXISTUNLOADED,
CLONEWORLD_WORLDEXISTLOADED,
CLONEWORLD_FROMWORLDFOLDERINVALID,
CLONEWORLD_COPYFAILED,
CREATEWORLD_INVALIDWORLDNAME,
@@ -398,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,
@@ -419,9 +328,6 @@ public enum MVCorei18n implements MessageKeyProvider {
WORLDCREATOR_BUKKITCREATIONFAILED,
// queue command result
QUEUECOMMAND_DEFAULTPROMPT,
QUEUECOMMAND_PROMPT,
QUEUECOMMAND_EXPIRED,
QUEUECOMMAND_NOCOMMANDINQUEUE,
QUEUECOMMAND_INVALIDOTP,
QUEUECOMMAND_COMMANDEXECUTIONERROR,
@@ -430,7 +336,6 @@ public enum MVCorei18n implements MessageKeyProvider {
CONTENTDISPLAY_NOCONTENT,
CONTENTDISPLAY_FILTER,
CONTENTDISPLAY_PAGE,
CONTENTDISPLAY_PAGEFILTER,
CONTENTDISPLAY_INVALIDPAGE,
CONTENTDISPLAY_NULL,
CONTENTDISPLAY_EMPTY,
@@ -446,21 +351,12 @@ 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,
GENERIC_ERROR,
GENERIC_ERROR_DETAILS,
GENERIC_NULL,
GENERIC_YOU,
GENERIC_PLAYERCOUNT,
GENERIC_TELEPORTPLAYERS_FAILED,
;
// END CHECKSTYLE-SUPPRESSION: Javadoc
@@ -1,23 +1,17 @@
package org.mvplugins.multiverse.core.locale.message;
import java.util.Objects;
import co.aikar.commands.ACFUtil;
import co.aikar.commands.CommandIssuer;
import co.aikar.commands.Locales;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class LocalizedMessage extends Message implements MessageKeyProvider {
private static @Nullable Locales locales;
@ApiStatus.Internal
public static void setDefaultLocalesManager(@Nullable Locales locales) {
LocalizedMessage.locales = locales;
}
private final @NotNull MessageKeyProvider messageKeyProvider;
LocalizedMessage(
@@ -28,33 +22,11 @@ public final class LocalizedMessage extends Message implements MessageKeyProvide
this.messageKeyProvider = messageKeyProvider;
}
/**
* {@inheritDoc}
*/
@Override
public MessageKey getMessageKey() {
return messageKeyProvider.getMessageKey();
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull String[] getReplacements() {
return locales == null ? super.getReplacements() : getReplacements(locales, null);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull String formatted() {
return locales == null ? super.formatted() : formatted(locales, null);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull String formatted(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) {
String[] parsedReplacements = getReplacements(locales, commandIssuer);
@@ -6,11 +6,9 @@ import co.aikar.commands.ACFUtil;
import co.aikar.commands.CommandIssuer;
import co.aikar.commands.Locales;
import co.aikar.locales.MessageKeyProvider;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.command.MVCommandManager;
/**
* A message that can be formatted with replacements and localized.
@@ -83,45 +81,15 @@ public sealed class Message permits LocalizedMessage {
}
/**
* Gets the replacements for this message. This is the raw, non-localized parsing of replacements.
* Gets the replacements for this message.
* <br/>
* This array is guaranteed to be of even length and suitable for use with
* {@link ACFUtil#replaceStrings(String, String...)}.
*
* @return The replacements
*
* @since 5.8
*/
@ApiStatus.AvailableSince("5.8")
public @NotNull String[] getRawReplacements() {
return toReplacementsArray(replacements);
}
/**
* Gets the replacements for this message with localization support when the plugin has loaded and injected its
* default {@link MVCommandManager#getLocales()} instance.
* Otherwise, it will fall back to use {@link getRawReplacements()}.
*
* @return The replacements
*/
public @NotNull String[] getReplacements() {
return getRawReplacements();
}
/**
* Gets the replacements for this message with localization support based on the command issuer's locale config.
* <br/>
* This array is guaranteed to be of even length and suitable for use with
* {@link ACFUtil#replaceStrings(String, String...)}.
*
* @param issuer The command issuer the message is for
* @return The replacements
*
* @since 5.8
*/
@ApiStatus.AvailableSince("5.8")
public @NotNull String[] getReplacements(@NotNull CommandIssuer issuer) {
return getReplacements(issuer.getManager().getLocales(), issuer);
return toReplacementsArray(replacements);
}
/**
@@ -147,35 +115,19 @@ public sealed class Message permits LocalizedMessage {
return message;
}
/**
* Gets the raw formatted message.
* <br/>
* This is the raw, non-localized message with replacements applied.
* This method will never use the locale message key.
*
* @return The formatted message
*
* @since 5.8
*/
@ApiStatus.AvailableSince("5.8")
public @NotNull String rawFormatted() {
String[] parsedReplacements = getRawReplacements();
if (parsedReplacements.length == 0) {
return raw();
}
return ACFUtil.replaceStrings(message, parsedReplacements);
}
/**
* Gets the formatted message.
* <br/>
* This is the localized message with replacements applied when the plugin has loaded and injected its default
* {@link MVCommandManager#getLocales()} instance. Otherwise, it will fall back to use {@link rawFormatted()}.
* This is the raw, non-localized message with replacements applied.
*
* @return The formatted message
*/
public @NotNull String formatted() {
return rawFormatted();
String[] parsedReplacements = getReplacements();
if (parsedReplacements.length == 0) {
return raw();
}
return ACFUtil.replaceStrings(message, parsedReplacements);
}
/**
@@ -194,9 +146,9 @@ public sealed class Message permits LocalizedMessage {
* Gets the formatted message from localization data.
* <br/>
* This is the localized message with replacements applied. The message is localized using the locale of the given
* command issuer.
* command issuer, if not null.
*
* @param commandIssuer The command issuer the message is for.
* @param commandIssuer The command issuer the message is for, or null for the console (default locale)
* @return The formatted, localized message
*/
public @NotNull String formatted(@NotNull CommandIssuer commandIssuer) {
@@ -226,7 +178,7 @@ public sealed class Message permits LocalizedMessage {
int i = 0;
for (MessageReplacement replacement : replacements) {
replacementsArray[i++] = replacement.getKey();
replacementsArray[i++] = replacement.getReplacement().fold(s -> s, Message::rawFormatted);
replacementsArray[i++] = replacement.getReplacement().fold(s -> s, Message::formatted);
}
return replacementsArray;
}
@@ -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,183 +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")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@Nullable
public static Class<?> getClass(String classPath) {
try {
@@ -199,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.
*
@@ -207,12 +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")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@Nullable
public static <C> Method getMethod(Class<C> clazz, String methodName, Class<?>... parameterTypes) {
try {
@@ -232,12 +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")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@Nullable
public static <C> Method getMethod(C classInstance, String methodName, Class<?>... parameterTypes) {
return getMethod(classInstance.getClass(), methodName, parameterTypes);
@@ -252,14 +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")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@Nullable
@SuppressWarnings("unchecked")
public static <C, R> R invokeMethod(C classInstance, Method method, Object...parameters) {
try {
return (R) method.invoke(classInstance, parameters);
@@ -275,12 +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")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@Nullable
public static <C> Field getField(Class<C> clazz, String fieldName) {
try {
@@ -299,12 +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")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@Nullable
public static <C> Field getField(C classInstance, String fieldName) {
return getField(classInstance.getClass(), fieldName);
@@ -319,12 +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")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@Nullable
public static <C, V> V getFieldValue(C classInstance, @Nullable Field field, @NotNull Class<V> fieldType) {
try {
@@ -347,12 +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")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@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,21 +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) {
return parseQuotesInArgs(args, " ");
}
/**
* Parse quotes in args into a single string. E.g. ["\"my", "string\""] -> ["my string"]
*
* @param args The args to parse
* @param separator The separator to use between args when joining them
* @return The parsed args
*
* @since 5.8
*/
@ApiStatus.AvailableSince("5.8")
public static @NotNull Collection<String> parseQuotesInArgs(@NotNull String[] args, @NotNull String separator) {
public static Collection<String> parseQuotesInArgs(String[] args) {
List<String> result = new ArrayList<>(args.length);
StringBuilder current = new StringBuilder();
boolean inQuotes = false;
@@ -145,13 +108,13 @@ public final class StringFormatter {
quoteStartIndex = i;
current.append(arg.substring(1));
} else if (inQuotes && arg.endsWith("\"")) {
current.append(separator).append(arg, 0, arg.length() - 1);
current.append(" ").append(arg, 0, arg.length() - 1);
result.add(current.toString());
current.setLength(0);
inQuotes = false;
quoteStartIndex = -1;
} else if (inQuotes) {
current.append(separator).append(arg);
current.append(" ").append(arg);
} else if (arg.startsWith("\"") && arg.endsWith("\"") && arg.length() > 1) {
// Fully quoted in one token
result.add(arg.substring(1, arg.length() - 1));
@@ -176,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,40 +0,0 @@
package org.mvplugins.multiverse.core.utils.compatibility;
import org.bukkit.entity.Entity;
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
/**
* Compatibility class used to handle API changes in {@link Entity} class.
*/
// TODO: Consider making this part of the public API in v5.9.
@ApiStatus.Internal
public final class EntityCompatibility {
private static final boolean HAS_GET_ENTITY_SPAWN_REASON_METHOD;
static {
HAS_GET_ENTITY_SPAWN_REASON_METHOD = ReflectHelper.hasMethod(Entity.class, "getEntitySpawnReason");
}
/**
* Gets the reason that initially spawned the entity when supported by the server.
*
* @param entity The entity to query.
* @return The entity's spawn reason, or null when the API is unavailable.
*/
@Nullable
public static SpawnReason getEntitySpawnReason(@NotNull Entity entity) {
if (HAS_GET_ENTITY_SPAWN_REASON_METHOD) {
return entity.getEntitySpawnReason();
}
return null;
}
private EntityCompatibility() {
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, 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);
@@ -220,7 +220,7 @@ public sealed interface Result<S extends SuccessReason, F extends FailureReason>
Success(S successReason, MessageReplacement[] replacements) {
this.successReason = successReason;
this.message = Message.of(successReason, replacements);
this.message = Message.of(successReason, "Success!", replacements);
}
@Override
@@ -273,7 +273,7 @@ public sealed interface Result<S extends SuccessReason, F extends FailureReason>
Failure(F failureReason, MessageReplacement[] replacements) {
this.failureReason = failureReason;
this.message = Message.of(failureReason, replacements);
this.message = Message.of(failureReason, "Failed!", replacements);
}
@Override
@@ -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;
}
}
Loaded 100 of 186 files, more files were not shown because too many files have changed in this diff. Show more