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