From 0726013289a9d8ac47b6bf26767b7678d8d7759f Mon Sep 17 00:00:00 2001 From: xXJanisXx Date: Tue, 25 Aug 2026 23:15:29 +0200 Subject: [PATCH 01/17] refactor: improve build scripts and bump dependencies --- build.gradle.kts | 59 ++++++++++-------------- gradle/libs.versions.toml | 18 ++++---- gradle/wrapper/gradle-wrapper.properties | 5 +- proxy-bungeecord/build.gradle.kts | 3 +- proxy-shared/build.gradle.kts | 5 +- settings.gradle.kts | 14 ++++-- 6 files changed, 45 insertions(+), 59 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index d99292a..e3593e0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,17 +1,14 @@ -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion plugins { alias(libs.plugins.kotlin) alias(libs.plugins.shadow) } -val baseVersion = "0.0.13" -val commitHash = System.getenv("COMMIT_HASH") -val snapshotversion = "${baseVersion}-dev.$commitHash" - allprojects { - group = "app.simplecloud.plugin.proxy" - version = if (commitHash != null) snapshotversion else baseVersion + group = "app.simplecloud.plugin" + version = "0.0.13" repositories { mavenCentral() @@ -19,24 +16,17 @@ allprojects { } subprojects { - apply(plugin = "org.jetbrains.kotlin.jvm") - apply(plugin = "com.gradleup.shadow") + apply { + plugin("kotlin") + plugin("com.gradleup.shadow") + } repositories { mavenCentral() maven("https://buf.build/gen/maven") - - maven { - name = "papermc" - url = uri("https://repo.papermc.io/repository/maven-public/") - } - maven { - url = uri("https://oss.sonatype.org/content/repositories/snapshots") - } - maven { - name = "simplecloudRepositorySnapshots" - url = uri("https://repo.simplecloud.app/snapshots") - } + maven("https://repo.papermc.io/repository/maven-public/") + maven("https://oss.sonatype.org/content/repositories/snapshots") + maven("https://repo.simplecloud.app/snapshots") } dependencies { @@ -49,26 +39,25 @@ subprojects { implementation(rootProject.libs.simplecloud.plugin) } - java { - toolchain.languageVersion.set(JavaLanguageVersion.of(21)) - } - kotlin { - jvmToolchain(21) + jvmToolchain(25) compilerOptions { - apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0) + jvmTarget = JvmTarget.JVM_25 + languageVersion = KotlinVersion.KOTLIN_2_4 + apiVersion = KotlinVersion.KOTLIN_2_4 } } - tasks.named("shadowJar", ShadowJar::class) { + java { + toolchain.languageVersion.set(JavaLanguageVersion.of(25)) + } + + tasks.test { + useJUnitPlatform() + } + + tasks.shadowJar { mergeServiceFiles() archiveFileName.set("${project.name}.jar") - - relocate("com.google.protobuf", "app.simplecloud.relocate.google.protobuf") - relocate("com.google.common", "app.simplecloud.relocate.google.common") - relocate("io.grpc", "app.simplecloud.relocate.io.grpc") - relocate("org.incendo", "app.simplecloud.plugin.proxy.relocate.incendo") - relocate("org.spongepowered", "app.simplecloud.plugin.proxy.relocate.spongepowered") - relocate("app.simplecloud.plugin.api", "app.simplecloud.plugin.proxy.relocate.plugin.api") } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 234e1f6..b495f60 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,26 +1,25 @@ [versions] kotlin = "2.4.10" kotlin-coroutines = "1.11.0" -shadow = "8.3.3" -velocity = "3.6.0-SNAPSHOT" +shadow = "9.6.1" +minotaur = "2.9.0" + +velocity = "4.1.0-SNAPSHOT" bungeecord = "1.21-R0.4" -adventure = "4.26.1" +adventure = "5.2.0" adventure-platform = "4.4.1" -gson = "2.14.0" - configurate-yaml = "4.2.0" configurate-kotlin = "4.2.0" -simplecloud-api = "0.1.0-platform.52-dev.1.1-7a06605" +simplecloud-api = "0.1.0-platform.54-dev.1.1-770dcc6" simplecloud-plugin = "0.0.1-platform.1778966459069-b1e45e2" command-cloud-core = "2.1.0" -command-cloud-velocity = "2.1.0-SNAPSHOT" -command-cloud-bungeecord = "2.1.0-SNAPSHOT" -minotaur = "2.8.7" +command-cloud-velocity = "2.0.0" +command-cloud-bungeecord = "2.0.0" [libraries] kotlin-jvm = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } @@ -37,7 +36,6 @@ bungeecord = { module = "net.md-5:bungeecord-api", version.ref = "bungeecord" } adventure-legacy-serializer = { module = "net.kyori:adventure-text-serializer-legacy", version.ref = "adventure" } adventure-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure" } adventure-bungeecord-platform = { module = "net.kyori:adventure-platform-bungeecord", version.ref = "adventure-platform" } -gson = { module = "com.google.code.gson:gson", version.ref = "gson" } configurate-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate-yaml" } configurate-kotlin = { module = "org.spongepowered:configurate-extra-kotlin", version.ref = "configurate-kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 34ace61..64a5b37 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Thu Mar 21 21:19:37 CET 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/proxy-bungeecord/build.gradle.kts b/proxy-bungeecord/build.gradle.kts index c3a3df6..c45f6f9 100644 --- a/proxy-bungeecord/build.gradle.kts +++ b/proxy-bungeecord/build.gradle.kts @@ -54,8 +54,7 @@ modrinth { "26.1.2", "26.2", ) - loaders.add("bungeecord") - loaders.add("waterfall") + loaders.addAll("bungeecord", "waterfall") changelog.set("https://docs.simplecloud.app/changelog") syncBodyFrom.set(rootProject.file("README.md").readText()) } diff --git a/proxy-shared/build.gradle.kts b/proxy-shared/build.gradle.kts index 1779457..8a2458d 100644 --- a/proxy-shared/build.gradle.kts +++ b/proxy-shared/build.gradle.kts @@ -5,10 +5,7 @@ dependencies { testImplementation(rootProject.libs.adventure.minimessage) implementation(rootProject.libs.configurate.yaml) - implementation(rootProject.libs.configurate.kotlin) { - exclude(group = "org.jetbrains.kotlin") - exclude(group = "org.jetbrains.kotlinx") - } + implementation(rootProject.libs.configurate.kotlin) compileOnly(rootProject.libs.command.cloud.core) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 2670a54..5568361 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,11 @@ plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } -rootProject.name = "proxy-essentials-plugin" -include("proxy-shared") -include("proxy-velocity") -include("proxy-bungeecord") + +include( + "proxy-bungeecord", + "proxy-shared", + "proxy-velocity" +) + +rootProject.name = "proxy-essentials-plugin" \ No newline at end of file From 835aa26648892efc7108019fd990f93bc5db7b0e Mon Sep 17 00:00:00 2001 From: Janis Date: Tue, 25 Aug 2026 23:48:58 +0200 Subject: [PATCH 02/17] refactor: improve versioning --- .gitignore | 36 +---- build.gradle.kts | 13 +- gradle/libs.versions.toml | 7 +- proxy-bungeecord/build.gradle.kts | 20 +-- .../src/main/resources/plugin.yml | 3 +- .../listener/ServerKickListenerTest.kt | 120 --------------- proxy-shared/build.gradle.kts | 13 +- .../shared/config/ReloadMessageConfigTest.kt | 79 ---------- .../config/ShowKickReasonHotReloadTest.kt | 45 ------ .../format/MotdMiniMessageFormatterTest.kt | 143 ------------------ .../handler/LocalPingSourceMatcherTest.kt | 39 ----- proxy-velocity/build.gradle.kts | 25 +-- .../proxy/velocity/BuildConstants.kt.peb | 5 + .../proxy/velocity/ProxyVelocityPlugin.kt | 19 ++- .../src/main/resources/velocity-plugin.json | 14 -- .../listener/ServerKickListenerTest.kt | 96 ------------ 16 files changed, 66 insertions(+), 611 deletions(-) delete mode 100644 proxy-bungeecord/src/test/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerKickListenerTest.kt delete mode 100644 proxy-shared/src/test/kotlin/app/simplecloud/plugin/proxy/shared/config/ReloadMessageConfigTest.kt delete mode 100644 proxy-shared/src/test/kotlin/app/simplecloud/plugin/proxy/shared/config/ShowKickReasonHotReloadTest.kt delete mode 100644 proxy-shared/src/test/kotlin/app/simplecloud/plugin/proxy/shared/format/MotdMiniMessageFormatterTest.kt delete mode 100644 proxy-shared/src/test/kotlin/app/simplecloud/plugin/proxy/shared/handler/LocalPingSourceMatcherTest.kt create mode 100644 proxy-velocity/src/main/kotlin-templates/app/simplecloud/plugin/proxy/velocity/BuildConstants.kt.peb delete mode 100644 proxy-velocity/src/main/resources/velocity-plugin.json delete mode 100644 proxy-velocity/src/test/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ServerKickListenerTest.kt diff --git a/.gitignore b/.gitignore index f1a3c39..27b634f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,43 +1,11 @@ .gradle build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/**/build/ -!**/src/test/**/build/ ### IntelliJ IDEA ### -.idea/modules.xml -.idea/jarRepositories.xml -.idea/compiler.xml -.idea/libraries/ -*.iws -*.iml -*.ipr -out/ -!**/src/main/**/out/ -!**/src/test/**/out/ - -### Eclipse ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache -bin/ -!**/src/main/**/bin/ -!**/src/test/**/bin/ - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ +.idea/ ### VS Code ### .vscode/ ### Mac OS ### -.DS_Store -.idea/ \ No newline at end of file +.DS_Store \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index e3593e0..0b88c65 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,9 +30,9 @@ subprojects { } dependencies { - implementation(rootProject.libs.kotlin.jvm) implementation(rootProject.libs.kotlin.reflect) - implementation(rootProject.libs.kotlin.coroutines) + implementation(rootProject.libs.kotlinx.coroutines.core) + testImplementation(rootProject.libs.kotlin.test) compileOnly(rootProject.libs.simplecloud.api) @@ -60,4 +60,13 @@ subprojects { mergeServiceFiles() archiveFileName.set("${project.name}.jar") } + + tasks.processResources { + filesMatching("plugin.yml") { + expand( + "version" to project.version + ) + } + } + } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b495f60..86914d2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,10 @@ [versions] kotlin = "2.4.10" -kotlin-coroutines = "1.11.0" +kotlinx-coroutines = "1.11.0" shadow = "9.6.1" minotaur = "2.9.0" +blossom = "2.2.0" velocity = "4.1.0-SNAPSHOT" bungeecord = "1.21-R0.4" @@ -22,10 +23,9 @@ command-cloud-velocity = "2.0.0" command-cloud-bungeecord = "2.0.0" [libraries] -kotlin-jvm = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } -kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } command-cloud-core = { module = "org.incendo:cloud-core", version.ref = "command-cloud-core" } command-cloud-velocity = { module = "org.incendo:cloud-velocity", version.ref = "command-cloud-velocity" } @@ -46,3 +46,4 @@ simplecloud-plugin = { module = "app.simplecloud.plugin:plugin-shared", version. kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } minotaur = { id = "com.modrinth.minotaur", version.ref = "minotaur" } +blossom = { id = "net.kyori.blossom", version.ref = "blossom" } \ No newline at end of file diff --git a/proxy-bungeecord/build.gradle.kts b/proxy-bungeecord/build.gradle.kts index c45f6f9..e005b14 100644 --- a/proxy-bungeecord/build.gradle.kts +++ b/proxy-bungeecord/build.gradle.kts @@ -4,23 +4,17 @@ plugins { dependencies { api(project(":proxy-shared")) - - compileOnly(rootProject.libs.bungeecord) - - testImplementation(kotlin("test")) - testImplementation(rootProject.libs.bungeecord) - - implementation(rootProject.libs.adventure.legacy.serializer) - implementation(rootProject.libs.adventure.minimessage) - implementation(rootProject.libs.adventure.bungeecord.platform) - - implementation(rootProject.libs.command.cloud.core) - implementation(rootProject.libs.command.cloud.bungeecord) + compileOnly(libs.bungeecord) + implementation(libs.adventure.legacy.serializer) + implementation(libs.adventure.minimessage) + implementation(libs.adventure.bungeecord.platform) + implementation(libs.command.cloud.core) + implementation(libs.command.cloud.bungeecord) } tasks.shadowJar { - relocate("net.kyori", "app.simplecloud.relocate.kyori") + relocate("net.kyori.adventure", "app.simplecloud.relocate.kyori") } modrinth { diff --git a/proxy-bungeecord/src/main/resources/plugin.yml b/proxy-bungeecord/src/main/resources/plugin.yml index e4a46e7..fbb77e3 100644 --- a/proxy-bungeecord/src/main/resources/plugin.yml +++ b/proxy-bungeecord/src/main/resources/plugin.yml @@ -1,5 +1,6 @@ name: proxy-essentials-bungeecord main: app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin -version: 1.0.0 +version: ${version} author: D151l +description: Configure SimpleCloud MOTDs, tablists, join states, player counts, and proxy layouts depends: [simplecloud-api] \ No newline at end of file diff --git a/proxy-bungeecord/src/test/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerKickListenerTest.kt b/proxy-bungeecord/src/test/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerKickListenerTest.kt deleted file mode 100644 index 5bfba5a..0000000 --- a/proxy-bungeecord/src/test/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerKickListenerTest.kt +++ /dev/null @@ -1,120 +0,0 @@ -package app.simplecloud.plugin.proxy.bungeecord.listener - -import net.md_5.bungee.api.chat.TextComponent -import net.md_5.bungee.api.chat.BaseComponent -import net.md_5.bungee.api.config.ServerInfo -import net.md_5.bungee.api.connection.ProxiedPlayer -import net.md_5.bungee.api.event.ServerKickEvent -import net.md_5.bungee.event.EventHandler -import java.lang.reflect.Proxy -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull -import kotlin.test.assertSame - -class ServerKickListenerTest { - - private var disconnectedReason: BaseComponent? = null - private val player = playerStub() - private val kickedFrom = stub(ServerInfo::class.java) - private val fallback = stub(ServerInfo::class.java) - private val serverReason = TextComponent("You are banned") - - @Test - fun `uses the server reason when no fallback is available`() { - val event = kickEvent(cancelServer = null) - - ServerKickListener { true }.handle(event) - - assertSame(serverReason, disconnectedReason) - } - - @Test - fun `does nothing when forwarding is disabled`() { - val event = kickEvent(cancelServer = null) - - ServerKickListener { false }.handle(event) - - assertNull(disconnectedReason) - } - - @Test - fun `leaves a fallback redirect untouched`() { - val event = kickEvent(cancelServer = fallback).apply { isCancelled = true } - - ServerKickListener { true }.handle(event) - - assertSame(fallback, event.cancelServer) - assertNull(disconnectedReason) - } - - @Test - fun `does nothing when the server did not provide a reason`() { - val event = kickEvent(cancelServer = null, reason = null) - - ServerKickListener { true }.handle(event) - - assertNull(disconnectedReason) - } - - @Test - fun `applies hot-reloaded setting without recreating listener`() { - var enabled = false - val listener = ServerKickListener { enabled } - val event = kickEvent(cancelServer = null) - - listener.handle(event) - assertNull(disconnectedReason) - - enabled = true - listener.handle(event) - - assertSame(serverReason, disconnectedReason) - } - - @Test - fun `runs after fallback listeners`() { - val annotation = ServerKickListener::class.java - .getDeclaredMethod("handle", ServerKickEvent::class.java) - .getAnnotation(EventHandler::class.java) - - assertEquals(Byte.MAX_VALUE, annotation.priority) - } - - private fun kickEvent( - cancelServer: ServerInfo?, - reason: BaseComponent? = serverReason - ): ServerKickEvent { - return ServerKickEvent( - player, - kickedFrom, - reason, - cancelServer, - ServerKickEvent.State.CONNECTED - ) - } - - @Suppress("UNCHECKED_CAST") - private fun playerStub(): ProxiedPlayer { - return Proxy.newProxyInstance( - ProxiedPlayer::class.java.classLoader, - arrayOf(ProxiedPlayer::class.java) - ) { _, method, args -> - if (method.name == "disconnect") { - if (disconnectedReason == null) { - disconnectedReason = args?.firstOrNull() as? BaseComponent - } - null - } else { - throw UnsupportedOperationException("Unexpected call to ${method.name}") - } - } as ProxiedPlayer - } - - @Suppress("UNCHECKED_CAST") - private fun stub(type: Class): T { - return Proxy.newProxyInstance(type.classLoader, arrayOf(type)) { _, method, _ -> - throw UnsupportedOperationException("Unexpected call to ${method.name}") - } as T - } -} diff --git a/proxy-shared/build.gradle.kts b/proxy-shared/build.gradle.kts index 8a2458d..172f194 100644 --- a/proxy-shared/build.gradle.kts +++ b/proxy-shared/build.gradle.kts @@ -1,11 +1,6 @@ dependencies { - compileOnly(rootProject.libs.adventure.minimessage) - - testImplementation(kotlin("test")) - testImplementation(rootProject.libs.adventure.minimessage) - - implementation(rootProject.libs.configurate.yaml) - implementation(rootProject.libs.configurate.kotlin) - - compileOnly(rootProject.libs.command.cloud.core) + implementation(libs.adventure.minimessage) + implementation(libs.configurate.yaml) + implementation(libs.configurate.kotlin) + implementation(libs.command.cloud.core) } diff --git a/proxy-shared/src/test/kotlin/app/simplecloud/plugin/proxy/shared/config/ReloadMessageConfigTest.kt b/proxy-shared/src/test/kotlin/app/simplecloud/plugin/proxy/shared/config/ReloadMessageConfigTest.kt deleted file mode 100644 index 4c92de3..0000000 --- a/proxy-shared/src/test/kotlin/app/simplecloud/plugin/proxy/shared/config/ReloadMessageConfigTest.kt +++ /dev/null @@ -1,79 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config - -import app.simplecloud.plugin.proxy.shared.config.message.MessageConfig -import java.nio.file.Files -import kotlin.test.Test -import kotlin.test.assertEquals - -class ReloadMessageConfigTest { - - @Test - fun `loads customizable reload messages`() { - val directory = Files.createTempDirectory("proxy-essentials-reload-messages-") - Files.writeString( - directory.resolve("messages.yml"), - """ - version: '2' - variables: - prefix: '[Proxy] ' - command: - reload: - start: 'Starting reload' - success: 'Reload complete' - failure: 'Reload failed: ' - """.trimIndent() - ) - - val yamlConfig = YamlConfig(directory.toString()) - try { - val messages = yamlConfig.load("messages").get() - - assertEquals("[Proxy] Starting reload", messages.resolve(messages.command.reload.start)) - assertEquals("[Proxy] Reload complete", messages.resolve(messages.command.reload.success)) - assertEquals( - "[Proxy] Reload failed: broken", - messages.resolve(messages.command.reload.failure).replace("", "broken") - ) - } finally { - yamlConfig.close() - directory.toFile().deleteRecursively() - } - } - - @Test - fun `adds reload messages to existing message configs`() { - val directory = Files.createTempDirectory("proxy-essentials-reload-message-migration-") - val messagesFile = directory.resolve("messages.yml") - Files.writeString( - messagesFile, - """ - version: '1' - variables: - prefix: 'Custom ' - kick: - network-full: 'Keep this custom message' - command: {} - """.trimIndent() - ) - - try { - OldConfigMigrator.migrate(directory) - - val yamlConfig = YamlConfig(directory.toString()) - try { - val messages = yamlConfig.load("messages").get() - assertEquals("2", messages.version) - assertEquals("Custom ", messages.variables.prefix) - assertEquals("Keep this custom message", messages.kick.networkFull) - assertEquals( - MessageConfig().command.reload.failure, - messages.command.reload.failure - ) - } finally { - yamlConfig.close() - } - } finally { - directory.toFile().deleteRecursively() - } - } -} diff --git a/proxy-shared/src/test/kotlin/app/simplecloud/plugin/proxy/shared/config/ShowKickReasonHotReloadTest.kt b/proxy-shared/src/test/kotlin/app/simplecloud/plugin/proxy/shared/config/ShowKickReasonHotReloadTest.kt deleted file mode 100644 index 4b7f4ad..0000000 --- a/proxy-shared/src/test/kotlin/app/simplecloud/plugin/proxy/shared/config/ShowKickReasonHotReloadTest.kt +++ /dev/null @@ -1,45 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config - -import java.nio.file.Files -import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class ShowKickReasonHotReloadTest { - - @Test - fun `reloads show kick reason after config file changes`() { - val directory = Files.createTempDirectory("proxy-essentials-hot-reload-") - val configFile = directory.resolve("config.yml") - Files.writeString(configFile, configWithShowKickReason(false)) - - val yamlConfig = YamlConfig(directory.toString()) - try { - val reactiveConfig = yamlConfig.load("config") - assertFalse(reactiveConfig.get().showKickReason) - - Files.writeString(configFile, configWithShowKickReason(true)) - - val deadline = System.nanoTime() + 5_000_000_000L - while (!reactiveConfig.get().showKickReason && System.nanoTime() < deadline) { - Thread.sleep(25) - } - - assertTrue(reactiveConfig.get().showKickReason) - } finally { - yamlConfig.close() - directory.toFile().deleteRecursively() - } - } - - private fun configWithShowKickReason(enabled: Boolean): String { - return """ - version: '2' - initial-state: public - initial-layout: public - show-kick-reason: $enabled - joinstates: [] - tablist: [] - """.trimIndent() - } -} diff --git a/proxy-shared/src/test/kotlin/app/simplecloud/plugin/proxy/shared/format/MotdMiniMessageFormatterTest.kt b/proxy-shared/src/test/kotlin/app/simplecloud/plugin/proxy/shared/format/MotdMiniMessageFormatterTest.kt deleted file mode 100644 index 197d3b9..0000000 --- a/proxy-shared/src/test/kotlin/app/simplecloud/plugin/proxy/shared/format/MotdMiniMessageFormatterTest.kt +++ /dev/null @@ -1,143 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.format - -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.ObjectComponent -import net.kyori.adventure.text.TextComponent -import net.kyori.adventure.text.format.TextDecoration -import net.kyori.adventure.text.minimessage.MiniMessage -import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder -import kotlin.math.abs -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertIs -import kotlin.test.assertTrue - -class MotdMiniMessageFormatterTest { - - private val miniMessage = MiniMessage.miniMessage() - private val pixel = "\u200c" - - @Test - fun `centers both motd lines independently`() { - val component = MotdMiniMessageFormatter.deserialize( - miniMessage, - "
iiii
", - "
WW
" - ) - - val lines = plainText(component).split('\n') - assertEquals(" ".repeat(28) + pixel.repeat(2) + "iiii", lines[0]) - assertEquals(" ".repeat(28) + "WW", lines[1]) - } - - @Test - fun `accounts for bold glyph width`() { - val component = MotdMiniMessageFormatter.deserialize( - miniMessage, - "
iiii
", - "" - ) - - assertEquals(" ".repeat(28) + "iiii\n", plainText(component)) - } - - @Test - fun `accounts for inherited bold glyph width`() { - val component = MotdMiniMessageFormatter.deserialize( - miniMessage, - "
iiii
", - "" - ) - - assertEquals(" ".repeat(28) + "iiii\n", plainText(component)) - } - - @Test - fun `measures resolved component placeholders`() { - val value = Component.text("iiii").decorate(TextDecoration.BOLD) - val component = MotdMiniMessageFormatter.deserialize( - miniMessage, - "
", - "", - listOf(Placeholder.component("value", value)) - ) - - assertEquals(" ".repeat(28) + "iiii\n", plainText(component)) - } - - @Test - fun `uses metrics for built in fonts and symbols`() { - val component = MotdMiniMessageFormatter.deserialize( - miniMessage, - "
cccc
", - "
WW๐Ÿ”ฅ
" - ) - - val lines = plainText(component).split('\n') - assertEquals(" ".repeat(28) + "cccc", lines[0]) - assertEquals(" ".repeat(27) + pixel.repeat(2) + "WW๐Ÿ”ฅ", lines[1]) - } - - @Test - fun `uses fractional unihex metrics for unicode symbols`() { - val component = MotdMiniMessageFormatter.deserialize( - miniMessage, - "
โฌข โฌข โฌข โฌข โฌข " + - "ส™สŸแดแด„แด‹ส™แดแดœษดแด….ษขษข " + - "โฌข โฌข โฌข โฌข โฌข", - "" - ) - - assertEquals( - " ".repeat(4) + pixel.repeat(2) + - "โฌข โฌข โฌข โฌข โฌข ส™สŸแดแด„แด‹ส™แดแดœษดแด….ษขษข โฌข โฌข โฌข โฌข โฌข\n", - plainText(component) - ) - } - - @Test - fun `accounts for sprite object components`() { - val component = MotdMiniMessageFormatter.deserialize( - miniMessage, - "
", - "" - ) - - assertEquals(" ".repeat(28) + pixel.repeat(2) + "\n", plainText(component)) - assertIs(component.descendants().single { it is ObjectComponent }) - } - - @Test - fun `centers relative to the complete server list row`() { - val text = "A simplecloud.app network" - val component = MotdMiniMessageFormatter.deserialize( - miniMessage, - "
$text", - " " - ) - - val firstLine = plainText(component).substringBefore('\n') - val padding = firstLine.takeWhile { it == ' ' || it == pixel.single() } - val paddingWidth = padding.count { it == ' ' } * 4f + padding.count { it == pixel.single() } - val textWidth = MinecraftTextWidth.width(miniMessage.deserialize(text)) - val visualCenter = 35f + paddingWidth + (textWidth / 2f) - - assertTrue(abs(visualCenter - (305f / 2f)) <= 0.5f) - assertEquals(" ", plainText(component).substringAfter('\n')) - } - - @Test - fun `leaves untagged lines unchanged`() { - val component = MotdMiniMessageFormatter.deserialize(miniMessage, "hello", "world") - - assertEquals("hello\nworld", plainText(component)) - } - - private fun plainText(component: Component): String { - val content = (component as? TextComponent)?.content().orEmpty() - return content + component.children().joinToString(separator = "") { plainText(it) } - } - - private fun Component.descendants(): List = - children() + children().flatMap { it.descendants() } -} diff --git a/proxy-shared/src/test/kotlin/app/simplecloud/plugin/proxy/shared/handler/LocalPingSourceMatcherTest.kt b/proxy-shared/src/test/kotlin/app/simplecloud/plugin/proxy/shared/handler/LocalPingSourceMatcherTest.kt deleted file mode 100644 index d9d80ee..0000000 --- a/proxy-shared/src/test/kotlin/app/simplecloud/plugin/proxy/shared/handler/LocalPingSourceMatcherTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.handler - -import java.net.InetAddress -import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class LocalPingSourceMatcherTest { - - @Test - fun `loopback addresses are local regardless of interface lookup`() { - val matcher = LocalPingSourceMatcher { false } - - assertTrue(matcher.isLocal(InetAddress.getLoopbackAddress())) - } - - @Test - fun `addresses assigned to another local interface are local`() { - val localAddress = InetAddress.getByName("192.0.2.10") - val matcher = LocalPingSourceMatcher { it == localAddress } - - assertTrue(matcher.isLocal(localAddress)) - } - - @Test - fun `unassigned and missing addresses are not local`() { - val matcher = LocalPingSourceMatcher { false } - - assertFalse(matcher.isLocal(InetAddress.getByName("192.0.2.20"))) - assertFalse(matcher.isLocal(null)) - } - - @Test - fun `failed interface lookups do not treat a source as local`() { - val matcher = LocalPingSourceMatcher { error("lookup failed") } - - assertFalse(matcher.isLocal(InetAddress.getByName("192.0.2.30"))) - } -} diff --git a/proxy-velocity/build.gradle.kts b/proxy-velocity/build.gradle.kts index 5b70ff1..9771b7b 100644 --- a/proxy-velocity/build.gradle.kts +++ b/proxy-velocity/build.gradle.kts @@ -1,20 +1,17 @@ plugins { alias(libs.plugins.minotaur) + alias(libs.plugins.blossom) + kotlin("kapt") } dependencies { api(project(":proxy-shared")) - compileOnly(rootProject.libs.adventure.minimessage) + compileOnly(libs.velocity) + kapt(libs.velocity) - compileOnly(rootProject.libs.velocity) - annotationProcessor(rootProject.libs.velocity) - - testImplementation(kotlin("test")) - testImplementation(rootProject.libs.velocity) - - implementation(rootProject.libs.command.cloud.core) - implementation(rootProject.libs.command.cloud.velocity) + implementation(libs.command.cloud.core) + implementation(libs.command.cloud.velocity) } modrinth { @@ -52,3 +49,13 @@ modrinth { changelog.set("https://docs.simplecloud.app/changelog") syncBodyFrom.set(rootProject.file("README.md").readText()) } + +sourceSets { + main { + blossom { + kotlinSources { + property("version", project.version.toString()) + } + } + } +} \ No newline at end of file diff --git a/proxy-velocity/src/main/kotlin-templates/app/simplecloud/plugin/proxy/velocity/BuildConstants.kt.peb b/proxy-velocity/src/main/kotlin-templates/app/simplecloud/plugin/proxy/velocity/BuildConstants.kt.peb new file mode 100644 index 0000000..66ba88c --- /dev/null +++ b/proxy-velocity/src/main/kotlin-templates/app/simplecloud/plugin/proxy/velocity/BuildConstants.kt.peb @@ -0,0 +1,5 @@ +package app.simplecloud.plugin.proxy.velocity + +object BuildConstants { + const val VERSION = "{{ version }}" +} \ No newline at end of file diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/ProxyVelocityPlugin.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/ProxyVelocityPlugin.kt index 9031f10..beb9bef 100644 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/ProxyVelocityPlugin.kt +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/ProxyVelocityPlugin.kt @@ -1,5 +1,6 @@ package app.simplecloud.plugin.proxy.velocity +import app.simplecloud.plugin.api.shared.extension.miniMessage import app.simplecloud.plugin.proxy.shared.ProxyPlugin import app.simplecloud.plugin.proxy.shared.format.MotdMiniMessageFormatter import app.simplecloud.plugin.proxy.shared.handler.command.CommandSender @@ -18,12 +19,13 @@ import com.velocitypowered.api.command.CommandSource import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.proxy.ProxyInitializeEvent import com.velocitypowered.api.event.proxy.ProxyShutdownEvent +import com.velocitypowered.api.plugin.Dependency +import com.velocitypowered.api.plugin.Plugin import com.velocitypowered.api.plugin.PluginContainer import com.velocitypowered.api.plugin.annotation.DataDirectory import com.velocitypowered.api.proxy.Player import com.velocitypowered.api.proxy.ProxyServer import net.kyori.adventure.text.Component -import net.kyori.adventure.text.minimessage.MiniMessage import org.incendo.cloud.SenderMapper import org.incendo.cloud.execution.ExecutionCoordinator import org.incendo.cloud.velocity.VelocityCommandManager @@ -31,6 +33,17 @@ import org.slf4j.Logger import java.nio.file.Path import kotlin.io.path.pathString +@Plugin( + id = "proxy-essentials-velocity", + name = "proxy-essentials-velocity", + version = BuildConstants.VERSION, + authors = ["D151l"], + description = "Configure SimpleCloud MOTDs, tablists, join states, player counts, and proxy layouts", + url = "https://github.com/simplecloudapp/proxy-essentials-plugin", + dependencies = [ + Dependency("simplecloud-api") + ] +) class ProxyVelocityPlugin @Inject constructor( val proxyServer: ProxyServer, @DataDirectory val dataDirectory: Path, @@ -39,9 +52,7 @@ class ProxyVelocityPlugin @Inject constructor( ) { val proxyPlugin = ProxyPlugin(dataDirectory.pathString) - val tabListHandler = TabListHandler(proxyPlugin, this, proxyServer) - private val miniMessage = MiniMessage.miniMessage() @Subscribe fun onProxyInitialize(event: ProxyInitializeEvent) { @@ -85,7 +96,7 @@ class ProxyVelocityPlugin @Inject constructor( fun deserializeToComponent(text: String, player: Player? = null): Component { val configureTagResolversEvent = this.proxyServer.eventManager.fire(ConfigureTagResolversEvent(player)).get() - return this.miniMessage.deserialize( + return miniMessage.deserialize( text, *configureTagResolversEvent.tagResolvers.toTypedArray() ) diff --git a/proxy-velocity/src/main/resources/velocity-plugin.json b/proxy-velocity/src/main/resources/velocity-plugin.json deleted file mode 100644 index ee8da21..0000000 --- a/proxy-velocity/src/main/resources/velocity-plugin.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "id": "proxy-essentials-velocity", - "name": "Proxy-Essentials-Velocity", - "version": "1.1.0", - "authors": [ - "D151l" - ], - "main": "app.simplecloud.plugin.proxy.velocity.ProxyVelocityPlugin", - "dependencies": [ - { - "id": "simplecloud-api" - } - ] -} \ No newline at end of file diff --git a/proxy-velocity/src/test/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ServerKickListenerTest.kt b/proxy-velocity/src/test/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ServerKickListenerTest.kt deleted file mode 100644 index 91a8ebd..0000000 --- a/proxy-velocity/src/test/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ServerKickListenerTest.kt +++ /dev/null @@ -1,96 +0,0 @@ -package app.simplecloud.plugin.proxy.velocity.listener - -import com.velocitypowered.api.event.Subscribe -import com.velocitypowered.api.event.player.KickedFromServerEvent -import com.velocitypowered.api.proxy.Player -import com.velocitypowered.api.proxy.server.RegisteredServer -import net.kyori.adventure.text.Component -import java.lang.reflect.Proxy -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertSame - -class ServerKickListenerTest { - - private val player = stub(Player::class.java) - private val server = stub(RegisteredServer::class.java) - private val serverReason = Component.text("You are banned") - private val proxyReason = Component.text("No fallback server available") - - @Test - fun `uses the server reason instead of a generic disconnect`() { - val event = kickEvent(KickedFromServerEvent.DisconnectPlayer.create(proxyReason)) - - ServerKickListener { true }.handle(event) - - val result = event.result as KickedFromServerEvent.DisconnectPlayer - assertSame(serverReason, result.reasonComponent) - } - - @Test - fun `keeps the proxy reason when forwarding is disabled`() { - val originalResult = KickedFromServerEvent.DisconnectPlayer.create(proxyReason) - val event = kickEvent(originalResult) - - ServerKickListener { false }.handle(event) - - assertSame(originalResult, event.result) - } - - @Test - fun `leaves a fallback redirect untouched`() { - val originalResult = KickedFromServerEvent.RedirectPlayer.create(server) - val event = kickEvent(originalResult) - - ServerKickListener { true }.handle(event) - - assertSame(originalResult, event.result) - } - - @Test - fun `keeps the proxy reason when the server did not provide one`() { - val originalResult = KickedFromServerEvent.DisconnectPlayer.create(proxyReason) - val event = KickedFromServerEvent(player, server, null, true, originalResult) - - ServerKickListener { true }.handle(event) - - assertSame(originalResult, event.result) - } - - @Test - fun `applies hot-reloaded setting without recreating listener`() { - var enabled = false - val listener = ServerKickListener { enabled } - val originalResult = KickedFromServerEvent.DisconnectPlayer.create(proxyReason) - val event = kickEvent(originalResult) - - listener.handle(event) - assertSame(originalResult, event.result) - - enabled = true - listener.handle(event) - - val result = event.result as KickedFromServerEvent.DisconnectPlayer - assertSame(serverReason, result.reasonComponent) - } - - @Test - fun `runs after fallback listeners`() { - val annotation = ServerKickListener::class.java - .getDeclaredMethod("handle", KickedFromServerEvent::class.java) - .getAnnotation(Subscribe::class.java) - - assertEquals(Short.MIN_VALUE, annotation.priority) - } - - private fun kickEvent(result: KickedFromServerEvent.ServerKickResult): KickedFromServerEvent { - return KickedFromServerEvent(player, server, serverReason, true, result) - } - - @Suppress("UNCHECKED_CAST") - private fun stub(type: Class): T { - return Proxy.newProxyInstance(type.classLoader, arrayOf(type)) { _, method, _ -> - throw UnsupportedOperationException("Unexpected call to ${method.name}") - } as T - } -} From 2940744f5d19709ffd542fc971bf907d5b6b871b Mon Sep 17 00:00:00 2001 From: Janis Date: Wed, 26 Aug 2026 00:52:41 +0200 Subject: [PATCH 03/17] refactor: implement plugin api for configs --- .../bungeecord/listener/ProxyPingListener.kt | 22 +- .../src/main/resources/config/placeholder.yml | 6 +- .../plugin/proxy/shared/ProxyPlugin.kt | 39 ++-- ...mmandMessageConfig.kt => MessageConfig.kt} | 108 ++++++---- .../{motd => }/MotdLayoutConfiguration.kt | 60 +++--- .../proxy/shared/config/OldConfigMigrator.kt | 70 ++++-- .../shared/config/PlaceHolderConfiguration.kt | 17 ++ .../proxy/shared/config/PlayerCountConfig.kt | 12 -- .../shared/config/ProxyEssentialsConfig.kt | 95 ++++++--- .../plugin/proxy/shared/config/YamlConfig.kt | 182 ---------------- .../shared/config/domain/DomainMotdRoute.kt | 16 -- .../config/message/KickMessageConfig.kt | 11 - .../shared/config/message/MessageConfig.kt | 13 -- .../shared/config/message/MessageVariables.kt | 8 - .../config/motd/MaxPlayerDisplayType.kt | 7 - .../shared/config/motd/MotdUpdateType.kt | 6 - .../shared/config/placeholder/PingColor.kt | 9 - .../placeholder/PlaceHolderConfiguration.kt | 16 -- .../shared/config/reactive/ReactiveConfig.kt | 29 --- .../config/reactive/ReactiveConfigInfo.kt | 7 - .../proxy/shared/config/state/JoinState.kt | 11 - .../config/state/JoinStatePermission.kt | 9 - .../shared/config/state/WhitelistConfig.kt | 9 - .../proxy/shared/config/tablis/TabList.kt | 9 - .../shared/config/tablis/TabListGroup.kt | 11 - .../proxy/shared/event/MotdConfiguration.kt | 13 -- .../event/TabListPlayerConfiguration.kt | 9 - .../shared/format/MotdMiniMessageFormatter.kt | 9 +- .../proxy/shared/handler/DomainMotdHandler.kt | 2 +- .../proxy/shared/handler/JoinStateResolver.kt | 2 +- .../proxy/shared/handler/MotdLayoutHandler.kt | 90 ++++---- .../proxy/shared/handler/ProxyJoinGate.kt | 4 +- .../proxy/shared/handler/TabListResolver.kt | 2 +- .../command/JoinStateCommandHandler.kt | 5 +- .../command/ProxyEssentialsCommandHandler.kt | 47 ++--- .../shared/resolver/TagResolverHelper.kt | 8 +- .../shared/temp/YamlDirectoryRepository.kt | 199 ------------------ .../LocalPingSourceMatcher.kt | 4 +- .../ServerIconLoader.kt | 4 +- .../shared/utilities/config/ConfigVersion.kt | 7 + .../shared/utilities/config/DefaultConfigs.kt | 40 ++++ .../velocity/listener/ProxyPingListener.kt | 22 +- .../src/main/resources/config/placeholder.yml | 6 +- 43 files changed, 433 insertions(+), 822 deletions(-) rename proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/{message/CommandMessageConfig.kt => MessageConfig.kt} (60%) rename proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/{motd => }/MotdLayoutConfiguration.kt (58%) create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/PlaceHolderConfiguration.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/PlayerCountConfig.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/YamlConfig.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/domain/DomainMotdRoute.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/message/KickMessageConfig.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/message/MessageConfig.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/message/MessageVariables.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/motd/MaxPlayerDisplayType.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/motd/MotdUpdateType.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/placeholder/PingColor.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/placeholder/PlaceHolderConfiguration.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/reactive/ReactiveConfig.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/reactive/ReactiveConfigInfo.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/state/JoinState.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/state/JoinStatePermission.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/state/WhitelistConfig.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/tablis/TabList.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/tablis/TabListGroup.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/event/MotdConfiguration.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/event/TabListPlayerConfiguration.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/temp/YamlDirectoryRepository.kt rename proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/{handler => utilities}/LocalPingSourceMatcher.kt (90%) rename proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/{handler => utilities}/ServerIconLoader.kt (98%) create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/config/ConfigVersion.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/config/DefaultConfigs.kt diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ProxyPingListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ProxyPingListener.kt index 05577f8..f877c53 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ProxyPingListener.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ProxyPingListener.kt @@ -1,9 +1,9 @@ package app.simplecloud.plugin.proxy.bungeecord.listener import app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin -import app.simplecloud.plugin.proxy.shared.config.motd.MaxPlayerDisplayType -import app.simplecloud.plugin.proxy.shared.handler.LocalPingSourceMatcher -import app.simplecloud.plugin.proxy.shared.handler.ServerIconLoader +import app.simplecloud.plugin.proxy.shared.config.MaxPlayerDisplayType +import app.simplecloud.plugin.proxy.shared.utilities.LocalPingSourceMatcher +import app.simplecloud.plugin.proxy.shared.utilities.ServerIconLoader import net.md_5.bungee.api.Favicon import net.md_5.bungee.api.ServerPing.* import net.md_5.bungee.api.event.ProxyPingEvent @@ -50,8 +50,8 @@ class ProxyPingListener( if (!isLocalPing) { // player list (hover text) - val samplePlayers = if (layout.playerList.enabled && layout.playerList.entries.isNotEmpty()) { - layout.playerList.entries.map { PlayerInfo(it, UUID.randomUUID()) }.toTypedArray() + val samplePlayers = if (layout.playerList.enabled && layout.playerList.playerList.isNotEmpty()) { + layout.playerList.playerList.map { PlayerInfo(it, UUID.randomUUID()) }.toTypedArray() } else { response.players.sample } @@ -60,11 +60,11 @@ class ProxyPingListener( val playerCountHandler = plugin.proxyPlugin.playerCountHandler val onlinePlayers = playerCountHandler.onlinePlayers(plugin.proxy.players.size) val realMax = playerCountHandler.maxPlayers(plugin.proxy.config.playerLimit) - val maxPlayers = if (layout.versionSettings.slots.enabled) { - when (layout.versionSettings.slots.type) { + val maxPlayers = if (layout.version.slots.enabled) { + when (layout.version.slots.type) { MaxPlayerDisplayType.REAL -> realMax - MaxPlayerDisplayType.FAKE -> layout.versionSettings.slots.fakeSlots - MaxPlayerDisplayType.DYNAMIC -> onlinePlayers + layout.versionSettings.slots.dynamicPlayerRange + MaxPlayerDisplayType.FAKE -> layout.version.slots.fakeSlots + MaxPlayerDisplayType.DYNAMIC -> onlinePlayers + layout.version.slots.dynamicPlayerRange } } else { realMax @@ -73,8 +73,8 @@ class ProxyPingListener( response.players = Players(maxPlayers, onlinePlayers, samplePlayers) // version name - if (layout.versionSettings.name.enabled) { - response.version = Protocol(layout.versionSettings.name.text, response.version.protocol) + if (layout.version.name.enabled) { + response.version = Protocol(layout.version.name.text, response.version.protocol) } } } diff --git a/proxy-bungeecord/src/main/resources/config/placeholder.yml b/proxy-bungeecord/src/main/resources/config/placeholder.yml index 31e9f6c..3f3892e 100644 --- a/proxy-bungeecord/src/main/resources/config/placeholder.yml +++ b/proxy-bungeecord/src/main/resources/config/placeholder.yml @@ -1,6 +1,6 @@ -currentDateFormat: "dd.MM.yyyy" -currentTimeFormat: "HH:mm:ss" -pingColors: +current-date-format: "dd.MM.yyyy" +current-time-format: "HH:mm:ss" +ping-colors: - ping: 0 color: "" - ping: 50 diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt index 7a2ff57..b7134bc 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt @@ -2,12 +2,12 @@ package app.simplecloud.plugin.proxy.shared import app.simplecloud.api.CloudApi import app.simplecloud.api.CloudApiOptions +import app.simplecloud.plugin.api.shared.config.ConfigurationFactory import app.simplecloud.plugin.proxy.shared.config.DefaultConfigInstaller +import app.simplecloud.plugin.proxy.shared.config.MessageConfig import app.simplecloud.plugin.proxy.shared.config.OldConfigMigrator +import app.simplecloud.plugin.proxy.shared.config.PlaceHolderConfiguration import app.simplecloud.plugin.proxy.shared.config.ProxyEssentialsConfig -import app.simplecloud.plugin.proxy.shared.config.YamlConfig -import app.simplecloud.plugin.proxy.shared.config.message.MessageConfig -import app.simplecloud.plugin.proxy.shared.config.placeholder.PlaceHolderConfiguration import app.simplecloud.plugin.proxy.shared.handler.CloudControllerHandler import app.simplecloud.plugin.proxy.shared.handler.DomainMotdHandler import app.simplecloud.plugin.proxy.shared.handler.JoinStateHandler @@ -17,23 +17,27 @@ import app.simplecloud.plugin.proxy.shared.handler.PlayerCountHandler import app.simplecloud.plugin.proxy.shared.handler.ProxyJoinGate import app.simplecloud.plugin.proxy.shared.handler.TabListResolver import java.io.File -import java.nio.file.Path class ProxyPlugin( dirPath: String ) { val api = CloudApi.create(CloudApiOptions.builder().component("proxy-essentials").build()) + private val dataDirectory = File(dirPath) + init { - val dataDirectory = Path.of(dirPath) - OldConfigMigrator.migrate(dataDirectory) - DefaultConfigInstaller.install(dataDirectory, javaClass.classLoader) + dataDirectory.mkdirs() + OldConfigMigrator.migrate(dataDirectory.toPath()) + DefaultConfigInstaller.install(dataDirectory.toPath(), javaClass.classLoader) } - val config = YamlConfig(dirPath) - val proxyEssentialsConfig = config.load("config") - val placeHolderConfiguration = config.load("placeholder") - val messagesConfiguration = config.load("messages") + val proxyEssentialsConfig = ConfigurationFactory(File(dataDirectory, "config.yml"), ProxyEssentialsConfig::class.java) + val messagesConfiguration = ConfigurationFactory(File(dataDirectory, "messages.yml"), MessageConfig::class.java) + val placeHolderConfiguration = ConfigurationFactory(File(dataDirectory, "placeholder.yml"), PlaceHolderConfiguration::class.java) + + init { + loadConfigurations() + } val serverIconsPath = "$dirPath/layout/server-icons" val motdLayoutHandler = MotdLayoutHandler(File("$dirPath/layout").toPath(), this) @@ -53,12 +57,21 @@ class ProxyPlugin( it.startSyncTask() } + fun reload() { + loadConfigurations() + motdLayoutHandler.loadMotdLayouts() + } + fun shutdown() { playerCountHandler.stop() joinStateHandler.stop() cloudControllerHandler.close() - motdLayoutHandler.close() domainMotdHandler.stop() - config.close() + } + + private fun loadConfigurations() { + proxyEssentialsConfig.loadOrCreate(ProxyEssentialsConfig()) + messagesConfiguration.loadOrCreate(MessageConfig()) + placeHolderConfiguration.loadOrCreate(PlaceHolderConfiguration()) } } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/message/CommandMessageConfig.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MessageConfig.kt similarity index 60% rename from proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/message/CommandMessageConfig.kt rename to proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MessageConfig.kt index b2f8e64..35f9068 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/message/CommandMessageConfig.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MessageConfig.kt @@ -1,15 +1,38 @@ -package app.simplecloud.plugin.proxy.shared.config.message +package app.simplecloud.plugin.proxy.shared.config +import app.simplecloud.plugin.api.shared.config.AbstractMessageConfig +import app.simplecloud.plugin.api.shared.config.VersionedConfig +import app.simplecloud.plugin.proxy.shared.utilities.config.ConfigVersion +import app.simplecloud.plugin.proxy.shared.utilities.config.DefaultConfigs import org.spongepowered.configurate.objectmapping.ConfigSerializable -import org.spongepowered.configurate.objectmapping.meta.Setting -private val PREFIX = "โšก " +@ConfigSerializable +data class MessageConfig( + override val version: Int = ConfigVersion.VERSION, + override val variables: Map = DefaultConfigs.VARIABLES, + val kick: KickMessageConfig = KickMessageConfig(), + val command: CommandMessages = CommandMessages() +) : VersionedConfig, AbstractMessageConfig() { + + fun resolve(message: String): String { + return variables.entries.fold(message) { resolved, (key, value) -> + resolved.replace("<$key>", value) + } + } +} @ConfigSerializable -data class UpdateMessages( - @Setting("update-success") val updateSuccess: String = "${PREFIX}Updated successfully.", - @Setting("update-failure") val updateFailure: String = "${PREFIX}Failed to update.", - @Setting("update-no-change") val updateNoChange: String = "${PREFIX}Nothing changed." +data class KickMessageConfig( + val noPermission: String = "The network is currently in maintenance mode. Please try again later.", + val networkFull: String = "The network is currently full. Please try again later.", + val noJoinState: String = "No join state found for server. Please try again later." +) + +@ConfigSerializable +data class CommandMessages( + val reload: ReloadCommandMessages = ReloadCommandMessages(), + val joinState: JoinStateCommandMessages = JoinStateCommandMessages(), + val layout: LayoutCommandMessages = LayoutCommandMessages() ) @ConfigSerializable @@ -19,12 +42,34 @@ data class ReloadCommandMessages( val failure: String = "${PREFIX}Failed to reload configurations: " ) +@ConfigSerializable +data class JoinStateCommandMessages( + val server: UpdateMessages = UpdateMessages( + "${PREFIX}Join state of server updated successfully.", + "${PREFIX}Failed to update join state of server.", + "${PREFIX}Join state of server did not change." + ), + val group: UpdateMessages = UpdateMessages( + "${PREFIX}Join state of group updated successfully.", + "${PREFIX}Failed to update join state of group.", + "${PREFIX}Join state of group did not change." + ), + val help: JoinStateHelpMessages = JoinStateHelpMessages(), + val list: JoinStateListMessages = JoinStateListMessages() +) + @ConfigSerializable data class JoinStateHelpMessages( val header: String = "${PREFIX}Commands of join state:", val entry: String = " " ) +@ConfigSerializable +data class JoinStateListMessages( + val groups: JoinStateGroupListMessages = JoinStateGroupListMessages(), + val states: JoinStateStateListMessages = JoinStateStateListMessages() +) + @ConfigSerializable data class JoinStateGroupListMessages( val header: String = "${PREFIX}Groups with their join states:", @@ -38,25 +83,14 @@ data class JoinStateStateListMessages( ) @ConfigSerializable -data class JoinStateListMessages( - val groups: JoinStateGroupListMessages = JoinStateGroupListMessages(), - val states: JoinStateStateListMessages = JoinStateStateListMessages() -) - -@ConfigSerializable -data class JoinStateCommandMessages( - val server: UpdateMessages = UpdateMessages( - "${PREFIX}Join state of server updated successfully.", - "${PREFIX}Failed to update join state of server.", - "${PREFIX}Join state of server did not change." - ), - val group: UpdateMessages = UpdateMessages( - "${PREFIX}Join state of group updated successfully.", - "${PREFIX}Failed to update join state of group.", - "${PREFIX}Join state of group did not change." - ), - val help: JoinStateHelpMessages = JoinStateHelpMessages(), - val list: JoinStateListMessages = JoinStateListMessages() +data class LayoutCommandMessages( + val help: LayoutHelpMessages = LayoutHelpMessages(), + val info: LayoutInfoMessages = LayoutInfoMessages(), + val set: UpdateMessages = UpdateMessages( + "${PREFIX}Layout for group updated successfully.", + "${PREFIX}Failed to update layout for group.", + "${PREFIX}Layout for group did not change." + ) ) @ConfigSerializable @@ -72,22 +106,10 @@ data class LayoutInfoMessages( ) @ConfigSerializable -data class LayoutSetMessages( - @Setting("update-success") val updateSuccess: String = "${PREFIX}Layout for group updated successfully.", - @Setting("update-failure") val updateFailure: String = "${PREFIX}Failed to update layout for group.", - @Setting("update-no-change") val updateNoChange: String = "${PREFIX}Layout for group did not change." -) - -@ConfigSerializable -data class LayoutCommandMessages( - val help: LayoutHelpMessages = LayoutHelpMessages(), - val info: LayoutInfoMessages = LayoutInfoMessages(), - val set: LayoutSetMessages = LayoutSetMessages() +data class UpdateMessages( + val updateSuccess: String = "${PREFIX}Updated successfully.", + val updateFailure: String = "${PREFIX}Failed to update.", + val updateNoChange: String = "${PREFIX}Nothing changed." ) -@ConfigSerializable -data class CommandMessages( - val reload: ReloadCommandMessages = ReloadCommandMessages(), - @Setting("join-state") val joinState: JoinStateCommandMessages = JoinStateCommandMessages(), - val layout: LayoutCommandMessages = LayoutCommandMessages() -) +const val PREFIX = "โšก " \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/motd/MotdLayoutConfiguration.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MotdLayoutConfiguration.kt similarity index 58% rename from proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/motd/MotdLayoutConfiguration.kt rename to proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MotdLayoutConfiguration.kt index d9b38db..9196d42 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/motd/MotdLayoutConfiguration.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MotdLayoutConfiguration.kt @@ -1,23 +1,36 @@ -package app.simplecloud.plugin.proxy.shared.config.motd +package app.simplecloud.plugin.proxy.shared.config import org.spongepowered.configurate.objectmapping.ConfigSerializable -import org.spongepowered.configurate.objectmapping.meta.Setting @ConfigSerializable -data class MotdEntry( - val name: String = "default", - val line1: String = "A simplecloud.app network", - val line2: String = " " +data class MotdLayoutConfiguration( + val configVersion: String = "2", + val motd: MotdConfig = MotdConfig(), + val serverIcon: ServerIconConfig = ServerIconConfig(), + val playerList: PlayerListConfig = PlayerListConfig(), + val version: VersionConfig = VersionConfig() ) @ConfigSerializable data class MotdConfig( val enabled: Boolean = true, - @Setting("update-type") val updateType: MotdUpdateType = MotdUpdateType.RANDOM, - @Setting("update-time") val updateTime: Int? = null, + val updateType: MotdUpdateType = MotdUpdateType.RANDOM, + val updateTime: Int? = null, val layouts: List = listOf(MotdEntry()) ) +@ConfigSerializable +data class MotdEntry( + val name: String = "default", + val line1: String = "A simplecloud.app network", + val line2: String = " " +) + +enum class MotdUpdateType { + RANDOM, + QUEUE +} + @ConfigSerializable data class ServerIconConfig( val enabled: Boolean = true, @@ -27,7 +40,13 @@ data class ServerIconConfig( @ConfigSerializable data class PlayerListConfig( val enabled: Boolean = true, - @Setting("player-list") val entries: List = listOf("", "simplecloud.app", "") + val playerList: List = listOf("", "simplecloud.app", "") +) + +@ConfigSerializable +data class VersionConfig( + val name: VersionNameConfig = VersionNameConfig(), + val slots: SlotsConfig = SlotsConfig() ) @ConfigSerializable @@ -40,21 +59,12 @@ data class VersionNameConfig( data class SlotsConfig( val enabled: Boolean = true, val type: MaxPlayerDisplayType = MaxPlayerDisplayType.REAL, - @Setting("fake-slots") val fakeSlots: Int = 100, - @Setting("dynamic-player-range") val dynamicPlayerRange: Int = 5 + val fakeSlots: Int = 100, + val dynamicPlayerRange: Int = 5 ) -@ConfigSerializable -data class VersionConfig( - val name: VersionNameConfig = VersionNameConfig(), - val slots: SlotsConfig = SlotsConfig() -) - -@ConfigSerializable -data class MotdLayoutConfiguration( - @Setting("config-version") val configVersion: String = "2", - val motd: MotdConfig = MotdConfig(), - @Setting("server-icon") val serverIcon: ServerIconConfig = ServerIconConfig(), - @Setting("player-list") val playerList: PlayerListConfig = PlayerListConfig(), - @Setting("version") val versionSettings: VersionConfig = VersionConfig() -) +enum class MaxPlayerDisplayType { + REAL, + FAKE, + DYNAMIC, +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/OldConfigMigrator.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/OldConfigMigrator.kt index 7ef9d0e..28a507f 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/OldConfigMigrator.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/OldConfigMigrator.kt @@ -1,7 +1,5 @@ package app.simplecloud.plugin.proxy.shared.config -import app.simplecloud.plugin.proxy.shared.config.message.MessageConfig -import app.simplecloud.plugin.proxy.shared.config.state.WhitelistConfig import org.spongepowered.configurate.CommentedConfigurationNode import org.spongepowered.configurate.kotlin.objectMapperFactory import org.spongepowered.configurate.yaml.NodeStyle @@ -232,7 +230,7 @@ object OldConfigMigrator { val target = loader.createNode() target.node("version").set(defaults.version) - target.node("variables").set(mapOf("prefix" to defaults.variables.prefix)) + target.node("variables").set(defaults.variables) target.node("kick").set( mapOf( "no-permission" to source.node("kick-message", "no-permission").stringOr(defaults.kick.noPermission), @@ -330,19 +328,38 @@ object OldConfigMigrator { if (!Files.exists(path)) return val source = path.loadYaml() - if (source.node("current-date-format").virtual() && source.node("current-time-format").virtual()) return + val legacyCamelCase = !source.node("currentDateFormat").virtual() || !source.node("currentTimeFormat").virtual() + val legacyKebabCase = !source.node("current-date-format").virtual() && + source.node("ping-colors").empty() && + !source.node("pingColors").empty() + if (!legacyCamelCase && !legacyKebabCase) return + + val defaults = PlaceHolderConfiguration() + val pingColors = source.node("ping-colors") + .childrenList() + .ifEmpty { source.node("pingColors").childrenList() } val loader = createLoader(path) val target = loader.createNode() - target.node("currentDateFormat").set(source.node("current-date-format").string ?: "dd.MM.yyyy") - target.node("currentTimeFormat").set(source.node("current-time-format").string ?: "HH:mm:ss") - target.node("pingColors").set( - source.node("ping-colors").childrenList().map { color -> - mapOf( - "ping" to color.node("ping").int, - "color" to color.node("color").string.orEmpty() - ) - } + target.node("current-date-format").set( + source.node("current-date-format").string + ?: source.node("currentDateFormat").string + ?: defaults.currentDateFormat + ) + target.node("current-time-format").set( + source.node("current-time-format").string + ?: source.node("currentTimeFormat").string + ?: defaults.currentTimeFormat + ) + target.node("ping-colors").set( + pingColors + .map { color -> + mapOf( + "ping" to color.node("ping").int, + "color" to color.node("color").string.orEmpty() + ) + } + .ifEmpty { defaults.pingColors.map { mapOf("ping" to it.ping, "color" to it.color) } } ) loader.save(target) @@ -354,11 +371,36 @@ object OldConfigMigrator { Files.list(layoutDirectory).use { paths -> paths .filter { it.isRegularFile() && it.extension == "yml" } - .forEach(::migrateLayout) + .forEach { path -> + removeStaleLayoutVersionLine(path) + migrateLayout(path) + } } deleteGeneratedDefaultLayoutIfPublicExists(layoutDirectory) } + /** + * Layouts migrated by older plugin versions can still carry the legacy `version: '1'` + * scalar in front of the `version:` section, which makes the file fail to parse. + */ + private fun removeStaleLayoutVersionLine(path: Path) { + val lines = Files.readString(path).lines().toMutableList() + val firstContentIndex = lines.indexOfFirst { it.isNotBlank() } + if (firstContentIndex == -1 || !lines[firstContentIndex].isLegacyLayoutVersionLine()) return + if (lines.drop(firstContentIndex + 1).none { it.trim() == "version:" }) return + + lines.removeAt(firstContentIndex) + if (firstContentIndex < lines.size && lines[firstContentIndex].isBlank()) { + lines.removeAt(firstContentIndex) + } + Files.writeString(path, lines.joinToString(System.lineSeparator())) + } + + private fun String.isLegacyLayoutVersionLine(): Boolean { + val trimmed = trim() + return trimmed == "version: '1'" || trimmed == "version: \"1\"" || trimmed == "version: 1" + } + private fun migrateLayout(path: Path) { val source = path.loadYaml() if (!source.isLegacyLayout()) { diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/PlaceHolderConfiguration.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/PlaceHolderConfiguration.kt new file mode 100644 index 0000000..327fa00 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/PlaceHolderConfiguration.kt @@ -0,0 +1,17 @@ +package app.simplecloud.plugin.proxy.shared.config + +import app.simplecloud.plugin.proxy.shared.utilities.config.DefaultConfigs +import org.spongepowered.configurate.objectmapping.ConfigSerializable + +@ConfigSerializable +data class PlaceHolderConfiguration( + val currentDateFormat: String = "dd.MM.yyyy", + val currentTimeFormat: String = "HH:mm:ss", + val pingColors: List = DefaultConfigs.PING_COLORS +) + +@ConfigSerializable +data class PingColor( + val ping: Int, + val color: String +) diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/PlayerCountConfig.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/PlayerCountConfig.kt deleted file mode 100644 index 97846ea..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/PlayerCountConfig.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config - -import org.spongepowered.configurate.objectmapping.ConfigSerializable -import org.spongepowered.configurate.objectmapping.meta.Setting - -@ConfigSerializable -data class PlayerCountConfig( - val enabled: Boolean = true, - @Setting("additional-groups") val additionalGroups: List = emptyList(), - @Setting("additional-persistent-servers") val additionalPersistentServers: List = emptyList(), - @Setting("update-time") val updateTime: Long = 20L -) diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ProxyEssentialsConfig.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ProxyEssentialsConfig.kt index 80b7d16..d779be3 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ProxyEssentialsConfig.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ProxyEssentialsConfig.kt @@ -1,42 +1,22 @@ package app.simplecloud.plugin.proxy.shared.config -import app.simplecloud.plugin.proxy.shared.config.domain.DomainMotdRoute -import app.simplecloud.plugin.proxy.shared.config.state.JoinState -import app.simplecloud.plugin.proxy.shared.config.state.JoinStatePermission -import app.simplecloud.plugin.proxy.shared.config.tablis.TabList -import app.simplecloud.plugin.proxy.shared.config.tablis.TabListGroup -import app.simplecloud.plugin.proxy.shared.config.state.WhitelistConfig +import app.simplecloud.plugin.api.shared.config.VersionedConfig +import app.simplecloud.plugin.proxy.shared.utilities.config.ConfigVersion +import app.simplecloud.plugin.proxy.shared.utilities.config.DefaultConfigs import org.spongepowered.configurate.objectmapping.ConfigSerializable -import org.spongepowered.configurate.objectmapping.meta.Setting @ConfigSerializable data class ProxyEssentialsConfig( - val version: String = "2", - @Setting("initial-state") val initialState: String = "public", - @Setting("initial-layout") val initialLayout: String = "public", - @Setting("show-kick-reason") val showKickReason: Boolean = false, - val joinstates: List = listOf( - JoinState("public", JoinStatePermission("", "simplecloud.proxy-essentials.join.full.public")), - JoinState( - "maintenance", - JoinStatePermission( - "simplecloud.proxy-essentials.join.maintenance", - "simplecloud.proxy-essentials.join.maintenance" - ), - "maintenance" - ) - ), + override val version: Int = ConfigVersion.VERSION, + val initialState: String = "public", + val initialLayout: String = "public", + val showKickReason: Boolean = false, + val joinstates: List = DefaultConfigs.JOIN_STATES, val domains: List = listOf(), val whitelist: WhitelistConfig = WhitelistConfig(), - @Setting("player-count") val playerCount: PlayerCountConfig = PlayerCountConfig(), - val tablist: List = listOf( - TabListGroup( - name = "global", - layout = listOf(TabList()), - updateTime = 20L - ) - ) -) { + val playerCount: PlayerCountConfig = PlayerCountConfig(), + val tablist: List = DefaultConfigs.TABLIST_GROUPS +) : VersionedConfig { fun tabListUpdateTimeMillis(): Long { val ticks = tablist.minOfOrNull { it.updateTime } ?: 20L return ticks * 50L @@ -50,3 +30,56 @@ data class ProxyEssentialsConfig( return playerCount.updateTime * 50L } } + +@ConfigSerializable +data class JoinState( + val name: String = "", + val permission: JoinStatePermission = JoinStatePermission(), + val forcedMotdLayout: String? = null +) + +@ConfigSerializable +data class JoinStatePermission( + val join: String = "", + val full: String = "" +) + +@ConfigSerializable +data class DomainMotdRoute( + val domain: String = "", + val target: String = "", + val rules: List = listOf() +) + +@ConfigSerializable +data class DomainMotdRule( + val state: String = "", + val layout: String = "" +) + +@ConfigSerializable +data class WhitelistConfig( + val enabled: Boolean = true, + val players: List = listOf("Notch") +) + +@ConfigSerializable +data class PlayerCountConfig( + val enabled: Boolean = true, + val additionalGroups: List = emptyList(), + val additionalPersistentServers: List = emptyList(), + val updateTime: Long = 20L +) + +@ConfigSerializable +data class TabListGroup( + val name: String = "*", + val layout: List = listOf(TabList()), + val updateTime: Long = 20L, +) + +@ConfigSerializable +data class TabList( + val header: String = "
SimpleCloud v3
", + val footer: String = "
players are playing on your network
sษชแดแด˜สŸแด‡แด„สŸแดแดœแด….แด€แด˜แด˜
", +) diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/YamlConfig.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/YamlConfig.kt deleted file mode 100644 index 6618724..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/YamlConfig.kt +++ /dev/null @@ -1,182 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config - -import app.simplecloud.plugin.proxy.shared.config.reactive.ReactiveConfig -import app.simplecloud.plugin.proxy.shared.config.reactive.ReactiveConfigInfo -import kotlinx.coroutines.* -import org.spongepowered.configurate.CommentedConfigurationNode -import org.spongepowered.configurate.kotlin.objectMapperFactory -import org.spongepowered.configurate.loader.ParsingException -import org.spongepowered.configurate.yaml.NodeStyle -import org.spongepowered.configurate.yaml.YamlConfigurationLoader -import java.io.File -import java.nio.file.* -import java.util.concurrent.ConcurrentHashMap -import java.util.logging.Logger - -open class YamlConfig(val dirPath: String) { - - private val logger = Logger.getLogger(YamlConfig::class.java.name) - private val watchService = FileSystems.getDefault().newWatchService() - private val configCache = ConcurrentHashMap() - private val reactiveConfigs = ConcurrentHashMap>>() - private var watcherJob: Job? = null - - init { - startWatcher() - } - - inline fun load(): ReactiveConfig { - return load(null) - } - - inline fun load(path: String?): ReactiveConfig { - return ReactiveConfig(this, path, T::class.java) - } - - internal fun loadDirect(path: String?, clazz: Class): T? { - val cacheKey = path ?: "default" - - try { - val node = buildNode(path, createIfMissing = false).first - val config = node.get(clazz) - - if (config != null) { - configCache[cacheKey] = config - } - - return config - } catch (ex: ParsingException) { - val file = File(if (path != null) "${dirPath}/${path.lowercase()}.yml" else dirPath) - logger.warning("Could not load config file ${file.name}. Using cached version if available.") - @Suppress("UNCHECKED_CAST") - return configCache[cacheKey] as? T - } - } - - internal fun registerReactiveConfig(path: String?, clazz: Class, reactiveConfig: ReactiveConfig) { - val cacheKey = path ?: "default" - val configs = reactiveConfigs.getOrPut(cacheKey) { mutableListOf() } - configs.add(ReactiveConfigInfo(clazz, reactiveConfig)) - } - - fun buildNode(path: String?, createIfMissing: Boolean = true): Pair { - val file = File(if (path != null) "${dirPath}/${path.lowercase()}.yml" else dirPath) - if (!file.exists()) { - if (!createIfMissing) { - val loader = createLoader(file.toPath()) - return Pair(loader.createNode(), loader) - } - file.parentFile.mkdirs() - file.createNewFile() - } - val loader = createLoader(file.toPath()) - return Pair(loader.load(), loader) - } - - private fun createLoader(path: Path): YamlConfigurationLoader { - val loader = YamlConfigurationLoader.builder() - .path(path) - .nodeStyle(NodeStyle.BLOCK) - .defaultOptions { options -> - options.serializers { builder -> - builder.registerAnnotatedObjects(objectMapperFactory()) - } - }.build() - return loader - } - - fun save(obj: T) { - save(null, obj) - } - - fun save(path: String?, obj: T) { - val pair = buildNode(path) - pair.first.set(obj) - pair.second.save(pair.first) - - // Update cache after successful save - val cacheKey = path ?: "default" - if (obj != null) { - configCache[cacheKey] = obj - } - } - - fun save(path: String?, reactiveConfig: ReactiveConfig) { - return save(path, reactiveConfig.get()) - } - - private fun startWatcher() { - val directory = File(dirPath).toPath() - if (!directory.toFile().exists()) { - directory.toFile().mkdirs() - } - - try { - directory.register( - watchService, - StandardWatchEventKinds.ENTRY_MODIFY - ) - - watcherJob = CoroutineScope(Dispatchers.IO).launch { - while (isActive) { - try { - val key = watchService.take() - for (event in key.pollEvents()) { - val path = event.context() as? Path ?: continue - val resolvedPath = directory.resolve(path) - - if (Files.isDirectory(resolvedPath) || !resolvedPath.toString().endsWith(".yml")) { - continue - } - - val kind = event.kind() - if (kind == StandardWatchEventKinds.ENTRY_MODIFY) { - handleFileChange(resolvedPath.toFile()) - } - } - key.reset() - } catch (e: Exception) { - logger.warning("Error in config watcher: ${e.message}") - } - } - } - } catch (e: Exception) { - logger.warning("Could not start config file watcher: ${e.message}") - } - } - - private fun handleFileChange(file: File) { - val fileName = file.nameWithoutExtension - val cacheKey = if (file.name == File(dirPath).name) "default" else fileName - - val reactiveConfigList = reactiveConfigs[cacheKey] ?: return - - reactiveConfigList.forEach { configInfo -> - try { - val configPath = if (cacheKey == "default") null else fileName - - @Suppress("UNCHECKED_CAST") - val typedConfigInfo = configInfo as ReactiveConfigInfo - val newValue = loadDirect(configPath, typedConfigInfo.clazz) - - typedConfigInfo.reactiveConfig.update(newValue) - - } catch (ex: ParsingException) { - logger.warning("Config file ${file.name} has parsing errors. Keeping old version.") - // Don't update the reactive config, keep the old cached version - } catch (e: Exception) { - logger.warning("Error updating reactive config: ${e.message}") - } - } - } - - fun close() { - watcherJob?.cancel() - try { - watchService.close() - } catch (e: Exception) { - logger.warning("Error closing watch service: ${e.message}") - } - } - -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/domain/DomainMotdRoute.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/domain/DomainMotdRoute.kt deleted file mode 100644 index 42b189c..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/domain/DomainMotdRoute.kt +++ /dev/null @@ -1,16 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config.domain - -import org.spongepowered.configurate.objectmapping.ConfigSerializable - -@ConfigSerializable -data class DomainMotdRule( - val state: String = "", - val layout: String = "" -) - -@ConfigSerializable -data class DomainMotdRoute( - val domain: String = "", - val target: String = "", - val rules: List = listOf() -) diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/message/KickMessageConfig.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/message/KickMessageConfig.kt deleted file mode 100644 index 72556ad..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/message/KickMessageConfig.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config.message - -import org.spongepowered.configurate.objectmapping.ConfigSerializable -import org.spongepowered.configurate.objectmapping.meta.Setting - -@ConfigSerializable -data class KickMessageConfig( - @Setting("no-permission") val noPermission: String = "The network is currently in maintenance mode. Please try again later.", - @Setting("network-full") val networkFull: String = "The network is currently full. Please try again later.", - @Setting("no-join-state") val noJoinState: String = "No join state found for server. Please try again later." -) \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/message/MessageConfig.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/message/MessageConfig.kt deleted file mode 100644 index fe2e827..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/message/MessageConfig.kt +++ /dev/null @@ -1,13 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config.message - -import org.spongepowered.configurate.objectmapping.ConfigSerializable - -@ConfigSerializable -data class MessageConfig( - val version: String = "2", - val variables: MessageVariables = MessageVariables(), - val kick: KickMessageConfig = KickMessageConfig(), - val command: CommandMessages = CommandMessages() -) { - fun resolve(message: String): String = message.replace("", variables.prefix) -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/message/MessageVariables.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/message/MessageVariables.kt deleted file mode 100644 index bef74f5..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/message/MessageVariables.kt +++ /dev/null @@ -1,8 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config.message - -import org.spongepowered.configurate.objectmapping.ConfigSerializable - -@ConfigSerializable -data class MessageVariables( - val prefix: String = "โšก " -) diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/motd/MaxPlayerDisplayType.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/motd/MaxPlayerDisplayType.kt deleted file mode 100644 index 81508cc..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/motd/MaxPlayerDisplayType.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config.motd - -enum class MaxPlayerDisplayType { - REAL, - FAKE, - DYNAMIC, -} \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/motd/MotdUpdateType.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/motd/MotdUpdateType.kt deleted file mode 100644 index 3100616..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/motd/MotdUpdateType.kt +++ /dev/null @@ -1,6 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config.motd - -enum class MotdUpdateType { - RANDOM, - QUEUE -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/placeholder/PingColor.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/placeholder/PingColor.kt deleted file mode 100644 index 1d7a447..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/placeholder/PingColor.kt +++ /dev/null @@ -1,9 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config.placeholder - -import org.spongepowered.configurate.objectmapping.ConfigSerializable - -@ConfigSerializable -data class PingColor( - val ping: Int, - val color: String -) diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/placeholder/PlaceHolderConfiguration.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/placeholder/PlaceHolderConfiguration.kt deleted file mode 100644 index 2fa1139..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/placeholder/PlaceHolderConfiguration.kt +++ /dev/null @@ -1,16 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config.placeholder - -import org.spongepowered.configurate.objectmapping.ConfigSerializable - -@ConfigSerializable -data class PlaceHolderConfiguration( - val currentDateFormat: String = "dd.MM.yyyy", - val currentTimeFormat: String = "HH:mm:ss", - val pingColors: List = listOf( - PingColor(0, ""), - PingColor(50, ""), - PingColor(100, ""), - PingColor(150, ""), - PingColor(200, "") - ) -) \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/reactive/ReactiveConfig.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/reactive/ReactiveConfig.kt deleted file mode 100644 index 8af4358..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/reactive/ReactiveConfig.kt +++ /dev/null @@ -1,29 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config.reactive - -import app.simplecloud.plugin.proxy.shared.config.YamlConfig - -class ReactiveConfig( - val config: YamlConfig, - val path: String?, - val clazz: Class -) { - - @Volatile - private var currentValue: T? = config.loadDirect(path, clazz) - - init { - config.registerReactiveConfig(path, clazz, this) - } - - fun get(): T = currentValue?: throw NullPointerException("Reactive config is not initialized") - - internal fun update(newValue: T?) { - currentValue = newValue - } - - fun reload() { - val newValue = config.loadDirect(path, clazz) - update(newValue) - } - -} \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/reactive/ReactiveConfigInfo.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/reactive/ReactiveConfigInfo.kt deleted file mode 100644 index 481b7a7..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/reactive/ReactiveConfigInfo.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config.reactive - - -data class ReactiveConfigInfo( - val clazz: Class, - val reactiveConfig: ReactiveConfig -) \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/state/JoinState.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/state/JoinState.kt deleted file mode 100644 index 6bda47c..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/state/JoinState.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config.state - -import org.spongepowered.configurate.objectmapping.ConfigSerializable -import org.spongepowered.configurate.objectmapping.meta.Setting - -@ConfigSerializable -data class JoinState( - val name: String = "", - val permission: JoinStatePermission = JoinStatePermission(), - @Setting("forced-motd-layout") val forcedMotdLayout: String? = null -) diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/state/JoinStatePermission.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/state/JoinStatePermission.kt deleted file mode 100644 index ff2cfbb..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/state/JoinStatePermission.kt +++ /dev/null @@ -1,9 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config.state - -import org.spongepowered.configurate.objectmapping.ConfigSerializable - -@ConfigSerializable -data class JoinStatePermission( - val join: String = "", - val full: String = "" -) diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/state/WhitelistConfig.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/state/WhitelistConfig.kt deleted file mode 100644 index 2b90a72..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/state/WhitelistConfig.kt +++ /dev/null @@ -1,9 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config.state - -import org.spongepowered.configurate.objectmapping.ConfigSerializable - -@ConfigSerializable -data class WhitelistConfig( - val enabled: Boolean = true, - val players: List = listOf("Notch") -) diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/tablis/TabList.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/tablis/TabList.kt deleted file mode 100644 index dd3890a..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/tablis/TabList.kt +++ /dev/null @@ -1,9 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config.tablis - -import org.spongepowered.configurate.objectmapping.ConfigSerializable - -@ConfigSerializable -data class TabList( - val header: String = "
SimpleCloud v3
", - val footer: String = "
players are playing on your network
sษชแดแด˜สŸแด‡แด„สŸแดแดœแด….แด€แด˜แด˜
", -) diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/tablis/TabListGroup.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/tablis/TabListGroup.kt deleted file mode 100644 index d2f073c..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/tablis/TabListGroup.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config.tablis - -import org.spongepowered.configurate.objectmapping.ConfigSerializable -import org.spongepowered.configurate.objectmapping.meta.Setting - -@ConfigSerializable -data class TabListGroup( - val name: String = "*", - val layout: List = listOf(TabList()), - @Setting("update-time") val updateTime: Long = 20L, -) diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/event/MotdConfiguration.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/event/MotdConfiguration.kt deleted file mode 100644 index ecf186a..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/event/MotdConfiguration.kt +++ /dev/null @@ -1,13 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.event - -import app.simplecloud.plugin.proxy.shared.config.motd.MaxPlayerDisplayType -import net.kyori.adventure.text.Component - -data class MotdConfiguration( - var messageOfTheDay: Component, - var playerInfo: List, - var versionName: String, - var maxPlayerDisplayType: MaxPlayerDisplayType?, - var dynamicPlayerRange: Int -) { -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/event/TabListPlayerConfiguration.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/event/TabListPlayerConfiguration.kt deleted file mode 100644 index 15bb202..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/event/TabListPlayerConfiguration.kt +++ /dev/null @@ -1,9 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.event - -import net.kyori.adventure.text.Component - -data class TabListPlayerConfiguration( - var header: Component, - var footer: Component, -) { -} \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/format/MotdMiniMessageFormatter.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/format/MotdMiniMessageFormatter.kt index 5102859..0ff481b 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/format/MotdMiniMessageFormatter.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/format/MotdMiniMessageFormatter.kt @@ -25,6 +25,7 @@ object MotdMiniMessageFormatter { private const val SPACE_WIDTH = 4 private const val PIXEL_PADDING_CHARACTER = '\u200c' private val defaultFont = Key.key("minecraft", "default") + private val mergesWithoutInsertion = Style.Merge.all().minus(Style.Merge.INSERTION) private val centerTagResolver = TagResolver.resolver(CENTER_TAG) { _, _ -> Tag.inserting(Component.empty().insertion(CENTER_MARKER)) @@ -46,6 +47,11 @@ object MotdMiniMessageFormatter { .append(applyCentering(miniMessage.deserialize(line2, *resolvers))) } + /** Drops the [CENTER_MARKER] insertion again once the marked component has been measured. */ + private fun clearInsertion(style: Style): Style { + return Style.empty().merge(style, Style.Merge.Strategy.ALWAYS, mergesWithoutInsertion) + } + private fun applyCentering(component: Component, inheritedStyle: Style = Style.empty()): Component { val style = component.style().merge(inheritedStyle, Style.Merge.Strategy.IF_ABSENT_ON_TARGET) var processed = component.children( @@ -55,7 +61,7 @@ object MotdMiniMessageFormatter { // Resolve on the finished tree so styles outside
and inserted components are measurable. if (processed.insertion() != CENTER_MARKER) return processed - processed = processed.insertion(null) + processed = processed.style(clearInsertion(processed.style())) val contentWidth = MinecraftTextWidth.width(processed, inheritedStyle) val paddingWidth = ((MOTD_CENTER_WIDTH - contentWidth) / 2f) .roundToInt() @@ -90,6 +96,7 @@ internal object MinecraftTextWidth { private const val UNIHEX_BOLD_OFFSET = 0.5f private const val UNICODE_CODE_POINT_COUNT = 0x110000 private val defaultFont = Key.key("minecraft", "default") + private val mergesWithoutInsertion = Style.Merge.all().minus(Style.Merge.INSERTION) private val altFont = Key.key("minecraft", "alt") private val uniformFont = Key.key("minecraft", "uniform") diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/DomainMotdHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/DomainMotdHandler.kt index 7db1d78..f77955b 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/DomainMotdHandler.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/DomainMotdHandler.kt @@ -1,7 +1,7 @@ package app.simplecloud.plugin.proxy.shared.handler import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.config.domain.DomainMotdRoute +import app.simplecloud.plugin.proxy.shared.config.DomainMotdRoute import kotlinx.coroutines.* import java.util.concurrent.ConcurrentHashMap import java.util.logging.Logger diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/JoinStateResolver.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/JoinStateResolver.kt index 641bead..eb9f4a1 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/JoinStateResolver.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/JoinStateResolver.kt @@ -2,7 +2,7 @@ package app.simplecloud.plugin.proxy.shared.handler import app.simplecloud.plugin.api.shared.pattern.ServerPatternIdentifier import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.config.state.JoinState +import app.simplecloud.plugin.proxy.shared.config.JoinState import java.util.logging.Logger class JoinStateResolver( diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/MotdLayoutHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/MotdLayoutHandler.kt index 6ad34ef..8d050f9 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/MotdLayoutHandler.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/MotdLayoutHandler.kt @@ -1,26 +1,27 @@ package app.simplecloud.plugin.proxy.shared.handler +import app.simplecloud.plugin.api.shared.config.YamlDirectoryRepository import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.config.motd.MotdEntry -import app.simplecloud.plugin.proxy.shared.config.motd.MotdLayoutConfiguration -import app.simplecloud.plugin.proxy.shared.config.motd.MotdUpdateType -import app.simplecloud.plugin.proxy.shared.temp.YamlDirectoryRepository -import java.io.File +import app.simplecloud.plugin.proxy.shared.config.MotdEntry +import app.simplecloud.plugin.proxy.shared.config.MotdLayoutConfiguration +import app.simplecloud.plugin.proxy.shared.config.MotdUpdateType import java.nio.file.Files import java.nio.file.Path import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import java.util.logging.Level import java.util.logging.Logger class MotdLayoutHandler( private val directory: Path, private val proxyPlugin: ProxyPlugin -) : YamlDirectoryRepository( +) : YamlDirectoryRepository( directory, MotdLayoutConfiguration::class.java, ) { private val logger = Logger.getLogger(MotdLayoutHandler::class.java.name) + private val layoutsByName = ConcurrentHashMap() private val queueIndexes = ConcurrentHashMap() private val listenerRegistered = AtomicBoolean(false) @@ -29,39 +30,42 @@ class MotdLayoutHandler( companion object { const val MOTD_LAYOUT_KEY = "motd-layout" + private const val LAYOUT_FILE_SUFFIX = ".yml" } - override fun transformBeforeLoad(file: File, content: String): String { - val lines = content.lines().toMutableList() - val firstContentIndex = lines.indexOfFirst { it.isNotBlank() } - if (firstContentIndex == -1 || !lines[firstContentIndex].isLayoutFileVersion()) return content + override fun save(entity: MotdLayoutConfiguration) { + val name = entity.motd.layouts.firstOrNull()?.name ?: proxyPlugin.proxyEssentialsConfig.get().initialLayout + save("$name$LAYOUT_FILE_SUFFIX", entity) + layoutsByName[name] = entity + } - val hasVersionSettings = lines - .drop(firstContentIndex + 1) - .any { it == "version:" } + override fun find(identifier: String): MotdLayoutConfiguration? = layoutsByName[identifier] - if (!hasVersionSettings) return content + fun loadMotdLayouts() { + if (!Files.isDirectory(directory)) { + layoutsByName.clear() + return + } - lines.removeAt(firstContentIndex) - if (firstContentIndex < lines.size && lines[firstContentIndex].isBlank()) { - lines.removeAt(firstContentIndex) + val loaded = mutableMapOf() + Files.list(directory).use { paths -> + paths + .filter { Files.isRegularFile(it) && it.fileName.toString().endsWith(LAYOUT_FILE_SUFFIX) } + .forEach { path -> + val layout = loadLayout(path) ?: return@forEach + loaded[path.fileName.toString().removeSuffix(LAYOUT_FILE_SUFFIX)] = layout + } } - return lines.joinToString(System.lineSeparator()) - } - fun loadMotdLayouts() { - load() + layoutsByName.keys.retainAll(loaded.keys) + layoutsByName.putAll(loaded) } - private fun getMotdLayout(name: String): MotdLayoutConfiguration = - entities[directory.resolve("${name}.yml").toFile()] ?: MotdLayoutConfiguration() - - fun getLayoutByName(name: String): MotdLayoutConfiguration? = - entities[directory.resolve("${name}.yml").toFile()] + fun getLayoutByName(name: String): MotdLayoutConfiguration? = find(name) fun getCurrentMotdLayout(): MotdLayoutConfiguration { val layoutName = localLayout?.takeIf(::hasLayout) ?: fallbackLayoutName() - return getMotdLayout(layoutName) + return find(layoutName) ?: MotdLayoutConfiguration() } fun setLocalLayout(layoutName: String?, logChange: Boolean = false) { @@ -86,13 +90,6 @@ class MotdLayoutHandler( } } - private fun fallbackLayoutName(): String { - val localState = proxyPlugin.joinStateHandler.localState - val config = proxyPlugin.proxyEssentialsConfig.get() - val joinState = config.joinstates.find { it.name == localState } - return joinState?.forcedMotdLayout ?: config.initialLayout - } - fun selectEntry(layout: MotdLayoutConfiguration, layoutKey: String): MotdEntry? { val entries = layout.motd.layouts if (entries.isEmpty()) return null @@ -111,19 +108,28 @@ class MotdLayoutHandler( return Files.list(directory).use { paths -> paths - .filter { Files.isRegularFile(it) && it.fileName.toString().endsWith(".yml") } - .map { it.fileName.toString().removeSuffix(".yml") } + .filter { Files.isRegularFile(it) && it.fileName.toString().endsWith(LAYOUT_FILE_SUFFIX) } + .map { it.fileName.toString().removeSuffix(LAYOUT_FILE_SUFFIX) } .sorted() .toList() } } - private fun hasLayout(layoutName: String): Boolean { - return Files.isRegularFile(directory.resolve("${layoutName}.yml")) + private fun loadLayout(path: Path): MotdLayoutConfiguration? { + return try { + load(path.toFile()) + } catch (e: Exception) { + logger.log(Level.WARNING, "Could not load MOTD layout ${path.fileName}", e) + null + } } - private fun String.isLayoutFileVersion(): Boolean { - val trimmed = trim() - return trimmed == "version: '1'" || trimmed == "version: \"1\"" || trimmed == "version: 1" + private fun fallbackLayoutName(): String { + val localState = proxyPlugin.joinStateHandler.localState + val config = proxyPlugin.proxyEssentialsConfig.get() + val joinState = config.joinstates.find { it.name == localState } + return joinState?.forcedMotdLayout ?: config.initialLayout } + + private fun hasLayout(layoutName: String): Boolean = layoutsByName.containsKey(layoutName) } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/ProxyJoinGate.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/ProxyJoinGate.kt index 23a9cda..a20c1a5 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/ProxyJoinGate.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/ProxyJoinGate.kt @@ -1,7 +1,7 @@ package app.simplecloud.plugin.proxy.shared.handler -import app.simplecloud.plugin.proxy.shared.config.message.KickMessageConfig -import app.simplecloud.plugin.proxy.shared.config.state.JoinState +import app.simplecloud.plugin.proxy.shared.config.KickMessageConfig +import app.simplecloud.plugin.proxy.shared.config.JoinState import java.util.logging.Logger class ProxyJoinGate( diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/TabListResolver.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/TabListResolver.kt index ba1e1e8..bbedf5d 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/TabListResolver.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/TabListResolver.kt @@ -1,6 +1,6 @@ package app.simplecloud.plugin.proxy.shared.handler -import app.simplecloud.plugin.proxy.shared.config.tablis.TabListGroup +import app.simplecloud.plugin.proxy.shared.config.TabListGroup class TabListResolver( private val getTabListGroups: () -> List diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/JoinStateCommandHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/JoinStateCommandHandler.kt index 4ef6088..32296bc 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/JoinStateCommandHandler.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/JoinStateCommandHandler.kt @@ -1,6 +1,7 @@ package app.simplecloud.plugin.proxy.shared.handler.command import app.simplecloud.plugin.proxy.shared.ProxyPlugin +import app.simplecloud.plugin.proxy.shared.config.MessageConfig import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -176,7 +177,7 @@ class JoinStateCommandHandler( ) } - private suspend fun handleSetGroup(sender: CommandSender, group: String, state: String, msgs: app.simplecloud.plugin.proxy.shared.config.message.MessageConfig) { + private suspend fun handleSetGroup(sender: CommandSender, group: String, state: String, msgs: MessageConfig) { if (proxyPlugin.joinStateResolver.resolveJoinState(state) == null) { sender.sendMessage(msgs.resolve(msgs.command.joinState.group.updateFailure)) return @@ -205,7 +206,7 @@ class JoinStateCommandHandler( ) } - private suspend fun handleSetService(sender: CommandSender, group: String, id: Int, state: String, msgs: app.simplecloud.plugin.proxy.shared.config.message.MessageConfig) { + private suspend fun handleSetService(sender: CommandSender, group: String, id: Int, state: String, msgs: MessageConfig) { if (proxyPlugin.joinStateResolver.resolveJoinState(state) == null) { sender.sendMessage(msgs.resolve(msgs.command.joinState.server.updateFailure)) return diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/ProxyEssentialsCommandHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/ProxyEssentialsCommandHandler.kt index 734a36f..44e08a0 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/ProxyEssentialsCommandHandler.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/ProxyEssentialsCommandHandler.kt @@ -42,6 +42,7 @@ class ProxyEssentialsCommandHandler( val helpEntry = msgs.command.joinState.help.entry sender.sendMessage(msgs.resolve("Available /scproxy commands:")) sender.sendMessage(helpEntry.replace("", "/scproxy help")) + sender.sendMessage(helpEntry.replace("", "/scproxy reload")) sender.sendMessage(helpEntry.replace("", "/scproxy joinstate help")) sender.sendMessage(helpEntry.replace("", "/scproxy joinstate info ")) sender.sendMessage(helpEntry.replace("", "/scproxy joinstate info ")) @@ -56,32 +57,30 @@ class ProxyEssentialsCommandHandler( commandManager.command( commandManager.commandBuilder("scproxy") .literal("reload") - .permission("simplecloud.proxy-essentials.command.help") - .handler { context: CommandContext -> - val sender = context.sender() + .permission("simplecloud.proxy-essentials.command.reload") + .handler { context: CommandContext -> reload(context.sender()) } + .build() + ) + } - CoroutineScope(Dispatchers.IO).launch { - try { - proxyPlugin.messagesConfiguration.reload() - val messages = proxyPlugin.messagesConfiguration.get() - sender.sendMessage(messages.resolve(messages.command.reload.start)) + private fun reload(sender: C) { + val messagesBeforeReload = proxyPlugin.messagesConfiguration.get() + sender.sendMessage(messagesBeforeReload.resolve(messagesBeforeReload.command.reload.start)) - proxyPlugin.proxyEssentialsConfig.reload() - proxyPlugin.placeHolderConfiguration.reload() - proxyPlugin.motdLayoutHandler.loadMotdLayouts() + CoroutineScope(Dispatchers.IO).launch { + try { + proxyPlugin.reload() - sender.sendMessage(messages.resolve(messages.command.reload.success)) - } catch (e: Exception) { - val messages = proxyPlugin.messagesConfiguration.get() - sender.sendMessage( - messages.resolve(messages.command.reload.failure) - .replace("", e.message ?: "Unknown error") - ) - logger.log(Level.SEVERE, "Error reloading configuration", e) - } - } - } - .build() - ) + val messages = proxyPlugin.messagesConfiguration.get() + sender.sendMessage(messages.resolve(messages.command.reload.success)) + } catch (e: Exception) { + val messages = proxyPlugin.messagesConfiguration.get() + sender.sendMessage( + messages.resolve(messages.command.reload.failure) + .replace("", e.message ?: "Unknown error") + ) + logger.log(Level.SEVERE, "Error reloading configuration", e) + } + } } } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/resolver/TagResolverHelper.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/resolver/TagResolverHelper.kt index 2b179ed..780aaa7 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/resolver/TagResolverHelper.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/resolver/TagResolverHelper.kt @@ -1,8 +1,8 @@ package app.simplecloud.plugin.proxy.shared.resolver -import app.simplecloud.plugin.proxy.shared.config.motd.MaxPlayerDisplayType -import app.simplecloud.plugin.proxy.shared.config.motd.MotdLayoutConfiguration -import app.simplecloud.plugin.proxy.shared.config.placeholder.PingColor +import app.simplecloud.plugin.proxy.shared.config.MaxPlayerDisplayType +import app.simplecloud.plugin.proxy.shared.config.MotdLayoutConfiguration +import app.simplecloud.plugin.proxy.shared.config.PingColor import net.kyori.adventure.text.minimessage.Context import net.kyori.adventure.text.minimessage.tag.Tag import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue @@ -52,7 +52,7 @@ object TagResolverHelper { realMaxPlayers: Int, motdConfiguration: MotdLayoutConfiguration ): TagResolver { - val slots = motdConfiguration.versionSettings.slots + val slots = motdConfiguration.version.slots val maxPlayers = if (!slots.enabled) realMaxPlayers else when (slots.type) { MaxPlayerDisplayType.REAL -> realMaxPlayers MaxPlayerDisplayType.FAKE -> slots.fakeSlots diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/temp/YamlDirectoryRepository.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/temp/YamlDirectoryRepository.kt deleted file mode 100644 index 4d3eafc..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/temp/YamlDirectoryRepository.kt +++ /dev/null @@ -1,199 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.temp - -import kotlinx.coroutines.* -import org.spongepowered.configurate.ConfigurationNode -import org.spongepowered.configurate.ConfigurationOptions -import org.spongepowered.configurate.kotlin.objectMapperFactory -import org.spongepowered.configurate.loader.ParsingException -import org.spongepowered.configurate.serialize.SerializationException -import org.spongepowered.configurate.serialize.TypeSerializer -import org.spongepowered.configurate.yaml.NodeStyle -import org.spongepowered.configurate.yaml.YamlConfigurationLoader -import java.io.BufferedReader -import java.io.File -import java.io.StringReader -import java.lang.reflect.Type -import java.nio.file.* - -abstract class YamlDirectoryRepository( - private val directory: Path, - private val clazz: Class, -) { - - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private val watchService = FileSystems.getDefault().newWatchService() - private var watcherJob: Job? = null - private val loaders = mutableMapOf() - protected val entities = mutableMapOf() - - fun delete(element: E): Boolean { - val file = entities.keys.find { entities[it] == element } ?: return false - return deleteFile(file) - } - - fun getAll(): List { - return entities.values.toList() - } - - fun load(): List { - if (!directory.toFile().exists()) { - directory.toFile().mkdirs() - } - - ensureWatcherRegistered() - - return Files.list(directory) - .toList() - .filter { !it.toFile().isDirectory && it.toString().endsWith(".yml") } - .mapNotNull { load(it.toFile()) } - } - - open fun watchUpdateEvent(file: File) {} - - protected open fun transformBeforeLoad(file: File, content: String): String = content - - private fun load(file: File): E? { - try { - val loader = getOrCreateLoader(file, forRead = true) - val node = loader.load(ConfigurationOptions.defaults()) - val entity = node.get(clazz) ?: return null - entities[file] = entity - return entity - } catch (ex: ParsingException) { - val existedBefore = entities.containsKey(file) - if (existedBefore) { - return null - } - return null - } catch (ex: SerializationException) { - val existedBefore = entities.containsKey(file) - if (existedBefore) { - return null - } - return null - } - } - - private fun deleteFile(file: File): Boolean { - val deletedSuccessfully = file.delete() - val removedSuccessfully = entities.remove(file) != null - return deletedSuccessfully && removedSuccessfully - } - - fun save(fileName: String, entity: E) { - val file = directory.resolve(fileName).toFile() - val loader = getOrCreateLoader(file, forRead = false) - val node = loader.createNode(ConfigurationOptions.defaults().serializers { - it.register(Enum::class.java, GenericEnumSerializer) - }) - node.set(clazz, entity) - loader.save(node) - entities[file] = entity - } - - private fun getOrCreateLoader(file: File, forRead: Boolean): YamlConfigurationLoader { - if (forRead) { - return createLoader(file, transformBeforeLoad(file, file.readText())) - } - - return loaders.getOrPut(file) { - createLoader(file) - } - } - - private fun createLoader(file: File, sourceContent: String? = null): YamlConfigurationLoader { - val builder = YamlConfigurationLoader.builder() - .path(file.toPath()) - .nodeStyle(NodeStyle.BLOCK) - .nodeStyle(NodeStyle.BLOCK) - .defaultOptions { options -> - options.serializers { builder -> - builder.registerAnnotatedObjects(objectMapperFactory()) - builder.register(Enum::class.java, GenericEnumSerializer) - } - } - - if (sourceContent != null) { - builder.source { BufferedReader(StringReader(sourceContent)) } - } - - return builder.build() - } - - private fun ensureWatcherRegistered() { - if (watcherJob?.isActive == true) { - return - } - directory.register( - watchService, - StandardWatchEventKinds.ENTRY_CREATE, - StandardWatchEventKinds.ENTRY_DELETE, - StandardWatchEventKinds.ENTRY_MODIFY - ) - - watcherJob = scope.launch { - while (isActive) { - try { - val key = watchService.take() - for (event in key.pollEvents()) { - val path = event.context() as? Path ?: continue - val resolvedPath = directory.resolve(path) - if (Files.isDirectory(resolvedPath) || !resolvedPath.toString().endsWith(".yml")) { - continue - } - val kind = event.kind() - when (kind) { - StandardWatchEventKinds.ENTRY_CREATE -> { - load(resolvedPath.toFile()) - } - - StandardWatchEventKinds.ENTRY_MODIFY -> { - load(resolvedPath.toFile()) - watchUpdateEvent(resolvedPath.toFile()) - } - - StandardWatchEventKinds.ENTRY_DELETE -> { - deleteFile(resolvedPath.toFile()) - } - } - } - if (!key.reset()) { - break - } - } catch (_: ClosedWatchServiceException) { - break - } catch (_: InterruptedException) { - break - } - } - } - } - - fun close() { - watcherJob?.cancel() - scope.cancel() - watchService.close() - } - - private object GenericEnumSerializer : TypeSerializer> { - override fun deserialize(type: Type, node: ConfigurationNode): Enum<*> { - val value = node.string ?: throw SerializationException("No value present in node") - - if (type !is Class<*> || !type.isEnum) { - throw SerializationException("Type is not an enum class") - } - - @Suppress("UNCHECKED_CAST") - return try { - java.lang.Enum.valueOf(type as Class>, value) - } catch (e: IllegalArgumentException) { - throw SerializationException("Invalid enum constant") - } - } - - override fun serialize(type: Type, obj: Enum<*>?, node: ConfigurationNode) { - node.set(obj?.name) - } - } - -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/LocalPingSourceMatcher.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/LocalPingSourceMatcher.kt similarity index 90% rename from proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/LocalPingSourceMatcher.kt rename to proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/LocalPingSourceMatcher.kt index 5fb646c..77fd8fd 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/LocalPingSourceMatcher.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/LocalPingSourceMatcher.kt @@ -1,4 +1,4 @@ -package app.simplecloud.plugin.proxy.shared.handler +package app.simplecloud.plugin.proxy.shared.utilities import java.net.InetAddress import java.net.NetworkInterface @@ -19,4 +19,4 @@ class LocalPingSourceMatcher( return runCatching { isLocalInterfaceAddress(address) }.getOrDefault(false) } -} +} \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/ServerIconLoader.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/ServerIconLoader.kt similarity index 98% rename from proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/ServerIconLoader.kt rename to proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/ServerIconLoader.kt index d600dfc..111a3d6 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/ServerIconLoader.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/ServerIconLoader.kt @@ -1,4 +1,4 @@ -package app.simplecloud.plugin.proxy.shared.handler +package app.simplecloud.plugin.proxy.shared.utilities import java.awt.RenderingHints import java.awt.image.BufferedImage @@ -102,4 +102,4 @@ class ServerIconLoader( private companion object { const val FAVICON_SIZE = 64 } -} +} \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/config/ConfigVersion.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/config/ConfigVersion.kt new file mode 100644 index 0000000..56b8807 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/config/ConfigVersion.kt @@ -0,0 +1,7 @@ +package app.simplecloud.plugin.proxy.shared.utilities.config + +object ConfigVersion { + + const val VERSION = 2 + +} \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/config/DefaultConfigs.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/config/DefaultConfigs.kt new file mode 100644 index 0000000..7a384f0 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/config/DefaultConfigs.kt @@ -0,0 +1,40 @@ +package app.simplecloud.plugin.proxy.shared.utilities.config + +import app.simplecloud.plugin.proxy.shared.config.JoinState +import app.simplecloud.plugin.proxy.shared.config.JoinStatePermission +import app.simplecloud.plugin.proxy.shared.config.PingColor +import app.simplecloud.plugin.proxy.shared.config.TabList +import app.simplecloud.plugin.proxy.shared.config.TabListGroup + +object DefaultConfigs { + + val VARIABLES: Map = mapOf("prefix" to "โšก ") + + val PING_COLORS: List = listOf( + PingColor(0, ""), + PingColor(50, ""), + PingColor(100, ""), + PingColor(150, ""), + PingColor(200, "") + ) + + val JOIN_STATES: List = listOf( + JoinState("public", JoinStatePermission("", "simplecloud.proxy-essentials.join.full.public")), + JoinState( + "maintenance", + JoinStatePermission( + "simplecloud.proxy-essentials.join.maintenance", + "simplecloud.proxy-essentials.join.maintenance" + ), + "maintenance" + ) + ) + + val TABLIST_GROUPS: List = listOf( + TabListGroup( + name = "global", + layout = listOf(TabList()), + updateTime = 20L + ) + ) +} \ No newline at end of file diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ProxyPingListener.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ProxyPingListener.kt index 98691c9..e586dfa 100644 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ProxyPingListener.kt +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ProxyPingListener.kt @@ -1,9 +1,9 @@ package app.simplecloud.plugin.proxy.velocity.listener import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.config.motd.MaxPlayerDisplayType -import app.simplecloud.plugin.proxy.shared.handler.LocalPingSourceMatcher -import app.simplecloud.plugin.proxy.shared.handler.ServerIconLoader +import app.simplecloud.plugin.proxy.shared.config.MaxPlayerDisplayType +import app.simplecloud.plugin.proxy.shared.utilities.LocalPingSourceMatcher +import app.simplecloud.plugin.proxy.shared.utilities.ServerIconLoader import app.simplecloud.plugin.proxy.velocity.ProxyVelocityPlugin import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.proxy.ProxyPingEvent @@ -54,18 +54,18 @@ class ProxyPingListener( val realMax = playerCountHandler.maxPlayers(plugin.proxyServer.configuration.showMaxPlayers) val samplePlayers: List = - if (layout.playerList.enabled && layout.playerList.entries.isNotEmpty()) { - layout.playerList.entries.map { SamplePlayer(it, UUID.randomUUID()) } + if (layout.playerList.enabled && layout.playerList.playerList.isNotEmpty()) { + layout.playerList.playerList.map { SamplePlayer(it, UUID.randomUUID()) } } else { players?.sample ?: emptyList() } // slots - val maxPlayers = if (layout.versionSettings.slots.enabled) { - when (layout.versionSettings.slots.type) { + val maxPlayers = if (layout.version.slots.enabled) { + when (layout.version.slots.type) { MaxPlayerDisplayType.REAL -> realMax - MaxPlayerDisplayType.FAKE -> layout.versionSettings.slots.fakeSlots - MaxPlayerDisplayType.DYNAMIC -> onlinePlayers + layout.versionSettings.slots.dynamicPlayerRange + MaxPlayerDisplayType.FAKE -> layout.version.slots.fakeSlots + MaxPlayerDisplayType.DYNAMIC -> onlinePlayers + layout.version.slots.dynamicPlayerRange } } else { realMax @@ -77,8 +77,8 @@ class ProxyPingListener( .samplePlayers(*samplePlayers.toTypedArray()) // version name - if (layout.versionSettings.name.enabled) { - builder.version(ServerPing.Version(event.ping.version.protocol, layout.versionSettings.name.text)) + if (layout.version.name.enabled) { + builder.version(ServerPing.Version(event.ping.version.protocol, layout.version.name.text)) } } diff --git a/proxy-velocity/src/main/resources/config/placeholder.yml b/proxy-velocity/src/main/resources/config/placeholder.yml index 31e9f6c..3f3892e 100644 --- a/proxy-velocity/src/main/resources/config/placeholder.yml +++ b/proxy-velocity/src/main/resources/config/placeholder.yml @@ -1,6 +1,6 @@ -currentDateFormat: "dd.MM.yyyy" -currentTimeFormat: "HH:mm:ss" -pingColors: +current-date-format: "dd.MM.yyyy" +current-time-format: "HH:mm:ss" +ping-colors: - ping: 0 color: "" - ping: 50 From 3363c3640fe08eef72f442b29bd220028905598d Mon Sep 17 00:00:00 2001 From: Janis Date: Wed, 26 Aug 2026 15:04:10 +0200 Subject: [PATCH 04/17] refactor: improve code readability --- .../bungeecord/BungeeCordCommandSender.kt | 21 +- .../proxy/bungeecord/ComponentExtension.kt | 9 + .../proxy/bungeecord/ProxyBungeeCordPlugin.kt | 129 +++---- .../listener/ConfigureTagResolversListener.kt | 36 -- .../bungeecord/listener/PostLoginListener.kt | 4 +- .../bungeecord/listener/ProxyPingListener.kt | 112 ++++--- .../bungeecord/listener/ServerKickListener.kt | 13 +- .../listener/ServerPreConnectListener.kt | 46 +-- .../ConfigureTagResolversEvent.kt | 10 +- .../ConfigureTagResolversListener.kt | 30 ++ .../{handler => tablist}/TabListHandler.kt | 40 ++- .../{listener => tablist}/TabListListener.kt | 10 +- .../plugin/proxy/shared/ProxyPlugin.kt | 78 +++-- .../shared/command/ProxyCommandSender.kt | 7 + .../command/ProxyEssentialsCommandHandler.kt | 88 +++++ .../commands/JoinStateCommandHandler.kt | 275 +++++++++++++++ .../command/commands/LayoutCommandHandler.kt | 183 ++++++++++ .../shared/config/MotdLayoutConfiguration.kt | 13 +- .../shared/config/ProxyEssentialsConfig.kt | 14 +- .../{ => migration}/OldConfigMigrator.kt | 13 +- .../CloudControllerHandler.kt | 314 ++++++++++-------- .../shared/format/MotdMiniMessageFormatter.kt | 187 ----------- .../proxy/shared/handler/JoinStateHandler.kt | 240 ------------- .../proxy/shared/handler/JoinStateResolver.kt | 72 ---- .../shared/handler/PlayerCountHandler.kt | 147 -------- .../proxy/shared/handler/ProxyJoinGate.kt | 45 --- .../shared/handler/command/CommandSender.kt | 6 - .../command/JoinStateCommandHandler.kt | 232 ------------- .../handler/command/LayoutCommandHandler.kt | 168 ---------- .../command/ProxyEssentialsCommandHandler.kt | 86 ----- .../shared/joinstate/JoinStateResolver.kt | 79 +++++ .../shared/joinstate/JoinStateService.kt | 221 ++++++++++++ .../proxy/shared/joinstate/ProxyJoinGate.kt | 48 +++ .../DomainMotdResolver.kt} | 67 ++-- .../MotdLayoutRepository.kt} | 83 ++--- .../{utilities => motd}/ServerIconLoader.kt | 70 ++-- .../TagResolverHelper.kt | 38 +-- .../TagResolverNames.kt | 2 +- .../proxy/shared/player/PlayerCountTracker.kt | 153 +++++++++ .../{handler => tablist}/TabListResolver.kt | 27 +- .../utilities/LocalPingSourceMatcher.kt | 13 +- .../shared/utilities/ProxyPermissions.kt | 13 + .../utilities/format/MinecraftTextWidth.kt | 114 +++++++ .../format/MotdMiniMessageFormatter.kt | 69 ++++ .../proxy/velocity/ProxyVelocityPlugin.kt | 113 ++++--- .../proxy/velocity/VelocityCommandSender.kt | 16 +- .../listener/ConfigureTagResolversListener.kt | 38 --- .../proxy/velocity/listener/LoginListener.kt | 2 +- .../velocity/listener/ProxyPingListener.kt | 121 +++---- .../velocity/listener/ServerKickListener.kt | 10 +- .../listener/ServerPreConnectListener.kt | 45 +-- .../ConfigureTagResolversEvent.kt | 5 +- .../ConfigureTagResolversListener.kt | 31 ++ .../{handler => tablist}/TabListHandler.kt | 39 ++- 54 files changed, 2096 insertions(+), 1949 deletions(-) create mode 100644 proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ComponentExtension.kt delete mode 100644 proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ConfigureTagResolversListener.kt rename proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/{event => placeholder}/ConfigureTagResolversEvent.kt (70%) create mode 100644 proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/placeholder/ConfigureTagResolversListener.kt rename proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/{handler => tablist}/TabListHandler.kt (51%) rename proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/{listener => tablist}/TabListListener.kt (67%) create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyCommandSender.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyEssentialsCommandHandler.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/JoinStateCommandHandler.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/LayoutCommandHandler.kt rename proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/{ => migration}/OldConfigMigrator.kt (98%) rename proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/{handler => controller}/CloudControllerHandler.kt (58%) delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/format/MotdMiniMessageFormatter.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/JoinStateHandler.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/JoinStateResolver.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/PlayerCountHandler.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/ProxyJoinGate.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/CommandSender.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/JoinStateCommandHandler.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/LayoutCommandHandler.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/ProxyEssentialsCommandHandler.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateResolver.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateService.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/ProxyJoinGate.kt rename proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/{handler/DomainMotdHandler.kt => motd/DomainMotdResolver.kt} (50%) rename proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/{handler/MotdLayoutHandler.kt => motd/MotdLayoutRepository.kt} (59%) rename proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/{utilities => motd}/ServerIconLoader.kt (64%) rename proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/{resolver => placeholder}/TagResolverHelper.kt (65%) rename proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/{resolver => placeholder}/TagResolverNames.kt (81%) create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/player/PlayerCountTracker.kt rename proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/{handler => tablist}/TabListResolver.kt (50%) create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/ProxyPermissions.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MinecraftTextWidth.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MotdMiniMessageFormatter.kt delete mode 100644 proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ConfigureTagResolversListener.kt rename proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/{event => placeholder}/ConfigureTagResolversEvent.kt (81%) create mode 100644 proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/placeholder/ConfigureTagResolversListener.kt rename proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/{handler => tablist}/TabListHandler.kt (50%) diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/BungeeCordCommandSender.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/BungeeCordCommandSender.kt index 8fb7eb9..f20db77 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/BungeeCordCommandSender.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/BungeeCordCommandSender.kt @@ -1,25 +1,16 @@ package app.simplecloud.plugin.proxy.bungeecord -import app.simplecloud.plugin.proxy.shared.handler.command.CommandSender +import app.simplecloud.plugin.proxy.shared.command.ProxyCommandSender import net.kyori.adventure.platform.bungeecord.BungeeAudiences -import net.kyori.adventure.text.Component import net.kyori.adventure.text.minimessage.MiniMessage -import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer -import net.md_5.bungee.api.chat.BaseComponent +import net.md_5.bungee.api.CommandSender -class BungeeCordCommandSender(private val commandSender: net.md_5.bungee.api.CommandSender, - private val adventure: BungeeAudiences? -) : CommandSender { - - fun getCommandSender(): net.md_5.bungee.api.CommandSender { - return commandSender - } +class BungeeCordCommandSender( + val commandSender: CommandSender, + private val adventure: BungeeAudiences? +) : ProxyCommandSender { override fun sendMessage(message: String) { adventure?.sender(commandSender)?.sendMessage(MiniMessage.miniMessage().deserialize(message)) } } - -fun Component.toBaseComponent(): BaseComponent { - return BungeeComponentSerializer.get().serialize(this)[0] -} \ No newline at end of file diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ComponentExtension.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ComponentExtension.kt new file mode 100644 index 0000000..c0da8d5 --- /dev/null +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ComponentExtension.kt @@ -0,0 +1,9 @@ +package app.simplecloud.plugin.proxy.bungeecord + +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer +import net.md_5.bungee.api.chat.BaseComponent + +fun Component.toBaseComponent(): BaseComponent { + return BungeeComponentSerializer.get().serialize(this)[0] +} diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ProxyBungeeCordPlugin.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ProxyBungeeCordPlugin.kt index 1f8b8e3..bd48682 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ProxyBungeeCordPlugin.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ProxyBungeeCordPlugin.kt @@ -1,94 +1,101 @@ package app.simplecloud.plugin.proxy.bungeecord -import app.simplecloud.plugin.proxy.bungeecord.event.ConfigureTagResolversEvent -import app.simplecloud.plugin.proxy.bungeecord.handler.TabListHandler -import app.simplecloud.plugin.proxy.bungeecord.listener.* +import app.simplecloud.plugin.api.shared.extension.miniMessage +import app.simplecloud.plugin.proxy.bungeecord.listener.PostLoginListener +import app.simplecloud.plugin.proxy.bungeecord.listener.ServerKickListener +import app.simplecloud.plugin.proxy.bungeecord.listener.ServerPreConnectListener +import app.simplecloud.plugin.proxy.bungeecord.listener.ProxyPingListener +import app.simplecloud.plugin.proxy.bungeecord.placeholder.ConfigureTagResolversEvent +import app.simplecloud.plugin.proxy.bungeecord.placeholder.ConfigureTagResolversListener +import app.simplecloud.plugin.proxy.bungeecord.tablist.TabListHandler +import app.simplecloud.plugin.proxy.bungeecord.tablist.TabListListener import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.format.MotdMiniMessageFormatter -import app.simplecloud.plugin.proxy.shared.handler.command.CommandSender -import app.simplecloud.plugin.proxy.shared.handler.command.JoinStateCommandHandler -import app.simplecloud.plugin.proxy.shared.handler.command.LayoutCommandHandler -import app.simplecloud.plugin.proxy.shared.handler.command.ProxyEssentialsCommandHandler +import app.simplecloud.plugin.proxy.shared.command.ProxyCommandSender +import app.simplecloud.plugin.proxy.shared.command.ProxyEssentialsCommandHandler +import app.simplecloud.plugin.proxy.shared.command.commands.JoinStateCommandHandler +import app.simplecloud.plugin.proxy.shared.command.commands.LayoutCommandHandler +import app.simplecloud.plugin.proxy.shared.utilities.format.MotdMiniMessageFormatter import net.kyori.adventure.platform.bungeecord.BungeeAudiences import net.kyori.adventure.text.Component -import net.kyori.adventure.text.minimessage.MiniMessage +import net.md_5.bungee.api.CommandSender import net.md_5.bungee.api.connection.ProxiedPlayer import net.md_5.bungee.api.plugin.Plugin import org.incendo.cloud.SenderMapper import org.incendo.cloud.bungee.BungeeCommandManager import org.incendo.cloud.execution.ExecutionCoordinator +class ProxyBungeeCordPlugin : Plugin() { -class ProxyBungeeCordPlugin: Plugin() { - - val proxyPlugin = ProxyPlugin(this.dataFolder.path) - + val proxyPlugin = ProxyPlugin(dataFolder.path) val tabListHandler = TabListHandler(this) private var adventure: BungeeAudiences? = null - private val miniMessage = MiniMessage.miniMessage() - override fun onEnable() { - this.proxyPlugin.motdLayoutHandler.loadMotdLayouts() - - this.adventure = BungeeAudiences.create(this) - this.proxy.pluginManager.registerListener(this, ProxyPingListener(this)) - this.proxy.pluginManager.registerListener(this, ConfigureTagResolversListener(this)) - this.proxy.pluginManager.registerListener(this, PostLoginListener(this)) - this.proxy.pluginManager.registerListener(this, ServerPreConnectListener(this)) - this.proxy.pluginManager.registerListener(this, ServerKickListener(this.proxyPlugin)) - this.proxy.pluginManager.registerListener(this, TabListListener(this)) - - if (this.proxyPlugin.proxyEssentialsConfig.get().tabListUpdateTimeMillis() > 0) - this.tabListHandler.startTabListTask() - else - this.logger.info("Tablist update time is set to 0, tablist will not be updated automatically") - - val executionCoordinator = ExecutionCoordinator.simpleCoordinator() - - val senderMapper = SenderMapper.create( - { commandSender -> BungeeCordCommandSender(commandSender, adventure) }, - { cloudSender -> (cloudSender as BungeeCordCommandSender).getCommandSender() } - ) + adventure = BungeeAudiences.create(this) + proxyPlugin.start() - val commandManager = BungeeCommandManager( - this, - executionCoordinator, - senderMapper - ) - - ProxyEssentialsCommandHandler(commandManager, this.proxyPlugin).loadCommands() - JoinStateCommandHandler(commandManager, this.proxyPlugin).loadCommands() - LayoutCommandHandler(commandManager, this.proxyPlugin).loadCommands() + registerListeners() + registerCommands() + startTabListTask() } override fun onDisable() { - this.adventure?.close() - this.adventure = null - this.tabListHandler.stopTabListTask() - this.proxyPlugin.shutdown() + adventure?.close() + adventure = null + tabListHandler.stopTabListTask() + proxyPlugin.shutdown() } fun adventure(): BungeeAudiences { - return adventure ?: throw IllegalStateException("Cannot retrieve audience provider while plugin is not enabled") + return adventure ?: error("Cannot retrieve audience provider while plugin is not enabled") } fun deserializeToComponent(text: String, player: ProxiedPlayer? = null): Component { - val configureTagResolversEvent = this.proxy.pluginManager.callEvent(ConfigureTagResolversEvent(player)) - return this.miniMessage.deserialize( - text, - *configureTagResolversEvent.tagResolvers.toTypedArray() - ) + val tagResolvers = fireConfigureTagResolvers(player) + return miniMessage.deserialize(text, *tagResolvers.toTypedArray()) } fun deserializeMotd(line1: String, line2: String): Component { - val configureTagResolversEvent = this.proxy.pluginManager.callEvent(ConfigureTagResolversEvent(null)) - return MotdMiniMessageFormatter.deserialize( - miniMessage, - line1, - line2, - configureTagResolversEvent.tagResolvers + val tagResolvers = fireConfigureTagResolvers(null) + return MotdMiniMessageFormatter.deserialize(miniMessage, line1, line2, tagResolvers) + } + + private fun registerListeners() { + val manager = proxy.pluginManager + + manager.registerListener(this, ProxyPingListener(this)) + manager.registerListener(this, ConfigureTagResolversListener(this)) + manager.registerListener(this, PostLoginListener(this)) + manager.registerListener(this, ServerPreConnectListener(this)) + manager.registerListener(this, ServerKickListener(proxyPlugin)) + manager.registerListener(this, TabListListener(this)) + } + + private fun registerCommands() { + val commandManager = BungeeCommandManager( + this, + ExecutionCoordinator.asyncCoordinator(), + SenderMapper.create( + { commandSender -> BungeeCordCommandSender(commandSender, adventure) }, + { cloudSender -> (cloudSender as BungeeCordCommandSender).commandSender } + ) ) + + ProxyEssentialsCommandHandler(commandManager, proxyPlugin).loadCommands() + JoinStateCommandHandler(commandManager, proxyPlugin).loadCommands() + LayoutCommandHandler(commandManager, proxyPlugin).loadCommands() } + + private fun startTabListTask() { + if (proxyPlugin.config.get().tabListUpdateTimeMillis() <= 0) { + logger.info("Tablist update time is set to 0, tablist will not be updated automatically") + return + } + + tabListHandler.startTabListTask() + } + + private fun fireConfigureTagResolvers(player: ProxiedPlayer?) = + proxy.pluginManager.callEvent(ConfigureTagResolversEvent(player)).tagResolvers } diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ConfigureTagResolversListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ConfigureTagResolversListener.kt deleted file mode 100644 index 7dee052..0000000 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ConfigureTagResolversListener.kt +++ /dev/null @@ -1,36 +0,0 @@ -package app.simplecloud.plugin.proxy.bungeecord.listener - -import app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin -import app.simplecloud.plugin.proxy.bungeecord.event.ConfigureTagResolversEvent -import app.simplecloud.plugin.proxy.shared.resolver.TagResolverHelper -import net.md_5.bungee.api.plugin.Listener -import net.md_5.bungee.event.EventHandler -import net.md_5.bungee.event.EventPriority - -class ConfigureTagResolversListener( - private val plugin: ProxyBungeeCordPlugin -) : Listener { - - @EventHandler(priority = EventPriority.LOWEST) - fun onConfigureTagResolvers(event: ConfigureTagResolversEvent) { - val player = event.player - val serverName = player?.server?.info?.name ?: "unknown" - val ping = player?.ping ?: -1 - val pingColors = plugin.proxyPlugin.placeHolderConfiguration.get().pingColors - val playerCountHandler = plugin.proxyPlugin.playerCountHandler - val onlinePlayers = playerCountHandler.onlinePlayers(plugin.proxy.players.size) - val realMaxPlayers = playerCountHandler.maxPlayers(plugin.proxy.config.playerLimit) - - event.withTagResolvers( - TagResolverHelper.getDefaultTagResolvers( - serverName, - ping.toLong(), - pingColors, - onlinePlayers, - realMaxPlayers, - plugin.proxyPlugin.motdLayoutHandler.getCurrentMotdLayout() - ) - ) - } - -} diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/PostLoginListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/PostLoginListener.kt index 0a963bd..58b32be 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/PostLoginListener.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/PostLoginListener.kt @@ -2,7 +2,7 @@ package app.simplecloud.plugin.proxy.bungeecord.listener import app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin import app.simplecloud.plugin.proxy.bungeecord.toBaseComponent -import app.simplecloud.plugin.proxy.shared.handler.ProxyJoinGate +import app.simplecloud.plugin.proxy.shared.joinstate.ProxyJoinGate import kotlinx.coroutines.runBlocking import net.md_5.bungee.api.event.PostLoginEvent import net.md_5.bungee.api.plugin.Listener @@ -10,7 +10,7 @@ import net.md_5.bungee.event.EventHandler import net.md_5.bungee.event.EventPriority class PostLoginListener( - private val plugin: ProxyBungeeCordPlugin, + private val plugin: ProxyBungeeCordPlugin ) : Listener { @EventHandler(priority = EventPriority.LOWEST) diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ProxyPingListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ProxyPingListener.kt index f877c53..c72dab0 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ProxyPingListener.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ProxyPingListener.kt @@ -1,81 +1,87 @@ package app.simplecloud.plugin.proxy.bungeecord.listener import app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin -import app.simplecloud.plugin.proxy.shared.config.MaxPlayerDisplayType +import app.simplecloud.plugin.proxy.bungeecord.toBaseComponent +import app.simplecloud.plugin.proxy.shared.config.MotdLayoutConfiguration +import app.simplecloud.plugin.proxy.shared.motd.ServerIconLoader import app.simplecloud.plugin.proxy.shared.utilities.LocalPingSourceMatcher -import app.simplecloud.plugin.proxy.shared.utilities.ServerIconLoader import net.md_5.bungee.api.Favicon -import net.md_5.bungee.api.ServerPing.* +import net.md_5.bungee.api.ServerPing import net.md_5.bungee.api.event.ProxyPingEvent import net.md_5.bungee.api.plugin.Listener import net.md_5.bungee.event.EventHandler import java.net.InetSocketAddress import java.nio.file.Path -import java.util.* +import java.util.UUID class ProxyPingListener( private val plugin: ProxyBungeeCordPlugin ) : Listener { - private val serverIconLoader = ServerIconLoader( - Path.of(plugin.proxyPlugin.serverIconsPath) - ) { image -> Favicon.create(image) } - private val localPingSourceMatcher = LocalPingSourceMatcher() + private val proxyPlugin = plugin.proxyPlugin + + private val serverIconLoader = ServerIconLoader(Path.of(proxyPlugin.serverIconsPath)) { image -> + Favicon.create(image) + } @EventHandler fun onPing(event: ProxyPingEvent) { - val virtualHost = event.connection.virtualHost?.hostName - val layout = virtualHost - ?.let { plugin.proxyPlugin.domainMotdHandler.getLayoutNameForDomain(it) } - ?.let { plugin.proxyPlugin.motdLayoutHandler.getLayoutByName(it) } - ?: plugin.proxyPlugin.motdLayoutHandler.getCurrentMotdLayout() - + val layout = resolveLayout(event.connection.virtualHost?.hostName) if (!layout.motd.enabled) return - val entry = plugin.proxyPlugin.motdLayoutHandler.selectEntry(layout, layout.configVersion) - ?: return - + val entry = proxyPlugin.layoutRepository.selectEntry(layout, layout.configVersion) ?: return val response = event.response - val motd = plugin.deserializeMotd(entry.line1, entry.line2) - response.descriptionComponent = net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer.get().serialize(motd)[0] + response.descriptionComponent = plugin.deserializeMotd(entry.line1, entry.line2).toBaseComponent() + + applyServerIcon(response, layout) + // The cloud pings the proxy itself to check its health, which must not affect the counts. val socketAddress = event.connection.socketAddress as? InetSocketAddress - val isLocalPing = localPingSourceMatcher.isLocal(socketAddress?.address) + if (!LocalPingSourceMatcher.isLocal(socketAddress?.address)) { + applyPlayerList(response, layout) + } + } - // server icon - if (layout.serverIcon.enabled) { - serverIconLoader.get(layout.serverIcon.file) { plugin.logger.warning(it) } - ?.let { response.setFavicon(it) } + private fun resolveLayout(virtualHost: String?): MotdLayoutConfiguration { + val layoutName = when (virtualHost) { + null -> null + else -> proxyPlugin.domainMotdHandler.getLayoutNameForDomain(virtualHost) + } + val domainLayout = when (layoutName) { + null -> null + else -> proxyPlugin.layoutRepository.getLayoutByName(layoutName) } - if (!isLocalPing) { - // player list (hover text) - val samplePlayers = if (layout.playerList.enabled && layout.playerList.playerList.isNotEmpty()) { - layout.playerList.playerList.map { PlayerInfo(it, UUID.randomUUID()) }.toTypedArray() - } else { - response.players.sample - } - - // slots - val playerCountHandler = plugin.proxyPlugin.playerCountHandler - val onlinePlayers = playerCountHandler.onlinePlayers(plugin.proxy.players.size) - val realMax = playerCountHandler.maxPlayers(plugin.proxy.config.playerLimit) - val maxPlayers = if (layout.version.slots.enabled) { - when (layout.version.slots.type) { - MaxPlayerDisplayType.REAL -> realMax - MaxPlayerDisplayType.FAKE -> layout.version.slots.fakeSlots - MaxPlayerDisplayType.DYNAMIC -> onlinePlayers + layout.version.slots.dynamicPlayerRange - } - } else { - realMax - } - - response.players = Players(maxPlayers, onlinePlayers, samplePlayers) - - // version name - if (layout.version.name.enabled) { - response.version = Protocol(layout.version.name.text, response.version.protocol) - } + return domainLayout ?: proxyPlugin.layoutRepository.getCurrentMotdLayout() + } + + private fun applyServerIcon(response: ServerPing, layout: MotdLayoutConfiguration) { + if (!layout.serverIcon.enabled) return + + val favicon = serverIconLoader.get(layout.serverIcon.file) { plugin.logger.warning(it) } ?: return + response.setFavicon(favicon) + } + + private fun applyPlayerList(response: ServerPing, layout: MotdLayoutConfiguration) { + val playerCountHandler = proxyPlugin.playerCountTracker + val onlinePlayers = playerCountHandler.onlinePlayers(plugin.proxy.players.size) + val realMaxPlayers = playerCountHandler.maxPlayers(plugin.proxy.config.playerLimit) + val maxPlayers = layout.version.slots.resolveMaxPlayers(onlinePlayers, realMaxPlayers) + + response.players = ServerPing.Players(maxPlayers, onlinePlayers, samplePlayers(layout, response)) + + if (layout.version.name.enabled) { + response.version = ServerPing.Protocol(layout.version.name.text, response.version.protocol) } } -} + + private fun samplePlayers(layout: MotdLayoutConfiguration, response: ServerPing): Array { + if (!layout.playerList.enabled || layout.playerList.playerList.isEmpty()) { + return response.players.sample + } + + return layout.playerList.playerList + .map { ServerPing.PlayerInfo(it, UUID.randomUUID()) } + .toTypedArray() + } +} \ No newline at end of file diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerKickListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerKickListener.kt index 2f16da7..425ecab 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerKickListener.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerKickListener.kt @@ -5,17 +5,14 @@ import net.md_5.bungee.api.event.ServerKickEvent import net.md_5.bungee.api.plugin.Listener import net.md_5.bungee.event.EventHandler -class ServerKickListener internal constructor( - private val showKickReason: () -> Boolean +class ServerKickListener( + private val proxyPlugin: ProxyPlugin ) : Listener { - constructor(proxyPlugin: ProxyPlugin) : this({ - proxyPlugin.proxyEssentialsConfig.get().showKickReason - }) - - @EventHandler(priority = Byte.MAX_VALUE) + @EventHandler(priority = Byte.MAX_VALUE) fun handle(event: ServerKickEvent) { - if (!showKickReason() || event.cancelServer != null) return + if (!proxyPlugin.config.get().showKickReason) return + if (event.cancelServer != null) return val serverReason = event.reason ?: return event.player.disconnect(serverReason) diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerPreConnectListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerPreConnectListener.kt index 393f851..cd72fd5 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerPreConnectListener.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerPreConnectListener.kt @@ -2,9 +2,8 @@ package app.simplecloud.plugin.proxy.bungeecord.listener import app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin import app.simplecloud.plugin.proxy.bungeecord.toBaseComponent -import app.simplecloud.plugin.proxy.shared.handler.ProxyJoinGate +import app.simplecloud.plugin.proxy.shared.joinstate.ProxyJoinGate import kotlinx.coroutines.runBlocking -import net.md_5.bungee.api.config.ServerInfo import net.md_5.bungee.api.connection.ProxiedPlayer import net.md_5.bungee.api.event.ServerConnectEvent import net.md_5.bungee.api.event.ServerConnectEvent.Reason @@ -14,63 +13,70 @@ import net.md_5.bungee.event.EventPriority import java.util.logging.Logger class ServerPreConnectListener( - private val plugin: ProxyBungeeCordPlugin, + private val plugin: ProxyBungeeCordPlugin ) : Listener { + private val logger = Logger.getLogger(ServerPreConnectListener::class.java.name) - private val resolver = plugin.proxyPlugin.joinStateResolver + private val proxyPlugin = plugin.proxyPlugin @EventHandler(priority = EventPriority.HIGH) fun handle(event: ServerConnectEvent) { if (isProxyJoin(event)) { - checkAllowProxyJoin(event.player, event) + checkAllowProxyJoin(event) if (event.isCancelled) { return } } - checkAllowServerSwitch(event.player, event, event.target) + checkAllowServerSwitch(event) } private fun isProxyJoin(event: ServerConnectEvent): Boolean { return event.reason == Reason.JOIN_PROXY || event.player.server == null } - private fun checkAllowProxyJoin(player: ProxiedPlayer, event: ServerConnectEvent) { + private fun checkAllowProxyJoin(event: ServerConnectEvent) { + val player = event.player val result = runBlocking { - plugin.proxyPlugin.proxyJoinGate.evaluate(player.name) { permission -> player.hasPermission(permission) } + proxyPlugin.proxyJoinGate.evaluate(player.name) { permission -> player.hasPermission(permission) } } if (result is ProxyJoinGate.Result.Denied) { - denyAccess(player, result.kickMessage, false, event) + disconnect(player, result.kickMessage, event) } } - private fun checkAllowServerSwitch(player: ProxiedPlayer, event: ServerConnectEvent, server: ServerInfo) { - val serverName = server.name + private fun checkAllowServerSwitch(event: ServerConnectEvent) { + val player = event.player + val serverName = event.target.name + val resolver = proxyPlugin.joinStateResolver runBlocking { val joinStateName = resolver.getJoinStateForServer(serverName) val joinState = resolver.resolveJoinState(joinStateName) + val kickMessages = proxyPlugin.messageConfig.get().kick if (joinState == null) { logger.severe("Neither join state '$joinStateName' nor default state found. Check configuration!") - denyAccess(player, plugin.proxyPlugin.messagesConfiguration.get().kick.noJoinState, true, event) + denyServerSwitch(player, kickMessages.noJoinState, event) return@runBlocking } - if (joinState.permission.join.isNotBlank() && !player.hasPermission(joinState.permission.join)) { + val joinPermission = joinState.permission.join + if (joinPermission.isNotBlank() && !player.hasPermission(joinPermission)) { logger.info("Player ${player.name} does not have permission to join $serverName.") - denyAccess(player, plugin.proxyPlugin.messagesConfiguration.get().kick.noPermission, true, event) + denyServerSwitch(player, kickMessages.noPermission, event) } } } - private fun denyAccess(player: ProxiedPlayer, message: String, isSubServer: Boolean, event: ServerConnectEvent) { - if (isSubServer) { - player.sendMessage(plugin.deserializeToComponent(message, player).toBaseComponent()) - } else { - player.disconnect(plugin.deserializeToComponent(message, player).toBaseComponent()) - } + private fun denyServerSwitch(player: ProxiedPlayer, message: String, event: ServerConnectEvent) { + player.sendMessage(plugin.deserializeToComponent(message, player).toBaseComponent()) + event.isCancelled = true + } + + private fun disconnect(player: ProxiedPlayer, message: String, event: ServerConnectEvent) { + player.disconnect(plugin.deserializeToComponent(message, player).toBaseComponent()) event.isCancelled = true } } diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/event/ConfigureTagResolversEvent.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/placeholder/ConfigureTagResolversEvent.kt similarity index 70% rename from proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/event/ConfigureTagResolversEvent.kt rename to proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/placeholder/ConfigureTagResolversEvent.kt index ddd1d22..49d2d0d 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/event/ConfigureTagResolversEvent.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/placeholder/ConfigureTagResolversEvent.kt @@ -1,21 +1,23 @@ -package app.simplecloud.plugin.proxy.bungeecord.event +package app.simplecloud.plugin.proxy.bungeecord.placeholder import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver import net.md_5.bungee.api.connection.ProxiedPlayer import net.md_5.bungee.api.plugin.Event +/** + * Fired before a message is deserialized so other plugins can contribute their own MiniMessage tags. + */ class ConfigureTagResolversEvent( val player: ProxiedPlayer? = null, val tagResolvers: MutableList = mutableListOf() -): Event() { +) : Event() { fun withTagResolvers(vararg tagResolvers: TagResolver): ConfigureTagResolversEvent { - return this.withTagResolvers(tagResolvers.toList()) + return withTagResolvers(tagResolvers.toList()) } fun withTagResolvers(tagResolvers: List): ConfigureTagResolversEvent { this.tagResolvers.addAll(tagResolvers) return this } - } diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/placeholder/ConfigureTagResolversListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/placeholder/ConfigureTagResolversListener.kt new file mode 100644 index 0000000..ff8f20d --- /dev/null +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/placeholder/ConfigureTagResolversListener.kt @@ -0,0 +1,30 @@ +package app.simplecloud.plugin.proxy.bungeecord.placeholder + +import app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin +import app.simplecloud.plugin.proxy.shared.placeholder.TagResolverHelper +import net.md_5.bungee.api.plugin.Listener +import net.md_5.bungee.event.EventHandler +import net.md_5.bungee.event.EventPriority + +class ConfigureTagResolversListener( + private val plugin: ProxyBungeeCordPlugin +) : Listener { + + @EventHandler(priority = EventPriority.LOWEST) + fun onConfigureTagResolvers(event: ConfigureTagResolversEvent) { + val proxyPlugin = plugin.proxyPlugin + val player = event.player + val playerCountHandler = proxyPlugin.playerCountTracker + + event.withTagResolvers( + TagResolverHelper.getDefaultTagResolvers( + serverName = player?.server?.info?.name ?: "unknown", + ping = (player?.ping ?: -1).toLong(), + pingColors = proxyPlugin.placeholderConfig.get().pingColors, + onlinePlayers = playerCountHandler.onlinePlayers(plugin.proxy.players.size), + realMaxPlayers = playerCountHandler.maxPlayers(plugin.proxy.config.playerLimit), + motdConfiguration = proxyPlugin.layoutRepository.getCurrentMotdLayout() + ) + ) + } +} diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/handler/TabListHandler.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/tablist/TabListHandler.kt similarity index 51% rename from proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/handler/TabListHandler.kt rename to proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/tablist/TabListHandler.kt index dcced82..b913c8a 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/handler/TabListHandler.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/tablist/TabListHandler.kt @@ -1,4 +1,4 @@ -package app.simplecloud.plugin.proxy.bungeecord.handler +package app.simplecloud.plugin.proxy.bungeecord.tablist import app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin import net.md_5.bungee.api.connection.ProxiedPlayer @@ -9,30 +9,37 @@ import java.util.logging.Logger class TabListHandler( private val plugin: ProxyBungeeCordPlugin ) { + private val logger = Logger.getLogger(TabListHandler::class.java.name) - private val resolver = plugin.proxyPlugin.tabListResolver - private lateinit var task: ScheduledTask + private var task: ScheduledTask? = null fun startTabListTask() { - task = plugin.proxy.scheduler.schedule(plugin, { - resolver.incrementIndices() - plugin.proxy.players.forEach { updateTabListForPlayer(it) } - }, 1, plugin.proxyPlugin.proxyEssentialsConfig.get().tabListUpdateTimeMillis(), TimeUnit.MILLISECONDS) + val updateTimeMillis = plugin.proxyPlugin.config.get().tabListUpdateTimeMillis() + + task = plugin.proxy.scheduler.schedule( + plugin, + { updateTabLists() }, + 1, + updateTimeMillis, + TimeUnit.MILLISECONDS + ) } fun stopTabListTask() { - if (!this::task.isInitialized) { + val runningTask = task + if (runningTask == null) { logger.warning("Can't stop tablist task because it is not initialized") return } - task.cancel() + + runningTask.cancel() + task = null } fun updateTabListForPlayer(player: ProxiedPlayer) { - if (player.server == null || player.server.info == null) return - - val serviceName = player.server.info.name - val tabListGroup = resolver.findTabListGroup(serviceName) ?: return + val serverInfo = player.server?.info ?: return + val resolver = plugin.proxyPlugin.tabListResolver + val tabListGroup = resolver.findTabListGroup(serverInfo.name) ?: return val (header, footer) = resolver.getCurrentTabList(tabListGroup) ?: return plugin.adventure().player(player).sendPlayerListHeaderAndFooter( @@ -40,4 +47,9 @@ class TabListHandler( plugin.deserializeToComponent(footer, player), ) } -} \ No newline at end of file + + private fun updateTabLists() { + plugin.proxyPlugin.tabListResolver.incrementIndices() + plugin.proxy.players.forEach { updateTabListForPlayer(it) } + } +} diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/TabListListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/tablist/TabListListener.kt similarity index 67% rename from proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/TabListListener.kt rename to proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/tablist/TabListListener.kt index fc0f484..951846b 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/TabListListener.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/tablist/TabListListener.kt @@ -1,4 +1,4 @@ -package app.simplecloud.plugin.proxy.bungeecord.listener +package app.simplecloud.plugin.proxy.bungeecord.tablist import app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin import net.md_5.bungee.api.event.ServerConnectedEvent @@ -7,12 +7,10 @@ import net.md_5.bungee.event.EventHandler class TabListListener( private val plugin: ProxyBungeeCordPlugin -): Listener { +) : Listener { @EventHandler fun onServerConnected(event: ServerConnectedEvent) { - val player = event.player - this.plugin.tabListHandler.updateTabListForPlayer(player) + plugin.tabListHandler.updateTabListForPlayer(event.player) } - -} \ No newline at end of file +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt index b7134bc..6757df0 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt @@ -5,73 +5,71 @@ import app.simplecloud.api.CloudApiOptions import app.simplecloud.plugin.api.shared.config.ConfigurationFactory import app.simplecloud.plugin.proxy.shared.config.DefaultConfigInstaller import app.simplecloud.plugin.proxy.shared.config.MessageConfig -import app.simplecloud.plugin.proxy.shared.config.OldConfigMigrator import app.simplecloud.plugin.proxy.shared.config.PlaceHolderConfiguration import app.simplecloud.plugin.proxy.shared.config.ProxyEssentialsConfig -import app.simplecloud.plugin.proxy.shared.handler.CloudControllerHandler -import app.simplecloud.plugin.proxy.shared.handler.DomainMotdHandler -import app.simplecloud.plugin.proxy.shared.handler.JoinStateHandler -import app.simplecloud.plugin.proxy.shared.handler.JoinStateResolver -import app.simplecloud.plugin.proxy.shared.handler.MotdLayoutHandler -import app.simplecloud.plugin.proxy.shared.handler.PlayerCountHandler -import app.simplecloud.plugin.proxy.shared.handler.ProxyJoinGate -import app.simplecloud.plugin.proxy.shared.handler.TabListResolver +import app.simplecloud.plugin.proxy.shared.config.migration.OldConfigMigrator +import app.simplecloud.plugin.proxy.shared.controller.CloudControllerHandler +import app.simplecloud.plugin.proxy.shared.joinstate.JoinStateResolver +import app.simplecloud.plugin.proxy.shared.joinstate.JoinStateService +import app.simplecloud.plugin.proxy.shared.joinstate.ProxyJoinGate +import app.simplecloud.plugin.proxy.shared.motd.DomainMotdResolver +import app.simplecloud.plugin.proxy.shared.motd.MotdLayoutRepository +import app.simplecloud.plugin.proxy.shared.player.PlayerCountTracker +import app.simplecloud.plugin.proxy.shared.tablist.TabListResolver import java.io.File class ProxyPlugin( dirPath: String ) { - val api = CloudApi.create(CloudApiOptions.builder().component("proxy-essentials").build()) + + val api: CloudApi = CloudApi.create(CloudApiOptions.builder().component("proxy-essentials").build()) private val dataDirectory = File(dirPath) + val config = ConfigurationFactory(File(dataDirectory, "config.yml"), ProxyEssentialsConfig::class.java) + val messageConfig = ConfigurationFactory(File(dataDirectory, "messages.yml"), MessageConfig::class.java) + val placeholderConfig = ConfigurationFactory(File(dataDirectory, "placeholder.yml"), PlaceHolderConfiguration::class.java) + init { dataDirectory.mkdirs() OldConfigMigrator.migrate(dataDirectory.toPath()) DefaultConfigInstaller.install(dataDirectory.toPath(), javaClass.classLoader) - } - - val proxyEssentialsConfig = ConfigurationFactory(File(dataDirectory, "config.yml"), ProxyEssentialsConfig::class.java) - val messagesConfiguration = ConfigurationFactory(File(dataDirectory, "messages.yml"), MessageConfig::class.java) - val placeHolderConfiguration = ConfigurationFactory(File(dataDirectory, "placeholder.yml"), PlaceHolderConfiguration::class.java) - - init { loadConfigurations() } val serverIconsPath = "$dirPath/layout/server-icons" - val motdLayoutHandler = MotdLayoutHandler(File("$dirPath/layout").toPath(), this) - val joinStateHandler = JoinStateHandler(this) - val cloudControllerHandler = CloudControllerHandler(this, joinStateHandler) - val playerCountHandler = PlayerCountHandler(this).also { it.start() } + val layoutRepository = MotdLayoutRepository(File("$dirPath/layout").toPath(), this) + val joinStateService = JoinStateService(this) + val cloudControllerHandler = CloudControllerHandler(this, joinStateService) + val playerCountTracker = PlayerCountTracker(this) val joinStateResolver = JoinStateResolver(this) - val proxyJoinGate = ProxyJoinGate( - localState = { joinStateHandler.localState }, - resolveJoinState = joinStateResolver::resolveJoinState, - isServerFull = joinStateResolver::isServerFull, - kickMessages = { messagesConfiguration.get().kick } - ) - val tabListResolver = TabListResolver { proxyEssentialsConfig.get().tablist } - val domainMotdHandler = DomainMotdHandler(this).also { - it.registerListener() - it.startSyncTask() - } + val proxyJoinGate = ProxyJoinGate(this) + val tabListResolver = TabListResolver(this) + val domainMotdHandler = DomainMotdResolver(this) - fun reload() { - loadConfigurations() - motdLayoutHandler.loadMotdLayouts() + fun start() { + layoutRepository.loadMotdLayouts() + cloudControllerHandler.start() + playerCountTracker.start() + domainMotdHandler.registerListener() + domainMotdHandler.startSyncTask() } fun shutdown() { - playerCountHandler.stop() - joinStateHandler.stop() + playerCountTracker.stop() + joinStateService.stop() cloudControllerHandler.close() domainMotdHandler.stop() } + fun reload() { + loadConfigurations() + layoutRepository.loadMotdLayouts() + } + private fun loadConfigurations() { - proxyEssentialsConfig.loadOrCreate(ProxyEssentialsConfig()) - messagesConfiguration.loadOrCreate(MessageConfig()) - placeHolderConfiguration.loadOrCreate(PlaceHolderConfiguration()) + config.loadOrCreate(ProxyEssentialsConfig()) + messageConfig.loadOrCreate(MessageConfig()) + placeholderConfig.loadOrCreate(PlaceHolderConfiguration()) } } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyCommandSender.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyCommandSender.kt new file mode 100644 index 0000000..999e384 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyCommandSender.kt @@ -0,0 +1,7 @@ +package app.simplecloud.plugin.proxy.shared.command + +interface ProxyCommandSender { + + fun sendMessage(message: String) + +} \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyEssentialsCommandHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyEssentialsCommandHandler.kt new file mode 100644 index 0000000..31c93b3 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyEssentialsCommandHandler.kt @@ -0,0 +1,88 @@ +package app.simplecloud.plugin.proxy.shared.command + +import app.simplecloud.plugin.proxy.shared.ProxyPlugin +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.incendo.cloud.CommandManager +import org.incendo.cloud.context.CommandContext +import java.util.logging.Level +import java.util.logging.Logger + +class ProxyEssentialsCommandHandler( + private val manager: CommandManager, + private val plugin: ProxyPlugin +) { + + private val logger = Logger.getLogger(ProxyEssentialsCommandHandler::class.java.name) + private val commands = listOf( + "/scproxy help", + "/scproxy reload", + "/scproxy joinstate help", + "/scproxy joinstate info ", + "/scproxy joinstate info ", + "/scproxy joinstate set ", + "/scproxy joinstate set ", + "/scproxy layout help", + "/scproxy layout info [group]", + "/scproxy layout set " + ) + + fun loadCommands() { + loadHelp() + loadReload() + } + + private fun loadHelp() { + manager.command( + manager.commandBuilder("scproxy") + .permission("simplecloud.proxy-essentials.command.help") + .handler { context: CommandContext -> sendHelp(context.sender()) } + .build() + ) + manager.command( + manager.commandBuilder("scproxy") + .literal("help") + .permission("simplecloud.proxy-essentials.command.help") + .handler { context: CommandContext -> sendHelp(context.sender()) } + .build() + ) + } + + private fun sendHelp(sender: C) { + val messages = plugin.messageConfig.get() + val entry = messages.command.joinState.help.entry + + sender.sendMessage(messages.resolve("Available /scproxy commands:")) + commands.forEach { command -> + sender.sendMessage(entry.replace("", command)) + } + } + + private fun loadReload() { + manager.command( + manager.commandBuilder("scproxy") + .literal("reload") + .permission("simplecloud.proxy-essentials.command.reload") + .handler { context: CommandContext -> reload(context.sender()) } + .build() + ) + } + + private fun reload(sender: C) { + val messagesBeforeReload = plugin.messageConfig.get() + sender.sendMessage(messagesBeforeReload.resolve(messagesBeforeReload.command.reload.start)) + + CoroutineScope(Dispatchers.IO).launch { + try { + plugin.reload() + val messages = plugin.messageConfig.get() + sender.sendMessage(messages.resolve(messages.command.reload.success)) + } catch (e: Exception) { + val messages = plugin.messageConfig.get() + sender.sendMessage(messages.resolve(messages.command.reload.failure).replace("", e.message ?: "Unknown error")) + logger.log(Level.SEVERE, "Error reloading configuration", e) + } + } + } +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/JoinStateCommandHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/JoinStateCommandHandler.kt new file mode 100644 index 0000000..947fa70 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/JoinStateCommandHandler.kt @@ -0,0 +1,275 @@ +package app.simplecloud.plugin.proxy.shared.command.commands + +import app.simplecloud.plugin.proxy.shared.ProxyPlugin +import app.simplecloud.plugin.proxy.shared.command.ProxyCommandSender +import app.simplecloud.plugin.proxy.shared.config.MessageConfig +import app.simplecloud.plugin.proxy.shared.utilities.ProxyPermissions +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.incendo.cloud.CommandManager +import org.incendo.cloud.context.CommandContext +import org.incendo.cloud.parser.standard.IntegerParser +import org.incendo.cloud.parser.standard.StringParser +import org.incendo.cloud.suggestion.Suggestion +import java.util.concurrent.CompletableFuture + +class JoinStateCommandHandler( + private val manager: CommandManager, + private val plugin: ProxyPlugin +) { + + private val index = 3 + private val commands = listOf( + "/scproxy joinstate help", + "/scproxy joinstate info ", + "/scproxy joinstate info ", + "/scproxy joinstate set ", + "/scproxy joinstate set ", + " can also be the name of a persistent server" + ) + + fun loadCommands() { + loadHelp() + loadInfo() + loadSet() + } + + private fun loadHelp() { + val builder = manager.commandBuilder("scproxy").literal("joinstate") + + manager.command( + builder.permission(ProxyPermissions.JOINSTATE_HELP) + .handler { context: CommandContext -> handleHelp(context.sender()) } + .build() + ) + manager.command( + builder.literal("help") + .permission(ProxyPermissions.JOINSTATE_HELP) + .handler { context: CommandContext -> handleHelp(context.sender()) } + .build() + ) + } + + private fun handleHelp(sender: ProxyCommandSender) { + val messages = plugin.messageConfig.get() + val entry = messages.command.joinState.help.entry + + sender.sendMessage(messages.resolve(messages.command.joinState.help.header)) + commands.forEach { command -> + sender.sendMessage(entry.replace("", command)) + } + } + + private fun loadInfo() { + manager.command( + manager.commandBuilder("scproxy") + .literal("joinstate") + .literal("info") + .required("group", StringParser.stringParser()) { _, _ -> suggestGroups() } + .optional("id", IntegerParser.integerParser(1)) { context, _ -> suggestNumericalIds(context) } + .permission(ProxyPermissions.JOINSTATE_INFO) + .handler { context: CommandContext -> + CoroutineScope(Dispatchers.IO).launch { handleInfo(context) } + } + .build() + ) + } + + private suspend fun handleInfo(context: CommandContext) { + val group = context.get("group") + val id = context.optional("id") + val messages = plugin.messageConfig.get() + + val name = when { + id.isPresent -> "$group-${id.get()}" + else -> group + } + val state = when { + id.isPresent -> plugin.joinStateService.getJoinStateAtService(group, id.get()) + else -> joinStateOf(group) + } + + context.sender().sendMessage( + messages.resolve(messages.command.joinState.list.groups.entry) + .replace("", name) + .replace("", state) + ) + } + + private fun loadSet() { + manager.command( + manager.commandBuilder("scproxy") + .literal("joinstate") + .literal("set") + .required("group", StringParser.stringParser()) { _, _ -> suggestGroups() } + .required("idOrJoinstate", StringParser.stringParser()) { context, _ -> + suggestIdsAndJoinStates(context) + } + .optional("joinstate", StringParser.stringParser()) { _, _ -> suggestJoinStates() } + .permission(ProxyPermissions.JOINSTATE_SET) + .handler { context: CommandContext -> + CoroutineScope(Dispatchers.IO).launch { handleSet(context) } + } + .build() + ) + } + + private suspend fun handleSet(context: CommandContext) { + val group = context.get("group") + val idOrJoinstate = context.get("idOrJoinstate") + val joinstate = context.optional("joinstate") + val messages = plugin.messageConfig.get() + + if (!joinstate.isPresent) { + handleSetGroup(context.sender(), group, idOrJoinstate, messages) + return + } + + val numericalId = idOrJoinstate.toIntOrNull() + if (numericalId == null) { + context.sender().sendMessage(messages.resolve(messages.command.joinState.server.updateFailure)) + return + } + + handleSetService(context.sender(), group, numericalId, joinstate.get(), messages) + } + + private suspend fun handleSetGroup( + sender: ProxyCommandSender, + group: String, + state: String, + messages: MessageConfig + ) { + if (plugin.joinStateResolver.resolveJoinState(state) == null) { + sender.sendMessage(messages.resolve(messages.command.joinState.group.updateFailure)) + return + } + + if (!plugin.cloudControllerHandler.groupExists(group)) { + handleSetPersistentServer(sender, group, state, messages) + return + } + + if (plugin.joinStateService.getJoinStateAtGroup(group) == state) { + sender.sendMessage(messages.resolve(messages.command.joinState.group.updateNoChange)) + return + } + + val updated = plugin.joinStateService.setJoinStateAtGroupAndAllServicesInGroup(group, state) + sender.sendMessage(messages.resolve(updateResult(messages, updated, isGroup = true))) + } + + private suspend fun handleSetPersistentServer( + sender: ProxyCommandSender, + serverName: String, + state: String, + messages: MessageConfig + ) { + if (plugin.joinStateService.getJoinStateAtPersistentServer(serverName) == state) { + sender.sendMessage(messages.resolve(messages.command.joinState.server.updateNoChange)) + return + } + + val updated = plugin.joinStateService.setJoinStateAtPersistentServer(serverName, state) + sender.sendMessage(messages.resolve(updateResult(messages, updated, isGroup = false))) + } + + private suspend fun handleSetService( + sender: ProxyCommandSender, + group: String, + numericalId: Int, + state: String, + messages: MessageConfig + ) { + if (plugin.joinStateResolver.resolveJoinState(state) == null) { + sender.sendMessage(messages.resolve(messages.command.joinState.server.updateFailure)) + return + } + + if (plugin.joinStateService.getJoinStateAtService(group, numericalId) == state) { + sender.sendMessage(messages.resolve(messages.command.joinState.server.updateNoChange)) + return + } + + val updated = plugin.joinStateService.setJoinStateAtService(group, numericalId, state) + if (updated && isCurrentService(group, numericalId)) { + plugin.joinStateService.localState = state + } + + sender.sendMessage(messages.resolve(updateResult(messages, updated, isGroup = false))) + } + + private fun isCurrentService(group: String, numericalId: Int): Boolean { + val currentServer = plugin.cloudControllerHandler.currentServer ?: return false + if (!currentServer.isFromGroup) return false + + return currentServer.group?.name == group && currentServer.numericalId == numericalId + } + + private fun updateResult(messages: MessageConfig, updated: Boolean, isGroup: Boolean): String { + val updateMessages = when { + isGroup -> messages.command.joinState.group + else -> messages.command.joinState.server + } + + return when { + updated -> updateMessages.updateSuccess + else -> updateMessages.updateFailure + } + } + + private suspend fun joinStateOf(target: String): String { + return when { + plugin.cloudControllerHandler.groupExists(target) -> + plugin.joinStateService.getJoinStateAtGroup(target) + + else -> plugin.joinStateService.getJoinStateAtPersistentServer(target) + } + } + + private fun suggestGroups(): CompletableFuture> = runBlocking { + val targets = plugin.cloudControllerHandler.getAllGroups() + + plugin.cloudControllerHandler.getAllPersistentServerNames() + + CompletableFuture.completedFuture(targets.map { Suggestion.suggestion(it) }) + } + + private fun suggestJoinStates(): CompletableFuture> { + val states = plugin.config.get().joinstates + .map { Suggestion.suggestion(it.name) } + + return CompletableFuture.completedFuture(states) + } + + private fun suggestNumericalIds(context: CommandContext): CompletableFuture> = runBlocking { + val group = groupToken(context) + ?: return@runBlocking CompletableFuture.completedFuture(emptyList()) + + val ids = plugin.cloudControllerHandler.getAllNumericalIdsFromGroup(group) + .map { Suggestion.suggestion(it.toString()) } + + CompletableFuture.completedFuture(ids) + } + + private fun suggestIdsAndJoinStates(context: CommandContext): CompletableFuture> { + val stateSuggestions = plugin.config.get().joinstates + .map { Suggestion.suggestion(it.name) } + val group = groupToken(context) ?: return CompletableFuture.completedFuture(stateSuggestions) + + return runBlocking { + val idSuggestions = plugin.cloudControllerHandler.getAllNumericalIdsFromGroup(group) + .map { Suggestion.suggestion(it.toString()) } + + CompletableFuture.completedFuture(idSuggestions + stateSuggestions) + } + } + + private fun groupToken(context: CommandContext): String? { + return context.rawInput().input() + .split(" ") + .filter { it.isNotBlank() } + .getOrNull(index) + } +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/LayoutCommandHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/LayoutCommandHandler.kt new file mode 100644 index 0000000..1ed702c --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/LayoutCommandHandler.kt @@ -0,0 +1,183 @@ +package app.simplecloud.plugin.proxy.shared.command.commands + +import app.simplecloud.plugin.proxy.shared.ProxyPlugin +import app.simplecloud.plugin.proxy.shared.command.ProxyCommandSender +import app.simplecloud.plugin.proxy.shared.config.MessageConfig +import app.simplecloud.plugin.proxy.shared.motd.MotdLayoutRepository +import app.simplecloud.plugin.proxy.shared.utilities.ProxyPermissions +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.incendo.cloud.CommandManager +import org.incendo.cloud.context.CommandContext +import org.incendo.cloud.parser.standard.StringParser +import org.incendo.cloud.suggestion.Suggestion +import java.util.concurrent.CompletableFuture + + +class LayoutCommandHandler( + private val manager: CommandManager, + private val plugin: ProxyPlugin +) { + private val commands = listOf( + "/scproxy layout help", + "/scproxy layout info [group]", + "/scproxy layout set " + ) + + fun loadCommands() { + loadHelp() + loadInfoDefault() + loadInfoGroup() + loadSet() + } + + private fun loadHelp() { + manager.command( + manager.commandBuilder("scproxy") + .literal("layout") + .permission(ProxyPermissions.LAYOUT_HELP) + .handler { context: CommandContext -> handleHelp(context.sender()) } + .build() + ) + manager.command( + manager.commandBuilder("scproxy") + .literal("layout") + .literal("help") + .permission(ProxyPermissions.LAYOUT_HELP) + .handler { context: CommandContext -> handleHelp(context.sender()) } + .build() + ) + } + + private fun handleHelp(sender: ProxyCommandSender) { + val messages = plugin.messageConfig.get() + val entry = messages.command.layout.help.entry + + sender.sendMessage(messages.resolve(messages.command.layout.help.header)) + commands.forEach { command -> + sender.sendMessage(entry.replace("", command)) + } + } + + private fun loadInfoDefault() { + manager.command( + manager.commandBuilder("scproxy") + .literal("layout") + .literal("info") + .permission(ProxyPermissions.LAYOUT_INFO) + .handler { context: CommandContext -> + sendLayoutInfo( + context.sender(), + group = "default", + layoutName = plugin.config.get().initialLayout + ) + } + .build() + ) + } + + private fun loadInfoGroup() { + manager.command( + manager.commandBuilder("scproxy") + .literal("layout") + .literal("info") + .required("group", StringParser.stringParser()) { _, _ -> suggestGroups() } + .permission(ProxyPermissions.LAYOUT_INFO) + .handler { context: CommandContext -> + CoroutineScope(Dispatchers.IO).launch { + val group = context.get("group") + sendLayoutInfo(context.sender(), group, resolveLayoutOfGroup(group)) + } + } + .build() + ) + } + + private suspend fun resolveLayoutOfGroup(group: String): String { + val groupLayout = plugin.cloudControllerHandler + .getGroupProperty(group, MotdLayoutRepository.KEY) + if (groupLayout != null) { + return groupLayout + } + + val config = plugin.config.get() + val stateName = plugin.joinStateService.getJoinStateAtGroup(group) + + return config.joinstates.find { it.name == stateName }?.forcedMotdLayout ?: config.initialLayout + } + + private fun sendLayoutInfo(sender: ProxyCommandSender, group: String, layoutName: String) { + val messages = plugin.messageConfig.get() + + sender.sendMessage(messages.resolve(messages.command.layout.info.header).replace("", group)) + sender.sendMessage(messages.resolve(messages.command.layout.info.entry).replace("", layoutName)) + } + + private fun loadSet() { + manager.command( + manager.commandBuilder("scproxy") + .literal("layout") + .literal("set") + .required("group", StringParser.stringParser()) { _, _ -> suggestGroups() } + .required("layout", StringParser.stringParser()) { _, _ -> suggestLayouts() } + .permission(ProxyPermissions.LAYOUT_SET) + .handler { context: CommandContext -> + CoroutineScope(Dispatchers.IO).launch { handleSet(context) } + } + .build() + ) + } + + private suspend fun handleSet(context: CommandContext) { + val group = context.get("group") + val layout = context.get("layout") + val messages = plugin.messageConfig.get() + + if (layout !in plugin.layoutRepository.getAvailableLayouts()) { + sendSetResult(context.sender(), messages, updated = false) + return + } + + val groupUpdated = plugin.cloudControllerHandler + .updateGroupProperty(group, MotdLayoutRepository.KEY, layout) + val servicesUpdated = plugin.cloudControllerHandler + .updateServicePropertyOnAllGroupServers(group, MotdLayoutRepository.KEY, layout) + val updated = groupUpdated && servicesUpdated + + if (updated && isCurrentGroup(group)) { + plugin.layoutRepository.setLocalLayout(layout) + } + + sendSetResult(context.sender(), messages, updated) + } + + private fun sendSetResult(sender: ProxyCommandSender, messages: MessageConfig, updated: Boolean) { + val message = when { + updated -> messages.command.layout.set.updateSuccess + else -> messages.command.layout.set.updateFailure + } + + sender.sendMessage(messages.resolve(message)) + } + + private fun isCurrentGroup(group: String): Boolean { + val currentServer = plugin.cloudControllerHandler.currentServer ?: return false + return currentServer.isFromGroup && currentServer.group?.name == group + } + + private fun suggestGroups(): CompletableFuture> = runBlocking { + val groups = plugin.cloudControllerHandler.getAllGroups() + .map { Suggestion.suggestion(it) } + + CompletableFuture.completedFuture(groups) + } + + private fun suggestLayouts(): CompletableFuture> { + val layouts = plugin.layoutRepository.getAvailableLayouts() + .map { Suggestion.suggestion(it) } + + return CompletableFuture.completedFuture(layouts) + } +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MotdLayoutConfiguration.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MotdLayoutConfiguration.kt index 9196d42..9241970 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MotdLayoutConfiguration.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MotdLayoutConfiguration.kt @@ -61,7 +61,18 @@ data class SlotsConfig( val type: MaxPlayerDisplayType = MaxPlayerDisplayType.REAL, val fakeSlots: Int = 100, val dynamicPlayerRange: Int = 5 -) +) { + /** The player limit to show in the server list, which may differ from the real one. */ + fun resolveMaxPlayers(onlinePlayers: Int, realMaxPlayers: Int): Int { + if (!enabled) return realMaxPlayers + + return when (type) { + MaxPlayerDisplayType.REAL -> realMaxPlayers + MaxPlayerDisplayType.FAKE -> fakeSlots + MaxPlayerDisplayType.DYNAMIC -> onlinePlayers + dynamicPlayerRange + } + } +} enum class MaxPlayerDisplayType { REAL, diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ProxyEssentialsConfig.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ProxyEssentialsConfig.kt index d779be3..14154d8 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ProxyEssentialsConfig.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ProxyEssentialsConfig.kt @@ -17,17 +17,25 @@ data class ProxyEssentialsConfig( val playerCount: PlayerCountConfig = PlayerCountConfig(), val tablist: List = DefaultConfigs.TABLIST_GROUPS ) : VersionedConfig { + + /** The tab list is updated as often as the most frequently updating group demands. */ fun tabListUpdateTimeMillis(): Long { - val ticks = tablist.minOfOrNull { it.updateTime } ?: 20L - return ticks * 50L + val ticks = tablist.minOfOrNull { it.updateTime } ?: DEFAULT_UPDATE_TIME_TICKS + return ticks * MILLIS_PER_TICK } + /** Returns null while the network wide player count is disabled. */ fun playerCountUpdateTimeMillis(): Long? { if (!playerCount.enabled || playerCount.updateTime <= 0L) { return null } - return playerCount.updateTime * 50L + return playerCount.updateTime * MILLIS_PER_TICK + } + + companion object { + private const val MILLIS_PER_TICK = 50L + private const val DEFAULT_UPDATE_TIME_TICKS = 20L } } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/OldConfigMigrator.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/OldConfigMigrator.kt similarity index 98% rename from proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/OldConfigMigrator.kt rename to proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/OldConfigMigrator.kt index 28a507f..07c6501 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/OldConfigMigrator.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/OldConfigMigrator.kt @@ -1,5 +1,9 @@ -package app.simplecloud.plugin.proxy.shared.config +package app.simplecloud.plugin.proxy.shared.config.migration +import app.simplecloud.plugin.proxy.shared.config.MessageConfig +import app.simplecloud.plugin.proxy.shared.config.PlaceHolderConfiguration +import app.simplecloud.plugin.proxy.shared.config.ProxyEssentialsConfig +import app.simplecloud.plugin.proxy.shared.config.WhitelistConfig import org.spongepowered.configurate.CommentedConfigurationNode import org.spongepowered.configurate.kotlin.objectMapperFactory import org.spongepowered.configurate.yaml.NodeStyle @@ -182,7 +186,7 @@ object OldConfigMigrator { target.node("joinstates").set(states) val whitelist = WhitelistConfig() target.node("whitelist").set( - mapOf( + mapOf( "enabled" to whitelist.enabled, "players" to whitelist.players ) @@ -487,10 +491,7 @@ object OldConfigMigrator { private fun disableServerIconForPreviouslyMigratedLayout(source: CommentedConfigurationNode): Boolean { val serverIcon = source.node("server-icon") val playerList = source.node("player-list") - val looksLikePreviouslyMigratedLayout = serverIcon.node("enabled").boolean && - serverIcon.node("file").string == "simplecloud.png" && - playerList.node("enabled").boolean == false && - playerList.node("player-list").childrenList().isEmpty() + val looksLikePreviouslyMigratedLayout = serverIcon.node("enabled").boolean && serverIcon.node("file").string == "simplecloud.png" && !playerList.node("enabled").boolean && playerList.node("player-list").childrenList().isEmpty() if (looksLikePreviouslyMigratedLayout) { serverIcon.node("enabled").set(false) diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/CloudControllerHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/controller/CloudControllerHandler.kt similarity index 58% rename from proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/CloudControllerHandler.kt rename to proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/controller/CloudControllerHandler.kt index 577315c..671b3aa 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/CloudControllerHandler.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/controller/CloudControllerHandler.kt @@ -1,80 +1,92 @@ -package app.simplecloud.plugin.proxy.shared.handler +package app.simplecloud.plugin.proxy.shared.controller +import app.simplecloud.api.group.Group import app.simplecloud.api.persistentserver.PersistentServer +import app.simplecloud.api.runtime.SimpleCloudRuntime import app.simplecloud.api.server.Server import app.simplecloud.api.server.ServerQuery import app.simplecloud.plugin.proxy.shared.ProxyPlugin +import app.simplecloud.plugin.proxy.shared.joinstate.JoinStateService +import app.simplecloud.plugin.proxy.shared.motd.MotdLayoutRepository import kotlinx.coroutines.* import kotlinx.coroutines.future.await import java.util.logging.Logger class CloudControllerHandler( private val plugin: ProxyPlugin, - private val joinStateHandler: JoinStateHandler + private val service: JoinStateService ) { + private val logger = Logger.getLogger(CloudControllerHandler::class.java.name) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private val serviceId = System.getenv("SIMPLECLOUD_UNIQUE_ID") @Volatile var currentServer: Server? = null private set - init { - initialize() - } - - private fun initialize() { - if (serviceId.isNullOrBlank()) { + fun start() { + if (SimpleCloudRuntime.serverId().isNullOrBlank()) { logger.warning("Environment variable SIMPLECLOUD_UNIQUE_ID is not set.") - joinStateHandler.registerListener() - plugin.motdLayoutHandler.registerListener() + registerListeners() return } scope.launch { try { - currentServer = plugin.api.server().getServerById(serviceId).await() + currentServer = plugin.api.server().getServerById(SimpleCloudRuntime.serverId()).await() logger.info("Initialized server: ${currentServer?.serverBase?.name}") - joinStateHandler.registerListener() - plugin.motdLayoutHandler.registerListener() + registerListeners() initializeJoinState() } catch (e: Exception) { logger.severe("Error retrieving server by ID: ${e.message}") - joinStateHandler.registerListener() - plugin.motdLayoutHandler.registerListener() + registerListeners() } } } + fun close() { + scope.cancel() + } + + private fun registerListeners() { + service.registerListener() + plugin.layoutRepository.registerListener() + } + private suspend fun initializeJoinState() { val server = currentServer ?: return - if (server.isFromGroup) { - val groupName = server.group?.name ?: return - joinStateHandler.localState = joinStateHandler.getJoinStateAtService(groupName, server.numericalId) - joinStateHandler.startGroupStateSyncTask() - plugin.motdLayoutHandler.setLocalLayout( - getServiceProperty(groupName, server.numericalId, MotdLayoutHandler.MOTD_LAYOUT_KEY) - ?: getGroupProperty(groupName, MotdLayoutHandler.MOTD_LAYOUT_KEY) - ) - } else { - val joinState = server.properties?.get(JoinStateHandler.JOINSTATE_KEY)?.toString() - if (!joinState.isNullOrEmpty()) { - joinStateHandler.localState = joinState - } else { - val persistentName = server.persistentServer?.name - if (persistentName != null) { - joinStateHandler.localState = joinStateHandler.ensureJoinStateAtPersistentServer(persistentName) - } - } - plugin.motdLayoutHandler.setLocalLayout(server.properties?.get(MotdLayoutHandler.MOTD_LAYOUT_KEY)?.toString()) + when { + server.isFromGroup -> initializeJoinStateOfGroupServer(server) + else -> initializeJoinStateOfPersistentServer(server) } } - fun close() { - scope.cancel() + private suspend fun initializeJoinStateOfGroupServer(server: Server) { + val groupName = server.group?.name ?: return + + service.localState = service.getJoinStateAtService(groupName, server.numericalId) + service.startGroupStateSyncTask() + plugin.layoutRepository.setLocalLayout( + getServiceProperty(groupName, server.numericalId, MotdLayoutRepository.KEY) + ?: getGroupProperty(groupName, MotdLayoutRepository.KEY) + ) + } + + private suspend fun initializeJoinStateOfPersistentServer(server: Server) { + val joinState = server.properties?.get(JoinStateService.KEY)?.toString() + val persistentServerName = server.persistentServer?.name + + when { + !joinState.isNullOrEmpty() -> service.localState = joinState + persistentServerName != null -> + service.localState = service.ensureJoinStateAtPersistentServer(persistentServerName) + } + + plugin.layoutRepository.setLocalLayout( + server.properties?.get(MotdLayoutRepository.KEY)?.toString() + ) } suspend fun updateServerProperty(serverId: String, key: String, value: String): Boolean { @@ -87,55 +99,75 @@ class CloudControllerHandler( } } + suspend fun groupExists(groupName: String): Boolean = getGroupByName(groupName) != null + suspend fun getGroupProperty(groupName: String, key: String): String? { return try { - getGroupByName(groupName)?.properties?.get(key)?.toString()?.takeIf { it.isNotBlank() } + getGroupByName(groupName)?.properties?.get(key)?.toString().orNullIfBlank() } catch (e: Exception) { logger.severe("Error retrieving group property: ${e.message}") null } } - private suspend fun getGroupByName(groupName: String) = try { - plugin.api.group().getGroupByName(groupName).await() - ?: findGroupCaseInsensitive(groupName) - } catch (_: Exception) { - try { - findGroupCaseInsensitive(groupName) - } catch (e2: Exception) { - logger.severe("Error retrieving group '$groupName': ${e2.message}") - null + suspend fun updateGroupProperty(groupName: String, key: String, value: String): Boolean { + val group = getGroupByName(groupName) ?: return false + return try { + plugin.api.group().updateGroupProperties(group.serverGroupId, mapOf(key to value)).await() + logger.info("Group property '$key' updated to '$value' for group '$groupName'") + true + } catch (e: Exception) { + logger.severe("Error updating group properties: ${e.message}") + false + } + } + + suspend fun getAllGroups(): List { + return try { + plugin.api.group().allGroups.await().map { it.name } + } catch (e: Exception) { + logger.severe("Error retrieving groups: ${e.message}") + emptyList() } } - private suspend fun findGroupCaseInsensitive(groupName: String) = - plugin.api.group().allGroups.await().firstOrNull { it.name.equals(groupName, ignoreCase = true) } + suspend fun getMaxPlayersInGroup(groupName: String): Int = getGroupByName(groupName)?.maxPlayers ?: 0 - private suspend fun resolveGroupNameCasing(groupName: String): String = - getGroupByName(groupName)?.name ?: groupName + suspend fun getOnlinePlayersInGroup(groupName: String): Int { + return try { + getServersByGroup(groupName).sumOf { it.playerCount ?: 0 } + } catch (e: Exception) { + logger.severe("Error retrieving online players in group: ${e.message}") + 0 + } + } - suspend fun groupExists(groupName: String): Boolean { - return getGroupByName(groupName) != null + suspend fun getAllNumericalIdsFromGroup(groupName: String): List { + return getServersByGroup(groupName).map { it.numericalId } } - suspend fun getPersistentServerByName(name: String): PersistentServer? = try { - plugin.api.persistentServer().getPersistentServerByName(name).await() - ?: findPersistentServerCaseInsensitive(name) - } catch (_: Exception) { - try { - findPersistentServerCaseInsensitive(name) - } catch (e2: Exception) { - logger.severe("Error retrieving persistent server '$name': ${e2.message}") + suspend fun getPersistentServerByName(name: String): PersistentServer? { + val persistentServer = try { + plugin.api.persistentServer().getPersistentServerByName(name).await() + } catch (_: Exception) { null } - } + if (persistentServer != null) { + return persistentServer + } - private suspend fun findPersistentServerCaseInsensitive(name: String) = - plugin.api.persistentServer().allPersistentServers.await().firstOrNull { it.name.equals(name, ignoreCase = true) } + return try { + plugin.api.persistentServer().allPersistentServers.await() + .firstOrNull { it.name.equals(name, ignoreCase = true) } + } catch (e: Exception) { + logger.severe("Error retrieving persistent server '$name': ${e.message}") + null + } + } suspend fun getPersistentServerProperty(name: String, key: String): String? { return try { - getPersistentServerByName(name)?.properties?.get(key)?.toString()?.takeIf { it.isNotBlank() } + getPersistentServerByName(name)?.properties?.get(key)?.toString().orNullIfBlank() } catch (e: Exception) { logger.severe("Error retrieving persistent server property: ${e.message}") null @@ -145,7 +177,9 @@ class CloudControllerHandler( suspend fun updatePersistentServerProperty(name: String, key: String, value: String): Boolean { val persistentServer = getPersistentServerByName(name) ?: return false return try { - plugin.api.persistentServer().updatePersistentServerProperty(persistentServer.persistentServerId, key, value).await() + plugin.api.persistentServer() + .updatePersistentServerProperty(persistentServer.persistentServerId, key, value) + .await() logger.info("Persistent server property '$key' updated to '$value' for persistent server '$name'") true } catch (e: Exception) { @@ -163,100 +197,37 @@ class CloudControllerHandler( } } - suspend fun getServerByNumericalId(groupName: String, numericalId: Int): Server? = try { - plugin.api.server().getAllServers( - ServerQuery.create() - .filterByServerGroupName(resolveGroupNameCasing(groupName)) - .filterByNumericalId(numericalId) - ).await()?.firstOrNull() - } catch (e: Exception) { - logger.severe("Error retrieving server '$groupName-$numericalId': ${e.message}") - null - } - - suspend fun getServersByGroup(groupName: String): List = try { - plugin.api.server().getAllServers( - ServerQuery.create() - .filterByServerGroupName(resolveGroupNameCasing(groupName)) - ).await() ?: emptyList() - } catch (e: Exception) { - logger.severe("Error retrieving servers for group '$groupName': ${e.message}") - emptyList() - } - - suspend fun getServiceProperty(groupName: String, numericalId: Int, key: String): String? { - val server = getServerByNumericalId(groupName, numericalId) ?: return null - return server.properties[key]?.toString()?.takeIf { it.isNotBlank() } - } - - suspend fun updateServiceProperty(groupName: String, numericalId: Int, key: String, value: String): Boolean { - val server = getServerByNumericalId(groupName, numericalId) - if (server == null) { - logger.severe("Server not found for group '$groupName' and numerical ID '$numericalId'") - return false - } - return updateServerProperty(server.serverId, key, value) - } - - suspend fun updateServicePropertyOnAllGroupServers(groupName: String, key: String, value: String): Boolean { - val servers = getServersByGroup(groupName) - var allSuccessful = true - servers.forEach { server -> - if (!updateServerProperty(server.serverId, key, value)) { - allSuccessful = false - } - } - if (allSuccessful) { - logger.info("Service property '$key' updated to '$value' for all services in group '$groupName'") - } - return allSuccessful - } - - suspend fun updateGroupProperty(groupName: String, key: String, value: String): Boolean { - val group = getGroupByName(groupName) ?: return false - try { - plugin.api.group().updateGroupProperties(group.serverGroupId, mapOf(key to value)).await() - logger.info("Group property '$key' updated to '$value' for group '$groupName'") - return true - } catch (e: Exception) { - logger.severe("Error updating group properties: ${e.message}") - return false - } - } - - suspend fun getOnlinePlayersInGroup(groupName: String): Int { + suspend fun getServersByGroup(groupName: String): List { return try { - getServersByGroup(groupName).sumOf { it.playerCount ?: 0 } + plugin.api.server().getAllServers( + ServerQuery.create().filterByServerGroupName(resolveGroupNameCasing(groupName)) + ).await() ?: emptyList() } catch (e: Exception) { - logger.severe("Error retrieving online players in group: ${e.message}") - 0 + logger.severe("Error retrieving servers for group '$groupName': ${e.message}") + emptyList() } } - suspend fun getMaxPlayersInGroup(groupName: String): Int { - return getGroupByName(groupName)?.maxPlayers ?: 0 - } - - suspend fun getAllGroups(): List { + suspend fun getServerByNumericalId(groupName: String, numericalId: Int): Server? { return try { - plugin.api.group().allGroups.await().map { it.name } + plugin.api.server().getAllServers( + ServerQuery.create() + .filterByServerGroupName(resolveGroupNameCasing(groupName)) + .filterByNumericalId(numericalId) + ).await()?.firstOrNull() } catch (e: Exception) { - logger.severe("Error retrieving groups: ${e.message}") - emptyList() + logger.severe("Error retrieving server '$groupName-$numericalId': ${e.message}") + null } } - suspend fun getAllNumericalIdsFromGroup(groupName: String): List { - return getServersByGroup(groupName).map { it.numericalId } - } - suspend fun getServerByName(name: String): Server? { return try { val servers = plugin.api.server().getAllServers(ServerQuery.create()).await() ?: return null servers.find { it.serverBase.name == name || it.persistentServer?.name == name } ?: servers.find { it.serverBase.name.equals(name, ignoreCase = true) - || it.persistentServer?.name.equals(name, ignoreCase = true) + || it.persistentServer?.name.equals(name, ignoreCase = true) } } catch (e: Exception) { logger.severe("Error retrieving server by name: ${e.message}") @@ -271,10 +242,63 @@ class CloudControllerHandler( return try { val servers = plugin.api.server().getAllServers(ServerQuery.create()).await() ?: return emptyList() - servers.filter { server -> server.persistentServer?.name?.let { it in names } == true } + servers.filter { server -> + val persistentServerName = server.persistentServer?.name ?: return@filter false + persistentServerName in names + } } catch (e: Exception) { logger.severe("Error retrieving persistent servers by name: ${e.message}") emptyList() } } + + suspend fun getServiceProperty(groupName: String, numericalId: Int, key: String): String? { + val server = getServerByNumericalId(groupName, numericalId) ?: return null + return server.properties[key]?.toString().orNullIfBlank() + } + + suspend fun updateServiceProperty(groupName: String, numericalId: Int, key: String, value: String): Boolean { + val server = getServerByNumericalId(groupName, numericalId) + if (server == null) { + logger.severe("Server not found for group '$groupName' and numerical ID '$numericalId'") + return false + } + return updateServerProperty(server.serverId, key, value) + } + + suspend fun updateServicePropertyOnAllGroupServers(groupName: String, key: String, value: String): Boolean { + val results = getServersByGroup(groupName).map { updateServerProperty(it.serverId, key, value) } + val allSuccessful = results.all { it } + + if (allSuccessful) { + logger.info("Service property '$key' updated to '$value' for all services in group '$groupName'") + } + return allSuccessful + } + + private suspend fun getGroupByName(groupName: String): Group? { + val group = try { + plugin.api.group().getGroupByName(groupName).await() + } catch (_: Exception) { + null + } + if (group != null) { + return group + } + + return try { + plugin.api.group().allGroups.await().firstOrNull { it.name.equals(groupName, ignoreCase = true) } + } catch (e: Exception) { + logger.severe("Error retrieving group '$groupName': ${e.message}") + null + } + } + + private suspend fun resolveGroupNameCasing(groupName: String): String = + getGroupByName(groupName)?.name ?: groupName + + private fun String?.orNullIfBlank(): String? { + if (isNullOrBlank()) return null + return this + } } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/format/MotdMiniMessageFormatter.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/format/MotdMiniMessageFormatter.kt deleted file mode 100644 index 0ff481b..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/format/MotdMiniMessageFormatter.kt +++ /dev/null @@ -1,187 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.format - -import net.kyori.adventure.key.Key -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.KeybindComponent -import net.kyori.adventure.text.ObjectComponent -import net.kyori.adventure.text.TextComponent -import net.kyori.adventure.text.TranslatableComponent -import net.kyori.adventure.text.format.Style -import net.kyori.adventure.text.format.TextDecoration -import net.kyori.adventure.text.minimessage.MiniMessage -import net.kyori.adventure.text.minimessage.tag.Tag -import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver -import kotlin.math.roundToInt - -object MotdMiniMessageFormatter { - - private const val CENTER_TAG = "center" - private const val CENTER_MARKER = "simplecloud:motd_center" - private const val SERVER_LIST_ROW_WIDTH = 305f - // MOTD text starts after the 32-pixel icon and its 3-pixel gap. - private const val MOTD_START_OFFSET = 35f - // Center relative to the complete row, not just the area remaining after the icon. - private const val MOTD_CENTER_WIDTH = SERVER_LIST_ROW_WIDTH - (MOTD_START_OFFSET * 2) - private const val SPACE_WIDTH = 4 - private const val PIXEL_PADDING_CHARACTER = '\u200c' - private val defaultFont = Key.key("minecraft", "default") - private val mergesWithoutInsertion = Style.Merge.all().minus(Style.Merge.INSERTION) - - private val centerTagResolver = TagResolver.resolver(CENTER_TAG) { _, _ -> - Tag.inserting(Component.empty().insertion(CENTER_MARKER)) - } - - fun deserialize( - miniMessage: MiniMessage, - line1: String, - line2: String, - tagResolvers: List = emptyList() - ): Component { - val resolvers = arrayOf(centerTagResolver, *tagResolvers.toTypedArray()) - - // Treat both configured lines independently so an omitted closing tag cannot leak into - // the other line. MiniMessage closes still-open tags at the end of each input. - return Component.empty() - .append(applyCentering(miniMessage.deserialize(line1, *resolvers))) - .append(Component.newline()) - .append(applyCentering(miniMessage.deserialize(line2, *resolvers))) - } - - /** Drops the [CENTER_MARKER] insertion again once the marked component has been measured. */ - private fun clearInsertion(style: Style): Style { - return Style.empty().merge(style, Style.Merge.Strategy.ALWAYS, mergesWithoutInsertion) - } - - private fun applyCentering(component: Component, inheritedStyle: Style = Style.empty()): Component { - val style = component.style().merge(inheritedStyle, Style.Merge.Strategy.IF_ABSENT_ON_TARGET) - var processed = component.children( - component.children().map { applyCentering(it, style) } - ) - - // Resolve on the finished tree so styles outside
and inserted components are measurable. - if (processed.insertion() != CENTER_MARKER) return processed - - processed = processed.style(clearInsertion(processed.style())) - val contentWidth = MinecraftTextWidth.width(processed, inheritedStyle) - val paddingWidth = ((MOTD_CENTER_WIDTH - contentWidth) / 2f) - .roundToInt() - .coerceAtLeast(0) - - if (paddingWidth == 0) return processed - - val padding = Component.text() - .append( - Component.text(" ".repeat(paddingWidth / SPACE_WIDTH)) - .font(defaultFont) - .decoration(TextDecoration.BOLD, false) - ) - // U+200C has zero advance in Minecraft's space provider. Bold contributes its - // one-pixel bold offset while the glyph itself remains invisible. - .append( - Component.text(PIXEL_PADDING_CHARACTER.toString().repeat(paddingWidth % SPACE_WIDTH)) - .font(defaultFont) - .decoration(TextDecoration.BOLD, true) - ) - .build() - - return Component.empty().append(padding).append(processed) - } -} - -internal object MinecraftTextWidth { - - private const val MISSING_GLYPH_WIDTH = 6f - private const val OBJECT_WIDTH = 8f - private const val BITMAP_BOLD_OFFSET = 1f - private const val UNIHEX_BOLD_OFFSET = 0.5f - private const val UNICODE_CODE_POINT_COUNT = 0x110000 - private val defaultFont = Key.key("minecraft", "default") - private val mergesWithoutInsertion = Style.Merge.all().minus(Style.Merge.INSERTION) - private val altFont = Key.key("minecraft", "alt") - private val uniformFont = Key.key("minecraft", "uniform") - - private val defaultWidths by lazy { loadWidths("default-font-widths.txt") } - private val altWidths by lazy { loadWidths("alt-font-widths.txt") } - private val unifontAdvances by lazy { loadAdvances("unifont-advances.bin") } - - fun width(component: Component, inheritedStyle: Style = Style.empty()): Float = - calculateWidth(component, inheritedStyle) - - private fun calculateWidth(component: Component, inheritedStyle: Style): Float { - val style = component.style().merge(inheritedStyle, Style.Merge.Strategy.IF_ABSENT_ON_TARGET) - val bold = style.hasDecoration(TextDecoration.BOLD) - - val ownWidth = when (component) { - is TextComponent -> component.content().codePoints().toArray().sumOf { - glyphWidth(it, style.font(), bold).toDouble() - }.toFloat() - - is ObjectComponent -> OBJECT_WIDTH + if (bold) 1f else 0f - is TranslatableComponent -> textWidth(component.fallback() ?: component.key(), style) - is KeybindComponent -> textWidth(component.keybind(), style) - else -> 0f - } - - return ownWidth + component.children().sumOf { calculateWidth(it, style).toDouble() }.toFloat() - } - - private fun textWidth(text: String, style: Style): Float { - val bold = style.hasDecoration(TextDecoration.BOLD) - return text.codePoints().toArray().sumOf { - glyphWidth(it, style.font(), bold).toDouble() - }.toFloat() - } - - private fun glyphWidth(codePoint: Int, font: Key?, bold: Boolean): Float { - val metrics = when { - codePoint == ' '.code -> GlyphMetrics(4f, BITMAP_BOLD_OFFSET) - codePoint == 0x200c -> GlyphMetrics(0f, BITMAP_BOLD_OFFSET) - font == null || font == defaultFont -> - defaultWidths[codePoint]?.let { GlyphMetrics(it, BITMAP_BOLD_OFFSET) } - ?: unifontWidth(codePoint)?.let { GlyphMetrics(it, UNIHEX_BOLD_OFFSET) } - - font == altFont -> altWidths[codePoint]?.let { GlyphMetrics(it, BITMAP_BOLD_OFFSET) } - font == uniformFont -> unifontWidth(codePoint)?.let { GlyphMetrics(it, UNIHEX_BOLD_OFFSET) } - else -> null - } ?: GlyphMetrics(MISSING_GLYPH_WIDTH, BITMAP_BOLD_OFFSET) - - return metrics.advance + if (bold) metrics.boldOffset else 0f - } - - private fun unifontWidth(codePoint: Int): Float? { - if (codePoint !in unifontAdvances.indices) return null - - val encodedAdvance = unifontAdvances[codePoint].toInt() and 0xff - return encodedAdvance.takeIf { it != 0 }?.div(2f) - } - - private fun loadWidths(resourceName: String): Map { - val resourcePath = "/app/simplecloud/plugin/proxy/shared/format/$resourceName" - val stream = checkNotNull(javaClass.getResourceAsStream(resourcePath)) { - "Missing font metrics resource $resourcePath" - } - - return stream.bufferedReader().useLines { lines -> - lines - .filterNot { it.isBlank() || it.startsWith('#') } - .associate { line -> - val (codePoint, width) = line.split('=', limit = 2) - codePoint.toInt(16) to width.toFloat() - } - } - } - - private fun loadAdvances(resourceName: String): ByteArray { - val resourcePath = "/app/simplecloud/plugin/proxy/shared/format/$resourceName" - val advances = checkNotNull(javaClass.getResourceAsStream(resourcePath)) { - "Missing font metrics resource $resourcePath" - }.use { it.readBytes() } - - check(advances.size == UNICODE_CODE_POINT_COUNT) { - "Invalid font metrics resource $resourcePath: expected $UNICODE_CODE_POINT_COUNT bytes, got ${advances.size}" - } - return advances - } - - private data class GlyphMetrics(val advance: Float, val boldOffset: Float) -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/JoinStateHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/JoinStateHandler.kt deleted file mode 100644 index 6dcbcae..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/JoinStateHandler.kt +++ /dev/null @@ -1,240 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.handler - -import app.simplecloud.api.server.Server -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import kotlinx.coroutines.* -import java.util.logging.Logger -import kotlin.time.Duration.Companion.milliseconds - -class JoinStateHandler( - private val proxyPlugin: ProxyPlugin -) { - - private val logger = Logger.getLogger(JoinStateHandler::class.java.name) - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private var groupStateSyncJob: Job? = null - - @Volatile - private var lastObservedGroupState: String? = null - - @Volatile - var localState: String = proxyPlugin.proxyEssentialsConfig.get().initialState - - companion object { - const val JOINSTATE_KEY = "joinstate" - } - - fun stop() { - groupStateSyncJob?.cancel() - scope.cancel() - } - - private fun defaultJoinState(): String = proxyPlugin.proxyEssentialsConfig.get().initialState - - suspend fun setJoinStateAtGroup(groupName: String, joinStateName: String): Boolean { - val updated = this.proxyPlugin.cloudControllerHandler.updateGroupProperty(groupName, JOINSTATE_KEY, joinStateName) - if (updated) { - lastObservedGroupState = joinStateName - } - return updated - } - - suspend fun getJoinStateAtGroup(groupName: String): String { - return proxyPlugin.cloudControllerHandler.getGroupProperty(groupName, JOINSTATE_KEY) - ?: defaultJoinState() - } - - suspend fun ensureJoinStateAtGroup(groupName: String): String { - val existingJoinState = proxyPlugin.cloudControllerHandler.getGroupProperty(groupName, JOINSTATE_KEY) - if (!existingJoinState.isNullOrBlank()) { - return existingJoinState - } - val fallbackState = defaultJoinState() - if (!setJoinStateAtGroup(groupName, fallbackState)) { - logger.warning("Could not persist default join state '$fallbackState' for group '$groupName'") - } - return fallbackState - } - - suspend fun setJoinStateAtService(groupName: String, numericalId: Int, joinStateName: String): Boolean { - return this.proxyPlugin.cloudControllerHandler.updateServiceProperty( - groupName, - numericalId, - JOINSTATE_KEY, - joinStateName - ) - } - - suspend fun getJoinStateAtService(groupName: String, numericalId: Int): String { - return proxyPlugin.cloudControllerHandler.getServiceProperty(groupName, numericalId, JOINSTATE_KEY) - ?: proxyPlugin.cloudControllerHandler.getGroupProperty(groupName, JOINSTATE_KEY) - ?: defaultJoinState() - } - - suspend fun getJoinStateAtPersistentServer(serverName: String): String { - return proxyPlugin.cloudControllerHandler.getPersistentServerProperty(serverName, JOINSTATE_KEY) - ?: defaultJoinState() - } - - suspend fun ensureJoinStateAtPersistentServer(serverName: String): String { - val existingJoinState = - proxyPlugin.cloudControllerHandler.getPersistentServerProperty(serverName, JOINSTATE_KEY) - - if (!existingJoinState.isNullOrBlank()) { - return existingJoinState - } - - val fallbackState = defaultJoinState() - - if (!setJoinStateAtPersistentServer(serverName, fallbackState)) { - logger.warning("Could not persist default join state '$fallbackState' for persistent server '$serverName'") - } - - return fallbackState - } - - suspend fun setJoinStateAtPersistentServer(serverName: String, joinStateName: String): Boolean { - val updated = - proxyPlugin.cloudControllerHandler.updatePersistentServerProperty(serverName, JOINSTATE_KEY, joinStateName) - - if (updated) { - val currentServer = proxyPlugin.cloudControllerHandler.currentServer - - if (currentServer != null && !currentServer.isFromGroup && currentServer.persistentServer?.name == serverName) { - val synchronized = proxyPlugin.cloudControllerHandler.updateServerProperty( - currentServer.serverId, - JOINSTATE_KEY, - joinStateName - ) - - if (synchronized) { - localState = joinStateName - } - } - } - - return updated - } - - suspend fun setJoinStateAtGroupAndAllServicesInGroup(groupName: String, joinStateName: String): Boolean { - val groupPropertyUpdated = setJoinStateAtGroup(groupName, joinStateName) - val servicePropertiesUpdated = this.proxyPlugin.cloudControllerHandler.updateServicePropertyOnAllGroupServers( - groupName, - JOINSTATE_KEY, - joinStateName - ) - val successful = groupPropertyUpdated && servicePropertiesUpdated - if (!successful) { - logger.severe("Error setting join state at group and all services in group $groupName.") - } - return successful - } - - fun startGroupStateSyncTask() { - if (groupStateSyncJob?.isActive == true) { - return - } - groupStateSyncJob = scope.launch { - while (isActive) { - try { - syncLocalStateWithGroupState() - } catch (e: Exception) { - logger.severe("Error while syncing local/group join state: ${e.message}") - } - delay(2000.milliseconds) - } - } - } - - fun registerListener() { - proxyPlugin.api.event().server().onUpdated { event -> - if (event.serverId != System.getenv("SIMPLECLOUD_UNIQUE_ID")) return@onUpdated - - val server = event.server - val state = server.properties?.get(JOINSTATE_KEY)?.toString() - - if (state.isNullOrBlank()) { - this.logger.warning("No join state found for server. Using default join state.") - - scope.launch { applyDefaultJoinStateForCurrentServer(server) } - return@onUpdated - } - - if (state == localState) { - return@onUpdated - } - - localState = state - this.logger.info("Join state changed to $state") - } - } - - private suspend fun applyDefaultJoinStateForCurrentServer(server: Server) { - try { - val stateToApply = if (server.isFromGroup) { - val groupName = server.group?.name ?: return - ensureJoinStateAtGroup(groupName) - } else { - val persistentName = server.persistentServer?.name - if (persistentName != null) ensureJoinStateAtPersistentServer(persistentName) else defaultJoinState() - } - - proxyPlugin.cloudControllerHandler.updateServerProperty(server.serverId, JOINSTATE_KEY, stateToApply) - localState = stateToApply - } catch (e: Exception) { - logger.severe("Error setting default join state: ${e.message}") - } - } - - private suspend fun syncLocalStateWithGroupState() { - val server = proxyPlugin.cloudControllerHandler.currentServer ?: return - if (!server.isFromGroup) return - - val groupName = server.group?.name ?: return - val numericalId = server.numericalId - - val groupState = proxyPlugin.cloudControllerHandler.getGroupProperty(groupName, JOINSTATE_KEY) - if (groupState.isNullOrBlank()) { - return - } - - val previousGroupState = lastObservedGroupState - if (previousGroupState == null) { - lastObservedGroupState = groupState - return - } - - val serviceState = proxyPlugin.cloudControllerHandler.getServiceProperty(groupName, numericalId, JOINSTATE_KEY) - if (serviceState.isNullOrBlank()) { - val synchronized = - proxyPlugin.cloudControllerHandler.updateServerProperty(server.serverId, JOINSTATE_KEY, groupState) - if (!synchronized) { - logger.warning("Could not synchronize missing service join state for '$groupName-$numericalId'") - return - } - localState = groupState - logger.info("Join state changed to $groupState because service state was missing.") - return - } - - if (groupState == previousGroupState) { - return - } - - lastObservedGroupState = groupState - - if (serviceState != previousGroupState) { - return - } - - val synchronized = - proxyPlugin.cloudControllerHandler.updateServerProperty(server.serverId, JOINSTATE_KEY, groupState) - if (!synchronized) { - logger.warning("Could not synchronize join state for '$groupName-$numericalId' with group state '$groupState'") - return - } - - localState = groupState - logger.info("Join state changed to $groupState because of group state change.") - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/JoinStateResolver.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/JoinStateResolver.kt deleted file mode 100644 index eb9f4a1..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/JoinStateResolver.kt +++ /dev/null @@ -1,72 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.handler - -import app.simplecloud.plugin.api.shared.pattern.ServerPatternIdentifier -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.config.JoinState -import java.util.logging.Logger - -class JoinStateResolver( - private val proxyPlugin: ProxyPlugin -) { - private val logger = Logger.getLogger(JoinStateResolver::class.java.name) - - private val identifier by lazy { - ServerPatternIdentifier( - "-", - "(?[a-zA-Z0-9_-]+)-(?\\d+)", - cloudApi = proxyPlugin.api - ) - } - - fun resolveJoinState(stateName: String): JoinState? { - val joinstates = proxyPlugin.proxyEssentialsConfig.get().joinstates - val defaultStateName = proxyPlugin.proxyEssentialsConfig.get().initialState - - return joinstates.find { it.name == stateName } - ?: run { - logger.info("Join state '$stateName' not found. Using default '$defaultStateName'.") - joinstates.find { it.name == defaultStateName } - } - } - - suspend fun getJoinStateForServer(serverName: String): String { - try { - val (groupName, numericalId) = identifier.parse(serverName) - return proxyPlugin.joinStateHandler.getJoinStateAtService(groupName, numericalId) - } catch (_: IllegalArgumentException) { - } - - val server = proxyPlugin.cloudControllerHandler.getServerByName(serverName) - if (server != null) { - val joinState = server.properties?.get(JoinStateHandler.JOINSTATE_KEY)?.toString() - if (!joinState.isNullOrEmpty()) { - return joinState - } - - val persistentName = server.persistentServer?.name - if (persistentName != null) { - return proxyPlugin.joinStateHandler.getJoinStateAtPersistentServer(persistentName) - } - } - - return proxyPlugin.proxyEssentialsConfig.get().initialState - } - - suspend fun isServerFull(): Boolean { - val server = proxyPlugin.cloudControllerHandler.currentServer ?: return false - - if (server.isFromGroup) { - val groupName = server.group?.name ?: return false - val maxPlayers = proxyPlugin.cloudControllerHandler.getMaxPlayersInGroup(groupName) - if (maxPlayers <= 0) { - return false - } - val onlinePlayers = proxyPlugin.cloudControllerHandler.getOnlinePlayersInGroup(groupName) - return onlinePlayers >= maxPlayers - } - - val maxPlayers = server.maxPlayers ?: return false - val onlinePlayers = server.playerCount ?: 0 - return onlinePlayers >= maxPlayers - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/PlayerCountHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/PlayerCountHandler.kt deleted file mode 100644 index c1affe9..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/PlayerCountHandler.kt +++ /dev/null @@ -1,147 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.handler - -import app.simplecloud.api.server.Server -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import kotlinx.coroutines.* -import java.util.logging.Logger - -class PlayerCountHandler( - private val proxyPlugin: ProxyPlugin -) { - private val logger = Logger.getLogger(PlayerCountHandler::class.java.name) - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private var syncJob: Job? = null - - @Volatile - private var snapshot: PlayerCountSnapshot? = null - - fun start() { - if (syncJob?.isActive == true) { - return - } - - syncJob = scope.launch { - while (isActive) { - val updateTimeMillis = proxyPlugin.proxyEssentialsConfig.get().playerCountUpdateTimeMillis() - if (updateTimeMillis == null) { - snapshot = null - delay(DISABLED_CHECK_INTERVAL_MILLIS) - continue - } - - refresh() - delay(updateTimeMillis) - } - } - } - - fun stop() { - syncJob?.cancel() - scope.cancel() - } - - fun onlinePlayers(currentProxyOnlinePlayers: Int): Int { - return currentProxyOnlinePlayers + (snapshot?.otherOnlinePlayers ?: 0) - } - - fun maxPlayers(currentProxyMaxPlayers: Int): Int { - return snapshot?.maxPlayers?.takeIf { it > 0 } ?: currentProxyMaxPlayers - } - - private suspend fun refresh() { - try { - snapshot = resolveSnapshot() - } catch (e: Exception) { - logger.severe("Error while syncing player count: ${e.message}") - } - } - - private suspend fun resolveSnapshot(): PlayerCountSnapshot? { - val count = resolveCount() - - if (count.otherOnlinePlayers <= 0 && count.maxPlayers <= 0) { - return null - } - - return PlayerCountSnapshot( - otherOnlinePlayers = count.otherOnlinePlayers, - maxPlayers = count.maxPlayers.takeIf { it > 0 } - ) - } - - private suspend fun resolveCount(): PlayerCount { - val currentServer = proxyPlugin.cloudControllerHandler.currentServer - val config = proxyPlugin.proxyEssentialsConfig.get().playerCount - val currentGroupName = currentServer - ?.takeIf { it.isFromGroup } - ?.group - ?.name - val currentPersistentServerName = currentServer - ?.takeIf { it.isFromPersistentServer } - ?.persistentServer - ?.name - - val currentServerId = currentServer?.serverId - val additionalGroupNames = config.additionalGroups - .map { it.trim() } - .filter { it.isNotEmpty() } - .filter { it != currentGroupName } - .distinct() - - val additionalPersistentServerNames = config.additionalPersistentServers - .map { it.trim() } - .filter { it.isNotEmpty() } - .filter { it != currentPersistentServerName } - .toSet() - - val currentGroupOtherOnlinePlayers = currentGroupName - ?.let { resolveGroupOnlinePlayers(it, currentServerId) } - ?: 0 - val currentMaxPlayers = resolveCurrentMaxPlayers(currentServer, currentGroupName) - val groupOnlinePlayers = additionalGroupNames.sumOf { proxyPlugin.cloudControllerHandler.getOnlinePlayersInGroup(it) } - val groupMaxPlayers = additionalGroupNames.sumOf { proxyPlugin.cloudControllerHandler.getMaxPlayersInGroup(it) } - val persistentServers = proxyPlugin.cloudControllerHandler - .getPersistentServersByNames(additionalPersistentServerNames) - val persistentOnlinePlayers = persistentServers.sumOf { it.playerCount?.toInt() ?: 0 } - val persistentMaxPlayers = persistentServers.sumOf { it.maxPlayers?.toInt() ?: 0 } - - return PlayerCount( - otherOnlinePlayers = currentGroupOtherOnlinePlayers + groupOnlinePlayers + persistentOnlinePlayers, - maxPlayers = currentMaxPlayers + groupMaxPlayers + persistentMaxPlayers - ) - } - - private suspend fun resolveGroupOnlinePlayers(groupName: String, excludedServerId: String?): Int { - return proxyPlugin.cloudControllerHandler - .getServersByGroup(groupName) - .asSequence() - .filter { it.serverId != excludedServerId } - .sumOf { it.playerCount?.toInt() ?: 0 } - } - - private suspend fun resolveCurrentMaxPlayers(currentServer: Server?, currentGroupName: String?): Int { - if (currentServer == null) { - return 0 - } - - if (currentGroupName != null) { - return proxyPlugin.cloudControllerHandler.getMaxPlayersInGroup(currentGroupName) - } - - return currentServer.maxPlayers?.toInt() ?: 0 - } - - private data class PlayerCount( - val otherOnlinePlayers: Int, - val maxPlayers: Int - ) - - private data class PlayerCountSnapshot( - val otherOnlinePlayers: Int, - val maxPlayers: Int? - ) - - private companion object { - const val DISABLED_CHECK_INTERVAL_MILLIS = 1000L - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/ProxyJoinGate.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/ProxyJoinGate.kt deleted file mode 100644 index a20c1a5..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/ProxyJoinGate.kt +++ /dev/null @@ -1,45 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.handler - -import app.simplecloud.plugin.proxy.shared.config.KickMessageConfig -import app.simplecloud.plugin.proxy.shared.config.JoinState -import java.util.logging.Logger - -class ProxyJoinGate( - private val localState: () -> String, - private val resolveJoinState: (String) -> JoinState?, - private val isServerFull: suspend () -> Boolean, - private val kickMessages: () -> KickMessageConfig -) { - private val logger = Logger.getLogger(ProxyJoinGate::class.java.name) - - sealed interface Result { - data object Allowed : Result - data class Denied(val kickMessage: String) : Result - } - - suspend fun evaluate(playerName: String, hasPermission: (String) -> Boolean): Result { - val stateName = localState() - val joinState = resolveJoinState(stateName) - val kick = kickMessages() - - if (joinState == null) { - logger.severe("Neither join state '$stateName' nor default state found. Check configuration!") - return Result.Denied(kick.noJoinState) - } - - if (joinState.permission.join.isNotBlank() && !hasPermission(joinState.permission.join)) { - logger.info("Player $playerName does not have permission to join the proxy. (JoinState: $stateName, Permission: ${joinState.permission.join})") - return Result.Denied(kick.noPermission) - } - - try { - if (isServerFull() && !hasPermission(joinState.permission.full)) { - return Result.Denied(kick.networkFull) - } - } catch (e: Exception) { - logger.severe("Error checking player limits: ${e.message}") - } - - return Result.Allowed - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/CommandSender.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/CommandSender.kt deleted file mode 100644 index 32dd79f..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/CommandSender.kt +++ /dev/null @@ -1,6 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.handler.command - -interface CommandSender { - - fun sendMessage(message: String) -} \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/JoinStateCommandHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/JoinStateCommandHandler.kt deleted file mode 100644 index 32296bc..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/JoinStateCommandHandler.kt +++ /dev/null @@ -1,232 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.handler.command - -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.config.MessageConfig -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.incendo.cloud.CommandManager -import org.incendo.cloud.context.CommandContext -import org.incendo.cloud.parser.standard.IntegerParser -import org.incendo.cloud.parser.standard.StringParser -import org.incendo.cloud.suggestion.Suggestion -import java.util.concurrent.CompletableFuture - -class JoinStateCommandHandler( - val commandManager: CommandManager, - val proxyPlugin: ProxyPlugin -) { - - fun loadCommands() { - loadHelp() - loadInfo() - loadSet() - } - - // โ”€โ”€ help โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - private fun loadHelp() { - val builder = commandManager.commandBuilder("scproxy").literal("joinstate") - commandManager.command( - builder.permission("simplecloud.proxy-essentials.command.joinstate.help") - .handler { context: CommandContext -> handleHelp(context) } - .build() - ) - commandManager.command( - builder.literal("help") - .permission("simplecloud.proxy-essentials.command.joinstate.help") - .handler { context: CommandContext -> handleHelp(context) } - .build() - ) - } - - private fun handleHelp(context: CommandContext) { - val msgs = proxyPlugin.messagesConfiguration.get() - val entry = msgs.command.joinState.help.entry - context.sender().sendMessage(msgs.resolve(msgs.command.joinState.help.header)) - context.sender().sendMessage(entry.replace("", "/scproxy joinstate help")) - context.sender().sendMessage(entry.replace("", "/scproxy joinstate info ")) - context.sender().sendMessage(entry.replace("", "/scproxy joinstate info ")) - context.sender().sendMessage(entry.replace("", "/scproxy joinstate set ")) - context.sender().sendMessage(entry.replace("", "/scproxy joinstate set ")) - context.sender().sendMessage(entry.replace("", " can also be the name of a persistent server")) - } - - // โ”€โ”€ info โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - // /scproxy joinstate info [id] - - private fun loadInfo() { - commandManager.command( - commandManager.commandBuilder("scproxy") - .literal("joinstate") - .literal("info") - .required("group", StringParser.stringParser()) { _, _ -> - runBlocking { - (proxyPlugin.cloudControllerHandler.getAllGroups() + proxyPlugin.cloudControllerHandler.getAllPersistentServerNames()) - .map { Suggestion.suggestion(it) } - .let { CompletableFuture.completedFuture(it) } - } - } - .optional("id", IntegerParser.integerParser(1)) { context, _ -> - runBlocking { - val parts = context.rawInput().input().split(" ").filter { it.isNotBlank() } - val group = parts.getOrNull(3) - ?: return@runBlocking CompletableFuture.completedFuture(emptyList()) - proxyPlugin.cloudControllerHandler.getAllNumericalIdsFromGroup(group) - .map { Suggestion.suggestion(it.toString()) } - .let { CompletableFuture.completedFuture(it) } - } - } - .permission("simplecloud.proxy-essentials.command.joinstate.info") - .handler { context: CommandContext -> - CoroutineScope(Dispatchers.IO).launch { - val group = context.get("group") - val optId = context.optional("id") - val msgs = proxyPlugin.messagesConfiguration.get() - - if (optId.isPresent) { - val id = optId.get() - val state = proxyPlugin.joinStateHandler.getJoinStateAtService(group, id) - context.sender().sendMessage( - msgs.resolve(msgs.command.joinState.list.groups.entry) - .replace("", "$group-$id") - .replace("", state) - ) - } else { - val state = if (proxyPlugin.cloudControllerHandler.groupExists(group)) { - proxyPlugin.joinStateHandler.getJoinStateAtGroup(group) - } else { - proxyPlugin.joinStateHandler.getJoinStateAtPersistentServer(group) - } - context.sender().sendMessage( - msgs.resolve(msgs.command.joinState.list.groups.entry) - .replace("", group) - .replace("", state) - ) - } - } - } - .build() - ) - } - - // โ”€โ”€ set โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - // /scproxy joinstate set [joinstate] - // โ€ข 2 args โ†’ group mode: idOrJoinstate = joinstate name - // โ€ข 3 args โ†’ service mode: idOrJoinstate = numerical id, joinstate = state name - - private fun loadSet() { - commandManager.command( - commandManager.commandBuilder("scproxy") - .literal("joinstate") - .literal("set") - .required("group", StringParser.stringParser()) { _, _ -> - runBlocking { - (proxyPlugin.cloudControllerHandler.getAllGroups() + proxyPlugin.cloudControllerHandler.getAllPersistentServerNames()) - .map { Suggestion.suggestion(it) } - .let { CompletableFuture.completedFuture(it) } - } - } - .required("idOrJoinstate", StringParser.stringParser()) { context, _ -> - val parts = context.rawInput().input().split(" ").filter { it.isNotBlank() } - // if we already have a group token, suggest IDs first, then states - val group = parts.getOrNull(3) - val stateSuggestions = proxyPlugin.proxyEssentialsConfig.get().joinstates - .map { Suggestion.suggestion(it.name) } - if (group == null) { - CompletableFuture.completedFuture(stateSuggestions) - } else { - runBlocking { - val idSuggestions = proxyPlugin.cloudControllerHandler - .getAllNumericalIdsFromGroup(group) - .map { Suggestion.suggestion(it.toString()) } - CompletableFuture.completedFuture(idSuggestions + stateSuggestions) - } - } - } - .optional("joinstate", StringParser.stringParser()) { _, _ -> - val states = proxyPlugin.proxyEssentialsConfig.get().joinstates - .map { Suggestion.suggestion(it.name) } - CompletableFuture.completedFuture(states) - } - .permission("simplecloud.proxy-essentials.command.joinstate.set") - .handler { context: CommandContext -> - CoroutineScope(Dispatchers.IO).launch { - val group = context.get("group") - val idOrJoinstate = context.get("idOrJoinstate") - val optJoinstate = context.optional("joinstate") - val msgs = proxyPlugin.messagesConfiguration.get() - - if (optJoinstate.isPresent) { - // service mode: idOrJoinstate must be a valid integer - val id = idOrJoinstate.toIntOrNull() - if (id == null) { - context.sender().sendMessage(msgs.resolve(msgs.command.joinState.server.updateFailure)) - return@launch - } - val state = optJoinstate.get() - handleSetService(context.sender(), group, id, state, msgs) - } else { - // group mode: idOrJoinstate is the state name - handleSetGroup(context.sender(), group, idOrJoinstate, msgs) - } - } - } - .build() - ) - } - - private suspend fun handleSetGroup(sender: CommandSender, group: String, state: String, msgs: MessageConfig) { - if (proxyPlugin.joinStateResolver.resolveJoinState(state) == null) { - sender.sendMessage(msgs.resolve(msgs.command.joinState.group.updateFailure)) - return - } - if (proxyPlugin.cloudControllerHandler.groupExists(group)) { - if (proxyPlugin.joinStateHandler.getJoinStateAtGroup(group) == state) { - sender.sendMessage(msgs.resolve(msgs.command.joinState.group.updateNoChange)) - return - } - val success = proxyPlugin.joinStateHandler.setJoinStateAtGroupAndAllServicesInGroup(group, state) - sender.sendMessage( - if (success) msgs.resolve(msgs.command.joinState.group.updateSuccess) - else msgs.resolve(msgs.command.joinState.group.updateFailure) - ) - return - } - - if (proxyPlugin.joinStateHandler.getJoinStateAtPersistentServer(group) == state) { - sender.sendMessage(msgs.resolve(msgs.command.joinState.server.updateNoChange)) - return - } - val success = proxyPlugin.joinStateHandler.setJoinStateAtPersistentServer(group, state) - sender.sendMessage( - if (success) msgs.resolve(msgs.command.joinState.server.updateSuccess) - else msgs.resolve(msgs.command.joinState.server.updateFailure) - ) - } - - private suspend fun handleSetService(sender: CommandSender, group: String, id: Int, state: String, msgs: MessageConfig) { - if (proxyPlugin.joinStateResolver.resolveJoinState(state) == null) { - sender.sendMessage(msgs.resolve(msgs.command.joinState.server.updateFailure)) - return - } - if (proxyPlugin.joinStateHandler.getJoinStateAtService(group, id) == state) { - sender.sendMessage(msgs.resolve(msgs.command.joinState.server.updateNoChange)) - return - } - val success = proxyPlugin.joinStateHandler.setJoinStateAtService(group, id, state) - if (success) { - val currentServer = proxyPlugin.cloudControllerHandler.currentServer - if (currentServer != null && currentServer.isFromGroup) { - val currentGroup = currentServer.group?.name - if (currentGroup == group && currentServer.numericalId == id) { - proxyPlugin.joinStateHandler.localState = state - } - } - sender.sendMessage(msgs.resolve(msgs.command.joinState.server.updateSuccess)) - } else { - sender.sendMessage(msgs.resolve(msgs.command.joinState.server.updateFailure)) - } - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/LayoutCommandHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/LayoutCommandHandler.kt deleted file mode 100644 index a8a019c..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/LayoutCommandHandler.kt +++ /dev/null @@ -1,168 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.handler.command - -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.handler.MotdLayoutHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.incendo.cloud.CommandManager -import org.incendo.cloud.context.CommandContext -import org.incendo.cloud.parser.standard.StringParser -import org.incendo.cloud.suggestion.Suggestion -import java.util.concurrent.CompletableFuture - -class LayoutCommandHandler( - private val commandManager: CommandManager, - private val proxyPlugin: ProxyPlugin -) { - - companion object { - const val MOTD_LAYOUT_KEY = MotdLayoutHandler.MOTD_LAYOUT_KEY - } - - fun loadCommands() { - loadHelp() - loadInfoDefault() - loadInfoGroup() - loadSet() - } - - private fun loadHelp() { - commandManager.command( - commandManager.commandBuilder("scproxy") - .literal("layout") - .permission("simplecloud.proxy-essentials.command.layout.help") - .handler { context: CommandContext -> handleHelp(context) } - .build() - ) - commandManager.command( - commandManager.commandBuilder("scproxy") - .literal("layout") - .literal("help") - .permission("simplecloud.proxy-essentials.command.layout.help") - .handler { context: CommandContext -> handleHelp(context) } - .build() - ) - } - - private fun handleHelp(context: CommandContext) { - val msgs = proxyPlugin.messagesConfiguration.get() - val entry = msgs.command.layout.help.entry - context.sender().sendMessage(msgs.resolve(msgs.command.layout.help.header)) - context.sender().sendMessage(entry.replace("", "/scproxy layout help")) - context.sender().sendMessage(entry.replace("", "/scproxy layout info [group]")) - context.sender().sendMessage(entry.replace("", "/scproxy layout set ")) - } - - private fun loadInfoDefault() { - commandManager.command( - commandManager.commandBuilder("scproxy") - .literal("layout") - .literal("info") - .permission("simplecloud.proxy-essentials.command.layout.info") - .handler { context: CommandContext -> - val config = proxyPlugin.proxyEssentialsConfig.get() - val msgs = proxyPlugin.messagesConfiguration.get() - val layoutName = config.initialLayout - context.sender().sendMessage( - msgs.resolve(msgs.command.layout.info.header).replace("", "default") - ) - context.sender().sendMessage( - msgs.resolve(msgs.command.layout.info.entry).replace("", layoutName) - ) - } - .build() - ) - } - - private fun loadInfoGroup() { - commandManager.command( - commandManager.commandBuilder("scproxy") - .literal("layout") - .literal("info") - .required("group", StringParser.stringParser()) { _, _ -> - runBlocking { - val groups = proxyPlugin.cloudControllerHandler.getAllGroups() - .map { Suggestion.suggestion(it) } - CompletableFuture.completedFuture(groups) - } - } - .permission("simplecloud.proxy-essentials.command.layout.info") - .handler { context: CommandContext -> - CoroutineScope(Dispatchers.IO).launch { - val group = context.get("group") - val msgs = proxyPlugin.messagesConfiguration.get() - - val layoutName = proxyPlugin.cloudControllerHandler.getGroupProperty(group, MOTD_LAYOUT_KEY) - ?: proxyPlugin.proxyEssentialsConfig.get().let { config -> - val stateName = proxyPlugin.joinStateHandler.getJoinStateAtGroup(group) - config.joinstates.find { it.name == stateName }?.forcedMotdLayout - ?: config.initialLayout - } - - context.sender().sendMessage( - msgs.resolve(msgs.command.layout.info.header).replace("", group) - ) - context.sender().sendMessage( - msgs.resolve(msgs.command.layout.info.entry).replace("", layoutName) - ) - } - } - .build() - ) - } - - private fun loadSet() { - commandManager.command( - commandManager.commandBuilder("scproxy") - .literal("layout") - .literal("set") - .required("group", StringParser.stringParser()) { _, _ -> - runBlocking { - val groups = proxyPlugin.cloudControllerHandler.getAllGroups() - .map { Suggestion.suggestion(it) } - CompletableFuture.completedFuture(groups) - } - } - .required("layout", StringParser.stringParser()) { _, _ -> - val layouts = proxyPlugin.motdLayoutHandler.getAvailableLayouts() - .map { Suggestion.suggestion(it) } - CompletableFuture.completedFuture(layouts) - } - .permission("simplecloud.proxy-essentials.command.layout.set") - .handler { context: CommandContext -> - CoroutineScope(Dispatchers.IO).launch { - val group = context.get("group") - val layout = context.get("layout") - val msgs = proxyPlugin.messagesConfiguration.get() - val availableLayouts = proxyPlugin.motdLayoutHandler.getAvailableLayouts() - - if (layout !in availableLayouts) { - context.sender().sendMessage(msgs.resolve(msgs.command.layout.set.updateFailure)) - return@launch - } - - val groupUpdated = proxyPlugin.cloudControllerHandler.updateGroupProperty(group, MOTD_LAYOUT_KEY, layout) - val servicesUpdated = proxyPlugin.cloudControllerHandler.updateServicePropertyOnAllGroupServers( - group, - MOTD_LAYOUT_KEY, - layout - ) - val success = groupUpdated && servicesUpdated - - if (success) { - val currentServer = proxyPlugin.cloudControllerHandler.currentServer - if (currentServer?.isFromGroup == true && currentServer.group?.name == group) { - proxyPlugin.motdLayoutHandler.setLocalLayout(layout) - } - context.sender().sendMessage(msgs.resolve(msgs.command.layout.set.updateSuccess)) - } else { - context.sender().sendMessage(msgs.resolve(msgs.command.layout.set.updateFailure)) - } - } - } - .build() - ) - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/ProxyEssentialsCommandHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/ProxyEssentialsCommandHandler.kt deleted file mode 100644 index 44e08a0..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/ProxyEssentialsCommandHandler.kt +++ /dev/null @@ -1,86 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.handler.command - -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import org.incendo.cloud.CommandManager -import org.incendo.cloud.context.CommandContext -import java.util.logging.Level -import java.util.logging.Logger - -class ProxyEssentialsCommandHandler( - private val commandManager: CommandManager, - private val proxyPlugin: ProxyPlugin -) { - private val logger = Logger.getLogger(ProxyEssentialsCommandHandler::class.java.name) - - fun loadCommands() { - loadHelp() - loadReload() - } - - private fun loadHelp() { - commandManager.command( - commandManager.commandBuilder("scproxy") - .permission("simplecloud.proxy-essentials.command.help") - .handler { context: CommandContext -> sendHelp(context) } - .build() - ) - commandManager.command( - commandManager.commandBuilder("scproxy") - .literal("help") - .permission("simplecloud.proxy-essentials.command.help") - .handler { context: CommandContext -> sendHelp(context) } - .build() - ) - } - - private fun sendHelp(context: CommandContext) { - val sender = context.sender() - val msgs = proxyPlugin.messagesConfiguration.get() - val helpEntry = msgs.command.joinState.help.entry - sender.sendMessage(msgs.resolve("Available /scproxy commands:")) - sender.sendMessage(helpEntry.replace("", "/scproxy help")) - sender.sendMessage(helpEntry.replace("", "/scproxy reload")) - sender.sendMessage(helpEntry.replace("", "/scproxy joinstate help")) - sender.sendMessage(helpEntry.replace("", "/scproxy joinstate info ")) - sender.sendMessage(helpEntry.replace("", "/scproxy joinstate info ")) - sender.sendMessage(helpEntry.replace("", "/scproxy joinstate set ")) - sender.sendMessage(helpEntry.replace("", "/scproxy joinstate set ")) - sender.sendMessage(helpEntry.replace("", "/scproxy layout help")) - sender.sendMessage(helpEntry.replace("", "/scproxy layout info [group]")) - sender.sendMessage(helpEntry.replace("", "/scproxy layout set ")) - } - - private fun loadReload() { - commandManager.command( - commandManager.commandBuilder("scproxy") - .literal("reload") - .permission("simplecloud.proxy-essentials.command.reload") - .handler { context: CommandContext -> reload(context.sender()) } - .build() - ) - } - - private fun reload(sender: C) { - val messagesBeforeReload = proxyPlugin.messagesConfiguration.get() - sender.sendMessage(messagesBeforeReload.resolve(messagesBeforeReload.command.reload.start)) - - CoroutineScope(Dispatchers.IO).launch { - try { - proxyPlugin.reload() - - val messages = proxyPlugin.messagesConfiguration.get() - sender.sendMessage(messages.resolve(messages.command.reload.success)) - } catch (e: Exception) { - val messages = proxyPlugin.messagesConfiguration.get() - sender.sendMessage( - messages.resolve(messages.command.reload.failure) - .replace("", e.message ?: "Unknown error") - ) - logger.log(Level.SEVERE, "Error reloading configuration", e) - } - } - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateResolver.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateResolver.kt new file mode 100644 index 0000000..30a59e2 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateResolver.kt @@ -0,0 +1,79 @@ +package app.simplecloud.plugin.proxy.shared.joinstate + +import app.simplecloud.plugin.api.shared.pattern.ServerPatternIdentifier +import app.simplecloud.plugin.proxy.shared.ProxyPlugin +import app.simplecloud.plugin.proxy.shared.config.JoinState +import java.util.logging.Logger + +class JoinStateResolver( + private val plugin: ProxyPlugin +) { + + private val logger = Logger.getLogger(JoinStateResolver::class.java.name) + + private val identifier by lazy { + ServerPatternIdentifier( + "-", + "(?[a-zA-Z0-9_-]+)-(?\\d+)", + cloudApi = plugin.api + ) + } + + fun resolveJoinState(stateName: String): JoinState? { + val config = plugin.config.get() + val joinState = config.joinstates.find { it.name == stateName } + if (joinState != null) { + return joinState + } + + logger.info("Join state '$stateName' not found. Using default '${config.initialState}'.") + return config.joinstates.find { it.name == config.initialState } + } + + suspend fun getJoinStateForServer(serverName: String): String { + val serviceJoinState = getJoinStateOfService(serverName) + if (serviceJoinState != null) { + return serviceJoinState + } + + val server = plugin.cloudControllerHandler.getServerByName(serverName) + ?: return plugin.config.get().initialState + + val joinState = server.properties?.get(JoinStateService.KEY)?.toString() + if (!joinState.isNullOrEmpty()) { + return joinState + } + + val persistentServerName = server.persistentServer?.name + ?: return plugin.config.get().initialState + + return plugin.joinStateService.getJoinStateAtPersistentServer(persistentServerName) + } + + private suspend fun getJoinStateOfService(serverName: String): String? { + val (groupName, numericalId) = try { + identifier.parse(serverName) + } catch (_: IllegalArgumentException) { + return null + } + + return plugin.joinStateService.getJoinStateAtService(groupName, numericalId) + } + + suspend fun isServerFull(): Boolean { + val server = plugin.cloudControllerHandler.currentServer ?: return false + + if (!server.isFromGroup) { + val maxPlayers = server.maxPlayers ?: return false + return (server.playerCount ?: 0) >= maxPlayers + } + + val groupName = server.group?.name ?: return false + val maxPlayers = plugin.cloudControllerHandler.getMaxPlayersInGroup(groupName) + if (maxPlayers <= 0) { + return false + } + + return plugin.cloudControllerHandler.getOnlinePlayersInGroup(groupName) >= maxPlayers + } +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateService.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateService.kt new file mode 100644 index 0000000..4b182ee --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateService.kt @@ -0,0 +1,221 @@ +package app.simplecloud.plugin.proxy.shared.joinstate + +import app.simplecloud.api.runtime.SimpleCloudRuntime +import app.simplecloud.api.server.Server +import app.simplecloud.plugin.proxy.shared.ProxyPlugin +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import java.util.logging.Logger +import kotlin.time.Duration.Companion.milliseconds + +class JoinStateService( + private val plugin: ProxyPlugin +) { + + private val logger = Logger.getLogger(JoinStateService::class.java.name) + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private var syncJob: Job? = null + + @Volatile + private var lastObservedGroupState: String? = null + + @Volatile + var localState: String = plugin.config.get().initialState + + fun stop() { + syncJob?.cancel() + scope.cancel() + } + + suspend fun getJoinStateAtGroup(groupName: String): String { + return controller().getGroupProperty(groupName, KEY) ?: defaultJoinState() + } + + suspend fun setJoinStateAtGroup(groupName: String, joinStateName: String): Boolean { + val updated = controller().updateGroupProperty(groupName, KEY, joinStateName) + if (updated) { + lastObservedGroupState = joinStateName + } + return updated + } + + suspend fun ensureJoinStateAtGroup(groupName: String): String { + val existingJoinState = controller().getGroupProperty(groupName, KEY) + if (!existingJoinState.isNullOrBlank()) { + return existingJoinState + } + + val fallbackState = defaultJoinState() + if (!setJoinStateAtGroup(groupName, fallbackState)) { + logger.warning("Could not persist default join state '$fallbackState' for group '$groupName'") + } + return fallbackState + } + + suspend fun getJoinStateAtService(groupName: String, numericalId: Int): String { + return controller().getServiceProperty(groupName, numericalId, KEY) + ?: controller().getGroupProperty(groupName, KEY) + ?: defaultJoinState() + } + + suspend fun setJoinStateAtService(groupName: String, numericalId: Int, joinStateName: String): Boolean { + return controller().updateServiceProperty(groupName, numericalId, KEY, joinStateName) + } + + suspend fun getJoinStateAtPersistentServer(serverName: String): String { + return controller().getPersistentServerProperty(serverName, KEY) ?: defaultJoinState() + } + + suspend fun ensureJoinStateAtPersistentServer(serverName: String): String { + val existingJoinState = controller().getPersistentServerProperty(serverName, KEY) + if (!existingJoinState.isNullOrBlank()) { + return existingJoinState + } + + val fallbackState = defaultJoinState() + if (!setJoinStateAtPersistentServer(serverName, fallbackState)) { + logger.warning("Could not persist default join state '$fallbackState' for persistent server '$serverName'") + } + return fallbackState + } + + suspend fun setJoinStateAtPersistentServer(serverName: String, joinStateName: String): Boolean { + val updated = controller().updatePersistentServerProperty(serverName, KEY, joinStateName) + if (updated) { + syncLocalStateWithPersistentServer(serverName, joinStateName) + } + return updated + } + + suspend fun setJoinStateAtGroupAndAllServicesInGroup(groupName: String, joinStateName: String): Boolean { + val groupPropertyUpdated = setJoinStateAtGroup(groupName, joinStateName) + val servicePropertiesUpdated = + controller().updateServicePropertyOnAllGroupServers(groupName, KEY, joinStateName) + + val successful = groupPropertyUpdated && servicePropertiesUpdated + if (!successful) { + logger.severe("Error setting join state at group and all services in group $groupName.") + } + return successful + } + + fun startGroupStateSyncTask() { + if (syncJob?.isActive == true) { + return + } + + syncJob = scope.launch { + while (isActive) { + try { + syncLocalStateWithGroupState() + } catch (e: Exception) { + logger.severe("Error while syncing local/group join state: ${e.message}") + } + delay(2000.milliseconds) + } + } + } + + fun registerListener() { + plugin.api.event().server().onUpdated { event -> + if (event.serverId != SimpleCloudRuntime.serverId()) return@onUpdated + + val server = event.server + val state = server.properties?.get(KEY)?.toString() + + when { + state.isNullOrBlank() -> { + logger.warning("No join state found for server. Using default join state.") + scope.launch { applyDefaultJoinStateForCurrentServer(server) } + } + + state != localState -> { + localState = state + logger.info("Join state changed to $state") + } + } + } + } + + private fun controller() = plugin.cloudControllerHandler + + private fun defaultJoinState(): String = plugin.config.get().initialState + + private suspend fun syncLocalStateWithPersistentServer(serverName: String, joinStateName: String) { + val currentServer = controller().currentServer ?: return + if (currentServer.isFromGroup || currentServer.persistentServer?.name != serverName) { + return + } + + val synchronized = controller().updateServerProperty(currentServer.serverId, KEY, joinStateName) + if (synchronized) { + localState = joinStateName + } + } + + private suspend fun applyDefaultJoinStateForCurrentServer(server: Server) { + try { + val persistentServerName = server.persistentServer?.name + val stateToApply = when { + server.isFromGroup -> ensureJoinStateAtGroup(server.group?.name ?: return) + persistentServerName != null -> ensureJoinStateAtPersistentServer(persistentServerName) + else -> defaultJoinState() + } + + controller().updateServerProperty(server.serverId, KEY, stateToApply) + localState = stateToApply + } catch (e: Exception) { + logger.severe("Error setting default join state: ${e.message}") + } + } + + private suspend fun syncLocalStateWithGroupState() { + val server = controller().currentServer ?: return + if (!server.isFromGroup) return + + val groupName = server.group?.name ?: return + val numericalId = server.numericalId + + val groupState = controller().getGroupProperty(groupName, KEY) + if (groupState.isNullOrBlank()) return + + val previousGroupState = lastObservedGroupState + if (previousGroupState == null) { + lastObservedGroupState = groupState + return + } + + val serviceState = controller().getServiceProperty(groupName, numericalId, KEY) + if (serviceState.isNullOrBlank()) { + applyGroupState(server.serverId, "$groupName-$numericalId", groupState, "service state was missing") + return + } + + if (groupState == previousGroupState) return + lastObservedGroupState = groupState + + if (serviceState != previousGroupState) return + applyGroupState(server.serverId, "$groupName-$numericalId", groupState, "of group state change") + } + + private suspend fun applyGroupState(serverId: String, serviceName: String, groupState: String, reason: String) { + if (!controller().updateServerProperty(serverId, KEY, groupState)) { + logger.warning("Could not synchronize join state for '$serviceName' with group state '$groupState'") + return + } + + localState = groupState + logger.info("Join state changed to $groupState because $reason.") + } + + companion object { + const val KEY = "joinstate" + } + +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/ProxyJoinGate.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/ProxyJoinGate.kt new file mode 100644 index 0000000..597489b --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/ProxyJoinGate.kt @@ -0,0 +1,48 @@ +package app.simplecloud.plugin.proxy.shared.joinstate + +import app.simplecloud.plugin.proxy.shared.ProxyPlugin +import java.util.logging.Logger + +class ProxyJoinGate( + private val plugin: ProxyPlugin +) { + + private val logger = Logger.getLogger(ProxyJoinGate::class.java.name) + + sealed interface Result { + data object Allowed : Result + data class Denied(val kickMessage: String) : Result + } + + suspend fun evaluate(playerName: String, hasPermission: (String) -> Boolean): Result { + val stateName = plugin.joinStateService.localState + val kickMessages = plugin.messageConfig.get().kick + val joinState = plugin.joinStateResolver.resolveJoinState(stateName) + + if (joinState == null) { + logger.severe("Neither join state '$stateName' nor default state found. Check configuration!") + return Result.Denied(kickMessages.noJoinState) + } + + val joinPermission = joinState.permission.join + if (joinPermission.isNotBlank() && !hasPermission(joinPermission)) { + logger.info("Player $playerName does not have permission to join the proxy. (JoinState: $stateName, Permission: $joinPermission)") + return Result.Denied(kickMessages.noPermission) + } + + if (isNetworkFull(hasPermission, joinState.permission.full)) { + return Result.Denied(kickMessages.networkFull) + } + + return Result.Allowed + } + + private suspend fun isNetworkFull(hasPermission: (String) -> Boolean, fullPermission: String): Boolean { + return try { + plugin.joinStateResolver.isServerFull() && !hasPermission(fullPermission) + } catch (e: Exception) { + logger.severe("Error checking player limits: ${e.message}") + false + } + } +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/DomainMotdHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/DomainMotdResolver.kt similarity index 50% rename from proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/DomainMotdHandler.kt rename to proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/DomainMotdResolver.kt index f77955b..a2c3e0c 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/DomainMotdHandler.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/DomainMotdResolver.kt @@ -1,16 +1,24 @@ -package app.simplecloud.plugin.proxy.shared.handler +package app.simplecloud.plugin.proxy.shared.motd import app.simplecloud.plugin.proxy.shared.ProxyPlugin import app.simplecloud.plugin.proxy.shared.config.DomainMotdRoute -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import java.util.concurrent.ConcurrentHashMap import java.util.logging.Logger import kotlin.time.Duration.Companion.milliseconds -class DomainMotdHandler( - private val proxyPlugin: ProxyPlugin +class DomainMotdResolver( + private val plugin: ProxyPlugin ) { - private val logger = Logger.getLogger(DomainMotdHandler::class.java.name) + + private val logger = Logger.getLogger(DomainMotdResolver::class.java.name) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private var syncJob: Job? = null @@ -34,9 +42,9 @@ class DomainMotdHandler( } fun registerListener() { - proxyPlugin.api.event().server().onUpdated { event -> + plugin.api.event().server().onUpdated { event -> val server = event.server - val matchingRoutes = proxyPlugin.proxyEssentialsConfig.get().domains.filter { route -> + val matchingRoutes = plugin.config.get().domains.filter { route -> route.target.equals(server.group?.name, ignoreCase = true) || route.target.equals(server.persistentServer?.name, ignoreCase = true) } @@ -55,43 +63,46 @@ class DomainMotdHandler( } private suspend fun syncAllRoutes() { - val routes = proxyPlugin.proxyEssentialsConfig.get().domains - val configuredDomains = routes.map { normalize(it.domain) }.toSet() - resolvedLayouts.keys.retainAll(configuredDomains) + val routes = plugin.config.get().domains + resolvedLayouts.keys.retainAll(routes.map { normalize(it.domain) }.toSet()) routes.forEach { refreshRoute(it) } } private suspend fun refreshRoute(route: DomainMotdRoute) { val domainKey = normalize(route.domain) - val layoutName = resolveRoute(route) - - if (layoutName != null) resolvedLayouts[domainKey] = layoutName - else resolvedLayouts.remove(domainKey) + when (val layoutName = resolveRoute(route)) { + null -> resolvedLayouts.remove(domainKey) + else -> resolvedLayouts[domainKey] = layoutName + } } private suspend fun resolveRoute(route: DomainMotdRoute): String? { - val target = route.target - val state = if (proxyPlugin.cloudControllerHandler.groupExists(target)) { - proxyPlugin.joinStateHandler.getJoinStateAtGroup(target) - } else { - proxyPlugin.joinStateHandler.getJoinStateAtPersistentServer(target) - } - val ruleLayout = route.rules.find { it.state == state }?.layout - ?.takeIf { hasLayout(it) } + val config = plugin.config.get() + val state = getJoinStateOfTarget(route.target) + val ruleLayout = getExistingLayout(route.rules.find { it.state == state }?.layout) if (ruleLayout != null) return ruleLayout - val config = proxyPlugin.proxyEssentialsConfig.get() - val globalLayout = config.joinstates.find { it.name == state }?.forcedMotdLayout - ?.takeIf { hasLayout(it) } + val forcedLayout = getExistingLayout(config.joinstates.find { it.name == state }?.forcedMotdLayout) + if (forcedLayout != null) return forcedLayout - if (globalLayout != null) return globalLayout + return getExistingLayout(config.initialLayout) + } - return config.initialLayout.takeIf { hasLayout(it) } + private suspend fun getJoinStateOfTarget(target: String): String { + return when { + plugin.cloudControllerHandler.groupExists(target) -> plugin.joinStateService.getJoinStateAtGroup(target) + else -> plugin.joinStateService.getJoinStateAtPersistentServer(target) + } } - private fun hasLayout(name: String): Boolean = proxyPlugin.motdLayoutHandler.getLayoutByName(name) != null + private fun getExistingLayout(name: String?): String? { + if (name == null || plugin.layoutRepository.getLayoutByName(name) == null) { + return null + } + return name + } private fun normalize(domain: String): String = domain.trim().lowercase() } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/MotdLayoutHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/MotdLayoutRepository.kt similarity index 59% rename from proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/MotdLayoutHandler.kt rename to proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/MotdLayoutRepository.kt index 8d050f9..4cdddeb 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/MotdLayoutHandler.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/MotdLayoutRepository.kt @@ -1,4 +1,4 @@ -package app.simplecloud.plugin.proxy.shared.handler +package app.simplecloud.plugin.proxy.shared.motd import app.simplecloud.plugin.api.shared.config.YamlDirectoryRepository import app.simplecloud.plugin.proxy.shared.ProxyPlugin @@ -13,14 +13,15 @@ import java.util.concurrent.atomic.AtomicInteger import java.util.logging.Level import java.util.logging.Logger -class MotdLayoutHandler( +class MotdLayoutRepository( private val directory: Path, - private val proxyPlugin: ProxyPlugin + private val plugin: ProxyPlugin ) : YamlDirectoryRepository( directory, MotdLayoutConfiguration::class.java, ) { - private val logger = Logger.getLogger(MotdLayoutHandler::class.java.name) + + private val logger = Logger.getLogger(MotdLayoutRepository::class.java.name) private val layoutsByName = ConcurrentHashMap() private val queueIndexes = ConcurrentHashMap() private val listenerRegistered = AtomicBoolean(false) @@ -28,19 +29,16 @@ class MotdLayoutHandler( @Volatile private var localLayout: String? = null - companion object { - const val MOTD_LAYOUT_KEY = "motd-layout" - private const val LAYOUT_FILE_SUFFIX = ".yml" - } - override fun save(entity: MotdLayoutConfiguration) { - val name = entity.motd.layouts.firstOrNull()?.name ?: proxyPlugin.proxyEssentialsConfig.get().initialLayout - save("$name$LAYOUT_FILE_SUFFIX", entity) + val name = entity.motd.layouts.firstOrNull()?.name ?: plugin.config.get().initialLayout + save("$name$SUFFIX", entity) layoutsByName[name] = entity } override fun find(identifier: String): MotdLayoutConfiguration? = layoutsByName[identifier] + fun getLayoutByName(name: String): MotdLayoutConfiguration? = find(name) + fun loadMotdLayouts() { if (!Files.isDirectory(directory)) { layoutsByName.clear() @@ -48,88 +46,93 @@ class MotdLayoutHandler( } val loaded = mutableMapOf() - Files.list(directory).use { paths -> - paths - .filter { Files.isRegularFile(it) && it.fileName.toString().endsWith(LAYOUT_FILE_SUFFIX) } - .forEach { path -> - val layout = loadLayout(path) ?: return@forEach - loaded[path.fileName.toString().removeSuffix(LAYOUT_FILE_SUFFIX)] = layout - } + getLayoutFileNames().forEach { name -> + val layout = loadLayout(name) + if (layout != null) { + loaded[name] = layout + } } layoutsByName.keys.retainAll(loaded.keys) layoutsByName.putAll(loaded) } - fun getLayoutByName(name: String): MotdLayoutConfiguration? = find(name) + fun getAvailableLayouts(): List = getLayoutFileNames().sorted() fun getCurrentMotdLayout(): MotdLayoutConfiguration { - val layoutName = localLayout?.takeIf(::hasLayout) ?: fallbackLayoutName() + val currentLayout = localLayout + val layoutName = when { + currentLayout != null && layoutsByName.containsKey(currentLayout) -> currentLayout + else -> getFallbackLayoutName() + } return find(layoutName) ?: MotdLayoutConfiguration() } fun setLocalLayout(layoutName: String?, logChange: Boolean = false) { - val nextLayout = layoutName?.takeIf { it.isNotBlank() } + val nextLayout = when { + layoutName.isNullOrBlank() -> null + else -> layoutName + } if (localLayout == nextLayout) return localLayout = nextLayout if (logChange) { - logger.info("MOTD layout changed to ${nextLayout ?: fallbackLayoutName()}") + logger.info("MOTD layout changed to ${nextLayout ?: getFallbackLayoutName()}") } } fun registerListener() { if (!listenerRegistered.compareAndSet(false, true)) return - proxyPlugin.api.event().server().onUpdated { event -> - val currentServer = proxyPlugin.cloudControllerHandler.currentServer ?: return@onUpdated + plugin.api.event().server().onUpdated { event -> + val currentServer = plugin.cloudControllerHandler.currentServer ?: return@onUpdated if (event.serverId != currentServer.serverId) return@onUpdated - val layout = event.server.properties?.get(MOTD_LAYOUT_KEY)?.toString() - setLocalLayout(layout, logChange = true) + setLocalLayout(event.server.properties?.get(KEY)?.toString(), logChange = true) } } fun selectEntry(layout: MotdLayoutConfiguration, layoutKey: String): MotdEntry? { val entries = layout.motd.layouts if (entries.isEmpty()) return null + return when (layout.motd.updateType) { MotdUpdateType.RANDOM -> entries.random() MotdUpdateType.QUEUE -> { val counter = queueIndexes.getOrPut(layoutKey) { AtomicInteger(0) } - val idx = counter.getAndIncrement() % entries.size - entries[idx] + entries[counter.getAndIncrement() % entries.size] } } } - fun getAvailableLayouts(): List { + private fun getLayoutFileNames(): List { if (!Files.isDirectory(directory)) return emptyList() return Files.list(directory).use { paths -> paths - .filter { Files.isRegularFile(it) && it.fileName.toString().endsWith(LAYOUT_FILE_SUFFIX) } - .map { it.fileName.toString().removeSuffix(LAYOUT_FILE_SUFFIX) } - .sorted() + .filter { Files.isRegularFile(it) && it.fileName.toString().endsWith(SUFFIX) } + .map { it.fileName.toString().removeSuffix(SUFFIX) } .toList() } } - private fun loadLayout(path: Path): MotdLayoutConfiguration? { + private fun loadLayout(name: String): MotdLayoutConfiguration? { return try { - load(path.toFile()) + load(directory.resolve("$name$SUFFIX").toFile()) } catch (e: Exception) { - logger.log(Level.WARNING, "Could not load MOTD layout ${path.fileName}", e) + logger.log(Level.WARNING, "Could not load MOTD layout $name$SUFFIX", e) null } } - private fun fallbackLayoutName(): String { - val localState = proxyPlugin.joinStateHandler.localState - val config = proxyPlugin.proxyEssentialsConfig.get() - val joinState = config.joinstates.find { it.name == localState } + private fun getFallbackLayoutName(): String { + val config = plugin.config.get() + val joinState = config.joinstates.find { it.name == plugin.joinStateService.localState } return joinState?.forcedMotdLayout ?: config.initialLayout } - private fun hasLayout(layoutName: String): Boolean = layoutsByName.containsKey(layoutName) + companion object { + const val KEY = "motd-layout" + private const val SUFFIX = ".yml" + } } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/ServerIconLoader.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/ServerIconLoader.kt similarity index 64% rename from proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/ServerIconLoader.kt rename to proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/ServerIconLoader.kt index 111a3d6..aa4e4cd 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/ServerIconLoader.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/ServerIconLoader.kt @@ -1,4 +1,4 @@ -package app.simplecloud.plugin.proxy.shared.utilities +package app.simplecloud.plugin.proxy.shared.motd import java.awt.RenderingHints import java.awt.image.BufferedImage @@ -17,19 +17,22 @@ class ServerIconLoader( private val cache = ConcurrentHashMap>() private val warnedInvalidNames = ConcurrentHashMap.newKeySet() + private companion object { + const val FAVICON_SIZE = 64 + } + init { Files.createDirectories(this.directory) } fun get(fileName: String, warn: (String) -> Unit): T? { val iconPath = resolveIconPath(fileName, warn) ?: return null - val snapshot = IconSnapshot( - path = iconPath, - lastModified = if (Files.exists(iconPath)) Files.getLastModifiedTime(iconPath).toMillis() else -1, - length = if (Files.exists(iconPath)) Files.size(iconPath) else -1 - ) + val snapshot = snapshotOf(iconPath) - cache[iconPath]?.takeIf { it.snapshot == snapshot }?.let { return it.icon } + val cached = cache[iconPath] + if (cached != null && cached.snapshot == snapshot) { + return cached.icon + } if (!Files.isRegularFile(iconPath)) { cache[iconPath] = CacheEntry(snapshot, null) @@ -37,15 +40,28 @@ class ServerIconLoader( return null } - return runCatching { + return try { val image = ImageIO.read(iconPath.toFile()) ?: error("Unsupported image format") - val favicon = createIcon(image.toFaviconImage()) - cache[iconPath] = CacheEntry(snapshot, favicon) - favicon - }.onFailure { + val icon = createIcon(image.toFaviconImage()) + cache[iconPath] = CacheEntry(snapshot, icon) + icon + } catch (e: Exception) { cache[iconPath] = CacheEntry(snapshot, null) - warn("Failed to load server icon ${iconPath.fileName}: ${it.message}") - }.getOrNull() + warn("Failed to load server icon ${iconPath.fileName}: ${e.message}") + null + } + } + + private fun snapshotOf(iconPath: Path): IconSnapshot { + if (!Files.exists(iconPath)) { + return IconSnapshot(iconPath, lastModified = -1, length = -1) + } + + return IconSnapshot( + path = iconPath, + lastModified = Files.getLastModifiedTime(iconPath).toMillis(), + length = Files.size(iconPath) + ) } private fun resolveIconPath(fileName: String, warn: (String) -> Unit): Path? { @@ -75,17 +91,17 @@ class ServerIconLoader( val targetX = (FAVICON_SIZE - targetWidth) / 2 val targetY = (FAVICON_SIZE - targetHeight) / 2 - return BufferedImage(FAVICON_SIZE, FAVICON_SIZE, BufferedImage.TYPE_INT_ARGB).also { favicon -> - val graphics = favicon.createGraphics() - try { - graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC) - graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY) - graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) - graphics.drawImage(this, targetX, targetY, targetWidth, targetHeight, null) - } finally { - graphics.dispose() - } + val favicon = BufferedImage(FAVICON_SIZE, FAVICON_SIZE, BufferedImage.TYPE_INT_ARGB) + val graphics = favicon.createGraphics() + try { + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC) + graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY) + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + graphics.drawImage(this, targetX, targetY, targetWidth, targetHeight, null) + } finally { + graphics.dispose() } + return favicon } private data class IconSnapshot( @@ -98,8 +114,4 @@ class ServerIconLoader( val snapshot: IconSnapshot, val icon: T? ) - - private companion object { - const val FAVICON_SIZE = 64 - } -} \ No newline at end of file +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/resolver/TagResolverHelper.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/placeholder/TagResolverHelper.kt similarity index 65% rename from proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/resolver/TagResolverHelper.kt rename to proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/placeholder/TagResolverHelper.kt index 780aaa7..bf9056a 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/resolver/TagResolverHelper.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/placeholder/TagResolverHelper.kt @@ -1,18 +1,14 @@ -package app.simplecloud.plugin.proxy.shared.resolver +package app.simplecloud.plugin.proxy.shared.placeholder -import app.simplecloud.plugin.proxy.shared.config.MaxPlayerDisplayType import app.simplecloud.plugin.proxy.shared.config.MotdLayoutConfiguration import app.simplecloud.plugin.proxy.shared.config.PingColor -import net.kyori.adventure.text.minimessage.Context import net.kyori.adventure.text.minimessage.tag.Tag -import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue import net.kyori.adventure.text.minimessage.tag.resolver.Formatter import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver import java.time.LocalDateTime import java.time.ZoneId - object TagResolverHelper { fun getDefaultTagResolvers( @@ -39,12 +35,16 @@ object TagResolverHelper { .maxByOrNull { it.ping } ?.color ?: "" + return Placeholder.parsed(TagResolverNames.PING, "$pingColor$ping") } fun getDateTagResolver(): TagResolver { - val now = LocalDateTime.now(ZoneId.systemDefault()) - return Formatter.date(TagResolverNames.DATE, now) + return Formatter.date(TagResolverNames.DATE, LocalDateTime.now(ZoneId.systemDefault())) + } + + fun getOnlinePlayersTagResolver(onlinePlayers: Int): TagResolver { + return Placeholder.unparsed(TagResolverNames.ONLINE_PLAYERS, onlinePlayers.toString()) } fun getMaxPlayersTagResolver( @@ -52,30 +52,16 @@ object TagResolverHelper { realMaxPlayers: Int, motdConfiguration: MotdLayoutConfiguration ): TagResolver { - val slots = motdConfiguration.version.slots - val maxPlayers = if (!slots.enabled) realMaxPlayers else when (slots.type) { - MaxPlayerDisplayType.REAL -> realMaxPlayers - MaxPlayerDisplayType.FAKE -> slots.fakeSlots - MaxPlayerDisplayType.DYNAMIC -> onlinePlayers + slots.dynamicPlayerRange - } + val maxPlayers = motdConfiguration.version.slots.resolveMaxPlayers(onlinePlayers, realMaxPlayers) return Placeholder.unparsed(TagResolverNames.MAX_PLAYERS, maxPlayers.toString()) } - fun getOnlinePlayersTagResolver(onlinePlayers: Int): TagResolver { - return Placeholder.unparsed(TagResolverNames.ONLINE_PLAYERS, onlinePlayers.toString()) - } - fun getEnvTagResolver(): TagResolver { - return TagResolver.resolver( - TagResolverNames.ENV - ) { args: ArgumentQueue, context: Context? -> - val envName = args.popOr("env name expected").value() - val envDefault = args.peek()?.value()?: "" - val env = System.getenv(envName) ?: envDefault + return TagResolver.resolver(TagResolverNames.ENV) { arguments, _ -> + val envName = arguments.popOr("env name expected").value() + val envDefault = arguments.peek()?.value() ?: "" - Tag.preProcessParsed(env) + Tag.preProcessParsed(System.getenv(envName) ?: envDefault) } - } - } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/resolver/TagResolverNames.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/placeholder/TagResolverNames.kt similarity index 81% rename from proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/resolver/TagResolverNames.kt rename to proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/placeholder/TagResolverNames.kt index dfc18e2..9e9dda1 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/resolver/TagResolverNames.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/placeholder/TagResolverNames.kt @@ -1,4 +1,4 @@ -package app.simplecloud.plugin.proxy.shared.resolver +package app.simplecloud.plugin.proxy.shared.placeholder object TagResolverNames { diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/player/PlayerCountTracker.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/player/PlayerCountTracker.kt new file mode 100644 index 0000000..0dc3d70 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/player/PlayerCountTracker.kt @@ -0,0 +1,153 @@ +package app.simplecloud.plugin.proxy.shared.player + +import app.simplecloud.plugin.proxy.shared.ProxyPlugin +import app.simplecloud.plugin.proxy.shared.config.PlayerCountConfig +import kotlinx.coroutines.* +import java.util.logging.Logger +import kotlin.time.Duration.Companion.milliseconds + +class PlayerCountTracker( + private val plugin: ProxyPlugin +) { + + private val logger = Logger.getLogger(PlayerCountTracker::class.java.name) + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private var syncJob: Job? = null + + @Volatile + private var snapshot: PlayerCountSnapshot? = null + + fun start() { + if (syncJob?.isActive == true) { + return + } + + syncJob = scope.launch { + while (isActive) { + val updateTime = plugin.config.get().playerCountUpdateTimeMillis() + if (updateTime == null) { + snapshot = null + delay(1000.milliseconds) + continue + } + + refresh() + delay(updateTime.milliseconds) + } + } + } + + fun stop() { + syncJob?.cancel() + scope.cancel() + } + + fun onlinePlayers(currentProxyOnlinePlayers: Int): Int { + return currentProxyOnlinePlayers + (snapshot?.otherOnlinePlayers ?: 0) + } + + fun maxPlayers(currentProxyMaxPlayers: Int): Int { + val maxPlayers = snapshot?.maxPlayers ?: return currentProxyMaxPlayers + if (maxPlayers <= 0) return currentProxyMaxPlayers + return maxPlayers + } + + private suspend fun refresh() { + try { + snapshot = resolveSnapshot() + } catch (e: Exception) { + logger.severe("Error while syncing player count: ${e.message}") + } + } + + private suspend fun resolveSnapshot(): PlayerCountSnapshot? { + val config = plugin.config.get().playerCount + val currentGroupName = getCurrentGroupName() + + val ownGroup = resolveOwnGroupCount(currentGroupName) + val additionalGroups = resolveAdditionalGroupCount(config, currentGroupName) + val persistentServers = resolvePersistentServerCount(config) + + val otherOnlinePlayers = ownGroup.otherOnlinePlayers + + additionalGroups.otherOnlinePlayers + + persistentServers.otherOnlinePlayers + val maxPlayers = ownGroup.maxPlayers + additionalGroups.maxPlayers + persistentServers.maxPlayers + + if (otherOnlinePlayers <= 0 && maxPlayers <= 0) { + return null + } + + return PlayerCountSnapshot( + otherOnlinePlayers = otherOnlinePlayers, + maxPlayers = maxPlayers + ) + } + + private suspend fun resolveOwnGroupCount(currentGroupName: String?): PlayerCount { + val currentServer = plugin.cloudControllerHandler.currentServer ?: return PlayerCount(0, 0) + + if (currentGroupName == null) { + return PlayerCount(otherOnlinePlayers = 0, maxPlayers = currentServer.maxPlayers ?: 0) + } + + val otherOnlinePlayers = plugin.cloudControllerHandler.getServersByGroup(currentGroupName) + .filter { it.serverId != currentServer.serverId } + .sumOf { it.playerCount ?: 0 } + + return PlayerCount( + otherOnlinePlayers = otherOnlinePlayers, + maxPlayers = plugin.cloudControllerHandler.getMaxPlayersInGroup(currentGroupName) + ) + } + + private suspend fun resolveAdditionalGroupCount( + config: PlayerCountConfig, + currentGroupName: String? + ): PlayerCount { + val groupNames = config.additionalGroups + .map { it.trim() } + .filter { it.isNotEmpty() && it != currentGroupName } + .distinct() + + return PlayerCount( + otherOnlinePlayers = groupNames.sumOf { plugin.cloudControllerHandler.getOnlinePlayersInGroup(it) }, + maxPlayers = groupNames.sumOf { plugin.cloudControllerHandler.getMaxPlayersInGroup(it) } + ) + } + + private suspend fun resolvePersistentServerCount(config: PlayerCountConfig): PlayerCount { + val serverNames = config.additionalPersistentServers + .map { it.trim() } + .filter { it.isNotEmpty() && it != getCurrentPersistentServerName() } + .toSet() + + val servers = plugin.cloudControllerHandler.getPersistentServersByNames(serverNames) + + return PlayerCount( + otherOnlinePlayers = servers.sumOf { it.playerCount ?: 0 }, + maxPlayers = servers.sumOf { it.maxPlayers ?: 0 } + ) + } + + private fun getCurrentGroupName(): String? { + val currentServer = plugin.cloudControllerHandler.currentServer ?: return null + if (!currentServer.isFromGroup) return null + return currentServer.group?.name + } + + private fun getCurrentPersistentServerName(): String? { + val currentServer = plugin.cloudControllerHandler.currentServer ?: return null + if (!currentServer.isFromPersistentServer) return null + return currentServer.persistentServer?.name + } + + private data class PlayerCount( + val otherOnlinePlayers: Int, + val maxPlayers: Int + ) + + private data class PlayerCountSnapshot( + val otherOnlinePlayers: Int, + val maxPlayers: Int + ) +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/TabListResolver.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/tablist/TabListResolver.kt similarity index 50% rename from proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/TabListResolver.kt rename to proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/tablist/TabListResolver.kt index bbedf5d..1e20da3 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/TabListResolver.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/tablist/TabListResolver.kt @@ -1,19 +1,21 @@ -package app.simplecloud.plugin.proxy.shared.handler +package app.simplecloud.plugin.proxy.shared.tablist +import app.simplecloud.plugin.proxy.shared.ProxyPlugin import app.simplecloud.plugin.proxy.shared.config.TabListGroup class TabListResolver( - private val getTabListGroups: () -> List + private val plugin: ProxyPlugin ) { - private val tabListIndex = mutableMapOf() + + private val index = mutableMapOf() fun incrementIndices() { - getTabListGroups().forEach { group -> - if (group.layout.isNotEmpty()) { - val currentIndex = tabListIndex.getOrPut(group.name) { 0 } - tabListIndex[group.name] = (currentIndex + 1) % group.layout.size + getTabListGroups() + .filter { it.layout.isNotEmpty() } + .forEach { group -> + val currentIndex = index.getOrPut(group.name) { 0 } + index[group.name] = (currentIndex + 1) % group.layout.size } - } } fun findTabListGroup(serviceName: String): TabListGroup? { @@ -28,10 +30,13 @@ class TabListResolver( fun getCurrentTabList(group: TabListGroup): Pair? { if (group.layout.isEmpty()) return null - val currentIndex = tabListIndex[group.name] ?: 0 - val normalizedIndex = currentIndex % group.layout.size - val tabList = group.layout[normalizedIndex] + val currentIndex = index[group.name] ?: 0 + val tabList = group.layout[currentIndex % group.layout.size] return tabList.header to tabList.footer } + + private fun getTabListGroups(): List { + return plugin.config.get().tablist + } } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/LocalPingSourceMatcher.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/LocalPingSourceMatcher.kt index 77fd8fd..7699ac4 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/LocalPingSourceMatcher.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/LocalPingSourceMatcher.kt @@ -3,11 +3,8 @@ package app.simplecloud.plugin.proxy.shared.utilities import java.net.InetAddress import java.net.NetworkInterface -class LocalPingSourceMatcher( - private val isLocalInterfaceAddress: (InetAddress) -> Boolean = { - NetworkInterface.getByInetAddress(it) != null - } -) { +object LocalPingSourceMatcher { + fun isLocal(address: InetAddress?): Boolean { if (address == null) { return false @@ -17,6 +14,10 @@ class LocalPingSourceMatcher( return true } - return runCatching { isLocalInterfaceAddress(address) }.getOrDefault(false) + return try { + NetworkInterface.getByInetAddress(address) != null + } catch (_: Exception) { + false + } } } \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/ProxyPermissions.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/ProxyPermissions.kt new file mode 100644 index 0000000..3e64bfd --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/ProxyPermissions.kt @@ -0,0 +1,13 @@ +package app.simplecloud.plugin.proxy.shared.utilities + +object ProxyPermissions { + + const val LAYOUT_HELP = "simplecloud.proxy-essentials.command.layout.help" + const val LAYOUT_INFO = "simplecloud.proxy-essentials.command.layout.info" + const val LAYOUT_SET = "simplecloud.proxy-essentials.command.layout.set" + + const val JOINSTATE_HELP = "simplecloud.proxy-essentials.command.joinstate.help" + const val JOINSTATE_INFO = "simplecloud.proxy-essentials.command.joinstate.info" + const val JOINSTATE_SET = "simplecloud.proxy-essentials.command.joinstate.set" + +} \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MinecraftTextWidth.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MinecraftTextWidth.kt new file mode 100644 index 0000000..f8d0bb0 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MinecraftTextWidth.kt @@ -0,0 +1,114 @@ +package app.simplecloud.plugin.proxy.shared.utilities.format + +import net.kyori.adventure.key.Key +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.KeybindComponent +import net.kyori.adventure.text.ObjectComponent +import net.kyori.adventure.text.TextComponent +import net.kyori.adventure.text.TranslatableComponent +import net.kyori.adventure.text.format.Style +import net.kyori.adventure.text.format.TextDecoration + +object MinecraftTextWidth { + + const val ZERO_WIDTH_NON_JOINER = 0x200c + + private const val RESOURCE_DIRECTORY = "/app/simplecloud/plugin/proxy/shared/format" + private const val MISSING_GLYPH_WIDTH = 6f + private const val OBJECT_WIDTH = 8f + private const val SPACE_WIDTH = 4f + private const val BITMAP_BOLD_OFFSET = 1f + private const val UNIHEX_BOLD_OFFSET = 0.5f + private const val UNICODE_CODE_POINT_COUNT = 0x110000 + + private val defaultFont = Key.key("minecraft", "default") + private val altFont = Key.key("minecraft", "alt") + private val uniformFont = Key.key("minecraft", "uniform") + + private val defaultWidths by lazy { loadWidths("default-font-widths.txt") } + private val altWidths by lazy { loadWidths("alt-font-widths.txt") } + private val unifontAdvances by lazy { loadAdvances("unifont-advances.bin") } + + fun width(component: Component, inheritedStyle: Style = Style.empty()): Float { + val style = component.style().merge(inheritedStyle, Style.Merge.Strategy.IF_ABSENT_ON_TARGET) + val ownWidth = when (component) { + is TextComponent -> textWidth(component.content(), style) + is ObjectComponent -> OBJECT_WIDTH + objectBoldOffset(style) + is TranslatableComponent -> textWidth(component.fallback() ?: component.key(), style) + is KeybindComponent -> textWidth(component.keybind(), style) + else -> 0f + } + + return ownWidth + component.children().sumOf { width(it, style).toDouble() }.toFloat() + } + + private fun objectBoldOffset(style: Style): Float { + if (!style.hasDecoration(TextDecoration.BOLD)) return 0f + return BITMAP_BOLD_OFFSET + } + + private fun textWidth(text: String, style: Style): Float { + val bold = style.hasDecoration(TextDecoration.BOLD) + return text.codePoints().toArray() + .sumOf { glyphWidth(it, style.font(), bold).toDouble() } + .toFloat() + } + + private fun glyphWidth(codePoint: Int, font: Key?, bold: Boolean): Float { + val metrics = metricsOf(codePoint, font) ?: GlyphMetrics(MISSING_GLYPH_WIDTH, BITMAP_BOLD_OFFSET) + if (!bold) return metrics.advance + return metrics.advance + metrics.boldOffset + } + + private fun metricsOf(codePoint: Int, font: Key?): GlyphMetrics? { + return when { + codePoint == ' '.code -> GlyphMetrics(SPACE_WIDTH, BITMAP_BOLD_OFFSET) + codePoint == ZERO_WIDTH_NON_JOINER -> GlyphMetrics(0f, BITMAP_BOLD_OFFSET) + font == null || font == defaultFont -> bitmapMetrics(defaultWidths, codePoint) ?: unihexMetrics(codePoint) + font == altFont -> bitmapMetrics(altWidths, codePoint) + font == uniformFont -> unihexMetrics(codePoint) + else -> null + } + } + + private fun bitmapMetrics(widths: Map, codePoint: Int): GlyphMetrics? { + val advance = widths[codePoint] ?: return null + return GlyphMetrics(advance, BITMAP_BOLD_OFFSET) + } + + private fun unihexMetrics(codePoint: Int): GlyphMetrics? { + if (codePoint !in unifontAdvances.indices) return null + + val encodedAdvance = unifontAdvances[codePoint].toInt() and 0xff + if (encodedAdvance == 0) return null + + return GlyphMetrics(encodedAdvance / 2f, UNIHEX_BOLD_OFFSET) + } + + private fun loadWidths(resourceName: String): Map { + return openResource(resourceName).bufferedReader().useLines { lines -> + lines + .filterNot { it.isBlank() || it.startsWith('#') } + .associate { line -> + val (codePoint, width) = line.split('=', limit = 2) + codePoint.toInt(16) to width.toFloat() + } + } + } + + private fun loadAdvances(resourceName: String): ByteArray { + val advances = openResource(resourceName).use { it.readBytes() } + + check(advances.size == UNICODE_CODE_POINT_COUNT) { + "Invalid font metrics resource $resourceName: expected $UNICODE_CODE_POINT_COUNT bytes, got ${advances.size}" + } + return advances + } + + private fun openResource(resourceName: String) = + checkNotNull(javaClass.getResourceAsStream("$RESOURCE_DIRECTORY/$resourceName")) { + "Missing font metrics resource $RESOURCE_DIRECTORY/$resourceName" + } + + private data class GlyphMetrics(val advance: Float, val boldOffset: Float) +} \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MotdMiniMessageFormatter.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MotdMiniMessageFormatter.kt new file mode 100644 index 0000000..e726bde --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MotdMiniMessageFormatter.kt @@ -0,0 +1,69 @@ +package app.simplecloud.plugin.proxy.shared.utilities.format + +import net.kyori.adventure.key.Key +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.Style +import net.kyori.adventure.text.format.TextDecoration +import net.kyori.adventure.text.minimessage.MiniMessage +import net.kyori.adventure.text.minimessage.tag.Tag +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver +import kotlin.collections.minus +import kotlin.math.roundToInt + +object MotdMiniMessageFormatter { + + private const val CENTER_TAG = "center" + private const val CENTER_MARKER = "simplecloud:motd_center" + private const val SERVER_LIST_ROW_WIDTH = 305f + private const val MOTD_START_OFFSET = 35f + private const val MOTD_CENTER_WIDTH = SERVER_LIST_ROW_WIDTH - (MOTD_START_OFFSET * 2) + private const val SPACE_WIDTH = 4 + + private val pixelPaddingCharacter = Character.toString(MinecraftTextWidth.ZERO_WIDTH_NON_JOINER) + private val defaultFont = Key.key("minecraft", "default") + private val mergesWithoutInsertion = Style.Merge.all().minus(Style.Merge.INSERTION) + private val centerTagResolver = TagResolver.resolver(CENTER_TAG) { _, _ -> Tag.inserting(Component.empty().insertion(CENTER_MARKER)) } + + fun deserialize( + miniMessage: MiniMessage, + line1: String, + line2: String, + tagResolvers: List = emptyList() + ): Component { + val resolvers = arrayOf(centerTagResolver, *tagResolvers.toTypedArray()) + return Component.empty() + .append(applyCentering(miniMessage.deserialize(line1, *resolvers))) + .append(Component.newline()) + .append(applyCentering(miniMessage.deserialize(line2, *resolvers))) + } + + private fun applyCentering(component: Component, inheritedStyle: Style = Style.empty()): Component { + val style = component.style().merge(inheritedStyle, Style.Merge.Strategy.IF_ABSENT_ON_TARGET) + val processed = component.children(component.children().map { applyCentering(it, style) }) + if (processed.insertion() != CENTER_MARKER) return processed + + val centered = processed.style(clearInsertion(processed.style())) + val paddingWidth = paddingWidthFor(centered, inheritedStyle) + if (paddingWidth == 0) return centered + + return Component.empty().append(createPadding(paddingWidth)).append(centered) + } + + private fun paddingWidthFor(component: Component, inheritedStyle: Style): Int { + val contentWidth = MinecraftTextWidth.width(component, inheritedStyle) + return ((MOTD_CENTER_WIDTH - contentWidth) / 2f) + .roundToInt() + .coerceAtLeast(0) + } + + private fun createPadding(paddingWidth: Int): Component { + return Component.text() + .append(Component.text(" ".repeat(paddingWidth / SPACE_WIDTH)).font(defaultFont).decoration(TextDecoration.BOLD, false)) + .append(Component.text(pixelPaddingCharacter.repeat(paddingWidth % SPACE_WIDTH)).font(defaultFont).decoration(TextDecoration.BOLD, true)) + .build() + } + + private fun clearInsertion(style: Style): Style { + return Style.empty().merge(style, Style.Merge.Strategy.ALWAYS, mergesWithoutInsertion) + } +} \ No newline at end of file diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/ProxyVelocityPlugin.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/ProxyVelocityPlugin.kt index beb9bef..b54ac6a 100644 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/ProxyVelocityPlugin.kt +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/ProxyVelocityPlugin.kt @@ -2,26 +2,23 @@ package app.simplecloud.plugin.proxy.velocity import app.simplecloud.plugin.api.shared.extension.miniMessage import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.format.MotdMiniMessageFormatter -import app.simplecloud.plugin.proxy.shared.handler.command.CommandSender -import app.simplecloud.plugin.proxy.shared.handler.command.JoinStateCommandHandler -import app.simplecloud.plugin.proxy.shared.handler.command.LayoutCommandHandler -import app.simplecloud.plugin.proxy.shared.handler.command.ProxyEssentialsCommandHandler -import app.simplecloud.plugin.proxy.velocity.event.ConfigureTagResolversEvent -import app.simplecloud.plugin.proxy.velocity.handler.TabListHandler -import app.simplecloud.plugin.proxy.velocity.listener.ConfigureTagResolversListener +import app.simplecloud.plugin.proxy.shared.command.ProxyEssentialsCommandHandler +import app.simplecloud.plugin.proxy.shared.command.commands.JoinStateCommandHandler +import app.simplecloud.plugin.proxy.shared.command.commands.LayoutCommandHandler +import app.simplecloud.plugin.proxy.shared.utilities.format.MotdMiniMessageFormatter import app.simplecloud.plugin.proxy.velocity.listener.LoginListener -import app.simplecloud.plugin.proxy.velocity.listener.ProxyPingListener import app.simplecloud.plugin.proxy.velocity.listener.ServerKickListener import app.simplecloud.plugin.proxy.velocity.listener.ServerPreConnectListener +import app.simplecloud.plugin.proxy.velocity.listener.ProxyPingListener +import app.simplecloud.plugin.proxy.velocity.placeholder.ConfigureTagResolversEvent +import app.simplecloud.plugin.proxy.velocity.placeholder.ConfigureTagResolversListener +import app.simplecloud.plugin.proxy.velocity.tablist.TabListHandler import com.google.inject.Inject -import com.velocitypowered.api.command.CommandSource import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.proxy.ProxyInitializeEvent import com.velocitypowered.api.event.proxy.ProxyShutdownEvent import com.velocitypowered.api.plugin.Dependency import com.velocitypowered.api.plugin.Plugin -import com.velocitypowered.api.plugin.PluginContainer import com.velocitypowered.api.plugin.annotation.DataDirectory import com.velocitypowered.api.proxy.Player import com.velocitypowered.api.proxy.ProxyServer @@ -45,71 +42,73 @@ import kotlin.io.path.pathString ] ) class ProxyVelocityPlugin @Inject constructor( - val proxyServer: ProxyServer, + val server: ProxyServer, @DataDirectory val dataDirectory: Path, val logger: Logger, - val pluginContainer: PluginContainer ) { - val proxyPlugin = ProxyPlugin(dataDirectory.pathString) - val tabListHandler = TabListHandler(proxyPlugin, this, proxyServer) + val plugin = ProxyPlugin(dataDirectory.pathString) + val tabListHandler = TabListHandler(plugin, this, server) @Subscribe fun onProxyInitialize(event: ProxyInitializeEvent) { - proxyPlugin.motdLayoutHandler.loadMotdLayouts() - - this.proxyServer.eventManager.register(this, ProxyPingListener(proxyPlugin, this)) - this.proxyServer.eventManager.register(this, ConfigureTagResolversListener(proxyPlugin, this)) - this.proxyServer.eventManager.register(this, LoginListener(proxyPlugin, this)) - this.proxyServer.eventManager.register(this, ServerPreConnectListener(proxyPlugin, this)) - this.proxyServer.eventManager.register(this, ServerKickListener(proxyPlugin)) - - if (proxyPlugin.proxyEssentialsConfig.get().tabListUpdateTimeMillis() > 0) - this.tabListHandler.startTabListTask() - else - this.logger.info("Tablist update time is set to 0, tablist will not be updated automatically") - - val executionCoordinator = ExecutionCoordinator.simpleCoordinator() + plugin.start() - val senderMapper = SenderMapper.create( - { commandSender -> VelocityCommandSender(commandSender, this) }, - { commandSender -> (commandSender as VelocityCommandSender).getCommandSource() } - ) - - val commandManager = VelocityCommandManager( - pluginContainer, - proxyServer, - executionCoordinator, - senderMapper - ) - - ProxyEssentialsCommandHandler(commandManager, proxyPlugin).loadCommands() - JoinStateCommandHandler(commandManager, proxyPlugin).loadCommands() - LayoutCommandHandler(commandManager, proxyPlugin).loadCommands() + registerListeners() + registerCommands() + startTabListTask() } @Subscribe fun onProxyShutdown(event: ProxyShutdownEvent) { - this.tabListHandler.stopTabListTask() - this.proxyPlugin.shutdown() + tabListHandler.stopTabListTask() + plugin.shutdown() } fun deserializeToComponent(text: String, player: Player? = null): Component { - val configureTagResolversEvent = this.proxyServer.eventManager.fire(ConfigureTagResolversEvent(player)).get() - return miniMessage.deserialize( - text, - *configureTagResolversEvent.tagResolvers.toTypedArray() - ) + val tagResolvers = fireConfigureTagResolvers(player) + return miniMessage.deserialize(text, *tagResolvers.toTypedArray()) } fun deserializeMotd(line1: String, line2: String): Component { - val configureTagResolversEvent = this.proxyServer.eventManager.fire(ConfigureTagResolversEvent(null)).get() - return MotdMiniMessageFormatter.deserialize( - miniMessage, - line1, - line2, - configureTagResolversEvent.tagResolvers + val tagResolvers = fireConfigureTagResolvers(null) + return MotdMiniMessageFormatter.deserialize(miniMessage, line1, line2, tagResolvers) + } + + private fun registerListeners() { + val manager = server.eventManager + + manager.register(this, ProxyPingListener(plugin, this)) + manager.register(this, ConfigureTagResolversListener(plugin, this)) + manager.register(this, LoginListener(plugin, this)) + manager.register(this, ServerPreConnectListener(plugin, this)) + manager.register(this, ServerKickListener(plugin)) + } + + private fun registerCommands() { + val manager = VelocityCommandManager( + server.pluginManager.ensurePluginContainer(this), + server, + ExecutionCoordinator.asyncCoordinator(), + SenderMapper.create( + { commandSource -> VelocityCommandSender(commandSource, this) }, + { cloudSender -> cloudSender.commandSource } + ) ) + + ProxyEssentialsCommandHandler(manager, plugin).loadCommands() + JoinStateCommandHandler(manager, plugin).loadCommands() + LayoutCommandHandler(manager, plugin).loadCommands() + } + + private fun startTabListTask() { + if (plugin.config.get().tabListUpdateTimeMillis() <= 0) { + logger.info("Tablist update time is set to 0, tablist will not be updated automatically") + return + } + + tabListHandler.startTabListTask() } + private fun fireConfigureTagResolvers(player: Player?) = server.eventManager.fire(ConfigureTagResolversEvent(player)).get().tagResolvers } diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/VelocityCommandSender.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/VelocityCommandSender.kt index a594946..e6920b9 100644 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/VelocityCommandSender.kt +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/VelocityCommandSender.kt @@ -1,18 +1,12 @@ package app.simplecloud.plugin.proxy.velocity -import app.simplecloud.plugin.proxy.shared.handler.command.CommandSender +import app.simplecloud.plugin.proxy.shared.command.ProxyCommandSender import com.velocitypowered.api.command.CommandSource -class VelocityCommandSender( - private val commandSource: CommandSource, - val proxyVelocityPlugin: ProxyVelocityPlugin -) : CommandSender { - - fun getCommandSource(): CommandSource { - return commandSource - } +class VelocityCommandSender(val commandSource: CommandSource, private val plugin: ProxyVelocityPlugin) : ProxyCommandSender { override fun sendMessage(message: String) { - commandSource.sendMessage(this.proxyVelocityPlugin.deserializeToComponent(message)) + commandSource.sendMessage(plugin.deserializeToComponent(message)) } -} \ No newline at end of file + +} diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ConfigureTagResolversListener.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ConfigureTagResolversListener.kt deleted file mode 100644 index 81f2fdd..0000000 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ConfigureTagResolversListener.kt +++ /dev/null @@ -1,38 +0,0 @@ -package app.simplecloud.plugin.proxy.velocity.listener - -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.resolver.TagResolverHelper -import app.simplecloud.plugin.proxy.velocity.ProxyVelocityPlugin -import app.simplecloud.plugin.proxy.velocity.event.ConfigureTagResolversEvent -import com.velocitypowered.api.event.PostOrder -import com.velocitypowered.api.event.Subscribe -import kotlin.jvm.optionals.getOrNull - -class ConfigureTagResolversListener( - private val proxyPlugin: ProxyPlugin, - private val plugin: ProxyVelocityPlugin -) { - - @Subscribe(order = PostOrder.FIRST) - fun onConfigureTagResolvers(event: ConfigureTagResolversEvent) { - val player = event.player - val serverName = player?.currentServer?.getOrNull()?.serverInfo?.name ?: "unknown" - val ping = player?.ping ?: -1 - val pingColors = proxyPlugin.placeHolderConfiguration.get().pingColors - val playerCountHandler = proxyPlugin.playerCountHandler - val onlinePlayers = playerCountHandler.onlinePlayers(plugin.proxyServer.allPlayers.size) - val realMaxPlayers = playerCountHandler.maxPlayers(plugin.proxyServer.configuration.showMaxPlayers) - - event.withTagResolvers( - TagResolverHelper.getDefaultTagResolvers( - serverName, - ping, - pingColors, - onlinePlayers, - realMaxPlayers, - proxyPlugin.motdLayoutHandler.getCurrentMotdLayout() - ) - ) - } - -} diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/LoginListener.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/LoginListener.kt index 937b9d5..f4e529e 100644 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/LoginListener.kt +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/LoginListener.kt @@ -1,7 +1,7 @@ package app.simplecloud.plugin.proxy.velocity.listener import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.handler.ProxyJoinGate +import app.simplecloud.plugin.proxy.shared.joinstate.ProxyJoinGate import app.simplecloud.plugin.proxy.velocity.ProxyVelocityPlugin import com.velocitypowered.api.event.PostOrder import com.velocitypowered.api.event.ResultedEvent diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ProxyPingListener.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ProxyPingListener.kt index e586dfa..8087fdf 100644 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ProxyPingListener.kt +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ProxyPingListener.kt @@ -1,17 +1,16 @@ package app.simplecloud.plugin.proxy.velocity.listener import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.config.MaxPlayerDisplayType +import app.simplecloud.plugin.proxy.shared.config.MotdLayoutConfiguration +import app.simplecloud.plugin.proxy.shared.motd.ServerIconLoader import app.simplecloud.plugin.proxy.shared.utilities.LocalPingSourceMatcher -import app.simplecloud.plugin.proxy.shared.utilities.ServerIconLoader import app.simplecloud.plugin.proxy.velocity.ProxyVelocityPlugin import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.proxy.ProxyPingEvent import com.velocitypowered.api.proxy.server.ServerPing -import com.velocitypowered.api.proxy.server.ServerPing.SamplePlayer import com.velocitypowered.api.util.Favicon import java.nio.file.Path -import java.util.* +import java.util.UUID import kotlin.jvm.optionals.getOrNull class ProxyPingListener( @@ -19,69 +18,73 @@ class ProxyPingListener( private val plugin: ProxyVelocityPlugin ) { - private val serverIconLoader = ServerIconLoader( - Path.of(proxyPlugin.serverIconsPath) - ) { image -> Favicon.create(image) } - private val localPingSourceMatcher = LocalPingSourceMatcher() + private val serverIconLoader = ServerIconLoader(Path.of(proxyPlugin.serverIconsPath)) { image -> + Favicon.create(image) + } @Subscribe fun onProxyPing(event: ProxyPingEvent) { - val virtualHost = event.connection.virtualHost.getOrNull()?.hostName - val layout = virtualHost - ?.let { proxyPlugin.domainMotdHandler.getLayoutNameForDomain(it) } - ?.let { proxyPlugin.motdLayoutHandler.getLayoutByName(it) } - ?: proxyPlugin.motdLayoutHandler.getCurrentMotdLayout() - + val layout = resolveLayout(event.connection.virtualHost.getOrNull()?.hostName) if (!layout.motd.enabled) return - val entry = proxyPlugin.motdLayoutHandler.selectEntry(layout, layout.configVersion) - ?: return - val motd = plugin.deserializeMotd(entry.line1, entry.line2) - val isLocalPing = localPingSourceMatcher.isLocal(event.connection.remoteAddress.address) - val builder = event.ping.asBuilder().description(motd) + val entry = proxyPlugin.layoutRepository.selectEntry(layout, layout.configVersion) ?: return + val builder = event.ping.asBuilder() + .description(plugin.deserializeMotd(entry.line1, entry.line2)) - // server icon - if (layout.serverIcon.enabled) { - serverIconLoader.get(layout.serverIcon.file) { plugin.logger.warn(it) } - ?.let { builder.favicon(it) } - } + applyServerIcon(builder, layout) - // player list (hover text) - if (!isLocalPing) { - val players = event.ping.players.getOrNull() - val playerCountHandler = proxyPlugin.playerCountHandler - val onlinePlayers = playerCountHandler.onlinePlayers(plugin.proxyServer.allPlayers.size) - val realMax = playerCountHandler.maxPlayers(plugin.proxyServer.configuration.showMaxPlayers) - - val samplePlayers: List = - if (layout.playerList.enabled && layout.playerList.playerList.isNotEmpty()) { - layout.playerList.playerList.map { SamplePlayer(it, UUID.randomUUID()) } - } else { - players?.sample ?: emptyList() - } - - // slots - val maxPlayers = if (layout.version.slots.enabled) { - when (layout.version.slots.type) { - MaxPlayerDisplayType.REAL -> realMax - MaxPlayerDisplayType.FAKE -> layout.version.slots.fakeSlots - MaxPlayerDisplayType.DYNAMIC -> onlinePlayers + layout.version.slots.dynamicPlayerRange - } - } else { - realMax - } - - builder - .onlinePlayers(onlinePlayers) - .maximumPlayers(maxPlayers) - .samplePlayers(*samplePlayers.toTypedArray()) - - // version name - if (layout.version.name.enabled) { - builder.version(ServerPing.Version(event.ping.version.protocol, layout.version.name.text)) - } + // The cloud pings the proxy itself to check its health, which must not affect the counts. + if (!LocalPingSourceMatcher.isLocal(event.connection.remoteAddress.address)) { + applyPlayerList(builder, layout, event.ping) } event.ping = builder.build() } -} + + private fun resolveLayout(virtualHost: String?): MotdLayoutConfiguration { + val layoutName = when (virtualHost) { + null -> null + else -> proxyPlugin.domainMotdHandler.getLayoutNameForDomain(virtualHost) + } + val domainLayout = when (layoutName) { + null -> null + else -> proxyPlugin.layoutRepository.getLayoutByName(layoutName) + } + + return domainLayout ?: proxyPlugin.layoutRepository.getCurrentMotdLayout() + } + + private fun applyServerIcon(builder: ServerPing.Builder, layout: MotdLayoutConfiguration) { + if (!layout.serverIcon.enabled) return + + val favicon = serverIconLoader.get(layout.serverIcon.file) { plugin.logger.warn(it) } ?: return + builder.favicon(favicon) + } + + private fun applyPlayerList( + builder: ServerPing.Builder, + layout: MotdLayoutConfiguration, + ping: ServerPing + ) { + val playerCountHandler = proxyPlugin.playerCountTracker + val onlinePlayers = playerCountHandler.onlinePlayers(plugin.server.allPlayers.size) + val realMaxPlayers = playerCountHandler.maxPlayers(plugin.server.configuration.showMaxPlayers) + + builder + .onlinePlayers(onlinePlayers) + .maximumPlayers(layout.version.slots.resolveMaxPlayers(onlinePlayers, realMaxPlayers)) + .samplePlayers(*samplePlayers(layout, ping).toTypedArray()) + + if (layout.version.name.enabled) { + builder.version(ServerPing.Version(ping.version.protocol, layout.version.name.text)) + } + } + + private fun samplePlayers(layout: MotdLayoutConfiguration, ping: ServerPing): List { + if (!layout.playerList.enabled || layout.playerList.playerList.isEmpty()) { + return ping.players.getOrNull()?.sample ?: emptyList() + } + + return layout.playerList.playerList.map { ServerPing.SamplePlayer(it, UUID.randomUUID()) } + } +} \ No newline at end of file diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ServerKickListener.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ServerKickListener.kt index b7a9de4..7a3bb4e 100644 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ServerKickListener.kt +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ServerKickListener.kt @@ -4,17 +4,13 @@ import app.simplecloud.plugin.proxy.shared.ProxyPlugin import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.player.KickedFromServerEvent -class ServerKickListener internal constructor( - private val showKickReason: () -> Boolean +class ServerKickListener( + private val proxyPlugin: ProxyPlugin ) { - constructor(proxyPlugin: ProxyPlugin) : this({ - proxyPlugin.proxyEssentialsConfig.get().showKickReason - }) - @Subscribe(priority = Short.MIN_VALUE) fun handle(event: KickedFromServerEvent) { - if (!showKickReason()) return + if (!proxyPlugin.config.get().showKickReason) return if (event.result !is KickedFromServerEvent.DisconnectPlayer) return event.serverKickReason.ifPresent { serverReason -> diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ServerPreConnectListener.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ServerPreConnectListener.kt index 9f47ff0..2bb991a 100644 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ServerPreConnectListener.kt +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ServerPreConnectListener.kt @@ -1,13 +1,12 @@ package app.simplecloud.plugin.proxy.velocity.listener import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.handler.ProxyJoinGate +import app.simplecloud.plugin.proxy.shared.joinstate.ProxyJoinGate import app.simplecloud.plugin.proxy.velocity.ProxyVelocityPlugin import com.velocitypowered.api.event.PostOrder import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.player.ServerPreConnectEvent import com.velocitypowered.api.proxy.Player -import com.velocitypowered.api.proxy.server.RegisteredServer import kotlinx.coroutines.runBlocking import java.util.logging.Logger @@ -15,57 +14,63 @@ class ServerPreConnectListener( private val proxyPlugin: ProxyPlugin, private val plugin: ProxyVelocityPlugin ) { + private val logger = Logger.getLogger(ServerPreConnectListener::class.java.name) - private val resolver = proxyPlugin.joinStateResolver @Subscribe(order = PostOrder.EARLY) fun handle(event: ServerPreConnectEvent) { if (event.previousServer == null) { - checkAllowProxyJoin(event.player, event) - if (event.result.isAllowed.not()) { + checkAllowProxyJoin(event) + if (!event.result.isAllowed) { return } } - checkAllowServerSwitch(event.player, event, event.originalServer) + checkAllowServerSwitch(event) } - private fun checkAllowProxyJoin(player: Player, event: ServerPreConnectEvent) { + private fun checkAllowProxyJoin(event: ServerPreConnectEvent) { + val player = event.player val result = runBlocking { proxyPlugin.proxyJoinGate.evaluate(player.username) { permission -> player.hasPermission(permission) } } if (result is ProxyJoinGate.Result.Denied) { - denyAccess(player, result.kickMessage, false, event) + disconnect(player, result.kickMessage, event) } } - private fun checkAllowServerSwitch(player: Player, event: ServerPreConnectEvent, server: RegisteredServer) { - val serverName = server.serverInfo.name + private fun checkAllowServerSwitch(event: ServerPreConnectEvent) { + val player = event.player + val serverName = event.originalServer.serverInfo.name + val resolver = proxyPlugin.joinStateResolver runBlocking { val joinStateName = resolver.getJoinStateForServer(serverName) val joinState = resolver.resolveJoinState(joinStateName) + val kickMessages = proxyPlugin.messageConfig.get().kick if (joinState == null) { logger.severe("Neither join state '$joinStateName' nor default state found. Check configuration!") - denyAccess(player, proxyPlugin.messagesConfiguration.get().kick.noJoinState, true, event) + denyServerSwitch(player, kickMessages.noJoinState, event) return@runBlocking } - if (joinState.permission.join.isNotBlank() && !player.hasPermission(joinState.permission.join)) { - logger.info("Player ${player.username} does not have permission to join $serverName. (JoinState: ${joinStateName}, Permission: ${joinState.permission.join})") - denyAccess(player, proxyPlugin.messagesConfiguration.get().kick.noPermission, true, event) + val joinPermission = joinState.permission.join + if (joinPermission.isNotBlank() && !player.hasPermission(joinPermission)) { + logger.info("Player ${player.username} does not have permission to join $serverName. (JoinState: $joinStateName, Permission: $joinPermission)") + denyServerSwitch(player, kickMessages.noPermission, event) } } } - private fun denyAccess(player: Player, message: String, isSubServer: Boolean, event: ServerPreConnectEvent) { - if (isSubServer) { - player.sendMessage(plugin.deserializeToComponent(message)) - } else { - player.disconnect(plugin.deserializeToComponent(message, player)) - } + private fun denyServerSwitch(player: Player, message: String, event: ServerPreConnectEvent) { + player.sendMessage(plugin.deserializeToComponent(message)) + event.result = ServerPreConnectEvent.ServerResult.denied() + } + + private fun disconnect(player: Player, message: String, event: ServerPreConnectEvent) { + player.disconnect(plugin.deserializeToComponent(message, player)) event.result = ServerPreConnectEvent.ServerResult.denied() } } diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/event/ConfigureTagResolversEvent.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/placeholder/ConfigureTagResolversEvent.kt similarity index 81% rename from proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/event/ConfigureTagResolversEvent.kt rename to proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/placeholder/ConfigureTagResolversEvent.kt index 21c45c1..172f498 100644 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/event/ConfigureTagResolversEvent.kt +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/placeholder/ConfigureTagResolversEvent.kt @@ -1,4 +1,4 @@ -package app.simplecloud.plugin.proxy.velocity.event +package app.simplecloud.plugin.proxy.velocity.placeholder import com.velocitypowered.api.proxy.Player import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver @@ -9,12 +9,11 @@ class ConfigureTagResolversEvent( ) { fun withTagResolvers(vararg tagResolvers: TagResolver): ConfigureTagResolversEvent { - return this.withTagResolvers(tagResolvers.toList()) + return withTagResolvers(tagResolvers.toList()) } fun withTagResolvers(tagResolvers: List): ConfigureTagResolversEvent { this.tagResolvers.addAll(tagResolvers) return this } - } diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/placeholder/ConfigureTagResolversListener.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/placeholder/ConfigureTagResolversListener.kt new file mode 100644 index 0000000..cf3bd7c --- /dev/null +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/placeholder/ConfigureTagResolversListener.kt @@ -0,0 +1,31 @@ +package app.simplecloud.plugin.proxy.velocity.placeholder + +import app.simplecloud.plugin.proxy.shared.ProxyPlugin +import app.simplecloud.plugin.proxy.shared.placeholder.TagResolverHelper +import app.simplecloud.plugin.proxy.velocity.ProxyVelocityPlugin +import com.velocitypowered.api.event.PostOrder +import com.velocitypowered.api.event.Subscribe +import kotlin.jvm.optionals.getOrNull + +class ConfigureTagResolversListener( + private val proxyPlugin: ProxyPlugin, + private val plugin: ProxyVelocityPlugin +) { + + @Subscribe(order = PostOrder.FIRST) + fun onConfigureTagResolvers(event: ConfigureTagResolversEvent) { + val player = event.player + val playerCountHandler = proxyPlugin.playerCountTracker + + event.withTagResolvers( + TagResolverHelper.getDefaultTagResolvers( + serverName = player?.currentServer?.getOrNull()?.serverInfo?.name ?: "unknown", + ping = player?.ping ?: -1, + pingColors = proxyPlugin.placeholderConfig.get().pingColors, + onlinePlayers = playerCountHandler.onlinePlayers(plugin.server.allPlayers.size), + realMaxPlayers = playerCountHandler.maxPlayers(plugin.server.configuration.showMaxPlayers), + motdConfiguration = proxyPlugin.layoutRepository.getCurrentMotdLayout() + ) + ) + } +} diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/handler/TabListHandler.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/tablist/TabListHandler.kt similarity index 50% rename from proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/handler/TabListHandler.kt rename to proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/tablist/TabListHandler.kt index 68aa9cb..8c0f221 100644 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/handler/TabListHandler.kt +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/tablist/TabListHandler.kt @@ -1,47 +1,56 @@ -package app.simplecloud.plugin.proxy.velocity.handler +package app.simplecloud.plugin.proxy.velocity.tablist import app.simplecloud.plugin.proxy.shared.ProxyPlugin import app.simplecloud.plugin.proxy.velocity.ProxyVelocityPlugin -import com.velocitypowered.api.proxy.ProxyServer import com.velocitypowered.api.proxy.Player +import com.velocitypowered.api.proxy.ProxyServer import com.velocitypowered.api.scheduler.ScheduledTask import java.util.concurrent.TimeUnit import java.util.logging.Logger +import kotlin.jvm.optionals.getOrNull class TabListHandler( private val proxyPlugin: ProxyPlugin, private val plugin: ProxyVelocityPlugin, private val proxyServer: ProxyServer ) { + private val logger = Logger.getLogger(TabListHandler::class.java.name) - private val resolver = proxyPlugin.tabListResolver - private lateinit var task: ScheduledTask + private var task: ScheduledTask? = null fun startTabListTask() { - task = proxyServer.scheduler.buildTask(plugin, Runnable { - resolver.incrementIndices() - proxyServer.allPlayers.forEach { updateTabListForPlayer(it) } - }).repeat(proxyPlugin.proxyEssentialsConfig.get().tabListUpdateTimeMillis(), TimeUnit.MILLISECONDS).schedule() + val updateTimeMillis = proxyPlugin.config.get().tabListUpdateTimeMillis() + + task = proxyServer.scheduler + .buildTask(plugin, Runnable { updateTabLists() }) + .repeat(updateTimeMillis, TimeUnit.MILLISECONDS) + .schedule() } fun stopTabListTask() { - if (!this::task.isInitialized) { + val runningTask = task + if (runningTask == null) { logger.warning("Can't stop tablist task because it is not initialized") return } - task.cancel() + + runningTask.cancel() + task = null } fun updateTabListForPlayer(player: Player) { - if (player.currentServer.isEmpty) return - - val serviceName = player.currentServer.get().serverInfo.name - val tabListGroup = resolver.findTabListGroup(serviceName) ?: return - val (header, footer) = resolver.getCurrentTabList(tabListGroup) ?: return + val currentServer = player.currentServer.getOrNull() ?: return + val tabListGroup = proxyPlugin.tabListResolver.findTabListGroup(currentServer.serverInfo.name) ?: return + val (header, footer) = proxyPlugin.tabListResolver.getCurrentTabList(tabListGroup) ?: return player.sendPlayerListHeaderAndFooter( plugin.deserializeToComponent(header, player), plugin.deserializeToComponent(footer, player), ) } + + private fun updateTabLists() { + proxyPlugin.tabListResolver.incrementIndices() + proxyServer.allPlayers.forEach { updateTabListForPlayer(it) } + } } From 624f534b2b7f39625f9614e466fb1c36435f6c4a Mon Sep 17 00:00:00 2001 From: Janis Date: Wed, 26 Aug 2026 15:22:12 +0200 Subject: [PATCH 05/17] refactor: rename plugin to simplecloud-proxy-essentials --- .../src/main/resources/plugin.yml | 2 +- .../plugin/proxy/shared/ProxyPlugin.kt | 2 + .../migration/LegacyDirectoryMigrator.kt | 86 +++++++++++++++++++ .../proxy/velocity/ProxyVelocityPlugin.kt | 4 +- 4 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/LegacyDirectoryMigrator.kt diff --git a/proxy-bungeecord/src/main/resources/plugin.yml b/proxy-bungeecord/src/main/resources/plugin.yml index fbb77e3..b98732c 100644 --- a/proxy-bungeecord/src/main/resources/plugin.yml +++ b/proxy-bungeecord/src/main/resources/plugin.yml @@ -1,4 +1,4 @@ -name: proxy-essentials-bungeecord +name: simplecloud-proxy-essentials main: app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin version: ${version} author: D151l diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt index 6757df0..a98f112 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt @@ -7,6 +7,7 @@ import app.simplecloud.plugin.proxy.shared.config.DefaultConfigInstaller import app.simplecloud.plugin.proxy.shared.config.MessageConfig import app.simplecloud.plugin.proxy.shared.config.PlaceHolderConfiguration import app.simplecloud.plugin.proxy.shared.config.ProxyEssentialsConfig +import app.simplecloud.plugin.proxy.shared.config.migration.LegacyDirectoryMigrator import app.simplecloud.plugin.proxy.shared.config.migration.OldConfigMigrator import app.simplecloud.plugin.proxy.shared.controller.CloudControllerHandler import app.simplecloud.plugin.proxy.shared.joinstate.JoinStateResolver @@ -31,6 +32,7 @@ class ProxyPlugin( val placeholderConfig = ConfigurationFactory(File(dataDirectory, "placeholder.yml"), PlaceHolderConfiguration::class.java) init { + LegacyDirectoryMigrator.migrate(dataDirectory.toPath()) dataDirectory.mkdirs() OldConfigMigrator.migrate(dataDirectory.toPath()) DefaultConfigInstaller.install(dataDirectory.toPath(), javaClass.classLoader) diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/LegacyDirectoryMigrator.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/LegacyDirectoryMigrator.kt new file mode 100644 index 0000000..acae5e0 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/LegacyDirectoryMigrator.kt @@ -0,0 +1,86 @@ +package app.simplecloud.plugin.proxy.shared.config.migration + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import java.util.logging.Level +import java.util.logging.Logger + +object LegacyDirectoryMigrator { + + private val logger = Logger.getLogger(LegacyDirectoryMigrator::class.java.name) + private val LEGACY_DIRECTORY_NAMES = listOf("proxy-essentials-velocity", "proxy-essentials-bungeecord") + private const val MARKER_FILE = ".migrated-from-legacy-directory" + + fun migrate(directory: Path) { + val legacyDirectory = findLegacyDirectory(directory) ?: return + + when { + isAlreadyMigrated(directory) -> warnAboutUnusedDirectory(legacyDirectory, directory) + else -> takeOverConfiguration(legacyDirectory, directory) + } + } + + private fun findLegacyDirectory(dataDirectory: Path): Path? { + val directory = dataDirectory.toAbsolutePath().normalize() + val pluginsDirectory = directory.parent ?: return null + + return LEGACY_DIRECTORY_NAMES + .map { pluginsDirectory.resolve(it) } + .find { it != directory && Files.isDirectory(it) } + } + + private fun isAlreadyMigrated(directory: Path): Boolean { + if (Files.exists(directory.resolve(MARKER_FILE))) { + return true + } + + if (!Files.isDirectory(directory)) { + return false + } + + return Files.list(directory).use { paths -> paths.findAny().isPresent } + } + + private fun takeOverConfiguration(legacyDirectory: Path, dataDirectory: Path) { + try { + copyDirectory(legacyDirectory, dataDirectory) + Files.createFile(dataDirectory.resolve(MARKER_FILE)) + } catch (e: Exception) { + logger.log(Level.SEVERE, "Could not take over the configuration of '${legacyDirectory.fileName}'", e) + return + } + + logger.info( + "Took over the configuration of '${legacyDirectory.fileName}' into '${dataDirectory.fileName}'. " + + "Rename that directory in your template to keep editing your configuration." + ) + } + + private fun warnAboutUnusedDirectory(legacyDirectory: Path, dataDirectory: Path) { + logger.warning( + "The directory '${legacyDirectory.fileName}' is not used anymore. " + + "Rename it to '${dataDirectory.fileName}' in your template to get rid of this warning." + ) + } + + private fun copyDirectory(source: Path, target: Path) { + Files.createDirectories(target) + + Files.walk(source).use { paths -> + paths.forEach { path -> copyEntry(source, target, path) } + } + } + + private fun copyEntry(source: Path, target: Path, path: Path) { + val destination = target.resolve(source.relativize(path).toString()) + + when { + Files.isDirectory(path) -> Files.createDirectories(destination) + else -> { + Files.createDirectories(destination.parent) + Files.copy(path, destination, StandardCopyOption.REPLACE_EXISTING) + } + } + } +} diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/ProxyVelocityPlugin.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/ProxyVelocityPlugin.kt index b54ac6a..622813f 100644 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/ProxyVelocityPlugin.kt +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/ProxyVelocityPlugin.kt @@ -31,8 +31,8 @@ import java.nio.file.Path import kotlin.io.path.pathString @Plugin( - id = "proxy-essentials-velocity", - name = "proxy-essentials-velocity", + id = "simplecloud-proxy-essentials", + name = "simplecloud-proxy-essentials", version = BuildConstants.VERSION, authors = ["D151l"], description = "Configure SimpleCloud MOTDs, tablists, join states, player counts, and proxy layouts", From c7f140d4b508d0bb671d589b04a3deea6b57f13d Mon Sep 17 00:00:00 2001 From: Janis Date: Wed, 26 Aug 2026 17:11:23 +0200 Subject: [PATCH 06/17] refactor: improve logging --- .gitignore | 3 ++ build.gradle.kts | 2 + gradle/libs.versions.toml | 6 +++ .../proxy/bungeecord/ProxyBungeeCordPlugin.kt | 25 +++++++----- .../listener/ServerPreConnectListener.kt | 6 +-- .../placeholder/ConfigureTagResolversEvent.kt | 23 ----------- .../ConfigureTagResolversListener.kt | 30 --------------- .../bungeecord/tablist/TabListHandler.kt | 6 +-- .../plugin/proxy/shared/ProxyPlugin.kt | 8 +++- .../command/ProxyEssentialsCommandHandler.kt | 17 +++++---- .../commands/JoinStateCommandHandler.kt | 5 +-- .../command/commands/LayoutCommandHandler.kt | 5 +-- .../migration/LegacyDirectoryMigrator.kt | 9 ++--- .../controller/CloudControllerHandler.kt | 38 +++++++++---------- .../shared/joinstate/JoinStateResolver.kt | 4 +- .../shared/joinstate/JoinStateService.kt | 27 +++++-------- .../proxy/shared/joinstate/ProxyJoinGate.kt | 8 ++-- .../proxy/shared/motd/DomainMotdResolver.kt | 6 +-- .../proxy/shared/motd/MotdLayoutRepository.kt | 7 ++-- .../proxy/shared/player/PlayerCountTracker.kt | 6 +-- .../utilities/format/MinecraftTextWidth.kt | 11 +++--- .../proxy/velocity/ProxyVelocityPlugin.kt | 29 +++++++++----- .../velocity/listener/ProxyPingListener.kt | 4 +- .../listener/ServerPreConnectListener.kt | 6 +-- .../placeholder/ConfigureTagResolversEvent.kt | 19 ---------- .../ConfigureTagResolversListener.kt | 31 --------------- .../proxy/velocity/tablist/TabListHandler.kt | 6 +-- 27 files changed, 132 insertions(+), 215 deletions(-) delete mode 100644 proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/placeholder/ConfigureTagResolversEvent.kt delete mode 100644 proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/placeholder/ConfigureTagResolversListener.kt delete mode 100644 proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/placeholder/ConfigureTagResolversEvent.kt delete mode 100644 proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/placeholder/ConfigureTagResolversListener.kt diff --git a/.gitignore b/.gitignore index 27b634f..5f3a971 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ build/ ### IntelliJ IDEA ### .idea/ +### Kotlin ### +.kotlin/ + ### VS Code ### .vscode/ diff --git a/build.gradle.kts b/build.gradle.kts index 0b88c65..a1cfd05 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,6 +34,7 @@ subprojects { implementation(rootProject.libs.kotlinx.coroutines.core) testImplementation(rootProject.libs.kotlin.test) + compileOnly(rootProject.libs.slf4j.api) compileOnly(rootProject.libs.simplecloud.api) implementation(rootProject.libs.simplecloud.plugin) @@ -57,6 +58,7 @@ subprojects { } tasks.shadowJar { + duplicatesStrategy = DuplicatesStrategy.INCLUDE mergeServiceFiles() archiveFileName.set("${project.name}.jar") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 86914d2..5d6f8c7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,8 @@ shadow = "9.6.1" minotaur = "2.9.0" blossom = "2.2.0" +slf4j = "2.0.18" + velocity = "4.1.0-SNAPSHOT" bungeecord = "1.21-R0.4" @@ -31,11 +33,15 @@ command-cloud-core = { module = "org.incendo:cloud-core", version.ref = "command command-cloud-velocity = { module = "org.incendo:cloud-velocity", version.ref = "command-cloud-velocity" } command-cloud-bungeecord = { module = "org.incendo:cloud-bungee", version.ref = "command-cloud-bungeecord" } +slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } + velocity = { module = "com.velocitypowered:velocity-api", version.ref = "velocity" } bungeecord = { module = "net.md-5:bungeecord-api", version.ref = "bungeecord" } + adventure-legacy-serializer = { module = "net.kyori:adventure-text-serializer-legacy", version.ref = "adventure" } adventure-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure" } adventure-bungeecord-platform = { module = "net.kyori:adventure-platform-bungeecord", version.ref = "adventure-platform" } + configurate-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate-yaml" } configurate-kotlin = { module = "org.spongepowered:configurate-extra-kotlin", version.ref = "configurate-kotlin" } diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ProxyBungeeCordPlugin.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ProxyBungeeCordPlugin.kt index bd48682..52b30a6 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ProxyBungeeCordPlugin.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ProxyBungeeCordPlugin.kt @@ -5,8 +5,6 @@ import app.simplecloud.plugin.proxy.bungeecord.listener.PostLoginListener import app.simplecloud.plugin.proxy.bungeecord.listener.ServerKickListener import app.simplecloud.plugin.proxy.bungeecord.listener.ServerPreConnectListener import app.simplecloud.plugin.proxy.bungeecord.listener.ProxyPingListener -import app.simplecloud.plugin.proxy.bungeecord.placeholder.ConfigureTagResolversEvent -import app.simplecloud.plugin.proxy.bungeecord.placeholder.ConfigureTagResolversListener import app.simplecloud.plugin.proxy.bungeecord.tablist.TabListHandler import app.simplecloud.plugin.proxy.bungeecord.tablist.TabListListener import app.simplecloud.plugin.proxy.shared.ProxyPlugin @@ -14,9 +12,11 @@ import app.simplecloud.plugin.proxy.shared.command.ProxyCommandSender import app.simplecloud.plugin.proxy.shared.command.ProxyEssentialsCommandHandler import app.simplecloud.plugin.proxy.shared.command.commands.JoinStateCommandHandler import app.simplecloud.plugin.proxy.shared.command.commands.LayoutCommandHandler +import app.simplecloud.plugin.proxy.shared.placeholder.TagResolverHelper import app.simplecloud.plugin.proxy.shared.utilities.format.MotdMiniMessageFormatter import net.kyori.adventure.platform.bungeecord.BungeeAudiences import net.kyori.adventure.text.Component +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver import net.md_5.bungee.api.CommandSender import net.md_5.bungee.api.connection.ProxiedPlayer import net.md_5.bungee.api.plugin.Plugin @@ -52,20 +52,17 @@ class ProxyBungeeCordPlugin : Plugin() { } fun deserializeToComponent(text: String, player: ProxiedPlayer? = null): Component { - val tagResolvers = fireConfigureTagResolvers(player) - return miniMessage.deserialize(text, *tagResolvers.toTypedArray()) + return miniMessage.deserialize(text, *tagResolvers(player).toTypedArray()) } fun deserializeMotd(line1: String, line2: String): Component { - val tagResolvers = fireConfigureTagResolvers(null) - return MotdMiniMessageFormatter.deserialize(miniMessage, line1, line2, tagResolvers) + return MotdMiniMessageFormatter.deserialize(miniMessage, line1, line2, tagResolvers(null)) } private fun registerListeners() { val manager = proxy.pluginManager manager.registerListener(this, ProxyPingListener(this)) - manager.registerListener(this, ConfigureTagResolversListener(this)) manager.registerListener(this, PostLoginListener(this)) manager.registerListener(this, ServerPreConnectListener(this)) manager.registerListener(this, ServerKickListener(proxyPlugin)) @@ -96,6 +93,16 @@ class ProxyBungeeCordPlugin : Plugin() { tabListHandler.startTabListTask() } - private fun fireConfigureTagResolvers(player: ProxiedPlayer?) = - proxy.pluginManager.callEvent(ConfigureTagResolversEvent(player)).tagResolvers + private fun tagResolvers(player: ProxiedPlayer?): List { + val playerCountTracker = proxyPlugin.playerCountTracker + + return TagResolverHelper.getDefaultTagResolvers( + serverName = player?.server?.info?.name ?: "unknown", + ping = (player?.ping ?: -1).toLong(), + pingColors = proxyPlugin.placeholderConfig.get().pingColors, + onlinePlayers = playerCountTracker.onlinePlayers(proxy.players.size), + realMaxPlayers = playerCountTracker.maxPlayers(proxy.config.playerLimit), + motdConfiguration = proxyPlugin.layoutRepository.getCurrentMotdLayout() + ) + } } diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerPreConnectListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerPreConnectListener.kt index cd72fd5..0d2f5a1 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerPreConnectListener.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerPreConnectListener.kt @@ -10,13 +10,13 @@ import net.md_5.bungee.api.event.ServerConnectEvent.Reason import net.md_5.bungee.api.plugin.Listener import net.md_5.bungee.event.EventHandler import net.md_5.bungee.event.EventPriority -import java.util.logging.Logger +import org.slf4j.LoggerFactory class ServerPreConnectListener( private val plugin: ProxyBungeeCordPlugin ) : Listener { - private val logger = Logger.getLogger(ServerPreConnectListener::class.java.name) + private val logger = LoggerFactory.getLogger(ServerPreConnectListener::class.java) private val proxyPlugin = plugin.proxyPlugin @EventHandler(priority = EventPriority.HIGH) @@ -57,7 +57,7 @@ class ServerPreConnectListener( val kickMessages = proxyPlugin.messageConfig.get().kick if (joinState == null) { - logger.severe("Neither join state '$joinStateName' nor default state found. Check configuration!") + logger.error("Neither join state '$joinStateName' nor default state found. Check configuration!") denyServerSwitch(player, kickMessages.noJoinState, event) return@runBlocking } diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/placeholder/ConfigureTagResolversEvent.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/placeholder/ConfigureTagResolversEvent.kt deleted file mode 100644 index 49d2d0d..0000000 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/placeholder/ConfigureTagResolversEvent.kt +++ /dev/null @@ -1,23 +0,0 @@ -package app.simplecloud.plugin.proxy.bungeecord.placeholder - -import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver -import net.md_5.bungee.api.connection.ProxiedPlayer -import net.md_5.bungee.api.plugin.Event - -/** - * Fired before a message is deserialized so other plugins can contribute their own MiniMessage tags. - */ -class ConfigureTagResolversEvent( - val player: ProxiedPlayer? = null, - val tagResolvers: MutableList = mutableListOf() -) : Event() { - - fun withTagResolvers(vararg tagResolvers: TagResolver): ConfigureTagResolversEvent { - return withTagResolvers(tagResolvers.toList()) - } - - fun withTagResolvers(tagResolvers: List): ConfigureTagResolversEvent { - this.tagResolvers.addAll(tagResolvers) - return this - } -} diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/placeholder/ConfigureTagResolversListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/placeholder/ConfigureTagResolversListener.kt deleted file mode 100644 index ff8f20d..0000000 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/placeholder/ConfigureTagResolversListener.kt +++ /dev/null @@ -1,30 +0,0 @@ -package app.simplecloud.plugin.proxy.bungeecord.placeholder - -import app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin -import app.simplecloud.plugin.proxy.shared.placeholder.TagResolverHelper -import net.md_5.bungee.api.plugin.Listener -import net.md_5.bungee.event.EventHandler -import net.md_5.bungee.event.EventPriority - -class ConfigureTagResolversListener( - private val plugin: ProxyBungeeCordPlugin -) : Listener { - - @EventHandler(priority = EventPriority.LOWEST) - fun onConfigureTagResolvers(event: ConfigureTagResolversEvent) { - val proxyPlugin = plugin.proxyPlugin - val player = event.player - val playerCountHandler = proxyPlugin.playerCountTracker - - event.withTagResolvers( - TagResolverHelper.getDefaultTagResolvers( - serverName = player?.server?.info?.name ?: "unknown", - ping = (player?.ping ?: -1).toLong(), - pingColors = proxyPlugin.placeholderConfig.get().pingColors, - onlinePlayers = playerCountHandler.onlinePlayers(plugin.proxy.players.size), - realMaxPlayers = playerCountHandler.maxPlayers(plugin.proxy.config.playerLimit), - motdConfiguration = proxyPlugin.layoutRepository.getCurrentMotdLayout() - ) - ) - } -} diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/tablist/TabListHandler.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/tablist/TabListHandler.kt index b913c8a..879f8b1 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/tablist/TabListHandler.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/tablist/TabListHandler.kt @@ -3,14 +3,14 @@ package app.simplecloud.plugin.proxy.bungeecord.tablist import app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin import net.md_5.bungee.api.connection.ProxiedPlayer import net.md_5.bungee.api.scheduler.ScheduledTask +import org.slf4j.LoggerFactory import java.util.concurrent.TimeUnit -import java.util.logging.Logger class TabListHandler( private val plugin: ProxyBungeeCordPlugin ) { - private val logger = Logger.getLogger(TabListHandler::class.java.name) + private val logger = LoggerFactory.getLogger(TabListHandler::class.java) private var task: ScheduledTask? = null fun startTabListTask() { @@ -28,7 +28,7 @@ class TabListHandler( fun stopTabListTask() { val runningTask = task if (runningTask == null) { - logger.warning("Can't stop tablist task because it is not initialized") + logger.warn("Can't stop tablist task because it is not initialized") return } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt index a98f112..520ec2e 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt @@ -17,16 +17,18 @@ import app.simplecloud.plugin.proxy.shared.motd.DomainMotdResolver import app.simplecloud.plugin.proxy.shared.motd.MotdLayoutRepository import app.simplecloud.plugin.proxy.shared.player.PlayerCountTracker import app.simplecloud.plugin.proxy.shared.tablist.TabListResolver +import org.slf4j.LoggerFactory import java.io.File class ProxyPlugin( dirPath: String ) { - val api: CloudApi = CloudApi.create(CloudApiOptions.builder().component("proxy-essentials").build()) - + private val logger = LoggerFactory.getLogger(ProxyPlugin::class.java) private val dataDirectory = File(dirPath) + val api: CloudApi = CloudApi.create(CloudApiOptions.builder().component("proxy-essentials").build()) + val config = ConfigurationFactory(File(dataDirectory, "config.yml"), ProxyEssentialsConfig::class.java) val messageConfig = ConfigurationFactory(File(dataDirectory, "messages.yml"), MessageConfig::class.java) val placeholderConfig = ConfigurationFactory(File(dataDirectory, "placeholder.yml"), PlaceHolderConfiguration::class.java) @@ -65,8 +67,10 @@ class ProxyPlugin( } fun reload() { + logger.info("Reloading Proxy Essentials...") loadConfigurations() layoutRepository.loadMotdLayouts() + logger.info("Successfully reloaded Proxy Essentials") } private fun loadConfigurations() { diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyEssentialsCommandHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyEssentialsCommandHandler.kt index 31c93b3..9caaa01 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyEssentialsCommandHandler.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyEssentialsCommandHandler.kt @@ -1,20 +1,18 @@ package app.simplecloud.plugin.proxy.shared.command import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import org.incendo.cloud.CommandManager import org.incendo.cloud.context.CommandContext -import java.util.logging.Level -import java.util.logging.Logger +import org.slf4j.LoggerFactory class ProxyEssentialsCommandHandler( private val manager: CommandManager, private val plugin: ProxyPlugin ) { - private val logger = Logger.getLogger(ProxyEssentialsCommandHandler::class.java.name) + private val logger = LoggerFactory.getLogger(ProxyEssentialsCommandHandler::class.java) + private val commands = listOf( "/scproxy help", "/scproxy reload", @@ -64,7 +62,9 @@ class ProxyEssentialsCommandHandler( manager.commandBuilder("scproxy") .literal("reload") .permission("simplecloud.proxy-essentials.command.reload") - .handler { context: CommandContext -> reload(context.sender()) } + .handler { context: CommandContext -> + reload(context.sender()) + } .build() ) } @@ -79,9 +79,10 @@ class ProxyEssentialsCommandHandler( val messages = plugin.messageConfig.get() sender.sendMessage(messages.resolve(messages.command.reload.success)) } catch (e: Exception) { + logger.error("Failed to reload Proxy Essentials", e) + val messages = plugin.messageConfig.get() sender.sendMessage(messages.resolve(messages.command.reload.failure).replace("", e.message ?: "Unknown error")) - logger.log(Level.SEVERE, "Error reloading configuration", e) } } } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/JoinStateCommandHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/JoinStateCommandHandler.kt index 947fa70..70bb67d 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/JoinStateCommandHandler.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/JoinStateCommandHandler.kt @@ -4,10 +4,7 @@ import app.simplecloud.plugin.proxy.shared.ProxyPlugin import app.simplecloud.plugin.proxy.shared.command.ProxyCommandSender import app.simplecloud.plugin.proxy.shared.config.MessageConfig import app.simplecloud.plugin.proxy.shared.utilities.ProxyPermissions -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.* import org.incendo.cloud.CommandManager import org.incendo.cloud.context.CommandContext import org.incendo.cloud.parser.standard.IntegerParser diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/LayoutCommandHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/LayoutCommandHandler.kt index 1ed702c..82b28e9 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/LayoutCommandHandler.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/LayoutCommandHandler.kt @@ -5,10 +5,7 @@ import app.simplecloud.plugin.proxy.shared.command.ProxyCommandSender import app.simplecloud.plugin.proxy.shared.config.MessageConfig import app.simplecloud.plugin.proxy.shared.motd.MotdLayoutRepository import app.simplecloud.plugin.proxy.shared.utilities.ProxyPermissions -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.* import org.incendo.cloud.CommandManager import org.incendo.cloud.context.CommandContext import org.incendo.cloud.parser.standard.StringParser diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/LegacyDirectoryMigrator.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/LegacyDirectoryMigrator.kt index acae5e0..6a77b08 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/LegacyDirectoryMigrator.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/LegacyDirectoryMigrator.kt @@ -1,14 +1,13 @@ package app.simplecloud.plugin.proxy.shared.config.migration +import org.slf4j.LoggerFactory import java.nio.file.Files import java.nio.file.Path import java.nio.file.StandardCopyOption -import java.util.logging.Level -import java.util.logging.Logger object LegacyDirectoryMigrator { - private val logger = Logger.getLogger(LegacyDirectoryMigrator::class.java.name) + private val logger = LoggerFactory.getLogger(LegacyDirectoryMigrator::class.java) private val LEGACY_DIRECTORY_NAMES = listOf("proxy-essentials-velocity", "proxy-essentials-bungeecord") private const val MARKER_FILE = ".migrated-from-legacy-directory" @@ -47,7 +46,7 @@ object LegacyDirectoryMigrator { copyDirectory(legacyDirectory, dataDirectory) Files.createFile(dataDirectory.resolve(MARKER_FILE)) } catch (e: Exception) { - logger.log(Level.SEVERE, "Could not take over the configuration of '${legacyDirectory.fileName}'", e) + logger.error("Could not take over the configuration of '${legacyDirectory.fileName}'") return } @@ -58,7 +57,7 @@ object LegacyDirectoryMigrator { } private fun warnAboutUnusedDirectory(legacyDirectory: Path, dataDirectory: Path) { - logger.warning( + logger.warn( "The directory '${legacyDirectory.fileName}' is not used anymore. " + "Rename it to '${dataDirectory.fileName}' in your template to get rid of this warning." ) diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/controller/CloudControllerHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/controller/CloudControllerHandler.kt index 671b3aa..0544a25 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/controller/CloudControllerHandler.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/controller/CloudControllerHandler.kt @@ -10,14 +10,14 @@ import app.simplecloud.plugin.proxy.shared.joinstate.JoinStateService import app.simplecloud.plugin.proxy.shared.motd.MotdLayoutRepository import kotlinx.coroutines.* import kotlinx.coroutines.future.await -import java.util.logging.Logger +import org.slf4j.LoggerFactory class CloudControllerHandler( private val plugin: ProxyPlugin, private val service: JoinStateService ) { - private val logger = Logger.getLogger(CloudControllerHandler::class.java.name) + private val logger = LoggerFactory.getLogger(CloudControllerHandler::class.java) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) @Volatile @@ -26,7 +26,7 @@ class CloudControllerHandler( fun start() { if (SimpleCloudRuntime.serverId().isNullOrBlank()) { - logger.warning("Environment variable SIMPLECLOUD_UNIQUE_ID is not set.") + logger.warn("Environment variable SIMPLECLOUD_UNIQUE_ID is not set.") registerListeners() return } @@ -39,7 +39,7 @@ class CloudControllerHandler( registerListeners() initializeJoinState() } catch (e: Exception) { - logger.severe("Error retrieving server by ID: ${e.message}") + logger.error("Error retrieving server by ID", e) registerListeners() } } @@ -94,7 +94,7 @@ class CloudControllerHandler( plugin.api.server().updateServerProperties(serverId, mapOf(key to value)).await() true } catch (e: Exception) { - logger.severe("Error updating server property: ${e.message}") + logger.error("Error updating server property", e) false } } @@ -105,7 +105,7 @@ class CloudControllerHandler( return try { getGroupByName(groupName)?.properties?.get(key)?.toString().orNullIfBlank() } catch (e: Exception) { - logger.severe("Error retrieving group property: ${e.message}") + logger.error("Error retrieving group property", e) null } } @@ -117,7 +117,7 @@ class CloudControllerHandler( logger.info("Group property '$key' updated to '$value' for group '$groupName'") true } catch (e: Exception) { - logger.severe("Error updating group properties: ${e.message}") + logger.error("Error updating group properties", e) false } } @@ -126,7 +126,7 @@ class CloudControllerHandler( return try { plugin.api.group().allGroups.await().map { it.name } } catch (e: Exception) { - logger.severe("Error retrieving groups: ${e.message}") + logger.error("Error retrieving groups", e) emptyList() } } @@ -137,7 +137,7 @@ class CloudControllerHandler( return try { getServersByGroup(groupName).sumOf { it.playerCount ?: 0 } } catch (e: Exception) { - logger.severe("Error retrieving online players in group: ${e.message}") + logger.error("Error retrieving online players in group", e) 0 } } @@ -160,7 +160,7 @@ class CloudControllerHandler( plugin.api.persistentServer().allPersistentServers.await() .firstOrNull { it.name.equals(name, ignoreCase = true) } } catch (e: Exception) { - logger.severe("Error retrieving persistent server '$name': ${e.message}") + logger.error("Error retrieving persistent server '$name'", e) null } } @@ -169,7 +169,7 @@ class CloudControllerHandler( return try { getPersistentServerByName(name)?.properties?.get(key)?.toString().orNullIfBlank() } catch (e: Exception) { - logger.severe("Error retrieving persistent server property: ${e.message}") + logger.error("Error retrieving persistent server property", e) null } } @@ -183,7 +183,7 @@ class CloudControllerHandler( logger.info("Persistent server property '$key' updated to '$value' for persistent server '$name'") true } catch (e: Exception) { - logger.severe("Error updating persistent server property: ${e.message}") + logger.error("Error updating persistent server property", e) false } } @@ -192,7 +192,7 @@ class CloudControllerHandler( return try { plugin.api.persistentServer().allPersistentServers.await().map { it.name } } catch (e: Exception) { - logger.severe("Error retrieving persistent servers: ${e.message}") + logger.error("Error retrieving persistent servers", e) emptyList() } } @@ -203,7 +203,7 @@ class CloudControllerHandler( ServerQuery.create().filterByServerGroupName(resolveGroupNameCasing(groupName)) ).await() ?: emptyList() } catch (e: Exception) { - logger.severe("Error retrieving servers for group '$groupName': ${e.message}") + logger.error("Error retrieving servers for group '$groupName'", e) emptyList() } } @@ -216,7 +216,7 @@ class CloudControllerHandler( .filterByNumericalId(numericalId) ).await()?.firstOrNull() } catch (e: Exception) { - logger.severe("Error retrieving server '$groupName-$numericalId': ${e.message}") + logger.error("Error retrieving server '$groupName-$numericalId'", e) null } } @@ -230,7 +230,7 @@ class CloudControllerHandler( || it.persistentServer?.name.equals(name, ignoreCase = true) } } catch (e: Exception) { - logger.severe("Error retrieving server by name: ${e.message}") + logger.error("Error retrieving server by name", e) null } } @@ -247,7 +247,7 @@ class CloudControllerHandler( persistentServerName in names } } catch (e: Exception) { - logger.severe("Error retrieving persistent servers by name: ${e.message}") + logger.error("Error retrieving persistent servers by name", e) emptyList() } } @@ -260,7 +260,7 @@ class CloudControllerHandler( suspend fun updateServiceProperty(groupName: String, numericalId: Int, key: String, value: String): Boolean { val server = getServerByNumericalId(groupName, numericalId) if (server == null) { - logger.severe("Server not found for group '$groupName' and numerical ID '$numericalId'") + logger.error("Server not found for group '$groupName' and numerical ID '$numericalId'") return false } return updateServerProperty(server.serverId, key, value) @@ -289,7 +289,7 @@ class CloudControllerHandler( return try { plugin.api.group().allGroups.await().firstOrNull { it.name.equals(groupName, ignoreCase = true) } } catch (e: Exception) { - logger.severe("Error retrieving group '$groupName': ${e.message}") + logger.error("Error retrieving group '$groupName'", e) null } } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateResolver.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateResolver.kt index 30a59e2..bdcf6dc 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateResolver.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateResolver.kt @@ -3,13 +3,13 @@ package app.simplecloud.plugin.proxy.shared.joinstate import app.simplecloud.plugin.api.shared.pattern.ServerPatternIdentifier import app.simplecloud.plugin.proxy.shared.ProxyPlugin import app.simplecloud.plugin.proxy.shared.config.JoinState -import java.util.logging.Logger +import org.slf4j.LoggerFactory class JoinStateResolver( private val plugin: ProxyPlugin ) { - private val logger = Logger.getLogger(JoinStateResolver::class.java.name) + private val logger = LoggerFactory.getLogger(JoinStateResolver::class.java) private val identifier by lazy { ServerPatternIdentifier( diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateService.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateService.kt index 4b182ee..4f99c80 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateService.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateService.kt @@ -3,22 +3,15 @@ package app.simplecloud.plugin.proxy.shared.joinstate import app.simplecloud.api.runtime.SimpleCloudRuntime import app.simplecloud.api.server.Server import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import java.util.logging.Logger +import kotlinx.coroutines.* +import org.slf4j.LoggerFactory import kotlin.time.Duration.Companion.milliseconds class JoinStateService( private val plugin: ProxyPlugin ) { - private val logger = Logger.getLogger(JoinStateService::class.java.name) + private val logger = LoggerFactory.getLogger(JoinStateService::class.java) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private var syncJob: Job? = null @@ -53,7 +46,7 @@ class JoinStateService( val fallbackState = defaultJoinState() if (!setJoinStateAtGroup(groupName, fallbackState)) { - logger.warning("Could not persist default join state '$fallbackState' for group '$groupName'") + logger.warn("Could not persist default join state '$fallbackState' for group '$groupName'") } return fallbackState } @@ -80,7 +73,7 @@ class JoinStateService( val fallbackState = defaultJoinState() if (!setJoinStateAtPersistentServer(serverName, fallbackState)) { - logger.warning("Could not persist default join state '$fallbackState' for persistent server '$serverName'") + logger.warn("Could not persist default join state '$fallbackState' for persistent server '$serverName'") } return fallbackState } @@ -100,7 +93,7 @@ class JoinStateService( val successful = groupPropertyUpdated && servicePropertiesUpdated if (!successful) { - logger.severe("Error setting join state at group and all services in group $groupName.") + logger.error("Error setting join state at group and all services in group $groupName.") } return successful } @@ -115,7 +108,7 @@ class JoinStateService( try { syncLocalStateWithGroupState() } catch (e: Exception) { - logger.severe("Error while syncing local/group join state: ${e.message}") + logger.error("Error while syncing local/group join state", e) } delay(2000.milliseconds) } @@ -131,7 +124,7 @@ class JoinStateService( when { state.isNullOrBlank() -> { - logger.warning("No join state found for server. Using default join state.") + logger.warn("No join state found for server. Using default join state.") scope.launch { applyDefaultJoinStateForCurrentServer(server) } } @@ -171,7 +164,7 @@ class JoinStateService( controller().updateServerProperty(server.serverId, KEY, stateToApply) localState = stateToApply } catch (e: Exception) { - logger.severe("Error setting default join state: ${e.message}") + logger.error("Error setting default join state", e) } } @@ -206,7 +199,7 @@ class JoinStateService( private suspend fun applyGroupState(serverId: String, serviceName: String, groupState: String, reason: String) { if (!controller().updateServerProperty(serverId, KEY, groupState)) { - logger.warning("Could not synchronize join state for '$serviceName' with group state '$groupState'") + logger.warn("Could not synchronize join state for '$serviceName' with group state '$groupState'") return } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/ProxyJoinGate.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/ProxyJoinGate.kt index 597489b..b7764a3 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/ProxyJoinGate.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/ProxyJoinGate.kt @@ -1,13 +1,13 @@ package app.simplecloud.plugin.proxy.shared.joinstate import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import java.util.logging.Logger +import org.slf4j.LoggerFactory class ProxyJoinGate( private val plugin: ProxyPlugin ) { - private val logger = Logger.getLogger(ProxyJoinGate::class.java.name) + private val logger = LoggerFactory.getLogger(ProxyJoinGate::class.java) sealed interface Result { data object Allowed : Result @@ -20,7 +20,7 @@ class ProxyJoinGate( val joinState = plugin.joinStateResolver.resolveJoinState(stateName) if (joinState == null) { - logger.severe("Neither join state '$stateName' nor default state found. Check configuration!") + logger.error("Neither join state '$stateName' nor default state found. Check configuration!") return Result.Denied(kickMessages.noJoinState) } @@ -41,7 +41,7 @@ class ProxyJoinGate( return try { plugin.joinStateResolver.isServerFull() && !hasPermission(fullPermission) } catch (e: Exception) { - logger.severe("Error checking player limits: ${e.message}") + logger.error("Error checking player limits", e) false } } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/DomainMotdResolver.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/DomainMotdResolver.kt index a2c3e0c..40b01ec 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/DomainMotdResolver.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/DomainMotdResolver.kt @@ -10,15 +10,15 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import org.slf4j.LoggerFactory import java.util.concurrent.ConcurrentHashMap -import java.util.logging.Logger import kotlin.time.Duration.Companion.milliseconds class DomainMotdResolver( private val plugin: ProxyPlugin ) { - private val logger = Logger.getLogger(DomainMotdResolver::class.java.name) + private val logger = LoggerFactory.getLogger(DomainMotdResolver::class.java) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private var syncJob: Job? = null @@ -34,7 +34,7 @@ class DomainMotdResolver( try { syncAllRoutes() } catch (e: Exception) { - logger.severe("Error while syncing domain MOTD routes: ${e.message}") + logger.error("Error while syncing domain MOTD routes", e) } delay(3000.milliseconds) } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/MotdLayoutRepository.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/MotdLayoutRepository.kt index 4cdddeb..ad8adf5 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/MotdLayoutRepository.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/MotdLayoutRepository.kt @@ -5,13 +5,12 @@ import app.simplecloud.plugin.proxy.shared.ProxyPlugin import app.simplecloud.plugin.proxy.shared.config.MotdEntry import app.simplecloud.plugin.proxy.shared.config.MotdLayoutConfiguration import app.simplecloud.plugin.proxy.shared.config.MotdUpdateType +import org.slf4j.LoggerFactory import java.nio.file.Files import java.nio.file.Path import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger -import java.util.logging.Level -import java.util.logging.Logger class MotdLayoutRepository( private val directory: Path, @@ -21,7 +20,7 @@ class MotdLayoutRepository( MotdLayoutConfiguration::class.java, ) { - private val logger = Logger.getLogger(MotdLayoutRepository::class.java.name) + private val logger = LoggerFactory.getLogger(MotdLayoutRepository::class.java) private val layoutsByName = ConcurrentHashMap() private val queueIndexes = ConcurrentHashMap() private val listenerRegistered = AtomicBoolean(false) @@ -120,7 +119,7 @@ class MotdLayoutRepository( return try { load(directory.resolve("$name$SUFFIX").toFile()) } catch (e: Exception) { - logger.log(Level.WARNING, "Could not load MOTD layout $name$SUFFIX", e) + logger.warn("Could not load MOTD layout $name$SUFFIX", e) null } } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/player/PlayerCountTracker.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/player/PlayerCountTracker.kt index 0dc3d70..4085658 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/player/PlayerCountTracker.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/player/PlayerCountTracker.kt @@ -3,14 +3,14 @@ package app.simplecloud.plugin.proxy.shared.player import app.simplecloud.plugin.proxy.shared.ProxyPlugin import app.simplecloud.plugin.proxy.shared.config.PlayerCountConfig import kotlinx.coroutines.* -import java.util.logging.Logger +import org.slf4j.LoggerFactory import kotlin.time.Duration.Companion.milliseconds class PlayerCountTracker( private val plugin: ProxyPlugin ) { - private val logger = Logger.getLogger(PlayerCountTracker::class.java.name) + private val logger = LoggerFactory.getLogger(PlayerCountTracker::class.java) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private var syncJob: Job? = null @@ -56,7 +56,7 @@ class PlayerCountTracker( try { snapshot = resolveSnapshot() } catch (e: Exception) { - logger.severe("Error while syncing player count: ${e.message}") + logger.error("Error while syncing player count", e) } } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MinecraftTextWidth.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MinecraftTextWidth.kt index f8d0bb0..087690e 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MinecraftTextWidth.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MinecraftTextWidth.kt @@ -14,6 +14,7 @@ object MinecraftTextWidth { const val ZERO_WIDTH_NON_JOINER = 0x200c private const val RESOURCE_DIRECTORY = "/app/simplecloud/plugin/proxy/shared/format" + private const val UNIFONT_ADVANCES_RESOURCE = "unifont-advances.bin" private const val MISSING_GLYPH_WIDTH = 6f private const val OBJECT_WIDTH = 8f private const val SPACE_WIDTH = 4f @@ -27,14 +28,14 @@ object MinecraftTextWidth { private val defaultWidths by lazy { loadWidths("default-font-widths.txt") } private val altWidths by lazy { loadWidths("alt-font-widths.txt") } - private val unifontAdvances by lazy { loadAdvances("unifont-advances.bin") } + private val unifontAdvances by lazy { loadAdvances() } fun width(component: Component, inheritedStyle: Style = Style.empty()): Float { val style = component.style().merge(inheritedStyle, Style.Merge.Strategy.IF_ABSENT_ON_TARGET) val ownWidth = when (component) { is TextComponent -> textWidth(component.content(), style) is ObjectComponent -> OBJECT_WIDTH + objectBoldOffset(style) - is TranslatableComponent -> textWidth(component.fallback() ?: component.key(), style) + is TranslatableComponent -> textWidth(component.fallback(), style) is KeybindComponent -> textWidth(component.keybind(), style) else -> 0f } @@ -96,11 +97,11 @@ object MinecraftTextWidth { } } - private fun loadAdvances(resourceName: String): ByteArray { - val advances = openResource(resourceName).use { it.readBytes() } + private fun loadAdvances(): ByteArray { + val advances = openResource(UNIFONT_ADVANCES_RESOURCE).use { it.readBytes() } check(advances.size == UNICODE_CODE_POINT_COUNT) { - "Invalid font metrics resource $resourceName: expected $UNICODE_CODE_POINT_COUNT bytes, got ${advances.size}" + "Invalid font metrics resource $UNIFONT_ADVANCES_RESOURCE: expected $UNICODE_CODE_POINT_COUNT bytes, got ${advances.size}" } return advances } diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/ProxyVelocityPlugin.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/ProxyVelocityPlugin.kt index 622813f..841d29f 100644 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/ProxyVelocityPlugin.kt +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/ProxyVelocityPlugin.kt @@ -5,13 +5,12 @@ import app.simplecloud.plugin.proxy.shared.ProxyPlugin import app.simplecloud.plugin.proxy.shared.command.ProxyEssentialsCommandHandler import app.simplecloud.plugin.proxy.shared.command.commands.JoinStateCommandHandler import app.simplecloud.plugin.proxy.shared.command.commands.LayoutCommandHandler +import app.simplecloud.plugin.proxy.shared.placeholder.TagResolverHelper import app.simplecloud.plugin.proxy.shared.utilities.format.MotdMiniMessageFormatter import app.simplecloud.plugin.proxy.velocity.listener.LoginListener import app.simplecloud.plugin.proxy.velocity.listener.ServerKickListener import app.simplecloud.plugin.proxy.velocity.listener.ServerPreConnectListener import app.simplecloud.plugin.proxy.velocity.listener.ProxyPingListener -import app.simplecloud.plugin.proxy.velocity.placeholder.ConfigureTagResolversEvent -import app.simplecloud.plugin.proxy.velocity.placeholder.ConfigureTagResolversListener import app.simplecloud.plugin.proxy.velocity.tablist.TabListHandler import com.google.inject.Inject import com.velocitypowered.api.event.Subscribe @@ -23,12 +22,14 @@ import com.velocitypowered.api.plugin.annotation.DataDirectory import com.velocitypowered.api.proxy.Player import com.velocitypowered.api.proxy.ProxyServer import net.kyori.adventure.text.Component +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver import org.incendo.cloud.SenderMapper import org.incendo.cloud.execution.ExecutionCoordinator import org.incendo.cloud.velocity.VelocityCommandManager -import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.nio.file.Path import kotlin.io.path.pathString +import kotlin.jvm.optionals.getOrNull @Plugin( id = "simplecloud-proxy-essentials", @@ -44,9 +45,9 @@ import kotlin.io.path.pathString class ProxyVelocityPlugin @Inject constructor( val server: ProxyServer, @DataDirectory val dataDirectory: Path, - val logger: Logger, ) { + private val logger = LoggerFactory.getLogger(ProxyVelocityPlugin::class.java) val plugin = ProxyPlugin(dataDirectory.pathString) val tabListHandler = TabListHandler(plugin, this, server) @@ -66,20 +67,17 @@ class ProxyVelocityPlugin @Inject constructor( } fun deserializeToComponent(text: String, player: Player? = null): Component { - val tagResolvers = fireConfigureTagResolvers(player) - return miniMessage.deserialize(text, *tagResolvers.toTypedArray()) + return miniMessage.deserialize(text, *tagResolvers(player).toTypedArray()) } fun deserializeMotd(line1: String, line2: String): Component { - val tagResolvers = fireConfigureTagResolvers(null) - return MotdMiniMessageFormatter.deserialize(miniMessage, line1, line2, tagResolvers) + return MotdMiniMessageFormatter.deserialize(miniMessage, line1, line2, tagResolvers(null)) } private fun registerListeners() { val manager = server.eventManager manager.register(this, ProxyPingListener(plugin, this)) - manager.register(this, ConfigureTagResolversListener(plugin, this)) manager.register(this, LoginListener(plugin, this)) manager.register(this, ServerPreConnectListener(plugin, this)) manager.register(this, ServerKickListener(plugin)) @@ -110,5 +108,16 @@ class ProxyVelocityPlugin @Inject constructor( tabListHandler.startTabListTask() } - private fun fireConfigureTagResolvers(player: Player?) = server.eventManager.fire(ConfigureTagResolversEvent(player)).get().tagResolvers + private fun tagResolvers(player: Player?): List { + val playerCountTracker = plugin.playerCountTracker + + return TagResolverHelper.getDefaultTagResolvers( + serverName = player?.currentServer?.getOrNull()?.serverInfo?.name ?: "unknown", + ping = player?.ping ?: -1, + pingColors = plugin.placeholderConfig.get().pingColors, + onlinePlayers = playerCountTracker.onlinePlayers(server.allPlayers.size), + realMaxPlayers = playerCountTracker.maxPlayers(server.configuration.showMaxPlayers), + motdConfiguration = plugin.layoutRepository.getCurrentMotdLayout() + ) + } } diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ProxyPingListener.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ProxyPingListener.kt index 8087fdf..54da395 100644 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ProxyPingListener.kt +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ProxyPingListener.kt @@ -9,6 +9,7 @@ import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.proxy.ProxyPingEvent import com.velocitypowered.api.proxy.server.ServerPing import com.velocitypowered.api.util.Favicon +import org.slf4j.LoggerFactory import java.nio.file.Path import java.util.UUID import kotlin.jvm.optionals.getOrNull @@ -18,6 +19,7 @@ class ProxyPingListener( private val plugin: ProxyVelocityPlugin ) { + private val logger = LoggerFactory.getLogger(ProxyPingListener::class.java) private val serverIconLoader = ServerIconLoader(Path.of(proxyPlugin.serverIconsPath)) { image -> Favicon.create(image) } @@ -57,7 +59,7 @@ class ProxyPingListener( private fun applyServerIcon(builder: ServerPing.Builder, layout: MotdLayoutConfiguration) { if (!layout.serverIcon.enabled) return - val favicon = serverIconLoader.get(layout.serverIcon.file) { plugin.logger.warn(it) } ?: return + val favicon = serverIconLoader.get(layout.serverIcon.file) { logger.warn(it) } ?: return builder.favicon(favicon) } diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ServerPreConnectListener.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ServerPreConnectListener.kt index 2bb991a..107fc2f 100644 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ServerPreConnectListener.kt +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ServerPreConnectListener.kt @@ -8,14 +8,14 @@ import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.player.ServerPreConnectEvent import com.velocitypowered.api.proxy.Player import kotlinx.coroutines.runBlocking -import java.util.logging.Logger +import org.slf4j.LoggerFactory class ServerPreConnectListener( private val proxyPlugin: ProxyPlugin, private val plugin: ProxyVelocityPlugin ) { - private val logger = Logger.getLogger(ServerPreConnectListener::class.java.name) + private val logger = LoggerFactory.getLogger(ServerPreConnectListener::class.java) @Subscribe(order = PostOrder.EARLY) fun handle(event: ServerPreConnectEvent) { @@ -51,7 +51,7 @@ class ServerPreConnectListener( val kickMessages = proxyPlugin.messageConfig.get().kick if (joinState == null) { - logger.severe("Neither join state '$joinStateName' nor default state found. Check configuration!") + logger.error("Neither join state '$joinStateName' nor default state found. Check configuration!") denyServerSwitch(player, kickMessages.noJoinState, event) return@runBlocking } diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/placeholder/ConfigureTagResolversEvent.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/placeholder/ConfigureTagResolversEvent.kt deleted file mode 100644 index 172f498..0000000 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/placeholder/ConfigureTagResolversEvent.kt +++ /dev/null @@ -1,19 +0,0 @@ -package app.simplecloud.plugin.proxy.velocity.placeholder - -import com.velocitypowered.api.proxy.Player -import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver - -class ConfigureTagResolversEvent( - val player: Player? = null, - val tagResolvers: MutableList = mutableListOf() -) { - - fun withTagResolvers(vararg tagResolvers: TagResolver): ConfigureTagResolversEvent { - return withTagResolvers(tagResolvers.toList()) - } - - fun withTagResolvers(tagResolvers: List): ConfigureTagResolversEvent { - this.tagResolvers.addAll(tagResolvers) - return this - } -} diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/placeholder/ConfigureTagResolversListener.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/placeholder/ConfigureTagResolversListener.kt deleted file mode 100644 index cf3bd7c..0000000 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/placeholder/ConfigureTagResolversListener.kt +++ /dev/null @@ -1,31 +0,0 @@ -package app.simplecloud.plugin.proxy.velocity.placeholder - -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.placeholder.TagResolverHelper -import app.simplecloud.plugin.proxy.velocity.ProxyVelocityPlugin -import com.velocitypowered.api.event.PostOrder -import com.velocitypowered.api.event.Subscribe -import kotlin.jvm.optionals.getOrNull - -class ConfigureTagResolversListener( - private val proxyPlugin: ProxyPlugin, - private val plugin: ProxyVelocityPlugin -) { - - @Subscribe(order = PostOrder.FIRST) - fun onConfigureTagResolvers(event: ConfigureTagResolversEvent) { - val player = event.player - val playerCountHandler = proxyPlugin.playerCountTracker - - event.withTagResolvers( - TagResolverHelper.getDefaultTagResolvers( - serverName = player?.currentServer?.getOrNull()?.serverInfo?.name ?: "unknown", - ping = player?.ping ?: -1, - pingColors = proxyPlugin.placeholderConfig.get().pingColors, - onlinePlayers = playerCountHandler.onlinePlayers(plugin.server.allPlayers.size), - realMaxPlayers = playerCountHandler.maxPlayers(plugin.server.configuration.showMaxPlayers), - motdConfiguration = proxyPlugin.layoutRepository.getCurrentMotdLayout() - ) - ) - } -} diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/tablist/TabListHandler.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/tablist/TabListHandler.kt index 8c0f221..912fbef 100644 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/tablist/TabListHandler.kt +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/tablist/TabListHandler.kt @@ -5,8 +5,8 @@ import app.simplecloud.plugin.proxy.velocity.ProxyVelocityPlugin import com.velocitypowered.api.proxy.Player import com.velocitypowered.api.proxy.ProxyServer import com.velocitypowered.api.scheduler.ScheduledTask +import org.slf4j.LoggerFactory import java.util.concurrent.TimeUnit -import java.util.logging.Logger import kotlin.jvm.optionals.getOrNull class TabListHandler( @@ -15,7 +15,7 @@ class TabListHandler( private val proxyServer: ProxyServer ) { - private val logger = Logger.getLogger(TabListHandler::class.java.name) + private val logger = LoggerFactory.getLogger(TabListHandler::class.java) private var task: ScheduledTask? = null fun startTabListTask() { @@ -30,7 +30,7 @@ class TabListHandler( fun stopTabListTask() { val runningTask = task if (runningTask == null) { - logger.warning("Can't stop tablist task because it is not initialized") + logger.warn("Can't stop tablist task because it is not initialized") return } From 2f807315858039c921ebf4b4d684e64471ee9f0c Mon Sep 17 00:00:00 2001 From: Janis Date: Wed, 26 Aug 2026 17:14:12 +0200 Subject: [PATCH 07/17] refactor: update release workflow --- .../{release-dev.yml => release.yml} | 49 +++++++------------ 1 file changed, 19 insertions(+), 30 deletions(-) rename .github/workflows/{release-dev.yml => release.yml} (59%) diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release.yml similarity index 59% rename from .github/workflows/release-dev.yml rename to .github/workflows/release.yml index 84ee52f..857c22e 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release.yml @@ -13,29 +13,26 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v7 with: ref: main + submodules: recursive - - name: Set up JDK 21 - uses: actions/setup-java@v3 + - name: Set up JDK 25 + uses: actions/setup-java@v5 with: - distribution: 'adopt' - java-version: '21' - - - name: Cache Gradle packages - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - gradle-${{ runner.os }} + distribution: 'temurin' + java-version: '25' - name: Make gradlew executable run: chmod +x ./gradlew + - name: Print Gradle Wrapper Properties + run: cat gradle/wrapper/gradle-wrapper.properties + + - name: Clear Gradle Cache + run: rm -rf ~/.gradle/caches && rm -rf ~/.gradle/wrapper + - name: Build ShadowJars run: ./gradlew clean build shadowJar @@ -53,21 +50,8 @@ jobs: MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} - name: Create Release - id: create_release - uses: actions/create-release@v1 - with: - tag_name: v${{ env.GRADLE_VERSION }} - release_name: v${{ env.GRADLE_VERSION }} - draft: false - prerelease: false - commitish: main - body: | - This release contains release builds for all Gradle modules. - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload ShadowJars to Release run: | + jars=() # Find JAR files in any submodule's build/libs directory for jar in $(find . -type f -name "*.jar" -path "*/build/libs/*.jar" -not -path "./build/libs/*"); do # Check if the filename contains a version number (e.g., a dash followed by numbers) @@ -75,8 +59,13 @@ jobs: echo "Skipping $jar due to version number" else echo "Uploading $jar" - gh release upload v${{ env.GRADLE_VERSION }} "$jar" + jars+=("$jar") fi done + gh release create "v${GRADLE_VERSION}" \ + --target main \ + --title "v${GRADLE_VERSION}" \ + --notes "This release contains release builds for all Gradle modules." \ + "${jars[@]}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 8dc71be88f7ea6578ddf5f296af024a0ae4dce7a Mon Sep 17 00:00:00 2001 From: Janis Date: Fri, 2 Oct 2026 22:49:44 +0200 Subject: [PATCH 08/17] refactor: rework --- .../bungeecord/BungeeCordCommandSender.kt | 16 - .../proxy/bungeecord/ComponentExtension.kt | 9 - .../proxy/bungeecord/ProxyBungeeCordPlugin.kt | 112 ++----- .../plugin/proxy/bungeecord/TabListHandler.kt | 37 +++ .../command/BungeeCordCommandSender.kt | 15 + .../bungeecord/listener/PostLoginListener.kt | 18 +- .../bungeecord/listener/ProxyPingListener.kt | 87 ++--- .../bungeecord/listener/ServerKickListener.kt | 12 +- .../listener/ServerPreConnectListener.kt | 77 +---- .../{tablist => listener}/TabListListener.kt | 8 +- .../platform/BungeeCordPlatformImpl.kt | 17 + .../bungeecord/tablist/TabListHandler.kt | 55 ---- .../resources/config/layout/maintenance.yml | 50 --- .../main/resources/config/layout/public.yml | 50 --- .../src/main/resources/config/placeholder.yml | 13 - .../plugin/proxy/shared/ProxyEssentials.kt | 87 +++++ .../plugin/proxy/shared/ProxyPlatform.kt | 12 + .../plugin/proxy/shared/ProxyPlugin.kt | 81 ----- .../proxy/shared/command/JoinStateCommand.kt | 209 ++++++++++++ .../proxy/shared/command/LayoutCommand.kt | 130 ++++++++ .../proxy/shared/command/ProxyCommand.kt | 62 ++++ .../shared/command/ProxyCommandSender.kt | 7 - .../command/ProxyEssentialsCommandHandler.kt | 89 ----- .../proxy/shared/command/ProxySender.kt | 8 + .../commands/JoinStateCommandHandler.kt | 272 ---------------- .../command/commands/LayoutCommandHandler.kt | 180 ----------- .../proxy/shared/config/ConfigVersion.kt | 6 + ...LayoutConfiguration.kt => LayoutConfig.kt} | 7 +- .../proxy/shared/config/MessageConfig.kt | 139 ++++---- .../shared/config/PlaceHolderConfiguration.kt | 17 - .../shared/config/ProxyEssentialsConfig.kt | 77 +++-- .../{ => migration}/DefaultConfigInstaller.kt | 3 +- .../migration/LegacyDirectoryMigrator.kt | 74 +---- .../config/migration/OldConfigMigrator.kt | 20 +- .../controller/CloudControllerHandler.kt | 304 ------------------ .../plugin/proxy/shared/joinstate/JoinGate.kt | 91 ++++++ .../proxy/shared/joinstate/JoinResult.kt | 8 + .../shared/joinstate/JoinStateResolver.kt | 79 ----- .../shared/joinstate/JoinStateService.kt | 258 ++++++--------- .../proxy/shared/joinstate/ProxyJoinGate.kt | 48 --- .../proxy/shared/layout/LayoutRepository.kt | 48 +++ .../proxy/shared/layout/LayoutService.kt | 139 ++++++++ .../proxy/shared/layout/ServerIconCache.kt | 79 +++++ .../proxy/shared/motd/DomainMotdResolver.kt | 108 ------- .../proxy/shared/motd/MotdLayoutRepository.kt | 137 -------- .../proxy/shared/motd/ServerIconLoader.kt | 117 ------- .../shared/placeholder/TagResolverHelper.kt | 67 ---- .../shared/placeholder/TagResolverNames.kt | 12 - .../proxy/shared/player/PlayerCountService.kt | 77 +++++ .../proxy/shared/player/PlayerCountTracker.kt | 153 --------- .../proxy/shared/tablist/TabListResolver.kt | 42 --- .../proxy/shared/tablist/TabListService.kt | 33 ++ ...xyPermissions.kt => CommandPermissions.kt} | 6 +- .../shared/utilities/config/ConfigVersion.kt | 7 - .../shared/utilities/config/DefaultConfigs.kt | 40 --- .../shared/utilities/text/MessageFormatter.kt | 68 ++++ .../{format => text}/MinecraftTextWidth.kt | 4 +- .../MotdMiniMessageFormatter.kt | 6 +- .../src/main/resources/config/config.yml | 22 +- .../resources/config/layout/maintenance.yml | 5 +- .../main/resources/config/layout/public.yml | 5 +- .../layout/server-icons/simplecloud-gray.png | Bin .../layout/server-icons/simplecloud.png | Bin .../src/main/resources/config/messages.yml | 30 +- .../format => widths}/alt-font-widths.txt | 0 .../format => widths}/default-font-widths.txt | 0 .../format => widths}/unifont-advances.bin | 0 .../proxy/velocity/ProxyVelocityPlugin.kt | 109 ++----- .../plugin/proxy/velocity/TabListHandler.kt | 42 +++ .../proxy/velocity/VelocityCommandSender.kt | 12 - .../velocity/command/VelocityCommandSender.kt | 14 + .../proxy/velocity/listener/LoginListener.kt | 20 +- .../velocity/listener/ProxyPingListener.kt | 89 ++--- .../velocity/listener/ServerKickListener.kt | 12 +- .../listener/ServerPreConnectListener.kt | 70 +--- .../velocity/listener/TabListListener.kt | 15 + .../velocity/platform/VelocityPlatformImpl.kt | 17 + .../proxy/velocity/tablist/TabListHandler.kt | 56 ---- .../src/main/resources/config/config.yml | 69 ---- .../layout/server-icons/simplecloud-gray.png | Bin 886 -> 0 bytes .../layout/server-icons/simplecloud.png | Bin 771 -> 0 bytes .../src/main/resources/config/messages.yml | 64 ---- .../src/main/resources/config/placeholder.yml | 13 - 83 files changed, 1677 insertions(+), 2974 deletions(-) delete mode 100644 proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/BungeeCordCommandSender.kt delete mode 100644 proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ComponentExtension.kt create mode 100644 proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/TabListHandler.kt create mode 100644 proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/command/BungeeCordCommandSender.kt rename proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/{tablist => listener}/TabListListener.kt (52%) create mode 100644 proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/platform/BungeeCordPlatformImpl.kt delete mode 100644 proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/tablist/TabListHandler.kt delete mode 100644 proxy-bungeecord/src/main/resources/config/layout/maintenance.yml delete mode 100644 proxy-bungeecord/src/main/resources/config/layout/public.yml delete mode 100644 proxy-bungeecord/src/main/resources/config/placeholder.yml create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyEssentials.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlatform.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/JoinStateCommand.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/LayoutCommand.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyCommand.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyCommandSender.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyEssentialsCommandHandler.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxySender.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/JoinStateCommandHandler.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/LayoutCommandHandler.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ConfigVersion.kt rename proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/{MotdLayoutConfiguration.kt => LayoutConfig.kt} (96%) delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/PlaceHolderConfiguration.kt rename proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/{ => migration}/DefaultConfigInstaller.kt (98%) delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/controller/CloudControllerHandler.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinGate.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinResult.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateResolver.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/ProxyJoinGate.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/layout/LayoutRepository.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/layout/LayoutService.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/layout/ServerIconCache.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/DomainMotdResolver.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/MotdLayoutRepository.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/ServerIconLoader.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/placeholder/TagResolverHelper.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/placeholder/TagResolverNames.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/player/PlayerCountService.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/player/PlayerCountTracker.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/tablist/TabListResolver.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/tablist/TabListService.kt rename proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/{ProxyPermissions.kt => CommandPermissions.kt} (77%) delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/config/ConfigVersion.kt delete mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/config/DefaultConfigs.kt create mode 100644 proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/text/MessageFormatter.kt rename proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/{format => text}/MinecraftTextWidth.kt (96%) rename proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/{format => text}/MotdMiniMessageFormatter.kt (92%) rename {proxy-bungeecord => proxy-shared}/src/main/resources/config/config.yml (82%) rename {proxy-velocity => proxy-shared}/src/main/resources/config/layout/maintenance.yml (90%) rename {proxy-velocity => proxy-shared}/src/main/resources/config/layout/public.yml (90%) rename {proxy-bungeecord => proxy-shared}/src/main/resources/config/layout/server-icons/simplecloud-gray.png (100%) rename {proxy-bungeecord => proxy-shared}/src/main/resources/config/layout/server-icons/simplecloud.png (100%) rename {proxy-bungeecord => proxy-shared}/src/main/resources/config/messages.yml (73%) rename proxy-shared/src/main/resources/{app/simplecloud/plugin/proxy/shared/format => widths}/alt-font-widths.txt (100%) rename proxy-shared/src/main/resources/{app/simplecloud/plugin/proxy/shared/format => widths}/default-font-widths.txt (100%) rename proxy-shared/src/main/resources/{app/simplecloud/plugin/proxy/shared/format => widths}/unifont-advances.bin (100%) create mode 100644 proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/TabListHandler.kt delete mode 100644 proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/VelocityCommandSender.kt create mode 100644 proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/command/VelocityCommandSender.kt create mode 100644 proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/TabListListener.kt create mode 100644 proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/platform/VelocityPlatformImpl.kt delete mode 100644 proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/tablist/TabListHandler.kt delete mode 100644 proxy-velocity/src/main/resources/config/config.yml delete mode 100644 proxy-velocity/src/main/resources/config/layout/server-icons/simplecloud-gray.png delete mode 100644 proxy-velocity/src/main/resources/config/layout/server-icons/simplecloud.png delete mode 100644 proxy-velocity/src/main/resources/config/messages.yml delete mode 100644 proxy-velocity/src/main/resources/config/placeholder.yml diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/BungeeCordCommandSender.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/BungeeCordCommandSender.kt deleted file mode 100644 index f20db77..0000000 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/BungeeCordCommandSender.kt +++ /dev/null @@ -1,16 +0,0 @@ -package app.simplecloud.plugin.proxy.bungeecord - -import app.simplecloud.plugin.proxy.shared.command.ProxyCommandSender -import net.kyori.adventure.platform.bungeecord.BungeeAudiences -import net.kyori.adventure.text.minimessage.MiniMessage -import net.md_5.bungee.api.CommandSender - -class BungeeCordCommandSender( - val commandSender: CommandSender, - private val adventure: BungeeAudiences? -) : ProxyCommandSender { - - override fun sendMessage(message: String) { - adventure?.sender(commandSender)?.sendMessage(MiniMessage.miniMessage().deserialize(message)) - } -} diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ComponentExtension.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ComponentExtension.kt deleted file mode 100644 index c0da8d5..0000000 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ComponentExtension.kt +++ /dev/null @@ -1,9 +0,0 @@ -package app.simplecloud.plugin.proxy.bungeecord - -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer -import net.md_5.bungee.api.chat.BaseComponent - -fun Component.toBaseComponent(): BaseComponent { - return BungeeComponentSerializer.get().serialize(this)[0] -} diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ProxyBungeeCordPlugin.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ProxyBungeeCordPlugin.kt index 52b30a6..de80e27 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ProxyBungeeCordPlugin.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ProxyBungeeCordPlugin.kt @@ -1,24 +1,13 @@ package app.simplecloud.plugin.proxy.bungeecord -import app.simplecloud.plugin.api.shared.extension.miniMessage +import app.simplecloud.plugin.proxy.bungeecord.command.BungeeCordCommandSender import app.simplecloud.plugin.proxy.bungeecord.listener.PostLoginListener +import app.simplecloud.plugin.proxy.bungeecord.listener.ProxyPingListener import app.simplecloud.plugin.proxy.bungeecord.listener.ServerKickListener import app.simplecloud.plugin.proxy.bungeecord.listener.ServerPreConnectListener -import app.simplecloud.plugin.proxy.bungeecord.listener.ProxyPingListener -import app.simplecloud.plugin.proxy.bungeecord.tablist.TabListHandler -import app.simplecloud.plugin.proxy.bungeecord.tablist.TabListListener -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.command.ProxyCommandSender -import app.simplecloud.plugin.proxy.shared.command.ProxyEssentialsCommandHandler -import app.simplecloud.plugin.proxy.shared.command.commands.JoinStateCommandHandler -import app.simplecloud.plugin.proxy.shared.command.commands.LayoutCommandHandler -import app.simplecloud.plugin.proxy.shared.placeholder.TagResolverHelper -import app.simplecloud.plugin.proxy.shared.utilities.format.MotdMiniMessageFormatter -import net.kyori.adventure.platform.bungeecord.BungeeAudiences -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver -import net.md_5.bungee.api.CommandSender -import net.md_5.bungee.api.connection.ProxiedPlayer +import app.simplecloud.plugin.proxy.bungeecord.listener.TabListListener +import app.simplecloud.plugin.proxy.bungeecord.platform.BungeeCordPlatformImpl +import app.simplecloud.plugin.proxy.shared.ProxyEssentials import net.md_5.bungee.api.plugin.Plugin import org.incendo.cloud.SenderMapper import org.incendo.cloud.bungee.BungeeCommandManager @@ -26,83 +15,30 @@ import org.incendo.cloud.execution.ExecutionCoordinator class ProxyBungeeCordPlugin : Plugin() { - val proxyPlugin = ProxyPlugin(dataFolder.path) - val tabListHandler = TabListHandler(this) - - private var adventure: BungeeAudiences? = null + private val essentials = ProxyEssentials(BungeeCordPlatformImpl(proxy, dataFolder.toPath())) + private val tabListHandler = TabListHandler(this, essentials) override fun onEnable() { - adventure = BungeeAudiences.create(this) - proxyPlugin.start() - - registerListeners() - registerCommands() - startTabListTask() - } - - override fun onDisable() { - adventure?.close() - adventure = null - tabListHandler.stopTabListTask() - proxyPlugin.shutdown() - } - - fun adventure(): BungeeAudiences { - return adventure ?: error("Cannot retrieve audience provider while plugin is not enabled") - } - - fun deserializeToComponent(text: String, player: ProxiedPlayer? = null): Component { - return miniMessage.deserialize(text, *tagResolvers(player).toTypedArray()) - } - - fun deserializeMotd(line1: String, line2: String): Component { - return MotdMiniMessageFormatter.deserialize(miniMessage, line1, line2, tagResolvers(null)) - } - - private fun registerListeners() { - val manager = proxy.pluginManager - - manager.registerListener(this, ProxyPingListener(this)) - manager.registerListener(this, PostLoginListener(this)) - manager.registerListener(this, ServerPreConnectListener(this)) - manager.registerListener(this, ServerKickListener(proxyPlugin)) - manager.registerListener(this, TabListListener(this)) - } - - private fun registerCommands() { - val commandManager = BungeeCommandManager( - this, - ExecutionCoordinator.asyncCoordinator(), - SenderMapper.create( - { commandSender -> BungeeCordCommandSender(commandSender, adventure) }, - { cloudSender -> (cloudSender as BungeeCordCommandSender).commandSender } + essentials.start() + + proxy.pluginManager.registerListener(this, PostLoginListener(essentials)) + proxy.pluginManager.registerListener(this, ServerPreConnectListener(essentials)) + proxy.pluginManager.registerListener(this, ProxyPingListener(essentials)) + proxy.pluginManager.registerListener(this, ServerKickListener(essentials)) + proxy.pluginManager.registerListener(this, TabListListener(tabListHandler)) + + essentials.registerCommands( + BungeeCommandManager( + this, + ExecutionCoordinator.asyncCoordinator(), + SenderMapper.create({ BungeeCordCommandSender(it) }, { it.commandSender }) ) ) - - ProxyEssentialsCommandHandler(commandManager, proxyPlugin).loadCommands() - JoinStateCommandHandler(commandManager, proxyPlugin).loadCommands() - LayoutCommandHandler(commandManager, proxyPlugin).loadCommands() + tabListHandler.start() } - private fun startTabListTask() { - if (proxyPlugin.config.get().tabListUpdateTimeMillis() <= 0) { - logger.info("Tablist update time is set to 0, tablist will not be updated automatically") - return - } - - tabListHandler.startTabListTask() - } - - private fun tagResolvers(player: ProxiedPlayer?): List { - val playerCountTracker = proxyPlugin.playerCountTracker - - return TagResolverHelper.getDefaultTagResolvers( - serverName = player?.server?.info?.name ?: "unknown", - ping = (player?.ping ?: -1).toLong(), - pingColors = proxyPlugin.placeholderConfig.get().pingColors, - onlinePlayers = playerCountTracker.onlinePlayers(proxy.players.size), - realMaxPlayers = playerCountTracker.maxPlayers(proxy.config.playerLimit), - motdConfiguration = proxyPlugin.layoutRepository.getCurrentMotdLayout() - ) + override fun onDisable() { + proxy.scheduler.cancel(this) + essentials.stop() } } diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/TabListHandler.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/TabListHandler.kt new file mode 100644 index 0000000..0f842d0 --- /dev/null +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/TabListHandler.kt @@ -0,0 +1,37 @@ +package app.simplecloud.plugin.proxy.bungeecord + +import app.simplecloud.plugin.proxy.shared.ProxyEssentials +import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer +import net.md_5.bungee.api.connection.ProxiedPlayer +import java.util.concurrent.TimeUnit + +class TabListHandler( + private val plugin: ProxyBungeeCordPlugin, + private val essentials: ProxyEssentials +) { + + fun start() { + val interval = essentials.tabListService.getUpdateIntervalMillis() + plugin.proxy.scheduler.schedule(plugin, { update(interval != null) }, interval ?: 1000L, TimeUnit.MILLISECONDS) + } + + fun update(player: ProxiedPlayer) { + val serverName = player.server?.info?.name ?: return + val tabList = essentials.tabListService.getTabList(serverName) ?: return + val formatter = essentials.messageFormatter + val serializer = BungeeComponentSerializer.get() + + player.setTabHeader( + serializer.serialize(formatter.formatForPlayer(tabList.header, serverName, player.ping.toLong())), + serializer.serialize(formatter.formatForPlayer(tabList.footer, serverName, player.ping.toLong())) + ) + } + + private fun update(enabled: Boolean) { + try { + if (enabled) plugin.proxy.players.forEach { update(it) } + } finally { + start() + } + } +} diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/command/BungeeCordCommandSender.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/command/BungeeCordCommandSender.kt new file mode 100644 index 0000000..d34a43e --- /dev/null +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/command/BungeeCordCommandSender.kt @@ -0,0 +1,15 @@ +package app.simplecloud.plugin.proxy.bungeecord.command + +import app.simplecloud.plugin.proxy.shared.command.ProxySender +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer +import net.md_5.bungee.api.CommandSender + +class BungeeCordCommandSender( + val commandSender: CommandSender +) : ProxySender { + + override fun sendMessage(message: Component) { + commandSender.sendMessage(*BungeeComponentSerializer.get().serialize(message)) + } +} \ No newline at end of file diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/PostLoginListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/PostLoginListener.kt index 58b32be..cb56d10 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/PostLoginListener.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/PostLoginListener.kt @@ -1,27 +1,27 @@ package app.simplecloud.plugin.proxy.bungeecord.listener -import app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin -import app.simplecloud.plugin.proxy.bungeecord.toBaseComponent -import app.simplecloud.plugin.proxy.shared.joinstate.ProxyJoinGate +import app.simplecloud.plugin.proxy.shared.ProxyEssentials +import app.simplecloud.plugin.proxy.shared.joinstate.JoinResult import kotlinx.coroutines.runBlocking +import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer import net.md_5.bungee.api.event.PostLoginEvent import net.md_5.bungee.api.plugin.Listener import net.md_5.bungee.event.EventHandler import net.md_5.bungee.event.EventPriority class PostLoginListener( - private val plugin: ProxyBungeeCordPlugin + private val essentials: ProxyEssentials ) : Listener { @EventHandler(priority = EventPriority.LOWEST) - fun handle(event: PostLoginEvent) { + fun onPostLogin(event: PostLoginEvent) { val player = event.player val result = runBlocking { - plugin.proxyPlugin.proxyJoinGate.evaluate(player.name) { permission -> player.hasPermission(permission) } + essentials.joinGate.checkProxyJoin(player.name, player.uniqueId, player::hasPermission) } + if (result !is JoinResult.Denied) return - if (result is ProxyJoinGate.Result.Denied) { - player.disconnect(plugin.deserializeToComponent(result.kickMessage, player).toBaseComponent()) - } + val message = essentials.messageFormatter.formatForPlayer(result.message, "unknown", player.ping.toLong()) + player.disconnect(*BungeeComponentSerializer.get().serialize(message)) } } diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ProxyPingListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ProxyPingListener.kt index c72dab0..54bce9a 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ProxyPingListener.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ProxyPingListener.kt @@ -1,87 +1,56 @@ package app.simplecloud.plugin.proxy.bungeecord.listener -import app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin -import app.simplecloud.plugin.proxy.bungeecord.toBaseComponent -import app.simplecloud.plugin.proxy.shared.config.MotdLayoutConfiguration -import app.simplecloud.plugin.proxy.shared.motd.ServerIconLoader +import app.simplecloud.plugin.proxy.shared.ProxyEssentials +import app.simplecloud.plugin.proxy.shared.layout.ServerIconCache import app.simplecloud.plugin.proxy.shared.utilities.LocalPingSourceMatcher +import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer import net.md_5.bungee.api.Favicon import net.md_5.bungee.api.ServerPing +import net.md_5.bungee.api.chat.TextComponent import net.md_5.bungee.api.event.ProxyPingEvent import net.md_5.bungee.api.plugin.Listener import net.md_5.bungee.event.EventHandler import java.net.InetSocketAddress -import java.nio.file.Path import java.util.UUID class ProxyPingListener( - private val plugin: ProxyBungeeCordPlugin + private val essentials: ProxyEssentials ) : Listener { - private val proxyPlugin = plugin.proxyPlugin - - private val serverIconLoader = ServerIconLoader(Path.of(proxyPlugin.serverIconsPath)) { image -> - Favicon.create(image) - } + private val serverIconCache = ServerIconCache(essentials.serverIconDirectory) @EventHandler - fun onPing(event: ProxyPingEvent) { - val layout = resolveLayout(event.connection.virtualHost?.hostName) - if (!layout.motd.enabled) return - - val entry = proxyPlugin.layoutRepository.selectEntry(layout, layout.configVersion) ?: return + fun onProxyPing(event: ProxyPingEvent) { + val layoutService = essentials.layoutService + val layoutName = layoutService.getLayoutName(event.connection.virtualHost?.hostString) + val layout = layoutService.getLayout(layoutName) val response = event.response - response.descriptionComponent = plugin.deserializeMotd(entry.line1, entry.line2).toBaseComponent() - - applyServerIcon(response, layout) - // The cloud pings the proxy itself to check its health, which must not affect the counts. - val socketAddress = event.connection.socketAddress as? InetSocketAddress - if (!LocalPingSourceMatcher.isLocal(socketAddress?.address)) { - applyPlayerList(response, layout) + val motdEntry = if (layout.motd.enabled) layoutService.getMotdEntry(layoutName, layout) else null + if (motdEntry != null) { + val motd = essentials.messageFormatter.formatMotd(motdEntry.line1, motdEntry.line2) + response.descriptionComponent = TextComponent(*BungeeComponentSerializer.get().serialize(motd)) } - } - private fun resolveLayout(virtualHost: String?): MotdLayoutConfiguration { - val layoutName = when (virtualHost) { - null -> null - else -> proxyPlugin.domainMotdHandler.getLayoutNameForDomain(virtualHost) - } - val domainLayout = when (layoutName) { - null -> null - else -> proxyPlugin.layoutRepository.getLayoutByName(layoutName) + val icon = if (layout.serverIcon.enabled) serverIconCache.get(layout.serverIcon.file) else null + if (icon != null) { + response.setFavicon(Favicon.create(icon)) } - return domainLayout ?: proxyPlugin.layoutRepository.getCurrentMotdLayout() - } - - private fun applyServerIcon(response: ServerPing, layout: MotdLayoutConfiguration) { - if (!layout.serverIcon.enabled) return - - val favicon = serverIconLoader.get(layout.serverIcon.file) { plugin.logger.warning(it) } ?: return - response.setFavicon(favicon) - } - - private fun applyPlayerList(response: ServerPing, layout: MotdLayoutConfiguration) { - val playerCountHandler = proxyPlugin.playerCountTracker - val onlinePlayers = playerCountHandler.onlinePlayers(plugin.proxy.players.size) - val realMaxPlayers = playerCountHandler.maxPlayers(plugin.proxy.config.playerLimit) - val maxPlayers = layout.version.slots.resolveMaxPlayers(onlinePlayers, realMaxPlayers) - - response.players = ServerPing.Players(maxPlayers, onlinePlayers, samplePlayers(layout, response)) - if (layout.version.name.enabled) { response.version = ServerPing.Protocol(layout.version.name.text, response.version.protocol) } - } - private fun samplePlayers(layout: MotdLayoutConfiguration, response: ServerPing): Array { - if (!layout.playerList.enabled || layout.playerList.playerList.isEmpty()) { - return response.players.sample + val address = event.connection.socketAddress as? InetSocketAddress + if (!LocalPingSourceMatcher.isLocal(address?.address)) { + val onlinePlayers = essentials.playerCountService.getOnlinePlayers() + val maxPlayers = layout.version.slots.resolveMaxPlayers(onlinePlayers, essentials.playerCountService.getMaxPlayers()) + val samplePlayers = if (layout.playerList.enabled && layout.playerList.playerList.isNotEmpty()) { + layout.playerList.playerList.map { ServerPing.PlayerInfo(it, UUID.randomUUID()) }.toTypedArray() + } else { + response.players.sample + } + response.players = ServerPing.Players(maxPlayers, onlinePlayers, samplePlayers) } - - return layout.playerList.playerList - .map { ServerPing.PlayerInfo(it, UUID.randomUUID()) } - .toTypedArray() } -} \ No newline at end of file +} diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerKickListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerKickListener.kt index 425ecab..7d4251e 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerKickListener.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerKickListener.kt @@ -1,20 +1,20 @@ package app.simplecloud.plugin.proxy.bungeecord.listener -import app.simplecloud.plugin.proxy.shared.ProxyPlugin +import app.simplecloud.plugin.proxy.shared.ProxyEssentials import net.md_5.bungee.api.event.ServerKickEvent import net.md_5.bungee.api.plugin.Listener import net.md_5.bungee.event.EventHandler class ServerKickListener( - private val proxyPlugin: ProxyPlugin + private val essentials: ProxyEssentials ) : Listener { @EventHandler(priority = Byte.MAX_VALUE) - fun handle(event: ServerKickEvent) { - if (!proxyPlugin.config.get().showKickReason) return + fun onServerKick(event: ServerKickEvent) { + if (!essentials.config.get().showKickReason) return if (event.cancelServer != null) return - val serverReason = event.reason ?: return - event.player.disconnect(serverReason) + val reason = event.reason ?: return + event.player.disconnect(reason) } } diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerPreConnectListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerPreConnectListener.kt index 0d2f5a1..1b2cb80 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerPreConnectListener.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerPreConnectListener.kt @@ -1,82 +1,35 @@ package app.simplecloud.plugin.proxy.bungeecord.listener -import app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin -import app.simplecloud.plugin.proxy.bungeecord.toBaseComponent -import app.simplecloud.plugin.proxy.shared.joinstate.ProxyJoinGate +import app.simplecloud.plugin.proxy.shared.ProxyEssentials +import app.simplecloud.plugin.proxy.shared.joinstate.JoinResult import kotlinx.coroutines.runBlocking -import net.md_5.bungee.api.connection.ProxiedPlayer +import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer import net.md_5.bungee.api.event.ServerConnectEvent -import net.md_5.bungee.api.event.ServerConnectEvent.Reason import net.md_5.bungee.api.plugin.Listener import net.md_5.bungee.event.EventHandler import net.md_5.bungee.event.EventPriority -import org.slf4j.LoggerFactory class ServerPreConnectListener( - private val plugin: ProxyBungeeCordPlugin + private val essentials: ProxyEssentials ) : Listener { - private val logger = LoggerFactory.getLogger(ServerPreConnectListener::class.java) - private val proxyPlugin = plugin.proxyPlugin - @EventHandler(priority = EventPriority.HIGH) - fun handle(event: ServerConnectEvent) { - if (isProxyJoin(event)) { - checkAllowProxyJoin(event) - if (event.isCancelled) { - return - } - } - - checkAllowServerSwitch(event) - } - - private fun isProxyJoin(event: ServerConnectEvent): Boolean { - return event.reason == Reason.JOIN_PROXY || event.player.server == null - } - - private fun checkAllowProxyJoin(event: ServerConnectEvent) { - val player = event.player - val result = runBlocking { - proxyPlugin.proxyJoinGate.evaluate(player.name) { permission -> player.hasPermission(permission) } - } - - if (result is ProxyJoinGate.Result.Denied) { - disconnect(player, result.kickMessage, event) - } - } - - private fun checkAllowServerSwitch(event: ServerConnectEvent) { + fun onServerConnect(event: ServerConnectEvent) { val player = event.player val serverName = event.target.name - val resolver = proxyPlugin.joinStateResolver - - runBlocking { - val joinStateName = resolver.getJoinStateForServer(serverName) - val joinState = resolver.resolveJoinState(joinStateName) - val kickMessages = proxyPlugin.messageConfig.get().kick - - if (joinState == null) { - logger.error("Neither join state '$joinStateName' nor default state found. Check configuration!") - denyServerSwitch(player, kickMessages.noJoinState, event) - return@runBlocking - } - - val joinPermission = joinState.permission.join - if (joinPermission.isNotBlank() && !player.hasPermission(joinPermission)) { - logger.info("Player ${player.name} does not have permission to join $serverName.") - denyServerSwitch(player, kickMessages.noPermission, event) - } + val result = runBlocking { + essentials.joinGate.checkServerSwitch(player.name, player.uniqueId, serverName, player::hasPermission) } - } + if (result !is JoinResult.Denied) return - private fun denyServerSwitch(player: ProxiedPlayer, message: String, event: ServerConnectEvent) { - player.sendMessage(plugin.deserializeToComponent(message, player).toBaseComponent()) + val message = essentials.messageFormatter.formatForPlayer(result.message, serverName, player.ping.toLong()) + val components = BungeeComponentSerializer.get().serialize(message) event.isCancelled = true - } - private fun disconnect(player: ProxiedPlayer, message: String, event: ServerConnectEvent) { - player.disconnect(plugin.deserializeToComponent(message, player).toBaseComponent()) - event.isCancelled = true + if (player.server == null) { + player.disconnect(*components) + } else { + player.sendMessage(*components) + } } } diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/tablist/TabListListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/TabListListener.kt similarity index 52% rename from proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/tablist/TabListListener.kt rename to proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/TabListListener.kt index 951846b..e2346ee 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/tablist/TabListListener.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/TabListListener.kt @@ -1,16 +1,16 @@ -package app.simplecloud.plugin.proxy.bungeecord.tablist +package app.simplecloud.plugin.proxy.bungeecord.listener -import app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin +import app.simplecloud.plugin.proxy.bungeecord.TabListHandler import net.md_5.bungee.api.event.ServerConnectedEvent import net.md_5.bungee.api.plugin.Listener import net.md_5.bungee.event.EventHandler class TabListListener( - private val plugin: ProxyBungeeCordPlugin + private val tabListHandler: TabListHandler ) : Listener { @EventHandler fun onServerConnected(event: ServerConnectedEvent) { - plugin.tabListHandler.updateTabListForPlayer(event.player) + tabListHandler.update(event.player) } } diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/platform/BungeeCordPlatformImpl.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/platform/BungeeCordPlatformImpl.kt new file mode 100644 index 0000000..8eba57d --- /dev/null +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/platform/BungeeCordPlatformImpl.kt @@ -0,0 +1,17 @@ +package app.simplecloud.plugin.proxy.bungeecord.platform + +import app.simplecloud.plugin.proxy.shared.ProxyPlatform +import net.md_5.bungee.api.ProxyServer +import java.nio.file.Path + +class BungeeCordPlatformImpl( + private val proxy: ProxyServer, + private val dataDirectory: Path +) : ProxyPlatform { + + override fun getDataDirectory() = dataDirectory + + override fun getOnlinePlayers() = proxy.onlineCount + + override fun getMaxPlayers() = proxy.config.playerLimit +} \ No newline at end of file diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/tablist/TabListHandler.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/tablist/TabListHandler.kt deleted file mode 100644 index 879f8b1..0000000 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/tablist/TabListHandler.kt +++ /dev/null @@ -1,55 +0,0 @@ -package app.simplecloud.plugin.proxy.bungeecord.tablist - -import app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin -import net.md_5.bungee.api.connection.ProxiedPlayer -import net.md_5.bungee.api.scheduler.ScheduledTask -import org.slf4j.LoggerFactory -import java.util.concurrent.TimeUnit - -class TabListHandler( - private val plugin: ProxyBungeeCordPlugin -) { - - private val logger = LoggerFactory.getLogger(TabListHandler::class.java) - private var task: ScheduledTask? = null - - fun startTabListTask() { - val updateTimeMillis = plugin.proxyPlugin.config.get().tabListUpdateTimeMillis() - - task = plugin.proxy.scheduler.schedule( - plugin, - { updateTabLists() }, - 1, - updateTimeMillis, - TimeUnit.MILLISECONDS - ) - } - - fun stopTabListTask() { - val runningTask = task - if (runningTask == null) { - logger.warn("Can't stop tablist task because it is not initialized") - return - } - - runningTask.cancel() - task = null - } - - fun updateTabListForPlayer(player: ProxiedPlayer) { - val serverInfo = player.server?.info ?: return - val resolver = plugin.proxyPlugin.tabListResolver - val tabListGroup = resolver.findTabListGroup(serverInfo.name) ?: return - val (header, footer) = resolver.getCurrentTabList(tabListGroup) ?: return - - plugin.adventure().player(player).sendPlayerListHeaderAndFooter( - plugin.deserializeToComponent(header, player), - plugin.deserializeToComponent(footer, player), - ) - } - - private fun updateTabLists() { - plugin.proxyPlugin.tabListResolver.incrementIndices() - plugin.proxy.players.forEach { updateTabListForPlayer(it) } - } -} diff --git a/proxy-bungeecord/src/main/resources/config/layout/maintenance.yml b/proxy-bungeecord/src/main/resources/config/layout/maintenance.yml deleted file mode 100644 index c688b4c..0000000 --- a/proxy-bungeecord/src/main/resources/config/layout/maintenance.yml +++ /dev/null @@ -1,50 +0,0 @@ -config-version: '2' - -# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -# Server List Layout -# Configures how your server appears in the multiplayer server list, -# including MOTD, server icon, and player count display. -# -# Read more @ https://docs.simplecloud.app/manual/plugins/proxy-essentials -# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - -motd: - enabled: true - # QUEUE, RANDOM - update-type: RANDOM - #update-type: QUEUE - #update-time: 10 - #update-time: 0 disables updates - # Wrap a line in
...
to center it using vanilla font metrics. - layouts: - - name: 'maintenance' - line1: 'A simplecloud.app network in maintenance' - line2: ' ' - -server-icon: - enabled: true - file: 'simplecloud-gray.png' - -# Text displayed when hovering over the player count in the server list -player-list: - enabled: true - player-list: - - '' - - 'simplecloud.app' - - '' - -# Server version text and player slots configuration -version: - name: - enabled: true - text: '<#DC2626>Maintenance' - slots: - enabled: true - # REAL - Displays the actual number of online players - # FAKE - Displays a fixed custom number of online players - # DYNAMIC - Displays a dynamic number based on actual players plus a range - type: REAL - #type: FAKE - #fake-slots: 100 - #type: DYNAMIC - #dynamic-player-range: 5 diff --git a/proxy-bungeecord/src/main/resources/config/layout/public.yml b/proxy-bungeecord/src/main/resources/config/layout/public.yml deleted file mode 100644 index 7aaa1f8..0000000 --- a/proxy-bungeecord/src/main/resources/config/layout/public.yml +++ /dev/null @@ -1,50 +0,0 @@ -config-version: '2' - -# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -# Server List Layout -# Configures how your server appears in the multiplayer server list, -# including MOTD, server icon, and player count display. -# -# Read more @ https://docs.simplecloud.app/manual/plugins/proxy-essentials -# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - -motd: - enabled: true - # QUEUE, RANDOM - update-type: RANDOM - #update-type: QUEUE - #update-time: 10 - #update-time: 0 disables updates - # Wrap a line in
...
to center it using vanilla font metrics. - layouts: - - name: 'public' - line1: 'A simplecloud.app network' - line2: ' ' - -server-icon: - enabled: true - file: 'simplecloud.png' - -# Text displayed when hovering over the player count in the server list -player-list: - enabled: true - player-list: - - '' - - 'simplecloud.app' - - '' - -# Server version text and player slots configuration -version: - name: - enabled: false - text: 'Example' - slots: - enabled: true - # REAL - Displays the actual number of online players - # FAKE - Displays a fixed custom number of online players - # DYNAMIC - Displays a dynamic number based on actual players plus a range - type: REAL - #type: FAKE - #fake-slots: 100 - #type: DYNAMIC - #dynamic-player-range: 5 diff --git a/proxy-bungeecord/src/main/resources/config/placeholder.yml b/proxy-bungeecord/src/main/resources/config/placeholder.yml deleted file mode 100644 index 3f3892e..0000000 --- a/proxy-bungeecord/src/main/resources/config/placeholder.yml +++ /dev/null @@ -1,13 +0,0 @@ -current-date-format: "dd.MM.yyyy" -current-time-format: "HH:mm:ss" -ping-colors: - - ping: 0 - color: "" - - ping: 50 - color: "" - - ping: 100 - color: "" - - ping: 150 - color: "" - - ping: 200 - color: "" diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyEssentials.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyEssentials.kt new file mode 100644 index 0000000..c3b8d3b --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyEssentials.kt @@ -0,0 +1,87 @@ +package app.simplecloud.plugin.proxy.shared + +import app.simplecloud.api.CloudApi +import app.simplecloud.api.CloudApiOptions +import app.simplecloud.api.runtime.SimpleCloudRuntime +import app.simplecloud.plugin.api.shared.config.ConfigurationFactory +import app.simplecloud.plugin.proxy.shared.command.JoinStateCommand +import app.simplecloud.plugin.proxy.shared.command.LayoutCommand +import app.simplecloud.plugin.proxy.shared.command.ProxySender +import app.simplecloud.plugin.proxy.shared.command.ProxyCommand +import app.simplecloud.plugin.proxy.shared.config.MessageConfig +import app.simplecloud.plugin.proxy.shared.config.ProxyEssentialsConfig +import app.simplecloud.plugin.proxy.shared.config.migration.DefaultConfigInstaller +import app.simplecloud.plugin.proxy.shared.config.migration.LegacyDirectoryMigrator +import app.simplecloud.plugin.proxy.shared.config.migration.OldConfigMigrator +import app.simplecloud.plugin.proxy.shared.joinstate.JoinGate +import app.simplecloud.plugin.proxy.shared.joinstate.JoinStateService +import app.simplecloud.plugin.proxy.shared.layout.LayoutRepository +import app.simplecloud.plugin.proxy.shared.layout.LayoutService +import app.simplecloud.plugin.proxy.shared.player.PlayerCountService +import app.simplecloud.plugin.proxy.shared.tablist.TabListService +import app.simplecloud.plugin.proxy.shared.utilities.text.MessageFormatter +import kotlinx.coroutines.* +import org.incendo.cloud.CommandManager +import org.slf4j.LoggerFactory +import java.nio.file.Path + +class ProxyEssentials( + platform: ProxyPlatform +) { + + private val logger = LoggerFactory.getLogger(ProxyEssentials::class.java) + private val dir = platform.getDataDirectory() + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val api = CloudApi.create(CloudApiOptions.builder().component("proxy-essentials").build()) + + val config = ConfigurationFactory(dir.resolve("config.yml").toFile(), ProxyEssentialsConfig::class.java) + val messages = ConfigurationFactory(dir.resolve("messages.yml").toFile(), MessageConfig::class.java) + + val joinStateService = JoinStateService(api, config, scope) + val layoutRepository = LayoutRepository(dir.resolve("layout")) + val layoutService = LayoutService(api, layoutRepository, joinStateService, config, scope) + val playerCountService = PlayerCountService(api, platform, config, scope) + val tabListService = TabListService(config) + val joinGate = JoinGate(api, joinStateService, config, messages) + val messageFormatter = MessageFormatter(messages, playerCountService, layoutService) + val serverIconDirectory: Path = dir.resolve("layout").resolve("server-icons") + + fun start() { + LegacyDirectoryMigrator.migrate(dir) + OldConfigMigrator.migrate(dir) + DefaultConfigInstaller.install(dir, javaClass.classLoader) + loadConfigs() + layoutService.startDomainSync() + + if (SimpleCloudRuntime.serverId().isBlank()) return + playerCountService.start() + scope.launch { + try { + joinStateService.start() + layoutService.start() + } catch (e: Exception) { + logger.error("Could not load this proxy from the cloud, so it uses the initial join state and layout", e) + } + } + } + + fun stop() { + scope.cancel() + } + + fun reload() { + loadConfigs() + } + + fun registerCommands(manager: CommandManager) { + ProxyCommand(manager, this, scope).register() + JoinStateCommand(manager, api, joinStateService, config, messages, messageFormatter, scope).register() + LayoutCommand(manager, api, layoutService, layoutRepository, joinStateService, config, messages, messageFormatter, scope).register() + } + + private fun loadConfigs() { + config.loadOrCreate(ProxyEssentialsConfig()) + messages.loadOrCreate(MessageConfig()) + layoutRepository.loadLayouts() + } +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlatform.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlatform.kt new file mode 100644 index 0000000..c472210 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlatform.kt @@ -0,0 +1,12 @@ +package app.simplecloud.plugin.proxy.shared + +import java.nio.file.Path + +interface ProxyPlatform { + + fun getDataDirectory(): Path + + fun getOnlinePlayers(): Int + + fun getMaxPlayers(): Int +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt deleted file mode 100644 index 520ec2e..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt +++ /dev/null @@ -1,81 +0,0 @@ -package app.simplecloud.plugin.proxy.shared - -import app.simplecloud.api.CloudApi -import app.simplecloud.api.CloudApiOptions -import app.simplecloud.plugin.api.shared.config.ConfigurationFactory -import app.simplecloud.plugin.proxy.shared.config.DefaultConfigInstaller -import app.simplecloud.plugin.proxy.shared.config.MessageConfig -import app.simplecloud.plugin.proxy.shared.config.PlaceHolderConfiguration -import app.simplecloud.plugin.proxy.shared.config.ProxyEssentialsConfig -import app.simplecloud.plugin.proxy.shared.config.migration.LegacyDirectoryMigrator -import app.simplecloud.plugin.proxy.shared.config.migration.OldConfigMigrator -import app.simplecloud.plugin.proxy.shared.controller.CloudControllerHandler -import app.simplecloud.plugin.proxy.shared.joinstate.JoinStateResolver -import app.simplecloud.plugin.proxy.shared.joinstate.JoinStateService -import app.simplecloud.plugin.proxy.shared.joinstate.ProxyJoinGate -import app.simplecloud.plugin.proxy.shared.motd.DomainMotdResolver -import app.simplecloud.plugin.proxy.shared.motd.MotdLayoutRepository -import app.simplecloud.plugin.proxy.shared.player.PlayerCountTracker -import app.simplecloud.plugin.proxy.shared.tablist.TabListResolver -import org.slf4j.LoggerFactory -import java.io.File - -class ProxyPlugin( - dirPath: String -) { - - private val logger = LoggerFactory.getLogger(ProxyPlugin::class.java) - private val dataDirectory = File(dirPath) - - val api: CloudApi = CloudApi.create(CloudApiOptions.builder().component("proxy-essentials").build()) - - val config = ConfigurationFactory(File(dataDirectory, "config.yml"), ProxyEssentialsConfig::class.java) - val messageConfig = ConfigurationFactory(File(dataDirectory, "messages.yml"), MessageConfig::class.java) - val placeholderConfig = ConfigurationFactory(File(dataDirectory, "placeholder.yml"), PlaceHolderConfiguration::class.java) - - init { - LegacyDirectoryMigrator.migrate(dataDirectory.toPath()) - dataDirectory.mkdirs() - OldConfigMigrator.migrate(dataDirectory.toPath()) - DefaultConfigInstaller.install(dataDirectory.toPath(), javaClass.classLoader) - loadConfigurations() - } - - val serverIconsPath = "$dirPath/layout/server-icons" - val layoutRepository = MotdLayoutRepository(File("$dirPath/layout").toPath(), this) - val joinStateService = JoinStateService(this) - val cloudControllerHandler = CloudControllerHandler(this, joinStateService) - val playerCountTracker = PlayerCountTracker(this) - val joinStateResolver = JoinStateResolver(this) - val proxyJoinGate = ProxyJoinGate(this) - val tabListResolver = TabListResolver(this) - val domainMotdHandler = DomainMotdResolver(this) - - fun start() { - layoutRepository.loadMotdLayouts() - cloudControllerHandler.start() - playerCountTracker.start() - domainMotdHandler.registerListener() - domainMotdHandler.startSyncTask() - } - - fun shutdown() { - playerCountTracker.stop() - joinStateService.stop() - cloudControllerHandler.close() - domainMotdHandler.stop() - } - - fun reload() { - logger.info("Reloading Proxy Essentials...") - loadConfigurations() - layoutRepository.loadMotdLayouts() - logger.info("Successfully reloaded Proxy Essentials") - } - - private fun loadConfigurations() { - config.loadOrCreate(ProxyEssentialsConfig()) - messageConfig.loadOrCreate(MessageConfig()) - placeholderConfig.loadOrCreate(PlaceHolderConfiguration()) - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/JoinStateCommand.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/JoinStateCommand.kt new file mode 100644 index 0000000..994fc8d --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/JoinStateCommand.kt @@ -0,0 +1,209 @@ +package app.simplecloud.plugin.proxy.shared.command + +import app.simplecloud.api.CloudApi +import app.simplecloud.plugin.api.shared.config.ConfigurationFactory +import app.simplecloud.plugin.proxy.shared.config.MessageConfig +import app.simplecloud.plugin.proxy.shared.config.ProxyEssentialsConfig +import app.simplecloud.plugin.proxy.shared.config.UpdateMessages +import app.simplecloud.plugin.proxy.shared.joinstate.JoinStateService +import app.simplecloud.plugin.proxy.shared.utilities.CommandPermissions +import app.simplecloud.plugin.proxy.shared.utilities.text.MessageFormatter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.future.await +import kotlinx.coroutines.future.future +import kotlinx.coroutines.launch +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import org.incendo.cloud.CommandManager +import org.incendo.cloud.context.CommandContext +import org.incendo.cloud.parser.standard.IntegerParser +import org.incendo.cloud.parser.standard.StringParser +import org.incendo.cloud.suggestion.Suggestion +import org.slf4j.LoggerFactory +import java.util.concurrent.CompletableFuture + +class JoinStateCommand( + private val manager: CommandManager, + private val api: CloudApi, + private val joinStateService: JoinStateService, + private val config: ConfigurationFactory, + private val messages: ConfigurationFactory, + private val formatter: MessageFormatter, + private val scope: CoroutineScope +) { + + private val logger = LoggerFactory.getLogger(JoinStateCommand::class.java) + + private val commands = listOf( + "/scproxy joinstate help", + "/scproxy joinstate list", + "/scproxy joinstate info [group]", + "/scproxy joinstate info ", + "/scproxy joinstate set ", + "/scproxy joinstate set ", + " can also be the name of a persistent server" + ) + + fun register() { + val root = manager.commandBuilder("scproxy").literal("joinstate") + + manager.command(root.permission(CommandPermissions.JOINSTATE_HELP).handler { sendHelp(it.sender()) }) + manager.command(root.literal("help").permission(CommandPermissions.JOINSTATE_HELP).handler { sendHelp(it.sender()) }) + manager.command(root.literal("list").permission(CommandPermissions.JOINSTATE_INFO).handler { sendStates(it.sender()) }) + manager.command(root.literal("info").permission(CommandPermissions.JOINSTATE_INFO).handler { sendAllTargets(it.sender()) }) + manager.command( + root.literal("info") + .required("group", StringParser.stringParser()) { _, _ -> suggestTargets() } + .optional("id", IntegerParser.integerParser(1)) { context, _ -> suggestNumericalIds(context) } + .permission(CommandPermissions.JOINSTATE_INFO) + .handler { sendInfo(it) } + ) + manager.command( + root.literal("set") + .required("group", StringParser.stringParser()) { _, _ -> suggestTargets() } + .required("idOrJoinstate", StringParser.stringParser()) { context, _ -> suggestIdsAndStates(context) } + .optional("joinstate", StringParser.stringParser()) { _, _ -> suggestStates() } + .permission(CommandPermissions.JOINSTATE_SET) + .handler { set(it) } + ) + } + + private fun sendHelp(sender: C) { + val help = messages.get().command.joinState.help + sender.sendMessage(formatter.format(help.header)) + commands.forEach { sender.sendMessage(formatter.format(help.entry, Placeholder.unparsed("command", it))) } + } + + private fun sendStates(sender: C) { + val states = messages.get().command.joinState.list.states + sender.sendMessage(formatter.format(states.header)) + config.get().joinstates.forEach { + val permission = Placeholder.unparsed("joinPermission", it.permission.join.ifBlank { "-" }) + sender.sendMessage(formatter.format(states.entry, Placeholder.unparsed("state", it.name), permission)) + } + } + + private fun sendAllTargets(sender: C) { + runAsync(sender, messages.get().command.failure) { + sender.sendMessage(formatter.format(messages.get().command.joinState.list.groups.header)) + getTargets().forEach { sendState(sender, it, joinStateService.getTargetState(it)) } + } + } + + private fun sendInfo(context: CommandContext) { + val target = context.get("group") + val id = context.optional("id") + + runAsync(context.sender(), messages.get().command.failure) { + if (id.isPresent) { + sendState(context.sender(), "$target-${id.get()}", joinStateService.getServiceState(target, id.get())) + } else { + sendState(context.sender(), target, joinStateService.getTargetState(target)) + } + } + } + + private fun sendState(sender: C, target: String, state: String) { + val entry = messages.get().command.joinState.list.groups.entry + sender.sendMessage(formatter.format(entry, Placeholder.unparsed("group", target), Placeholder.unparsed("state", state))) + } + + private fun set(context: CommandContext) { + val target = context.get("group") + val idOrState = context.get("idOrJoinstate") + val state = context.optional("joinstate") + + if (!state.isPresent) { + setTargetState(context.sender(), target, idOrState) + return + } + + val numericalId = idOrState.toIntOrNull() + if (numericalId == null) { + context.sender().sendMessage(formatter.format(messages.get().command.joinState.server.updateFailure)) + return + } + + setServiceState(context.sender(), target, numericalId, state.get()) + } + + private fun setTargetState(sender: C, target: String, state: String) { + val groupMessages = messages.get().command.joinState.group + if (config.get().joinstates.none { it.name == state }) { + sender.sendMessage(formatter.format(groupMessages.updateFailure)) + return + } + + runAsync(sender, groupMessages.updateFailure) { + if (joinStateService.isGroup(target)) { + updateState(sender, groupMessages, joinStateService.getGroupState(target), state) { + joinStateService.setGroupState(target, state) + } + } else { + updateState(sender, messages.get().command.joinState.server, joinStateService.getPersistentServerState(target), state) { + joinStateService.setPersistentServerState(target, state) + } + } + } + } + + private fun setServiceState(sender: C, groupName: String, numericalId: Int, state: String) { + val serverMessages = messages.get().command.joinState.server + if (config.get().joinstates.none { it.name == state }) { + sender.sendMessage(formatter.format(serverMessages.updateFailure)) + return + } + + runAsync(sender, serverMessages.updateFailure) { + updateState(sender, serverMessages, joinStateService.getServiceState(groupName, numericalId), state) { + joinStateService.setServiceState(groupName, numericalId, state) + } + } + } + + private suspend fun updateState(sender: C, updateMessages: UpdateMessages, currentState: String, state: String, update: suspend () -> Unit) { + if (currentState == state) { + sender.sendMessage(formatter.format(updateMessages.updateNoChange)) + return + } + + update() + sender.sendMessage(formatter.format(updateMessages.updateSuccess)) + } + + private fun runAsync(sender: C, failureMessage: String, block: suspend () -> Unit) { + scope.launch { + try { + block() + } catch (e: Exception) { + logger.error("The join state command failed", e) + sender.sendMessage(formatter.format(failureMessage)) + } + } + } + + private suspend fun getTargets(): List = + api.group().allGroups.await().map { it.name } + api.persistentServer().allPersistentServers.await().map { it.name } + + private suspend fun getNumericalIds(groupName: String): List = + api.server().getServersByGroup(groupName).await().map { it.numericalId } + + private fun suggestTargets(): CompletableFuture> = + scope.future { getTargets().map { Suggestion.suggestion(it) } } + + private fun suggestStates(): CompletableFuture> = + CompletableFuture.completedFuture(config.get().joinstates.map { Suggestion.suggestion(it.name) }) + + private fun suggestNumericalIds(context: CommandContext): CompletableFuture> { + val groupName = getGroupArgument(context) ?: return CompletableFuture.completedFuture(emptyList()) + return scope.future { getNumericalIds(groupName).map { Suggestion.suggestion(it.toString()) } } + } + + private fun suggestIdsAndStates(context: CommandContext): CompletableFuture> { + val states = config.get().joinstates.map { Suggestion.suggestion(it.name) } + val groupName = getGroupArgument(context) ?: return CompletableFuture.completedFuture(states) + return scope.future { getNumericalIds(groupName).map { Suggestion.suggestion(it.toString()) } + states } + } + + private fun getGroupArgument(context: CommandContext): String? = + context.rawInput().input().split(" ").filter { it.isNotBlank() }.getOrNull(3) +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/LayoutCommand.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/LayoutCommand.kt new file mode 100644 index 0000000..710f0eb --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/LayoutCommand.kt @@ -0,0 +1,130 @@ +package app.simplecloud.plugin.proxy.shared.command + +import app.simplecloud.api.CloudApi +import app.simplecloud.plugin.api.shared.config.ConfigurationFactory +import app.simplecloud.plugin.proxy.shared.config.MessageConfig +import app.simplecloud.plugin.proxy.shared.config.ProxyEssentialsConfig +import app.simplecloud.plugin.proxy.shared.joinstate.JoinStateService +import app.simplecloud.plugin.proxy.shared.layout.LayoutRepository +import app.simplecloud.plugin.proxy.shared.layout.LayoutService +import app.simplecloud.plugin.proxy.shared.utilities.CommandPermissions +import app.simplecloud.plugin.proxy.shared.utilities.text.MessageFormatter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.future.await +import kotlinx.coroutines.future.future +import kotlinx.coroutines.launch +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import org.incendo.cloud.CommandManager +import org.incendo.cloud.context.CommandContext +import org.incendo.cloud.parser.standard.StringParser +import org.incendo.cloud.suggestion.Suggestion +import org.slf4j.LoggerFactory +import java.util.concurrent.CompletableFuture + +class LayoutCommand( + private val manager: CommandManager, + private val api: CloudApi, + private val layoutService: LayoutService, + private val layoutRepository: LayoutRepository, + private val joinStateService: JoinStateService, + private val config: ConfigurationFactory, + private val messages: ConfigurationFactory, + private val formatter: MessageFormatter, + private val scope: CoroutineScope +) { + + private val logger = LoggerFactory.getLogger(LayoutCommand::class.java) + + private val commands = listOf( + "/scproxy layout help", + "/scproxy layout info [group]", + "/scproxy layout set " + ) + + fun register() { + val root = manager.commandBuilder("scproxy").literal("layout") + + manager.command(root.permission(CommandPermissions.LAYOUT_HELP).handler { sendHelp(it.sender()) }) + manager.command(root.literal("help").permission(CommandPermissions.LAYOUT_HELP).handler { sendHelp(it.sender()) }) + manager.command( + root.literal("info") + .permission(CommandPermissions.LAYOUT_INFO) + .handler { sendInfo(it.sender(), "default", config.get().initialLayout) } + ) + manager.command( + root.literal("info") + .required("group", StringParser.stringParser()) { _, _ -> suggestGroups() } + .permission(CommandPermissions.LAYOUT_INFO) + .handler { sendGroupInfo(it) } + ) + manager.command( + root.literal("set") + .required("group", StringParser.stringParser()) { _, _ -> suggestGroups() } + .required("layout", StringParser.stringParser()) { _, _ -> suggestLayouts() } + .permission(CommandPermissions.LAYOUT_SET) + .handler { set(it) } + ) + } + + private fun sendHelp(sender: C) { + val help = messages.get().command.layout.help + sender.sendMessage(formatter.format(help.header)) + commands.forEach { sender.sendMessage(formatter.format(help.entry, Placeholder.unparsed("command", it))) } + } + + private fun sendGroupInfo(context: CommandContext) { + val groupName = context.get("group") + + runAsync(context.sender(), messages.get().command.failure) { + val config = config.get() + val state = joinStateService.getGroupState(groupName) + val forcedLayout = config.joinstates.find { it.name == state }?.forcedMotdLayout + val layout = forcedLayout ?: layoutService.getGroupLayout(groupName) ?: config.initialLayout + sendInfo(context.sender(), groupName, layout) + } + } + + private fun sendInfo(sender: C, groupName: String, layout: String) { + val info = messages.get().command.layout.info + sender.sendMessage(formatter.format(info.header, Placeholder.unparsed("group", groupName))) + sender.sendMessage(formatter.format(info.entry, Placeholder.unparsed("layout", layout))) + } + + private fun set(context: CommandContext) { + val groupName = context.get("group") + val layout = context.get("layout") + val setMessages = messages.get().command.layout.set + + if (layoutRepository.find(layout) == null) { + context.sender().sendMessage(formatter.format(setMessages.updateFailure)) + return + } + + runAsync(context.sender(), setMessages.updateFailure) { + if (layoutService.getGroupLayout(groupName) == layout) { + context.sender().sendMessage(formatter.format(setMessages.updateNoChange)) + return@runAsync + } + + layoutService.setGroupLayout(groupName, layout) + context.sender().sendMessage(formatter.format(setMessages.updateSuccess)) + } + } + + private fun runAsync(sender: C, failureMessage: String, block: suspend () -> Unit) { + scope.launch { + try { + block() + } catch (e: Exception) { + logger.error("The layout command failed", e) + sender.sendMessage(formatter.format(failureMessage)) + } + } + } + + private fun suggestGroups(): CompletableFuture> = + scope.future { api.group().allGroups.await().map { Suggestion.suggestion(it.name) } } + + private fun suggestLayouts(): CompletableFuture> = + CompletableFuture.completedFuture(layoutRepository.getNames().map { Suggestion.suggestion(it) }) +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyCommand.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyCommand.kt new file mode 100644 index 0000000..e8b588d --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyCommand.kt @@ -0,0 +1,62 @@ +package app.simplecloud.plugin.proxy.shared.command + +import app.simplecloud.plugin.proxy.shared.ProxyEssentials +import app.simplecloud.plugin.proxy.shared.utilities.CommandPermissions +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import org.incendo.cloud.CommandManager +import org.slf4j.LoggerFactory + +class ProxyCommand( + private val manager: CommandManager, + private val essentials: ProxyEssentials, + private val scope: CoroutineScope +) { + + private val logger = LoggerFactory.getLogger(ProxyCommand::class.java) + private val formatter = essentials.messageFormatter + + private val commands = listOf( + "/scproxy help", + "/scproxy reload", + "/scproxy joinstate help", + "/scproxy joinstate list", + "/scproxy joinstate info [group]", + "/scproxy joinstate info ", + "/scproxy joinstate set ", + "/scproxy joinstate set ", + "/scproxy layout help", + "/scproxy layout info [group]", + "/scproxy layout set " + ) + + fun register() { + val root = manager.commandBuilder("scproxy") + + manager.command(root.permission(CommandPermissions.HELP).handler { sendHelp(it.sender()) }) + manager.command(root.literal("help").permission(CommandPermissions.HELP).handler { sendHelp(it.sender()) }) + manager.command(root.literal("reload").permission(CommandPermissions.RELOAD).handler { reload(it.sender()) }) + } + + private fun sendHelp(sender: C) { + val help = essentials.messages.get().command.help + sender.sendMessage(formatter.format(help.header)) + commands.forEach { sender.sendMessage(formatter.format(help.entry, Placeholder.unparsed("command", it))) } + } + + private fun reload(sender: C) { + sender.sendMessage(formatter.format(essentials.messages.get().command.reload.start)) + + scope.launch { + try { + essentials.reload() + sender.sendMessage(formatter.format(essentials.messages.get().command.reload.success)) + } catch (e: Exception) { + logger.error("Could not reload Proxy Essentials", e) + val error = Placeholder.unparsed("error", e.message ?: e.javaClass.simpleName) + sender.sendMessage(formatter.format(essentials.messages.get().command.reload.failure, error)) + } + } + } +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyCommandSender.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyCommandSender.kt deleted file mode 100644 index 999e384..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyCommandSender.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.command - -interface ProxyCommandSender { - - fun sendMessage(message: String) - -} \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyEssentialsCommandHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyEssentialsCommandHandler.kt deleted file mode 100644 index 9caaa01..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyEssentialsCommandHandler.kt +++ /dev/null @@ -1,89 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.command - -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import kotlinx.coroutines.* -import org.incendo.cloud.CommandManager -import org.incendo.cloud.context.CommandContext -import org.slf4j.LoggerFactory - -class ProxyEssentialsCommandHandler( - private val manager: CommandManager, - private val plugin: ProxyPlugin -) { - - private val logger = LoggerFactory.getLogger(ProxyEssentialsCommandHandler::class.java) - - private val commands = listOf( - "/scproxy help", - "/scproxy reload", - "/scproxy joinstate help", - "/scproxy joinstate info ", - "/scproxy joinstate info ", - "/scproxy joinstate set ", - "/scproxy joinstate set ", - "/scproxy layout help", - "/scproxy layout info [group]", - "/scproxy layout set " - ) - - fun loadCommands() { - loadHelp() - loadReload() - } - - private fun loadHelp() { - manager.command( - manager.commandBuilder("scproxy") - .permission("simplecloud.proxy-essentials.command.help") - .handler { context: CommandContext -> sendHelp(context.sender()) } - .build() - ) - manager.command( - manager.commandBuilder("scproxy") - .literal("help") - .permission("simplecloud.proxy-essentials.command.help") - .handler { context: CommandContext -> sendHelp(context.sender()) } - .build() - ) - } - - private fun sendHelp(sender: C) { - val messages = plugin.messageConfig.get() - val entry = messages.command.joinState.help.entry - - sender.sendMessage(messages.resolve("Available /scproxy commands:")) - commands.forEach { command -> - sender.sendMessage(entry.replace("", command)) - } - } - - private fun loadReload() { - manager.command( - manager.commandBuilder("scproxy") - .literal("reload") - .permission("simplecloud.proxy-essentials.command.reload") - .handler { context: CommandContext -> - reload(context.sender()) - } - .build() - ) - } - - private fun reload(sender: C) { - val messagesBeforeReload = plugin.messageConfig.get() - sender.sendMessage(messagesBeforeReload.resolve(messagesBeforeReload.command.reload.start)) - - CoroutineScope(Dispatchers.IO).launch { - try { - plugin.reload() - val messages = plugin.messageConfig.get() - sender.sendMessage(messages.resolve(messages.command.reload.success)) - } catch (e: Exception) { - logger.error("Failed to reload Proxy Essentials", e) - - val messages = plugin.messageConfig.get() - sender.sendMessage(messages.resolve(messages.command.reload.failure).replace("", e.message ?: "Unknown error")) - } - } - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxySender.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxySender.kt new file mode 100644 index 0000000..de878c7 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxySender.kt @@ -0,0 +1,8 @@ +package app.simplecloud.plugin.proxy.shared.command + +import net.kyori.adventure.text.Component + +interface ProxySender { + + fun sendMessage(message: Component) +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/JoinStateCommandHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/JoinStateCommandHandler.kt deleted file mode 100644 index 70bb67d..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/JoinStateCommandHandler.kt +++ /dev/null @@ -1,272 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.command.commands - -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.command.ProxyCommandSender -import app.simplecloud.plugin.proxy.shared.config.MessageConfig -import app.simplecloud.plugin.proxy.shared.utilities.ProxyPermissions -import kotlinx.coroutines.* -import org.incendo.cloud.CommandManager -import org.incendo.cloud.context.CommandContext -import org.incendo.cloud.parser.standard.IntegerParser -import org.incendo.cloud.parser.standard.StringParser -import org.incendo.cloud.suggestion.Suggestion -import java.util.concurrent.CompletableFuture - -class JoinStateCommandHandler( - private val manager: CommandManager, - private val plugin: ProxyPlugin -) { - - private val index = 3 - private val commands = listOf( - "/scproxy joinstate help", - "/scproxy joinstate info ", - "/scproxy joinstate info ", - "/scproxy joinstate set ", - "/scproxy joinstate set ", - " can also be the name of a persistent server" - ) - - fun loadCommands() { - loadHelp() - loadInfo() - loadSet() - } - - private fun loadHelp() { - val builder = manager.commandBuilder("scproxy").literal("joinstate") - - manager.command( - builder.permission(ProxyPermissions.JOINSTATE_HELP) - .handler { context: CommandContext -> handleHelp(context.sender()) } - .build() - ) - manager.command( - builder.literal("help") - .permission(ProxyPermissions.JOINSTATE_HELP) - .handler { context: CommandContext -> handleHelp(context.sender()) } - .build() - ) - } - - private fun handleHelp(sender: ProxyCommandSender) { - val messages = plugin.messageConfig.get() - val entry = messages.command.joinState.help.entry - - sender.sendMessage(messages.resolve(messages.command.joinState.help.header)) - commands.forEach { command -> - sender.sendMessage(entry.replace("", command)) - } - } - - private fun loadInfo() { - manager.command( - manager.commandBuilder("scproxy") - .literal("joinstate") - .literal("info") - .required("group", StringParser.stringParser()) { _, _ -> suggestGroups() } - .optional("id", IntegerParser.integerParser(1)) { context, _ -> suggestNumericalIds(context) } - .permission(ProxyPermissions.JOINSTATE_INFO) - .handler { context: CommandContext -> - CoroutineScope(Dispatchers.IO).launch { handleInfo(context) } - } - .build() - ) - } - - private suspend fun handleInfo(context: CommandContext) { - val group = context.get("group") - val id = context.optional("id") - val messages = plugin.messageConfig.get() - - val name = when { - id.isPresent -> "$group-${id.get()}" - else -> group - } - val state = when { - id.isPresent -> plugin.joinStateService.getJoinStateAtService(group, id.get()) - else -> joinStateOf(group) - } - - context.sender().sendMessage( - messages.resolve(messages.command.joinState.list.groups.entry) - .replace("", name) - .replace("", state) - ) - } - - private fun loadSet() { - manager.command( - manager.commandBuilder("scproxy") - .literal("joinstate") - .literal("set") - .required("group", StringParser.stringParser()) { _, _ -> suggestGroups() } - .required("idOrJoinstate", StringParser.stringParser()) { context, _ -> - suggestIdsAndJoinStates(context) - } - .optional("joinstate", StringParser.stringParser()) { _, _ -> suggestJoinStates() } - .permission(ProxyPermissions.JOINSTATE_SET) - .handler { context: CommandContext -> - CoroutineScope(Dispatchers.IO).launch { handleSet(context) } - } - .build() - ) - } - - private suspend fun handleSet(context: CommandContext) { - val group = context.get("group") - val idOrJoinstate = context.get("idOrJoinstate") - val joinstate = context.optional("joinstate") - val messages = plugin.messageConfig.get() - - if (!joinstate.isPresent) { - handleSetGroup(context.sender(), group, idOrJoinstate, messages) - return - } - - val numericalId = idOrJoinstate.toIntOrNull() - if (numericalId == null) { - context.sender().sendMessage(messages.resolve(messages.command.joinState.server.updateFailure)) - return - } - - handleSetService(context.sender(), group, numericalId, joinstate.get(), messages) - } - - private suspend fun handleSetGroup( - sender: ProxyCommandSender, - group: String, - state: String, - messages: MessageConfig - ) { - if (plugin.joinStateResolver.resolveJoinState(state) == null) { - sender.sendMessage(messages.resolve(messages.command.joinState.group.updateFailure)) - return - } - - if (!plugin.cloudControllerHandler.groupExists(group)) { - handleSetPersistentServer(sender, group, state, messages) - return - } - - if (plugin.joinStateService.getJoinStateAtGroup(group) == state) { - sender.sendMessage(messages.resolve(messages.command.joinState.group.updateNoChange)) - return - } - - val updated = plugin.joinStateService.setJoinStateAtGroupAndAllServicesInGroup(group, state) - sender.sendMessage(messages.resolve(updateResult(messages, updated, isGroup = true))) - } - - private suspend fun handleSetPersistentServer( - sender: ProxyCommandSender, - serverName: String, - state: String, - messages: MessageConfig - ) { - if (plugin.joinStateService.getJoinStateAtPersistentServer(serverName) == state) { - sender.sendMessage(messages.resolve(messages.command.joinState.server.updateNoChange)) - return - } - - val updated = plugin.joinStateService.setJoinStateAtPersistentServer(serverName, state) - sender.sendMessage(messages.resolve(updateResult(messages, updated, isGroup = false))) - } - - private suspend fun handleSetService( - sender: ProxyCommandSender, - group: String, - numericalId: Int, - state: String, - messages: MessageConfig - ) { - if (plugin.joinStateResolver.resolveJoinState(state) == null) { - sender.sendMessage(messages.resolve(messages.command.joinState.server.updateFailure)) - return - } - - if (plugin.joinStateService.getJoinStateAtService(group, numericalId) == state) { - sender.sendMessage(messages.resolve(messages.command.joinState.server.updateNoChange)) - return - } - - val updated = plugin.joinStateService.setJoinStateAtService(group, numericalId, state) - if (updated && isCurrentService(group, numericalId)) { - plugin.joinStateService.localState = state - } - - sender.sendMessage(messages.resolve(updateResult(messages, updated, isGroup = false))) - } - - private fun isCurrentService(group: String, numericalId: Int): Boolean { - val currentServer = plugin.cloudControllerHandler.currentServer ?: return false - if (!currentServer.isFromGroup) return false - - return currentServer.group?.name == group && currentServer.numericalId == numericalId - } - - private fun updateResult(messages: MessageConfig, updated: Boolean, isGroup: Boolean): String { - val updateMessages = when { - isGroup -> messages.command.joinState.group - else -> messages.command.joinState.server - } - - return when { - updated -> updateMessages.updateSuccess - else -> updateMessages.updateFailure - } - } - - private suspend fun joinStateOf(target: String): String { - return when { - plugin.cloudControllerHandler.groupExists(target) -> - plugin.joinStateService.getJoinStateAtGroup(target) - - else -> plugin.joinStateService.getJoinStateAtPersistentServer(target) - } - } - - private fun suggestGroups(): CompletableFuture> = runBlocking { - val targets = plugin.cloudControllerHandler.getAllGroups() + - plugin.cloudControllerHandler.getAllPersistentServerNames() - - CompletableFuture.completedFuture(targets.map { Suggestion.suggestion(it) }) - } - - private fun suggestJoinStates(): CompletableFuture> { - val states = plugin.config.get().joinstates - .map { Suggestion.suggestion(it.name) } - - return CompletableFuture.completedFuture(states) - } - - private fun suggestNumericalIds(context: CommandContext): CompletableFuture> = runBlocking { - val group = groupToken(context) - ?: return@runBlocking CompletableFuture.completedFuture(emptyList()) - - val ids = plugin.cloudControllerHandler.getAllNumericalIdsFromGroup(group) - .map { Suggestion.suggestion(it.toString()) } - - CompletableFuture.completedFuture(ids) - } - - private fun suggestIdsAndJoinStates(context: CommandContext): CompletableFuture> { - val stateSuggestions = plugin.config.get().joinstates - .map { Suggestion.suggestion(it.name) } - val group = groupToken(context) ?: return CompletableFuture.completedFuture(stateSuggestions) - - return runBlocking { - val idSuggestions = plugin.cloudControllerHandler.getAllNumericalIdsFromGroup(group) - .map { Suggestion.suggestion(it.toString()) } - - CompletableFuture.completedFuture(idSuggestions + stateSuggestions) - } - } - - private fun groupToken(context: CommandContext): String? { - return context.rawInput().input() - .split(" ") - .filter { it.isNotBlank() } - .getOrNull(index) - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/LayoutCommandHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/LayoutCommandHandler.kt deleted file mode 100644 index 82b28e9..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/LayoutCommandHandler.kt +++ /dev/null @@ -1,180 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.command.commands - -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.command.ProxyCommandSender -import app.simplecloud.plugin.proxy.shared.config.MessageConfig -import app.simplecloud.plugin.proxy.shared.motd.MotdLayoutRepository -import app.simplecloud.plugin.proxy.shared.utilities.ProxyPermissions -import kotlinx.coroutines.* -import org.incendo.cloud.CommandManager -import org.incendo.cloud.context.CommandContext -import org.incendo.cloud.parser.standard.StringParser -import org.incendo.cloud.suggestion.Suggestion -import java.util.concurrent.CompletableFuture - - -class LayoutCommandHandler( - private val manager: CommandManager, - private val plugin: ProxyPlugin -) { - private val commands = listOf( - "/scproxy layout help", - "/scproxy layout info [group]", - "/scproxy layout set " - ) - - fun loadCommands() { - loadHelp() - loadInfoDefault() - loadInfoGroup() - loadSet() - } - - private fun loadHelp() { - manager.command( - manager.commandBuilder("scproxy") - .literal("layout") - .permission(ProxyPermissions.LAYOUT_HELP) - .handler { context: CommandContext -> handleHelp(context.sender()) } - .build() - ) - manager.command( - manager.commandBuilder("scproxy") - .literal("layout") - .literal("help") - .permission(ProxyPermissions.LAYOUT_HELP) - .handler { context: CommandContext -> handleHelp(context.sender()) } - .build() - ) - } - - private fun handleHelp(sender: ProxyCommandSender) { - val messages = plugin.messageConfig.get() - val entry = messages.command.layout.help.entry - - sender.sendMessage(messages.resolve(messages.command.layout.help.header)) - commands.forEach { command -> - sender.sendMessage(entry.replace("", command)) - } - } - - private fun loadInfoDefault() { - manager.command( - manager.commandBuilder("scproxy") - .literal("layout") - .literal("info") - .permission(ProxyPermissions.LAYOUT_INFO) - .handler { context: CommandContext -> - sendLayoutInfo( - context.sender(), - group = "default", - layoutName = plugin.config.get().initialLayout - ) - } - .build() - ) - } - - private fun loadInfoGroup() { - manager.command( - manager.commandBuilder("scproxy") - .literal("layout") - .literal("info") - .required("group", StringParser.stringParser()) { _, _ -> suggestGroups() } - .permission(ProxyPermissions.LAYOUT_INFO) - .handler { context: CommandContext -> - CoroutineScope(Dispatchers.IO).launch { - val group = context.get("group") - sendLayoutInfo(context.sender(), group, resolveLayoutOfGroup(group)) - } - } - .build() - ) - } - - private suspend fun resolveLayoutOfGroup(group: String): String { - val groupLayout = plugin.cloudControllerHandler - .getGroupProperty(group, MotdLayoutRepository.KEY) - if (groupLayout != null) { - return groupLayout - } - - val config = plugin.config.get() - val stateName = plugin.joinStateService.getJoinStateAtGroup(group) - - return config.joinstates.find { it.name == stateName }?.forcedMotdLayout ?: config.initialLayout - } - - private fun sendLayoutInfo(sender: ProxyCommandSender, group: String, layoutName: String) { - val messages = plugin.messageConfig.get() - - sender.sendMessage(messages.resolve(messages.command.layout.info.header).replace("", group)) - sender.sendMessage(messages.resolve(messages.command.layout.info.entry).replace("", layoutName)) - } - - private fun loadSet() { - manager.command( - manager.commandBuilder("scproxy") - .literal("layout") - .literal("set") - .required("group", StringParser.stringParser()) { _, _ -> suggestGroups() } - .required("layout", StringParser.stringParser()) { _, _ -> suggestLayouts() } - .permission(ProxyPermissions.LAYOUT_SET) - .handler { context: CommandContext -> - CoroutineScope(Dispatchers.IO).launch { handleSet(context) } - } - .build() - ) - } - - private suspend fun handleSet(context: CommandContext) { - val group = context.get("group") - val layout = context.get("layout") - val messages = plugin.messageConfig.get() - - if (layout !in plugin.layoutRepository.getAvailableLayouts()) { - sendSetResult(context.sender(), messages, updated = false) - return - } - - val groupUpdated = plugin.cloudControllerHandler - .updateGroupProperty(group, MotdLayoutRepository.KEY, layout) - val servicesUpdated = plugin.cloudControllerHandler - .updateServicePropertyOnAllGroupServers(group, MotdLayoutRepository.KEY, layout) - val updated = groupUpdated && servicesUpdated - - if (updated && isCurrentGroup(group)) { - plugin.layoutRepository.setLocalLayout(layout) - } - - sendSetResult(context.sender(), messages, updated) - } - - private fun sendSetResult(sender: ProxyCommandSender, messages: MessageConfig, updated: Boolean) { - val message = when { - updated -> messages.command.layout.set.updateSuccess - else -> messages.command.layout.set.updateFailure - } - - sender.sendMessage(messages.resolve(message)) - } - - private fun isCurrentGroup(group: String): Boolean { - val currentServer = plugin.cloudControllerHandler.currentServer ?: return false - return currentServer.isFromGroup && currentServer.group?.name == group - } - - private fun suggestGroups(): CompletableFuture> = runBlocking { - val groups = plugin.cloudControllerHandler.getAllGroups() - .map { Suggestion.suggestion(it) } - - CompletableFuture.completedFuture(groups) - } - - private fun suggestLayouts(): CompletableFuture> { - val layouts = plugin.layoutRepository.getAvailableLayouts() - .map { Suggestion.suggestion(it) } - - return CompletableFuture.completedFuture(layouts) - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ConfigVersion.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ConfigVersion.kt new file mode 100644 index 0000000..acff972 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ConfigVersion.kt @@ -0,0 +1,6 @@ +package app.simplecloud.plugin.proxy.shared.config + +object ConfigVersion { + + const val VERSION = 2 +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MotdLayoutConfiguration.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/LayoutConfig.kt similarity index 96% rename from proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MotdLayoutConfiguration.kt rename to proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/LayoutConfig.kt index 9241970..9471e56 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MotdLayoutConfiguration.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/LayoutConfig.kt @@ -3,7 +3,7 @@ package app.simplecloud.plugin.proxy.shared.config import org.spongepowered.configurate.objectmapping.ConfigSerializable @ConfigSerializable -data class MotdLayoutConfiguration( +data class LayoutConfig( val configVersion: String = "2", val motd: MotdConfig = MotdConfig(), val serverIcon: ServerIconConfig = ServerIconConfig(), @@ -15,7 +15,7 @@ data class MotdLayoutConfiguration( data class MotdConfig( val enabled: Boolean = true, val updateType: MotdUpdateType = MotdUpdateType.RANDOM, - val updateTime: Int? = null, + val updateTime: Long? = null, val layouts: List = listOf(MotdEntry()) ) @@ -62,6 +62,7 @@ data class SlotsConfig( val fakeSlots: Int = 100, val dynamicPlayerRange: Int = 5 ) { + /** The player limit to show in the server list, which may differ from the real one. */ fun resolveMaxPlayers(onlinePlayers: Int, realMaxPlayers: Int): Int { if (!enabled) return realMaxPlayers @@ -77,5 +78,5 @@ data class SlotsConfig( enum class MaxPlayerDisplayType { REAL, FAKE, - DYNAMIC, + DYNAMIC } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MessageConfig.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MessageConfig.kt index 35f9068..2b9a6e7 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MessageConfig.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MessageConfig.kt @@ -2,27 +2,38 @@ package app.simplecloud.plugin.proxy.shared.config import app.simplecloud.plugin.api.shared.config.AbstractMessageConfig import app.simplecloud.plugin.api.shared.config.VersionedConfig -import app.simplecloud.plugin.proxy.shared.utilities.config.ConfigVersion -import app.simplecloud.plugin.proxy.shared.utilities.config.DefaultConfigs import org.spongepowered.configurate.objectmapping.ConfigSerializable @ConfigSerializable data class MessageConfig( override val version: Int = ConfigVersion.VERSION, - override val variables: Map = DefaultConfigs.VARIABLES, - val kick: KickMessageConfig = KickMessageConfig(), + override val variables: Map = mapOf("prefix" to "โšก "), + val format: FormatConfig = FormatConfig(), + val kick: KickMessages = KickMessages(), val command: CommandMessages = CommandMessages() -) : VersionedConfig, AbstractMessageConfig() { +) : VersionedConfig, AbstractMessageConfig() - fun resolve(message: String): String { - return variables.entries.fold(message) { resolved, (key, value) -> - resolved.replace("<$key>", value) - } - } -} +@ConfigSerializable +data class FormatConfig( + val date: String = "dd.MM.yyyy", + val time: String = "HH:mm:ss", + val pingColors: List = listOf( + PingColor(0, ""), + PingColor(50, ""), + PingColor(100, ""), + PingColor(150, ""), + PingColor(200, "") + ) +) @ConfigSerializable -data class KickMessageConfig( +data class PingColor( + val ping: Int = 0, + val color: String = "" +) + +@ConfigSerializable +data class KickMessages( val noPermission: String = "The network is currently in maintenance mode. Please try again later.", val networkFull: String = "The network is currently full. Please try again later.", val noJoinState: String = "No join state found for server. Please try again later." @@ -30,86 +41,80 @@ data class KickMessageConfig( @ConfigSerializable data class CommandMessages( - val reload: ReloadCommandMessages = ReloadCommandMessages(), - val joinState: JoinStateCommandMessages = JoinStateCommandMessages(), - val layout: LayoutCommandMessages = LayoutCommandMessages() + val help: ListMessages = ListMessages( + header = "Available /scproxy commands:", + entry = " " + ), + val failure: String = "The command failed, check the console for details.", + val reload: ReloadMessages = ReloadMessages(), + val joinState: JoinStateMessages = JoinStateMessages(), + val layout: LayoutMessages = LayoutMessages() ) @ConfigSerializable -data class ReloadCommandMessages( - val start: String = "${PREFIX}Reloading ProxyEssentials configurations...", - val success: String = "${PREFIX}Successfully reloaded all ProxyEssentials configurations.", - val failure: String = "${PREFIX}Failed to reload configurations: " +data class ReloadMessages( + val start: String = "Reloading ProxyEssentials configurations...", + val success: String = "Successfully reloaded all ProxyEssentials configurations.", + val failure: String = "Failed to reload configurations: " ) @ConfigSerializable -data class JoinStateCommandMessages( +data class JoinStateMessages( val server: UpdateMessages = UpdateMessages( - "${PREFIX}Join state of server updated successfully.", - "${PREFIX}Failed to update join state of server.", - "${PREFIX}Join state of server did not change." + updateSuccess = "Join state of server updated successfully.", + updateFailure = "Failed to update join state of server.", + updateNoChange = "Join state of server did not change." ), val group: UpdateMessages = UpdateMessages( - "${PREFIX}Join state of group updated successfully.", - "${PREFIX}Failed to update join state of group.", - "${PREFIX}Join state of group did not change." + updateSuccess = "Join state of group updated successfully.", + updateFailure = "Failed to update join state of group.", + updateNoChange = "Join state of group did not change." + ), + val help: ListMessages = ListMessages( + header = "Commands of join state:", + entry = " " ), - val help: JoinStateHelpMessages = JoinStateHelpMessages(), val list: JoinStateListMessages = JoinStateListMessages() ) -@ConfigSerializable -data class JoinStateHelpMessages( - val header: String = "${PREFIX}Commands of join state:", - val entry: String = " " -) - @ConfigSerializable data class JoinStateListMessages( - val groups: JoinStateGroupListMessages = JoinStateGroupListMessages(), - val states: JoinStateStateListMessages = JoinStateStateListMessages() -) - -@ConfigSerializable -data class JoinStateGroupListMessages( - val header: String = "${PREFIX}Groups with their join states:", - val entry: String = " - " -) - -@ConfigSerializable -data class JoinStateStateListMessages( - val header: String = "${PREFIX}Available join states:", - val entry: String = " - " + val groups: ListMessages = ListMessages( + header = "Groups with their join states:", + entry = " - " + ), + val states: ListMessages = ListMessages( + header = "Available join states:", + entry = " - " + ) ) @ConfigSerializable -data class LayoutCommandMessages( - val help: LayoutHelpMessages = LayoutHelpMessages(), - val info: LayoutInfoMessages = LayoutInfoMessages(), +data class LayoutMessages( + val help: ListMessages = ListMessages( + header = "Commands of layout:", + entry = " " + ), + val info: ListMessages = ListMessages( + header = "Layout for :", + entry = " " + ), val set: UpdateMessages = UpdateMessages( - "${PREFIX}Layout for group updated successfully.", - "${PREFIX}Failed to update layout for group.", - "${PREFIX}Layout for group did not change." + updateSuccess = "Layout for group updated successfully.", + updateFailure = "Failed to update layout for group.", + updateNoChange = "Layout for group did not change." ) ) @ConfigSerializable -data class LayoutHelpMessages( - val header: String = "${PREFIX}Commands of layout:", - val entry: String = " " -) - -@ConfigSerializable -data class LayoutInfoMessages( - val header: String = "${PREFIX}Layout for :", - val entry: String = " " +data class ListMessages( + val header: String = "", + val entry: String = "" ) @ConfigSerializable data class UpdateMessages( - val updateSuccess: String = "${PREFIX}Updated successfully.", - val updateFailure: String = "${PREFIX}Failed to update.", - val updateNoChange: String = "${PREFIX}Nothing changed." + val updateSuccess: String = "Updated successfully.", + val updateFailure: String = "Failed to update.", + val updateNoChange: String = "Nothing changed." ) - -const val PREFIX = "โšก " \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/PlaceHolderConfiguration.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/PlaceHolderConfiguration.kt deleted file mode 100644 index 327fa00..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/PlaceHolderConfiguration.kt +++ /dev/null @@ -1,17 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config - -import app.simplecloud.plugin.proxy.shared.utilities.config.DefaultConfigs -import org.spongepowered.configurate.objectmapping.ConfigSerializable - -@ConfigSerializable -data class PlaceHolderConfiguration( - val currentDateFormat: String = "dd.MM.yyyy", - val currentTimeFormat: String = "HH:mm:ss", - val pingColors: List = DefaultConfigs.PING_COLORS -) - -@ConfigSerializable -data class PingColor( - val ping: Int, - val color: String -) diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ProxyEssentialsConfig.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ProxyEssentialsConfig.kt index 14154d8..ed4e04b 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ProxyEssentialsConfig.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ProxyEssentialsConfig.kt @@ -1,9 +1,8 @@ package app.simplecloud.plugin.proxy.shared.config import app.simplecloud.plugin.api.shared.config.VersionedConfig -import app.simplecloud.plugin.proxy.shared.utilities.config.ConfigVersion -import app.simplecloud.plugin.proxy.shared.utilities.config.DefaultConfigs import org.spongepowered.configurate.objectmapping.ConfigSerializable +import java.util.UUID @ConfigSerializable data class ProxyEssentialsConfig( @@ -11,36 +10,28 @@ data class ProxyEssentialsConfig( val initialState: String = "public", val initialLayout: String = "public", val showKickReason: Boolean = false, - val joinstates: List = DefaultConfigs.JOIN_STATES, - val domains: List = listOf(), + val joinstates: List = listOf( + JoinStateConfig( + name = "public", + permission = JoinStatePermission(full = "simplecloud.proxy-essentials.join.full.public") + ), + JoinStateConfig( + name = "maintenance", + permission = JoinStatePermission( + join = "simplecloud.proxy-essentials.join.maintenance", + full = "simplecloud.proxy-essentials.join.maintenance" + ), + forcedMotdLayout = "maintenance" + ) + ), + val domains: List = emptyList(), val whitelist: WhitelistConfig = WhitelistConfig(), val playerCount: PlayerCountConfig = PlayerCountConfig(), - val tablist: List = DefaultConfigs.TABLIST_GROUPS -) : VersionedConfig { - - /** The tab list is updated as often as the most frequently updating group demands. */ - fun tabListUpdateTimeMillis(): Long { - val ticks = tablist.minOfOrNull { it.updateTime } ?: DEFAULT_UPDATE_TIME_TICKS - return ticks * MILLIS_PER_TICK - } - - /** Returns null while the network wide player count is disabled. */ - fun playerCountUpdateTimeMillis(): Long? { - if (!playerCount.enabled || playerCount.updateTime <= 0L) { - return null - } - - return playerCount.updateTime * MILLIS_PER_TICK - } - - companion object { - private const val MILLIS_PER_TICK = 50L - private const val DEFAULT_UPDATE_TIME_TICKS = 20L - } -} + val tablist: List = listOf(TabListGroup()) +) : VersionedConfig @ConfigSerializable -data class JoinState( +data class JoinStateConfig( val name: String = "", val permission: JoinStatePermission = JoinStatePermission(), val forcedMotdLayout: String? = null @@ -53,23 +44,31 @@ data class JoinStatePermission( ) @ConfigSerializable -data class DomainMotdRoute( +data class DomainRoute( val domain: String = "", val target: String = "", - val rules: List = listOf() + val rules: List = emptyList() ) @ConfigSerializable -data class DomainMotdRule( +data class DomainRule( val state: String = "", val layout: String = "" ) @ConfigSerializable data class WhitelistConfig( - val enabled: Boolean = true, - val players: List = listOf("Notch") -) + val enabled: Boolean = false, + val players: List = emptyList() +) { + + fun contains(name: String, uuid: UUID): Boolean { + if (!enabled) return false + + val uuidString = uuid.toString() + return players.any { it.equals(name, ignoreCase = true) || it.equals(uuidString, ignoreCase = true) } + } +} @ConfigSerializable data class PlayerCountConfig( @@ -81,13 +80,13 @@ data class PlayerCountConfig( @ConfigSerializable data class TabListGroup( - val name: String = "*", - val layout: List = listOf(TabList()), - val updateTime: Long = 20L, + val name: String = "global", + val layout: List = listOf(TabListEntry()), + val updateTime: Long = 20L ) @ConfigSerializable -data class TabList( +data class TabListEntry( val header: String = "
SimpleCloud v3
", - val footer: String = "
players are playing on your network
sษชแดแด˜สŸแด‡แด„สŸแดแดœแด….แด€แด˜แด˜
", + val footer: String = "
players are playing on your network
sษชแดแด˜สŸแด‡แด„สŸแดแดœแด….แด€แด˜แด˜
" ) diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/DefaultConfigInstaller.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/DefaultConfigInstaller.kt similarity index 98% rename from proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/DefaultConfigInstaller.kt rename to proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/DefaultConfigInstaller.kt index 0fe4a0a..95a1720 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/DefaultConfigInstaller.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/DefaultConfigInstaller.kt @@ -1,4 +1,4 @@ -package app.simplecloud.plugin.proxy.shared.config +package app.simplecloud.plugin.proxy.shared.config.migration import java.net.JarURLConnection import java.nio.file.Files @@ -115,7 +115,6 @@ object DefaultConfigInstaller { private val KNOWN_RESOURCES = listOf( "config/config.yml", "config/messages.yml", - "config/placeholder.yml", "config/layout/public.yml", "config/layout/maintenance.yml", "config/layout/server-icons/simplecloud.png", diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/LegacyDirectoryMigrator.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/LegacyDirectoryMigrator.kt index 6a77b08..b1d7869 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/LegacyDirectoryMigrator.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/LegacyDirectoryMigrator.kt @@ -3,83 +3,37 @@ package app.simplecloud.plugin.proxy.shared.config.migration import org.slf4j.LoggerFactory import java.nio.file.Files import java.nio.file.Path -import java.nio.file.StandardCopyOption object LegacyDirectoryMigrator { private val logger = LoggerFactory.getLogger(LegacyDirectoryMigrator::class.java) private val LEGACY_DIRECTORY_NAMES = listOf("proxy-essentials-velocity", "proxy-essentials-bungeecord") - private const val MARKER_FILE = ".migrated-from-legacy-directory" - fun migrate(directory: Path) { - val legacyDirectory = findLegacyDirectory(directory) ?: return - - when { - isAlreadyMigrated(directory) -> warnAboutUnusedDirectory(legacyDirectory, directory) - else -> takeOverConfiguration(legacyDirectory, directory) - } - } - - private fun findLegacyDirectory(dataDirectory: Path): Path? { + fun migrate(dataDirectory: Path) { val directory = dataDirectory.toAbsolutePath().normalize() - val pluginsDirectory = directory.parent ?: return null - - return LEGACY_DIRECTORY_NAMES + val pluginsDirectory = directory.parent ?: return + val legacyDirectory = LEGACY_DIRECTORY_NAMES .map { pluginsDirectory.resolve(it) } - .find { it != directory && Files.isDirectory(it) } - } + .find { it != directory && Files.isDirectory(it) } ?: return - private fun isAlreadyMigrated(directory: Path): Boolean { - if (Files.exists(directory.resolve(MARKER_FILE))) { - return true - } - - if (!Files.isDirectory(directory)) { - return false + if (Files.isDirectory(directory) && Files.list(directory).use { it.findAny().isPresent }) { + logger.warn( + "The directory '${legacyDirectory.fileName}' is not used anymore. " + + "Rename it to '${directory.fileName}' in your template to get rid of this warning." + ) + return } - return Files.list(directory).use { paths -> paths.findAny().isPresent } - } - - private fun takeOverConfiguration(legacyDirectory: Path, dataDirectory: Path) { try { - copyDirectory(legacyDirectory, dataDirectory) - Files.createFile(dataDirectory.resolve(MARKER_FILE)) + legacyDirectory.toFile().copyRecursively(directory.toFile(), overwrite = true) } catch (e: Exception) { - logger.error("Could not take over the configuration of '${legacyDirectory.fileName}'") + logger.error("Could not take over the configuration of '${legacyDirectory.fileName}', so the default configuration is used", e) return } logger.info( - "Took over the configuration of '${legacyDirectory.fileName}' into '${dataDirectory.fileName}'. " + - "Rename that directory in your template to keep editing your configuration." + "Took over the configuration of '${legacyDirectory.fileName}' into '${directory.fileName}'. " + + "Rename that directory in your template to keep editing your configuration." ) } - - private fun warnAboutUnusedDirectory(legacyDirectory: Path, dataDirectory: Path) { - logger.warn( - "The directory '${legacyDirectory.fileName}' is not used anymore. " + - "Rename it to '${dataDirectory.fileName}' in your template to get rid of this warning." - ) - } - - private fun copyDirectory(source: Path, target: Path) { - Files.createDirectories(target) - - Files.walk(source).use { paths -> - paths.forEach { path -> copyEntry(source, target, path) } - } - } - - private fun copyEntry(source: Path, target: Path, path: Path) { - val destination = target.resolve(source.relativize(path).toString()) - - when { - Files.isDirectory(path) -> Files.createDirectories(destination) - else -> { - Files.createDirectories(destination.parent) - Files.copy(path, destination, StandardCopyOption.REPLACE_EXISTING) - } - } - } } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/OldConfigMigrator.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/OldConfigMigrator.kt index 07c6501..45fbe6a 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/OldConfigMigrator.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/OldConfigMigrator.kt @@ -1,18 +1,18 @@ package app.simplecloud.plugin.proxy.shared.config.migration import app.simplecloud.plugin.proxy.shared.config.MessageConfig -import app.simplecloud.plugin.proxy.shared.config.PlaceHolderConfiguration +import app.simplecloud.plugin.proxy.shared.config.FormatConfig import app.simplecloud.plugin.proxy.shared.config.ProxyEssentialsConfig import app.simplecloud.plugin.proxy.shared.config.WhitelistConfig -import org.spongepowered.configurate.CommentedConfigurationNode -import org.spongepowered.configurate.kotlin.objectMapperFactory -import org.spongepowered.configurate.yaml.NodeStyle -import org.spongepowered.configurate.yaml.YamlConfigurationLoader import java.nio.file.Files import java.nio.file.Path import kotlin.io.path.extension import kotlin.io.path.isRegularFile import kotlin.io.path.name +import org.spongepowered.configurate.CommentedConfigurationNode +import org.spongepowered.configurate.kotlin.objectMapperFactory +import org.spongepowered.configurate.yaml.NodeStyle +import org.spongepowered.configurate.yaml.YamlConfigurationLoader object OldConfigMigrator { @@ -338,7 +338,7 @@ object OldConfigMigrator { !source.node("pingColors").empty() if (!legacyCamelCase && !legacyKebabCase) return - val defaults = PlaceHolderConfiguration() + val defaults = FormatConfig() val pingColors = source.node("ping-colors") .childrenList() .ifEmpty { source.node("pingColors").childrenList() } @@ -348,12 +348,12 @@ object OldConfigMigrator { target.node("current-date-format").set( source.node("current-date-format").string ?: source.node("currentDateFormat").string - ?: defaults.currentDateFormat + ?: defaults.date ) target.node("current-time-format").set( source.node("current-time-format").string ?: source.node("currentTimeFormat").string - ?: defaults.currentTimeFormat + ?: defaults.time ) target.node("ping-colors").set( pingColors @@ -707,7 +707,7 @@ object OldConfigMigrator { "Initial settings used when a group is created for the first time." private const val WHITELIST_COMMENT = - "Global whitelist for direct player access.\n" + + "Players on this list can always join, even during maintenance or when the network is full.\n" + "Prefer permission-based access for regular users.\n" + "Use this list only for administrators or emergency access." @@ -765,7 +765,7 @@ object OldConfigMigrator { "# Initial settings used when a group is created for the first time.\n" private const val WHITELIST_YAML_COMMENT = - "\n# Global whitelist for direct player access.\n" + + "\n# Players on this list can always join, even during maintenance or when the network is full.\n" + "# Prefer permission-based access for regular users.\n" + "# Use this list only for administrators or emergency access.\n" diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/controller/CloudControllerHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/controller/CloudControllerHandler.kt deleted file mode 100644 index 0544a25..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/controller/CloudControllerHandler.kt +++ /dev/null @@ -1,304 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.controller - -import app.simplecloud.api.group.Group -import app.simplecloud.api.persistentserver.PersistentServer -import app.simplecloud.api.runtime.SimpleCloudRuntime -import app.simplecloud.api.server.Server -import app.simplecloud.api.server.ServerQuery -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.joinstate.JoinStateService -import app.simplecloud.plugin.proxy.shared.motd.MotdLayoutRepository -import kotlinx.coroutines.* -import kotlinx.coroutines.future.await -import org.slf4j.LoggerFactory - -class CloudControllerHandler( - private val plugin: ProxyPlugin, - private val service: JoinStateService -) { - - private val logger = LoggerFactory.getLogger(CloudControllerHandler::class.java) - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - - @Volatile - var currentServer: Server? = null - private set - - fun start() { - if (SimpleCloudRuntime.serverId().isNullOrBlank()) { - logger.warn("Environment variable SIMPLECLOUD_UNIQUE_ID is not set.") - registerListeners() - return - } - - scope.launch { - try { - currentServer = plugin.api.server().getServerById(SimpleCloudRuntime.serverId()).await() - logger.info("Initialized server: ${currentServer?.serverBase?.name}") - - registerListeners() - initializeJoinState() - } catch (e: Exception) { - logger.error("Error retrieving server by ID", e) - registerListeners() - } - } - } - - fun close() { - scope.cancel() - } - - private fun registerListeners() { - service.registerListener() - plugin.layoutRepository.registerListener() - } - - private suspend fun initializeJoinState() { - val server = currentServer ?: return - - when { - server.isFromGroup -> initializeJoinStateOfGroupServer(server) - else -> initializeJoinStateOfPersistentServer(server) - } - } - - private suspend fun initializeJoinStateOfGroupServer(server: Server) { - val groupName = server.group?.name ?: return - - service.localState = service.getJoinStateAtService(groupName, server.numericalId) - service.startGroupStateSyncTask() - plugin.layoutRepository.setLocalLayout( - getServiceProperty(groupName, server.numericalId, MotdLayoutRepository.KEY) - ?: getGroupProperty(groupName, MotdLayoutRepository.KEY) - ) - } - - private suspend fun initializeJoinStateOfPersistentServer(server: Server) { - val joinState = server.properties?.get(JoinStateService.KEY)?.toString() - val persistentServerName = server.persistentServer?.name - - when { - !joinState.isNullOrEmpty() -> service.localState = joinState - persistentServerName != null -> - service.localState = service.ensureJoinStateAtPersistentServer(persistentServerName) - } - - plugin.layoutRepository.setLocalLayout( - server.properties?.get(MotdLayoutRepository.KEY)?.toString() - ) - } - - suspend fun updateServerProperty(serverId: String, key: String, value: String): Boolean { - return try { - plugin.api.server().updateServerProperties(serverId, mapOf(key to value)).await() - true - } catch (e: Exception) { - logger.error("Error updating server property", e) - false - } - } - - suspend fun groupExists(groupName: String): Boolean = getGroupByName(groupName) != null - - suspend fun getGroupProperty(groupName: String, key: String): String? { - return try { - getGroupByName(groupName)?.properties?.get(key)?.toString().orNullIfBlank() - } catch (e: Exception) { - logger.error("Error retrieving group property", e) - null - } - } - - suspend fun updateGroupProperty(groupName: String, key: String, value: String): Boolean { - val group = getGroupByName(groupName) ?: return false - return try { - plugin.api.group().updateGroupProperties(group.serverGroupId, mapOf(key to value)).await() - logger.info("Group property '$key' updated to '$value' for group '$groupName'") - true - } catch (e: Exception) { - logger.error("Error updating group properties", e) - false - } - } - - suspend fun getAllGroups(): List { - return try { - plugin.api.group().allGroups.await().map { it.name } - } catch (e: Exception) { - logger.error("Error retrieving groups", e) - emptyList() - } - } - - suspend fun getMaxPlayersInGroup(groupName: String): Int = getGroupByName(groupName)?.maxPlayers ?: 0 - - suspend fun getOnlinePlayersInGroup(groupName: String): Int { - return try { - getServersByGroup(groupName).sumOf { it.playerCount ?: 0 } - } catch (e: Exception) { - logger.error("Error retrieving online players in group", e) - 0 - } - } - - suspend fun getAllNumericalIdsFromGroup(groupName: String): List { - return getServersByGroup(groupName).map { it.numericalId } - } - - suspend fun getPersistentServerByName(name: String): PersistentServer? { - val persistentServer = try { - plugin.api.persistentServer().getPersistentServerByName(name).await() - } catch (_: Exception) { - null - } - if (persistentServer != null) { - return persistentServer - } - - return try { - plugin.api.persistentServer().allPersistentServers.await() - .firstOrNull { it.name.equals(name, ignoreCase = true) } - } catch (e: Exception) { - logger.error("Error retrieving persistent server '$name'", e) - null - } - } - - suspend fun getPersistentServerProperty(name: String, key: String): String? { - return try { - getPersistentServerByName(name)?.properties?.get(key)?.toString().orNullIfBlank() - } catch (e: Exception) { - logger.error("Error retrieving persistent server property", e) - null - } - } - - suspend fun updatePersistentServerProperty(name: String, key: String, value: String): Boolean { - val persistentServer = getPersistentServerByName(name) ?: return false - return try { - plugin.api.persistentServer() - .updatePersistentServerProperty(persistentServer.persistentServerId, key, value) - .await() - logger.info("Persistent server property '$key' updated to '$value' for persistent server '$name'") - true - } catch (e: Exception) { - logger.error("Error updating persistent server property", e) - false - } - } - - suspend fun getAllPersistentServerNames(): List { - return try { - plugin.api.persistentServer().allPersistentServers.await().map { it.name } - } catch (e: Exception) { - logger.error("Error retrieving persistent servers", e) - emptyList() - } - } - - suspend fun getServersByGroup(groupName: String): List { - return try { - plugin.api.server().getAllServers( - ServerQuery.create().filterByServerGroupName(resolveGroupNameCasing(groupName)) - ).await() ?: emptyList() - } catch (e: Exception) { - logger.error("Error retrieving servers for group '$groupName'", e) - emptyList() - } - } - - suspend fun getServerByNumericalId(groupName: String, numericalId: Int): Server? { - return try { - plugin.api.server().getAllServers( - ServerQuery.create() - .filterByServerGroupName(resolveGroupNameCasing(groupName)) - .filterByNumericalId(numericalId) - ).await()?.firstOrNull() - } catch (e: Exception) { - logger.error("Error retrieving server '$groupName-$numericalId'", e) - null - } - } - - suspend fun getServerByName(name: String): Server? { - return try { - val servers = plugin.api.server().getAllServers(ServerQuery.create()).await() ?: return null - servers.find { it.serverBase.name == name || it.persistentServer?.name == name } - ?: servers.find { - it.serverBase.name.equals(name, ignoreCase = true) - || it.persistentServer?.name.equals(name, ignoreCase = true) - } - } catch (e: Exception) { - logger.error("Error retrieving server by name", e) - null - } - } - - suspend fun getPersistentServersByNames(names: Set): List { - if (names.isEmpty()) { - return emptyList() - } - - return try { - val servers = plugin.api.server().getAllServers(ServerQuery.create()).await() ?: return emptyList() - servers.filter { server -> - val persistentServerName = server.persistentServer?.name ?: return@filter false - persistentServerName in names - } - } catch (e: Exception) { - logger.error("Error retrieving persistent servers by name", e) - emptyList() - } - } - - suspend fun getServiceProperty(groupName: String, numericalId: Int, key: String): String? { - val server = getServerByNumericalId(groupName, numericalId) ?: return null - return server.properties[key]?.toString().orNullIfBlank() - } - - suspend fun updateServiceProperty(groupName: String, numericalId: Int, key: String, value: String): Boolean { - val server = getServerByNumericalId(groupName, numericalId) - if (server == null) { - logger.error("Server not found for group '$groupName' and numerical ID '$numericalId'") - return false - } - return updateServerProperty(server.serverId, key, value) - } - - suspend fun updateServicePropertyOnAllGroupServers(groupName: String, key: String, value: String): Boolean { - val results = getServersByGroup(groupName).map { updateServerProperty(it.serverId, key, value) } - val allSuccessful = results.all { it } - - if (allSuccessful) { - logger.info("Service property '$key' updated to '$value' for all services in group '$groupName'") - } - return allSuccessful - } - - private suspend fun getGroupByName(groupName: String): Group? { - val group = try { - plugin.api.group().getGroupByName(groupName).await() - } catch (_: Exception) { - null - } - if (group != null) { - return group - } - - return try { - plugin.api.group().allGroups.await().firstOrNull { it.name.equals(groupName, ignoreCase = true) } - } catch (e: Exception) { - logger.error("Error retrieving group '$groupName'", e) - null - } - } - - private suspend fun resolveGroupNameCasing(groupName: String): String = - getGroupByName(groupName)?.name ?: groupName - - private fun String?.orNullIfBlank(): String? { - if (isNullOrBlank()) return null - return this - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinGate.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinGate.kt new file mode 100644 index 0000000..a8ba1f3 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinGate.kt @@ -0,0 +1,91 @@ +package app.simplecloud.plugin.proxy.shared.joinstate + +import app.simplecloud.api.CloudApi +import app.simplecloud.api.runtime.SimpleCloudRuntime +import app.simplecloud.plugin.api.shared.config.ConfigurationFactory +import app.simplecloud.plugin.proxy.shared.config.JoinStateConfig +import app.simplecloud.plugin.proxy.shared.config.MessageConfig +import app.simplecloud.plugin.proxy.shared.config.ProxyEssentialsConfig +import kotlinx.coroutines.future.await +import org.slf4j.LoggerFactory +import java.util.UUID + +class JoinGate( + private val api: CloudApi, + private val service: JoinStateService, + private val config: ConfigurationFactory, + private val messages: ConfigurationFactory +) { + + private val logger = LoggerFactory.getLogger(JoinGate::class.java) + + suspend fun checkProxyJoin(name: String, uuid: UUID, hasPermission: (String) -> Boolean): JoinResult { + if (config.get().whitelist.contains(name, uuid)) return JoinResult.Allowed + + val kick = messages.get().kick + val joinState = findJoinState(service.getLocalState()) ?: return JoinResult.Denied(kick.noJoinState) + if (!hasJoinPermission(joinState, hasPermission)) { + logger.info("$name is missing the permission '${joinState.permission.join}' to join the proxy") + return JoinResult.Denied(kick.noPermission) + } + + if (isProxyFull() && !hasPermission(joinState.permission.full)) return JoinResult.Denied(kick.networkFull) + return JoinResult.Allowed + } + + suspend fun checkServerSwitch(name: String, uuid: UUID, serverName: String, hasPermission: (String) -> Boolean): JoinResult { + if (config.get().whitelist.contains(name, uuid)) return JoinResult.Allowed + + val kick = messages.get().kick + val joinState = findJoinState(getServerState(serverName)) ?: return JoinResult.Denied(kick.noJoinState) + if (!hasJoinPermission(joinState, hasPermission)) { + logger.info("$name is missing the permission '${joinState.permission.join}' to join '$serverName'") + return JoinResult.Denied(kick.noPermission) + } + + return JoinResult.Allowed + } + + private fun findJoinState(name: String): JoinStateConfig? { + val config = config.get() + val joinState = config.joinstates.find { it.name == name } ?: config.joinstates.find { it.name == config.initialState } + if (joinState == null) { + logger.error("Neither the join state '$name' nor the initial state '${config.initialState}' exist in config.yml, so nobody can join.") + } + return joinState + } + + private fun hasJoinPermission(joinState: JoinStateConfig, hasPermission: (String) -> Boolean): Boolean { + val permission = joinState.permission.join + return permission.isBlank() || hasPermission(permission) + } + + private suspend fun getServerState(serverName: String): String { + return try { + service.getServerState(serverName) + } catch (e: Exception) { + logger.error("Could not load the join state of '$serverName', using the initial state instead", e) + config.get().initialState + } + } + + private suspend fun isProxyFull(): Boolean { + val serverId = SimpleCloudRuntime.serverId() + if (serverId.isBlank()) return false + + return try { + val server = api.server().getServerById(serverId).await() + val group = server.group + if (group == null) { + (server.playerCount ?: 0) >= server.maxPlayers + } else { + val maxPlayers = group.maxPlayers ?: 0 + val onlinePlayers = api.server().getServersByGroup(group.name).await().sumOf { it.playerCount ?: 0 } + maxPlayers in 1..onlinePlayers + } + } catch (e: Exception) { + logger.error("Could not check whether the proxy is full, letting the player join", e) + false + } + } +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinResult.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinResult.kt new file mode 100644 index 0000000..8d4bf89 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinResult.kt @@ -0,0 +1,8 @@ +package app.simplecloud.plugin.proxy.shared.joinstate + +sealed interface JoinResult { + + data object Allowed : JoinResult + + data class Denied(val message: String) : JoinResult +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateResolver.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateResolver.kt deleted file mode 100644 index bdcf6dc..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateResolver.kt +++ /dev/null @@ -1,79 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.joinstate - -import app.simplecloud.plugin.api.shared.pattern.ServerPatternIdentifier -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.config.JoinState -import org.slf4j.LoggerFactory - -class JoinStateResolver( - private val plugin: ProxyPlugin -) { - - private val logger = LoggerFactory.getLogger(JoinStateResolver::class.java) - - private val identifier by lazy { - ServerPatternIdentifier( - "-", - "(?[a-zA-Z0-9_-]+)-(?\\d+)", - cloudApi = plugin.api - ) - } - - fun resolveJoinState(stateName: String): JoinState? { - val config = plugin.config.get() - val joinState = config.joinstates.find { it.name == stateName } - if (joinState != null) { - return joinState - } - - logger.info("Join state '$stateName' not found. Using default '${config.initialState}'.") - return config.joinstates.find { it.name == config.initialState } - } - - suspend fun getJoinStateForServer(serverName: String): String { - val serviceJoinState = getJoinStateOfService(serverName) - if (serviceJoinState != null) { - return serviceJoinState - } - - val server = plugin.cloudControllerHandler.getServerByName(serverName) - ?: return plugin.config.get().initialState - - val joinState = server.properties?.get(JoinStateService.KEY)?.toString() - if (!joinState.isNullOrEmpty()) { - return joinState - } - - val persistentServerName = server.persistentServer?.name - ?: return plugin.config.get().initialState - - return plugin.joinStateService.getJoinStateAtPersistentServer(persistentServerName) - } - - private suspend fun getJoinStateOfService(serverName: String): String? { - val (groupName, numericalId) = try { - identifier.parse(serverName) - } catch (_: IllegalArgumentException) { - return null - } - - return plugin.joinStateService.getJoinStateAtService(groupName, numericalId) - } - - suspend fun isServerFull(): Boolean { - val server = plugin.cloudControllerHandler.currentServer ?: return false - - if (!server.isFromGroup) { - val maxPlayers = server.maxPlayers ?: return false - return (server.playerCount ?: 0) >= maxPlayers - } - - val groupName = server.group?.name ?: return false - val maxPlayers = plugin.cloudControllerHandler.getMaxPlayersInGroup(groupName) - if (maxPlayers <= 0) { - return false - } - - return plugin.cloudControllerHandler.getOnlinePlayersInGroup(groupName) >= maxPlayers - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateService.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateService.kt index 4f99c80..5151331 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateService.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateService.kt @@ -1,214 +1,154 @@ package app.simplecloud.plugin.proxy.shared.joinstate +import app.simplecloud.api.CloudApi import app.simplecloud.api.runtime.SimpleCloudRuntime import app.simplecloud.api.server.Server -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import kotlinx.coroutines.* +import app.simplecloud.plugin.api.shared.config.ConfigurationFactory +import app.simplecloud.plugin.proxy.shared.config.ProxyEssentialsConfig +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.future.await +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import org.slf4j.LoggerFactory -import kotlin.time.Duration.Companion.milliseconds +import java.util.concurrent.atomic.AtomicReference +import kotlin.time.Duration.Companion.seconds class JoinStateService( - private val plugin: ProxyPlugin + private val api: CloudApi, + private val config: ConfigurationFactory, + private val scope: CoroutineScope ) { private val logger = LoggerFactory.getLogger(JoinStateService::class.java) - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private var syncJob: Job? = null + private val localState = AtomicReference() + private val lastGroupState = AtomicReference() - @Volatile - private var lastObservedGroupState: String? = null + fun getLocalState(): String = localState.get() ?: config.get().initialState - @Volatile - var localState: String = plugin.config.get().initialState + suspend fun start() { + val server = api.server().getServerById(SimpleCloudRuntime.serverId()).await() + localState.set(readState(server.properties) ?: applyDefaultState(server)) - fun stop() { - syncJob?.cancel() - scope.cancel() - } - - suspend fun getJoinStateAtGroup(groupName: String): String { - return controller().getGroupProperty(groupName, KEY) ?: defaultJoinState() - } + api.event().server().onUpdated { event -> + if (event.serverId != server.serverId) return@onUpdated + scope.launch { updateLocalState(event.server) } + } - suspend fun setJoinStateAtGroup(groupName: String, joinStateName: String): Boolean { - val updated = controller().updateGroupProperty(groupName, KEY, joinStateName) - if (updated) { - lastObservedGroupState = joinStateName + val groupName = server.group?.name ?: return + scope.launch { + while (isActive) { + try { + followGroupState(server.serverId, groupName) + } catch (e: Exception) { + logger.error("Could not synchronize the join state with group '$groupName'", e) + } + delay(2.seconds) + } } - return updated } - suspend fun ensureJoinStateAtGroup(groupName: String): String { - val existingJoinState = controller().getGroupProperty(groupName, KEY) - if (!existingJoinState.isNullOrBlank()) { - return existingJoinState - } + suspend fun getGroupState(groupName: String): String = + readState(api.group().getGroupByName(groupName).await().properties) ?: config.get().initialState - val fallbackState = defaultJoinState() - if (!setJoinStateAtGroup(groupName, fallbackState)) { - logger.warn("Could not persist default join state '$fallbackState' for group '$groupName'") - } - return fallbackState + suspend fun getServiceState(groupName: String, numericalId: Int): String { + val service = api.server().getServerByNumericalId(groupName, numericalId).await() + return readState(service.properties) ?: getGroupState(groupName) } - suspend fun getJoinStateAtService(groupName: String, numericalId: Int): String { - return controller().getServiceProperty(groupName, numericalId, KEY) - ?: controller().getGroupProperty(groupName, KEY) - ?: defaultJoinState() - } + suspend fun getPersistentServerState(name: String): String = + readState(api.persistentServer().getPersistentServerByName(name).await().properties) ?: config.get().initialState - suspend fun setJoinStateAtService(groupName: String, numericalId: Int, joinStateName: String): Boolean { - return controller().updateServiceProperty(groupName, numericalId, KEY, joinStateName) + suspend fun getTargetState(name: String): String { + if (isGroup(name)) return getGroupState(name) + return getPersistentServerState(name) } - suspend fun getJoinStateAtPersistentServer(serverName: String): String { - return controller().getPersistentServerProperty(serverName, KEY) ?: defaultJoinState() - } + suspend fun getServerState(serverName: String): String { + val server = api.server().allServers.await().find { getServerName(it) == serverName } ?: return config.get().initialState + val state = readState(server.properties) + if (state != null) return state - suspend fun ensureJoinStateAtPersistentServer(serverName: String): String { - val existingJoinState = controller().getPersistentServerProperty(serverName, KEY) - if (!existingJoinState.isNullOrBlank()) { - return existingJoinState - } + val groupName = server.group?.name + if (groupName != null) return getGroupState(groupName) - val fallbackState = defaultJoinState() - if (!setJoinStateAtPersistentServer(serverName, fallbackState)) { - logger.warn("Could not persist default join state '$fallbackState' for persistent server '$serverName'") - } - return fallbackState + val persistentServerName = server.persistentServer?.name ?: return config.get().initialState + return getPersistentServerState(persistentServerName) } - suspend fun setJoinStateAtPersistentServer(serverName: String, joinStateName: String): Boolean { - val updated = controller().updatePersistentServerProperty(serverName, KEY, joinStateName) - if (updated) { - syncLocalStateWithPersistentServer(serverName, joinStateName) - } - return updated + suspend fun setGroupState(groupName: String, state: String) { + val group = api.group().getGroupByName(groupName).await() + api.group().updateGroupProperty(group.serverGroupId, KEY, state).await() + api.server().getServersByGroup(groupName).await().forEach { setServerState(it.serverId, state) } } - suspend fun setJoinStateAtGroupAndAllServicesInGroup(groupName: String, joinStateName: String): Boolean { - val groupPropertyUpdated = setJoinStateAtGroup(groupName, joinStateName) - val servicePropertiesUpdated = - controller().updateServicePropertyOnAllGroupServers(groupName, KEY, joinStateName) - - val successful = groupPropertyUpdated && servicePropertiesUpdated - if (!successful) { - logger.error("Error setting join state at group and all services in group $groupName.") - } - return successful + suspend fun setServiceState(groupName: String, numericalId: Int, state: String) { + val service = api.server().getServerByNumericalId(groupName, numericalId).await() + setServerState(service.serverId, state) } - fun startGroupStateSyncTask() { - if (syncJob?.isActive == true) { - return - } - - syncJob = scope.launch { - while (isActive) { - try { - syncLocalStateWithGroupState() - } catch (e: Exception) { - logger.error("Error while syncing local/group join state", e) - } - delay(2000.milliseconds) - } - } + suspend fun setPersistentServerState(name: String, state: String) { + val persistentServer = api.persistentServer().getPersistentServerByName(name).await() + api.persistentServer().updatePersistentServerProperty(persistentServer.persistentServerId, KEY, state).await() + api.server().allServers.await() + .filter { it.persistentServerId == persistentServer.persistentServerId } + .forEach { setServerState(it.serverId, state) } } - fun registerListener() { - plugin.api.event().server().onUpdated { event -> - if (event.serverId != SimpleCloudRuntime.serverId()) return@onUpdated + suspend fun isGroup(name: String): Boolean = api.group().allGroups.await().any { it.name == name } - val server = event.server - val state = server.properties?.get(KEY)?.toString() - - when { - state.isNullOrBlank() -> { - logger.warn("No join state found for server. Using default join state.") - scope.launch { applyDefaultJoinStateForCurrentServer(server) } - } - - state != localState -> { - localState = state - logger.info("Join state changed to $state") - } - } - } + private suspend fun setServerState(serverId: String, state: String) { + api.server().updateServerProperty(serverId, KEY, state).await() } - private fun controller() = plugin.cloudControllerHandler - - private fun defaultJoinState(): String = plugin.config.get().initialState - - private suspend fun syncLocalStateWithPersistentServer(serverName: String, joinStateName: String) { - val currentServer = controller().currentServer ?: return - if (currentServer.isFromGroup || currentServer.persistentServer?.name != serverName) { + private suspend fun updateLocalState(server: Server) { + val state = try { + readState(server.properties) ?: applyDefaultState(server) + } catch (e: Exception) { + logger.error("Could not apply the default join state to this server", e) return } - val synchronized = controller().updateServerProperty(currentServer.serverId, KEY, joinStateName) - if (synchronized) { - localState = joinStateName + if (localState.getAndSet(state) != state) { + logger.info("Join state changed to '$state'") } } - private suspend fun applyDefaultJoinStateForCurrentServer(server: Server) { - try { - val persistentServerName = server.persistentServer?.name - val stateToApply = when { - server.isFromGroup -> ensureJoinStateAtGroup(server.group?.name ?: return) - persistentServerName != null -> ensureJoinStateAtPersistentServer(persistentServerName) - else -> defaultJoinState() - } - - controller().updateServerProperty(server.serverId, KEY, stateToApply) - localState = stateToApply - } catch (e: Exception) { - logger.error("Error setting default join state", e) + private suspend fun applyDefaultState(server: Server): String { + val groupName = server.group?.name + val persistentServerName = server.persistentServer?.name + val state = when { + groupName != null -> getGroupState(groupName) + persistentServerName != null -> getPersistentServerState(persistentServerName) + else -> config.get().initialState } - } - - private suspend fun syncLocalStateWithGroupState() { - val server = controller().currentServer ?: return - if (!server.isFromGroup) return - val groupName = server.group?.name ?: return - val numericalId = server.numericalId - - val groupState = controller().getGroupProperty(groupName, KEY) - if (groupState.isNullOrBlank()) return - - val previousGroupState = lastObservedGroupState - if (previousGroupState == null) { - lastObservedGroupState = groupState - return - } - - val serviceState = controller().getServiceProperty(groupName, numericalId, KEY) - if (serviceState.isNullOrBlank()) { - applyGroupState(server.serverId, "$groupName-$numericalId", groupState, "service state was missing") - return - } + setServerState(server.serverId, state) + return state + } - if (groupState == previousGroupState) return - lastObservedGroupState = groupState + private suspend fun followGroupState(serverId: String, groupName: String) { + val groupState = readState(api.group().getGroupByName(groupName).await().properties) ?: return + val previousGroupState = lastGroupState.getAndSet(groupState) + if (previousGroupState == null || previousGroupState == groupState) return + if (getLocalState() != previousGroupState) return - if (serviceState != previousGroupState) return - applyGroupState(server.serverId, "$groupName-$numericalId", groupState, "of group state change") + setServerState(serverId, groupState) } - private suspend fun applyGroupState(serverId: String, serviceName: String, groupState: String, reason: String) { - if (!controller().updateServerProperty(serverId, KEY, groupState)) { - logger.warn("Could not synchronize join state for '$serviceName' with group state '$groupState'") - return - } + private fun readState(properties: Map?): String? { + val state = properties?.get(KEY)?.toString() + if (state.isNullOrBlank()) return null + return state + } - localState = groupState - logger.info("Join state changed to $groupState because $reason.") + private fun getServerName(server: Server): String { + val groupName = server.group?.name + if (groupName != null) return "$groupName-${server.numericalId}" + return server.persistentServer?.name ?: server.serverId } companion object { const val KEY = "joinstate" } - } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/ProxyJoinGate.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/ProxyJoinGate.kt deleted file mode 100644 index b7764a3..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/ProxyJoinGate.kt +++ /dev/null @@ -1,48 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.joinstate - -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import org.slf4j.LoggerFactory - -class ProxyJoinGate( - private val plugin: ProxyPlugin -) { - - private val logger = LoggerFactory.getLogger(ProxyJoinGate::class.java) - - sealed interface Result { - data object Allowed : Result - data class Denied(val kickMessage: String) : Result - } - - suspend fun evaluate(playerName: String, hasPermission: (String) -> Boolean): Result { - val stateName = plugin.joinStateService.localState - val kickMessages = plugin.messageConfig.get().kick - val joinState = plugin.joinStateResolver.resolveJoinState(stateName) - - if (joinState == null) { - logger.error("Neither join state '$stateName' nor default state found. Check configuration!") - return Result.Denied(kickMessages.noJoinState) - } - - val joinPermission = joinState.permission.join - if (joinPermission.isNotBlank() && !hasPermission(joinPermission)) { - logger.info("Player $playerName does not have permission to join the proxy. (JoinState: $stateName, Permission: $joinPermission)") - return Result.Denied(kickMessages.noPermission) - } - - if (isNetworkFull(hasPermission, joinState.permission.full)) { - return Result.Denied(kickMessages.networkFull) - } - - return Result.Allowed - } - - private suspend fun isNetworkFull(hasPermission: (String) -> Boolean, fullPermission: String): Boolean { - return try { - plugin.joinStateResolver.isServerFull() && !hasPermission(fullPermission) - } catch (e: Exception) { - logger.error("Error checking player limits", e) - false - } - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/layout/LayoutRepository.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/layout/LayoutRepository.kt new file mode 100644 index 0000000..670bf61 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/layout/LayoutRepository.kt @@ -0,0 +1,48 @@ +package app.simplecloud.plugin.proxy.shared.layout + +import app.simplecloud.plugin.api.shared.config.YamlDirectoryRepository +import app.simplecloud.plugin.proxy.shared.config.LayoutConfig +import org.slf4j.LoggerFactory +import java.nio.file.Files +import java.nio.file.Path +import java.util.concurrent.atomic.AtomicReference +import kotlin.io.path.extension +import kotlin.io.path.isRegularFile +import kotlin.io.path.nameWithoutExtension + +class LayoutRepository( + private val directory: Path +) : YamlDirectoryRepository(directory, LayoutConfig::class.java) { + + private val logger = LoggerFactory.getLogger(LayoutRepository::class.java) + private val layouts = AtomicReference>(emptyMap()) + + override fun find(identifier: String): LayoutConfig? = layouts.get()[identifier] + + override fun save(entity: LayoutConfig) { + throw UnsupportedOperationException("Layouts are only edited in their files") + } + + fun getNames(): List = layouts.get().keys.sorted() + + fun loadLayouts() { + if (!Files.isDirectory(directory)) { + logger.warn("The layout directory '$directory' does not exist, so the server list uses the default layout.") + layouts.set(emptyMap()) + return + } + + val files = Files.list(directory).use { paths -> paths.filter { it.isRegularFile() && it.extension == "yml" }.toList() } + layouts.set(files.mapNotNull { loadLayout(it) }.toMap()) + } + + private fun loadLayout(file: Path): Pair? { + return try { + val layout = load(file.toFile()) ?: error("the file is empty") + file.nameWithoutExtension to layout + } catch (e: Exception) { + logger.error("Could not load the layout '${file.fileName}', it is skipped until the next reload", e) + null + } + } +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/layout/LayoutService.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/layout/LayoutService.kt new file mode 100644 index 0000000..019877a --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/layout/LayoutService.kt @@ -0,0 +1,139 @@ +package app.simplecloud.plugin.proxy.shared.layout + +import app.simplecloud.api.CloudApi +import app.simplecloud.api.runtime.SimpleCloudRuntime +import app.simplecloud.plugin.api.shared.config.ConfigurationFactory +import app.simplecloud.plugin.proxy.shared.config.DomainRoute +import app.simplecloud.plugin.proxy.shared.config.LayoutConfig +import app.simplecloud.plugin.proxy.shared.config.MotdEntry +import app.simplecloud.plugin.proxy.shared.config.MotdUpdateType +import app.simplecloud.plugin.proxy.shared.config.ProxyEssentialsConfig +import app.simplecloud.plugin.proxy.shared.joinstate.JoinStateService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.future.await +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.slf4j.LoggerFactory +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference +import kotlin.random.Random +import kotlin.time.Duration.Companion.seconds + +class LayoutService( + private val api: CloudApi, + private val repository: LayoutRepository, + private val joinStateService: JoinStateService, + private val config: ConfigurationFactory, + private val scope: CoroutineScope +) { + + private val logger = LoggerFactory.getLogger(LayoutService::class.java) + private val localLayout = AtomicReference() + private val domainLayouts = AtomicReference>(emptyMap()) + private val pingCounters = ConcurrentHashMap() + + suspend fun start() { + val server = api.server().getServerById(SimpleCloudRuntime.serverId()).await() + val groupName = server.group?.name + val groupLayout = if (groupName == null) null else getGroupLayout(groupName) + localLayout.set(readLayout(server.properties) ?: groupLayout) + + api.event().server().onUpdated { event -> + if (event.serverId != server.serverId) return@onUpdated + + val layout = readLayout(event.server.properties) + if (layout != null && localLayout.getAndSet(layout) != layout) { + logger.info("Layout changed to '$layout'") + } + } + } + + fun startDomainSync() { + scope.launch { + while (isActive) { + try { + updateDomainLayouts() + } catch (e: Exception) { + logger.error("Could not update the layouts of the configured domains", e) + } + delay(3.seconds) + } + } + } + + fun getLayout(name: String): LayoutConfig = repository.find(name) ?: LayoutConfig() + + fun getLayoutName(domain: String?): String { + val config = config.get() + val domainLayout = if (domain == null) null else domainLayouts.get()[domain.lowercase()] + val forcedLayout = config.joinstates.find { it.name == joinStateService.getLocalState() }?.forcedMotdLayout + val candidates = listOf(domainLayout, forcedLayout, localLayout.get(), config.initialLayout) + + return candidates.filterNotNull().firstOrNull { repository.find(it) != null } ?: config.initialLayout + } + + fun getMotdEntry(layoutName: String, layout: LayoutConfig): MotdEntry? { + val motd = layout.motd + if (motd.layouts.isEmpty()) return null + + val updateTime = motd.updateTime ?: return motd.layouts[getPingIndex(layoutName, motd.updateType, motd.layouts.size)] + if (updateTime <= 0) return motd.layouts.first() + + val timeSlot = System.currentTimeMillis() / (updateTime * 50L) + val index = when (motd.updateType) { + MotdUpdateType.RANDOM -> Random(timeSlot).nextInt(motd.layouts.size) + MotdUpdateType.QUEUE -> timeSlot.mod(motd.layouts.size) + } + return motd.layouts[index] + } + + suspend fun getGroupLayout(groupName: String): String? = + readLayout(api.group().getGroupByName(groupName).await().properties) + + suspend fun setGroupLayout(groupName: String, layout: String) { + val group = api.group().getGroupByName(groupName).await() + api.group().updateGroupProperty(group.serverGroupId, KEY, layout).await() + api.server().getServersByGroup(groupName).await().forEach { + api.server().updateServerProperty(it.serverId, KEY, layout).await() + } + } + + private suspend fun updateDomainLayouts() { + val resolved = config.get().domains.mapNotNull { route -> + val layout = getDomainLayout(route) ?: return@mapNotNull null + route.domain.lowercase() to layout + } + domainLayouts.set(resolved.toMap()) + } + + private suspend fun getDomainLayout(route: DomainRoute): String? { + val config = config.get() + val state = joinStateService.getTargetState(route.target) + val candidates = listOf( + route.rules.find { it.state == state }?.layout, + config.joinstates.find { it.name == state }?.forcedMotdLayout, + config.initialLayout + ) + + return candidates.filterNotNull().firstOrNull { repository.find(it) != null } + } + + private fun getPingIndex(layoutName: String, updateType: MotdUpdateType, size: Int): Int { + return when (updateType) { + MotdUpdateType.RANDOM -> Random.nextInt(size) + MotdUpdateType.QUEUE -> pingCounters.computeIfAbsent(layoutName) { AtomicInteger() }.getAndIncrement().mod(size) + } + } + + private fun readLayout(properties: Map?): String? { + val layout = properties?.get(KEY)?.toString() + if (layout.isNullOrBlank()) return null + return layout + } + + companion object { + const val KEY = "motd-layout" + } +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/layout/ServerIconCache.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/layout/ServerIconCache.kt new file mode 100644 index 0000000..5e6b031 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/layout/ServerIconCache.kt @@ -0,0 +1,79 @@ +package app.simplecloud.plugin.proxy.shared.layout + +import org.slf4j.LoggerFactory +import java.awt.RenderingHints +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import java.nio.file.Files +import java.nio.file.Path +import java.util.Base64 +import java.util.concurrent.ConcurrentHashMap +import javax.imageio.ImageIO +import kotlin.math.roundToInt + +class ServerIconCache( + directory: Path +) { + + private val logger = LoggerFactory.getLogger(ServerIconCache::class.java) + private val directory = directory.toAbsolutePath().normalize() + private val icons = ConcurrentHashMap>() + + fun get(fileName: String): String? { + val file = directory.resolve(fileName).normalize() + val lastModified = getLastModified(file) + val cached = icons[file] + if (cached != null && cached.first == lastModified) return cached.second + + val icon = loadIcon(fileName, file, lastModified) + icons[file] = lastModified to icon + return icon + } + + private fun getLastModified(file: Path): Long { + if (!file.startsWith(directory) || !Files.isRegularFile(file)) return MISSING + return Files.getLastModifiedTime(file).toMillis() + } + + private fun loadIcon(fileName: String, file: Path, lastModified: Long): String? { + if (lastModified == MISSING) { + logger.warn("The server icon '$fileName' does not exist in '$directory'") + return null + } + + return try { + val image = ImageIO.read(file.toFile()) ?: error("the image format is not supported") + val output = ByteArrayOutputStream() + ImageIO.write(scaleToFavicon(image), "png", output) + "data:image/png;base64," + Base64.getEncoder().encodeToString(output.toByteArray()) + } catch (e: Exception) { + logger.warn("Could not load the server icon '$fileName': ${e.message}") + null + } + } + + private fun scaleToFavicon(image: BufferedImage): BufferedImage { + if (image.width == FAVICON_SIZE && image.height == FAVICON_SIZE) return image + + val scale = minOf(FAVICON_SIZE.toDouble() / image.width, FAVICON_SIZE.toDouble() / image.height) + val width = (image.width * scale).roundToInt().coerceAtLeast(1) + val height = (image.height * scale).roundToInt().coerceAtLeast(1) + + val favicon = BufferedImage(FAVICON_SIZE, FAVICON_SIZE, BufferedImage.TYPE_INT_ARGB) + val graphics = favicon.createGraphics() + try { + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC) + graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY) + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + graphics.drawImage(image, (FAVICON_SIZE - width) / 2, (FAVICON_SIZE - height) / 2, width, height, null) + } finally { + graphics.dispose() + } + return favicon + } + + private companion object { + const val FAVICON_SIZE = 64 + const val MISSING = -1L + } +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/DomainMotdResolver.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/DomainMotdResolver.kt deleted file mode 100644 index 40b01ec..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/DomainMotdResolver.kt +++ /dev/null @@ -1,108 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.motd - -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.config.DomainMotdRoute -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import org.slf4j.LoggerFactory -import java.util.concurrent.ConcurrentHashMap -import kotlin.time.Duration.Companion.milliseconds - -class DomainMotdResolver( - private val plugin: ProxyPlugin -) { - - private val logger = LoggerFactory.getLogger(DomainMotdResolver::class.java) - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private var syncJob: Job? = null - - private val resolvedLayouts = ConcurrentHashMap() - - fun getLayoutNameForDomain(virtualHost: String): String? = resolvedLayouts[normalize(virtualHost)] - - fun startSyncTask() { - if (syncJob?.isActive == true) return - - syncJob = scope.launch { - while (isActive) { - try { - syncAllRoutes() - } catch (e: Exception) { - logger.error("Error while syncing domain MOTD routes", e) - } - delay(3000.milliseconds) - } - } - } - - fun registerListener() { - plugin.api.event().server().onUpdated { event -> - val server = event.server - val matchingRoutes = plugin.config.get().domains.filter { route -> - route.target.equals(server.group?.name, ignoreCase = true) || - route.target.equals(server.persistentServer?.name, ignoreCase = true) - } - - if (matchingRoutes.isEmpty()) return@onUpdated - - scope.launch { - matchingRoutes.forEach { refreshRoute(it) } - } - } - } - - fun stop() { - syncJob?.cancel() - scope.cancel() - } - - private suspend fun syncAllRoutes() { - val routes = plugin.config.get().domains - resolvedLayouts.keys.retainAll(routes.map { normalize(it.domain) }.toSet()) - - routes.forEach { refreshRoute(it) } - } - - private suspend fun refreshRoute(route: DomainMotdRoute) { - val domainKey = normalize(route.domain) - when (val layoutName = resolveRoute(route)) { - null -> resolvedLayouts.remove(domainKey) - else -> resolvedLayouts[domainKey] = layoutName - } - } - - private suspend fun resolveRoute(route: DomainMotdRoute): String? { - val config = plugin.config.get() - val state = getJoinStateOfTarget(route.target) - - val ruleLayout = getExistingLayout(route.rules.find { it.state == state }?.layout) - if (ruleLayout != null) return ruleLayout - - val forcedLayout = getExistingLayout(config.joinstates.find { it.name == state }?.forcedMotdLayout) - if (forcedLayout != null) return forcedLayout - - return getExistingLayout(config.initialLayout) - } - - private suspend fun getJoinStateOfTarget(target: String): String { - return when { - plugin.cloudControllerHandler.groupExists(target) -> plugin.joinStateService.getJoinStateAtGroup(target) - else -> plugin.joinStateService.getJoinStateAtPersistentServer(target) - } - } - - private fun getExistingLayout(name: String?): String? { - if (name == null || plugin.layoutRepository.getLayoutByName(name) == null) { - return null - } - return name - } - - private fun normalize(domain: String): String = domain.trim().lowercase() -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/MotdLayoutRepository.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/MotdLayoutRepository.kt deleted file mode 100644 index ad8adf5..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/MotdLayoutRepository.kt +++ /dev/null @@ -1,137 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.motd - -import app.simplecloud.plugin.api.shared.config.YamlDirectoryRepository -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.config.MotdEntry -import app.simplecloud.plugin.proxy.shared.config.MotdLayoutConfiguration -import app.simplecloud.plugin.proxy.shared.config.MotdUpdateType -import org.slf4j.LoggerFactory -import java.nio.file.Files -import java.nio.file.Path -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger - -class MotdLayoutRepository( - private val directory: Path, - private val plugin: ProxyPlugin -) : YamlDirectoryRepository( - directory, - MotdLayoutConfiguration::class.java, -) { - - private val logger = LoggerFactory.getLogger(MotdLayoutRepository::class.java) - private val layoutsByName = ConcurrentHashMap() - private val queueIndexes = ConcurrentHashMap() - private val listenerRegistered = AtomicBoolean(false) - - @Volatile - private var localLayout: String? = null - - override fun save(entity: MotdLayoutConfiguration) { - val name = entity.motd.layouts.firstOrNull()?.name ?: plugin.config.get().initialLayout - save("$name$SUFFIX", entity) - layoutsByName[name] = entity - } - - override fun find(identifier: String): MotdLayoutConfiguration? = layoutsByName[identifier] - - fun getLayoutByName(name: String): MotdLayoutConfiguration? = find(name) - - fun loadMotdLayouts() { - if (!Files.isDirectory(directory)) { - layoutsByName.clear() - return - } - - val loaded = mutableMapOf() - getLayoutFileNames().forEach { name -> - val layout = loadLayout(name) - if (layout != null) { - loaded[name] = layout - } - } - - layoutsByName.keys.retainAll(loaded.keys) - layoutsByName.putAll(loaded) - } - - fun getAvailableLayouts(): List = getLayoutFileNames().sorted() - - fun getCurrentMotdLayout(): MotdLayoutConfiguration { - val currentLayout = localLayout - val layoutName = when { - currentLayout != null && layoutsByName.containsKey(currentLayout) -> currentLayout - else -> getFallbackLayoutName() - } - return find(layoutName) ?: MotdLayoutConfiguration() - } - - fun setLocalLayout(layoutName: String?, logChange: Boolean = false) { - val nextLayout = when { - layoutName.isNullOrBlank() -> null - else -> layoutName - } - if (localLayout == nextLayout) return - - localLayout = nextLayout - if (logChange) { - logger.info("MOTD layout changed to ${nextLayout ?: getFallbackLayoutName()}") - } - } - - fun registerListener() { - if (!listenerRegistered.compareAndSet(false, true)) return - - plugin.api.event().server().onUpdated { event -> - val currentServer = plugin.cloudControllerHandler.currentServer ?: return@onUpdated - if (event.serverId != currentServer.serverId) return@onUpdated - - setLocalLayout(event.server.properties?.get(KEY)?.toString(), logChange = true) - } - } - - fun selectEntry(layout: MotdLayoutConfiguration, layoutKey: String): MotdEntry? { - val entries = layout.motd.layouts - if (entries.isEmpty()) return null - - return when (layout.motd.updateType) { - MotdUpdateType.RANDOM -> entries.random() - MotdUpdateType.QUEUE -> { - val counter = queueIndexes.getOrPut(layoutKey) { AtomicInteger(0) } - entries[counter.getAndIncrement() % entries.size] - } - } - } - - private fun getLayoutFileNames(): List { - if (!Files.isDirectory(directory)) return emptyList() - - return Files.list(directory).use { paths -> - paths - .filter { Files.isRegularFile(it) && it.fileName.toString().endsWith(SUFFIX) } - .map { it.fileName.toString().removeSuffix(SUFFIX) } - .toList() - } - } - - private fun loadLayout(name: String): MotdLayoutConfiguration? { - return try { - load(directory.resolve("$name$SUFFIX").toFile()) - } catch (e: Exception) { - logger.warn("Could not load MOTD layout $name$SUFFIX", e) - null - } - } - - private fun getFallbackLayoutName(): String { - val config = plugin.config.get() - val joinState = config.joinstates.find { it.name == plugin.joinStateService.localState } - return joinState?.forcedMotdLayout ?: config.initialLayout - } - - companion object { - const val KEY = "motd-layout" - private const val SUFFIX = ".yml" - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/ServerIconLoader.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/ServerIconLoader.kt deleted file mode 100644 index aa4e4cd..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/motd/ServerIconLoader.kt +++ /dev/null @@ -1,117 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.motd - -import java.awt.RenderingHints -import java.awt.image.BufferedImage -import java.nio.file.Files -import java.nio.file.Path -import java.util.concurrent.ConcurrentHashMap -import javax.imageio.ImageIO -import kotlin.math.roundToInt - -class ServerIconLoader( - directory: Path, - private val createIcon: (BufferedImage) -> T -) { - - private val directory = directory.toAbsolutePath().normalize() - private val cache = ConcurrentHashMap>() - private val warnedInvalidNames = ConcurrentHashMap.newKeySet() - - private companion object { - const val FAVICON_SIZE = 64 - } - - init { - Files.createDirectories(this.directory) - } - - fun get(fileName: String, warn: (String) -> Unit): T? { - val iconPath = resolveIconPath(fileName, warn) ?: return null - val snapshot = snapshotOf(iconPath) - - val cached = cache[iconPath] - if (cached != null && cached.snapshot == snapshot) { - return cached.icon - } - - if (!Files.isRegularFile(iconPath)) { - cache[iconPath] = CacheEntry(snapshot, null) - warn("Server icon does not exist: ${iconPath.fileName}") - return null - } - - return try { - val image = ImageIO.read(iconPath.toFile()) ?: error("Unsupported image format") - val icon = createIcon(image.toFaviconImage()) - cache[iconPath] = CacheEntry(snapshot, icon) - icon - } catch (e: Exception) { - cache[iconPath] = CacheEntry(snapshot, null) - warn("Failed to load server icon ${iconPath.fileName}: ${e.message}") - null - } - } - - private fun snapshotOf(iconPath: Path): IconSnapshot { - if (!Files.exists(iconPath)) { - return IconSnapshot(iconPath, lastModified = -1, length = -1) - } - - return IconSnapshot( - path = iconPath, - lastModified = Files.getLastModifiedTime(iconPath).toMillis(), - length = Files.size(iconPath) - ) - } - - private fun resolveIconPath(fileName: String, warn: (String) -> Unit): Path? { - if (fileName.isBlank()) { - warnOnce(fileName, warn, "Server icon file name is blank") - return null - } - - val iconPath = directory.resolve(fileName).normalize() - if (!iconPath.startsWith(directory)) { - warnOnce(fileName, warn, "Server icon path is outside of the server-icons directory: $fileName") - return null - } - return iconPath - } - - private fun warnOnce(fileName: String, warn: (String) -> Unit, message: String) { - if (warnedInvalidNames.add(fileName)) warn(message) - } - - private fun BufferedImage.toFaviconImage(): BufferedImage { - if (width == FAVICON_SIZE && height == FAVICON_SIZE) return this - - val scale = minOf(FAVICON_SIZE.toDouble() / width, FAVICON_SIZE.toDouble() / height) - val targetWidth = (width * scale).roundToInt().coerceAtLeast(1) - val targetHeight = (height * scale).roundToInt().coerceAtLeast(1) - val targetX = (FAVICON_SIZE - targetWidth) / 2 - val targetY = (FAVICON_SIZE - targetHeight) / 2 - - val favicon = BufferedImage(FAVICON_SIZE, FAVICON_SIZE, BufferedImage.TYPE_INT_ARGB) - val graphics = favicon.createGraphics() - try { - graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC) - graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY) - graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) - graphics.drawImage(this, targetX, targetY, targetWidth, targetHeight, null) - } finally { - graphics.dispose() - } - return favicon - } - - private data class IconSnapshot( - val path: Path, - val lastModified: Long, - val length: Long - ) - - private data class CacheEntry( - val snapshot: IconSnapshot, - val icon: T? - ) -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/placeholder/TagResolverHelper.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/placeholder/TagResolverHelper.kt deleted file mode 100644 index bf9056a..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/placeholder/TagResolverHelper.kt +++ /dev/null @@ -1,67 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.placeholder - -import app.simplecloud.plugin.proxy.shared.config.MotdLayoutConfiguration -import app.simplecloud.plugin.proxy.shared.config.PingColor -import net.kyori.adventure.text.minimessage.tag.Tag -import net.kyori.adventure.text.minimessage.tag.resolver.Formatter -import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder -import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver -import java.time.LocalDateTime -import java.time.ZoneId - -object TagResolverHelper { - - fun getDefaultTagResolvers( - serverName: String, - ping: Long, - pingColors: List, - onlinePlayers: Int, - realMaxPlayers: Int, - motdConfiguration: MotdLayoutConfiguration - ): List { - return listOf( - Placeholder.unparsed(TagResolverNames.SERVER_NAME, serverName), - getPingTagResolver(ping, pingColors), - getDateTagResolver(), - getOnlinePlayersTagResolver(onlinePlayers), - getMaxPlayersTagResolver(onlinePlayers, realMaxPlayers, motdConfiguration), - getEnvTagResolver() - ) - } - - fun getPingTagResolver(ping: Long, pingColors: List): TagResolver { - val pingColor = pingColors - .filter { ping >= it.ping } - .maxByOrNull { it.ping } - ?.color - ?: "" - - return Placeholder.parsed(TagResolverNames.PING, "$pingColor$ping") - } - - fun getDateTagResolver(): TagResolver { - return Formatter.date(TagResolverNames.DATE, LocalDateTime.now(ZoneId.systemDefault())) - } - - fun getOnlinePlayersTagResolver(onlinePlayers: Int): TagResolver { - return Placeholder.unparsed(TagResolverNames.ONLINE_PLAYERS, onlinePlayers.toString()) - } - - fun getMaxPlayersTagResolver( - onlinePlayers: Int, - realMaxPlayers: Int, - motdConfiguration: MotdLayoutConfiguration - ): TagResolver { - val maxPlayers = motdConfiguration.version.slots.resolveMaxPlayers(onlinePlayers, realMaxPlayers) - return Placeholder.unparsed(TagResolverNames.MAX_PLAYERS, maxPlayers.toString()) - } - - fun getEnvTagResolver(): TagResolver { - return TagResolver.resolver(TagResolverNames.ENV) { arguments, _ -> - val envName = arguments.popOr("env name expected").value() - val envDefault = arguments.peek()?.value() ?: "" - - Tag.preProcessParsed(System.getenv(envName) ?: envDefault) - } - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/placeholder/TagResolverNames.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/placeholder/TagResolverNames.kt deleted file mode 100644 index 9e9dda1..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/placeholder/TagResolverNames.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.placeholder - -object TagResolverNames { - - const val PING = "ping" - const val DATE = "date" - const val MAX_PLAYERS = "max_players" - const val ONLINE_PLAYERS = "online_players" - const val SERVER_NAME = "server_name" - const val ENV = "env" - -} \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/player/PlayerCountService.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/player/PlayerCountService.kt new file mode 100644 index 0000000..a2abc9c --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/player/PlayerCountService.kt @@ -0,0 +1,77 @@ +package app.simplecloud.plugin.proxy.shared.player + +import app.simplecloud.api.CloudApi +import app.simplecloud.api.runtime.SimpleCloudRuntime +import app.simplecloud.plugin.api.shared.config.ConfigurationFactory +import app.simplecloud.plugin.proxy.shared.ProxyPlatform +import app.simplecloud.plugin.proxy.shared.config.PlayerCountConfig +import app.simplecloud.plugin.proxy.shared.config.ProxyEssentialsConfig +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.future.await +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.slf4j.LoggerFactory +import java.util.concurrent.atomic.AtomicInteger +import kotlin.time.Duration.Companion.milliseconds + +class PlayerCountService( + private val api: CloudApi, + private val platform: ProxyPlatform, + private val config: ConfigurationFactory, + private val scope: CoroutineScope +) { + + private val logger = LoggerFactory.getLogger(PlayerCountService::class.java) + private val otherOnlinePlayers = AtomicInteger() + private val networkMaxPlayers = AtomicInteger() + + fun getOnlinePlayers(): Int = platform.getOnlinePlayers() + otherOnlinePlayers.get() + + fun getMaxPlayers(): Int { + val maxPlayers = networkMaxPlayers.get() + if (maxPlayers <= 0) return platform.getMaxPlayers() + return maxPlayers + } + + fun start() { + scope.launch { + while (isActive) { + val playerCount = config.get().playerCount + update(playerCount) + if (playerCount.updateTime <= 0) return@launch + + delay((playerCount.updateTime * 50L).milliseconds) + } + } + } + + private suspend fun update(config: PlayerCountConfig) { + if (!config.enabled) { + otherOnlinePlayers.set(0) + networkMaxPlayers.set(0) + return + } + + try { + count(config) + } catch (e: Exception) { + logger.error("Could not count the players of the network, showing the last known count", e) + } + } + + private suspend fun count(config: PlayerCountConfig) { + val server = api.server().getServerById(SimpleCloudRuntime.serverId()).await() + val groupNames = (listOfNotNull(server.group?.name) + config.additionalGroups).distinct() + val groups = groupNames.map { api.group().getGroupByName(it).await() } + val groupServers = groupNames.flatMap { api.server().getServersByGroup(it).await() } + val persistentServers = api.server().allServers.await().filter { it.persistentServer?.name in config.additionalPersistentServers } + + val ownMaxPlayers = if (server.group == null) server.maxPlayers ?: 0 else 0 + val otherServers = (groupServers + persistentServers).filter { it.serverId != server.serverId } + + otherOnlinePlayers.set(otherServers.sumOf { it.playerCount ?: 0 }) + networkMaxPlayers.set(ownMaxPlayers + groups.sumOf { it.maxPlayers ?: 0 } + persistentServers.sumOf { it.maxPlayers ?: 0 }) + } + +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/player/PlayerCountTracker.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/player/PlayerCountTracker.kt deleted file mode 100644 index 4085658..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/player/PlayerCountTracker.kt +++ /dev/null @@ -1,153 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.player - -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.config.PlayerCountConfig -import kotlinx.coroutines.* -import org.slf4j.LoggerFactory -import kotlin.time.Duration.Companion.milliseconds - -class PlayerCountTracker( - private val plugin: ProxyPlugin -) { - - private val logger = LoggerFactory.getLogger(PlayerCountTracker::class.java) - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private var syncJob: Job? = null - - @Volatile - private var snapshot: PlayerCountSnapshot? = null - - fun start() { - if (syncJob?.isActive == true) { - return - } - - syncJob = scope.launch { - while (isActive) { - val updateTime = plugin.config.get().playerCountUpdateTimeMillis() - if (updateTime == null) { - snapshot = null - delay(1000.milliseconds) - continue - } - - refresh() - delay(updateTime.milliseconds) - } - } - } - - fun stop() { - syncJob?.cancel() - scope.cancel() - } - - fun onlinePlayers(currentProxyOnlinePlayers: Int): Int { - return currentProxyOnlinePlayers + (snapshot?.otherOnlinePlayers ?: 0) - } - - fun maxPlayers(currentProxyMaxPlayers: Int): Int { - val maxPlayers = snapshot?.maxPlayers ?: return currentProxyMaxPlayers - if (maxPlayers <= 0) return currentProxyMaxPlayers - return maxPlayers - } - - private suspend fun refresh() { - try { - snapshot = resolveSnapshot() - } catch (e: Exception) { - logger.error("Error while syncing player count", e) - } - } - - private suspend fun resolveSnapshot(): PlayerCountSnapshot? { - val config = plugin.config.get().playerCount - val currentGroupName = getCurrentGroupName() - - val ownGroup = resolveOwnGroupCount(currentGroupName) - val additionalGroups = resolveAdditionalGroupCount(config, currentGroupName) - val persistentServers = resolvePersistentServerCount(config) - - val otherOnlinePlayers = ownGroup.otherOnlinePlayers + - additionalGroups.otherOnlinePlayers + - persistentServers.otherOnlinePlayers - val maxPlayers = ownGroup.maxPlayers + additionalGroups.maxPlayers + persistentServers.maxPlayers - - if (otherOnlinePlayers <= 0 && maxPlayers <= 0) { - return null - } - - return PlayerCountSnapshot( - otherOnlinePlayers = otherOnlinePlayers, - maxPlayers = maxPlayers - ) - } - - private suspend fun resolveOwnGroupCount(currentGroupName: String?): PlayerCount { - val currentServer = plugin.cloudControllerHandler.currentServer ?: return PlayerCount(0, 0) - - if (currentGroupName == null) { - return PlayerCount(otherOnlinePlayers = 0, maxPlayers = currentServer.maxPlayers ?: 0) - } - - val otherOnlinePlayers = plugin.cloudControllerHandler.getServersByGroup(currentGroupName) - .filter { it.serverId != currentServer.serverId } - .sumOf { it.playerCount ?: 0 } - - return PlayerCount( - otherOnlinePlayers = otherOnlinePlayers, - maxPlayers = plugin.cloudControllerHandler.getMaxPlayersInGroup(currentGroupName) - ) - } - - private suspend fun resolveAdditionalGroupCount( - config: PlayerCountConfig, - currentGroupName: String? - ): PlayerCount { - val groupNames = config.additionalGroups - .map { it.trim() } - .filter { it.isNotEmpty() && it != currentGroupName } - .distinct() - - return PlayerCount( - otherOnlinePlayers = groupNames.sumOf { plugin.cloudControllerHandler.getOnlinePlayersInGroup(it) }, - maxPlayers = groupNames.sumOf { plugin.cloudControllerHandler.getMaxPlayersInGroup(it) } - ) - } - - private suspend fun resolvePersistentServerCount(config: PlayerCountConfig): PlayerCount { - val serverNames = config.additionalPersistentServers - .map { it.trim() } - .filter { it.isNotEmpty() && it != getCurrentPersistentServerName() } - .toSet() - - val servers = plugin.cloudControllerHandler.getPersistentServersByNames(serverNames) - - return PlayerCount( - otherOnlinePlayers = servers.sumOf { it.playerCount ?: 0 }, - maxPlayers = servers.sumOf { it.maxPlayers ?: 0 } - ) - } - - private fun getCurrentGroupName(): String? { - val currentServer = plugin.cloudControllerHandler.currentServer ?: return null - if (!currentServer.isFromGroup) return null - return currentServer.group?.name - } - - private fun getCurrentPersistentServerName(): String? { - val currentServer = plugin.cloudControllerHandler.currentServer ?: return null - if (!currentServer.isFromPersistentServer) return null - return currentServer.persistentServer?.name - } - - private data class PlayerCount( - val otherOnlinePlayers: Int, - val maxPlayers: Int - ) - - private data class PlayerCountSnapshot( - val otherOnlinePlayers: Int, - val maxPlayers: Int - ) -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/tablist/TabListResolver.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/tablist/TabListResolver.kt deleted file mode 100644 index 1e20da3..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/tablist/TabListResolver.kt +++ /dev/null @@ -1,42 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.tablist - -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.config.TabListGroup - -class TabListResolver( - private val plugin: ProxyPlugin -) { - - private val index = mutableMapOf() - - fun incrementIndices() { - getTabListGroups() - .filter { it.layout.isNotEmpty() } - .forEach { group -> - val currentIndex = index.getOrPut(group.name) { 0 } - index[group.name] = (currentIndex + 1) % group.layout.size - } - } - - fun findTabListGroup(serviceName: String): TabListGroup? { - val groups = getTabListGroups() - - return groups.find { it.name.equals(serviceName, true) } - ?: groups.find { serviceName.startsWith(it.name, true) } - ?: groups.find { it.name == "*" } - ?: groups.find { it.name.equals("global", true) } - } - - fun getCurrentTabList(group: TabListGroup): Pair? { - if (group.layout.isEmpty()) return null - - val currentIndex = index[group.name] ?: 0 - val tabList = group.layout[currentIndex % group.layout.size] - - return tabList.header to tabList.footer - } - - private fun getTabListGroups(): List { - return plugin.config.get().tablist - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/tablist/TabListService.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/tablist/TabListService.kt new file mode 100644 index 0000000..a341772 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/tablist/TabListService.kt @@ -0,0 +1,33 @@ +package app.simplecloud.plugin.proxy.shared.tablist + +import app.simplecloud.plugin.api.shared.config.ConfigurationFactory +import app.simplecloud.plugin.proxy.shared.config.ProxyEssentialsConfig +import app.simplecloud.plugin.proxy.shared.config.TabListEntry + +class TabListService( + private val config: ConfigurationFactory +) { + + fun getTabList(serverName: String): TabListEntry? { + val groups = config.get().tablist + val group = groups.find { it.name.equals(serverName, ignoreCase = true) } + ?: groups.find { serverName.startsWith(it.name, ignoreCase = true) } + ?: groups.find { it.name == "*" } + ?: groups.find { it.name.equals("global", ignoreCase = true) } + ?: return null + if (group.layout.isEmpty()) return null + if (group.updateTime <= 0) return group.layout.first() + + val timeSlot = System.currentTimeMillis() / (group.updateTime * MILLIS_PER_TICK) + return group.layout[timeSlot.mod(group.layout.size)] + } + + fun getUpdateIntervalMillis(): Long? { + val updateTime = config.get().tablist.map { it.updateTime }.filter { it > 0 }.minOrNull() ?: return null + return updateTime * MILLIS_PER_TICK + } + + private companion object { + const val MILLIS_PER_TICK = 50L + } +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/ProxyPermissions.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/CommandPermissions.kt similarity index 77% rename from proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/ProxyPermissions.kt rename to proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/CommandPermissions.kt index 3e64bfd..903e7e8 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/ProxyPermissions.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/CommandPermissions.kt @@ -1,6 +1,9 @@ package app.simplecloud.plugin.proxy.shared.utilities -object ProxyPermissions { +object CommandPermissions { + + const val HELP = "simplecloud.proxy-essentials.command.help" + const val RELOAD = "simplecloud.proxy-essentials.command.reload" const val LAYOUT_HELP = "simplecloud.proxy-essentials.command.layout.help" const val LAYOUT_INFO = "simplecloud.proxy-essentials.command.layout.info" @@ -9,5 +12,4 @@ object ProxyPermissions { const val JOINSTATE_HELP = "simplecloud.proxy-essentials.command.joinstate.help" const val JOINSTATE_INFO = "simplecloud.proxy-essentials.command.joinstate.info" const val JOINSTATE_SET = "simplecloud.proxy-essentials.command.joinstate.set" - } \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/config/ConfigVersion.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/config/ConfigVersion.kt deleted file mode 100644 index 56b8807..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/config/ConfigVersion.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.utilities.config - -object ConfigVersion { - - const val VERSION = 2 - -} \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/config/DefaultConfigs.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/config/DefaultConfigs.kt deleted file mode 100644 index 7a384f0..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/config/DefaultConfigs.kt +++ /dev/null @@ -1,40 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.utilities.config - -import app.simplecloud.plugin.proxy.shared.config.JoinState -import app.simplecloud.plugin.proxy.shared.config.JoinStatePermission -import app.simplecloud.plugin.proxy.shared.config.PingColor -import app.simplecloud.plugin.proxy.shared.config.TabList -import app.simplecloud.plugin.proxy.shared.config.TabListGroup - -object DefaultConfigs { - - val VARIABLES: Map = mapOf("prefix" to "โšก ") - - val PING_COLORS: List = listOf( - PingColor(0, ""), - PingColor(50, ""), - PingColor(100, ""), - PingColor(150, ""), - PingColor(200, "") - ) - - val JOIN_STATES: List = listOf( - JoinState("public", JoinStatePermission("", "simplecloud.proxy-essentials.join.full.public")), - JoinState( - "maintenance", - JoinStatePermission( - "simplecloud.proxy-essentials.join.maintenance", - "simplecloud.proxy-essentials.join.maintenance" - ), - "maintenance" - ) - ) - - val TABLIST_GROUPS: List = listOf( - TabListGroup( - name = "global", - layout = listOf(TabList()), - updateTime = 20L - ) - ) -} \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/text/MessageFormatter.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/text/MessageFormatter.kt new file mode 100644 index 0000000..830c90e --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/text/MessageFormatter.kt @@ -0,0 +1,68 @@ +package app.simplecloud.plugin.proxy.shared.utilities.text + +import app.simplecloud.plugin.api.shared.config.ConfigurationFactory +import app.simplecloud.plugin.proxy.shared.config.MessageConfig +import app.simplecloud.plugin.proxy.shared.layout.LayoutService +import app.simplecloud.plugin.proxy.shared.player.PlayerCountService +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.minimessage.MiniMessage +import net.kyori.adventure.text.minimessage.tag.Tag +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class MessageFormatter( + private val messages: ConfigurationFactory, + private val playerCountService: PlayerCountService, + private val layoutService: LayoutService +) { + + private val miniMessage = MiniMessage.miniMessage() + + fun format(text: String, vararg resolvers: TagResolver): Component = + messages.get().msg(text, getPlaceholders("unknown", -1), *resolvers) + + fun formatForPlayer(text: String, serverName: String, ping: Long): Component = + messages.get().msg(text, getPlaceholders(serverName, ping)) + + fun formatMotd(line1: String, line2: String): Component = + MotdMiniMessageFormatter.deserialize(miniMessage, line1, line2, listOf(getPlaceholders("unknown", -1))) + + private fun getPlaceholders(serverName: String, ping: Long): TagResolver { + val format = messages.get().format + val onlinePlayers = playerCountService.getOnlinePlayers() + val slots = layoutService.getLayout(layoutService.getLayoutName(null)).version.slots + + return TagResolver.resolver( + Placeholder.unparsed("server_name", serverName), + Placeholder.parsed("ping", getPingColor(ping) + ping), + Placeholder.unparsed("online_players", onlinePlayers.toString()), + Placeholder.unparsed("max_players", slots.resolveMaxPlayers(onlinePlayers, playerCountService.getMaxPlayers()).toString()), + getDateTimeResolver("date", format.date), + getDateTimeResolver("time", format.time), + getEnvironmentResolver() + ) + } + + private fun getPingColor(ping: Long): String { + val pingColor = messages.get().format.pingColors.filter { ping >= it.ping }.maxByOrNull { it.ping } + if (pingColor == null) return "" + return pingColor.color + } + + private fun getDateTimeResolver(name: String, defaultPattern: String): TagResolver { + return TagResolver.resolver(name) { arguments, _ -> + val pattern = if (arguments.hasNext()) arguments.pop().value() else defaultPattern + Tag.selfClosingInserting(Component.text(LocalDateTime.now().format(DateTimeFormatter.ofPattern(pattern)))) + } + } + + private fun getEnvironmentResolver(): TagResolver { + return TagResolver.resolver("env") { arguments, _ -> + val name = arguments.popOr("env name expected").value() + val default = if (arguments.hasNext()) arguments.pop().value() else "" + Tag.preProcessParsed(System.getenv(name) ?: default) + } + } +} \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MinecraftTextWidth.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/text/MinecraftTextWidth.kt similarity index 96% rename from proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MinecraftTextWidth.kt rename to proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/text/MinecraftTextWidth.kt index 087690e..91bfbbc 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MinecraftTextWidth.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/text/MinecraftTextWidth.kt @@ -1,4 +1,4 @@ -package app.simplecloud.plugin.proxy.shared.utilities.format +package app.simplecloud.plugin.proxy.shared.utilities.text import net.kyori.adventure.key.Key import net.kyori.adventure.text.Component @@ -13,7 +13,7 @@ object MinecraftTextWidth { const val ZERO_WIDTH_NON_JOINER = 0x200c - private const val RESOURCE_DIRECTORY = "/app/simplecloud/plugin/proxy/shared/format" + private const val RESOURCE_DIRECTORY = "/widths" private const val UNIFONT_ADVANCES_RESOURCE = "unifont-advances.bin" private const val MISSING_GLYPH_WIDTH = 6f private const val OBJECT_WIDTH = 8f diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MotdMiniMessageFormatter.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/text/MotdMiniMessageFormatter.kt similarity index 92% rename from proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MotdMiniMessageFormatter.kt rename to proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/text/MotdMiniMessageFormatter.kt index e726bde..f94d310 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/format/MotdMiniMessageFormatter.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/text/MotdMiniMessageFormatter.kt @@ -1,4 +1,4 @@ -package app.simplecloud.plugin.proxy.shared.utilities.format +package app.simplecloud.plugin.proxy.shared.utilities.text import net.kyori.adventure.key.Key import net.kyori.adventure.text.Component @@ -59,7 +59,9 @@ object MotdMiniMessageFormatter { private fun createPadding(paddingWidth: Int): Component { return Component.text() .append(Component.text(" ".repeat(paddingWidth / SPACE_WIDTH)).font(defaultFont).decoration(TextDecoration.BOLD, false)) - .append(Component.text(pixelPaddingCharacter.repeat(paddingWidth % SPACE_WIDTH)).font(defaultFont).decoration(TextDecoration.BOLD, true)) + .append( + Component.text(pixelPaddingCharacter.repeat(paddingWidth % SPACE_WIDTH)).font(defaultFont).decoration( + TextDecoration.BOLD, true)) .build() } diff --git a/proxy-bungeecord/src/main/resources/config/config.yml b/proxy-shared/src/main/resources/config/config.yml similarity index 82% rename from proxy-bungeecord/src/main/resources/config/config.yml rename to proxy-shared/src/main/resources/config/config.yml index 532e92a..55abeb8 100644 --- a/proxy-bungeecord/src/main/resources/config/config.yml +++ b/proxy-shared/src/main/resources/config/config.yml @@ -27,14 +27,24 @@ joinstates: full: 'simplecloud.proxy-essentials.join.maintenance' forced-motd-layout: maintenance -# Global whitelist for direct player access. +# Players on this list can always join, even during maintenance or when the network is full. # Prefer permission-based access for regular users. # Use this list only for administrators or emergency access. whitelist: - enabled: true + enabled: false # Supports player names and UUIDs. - players: - - Notch + players: [] + +# Shows a different layout depending on the domain players use to ping the network. +# The layout follows the join state of the target group or persistent server: +# a matching rule wins, then the forced-motd-layout of the join state, then initial-layout. +domains: [] +#domains: +#- domain: 'play.example.com' +# target: 'lobby' +# rules: +# - state: maintenance +# layout: maintenance # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # Player Count @@ -50,7 +60,7 @@ player-count: additional-groups: [] # Persistent servers included in the displayed player count. additional-persistent-servers: [] - # Player count update interval in ticks. Set to 0 to disable automatic updates. + # Player count update interval in ticks. Set to 0 to count only once on startup. update-time: 20 # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -65,5 +75,5 @@ tablist: layout: - header: '
SimpleCloud v3
' footer: '
players are playing on your network
sษชแดแด˜สŸแด‡แด„สŸแดแดœแด….แด€แด˜แด˜
' - # Update interval in ticks. + # Interval in ticks after which the next layout entry is shown. Set to 0 to always show the first entry. update-time: 20 diff --git a/proxy-velocity/src/main/resources/config/layout/maintenance.yml b/proxy-shared/src/main/resources/config/layout/maintenance.yml similarity index 90% rename from proxy-velocity/src/main/resources/config/layout/maintenance.yml rename to proxy-shared/src/main/resources/config/layout/maintenance.yml index c688b4c..32a36a0 100644 --- a/proxy-velocity/src/main/resources/config/layout/maintenance.yml +++ b/proxy-shared/src/main/resources/config/layout/maintenance.yml @@ -13,8 +13,9 @@ motd: # QUEUE, RANDOM update-type: RANDOM #update-type: QUEUE - #update-time: 10 - #update-time: 0 disables updates + # Interval in ticks after which the next entry is shown. Without it, the entry changes on every ping. + # Set to 0 to always show the first entry. + #update-time: 100 # Wrap a line in
...
to center it using vanilla font metrics. layouts: - name: 'maintenance' diff --git a/proxy-velocity/src/main/resources/config/layout/public.yml b/proxy-shared/src/main/resources/config/layout/public.yml similarity index 90% rename from proxy-velocity/src/main/resources/config/layout/public.yml rename to proxy-shared/src/main/resources/config/layout/public.yml index 7aaa1f8..6d43d27 100644 --- a/proxy-velocity/src/main/resources/config/layout/public.yml +++ b/proxy-shared/src/main/resources/config/layout/public.yml @@ -13,8 +13,9 @@ motd: # QUEUE, RANDOM update-type: RANDOM #update-type: QUEUE - #update-time: 10 - #update-time: 0 disables updates + # Interval in ticks after which the next entry is shown. Without it, the entry changes on every ping. + # Set to 0 to always show the first entry. + #update-time: 100 # Wrap a line in
...
to center it using vanilla font metrics. layouts: - name: 'public' diff --git a/proxy-bungeecord/src/main/resources/config/layout/server-icons/simplecloud-gray.png b/proxy-shared/src/main/resources/config/layout/server-icons/simplecloud-gray.png similarity index 100% rename from proxy-bungeecord/src/main/resources/config/layout/server-icons/simplecloud-gray.png rename to proxy-shared/src/main/resources/config/layout/server-icons/simplecloud-gray.png diff --git a/proxy-bungeecord/src/main/resources/config/layout/server-icons/simplecloud.png b/proxy-shared/src/main/resources/config/layout/server-icons/simplecloud.png similarity index 100% rename from proxy-bungeecord/src/main/resources/config/layout/server-icons/simplecloud.png rename to proxy-shared/src/main/resources/config/layout/server-icons/simplecloud.png diff --git a/proxy-bungeecord/src/main/resources/config/messages.yml b/proxy-shared/src/main/resources/config/messages.yml similarity index 73% rename from proxy-bungeecord/src/main/resources/config/messages.yml rename to proxy-shared/src/main/resources/config/messages.yml index ffb2e37..5c573e6 100644 --- a/proxy-bungeecord/src/main/resources/config/messages.yml +++ b/proxy-shared/src/main/resources/config/messages.yml @@ -8,6 +8,26 @@ version: '2' variables: prefix: "โšก " +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Format +# Default formats of and