diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml deleted file mode 100644 index 84ee52f..0000000 --- a/.github/workflows/release-dev.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: Create Release with ShadowJars - -on: - push: - branches: - - main - workflow_dispatch: - -jobs: - build: - name: Build ShadowJars and Create Release - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - ref: main - - - name: Set up JDK 21 - uses: actions/setup-java@v3 - 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 }} - - - name: Make gradlew executable - run: chmod +x ./gradlew - - - name: Build ShadowJars - run: ./gradlew clean build shadowJar - - - name: Get Gradle Version - id: gradle_version - run: echo "GRADLE_VERSION=$(./gradlew properties -q | grep "version:" | awk '{print $2}')" >> $GITHUB_ENV - - - name: Get Commit Hash - id: commit_hash - run: echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - - - name: Publish to Modrinth - run: ./gradlew modrinth && ./gradlew modrinthSyncBody - env: - 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: | - # 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) - if [[ $(basename "$jar") =~ -[0-9]+\.[0-9]+ ]]; then - echo "Skipping $jar due to version number" - else - echo "Uploading $jar" - gh release upload v${{ env.GRADLE_VERSION }} "$jar" - fi - done - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5f14149 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,60 @@ +name: Create Release and Publish to Modrinth + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + name: Build Modules and Create Release + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + submodules: recursive + + - name: Set up JDK + uses: actions/setup-java@v6 + with: + distribution: 'temurin' + java-version: '25' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Make gradlew executable + run: chmod +x ./gradlew + + - name: Build ShadowJars + run: ./gradlew build shadowJar + + - name: Get Gradle Version + run: echo "GRADLE_VERSION=$(./gradlew properties -q | grep "version:" | awk '{print $2}')" >> $GITHUB_ENV + + - name: Publish to Modrinth + run: ./gradlew modrinth modrinthSyncBody + env: + MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} + + - name: Create Release + run: | + jars=() + for jar in $(find . -type f -name "*.jar" -path "*/build/libs/*.jar" -not -path "./build/libs/*"); do + if [[ $(basename "$jar") =~ -[0-9]+\.[0-9]+ ]]; then + echo "Skipping $jar due to version number" + else + echo "Uploading $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 }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index f1a3c39..5f3a971 100644 --- a/.gitignore +++ b/.gitignore @@ -1,43 +1,14 @@ .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/ +.idea/ -### 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/ +### Kotlin ### +.kotlin/ ### 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/LICENSE b/LICENSE index 94395cc..fc27a6f 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [2024] [SimpleCloud] + Copyright 2024-2026 SimpleCloud Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index bb067a2..89d4dfa 100644 --- a/README.md +++ b/README.md @@ -29,21 +29,14 @@ The Proxy Plugin enhances your server network with customizable MOTD (Server Inf ## Features -- [x] **Mutliple MOTD Layouts**: Create multiple MOTD Layouts for different states! -- [x] **Centered MOTD Lines**: Wrap a line in `
...
` to center MiniMessage text, formatting, symbols, and object components. -- [x] **Custom Tablist**: Send your Players custom Tablist Header and Footer! -- [x] **Joinstates**: Create different joinstates so your players can only join with certain permissions (you can call it advanced maintenance feature) - -### Centering MOTD lines - -The `
` tag is available in the two MOTD line fields: - -```yaml -line1: '
A simplecloud.app network
' -line2: '
Welcome!
' -``` - -Centering uses vanilla bitmap and Unihex font metrics, with invisible one-pixel padding for precise placement. Custom resource-pack fonts and client-localized components can differ because their final glyph widths are only known by the client. +- [x] **Join States**: Decide who can join with permissions, for example during maintenance. +- [x] **Whitelist**: Players on the list can always join. +- [x] **Server List Layouts**: Set the MOTD, server icon, hover text, version text and player slots. +- [x] **Rotating MOTDs**: Show your MOTDs in random order or one after another. +- [x] **Centered MOTD Lines**: Wrap a line in `
...
` to center it. +- [x] **Domain Layouts**: Show a different server list layout for each domain. +- [x] **Network Player Count**: Show the players of your whole network instead of one proxy. +- [x] **Tab List**: Set a header and footer for each server group. ## Contributing Contributions to SimpleCloud are welcome and highly appreciated. However, before you jump right into it, we would like you to read our [Contribution Guide][docs-contribute]. diff --git a/build.gradle.kts b/build.gradle.kts index 591216a..d324988 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.14" -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 = "1.0.0" repositories { mavenCentral() @@ -19,56 +16,58 @@ 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 { - 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.slf4j.api) compileOnly(rootProject.libs.simplecloud.api) 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 { + duplicatesStrategy = DuplicatesStrategy.INCLUDE 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") + tasks.processResources { + filesMatching("plugin.yml") { + expand( + "version" to project.version + ) + } } + } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 234e1f6..b9f3e2b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,50 +1,60 @@ [versions] -kotlin = "2.4.10" -kotlin-coroutines = "1.11.0" -shadow = "8.3.3" +kotlin = "2.4.20" +kotlinx-coroutines = "1.11.0" -velocity = "3.6.0-SNAPSHOT" +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" -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" +cloud-command-core = "2.1.0" +cloud-command-velocity = "2.0.1" +cloud-command-bungee = "2.0.1" [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" } - -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" } -command-cloud-bungeecord = { module = "org.incendo:cloud-bungee", version.ref = "command-cloud-bungeecord" } - -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" } -gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } + +cloud-command-core = { module = "org.incendo:cloud-core", version.ref = "cloud-command-core" } +cloud-command-coroutines = { module = "org.incendo:cloud-kotlin-coroutines", version.ref = "cloud-command-core" } +cloud-command-velocity = { module = "org.incendo:cloud-velocity", version.ref = "cloud-command-velocity" } +cloud-command-bungee = { module = "org.incendo:cloud-bungee", version.ref = "cloud-command-bungee" } + +slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } + +velocity-api = { module = "com.velocitypowered:velocity-api", version.ref = "velocity" } +bungeecord-api = { module = "net.md-5:bungeecord-api", version.ref = "bungeecord" } + +adventure-api = { module = "net.kyori:adventure-api", version.ref = "adventure" } +adventure-text-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure" } +adventure-platform-bungeecord = { 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" } simplecloud-api = { module = "app.simplecloud.api:api", version.ref = "simplecloud-api" } simplecloud-plugin = { module = "app.simplecloud.plugin:plugin-shared", version.ref = "simplecloud-plugin" } +[bundles] +configurate = ["configurate-yaml", "configurate-kotlin"] +cloud = ["cloud-command-core", "cloud-command-coroutines"] +adventure = ["adventure-api", "adventure-text-minimessage"] + [plugins] 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/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 34ace61..b589e64 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.8.0-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 9509fc3..524a376 100644 --- a/proxy-bungeecord/build.gradle.kts +++ b/proxy-bungeecord/build.gradle.kts @@ -3,24 +3,10 @@ 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) -} - - -tasks.shadowJar { - relocate("net.kyori", "app.simplecloud.relocate.kyori") + implementation(project(":proxy-shared")) + implementation(libs.adventure.platform.bungeecord) + implementation(libs.cloud.command.bungee) + compileOnly(libs.bungeecord.api) } modrinth { @@ -55,8 +41,7 @@ modrinth { "26.2", "26.3", ) - 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-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 8fb7eb9..0000000 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/BungeeCordCommandSender.kt +++ /dev/null @@ -1,25 +0,0 @@ -package app.simplecloud.plugin.proxy.bungeecord - -import app.simplecloud.plugin.proxy.shared.handler.command.CommandSender -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 - -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 - } - - 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/BungeeProxyPlugin.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/BungeeProxyPlugin.kt new file mode 100644 index 0000000..3593394 --- /dev/null +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/BungeeProxyPlugin.kt @@ -0,0 +1,38 @@ +package app.simplecloud.plugin.proxy.bungeecord + +import app.simplecloud.plugin.proxy.bungeecord.command.BungeeCommandSender +import app.simplecloud.plugin.proxy.bungeecord.listener.ConnectionListener +import app.simplecloud.plugin.proxy.bungeecord.listener.ProxyPingListener +import app.simplecloud.plugin.proxy.bungeecord.platform.BungeePlatformImpl +import app.simplecloud.plugin.proxy.shared.ProxyEssentials +import app.simplecloud.plugin.proxy.shared.command.ProxyCommandHandler +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 BungeeProxyPlugin : Plugin() { + + private val essentials = ProxyEssentials(BungeePlatformImpl(proxy, dataFolder.toPath())) + + override fun onEnable() { + essentials.start() + + proxy.pluginManager.registerListener(this, ConnectionListener(essentials)) + proxy.pluginManager.registerListener(this, ProxyPingListener(essentials)) + + ProxyCommandHandler(createCommandManager(), essentials).register() + } + + override fun onDisable() { + essentials.stop() + } + + private fun createCommandManager(): BungeeCommandManager { + return BungeeCommandManager( + this, + ExecutionCoordinator.asyncCoordinator(), + SenderMapper.create(::BungeeCommandSender, BungeeCommandSender::commandSender) + ) + } +} 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 deleted file mode 100644 index 1f8b8e3..0000000 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ProxyBungeeCordPlugin.kt +++ /dev/null @@ -1,94 +0,0 @@ -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.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 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.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() { - - val proxyPlugin = ProxyPlugin(this.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() } - ) - - val commandManager = BungeeCommandManager( - this, - executionCoordinator, - senderMapper - ) - - ProxyEssentialsCommandHandler(commandManager, this.proxyPlugin).loadCommands() - JoinStateCommandHandler(commandManager, this.proxyPlugin).loadCommands() - LayoutCommandHandler(commandManager, this.proxyPlugin).loadCommands() - } - - override fun onDisable() { - this.adventure?.close() - this.adventure = null - this.tabListHandler.stopTabListTask() - this.proxyPlugin.shutdown() - } - - fun adventure(): BungeeAudiences { - return adventure ?: throw IllegalStateException("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() - ) - } - - fun deserializeMotd(line1: String, line2: String): Component { - val configureTagResolversEvent = this.proxy.pluginManager.callEvent(ConfigureTagResolversEvent(null)) - return MotdMiniMessageFormatter.deserialize( - miniMessage, - line1, - line2, - configureTagResolversEvent.tagResolvers - ) - } -} diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/command/BungeeCommandSender.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/command/BungeeCommandSender.kt new file mode 100644 index 0000000..c78a250 --- /dev/null +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/command/BungeeCommandSender.kt @@ -0,0 +1,17 @@ +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 BungeeCommandSender( + val commandSender: CommandSender +) : ProxySender { + + override fun sendMessage(message: Component) { + commandSender.sendMessage(*BungeeComponentSerializer.get().serialize(message)) + } + + override fun hasPermission(permission: String) = commandSender.hasPermission(permission) +} \ No newline at end of file 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/event/ConfigureTagResolversEvent.kt deleted file mode 100644 index ddd1d22..0000000 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/event/ConfigureTagResolversEvent.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.simplecloud.plugin.proxy.bungeecord.event - -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 - -class ConfigureTagResolversEvent( - val player: ProxiedPlayer? = null, - val tagResolvers: MutableList = mutableListOf() -): Event() { - - fun withTagResolvers(vararg tagResolvers: TagResolver): ConfigureTagResolversEvent { - return this.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/handler/TabListHandler.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/handler/TabListHandler.kt deleted file mode 100644 index dcced82..0000000 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/handler/TabListHandler.kt +++ /dev/null @@ -1,43 +0,0 @@ -package app.simplecloud.plugin.proxy.bungeecord.handler - -import app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin -import net.md_5.bungee.api.connection.ProxiedPlayer -import net.md_5.bungee.api.scheduler.ScheduledTask -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 resolver = plugin.proxyPlugin.tabListResolver - private lateinit var task: ScheduledTask - - fun startTabListTask() { - task = plugin.proxy.scheduler.schedule(plugin, { - resolver.incrementIndices() - plugin.proxy.players.forEach { updateTabListForPlayer(it) } - }, 1, plugin.proxyPlugin.proxyEssentialsConfig.get().tabListUpdateTimeMillis(), TimeUnit.MILLISECONDS) - } - - fun stopTabListTask() { - if (!this::task.isInitialized) { - logger.warning("Can't stop tablist task because it is not initialized") - return - } - task.cancel() - } - - 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 (header, footer) = resolver.getCurrentTabList(tabListGroup) ?: return - - plugin.adventure().player(player).sendPlayerListHeaderAndFooter( - plugin.deserializeToComponent(header, player), - plugin.deserializeToComponent(footer, player), - ) - } -} \ No newline at end of file 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/ConnectionListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ConnectionListener.kt new file mode 100644 index 0000000..4ac1d04 --- /dev/null +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ConnectionListener.kt @@ -0,0 +1,63 @@ +package app.simplecloud.plugin.proxy.bungeecord.listener + +import app.simplecloud.plugin.proxy.bungeecord.platform.BungeeProxyPlayer +import app.simplecloud.plugin.proxy.shared.ProxyEssentials +import app.simplecloud.plugin.proxy.shared.joinstate.JoinResult +import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer +import net.md_5.bungee.api.event.PostLoginEvent +import net.md_5.bungee.api.event.ServerConnectEvent +import net.md_5.bungee.api.event.ServerConnectedEvent +import net.md_5.bungee.api.event.ServerKickEvent +import net.md_5.bungee.api.plugin.Listener +import net.md_5.bungee.event.EventHandler +import net.md_5.bungee.event.EventPriority + +class ConnectionListener( + private val essentials: ProxyEssentials +) : Listener { + + @EventHandler(priority = EventPriority.LOWEST) + fun onPostLogin(event: PostLoginEvent) { + val player = event.player + val result = essentials.joinGate.checkProxyJoin(player.name, player.uniqueId, player::hasPermission) + if (result !is JoinResult.Denied) return + + val message = essentials.messageFormatter.formatForPlayer(result.message, "unknown", player.ping.toLong()) + player.disconnect(*BungeeComponentSerializer.get().serialize(message)) + } + + @EventHandler(priority = EventPriority.HIGHEST) + fun onServerConnect(event: ServerConnectEvent) { + if (event.isCancelled) return + + val player = event.player + val serverName = event.target.name + val result = essentials.joinGate.checkServerSwitch(player.name, player.uniqueId, serverName, player::hasPermission) + if (result !is JoinResult.Denied) return + + val message = essentials.messageFormatter.formatForPlayer(result.message, serverName, player.ping.toLong()) + val components = BungeeComponentSerializer.get().serialize(message) + event.isCancelled = true + + if (player.server == null) { + player.disconnect(*components) + } else { + player.sendMessage(*components) + } + } + + @EventHandler + fun onServerConnected(event: ServerConnectedEvent) { + essentials.tabListService.update(BungeeProxyPlayer(event.player)) + } + + @EventHandler(priority = Byte.MAX_VALUE) + fun onServerKick(event: ServerKickEvent) { + if (!essentials.config.get().showKickReason) return + if (event.cancelServer != null) return + + val reason = event.reason ?: return + event.player.disconnect(reason) + } + +} \ 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 deleted file mode 100644 index 0a963bd..0000000 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/PostLoginListener.kt +++ /dev/null @@ -1,27 +0,0 @@ -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 kotlinx.coroutines.runBlocking -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, -) : Listener { - - @EventHandler(priority = EventPriority.LOWEST) - fun handle(event: PostLoginEvent) { - val player = event.player - val result = runBlocking { - plugin.proxyPlugin.proxyJoinGate.evaluate(player.name) { permission -> player.hasPermission(permission) } - } - - if (result is ProxyJoinGate.Result.Denied) { - player.disconnect(plugin.deserializeToComponent(result.kickMessage, player).toBaseComponent()) - } - } -} 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..42ed8a3 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,60 @@ 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.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.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.io.ByteArrayInputStream import java.net.InetSocketAddress -import java.nio.file.Path -import java.util.* +import java.util.Base64 +import java.util.UUID +import javax.imageio.ImageIO class ProxyPingListener( - private val plugin: ProxyBungeeCordPlugin + private val essentials: ProxyEssentials ) : Listener { - private val serverIconLoader = ServerIconLoader( - Path.of(plugin.proxyPlugin.serverIconsPath) - ) { image -> Favicon.create(image) } - private val localPingSourceMatcher = LocalPingSourceMatcher() + private val serverIconCache = ServerIconCache(essentials.serverIconDirectory) @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() - - if (!layout.motd.enabled) return - - val entry = plugin.proxyPlugin.motdLayoutHandler.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 - val motd = plugin.deserializeMotd(entry.line1, entry.line2) - response.descriptionComponent = net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer.get().serialize(motd)[0] - val socketAddress = event.connection.socketAddress as? InetSocketAddress - val isLocalPing = localPingSourceMatcher.isLocal(socketAddress?.address) + 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)) + } - // server icon - if (layout.serverIcon.enabled) { - serverIconLoader.get(layout.serverIcon.file) { plugin.logger.warning(it) } - ?.let { response.setFavicon(it) } + val icon = if (layout.serverIcon.enabled) serverIconCache.get(layout.serverIcon.file) else null + if (icon != null) { + val bytes = Base64.getDecoder().decode(icon.substringAfter(",")) + response.setFavicon(Favicon.create(ImageIO.read(ByteArrayInputStream(bytes)))) } - 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() - } else { - response.players.sample - } + if (layout.version.name.enabled) { + response.version = ServerPing.Protocol(layout.version.name.text, response.version.protocol) + } - // 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.versionSettings.slots.enabled) { - when (layout.versionSettings.slots.type) { - MaxPlayerDisplayType.REAL -> realMax - MaxPlayerDisplayType.FAKE -> layout.versionSettings.slots.fakeSlots - MaxPlayerDisplayType.DYNAMIC -> onlinePlayers + layout.versionSettings.slots.dynamicPlayerRange - } + 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 { - realMax - } - - response.players = Players(maxPlayers, onlinePlayers, samplePlayers) - - // version name - if (layout.versionSettings.name.enabled) { - response.version = Protocol(layout.versionSettings.name.text, response.version.protocol) + response.players.sample } + response.players = ServerPing.Players(maxPlayers, onlinePlayers, samplePlayers) } } } 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 deleted file mode 100644 index 2f16da7..0000000 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerKickListener.kt +++ /dev/null @@ -1,23 +0,0 @@ -package app.simplecloud.plugin.proxy.bungeecord.listener - -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -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 -) : Listener { - - constructor(proxyPlugin: ProxyPlugin) : this({ - proxyPlugin.proxyEssentialsConfig.get().showKickReason - }) - - @EventHandler(priority = Byte.MAX_VALUE) - fun handle(event: ServerKickEvent) { - if (!showKickReason() || 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 deleted file mode 100644 index 393f851..0000000 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerPreConnectListener.kt +++ /dev/null @@ -1,76 +0,0 @@ -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 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 -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 - -class ServerPreConnectListener( - private val plugin: ProxyBungeeCordPlugin, -) : Listener { - private val logger = Logger.getLogger(ServerPreConnectListener::class.java.name) - private val resolver = plugin.proxyPlugin.joinStateResolver - - @EventHandler(priority = EventPriority.HIGH) - fun handle(event: ServerConnectEvent) { - if (isProxyJoin(event)) { - checkAllowProxyJoin(event.player, event) - if (event.isCancelled) { - return - } - } - - checkAllowServerSwitch(event.player, event, event.target) - } - - private fun isProxyJoin(event: ServerConnectEvent): Boolean { - return event.reason == Reason.JOIN_PROXY || event.player.server == null - } - - private fun checkAllowProxyJoin(player: ProxiedPlayer, event: ServerConnectEvent) { - val result = runBlocking { - plugin.proxyPlugin.proxyJoinGate.evaluate(player.name) { permission -> player.hasPermission(permission) } - } - - if (result is ProxyJoinGate.Result.Denied) { - denyAccess(player, result.kickMessage, false, event) - } - } - - private fun checkAllowServerSwitch(player: ProxiedPlayer, event: ServerConnectEvent, server: ServerInfo) { - val serverName = server.name - - runBlocking { - val joinStateName = resolver.getJoinStateForServer(serverName) - val joinState = resolver.resolveJoinState(joinStateName) - - 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) - return@runBlocking - } - - if (joinState.permission.join.isNotBlank() && !player.hasPermission(joinState.permission.join)) { - logger.info("Player ${player.name} does not have permission to join $serverName.") - denyAccess(player, plugin.proxyPlugin.messagesConfiguration.get().kick.noPermission, true, 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()) - } - event.isCancelled = true - } -} 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/listener/TabListListener.kt deleted file mode 100644 index fc0f484..0000000 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/TabListListener.kt +++ /dev/null @@ -1,18 +0,0 @@ -package app.simplecloud.plugin.proxy.bungeecord.listener - -import app.simplecloud.plugin.proxy.bungeecord.ProxyBungeeCordPlugin -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 -): Listener { - - @EventHandler - fun onServerConnected(event: ServerConnectedEvent) { - val player = event.player - this.plugin.tabListHandler.updateTabListForPlayer(player) - } - -} \ No newline at end of file diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/platform/BungeePlatformImpl.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/platform/BungeePlatformImpl.kt new file mode 100644 index 0000000..be61a75 --- /dev/null +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/platform/BungeePlatformImpl.kt @@ -0,0 +1,19 @@ +package app.simplecloud.plugin.proxy.bungeecord.platform + +import app.simplecloud.plugin.proxy.shared.platform.ProxyPlatform +import net.md_5.bungee.api.ProxyServer +import java.nio.file.Path + +class BungeePlatformImpl( + private val proxy: ProxyServer, + private val dataDirectory: Path +) : ProxyPlatform { + + override fun getDataDirectory() = dataDirectory + + override fun getOnlinePlayers() = proxy.onlineCount + + override fun getMaxPlayers() = proxy.config.playerLimit + + override fun getPlayers() = proxy.players.map { BungeeProxyPlayer(it) } +} \ No newline at end of file diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/platform/BungeeProxyPlayer.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/platform/BungeeProxyPlayer.kt new file mode 100644 index 0000000..18326e1 --- /dev/null +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/platform/BungeeProxyPlayer.kt @@ -0,0 +1,20 @@ +package app.simplecloud.plugin.proxy.bungeecord.platform + +import app.simplecloud.plugin.proxy.shared.platform.ProxyPlayer +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer +import net.md_5.bungee.api.connection.ProxiedPlayer + +class BungeeProxyPlayer( + private val player: ProxiedPlayer +) : ProxyPlayer { + + override fun getServerName(): String? = player.server?.info?.name + + override fun getPing() = player.ping.toLong() + + override fun sendPlayerListHeaderAndFooter(header: Component, footer: Component) { + val serializer = BungeeComponentSerializer.get() + player.setTabHeader(serializer.serialize(header), serializer.serialize(footer)) + } +} diff --git a/proxy-bungeecord/src/main/resources/config/config.yml b/proxy-bungeecord/src/main/resources/config/config.yml deleted file mode 100644 index 532e92a..0000000 --- a/proxy-bungeecord/src/main/resources/config/config.yml +++ /dev/null @@ -1,69 +0,0 @@ -version: '2' - -# ─────────────────────────────────────────────────────────────────────────────── -# Join States -# Configures who can join the proxy and how full or restricted states behave. -# -# Read more @ https://docs.simplecloud.app/manual/plugins/proxy-essentials -# ─────────────────────────────────────────────────────────────────────────────── - -# Initial settings used when a group is created for the first time. -initial-state: public -initial-layout: public - -# Disconnects players with the backend server's kick reason instead of sending them to a fallback server. -# Disable to keep the proxy's standard fallback handling. -show-kick-reason: false - -joinstates: -- name: public - permission: - join: '' - full: 'simplecloud.proxy-essentials.join.full.public' - forced-motd-layout: null -- name: maintenance - permission: - join: 'simplecloud.proxy-essentials.join.maintenance' - full: 'simplecloud.proxy-essentials.join.maintenance' - forced-motd-layout: maintenance - -# Global whitelist for direct player access. -# Prefer permission-based access for regular users. -# Use this list only for administrators or emergency access. -whitelist: - enabled: true - # Supports player names and UUIDs. - players: - - Notch - -# ─────────────────────────────────────────────────────────────────────────────── -# Player Count -# Displays the summed player count for this proxy group and optional extra targets. -# -# Read more @ https://docs.simplecloud.app/manual/plugins/proxy-essentials -# ─────────────────────────────────────────────────────────────────────────────── - -player-count: - # Enables summed player counts for the server list and placeholders. - enabled: true - # Additional SimpleCloud groups included in the displayed 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. - update-time: 20 - -# ─────────────────────────────────────────────────────────────────────────────── -# Tablist -# Configures tablist layouts and update intervals for connected players. -# -# Read more @ https://docs.simplecloud.app/manual/plugins/proxy-essentials -# ─────────────────────────────────────────────────────────────────────────────── - -tablist: -- name: global - layout: - - header: '
SimpleCloud v3
' - footer: '
players are playing on your network
sɪᴍᴘʟᴇᴄʟᴏᴜᴅ.ᴀᴘᴘ
' - # Update interval in ticks. - update-time: 20 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/messages.yml b/proxy-bungeecord/src/main/resources/config/messages.yml deleted file mode 100644 index ffb2e37..0000000 --- a/proxy-bungeecord/src/main/resources/config/messages.yml +++ /dev/null @@ -1,64 +0,0 @@ -version: '2' - -# ─────────────────────────────────────────────────────────────────────────────── -# Variables -# Reusable variables that can be used throughout the messages. -# Usage: will be replaced with the defined value. -# ─────────────────────────────────────────────────────────────────────────────── -variables: - prefix: "⚡ " - -# ─────────────────────────────────────────────────────────────────────────────── -# Kick Messages -# ─────────────────────────────────────────────────────────────────────────────── -kick: - no-permission: "The network is currently in maintenance mode. Please try again later." - network-full: "The network is currently full. Please try again later." - no-join-state: "No join state found for server. Please try again later." - -# ─────────────────────────────────────────────────────────────────────────────── -# Command Messages -# ─────────────────────────────────────────────────────────────────────────────── -command: - reload: - start: "Reloading ProxyEssentials configurations..." - success: "Successfully reloaded all ProxyEssentials configurations." - failure: "Failed to reload configurations: " - - join-state: - server: - update-success: "Join state of server updated successfully." - update-failure: "Failed to update join state of server." - update-no-change: "Join state of server did not change." - - group: - update-success: "Join state of group updated successfully." - update-failure: "Failed to update join state of group." - update-no-change: "Join state of group did not change." - - help: - header: "Commands of join state:" - entry: " " - - list: - groups: - header: "Groups with every join states:" - entry: " - " - - states: - header: "List of every join states:" - entry: " - " - - layout: - help: - header: "Commands of layout:" - entry: " " - - info: - header: "Layout for :" - entry: " " - - set: - update-success: "Layout for group updated successfully." - update-failure: "Failed to update layout for group." - update-no-change: "Layout for group did not change." 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 31e9f6c..0000000 --- a/proxy-bungeecord/src/main/resources/config/placeholder.yml +++ /dev/null @@ -1,13 +0,0 @@ -currentDateFormat: "dd.MM.yyyy" -currentTimeFormat: "HH:mm:ss" -pingColors: - - ping: 0 - color: "" - - ping: 50 - color: "" - - ping: 100 - color: "" - - ping: 150 - color: "" - - ping: 200 - color: "" diff --git a/proxy-bungeecord/src/main/resources/plugin.yml b/proxy-bungeecord/src/main/resources/plugin.yml index e4a46e7..8fd0c90 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 +name: simplecloud-proxy-essentials +main: app.simplecloud.plugin.proxy.bungeecord.BungeeProxyPlugin +description: Essential proxy features for your SimpleCloud network. +version: ${version} author: D151l 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 1779457..1a423a2 100644 --- a/proxy-shared/build.gradle.kts +++ b/proxy-shared/build.gradle.kts @@ -1,14 +1,5 @@ dependencies { - compileOnly(rootProject.libs.adventure.minimessage) - - testImplementation(kotlin("test")) - 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") - } - - compileOnly(rootProject.libs.command.cloud.core) + api(libs.bundles.adventure) + api(libs.bundles.configurate) + api(libs.bundles.cloud) } 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..da21e21 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyEssentials.kt @@ -0,0 +1,79 @@ +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.config.MessageConfig +import app.simplecloud.plugin.proxy.shared.config.ProxyEssentialsConfig +import app.simplecloud.plugin.proxy.shared.config.DefaultConfigInstaller +import app.simplecloud.plugin.proxy.shared.config.migration.LegacyDirectoryMigrator +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.platform.ProxyPlatform +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.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) + val api: CloudApi = 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 joinGate = JoinGate(api, joinStateService, config, messages, scope) + val messageFormatter = MessageFormatter(messages, playerCountService, layoutService) + val tabListService = TabListService(platform, config, messageFormatter, scope) + val serverIconDirectory: Path = dir.resolve("layout").resolve("server-icons") + + fun start() { + LegacyDirectoryMigrator.migrate(dir) + DefaultConfigInstaller.install(dir, javaClass.classLoader) + loadConfigs() + layoutService.startDomainSync() + tabListService.start() + + if (SimpleCloudRuntime.serverId().isBlank()) return + joinStateService.startServerStateSync() + joinGate.start() + playerCountService.start() + scope.launch { + try { + joinStateService.start() + layoutService.start() + } catch (e: Exception) { + logger.error("Failed to load this server from the controller, so it uses the initial join state and layout", e) + } + } + } + + fun stop() { + scope.cancel() + api.close() + } + + fun reload() { + loadConfigs() + } + + private fun loadConfigs() { + config.loadOrCreate(ProxyEssentialsConfig()) + messages.loadOrCreate(MessageConfig()) + layoutRepository.loadLayouts() + } +} 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 7a2ff57..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt +++ /dev/null @@ -1,64 +0,0 @@ -package app.simplecloud.plugin.proxy.shared - -import app.simplecloud.api.CloudApi -import app.simplecloud.api.CloudApiOptions -import app.simplecloud.plugin.proxy.shared.config.DefaultConfigInstaller -import app.simplecloud.plugin.proxy.shared.config.OldConfigMigrator -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 -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 java.io.File -import java.nio.file.Path - -class ProxyPlugin( - dirPath: String -) { - val api = CloudApi.create(CloudApiOptions.builder().component("proxy-essentials").build()) - - init { - val dataDirectory = Path.of(dirPath) - OldConfigMigrator.migrate(dataDirectory) - DefaultConfigInstaller.install(dataDirectory, javaClass.classLoader) - } - - val config = YamlConfig(dirPath) - val proxyEssentialsConfig = config.load("config") - val placeHolderConfiguration = config.load("placeholder") - val messagesConfiguration = config.load("messages") - - 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 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() - } - - fun shutdown() { - playerCountHandler.stop() - joinStateHandler.stop() - cloudControllerHandler.close() - motdLayoutHandler.close() - domainMotdHandler.stop() - config.close() - } -} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyCommandHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyCommandHandler.kt new file mode 100644 index 0000000..cd0b17c --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxyCommandHandler.kt @@ -0,0 +1,58 @@ +package app.simplecloud.plugin.proxy.shared.command + +import app.simplecloud.plugin.proxy.shared.ProxyEssentials +import app.simplecloud.plugin.proxy.shared.command.commands.JoinStateCommand +import app.simplecloud.plugin.proxy.shared.command.commands.LayoutCommand +import app.simplecloud.plugin.proxy.shared.command.commands.ReloadCommand +import app.simplecloud.plugin.proxy.shared.utilities.CommandPermissions +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import org.incendo.cloud.CommandManager +import org.incendo.cloud.context.CommandContext +import org.incendo.cloud.permission.Permission + +class ProxyCommandHandler( + private val manager: CommandManager, + private val plugin: ProxyEssentials +) { + + fun register() { + manager.command( + manager.commandBuilder("scproxy") + .handler { context -> sendHelp(context) } + .permission(Permission.permission(CommandPermissions.HELP)) + .build() + ) + manager.command( + manager.commandBuilder("scproxy") + .literal("help") + .handler { context -> sendHelp(context) } + .permission(Permission.permission(CommandPermissions.HELP)) + .build() + ) + JoinStateCommand(plugin.api, plugin, manager).register() + LayoutCommand(plugin.api, plugin, manager).register() + ReloadCommand(plugin, manager).register() + } + + private fun sendHelp(context: CommandContext) { + val sender = context.sender() + val help = plugin.messages.get().command.help + val formatter = plugin.messageFormatter + val entries = listOf( + "/scproxy reload" to CommandPermissions.RELOAD, + "/scproxy joinstate help" to CommandPermissions.JOINSTATE_HELP, + "/scproxy joinstate list" to CommandPermissions.JOINSTATE_INFO, + "/scproxy joinstate info" to CommandPermissions.JOINSTATE_INFO, + "/scproxy joinstate info [id]" to CommandPermissions.JOINSTATE_INFO, + "/scproxy joinstate set [id]" to CommandPermissions.JOINSTATE_SET, + "/scproxy layout help" to CommandPermissions.LAYOUT_HELP, + "/scproxy layout info [group]" to CommandPermissions.LAYOUT_INFO, + "/scproxy layout set " to CommandPermissions.LAYOUT_SET + ).filter { (_, permission) -> sender.hasPermission(permission) } + + sender.sendMessage(formatter.format(help.header)) + entries.forEach { (command, _) -> + sender.sendMessage(formatter.format(help.entry, Placeholder.unparsed("command", command))) + } + } +} 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..4f0ef16 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxySender.kt @@ -0,0 +1,10 @@ +package app.simplecloud.plugin.proxy.shared.command + +import net.kyori.adventure.text.Component + +interface ProxySender { + + fun sendMessage(message: Component) + + fun hasPermission(permission: String): Boolean +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxySuggestions.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxySuggestions.kt new file mode 100644 index 0000000..e5c58ae --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/ProxySuggestions.kt @@ -0,0 +1,60 @@ +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.ProxyEssentialsConfig +import app.simplecloud.plugin.proxy.shared.layout.LayoutRepository +import kotlinx.coroutines.future.await +import org.incendo.cloud.kotlin.coroutines.SuspendingSuggestionProvider +import org.incendo.cloud.suggestion.Suggestion +import org.incendo.cloud.suggestion.SuggestionProvider + +object ProxySuggestions { + + fun groups(api: CloudApi): SuggestionProvider = + SuspendingSuggestionProvider { _, _ -> + try { + api.group().allGroups.await().map { Suggestion.suggestion(it.name) } + } catch (_: Exception) { + emptyList() + } + }.asSuggestionProvider() + + fun targetTypes(): SuggestionProvider = + SuspendingSuggestionProvider { _, _ -> + listOf(Suggestion.suggestion("group"), Suggestion.suggestion("ps")) + }.asSuggestionProvider() + + fun targets(api: CloudApi, arg: String = "targetType"): SuggestionProvider = + SuspendingSuggestionProvider { context, _ -> + try { + if (context.get(arg).equals("ps", true)) { + api.persistentServer().allPersistentServers.await().map { Suggestion.suggestion(it.name) } + } else { + api.group().allGroups.await().map { Suggestion.suggestion(it.name) } + } + } catch (_: Exception) { + emptyList() + } + }.asSuggestionProvider() + + fun serverIds(api: CloudApi, arg: String = "target"): SuggestionProvider = + SuspendingSuggestionProvider { context, _ -> + try { + val group = context.get(arg) + api.server().getServersByGroup(group).await().map { Suggestion.suggestion(it.numericalId.toString()) } + } catch (_: Exception) { + emptyList() + } + }.asSuggestionProvider() + + fun joinStates(config: ConfigurationFactory): SuggestionProvider = + SuspendingSuggestionProvider { _, _ -> + config.get().joinstates.map { Suggestion.suggestion(it.name) } + }.asSuggestionProvider() + + fun layouts(repository: LayoutRepository): SuggestionProvider = + SuspendingSuggestionProvider { _, _ -> + repository.getNames().map { Suggestion.suggestion(it) } + }.asSuggestionProvider() +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/JoinStateCommand.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/JoinStateCommand.kt new file mode 100644 index 0000000..bc9d57a --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/JoinStateCommand.kt @@ -0,0 +1,207 @@ +package app.simplecloud.plugin.proxy.shared.command.commands + +import app.simplecloud.api.CloudApi +import app.simplecloud.plugin.proxy.shared.ProxyEssentials +import app.simplecloud.plugin.proxy.shared.command.ProxySender +import app.simplecloud.plugin.proxy.shared.command.ProxySuggestions +import app.simplecloud.plugin.proxy.shared.utilities.CommandPermissions +import kotlinx.coroutines.future.await +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import org.incendo.cloud.CommandManager +import org.incendo.cloud.context.CommandContext +import org.incendo.cloud.kotlin.coroutines.extension.suspendingHandler +import org.incendo.cloud.parser.standard.IntegerParser.integerParser +import org.incendo.cloud.parser.standard.StringParser.stringParser +import org.incendo.cloud.permission.Permission +import org.slf4j.LoggerFactory + +class JoinStateCommand( + private val api: CloudApi, + private val plugin: ProxyEssentials, + private val manager: CommandManager +) { + + private val logger = LoggerFactory.getLogger(JoinStateCommand::class.java) + + fun register() { + registerHelp() + registerList() + registerInfo() + registerSet() + } + + private fun registerHelp() { + manager.command( + manager.commandBuilder("scproxy") + .literal("joinstate") + .handler { context -> sendHelp(context) } + .permission(Permission.permission(CommandPermissions.JOINSTATE_HELP)) + .build() + ) + manager.command( + manager.commandBuilder("scproxy") + .literal("joinstate") + .literal("help") + .handler { context -> sendHelp(context) } + .permission(Permission.permission(CommandPermissions.JOINSTATE_HELP)) + .build() + ) + } + + private fun registerList() { + manager.command( + manager.commandBuilder("scproxy") + .literal("joinstate") + .literal("list") + .handler { context -> + val sender = context.sender() + val states = plugin.messages.get().command.joinState.list.states + sender.sendMessage(plugin.messageFormatter.format(states.header)) + plugin.config.get().joinstates.forEach { + val state = Placeholder.unparsed("state", it.name) + val permission = Placeholder.unparsed("join_permission", it.permission.join.ifBlank { "-" }) + sender.sendMessage(plugin.messageFormatter.format(states.entry, state, permission)) + } + } + .permission(Permission.permission(CommandPermissions.JOINSTATE_INFO)) + .build() + ) + } + + private fun registerInfo() { + manager.command( + manager.commandBuilder("scproxy") + .literal("joinstate") + .literal("info") + .suspendingHandler { context -> + val sender = context.sender() + try { + sender.sendMessage(plugin.messageFormatter.format(plugin.messages.get().command.joinState.list.groups.header)) + api.group().allGroups.await().forEach { + sendState(sender, it.name, plugin.joinStateService.getGroupState(it.name)) + } + api.persistentServer().allPersistentServers.await().forEach { + sendState(sender, it.name, plugin.joinStateService.getPersistentServerState(it.name)) + } + } catch (e: Exception) { + logger.error("Could not list the join states", e) + sender.sendMessage(plugin.messageFormatter.format(plugin.messages.get().command.failure)) + } + } + .permission(Permission.permission(CommandPermissions.JOINSTATE_INFO)) + .build() + ) + manager.command( + manager.commandBuilder("scproxy") + .literal("joinstate") + .literal("info") + .required("targetType", stringParser(), ProxySuggestions.targetTypes()) + .required("target", stringParser(), ProxySuggestions.targets(api)) + .optional("id", integerParser(1), ProxySuggestions.serverIds(api)) + .suspendingHandler { context -> + val sender = context.sender() + val targetType = context.get("targetType") + val target = context.get("target") + val id = context.getOrDefault("id", null as Int?) + val messages = plugin.messages.get().command + try { + if (!isTargetTypeValid(targetType)) { + sender.sendMessage(plugin.messageFormatter.format(messages.invalidTargetType)) + return@suspendingHandler + } + + when { + targetType.equals("ps", true) -> sendState(sender, target, plugin.joinStateService.getPersistentServerState(target)) + id != null -> sendState(sender, "$target-$id", plugin.joinStateService.getServiceState(target, id)) + else -> sendState(sender, target, plugin.joinStateService.getGroupState(target)) + } + } catch (e: Exception) { + logger.error("Could not load the join state of '$target'", e) + sender.sendMessage(plugin.messageFormatter.format(messages.failure)) + } + } + .permission(Permission.permission(CommandPermissions.JOINSTATE_INFO)) + .build() + ) + } + + private fun registerSet() { + manager.command( + manager.commandBuilder("scproxy") + .literal("joinstate") + .literal("set") + .required("targetType", stringParser(), ProxySuggestions.targetTypes()) + .required("target", stringParser(), ProxySuggestions.targets(api)) + .required("joinstate", stringParser(), ProxySuggestions.joinStates(plugin.config)) + .optional("id", integerParser(1), ProxySuggestions.serverIds(api)) + .suspendingHandler { context -> + val sender = context.sender() + val targetType = context.get("targetType") + val target = context.get("target") + val state = context.get("joinstate") + val id = context.getOrDefault("id", null as Int?) + val messages = plugin.messages.get().command + val isGroup = targetType.equals("group", true) && id == null + val updateMessages = if (isGroup) messages.joinState.group else messages.joinState.server + + if (!isTargetTypeValid(targetType)) { + sender.sendMessage(plugin.messageFormatter.format(messages.invalidTargetType)) + return@suspendingHandler + } + + if (plugin.config.get().joinstates.none { it.name == state }) { + sender.sendMessage(plugin.messageFormatter.format(updateMessages.updateFailure)) + return@suspendingHandler + } + + try { + val currentState = when { + targetType.equals("ps", true) -> plugin.joinStateService.getPersistentServerState(target) + id != null -> plugin.joinStateService.getServiceState(target, id) + else -> plugin.joinStateService.getGroupState(target) + } + if (currentState == state) { + sender.sendMessage(plugin.messageFormatter.format(updateMessages.updateNoChange)) + return@suspendingHandler + } + + when { + targetType.equals("ps", true) -> plugin.joinStateService.setPersistentServerState(target, state) + id != null -> plugin.joinStateService.setServiceState(target, id, state) + else -> plugin.joinStateService.setGroupState(target, state) + } + sender.sendMessage(plugin.messageFormatter.format(updateMessages.updateSuccess)) + } catch (e: Exception) { + logger.error("Could not set the join state of '$target' to '$state'", e) + sender.sendMessage(plugin.messageFormatter.format(updateMessages.updateFailure)) + } + } + .permission(Permission.permission(CommandPermissions.JOINSTATE_SET)) + .build() + ) + } + + private fun sendHelp(context: CommandContext) { + val sender = context.sender() + val help = plugin.messages.get().command.joinState.help + val commands = listOf( + "/scproxy joinstate help", + "/scproxy joinstate list", + "/scproxy joinstate info", + "/scproxy joinstate info [id]", + "/scproxy joinstate set [id]" + ) + + sender.sendMessage(plugin.messageFormatter.format(help.header)) + commands.forEach { sender.sendMessage(plugin.messageFormatter.format(help.entry, Placeholder.unparsed("command", it))) } + } + + private fun sendState(sender: C, target: String, state: String) { + val entry = plugin.messages.get().command.joinState.list.groups.entry + sender.sendMessage(plugin.messageFormatter.format(entry, Placeholder.unparsed("group", target), Placeholder.unparsed("state", state))) + } + + private fun isTargetTypeValid(targetType: String): Boolean { + return targetType.equals("group", true) || targetType.equals("ps", true) + } +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/LayoutCommand.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/LayoutCommand.kt new file mode 100644 index 0000000..b4caa1c --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/LayoutCommand.kt @@ -0,0 +1,135 @@ +package app.simplecloud.plugin.proxy.shared.command.commands + +import app.simplecloud.api.CloudApi +import app.simplecloud.plugin.proxy.shared.ProxyEssentials +import app.simplecloud.plugin.proxy.shared.command.ProxySender +import app.simplecloud.plugin.proxy.shared.command.ProxySuggestions +import app.simplecloud.plugin.proxy.shared.utilities.CommandPermissions +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import org.incendo.cloud.CommandManager +import org.incendo.cloud.context.CommandContext +import org.incendo.cloud.kotlin.coroutines.extension.suspendingHandler +import org.incendo.cloud.parser.standard.StringParser.stringParser +import org.incendo.cloud.permission.Permission +import org.slf4j.LoggerFactory + +class LayoutCommand( + private val api: CloudApi, + private val plugin: ProxyEssentials, + private val manager: CommandManager +) { + + private val logger = LoggerFactory.getLogger(LayoutCommand::class.java) + + fun register() { + registerHelp() + registerInfo() + registerSet() + } + + private fun registerHelp() { + manager.command( + manager.commandBuilder("scproxy") + .literal("layout") + .handler { context -> sendHelp(context) } + .permission(Permission.permission(CommandPermissions.LAYOUT_HELP)) + .build() + ) + manager.command( + manager.commandBuilder("scproxy") + .literal("layout") + .literal("help") + .handler { context -> sendHelp(context) } + .permission(Permission.permission(CommandPermissions.LAYOUT_HELP)) + .build() + ) + } + + private fun registerInfo() { + manager.command( + manager.commandBuilder("scproxy") + .literal("layout") + .literal("info") + .handler { context -> sendInfo(context.sender(), "default", plugin.config.get().initialLayout) } + .permission(Permission.permission(CommandPermissions.LAYOUT_INFO)) + .build() + ) + manager.command( + manager.commandBuilder("scproxy") + .literal("layout") + .literal("info") + .required("group", stringParser(), ProxySuggestions.groups(api)) + .suspendingHandler { context -> + val sender = context.sender() + val groupName = context.get("group") + try { + val config = plugin.config.get() + val state = plugin.joinStateService.getGroupState(groupName) + val forcedLayout = config.joinstates.find { it.name == state }?.forcedMotdLayout + val layout = forcedLayout ?: plugin.layoutService.getGroupLayout(groupName) ?: config.initialLayout + sendInfo(sender, groupName, layout) + } catch (e: Exception) { + logger.error("Could not load the layout of group '$groupName'", e) + sender.sendMessage(plugin.messageFormatter.format(plugin.messages.get().command.failure)) + } + } + .permission(Permission.permission(CommandPermissions.LAYOUT_INFO)) + .build() + ) + } + + private fun registerSet() { + manager.command( + manager.commandBuilder("scproxy") + .literal("layout") + .literal("set") + .required("group", stringParser(), ProxySuggestions.groups(api)) + .required("layout", stringParser(), ProxySuggestions.layouts(plugin.layoutRepository)) + .suspendingHandler { context -> + val sender = context.sender() + val groupName = context.get("group") + val layout = context.get("layout") + val messages = plugin.messages.get().command.layout.set + + if (plugin.layoutRepository.find(layout) == null) { + sender.sendMessage(plugin.messageFormatter.format(messages.updateFailure)) + return@suspendingHandler + } + + try { + if (plugin.layoutService.getGroupLayout(groupName) == layout) { + sender.sendMessage(plugin.messageFormatter.format(messages.updateNoChange)) + return@suspendingHandler + } + + plugin.layoutService.setGroupLayout(groupName, layout) + sender.sendMessage(plugin.messageFormatter.format(messages.updateSuccess)) + } catch (e: Exception) { + logger.error("Could not set the layout of group '$groupName' to '$layout'", e) + sender.sendMessage(plugin.messageFormatter.format(messages.updateFailure)) + } + } + .permission(Permission.permission(CommandPermissions.LAYOUT_SET)) + .build() + ) + } + + private fun sendHelp(context: CommandContext) { + val sender = context.sender() + val help = plugin.messages.get().command.layout.help + val commands = listOf( + "/scproxy layout help", + "/scproxy layout info [group]", + "/scproxy layout set " + ) + + sender.sendMessage(plugin.messageFormatter.format(help.header)) + commands.forEach { sender.sendMessage(plugin.messageFormatter.format(help.entry, Placeholder.unparsed("command", it))) } + } + + private fun sendInfo(sender: C, groupName: String, layout: String) { + val info = plugin.messages.get().command.layout.info + sender.sendMessage(plugin.messageFormatter.format(info.header, Placeholder.unparsed("group", groupName))) + sender.sendMessage(plugin.messageFormatter.format(info.entry, Placeholder.unparsed("layout", layout))) + } +} diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/ReloadCommand.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/ReloadCommand.kt new file mode 100644 index 0000000..d867e49 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/command/commands/ReloadCommand.kt @@ -0,0 +1,40 @@ +package app.simplecloud.plugin.proxy.shared.command.commands + +import app.simplecloud.plugin.proxy.shared.ProxyEssentials +import app.simplecloud.plugin.proxy.shared.command.ProxySender +import app.simplecloud.plugin.proxy.shared.utilities.CommandPermissions +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import org.incendo.cloud.CommandManager +import org.incendo.cloud.kotlin.coroutines.extension.suspendingHandler +import org.incendo.cloud.permission.Permission +import org.slf4j.LoggerFactory + +class ReloadCommand( + private val plugin: ProxyEssentials, + private val manager: CommandManager +) { + + private val logger = LoggerFactory.getLogger(ReloadCommand::class.java) + + fun register() { + manager.command( + manager.commandBuilder("scproxy") + .literal("reload") + .suspendingHandler { context -> + val sender = context.sender() + val formatter = plugin.messageFormatter + sender.sendMessage(formatter.format(plugin.messages.get().command.reload.start)) + try { + plugin.reload() + sender.sendMessage(formatter.format(plugin.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(plugin.messages.get().command.reload.failure, error)) + } + } + .permission(Permission.permission(CommandPermissions.RELOAD)) + .build() + ) + } +} 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/DefaultConfigInstaller.kt index 0fe4a0a..8725bde 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/DefaultConfigInstaller.kt @@ -1,124 +1,32 @@ package app.simplecloud.plugin.proxy.shared.config -import java.net.JarURLConnection import java.nio.file.Files import java.nio.file.Path -import java.util.jar.JarFile object DefaultConfigInstaller { - private const val RESOURCE_ROOT = "config" + private val RESOURCES = listOf( + "config.yml", + "messages.yml", + "layout/public.yml", + "layout/maintenance.yml", + "layout/server-icons/simplecloud.png", + "layout/server-icons/simplecloud-gray.png" + ) fun install(dataDirectory: Path, classLoader: ClassLoader) { - Files.createDirectories(dataDirectory) - - val resourceUrl = classLoader.getResource(RESOURCE_ROOT) - ?: return installKnownResources(dataDirectory, classLoader) - when (resourceUrl.protocol) { - "file" -> installFromDirectory(Path.of(resourceUrl.toURI()), dataDirectory) - "jar" -> installFromJar(resourceUrl.openConnection() as JarURLConnection, dataDirectory) - else -> installKnownResources(dataDirectory, classLoader) - } - } + RESOURCES.forEach { + val target = dataDirectory.resolve(it) + val resourceName = "config/$it" - private fun installFromDirectory(resourceDirectory: Path, dataDirectory: Path) { - Files.walk(resourceDirectory).use { paths -> - paths - .filter { Files.isRegularFile(it) } - .forEach { source -> - val target = dataDirectory.resolve(resourceDirectory.relativize(source).toString()) - copyIfMissing(source, target) - } - } - } + if (Files.exists(target)) return@forEach - private fun installFromJar(connection: JarURLConnection, dataDirectory: Path) { - val root = connection.entryName.trimEnd('/') + "/" - val jarFile: JarFile = connection.jarFile - - jarFile.entries().asSequence() - .filter { !it.isDirectory && it.name.startsWith(root) } - .forEach { entry -> - val target = dataDirectory.resolve(entry.name.removePrefix(root)) - jarFile.getInputStream(entry).use { input -> - copyIfMissingOrDamaged(input.readAllBytes(), target) - } + val resource = checkNotNull(classLoader.getResourceAsStream(resourceName)) { + "Missing bundled $resourceName" } - } - private fun copyIfMissing(source: Path, target: Path) { - copyIfMissingOrDamaged(Files.readAllBytes(source), target) - } - - private fun installKnownResources(dataDirectory: Path, classLoader: ClassLoader) { - KNOWN_RESOURCES.forEach { resource -> - val target = dataDirectory.resolve(resource.removePrefix("$RESOURCE_ROOT/")) - val stream = classLoader.getResourceAsStream(resource) ?: return@forEach - stream.use { copyIfMissingOrDamaged(it.readAllBytes(), target) } + Files.createDirectories(target.parent) + resource.use { stream -> Files.copy(stream, target) } } } - - private fun copyIfMissingOrDamaged(sourceBytes: ByteArray, target: Path) { - if (Files.exists(target) && !isDamagedDefaultLayout(sourceBytes, target)) return - - Files.createDirectories(target.parent) - Files.write(target, sourceBytes) - } - - private fun isDamagedDefaultLayout(sourceBytes: ByteArray, target: Path): Boolean { - val normalizedPath = target.toString().replace('\\', '/') - if (!normalizedPath.endsWith("layout/public.yml") && !normalizedPath.endsWith("layout/maintenance.yml")) { - return false - } - - val source = sourceBytes.toString(Charsets.UTF_8) - val targetContent = Files.readString(target) - val damagedSource = source.removeFirstLayoutVersionScalar() - val legacySource = source.replaceFirstLayoutConfigVersionWithVersion() - return (damagedSource != source && targetContent.normalizeLineEndings() == damagedSource.normalizeLineEndings()) || - (legacySource != source && targetContent.normalizeLineEndings() == legacySource.normalizeLineEndings()) - } - - private fun String.removeFirstLayoutVersionScalar(): String { - val lines = lines().toMutableList() - val firstContentIndex = lines.indexOfFirst { it.isNotBlank() } - if (firstContentIndex == -1 || !lines[firstContentIndex].isLayoutFileVersion()) return this - - lines.removeAt(firstContentIndex) - if (firstContentIndex < lines.size && lines[firstContentIndex].isBlank()) { - lines.removeAt(firstContentIndex) - } - return lines.joinToString("\n") - } - - private fun String.replaceFirstLayoutConfigVersionWithVersion(): String { - val lines = lines().toMutableList() - val firstContentIndex = lines.indexOfFirst { it.isNotBlank() } - if (firstContentIndex == -1 || !lines[firstContentIndex].isLayoutConfigVersion()) return this - - lines[firstContentIndex] = lines[firstContentIndex].replaceFirst("config-version:", "version:") - return lines.joinToString("\n") - } - - private fun String.normalizeLineEndings(): String = replace("\r\n", "\n").trimEnd() - - private fun String.isLayoutFileVersion(): Boolean { - val trimmed = trim() - return trimmed == "version: '1'" || trimmed == "version: \"1\"" || trimmed == "version: 1" - } - - private fun String.isLayoutConfigVersion(): Boolean { - val trimmed = trim() - return trimmed == "config-version: '1'" || trimmed == "config-version: \"1\"" || trimmed == "config-version: 1" - } - - 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", - "config/layout/server-icons/simplecloud-gray.png" - ) -} +} \ 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/LayoutConfig.kt similarity index 51% 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/LayoutConfig.kt index d9b38db..08ab588 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/LayoutConfig.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 LayoutConfig( + 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: Long? = 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,22 @@ 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 +) { + fun resolveMaxPlayers(onlinePlayers: Int, realMaxPlayers: Int): Int { + if (!enabled) return realMaxPlayers -@ConfigSerializable -data class VersionConfig( - val name: VersionNameConfig = VersionNameConfig(), - val slots: SlotsConfig = SlotsConfig() -) + return when (type) { + MaxPlayerDisplayType.REAL -> realMaxPlayers + MaxPlayerDisplayType.FAKE -> fakeSlots + MaxPlayerDisplayType.DYNAMIC -> onlinePlayers + dynamicPlayerRange + } + } +} -@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/MessageConfig.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MessageConfig.kt new file mode 100644 index 0000000..96aad9c --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/MessageConfig.kt @@ -0,0 +1,122 @@ +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.ConfigVersion +import org.spongepowered.configurate.objectmapping.ConfigSerializable + +@ConfigSerializable +data class MessageConfig( + override val version: Int = ConfigVersion.VERSION, + override val variables: Map = mapOf("prefix" to "⚡ "), + val format: FormatConfig = FormatConfig(), + val kick: KickMessages = KickMessages(), + val command: CommandMessages = CommandMessages() +) : VersionedConfig, AbstractMessageConfig() + +@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 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." +) + +@ConfigSerializable +data class CommandMessages( + val help: ListMessages = ListMessages( + header = "Available /scproxy commands:", + entry = " " + ), + val failure: String = "The command failed, check the console for details.", + val invalidTargetType: String = "The target type has to be group or ps.", + val reload: ReloadMessages = ReloadMessages(), + val joinState: JoinStateMessages = JoinStateMessages(), + val layout: LayoutMessages = LayoutMessages() +) + +@ConfigSerializable +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 JoinStateMessages( + val server: UpdateMessages = UpdateMessages( + 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( + 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 list: JoinStateListMessages = JoinStateListMessages() +) + +@ConfigSerializable +data class JoinStateListMessages( + val groups: ListMessages = ListMessages( + header = "Groups with their join states:", + entry = " - " + ), + val states: ListMessages = ListMessages( + header = "Available join states:", + entry = " - " + ) +) + +@ConfigSerializable +data class LayoutMessages( + val help: ListMessages = ListMessages( + header = "Commands of layout:", + entry = " " + ), + val info: ListMessages = ListMessages( + header = "Layout for :", + entry = " " + ), + val set: UpdateMessages = UpdateMessages( + updateSuccess = "Layout for group updated successfully.", + updateFailure = "Failed to update layout for group.", + updateNoChange = "Layout for group did not change." + ) +) + +@ConfigSerializable +data class ListMessages( + val header: String = "", + val entry: String = "" +) + +@ConfigSerializable +data class UpdateMessages( + val updateSuccess: String = "Updated successfully.", + val updateFailure: String = "Failed to update.", + val updateNoChange: String = "Nothing changed." +) 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 deleted file mode 100644 index 7ef9d0e..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/OldConfigMigrator.kt +++ /dev/null @@ -1,774 +0,0 @@ -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 -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 - -object OldConfigMigrator { - - private const val CURRENT_CONFIG_VERSION = "2" - private const val CURRENT_LAYOUT_VERSION = "2" - - fun migrate(dataDirectory: Path) { - Files.createDirectories(dataDirectory) - - migrateMainConfig(dataDirectory) - ensureMainConfigDefaults(dataDirectory.resolve("config.yml")) - migrateMessages(dataDirectory.resolve("messages.yml")) - ensureMessageDefaults(dataDirectory.resolve("messages.yml")) - migratePlaceholder(dataDirectory.resolve("placeholder.yml")) - migrateLayouts(dataDirectory.resolve("layout")) - } - - private fun migrateMainConfig(dataDirectory: Path) { - val target = dataDirectory.resolve("config.yml") - val joinStateFile = dataDirectory.resolve("joinstate.yml") - val tabListFile = dataDirectory.resolve("tablist.yml") - if (!Files.exists(joinStateFile) && !Files.exists(tabListFile)) return - - val joinStateNode = joinStateFile.takeIf(Files::exists)?.loadYaml() - val tabListNode = tabListFile.takeIf(Files::exists)?.loadYaml() - if (Files.exists(target)) { - if (target.loadYaml().containsMigratedMainConfig(joinStateNode, tabListNode)) { - deleteIfExists(joinStateFile) - deleteIfExists(tabListFile) - return - } - } - - val loader = createLoader(target) - val node = loader.createNode() - - node.node("version").set(CURRENT_CONFIG_VERSION) - migrateJoinStates(joinStateNode, node) - node.node("show-kick-reason").set(ProxyEssentialsConfig().showKickReason) - node.node("player-count").set(defaultPlayerCount()) - migrateTabList(tabListNode, node) - node.applyMainConfigComments() - - loader.save(node) - target.writeCommentedYaml(::decorateMainConfigYaml) - deleteIfExists(joinStateFile) - deleteIfExists(tabListFile) - } - - private fun ensureMainConfigDefaults(target: Path) { - if (!Files.exists(target)) return - - val loader = createLoader(target) - val node = loader.load() - val playerCountNode = node.node("player-count") - var changed = false - - if (node.node("show-kick-reason").virtual()) { - node.node("show-kick-reason").set(ProxyEssentialsConfig().showKickReason) - changed = true - } - - if (playerCountNode.virtual()) { - playerCountNode.set(defaultPlayerCount()) - changed = true - } else if (playerCountNode.node("enabled").virtual()) { - playerCountNode.node("enabled").set(ProxyEssentialsConfig().playerCount.enabled) - changed = true - } - - if (!changed) return - - node.applyMainConfigComments() - loader.save(node) - target.writeCommentedYaml(::decorateMainConfigYaml) - } - - private fun CommentedConfigurationNode.containsMigratedMainConfig( - joinStateNode: CommentedConfigurationNode?, - tabListNode: CommentedConfigurationNode? - ): Boolean { - if (node("version").string != CURRENT_CONFIG_VERSION) return false - - if (joinStateNode != null) { - val defaultState = joinStateNode.node("default-state").string ?: "public" - val expectedInitialLayout = joinStateNode.node("join-states") - .childrenList() - .firstOrNull { it.node("name").string == defaultState } - ?.node("motd-layout-by-proxy") - ?.string - ?: "public" - - if (node("initial-state").string != defaultState || node("initial-layout").string != expectedInitialLayout) { - return false - } - - val legacyStates = joinStateNode.node("join-states").childrenList() - val currentStates = node("joinstates").childrenList() - for (legacyState in legacyStates) { - val name = legacyState.node("name").string ?: return false - val currentState = currentStates.firstOrNull { it.node("name").string == name } ?: return false - if (currentState.node("permission", "join").string != legacyState.node("join-permission").string.orEmpty()) { - return false - } - if (currentState.node("permission", "full").string != legacyState.node("full-join-permission").string.orEmpty()) { - return false - } - if (currentState.node("forced-motd-layout").string != legacyState.node("motd-layout-by-proxy").string) { - return false - } - } - } - - if (tabListNode != null) { - val expectedUpdateTime = tabListNode.node("tab-list-update-time") - .long - .takeIf { it > 0 } - ?.let { (it / 50L).coerceAtLeast(1L) } - ?: 20L - - val legacyGroups = tabListNode.node("groups").childrenList() - val currentGroups = node("tablist").childrenList() - for (legacyGroup in legacyGroups) { - val name = legacyGroup.node("group-or-service").string ?: return false - val currentGroup = currentGroups.firstOrNull { it.node("name").string == name } ?: return false - if (currentGroup.node("update-time").long != expectedUpdateTime) return false - - val legacyLayouts = legacyGroup.node("tab-lists").childrenList() - val currentLayouts = currentGroup.node("layout").childrenList() - if (currentLayouts.size < legacyLayouts.size) return false - - for ((index, legacyLayout) in legacyLayouts.withIndex()) { - val currentLayout = currentLayouts[index] - if (currentLayout.node("header").string != legacyLayout.node("header").legacyTextList()) return false - if (currentLayout.node("footer").string != legacyLayout.node("footer").legacyTextList()) return false - } - } - } - - return true - } - - private fun migrateJoinStates(source: CommentedConfigurationNode?, target: CommentedConfigurationNode) { - val defaultState = source?.node("default-state")?.string ?: "public" - val oldStates = source?.node("join-states")?.childrenList().orEmpty() - - target.node("initial-state").set(defaultState) - target.node("initial-layout").set( - oldStates - .firstOrNull { it.node("name").string == defaultState } - ?.node("motd-layout-by-proxy") - ?.string - ?: "public" - ) - - val states = if (oldStates.isEmpty()) { - defaultJoinStates() - } else { - oldStates.map { state -> - mapOf( - "name" to (state.node("name").string ?: ""), - "permission" to mapOf( - "join" to (state.node("join-permission").string ?: ""), - "full" to (state.node("full-join-permission").string ?: "") - ), - "forced-motd-layout" to state.node("motd-layout-by-proxy").string - ) - } - } - - target.node("joinstates").set(states) - val whitelist = WhitelistConfig() - target.node("whitelist").set( - mapOf( - "enabled" to whitelist.enabled, - "players" to whitelist.players - ) - ) - } - - private fun migrateTabList(source: CommentedConfigurationNode?, target: CommentedConfigurationNode) { - val updateTimeTicks = source - ?.node("tab-list-update-time") - ?.long - ?.takeIf { it > 0 } - ?.let { (it / 50L).coerceAtLeast(1L) } - ?: 20L - - val oldGroups = source?.node("groups")?.childrenList().orEmpty() - val tabList = if (oldGroups.isEmpty()) { - defaultTabList() - } else { - oldGroups.map { group -> - mapOf( - "name" to (group.node("group-or-service").string ?: "*"), - "layout" to group.node("tab-lists").childrenList().map { layout -> - mapOf( - "header" to layout.node("header").legacyTextList(), - "footer" to layout.node("footer").legacyTextList() - ) - }, - "update-time" to updateTimeTicks - ) - } - } - - target.node("tablist").set(tabList) - } - - private fun migrateMessages(path: Path) { - if (!Files.exists(path)) return - - val source = path.loadYaml() - if (source.node("kick-message").virtual() && source.node("command-message").virtual()) return - - val defaults = MessageConfig() - val command = source.node("command-message") - val loader = createLoader(path) - val target = loader.createNode() - - target.node("version").set(defaults.version) - target.node("variables").set(mapOf("prefix" to defaults.variables.prefix)) - target.node("kick").set( - mapOf( - "no-permission" to source.node("kick-message", "no-permission").stringOr(defaults.kick.noPermission), - "network-full" to source.node("kick-message", "network-full").stringOr(defaults.kick.networkFull), - "no-join-state" to source.node("kick-message", "no-join-state").stringOr(defaults.kick.noJoinState) - ) - ) - target.node("command").set( - mapOf( - "join-state" to mapOf( - "server" to mapOf( - "update-success" to command.node("join-state-server-update-success").stringOr(defaults.command.joinState.server.updateSuccess), - "update-failure" to command.node("join-state-server-update-failure").stringOr(defaults.command.joinState.server.updateFailure), - "update-no-change" to command.node("join-state-server-update-no-change").stringOr(defaults.command.joinState.server.updateNoChange) - ), - "group" to mapOf( - "update-success" to command.node("join-state-group-update-success").stringOr(defaults.command.joinState.group.updateSuccess), - "update-failure" to command.node("join-state-group-update-failure").stringOr(defaults.command.joinState.group.updateFailure), - "update-no-change" to command.node("join-state-group-update-no-change").stringOr(defaults.command.joinState.group.updateNoChange) - ), - "help" to mapOf( - "header" to command.node("join-state-help-header").stringOr(defaults.command.joinState.help.header), - "entry" to command.node("join-state-help-command").stringOr(defaults.command.joinState.help.entry) - ), - "list" to mapOf( - "groups" to mapOf( - "header" to command.node("join-state-group-list-header").stringOr(defaults.command.joinState.list.groups.header), - "entry" to command.node("join-state-group-list-entry").stringOr(defaults.command.joinState.list.groups.entry) - ), - "states" to mapOf( - "header" to command.node("join-state-state-list-header").stringOr(defaults.command.joinState.list.states.header), - "entry" to command.node("join-state-state-list-entry").stringOr(defaults.command.joinState.list.states.entry) - ) - ) - ), - "layout" to mapOf( - "help" to mapOf( - "header" to defaults.command.layout.help.header, - "entry" to defaults.command.layout.help.entry - ), - "info" to mapOf( - "header" to defaults.command.layout.info.header, - "entry" to defaults.command.layout.info.entry - ), - "set" to mapOf( - "update-success" to defaults.command.layout.set.updateSuccess, - "update-failure" to defaults.command.layout.set.updateFailure, - "update-no-change" to defaults.command.layout.set.updateNoChange - ) - ), - "reload" to mapOf( - "start" to defaults.command.reload.start, - "success" to defaults.command.reload.success, - "failure" to defaults.command.reload.failure - ) - ) - ) - target.applyMessageComments() - - loader.save(target) - path.writeCommentedYaml(::decorateMessagesYaml) - } - - private fun ensureMessageDefaults(path: Path) { - if (!Files.exists(path)) return - - val loader = createLoader(path) - val node = loader.load() - val defaults = MessageConfig() - var changed = false - - if (node.node("version").string == null || node.node("version").string == "1") { - node.node("version").set(defaults.version) - changed = true - } - - val reload = node.node("command", "reload") - listOf( - "start" to defaults.command.reload.start, - "success" to defaults.command.reload.success, - "failure" to defaults.command.reload.failure - ).forEach { (key, value) -> - if (reload.node(key).virtual()) { - reload.node(key).set(value) - changed = true - } - } - - if (changed) { - loader.save(node) - } - } - - private fun migratePlaceholder(path: Path) { - if (!Files.exists(path)) return - - val source = path.loadYaml() - if (source.node("current-date-format").virtual() && source.node("current-time-format").virtual()) return - - 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() - ) - } - ) - - loader.save(target) - } - - private fun migrateLayouts(layoutDirectory: Path) { - if (!Files.isDirectory(layoutDirectory)) return - - Files.list(layoutDirectory).use { paths -> - paths - .filter { it.isRegularFile() && it.extension == "yml" } - .forEach(::migrateLayout) - } - deleteGeneratedDefaultLayoutIfPublicExists(layoutDirectory) - } - - private fun migrateLayout(path: Path) { - val source = path.loadYaml() - if (!source.isLegacyLayout()) { - updateCurrentLayoutVersion(path, source) - return - } - - val legacyIconFile = source.node("server-icon").string - val loader = createLoader(path) - val target = loader.createNode() - val layoutName = path.name.removeSuffix(".yml") - val firstLines = source.node("first-lines").legacyLines(default = listOf("A simplecloud.app network")) - val secondLines = source.node("second-lines").legacyLines(default = listOf(" ")) - val entryCount = maxOf(firstLines.size, secondLines.size, 1) - val versionName = source.node("version-name").string.orEmpty() - val playerInfo = source.node("player-info").legacyLines(default = emptyList()) - val displayType = source.node("max-player-display-type").string ?: "REAL" - val dynamicPlayerRange = source.node("dynamic-player-range").int.takeIf { it > 0 } ?: 5 - - target.node("config-version").set(CURRENT_LAYOUT_VERSION) - target.node("motd").set( - mapOf( - "enabled" to source.node("enabled").booleanOr(true), - "update-type" to "RANDOM", - "layouts" to (0 until entryCount).map { index -> - mapOf( - "name" to layoutName, - "line1" to firstLines.getOrElse(index) { firstLines.lastOrNull().orEmpty() }, - "line2" to secondLines.getOrElse(index) { secondLines.lastOrNull().orEmpty() } - ) - } - ) - ) - target.node("server-icon").set( - mapOf( - "enabled" to false, - "file" to (legacyIconFile ?: "simplecloud.png") - ) - ) - target.node("player-list").set( - mapOf( - "enabled" to playerInfo.isNotEmpty(), - "player-list" to playerInfo - ) - ) - target.node("version").set( - mapOf( - "name" to mapOf( - "enabled" to versionName.isNotBlank(), - "text" to versionName.ifBlank { "Example" } - ), - "slots" to mapOf( - "enabled" to true, - "type" to displayType, - "fake-slots" to 100, - "dynamic-player-range" to dynamicPlayerRange - ) - ) - ) - target.applyLayoutComments() - - loader.save(target) - path.writeCommentedYaml(::decorateLayoutYaml) - } - - private fun updateCurrentLayoutVersion(path: Path, source: CommentedConfigurationNode) { - if (source.node("motd").virtual()) { - return - } - - var changed = false - if (source.node("config-version").string != CURRENT_LAYOUT_VERSION) { - source.node("config-version").set(CURRENT_LAYOUT_VERSION) - changed = true - } - changed = disableServerIconForPreviouslyMigratedLayout(source) || changed - if (!changed) return - - source.applyLayoutComments() - createLoader(path).save(source) - path.writeCommentedYaml(::decorateLayoutYaml) - } - - 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() - - if (looksLikePreviouslyMigratedLayout) { - serverIcon.node("enabled").set(false) - return true - } - return false - } - - private fun deleteGeneratedDefaultLayoutIfPublicExists(layoutDirectory: Path) { - val publicLayout = layoutDirectory.resolve("public.yml") - val defaultLayout = layoutDirectory.resolve("default.yml") - if (!Files.exists(publicLayout) || !Files.exists(defaultLayout)) return - - val defaultNode = defaultLayout.loadYaml() - val firstMotd = defaultNode.node("motd", "layouts").childrenList().firstOrNull() ?: return - val isGeneratedDefault = defaultNode.node("motd").virtual().not() && - firstMotd.node("name").string == "default" && - firstMotd.node("line1").string == "A simplecloud.app network" && - firstMotd.node("line2").string == " " - - if (isGeneratedDefault) { - deleteIfExists(defaultLayout) - } - } - - private fun CommentedConfigurationNode.isLegacyLayout(): Boolean { - val version = node("version").string ?: return false - return version == "1" && - !node("first-lines").virtual() && - node("motd").virtual() - } - - private fun CommentedConfigurationNode.legacyTextList(): String { - return legacyLines(default = emptyList()).joinToString("
") - } - - private fun CommentedConfigurationNode.legacyLines(default: List): List { - if (virtual()) return default - if (isList) return childrenList().map { it.string.orEmpty() } - return listOf(string.orEmpty()) - } - - private fun CommentedConfigurationNode.stringOr(default: String): String { - return string ?: default - } - - private fun CommentedConfigurationNode.booleanOr(default: Boolean): Boolean { - return if (virtual()) default else boolean - } - - private fun CommentedConfigurationNode.applyMainConfigComments() { - node("initial-state").comment(JOIN_STATES_COMMENT) - node("show-kick-reason").comment(SHOW_KICK_REASON_COMMENT) - node("whitelist").comment(WHITELIST_COMMENT) - node("whitelist", "players").comment("Supports player names and UUIDs.") - node("player-count").comment(PLAYER_COUNT_COMMENT) - node("player-count", "enabled").comment("Enables summed player counts for the server list and placeholders.") - node("player-count", "additional-groups").comment("Additional SimpleCloud groups included in the displayed player count.") - node("player-count", "additional-persistent-servers").comment("Persistent servers included in the displayed player count.") - node("player-count", "update-time").comment("Player count update interval in ticks. Set to 0 to disable automatic updates.") - node("tablist").comment(TABLIST_COMMENT) - node("tablist").childrenList().forEach { group -> - group.node("update-time").comment("Update interval in ticks.") - } - } - - private fun CommentedConfigurationNode.applyLayoutComments() { - node("motd").comment(SERVER_LIST_LAYOUT_COMMENT) - node("motd", "update-type").comment("QUEUE, RANDOM") - node("server-icon").comment("Server icon displayed in the multiplayer server list.") - node("player-list").comment("Text displayed when hovering over the player count in the server list") - node("version").comment("Server version text and player slots configuration") - node("version", "slots", "type").comment( - "REAL - Displays the actual number of online players\n" + - "FAKE - Displays a fixed custom number of online players\n" + - "DYNAMIC - Displays a dynamic number based on actual players plus a range" - ) - } - - private fun CommentedConfigurationNode.applyMessageComments() { - node("variables").comment(VARIABLES_COMMENT) - node("kick").comment(KICK_MESSAGES_COMMENT) - node("command").comment(COMMAND_MESSAGES_COMMENT) - } - - private fun defaultJoinStates(): List> { - return ProxyEssentialsConfig().joinstates.map { state -> - mapOf( - "name" to state.name, - "permission" to mapOf( - "join" to state.permission.join, - "full" to state.permission.full - ), - "forced-motd-layout" to state.forcedMotdLayout - ) - } - } - - private fun defaultTabList(): List> { - return ProxyEssentialsConfig().tablist.map { group -> - mapOf( - "name" to group.name, - "layout" to group.layout.map { layout -> - mapOf( - "header" to layout.header, - "footer" to layout.footer - ) - }, - "update-time" to group.updateTime - ) - } - } - - private fun defaultPlayerCount(): Map { - val playerCount = ProxyEssentialsConfig().playerCount - return mapOf( - "enabled" to playerCount.enabled, - "additional-groups" to playerCount.additionalGroups, - "additional-persistent-servers" to playerCount.additionalPersistentServers, - "update-time" to playerCount.updateTime - ) - } - - private fun Path.loadYaml(): CommentedConfigurationNode { - return createLoader(this).load() - } - - private fun deleteIfExists(path: Path) { - Files.deleteIfExists(path) - } - - private fun Path.writeCommentedYaml(decorate: (String) -> String) { - Files.writeString(this, decorate(Files.readString(this))) - } - - private fun decorateMainConfigYaml(content: String): String { - return content - .insertBefore("initial-state:", JOIN_STATES_YAML_COMMENT) - .insertBefore("show-kick-reason:", SHOW_KICK_REASON_YAML_COMMENT) - .insertBefore("whitelist:", WHITELIST_YAML_COMMENT) - .insertBefore(" players:", " # Supports player names and UUIDs.\n") - .insertBefore("player-count:", PLAYER_COUNT_YAML_COMMENT) - .insertBeforeInSection("player-count:", " enabled:", " # Enables summed player counts for the server list and placeholders.\n") - .insertBeforeInSection("player-count:", " additional-groups:", " # Additional SimpleCloud groups included in the displayed player count.\n") - .insertBeforeInSection("player-count:", " additional-persistent-servers:", " # Persistent servers included in the displayed player count.\n") - .insertBeforeInSection("player-count:", " update-time:", " # Player count update interval in ticks. Set to 0 to disable automatic updates.\n") - .insertBefore("tablist:", TABLIST_YAML_COMMENT) - .insertBeforeInSection("tablist:", " update-time:", " # Update interval in ticks.\n") - } - - private fun decorateLayoutYaml(content: String): String { - return content - .insertBefore("motd:", SERVER_LIST_LAYOUT_YAML_COMMENT) - .insertBefore(" update-type:", " # QUEUE, RANDOM\n") - .insertBefore("player-list:", "# Text displayed when hovering over the player count in the server list\n") - .insertBefore("version:", "# Server version text and player slots configuration\n") - .insertBefore(" type:", " # REAL - Displays the actual number of online players\n" + - " # FAKE - Displays a fixed custom number of online players\n" + - " # DYNAMIC - Displays a dynamic number based on actual players plus a range\n") - } - - private fun decorateMessagesYaml(content: String): String { - return content - .insertBefore("variables:", VARIABLES_YAML_COMMENT) - .insertBefore("kick:", KICK_MESSAGES_YAML_COMMENT) - .insertBefore("command:", COMMAND_MESSAGES_YAML_COMMENT) - } - - private fun String.insertBefore(linePrefix: String, comment: String): String { - if (comment.isEmpty() || contains(comment.trimEnd())) return this - val lines = lines().toMutableList() - val index = lines.indexOfFirst { it.startsWith(linePrefix) } - if (index == -1) return this - val commentLines = comment.trimEnd().lines() - lines.addAll(index, commentLines) - return lines.joinToString("\n").trimEnd() + "\n" - } - - private fun String.insertBeforeInSection(sectionPrefix: String, linePrefix: String, comment: String): String { - if (comment.isEmpty() || contains(comment.trimEnd())) return this - val lines = lines().toMutableList() - val sectionIndex = lines.indexOfFirst { it.startsWith(sectionPrefix) } - if (sectionIndex == -1) return this - - val relativeIndex = lines - .drop(sectionIndex + 1) - .indexOfFirst { it.startsWith(linePrefix) } - if (relativeIndex == -1) return this - - val commentLines = comment.trimEnd().lines() - lines.addAll(sectionIndex + 1 + relativeIndex, commentLines) - return lines.joinToString("\n").trimEnd() + "\n" - } - - private fun createLoader(path: Path): YamlConfigurationLoader { - return YamlConfigurationLoader.builder() - .path(path) - .nodeStyle(NodeStyle.BLOCK) - .defaultOptions { options -> - options.serializers { builder -> - builder.registerAnnotatedObjects(objectMapperFactory()) - } - } - .build() - } - - private const val JOIN_STATES_COMMENT = - "───────────────────────────────────────────────────────────────────────────────\n" + - "Join States\n" + - "Configures who can join the proxy and how full or restricted states behave.\n\n" + - "Read more @ https://docs.simplecloud.app/manual/plugins/proxy-essentials\n" + - "───────────────────────────────────────────────────────────────────────────────\n\n" + - "Initial settings used when a group is created for the first time." - - private const val WHITELIST_COMMENT = - "Global whitelist for direct player access.\n" + - "Prefer permission-based access for regular users.\n" + - "Use this list only for administrators or emergency access." - - private const val SHOW_KICK_REASON_COMMENT = - "Disconnects players with the backend server's kick reason instead of sending them to a fallback server.\n" + - "Disable to keep the proxy's standard fallback handling." - - private const val PLAYER_COUNT_COMMENT = - "───────────────────────────────────────────────────────────────────────────────\n" + - "Player Count\n" + - "Displays the summed player count for this proxy group and optional extra targets.\n\n" + - "Read more @ https://docs.simplecloud.app/manual/plugins/proxy-essentials\n" + - "───────────────────────────────────────────────────────────────────────────────" - - private const val TABLIST_COMMENT = - "───────────────────────────────────────────────────────────────────────────────\n" + - "Tablist\n" + - "Configures tablist layouts and update intervals for connected players.\n\n" + - "Read more @ https://docs.simplecloud.app/manual/plugins/proxy-essentials\n" + - "───────────────────────────────────────────────────────────────────────────────" - - private const val SERVER_LIST_LAYOUT_COMMENT = - "───────────────────────────────────────────────────────────────────────────────\n" + - "Server List Layout\n" + - "Configures how your server appears in the multiplayer server list,\n" + - "including MOTD, server icon, and player count display.\n\n" + - "Read more @ https://docs.simplecloud.app/manual/plugins/proxy-essentials\n" + - "───────────────────────────────────────────────────────────────────────────────" - - private const val VARIABLES_COMMENT = - "───────────────────────────────────────────────────────────────────────────────\n" + - "Variables\n" + - "Reusable variables that can be used throughout the messages.\n" + - "Usage: will be replaced with the defined value.\n" + - "───────────────────────────────────────────────────────────────────────────────" - - private const val KICK_MESSAGES_COMMENT = - "───────────────────────────────────────────────────────────────────────────────\n" + - "Kick Messages\n" + - "───────────────────────────────────────────────────────────────────────────────" - - private const val COMMAND_MESSAGES_COMMENT = - "───────────────────────────────────────────────────────────────────────────────\n" + - "Command Messages\n" + - "───────────────────────────────────────────────────────────────────────────────" - - private const val JOIN_STATES_YAML_COMMENT = - "\n# ───────────────────────────────────────────────────────────────────────────────\n" + - "# Join States\n" + - "# Configures who can join the proxy and how full or restricted states behave.\n" + - "#\n" + - "# Read more @ https://docs.simplecloud.app/manual/plugins/proxy-essentials\n" + - "# ───────────────────────────────────────────────────────────────────────────────\n" + - "\n" + - "# 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" + - "# Prefer permission-based access for regular users.\n" + - "# Use this list only for administrators or emergency access.\n" - - private const val SHOW_KICK_REASON_YAML_COMMENT = - "\n# Disconnects players with the backend server's kick reason instead of sending them to a fallback server.\n" + - "# Disable to keep the proxy's standard fallback handling.\n" - - private const val PLAYER_COUNT_YAML_COMMENT = - "\n# ───────────────────────────────────────────────────────────────────────────────\n" + - "# Player Count\n" + - "# Displays the summed player count for this proxy group and optional extra targets.\n" + - "#\n" + - "# Read more @ https://docs.simplecloud.app/manual/plugins/proxy-essentials\n" + - "# ───────────────────────────────────────────────────────────────────────────────\n" - - private const val TABLIST_YAML_COMMENT = - "\n# ───────────────────────────────────────────────────────────────────────────────\n" + - "# Tablist\n" + - "# Configures tablist layouts and update intervals for connected players.\n" + - "#\n" + - "# Read more @ https://docs.simplecloud.app/manual/plugins/proxy-essentials\n" + - "# ───────────────────────────────────────────────────────────────────────────────\n" - - private const val SERVER_LIST_LAYOUT_YAML_COMMENT = - "\n# ───────────────────────────────────────────────────────────────────────────────\n" + - "# Server List Layout\n" + - "# Configures how your server appears in the multiplayer server list,\n" + - "# including MOTD, server icon, and player count display.\n" + - "#\n" + - "# Read more @ https://docs.simplecloud.app/manual/plugins/proxy-essentials\n" + - "# ───────────────────────────────────────────────────────────────────────────────\n" - - private const val VARIABLES_YAML_COMMENT = - "\n# ───────────────────────────────────────────────────────────────────────────────\n" + - "# Variables\n" + - "# Reusable variables that can be used throughout the messages.\n" + - "# Usage: will be replaced with the defined value.\n" + - "# ───────────────────────────────────────────────────────────────────────────────\n" - - private const val KICK_MESSAGES_YAML_COMMENT = - "\n# ───────────────────────────────────────────────────────────────────────────────\n" + - "# Kick Messages\n" + - "# ───────────────────────────────────────────────────────────────────────────────\n" - - private const val COMMAND_MESSAGES_YAML_COMMENT = - "\n# ───────────────────────────────────────────────────────────────────────────────\n" + - "# Command Messages\n" + - "# ───────────────────────────────────────────────────────────────────────────────\n" -} 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..f268e92 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,52 +1,93 @@ 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.ConfigVersion import org.spongepowered.configurate.objectmapping.ConfigSerializable -import org.spongepowered.configurate.objectmapping.meta.Setting +import java.util.UUID @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" + override val version: Int = ConfigVersion.VERSION, + val initialState: String = "public", + val initialLayout: String = "public", + val showKickReason: Boolean = false, + 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" ), - "maintenance" + forcedMotdLayout = "maintenance" ) ), - val domains: List = listOf(), + val domains: List = emptyList(), 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 = listOf(TabListGroup()) +) : VersionedConfig + +@ConfigSerializable +data class JoinStateConfig( + val name: String = "", + val permission: JoinStatePermission = JoinStatePermission(), + val forcedMotdLayout: String? = null +) + +@ConfigSerializable +data class JoinStatePermission( + val join: String = "", + val full: String = "" +) + +@ConfigSerializable +data class DomainRoute( + val domain: String = "", + val target: String = "", + val rules: List = emptyList() +) + +@ConfigSerializable +data class DomainRule( + val state: String = "", + val layout: String = "" +) + +@ConfigSerializable +data class WhitelistConfig( + val enabled: Boolean = false, + val players: List = emptyList() ) { - fun tabListUpdateTimeMillis(): Long { - val ticks = tablist.minOfOrNull { it.updateTime } ?: 20L - return ticks * 50L - } - fun playerCountUpdateTimeMillis(): Long? { - if (!playerCount.enabled || playerCount.updateTime <= 0L) { - return null - } + fun contains(name: String, uuid: UUID): Boolean { + if (!enabled) return false - return playerCount.updateTime * 50L + val uuidString = uuid.toString() + return players.any { it.equals(name, ignoreCase = true) || it.equals(uuidString, ignoreCase = true) } } } + +@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 = "global", + val layout: List = listOf(TabListEntry()), + val updateTime: Long = 20L +) + +@ConfigSerializable +data class TabListEntry( + 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/CommandMessageConfig.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/message/CommandMessageConfig.kt deleted file mode 100644 index b2f8e64..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/message/CommandMessageConfig.kt +++ /dev/null @@ -1,93 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.config.message - -import org.spongepowered.configurate.objectmapping.ConfigSerializable -import org.spongepowered.configurate.objectmapping.meta.Setting - -private val PREFIX = "⚡ " - -@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." -) - -@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: " -) - -@ConfigSerializable -data class JoinStateHelpMessages( - val header: String = "${PREFIX}Commands of join state:", - val entry: String = " " -) - -@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 = " - " -) - -@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() -) - -@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 = " " -) - -@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() -) - -@ConfigSerializable -data class CommandMessages( - val reload: ReloadCommandMessages = ReloadCommandMessages(), - @Setting("join-state") val joinState: JoinStateCommandMessages = JoinStateCommandMessages(), - val layout: LayoutCommandMessages = LayoutCommandMessages() -) 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/migration/LegacyDirectoryMigrator.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/LegacyDirectoryMigrator.kt new file mode 100644 index 0000000..b1d7869 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/migration/LegacyDirectoryMigrator.kt @@ -0,0 +1,39 @@ +package app.simplecloud.plugin.proxy.shared.config.migration + +import org.slf4j.LoggerFactory +import java.nio.file.Files +import java.nio.file.Path + +object LegacyDirectoryMigrator { + + private val logger = LoggerFactory.getLogger(LegacyDirectoryMigrator::class.java) + private val LEGACY_DIRECTORY_NAMES = listOf("proxy-essentials-velocity", "proxy-essentials-bungeecord") + + fun migrate(dataDirectory: Path) { + val directory = dataDirectory.toAbsolutePath().normalize() + val pluginsDirectory = directory.parent ?: return + val legacyDirectory = LEGACY_DIRECTORY_NAMES + .map { pluginsDirectory.resolve(it) } + .find { it != directory && Files.isDirectory(it) } ?: return + + 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 + } + + try { + legacyDirectory.toFile().copyRecursively(directory.toFile(), overwrite = true) + } catch (e: Exception) { + 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 '${directory.fileName}'. " + + "Rename that directory in your template to keep editing your configuration." + ) + } +} 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 deleted file mode 100644 index 5102859..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/format/MotdMiniMessageFormatter.kt +++ /dev/null @@ -1,180 +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 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))) - } - - 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.insertion(null) - 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 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/CloudControllerHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/CloudControllerHandler.kt deleted file mode 100644 index 577315c..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/CloudControllerHandler.kt +++ /dev/null @@ -1,280 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.handler - -import app.simplecloud.api.persistentserver.PersistentServer -import app.simplecloud.api.server.Server -import app.simplecloud.api.server.ServerQuery -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -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 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()) { - logger.warning("Environment variable SIMPLECLOUD_UNIQUE_ID is not set.") - joinStateHandler.registerListener() - plugin.motdLayoutHandler.registerListener() - return - } - - scope.launch { - try { - currentServer = plugin.api.server().getServerById(serviceId).await() - logger.info("Initialized server: ${currentServer?.serverBase?.name}") - - joinStateHandler.registerListener() - plugin.motdLayoutHandler.registerListener() - initializeJoinState() - } catch (e: Exception) { - logger.severe("Error retrieving server by ID: ${e.message}") - joinStateHandler.registerListener() - plugin.motdLayoutHandler.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()) - } - } - - fun close() { - scope.cancel() - } - - 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.severe("Error updating server property: ${e.message}") - false - } - } - - suspend fun getGroupProperty(groupName: String, key: String): String? { - return try { - getGroupByName(groupName)?.properties?.get(key)?.toString()?.takeIf { it.isNotBlank() } - } 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 - } - } - - private suspend fun findGroupCaseInsensitive(groupName: String) = - plugin.api.group().allGroups.await().firstOrNull { it.name.equals(groupName, ignoreCase = true) } - - private suspend fun resolveGroupNameCasing(groupName: String): String = - getGroupByName(groupName)?.name ?: groupName - - suspend fun groupExists(groupName: String): Boolean { - return getGroupByName(groupName) != null - } - - 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}") - null - } - } - - private suspend fun findPersistentServerCaseInsensitive(name: String) = - plugin.api.persistentServer().allPersistentServers.await().firstOrNull { it.name.equals(name, ignoreCase = true) } - - suspend fun getPersistentServerProperty(name: String, key: String): String? { - return try { - getPersistentServerByName(name)?.properties?.get(key)?.toString()?.takeIf { it.isNotBlank() } - } catch (e: Exception) { - logger.severe("Error retrieving persistent server property: ${e.message}") - 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.severe("Error updating persistent server property: ${e.message}") - false - } - } - - suspend fun getAllPersistentServerNames(): List { - return try { - plugin.api.persistentServer().allPersistentServers.await().map { it.name } - } catch (e: Exception) { - logger.severe("Error retrieving persistent servers: ${e.message}") - emptyList() - } - } - - 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 { - return try { - getServersByGroup(groupName).sumOf { it.playerCount ?: 0 } - } catch (e: Exception) { - logger.severe("Error retrieving online players in group: ${e.message}") - 0 - } - } - - suspend fun getMaxPlayersInGroup(groupName: String): Int { - return getGroupByName(groupName)?.maxPlayers ?: 0 - } - - 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() - } - } - - 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) - } - } catch (e: Exception) { - logger.severe("Error retrieving server by name: ${e.message}") - 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 -> server.persistentServer?.name?.let { it in names } == true } - } catch (e: Exception) { - logger.severe("Error retrieving persistent servers by name: ${e.message}") - emptyList() - } - } -} 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 deleted file mode 100644 index 7db1d78..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/DomainMotdHandler.kt +++ /dev/null @@ -1,97 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.handler - -import app.simplecloud.plugin.proxy.shared.ProxyPlugin -import app.simplecloud.plugin.proxy.shared.config.domain.DomainMotdRoute -import kotlinx.coroutines.* -import java.util.concurrent.ConcurrentHashMap -import java.util.logging.Logger -import kotlin.time.Duration.Companion.milliseconds - -class DomainMotdHandler( - private val proxyPlugin: ProxyPlugin -) { - private val logger = Logger.getLogger(DomainMotdHandler::class.java.name) - 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.severe("Error while syncing domain MOTD routes: ${e.message}") - } - delay(3000.milliseconds) - } - } - } - - fun registerListener() { - proxyPlugin.api.event().server().onUpdated { event -> - val server = event.server - val matchingRoutes = proxyPlugin.proxyEssentialsConfig.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 = proxyPlugin.proxyEssentialsConfig.get().domains - val configuredDomains = routes.map { normalize(it.domain) }.toSet() - resolvedLayouts.keys.retainAll(configuredDomains) - - 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) - } - - 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) } - - if (ruleLayout != null) return ruleLayout - - val config = proxyPlugin.proxyEssentialsConfig.get() - val globalLayout = config.joinstates.find { it.name == state }?.forcedMotdLayout - ?.takeIf { hasLayout(it) } - - if (globalLayout != null) return globalLayout - - return config.initialLayout.takeIf { hasLayout(it) } - } - - private fun hasLayout(name: String): Boolean = proxyPlugin.motdLayoutHandler.getLayoutByName(name) != null - - private fun normalize(domain: String): String = domain.trim().lowercase() -} 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 641bead..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.state.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/LocalPingSourceMatcher.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/LocalPingSourceMatcher.kt deleted file mode 100644 index 5fb646c..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/LocalPingSourceMatcher.kt +++ /dev/null @@ -1,22 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.handler - -import java.net.InetAddress -import java.net.NetworkInterface - -class LocalPingSourceMatcher( - private val isLocalInterfaceAddress: (InetAddress) -> Boolean = { - NetworkInterface.getByInetAddress(it) != null - } -) { - fun isLocal(address: InetAddress?): Boolean { - if (address == null) { - return false - } - - if (address.isLoopbackAddress) { - return true - } - - return runCatching { isLocalInterfaceAddress(address) }.getOrDefault(false) - } -} 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 deleted file mode 100644 index 6ad34ef..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/MotdLayoutHandler.kt +++ /dev/null @@ -1,129 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.handler - -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 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.logging.Logger - -class MotdLayoutHandler( - private val directory: Path, - private val proxyPlugin: ProxyPlugin -) : YamlDirectoryRepository( - directory, - MotdLayoutConfiguration::class.java, -) { - private val logger = Logger.getLogger(MotdLayoutHandler::class.java.name) - private val queueIndexes = ConcurrentHashMap() - private val listenerRegistered = AtomicBoolean(false) - - @Volatile - private var localLayout: String? = null - - companion object { - const val MOTD_LAYOUT_KEY = "motd-layout" - } - - 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 - - val hasVersionSettings = lines - .drop(firstContentIndex + 1) - .any { it == "version:" } - - if (!hasVersionSettings) return content - - lines.removeAt(firstContentIndex) - if (firstContentIndex < lines.size && lines[firstContentIndex].isBlank()) { - lines.removeAt(firstContentIndex) - } - return lines.joinToString(System.lineSeparator()) - } - - fun loadMotdLayouts() { - load() - } - - 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 getCurrentMotdLayout(): MotdLayoutConfiguration { - val layoutName = localLayout?.takeIf(::hasLayout) ?: fallbackLayoutName() - return getMotdLayout(layoutName) - } - - fun setLocalLayout(layoutName: String?, logChange: Boolean = false) { - val nextLayout = layoutName?.takeIf { it.isNotBlank() } - if (localLayout == nextLayout) return - - localLayout = nextLayout - if (logChange) { - logger.info("MOTD layout changed to ${nextLayout ?: fallbackLayoutName()}") - } - } - - fun registerListener() { - if (!listenerRegistered.compareAndSet(false, true)) return - - proxyPlugin.api.event().server().onUpdated { event -> - val currentServer = proxyPlugin.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) - } - } - - 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 - 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] - } - } - } - - fun getAvailableLayouts(): List { - if (!Files.isDirectory(directory)) return emptyList() - - return Files.list(directory).use { paths -> - paths - .filter { Files.isRegularFile(it) && it.fileName.toString().endsWith(".yml") } - .map { it.fileName.toString().removeSuffix(".yml") } - .sorted() - .toList() - } - } - - private fun hasLayout(layoutName: String): Boolean { - return Files.isRegularFile(directory.resolve("${layoutName}.yml")) - } - - private fun String.isLayoutFileVersion(): Boolean { - val trimmed = trim() - return trimmed == "version: '1'" || trimmed == "version: \"1\"" || trimmed == "version: 1" - } -} 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 23a9cda..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.message.KickMessageConfig -import app.simplecloud.plugin.proxy.shared.config.state.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/ServerIconLoader.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/ServerIconLoader.kt deleted file mode 100644 index d600dfc..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/ServerIconLoader.kt +++ /dev/null @@ -1,105 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.handler - -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() - - 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 - ) - - cache[iconPath]?.takeIf { it.snapshot == snapshot }?.let { return it.icon } - - if (!Files.isRegularFile(iconPath)) { - cache[iconPath] = CacheEntry(snapshot, null) - warn("Server icon does not exist: ${iconPath.fileName}") - return null - } - - return runCatching { - val image = ImageIO.read(iconPath.toFile()) ?: error("Unsupported image format") - val favicon = createIcon(image.toFaviconImage()) - cache[iconPath] = CacheEntry(snapshot, favicon) - favicon - }.onFailure { - cache[iconPath] = CacheEntry(snapshot, null) - warn("Failed to load server icon ${iconPath.fileName}: ${it.message}") - }.getOrNull() - } - - 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 - - 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() - } - } - } - - private data class IconSnapshot( - val path: Path, - val lastModified: Long, - val length: Long - ) - - private data class CacheEntry( - val snapshot: IconSnapshot, - val icon: T? - ) - - private companion object { - const val FAVICON_SIZE = 64 - } -} 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 deleted file mode 100644 index ba1e1e8..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/TabListResolver.kt +++ /dev/null @@ -1,37 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.handler - -import app.simplecloud.plugin.proxy.shared.config.tablis.TabListGroup - -class TabListResolver( - private val getTabListGroups: () -> List -) { - private val tabListIndex = 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 - } - } - } - - 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 = tabListIndex[group.name] ?: 0 - val normalizedIndex = currentIndex % group.layout.size - val tabList = group.layout[normalizedIndex] - - return tabList.header to tabList.footer - } -} 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 4ef6088..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/JoinStateCommandHandler.kt +++ /dev/null @@ -1,231 +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 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: app.simplecloud.plugin.proxy.shared.config.message.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: app.simplecloud.plugin.proxy.shared.config.message.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 734a36f..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/command/ProxyEssentialsCommandHandler.kt +++ /dev/null @@ -1,87 +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 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.help") - .handler { context: CommandContext -> - val sender = context.sender() - - CoroutineScope(Dispatchers.IO).launch { - try { - proxyPlugin.messagesConfiguration.reload() - val messages = proxyPlugin.messagesConfiguration.get() - sender.sendMessage(messages.resolve(messages.command.reload.start)) - - proxyPlugin.proxyEssentialsConfig.reload() - proxyPlugin.placeHolderConfiguration.reload() - proxyPlugin.motdLayoutHandler.loadMotdLayouts() - - 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() - ) - } -} 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..6cda010 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinGate.kt @@ -0,0 +1,101 @@ +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.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.UUID +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.time.Duration.Companion.seconds + +class JoinGate( + private val api: CloudApi, + private val service: JoinStateService, + private val config: ConfigurationFactory, + private val messages: ConfigurationFactory, + private val scope: CoroutineScope +) { + + private val logger = LoggerFactory.getLogger(JoinGate::class.java) + private val proxyFull = AtomicBoolean(false) + + fun start() { + scope.launch { + while (isActive) { + try { + proxyFull.set(isProxyFull()) + } catch (e: Exception) { + logger.error("Could not check whether the proxy is full, keeping the last known value", e) + } + delay(2.seconds) + } + } + } + + 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 (proxyFull.get() && !hasPermission(joinState.permission.full)) return JoinResult.Denied(kick.networkFull) + return JoinResult.Allowed + } + + 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 state = service.getServerState(serverName) + if (state == null) { + logger.warn("The join states of the servers are not loaded yet, so $name cannot join '$serverName'") + return JoinResult.Denied(kick.noJoinState) + } + + val joinState = findJoinState(state) ?: 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 joinState = config.get().joinstates.find { it.name == name } + if (joinState == null) { + logger.error("The join state '$name' does not exist in config.yml, so nobody can join until it is added or the join state is changed.") + } + return joinState + } + + private fun hasJoinPermission(joinState: JoinStateConfig, hasPermission: (String) -> Boolean): Boolean { + val permission = joinState.permission.join + return permission.isBlank() || hasPermission(permission) + } + + private suspend fun isProxyFull(): Boolean { + val server = api.server().getServerById(SimpleCloudRuntime.serverId()).await() + val group = server.group + if (group == null) { + val maxPlayers = server.maxPlayers ?: 0 + return maxPlayers in 1..(server.playerCount ?: 0) + } + + val maxPlayers = group.maxPlayers ?: 0 + val onlinePlayers = api.server().getServersByGroup(group.name).await().sumOf { it.playerCount ?: 0 } + return maxPlayers in 1..onlinePlayers + } +} 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/JoinStateService.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateService.kt new file mode 100644 index 0000000..0845ada --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/joinstate/JoinStateService.kt @@ -0,0 +1,179 @@ +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.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 java.util.concurrent.atomic.AtomicReference +import kotlin.time.Duration.Companion.seconds + +class JoinStateService( + private val api: CloudApi, + private val config: ConfigurationFactory, + private val scope: CoroutineScope +) { + + private val logger = LoggerFactory.getLogger(JoinStateService::class.java) + private val localState = AtomicReference() + private val lastGroupState = AtomicReference() + private val serverStates = AtomicReference?>() + + fun getLocalState(): String = localState.get() ?: config.get().initialState + + suspend fun start() { + val server = api.server().getServerById(SimpleCloudRuntime.serverId()).await() + localState.set(readState(server.properties) ?: applyDefaultState(server)) + + api.event().server().onUpdated { event -> + if (event.serverId != server.serverId) return@onUpdated + scope.launch { updateLocalState(event.server) } + } + + 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) + } + } + } + + suspend fun getGroupState(groupName: String): String = + readState(api.group().getGroupByName(groupName).await().properties) ?: config.get().initialState + + suspend fun getServiceState(groupName: String, numericalId: Int): String { + val service = api.server().getServerByNumericalId(groupName, numericalId).await() + return readState(service.properties) ?: getGroupState(groupName) + } + + suspend fun getPersistentServerState(name: String): String = + readState(api.persistentServer().getPersistentServerByName(name).await().properties) ?: config.get().initialState + + suspend fun getTargetState(name: String): String { + if (isGroup(name)) return getGroupState(name) + return getPersistentServerState(name) + } + + fun startServerStateSync() { + scope.launch { + while (isActive) { + try { + refreshServerStates() + } catch (e: Exception) { + logger.error("Could not load the join states of the servers, keeping the last known states", e) + } + delay(2.seconds) + } + } + } + + fun getServerState(serverName: String): String? { + val states = serverStates.get() ?: return null + return states[serverName] ?: config.get().initialState + } + + 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 setServiceState(groupName: String, numericalId: Int, state: String) { + val service = api.server().getServerByNumericalId(groupName, numericalId).await() + setServerState(service.serverId, state) + } + + 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) } + } + + suspend fun isGroup(name: String): Boolean = api.group().allGroups.await().any { it.name == name } + + private suspend fun setServerState(serverId: String, state: String) { + api.server().updateServerProperty(serverId, KEY, state).await() + } + + 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 + } + + if (localState.getAndSet(state) != state) { + logger.info("Join state changed to '$state'") + } + } + + 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 + } + + setServerState(server.serverId, state) + return state + } + + 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 + + setServerState(serverId, groupState) + } + + private suspend fun refreshServerStates() { + val groupStates = api.group().allGroups.await().associate { it.name to readState(it.properties) } + val persistentServerStates = api.persistentServer().allPersistentServers.await().associate { it.name to readState(it.properties) } + val states = api.server().allServers.await().associate { getServerName(it) to resolveState(it, groupStates, persistentServerStates) } + serverStates.set(states) + } + + private fun resolveState(server: Server, groupStates: Map, persistentServerStates: Map): String { + val state = readState(server.properties) + if (state != null) return state + + val groupName = server.group?.name + if (groupName != null) return groupStates[groupName] ?: config.get().initialState + + val persistentServerName = server.persistentServer?.name ?: return config.get().initialState + return persistentServerStates[persistentServerName] ?: config.get().initialState + } + + private fun readState(properties: Map?): String? { + val state = properties?.get(KEY)?.toString() + if (state.isNullOrBlank()) return null + return state + } + + 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/layout/LayoutRepository.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/layout/LayoutRepository.kt new file mode 100644 index 0000000..3469931 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/layout/LayoutRepository.kt @@ -0,0 +1,46 @@ +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.* + +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() + } + + 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}'", 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..a15b43f --- /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 service: 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 == service.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 = service.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/platform/ProxyPlatform.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/platform/ProxyPlatform.kt new file mode 100644 index 0000000..9e6eb6b --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/platform/ProxyPlatform.kt @@ -0,0 +1,14 @@ +package app.simplecloud.plugin.proxy.shared.platform + +import java.nio.file.Path + +interface ProxyPlatform { + + fun getDataDirectory(): Path + + fun getOnlinePlayers(): Int + + fun getMaxPlayers(): Int + + fun getPlayers(): List +} \ No newline at end of file diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/platform/ProxyPlayer.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/platform/ProxyPlayer.kt new file mode 100644 index 0000000..4d794ad --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/platform/ProxyPlayer.kt @@ -0,0 +1,12 @@ +package app.simplecloud.plugin.proxy.shared.platform + +import net.kyori.adventure.text.Component + +interface ProxyPlayer { + + fun getServerName(): String? + + fun getPing(): Long + + fun sendPlayerListHeaderAndFooter(header: Component, footer: Component) +} 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..1c58b80 --- /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.platform.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 { + updateCounts(config) + } catch (e: Exception) { + logger.error("Could not count the players of the network, showing the last known count", e) + } + } + + private suspend fun updateCounts(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/resolver/TagResolverHelper.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/resolver/TagResolverHelper.kt deleted file mode 100644 index 2b179ed..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/resolver/TagResolverHelper.kt +++ /dev/null @@ -1,81 +0,0 @@ -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 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( - 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 { - val now = LocalDateTime.now(ZoneId.systemDefault()) - return Formatter.date(TagResolverNames.DATE, now) - } - - fun getMaxPlayersTagResolver( - onlinePlayers: Int, - realMaxPlayers: Int, - motdConfiguration: MotdLayoutConfiguration - ): TagResolver { - val slots = motdConfiguration.versionSettings.slots - val maxPlayers = if (!slots.enabled) realMaxPlayers else when (slots.type) { - MaxPlayerDisplayType.REAL -> realMaxPlayers - MaxPlayerDisplayType.FAKE -> slots.fakeSlots - MaxPlayerDisplayType.DYNAMIC -> onlinePlayers + slots.dynamicPlayerRange - } - 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 - - Tag.preProcessParsed(env) - } - - } - -} 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/resolver/TagResolverNames.kt deleted file mode 100644 index dfc18e2..0000000 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/resolver/TagResolverNames.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.simplecloud.plugin.proxy.shared.resolver - -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/tablist/TabListService.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/tablist/TabListService.kt new file mode 100644 index 0000000..e3acb34 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/tablist/TabListService.kt @@ -0,0 +1,70 @@ +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 +import app.simplecloud.plugin.proxy.shared.platform.ProxyPlatform +import app.simplecloud.plugin.proxy.shared.platform.ProxyPlayer +import app.simplecloud.plugin.proxy.shared.utilities.text.MessageFormatter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.slf4j.LoggerFactory +import kotlin.time.Duration.Companion.milliseconds + +class TabListService( + private val platform: ProxyPlatform, + private val config: ConfigurationFactory, + private val formatter: MessageFormatter, + private val scope: CoroutineScope +) { + + private val logger = LoggerFactory.getLogger(TabListService::class.java) + + fun start() { + scope.launch { + while (isActive) { + val interval = getUpdateIntervalMillis() + delay((interval ?: 1000L).milliseconds) + if (interval == null) continue + + try { + platform.getPlayers().forEach { update(it) } + } catch (e: Exception) { + logger.error("Could not update the tablist of the online players", e) + } + } + } + } + + fun update(player: ProxyPlayer) { + val serverName = player.getServerName() ?: return + val tabList = getTabList(serverName) ?: return + + player.sendPlayerListHeaderAndFooter( + formatter.formatForPlayer(tabList.header, serverName, player.getPing()), + formatter.formatForPlayer(tabList.footer, serverName, player.getPing()) + ) + } + + private 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 * 50L) + return group.layout[timeSlot.mod(group.layout.size)] + } + + private fun getUpdateIntervalMillis(): Long? { + val updateTime = config.get().tablist.map { it.updateTime }.filter { it > 0 }.minOrNull() ?: return null + return updateTime * 50L + } + +} 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/utilities/CommandPermissions.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/CommandPermissions.kt new file mode 100644 index 0000000..903e7e8 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/CommandPermissions.kt @@ -0,0 +1,15 @@ +package app.simplecloud.plugin.proxy.shared.utilities + +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" + 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/ConfigVersion.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/ConfigVersion.kt new file mode 100644 index 0000000..ebb2df4 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/ConfigVersion.kt @@ -0,0 +1,6 @@ +package app.simplecloud.plugin.proxy.shared.utilities + +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/LocalPingSourceMatcher.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/LocalPingSourceMatcher.kt new file mode 100644 index 0000000..7699ac4 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/LocalPingSourceMatcher.kt @@ -0,0 +1,23 @@ +package app.simplecloud.plugin.proxy.shared.utilities + +import java.net.InetAddress +import java.net.NetworkInterface + +object LocalPingSourceMatcher { + + fun isLocal(address: InetAddress?): Boolean { + if (address == null) { + return false + } + + if (address.isLoopbackAddress) { + return true + } + + 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/text/MessageFormatter.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/text/MessageFormatter.kt new file mode 100644 index 0000000..7a5e8e4 --- /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 = + MotdFormatter.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/text/MinecraftTextWidth.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/text/MinecraftTextWidth.kt new file mode 100644 index 0000000..7f067b2 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/text/MinecraftTextWidth.kt @@ -0,0 +1,115 @@ +package app.simplecloud.plugin.proxy.shared.utilities.text + +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 = "/widths" + 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 + 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() } + + 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(): ByteArray { + val advances = openResource(UNIFONT_ADVANCES_RESOURCE).use { it.readBytes() } + + check(advances.size == UNICODE_CODE_POINT_COUNT) { + "Invalid font metrics resource $UNIFONT_ADVANCES_RESOURCE: 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/text/MotdFormatter.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/text/MotdFormatter.kt new file mode 100644 index 0000000..d87482a --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/utilities/text/MotdFormatter.kt @@ -0,0 +1,71 @@ +package app.simplecloud.plugin.proxy.shared.utilities.text + +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 MotdFormatter { + + 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/resources/config/config.yml b/proxy-shared/src/main/resources/config/config.yml similarity index 82% rename from proxy-velocity/src/main/resources/config/config.yml rename to proxy-shared/src/main/resources/config/config.yml index 532e92a..55abeb8 100644 --- a/proxy-velocity/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-bungeecord/src/main/resources/config/layout/public.yml b/proxy-shared/src/main/resources/config/layout/public.yml similarity index 90% rename from proxy-bungeecord/src/main/resources/config/layout/public.yml rename to proxy-shared/src/main/resources/config/layout/public.yml index 7aaa1f8..6d43d27 100644 --- a/proxy-bungeecord/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-velocity/src/main/resources/config/messages.yml b/proxy-shared/src/main/resources/config/messages.yml similarity index 71% rename from proxy-velocity/src/main/resources/config/messages.yml rename to proxy-shared/src/main/resources/config/messages.yml index ffb2e37..da24a41 100644 --- a/proxy-velocity/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