diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..966a055 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,32 @@ +name: Build Pull Request + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + +jobs: + build: + name: Build + 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 with Gradle + run: ./gradlew clean build shadowJar \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 857c22e..3da3a76 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Create Release with ShadowJars +name: Release and Publish on: push: @@ -8,7 +8,7 @@ on: jobs: build: - name: Build ShadowJars and Create Release + name: Build and Publish runs-on: ubuntu-latest steps: @@ -18,32 +18,33 @@ jobs: ref: main submodules: recursive - - name: Set up JDK 25 - uses: actions/setup-java@v5 + - 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 + with: + cache-disabled: true + - name: Make gradlew executable run: chmod +x ./gradlew - - name: Print Gradle Wrapper Properties - run: cat gradle/wrapper/gradle-wrapper.properties - - - name: Clear Gradle Cache - run: rm -rf ~/.gradle/caches && rm -rf ~/.gradle/wrapper - - name: Build ShadowJars run: ./gradlew clean build shadowJar + - name: Publish to SimpleCloud Repository + run: ./gradlew publish + env: + SIMPLECLOUD_USERNAME: ${{ secrets.SIMPLECLOUD_USERNAME }} + SIMPLECLOUD_PASSWORD: ${{ secrets.SIMPLECLOUD_PASSWORD }} + - 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: @@ -52,9 +53,7 @@ jobs: - name: Create Release run: | jars=() - # Find JAR files in any submodule's build/libs directory for jar in $(find . -type f -name "*.jar" -path "*/build/libs/*.jar" -not -path "./build/libs/*"); do - # Check if the filename contains a version number (e.g., a dash followed by numbers) if [[ $(basename "$jar") =~ -[0-9]+\.[0-9]+ ]]; then echo "Skipping $jar due to version number" else diff --git a/README.md b/README.md index 7efb899..0fbea5f 100644 --- a/README.md +++ b/README.md @@ -25,16 +25,17 @@ > All information about this project can be found in our detailed [documentation][docs-thisproject]. -The Prefixes Plugin provides comprehensive rank management for chat, tab list and name tag displays. Groups are either defined in the plugin config or read from LuckPerms, and within a SimpleCloud network chat messages and tab list entries can be synced across servers. +A Paper plugin and Minestom library for prefixes, suffixes and name colors in chat, the tab list and name tags. Groups come from a simple config or straight from LuckPerms, and on a SimpleCloud network chat and tab list can be shared across servers. ## Features -- [x] **LuckPerms Integration**: Use the built-in config groups or let the plugin convert your LuckPerms groups into prefix groups. -- [x] **Chat, Tab List & Name Tags**: Prefixes, suffixes, colors and display names are applied to chat messages, the tab list and player name tags. -- [x] **Cross-Server Sync**: Share chat messages and tab list entries between all servers, selected server groups or persistent servers of your network. -- [x] **MiniMessage Formats**: Prefixes, suffixes, display names and chat formats are fully customizable with placeholder support. -- [x] **Supported Server Software**: Supports Paper and Forks. -- [x] **Quick Setup**: Easy installation process for all supported software. +- [x] **Chat, Tab List & Name Tags**: Prefixes, suffixes and colors everywhere. +- [x] **Config or LuckPerms**: Define groups in the config or use your LuckPerms groups. +- [x] **Sorted Tab List**: Players are ordered by group priority. +- [x] **Network Sync**: Share chat and tab list across your SimpleCloud network. +- [x] **MiniMessage Formats**: Fully customizable with placeholders. +- [x] **Developer API**: Set prefixes per player or even per viewer, e.g. for teams or friends. +- [x] **Supported Software**: Paper(and forks) and Minestom as a Library. ## Dependency @@ -68,7 +69,7 @@ Clone the repository with its pinned Custom Names source before building: ```shell git clone --recurse-submodules https://github.com/simplecloudapp/prefixes-plugin.git cd prefixes-plugin -bash ./gradlew build +./gradlew build ``` ## License diff --git a/build.gradle.kts b/build.gradle.kts index 5467b91..28cd48d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,19 +2,25 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinVersion plugins { + id("maven-publish") alias(libs.plugins.kotlin) alias(libs.plugins.shadow) } +check(file("custom-names/custom-names-api/src/main/kotlin").isDirectory) { + "The custom-names submodule is missing, run 'git submodule update --init' first" +} + allprojects { group = "app.simplecloud.plugin" - version = "1.0.3" + version = "1.1.0" repositories { mavenCentral() - maven("https://repo.papermc.io/repository/maven-public/") maven("https://oss.sonatype.org/content/repositories/snapshots") maven("https://oss.sonatype.org/content/repositories/central") + maven("https://repo.papermc.io/repository/maven-public/") + maven("https://repo.spectr.is/snapshots") maven("https://repo.simplecloud.app/snapshots") maven("https://buf.build/gen/maven") } @@ -24,6 +30,7 @@ subprojects { apply { plugin("org.jetbrains.kotlin.jvm") plugin("com.gradleup.shadow") + plugin("maven-publish") } dependencies { @@ -31,7 +38,6 @@ subprojects { testImplementation(rootProject.libs.kotlin.test) compileOnly(rootProject.libs.luckperms.api) - compileOnly(rootProject.libs.simplecloud.api) } kotlin { @@ -53,7 +59,9 @@ subprojects { tasks.shadowJar { mergeServiceFiles() + duplicatesStrategy = DuplicatesStrategy.INCLUDE archiveFileName.set("${project.name}.jar") + exclude("google/protobuf/**", "src/google/protobuf/**") } tasks.processResources { @@ -62,4 +70,23 @@ subprojects { ) } + publishing { + publications { + create("maven") { + from(components["java"]) + } + } + + repositories { + maven { + name = "simplecloud" + url = uri("https://repo.simplecloud.app/snapshots") + credentials { + username = System.getenv("SIMPLECLOUD_USERNAME") + password = System.getenv("SIMPLECLOUD_PASSWORD") + } + } + } + } + } \ No newline at end of file diff --git a/custom-names b/custom-names index ff3c7bb..6dfdd93 160000 --- a/custom-names +++ b/custom-names @@ -1 +1 @@ -Subproject commit ff3c7bb66bc8fa86c058b51641bf0452866ebf16 +Subproject commit 6dfdd937754769942ab9a9312c6ea191389f9e0d diff --git a/gradle.properties b/gradle.properties index 7fc6f1f..5f59712 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,5 @@ kotlin.code.style=official +org.gradle.configuration-cache=true +org.gradle.caching=true +org.gradle.parallel=true +org.gradle.tooling.parallel=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 137b55c..dc78423 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,47 +1,51 @@ [versions] -kotlin = "2.4.10" +kotlin = "2.4.21" kotlinx-coroutines = "1.11.0" -shadow = "9.5.1" +shadow = "9.6.1" minotaur = "2.9.0" -paperweight = "2.0.0-beta.21" +paperweight = "2.0.0-beta.24" protobuf = "0.10.0" -simplecloud-api = "0.1.0-platform.40" simplecloud-plugin-api = "0.0.1-platform.1778966459069-b1e45e2" paper = "26.3.build.+" +minestom = "26_3-SNAPSHOT" adventure = "5.2.0" luckperms = "5.5" reflection-remapper = "0.1.3" -cloud-command = "2.0.0" -cloud-command-platform = "2.0.0" +cloud-command = "2.1.0" +cloud-command-platform = "2.0.1" +cloud-command-minestom = "2.2.0-SNAPSHOT" configurate = "4.2.0" -protoc = "4.35.1" -protobuf-kotlin = "4.35.1" +protoc = "4.36.2" +protobuf-kotlin = "4.36.2" -jnats = "2.26.0" +jnats = "2.26.3" +slf4j = "2.0.20" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } -simplecloud-api = { module = "app.simplecloud.api:api", version.ref = "simplecloud-api" } simplecloud-plugin-api = { module = "app.simplecloud.plugin:plugin-shared", version.ref = "simplecloud-plugin-api" } adventure-api = { module = "net.kyori:adventure-api", version.ref = "adventure" } adventure-text-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure" } adventure-text-serializer-gson = { module = "net.kyori:adventure-text-serializer-gson", version.ref = "adventure" } +adventure-text-serializer-legacy = { module = "net.kyori:adventure-text-serializer-legacy", version.ref = "adventure" } cloud-command-core = { module = "org.incendo:cloud-core", version.ref = "cloud-command" } cloud-command-coroutines = { module = "org.incendo:cloud-kotlin-coroutines", version.ref = "cloud-command" } cloud-command-paper = { module = "org.incendo:cloud-paper", version.ref = "cloud-command-platform" } +cloud-command-minestom = { module = "org.incendo:cloud-minestom", version.ref = "cloud-command-minestom" } paper-api = { module = "io.papermc.paper:paper-api", version.ref = "paper" } +minestom = { module = "net.minestom:minestom", version.ref = "minestom" } luckperms-api = { module = "net.luckperms:api", version.ref = "luckperms" } reflection-remapper = { module = "xyz.jpenilla:reflection-remapper", version.ref = "reflection-remapper" } @@ -50,13 +54,14 @@ configurate-yaml = { module = "org.spongepowered:configurate-yaml", version.ref configurate-kotlin = { module = "org.spongepowered:configurate-extra-kotlin", version.ref = "configurate" } protoc = { module = "com.google.protobuf:protoc", version.ref = "protoc" } -protobuf-kotlin = { module = "com.google.protobuf:protobuf-kotlin", version.ref = "protobuf-kotlin" } +protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf-kotlin" } jnats = { module = "io.nats:jnats", version.ref = "jnats" } +slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } [bundles] configurate = ["configurate-kotlin", "configurate-yaml"] -adventure = ["adventure-api", "adventure-text-minimessage", "adventure-text-serializer-gson"] +adventure = ["adventure-api", "adventure-text-minimessage", "adventure-text-serializer-gson", "adventure-text-serializer-legacy"] cloud-command = ["cloud-command-core", "cloud-command-coroutines"] [plugins] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 53761cb..b2f8f63 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists \ No newline at end of file diff --git a/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/PrefixesApi.kt b/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/PrefixesApi.kt index cece107..dd02b8c 100644 --- a/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/PrefixesApi.kt +++ b/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/PrefixesApi.kt @@ -1,38 +1,106 @@ package app.simplecloud.prefixes.api +import app.simplecloud.prefixes.api.group.GroupProvider import app.simplecloud.prefixes.api.group.PrefixesGroup import app.simplecloud.prefixes.api.group.PrefixesPlayerData +import net.kyori.adventure.audience.Audience +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.TextColor import java.util.UUID import java.util.concurrent.CompletableFuture /** - * Main entrypoint to interacting with prefixes. + * Entrypoint to interacting with prefixes. */ interface PrefixesApi { - /** - * Returns all registered [PrefixesGroup] ordered by priority. - */ - fun getGroups(): Collection + /** Returns all registered groups ordered by priority. */ + fun getGroups(): CompletableFuture> - /** - * Returns the highest-priority applicable [PrefixesGroup] of a player. - * @param id Target player UUID - */ + /** Returns the primary group of the player [id], or null if none matches. */ fun getPrimaryGroup(id: UUID): CompletableFuture - /** - * Returns the prefix data for a player. - * @param id Target player UUID - */ + /** Returns the prefix data everyone sees for the player [id]. */ fun getPrefixData(id: UUID): CompletableFuture - /** - * Adds a new group to the group provider. - * - * @param group The group to add - * @return A future completing with `true` if the group was created, or `false` if a group with that name already exists. - */ + /** Returns the prefix data [viewer] sees for the player [id]. */ + fun getPrefixData(id: UUID, viewer: Audience): CompletableFuture + + /** Sets the [prefix] of the player [id] for everyone. */ + fun setPrefix(id: UUID, prefix: Component) + + /** Sets the [prefix] of the player [id] for [viewers]. */ + fun setPrefix(id: UUID, prefix: Component, viewers: Audience) + + /** Sets the [suffix] of the player [id] for everyone. */ + fun setSuffix(id: UUID, suffix: Component) + + /** Sets the [suffix] of the player [id] for [viewers]. */ + fun setSuffix(id: UUID, suffix: Component, viewers: Audience) + + /** Sets the name [color] of the player [id] for everyone. */ + fun setColor(id: UUID, color: TextColor) + + /** Sets the name [color] of the player [id] for [viewers]. */ + fun setColor(id: UUID, color: TextColor, viewers: Audience) + + /** Sets the tablist [priority] of the player [id] for everyone, higher is listed further up. */ + fun setPriority(id: UUID, priority: Int) + + /** Sets the tablist [priority] of the player [id] for [viewers]. */ + fun setPriority(id: UUID, priority: Int, viewers: Audience) + + /** Shows the player [id] with the group named [group] for everyone, completes with `false` if it does not exist. */ + fun setGroup(id: UUID, group: String): CompletableFuture + + /** Shows the player [id] with the group named [group] to [viewers], completes with `false` if it does not exist. */ + fun setGroup(id: UUID, group: String, viewers: Audience): CompletableFuture + + /** Shows all players in [targets] with the group named [group] for everyone, completes with `false` if it does not exist. */ + fun setGroup(targets: Audience, group: String): CompletableFuture + + /** Shows all players in [targets] with the group named [group] to [viewers], completes with `false` if it does not exist. */ + fun setGroup(targets: Audience, group: String, viewers: Audience): CompletableFuture + + /** Shows the player [id] with [group] for everyone. */ + fun setGroup(id: UUID, group: PrefixesGroup) + + /** Shows the player [id] with [group] to [viewers]. */ + fun setGroup(id: UUID, group: PrefixesGroup, viewers: Audience) + + /** Shows all players in [targets] with [group] for everyone. */ + fun setGroup(targets: Audience, group: PrefixesGroup) + + /** Shows all players in [targets] with [group] to [viewers]. */ + fun setGroup(targets: Audience, group: PrefixesGroup, viewers: Audience) + + /** Removes all manually set values of the player [id]. */ + fun reset(id: UUID) + + /** Removes all manually set values of the players in [targets]. */ + fun reset(targets: Audience) + + /** Removes the values set for [viewers] from the players in [targets]. */ + fun reset(targets: Audience, viewers: Audience) + + /** Adds [group] to the current group provider, completes with `false` if it already exists. */ fun addGroup(group: PrefixesGroup): CompletableFuture + /** Registers [provider] under its name. */ + fun registerGroupProvider(provider: GroupProvider) + + /** Removes the group provider named [name]. */ + fun unregisterGroupProvider(name: String) + + /** Returns the group provider currently used. */ + fun getGroupProvider(): GroupProvider + + /** Returns all registered group providers. */ + fun getGroupProviders(): Collection + + /** Reloads the group of the player [id], manually set values stay. */ + fun refreshPlayer(id: UUID) + + /** Reloads the groups of all players on this server, manually set values stay. */ + fun refreshAll() } diff --git a/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/PrefixesApiProvider.kt b/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/PrefixesApiProvider.kt index ccfcb58..3e6b11f 100644 --- a/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/PrefixesApiProvider.kt +++ b/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/PrefixesApiProvider.kt @@ -7,13 +7,13 @@ import org.jetbrains.annotations.ApiStatus */ object PrefixesApiProvider { + @Volatile private var instance: PrefixesApi? = null /** - * Gets the registered [PrefixesApi] instance. + * Returns the [PrefixesApi] instance. * - * @return The prefixes API instance - * @throws IllegalStateException If the API is not loaded yet + * @throws IllegalStateException if prefixes is not loaded yet */ @JvmStatic fun get(): PrefixesApi { diff --git a/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/group/GroupProvider.kt b/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/group/GroupProvider.kt index 6f296c4..914b3f3 100644 --- a/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/group/GroupProvider.kt +++ b/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/group/GroupProvider.kt @@ -4,33 +4,19 @@ import java.util.UUID import java.util.concurrent.CompletableFuture /** - * Represents a source provider for groups. + * A source provider of prefix groups, e.g. the config or LuckPerms. */ interface GroupProvider { - /** - * The unique identifier name of this provider. - */ - val name: String + /** Returns the name of this provider. */ + fun getName(): String - /** - * Returns all registered prefix groups. - */ - fun getGroups(): Collection + /** Returns all groups of this provider. */ + fun getGroups(): CompletableFuture> - /** - * Gets the highest-priority applicable group of a player. - * - * @param id The player's UUID - * @return A future completing with the player's highest-priority group, or null if none matches. - */ + /** Returns the primary group of the player [id], or null if none matches. */ fun getGroup(id: UUID): CompletableFuture - /** - * Adds a new group to this provider's source. - * - * @param group The group to add - * @return A future completing with `true` if the group was created, or `false` if a group with that name already exists. - */ + /** Adds [group] to this provider, completes with `false` if it already exists. */ fun addGroup(group: PrefixesGroup): CompletableFuture } diff --git a/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/group/PrefixesGroup.kt b/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/group/PrefixesGroup.kt index 281f359..68f549e 100644 --- a/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/group/PrefixesGroup.kt +++ b/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/group/PrefixesGroup.kt @@ -10,60 +10,30 @@ import java.util.concurrent.CompletableFuture */ interface PrefixesGroup { - /** - * The name of this group. - */ + /** The name of this group. */ val name: String - /** - * The priority of this group (higher priority values override lower ones). - */ + /** The priority of this group. */ val priority: Int - /** - * The permission required for this group, or an empty string if none. - */ + /** The permission required for this group, or an empty string if none. */ val permission: String - /** - * The prefix of this group. - */ - val prefix: Component? + /** The prefix of this group. */ + val prefix: Component - /** - * The suffix of this group. - */ - val suffix: Component? + /** The suffix of this group. */ + val suffix: Component - /** - * The primary color of this group. - */ - val color: TextColor? + /** The name color of this group. */ + val color: TextColor - /** - * The display name format of this group. - */ + /** The display name of this group. */ val displayName: String - /** - * The chat format of this group. - */ + /** The chat format of this group. */ val chatFormat: String - /** - * Checks whether the specified player is a member of this group. - * - * @param id the UUID of the player - * @return `true` if the player belongs to this group, otherwise `false` - */ - fun containsPlayer(id: UUID): Boolean - - /** - * Checks asynchronously whether the specified player is a member of this group. - * - * @param id the UUID of the player - * @return a future completing with `true` if the player belongs to this group, - * otherwise `false` - */ - fun containsPlayerAsync(id: UUID): CompletableFuture -} \ No newline at end of file + /** Returns whether the player [id] is a member of this group. */ + fun containsPlayer(id: UUID): CompletableFuture +} diff --git a/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/group/PrefixesPlayerData.kt b/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/group/PrefixesPlayerData.kt index bd55687..ad92fac 100644 --- a/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/group/PrefixesPlayerData.kt +++ b/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/group/PrefixesPlayerData.kt @@ -4,7 +4,7 @@ import net.kyori.adventure.text.Component import net.kyori.adventure.text.format.TextColor /** - * Represents the prefixes data of a player. + * The prefix data from a player. */ data class PrefixesPlayerData( val prefix: Component, @@ -13,4 +13,4 @@ data class PrefixesPlayerData( val displayName: Component, val chatFormat: String, val priority: Int -) \ No newline at end of file +) diff --git a/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/util/ColorParser.kt b/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/util/ColorParser.kt deleted file mode 100644 index edafd37..0000000 --- a/prefixes-api/src/main/kotlin/app/simplecloud/prefixes/api/util/ColorParser.kt +++ /dev/null @@ -1,23 +0,0 @@ -package app.simplecloud.prefixes.api.util - -import net.kyori.adventure.text.format.TextColor -import net.kyori.adventure.text.minimessage.MiniMessage - -/** - * Parses a color from a MiniMessage tag or a plain hex string. - * - * @param color The color to parse - * @return The parsed color, or `null` if [color] is empty or holds no color - */ -fun parseColor(color: String): TextColor? { - if (color.isEmpty()) return null - val trimmed = color.trim() - if (trimmed.startsWith("#")) { - return TextColor.fromHexString(trimmed) - } - if (trimmed.startsWith("<#") && trimmed.endsWith(">")) { - return TextColor.fromHexString(trimmed.substring(1, trimmed.length - 1)) - } - val parsed = MiniMessage.miniMessage().deserialize(trimmed) - return parsed.color() ?: parsed.style().color() -} diff --git a/prefixes-minestom/build.gradle.kts b/prefixes-minestom/build.gradle.kts new file mode 100644 index 0000000..d477e9a --- /dev/null +++ b/prefixes-minestom/build.gradle.kts @@ -0,0 +1,15 @@ +dependencies { + api(project(":prefixes-shared")) + compileOnly(libs.minestom) + compileOnly(libs.slf4j.api) + implementation(libs.cloud.command.minestom) +} + +sourceSets { + main { + kotlin { + srcDir(rootProject.layout.projectDirectory.dir("custom-names").dir("custom-names-api/src/main/kotlin")) + srcDir(rootProject.layout.projectDirectory.dir("custom-names").dir("custom-names-minestom/src/main/kotlin")) + } + } +} \ No newline at end of file diff --git a/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/PrefixesMinestom.kt b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/PrefixesMinestom.kt new file mode 100644 index 0000000..65634d9 --- /dev/null +++ b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/PrefixesMinestom.kt @@ -0,0 +1,119 @@ +package app.simplecloud.prefixes.minestom + +import app.simplecloud.prefixes.api.PrefixesApi +import app.simplecloud.prefixes.api.PrefixesApiProvider +import app.simplecloud.prefixes.minestom.command.PrefixesMinestomSenderMapper +import app.simplecloud.prefixes.minestom.display.MinestomDisplayManager +import app.simplecloud.prefixes.minestom.display.MinestomTablist +import app.simplecloud.prefixes.minestom.listener.PlayerListener +import app.simplecloud.prefixes.minestom.permission.MinestomPermissions +import app.simplecloud.prefixes.minestom.platform.MinestomPlatformImpl +import app.simplecloud.prefixes.minestom.platform.MinestomPrefixesListener +import app.simplecloud.prefixes.shared.Prefixes +import app.simplecloud.prefixes.shared.command.PrefixesCommand +import app.simplecloud.prefixes.shared.PrefixesConstants +import app.simplecloud.prefixes.shared.utilities.PrefixesCoroutineDetails +import kotlinx.coroutines.* +import net.luckperms.api.LuckPerms +import net.minestom.server.MinecraftServer +import net.minestom.server.adventure.audience.Audiences +import net.minestom.server.entity.Player +import net.minestom.server.event.EventNode +import org.incendo.cloud.execution.ExecutionCoordinator +import org.incendo.cloud.minestom.MinestomCommandManager +import space.chunks.customname.minestom.CustomNamesMinestom +import java.nio.file.Path +import java.util.function.BiPredicate +import kotlin.time.Duration.Companion.seconds + +class PrefixesMinestom private constructor( + directory: Path, + permission: BiPredicate?, + private val registerCommands: Boolean, + luckPerms: LuckPerms? +) { + + private val node = EventNode.all("simplecloud-prefixes").setPriority(Int.MAX_VALUE) + private val permissions = MinestomPermissions(permission) + private val platform = MinestomPlatformImpl(directory, permissions.getChecker(), luckPerms) + private val prefixes = Prefixes(platform) + private val manager = MinestomDisplayManager(prefixes, CustomNamesMinestom.getManager()) + private val tablist = MinestomTablist() + private val logger = PrefixesConstants.LOGGER + + fun shutdown() { + MinecraftServer.getGlobalEventHandler().removeChild(node) + prefixes.shutdown() + CustomNamesMinestom.shutdown() + } + + private fun init() { + CustomNamesMinestom.init() + prefixes.startup() + prefixes.addListener(MinestomPrefixesListener(prefixes, manager, tablist)) + + PlayerListener(prefixes, manager, tablist).register(node) + MinecraftServer.getGlobalEventHandler().addChild(node) + + if (prefixes.config.get().general.source.equals(PrefixesConstants.CONFIG_SOURCE, ignoreCase = true) && !permissions.hasCheck) { + logger.warn("Source Type is set to '${PrefixesConstants.CONFIG_SOURCE}', but no permission check was registered!") + } + + registerSync() + registerCommands() + prefixes.api.refreshAll() + } + + private fun registerSync() { + val sync = prefixes.sync ?: return + + sync.subscriber.subscribeChatMessage { message -> Audiences.players().sendMessage(message) } + sync.subscriber.subscribeTablist( + onUpdate = tablist::update, + onRemove = { publisherId, id -> tablist.remove(publisherId, id) }, + onRequest = { MinecraftServer.getSchedulerManager().scheduleNextTick { manager.publishAll() } } + ) + sync.publisher.publishTablistRequest() + + PrefixesConstants.SCOPE.launch(PrefixesCoroutineDetails(null, "publish tablist")) { + while (isActive) { + delay(30.seconds) + MinecraftServer.getSchedulerManager().scheduleNextTick { manager.publishAll() } + tablist.expire() + } + } + } + + private fun registerCommands() { + if (!registerCommands) return + + val commandManager = MinestomCommandManager( + ExecutionCoordinator.asyncCoordinator(), + PrefixesMinestomSenderMapper(permissions), + permissions::hasPermission + ) + + PrefixesCommand(commandManager, prefixes).register() + } + + companion object { + /** Returns the [PrefixesApi] instance, throws an [IllegalStateException] if prefixes is not enabled yet. */ + @JvmStatic + fun getApi(): PrefixesApi = PrefixesApiProvider.get() + + /** Creates a [PrefixesMinestomBuilder] config files are created in [directory]. */ + @JvmStatic + fun builder(directory: Path): PrefixesMinestomBuilder = PrefixesMinestomBuilder(directory) + + internal fun start( + directory: Path, + permission: BiPredicate?, + registerCommands: Boolean, + luckPerms: LuckPerms? + ): PrefixesMinestom { + val instance = PrefixesMinestom(directory, permission, registerCommands, luckPerms) + instance.init() + return instance + } + } +} diff --git a/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/PrefixesMinestomBuilder.kt b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/PrefixesMinestomBuilder.kt new file mode 100644 index 0000000..6b75e7a --- /dev/null +++ b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/PrefixesMinestomBuilder.kt @@ -0,0 +1,32 @@ +package app.simplecloud.prefixes.minestom + +import net.luckperms.api.LuckPerms +import net.minestom.server.entity.Player +import java.nio.file.Path +import java.util.function.BiPredicate + +/** + * Builds a [PrefixesMinestom] instance. + */ +@ConsistentCopyVisibility +data class PrefixesMinestomBuilder internal constructor( + private val directory: Path, + private val permission: BiPredicate? = null, + private val registerCommands: Boolean = true, + private val luckPerms: LuckPerms? = null +) { + + /** + * Sets the check that decides which group a player gets and whether they may use the prefixes commands. + */ + fun permission(check: BiPredicate): PrefixesMinestomBuilder = copy(permission = check) + + /** Sets whether the prefixes commands are registered. */ + fun registerCommands(enabled: Boolean): PrefixesMinestomBuilder = copy(registerCommands = enabled) + + /** Sets the [LuckPerms] instance, only needed when it is not available through the LuckPerms provider. */ + fun luckPerms(luckPerms: LuckPerms): PrefixesMinestomBuilder = copy(luckPerms = luckPerms) + + /** Creates the instance and starts prefixes. */ + fun enable(): PrefixesMinestom = PrefixesMinestom.start(directory, permission, registerCommands, luckPerms) +} diff --git a/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/command/PrefixesMinestomSender.kt b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/command/PrefixesMinestomSender.kt new file mode 100644 index 0000000..e863be1 --- /dev/null +++ b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/command/PrefixesMinestomSender.kt @@ -0,0 +1,17 @@ +package app.simplecloud.prefixes.minestom.command + +import app.simplecloud.prefixes.minestom.permission.MinestomPermissions +import app.simplecloud.prefixes.shared.command.PrefixesSender +import net.kyori.adventure.text.Component +import net.minestom.server.command.CommandSender + +class PrefixesMinestomSender( + val sender: CommandSender, + private val permissions: MinestomPermissions +) : PrefixesSender { + + override fun sendMessage(message: Component) = sender.sendMessage(message) + + override fun hasPermission(permission: String): Boolean = permissions.hasPermission(sender, permission) + +} diff --git a/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/command/PrefixesMinestomSenderMapper.kt b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/command/PrefixesMinestomSenderMapper.kt new file mode 100644 index 0000000..462b278 --- /dev/null +++ b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/command/PrefixesMinestomSenderMapper.kt @@ -0,0 +1,15 @@ +package app.simplecloud.prefixes.minestom.command + +import app.simplecloud.prefixes.minestom.permission.MinestomPermissions +import net.minestom.server.command.CommandSender +import org.incendo.cloud.SenderMapper + +class PrefixesMinestomSenderMapper( + private val permissions: MinestomPermissions +) : SenderMapper { + + override fun map(base: CommandSender): PrefixesMinestomSender = PrefixesMinestomSender(base, permissions) + + override fun reverse(mapped: PrefixesMinestomSender): CommandSender = mapped.sender + +} diff --git a/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/display/MinestomDisplayManager.kt b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/display/MinestomDisplayManager.kt new file mode 100644 index 0000000..f2ace28 --- /dev/null +++ b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/display/MinestomDisplayManager.kt @@ -0,0 +1,183 @@ +package app.simplecloud.prefixes.minestom.display + +import app.simplecloud.prefixes.api.group.PrefixesPlayerData +import app.simplecloud.prefixes.shared.Prefixes +import app.simplecloud.prefixes.shared.data.ViewerKey +import app.simplecloud.prefixes.shared.sync.tablist.ProfileProperty +import app.simplecloud.prefixes.shared.sync.tablist.TablistEntry +import app.simplecloud.prefixes.shared.sync.tablist.TablistGameMode +import app.simplecloud.prefixes.shared.PrefixesConstants +import app.simplecloud.prefixes.shared.utilities.PrefixesCoroutineDetails +import app.simplecloud.prefixes.shared.utilities.PlayerDisplayFormatter +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import net.kyori.adventure.text.Component +import net.minestom.server.MinecraftServer +import net.minestom.server.entity.Entity +import net.minestom.server.entity.Player +import net.minestom.server.network.packet.server.play.PlayerInfoUpdatePacket +import net.minestom.server.network.packet.server.play.TeamsPacket +import space.chunks.customname.api.CustomNameManager +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +class MinestomDisplayManager( + private val prefixes: Prefixes, + private val customNameManager: CustomNameManager +) { + + private val registry = prefixes.dataRegistry + + private val teams = ConcurrentHashMap() + private val entries = ConcurrentHashMap() + + fun updatePlayer(player: Player) { + PrefixesConstants.SCOPE.launch(PrefixesCoroutineDetails("${player.username} (${player.uuid})", "update player")) { + val group = prefixes.api.getPrimaryGroup(player.uuid).await() + MinecraftServer.getSchedulerManager().scheduleNextTick { + if (!player.isOnline) return@scheduleNextTick + registry.loadGroup(player.uuid, group) + show(player) + } + } + } + + fun removePlayer(player: Player) { + val id = player.uuid + registry.remove(id) + teams.keys.filter { key -> key.target == id || key.viewer == id }.forEach { key -> + if (key.viewer == id) teams.remove(key) else removeTeam(key) + } + + unpublish(id) + } + + fun clear() { + unpublishAll() + teams.keys.toList().forEach(::removeTeam) + onlinePlayers().forEach { player -> player.displayName = null } + } + + fun show(player: Player) { + if (!player.isOnline) return + val data = registry.getData(player.uuid) ?: return + + player.displayName = if (prefixes.config.get().features.tablist) PlayerDisplayFormatter.formatTablistName(data, player.username) else null + + val team = createTeam(player, data) + onlinePlayers().forEach { viewer -> showTo(player, viewer, team) } + + updateNameTag(player, data) + publish(player) + } + + fun refreshNameTag(player: Player) { + val data = registry.getData(player.uuid) ?: return + updateNameTag(player, data) + } + + fun addViewer(viewer: Player) { + onlinePlayers().forEach { player -> + val data = registry.getData(player.uuid) ?: return@forEach + showTo(player, viewer, createTeam(player, data)) + } + } + + fun publishAll() { + onlinePlayers().forEach { player -> publish(player, true) } + } + + fun unpublishAll() { + entries.keys.forEach(::unpublish) + } + + private fun showTo(player: Player, viewer: Player, team: TeamsPacket?) { + val viewerData = registry.getViewerData(player.uuid, viewer.uuid) + if (viewerData == null) { + updateTeam(player, viewer, team) + return + } + + updateTeam(player, viewer, createTeam(player, viewerData)) + if (prefixes.config.get().features.tablist) { + viewer.sendPacket(createListNamePacket(player, PlayerDisplayFormatter.formatTablistName(viewerData, player.username))) + } + } + + private fun updateNameTag(player: Player, data: PrefixesPlayerData) { + val name = customNameManager.forEntity(player) + name.setName { viewer -> (registry.getViewerData(player.uuid, viewer) ?: data).displayName } + name.setHidden(!prefixes.config.get().features.displayName) + } + + private fun publish(player: Player, force: Boolean = false) { + if (!prefixes.config.get().isTablistSynced()) return + val publisher = prefixes.sync?.publisher ?: return + val data = registry.getData(player.uuid) ?: return + + val skin = player.skin + val entry = TablistEntry( + uniqueId = player.uuid, + name = player.username, + displayName = PlayerDisplayFormatter.formatTablistName(data, player.username), + priority = data.priority, + profileProperties = if (skin == null) emptyList() else listOf(ProfileProperty("textures", skin.textures(), skin.signature())), + latency = player.latency, + gameMode = TablistGameMode.valueOf(player.gameMode.name), + showHat = (player.settings.displayedSkinParts.toInt() and 0x40) != 0, + listOrder = player.listOrder + ) + + val previous = entries.put(player.uuid, entry) + if (force || previous != entry) { + publisher.publishTablistEntry(entry) + } + } + + private fun unpublish(id: UUID) { + if (entries.remove(id) == null) return + prefixes.sync?.publisher?.publishTablistRemove(id) + } + + private fun createTeam(player: Player, data: PrefixesPlayerData): TeamsPacket? { + val features = prefixes.config.get().features + return when { + features.tablist -> MinestomPlayerTeam.createPacket(player.username, data.priority, data.prefix, data.suffix, data.color, features.displayName) + features.displayName -> MinestomPlayerTeam.createPacket(player.username, priority = 0, hideNameTag = true) + else -> null + } + } + + private fun updateTeam(player: Player, viewer: Player, team: TeamsPacket?) { + val key = ViewerKey(player.uuid, viewer.uuid) + removeTeam(key) + if (team == null) return + + teams[key] = team + viewer.sendPacket(team) + } + + private fun removeTeam(key: ViewerKey) { + val team = teams.remove(key) ?: return + val viewer = MinecraftServer.getConnectionManager().getOnlinePlayerByUuid(key.viewer) ?: return + viewer.sendPacket(MinestomPlayerTeam.removePacket(team)) + } + + private fun createListNamePacket(player: Player, name: Component): PlayerInfoUpdatePacket { + val entry = PlayerInfoUpdatePacket.Entry( + player.uuid, + player.username, + emptyList(), + true, + player.latency, + player.gameMode, + name, + null, + player.listOrder, + true + ) + return PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME, entry) + } + + private fun onlinePlayers(): Collection = MinecraftServer.getConnectionManager().onlinePlayers +} diff --git a/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/display/MinestomPlayerTeam.kt b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/display/MinestomPlayerTeam.kt new file mode 100644 index 0000000..c6deeb7 --- /dev/null +++ b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/display/MinestomPlayerTeam.kt @@ -0,0 +1,42 @@ +package app.simplecloud.prefixes.minestom.display + +import app.simplecloud.prefixes.shared.utilities.PriorityFormatter +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.TextColor +import net.minestom.server.color.TeamColor +import net.minestom.server.network.packet.server.play.TeamsPacket +import net.minestom.server.network.packet.server.play.TeamsPacket.CollisionRule +import net.minestom.server.network.packet.server.play.TeamsPacket.NameTagVisibility + +object MinestomPlayerTeam { + + fun createPacket( + name: String, + priority: Int, + prefix: Component = Component.empty(), + suffix: Component = Component.empty(), + color: TextColor = NamedTextColor.WHITE, + hideNameTag: Boolean + ): TeamsPacket { + val settings = TeamsPacket.Settings( + Component.empty(), + prefix, + suffix, + if (hideNameTag) NameTagVisibility.NEVER else NameTagVisibility.ALWAYS, + CollisionRule.ALWAYS, + toTeamColor(color), + 0 + ) + + return TeamsPacket(createName(name, priority), TeamsPacket.CreateTeamAction(settings, listOf(name))) + } + + fun removePacket(packet: TeamsPacket): TeamsPacket = TeamsPacket(packet.teamName(), TeamsPacket.RemoveTeamAction()) + + private fun createName(name: String, priority: Int): String = "${PriorityFormatter.format(priority)}_$name" + + private fun toTeamColor(color: TextColor): TeamColor { + return TeamColor.valueOf(NamedTextColor.nearestTo(color).toString().uppercase()) + } +} diff --git a/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/display/MinestomTablist.kt b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/display/MinestomTablist.kt new file mode 100644 index 0000000..f38a405 --- /dev/null +++ b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/display/MinestomTablist.kt @@ -0,0 +1,61 @@ +package app.simplecloud.prefixes.minestom.display + +import app.simplecloud.prefixes.shared.sync.tablist.SyncedTablist +import app.simplecloud.prefixes.shared.sync.tablist.TablistEntry +import net.minestom.server.MinecraftServer +import net.minestom.server.entity.GameMode +import net.minestom.server.entity.Player +import net.minestom.server.network.packet.server.ServerPacket +import net.minestom.server.network.packet.server.play.PlayerInfoRemovePacket +import net.minestom.server.network.packet.server.play.PlayerInfoUpdatePacket +import net.minestom.server.network.packet.server.play.PlayerInfoUpdatePacket.Action +import net.minestom.server.network.packet.server.play.TeamsPacket +import net.minestom.server.utils.PacketSendingUtils +import java.util.EnumSet +import java.util.UUID + +class MinestomTablist : SyncedTablist() { + + override fun isOnline(id: UUID): Boolean = MinecraftServer.getConnectionManager().getOnlinePlayerByUuid(id) != null + + override fun createInfoPacket(entries: List): ServerPacket { + val packetEntries = entries.map { entry -> + PlayerInfoUpdatePacket.Entry( + entry.uniqueId, + entry.name, + entry.profileProperties.map { property -> PlayerInfoUpdatePacket.Property(property.name, property.value, property.signature) }, + true, + entry.latency, + GameMode.valueOf(entry.gameMode.name), + entry.displayName, + null, + entry.listOrder, + entry.showHat + ) + } + + return PlayerInfoUpdatePacket(getUpdateActions(), packetEntries) + } + + override fun createInfoRemovePacket(id: UUID): ServerPacket = PlayerInfoRemovePacket(id) + + override fun createTeamPacket(entry: TablistEntry): ServerPacket = createTeam(entry) + + override fun createTeamRemovePacket(entry: TablistEntry): ServerPacket = MinestomPlayerTeam.removePacket(createTeam(entry)) + + override fun send(player: Player, packet: ServerPacket) = player.sendPacket(packet) + + override fun broadcast(packet: ServerPacket) = PacketSendingUtils.broadcastPlayPacket(packet) + + private fun createTeam(entry: TablistEntry): TeamsPacket = MinestomPlayerTeam.createPacket(entry.name, entry.priority, hideNameTag = true) + + private fun getUpdateActions(): EnumSet = EnumSet.of( + Action.ADD_PLAYER, + Action.UPDATE_LISTED, + Action.UPDATE_GAME_MODE, + Action.UPDATE_LATENCY, + Action.UPDATE_DISPLAY_NAME, + Action.UPDATE_LIST_ORDER, + Action.UPDATE_HAT + ) +} diff --git a/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/listener/PlayerListener.kt b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/listener/PlayerListener.kt new file mode 100644 index 0000000..9d4f23c --- /dev/null +++ b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/listener/PlayerListener.kt @@ -0,0 +1,67 @@ +package app.simplecloud.prefixes.minestom.listener + +import app.simplecloud.prefixes.minestom.display.MinestomDisplayManager +import app.simplecloud.prefixes.minestom.display.MinestomTablist +import app.simplecloud.prefixes.shared.Prefixes +import app.simplecloud.prefixes.shared.utilities.PlayerDisplayFormatter +import net.kyori.adventure.text.Component +import net.minestom.server.event.Event +import net.minestom.server.event.EventNode +import net.minestom.server.event.player.PlayerChatEvent +import net.minestom.server.event.player.PlayerDisconnectEvent +import net.minestom.server.event.player.PlayerSpawnEvent + +class PlayerListener( + private val prefixes: Prefixes, + private val manager: MinestomDisplayManager, + private val tablist: MinestomTablist +) { + + fun register(node: EventNode) { + node.addListener(PlayerSpawnEvent::class.java, ::onSpawn) + node.addListener(PlayerDisconnectEvent::class.java, ::onDisconnect) + node.addListener(PlayerChatEvent::class.java, ::onChat) + } + + private fun onSpawn(event: PlayerSpawnEvent) { + if (!event.isFirstSpawn) { + manager.refreshNameTag(event.player) + return + } + + tablist.remove(event.player.uuid) + manager.updatePlayer(event.player) + manager.addViewer(event.player) + + if (prefixes.config.get().isTablistSynced()) { + tablist.sync(event.player) + } + } + + private fun onDisconnect(event: PlayerDisconnectEvent) { + manager.removePlayer(event.player) + } + + private fun onChat(event: PlayerChatEvent) { + if (event.isCancelled) return + + val features = prefixes.config.get().features + if (!features.chat) return + + val player = event.player + val data = prefixes.dataRegistry.getData(player.uuid) ?: return + val rawMessage = Component.text(event.rawMessage) + val message = PlayerDisplayFormatter.formatChatMessage(data, player.username, rawMessage, features.displayName) + + val viewerMessages = event.recipients.mapNotNull { viewer -> + val viewerData = prefixes.dataRegistry.getViewerData(player.uuid, viewer.uuid) ?: return@mapNotNull null + viewer to PlayerDisplayFormatter.formatChatMessage(viewerData, player.username, rawMessage, features.displayName) + } + + event.recipients.removeAll(viewerMessages.map { (viewer, _) -> viewer }.toSet()) + event.formattedMessage = message + + viewerMessages.forEach { (viewer, viewerMessage) -> viewer.sendMessage(viewerMessage) } + prefixes.sync?.publisher?.publishChatMessage(message) + } +} diff --git a/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/permission/MinestomPermissions.kt b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/permission/MinestomPermissions.kt new file mode 100644 index 0000000..742c50a --- /dev/null +++ b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/permission/MinestomPermissions.kt @@ -0,0 +1,31 @@ +package app.simplecloud.prefixes.minestom.permission + +import app.simplecloud.plugin.api.shared.permission.PermissionChecker +import net.minestom.server.MinecraftServer +import net.minestom.server.command.CommandSender +import net.minestom.server.command.ConsoleSender +import net.minestom.server.entity.Player +import java.util.UUID +import java.util.function.BiPredicate + +class MinestomPermissions(private val check: BiPredicate?) { + + val hasCheck: Boolean = check != null + + fun hasPermission(sender: CommandSender, permission: String): Boolean { + if (permission.isEmpty()) return true + if (sender is ConsoleSender) return true + if (sender !is Player) return false + + if (check != null) return check.test(sender, permission) + return sender.permissionLevel >= 4 + } + + fun getChecker(): PermissionChecker = PermissionChecker { id, permission -> + if (check == null) return@PermissionChecker false + val player = MinecraftServer.getConnectionManager().getOnlinePlayerByUuid(id) ?: return@PermissionChecker false + + check.test(player, permission) + } + +} diff --git a/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/platform/MinestomPlatformImpl.kt b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/platform/MinestomPlatformImpl.kt new file mode 100644 index 0000000..25d6285 --- /dev/null +++ b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/platform/MinestomPlatformImpl.kt @@ -0,0 +1,29 @@ +package app.simplecloud.prefixes.minestom.platform + +import app.simplecloud.plugin.api.shared.permission.PermissionChecker +import app.simplecloud.prefixes.shared.platform.PrefixesPlatform +import net.luckperms.api.LuckPerms +import net.luckperms.api.LuckPermsProvider +import net.minestom.server.MinecraftServer +import java.io.File +import java.nio.file.Path +import java.util.UUID + +class MinestomPlatformImpl( + private val path: Path, + private val permissionChecker: PermissionChecker, + private val luckPerms: LuckPerms? +) : PrefixesPlatform { + + override fun getDataDirectory(): File = path.toFile() + + override fun getPermissionChecker(): PermissionChecker = permissionChecker + + override fun getLuckPerms(): LuckPerms? = luckPerms ?: runCatching { LuckPermsProvider.get() }.getOrNull() + + override fun getPlayerName(id: UUID): String { + return MinecraftServer.getConnectionManager().getOnlinePlayerByUuid(id)?.username ?: id.toString() + } + + override fun isOnline(id: UUID): Boolean = MinecraftServer.getConnectionManager().getOnlinePlayerByUuid(id) != null +} diff --git a/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/platform/MinestomPrefixesListener.kt b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/platform/MinestomPrefixesListener.kt new file mode 100644 index 0000000..bad7428 --- /dev/null +++ b/prefixes-minestom/src/main/kotlin/app/simplecloud/prefixes/minestom/platform/MinestomPrefixesListener.kt @@ -0,0 +1,50 @@ +package app.simplecloud.prefixes.minestom.platform + +import app.simplecloud.prefixes.minestom.display.MinestomDisplayManager +import app.simplecloud.prefixes.minestom.display.MinestomTablist +import app.simplecloud.prefixes.shared.Prefixes +import app.simplecloud.prefixes.shared.platform.PrefixesListener +import net.minestom.server.MinecraftServer +import java.util.UUID + +class MinestomPrefixesListener( + private val prefixes: Prefixes, + private val manager: MinestomDisplayManager, + private val tablist: MinestomTablist +) : PrefixesListener { + + override fun onReload() { + onAllPlayersUpdate() + + if (!prefixes.config.get().isTablistSynced()) { + manager.unpublishAll() + tablist.clear() + return + } + + prefixes.sync?.publisher?.publishTablistRequest() + } + + override fun onShutdown() { + manager.clear() + tablist.clear() + } + + override fun onPlayerUpdate(id: UUID) { + val player = MinecraftServer.getConnectionManager().getOnlinePlayerByUuid(id) ?: return + manager.updatePlayer(player) + } + + override fun onPlayerDataChange(id: UUID) { + MinecraftServer.getSchedulerManager().scheduleNextTick { + val player = MinecraftServer.getConnectionManager().getOnlinePlayerByUuid(id) ?: return@scheduleNextTick + manager.show(player) + } + } + + override fun onAllPlayersUpdate() { + MinecraftServer.getConnectionManager().onlinePlayers.forEach { player -> + manager.updatePlayer(player) + } + } +} diff --git a/prefixes-paper/build.gradle.kts b/prefixes-paper/build.gradle.kts index e8a7683..d776fc6 100644 --- a/prefixes-paper/build.gradle.kts +++ b/prefixes-paper/build.gradle.kts @@ -3,14 +3,6 @@ plugins { alias(libs.plugins.paperweight.userdev) } -val customNamesRoot = rootProject.layout.projectDirectory.dir("custom-names") -require( - customNamesRoot.dir("custom-names-api/src/main/kotlin").asFile.isDirectory && - customNamesRoot.dir("custom-names-plugin/src/main/kotlin").asFile.isDirectory -) { - "The custom-names submodule is missing. Run `git submodule update --init --recursive`." -} - dependencies { api(project(":prefixes-shared")) compileOnly(libs.paper.api) @@ -22,8 +14,8 @@ dependencies { sourceSets { main { kotlin { - srcDir(customNamesRoot.dir("custom-names-api/src/main/kotlin")) - srcDir(customNamesRoot.dir("custom-names-plugin/src/main/kotlin")) + srcDir(rootProject.layout.projectDirectory.dir("custom-names").dir("custom-names-api/src/main/kotlin")) + srcDir(rootProject.layout.projectDirectory.dir("custom-names").dir("custom-names-paper/src/main/kotlin")) exclude("**/CustomNamesPlugin.kt") } } @@ -32,8 +24,6 @@ sourceSets { tasks.shadowJar { relocate("com.google.protobuf", "app.simplecloud.prefixes.libs.protobuf") relocate("space.chunks.customname", "app.simplecloud.prefixes.libs.customname") - relocate("xyz.jpenilla.reflectionremapper", "app.simplecloud.prefixes.libs.reflectionremapper") - exclude("org/bouncycastle/jcajce/io/DigestUpdatingOutputStream.class") } modrinth { @@ -44,17 +34,9 @@ modrinth { uploadFile.set(tasks.shadowJar) gameVersions.addAll( "26.2", - "26.3" + "26.3", ) - loaders.addAll("paper", "purpur") + loaders.addAll("paper", "purpur", "folia") changelog.set("https://docs.simplecloud.app/changelog") syncBodyFrom.set(rootProject.file("README.md").readText()) -} - -// Paper bootstrap writes logs; keep regression-test output under build/. -tasks.test { - workingDir = layout.buildDirectory.dir("test-server").get().asFile - doFirst { - workingDir.mkdirs() - } -} +} \ No newline at end of file diff --git a/prefixes-paper/src/main/java/org/incendo/cloud/bukkit/parser/ItemStackParser.java b/prefixes-paper/src/main/java/org/incendo/cloud/bukkit/parser/ItemStackParser.java deleted file mode 100644 index 294445c..0000000 --- a/prefixes-paper/src/main/java/org/incendo/cloud/bukkit/parser/ItemStackParser.java +++ /dev/null @@ -1,358 +0,0 @@ -// -// MIT License -// -// Copyright (c) 2024 Incendo -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// Backport for Paper 26.3 from https://github.com/Incendo/cloud-minecraft/issues/174. -// Based on cloud-bukkit 2.0.0; remove when upstream ships the fallback. -package org.incendo.cloud.bukkit.parser; - -import com.google.common.base.Suppliers; -import com.mojang.brigadier.arguments.ArgumentType; -import com.mojang.brigadier.exceptions.CommandSyntaxException; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.CompletableFuture; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import org.apiguardian.api.API; -import org.bukkit.Material; -import org.bukkit.NamespacedKey; -import org.bukkit.inventory.ItemStack; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser; -import org.incendo.cloud.bukkit.BukkitCommandManager; -import org.incendo.cloud.bukkit.data.ProtoItemStack; -import org.incendo.cloud.bukkit.internal.CommandBuildContextSupplier; -import org.incendo.cloud.bukkit.internal.CraftBukkitReflection; -import org.incendo.cloud.bukkit.internal.MinecraftArgumentTypes; -import org.incendo.cloud.component.CommandComponent; -import org.incendo.cloud.context.CommandContext; -import org.incendo.cloud.context.CommandInput; -import org.incendo.cloud.parser.ArgumentParseResult; -import org.incendo.cloud.parser.ArgumentParser; -import org.incendo.cloud.parser.ParserDescriptor; -import org.incendo.cloud.suggestion.BlockingSuggestionProvider; -import org.incendo.cloud.suggestion.SuggestionProvider; - -import static java.util.Objects.requireNonNull; - -/** - * Parser for a {@link Material} and optional extra NBT data into a {@link ProtoItemStack}. - * - *

This parser only provides basic suggestions by default. On Minecraft 1.13 and newer, enabling Brigadier - * compatibility through {@link BukkitCommandManager#registerBrigadier()} will allow client side validation and - * suggestions to be utilized.

- * - * @param Command sender type - * @since 1.5.0 - */ -public class ItemStackParser implements ArgumentParser.FutureArgumentParser { - - /** - * Creates a new item stack parser. - * - * @param command sender type - * @return the created parser - * @since 2.0.0 - */ - @API(status = API.Status.STABLE, since = "2.0.0") - public static @NonNull ParserDescriptor itemStackParser() { - return ParserDescriptor.of(new ItemStackParser<>(), ProtoItemStack.class); - } - - /** - * Returns a {@link CommandComponent.Builder} using {@link #itemStackParser()} as the parser. - * - * @param the command sender type - * @return the component builder - * @since 2.0.0 - */ - @API(status = API.Status.STABLE, since = "2.0.0") - public static CommandComponent.@NonNull Builder itemStackComponent() { - return CommandComponent.builder().parser(itemStackParser()); - } - - private final ArgumentParser parser; - - private static @Nullable Class findItemInputClass() { - final Class[] classes = new Class[]{ - CraftBukkitReflection.findNMSClass("ArgumentPredicateItemStack"), - CraftBukkitReflection.findMCClass("commands.arguments.item.ArgumentPredicateItemStack"), - CraftBukkitReflection.findMCClass("commands.arguments.item.ItemInput") - }; - for (final Class clazz : classes) { - if (clazz != null) { - return clazz; - } - } - return null; - } - - /** - * Create a new {@link ItemStackParser}. - * - * @since 1.5.0 - */ - public ItemStackParser() { - if (findItemInputClass() != null) { - this.parser = new ModernParser<>(); - } else { - this.parser = new LegacyParser<>(); - } - } - - @Override - public final @NonNull CompletableFuture<@NonNull ArgumentParseResult> parseFuture( - final @NonNull CommandContext commandContext, - final @NonNull CommandInput commandInput - ) { - return this.parser.parseFuture(commandContext, commandInput); - } - - @Override - public final @NonNull SuggestionProvider suggestionProvider() { - return this.parser.suggestionProvider(); - } - - - private static final class ModernParser implements ArgumentParser.FutureArgumentParser { - - private static final Class NMS_ITEM_STACK_CLASS = CraftBukkitReflection.needNMSClassOrElse( - "ItemStack", - "net.minecraft.world.item.ItemStack" - ); - private static final Class CRAFT_ITEM_STACK_CLASS = - CraftBukkitReflection.needOBCClass("inventory.CraftItemStack"); - private static final Supplier> ARGUMENT_ITEM_STACK_CLASS = - Suppliers.memoize(() -> MinecraftArgumentTypes.getClassByKey(NamespacedKey.minecraft("item_stack"))); - private static final Class ITEM_INPUT_CLASS = requireNonNull(findItemInputClass(), "ItemInput class"); - private static final Class NMS_ITEM_CLASS = CraftBukkitReflection.needNMSClassOrElse( - "Item", - "net.minecraft.world.item.Item" - ); - private static final Supplier GET_MATERIAL_METHOD = Suppliers.memoize(() -> CraftBukkitReflection - .needMethod(CraftBukkitReflection.needOBCClass("util.CraftMagicNumbers"), "getMaterial", NMS_ITEM_CLASS)); - private static final Method CREATE_ITEM_STACK_METHOD = CraftBukkitReflection.firstNonNullOrThrow( - () -> "Couldn't find createItemStack method on ItemInput", - CraftBukkitReflection.findMethod(ITEM_INPUT_CLASS, "a", int.class, boolean.class), - CraftBukkitReflection.findMethod(ITEM_INPUT_CLASS, "createItemStack", int.class, boolean.class), - CraftBukkitReflection.findMethod(ITEM_INPUT_CLASS, "createItemStack", int.class) - ); - private static final Method AS_BUKKIT_STACK_METHOD = CraftBukkitReflection.firstNonNullOrThrow( - () -> "Couldn't find asBukkitCopy or asCraftMirror method on CraftItemStack", - CraftBukkitReflection.findMethod(CRAFT_ITEM_STACK_CLASS, "asBukkitCopy", NMS_ITEM_STACK_CLASS), - CraftBukkitReflection.findMethod(CRAFT_ITEM_STACK_CLASS, "asCraftMirror", NMS_ITEM_STACK_CLASS), - CraftBukkitReflection.findMethod(CRAFT_ITEM_STACK_CLASS, "asBukkitMirror", NMS_ITEM_STACK_CLASS) - ); - private static final Field ITEM_FIELD = CraftBukkitReflection.firstNonNullOrThrow( - () -> "Couldn't find item field on ItemInput", - CraftBukkitReflection.findField(ITEM_INPUT_CLASS, "b"), - CraftBukkitReflection.findField(ITEM_INPUT_CLASS, "item") - ); - private static final Field EXTRA_DATA_FIELD = CraftBukkitReflection.firstNonNullOrThrow( - () -> "Couldn't find tag field on ItemInput", - CraftBukkitReflection.findField(ITEM_INPUT_CLASS, "c"), - CraftBukkitReflection.findField(ITEM_INPUT_CLASS, "tag"), - CraftBukkitReflection.findField(ITEM_INPUT_CLASS, "components") - ); - private static final Class HOLDER_CLASS = CraftBukkitReflection.findMCClass("core.Holder"); - private static final @Nullable Method VALUE_METHOD = HOLDER_CLASS == null - ? null - : CraftBukkitReflection.firstNonNullOrThrow( - () -> "Couldn't find Holder#value", - CraftBukkitReflection.findMethod(HOLDER_CLASS, "value"), - CraftBukkitReflection.findMethod(HOLDER_CLASS, "a") - ); - private static final Class NBT_TAG_CLASS = CraftBukkitReflection.firstNonNullOrThrow( - () -> "Cloud not find net.minecraft.nbt.Tag", - CraftBukkitReflection.findClass("net.minecraft.nbt.Tag"), - CraftBukkitReflection.findClass("net.minecraft.nbt.NBTBase"), - CraftBukkitReflection.findNMSClass("NBTBase") - ); - - private final ArgumentParser parser; - - ModernParser() { - this.parser = this.createParser(); - } - - @SuppressWarnings("unchecked") - private ArgumentParser createParser() { - final Supplier> inst = () -> { - final Constructor ctr = ARGUMENT_ITEM_STACK_CLASS.get().getDeclaredConstructors()[0]; - try { - if (ctr.getParameterCount() == 0) { - return (ArgumentType) ctr.newInstance(); - } else { - // 1.19+ - return (ArgumentType) ctr.newInstance(CommandBuildContextSupplier.commandBuildContext()); - } - } catch (final ReflectiveOperationException e) { - throw new RuntimeException("Failed to initialize modern ItemStack parser.", e); - } - }; - return new WrappedBrigadierParser(inst) - .flatMapSuccess((ctx, itemInput) -> ArgumentParseResult.successFuture( - new ModernProtoItemStack(itemInput))); - } - - @Override - public @NonNull CompletableFuture<@NonNull ArgumentParseResult<@NonNull ProtoItemStack>> parseFuture( - final @NonNull CommandContext<@NonNull C> commandContext, - final @NonNull CommandInput commandInput - ) { - // Minecraft has a parser for this - just use it - return this.parser.parseFuture(commandContext, commandInput); - } - - @Override - public @NonNull SuggestionProvider suggestionProvider() { - return this.parser.suggestionProvider(); - } - - - private static final class ModernProtoItemStack implements ProtoItemStack { - - private final Object itemInput; - private final Material material; - private final boolean hasExtraData; - - ModernProtoItemStack(final @NonNull Object itemInput) { - this.itemInput = itemInput; - try { - Object item = ITEM_FIELD.get(itemInput); - if (HOLDER_CLASS != null && HOLDER_CLASS.isInstance(item)) { - item = VALUE_METHOD.invoke(item); - } - this.material = (Material) GET_MATERIAL_METHOD.get().invoke(null, item); - final Object extraData = EXTRA_DATA_FIELD.get(itemInput); - if (NBT_TAG_CLASS.isInstance(extraData) || extraData == null) { - this.hasExtraData = extraData != null; - } else { - final List isEmptyMethod = Arrays.stream(extraData.getClass().getMethods()) - .filter(it -> it.getParameterCount() == 0 && it.getReturnType().equals(boolean.class)) - .collect(Collectors.toList()); - if (isEmptyMethod.size() != 1) { - throw new IllegalStateException( - "Failed to locate DataComponentMap/Patch#isEmpty; size=" + isEmptyMethod.size()); - } - this.hasExtraData = !(boolean) isEmptyMethod.get(0).invoke(extraData); - } - } catch (final ReflectiveOperationException ex) { - throw new RuntimeException(ex); - } - } - - @Override - public @NonNull Material material() { - return this.material; - } - - @Override - public boolean hasExtraData() { - return this.hasExtraData; - } - - @Override - public @NonNull ItemStack createItemStack(final int stackSize) { - try { - final Object nmsItemStack = CREATE_ITEM_STACK_METHOD.getParameterCount() == 1 - ? CREATE_ITEM_STACK_METHOD.invoke(this.itemInput, stackSize) - : CREATE_ITEM_STACK_METHOD.invoke(this.itemInput, stackSize, true); - return (ItemStack) AS_BUKKIT_STACK_METHOD.invoke( - null, - nmsItemStack - ); - } catch (final InvocationTargetException ex) { - final Throwable cause = ex.getCause(); - if (cause instanceof CommandSyntaxException) { - throw new IllegalArgumentException(cause.getMessage(), cause); - } - throw new RuntimeException(ex); - } catch (final ReflectiveOperationException e) { - throw new RuntimeException(e); - } - } - } - } - - private static final class LegacyParser implements ArgumentParser.FutureArgumentParser, - BlockingSuggestionProvider.Strings { - - private final ArgumentParser parser = new MaterialParser() - .mapSuccess((ctx, material) -> CompletableFuture.completedFuture(new LegacyProtoItemStack(material))); - - @Override - public @NonNull CompletableFuture<@NonNull ArgumentParseResult<@NonNull ProtoItemStack>> parseFuture( - final @NonNull CommandContext<@NonNull C> commandContext, - final @NonNull CommandInput commandInput - ) { - return this.parser.parseFuture(commandContext, commandInput); - } - - @Override - public @NonNull Iterable<@NonNull String> stringSuggestions(final @NonNull CommandContext commandContext, - final @NonNull CommandInput input) { - return Arrays.stream(Material.values()) - .filter(Material::isItem) - .map(value -> value.name().toLowerCase(Locale.ROOT)) - .collect(Collectors.toList()); - } - - private static final class LegacyProtoItemStack implements ProtoItemStack { - - private final Material material; - - private LegacyProtoItemStack(final @NonNull Material material) { - this.material = material; - } - - @Override - public @NonNull Material material() { - return this.material; - } - - @Override - public boolean hasExtraData() { - return false; - } - - @Override - public @NonNull ItemStack createItemStack(final int stackSize) throws IllegalArgumentException { - if (stackSize > this.material.getMaxStackSize()) { - throw new IllegalArgumentException(String.format( - "The maximum stack size for %s is %d", - this.material, - this.material.getMaxStackSize() - )); - } - return new ItemStack(this.material, stackSize); - } - } - } -} diff --git a/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/PrefixesPaper.kt b/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/PrefixesPaper.kt index 071b801..804a983 100644 --- a/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/PrefixesPaper.kt +++ b/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/PrefixesPaper.kt @@ -4,18 +4,18 @@ import app.simplecloud.prefixes.api.PrefixesApi import app.simplecloud.prefixes.paper.command.PrefixesPaperSenderMapper import app.simplecloud.prefixes.paper.display.PaperDisplayManager import app.simplecloud.prefixes.paper.display.PaperTablist -import app.simplecloud.prefixes.paper.listener.LuckPermsListener import app.simplecloud.prefixes.paper.listener.PlayerListener import app.simplecloud.prefixes.paper.platform.PaperPlatformImpl +import app.simplecloud.prefixes.paper.platform.PaperPrefixesListener import app.simplecloud.prefixes.shared.Prefixes import app.simplecloud.prefixes.shared.command.PrefixesCommand -import app.simplecloud.prefixes.shared.config.SourceType import org.bukkit.Bukkit import org.bukkit.plugin.ServicePriority import org.bukkit.plugin.java.JavaPlugin import org.incendo.cloud.execution.ExecutionCoordinator import org.incendo.cloud.paper.PaperCommandManager -import space.chunks.customname.plugin.CustomNameManagerImpl +import space.chunks.customname.paper.CustomNameManagerImpl +import java.util.concurrent.TimeUnit class PrefixesPaper : JavaPlugin() { @@ -25,74 +25,42 @@ class PrefixesPaper : JavaPlugin() { val customNameManager = CustomNameManagerImpl(this) customNameManager.registerListeners() - val platform = PaperPlatformImpl(this) - val prefixes = Prefixes(platform) + val prefixes = Prefixes(PaperPlatformImpl(this)) this.prefixes = prefixes prefixes.startup() - // Register Prefixes API as a Bukkit Service. Bukkit.getServicesManager().register(PrefixesApi::class.java, prefixes.api, this, ServicePriority.Normal) val manager = PaperDisplayManager(this, prefixes, customNameManager) val tablist = PaperTablist() - Bukkit.getPluginManager().registerEvents( - PlayerListener(prefixes, manager, tablist, prefixes.sync?.publisher), - this - ) - - // Apply chat and tablist updates from the configured sync sources. - prefixes.sync?.let { sync -> - val config = prefixes.config.get() - if (config.features.chat && config.sync.channels.chat) { - sync.subscriber.subscribeChatMessage { message -> Bukkit.getServer().sendMessage(message) } - } - if (config.features.tablist && config.sync.channels.tablist) { - sync.subscriber.subscribeTablist( - onUpdate = tablist::update, - onRemove = tablist::remove, - onRequest = { manager.sync(force = true) } - ) - sync.publisher.publishTablistRequest() - - Bukkit.getScheduler().runTaskTimer(this, Runnable { manager.sync() }, 600L, 600L) - } - } - - val source = prefixes.config.get().general.source - logger.info("Using Source Type: ${source.name}") - - // Register LuckPerms listener. - if (source == SourceType.LUCKPERMS) { - val lp = platform.luckPerms - if (lp != null) { - LuckPermsListener(this, lp, manager).register() - } else { - logger.warning("Source Type is set to LUCKPERMS, but LuckPerms was not found on the server!") - } - } - - // Refresh all online players on reload. - prefixes.onReload = { - val config = prefixes.config.get() - Bukkit.getOnlinePlayers().forEach { player -> - manager.updatePlayer(player) - } - - if (!config.features.tablist || !config.sync.enabled || !config.sync.channels.tablist) { - tablist.clear() - } else { - prefixes.sync?.publisher?.publishTablistRequest() - } - } + Bukkit.getPluginManager().registerEvents(PlayerListener(prefixes, manager, tablist), this) + + registerSync(prefixes, manager, tablist) + + prefixes.addListener(PaperPrefixesListener(this, prefixes, manager, tablist)) registerCommands(prefixes) + prefixes.api.refreshAll() } override fun onDisable() { prefixes?.shutdown() } + private fun registerSync(prefixes: Prefixes, manager: PaperDisplayManager, tablist: PaperTablist) { + val sync = prefixes.sync ?: return + + sync.subscriber.subscribeChatMessage { message -> Bukkit.getServer().sendMessage(message) } + sync.subscriber.subscribeTablist(tablist::update, tablist::remove) { manager.publishAll() } + sync.publisher.publishTablistRequest() + + Bukkit.getAsyncScheduler().runAtFixedRate(this, { + manager.publishAll() + tablist.expire() + }, 30L, 30L, TimeUnit.SECONDS) + } + private fun registerCommands(prefixes: Prefixes) { val manager = PaperCommandManager.builder(PrefixesPaperSenderMapper()) .executionCoordinator(ExecutionCoordinator.asyncCoordinator()) diff --git a/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/display/PaperDisplayManager.kt b/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/display/PaperDisplayManager.kt index 2a74c4b..9c470a5 100644 --- a/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/display/PaperDisplayManager.kt +++ b/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/display/PaperDisplayManager.kt @@ -2,168 +2,184 @@ package app.simplecloud.prefixes.paper.display import app.simplecloud.prefixes.api.group.PrefixesPlayerData import app.simplecloud.prefixes.shared.Prefixes +import app.simplecloud.prefixes.shared.data.ViewerKey import app.simplecloud.prefixes.shared.sync.tablist.ProfileProperty import app.simplecloud.prefixes.shared.sync.tablist.TablistEntry import app.simplecloud.prefixes.shared.sync.tablist.TablistGameMode +import app.simplecloud.prefixes.shared.PrefixesConstants +import app.simplecloud.prefixes.shared.utilities.PrefixesCoroutineDetails import app.simplecloud.prefixes.shared.utilities.PlayerDisplayFormatter import com.destroystokyo.paper.ClientOption +import io.papermc.paper.adventure.PaperAdventure +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch import net.kyori.adventure.text.Component +import net.minecraft.network.protocol.Packet +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket import net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket import org.bukkit.Bukkit import org.bukkit.craftbukkit.entity.CraftPlayer +import org.bukkit.entity.Entity import org.bukkit.entity.Player import org.bukkit.plugin.Plugin import space.chunks.customname.api.CustomNameManager +import java.util.EnumSet import java.util.UUID import java.util.concurrent.ConcurrentHashMap class PaperDisplayManager( private val plugin: Plugin, private val prefixes: Prefixes, - private val customNameManager: CustomNameManager + private val customNameManager: CustomNameManager ) { - private val cache = ConcurrentHashMap() - private val playerTeams = ConcurrentHashMap() - private val publishedEntries = ConcurrentHashMap() - fun getPlayer(id: UUID): PrefixesPlayerData? { - return cache[id] - } + private val registry = prefixes.dataRegistry - fun addPlayer(player: Player) { - updatePlayer(player) - Bukkit.getScheduler().runTask(plugin, Runnable { - if (player.isOnline && cache[player.uniqueId] == null) { - publish(player, Component.text(player.name), 0) - } - }) - } + private val teams = ConcurrentHashMap() + private val entries = ConcurrentHashMap() fun updatePlayer(player: Player) { - prefixes.api.getPrefixData(player.uniqueId).thenAccept { data -> - Bukkit.getScheduler().runTask(plugin, Runnable { - if (!player.isOnline) return@Runnable - cache[player.uniqueId] = data - - val features = prefixes.config.get().features - val displayName = PlayerDisplayFormatter.displayName( - data, - player.name, - features.displayName - ) - - if (features.tablist) { - player.playerListName(PlayerDisplayFormatter.formatTablistName(data, displayName)) - } else { - player.playerListName(null) - } - - playerTeams.remove(player.uniqueId)?.let { previous -> - broadcast(ClientboundSetPlayerTeamPacket.createRemovePacket(previous)) - } - - val team = when { - features.tablist -> PaperPlayerTeam( - player.name, - data.priority, - data.prefix, - data.suffix, - data.color, - hideNameTag = features.displayName - ) - features.displayName -> PaperPlayerTeam(player.name, priority = 0) - else -> null - } - team?.let { - playerTeams[player.uniqueId] = it - broadcast(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(it, true)) - } - - val name = customNameManager.forEntity(player) - if (features.displayName) { - name.setName(displayName) - name.setHidden(false) - } else { - name.setHidden(true) - } - - publish(player, data, displayName) - }) - }.exceptionally { _ -> - plugin.logger.warning("Failed to update prefix data of ${player.name}") - null + PrefixesConstants.SCOPE.launch(PrefixesCoroutineDetails("${player.name} (${player.uniqueId})", "update player")) { + val group = prefixes.api.getPrimaryGroup(player.uniqueId).await() + player.scheduler.run(plugin, { + registry.loadGroup(player.uniqueId, group) + show(player) + }, null) } } fun removePlayer(player: Player) { - cache.remove(player.uniqueId) - playerTeams.remove(player.uniqueId)?.let { team -> - broadcast(ClientboundSetPlayerTeamPacket.createRemovePacket(team)) + val id = player.uniqueId + registry.remove(id) + teams.keys.filter { key -> key.target == id || key.viewer == id }.forEach { key -> + if (key.viewer == id) teams.remove(key) else removeTeam(key) } - publishedEntries.remove(player.uniqueId) - prefixes.sync?.publisher?.publishTablistRemove(player.uniqueId) + unpublish(id) + } + + fun clear() { + unpublishAll() + teams.keys.toList().forEach(::removeTeam) + Bukkit.getOnlinePlayers().forEach { player -> player.playerListName(null) } } - fun syncPlayers(player: Player) { - val connection = (player as CraftPlayer).handle.connection + fun show(player: Player) { + if (!player.isOnline) return + val data = registry.getData(player.uniqueId) ?: return + val features = prefixes.config.get().features + + player.playerListName(if (features.tablist) PlayerDisplayFormatter.formatTablistName(data, player.name) else null) + val team = createTeam(player, data) + Bukkit.getOnlinePlayers().forEach { viewer -> showTo(player, viewer, team) } + + val name = customNameManager.forEntity(player) + name.setName { viewer -> (registry.getViewerData(player.uniqueId, viewer) ?: data).displayName } + name.setHidden(!features.displayName) + + publish(player) + } - playerTeams.values.forEach { team -> - connection.send(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(team, true)) + fun addViewer(viewer: Player) { + Bukkit.getOnlinePlayers().forEach { player -> + val data = registry.getData(player.uniqueId) ?: return@forEach + showTo(player, viewer, createTeam(player, data)) } } - fun sync(force: Boolean = false) { + fun publishAll() { Bukkit.getOnlinePlayers().forEach { player -> - val data = cache[player.uniqueId] ?: return@forEach - val features = prefixes.config.get().features - val displayName = PlayerDisplayFormatter.displayName( - data, - player.name, - features.displayName - ) - publish(player, data, displayName, force) + player.scheduler.run(plugin, { publish(player, true) }, null) } } - private fun publish( - player: Player, - data: PrefixesPlayerData, - displayName: Component, - force: Boolean = false - ) { - publish(player, PlayerDisplayFormatter.formatTablistName(data, displayName), data.priority, force) + fun unpublishAll() { + entries.keys.forEach(::unpublish) } - private fun publish(player: Player, displayName: Component, priority: Int, force: Boolean = false) { - val config = prefixes.config.get() - if (!config.features.tablist || !config.sync.enabled || !config.sync.channels.tablist) return - val publisher = prefixes.sync?.publisher ?: return - val profileProperties = player.playerProfile.properties.map { property -> - ProfileProperty(property.name, property.value, property.signature) + private fun showTo(player: Player, viewer: Player, team: PaperPlayerTeam?) { + val viewerData = registry.getViewerData(player.uniqueId, viewer.uniqueId) + if (viewerData == null) { + updateTeam(player, viewer, team) + return + } + + updateTeam(player, viewer, createTeam(player, viewerData)) + if (prefixes.config.get().features.tablist) { + send(viewer, createListNamePacket(player, PlayerDisplayFormatter.formatTablistName(viewerData, player.name))) } + } + + private fun publish(player: Player, force: Boolean = false) { + if (!prefixes.config.get().isTablistSynced()) return + val publisher = prefixes.sync?.publisher ?: return + val data = registry.getData(player.uniqueId) ?: return val entry = TablistEntry( uniqueId = player.uniqueId, name = player.name, - displayName = displayName, - priority = priority, - profileProperties = profileProperties, + displayName = PlayerDisplayFormatter.formatTablistName(data, player.name), + priority = data.priority, + profileProperties = player.playerProfile.properties.map { property -> ProfileProperty(property.name, property.value, property.signature) }, latency = player.ping, gameMode = TablistGameMode.valueOf(player.gameMode.name), showHat = player.getClientOption(ClientOption.SKIN_PARTS).hasHatsEnabled(), listOrder = player.playerListOrder ) - val previous = publishedEntries.put(player.uniqueId, entry) - if (!force && previous == entry) return - publisher.publishTablistEntry(entry) + val previous = entries.put(player.uniqueId, entry) + if (force || previous != entry) { + publisher.publishTablistEntry(entry) + } } - private fun broadcast(packet: ClientboundSetPlayerTeamPacket) { - Bukkit.getOnlinePlayers().forEach { player -> - (player as CraftPlayer).handle.connection.send(packet) + private fun unpublish(id: UUID) { + if (entries.remove(id) == null) return + prefixes.sync?.publisher?.publishTablistRemove(id) + } + + private fun createTeam(player: Player, data: PrefixesPlayerData): PaperPlayerTeam? { + val features = prefixes.config.get().features + return when { + features.tablist -> PaperPlayerTeam(player.name, data.priority, data.prefix, data.suffix, data.color, features.displayName) + features.displayName -> PaperPlayerTeam(player.name, priority = 0, hideNameTag = true) + else -> null + } + } + + private fun updateTeam(player: Player, viewer: Player, team: PaperPlayerTeam?) { + teams.compute(ViewerKey(player.uniqueId, viewer.uniqueId)) { _, current -> + if (current != null) send(viewer, ClientboundSetPlayerTeamPacket.createRemovePacket(current)) + if (team != null) send(viewer, ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(team, true)) + team + } + } + + private fun removeTeam(key: ViewerKey) { + teams.computeIfPresent(key) { _, team -> + val viewer = Bukkit.getPlayer(key.viewer) + if (viewer != null) send(viewer, ClientboundSetPlayerTeamPacket.createRemovePacket(team)) + null } } + private fun createListNamePacket(player: Player, name: Component): ClientboundPlayerInfoUpdatePacket { + val handle = (player as CraftPlayer).handle + val entry = ClientboundPlayerInfoUpdatePacket.Entry( + player.uniqueId, + handle.gameProfile, + true, + player.ping, + handle.gameMode.gameModeForPlayer, + PaperAdventure.asVanilla(name), + true, + player.playerListOrder, + null + ) + return ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME), listOf(entry)) + } + + private fun send(viewer: Player, packet: Packet<*>) { + (viewer as CraftPlayer).handle.connection.send(packet) + } } diff --git a/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/display/PaperPlayerTeam.kt b/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/display/PaperPlayerTeam.kt index 6b07e0c..fb9dcc3 100644 --- a/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/display/PaperPlayerTeam.kt +++ b/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/display/PaperPlayerTeam.kt @@ -16,7 +16,7 @@ class PaperPlayerTeam( prefix: Component = Component.empty(), suffix: Component = Component.empty(), color: TextColor = NamedTextColor.WHITE, - hideNameTag: Boolean = true + hideNameTag: Boolean ) : PlayerTeam(Scoreboard(), "${PriorityFormatter.format(priority)}_$name") { init { diff --git a/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/display/PaperTablist.kt b/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/display/PaperTablist.kt index 97dcd4d..64a2b3b 100644 --- a/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/display/PaperTablist.kt +++ b/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/display/PaperTablist.kt @@ -1,7 +1,7 @@ package app.simplecloud.prefixes.paper.display +import app.simplecloud.prefixes.shared.sync.tablist.SyncedTablist import app.simplecloud.prefixes.shared.sync.tablist.TablistEntry -import app.simplecloud.prefixes.shared.sync.tablist.TablistGameMode import com.google.common.collect.ImmutableMultimap import com.mojang.authlib.GameProfile import com.mojang.authlib.properties.Property @@ -10,6 +10,7 @@ import io.papermc.paper.adventure.PaperAdventure import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket.Action import net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket import net.minecraft.world.level.GameType import org.bukkit.Bukkit @@ -17,81 +18,19 @@ import org.bukkit.craftbukkit.entity.CraftPlayer import org.bukkit.entity.Player import java.util.EnumSet import java.util.UUID -import java.util.concurrent.ConcurrentHashMap -private const val SYNC_BATCH_SIZE = 100 +class PaperTablist : SyncedTablist>() { -class PaperTablist { + override fun isOnline(id: UUID): Boolean = Bukkit.getPlayer(id) != null - private val entries = ConcurrentHashMap() - - fun update(publisherId: String, entry: TablistEntry) { - if (Bukkit.getPlayer(entry.uniqueId) != null) return - - val previous = entries.put(entry.uniqueId, SourcedTablistEntry(publisherId, entry))?.entry - val actions = getActions(entry, previous) - - if (actions.isNotEmpty()) { - broadcast(createInfoPacket(listOf(entry), actions)) - } - if (previous == null || previous.name != entry.name || previous.priority != entry.priority) { - previous?.let { broadcast(ClientboundSetPlayerTeamPacket.createRemovePacket(createTeam(it))) } - broadcast(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(createTeam(entry), true)) - } - } - - fun remove(id: UUID) { - removeEntry(null, id) - } - - fun remove(publisherId: String, id: UUID) { - removeEntry(publisherId, id) - } - - fun clear() { - entries.keys.toList().forEach(::remove) - } - - private fun removeEntry(publisherId: String?, id: UUID) { - val sourcedEntry = entries[id] ?: return - if (publisherId != null && sourcedEntry.publisherId != publisherId) return - if (!entries.remove(id, sourcedEntry)) return - - if (Bukkit.getPlayer(id) != null) return - - val entry = sourcedEntry.entry - broadcast(ClientboundSetPlayerTeamPacket.createRemovePacket(createTeam(entry))) - broadcast(ClientboundPlayerInfoRemovePacket(listOf(id))) - } - - fun sync(player: Player) { - val connection = (player as CraftPlayer).handle.connection - - val visibleEntries = entries.values - .map(SourcedTablistEntry::entry) - .filter { Bukkit.getPlayer(it.uniqueId) == null } - - visibleEntries.chunked(SYNC_BATCH_SIZE).forEach { batch -> - connection.send(createInfoPacket(batch, getFullUpdateActions())) - } - visibleEntries.forEach { entry -> - connection.send(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(createTeam(entry), true)) - } - } - - private fun createTeam(entry: TablistEntry) = PaperPlayerTeam(entry.name, entry.priority) - - private fun createInfoPacket( - tablistEntries: Collection, - actions: EnumSet - ): ClientboundPlayerInfoUpdatePacket { - val packetEntries = tablistEntries.map { entry -> + override fun createInfoPacket(entries: List): Packet<*> { + val packetEntries = entries.map { entry -> ClientboundPlayerInfoUpdatePacket.Entry( entry.uniqueId, createGameProfile(entry), true, entry.latency, - getGameType(entry.gameMode), + GameType.valueOf(entry.gameMode.name), PaperAdventure.asVanilla(entry.displayName), entry.showHat, entry.listOrder, @@ -99,73 +38,42 @@ class PaperTablist { ) } - return ClientboundPlayerInfoUpdatePacket(actions, packetEntries) + return ClientboundPlayerInfoUpdatePacket(getUpdateActions(), packetEntries) } - private fun getActions( - entry: TablistEntry, - previous: TablistEntry? - ): EnumSet { - if (previous == null || - previous.name != entry.name || - previous.profileProperties != entry.profileProperties - ) { - return getFullUpdateActions() - } + override fun createInfoRemovePacket(id: UUID): Packet<*> = ClientboundPlayerInfoRemovePacket(listOf(id)) - val actions = EnumSet.noneOf(ClientboundPlayerInfoUpdatePacket.Action::class.java) - if (previous.gameMode != entry.gameMode) { - actions.add(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE) - } - if (previous.latency != entry.latency) { - actions.add(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY) - } - if (previous.displayName != entry.displayName) { - actions.add(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME) - } - if (previous.listOrder != entry.listOrder) { - actions.add(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LIST_ORDER) - } - if (previous.showHat != entry.showHat) { - actions.add(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_HAT) - } - return actions + override fun createTeamPacket(entry: TablistEntry): Packet<*> = + ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(createTeam(entry), true) + + override fun createTeamRemovePacket(entry: TablistEntry): Packet<*> = + ClientboundSetPlayerTeamPacket.createRemovePacket(createTeam(entry)) + + override fun send(player: Player, packet: Packet<*>) { + (player as CraftPlayer).handle.connection.send(packet) } - private fun getFullUpdateActions(): EnumSet = - EnumSet.of( - ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER, - ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED, - ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE, - ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY, - ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME, - ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LIST_ORDER, - ClientboundPlayerInfoUpdatePacket.Action.UPDATE_HAT - ) + override fun broadcast(packet: Packet<*>) { + Bukkit.getOnlinePlayers().forEach { player -> send(player, packet) } + } + + private fun createTeam(entry: TablistEntry): PaperPlayerTeam = PaperPlayerTeam(entry.name, entry.priority, hideNameTag = true) + + private fun getUpdateActions(): EnumSet = EnumSet.of( + Action.ADD_PLAYER, + Action.UPDATE_LISTED, + Action.UPDATE_GAME_MODE, + Action.UPDATE_LATENCY, + Action.UPDATE_DISPLAY_NAME, + Action.UPDATE_LIST_ORDER, + Action.UPDATE_HAT + ) private fun createGameProfile(entry: TablistEntry): GameProfile { val properties = ImmutableMultimap.builder() - entry.profileProperties.forEach { property -> - val profileProperty = property.signature - ?.let { Property(property.name, property.value, it) } - ?: Property(property.name, property.value) - properties.put(property.name, profileProperty) + properties.put(property.name, Property(property.name, property.value, property.signature)) } - return GameProfile(entry.uniqueId, entry.name, PropertyMap(properties.build())) } - - private fun getGameType(gameMode: TablistGameMode): GameType = when (gameMode) { - TablistGameMode.SURVIVAL -> GameType.SURVIVAL - TablistGameMode.CREATIVE -> GameType.CREATIVE - TablistGameMode.ADVENTURE -> GameType.ADVENTURE - TablistGameMode.SPECTATOR -> GameType.SPECTATOR - } - - private fun broadcast(packet: Packet<*>) { - Bukkit.getOnlinePlayers().forEach { player -> - (player as CraftPlayer).handle.connection.send(packet) - } - } } diff --git a/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/display/SourcedTablistEntry.kt b/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/display/SourcedTablistEntry.kt deleted file mode 100644 index 3809640..0000000 --- a/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/display/SourcedTablistEntry.kt +++ /dev/null @@ -1,8 +0,0 @@ -package app.simplecloud.prefixes.paper.display - -import app.simplecloud.prefixes.shared.sync.tablist.TablistEntry - -internal data class SourcedTablistEntry( - val publisherId: String, - val entry: TablistEntry -) diff --git a/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/listener/LuckPermsListener.kt b/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/listener/LuckPermsListener.kt deleted file mode 100644 index 661e531..0000000 --- a/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/listener/LuckPermsListener.kt +++ /dev/null @@ -1,39 +0,0 @@ -package app.simplecloud.prefixes.paper.listener - -import app.simplecloud.prefixes.paper.display.PaperDisplayManager -import net.luckperms.api.LuckPerms -import net.luckperms.api.event.node.NodeMutateEvent -import net.luckperms.api.event.user.UserDataRecalculateEvent -import net.luckperms.api.model.user.User -import org.bukkit.Bukkit -import org.bukkit.plugin.Plugin - -class LuckPermsListener( - private val plugin: Plugin, - private val luckPerms: LuckPerms, - private val manager: PaperDisplayManager -) { - - fun register() { - luckPerms.eventBus.subscribe(plugin, UserDataRecalculateEvent::class.java) { event -> - Bukkit.getScheduler().runTask(plugin, Runnable { - val player = Bukkit.getPlayer(event.user.uniqueId) ?: return@Runnable - manager.updatePlayer(player) - }) - } - - luckPerms.eventBus.subscribe(plugin, NodeMutateEvent::class.java) { event -> - Bukkit.getScheduler().runTask(plugin, Runnable { - if (event.isUser) { - val user = event.target as? User ?: return@Runnable - val player = Bukkit.getPlayer(user.uniqueId) ?: return@Runnable - manager.updatePlayer(player) - } else if (event.isGroup) { - Bukkit.getOnlinePlayers().forEach { player -> - manager.updatePlayer(player) - } - } - }) - } - } -} \ No newline at end of file diff --git a/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/listener/PlayerListener.kt b/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/listener/PlayerListener.kt index 59bcbcf..a25d615 100644 --- a/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/listener/PlayerListener.kt +++ b/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/listener/PlayerListener.kt @@ -3,7 +3,6 @@ package app.simplecloud.prefixes.paper.listener import app.simplecloud.prefixes.paper.display.PaperDisplayManager import app.simplecloud.prefixes.paper.display.PaperTablist import app.simplecloud.prefixes.shared.Prefixes -import app.simplecloud.prefixes.shared.sync.SyncPublisher import app.simplecloud.prefixes.shared.utilities.PlayerDisplayFormatter import io.papermc.paper.event.player.AsyncChatEvent import org.bukkit.event.EventHandler @@ -15,19 +14,18 @@ import org.bukkit.event.player.PlayerQuitEvent class PlayerListener( private val prefixes: Prefixes, private val manager: PaperDisplayManager, - private val tablist: PaperTablist, - private val publisher: SyncPublisher? + private val tablist: PaperTablist ) : Listener { @EventHandler fun onJoin(event: PlayerJoinEvent) { - tablist.remove(event.player.uniqueId) - manager.addPlayer(event.player) - manager.syncPlayers(event.player) + val player = event.player + tablist.remove(player.uniqueId) + manager.updatePlayer(player) + manager.addViewer(player) - val config = prefixes.config.get() - if (config.features.tablist && config.sync.enabled && config.sync.channels.tablist) { - tablist.sync(event.player) + if (prefixes.config.get().isTablistSynced()) { + tablist.sync(player) } } @@ -42,17 +40,13 @@ class PlayerListener( if (!features.chat) return val player = event.player - val data = manager.getPlayer(player.uniqueId) ?: return - val displayName = PlayerDisplayFormatter.displayName(data, player.name, features.displayName) - - val message = PlayerDisplayFormatter.formatChatMessage( - data, - player.name, - event.message(), - displayName - ) - - event.renderer { _, _, _, _ -> message } - publisher?.publishChatMessage(message) + val data = prefixes.dataRegistry.getData(player.uniqueId) ?: return + val message = PlayerDisplayFormatter.formatChatMessage(data, player.name, event.message(), features.displayName) + + event.renderer { _, _, _, viewer -> + val viewerData = prefixes.dataRegistry.getViewerData(player.uniqueId, viewer) + if (viewerData == null) message else PlayerDisplayFormatter.formatChatMessage(viewerData, player.name, event.message(), features.displayName) + } + prefixes.sync?.publisher?.publishChatMessage(message) } } diff --git a/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/platform/PaperPlatformImpl.kt b/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/platform/PaperPlatformImpl.kt index b779aaf..70eeff5 100644 --- a/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/platform/PaperPlatformImpl.kt +++ b/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/platform/PaperPlatformImpl.kt @@ -1,29 +1,33 @@ package app.simplecloud.prefixes.paper.platform -import app.simplecloud.prefixes.shared.platform.PrefixesPlatform import app.simplecloud.plugin.api.shared.permission.PermissionChecker +import app.simplecloud.prefixes.shared.platform.PrefixesPlatform import net.luckperms.api.LuckPerms import org.bukkit.Bukkit import org.bukkit.plugin.Plugin import java.io.File import java.util.UUID -class PaperPlatformImpl(plugin: Plugin) : PrefixesPlatform { +class PaperPlatformImpl(private val plugin: Plugin) : PrefixesPlatform { - override val dataDirectory: File = plugin.dataFolder + override fun getDataDirectory(): File { + return plugin.dataFolder + } - override val permissionChecker: PermissionChecker = PermissionChecker { id, permission -> - Bukkit.getPlayer(id)?.hasPermission(permission) ?: false + override fun getPermissionChecker(): PermissionChecker { + return PermissionChecker { id, permission -> + Bukkit.getPlayer(id)?.hasPermission(permission) ?: false + } } - override val playerResolver: (UUID) -> String = { id -> - Bukkit.getOfflinePlayer(id).name ?: id.toString() + override fun getLuckPerms(): LuckPerms? { + if (!Bukkit.getPluginManager().isPluginEnabled("LuckPerms")) return null + return Bukkit.getServicesManager().getRegistration(LuckPerms::class.java)?.provider } - override val luckPerms: LuckPerms? - get() = if (Bukkit.getPluginManager().isPluginEnabled("LuckPerms")) { - Bukkit.getServicesManager().getRegistration(LuckPerms::class.java)?.provider - } else { - null - } -} \ No newline at end of file + override fun getPlayerName(id: UUID): String { + return Bukkit.getOfflinePlayer(id).name ?: id.toString() + } + + override fun isOnline(id: UUID): Boolean = Bukkit.getPlayer(id) != null +} diff --git a/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/platform/PaperPrefixesListener.kt b/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/platform/PaperPrefixesListener.kt new file mode 100644 index 0000000..134f7c5 --- /dev/null +++ b/prefixes-paper/src/main/kotlin/app/simplecloud/prefixes/paper/platform/PaperPrefixesListener.kt @@ -0,0 +1,50 @@ +package app.simplecloud.prefixes.paper.platform + +import app.simplecloud.prefixes.paper.display.PaperDisplayManager +import app.simplecloud.prefixes.paper.display.PaperTablist +import app.simplecloud.prefixes.shared.Prefixes +import app.simplecloud.prefixes.shared.platform.PrefixesListener +import org.bukkit.Bukkit +import org.bukkit.plugin.Plugin +import java.util.UUID + +class PaperPrefixesListener( + private val plugin: Plugin, + private val prefixes: Prefixes, + private val manager: PaperDisplayManager, + private val tablist: PaperTablist +) : PrefixesListener { + + override fun onReload() { + onAllPlayersUpdate() + + if (!prefixes.config.get().isTablistSynced()) { + manager.unpublishAll() + tablist.clear() + return + } + + prefixes.sync?.publisher?.publishTablistRequest() + } + + override fun onShutdown() { + manager.clear() + tablist.clear() + } + + override fun onPlayerUpdate(id: UUID) { + val player = Bukkit.getPlayer(id) ?: return + manager.updatePlayer(player) + } + + override fun onPlayerDataChange(id: UUID) { + val player = Bukkit.getPlayer(id) ?: return + player.scheduler.run(plugin, { manager.show(player) }, null) + } + + override fun onAllPlayersUpdate() { + Bukkit.getOnlinePlayers().forEach { player -> + manager.updatePlayer(player) + } + } +} diff --git a/prefixes-paper/src/main/resources/paper-plugin.yml b/prefixes-paper/src/main/resources/paper-plugin.yml index a18be6d..435f5c0 100644 --- a/prefixes-paper/src/main/resources/paper-plugin.yml +++ b/prefixes-paper/src/main/resources/paper-plugin.yml @@ -1,15 +1,13 @@ name: simplecloud-prefixes version: ${version} -api-version: 26.2 +description: Prefixes, suffixes and name colors for chat, tab list and name tags. Works with LuckPerms and syncs across your SimpleCloud network +api-version: 26.3 +folia-supported: true main: app.simplecloud.prefixes.paper.PrefixesPaper dependencies: server: - simplecloud-api: - load: BEFORE - required: true - join-classpath: true LuckPerms: load: BEFORE required: false diff --git a/prefixes-paper/src/test/kotlin/app/simplecloud/prefixes/paper/CloudItemStackParserTest.kt b/prefixes-paper/src/test/kotlin/app/simplecloud/prefixes/paper/CloudItemStackParserTest.kt deleted file mode 100644 index e70281e..0000000 --- a/prefixes-paper/src/test/kotlin/app/simplecloud/prefixes/paper/CloudItemStackParserTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package app.simplecloud.prefixes.paper - -import net.minecraft.SharedConstants -import net.minecraft.server.Bootstrap -import org.incendo.cloud.bukkit.parser.ItemStackParser -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -class CloudItemStackParserTest { - @Test - fun `item parser initializes against Paper 26_3`() { - SharedConstants.tryDetectVersion() - Bootstrap.bootStrap() - - assertNotNull(ItemStackParser.itemStackParser()) - val parser = Class.forName("org.incendo.cloud.bukkit.parser.ItemStackParser\$ModernParser") - val field = parser.getDeclaredField("AS_BUKKIT_STACK_METHOD") - field.isAccessible = true - val method = field.get(null) as java.lang.reflect.Method - assertEquals("asBukkitMirror", method.name) - } -} diff --git a/prefixes-shared/build.gradle.kts b/prefixes-shared/build.gradle.kts index 782aaec..9db4d92 100644 --- a/prefixes-shared/build.gradle.kts +++ b/prefixes-shared/build.gradle.kts @@ -5,11 +5,14 @@ plugins { dependencies { api(project(":prefixes-api")) api(libs.simplecloud.plugin.api) - implementation(libs.jnats) - implementation(libs.protobuf.kotlin) + implementation(libs.jnats) { + // only needed for NKey + exclude(group = "org.bouncycastle") + } + implementation(libs.slf4j.api) + implementation(libs.protobuf.kotlin.lite) implementation(libs.bundles.configurate) implementation(libs.bundles.cloud.command) - testImplementation(libs.luckperms.api) } sourceSets { @@ -34,7 +37,12 @@ protobuf { generateProtoTasks { all().forEach { it.builtins { - create("kotlin") + named("java") { + option("lite") + } + create("kotlin") { + option("lite") + } } } } @@ -42,6 +50,6 @@ protobuf { configurations.all { resolutionStrategy { - force("com.google.protobuf:protobuf-java:4.35.1") + force("com.google.protobuf:protobuf-javalite:4.36.2") } -} +} \ No newline at end of file diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/Prefixes.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/Prefixes.kt index 4ef5816..4d60dff 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/Prefixes.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/Prefixes.kt @@ -1,76 +1,134 @@ package app.simplecloud.prefixes.shared -import app.simplecloud.prefixes.api.PrefixesApi +import app.simplecloud.plugin.api.shared.config.ConfigurationFactory import app.simplecloud.prefixes.api.PrefixesApiProvider import app.simplecloud.prefixes.shared.api.PrefixesApiImpl import app.simplecloud.prefixes.shared.config.DefaultConfigInstaller import app.simplecloud.prefixes.shared.config.MessageConfig import app.simplecloud.prefixes.shared.config.PrefixesConfig -import app.simplecloud.prefixes.shared.config.SourceType +import app.simplecloud.prefixes.shared.config.SyncConfig +import app.simplecloud.prefixes.shared.data.PrefixesDataRegistry +import app.simplecloud.prefixes.shared.group.GroupProviderRegistry import app.simplecloud.prefixes.shared.group.config.ConfigGroupProvider import app.simplecloud.prefixes.shared.group.luckperms.LuckPermsGroupProvider +import app.simplecloud.prefixes.shared.group.luckperms.LuckPermsListener +import app.simplecloud.prefixes.shared.platform.PrefixesListener import app.simplecloud.prefixes.shared.platform.PrefixesPlatform import app.simplecloud.prefixes.shared.sync.PrefixesSync -import app.simplecloud.plugin.api.shared.config.ConfigurationFactory +import app.simplecloud.prefixes.shared.utilities.ColorParser +import kotlinx.coroutines.cancelChildren +import net.luckperms.api.event.EventSubscription import java.io.File +import java.util.concurrent.CopyOnWriteArrayList class Prefixes(private val platform: PrefixesPlatform) { - private val configFile = File(platform.dataDirectory, "config.yml").also { file -> - DefaultConfigInstaller.install(file.toPath(), javaClass.classLoader) - } - val config = ConfigurationFactory( - configFile, - PrefixesConfig::class.java - ) - val messages = ConfigurationFactory(File(platform.dataDirectory, "messages.yml"), MessageConfig::class.java) - - init { - config.loadOrCreate(PrefixesConfig()) - messages.loadOrCreate(MessageConfig()) - } + private val logger = PrefixesConstants.LOGGER + private val listeners = CopyOnWriteArrayList() + private val subscriptions = CopyOnWriteArrayList>() + private val luckPerms = platform.getLuckPerms() - private val configGroupProvider = ConfigGroupProvider(config, platform.permissionChecker) - private val lpGroupProvider = platform.luckPerms?.let { LuckPermsGroupProvider(it) } + val config = createConfig() + val messages = createMessages() - val api = createApi() + val registry = createGroupRegistry() + val dataRegistry = PrefixesDataRegistry(platform) + val api = PrefixesApiImpl(registry, dataRegistry, listeners, platform) val sync = createSync() fun startup() { PrefixesApiProvider.register(api) + validateConfig() + + if (luckPerms != null) { + subscriptions.addAll(LuckPermsListener(luckPerms, api).register()) + } } fun shutdown() { + subscriptions.forEach { subscription -> subscription.close() } + listeners.forEach(PrefixesListener::onShutdown) PrefixesApiProvider.unregister() sync?.shutdown() + PrefixesConstants.SCOPE.coroutineContext.cancelChildren() + } + + fun addListener(listener: PrefixesListener) { + listeners.add(listener) + } + + fun reload(): Boolean { + return runCatching { + logger.info("Reloading simplecloud prefixes...") + val previousSync = config.get().sync + config.reload() + messages.reload() + validateConfig() + warnIfRestartRequired(previousSync) + listeners.forEach(PrefixesListener::onReload) + }.onSuccess { + logger.info("Successfully reloaded simplecloud prefixes") + }.onFailure { throwable -> + logger.error("Failed to reload simplecloud prefixes", throwable) + }.isSuccess + } + + private fun validateConfig() { + registry.validateSource() + config.get().groups + .filter { group -> group.color.isNotBlank() && ColorParser.parse(group.color) == null } + .forEach { group -> logger.warn("The color '${group.color}' of the group '${group.name}' is invalid, using white instead") } + } + + private fun warnIfRestartRequired(previous: SyncConfig) { + val current = config.get().sync + if ((sync == null && current.enabled) || previous.sources != current.sources) { + logger.warn("Enabling sync or changing sync.sources only takes effect after a restart") + } + } + + private fun createConfig(): ConfigurationFactory { + val file = File(platform.getDataDirectory(), "config.yml") + DefaultConfigInstaller.install(file.toPath(), javaClass.classLoader) + + val factory = ConfigurationFactory(file, PrefixesConfig::class.java) + factory.loadOrCreate(PrefixesConfig()) + return factory } - fun reload() { - config.reload() - messages.reload() - onReload?.invoke() + private fun createMessages(): ConfigurationFactory { + val file = File(platform.getDataDirectory(), "messages.yml") + DefaultConfigInstaller.install(file.toPath(), javaClass.classLoader) + + val factory = ConfigurationFactory(file, MessageConfig::class.java) + factory.loadOrCreate(MessageConfig()) + return factory } - var onReload: (() -> Unit)? = null - - private fun createApi(): PrefixesApi = PrefixesApiImpl({ - val source = config.get().general.source - if (source == SourceType.LUCKPERMS && lpGroupProvider != null) { - lpGroupProvider - } else { - configGroupProvider - }}, - platform.playerResolver - ) - - private fun createSync(): PrefixesSync? = config.get() - .takeIf { config -> - config.sync.enabled && - ( - config.features.chat && config.sync.channels.chat || - config.features.tablist && config.sync.channels.tablist - ) + private fun createGroupRegistry(): GroupProviderRegistry { + val registry = GroupProviderRegistry(config, ConfigGroupProvider(config, platform.getPermissionChecker())) + + if (luckPerms != null) { + registry.register(LuckPermsGroupProvider(luckPerms)) } - ?.let { PrefixesSync(config) } + return registry + } + + private fun createSync(): PrefixesSync? { + if (!config.get().sync.enabled) return null + + if (!isSimpleCloudAvailable()) { + logger.warn("Sync is enabled in the config, but this Server is not running in a simplecloud network.") + return null + } + + return runCatching { + PrefixesSync(config) + }.onFailure { throwable -> + logger.error("Failed to initialize sync", throwable) + }.getOrNull() + } + + private fun isSimpleCloudAvailable(): Boolean = System.getenv("SIMPLECLOUD_UNIQUE_ID") != null } diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/PrefixesConstants.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/PrefixesConstants.kt new file mode 100644 index 0000000..df7e8a7 --- /dev/null +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/PrefixesConstants.kt @@ -0,0 +1,46 @@ +package app.simplecloud.prefixes.shared + +import app.simplecloud.prefixes.shared.utilities.PrefixesCoroutineDetails +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +object PrefixesConstants { + + const val CONFIG_SOURCE = "config" + + const val CURRENT_SYNC_SOURCE = "CURRENT" + const val ALL_SYNC_SOURCE = "ALL" + + const val DEFAULT_DISPLAY_NAME = "" + const val DEFAULT_CHAT_FORMAT = " <#475569>» <#F8FAFC>" + + val LOGGER: Logger = LoggerFactory.getLogger("simplecloud-prefixes") + + val SCOPE = CoroutineScope( + CoroutineName("simplecloud-prefixes") + + SupervisorJob() + + Dispatchers.IO + + CoroutineExceptionHandler { context, exception -> + val details = context[PrefixesCoroutineDetails] + if (details == null) { + LOGGER.error("An error occurred in a coroutine!", exception) + return@CoroutineExceptionHandler + } + + LOGGER.error( + """ + An error occurred in a coroutine! + - Player: ${details.player ?: "N/A"} + - Launch reason: ${details.reason} + """.trimIndent(), + exception + ) + } + ) + +} \ No newline at end of file diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/api/PrefixesApiImpl.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/api/PrefixesApiImpl.kt index e092ba4..6c82b39 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/api/PrefixesApiImpl.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/api/PrefixesApiImpl.kt @@ -1,47 +1,157 @@ package app.simplecloud.prefixes.shared.api -import app.simplecloud.plugin.api.shared.extension.miniMessage import app.simplecloud.prefixes.api.PrefixesApi import app.simplecloud.prefixes.api.group.GroupProvider import app.simplecloud.prefixes.api.group.PrefixesGroup import app.simplecloud.prefixes.api.group.PrefixesPlayerData +import app.simplecloud.prefixes.shared.data.PrefixesDataRegistry +import app.simplecloud.prefixes.shared.data.PrefixesOverride +import app.simplecloud.prefixes.shared.group.GroupProviderRegistry +import app.simplecloud.prefixes.shared.platform.PrefixesListener +import app.simplecloud.prefixes.shared.platform.PrefixesPlatform +import app.simplecloud.prefixes.shared.utilities.AudienceResolver +import app.simplecloud.prefixes.shared.PrefixesConstants +import kotlinx.coroutines.future.await +import kotlinx.coroutines.future.future +import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import net.kyori.adventure.text.format.TextColor import java.util.UUID import java.util.concurrent.CompletableFuture class PrefixesApiImpl( - private val provider: () -> GroupProvider, - private val playerResolver: (UUID) -> String + private val registry: GroupProviderRegistry, + private val dataRegistry: PrefixesDataRegistry, + private val listeners: List, + private val platform: PrefixesPlatform ) : PrefixesApi { - override fun getGroups(): Collection = provider().getGroups() + override fun getGroups(): CompletableFuture> = registry.getCurrentGroupProvider().getGroups() - override fun getPrimaryGroup(id: UUID): CompletableFuture = provider().getGroup(id) - - override fun addGroup(group: PrefixesGroup): CompletableFuture = provider().addGroup(group) + override fun getPrimaryGroup(id: UUID): CompletableFuture = registry.getCurrentGroupProvider().getGroup(id) override fun getPrefixData(id: UUID): CompletableFuture { - return getPrimaryGroup(id).thenApply { group -> - val prefix = group?.prefix ?: Component.empty() - val suffix = group?.suffix ?: Component.empty() - val color = group?.color ?: NamedTextColor.WHITE - val priority = group?.priority ?: 0 - val displayName = group?.displayName ?: "" - val chatFormat = group?.chatFormat ?: " " - - val player = playerResolver(id) - val display = miniMessage.deserialize( - displayName, - Placeholder.styling("color", color), - Placeholder.unparsed("playername", player) - ) - - PrefixesPlayerData(prefix, suffix, color, display, chatFormat, priority) + return PrefixesConstants.SCOPE.future { dataRegistry.getData(id, getPrimaryGroup(id).await()) } + } + + override fun getPrefixData(id: UUID, viewer: Audience): CompletableFuture { + val viewerId = AudienceResolver.resolveId(viewer) ?: return getPrefixData(id) + return PrefixesConstants.SCOPE.future { dataRegistry.getData(id, getPrimaryGroup(id).await(), viewerId) } + } + + override fun setPrefix(id: UUID, prefix: Component) = set(id, PrefixesOverride(prefix = prefix)) + + override fun setPrefix(id: UUID, prefix: Component, viewers: Audience) = set(id, PrefixesOverride(prefix = prefix), viewers) + + override fun setSuffix(id: UUID, suffix: Component) = set(id, PrefixesOverride(suffix = suffix)) + override fun setSuffix(id: UUID, suffix: Component, viewers: Audience) = set(id, PrefixesOverride(suffix = suffix), viewers) + + override fun setColor(id: UUID, color: TextColor) = set(id, PrefixesOverride(color = color)) + + override fun setColor(id: UUID, color: TextColor, viewers: Audience) = set(id, PrefixesOverride(color = color), viewers) + + override fun setPriority(id: UUID, priority: Int) = set(id, PrefixesOverride(priority = priority)) + + override fun setPriority(id: UUID, priority: Int, viewers: Audience) = set(id, PrefixesOverride(priority = priority), viewers) + + override fun setGroup(id: UUID, group: String): CompletableFuture { + return PrefixesConstants.SCOPE.future { + val found = findGroup(group) ?: return@future false + setGroup(id, found) + true } + } + override fun setGroup(id: UUID, group: String, viewers: Audience): CompletableFuture { + return PrefixesConstants.SCOPE.future { + val found = findGroup(group) ?: return@future false + setGroup(id, found, viewers) + true + } + } + + override fun setGroup(targets: Audience, group: String): CompletableFuture { + return PrefixesConstants.SCOPE.future { + val found = findGroup(group) ?: return@future false + setGroup(targets, found) + true + } + } + + override fun setGroup(targets: Audience, group: String, viewers: Audience): CompletableFuture { + return PrefixesConstants.SCOPE.future { + val found = findGroup(group) ?: return@future false + setGroup(targets, found, viewers) + true + } + } + + override fun setGroup(id: UUID, group: PrefixesGroup) = set(id, PrefixesOverride.of(group)) + + override fun setGroup(id: UUID, group: PrefixesGroup, viewers: Audience) = set(id, PrefixesOverride.of(group), viewers) + + override fun setGroup(targets: Audience, group: PrefixesGroup) { + AudienceResolver.resolveIds(targets).forEach { id -> setGroup(id, group) } } -} \ No newline at end of file + override fun setGroup(targets: Audience, group: PrefixesGroup, viewers: Audience) { + val viewerIds = AudienceResolver.resolveIds(viewers) + AudienceResolver.resolveIds(targets).forEach { id -> set(id, PrefixesOverride.of(group), viewerIds) } + } + + override fun reset(id: UUID) { + dataRegistry.reset(id) + notifyDataChange(id) + } + + override fun reset(targets: Audience) { + AudienceResolver.resolveIds(targets).forEach(::reset) + } + + override fun reset(targets: Audience, viewers: Audience) { + val viewerIds = AudienceResolver.resolveIds(viewers) + AudienceResolver.resolveIds(targets).forEach { id -> + viewerIds.forEach { viewer -> dataRegistry.reset(id, viewer) } + notifyDataChange(id) + } + } + + override fun addGroup(group: PrefixesGroup): CompletableFuture = registry.getCurrentGroupProvider().addGroup(group) + + override fun registerGroupProvider(provider: GroupProvider) = registry.register(provider) + + override fun unregisterGroupProvider(name: String) = registry.unregister(name) + + override fun getGroupProvider(): GroupProvider = registry.getCurrentGroupProvider() + + override fun getGroupProviders(): Collection = registry.getAllGroupProviders() + + override fun refreshPlayer(id: UUID) { listeners.forEach { it.onPlayerUpdate(id) } } + + override fun refreshAll() { listeners.forEach(PrefixesListener::onAllPlayersUpdate) } + + private fun set(id: UUID, override: PrefixesOverride) { + if (!platform.isOnline(id)) return + dataRegistry.set(id, override) + notifyDataChange(id) + } + + private fun set(id: UUID, override: PrefixesOverride, viewers: Audience) { + set(id, override, AudienceResolver.resolveIds(viewers)) + } + + private fun set(id: UUID, override: PrefixesOverride, viewers: List) { + if (!platform.isOnline(id)) return + viewers.forEach { viewer -> dataRegistry.set(id, viewer, override) } + notifyDataChange(id) + } + + private suspend fun findGroup(name: String): PrefixesGroup? { + return getGroups().await().firstOrNull { group -> group.name.equals(name, ignoreCase = true) } + } + + private fun notifyDataChange(id: UUID) { + listeners.forEach { listener -> listener.onPlayerDataChange(id) } + } +} diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/command/PrefixesCommand.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/command/PrefixesCommand.kt index ea0b15d..34e9930 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/command/PrefixesCommand.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/command/PrefixesCommand.kt @@ -4,42 +4,23 @@ import app.simplecloud.prefixes.shared.Prefixes import app.simplecloud.prefixes.shared.utilities.PrefixesPermissions import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder import org.incendo.cloud.CommandManager -import org.incendo.cloud.kotlin.coroutines.extension.suspendingHandler -import java.util.logging.Level -import java.util.logging.Logger class PrefixesCommand( private val manager: CommandManager, private val prefixes: Prefixes ) { - private val logger = Logger.getLogger(PrefixesCommand::class.java.name) - private val messages get() = prefixes.messages.get() fun register() { - registerRoot() registerHelp() registerReload() } - private fun registerRoot() { - manager.command( - manager.commandBuilder("scprefix") - .handler { - sendHelp(it.sender()) - } - ) - } - private fun registerHelp() { - manager.command( - manager.commandBuilder("scprefix") - .literal("help") - .handler { - sendHelp(it.sender()) - } - ) + val root = manager.commandBuilder("scprefix") + manager.command(root.handler { sendHelp(it.sender()) }) + manager.command(root.literal("help").handler { sendHelp(it.sender()) }) } private fun registerReload() { @@ -47,21 +28,9 @@ class PrefixesCommand( manager.commandBuilder("scprefix") .literal("reload") .permission(PrefixesPermissions.RELOAD) - .suspendingHandler { - val sender = it.sender() - if (!sender.hasPermission(PrefixesPermissions.RELOAD)) { - sender.sendMessage(messages.msg(messages.command.permission.denied)) - return@suspendingHandler - } - - runCatching { - prefixes.reload() - }.onSuccess { - sender.sendMessage(messages.msg(messages.command.reload.success)) - }.onFailure { throwable -> - logger.log(Level.SEVERE, "Failed to reload SimpleCloud Prefixes", throwable) - sender.sendMessage(messages.msg(messages.command.reload.failed)) - } + .handler { + val message = if (prefixes.reload()) messages.command.reload.success else messages.command.reload.failed + it.sender().sendMessage(messages.msg(message)) } ) } @@ -79,4 +48,4 @@ class PrefixesCommand( sender.sendMessage(messages.msg(messages.command.help.entry, Placeholder.unparsed("command", "/scprefix reload"))) } } -} \ No newline at end of file +} diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/config/DefaultConfigInstaller.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/config/DefaultConfigInstaller.kt index 7ee656b..21a3161 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/config/DefaultConfigInstaller.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/config/DefaultConfigInstaller.kt @@ -5,13 +5,12 @@ import java.nio.file.Path object DefaultConfigInstaller { - private const val CONFIG_RESOURCE = "config.yml" - fun install(target: Path, classLoader: ClassLoader) { if (Files.exists(target)) return - val resource = checkNotNull(classLoader.getResourceAsStream(CONFIG_RESOURCE)) { - "Missing bundled $CONFIG_RESOURCE" + val name = target.fileName.toString() + val resource = checkNotNull(classLoader.getResourceAsStream(name)) { + "Missing bundled $name" } Files.createDirectories(target.parent) diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/config/MessageConfig.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/config/MessageConfig.kt index bbf1fe0..e5ee579 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/config/MessageConfig.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/config/MessageConfig.kt @@ -3,20 +3,18 @@ package app.simplecloud.prefixes.shared.config import app.simplecloud.plugin.api.shared.config.AbstractMessageConfig import app.simplecloud.plugin.api.shared.config.VersionedConfig import app.simplecloud.prefixes.shared.utilities.config.ConfigVersion -import app.simplecloud.prefixes.shared.utilities.config.DefaultConfigs import org.spongepowered.configurate.objectmapping.ConfigSerializable @ConfigSerializable data class MessageConfig( override val version: Int = ConfigVersion.VERSION, - override val variables: Map = DefaultConfigs.VARIABLES, + override val variables: Map = mapOf("prefix" to "⚡"), val command: CommandMessages = CommandMessages() ) : VersionedConfig, AbstractMessageConfig() @ConfigSerializable data class CommandMessages( val help: CommandHelpMessages = CommandHelpMessages(), - val usage: CommandUsageMessages = CommandUsageMessages(), val permission: CommandPermissionMessages = CommandPermissionMessages(), val reload: CommandReloadMessages = CommandReloadMessages() ) @@ -27,11 +25,6 @@ data class CommandHelpMessages( val entry: String = "<#E2E8F0>" ) -@ConfigSerializable -data class CommandUsageMessages( - val invalid: String = " <#DC2626>Use <#F8FAFC> <#DC2626>instead." -) - @ConfigSerializable data class CommandPermissionMessages( val denied: String = " <#DC2626>You do not have permission to use this command." diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/config/PrefixesConfig.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/config/PrefixesConfig.kt index 3461ae4..ba47fe9 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/config/PrefixesConfig.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/config/PrefixesConfig.kt @@ -1,8 +1,8 @@ package app.simplecloud.prefixes.shared.config import app.simplecloud.plugin.api.shared.config.VersionedConfig +import app.simplecloud.prefixes.shared.PrefixesConstants import app.simplecloud.prefixes.shared.utilities.config.ConfigVersion -import app.simplecloud.prefixes.shared.utilities.config.DefaultConfigs import org.spongepowered.configurate.objectmapping.ConfigSerializable @ConfigSerializable @@ -11,20 +11,20 @@ data class PrefixesConfig( val general: GeneralConfig = GeneralConfig(), val features: FeaturesConfig = FeaturesConfig(), val sync: SyncConfig = SyncConfig(), - val groups: List = DefaultConfigs.GROUPS -) : VersionedConfig + val groups: List = emptyList() +) : VersionedConfig { + + fun isChatSynced(): Boolean = features.chat && sync.enabled && sync.channels.chat + + fun isTablistSynced(): Boolean = features.tablist && sync.enabled && sync.channels.tablist +} @ConfigSerializable data class GeneralConfig( - val source: SourceType = SourceType.CONFIG, + val source: String = PrefixesConstants.CONFIG_SOURCE, val defaultGroup: String = "default" ) -enum class SourceType { - CONFIG, - LUCKPERMS -} - @ConfigSerializable data class FeaturesConfig( val chat: Boolean = true, @@ -34,9 +34,9 @@ data class FeaturesConfig( @ConfigSerializable data class SyncConfig( - val enabled: Boolean = true, + val enabled: Boolean = false, val channels: SyncChannels = SyncChannels(), - val sources: List = listOf(CURRENT_SYNC_SOURCE) + val sources: List = listOf(PrefixesConstants.CURRENT_SYNC_SOURCE) ) @ConfigSerializable @@ -55,7 +55,4 @@ data class ConfigGroup( val color: String = "", val displayName: String = "", val chatFormat: String = "" -) - -const val CURRENT_SYNC_SOURCE = "CURRENT" -const val ALL_SYNC_SOURCE = "ALL" +) \ No newline at end of file diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/data/PrefixesDataRegistry.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/data/PrefixesDataRegistry.kt new file mode 100644 index 0000000..21ac740 --- /dev/null +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/data/PrefixesDataRegistry.kt @@ -0,0 +1,94 @@ +package app.simplecloud.prefixes.shared.data + +import app.simplecloud.plugin.api.shared.extension.miniMessage +import app.simplecloud.prefixes.api.group.PrefixesGroup +import app.simplecloud.prefixes.api.group.PrefixesPlayerData +import app.simplecloud.prefixes.shared.platform.PrefixesPlatform +import app.simplecloud.prefixes.shared.utilities.AudienceResolver +import app.simplecloud.prefixes.shared.PrefixesConstants +import net.kyori.adventure.audience.Audience +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +class PrefixesDataRegistry(private val platform: PrefixesPlatform) { + + private val groups = ConcurrentHashMap() + private val overrides = ConcurrentHashMap() + private val viewerOverrides = ConcurrentHashMap() + + fun getData(id: UUID): PrefixesPlayerData? { + val group = groups[id] ?: return null + return createData(id, group.merge(overrides[id])) + } + + fun getData(id: UUID, group: PrefixesGroup?): PrefixesPlayerData { + return createData(id, PrefixesOverride.of(group).merge(overrides[id])) + } + + fun getData(id: UUID, group: PrefixesGroup?, viewer: UUID): PrefixesPlayerData { + return createData(id, PrefixesOverride.of(group).merge(overrides[id]).merge(viewerOverrides[ViewerKey(id, viewer)])) + } + + fun getViewerData(id: UUID, viewer: UUID): PrefixesPlayerData? { + val group = groups[id] ?: return null + val viewerOverride = viewerOverrides[ViewerKey(id, viewer)] ?: return null + return createData(id, group.merge(overrides[id]).merge(viewerOverride)) + } + + fun getViewerData(id: UUID, viewer: Audience): PrefixesPlayerData? { + val viewerId = AudienceResolver.resolveId(viewer) ?: return null + return getViewerData(id, viewerId) + } + + fun loadGroup(id: UUID, group: PrefixesGroup?) { + groups[id] = PrefixesOverride.of(group) + } + + fun set(id: UUID, override: PrefixesOverride) { + overrides.merge(id, override) { current, new -> current.merge(new) } + } + + fun set(id: UUID, viewer: UUID, override: PrefixesOverride) { + viewerOverrides.merge(ViewerKey(id, viewer), override) { current, new -> current.merge(new) } + } + + fun reset(id: UUID) { + overrides.remove(id) + viewerOverrides.keys.removeIf { key -> key.target == id } + } + + fun reset(id: UUID, viewer: UUID) { + viewerOverrides.remove(ViewerKey(id, viewer)) + } + + fun remove(id: UUID) { + groups.remove(id) + overrides.remove(id) + viewerOverrides.keys.removeIf { key -> key.target == id || key.viewer == id } + } + + private fun createData(id: UUID, override: PrefixesOverride): PrefixesPlayerData { + val prefix = override.prefix ?: Component.empty() + val suffix = override.suffix ?: Component.empty() + val color = override.color ?: NamedTextColor.WHITE + val displayName = miniMessage.deserialize( + override.displayName ?: PrefixesConstants.DEFAULT_DISPLAY_NAME, + Placeholder.component("prefix", prefix), + Placeholder.component("suffix", suffix), + Placeholder.styling("color", color), + Placeholder.unparsed("playername", platform.getPlayerName(id)) + ) + + return PrefixesPlayerData( + prefix, + suffix, + color, + displayName, + override.chatFormat ?: PrefixesConstants.DEFAULT_CHAT_FORMAT, + override.priority ?: 0 + ) + } +} diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/data/PrefixesOverride.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/data/PrefixesOverride.kt new file mode 100644 index 0000000..ec285b9 --- /dev/null +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/data/PrefixesOverride.kt @@ -0,0 +1,43 @@ +package app.simplecloud.prefixes.shared.data + +import app.simplecloud.prefixes.api.group.PrefixesGroup +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.TextColor + +data class PrefixesOverride( + val prefix: Component? = null, + val suffix: Component? = null, + val color: TextColor? = null, + val priority: Int? = null, + val displayName: String? = null, + val chatFormat: String? = null +) { + + fun merge(other: PrefixesOverride?): PrefixesOverride { + if (other == null) return this + + return PrefixesOverride( + other.prefix ?: prefix, + other.suffix ?: suffix, + other.color ?: color, + other.priority ?: priority, + other.displayName ?: displayName, + other.chatFormat ?: chatFormat + ) + } + + companion object { + fun of(group: PrefixesGroup?): PrefixesOverride { + if (group == null) return PrefixesOverride() + + return PrefixesOverride( + group.prefix, + group.suffix, + group.color, + group.priority, + group.displayName.ifBlank { null }, + group.chatFormat.ifBlank { null } + ) + } + } +} diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/data/ViewerKey.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/data/ViewerKey.kt new file mode 100644 index 0000000..35d5e56 --- /dev/null +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/data/ViewerKey.kt @@ -0,0 +1,5 @@ +package app.simplecloud.prefixes.shared.data + +import java.util.UUID + +data class ViewerKey(val target: UUID, val viewer: UUID) diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/GroupProviderRegistry.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/GroupProviderRegistry.kt new file mode 100644 index 0000000..f678943 --- /dev/null +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/GroupProviderRegistry.kt @@ -0,0 +1,45 @@ +package app.simplecloud.prefixes.shared.group + +import app.simplecloud.plugin.api.shared.config.ConfigurationFactory +import app.simplecloud.prefixes.api.group.GroupProvider +import app.simplecloud.prefixes.shared.config.PrefixesConfig +import app.simplecloud.prefixes.shared.PrefixesConstants +import java.util.concurrent.ConcurrentHashMap + +class GroupProviderRegistry( + private val config: ConfigurationFactory, + private val provider: GroupProvider +) { + + private val logger = PrefixesConstants.LOGGER + private val providers = ConcurrentHashMap() + + init { + register(provider) + } + + fun register(provider: GroupProvider) { + val key = provider.getName().trim().lowercase() + providers[key] = provider + } + + fun unregister(name: String) { + providers.remove(name.trim().lowercase()) + } + + fun getAllGroupProviders(): Collection = providers.values.toList() + + fun getCurrentGroupProvider(): GroupProvider = providers[getSource()] ?: provider + + fun validateSource() { + val source = getSource() + if (providers.containsKey(source)) { + logger.info("Using the group source '$source'") + return + } + + logger.warn("No group provider named '$source' is registered yet, using '${provider.getName()}' until it is registered") + } + + private fun getSource(): String = config.get().general.source.trim().lowercase() +} diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/config/ConfigGroupProvider.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/config/ConfigGroupProvider.kt index 2966d69..eaf8629 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/config/ConfigGroupProvider.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/config/ConfigGroupProvider.kt @@ -7,6 +7,9 @@ import app.simplecloud.prefixes.api.group.GroupProvider import app.simplecloud.prefixes.api.group.PrefixesGroup import app.simplecloud.prefixes.shared.config.ConfigGroup import app.simplecloud.prefixes.shared.config.PrefixesConfig +import app.simplecloud.prefixes.shared.utilities.ColorParser +import app.simplecloud.prefixes.shared.PrefixesConstants +import kotlinx.coroutines.future.future import java.util.UUID import java.util.concurrent.CompletableFuture @@ -15,51 +18,55 @@ class ConfigGroupProvider( private val permissionChecker: PermissionChecker ) : GroupProvider { - override val name: String = "Config" + private val logger = PrefixesConstants.LOGGER - override fun getGroups(): Collection { - val config = configFactory.get() - return config.groups - .map { ConfigPrefixesGroup(it, permissionChecker) } - .sortedByDescending { it.priority } + override fun getName(): String = PrefixesConstants.CONFIG_SOURCE + + override fun getGroups(): CompletableFuture> { + return CompletableFuture.completedFuture(loadGroups()) } override fun getGroup(id: UUID): CompletableFuture { - return CompletableFuture.supplyAsync { - val groups = getGroups() - val config = configFactory.get() - - // Check if player has permission for any group, highest priority first. - val group = groups.firstOrNull { - it.name != config.general.defaultGroup && it.containsPlayer(id) - } - - // Fallback to the default group if no group matched. - group ?: groups.firstOrNull { it.name == config.general.defaultGroup } - } + val groups = loadGroups() + val group = groups.firstOrNull { !isDefault(it) && it.hasPermission(id) } ?: groups.firstOrNull(::isDefault) + return CompletableFuture.completedFuture(group) } override fun addGroup(group: PrefixesGroup): CompletableFuture { - return CompletableFuture.supplyAsync { - synchronized(this) { + return PrefixesConstants.SCOPE.future { + synchronized(this@ConfigGroupProvider) { val config = configFactory.get() if (config.groups.any { it.name.equals(group.name, ignoreCase = true) }) { return@synchronized false } - configFactory.save(config.copy(groups = config.groups + createConfigGroup(group))) - true + try { + configFactory.save(config.copy(groups = config.groups + createConfigGroup(group))) + true + } catch (e: Exception) { + logger.error("Failed to save group '${group.name}' to config", e) + false + } } } } + private fun loadGroups(): List { + return configFactory.get().groups + .map { ConfigPrefixesGroup(it, permissionChecker) } + .sortedByDescending { it.priority } + } + + private fun isDefault(group: PrefixesGroup): Boolean = + group.name.equals(configFactory.get().general.defaultGroup, ignoreCase = true) + private fun createConfigGroup(group: PrefixesGroup) = ConfigGroup( name = group.name, priority = group.priority, permission = group.permission, - prefix = group.prefix?.let { miniMessage.serialize(it) } ?: "", - suffix = group.suffix?.let { miniMessage.serialize(it) } ?: "", - color = group.color?.let { "<${it.asHexString().uppercase()}>" } ?: "", + prefix = miniMessage.serialize(group.prefix), + suffix = miniMessage.serialize(group.suffix), + color = ColorParser.serialize(group.color), displayName = group.displayName, chatFormat = group.chatFormat ) diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/config/ConfigPrefixesGroup.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/config/ConfigPrefixesGroup.kt index 55c47c3..5f99ebd 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/config/ConfigPrefixesGroup.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/config/ConfigPrefixesGroup.kt @@ -4,8 +4,9 @@ import app.simplecloud.plugin.api.shared.extension.miniMessage import app.simplecloud.plugin.api.shared.permission.PermissionChecker import app.simplecloud.prefixes.api.group.PrefixesGroup import app.simplecloud.prefixes.shared.config.ConfigGroup -import app.simplecloud.prefixes.api.util.parseColor +import app.simplecloud.prefixes.shared.utilities.ColorParser import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor import net.kyori.adventure.text.format.TextColor import java.util.UUID import java.util.concurrent.CompletableFuture @@ -14,22 +15,22 @@ class ConfigPrefixesGroup( private val group: ConfigGroup, private val permissionChecker: PermissionChecker ) : PrefixesGroup { + override val name: String = group.name override val priority: Int = group.priority override val permission: String = group.permission - override val prefix: Component? = miniMessage.deserialize(group.prefix) - override val suffix: Component? = miniMessage.deserialize(group.suffix) - override val color: TextColor? = parseColor(group.color) + override val prefix: Component = miniMessage.deserialize(group.prefix) + override val suffix: Component = miniMessage.deserialize(group.suffix) + override val color: TextColor = ColorParser.parse(group.color) ?: NamedTextColor.WHITE override val displayName: String = group.displayName override val chatFormat: String = group.chatFormat - override fun containsPlayer(id: UUID): Boolean { - if (permission.isEmpty()) return true // Default group matches everyone - return permissionChecker.checkPermission(id, permission) + override fun containsPlayer(id: UUID): CompletableFuture { + return CompletableFuture.completedFuture(hasPermission(id)) } - override fun containsPlayerAsync(id: UUID): CompletableFuture { - return CompletableFuture.supplyAsync { containsPlayer(id) } + fun hasPermission(id: UUID): Boolean { + if (permission.isEmpty()) return true + return permissionChecker.checkPermission(id, permission) } - -} \ No newline at end of file +} diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/luckperms/LuckPermsGroup.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/luckperms/LuckPermsGroup.kt index 6dd2369..431fa08 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/luckperms/LuckPermsGroup.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/luckperms/LuckPermsGroup.kt @@ -2,40 +2,51 @@ package app.simplecloud.prefixes.shared.group.luckperms import app.simplecloud.plugin.api.shared.extension.miniMessage import app.simplecloud.prefixes.api.group.PrefixesGroup -import app.simplecloud.prefixes.api.util.parseColor +import app.simplecloud.prefixes.shared.utilities.ColorParser +import app.simplecloud.prefixes.shared.PrefixesConstants +import kotlinx.coroutines.future.await +import kotlinx.coroutines.future.future import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor import net.kyori.adventure.text.format.TextColor import net.luckperms.api.LuckPerms import net.luckperms.api.model.group.Group +import net.luckperms.api.model.user.User import java.util.UUID import java.util.concurrent.CompletableFuture class LuckPermsGroup( private val group: Group, - private val luckPerms: LuckPerms, + private val luckPerms: LuckPerms ) : PrefixesGroup { override val name: String = group.name override val priority: Int = group.weight.orElse(0) override val permission: String = "group.${group.name}" - override val prefix: Component? = miniMessage.deserialize(group.cachedData.metaData.prefix ?: "") - override val suffix: Component? = miniMessage.deserialize(group.cachedData.metaData.suffix ?: "") - override val color: TextColor? = getMetaValue("color")?.let { parseColor(it) } ?: TextColor.fromHexString("#FFFFFF") - override val displayName: String = getMetaValue("display-name") ?: "" - override val chatFormat: String = getMetaValue("chat-format") ?: " <#475569>» <#F8FAFC>" - - override fun containsPlayer(id: UUID): Boolean { - return containsPlayerAsync(id).join() - } + override val prefix: Component = miniMessage.deserialize(group.cachedData.metaData.prefix ?: "") + override val suffix: Component = miniMessage.deserialize(group.cachedData.metaData.suffix ?: "") + override val color: TextColor = parseColor() + override val displayName: String = getMetaValue("display-name") ?: "" + override val chatFormat: String = getMetaValue("chat-format") ?: "" - override fun containsPlayerAsync(id: UUID): CompletableFuture { - return luckPerms.userManager.loadUser(id).thenApply { user -> - user.primaryGroup == name || user.getInheritedGroups(user.queryOptions).any { it.name == name } + override fun containsPlayer(id: UUID): CompletableFuture { + val user = luckPerms.userManager.getUser(id) + if (user != null) { + return CompletableFuture.completedFuture(isMember(user)) } + + return PrefixesConstants.SCOPE.future { isMember(luckPerms.userManager.loadUser(id).await()) } + } + + private fun isMember(user: User): Boolean = + user.primaryGroup == name || user.getInheritedGroups(user.queryOptions).any { it.name == name } + + private fun parseColor(): TextColor { + val value = getMetaValue("color") ?: return NamedTextColor.WHITE + return ColorParser.parse(value) ?: NamedTextColor.WHITE } private fun getMetaValue(key: String): String? { return group.cachedData.metaData.getMetaValue(key) } - -} \ No newline at end of file +} diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/luckperms/LuckPermsGroupProvider.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/luckperms/LuckPermsGroupProvider.kt index 0b3eee8..b96de93 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/luckperms/LuckPermsGroupProvider.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/luckperms/LuckPermsGroupProvider.kt @@ -3,7 +3,12 @@ package app.simplecloud.prefixes.shared.group.luckperms import app.simplecloud.plugin.api.shared.extension.miniMessage import app.simplecloud.prefixes.api.group.GroupProvider import app.simplecloud.prefixes.api.group.PrefixesGroup +import app.simplecloud.prefixes.shared.utilities.ColorParser +import app.simplecloud.prefixes.shared.PrefixesConstants +import kotlinx.coroutines.future.await +import kotlinx.coroutines.future.future import net.luckperms.api.LuckPerms +import net.luckperms.api.model.data.NodeMap import net.luckperms.api.model.group.Group import net.luckperms.api.model.user.User import net.luckperms.api.node.types.MetaNode @@ -17,12 +22,16 @@ class LuckPermsGroupProvider( private val luckPerms: LuckPerms ) : GroupProvider { - override val name: String = "LuckPerms" + private val logger = PrefixesConstants.LOGGER - override fun getGroups(): Collection { - return luckPerms.groupManager.loadedGroups + override fun getName(): String = "luckperms" + + override fun getGroups(): CompletableFuture> { + val groups = luckPerms.groupManager.loadedGroups .map { LuckPermsGroup(it, luckPerms) } .sortedByDescending { it.priority } + + return CompletableFuture.completedFuture(groups) } override fun getGroup(id: UUID): CompletableFuture { @@ -31,37 +40,52 @@ class LuckPermsGroupProvider( return CompletableFuture.completedFuture(resolveGroup(user)) } - return luckPerms.userManager.loadUser(id).thenApply(::resolveGroup) + return PrefixesConstants.SCOPE.future { loadGroup(id) } } override fun addGroup(group: PrefixesGroup): CompletableFuture { - return luckPerms.groupManager.loadGroup(group.name).thenCompose { existing -> - if (existing.isPresent) { - return@thenCompose CompletableFuture.completedFuture(false) - } - - luckPerms.groupManager.createAndLoadGroup(group.name).thenCompose { created -> - applyPrefixes(created, group) - luckPerms.groupManager.saveGroup(created).thenApply { true } - } + return PrefixesConstants.SCOPE.future { + val existing = luckPerms.groupManager.loadGroup(group.name).await() + if (existing.isPresent) return@future false + + val created = luckPerms.groupManager.createAndLoadGroup(group.name).await() + applyPrefixes(created, group) + luckPerms.groupManager.saveGroup(created).await() + true + } + } + + private suspend fun loadGroup(id: UUID): PrefixesGroup? { + return try { + resolveGroup(luckPerms.userManager.loadUser(id).await()) + } catch (exception: Exception) { + logger.error("Failed to load LuckPerms user $id", exception) + null } } private fun applyPrefixes(target: Group, source: PrefixesGroup) { - target.data().add(WeightNode.builder(source.priority).build()) + val data = target.data() + data.add(WeightNode.builder(source.priority).build()) - source.prefix?.let { miniMessage.serialize(it) }?.takeIf { it.isNotEmpty() }?.let { - target.data().add(PrefixNode.builder(it, source.priority).build()) + val prefix = miniMessage.serialize(source.prefix) + if (prefix.isNotEmpty()) { + data.add(PrefixNode.builder(prefix, source.priority).build()) } - source.suffix?.let { miniMessage.serialize(it) }?.takeIf { it.isNotEmpty() }?.let { - target.data().add(SuffixNode.builder(it, source.priority).build()) - } - source.color?.let { "<${it.asHexString().uppercase()}>" }?.let { - target.data().add(MetaNode.builder("color", it).build()) + + val suffix = miniMessage.serialize(source.suffix) + if (suffix.isNotEmpty()) { + data.add(SuffixNode.builder(suffix, source.priority).build()) } - target.data().add(MetaNode.builder("display-name", source.displayName).build()) - target.data().add(MetaNode.builder("chat-format", source.chatFormat).build()) + data.add(MetaNode.builder("color", ColorParser.serialize(source.color)).build()) + addMeta(data, "display-name", source.displayName) + addMeta(data, "chat-format", source.chatFormat) + } + + private fun addMeta(data: NodeMap, key: String, value: String) { + if (value.isBlank()) return + data.add(MetaNode.builder(key, value).build()) } private fun resolveGroup(user: User): PrefixesGroup? { @@ -70,18 +94,18 @@ class LuckPermsGroupProvider( val primaryGroup = luckPerms.groupManager.getGroup(primaryGroupName) val candidates = if (primaryGroup == null) inheritedGroups else inheritedGroups + primaryGroup - return selectHighestWeightedGroup(candidates, primaryGroupName) - ?.let { LuckPermsGroup(it, luckPerms) } + val group = selectHighestWeightedGroup(candidates, primaryGroupName) ?: return null + return LuckPermsGroup(group, luckPerms) } -} -internal fun selectHighestWeightedGroup(groups: Iterable, primaryGroupName: String): Group? { - return groups - .distinctBy { it.name } - .sortedWith( - compareByDescending { it.weight.orElse(0) } - .thenByDescending { it.name == primaryGroupName } - .thenBy { it.name } - ) - .firstOrNull() + private fun selectHighestWeightedGroup(groups: Iterable, primaryGroupName: String): Group? { + return groups + .distinctBy { it.name } + .sortedWith( + compareByDescending { it.weight.orElse(0) } + .thenByDescending { it.name == primaryGroupName } + .thenBy { it.name } + ) + .firstOrNull() + } } diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/luckperms/LuckPermsListener.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/luckperms/LuckPermsListener.kt new file mode 100644 index 0000000..6c1d25c --- /dev/null +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/group/luckperms/LuckPermsListener.kt @@ -0,0 +1,27 @@ +package app.simplecloud.prefixes.shared.group.luckperms + +import app.simplecloud.prefixes.api.PrefixesApi +import net.luckperms.api.LuckPerms +import net.luckperms.api.event.EventSubscription +import net.luckperms.api.event.node.NodeMutateEvent +import net.luckperms.api.event.user.UserDataRecalculateEvent +import net.luckperms.api.model.group.Group +import net.luckperms.api.model.user.User + +class LuckPermsListener( + private val luckPerms: LuckPerms, + private val api: PrefixesApi +) { + + fun register(): List> = listOf( + luckPerms.eventBus.subscribe(UserDataRecalculateEvent::class.java) { event -> api.refreshPlayer(event.user.uniqueId) }, + luckPerms.eventBus.subscribe(NodeMutateEvent::class.java, ::onNodeMutate) + ) + + private fun onNodeMutate(event: NodeMutateEvent) { + when (val target = event.target) { + is User -> api.refreshPlayer(target.uniqueId) + is Group -> api.refreshAll() + } + } +} diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/platform/PrefixesListener.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/platform/PrefixesListener.kt new file mode 100644 index 0000000..98d9246 --- /dev/null +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/platform/PrefixesListener.kt @@ -0,0 +1,34 @@ +package app.simplecloud.prefixes.shared.platform + +import java.util.UUID + +/** + * Represents a listener for prefixes events on a platform. + */ +interface PrefixesListener { + + /** + * Called after the config and messages were reloaded. + */ + fun onReload() + + /** + * Called when prefixes shuts down. + */ + fun onShutdown() + + /** + * Called when the group of a player has to be reloaded. + */ + fun onPlayerUpdate(id: UUID) + + /** + * Called when manually set values of a player changed. + */ + fun onPlayerDataChange(id: UUID) + + /** + * Called when the groups of all players have to be reloaded. + */ + fun onAllPlayersUpdate() +} diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/platform/PrefixesPlatform.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/platform/PrefixesPlatform.kt index 36155a0..7a4168b 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/platform/PrefixesPlatform.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/platform/PrefixesPlatform.kt @@ -6,27 +6,32 @@ import java.io.File import java.util.UUID /** - * Represents the platform environment on which the plugin runs. + * Represents a platform on which prefixes runs. */ interface PrefixesPlatform { /** - * The platform-specific plugin data directory. + * Returns this platform's data directory. */ - val dataDirectory: File + fun getDataDirectory(): File /** - * The platform-specific permission checker. + * Returns this platform's permission checker. */ - val permissionChecker: PermissionChecker + fun getPermissionChecker(): PermissionChecker /** - * The platform-specific player name resolver. + * Returns the LuckPerms instance on the platform, or null if not available. */ - val playerResolver: (UUID) -> String + fun getLuckPerms(): LuckPerms? /** - * The LuckPerms instance on the platform, or null if not available. + * Returns the name of a player. */ - val luckPerms: LuckPerms? + fun getPlayerName(id: UUID): String + + /** + * Returns whether a player is online on this server. + */ + fun isOnline(id: UUID): Boolean } diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/PrefixesSubjects.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/PrefixesSubjects.kt similarity index 64% rename from prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/PrefixesSubjects.kt rename to prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/PrefixesSubjects.kt index 1027576..3aa99d0 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/PrefixesSubjects.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/PrefixesSubjects.kt @@ -1,13 +1,11 @@ -package app.simplecloud.prefixes.shared.utilities +package app.simplecloud.prefixes.shared.sync -import app.simplecloud.api.runtime.SimpleCloudRuntime -import app.simplecloud.prefixes.shared.config.ALL_SYNC_SOURCE -import app.simplecloud.prefixes.shared.config.CURRENT_SYNC_SOURCE +import app.simplecloud.prefixes.shared.PrefixesConstants class PrefixesSubjects( - networkId: String, - private val origin: String = SimpleCloudRuntime.groupName().ifBlank { SimpleCloudRuntime.serverId() }, - serverId: String = SimpleCloudRuntime.serverId() + networkId: String = System.getenv().getOrDefault("SIMPLECLOUD_NETWORK_ID", "default"), + serverId: String = System.getenv().getOrDefault("SIMPLECLOUD_UNIQUE_ID", ""), + private val origin: String = System.getenv().getOrDefault("SIMPLECLOUD_GROUP", "").ifBlank { serverId } ) { private val root = "$networkId.prefixes" @@ -27,12 +25,12 @@ class PrefixesSubjects( .distinct() .toList() - if (ALL_SYNC_SOURCE in resolvedSources) { + if (PrefixesConstants.ALL_SYNC_SOURCE in resolvedSources) { return listOf("$root.*.*.$subject") } return resolvedSources - .map { source -> if (source == CURRENT_SYNC_SOURCE) origin else source } + .map { source -> if (source == PrefixesConstants.CURRENT_SYNC_SOURCE) origin else source } .distinct() .map { source -> "$root.$source.*.$subject" } } @@ -43,4 +41,4 @@ class PrefixesSubjects( const val TABLIST_REMOVE = "tablist.remove" const val TABLIST_REQUEST = "tablist.request" } -} +} \ No newline at end of file diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/PrefixesSync.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/PrefixesSync.kt index 00ee305..f745715 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/PrefixesSync.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/PrefixesSync.kt @@ -1,35 +1,47 @@ package app.simplecloud.prefixes.shared.sync -import app.simplecloud.api.CloudApi import app.simplecloud.plugin.api.shared.config.ConfigurationFactory import app.simplecloud.prefixes.shared.config.PrefixesConfig -import app.simplecloud.prefixes.shared.utilities.PrefixesSubjects +import app.simplecloud.prefixes.shared.PrefixesConstants import io.nats.client.Connection import io.nats.client.Nats import io.nats.client.Options +import java.time.Duration -class PrefixesSync(config: ConfigurationFactory) { +class PrefixesSync( + config: ConfigurationFactory +) { - private val api = CloudApi.create() + private val logger = PrefixesConstants.LOGGER private val connection = createNatsConnection() - private val subjects = PrefixesSubjects(api.networkId) + private val subjects = PrefixesSubjects() val publisher = SyncPublisher(connection, subjects, config) val subscriber = SyncSubscriber(connection, subjects, config) fun shutdown() { - subscriber.close() - connection.close() - api.close() + runCatching { + logger.info("Shutdown prefixes sync...") + connection.flush(Duration.ofSeconds(1)) + subscriber.close() + connection.close() + }.onFailure { throwable -> + logger.error("Failed to shutdown sync", throwable) + }.onSuccess { + logger.info("Successfully shutdown sync") + } } private fun createNatsConnection(): Connection { - return Nats.connect( + val nats = Nats.connect( Options.builder() .server(System.getenv().getOrDefault("SIMPLECLOUD_NATS_URL", "wss://nats.simplecloud.app:443")) .userInfo(System.getenv().getOrDefault("SIMPLECLOUD_NETWORK_ID", "default"), System.getenv().getOrDefault("SIMPLECLOUD_NETWORK_SECRET", "")) .maxReconnects(-1) .build() ) + + logger.info("Connected to NATS server (${nats.connectedUrl})") + return nats } } diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/SyncPublisher.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/SyncPublisher.kt index fc9ead3..3d5ff92 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/SyncPublisher.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/SyncPublisher.kt @@ -1,13 +1,11 @@ package app.simplecloud.prefixes.shared.sync import app.simplecloud.plugin.api.shared.config.ConfigurationFactory -import app.simplecloud.prefixes.shared.config.FeaturesConfig import app.simplecloud.prefixes.shared.config.PrefixesConfig -import app.simplecloud.prefixes.shared.config.SyncChannels import app.simplecloud.prefixes.shared.sync.tablist.TablistEntry import app.simplecloud.prefixes.shared.sync.tablist.TablistEntryMapper import app.simplecloud.prefixes.shared.utilities.ComponentSerializer -import app.simplecloud.prefixes.shared.utilities.PrefixesSubjects +import app.simplecloud.prefixes.shared.PrefixesConstants import app.simplecloud.prefixes.v1.ChatMessageEvent import app.simplecloud.prefixes.v1.TablistEntryRemoveEvent import app.simplecloud.prefixes.v1.TablistSyncRequest @@ -21,51 +19,36 @@ class SyncPublisher( private val subjects: PrefixesSubjects, private val config: ConfigurationFactory ) { + private val logger = PrefixesConstants.LOGGER - fun publishChatMessage(message: Component) = publish( - FeaturesConfig::chat, - SyncChannels::chat, - PrefixesSubjects.CHAT - ) { + fun publishChatMessage(message: Component) = publish(PrefixesConfig::isChatSynced, PrefixesSubjects.CHAT) { ChatMessageEvent.newBuilder() .setJson(ComponentSerializer.serialize(message)) .build() } - fun publishTablistEntry(entry: TablistEntry) = publish( - FeaturesConfig::tablist, - SyncChannels::tablist, - PrefixesSubjects.TABLIST_UPDATE - ) { + fun publishTablistEntry(entry: TablistEntry) = publish(PrefixesConfig::isTablistSynced, PrefixesSubjects.TABLIST_UPDATE) { TablistEntryMapper.toDefinition(entry) } - fun publishTablistRemove(uniqueId: UUID) = publish( - FeaturesConfig::tablist, - SyncChannels::tablist, - PrefixesSubjects.TABLIST_REMOVE - ) { + fun publishTablistRemove(uniqueId: UUID) = publish({ true }, PrefixesSubjects.TABLIST_REMOVE) { TablistEntryRemoveEvent.newBuilder() .setPlayerId(uniqueId.toString()) .build() } - fun publishTablistRequest() = publish( - FeaturesConfig::tablist, - SyncChannels::tablist, - PrefixesSubjects.TABLIST_REQUEST - ) { + fun publishTablistRequest() = publish(PrefixesConfig::isTablistSynced, PrefixesSubjects.TABLIST_REQUEST) { TablistSyncRequest.getDefaultInstance() } - private fun publish( - feature: (FeaturesConfig) -> Boolean, - channel: (SyncChannels) -> Boolean, - subject: String, - message: () -> MessageLite - ) { - val config = config.get() - if (!feature(config.features) || !config.sync.enabled || !channel(config.sync.channels)) return - connection.publish(subjects.own(subject), message().toByteArray()) + private fun publish(enabled: (PrefixesConfig) -> Boolean, subject: String, message: () -> MessageLite) { + if (!enabled(config.get())) return + + try { + connection.publish(subjects.own(subject), message().toByteArray()) + } catch (e: Exception) { + logger.error("Failed to publish sync message on '$subject'", e) + } } + } diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/SyncSubscriber.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/SyncSubscriber.kt index 4066a01..d5fbc96 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/SyncSubscriber.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/SyncSubscriber.kt @@ -1,13 +1,11 @@ package app.simplecloud.prefixes.shared.sync import app.simplecloud.plugin.api.shared.config.ConfigurationFactory -import app.simplecloud.prefixes.shared.config.FeaturesConfig import app.simplecloud.prefixes.shared.config.PrefixesConfig -import app.simplecloud.prefixes.shared.config.SyncChannels import app.simplecloud.prefixes.shared.sync.tablist.TablistEntry import app.simplecloud.prefixes.shared.sync.tablist.TablistEntryMapper import app.simplecloud.prefixes.shared.utilities.ComponentSerializer -import app.simplecloud.prefixes.shared.utilities.PrefixesSubjects +import app.simplecloud.prefixes.shared.PrefixesConstants import app.simplecloud.prefixes.v1.ChatMessageEvent import app.simplecloud.prefixes.v1.TablistEntryRemoveEvent import app.simplecloud.prefixes.v1.TablistEntryUpdateEvent @@ -22,11 +20,11 @@ class SyncSubscriber( private val subjects: PrefixesSubjects, private val config: ConfigurationFactory ) { - + private val logger = PrefixesConstants.LOGGER private val dispatcher = connection.createDispatcher(null) fun subscribeChatMessage(handler: (Component) -> Unit) { - subscribe(FeaturesConfig::chat, SyncChannels::chat, PrefixesSubjects.CHAT) { message -> + subscribe(PrefixesConfig::isChatSynced, PrefixesSubjects.CHAT) { message -> handler(ComponentSerializer.deserialize(ChatMessageEvent.parseFrom(message.data).json)) } } @@ -36,59 +34,49 @@ class SyncSubscriber( onRemove: (String, UUID) -> Unit, onRequest: () -> Unit ) { - subscribe( - FeaturesConfig::tablist, - SyncChannels::tablist, - PrefixesSubjects.TABLIST_UPDATE - ) { message -> + subscribe(PrefixesConfig::isTablistSynced, PrefixesSubjects.TABLIST_UPDATE) { message -> onUpdate( subjects.publisherId(message.subject, PrefixesSubjects.TABLIST_UPDATE), TablistEntryMapper.fromDefinition(TablistEntryUpdateEvent.parseFrom(message.data)) ) } - subscribe( - FeaturesConfig::tablist, - SyncChannels::tablist, - PrefixesSubjects.TABLIST_REMOVE - ) { message -> + subscribe(PrefixesConfig::isTablistSynced, PrefixesSubjects.TABLIST_REMOVE) { message -> onRemove( subjects.publisherId(message.subject, PrefixesSubjects.TABLIST_REMOVE), UUID.fromString(TablistEntryRemoveEvent.parseFrom(message.data).playerId) ) } - subscribe( - FeaturesConfig::tablist, - SyncChannels::tablist, - PrefixesSubjects.TABLIST_REQUEST - ) { + subscribe(PrefixesConfig::isTablistSynced, PrefixesSubjects.TABLIST_REQUEST) { onRequest() } } fun close() { - dispatcher.drain(Duration.ofSeconds(1)) + try { + dispatcher.drain(Duration.ofSeconds(1)) + } catch (e: Exception) { + logger.error("Failed to drain prefixes sync dispatcher", e) + } } - private fun subscribe( - feature: (FeaturesConfig) -> Boolean, - channel: (SyncChannels) -> Boolean, - subject: String, - handler: (Message) -> Unit - ) { - val config = config.get() - if (!feature(config.features) || !config.sync.enabled || !channel(config.sync.channels)) return + private fun subscribe(enabled: (PrefixesConfig) -> Boolean, subject: String, handler: (Message) -> Unit) { + subjects.patterns(config.get().sync.sources, subject).forEach { pattern -> + runCatching { subscribe(pattern, enabled, handler) }.onFailure { throwable -> + logger.error("Failed to subscribe to the sync subject '$pattern'", throwable) + } + } + } - subjects.patterns(config.sync.sources, subject).forEach { pattern -> - dispatcher.subscribe(pattern) { message -> - val currentConfig = this.config.get() - if (feature(currentConfig.features) && - currentConfig.sync.enabled && - channel(currentConfig.sync.channels) && - !subjects.isOwn(message.subject) - ) { - handler(message) - } + private fun subscribe(pattern: String, enabled: (PrefixesConfig) -> Boolean, handler: (Message) -> Unit) { + dispatcher.subscribe(pattern) { message -> + if (!enabled(config.get()) || subjects.isOwn(message.subject)) return@subscribe + + try { + handler(message) + } catch (e: Exception) { + logger.error("Failed to handle sync message on '${message.subject}'", e) } } } -} + +} \ No newline at end of file diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/tablist/SourcedTablistEntry.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/tablist/SourcedTablistEntry.kt new file mode 100644 index 0000000..aaab20a --- /dev/null +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/tablist/SourcedTablistEntry.kt @@ -0,0 +1,9 @@ +package app.simplecloud.prefixes.shared.sync.tablist + +import kotlin.time.TimeMark + +data class SourcedTablistEntry( + val publisherId: String, + val entry: TablistEntry, + val updatedAt: TimeMark +) \ No newline at end of file diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/tablist/SyncedTablist.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/tablist/SyncedTablist.kt new file mode 100644 index 0000000..b65c8a6 --- /dev/null +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/tablist/SyncedTablist.kt @@ -0,0 +1,74 @@ +package app.simplecloud.prefixes.shared.sync.tablist + +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration.Companion.seconds +import kotlin.time.TimeSource + +abstract class SyncedTablist { + + private val entries = ConcurrentHashMap() + + fun update(publisherId: String, entry: TablistEntry) { + if (isOnline(entry.uniqueId)) return + + val previous = entries.put(entry.uniqueId, SourcedTablistEntry(publisherId, entry, TimeSource.Monotonic.markNow()))?.entry + if (previous != entry) { + broadcast(createInfoPacket(listOf(entry))) + } + if (previous == null || previous.name != entry.name || previous.priority != entry.priority) { + if (previous != null) broadcast(createTeamRemovePacket(previous)) + broadcast(createTeamPacket(entry)) + } + } + + fun remove(id: UUID) { + val sourced = entries.remove(id) ?: return + hide(sourced.entry) + } + + fun remove(publisherId: String, id: UUID) { + val sourced = entries[id] ?: return + if (sourced.publisherId != publisherId) return + if (entries.remove(id, sourced)) hide(sourced.entry) + } + + fun clear() { + entries.keys.toList().forEach { id -> remove(id) } + } + + fun expire() { + entries.entries + .filter { (_, sourced) -> sourced.updatedAt.elapsedNow() > 90.seconds } + .forEach { (id, sourced) -> if (entries.remove(id, sourced)) hide(sourced.entry) } + } + + fun sync(player: P) { + val visible = entries.values.map(SourcedTablistEntry::entry).filter { entry -> !isOnline(entry.uniqueId) } + + visible.chunked(100).forEach { batch -> send(player, createInfoPacket(batch)) } + visible.forEach { entry -> send(player, createTeamPacket(entry)) } + } + + protected abstract fun isOnline(id: UUID): Boolean + + protected abstract fun createInfoPacket(entries: List): K + + protected abstract fun createInfoRemovePacket(id: UUID): K + + protected abstract fun createTeamPacket(entry: TablistEntry): K + + protected abstract fun createTeamRemovePacket(entry: TablistEntry): K + + protected abstract fun send(player: P, packet: K) + + protected abstract fun broadcast(packet: K) + + private fun hide(entry: TablistEntry) { + broadcast(createTeamRemovePacket(entry)) + if (!isOnline(entry.uniqueId)) { + broadcast(createInfoRemovePacket(entry.uniqueId)) + } + } + +} \ No newline at end of file diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/tablist/TablistEntry.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/tablist/TablistEntry.kt index 7cf4cff..22e2e4e 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/tablist/TablistEntry.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/tablist/TablistEntry.kt @@ -3,9 +3,6 @@ package app.simplecloud.prefixes.shared.sync.tablist import net.kyori.adventure.text.Component import java.util.UUID -/** - * Represents the tablist entry of a player on another server. - */ data class TablistEntry( val uniqueId: UUID, val name: String, diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/tablist/TablistEntryMapper.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/tablist/TablistEntryMapper.kt index 5d36961..29d86ff 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/tablist/TablistEntryMapper.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/sync/tablist/TablistEntryMapper.kt @@ -29,7 +29,7 @@ object TablistEntryMapper { profileProperties = definition.profilePropertiesList.map(::fromDefinition), latency = definition.latency, gameMode = fromDefinition(definition.gameMode), - showHat = definition.showHat.takeIf { definition.hasShowHat() } ?: true, + showHat = showHat(definition), listOrder = definition.listOrder ) @@ -37,16 +37,31 @@ object TablistEntryMapper { val definition = TablistProfileProperty.newBuilder() .setName(property.name) .setValue(property.value) - property.signature?.let(definition::setSignature) + + val signature = property.signature + if (signature != null) { + definition.setSignature(signature) + } + return definition.build() } private fun fromDefinition(property: TablistProfileProperty) = ProfileProperty( name = property.name, value = property.value, - signature = property.signature.takeIf { property.hasSignature() } + signature = signature(property) ) + private fun showHat(definition: TablistEntryUpdateEvent): Boolean { + if (!definition.hasShowHat()) return true + return definition.showHat + } + + private fun signature(property: TablistProfileProperty): String? { + if (!property.hasSignature()) return null + return property.signature + } + private fun toDefinition(gameMode: TablistGameMode): TablistGameModeDefinition = when (gameMode) { TablistGameMode.SURVIVAL -> TablistGameModeDefinition.TABLIST_GAME_MODE_SURVIVAL TablistGameMode.CREATIVE -> TablistGameModeDefinition.TABLIST_GAME_MODE_CREATIVE diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/AudienceResolver.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/AudienceResolver.kt new file mode 100644 index 0000000..729ff4c --- /dev/null +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/AudienceResolver.kt @@ -0,0 +1,14 @@ +package app.simplecloud.prefixes.shared.utilities + +import net.kyori.adventure.audience.Audience +import net.kyori.adventure.identity.Identity +import java.util.UUID + +object AudienceResolver { + + fun resolveIds(audience: Audience): List = buildList { + audience.forEachAudience { member -> member.get(Identity.UUID).ifPresent(::add) } + } + + fun resolveId(audience: Audience): UUID? = audience.get(Identity.UUID).orElse(null) +} diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/ColorParser.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/ColorParser.kt new file mode 100644 index 0000000..7cb63fa --- /dev/null +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/ColorParser.kt @@ -0,0 +1,20 @@ +package app.simplecloud.prefixes.shared.utilities + +import net.kyori.adventure.text.format.TextColor +import net.kyori.adventure.text.minimessage.MiniMessage + +object ColorParser { + + fun parse(color: String): TextColor? { + val value = color.trim().removePrefix("<").removeSuffix(">").lowercase() + if (value.isEmpty()) return null + + if (value.startsWith("#")) { + return TextColor.fromHexString(value) + } + + return MiniMessage.miniMessage().deserialize("<$value>.").color() + } + + fun serialize(color: TextColor): String = "<${color.asHexString().uppercase()}>" +} diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/ComponentSerializer.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/ComponentSerializer.kt index f72035a..23196fe 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/ComponentSerializer.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/ComponentSerializer.kt @@ -7,11 +7,7 @@ object ComponentSerializer { private val serializer = GsonComponentSerializer.gson() - fun serialize(component: Component): String { - return serializer.serialize(component) - } + fun serialize(component: Component): String = serializer.serialize(component) - fun deserialize(json: String): Component { - return serializer.deserialize(json) - } + fun deserialize(json: String): Component = serializer.deserialize(json) } diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/PlayerDisplayFormatter.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/PlayerDisplayFormatter.kt index 3ddb6e9..e21f673 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/PlayerDisplayFormatter.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/PlayerDisplayFormatter.kt @@ -7,20 +7,17 @@ import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder object PlayerDisplayFormatter { - fun displayName(data: PrefixesPlayerData, playerName: String, enabled: Boolean): Component { - return if (enabled) data.displayName else Component.text(playerName) + fun formatDisplayName(data: PrefixesPlayerData, playerName: String, displayNameEnabled: Boolean): Component { + return if (displayNameEnabled) data.displayName else Component.text(playerName) } - fun formatTablistName(data: PrefixesPlayerData, displayName: Component = data.displayName): Component { - return data.prefix.append(displayName).append(data.suffix) + fun formatTablistName(data: PrefixesPlayerData, playerName: String): Component { + return data.prefix + .append(Component.text(playerName, data.color)) + .append(data.suffix) } - fun formatChatMessage( - data: PrefixesPlayerData, - playerName: String, - message: Component, - displayName: Component = data.displayName - ): Component { + fun formatChatMessage(data: PrefixesPlayerData, playerName: String, message: Component, displayNameEnabled: Boolean): Component { return miniMessage.deserialize( data.chatFormat, Placeholder.component("prefix", data.prefix), @@ -28,7 +25,7 @@ object PlayerDisplayFormatter { Placeholder.styling("color", data.color), Placeholder.unparsed("playername", playerName), Placeholder.unparsed("name", playerName), - Placeholder.component("displayname", displayName), + Placeholder.component("displayname", formatDisplayName(data, playerName, displayNameEnabled)), Placeholder.component("message", message) ) } diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/PrefixesCoroutineDetails.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/PrefixesCoroutineDetails.kt new file mode 100644 index 0000000..9c424a0 --- /dev/null +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/PrefixesCoroutineDetails.kt @@ -0,0 +1,12 @@ +package app.simplecloud.prefixes.shared.utilities + +import kotlin.coroutines.AbstractCoroutineContextElement +import kotlin.coroutines.CoroutineContext + +data class PrefixesCoroutineDetails( + val player: String?, + val reason: String +) : AbstractCoroutineContextElement(PrefixesCoroutineDetails) { + + companion object Key : CoroutineContext.Key +} \ No newline at end of file diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/PriorityFormatter.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/PriorityFormatter.kt index 5be4cba..ff7d131 100644 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/PriorityFormatter.kt +++ b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/PriorityFormatter.kt @@ -3,7 +3,7 @@ package app.simplecloud.prefixes.shared.utilities object PriorityFormatter { fun format(priority: Int): String { - val inverted = (1000 - priority).coerceIn(0, 999) - return String.format("%03d", inverted) + val inverted = Int.MAX_VALUE.toLong() - priority + return inverted.toString().padStart(10, '0') } } diff --git a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/config/DefaultConfigs.kt b/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/config/DefaultConfigs.kt deleted file mode 100644 index c1ea24c..0000000 --- a/prefixes-shared/src/main/kotlin/app/simplecloud/prefixes/shared/utilities/config/DefaultConfigs.kt +++ /dev/null @@ -1,42 +0,0 @@ -package app.simplecloud.prefixes.shared.utilities.config - -import app.simplecloud.prefixes.shared.config.ConfigGroup - -object DefaultConfigs { - - val VARIABLES: Map = mapOf("prefix" to "⚡") - - val GROUPS: List = listOf( - ConfigGroup( - name = "owner", - priority = 100, - permission = "simplecloud.prefix.group.owner", - prefix = "<#DC2626>Owner <#475569>| ", - suffix = "", - color = "<#DC2626>", - displayName = "", - chatFormat = " <#475569>» <#F8FAFC>" - ), - ConfigGroup( - name = "admin", - priority = 90, - permission = "simplecloud.prefix.group.admin", - prefix = "<#F59E0B>Admin <#475569>| ", - suffix = "", - color = "<#F59E0B>", - displayName = "", - chatFormat = " <#475569>» <#F8FAFC>" - ), - ConfigGroup( - name = "default", - priority = 0, - permission = "", - prefix = "<#94A3B8>Player <#475569>| ", - suffix = "", - color = "<#94A3B8>", - displayName = "", - chatFormat = " <#475569>» <#F8FAFC>" - ) - ) - -} \ No newline at end of file diff --git a/prefixes-shared/src/main/proto/simplecloud/prefixes/v1/prefixes.proto b/prefixes-shared/src/main/proto/simplecloud/prefixes/v1/prefixes.proto index bc4c92e..e33c28c 100644 --- a/prefixes-shared/src/main/proto/simplecloud/prefixes/v1/prefixes.proto +++ b/prefixes-shared/src/main/proto/simplecloud/prefixes/v1/prefixes.proto @@ -39,4 +39,4 @@ message TablistEntryRemoveEvent { string player_id = 1; } -message TablistSyncRequest { } +message TablistSyncRequest { } \ No newline at end of file diff --git a/prefixes-shared/src/main/resources/config.yml b/prefixes-shared/src/main/resources/config.yml index 9912982..34940a4 100644 --- a/prefixes-shared/src/main/resources/config.yml +++ b/prefixes-shared/src/main/resources/config.yml @@ -6,12 +6,12 @@ version: 1 # Controls how player groups are resolved. # ───────────────────────────────────────────────────────────────────────────── general: - # Where groups are read from. - # Available: CONFIG, LUCKPERMS - source: CONFIG + # Where groups are loaded from. + # Available: config, luckperms, or the name of a custom provider. + source: config # Group used when a player matches no other group. - # Must reference a group defined below when source is CONFIG. + # Must reference a group defined below when source is config. default-group: default # ───────────────────────────────────────────────────────────────────────────── @@ -22,21 +22,21 @@ features: # Format chat messages using the group's chat-format. chat: true - # Apply prefixes, suffixes, colors and sorting to the tab list. + # Show the prefix, the colored player name and the suffix in the tab list, + # sorted by group priority. tablist: true - # Apply the group's configured display-name. - # When disabled, the player's normal Minecraft name is used. + # Show the group's display-name above the player's head. + # When disabled, the normal Minecraft name tag is shown instead. display-name: true # ───────────────────────────────────────────────────────────────────────────── # Sync # Shares chat messages and tab list entries across the SimpleCloud network. -# Disabling sync does not disable local formatting. # ───────────────────────────────────────────────────────────────────────────── sync: # Master switch for all cross-server communication. - enabled: true + enabled: false # Select which features are synchronized. # The corresponding feature above must also be enabled. @@ -57,15 +57,22 @@ sync: # ───────────────────────────────────────────────────────────────────────────── # Groups -# Only used when general.source is CONFIG. +# Only used when general.source is config. # -# Supported placeholders: +# Placeholders in display-name: # Group prefix # Group suffix # Group color # Normal Minecraft player name +# +# Placeholders in chat-format: +# Group prefix +# Group suffix +# Group color +# Normal Minecraft player name (alias: ) # Formatted display name # Chat message +# # ───────────────────────────────────────────────────────────────────────────── groups: - name: owner @@ -77,7 +84,7 @@ groups: suffix: '' # Used by the placeholder and player name coloring. color: '<#DC2626>' - # Formatted player name. + # Name shown above the player's head, e.g. ''. display-name: '' # Format used when features.chat is enabled. chat-format: ' <#475569>» <#F8FAFC>' diff --git a/prefixes-shared/src/main/resources/messages.yml b/prefixes-shared/src/main/resources/messages.yml new file mode 100644 index 0000000..aa576c5 --- /dev/null +++ b/prefixes-shared/src/main/resources/messages.yml @@ -0,0 +1,25 @@ +# The messages format version. Do not change this value. +version: 1 + +# ─────────────────────────────────────────────────────────────────────────────── +# Variables +# Reusable variables that can be used throughout the messages. +# Usage: will be replaced with the defined value. +# ─────────────────────────────────────────────────────────────────────────────── +variables: + prefix: '⚡' + +# ─────────────────────────────────────────────────────────────────────────────── +# Command Messages +# ─────────────────────────────────────────────────────────────────────────────── +command: + help: + title: ' <#0EA5E9>SimpleCloud Prefixes commands' + entry: '<#E2E8F0>' + + permission: + denied: ' <#DC2626>You do not have permission to use this command.' + + reload: + success: ' <#A3E635>SimpleCloud Prefixes was successfully reloaded.' + failed: ' <#DC2626>SimpleCloud Prefixes could not be reloaded.' diff --git a/prefixes-shared/src/test/kotlin/app/simplecloud/prefixes/shared/config/PrefixesConfigTest.kt b/prefixes-shared/src/test/kotlin/app/simplecloud/prefixes/shared/config/PrefixesConfigTest.kt deleted file mode 100644 index 393fda6..0000000 --- a/prefixes-shared/src/test/kotlin/app/simplecloud/prefixes/shared/config/PrefixesConfigTest.kt +++ /dev/null @@ -1,49 +0,0 @@ -package app.simplecloud.prefixes.shared.config - -import app.simplecloud.plugin.api.shared.config.ConfigurationFactory -import java.nio.file.Files -import kotlin.io.path.readText -import kotlin.io.path.writeText -import kotlin.test.Test -import kotlin.test.assertContains -import kotlin.test.assertEquals - -class PrefixesConfigTest { - - @Test - fun `installs and loads the documented version one config`() { - val directory = Files.createTempDirectory("prefixes-config-test") - val path = directory.resolve("config.yml") - - try { - DefaultConfigInstaller.install(path, javaClass.classLoader) - - val yaml = path.readText() - assertContains(yaml, "version: 1") - assertContains(yaml, "# General") - assertContains(yaml, "features:") - assertContains(yaml, "display-name: true") - assertContains(yaml, "# Sync") - assertContains(yaml, "channels:") - assertContains(yaml, "sources:") - assertContains(yaml, "CURRENT") - assertContains(yaml, "Special values:") - assertContains(yaml, "# Groups") - assertContains(yaml, "Supported placeholders:") - - val factory = ConfigurationFactory(path.toFile(), PrefixesConfig::class.java) - val expected = PrefixesConfig() - val loaded = factory.loadOrCreate(expected) - - assertEquals(expected, loaded) - - val customized = "$yaml\n# This user comment must be kept.\n" - path.writeText(customized) - DefaultConfigInstaller.install(path, javaClass.classLoader) - assertEquals(customized, path.readText()) - } finally { - Files.deleteIfExists(path) - Files.deleteIfExists(directory) - } - } -} diff --git a/prefixes-shared/src/test/kotlin/app/simplecloud/prefixes/shared/group/luckperms/LuckPermsGroupProviderTest.kt b/prefixes-shared/src/test/kotlin/app/simplecloud/prefixes/shared/group/luckperms/LuckPermsGroupProviderTest.kt deleted file mode 100644 index aa3211e..0000000 --- a/prefixes-shared/src/test/kotlin/app/simplecloud/prefixes/shared/group/luckperms/LuckPermsGroupProviderTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -package app.simplecloud.prefixes.shared.group.luckperms - -import net.luckperms.api.model.group.Group -import java.lang.reflect.Proxy -import java.util.OptionalInt -import kotlin.test.Test -import kotlin.test.assertEquals - -class LuckPermsGroupProviderTest { - - @Test - fun `selects the applicable group with the highest weight`() { - val selected = selectHighestWeightedGroup( - listOf( - group("member", 10), - group("admin", 100), - group("moderator", 50) - ), - primaryGroupName = "member" - ) - - assertEquals("admin", selected?.name) - } - - @Test - fun `prefers the primary group when weights are equal`() { - val selected = selectHighestWeightedGroup( - listOf( - group("admin", 100), - group("moderator", 100) - ), - primaryGroupName = "moderator" - ) - - assertEquals("moderator", selected?.name) - } - - @Test - fun `uses the group name as a stable final tie breaker`() { - val groups = listOf( - group("moderator", 100), - group("admin", 100) - ) - - val selected = selectHighestWeightedGroup(groups, primaryGroupName = "member") - val selectedFromReversedInput = selectHighestWeightedGroup(groups.reversed(), primaryGroupName = "member") - - assertEquals("admin", selected?.name) - assertEquals("admin", selectedFromReversedInput?.name) - } - - @Test - fun `treats a missing group weight as zero`() { - val selected = selectHighestWeightedGroup( - listOf( - group("unweighted"), - group("negative", -1) - ), - primaryGroupName = "negative" - ) - - assertEquals("unweighted", selected?.name) - } - - private fun group(name: String, weight: Int? = null): Group { - return Proxy.newProxyInstance( - Group::class.java.classLoader, - arrayOf(Group::class.java) - ) { proxy, method, arguments -> - when (method.name) { - "getName" -> name - "getWeight" -> weight?.let(OptionalInt::of) ?: OptionalInt.empty() - "equals" -> proxy === arguments?.firstOrNull() - "hashCode" -> System.identityHashCode(proxy) - "toString" -> "Group(name=$name, weight=$weight)" - else -> error("Unexpected Group method: ${method.name}") - } - } as Group - } -} diff --git a/prefixes-shared/src/test/kotlin/app/simplecloud/prefixes/shared/sync/tablist/TablistEntryTest.kt b/prefixes-shared/src/test/kotlin/app/simplecloud/prefixes/shared/sync/tablist/TablistEntryTest.kt deleted file mode 100644 index 18f3ba4..0000000 --- a/prefixes-shared/src/test/kotlin/app/simplecloud/prefixes/shared/sync/tablist/TablistEntryTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -package app.simplecloud.prefixes.shared.sync.tablist - -import net.kyori.adventure.text.Component -import java.util.UUID -import kotlin.test.Test -import kotlin.test.assertEquals - -class TablistEntryTest { - - @Test - fun `serializes all player info`() { - val entry = TablistEntry( - uniqueId = UUID.fromString("c1688a0f-13e4-4b1c-82c6-7f576f4d9021"), - name = "Player", - displayName = Component.text("Displayed Player"), - priority = 42, - profileProperties = listOf( - ProfileProperty("textures", "texture-value", "texture-signature"), - ProfileProperty("unsigned", "value", null) - ), - latency = 123, - gameMode = TablistGameMode.CREATIVE, - showHat = false, - listOrder = 7 - ) - - assertEquals(entry, TablistEntryMapper.fromDefinition(TablistEntryMapper.toDefinition(entry))) - } - - @Test - fun `uses safe defaults for older sync messages`() { - val definition = TablistEntry( - uniqueId = UUID.fromString("b2460751-a9e8-4b4a-ace2-25b883231667"), - name = "Player", - displayName = Component.text("Player"), - priority = 0 - ).let(TablistEntryMapper::toDefinition).toBuilder() - .clearGameMode() - .clearShowHat() - .build() - - val entry = TablistEntryMapper.fromDefinition(definition) - - assertEquals(TablistGameMode.SURVIVAL, entry.gameMode) - assertEquals(true, entry.showHat) - } -} diff --git a/prefixes-shared/src/test/kotlin/app/simplecloud/prefixes/shared/utilities/PlayerDisplayFormatterTest.kt b/prefixes-shared/src/test/kotlin/app/simplecloud/prefixes/shared/utilities/PlayerDisplayFormatterTest.kt deleted file mode 100644 index 0ff4b65..0000000 --- a/prefixes-shared/src/test/kotlin/app/simplecloud/prefixes/shared/utilities/PlayerDisplayFormatterTest.kt +++ /dev/null @@ -1,38 +0,0 @@ -package app.simplecloud.prefixes.shared.utilities - -import app.simplecloud.prefixes.api.group.PrefixesPlayerData -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import kotlin.test.Test -import kotlin.test.assertEquals - -class PlayerDisplayFormatterTest { - - private val data = PrefixesPlayerData( - prefix = Component.text("[Admin] "), - suffix = Component.text(" !"), - color = NamedTextColor.RED, - displayName = Component.text("Fancy"), - chatFormat = ": ", - priority = 90 - ) - - @Test - fun `uses the configured display name when enabled`() { - assertEquals( - Component.text("Fancy"), - PlayerDisplayFormatter.displayName(data, "Player", enabled = true) - ) - } - - @Test - fun `uses the minecraft player name when display names are disabled`() { - val displayName = PlayerDisplayFormatter.displayName(data, "Player", enabled = false) - - assertEquals(Component.text("Player"), displayName) - assertEquals( - Component.text("[Admin] ").append(displayName).append(Component.text(" !")), - PlayerDisplayFormatter.formatTablistName(data, displayName) - ) - } -} diff --git a/prefixes-shared/src/test/kotlin/app/simplecloud/prefixes/shared/utilities/PrefixesSubjectsTest.kt b/prefixes-shared/src/test/kotlin/app/simplecloud/prefixes/shared/utilities/PrefixesSubjectsTest.kt deleted file mode 100644 index 0b6a49a..0000000 --- a/prefixes-shared/src/test/kotlin/app/simplecloud/prefixes/shared/utilities/PrefixesSubjectsTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -package app.simplecloud.prefixes.shared.utilities - -import app.simplecloud.prefixes.shared.config.ALL_SYNC_SOURCE -import app.simplecloud.prefixes.shared.config.CURRENT_SYNC_SOURCE -import kotlin.test.Test -import kotlin.test.assertEquals - -class PrefixesSubjectsTest { - - private val subjects = PrefixesSubjects( - networkId = "network", - origin = "lobby", - serverId = "lobby-1" - ) - - @Test - fun `extracts the same publisher from update and remove subjects`() { - val updateSubject = subjects.own(PrefixesSubjects.TABLIST_UPDATE) - val removeSubject = subjects.own(PrefixesSubjects.TABLIST_REMOVE) - - assertEquals( - "network.prefixes.lobby.lobby-1", - subjects.publisherId(updateSubject, PrefixesSubjects.TABLIST_UPDATE) - ) - assertEquals( - "network.prefixes.lobby.lobby-1", - subjects.publisherId(removeSubject, PrefixesSubjects.TABLIST_REMOVE) - ) - } - - @Test - fun `resolves the current server group`() { - assertEquals( - listOf("network.prefixes.lobby.*.chat"), - subjects.patterns(listOf(CURRENT_SYNC_SOURCE), PrefixesSubjects.CHAT) - ) - } - - @Test - fun `accepts group names and persistent server ids in the same source list`() { - assertEquals( - listOf( - "network.prefixes.survival.*.chat", - "network.prefixes.standalone-lobby-1.*.chat" - ), - subjects.patterns( - listOf("survival", "standalone-lobby-1"), - PrefixesSubjects.CHAT - ) - ) - } - - @Test - fun `resolves all servers to one wildcard pattern`() { - assertEquals( - listOf("network.prefixes.*.*.tablist.update"), - subjects.patterns(listOf(ALL_SYNC_SOURCE), PrefixesSubjects.TABLIST_UPDATE) - ) - } - - @Test - fun `ignores blank and duplicate sources`() { - assertEquals( - listOf("network.prefixes.lobby.*.chat"), - subjects.patterns( - listOf("", " lobby ", CURRENT_SYNC_SOURCE, CURRENT_SYNC_SOURCE), - PrefixesSubjects.CHAT - ) - ) - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 99b5e3c..a6e6b4b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,8 +4,9 @@ plugins { include( "prefixes-api", - "prefixes-shared", - "prefixes-paper" + "prefixes-minestom", + "prefixes-paper", + "prefixes-shared" ) rootProject.name = "prefixes" \ No newline at end of file