diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..12b3274 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,38 @@ +name: Build Pull Request + +on: + pull_request: + branches: + - main + types: [opened, synchronize, reopened, ready_for_review] + +concurrency: + group: build-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + name: build + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - 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 and test + run: ./gradlew clean build diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml deleted file mode 100644 index c45de60..0000000 --- a/.github/workflows/release-dev.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Create Release - -on: - workflow_dispatch: - -jobs: - build: - name: Build ShadowJars and Create Release - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - ref: develop # Ensure it works from the develop branch - - - name: Set up JDK 21 - uses: actions/setup-java@v3 - with: - distribution: 'adopt' - java-version: '21' - - - name: Cache Gradle packages - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - gradle-${{ runner.os }} - - - name: Make gradlew executable - run: chmod +x ./gradlew - - - name: 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 SimpleCloud Repository - run: ./gradlew publishMavenJavaPublicationToSimplecloudRepository - env: - COMMIT_HASH: ${{ env.COMMIT_HASH }} - SIMPLECLOUD_USERNAME: ${{ secrets.SIMPLECLOUD_USERNAME }} - SIMPLECLOUD_PASSWORD: ${{ secrets.SIMPLECLOUD_PASSWORD }} - - - name: Create Release - id: create_release - uses: actions/create-release@v1 - with: - tag_name: v${{ env.GRADLE_VERSION }}-dev.${{ env.COMMIT_HASH }} - release_name: v${{ env.GRADLE_VERSION }}-dev.${{ env.COMMIT_HASH }} - draft: false - prerelease: true - commitish: develop - body: | - This release contains dev builds for all Gradle modules. - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..47b038d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,94 @@ +name: Release and Publish + +on: + push: + branches: + - main + - develop + workflow_dispatch: + inputs: + type: + description: 'Release type' + required: true + default: 'dev' + type: choice + options: + - dev + - release + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: write + +jobs: + release: + name: Build and Publish + runs-on: ubuntu-latest + + env: + GRADLE_ARGS: ${{ ((github.event_name == 'push' && github.ref_name == 'main') || inputs.type == 'release') && '-Prelease' || '' }} + IS_RELEASE: ${{ (github.event_name == 'push' && github.ref_name == 'main') || inputs.type == 'release' }} + + steps: + - name: Check branch + if: env.IS_RELEASE == 'true' && github.ref_name != 'main' + run: | + echo "::error::Releases can only be created from main" + exit 1 + + - name: Checkout repository + uses: actions/checkout@v7 + + - 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: Get Gradle Version + run: echo "GRADLE_VERSION=$(./gradlew properties -q $GRADLE_ARGS | awk '/^version:/ {print $2}')" >> $GITHUB_ENV + + - name: Build + run: ./gradlew clean build $GRADLE_ARGS + + - name: Publish to SimpleCloud Repository + run: ./gradlew publish $GRADLE_ARGS + env: + SIMPLECLOUD_USERNAME: ${{ secrets.SIMPLECLOUD_USERNAME }} + SIMPLECLOUD_PASSWORD: ${{ secrets.SIMPLECLOUD_PASSWORD }} + + - name: Create Release + run: | + jars=() + for jar in $(find . -type f -name "*.jar" -path "*/build/libs/*.jar" -not -path "./build/libs/*"); do + echo "Uploading $jar" + jars+=("$jar") + done + + if [ "$IS_RELEASE" = "true" ]; then + flags=() + notes="This release contains release builds for all Gradle modules." + else + flags=(--prerelease) + notes="This release contains dev builds for all Gradle modules." + fi + + gh release create "v${GRADLE_VERSION}" \ + --target "${{ github.sha }}" \ + --title "v${GRADLE_VERSION}" \ + --notes "$notes" \ + "${flags[@]}" \ + "${jars[@]}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index b1dff0d..108d567 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,7 @@ build/ !**/src/test/**/build/ ### IntelliJ IDEA ### -.idea/modules.xml -.idea/jarRepositories.xml -.idea/compiler.xml -.idea/libraries/ +.idea/ *.iws *.iml *.ipr diff --git a/LICENSE b/LICENSE index 2cd34d3..019b392 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [2025] [SimpleCloud] + Copyright 2025-2026 SimpleCloud Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/build.gradle.kts b/build.gradle.kts index de93257..53464ea 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,69 +1,82 @@ +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.sonatype.central.portal.publisher) - `maven-publish` } -val baseVersion = "0.0.1" -val commitHash = System.getenv("COMMIT_HASH") -val snapshotVersion = "${baseVersion}-dev.$commitHash" +val baseVersion = "1.0.0" +val isRelease = providers.gradleProperty("release").isPresent +val commitHash = providers.exec { + commandLine("git", "rev-parse", "--short", "HEAD") +}.standardOutput.asText.get().trim() allprojects { group = "app.simplecloud.plugin" - version = if (commitHash != null) snapshotVersion else baseVersion + version = if (isRelease) baseVersion else "$baseVersion-dev.$commitHash" repositories { mavenCentral() mavenLocal() - maven("https://libraries.minecraft.net") maven("https://buf.build/gen/maven") + maven("https://repo.spectr.is/snapshots") maven("https://repo.simplecloud.app/snapshots") - maven("https://buf.build/gen/maven") + maven("https://repo.papermc.io/repository/maven-public") } } subprojects { - apply(plugin = "org.jetbrains.kotlin.jvm") - apply(plugin = "net.thebugmc.gradle.sonatype-central-portal-publisher") + apply { + plugin("maven-publish") + plugin("org.jetbrains.kotlin.jvm") + } dependencies { - compileOnly(rootProject.libs.kotlinJvm) - compileOnly(rootProject.libs.bundles.simpleCloudController) + testImplementation(rootProject.libs.kotlin.test) + compileOnly(rootProject.libs.kotlin.jvm) + compileOnly(rootProject.libs.kotlin.coroutines) + compileOnly(rootProject.libs.simplecloud.api) + compileOnly(rootProject.libs.bundles.cloud) compileOnly(rootProject.libs.bundles.adventure) + compileOnly(rootProject.libs.bundles.configurate) + compileOnly(rootProject.libs.slf4j.api) } kotlin { - jvmToolchain(21) + jvmToolchain(25) + compilerOptions { + jvmTarget = JvmTarget.JVM_25 + languageVersion = KotlinVersion.KOTLIN_2_4 + apiVersion = KotlinVersion.KOTLIN_2_4 + } + } + + java { + toolchain.languageVersion.set(JavaLanguageVersion.of(25)) + } + + tasks.test { + useJUnitPlatform() } publishing { + publications { + create("maven") { + from(components["java"]) + } + } + repositories { maven { name = "simplecloud" - url = uri("https://repo.simplecloud.app/snapshots/") + url = uri("https://repo.simplecloud.app/snapshots") credentials { - username = System.getenv("SIMPLECLOUD_USERNAME")?: (project.findProperty("simplecloudUsername") as? String) - password = System.getenv("SIMPLECLOUD_PASSWORD")?: (project.findProperty("simplecloudPassword") as? String) - } - authentication { - create("basic") + username = System.getenv("SIMPLECLOUD_USERNAME") + password = System.getenv("SIMPLECLOUD_PASSWORD") } } } - - publications { - create("mavenJava") { - from(components["java"]) - } - } } - signing { - if (commitHash != null) { - return@signing - } - - sign(publishing.publications) - useGpgCmd() - } } \ No newline at end of file diff --git a/docs/config-migration-system.md b/docs/config-migration-system.md new file mode 100644 index 0000000..5bb953a --- /dev/null +++ b/docs/config-migration-system.md @@ -0,0 +1,152 @@ +# Config Migration System + +The migration system lets you upgrade old YAML configs to the current structure before they are deserialized. +It is designed to plug directly into `ConfigurationFactory`, `YamlDirectoryRepository`, and `YamlFileConfigurator`. + +## Main pieces + +- `VersionedConfig`: marks a config as migration-capable +- `ConfigMigrator`: defines the current version and the upgrade steps +- root YAML key `version`: stores the config version on disk + +## Defining a versioned config + +Your config should implement `VersionedConfig` and expose the current version. + +```kotlin +import app.simplecloud.plugin.api.shared.config.VersionedConfig +import org.spongepowered.configurate.objectmapping.ConfigSerializable + +@ConfigSerializable +class WarpConfig : VersionedConfig { + override var version: Int = 2 + + var name: String = "" + var world: String = "world" + var enabled: Boolean = true +} +``` + +## Creating a migrator + +`ConfigMigrator` upgrades a YAML node step by step. +Each migration receives the raw Configurate node, so you can rename fields, move sections, or add defaults. + +```kotlin +import app.simplecloud.plugin.api.shared.config.ConfigMigrator + +val warpMigrator = ConfigMigrator.builder(currentVersion = 2) + .migrate(fromVersion = 0, toVersion = 1) { node -> + val oldWorldName = node.node("location", "worldName").string ?: return@migrate + node.node("world").set(oldWorldName) + node.node("location").set(null) + } + .migrate(fromVersion = 1, toVersion = 2) { node -> + if (node.node("enabled").virtual()) { + node.node("enabled").set(true) + } + } + .build() +``` + +## Using migrations with a single config + +```kotlin +import app.simplecloud.plugin.api.shared.config.ConfigurationFactory + +val factory = ConfigurationFactory( + file = dataDirectory.resolve("config.yml").toFile(), + javaClass = WarpConfig::class.java, + configMigrator = warpMigrator, +) + +val config = factory.loadOrCreate(WarpConfig()) +``` + +When an older file is loaded: + +1. the YAML is parsed +2. the migrator checks the stored `version` +3. all missing migration steps are applied in order +4. the updated YAML is written back to disk +5. the final structure is deserialized into your config class + +## Using migrations with a directory repository + +```kotlin +import app.simplecloud.plugin.api.shared.config.YamlDirectoryRepository +import java.nio.file.Path + +class WarpRepository( + directory: Path, +) : YamlDirectoryRepository( + directory = directory, + javaClass = WarpConfig::class.java, + configMigrator = warpMigrator, +) { + + override fun save(entity: WarpConfig) { + save("${entity.name}.yml", entity) + } + + override fun find(identifier: String): WarpConfig? { + return findAll().firstOrNull { it.name == identifier } + } +} +``` + +Calling `load()` will migrate each YAML file as it is read. + +## Legacy files without a version + +Files that do not have a `version` key are treated as version `0` by default. +That makes it easy to introduce migrations for older configs that existed before versioning. + +Example legacy file: + +```yaml +name: spawn +location: + worldName: lobby +``` + +After migration: + +```yaml +version: 2 +name: spawn +world: lobby +enabled: true +``` + +## Changing the fallback version + +If your oldest supported files should be treated as something other than version `0`, set a fallback version: + +```kotlin +val migrator = ConfigMigrator.builder(currentVersion = 3) + .fallbackVersion(1) + .migrate(fromVersion = 1, toVersion = 2) { node -> + node.node("newField").set("default") + } + .migrate(fromVersion = 2, toVersion = 3) { node -> + node.node("anotherField").set(true) + } + .build() +``` + +## Important behavior + +- migrations only work for configs that implement `VersionedConfig` +- the migration chain must be complete from the file version to `currentVersion` +- if a file version is newer than the library supports, loading fails +- if a migration step is missing, loading fails +- migrated files are persisted automatically after a successful load + +## Best practices + +- increase `version` only when the YAML structure changes +- keep each migration small and focused +- prefer additive migrations where possible +- remove old fields explicitly with `node.node("oldField").set(null)` when renaming +- test migrations with real old YAML examples diff --git a/docs/config-system.md b/docs/config-system.md new file mode 100644 index 0000000..5a53053 --- /dev/null +++ b/docs/config-system.md @@ -0,0 +1,158 @@ +# Config System + +The config system in `plugin-shared` is built around Configurate YAML serialization. +Most configs should be regular Kotlin classes annotated with `@ConfigSerializable`. + +## Main pieces + +- `ConfigurationFactory`: loads or creates one config file +- `YamlDirectoryRepository`: manages many YAML files in one directory +- `YamlFileConfigurator`: low-level file serializer if you need direct access +- `AbstractMessageConfig`: optional base class for MiniMessage-based message configs + +## Single config file + +Use `ConfigurationFactory` when you have one config file such as `config.yml`. + +```kotlin +import app.simplecloud.plugin.api.shared.config.ConfigurationFactory +import org.spongepowered.configurate.objectmapping.ConfigSerializable +import java.io.File + +@ConfigSerializable +class PluginConfig { + var prefix: String = "[Example]" + var enabled: Boolean = true +} + +val factory = ConfigurationFactory( + File(dataDirectory.toFile(), "config.yml"), + PluginConfig::class.java, +) + +val config = factory.loadOrCreate(PluginConfig()) + +if (config.enabled) { + println(config.prefix) +} + +config.enabled = false +factory.save(config) +``` + +### Useful methods + +- `loadOrCreate(defaultConfig)`: loads the file, or creates it from the default object +- `get()`: returns the cached config after it has been loaded +- `save(entry)`: writes the config back to disk +- `reload()`: reloads the file into the cache + +## Directory-based configs + +Use `YamlDirectoryRepository` when each entity should live in its own YAML file. +This works well for things like warps, templates, arenas, or message profiles. + +```kotlin +import app.simplecloud.plugin.api.shared.config.YamlDirectoryRepository +import org.spongepowered.configurate.objectmapping.ConfigSerializable +import java.nio.file.Path + +@ConfigSerializable +class WarpConfig { + var name: String = "" + var world: String = "world" + var x: Double = 0.0 + var y: Double = 64.0 + var z: Double = 0.0 +} + +class WarpRepository( + directory: Path, +) : YamlDirectoryRepository( + directory = directory, + javaClass = WarpConfig::class.java, +) { + + override fun save(entity: WarpConfig) { + save("${entity.name}.yml", entity) + } + + override fun find(identifier: String): WarpConfig? { + return findAll().firstOrNull { it.name.equals(identifier, ignoreCase = true) } + } +} +``` + +Usage: + +```kotlin +val repository = WarpRepository(dataDirectory.resolve("warps")) + +repository.load() + +val lobby = WarpConfig().apply { + name = "lobby" + world = "world" + x = 10.0 + y = 65.0 + z = -5.0 +} + +repository.save(lobby) + +val loadedLobby = repository.find("lobby") +val allWarps = repository.findAll() +``` + +### Repository flow + +- implement your own repository class +- extend `YamlDirectoryRepository` +- decide how files are named in `save(entity)` +- decide how lookups work in `find(identifier)` +- call `load()` during startup to populate the cache + +## Direct YAML access + +`YamlFileConfigurator` is the lower-level serializer used by both higher abstractions. +Use it if you want YAML serialization without repository or factory behavior. + +```kotlin +import app.simplecloud.plugin.api.shared.config.YamlFileConfigurator + +val configurator = YamlFileConfigurator(PluginConfig::class.java) +val file = dataDirectory.resolve("config.yml").toFile() + +configurator.save(file, PluginConfig()) +val loaded = configurator.load(file) +``` + +## Message configs + +`AbstractMessageConfig` is helpful when your config stores MiniMessage strings and shared variables. + +```kotlin +import app.simplecloud.plugin.api.shared.config.AbstractMessageConfig + +class MessagesConfig : AbstractMessageConfig() { + override val variables = mapOf( + "prefix" to "[Example]" + ) + + val noPermission = " You do not have permission." +} +``` + +Then render a message with: + +```kotlin +val component = messagesConfig.msg(messagesConfig.noPermission) +``` + +## Best practices + +- annotate config classes with `@ConfigSerializable` +- prefer mutable `var` fields with default values for YAML-backed classes +- use `ConfigurationFactory` for one file and `YamlDirectoryRepository` for many files +- load repositories during startup so `findAll()` and `find()` work from cache +- keep config classes simple and serialization-friendly diff --git a/docs/matcher-system.md b/docs/matcher-system.md new file mode 100644 index 0000000..15e4a49 --- /dev/null +++ b/docs/matcher-system.md @@ -0,0 +1,113 @@ +# Matcher System + +The matcher system is a small utility for checking whether names or values match a configured rule. +The main user-facing type is `ServerMatcherConfiguration`. + +## Main pieces + +- `ServerMatcherConfiguration`: a serializable matcher config +- `OperationType`: the available match operations +- operation matcher implementations: the low-level matching logic + +## Basic usage + +`ServerMatcherConfiguration` is a YAML-friendly wrapper around an operation, a comparison value, and an optional negation flag. + +```kotlin +import app.simplecloud.plugin.api.shared.matcher.OperationType +import app.simplecloud.plugin.api.shared.matcher.ServerMatcherConfiguration + +val matcher = ServerMatcherConfiguration( + operation = OperationType.STARTS_WITH, + value = "Lobby-", + negate = false, +) + +val matches = matcher.matches("Lobby-1") +``` + +## Matching multiple names + +```kotlin +val matcher = ServerMatcherConfiguration( + operation = OperationType.CONTAINS, + value = "event", +) + +val names = listOf("lobby", "event-1", "event-2") + +val anyMatches = matcher.anyMatches(names) +val allMatch = matcher.allMatches(names) +``` + +## Available operations + +These operations are available through `OperationType`: + +- `REGEX` +- `PATTERN` +- `EQUALS` +- `CONTAINS` +- `STARTS_WITH` +- `ENDS_WITH` +- `GREATER_THAN` + +## YAML example + +Because `ServerMatcherConfiguration` is `@ConfigSerializable`, it can be used directly inside your config classes. + +```kotlin +import app.simplecloud.plugin.api.shared.matcher.ServerMatcherConfiguration +import org.spongepowered.configurate.objectmapping.ConfigSerializable + +@ConfigSerializable +class ExampleConfig { + var targetMatcher: ServerMatcherConfiguration = ServerMatcherConfiguration() +} +``` + +Example YAML: + +```yaml +targetMatcher: + operation: STARTS_WITH + value: Lobby- + negate: false +``` + +## Negation + +Use `negate = true` to invert the result. + +```kotlin +val matcher = ServerMatcherConfiguration( + operation = OperationType.ENDS_WITH, + value = "-dev", + negate = true, +) + +matcher.matches("proxy-1") +``` + +This returns `true` because `"proxy-1"` does not end with `"-dev"`. + +## Choosing an operation + +- use `EQUALS` for exact names +- use `STARTS_WITH` or `ENDS_WITH` for prefixes and suffixes +- use `CONTAINS` for simple substring checks +- use `REGEX` when you need explicit regular expressions +- use `PATTERN` only if you specifically want the alternate Java `Pattern`-based matcher behavior in this library + +## Important limitation + +`ServerMatcherConfiguration` currently stores its comparison `value` as a `String`. +That means it is mainly intended for string-based matching. + +`GREATER_THAN` exists in the lower-level `OperationType` API for integer comparisons, but it is not a natural fit for `ServerMatcherConfiguration` because that wrapper does not store numeric values. + +## Best practices + +- use `ServerMatcherConfiguration` inside other config classes when you want user-configurable filters +- prefer simple operations like `EQUALS` or `STARTS_WITH` unless you really need regex +- document the expected input format when exposing `REGEX` or `PATTERN` to end users diff --git a/docs/placeholder-system.md b/docs/placeholder-system.md new file mode 100644 index 0000000..a058fb2 --- /dev/null +++ b/docs/placeholder-system.md @@ -0,0 +1,191 @@ +# Placeholder System + +The placeholder system builds MiniMessage `TagResolver`s for SimpleCloud objects such as servers, groups, and persistent servers. +It is designed to turn strings like `` or `` into rendered Adventure components. + +## Main pieces + +- `PlaceholderProvider`: entry point for the built-in providers +- `ServerPlaceholderProvider` +- `GroupPlaceholderProvider` +- `PersistentServerPlaceholderProvider` +- `ArgumentsResolver`: dynamic placeholders with arguments such as `` +- `String.appendToComponent(...)`: convenience extensions for rendering messages + +## Quick start + +If you already have a `Server`, `Group`, or `PersistentServer`, the easiest API is the extension function on `String`. + +```kotlin +import app.simplecloud.plugin.api.shared.extension.appendToComponent + +val component = " is online on :".appendToComponent(server) +``` + +This returns an Adventure `Component`. + +## Built-in entry points + +The shared object exposes three built-in providers: + +```kotlin +import app.simplecloud.plugin.api.shared.placeholder.PlaceholderProvider + +val serverProvider = PlaceholderProvider.serverPlaceholderProvider +val groupProvider = PlaceholderProvider.groupPlaceholderProvider +val persistentServerProvider = PlaceholderProvider.persistentServerPlaceholderProvider +``` + +You can use the providers directly if you want a `TagResolver` or if you want to append placeholders to a string manually. + +## Server placeholders + +Server placeholders are backed by `SingleServerPlaceholderExecutor`. +Available keys include: + +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` + +Example: + +```kotlin +val component = " (/)".appendToComponent(server) +``` + +## Group placeholders + +Group placeholders are backed by `SingleGroupPlaceholderExecutor`. +Available keys include: + +- `` +- `` +- `` +- `` +- `` +- `` + +Example: + +```kotlin +val component = " has players online".appendToComponent(group) +``` + +## Persistent server placeholders + +Persistent server placeholders are backed by `SinglePersistentServerPlaceholderExecutor`. +Available keys include: + +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` + +Example: + +```kotlin +val component = " is running on type ".appendToComponent(persistentServer) +``` + +## Argument-based placeholders + +Some placeholders accept arguments. +These are powered by `ArgumentsResolver`. + +Built-in resolvers include: + +- `PropertiesArgumentsResolver`: `` +- `EnvironmentArgumentsResolver`: `` + +Examples: + +```kotlin +val component = "".appendToComponent(server) +val component2 = "".appendToComponent(server) +``` + +For groups, additional resolvers are registered automatically: + +- `` or `` +- `` or `` + +## Prefixing placeholders + +You can prefix all generated keys to avoid collisions when combining multiple resolver sets. + +```kotlin +val component = PlaceholderProvider.serverPlaceholderProvider.append( + server, + " -> :", + prefix = "source", +) +``` + +With `prefix = "source"`, `` becomes `` and `` becomes ``. + +## Working with multiple values + +The providers can also build a resolver from multiple values. +That is mostly useful when you want argument resolvers aggregated from several objects. + +```kotlin +val resolver = PlaceholderProvider.groupPlaceholderProvider.getTagResolver(listOf(groupA, groupB)) +``` + +You can then pass that resolver into your own MiniMessage rendering flow. + +## Direct provider usage + +```kotlin +val resolver = PlaceholderProvider.serverPlaceholderProvider.getTagResolver(listOf(server)) +val component = PlaceholderProvider.serverPlaceholderProvider.append( + server, + " - players" +) +``` + +## Custom argument resolvers + +You can contribute additional resolvers when rendering: + +```kotlin +import app.simplecloud.plugin.api.shared.placeholder.argument.ArgumentsResolver +import net.kyori.adventure.text.minimessage.tag.Tag +import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue + +class RankResolver : ArgumentsResolver { + override fun getKey() = "rank" + + override suspend fun resolve(arguments: ArgumentQueue): Tag? { + val playerName = arguments.popOr("player expected").value() + return Tag.preProcessParsed(fetchRank(playerName)) + } +} + +val component = " - rank: ".appendToComponent( + server, + RankResolver(), +) +``` + +## Best practices + +- use the `String.appendToComponent(...)` extensions for the most concise API +- use prefixes when combining resolvers from different domains +- keep custom argument resolvers fast and predictable +- supply sensible defaults for property- and environment-based placeholders diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 85d6996..adaa298 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,22 +1,56 @@ [versions] -kotlin = "2.0.0" -simpleCloudController = "0.0.30-dev.e3e27fc" -sonatype-central-portal-publisher = "1.2.3" -adventure = "4.18.0" +kotlin = "2.4.21" +kotlinx-coroutines = "1.11.0" + +simplecloud-api = "0.1.0-platform.58-dev.5.1-fe01ce5" + +velocity = "4.1.0-SNAPSHOT" +bungeecord = "1.21-R0.4" +paper = "26.3.build.+" +minestom = "26_3-SNAPSHOT" + +adventure = "5.2.0" + +configurate = "4.2.0" +slf4j = "2.0.20" + +cloud = "2.1.0" +cloud-platform = "2.0.0" +cloud-minestom = "2.2.0-SNAPSHOT" [libraries] -kotlinJvm = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } +kotlin-jvm = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } + +simplecloud-api = { module = "app.simplecloud.api:api", version.ref = "simplecloud-api" } + +velocity-api = { module = "com.velocitypowered:velocity-api", version.ref = "velocity" } +bungeecord-api = { module = "net.md-5:bungeecord-api", version.ref = "bungeecord" } +paper-api = { module = "io.papermc.paper:paper-api", version.ref = "paper" } +minestom = { module = "net.minestom:minestom", version.ref = "minestom" } + +adventure = { 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" } + +configurate-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate" } +configurate-gson = { module = "org.spongepowered:configurate-gson", version.ref = "configurate" } +configurate-extra-kotlin = { module = "org.spongepowered:configurate-extra-kotlin", version.ref = "configurate" } -simpleCloudControllerApi = { module = "app.simplecloud.controller:controller-api", version.ref = "simpleCloudController" } -simpleCloudControllerShared = { module = "app.simplecloud.controller:controller-shared", version.ref = "simpleCloudController" } +slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } -adventureApi = { module = "net.kyori:adventure-api", version.ref = "adventure" } -adventureMiniMessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure" } +cloud-core = { module = "org.incendo:cloud-core", version.ref = "cloud" } +cloud-coroutines = { module = "org.incendo:cloud-kotlin-coroutines", version.ref = "cloud" } +cloud-velocity = { module = "org.incendo:cloud-velocity", version.ref = "cloud-platform" } +cloud-bungee = { module = "org.incendo:cloud-bungee", version.ref = "cloud-platform" } +cloud-paper = { module = "org.incendo:cloud-paper", version.ref = "cloud-platform" } +cloud-minestom = { module = "org.incendo:cloud-minestom", version.ref = "cloud-minestom" } [bundles] -simpleCloudController = ["simpleCloudControllerApi", "simpleCloudControllerShared"] -adventure = ["adventureApi", "adventureMiniMessage"] +adventure = ["adventure", "adventure-text-minimessage"] +configurate = ["configurate-yaml", "configurate-gson", "configurate-extra-kotlin"] +cloud = ["cloud-core", "cloud-coroutines"] [plugins] -kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -sonatype-central-portal-publisher = { id = "net.thebugmc.gradle.sonatype-central-portal-publisher", version.ref = "sonatype-central-portal-publisher" } \ No newline at end of file +kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e583..a4b76b9 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ed33eb3..1a31fd1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Sun Dec 29 18:17:48 CET 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c787..a58591e 100755 --- a/gradlew +++ b/gradlew @@ -231,4 +231,4 @@ eval "set -- $( tr '\n' ' ' )" '"$@"' -exec "$JAVACMD" "$@" +exec "$JAVACMD" "$@" \ No newline at end of file diff --git a/gradlew.bat b/gradlew.bat index 107acd3..477c896 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -86,4 +86,4 @@ exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal -:omega +:omega \ No newline at end of file diff --git a/plugin-bungeecord/build.gradle.kts b/plugin-bungeecord/build.gradle.kts new file mode 100644 index 0000000..a733652 --- /dev/null +++ b/plugin-bungeecord/build.gradle.kts @@ -0,0 +1,6 @@ +dependencies { + compileOnly(project(":plugin-shared")) + compileOnly(libs.cloud.bungee) + compileOnly(libs.bungeecord.api) + compileOnly(libs.adventure.text.serializer.gson) +} diff --git a/plugin-bungeecord/src/main/kotlin/app/simplecloud/plugin/api/bungeecord/command/BungeeCloudSender.kt b/plugin-bungeecord/src/main/kotlin/app/simplecloud/plugin/api/bungeecord/command/BungeeCloudSender.kt new file mode 100644 index 0000000..e70ffb0 --- /dev/null +++ b/plugin-bungeecord/src/main/kotlin/app/simplecloud/plugin/api/bungeecord/command/BungeeCloudSender.kt @@ -0,0 +1,30 @@ +package app.simplecloud.plugin.api.bungeecord.command + +import app.simplecloud.plugin.api.bungeecord.text.BungeeComponentConverter +import app.simplecloud.plugin.api.shared.command.CloudSender +import net.kyori.adventure.text.Component +import net.md_5.bungee.api.CommandSender +import net.md_5.bungee.api.connection.ProxiedPlayer +import java.util.UUID + +class BungeeCloudSender( + private val commandSender: CommandSender +) : CloudSender { + + fun getCommandSender(): CommandSender { + return commandSender + } + + override fun getUniqueId(): UUID? { + return (commandSender as? ProxiedPlayer)?.uniqueId + } + + override fun hasPermission(permission: String): Boolean { + return commandSender.hasPermission(permission) + } + + override fun sendMessage(message: Component) { + commandSender.sendMessage(BungeeComponentConverter.toBungee(message)) + } + +} diff --git a/plugin-bungeecord/src/main/kotlin/app/simplecloud/plugin/api/bungeecord/command/BungeeCloudSenderMapper.kt b/plugin-bungeecord/src/main/kotlin/app/simplecloud/plugin/api/bungeecord/command/BungeeCloudSenderMapper.kt new file mode 100644 index 0000000..698ddee --- /dev/null +++ b/plugin-bungeecord/src/main/kotlin/app/simplecloud/plugin/api/bungeecord/command/BungeeCloudSenderMapper.kt @@ -0,0 +1,16 @@ +package app.simplecloud.plugin.api.bungeecord.command + +import net.md_5.bungee.api.CommandSender +import org.incendo.cloud.SenderMapper + +class BungeeCloudSenderMapper : SenderMapper { + + override fun map(base: CommandSender): BungeeCloudSender { + return BungeeCloudSender(base) + } + + override fun reverse(mapped: BungeeCloudSender): CommandSender { + return mapped.getCommandSender() + } + +} diff --git a/plugin-bungeecord/src/main/kotlin/app/simplecloud/plugin/api/bungeecord/permission/BungeeCordPermissionChecker.kt b/plugin-bungeecord/src/main/kotlin/app/simplecloud/plugin/api/bungeecord/permission/BungeeCordPermissionChecker.kt new file mode 100644 index 0000000..9753dfb --- /dev/null +++ b/plugin-bungeecord/src/main/kotlin/app/simplecloud/plugin/api/bungeecord/permission/BungeeCordPermissionChecker.kt @@ -0,0 +1,12 @@ +package app.simplecloud.plugin.api.bungeecord.permission + +import app.simplecloud.plugin.api.shared.permission.PermissionChecker +import net.md_5.bungee.api.connection.ProxiedPlayer + +class BungeeCordPermissionChecker : PermissionChecker { + + override fun checkPermission(player: ProxiedPlayer, name: String): Boolean { + return player.hasPermission(name) + } + +} diff --git a/plugin-bungeecord/src/main/kotlin/app/simplecloud/plugin/api/bungeecord/text/BungeeComponentConverter.kt b/plugin-bungeecord/src/main/kotlin/app/simplecloud/plugin/api/bungeecord/text/BungeeComponentConverter.kt new file mode 100644 index 0000000..9d85dc2 --- /dev/null +++ b/plugin-bungeecord/src/main/kotlin/app/simplecloud/plugin/api/bungeecord/text/BungeeComponentConverter.kt @@ -0,0 +1,26 @@ +package app.simplecloud.plugin.api.bungeecord.text + +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer +import net.md_5.bungee.api.chat.BaseComponent +import net.md_5.bungee.chat.ComponentSerializer + +/** + * Converts between Adventure and BungeeCord components. + */ +object BungeeComponentConverter { + + private val serializer = GsonComponentSerializer.gson() + + /** + * Converts the given [component] into a [BaseComponent]. + */ + fun toBungee(component: Component): BaseComponent = + ComponentSerializer.deserialize(serializer.serialize(component)) + + /** + * Converts the given [component] into an Adventure [Component]. + */ + fun fromBungee(component: BaseComponent): Component = + serializer.deserialize(ComponentSerializer.toString(component)) +} diff --git a/plugin-minestom/build.gradle.kts b/plugin-minestom/build.gradle.kts new file mode 100644 index 0000000..32e39cb --- /dev/null +++ b/plugin-minestom/build.gradle.kts @@ -0,0 +1,5 @@ +dependencies { + compileOnly(project(":plugin-shared")) + compileOnly(libs.minestom) + compileOnly(libs.cloud.minestom) +} diff --git a/plugin-minestom/src/main/kotlin/app/simplecloud/plugin/api/minestom/command/MinestomCloudSender.kt b/plugin-minestom/src/main/kotlin/app/simplecloud/plugin/api/minestom/command/MinestomCloudSender.kt new file mode 100644 index 0000000..92067f5 --- /dev/null +++ b/plugin-minestom/src/main/kotlin/app/simplecloud/plugin/api/minestom/command/MinestomCloudSender.kt @@ -0,0 +1,31 @@ +package app.simplecloud.plugin.api.minestom.command + +import app.simplecloud.plugin.api.minestom.permission.MinestomPermissions +import app.simplecloud.plugin.api.shared.command.CloudSender +import net.kyori.adventure.text.Component +import net.minestom.server.command.CommandSender +import net.minestom.server.entity.Player +import java.util.UUID + +class MinestomCloudSender( + private val commandSender: CommandSender, + private val permissions: MinestomPermissions +) : CloudSender { + + fun getCommandSender(): CommandSender { + return commandSender + } + + override fun getUniqueId(): UUID? { + return (commandSender as? Player)?.uuid + } + + override fun hasPermission(permission: String): Boolean { + return permissions.hasPermission(commandSender, permission) + } + + override fun sendMessage(message: Component) { + commandSender.sendMessage(message) + } + +} diff --git a/plugin-minestom/src/main/kotlin/app/simplecloud/plugin/api/minestom/command/MinestomCloudSenderMapper.kt b/plugin-minestom/src/main/kotlin/app/simplecloud/plugin/api/minestom/command/MinestomCloudSenderMapper.kt new file mode 100644 index 0000000..6ca2b6a --- /dev/null +++ b/plugin-minestom/src/main/kotlin/app/simplecloud/plugin/api/minestom/command/MinestomCloudSenderMapper.kt @@ -0,0 +1,19 @@ +package app.simplecloud.plugin.api.minestom.command + +import app.simplecloud.plugin.api.minestom.permission.MinestomPermissions +import net.minestom.server.command.CommandSender +import org.incendo.cloud.SenderMapper + +class MinestomCloudSenderMapper( + private val permissions: MinestomPermissions +) : SenderMapper { + + override fun map(base: CommandSender): MinestomCloudSender { + return MinestomCloudSender(base, permissions) + } + + override fun reverse(mapped: MinestomCloudSender): CommandSender { + return mapped.getCommandSender() + } + +} diff --git a/plugin-minestom/src/main/kotlin/app/simplecloud/plugin/api/minestom/permission/MinestomPermissionChecker.kt b/plugin-minestom/src/main/kotlin/app/simplecloud/plugin/api/minestom/permission/MinestomPermissionChecker.kt new file mode 100644 index 0000000..63ed96c --- /dev/null +++ b/plugin-minestom/src/main/kotlin/app/simplecloud/plugin/api/minestom/permission/MinestomPermissionChecker.kt @@ -0,0 +1,12 @@ +package app.simplecloud.plugin.api.minestom.permission + +import app.simplecloud.plugin.api.shared.permission.PermissionChecker +import net.minestom.server.entity.Player + +class MinestomPermissionChecker(private val permissions: MinestomPermissions) : PermissionChecker { + + override fun checkPermission(player: Player, name: String): Boolean { + return permissions.hasPermission(player, name) + } + +} diff --git a/plugin-minestom/src/main/kotlin/app/simplecloud/plugin/api/minestom/permission/MinestomPermissions.kt b/plugin-minestom/src/main/kotlin/app/simplecloud/plugin/api/minestom/permission/MinestomPermissions.kt new file mode 100644 index 0000000..7bd91ef --- /dev/null +++ b/plugin-minestom/src/main/kotlin/app/simplecloud/plugin/api/minestom/permission/MinestomPermissions.kt @@ -0,0 +1,29 @@ +package app.simplecloud.plugin.api.minestom.permission + +import net.minestom.server.command.CommandSender +import net.minestom.server.command.ConsoleSender +import net.minestom.server.entity.Player +import java.util.function.BiPredicate + +/** + * Utility for checking permissions on a Minestom server. + * Without a [check], only players with permission level 4 have permissions. + */ +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(): MinestomPermissionChecker { + return MinestomPermissionChecker(this) + } + +} diff --git a/plugin-paper/build.gradle.kts b/plugin-paper/build.gradle.kts new file mode 100644 index 0000000..cad79ca --- /dev/null +++ b/plugin-paper/build.gradle.kts @@ -0,0 +1,5 @@ +dependencies { + compileOnly(project(":plugin-shared")) + compileOnly(libs.paper.api) + compileOnly(libs.cloud.paper) +} diff --git a/plugin-paper/src/main/kotlin/app/simplecloud/plugin/api/paper/command/PaperCloudSender.kt b/plugin-paper/src/main/kotlin/app/simplecloud/plugin/api/paper/command/PaperCloudSender.kt new file mode 100644 index 0000000..e4b0ba3 --- /dev/null +++ b/plugin-paper/src/main/kotlin/app/simplecloud/plugin/api/paper/command/PaperCloudSender.kt @@ -0,0 +1,32 @@ +package app.simplecloud.plugin.api.paper.command + +import app.simplecloud.plugin.api.shared.command.CloudSender +import io.papermc.paper.command.brigadier.CommandSourceStack +import net.kyori.adventure.text.Component +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player +import java.util.UUID + +class PaperCloudSender(private val sourceStack: CommandSourceStack) : CloudSender { + + fun getSourceStack(): CommandSourceStack { + return sourceStack + } + + fun getCommandSender(): CommandSender { + return sourceStack.sender + } + + override fun getUniqueId(): UUID? { + return (sourceStack.sender as? Player)?.uniqueId + } + + override fun hasPermission(permission: String): Boolean { + return sourceStack.sender.hasPermission(permission) + } + + override fun sendMessage(message: Component) { + sourceStack.sender.sendMessage(message) + } + +} diff --git a/plugin-paper/src/main/kotlin/app/simplecloud/plugin/api/paper/command/PaperCloudSenderMapper.kt b/plugin-paper/src/main/kotlin/app/simplecloud/plugin/api/paper/command/PaperCloudSenderMapper.kt new file mode 100644 index 0000000..7ce54d3 --- /dev/null +++ b/plugin-paper/src/main/kotlin/app/simplecloud/plugin/api/paper/command/PaperCloudSenderMapper.kt @@ -0,0 +1,16 @@ +package app.simplecloud.plugin.api.paper.command + +import io.papermc.paper.command.brigadier.CommandSourceStack +import org.incendo.cloud.SenderMapper + +object PaperCloudSenderMapper : SenderMapper { + + override fun map(base: CommandSourceStack): PaperCloudSender { + return PaperCloudSender(base) + } + + override fun reverse(mapped: PaperCloudSender): CommandSourceStack { + return mapped.getSourceStack() + } + +} diff --git a/plugin-paper/src/main/kotlin/app/simplecloud/plugin/api/paper/permission/PaperPermissionChecker.kt b/plugin-paper/src/main/kotlin/app/simplecloud/plugin/api/paper/permission/PaperPermissionChecker.kt new file mode 100644 index 0000000..f3bda7b --- /dev/null +++ b/plugin-paper/src/main/kotlin/app/simplecloud/plugin/api/paper/permission/PaperPermissionChecker.kt @@ -0,0 +1,12 @@ +package app.simplecloud.plugin.api.paper.permission + +import app.simplecloud.plugin.api.shared.permission.PermissionChecker +import org.bukkit.entity.Player + +class PaperPermissionChecker : PermissionChecker { + + override fun checkPermission(player: Player, name: String): Boolean { + return player.hasPermission(name) + } + +} diff --git a/plugin-shared/build.gradle.kts b/plugin-shared/build.gradle.kts index e69de29..69b904b 100644 --- a/plugin-shared/build.gradle.kts +++ b/plugin-shared/build.gradle.kts @@ -0,0 +1,3 @@ +dependencies { + testImplementation(rootProject.libs.bundles.configurate) +} diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/extension/Extension.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/adventure/AdventureExt.kt similarity index 51% rename from plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/extension/Extension.kt rename to plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/adventure/AdventureExt.kt index 913c9b7..9d87bfc 100644 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/extension/Extension.kt +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/adventure/AdventureExt.kt @@ -1,23 +1,32 @@ -package app.simplecloud.plugin.api.shared.extension +package app.simplecloud.plugin.api.shared.adventure -import app.simplecloud.controller.shared.group.Group -import app.simplecloud.controller.shared.server.Server +import app.simplecloud.api.group.Group +import app.simplecloud.api.persistentserver.PersistentServer +import app.simplecloud.api.server.Server import app.simplecloud.plugin.api.shared.placeholder.PlaceholderProvider import app.simplecloud.plugin.api.shared.placeholder.argument.ArgumentsResolver import net.kyori.adventure.text.Component import net.kyori.adventure.text.minimessage.MiniMessage +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver -/** - * @author Niklas Nieberler - */ - val miniMessage = MiniMessage.miniMessage() +private val playerNameRegex = Regex("""^[^<>:'"\\\s]{1,32}$""") + fun text(message: String, vararg tagResolver: TagResolver): Component { return miniMessage.deserialize(message, *tagResolver) } +fun tags(vararg entries: Pair): TagResolver { + return TagResolver.resolver( + entries.map { (key, value) -> + val text = value?.toString() ?: "" + if (key == "playername" && playerNameRegex.matches(text)) Placeholder.parsed(key, text) else Placeholder.unparsed(key, text) + } + ) +} + suspend fun String.appendToComponent( server: Server, prefix: String? = null, @@ -32,4 +41,12 @@ suspend fun String.appendToComponent( vararg argumentsResolver: ArgumentsResolver ): Component { return PlaceholderProvider.groupPlaceholderProvider.append(group, this, prefix, *argumentsResolver) +} + +suspend fun String.appendToComponent( + persistentServer: PersistentServer, + prefix: String? = null, + vararg argumentsResolver: ArgumentsResolver +): Component { + return PlaceholderProvider.persistentServerPlaceholderProvider.append(persistentServer, this, prefix, *argumentsResolver) } \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/command/CloudSender.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/command/CloudSender.kt new file mode 100644 index 0000000..d53c292 --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/command/CloudSender.kt @@ -0,0 +1,31 @@ +package app.simplecloud.plugin.api.shared.command + +import net.kyori.adventure.text.Component +import java.util.UUID + +/** + * A command sender, e.g. a player or the console. + */ +interface CloudSender { + + /** + * The unique id of the player, or `null` if the sender is not a player. + */ + fun getUniqueId(): UUID? + + /** + * Whether the sender has the given [permission]. + */ + fun hasPermission(permission: String): Boolean + + /** + * Sends the [message] to the sender. + */ + fun sendMessage(message: Component) + + /** + * Whether the sender is a player. + */ + fun isPlayer(): Boolean = getUniqueId() != null + +} diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/command/CloudSuggestions.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/command/CloudSuggestions.kt new file mode 100644 index 0000000..93901fe --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/command/CloudSuggestions.kt @@ -0,0 +1,236 @@ +package app.simplecloud.plugin.api.shared.command + +import app.simplecloud.api.CloudApi +import app.simplecloud.api.group.GroupServerType +import app.simplecloud.api.server.Server +import app.simplecloud.api.server.ServerState +import kotlinx.coroutines.future.await +import org.incendo.cloud.kotlin.coroutines.SuspendingSuggestionProvider +import org.incendo.cloud.suggestion.Suggestion +import org.incendo.cloud.suggestion.SuggestionProvider + +/** + * Commonly used suggestion providers for commands. + */ +object CloudSuggestions { + + const val TARGET_TYPE_GROUP = "group" + const val TARGET_TYPE_PERSISTENT_SERVER = "ps" + + /** + * Suggests the names of all groups. + */ + fun groups(api: CloudApi): SuggestionProvider = + SuspendingSuggestionProvider { _, _ -> + try { + api.group().allGroups.await().map { Suggestion.suggestion(it.name) } + } catch (_: Exception) { + emptyList() + } + }.asSuggestionProvider() + + /** + * Suggests the names of all groups of the given [type]. + */ + fun groups(api: CloudApi, type: GroupServerType): SuggestionProvider = + SuspendingSuggestionProvider { _, _ -> + try { + api.group().allGroups.await() + .filter { it.type == type } + .map { Suggestion.suggestion(it.name) } + } catch (_: Exception) { + emptyList() + } + }.asSuggestionProvider() + + /** + * Suggests the names of all proxy groups. + */ + fun proxyGroups(api: CloudApi): SuggestionProvider = + groups(api, GroupServerType.PROXY) + + /** + * Suggests the names of all server groups. + */ + fun serverGroups(api: CloudApi): SuggestionProvider = + groups(api, GroupServerType.SERVER) + + /** + * Suggests the numerical ids of the servers of the group stored in the argument [groupArgument]. + */ + fun serverIds(api: CloudApi, groupArgument: String = "group"): SuggestionProvider = + SuspendingSuggestionProvider { context, _ -> + try { + val group = context.get(groupArgument) + api.server().getServersByGroup(group).await() + .sortedBy { it.numericalId } + .map { Suggestion.suggestion(it.numericalId.toString()) } + } catch (_: Exception) { + emptyList() + } + }.asSuggestionProvider() + + /** + * Suggests the names of all running servers. + */ + fun servers(api: CloudApi): SuggestionProvider = + SuspendingSuggestionProvider { _, _ -> + try { + api.server().allServers.await() + .mapNotNull { serverName(it) } + .map { Suggestion.suggestion(it) } + } catch (_: Exception) { + emptyList() + } + }.asSuggestionProvider() + + /** + * Suggests the names of all running servers in one of the given [states]. + */ + fun servers(api: CloudApi, vararg states: ServerState): SuggestionProvider { + val stateSet = states.toSet() + return SuspendingSuggestionProvider { _, _ -> + try { + api.server().allServers.await() + .filter { it.state in stateSet } + .mapNotNull { serverName(it) } + .map { Suggestion.suggestion(it) } + } catch (_: Exception) { + emptyList() + } + }.asSuggestionProvider() + } + + /** + * Suggests the names of all running servers that players can join. + */ + fun availableServers(api: CloudApi): SuggestionProvider = + servers(api, ServerState.AVAILABLE, ServerState.INGAME) + + /** + * Suggests the names of all persistent servers. + */ + fun persistentServers(api: CloudApi): SuggestionProvider = + SuspendingSuggestionProvider { _, _ -> + try { + api.persistentServer().allPersistentServers.await().map { Suggestion.suggestion(it.name) } + } catch (_: Exception) { + emptyList() + } + }.asSuggestionProvider() + + /** + * Suggests the names of all online players. + */ + fun onlinePlayers(api: CloudApi): SuggestionProvider = + SuspendingSuggestionProvider { _, _ -> + try { + api.player().onlinePlayers.await().map { Suggestion.suggestion(it.name) } + } catch (_: Exception) { + emptyList() + } + }.asSuggestionProvider() + + /** + * Suggests the names of all online players except the command sender. + */ + fun otherOnlinePlayers(api: CloudApi): SuggestionProvider = + SuspendingSuggestionProvider { context, _ -> + try { + val uniqueId = context.sender().getUniqueId() + api.player().onlinePlayers.await() + .filter { it.uniqueId != uniqueId } + .map { Suggestion.suggestion(it.name) } + } catch (_: Exception) { + emptyList() + } + }.asSuggestionProvider() + + /** + * Suggests the names of all players connected to the server stored in the argument [serverArgument]. + */ + //fun playersOnServer(api: CloudApi, serverArgument: String = "server"): SuggestionProvider = + // SuspendingSuggestionProvider { context, _ -> + // try { + // val server = context.get(serverArgument) + // api.player().getOnlinePlayers(server).await().map { Suggestion.suggestion(it.name) } + // } catch (_: Exception) { + // emptyList() + // } + // }.asSuggestionProvider() + + /** + * Suggests the names of all players connected to the proxy stored in the argument [proxyArgument]. + */ + fun playersOnProxy(api: CloudApi, proxyArgument: String = "proxy"): SuggestionProvider = + SuspendingSuggestionProvider { context, _ -> + try { + val proxy = context.get(proxyArgument) + api.player().onlinePlayers.await() + .filter { it.connectedProxyName.equals(proxy, true) } + .map { Suggestion.suggestion(it.name) } + } catch (_: Exception) { + emptyList() + } + }.asSuggestionProvider() + + /** + * Suggests the target types [TARGET_TYPE_GROUP] and [TARGET_TYPE_PERSISTENT_SERVER]. + */ + fun targetTypes(): SuggestionProvider = + of(TARGET_TYPE_GROUP, TARGET_TYPE_PERSISTENT_SERVER) + + /** + * Suggests group or persistent server names, depending on the target type stored in the argument [targetTypeArgument]. + */ + fun targets(api: CloudApi, targetTypeArgument: String = "targetType"): SuggestionProvider = + SuspendingSuggestionProvider { context, _ -> + try { + val targetType = context.get(targetTypeArgument) + when { + targetType.equals(TARGET_TYPE_GROUP, true) -> api.group().allGroups.await().map { Suggestion.suggestion(it.name) } + targetType.equals(TARGET_TYPE_PERSISTENT_SERVER, true) -> api.persistentServer().allPersistentServers.await().map { Suggestion.suggestion(it.name) } + else -> emptyList() + } + } catch (_: Exception) { + emptyList() + } + }.asSuggestionProvider() + + /** + * Suggests all known [ServerState]s. + */ + fun serverStates(): SuggestionProvider = + of(ServerState.entries.filter { it != ServerState.UNKNOWN_STATE }.map { it.name.lowercase() }) + + //fun selectionModes(): SuggestionProvider = + // of(ServerSelectionMode.entries.map { it.name.lowercase() }) + + /** + * Suggests all known [GroupServerType]s. + */ + fun serverTypes(): SuggestionProvider = + of(GroupServerType.entries.filter { it != GroupServerType.UNKNOWN_SERVER }.map { it.name.lowercase() }) + + /** + * Suggests the given fixed [values]. + */ + fun of(vararg values: String): SuggestionProvider = + of(values.toList()) + + /** + * Suggests the given fixed [values]. + */ + fun of(values: Collection): SuggestionProvider { + val suggestions = values.map { Suggestion.suggestion(it) } + return SuggestionProvider.blocking { _, _ -> suggestions } + } + + private fun serverName(server: Server): String? { + val persistentServer = server.persistentServer + if (persistentServer != null) return persistentServer.name + val group = server.group ?: return null + return "${group.name}-${server.numericalId}" + } + +} diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/AbstractMessageConfig.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/AbstractMessageConfig.kt new file mode 100644 index 0000000..3bc8505 --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/AbstractMessageConfig.kt @@ -0,0 +1,29 @@ +package app.simplecloud.plugin.api.shared.config + +import app.simplecloud.plugin.api.shared.adventure.miniMessage +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.minimessage.tag.Tag +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver + +/** + * Base class for a message configuration with variables. + */ +abstract class AbstractMessageConfig { + + abstract val variables: Map + + private val resolver: TagResolver by lazy { + TagResolver.resolver( + variables.map { (key, value) -> + TagResolver.resolver(key, Tag.selfClosingInserting(miniMessage.deserialize(value))) + } + ) + } + + /** + * Parses the [message] with the [variables] and the [extra] resolvers. + */ + fun msg(message: String, vararg extra: TagResolver): Component { + return miniMessage.deserialize(message, TagResolver.resolver(resolver, *extra)) + } +} diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/ConfigMigrator.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/ConfigMigrator.kt new file mode 100644 index 0000000..e3e4350 --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/ConfigMigrator.kt @@ -0,0 +1,117 @@ +package app.simplecloud.plugin.api.shared.config + +import org.spongepowered.configurate.CommentedConfigurationNode + +/** + * A single migration step that modifies a yaml node in place. + */ +fun interface ConfigMigration { + fun migrate(node: CommentedConfigurationNode) +} + +/** + * Applies sequential migrations to bring a yaml config node to the current version. + * + * ``` + * ConfigMigrator.builder(currentVersion = 2) + * .migrate(1, 2) { node -> ... } + * .build() + * ``` + */ +class ConfigMigrator private constructor( + private val currentVersion: Int, + private val fallbackVersion: Int, + private val migrationsByFromVersion: Map, +) { + + init { + require(currentVersion >= fallbackVersion) { + "The current config version must be greater than or equal to the fallback version" + } + } + + /** + * Migrates the [node] to the current version and returns whether anything was migrated. + */ + fun migrate(node: CommentedConfigurationNode): Boolean { + var version = node.node(VERSION_NODE).getInt(fallbackVersion) + + require(version <= currentVersion) { + "The config version $version is newer than the supported version $currentVersion" + } + + var migrated = false + while (version < currentVersion) { + val migration = checkNotNull(migrationsByFromVersion[version]) { + "Missing config migration from version $version to $currentVersion" + } + + migration.action.migrate(node) + version = migration.toVersion + node.node(VERSION_NODE).set(version) + migrated = true + } + + return migrated + } + + class Builder internal constructor( + private val currentVersion: Int, + ) { + + private var fallbackVersion = 0 + private val migrations = mutableMapOf() + + /** + * The version that is assumed if the config has no version node yet. Defaults to `0`. + */ + fun fallbackVersion(version: Int): Builder = apply { + require(version >= 0) { + "The fallback version must be greater than or equal to 0" + } + fallbackVersion = version + } + + /** + * Registers a migration from [fromVersion] to [toVersion]. + */ + fun migrate(fromVersion: Int, toVersion: Int, action: ConfigMigration): Builder = apply { + require(fromVersion >= 0) { + "The source config version must be greater than or equal to 0" + } + require(toVersion > fromVersion) { + "The target config version must be greater than the source config version" + } + require(toVersion <= currentVersion) { + "The target config version must not exceed the current config version" + } + require(fromVersion !in migrations) { + "A migration from version $fromVersion is already registered" + } + + migrations[fromVersion] = MigrationStep(toVersion, action) + } + + fun build(): ConfigMigrator = ConfigMigrator( + currentVersion = currentVersion, + fallbackVersion = fallbackVersion, + migrationsByFromVersion = migrations.toMap(), + ) + } + + private data class MigrationStep( + val toVersion: Int, + val action: ConfigMigration, + ) + + companion object { + const val VERSION_NODE = "version" + + fun builder(currentVersion: Int): Builder { + require(currentVersion >= 0) { + "The current config version must be greater than or equal to 0" + } + return Builder(currentVersion) + } + } +} diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/ConfigurationFactory.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/ConfigurationFactory.kt new file mode 100644 index 0000000..d8e9a8e --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/ConfigurationFactory.kt @@ -0,0 +1,89 @@ +package app.simplecloud.plugin.api.shared.config + +import java.io.File +import java.nio.file.Path + +/** + * @author Niklas Nieberler + */ +class ConfigurationFactory( + private val file: File, + javaClass: Class, + configMigrator: ConfigMigrator? = null, +) { + + private val configurator = YamlFileConfigurator(javaClass, configMigrator) + + private var config: E? = null + + /** + * Loads the config, or creates the file from [defaultConfig] if it does not exist. + */ + fun loadOrCreate(defaultConfig: E): E { + if (file.exists()) { + return load() ?: throw NullPointerException("failed to load config") + } + + save(defaultConfig) + return defaultConfig + } + + /** + * The cached config. Throws if it has not been loaded yet. + */ + fun get(): E { + return config ?: throw NullPointerException("failed to find config") + } + + /** + * Writes the [entry] to the file and caches it. + */ + fun save(entry: E) { + configurator.save(file, entry) + config = entry + } + + /** + * Reloads the file into the cache. + */ + fun reload() { + load() + } + + private fun load(): E? { + return configurator.load(file).also { config = it } + } + + companion object { + + /** + * Installs the bundled default file if missing, creates the factory and loads the config. + * + * The bundled file is looked up by [fileName] with the class loader of the config class. + */ + inline fun create( + dataDirectory: File, + fileName: String, + default: E, + migrator: ConfigMigrator? = null, + classLoader: ClassLoader = E::class.java.classLoader, + ): ConfigurationFactory { + val file = File(dataDirectory, fileName) + DefaultConfigInstaller.install(file.toPath(), classLoader) + return ConfigurationFactory(file, E::class.java, migrator).also { it.loadOrCreate(default) } + } + + /** + * Same as [create] but with a [Path] as data directory. + */ + inline fun create( + dataDirectory: Path, + fileName: String, + default: E, + migrator: ConfigMigrator? = null, + classLoader: ClassLoader = E::class.java.classLoader, + ): ConfigurationFactory { + return create(dataDirectory.toFile(), fileName, default, migrator, classLoader) + } + } +} diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/DefaultConfigInstaller.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/DefaultConfigInstaller.kt new file mode 100644 index 0000000..166da06 --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/DefaultConfigInstaller.kt @@ -0,0 +1,38 @@ +package app.simplecloud.plugin.api.shared.config + +import java.nio.file.Files +import java.nio.file.Path + +/** + * Copies bundled default files from the jar into the data directory. + */ +object DefaultConfigInstaller { + + /** + * Copies the bundled resource with the same file name as [target] if [target] does not exist yet. + */ + fun install(target: Path, classLoader: ClassLoader) { + copy(target, target.fileName.toString(), classLoader) + } + + /** + * Copies all [resources] into [dataDirectory]. Paths may contain sub directories, e.g. `layout/public.yml`. + */ + fun installAll(dataDirectory: Path, resources: List, classLoader: ClassLoader, resourcePrefix: String = "") { + val prefix = if (resourcePrefix.isEmpty() || resourcePrefix.endsWith("/")) resourcePrefix else "$resourcePrefix/" + resources.forEach { resource -> + copy(dataDirectory.resolve(resource), prefix + resource, classLoader) + } + } + + private fun copy(target: Path, resourceName: String, classLoader: ClassLoader) { + if (Files.exists(target)) return + + val resource = checkNotNull(classLoader.getResourceAsStream(resourceName)) { + "Missing bundled $resourceName" + } + + Files.createDirectories(target.parent) + resource.use { stream -> Files.copy(stream, target) } + } +} diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/VersionedConfig.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/VersionedConfig.kt new file mode 100644 index 0000000..7bf1324 --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/VersionedConfig.kt @@ -0,0 +1,8 @@ +package app.simplecloud.plugin.api.shared.config + +/** + * Marks a config as versioned, so it can be used with a [ConfigMigrator]. + */ +interface VersionedConfig { + val version: Int +} diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/YamlDirectoryRepository.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/YamlDirectoryRepository.kt new file mode 100644 index 0000000..e1f9bc4 --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/YamlDirectoryRepository.kt @@ -0,0 +1,77 @@ +package app.simplecloud.plugin.api.shared.config + +import java.io.File +import java.nio.file.Files +import java.nio.file.Path + +/** + * @author Niklas Nieberler + */ +abstract class YamlDirectoryRepository( + private val directory: Path, + javaClass: Class, + configMigrator: ConfigMigrator? = null, +) { + + private val configurator = YamlFileConfigurator(javaClass, configMigrator) + + private val cachedEntities = mutableMapOf() + + /** + * Saves the [entity] to the directory. + */ + abstract fun save(entity: E) + + /** + * Finds a cached entity by its [identifier]. + */ + abstract fun find(identifier: I): E? + + /** + * All cached entities. + */ + fun findAll(): List { + return cachedEntities.values.toList() + } + + /** + * Loads all `.yml` and `.yaml` files of the directory and creates it if missing. + */ + fun load(): List { + Files.createDirectories(directory) + + return Files.walk(directory).use { paths -> + paths.toList() + .map { it.toFile() } + .filter { it.extension == "yml" || it.extension == "yaml" } + .mapNotNull { load(it) } + } + } + + /** + * Loads and caches the entity of a single [file]. + */ + fun load(file: File): E? { + val entity = configurator.load(file) ?: return null + cachedEntities[file] = entity + return entity + } + + /** + * Saves the [entity] to the file [name] inside the directory. + */ + protected fun save(name: String, entity: E) { + val file = directory.resolve(name).toFile() + configurator.save(file, entity) + cachedEntities[file] = entity + } + + /** + * Deletes the file of the [entity]. + */ + fun delete(entity: E) { + val file = cachedEntities.entries.find { it.value == entity }?.key ?: return + file.delete() + cachedEntities.remove(file) + } +} diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/YamlFileConfigurator.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/YamlFileConfigurator.kt new file mode 100644 index 0000000..ed405c2 --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/YamlFileConfigurator.kt @@ -0,0 +1,73 @@ +package app.simplecloud.plugin.api.shared.config + +import app.simplecloud.plugin.api.shared.config.serializer.GenericEnumSerializer +import org.spongepowered.configurate.CommentedConfigurationNode +import org.spongepowered.configurate.kotlin.objectMapperFactory +import org.spongepowered.configurate.yaml.NodeStyle +import org.spongepowered.configurate.yaml.YamlConfigurationLoader +import java.io.File + +/** + * @author Niklas Nieberler + */ +class YamlFileConfigurator( + private val javaClass: Class, + private val configMigrator: ConfigMigrator? = null, +) { + + init { + require(configMigrator == null || VersionedConfig::class.java.isAssignableFrom(javaClass)) { + "Config migrations can only be used with VersionedConfig types" + } + } + + private val configurationLoaders = hashMapOf() + + private val defaultConfigurationBuilder = YamlConfigurationLoader.builder() + .nodeStyle(NodeStyle.BLOCK) + .defaultOptions { options -> + options.serializers { builder -> + builder.registerAnnotatedObjects(objectMapperFactory()) + builder.register(Enum::class.java, GenericEnumSerializer) + builder.register({ type -> type is Class<*> && type.isEnum }, GenericEnumSerializer) + } + } + + /** + * Writes the [entity] to the [file]. + */ + fun save(file: File, entity: E) { + val (node, loader) = buildNode(file) + node.set(javaClass, entity) + loader.save(node) + } + + /** + * Reads the [file], migrates it if needed and saves the migrated result. + */ + fun load(file: File): E? { + val (node, loader) = buildNode(file) + if (configMigrator?.migrate(node) == true) { + loader.save(node) + } + return node.get(javaClass) + } + + /** + * Loads the raw node of the [file] together with its loader. + */ + fun buildNode(file: File): Pair { + val loader = getOrCreateConfigurationLoader(file) + return loader.load() to loader + } + + /** + * The cached loader of the [file]. + */ + fun getOrCreateConfigurationLoader(file: File): YamlConfigurationLoader { + return configurationLoaders.getOrPut(file) { + defaultConfigurationBuilder.path(file.toPath()).build() + } + } + +} diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/serializer/GenericEnumSerializer.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/serializer/GenericEnumSerializer.kt new file mode 100644 index 0000000..4a1bf63 --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/config/serializer/GenericEnumSerializer.kt @@ -0,0 +1,32 @@ +package app.simplecloud.plugin.api.shared.config.serializer + +import org.spongepowered.configurate.ConfigurationNode +import org.spongepowered.configurate.serialize.SerializationException +import org.spongepowered.configurate.serialize.TypeSerializer +import java.lang.reflect.Type + +/** + * @author Niklas Nieberler + */ +object GenericEnumSerializer : TypeSerializer> { + + @Suppress("UNCHECKED_CAST") + override fun deserialize(type: Type, node: ConfigurationNode): Enum<*> { + val value = node.string ?: throw SerializationException("No value present in node") + + if (type !is Class<*> || !type.isEnum) { + throw SerializationException("Type is not an enum class") + } + + return try { + java.lang.Enum.valueOf(type as Class>, value) + } catch (_: IllegalArgumentException) { + throw SerializationException("Invalid enum constant") + } + } + + override fun serialize(type: Type, obj: Enum<*>?, node: ConfigurationNode) { + node.set(obj?.name) + } + +} diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/MatcherType.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/MatcherType.kt deleted file mode 100644 index afcd098..0000000 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/MatcherType.kt +++ /dev/null @@ -1,31 +0,0 @@ -package app.simplecloud.plugin.api.shared.matcher - -import app.simplecloud.plugin.api.shared.matcher.operation.* - -enum class MatcherType( - private val matcher: OperationMatcher -) { - - REGEX(RegexOperationMatcher), - EQUALS(EqualsOperationMatcher), - CONTAINS(ContainsOperationMatcher), - STARTS_WITH(StartsWithOperationMatcher), - ENDS_WITH(EndsWithOperationMatcher); - - fun matches(name: String, value: String, negate: Boolean): Boolean { - val matches = this.matcher.matches(name, value) - if (negate) { - return matches.not() - } - return matches - } - - fun anyMatches(names: List, value: String, negate: Boolean): Boolean { - return names.any { matches(it, value, negate) } - } - - fun allMatches(names: List, value: String, negate: Boolean): Boolean { - return names.all { matches(it, value, negate) } - } - -} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/OperationType.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/OperationType.kt new file mode 100644 index 0000000..581d2d8 --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/OperationType.kt @@ -0,0 +1,48 @@ +package app.simplecloud.plugin.api.shared.matcher + +import app.simplecloud.plugin.api.shared.matcher.operation.NumericOperationMatcher +import app.simplecloud.plugin.api.shared.matcher.operation.OperationMatcher +import app.simplecloud.plugin.api.shared.matcher.operation.StringOperationMatcher +import app.simplecloud.plugin.api.shared.matcher.operation.impl.* + +enum class OperationType( + private val matcher: OperationMatcher<*, *> +) { + + REGEX(RegexOperationMatcher), + PATTERN(PatternOperationMatcher), + EQUALS(EqualsOperationMatcher), + CONTAINS(ContainsOperationMatcher), + STARTS_WITH(StartsWithOperationMatcher), + ENDS_WITH(EndsWithOperationMatcher), + GREATER_THAN(GreaterThanOperationMatcher); + + fun matches(key: Any, value: Any, negate: Boolean): Boolean { + val matches = when (matcher) { + is StringOperationMatcher -> { + if (key is String && value is String) { + matcher.matches(key, value) + } else false + } + + is NumericOperationMatcher -> { + if (key is Int && value is Int) { + matcher.matches(key, value) + } else false + } + + else -> false + } + + return if (negate) matches.not() else matches + } + + fun anyMatches(names: List, value: String, negate: Boolean): Boolean { + return names.any { matches(it, value, negate) } + } + + fun allMatches(names: List, value: String, negate: Boolean): Boolean { + return names.all { matches(it, value, negate) } + } + +} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/ServerMatcherConfiguration.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/ServerMatcherConfiguration.kt index 5056bf5..9ffa5c0 100644 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/ServerMatcherConfiguration.kt +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/ServerMatcherConfiguration.kt @@ -8,7 +8,7 @@ import org.spongepowered.configurate.objectmapping.ConfigSerializable @ConfigSerializable data class ServerMatcherConfiguration( - val operation: MatcherType = MatcherType.STARTS_WITH, + val operation: OperationType = OperationType.STARTS_WITH, val value: String = "", val negate: Boolean = false, ) { diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/ContainsOperationMatcher.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/ContainsOperationMatcher.kt deleted file mode 100644 index 497e607..0000000 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/ContainsOperationMatcher.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.simplecloud.plugin.api.shared.matcher.operation - -import org.spongepowered.configurate.objectmapping.ConfigSerializable - -@ConfigSerializable -object ContainsOperationMatcher : OperationMatcher { - - override fun matches(name: String, value: String): Boolean { - return name.contains(value, ignoreCase = true) - } - -} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/EndsWithOperationMatcher.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/EndsWithOperationMatcher.kt deleted file mode 100644 index ef4f5b3..0000000 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/EndsWithOperationMatcher.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.simplecloud.plugin.api.shared.matcher.operation - -import org.spongepowered.configurate.objectmapping.ConfigSerializable - -@ConfigSerializable -object EndsWithOperationMatcher : OperationMatcher { - - override fun matches(name: String, value: String): Boolean { - return name.endsWith(value, ignoreCase = true) - } - -} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/EqualsOperationMatcher.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/EqualsOperationMatcher.kt deleted file mode 100644 index 1051bb9..0000000 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/EqualsOperationMatcher.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.simplecloud.plugin.api.shared.matcher.operation - -import org.spongepowered.configurate.objectmapping.ConfigSerializable - -@ConfigSerializable -object EqualsOperationMatcher : OperationMatcher { - - override fun matches(name: String, value: String): Boolean { - return name.equals(value, ignoreCase = true) - } - -} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/NumericOperationMatcher.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/NumericOperationMatcher.kt new file mode 100644 index 0000000..dfdc488 --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/NumericOperationMatcher.kt @@ -0,0 +1,3 @@ +package app.simplecloud.plugin.api.shared.matcher.operation + +interface NumericOperationMatcher : OperationMatcher \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/OperationMatcher.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/OperationMatcher.kt index cfe5e80..d597e0b 100644 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/OperationMatcher.kt +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/OperationMatcher.kt @@ -1,7 +1,7 @@ package app.simplecloud.plugin.api.shared.matcher.operation -interface OperationMatcher { +interface OperationMatcher { - fun matches(name: String, value: String): Boolean + fun matches(key: K, value: V): Boolean } \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/RegexOperationMatcher.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/RegexOperationMatcher.kt deleted file mode 100644 index 23b0006..0000000 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/RegexOperationMatcher.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.simplecloud.plugin.api.shared.matcher.operation - -import org.spongepowered.configurate.objectmapping.ConfigSerializable - -@ConfigSerializable -object RegexOperationMatcher : OperationMatcher { - - override fun matches(name: String, value: String): Boolean { - return Regex(value).matches(name) - } - -} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/StartsWithOperationMatcher.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/StartsWithOperationMatcher.kt deleted file mode 100644 index c0ca9b4..0000000 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/StartsWithOperationMatcher.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.simplecloud.plugin.api.shared.matcher.operation - -import org.spongepowered.configurate.objectmapping.ConfigSerializable - -@ConfigSerializable -object StartsWithOperationMatcher : OperationMatcher { - - override fun matches(name: String, value: String): Boolean { - return name.startsWith(value, ignoreCase = true) - } - -} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/StringOperationMatcher.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/StringOperationMatcher.kt new file mode 100644 index 0000000..c976d0e --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/StringOperationMatcher.kt @@ -0,0 +1,3 @@ +package app.simplecloud.plugin.api.shared.matcher.operation + +interface StringOperationMatcher : OperationMatcher diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/ContainsOperationMatcher.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/ContainsOperationMatcher.kt new file mode 100644 index 0000000..3a0243e --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/ContainsOperationMatcher.kt @@ -0,0 +1,13 @@ +package app.simplecloud.plugin.api.shared.matcher.operation.impl + +import app.simplecloud.plugin.api.shared.matcher.operation.StringOperationMatcher +import org.spongepowered.configurate.objectmapping.ConfigSerializable + +@ConfigSerializable +object ContainsOperationMatcher : StringOperationMatcher { + + override fun matches(key: String, value: String): Boolean { + return key.contains(value, ignoreCase = true) + } + +} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/EndsWithOperationMatcher.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/EndsWithOperationMatcher.kt new file mode 100644 index 0000000..dd369fe --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/EndsWithOperationMatcher.kt @@ -0,0 +1,13 @@ +package app.simplecloud.plugin.api.shared.matcher.operation.impl + +import app.simplecloud.plugin.api.shared.matcher.operation.StringOperationMatcher +import org.spongepowered.configurate.objectmapping.ConfigSerializable + +@ConfigSerializable +object EndsWithOperationMatcher : StringOperationMatcher { + + override fun matches(key: String, value: String): Boolean { + return key.endsWith(value, ignoreCase = true) + } + +} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/EqualsOperationMatcher.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/EqualsOperationMatcher.kt new file mode 100644 index 0000000..98c2729 --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/EqualsOperationMatcher.kt @@ -0,0 +1,13 @@ +package app.simplecloud.plugin.api.shared.matcher.operation.impl + +import app.simplecloud.plugin.api.shared.matcher.operation.StringOperationMatcher +import org.spongepowered.configurate.objectmapping.ConfigSerializable + +@ConfigSerializable +object EqualsOperationMatcher : StringOperationMatcher { + + override fun matches(key: String, value: String): Boolean { + return key.equals(value, ignoreCase = true) + } + +} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/GreaterThanOperationMatcher.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/GreaterThanOperationMatcher.kt new file mode 100644 index 0000000..41dfc72 --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/GreaterThanOperationMatcher.kt @@ -0,0 +1,11 @@ +package app.simplecloud.plugin.api.shared.matcher.operation.impl + +import app.simplecloud.plugin.api.shared.matcher.operation.NumericOperationMatcher + +object GreaterThanOperationMatcher : NumericOperationMatcher { + + override fun matches(key: Int, value: Int): Boolean { + return key > value + } + +} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/PatternOperationMatcher.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/PatternOperationMatcher.kt new file mode 100644 index 0000000..7dd32bd --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/PatternOperationMatcher.kt @@ -0,0 +1,14 @@ +package app.simplecloud.plugin.api.shared.matcher.operation.impl + +import app.simplecloud.plugin.api.shared.matcher.operation.StringOperationMatcher +import org.spongepowered.configurate.objectmapping.ConfigSerializable +import java.util.regex.Pattern + +@ConfigSerializable +object PatternOperationMatcher : StringOperationMatcher { + + override fun matches(key: String, value: String): Boolean { + return Pattern.matches(key, value) + } + +} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/RegexOperationMatcher.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/RegexOperationMatcher.kt new file mode 100644 index 0000000..4bc5c14 --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/RegexOperationMatcher.kt @@ -0,0 +1,13 @@ +package app.simplecloud.plugin.api.shared.matcher.operation.impl + +import app.simplecloud.plugin.api.shared.matcher.operation.StringOperationMatcher +import org.spongepowered.configurate.objectmapping.ConfigSerializable + +@ConfigSerializable +object RegexOperationMatcher : StringOperationMatcher { + + override fun matches(key: String, value: String): Boolean { + return Regex(value).matches(key) + } + +} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/StartsWithOperationMatcher.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/StartsWithOperationMatcher.kt new file mode 100644 index 0000000..d641d1c --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/matcher/operation/impl/StartsWithOperationMatcher.kt @@ -0,0 +1,13 @@ +package app.simplecloud.plugin.api.shared.matcher.operation.impl + +import app.simplecloud.plugin.api.shared.matcher.operation.StringOperationMatcher +import org.spongepowered.configurate.objectmapping.ConfigSerializable + +@ConfigSerializable +object StartsWithOperationMatcher : StringOperationMatcher { + + override fun matches(key: String, value: String): Boolean { + return key.startsWith(value, ignoreCase = true) + } + +} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/pattern/ServerPatternIdentifier.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/pattern/ServerPatternIdentifier.kt index 92c9cba..e1ce9b4 100644 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/pattern/ServerPatternIdentifier.kt +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/pattern/ServerPatternIdentifier.kt @@ -1,28 +1,21 @@ package app.simplecloud.plugin.api.shared.pattern -import app.simplecloud.controller.api.ControllerApi -import app.simplecloud.controller.shared.group.Group -import app.simplecloud.controller.shared.server.Server - -/** - * @author Niklas Nieberler - */ +import app.simplecloud.api.CloudApi +import app.simplecloud.api.group.Group +import app.simplecloud.api.server.Server +import app.simplecloud.plugin.api.shared.pretty.StringPrettifier +import kotlinx.coroutines.future.await class ServerPatternIdentifier( private val pattern: String = "-", regexPattern: String = pattern .replace("", "(?[a-zA-Z]+)") .replace("", "(?\\d+)"), - private val controllerApi: ControllerApi.Coroutine = ControllerApi.createCoroutineApi() + private val cloudApi: CloudApi = CloudApi.create() ) { private val regex = Regex(regexPattern) - /** - * Gets the group and numerical id matching the [pattern] in a [Pair] - * @param name the server name - * @param customRegex custom regex pattern - */ fun parse(name: String, customRegex: Regex? = null): Pair { val matchResult = customRegex?.matchEntire(name) ?: this.regex.matchEntire(name) if (matchResult == null) @@ -35,33 +28,26 @@ class ServerPatternIdentifier( return Pair(groupName, numericalId) } - /** - * Gets a server string as a set in the pattern - * @param server replaces it to pattern - */ fun parseServerToPattern(server: Server): String { return this.pattern - .replace("", server.group) - .replace("", server.uniqueId) - .replace("", server.uniqueId) + .replace("", server.serverBase.name) + .replace("", server.properties["pretty-name"]?.toString() ?: StringPrettifier.prettify(server.serverBase.name)) + .replace("", server.serverId) + .replace("", server.serverId) .replace("", server.numericalId.toString()) } - /** - * Gets the [Group] by the matching [pattern] - * @param name the server name - */ suspend fun getGroup(name: String): Group? { val groupName = parse(name).first - return this.controllerApi.getGroups().getGroupByName(groupName) + return try { + this.cloudApi.group().getGroupByName(groupName).await() + } catch (e: Exception) { + null + } } - /** - * Gets the numerical id by the matching [pattern] - * @param name the server name - */ fun getNumericalId(name: String): Int { return parse(name).second } -} \ No newline at end of file +} diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/permission/PermissionChecker.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/permission/PermissionChecker.kt new file mode 100644 index 0000000..216cb80 --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/permission/PermissionChecker.kt @@ -0,0 +1,16 @@ +package app.simplecloud.plugin.api.shared.permission + +/** + * @author Niklas Nieberler + */ + +fun interface PermissionChecker

{ + + /** + * Checks the permission by a player + * @param player to check the permission + * @param name of the permission + */ + fun checkPermission(player: P, name: String): Boolean + +} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/PlaceholderProvider.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/PlaceholderProvider.kt index ea02261..24205dd 100644 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/PlaceholderProvider.kt +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/PlaceholderProvider.kt @@ -1,6 +1,7 @@ package app.simplecloud.plugin.api.shared.placeholder import app.simplecloud.plugin.api.shared.placeholder.provider.GroupPlaceholderProvider +import app.simplecloud.plugin.api.shared.placeholder.provider.PersistentServerPlaceholderProvider import app.simplecloud.plugin.api.shared.placeholder.provider.ServerPlaceholderProvider /** @@ -13,4 +14,6 @@ object PlaceholderProvider { val groupPlaceholderProvider = GroupPlaceholderProvider() + val persistentServerPlaceholderProvider = PersistentServerPlaceholderProvider() + } \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/argument/PropertiesArgumentsResolver.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/argument/PropertiesArgumentsResolver.kt index 5ae25e2..44badb3 100644 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/argument/PropertiesArgumentsResolver.kt +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/argument/PropertiesArgumentsResolver.kt @@ -8,7 +8,7 @@ import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue */ class PropertiesArgumentsResolver( - private val properties: Map + private val properties: Map ) : ArgumentsResolver { override fun getKey() = "property" @@ -17,7 +17,7 @@ class PropertiesArgumentsResolver( val argumentName = arguments.popOr("property expected").value() val defaultArgument = arguments.peek()?.value() ?: "" val string = this.properties[argumentName] ?: defaultArgument - return Tag.preProcessParsed(string) + return Tag.preProcessParsed(string.toString()) } } \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/argument/group/PlayerCountArgumentsResolver.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/argument/group/PlayerCountArgumentsResolver.kt index ee13478..c319290 100644 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/argument/group/PlayerCountArgumentsResolver.kt +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/argument/group/PlayerCountArgumentsResolver.kt @@ -1,21 +1,15 @@ package app.simplecloud.plugin.api.shared.placeholder.argument.group -import app.simplecloud.controller.api.ControllerApi -import app.simplecloud.controller.shared.group.Group +import app.simplecloud.api.CloudApi +import app.simplecloud.api.group.Group +import app.simplecloud.api.server.ServerState import app.simplecloud.plugin.api.shared.placeholder.argument.ArgumentsResolver -import build.buf.gen.simplecloud.controller.v1.ServerState +import kotlinx.coroutines.future.await import net.kyori.adventure.text.minimessage.tag.Tag import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue -import kotlin.collections.filter -import kotlin.collections.firstOrNull -import kotlin.collections.sumOf - -/** - * @author Niklas Nieberler - */ class PlayerCountArgumentsResolver( - private val controllerApi: ControllerApi.Coroutine, + private val cloudApi: CloudApi, private val group: Group, ) : ArgumentsResolver { @@ -27,10 +21,13 @@ class PlayerCountArgumentsResolver( return Tag.preProcessParsed(findPlayerCount(this.group, serverState).toString()) } - private suspend fun findPlayerCount(group: Group, state: ServerState?): Long { - return this.controllerApi.getServers().getServersByGroup(group) - .filter { it.state == state } - .sumOf { it.playerCount } + private suspend fun findPlayerCount(group: Group, state: ServerState?): Int { + val servers = this.cloudApi.server().getServersByGroup(group.name).await() + return if (state != null) { + servers.filter { it.state == state }.sumOf { it.playerCount } + } else { + servers.sumOf { it.playerCount } + } } -} \ No newline at end of file +} diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/argument/group/ServerCountArgumentsResolver.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/argument/group/ServerCountArgumentsResolver.kt index 5e2dd5c..2afedc6 100644 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/argument/group/ServerCountArgumentsResolver.kt +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/argument/group/ServerCountArgumentsResolver.kt @@ -1,20 +1,15 @@ package app.simplecloud.plugin.api.shared.placeholder.argument.group -import app.simplecloud.controller.api.ControllerApi -import app.simplecloud.controller.shared.group.Group +import app.simplecloud.api.CloudApi +import app.simplecloud.api.group.Group +import app.simplecloud.api.server.ServerState import app.simplecloud.plugin.api.shared.placeholder.argument.ArgumentsResolver -import build.buf.gen.simplecloud.controller.v1.ServerState +import kotlinx.coroutines.future.await import net.kyori.adventure.text.minimessage.tag.Tag import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue -import kotlin.collections.filter -import kotlin.collections.firstOrNull - -/** - * @author Niklas Nieberler - */ class ServerCountArgumentsResolver( - private val controllerApi: ControllerApi.Coroutine, + private val cloudApi: CloudApi, private val group: Group, ) : ArgumentsResolver { @@ -23,13 +18,16 @@ class ServerCountArgumentsResolver( override suspend fun resolve(arguments: ArgumentQueue): Tag? { val text = arguments.popOr("all").value() val serverState = ServerState.entries.firstOrNull { it.name.equals(text, true) } - return Tag.preProcessParsed(findPlayerCount(this.group, serverState).toString()) + return Tag.preProcessParsed(findServerCount(this.group, serverState).toString()) } - private suspend fun findPlayerCount(group: Group, state: ServerState?): Int { - return this.controllerApi.getServers().getServersByGroup(group) - .filter { it.state == state } - .size + private suspend fun findServerCount(group: Group, state: ServerState?): Int { + val servers = this.cloudApi.server().getServersByGroup(group.name).await() + return if (state != null) { + servers.filter { it.state == state }.size + } else { + servers.size + } } -} \ No newline at end of file +} diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/provider/AbstractPlaceholderProvider.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/provider/AbstractPlaceholderProvider.kt index bffc9dd..a540a71 100644 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/provider/AbstractPlaceholderProvider.kt +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/provider/AbstractPlaceholderProvider.kt @@ -1,11 +1,12 @@ package app.simplecloud.plugin.api.shared.placeholder.provider -import app.simplecloud.controller.api.ControllerApi -import app.simplecloud.plugin.api.shared.extension.text +import app.simplecloud.api.CloudApi +import app.simplecloud.plugin.api.shared.adventure.text import app.simplecloud.plugin.api.shared.placeholder.argument.ArgumentsResolver import app.simplecloud.plugin.api.shared.placeholder.single.SinglePlaceholderExecutor import kotlinx.coroutines.runBlocking import net.kyori.adventure.text.Component +import net.kyori.adventure.text.minimessage.tag.Tag import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver /** @@ -16,35 +17,35 @@ abstract class AbstractPlaceholderProvider( private val executor: SinglePlaceholderExecutor, ) { - private val controllerApi = ControllerApi.createCoroutineApi() + private val cloudApi = CloudApi.create() /** * Gets the list of all available [ArgumentsResolver] - * @param controllerApi the instance of [ControllerApi.Coroutine] + * @param cloudApi the instance of [CloudApi] * @param value for the placeholder */ abstract suspend fun getArgumentsResolvers( - controllerApi: ControllerApi.Coroutine, + cloudApi: CloudApi, value: T, ): List /** * Gets the sum of all [TagResolver] - * @param value for the placeholder + * @param values for the placeholder * @param prefix of the placeholder key */ suspend fun getTagResolver( - value: T, + values: List, prefix: String? = null, vararg argumentsResolver: ArgumentsResolver, ): TagResolver { - val availableArgumentsResolver = listOf( - *getArgumentsResolvers(this.controllerApi, value).toTypedArray(), - *argumentsResolver - ) - val singleTagResolver = this.executor.getTagResolver(this.controllerApi, value, prefix) + val availableArgumentsResolver = buildList { + addAll(values.flatMap { getArgumentsResolvers(cloudApi, it) }) + addAll(argumentsResolver) + } + val singleTagResolver = values.map { this.executor.getTagResolver(this.cloudApi, it, prefix) } return TagResolver.resolver( - singleTagResolver, + *singleTagResolver.toTypedArray(), *availableArgumentsResolver .map { convertArgumentsResolverToTagResolver(it, prefix) } .toTypedArray() @@ -65,7 +66,25 @@ abstract class AbstractPlaceholderProvider( ): Component { return text( string, - getTagResolver(value, prefix, *argumentsResolver), + getTagResolver(listOf(value), prefix, *argumentsResolver), + ) + } + + /** + * Serializes the string to a [Component] + * @param values for the placeholder + * @param string the message + * @param prefix of the placeholder key + */ + suspend fun append( + values: List, + string: String, + prefix: String? = null, + vararg argumentsResolver: ArgumentsResolver, + ): Component { + return text( + string, + getTagResolver(values, prefix, *argumentsResolver), ) } @@ -73,7 +92,7 @@ abstract class AbstractPlaceholderProvider( val key = resolver.getKey() val resolvedKey = prefix?.let { "${it}_$key" } ?: key return TagResolver.resolver(resolvedKey) { arguments, _ -> - return@resolver runBlocking { resolver.resolve(arguments) } + return@resolver runBlocking { resolver.resolve(arguments) as Tag } } } diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/provider/GroupPlaceholderProvider.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/provider/GroupPlaceholderProvider.kt index 67e5dd8..16e9726 100644 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/provider/GroupPlaceholderProvider.kt +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/provider/GroupPlaceholderProvider.kt @@ -1,23 +1,20 @@ package app.simplecloud.plugin.api.shared.placeholder.provider -import app.simplecloud.controller.api.ControllerApi -import app.simplecloud.controller.shared.group.Group -import app.simplecloud.plugin.api.shared.placeholder.argument.* -import app.simplecloud.plugin.api.shared.placeholder.argument.group.* +import app.simplecloud.api.CloudApi +import app.simplecloud.api.group.Group +import app.simplecloud.plugin.api.shared.placeholder.argument.PropertiesArgumentsResolver +import app.simplecloud.plugin.api.shared.placeholder.argument.group.PlayerCountArgumentsResolver +import app.simplecloud.plugin.api.shared.placeholder.argument.group.ServerCountArgumentsResolver import app.simplecloud.plugin.api.shared.placeholder.single.SingleGroupPlaceholderExecutor -/** - * @author Niklas Nieberler - */ - class GroupPlaceholderProvider : AbstractPlaceholderProvider( SingleGroupPlaceholderExecutor() ) { - override suspend fun getArgumentsResolvers(controllerApi: ControllerApi.Coroutine, value: Group) = listOf( + override suspend fun getArgumentsResolvers(cloudApi: CloudApi, value: Group) = listOf( PropertiesArgumentsResolver(value.properties), - PlayerCountArgumentsResolver(controllerApi, value), - ServerCountArgumentsResolver(controllerApi, value) + PlayerCountArgumentsResolver(cloudApi, value), + ServerCountArgumentsResolver(cloudApi, value) ) -} \ No newline at end of file +} diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/provider/PersistentServerPlaceholderProvider.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/provider/PersistentServerPlaceholderProvider.kt new file mode 100644 index 0000000..1ade4b9 --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/provider/PersistentServerPlaceholderProvider.kt @@ -0,0 +1,20 @@ +package app.simplecloud.plugin.api.shared.placeholder.provider + +import app.simplecloud.api.CloudApi +import app.simplecloud.api.persistentserver.PersistentServer +import app.simplecloud.plugin.api.shared.placeholder.argument.* +import app.simplecloud.plugin.api.shared.placeholder.single.SinglePersistentServerPlaceholderExecutor + +/** + * @author Niklas Nieberler + */ + +class PersistentServerPlaceholderProvider : AbstractPlaceholderProvider( + SinglePersistentServerPlaceholderExecutor() +) { + + override suspend fun getArgumentsResolvers(cloudApi: CloudApi, value: PersistentServer) = listOf( + PropertiesArgumentsResolver(value.properties) + ) + +} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/provider/ServerPlaceholderProvider.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/provider/ServerPlaceholderProvider.kt index c08f797..8a78b99 100644 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/provider/ServerPlaceholderProvider.kt +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/provider/ServerPlaceholderProvider.kt @@ -1,7 +1,7 @@ package app.simplecloud.plugin.api.shared.placeholder.provider -import app.simplecloud.controller.api.ControllerApi -import app.simplecloud.controller.shared.server.Server +import app.simplecloud.api.CloudApi +import app.simplecloud.api.server.Server import app.simplecloud.plugin.api.shared.placeholder.argument.* import app.simplecloud.plugin.api.shared.placeholder.single.SingleServerPlaceholderExecutor @@ -13,7 +13,7 @@ class ServerPlaceholderProvider : AbstractPlaceholderProvider( SingleServerPlaceholderExecutor() ) { - override suspend fun getArgumentsResolvers(controllerApi: ControllerApi.Coroutine, value: Server) = listOf( + override suspend fun getArgumentsResolvers(cloudApi: CloudApi, value: Server) = listOf( PropertiesArgumentsResolver(value.properties) ) diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/single/SingleGroupPlaceholderExecutor.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/single/SingleGroupPlaceholderExecutor.kt index 5e09542..0eb254d 100644 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/single/SingleGroupPlaceholderExecutor.kt +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/single/SingleGroupPlaceholderExecutor.kt @@ -1,30 +1,24 @@ package app.simplecloud.plugin.api.shared.placeholder.single -import app.simplecloud.controller.api.ControllerApi -import app.simplecloud.controller.shared.group.Group +import app.simplecloud.api.CloudApi +import app.simplecloud.api.group.Group import app.simplecloud.plugin.api.shared.placeholder.async.AsyncPlaceholder - -/** - * @author Niklas Nieberler - */ +import kotlinx.coroutines.future.await class SingleGroupPlaceholderExecutor : SinglePlaceholderExecutor { - override fun getAsyncPlaceholders(controllerApi: ControllerApi.Coroutine) = listOf>( - AsyncPlaceholder("group_name") { it.name }, + override fun getAsyncPlaceholders(cloudApi: CloudApi) = listOf>( + AsyncPlaceholder("name") { it.name }, AsyncPlaceholder("type") { it.type }, AsyncPlaceholder("max_players") { it.maxPlayers }, AsyncPlaceholder("min_memory") { it.minMemory }, AsyncPlaceholder("max_memory") { it.maxMemory }, - AsyncPlaceholder("start_port") { it.startPort }, - AsyncPlaceholder("min_online_count") { it.minOnlineCount }, - AsyncPlaceholder("max_online_count") { it.maxOnlineCount }, - AsyncPlaceholder("online_players") { getOnlinePlayersByGroup(controllerApi, it) }, + AsyncPlaceholder("online_players") { getOnlinePlayersByGroup(cloudApi, it) }, ) - private suspend fun getOnlinePlayersByGroup(controllerApi: ControllerApi.Coroutine, group: Group): Long { - return controllerApi.getServers().getServersByGroup(group) + private suspend fun getOnlinePlayersByGroup(cloudApi: CloudApi, group: Group): Int { + return cloudApi.server().getServersByGroup(group.name).await() .sumOf { it.playerCount } } -} \ No newline at end of file +} diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/single/SinglePersistentServerPlaceholderExecutor.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/single/SinglePersistentServerPlaceholderExecutor.kt new file mode 100644 index 0000000..f0cb0ed --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/single/SinglePersistentServerPlaceholderExecutor.kt @@ -0,0 +1,28 @@ +package app.simplecloud.plugin.api.shared.placeholder.single + +import app.simplecloud.api.CloudApi +import app.simplecloud.api.persistentserver.PersistentServer +import app.simplecloud.plugin.api.shared.placeholder.async.AsyncPlaceholder +import app.simplecloud.plugin.api.shared.pretty.StringPrettifier + +/** + * @author Niklas Nieberler + */ + +class SinglePersistentServerPlaceholderExecutor : SinglePlaceholderExecutor { + + override fun getAsyncPlaceholders(cloudApi: CloudApi) = listOf>( + AsyncPlaceholder("id") { it.persistentServerId }, + AsyncPlaceholder("name") { it.name }, + AsyncPlaceholder("pretty_name") { + it.properties["pretty-name"] ?: StringPrettifier.prettify(it.name) + }, + AsyncPlaceholder("type") { it.type }, + AsyncPlaceholder("online_players") { it.playerCount }, + AsyncPlaceholder("max_players") { it.maxPlayers }, + AsyncPlaceholder("min_memory") { it.minMemory }, + AsyncPlaceholder("max_memory") { it.maxMemory }, + AsyncPlaceholder("motd") { it.properties["motd"] ?: "A Minecraft server" } + ) + +} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/single/SinglePlaceholderExecutor.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/single/SinglePlaceholderExecutor.kt index 5140084..bd80632 100644 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/single/SinglePlaceholderExecutor.kt +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/single/SinglePlaceholderExecutor.kt @@ -1,6 +1,6 @@ package app.simplecloud.plugin.api.shared.placeholder.single -import app.simplecloud.controller.api.ControllerApi +import app.simplecloud.api.CloudApi import app.simplecloud.plugin.api.shared.placeholder.async.AsyncPlaceholder import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver @@ -12,22 +12,22 @@ interface SinglePlaceholderExecutor { /** * Gets a list with all available [AsyncPlaceholder] - * @param controllerApi the instance of [ControllerApi.Coroutine] + * @param cloudApi the instance of [app.simplecloud.api.CloudApi] */ - fun getAsyncPlaceholders(controllerApi: ControllerApi.Coroutine): List> + fun getAsyncPlaceholders(cloudApi: CloudApi): List> /** * Gets a [TagResolver] with all available tag resolvers from the [getAsyncPlaceholders] method - * @param controllerApi the instance of [ControllerApi.Coroutine] + * @param cloudApi the instance of [CloudApi] * @param value for the placeholder * @param prefix first name for the placeholder key */ suspend fun getTagResolver( - controllerApi: ControllerApi.Coroutine, + cloudApi: CloudApi, value: T, prefix: String? = null, ): TagResolver { - return TagResolver.resolver(getAsyncPlaceholders(controllerApi) + return TagResolver.resolver(getAsyncPlaceholders(cloudApi) .map { it.invokeTagResolver(value, prefix) }) } diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/single/SingleServerPlaceholderExecutor.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/single/SingleServerPlaceholderExecutor.kt index 812cc8d..d66bdc4 100644 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/single/SingleServerPlaceholderExecutor.kt +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/placeholder/single/SingleServerPlaceholderExecutor.kt @@ -1,8 +1,9 @@ package app.simplecloud.plugin.api.shared.placeholder.single -import app.simplecloud.controller.api.ControllerApi -import app.simplecloud.controller.shared.server.Server +import app.simplecloud.api.CloudApi +import app.simplecloud.api.server.Server import app.simplecloud.plugin.api.shared.placeholder.async.AsyncPlaceholder +import app.simplecloud.plugin.api.shared.pretty.StringPrettifier /** * @author Niklas Nieberler @@ -10,18 +11,22 @@ import app.simplecloud.plugin.api.shared.placeholder.async.AsyncPlaceholder class SingleServerPlaceholderExecutor : SinglePlaceholderExecutor { - override fun getAsyncPlaceholders(controllerApi: ControllerApi.Coroutine) = listOf>( - AsyncPlaceholder("id") { it.uniqueId }, + override fun getAsyncPlaceholders(cloudApi: CloudApi) = listOf>( + AsyncPlaceholder("id") { it.serverId }, AsyncPlaceholder("numerical_id") { it.numericalId }, - AsyncPlaceholder("group_name") { it.group }, - AsyncPlaceholder("type") { it.type }, + AsyncPlaceholder("group_name") { it.serverBase.name }, + AsyncPlaceholder("group_pretty_name") { + it.properties["pretty-name"] ?: StringPrettifier.prettify(it.serverBase.name) + }, + AsyncPlaceholder("type") { it.serverBase.type }, AsyncPlaceholder("state") { it.state }, AsyncPlaceholder("ip") { it.ip }, AsyncPlaceholder("port") { it.port }, AsyncPlaceholder("online_players") { it.playerCount }, AsyncPlaceholder("max_players") { it.maxPlayers }, AsyncPlaceholder("min_memory") { it.minMemory }, - AsyncPlaceholder("max_memory") { it.maxMemory } + AsyncPlaceholder("max_memory") { it.maxMemory }, + AsyncPlaceholder("motd") { it.properties["motd"] ?: "A Minecraft server" } ) } \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/pretty/PrettifyCase.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/pretty/PrettifyCase.kt new file mode 100644 index 0000000..4ae7f66 --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/pretty/PrettifyCase.kt @@ -0,0 +1,10 @@ +package app.simplecloud.plugin.api.shared.pretty + +enum class PrettifyCase { + TITLE, + CAMEL, + PASCAL, + SENTENCE, + KEBAB, + SNAKE +} diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/pretty/StringPrettifier.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/pretty/StringPrettifier.kt new file mode 100644 index 0000000..e188366 --- /dev/null +++ b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/pretty/StringPrettifier.kt @@ -0,0 +1,64 @@ +package app.simplecloud.plugin.api.shared.pretty + +object StringPrettifier { + + /** + * Converts various string formats to different cases + */ + fun prettify(input: String, case: PrettifyCase = PrettifyCase.TITLE): String { + return when (case) { + PrettifyCase.TITLE -> toTitleCase(input) + PrettifyCase.CAMEL -> toCamelCase(input) + PrettifyCase.PASCAL -> toPascalCase(input) + PrettifyCase.SENTENCE -> toSentenceCase(input) + PrettifyCase.KEBAB -> toKebabCase(input) + PrettifyCase.SNAKE -> toSnakeCase(input) + } + } + + private fun toTitleCase(input: String): String { + return input.split(Regex("[\\s_-]")) + .filter { it.isNotEmpty() } + .joinToString(" ") { word -> + word.lowercase().replaceFirstChar { it.uppercase() } + } + } + + private fun toCamelCase(input: String): String { + return input.split(Regex("[\\s_-]")) + .filter { it.isNotEmpty() } + .mapIndexed { index, word -> + if (index == 0) word.lowercase() + else word.lowercase().replaceFirstChar { it.uppercase() } + } + .joinToString("") + } + + private fun toPascalCase(input: String): String { + return input.split(Regex("[\\s_-]")) + .filter { it.isNotEmpty() } + .joinToString("") { word -> + word.lowercase().replaceFirstChar { it.uppercase() } + } + } + + private fun toSentenceCase(input: String): String { + return input.split(Regex("[\\s_-]")) + .filter { it.isNotEmpty() } + .joinToString(" ") { it.lowercase() } + .replaceFirstChar { it.uppercase() } + } + + private fun toKebabCase(input: String): String { + return input.split(Regex("[\\s_]")) + .filter { it.isNotEmpty() } + .joinToString("-") { it.lowercase() } + } + + private fun toSnakeCase(input: String): String { + return input.split(Regex("[\\s-]")) + .filter { it.isNotEmpty() } + .joinToString("_") { it.lowercase() } + } + +} \ No newline at end of file diff --git a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/repository/YamlDirectoryRepository.kt b/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/repository/YamlDirectoryRepository.kt deleted file mode 100644 index 6260d4f..0000000 --- a/plugin-shared/src/main/kotlin/app/simplecloud/plugin/api/shared/repository/YamlDirectoryRepository.kt +++ /dev/null @@ -1,156 +0,0 @@ -package app.simplecloud.plugin.api.shared.repository - -import kotlinx.coroutines.* -import org.spongepowered.configurate.ConfigurationNode -import org.spongepowered.configurate.ConfigurationOptions -import org.spongepowered.configurate.kotlin.objectMapperFactory -import org.spongepowered.configurate.loader.ParsingException -import org.spongepowered.configurate.serialize.SerializationException -import org.spongepowered.configurate.serialize.TypeSerializer -import org.spongepowered.configurate.yaml.NodeStyle -import org.spongepowered.configurate.yaml.YamlConfigurationLoader -import java.io.File -import java.lang.reflect.Type -import java.nio.file.* - -abstract class YamlDirectoryRepository( - private val directory: Path, - private val clazz: Class, -) { - - private val watchService = FileSystems.getDefault().newWatchService() - private val loaders = mutableMapOf() - protected val entities = mutableMapOf() - - fun delete(element: E): Boolean { - val file = entities.keys.find { entities[it] == element } ?: return false - return deleteFile(file) - } - - fun getAll(): List { - return entities.values.toList() - } - - fun load(): List { - if (!directory.toFile().exists()) { - directory.toFile().mkdirs() - } - - registerWatcher() - - return Files.list(directory) - .toList() - .filter { !it.toFile().isDirectory && it.toString().endsWith(".yml") } - .mapNotNull { load(it.toFile()) } - } - - open fun watchUpdateEvent(file: File) {} - - private fun load(file: File): E? { - try { - val loader = getOrCreateLoader(file) - val node = loader.load(ConfigurationOptions.defaults()) - val entity = node.get(clazz) ?: return null - entities[file] = entity - return entity - } catch (ex: ParsingException) { - val existedBefore = entities.containsKey(file) - if (existedBefore) { - return null - } - return null - } - } - - private fun deleteFile(file: File): Boolean { - val deletedSuccessfully = file.delete() - val removedSuccessfully = entities.remove(file) != null - return deletedSuccessfully && removedSuccessfully - } - - fun save(fileName: String, entity: E) { - val file = directory.resolve(fileName).toFile() - val loader = getOrCreateLoader(file) - val node = loader.createNode(ConfigurationOptions.defaults().serializers { - it.register(Enum::class.java, GenericEnumSerializer) - }) - node.set(clazz, entity) - loader.save(node) - entities[file] = entity - } - - private fun getOrCreateLoader(file: File): YamlConfigurationLoader { - return loaders.getOrPut(file) { - YamlConfigurationLoader.builder() - .path(file.toPath()) - .nodeStyle(NodeStyle.BLOCK) - .nodeStyle(NodeStyle.BLOCK) - .defaultOptions { options -> - options.serializers { builder -> - builder.registerAnnotatedObjects(objectMapperFactory()) - builder.register(Enum::class.java, GenericEnumSerializer) - } - }.build() - } - } - - private fun registerWatcher(): Job { - directory.register( - watchService, - StandardWatchEventKinds.ENTRY_CREATE, - StandardWatchEventKinds.ENTRY_DELETE, - StandardWatchEventKinds.ENTRY_MODIFY - ) - - return CoroutineScope(Dispatchers.IO).launch { - while (isActive) { - val key = watchService.take() - for (event in key.pollEvents()) { - val path = event.context() as? Path ?: continue - val resolvedPath = directory.resolve(path) - if (Files.isDirectory(resolvedPath) || !resolvedPath.toString().endsWith(".yml")) { - continue - } - val kind = event.kind() - when (kind) { - StandardWatchEventKinds.ENTRY_CREATE -> { - load(resolvedPath.toFile()) - } - - StandardWatchEventKinds.ENTRY_MODIFY -> { - load(resolvedPath.toFile()) - watchUpdateEvent(resolvedPath.toFile()) - } - - StandardWatchEventKinds.ENTRY_DELETE -> { - deleteFile(resolvedPath.toFile()) - } - } - } - key.reset() - } - } - } - - private object GenericEnumSerializer : TypeSerializer> { - override fun deserialize(type: Type, node: ConfigurationNode): Enum<*> { - val value = node.string ?: throw SerializationException("No value present in node") - - if (type !is Class<*> || !type.isEnum) { - throw SerializationException("Type is not an enum class") - } - - @Suppress("UNCHECKED_CAST") - return try { - java.lang.Enum.valueOf(type as Class>, value) - } catch (e: IllegalArgumentException) { - throw SerializationException("Invalid enum constant") - } - } - - override fun serialize(type: Type, obj: Enum<*>?, node: ConfigurationNode) { - node.set(obj?.name) - } - } - -} \ No newline at end of file diff --git a/plugin-shared/src/test/kotlin/app/simplecloud/plugin/api/shared/config/ConfigurationMigrationTest.kt b/plugin-shared/src/test/kotlin/app/simplecloud/plugin/api/shared/config/ConfigurationMigrationTest.kt new file mode 100644 index 0000000..1afbab7 --- /dev/null +++ b/plugin-shared/src/test/kotlin/app/simplecloud/plugin/api/shared/config/ConfigurationMigrationTest.kt @@ -0,0 +1,108 @@ +package app.simplecloud.plugin.api.shared.config + +import java.nio.file.Files +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.spongepowered.configurate.objectmapping.ConfigSerializable + +class ConfigurationMigrationTest { + + @Test + fun `factory migrates outdated configs and rewrites yaml`() { + val tempDirectory = Files.createTempDirectory("config-migration-factory") + val file = tempDirectory.resolve("config.yml").toFile() + file.writeText( + """ + name: legacy-value + """.trimIndent() + ) + + val configurationFactory = ConfigurationFactory( + file = file, + javaClass = TestVersionedConfig::class.java, + configMigrator = testMigrator(), + ) + + val config = configurationFactory.loadOrCreate(TestVersionedConfig()) + + assertEquals(2, config.version) + assertEquals("legacy-value", config.message) + assertTrue(config.enabled) + + val migratedYaml = file.readText() + assertTrue(migratedYaml.contains("version: 2")) + assertTrue(migratedYaml.contains("message: legacy-value")) + assertTrue(migratedYaml.contains("enabled: true")) + assertFalse(migratedYaml.contains("name:")) + } + + @Test + fun `directory repository migrates loaded entries`() { + val tempDirectory = Files.createTempDirectory("config-migration-repository") + val file = tempDirectory.resolve("entry.yml").toFile() + file.writeText( + """ + version: 1 + message: repository-value + """.trimIndent() + ) + + val repository = TestYamlDirectoryRepository( + directory = tempDirectory, + configMigrator = testMigrator(), + ) + + val loadedEntries = repository.load() + + assertEquals(1, loadedEntries.size) + val config = loadedEntries.first() + assertEquals(2, config.version) + assertEquals("repository-value", config.message) + assertTrue(config.enabled) + + val migratedYaml = file.readText() + assertTrue(migratedYaml.contains("version: 2")) + assertTrue(migratedYaml.contains("enabled: true")) + } + + private fun testMigrator(): ConfigMigrator { + return ConfigMigrator.builder(currentVersion = 2) + .migrate(fromVersion = 0, toVersion = 1) { node -> + val legacyName = node.node("name").string ?: return@migrate + node.node("message").set(legacyName) + node.node("name").set(null) + } + .migrate(fromVersion = 1, toVersion = 2) { node -> + node.node("enabled").set(true) + } + .build() + } + + @ConfigSerializable + private class TestVersionedConfig : VersionedConfig { + override var version: Int = 2 + var message: String = "default-message" + var enabled: Boolean = true + } + + private class TestYamlDirectoryRepository( + directory: Path, + configMigrator: ConfigMigrator, + ) : YamlDirectoryRepository( + directory = directory, + javaClass = TestVersionedConfig::class.java, + configMigrator = configMigrator, + ) { + + override fun save(entity: TestVersionedConfig) { + save("${entity.message}.yml", entity) + } + + override fun find(identifier: String): TestVersionedConfig? { + return findAll().firstOrNull { it.message == identifier } + } + } +} diff --git a/plugin-shared/src/test/kotlin/app/simplecloud/plugin/api/shared/pretty/StringPrettifierTest.kt b/plugin-shared/src/test/kotlin/app/simplecloud/plugin/api/shared/pretty/StringPrettifierTest.kt new file mode 100644 index 0000000..28cca93 --- /dev/null +++ b/plugin-shared/src/test/kotlin/app/simplecloud/plugin/api/shared/pretty/StringPrettifierTest.kt @@ -0,0 +1,65 @@ +package app.simplecloud.plugin.api.shared.pretty + +import kotlin.test.* + +class StringPrettifierTest { + + @Test + fun testTitleCaseConversion() { + assertEquals("Hello World", StringPrettifier.prettify("hello-world")) + assertEquals("Hello World", StringPrettifier.prettify("hello_world")) + assertEquals("Hello World", StringPrettifier.prettify("hello world")) + assertEquals("Hello Beautiful World", StringPrettifier.prettify("hello-beautiful_world")) + } + + @Test + fun testCamelCaseConversion() { + assertEquals("helloWorld", StringPrettifier.prettify("hello-world", PrettifyCase.CAMEL)) + assertEquals("helloWorld", StringPrettifier.prettify("hello_world", PrettifyCase.CAMEL)) + assertEquals("helloBeautifulWorld", StringPrettifier.prettify("hello beautiful world", PrettifyCase.CAMEL)) + assertEquals("myVariableName", StringPrettifier.prettify("my-variable-name", PrettifyCase.CAMEL)) + } + + @Test + fun testPascalCaseConversion() { + assertEquals("HelloWorld", StringPrettifier.prettify("hello-world", PrettifyCase.PASCAL)) + assertEquals("HelloWorld", StringPrettifier.prettify("hello_world", PrettifyCase.PASCAL)) + assertEquals("HelloBeautifulWorld", StringPrettifier.prettify("hello beautiful world", PrettifyCase.PASCAL)) + assertEquals("MyVariableName", StringPrettifier.prettify("my-variable-name", PrettifyCase.PASCAL)) + } + + @Test + fun testSentenceCaseConversion() { + assertEquals("Hello world", StringPrettifier.prettify("hello-world", PrettifyCase.SENTENCE)) + assertEquals("Hello beautiful world", StringPrettifier.prettify("hello_beautiful_world", PrettifyCase.SENTENCE)) + assertEquals("My variable name", StringPrettifier.prettify("my-variable-name", PrettifyCase.SENTENCE)) + } + + @Test + fun testKebabCaseConversion() { + assertEquals("hello-world", StringPrettifier.prettify("hello world", PrettifyCase.KEBAB)) + assertEquals("hello-beautiful-world", StringPrettifier.prettify("hello_beautiful_world", PrettifyCase.KEBAB)) + assertEquals("my-variable-name", StringPrettifier.prettify("my variable name", PrettifyCase.KEBAB)) + } + + @Test + fun testSnakeCaseConversion() { + assertEquals("hello_world", StringPrettifier.prettify("hello-world", PrettifyCase.SNAKE)) + assertEquals("hello_beautiful_world", StringPrettifier.prettify("hello beautiful world", PrettifyCase.SNAKE)) + assertEquals("my_variable_name", StringPrettifier.prettify("my-variable-name", PrettifyCase.SNAKE)) + } + + @Test + fun testEmptyStringHandling() { + assertEquals("", StringPrettifier.prettify("")) + assertEquals("", StringPrettifier.prettify("", PrettifyCase.CAMEL)) + assertEquals("", StringPrettifier.prettify("", PrettifyCase.PASCAL)) + } + + @Test + fun testMultipleDelimiterHandling() { + assertEquals("Hello Beautiful World", StringPrettifier.prettify("hello-beautiful_world")) + assertEquals("helloBeautifulWorld", StringPrettifier.prettify("hello-beautiful_world", PrettifyCase.CAMEL)) + assertEquals("HelloBeautifulWorld", StringPrettifier.prettify("hello-beautiful_world", PrettifyCase.PASCAL)) + } +} diff --git a/plugin-velocity/build.gradle.kts b/plugin-velocity/build.gradle.kts new file mode 100644 index 0000000..12b0664 --- /dev/null +++ b/plugin-velocity/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + kotlin("kapt") +} + +dependencies { + compileOnly(project(":plugin-shared")) + compileOnly(libs.cloud.velocity) + compileOnly(libs.velocity.api) + kapt(libs.velocity.api) +} \ No newline at end of file diff --git a/plugin-velocity/src/main/kotlin/app/simplecloud/plugin/api/velocity/command/VelocityCloudSender.kt b/plugin-velocity/src/main/kotlin/app/simplecloud/plugin/api/velocity/command/VelocityCloudSender.kt new file mode 100644 index 0000000..9527ac2 --- /dev/null +++ b/plugin-velocity/src/main/kotlin/app/simplecloud/plugin/api/velocity/command/VelocityCloudSender.kt @@ -0,0 +1,27 @@ +package app.simplecloud.plugin.api.velocity.command + +import app.simplecloud.plugin.api.shared.command.CloudSender +import com.velocitypowered.api.command.CommandSource +import com.velocitypowered.api.proxy.Player +import net.kyori.adventure.text.Component +import java.util.UUID + +class VelocityCloudSender(private val commandSource: CommandSource) : CloudSender { + + fun getCommandSource(): CommandSource { + return commandSource + } + + override fun getUniqueId(): UUID? { + return (commandSource as? Player)?.uniqueId + } + + override fun hasPermission(permission: String): Boolean { + return commandSource.hasPermission(permission) + } + + override fun sendMessage(message: Component) { + commandSource.sendMessage(message) + } + +} diff --git a/plugin-velocity/src/main/kotlin/app/simplecloud/plugin/api/velocity/command/VelocityCloudSenderMapper.kt b/plugin-velocity/src/main/kotlin/app/simplecloud/plugin/api/velocity/command/VelocityCloudSenderMapper.kt new file mode 100644 index 0000000..e8ef1e8 --- /dev/null +++ b/plugin-velocity/src/main/kotlin/app/simplecloud/plugin/api/velocity/command/VelocityCloudSenderMapper.kt @@ -0,0 +1,16 @@ +package app.simplecloud.plugin.api.velocity.command + +import com.velocitypowered.api.command.CommandSource +import org.incendo.cloud.SenderMapper + +object VelocityCloudSenderMapper : SenderMapper { + + override fun map(base: CommandSource): VelocityCloudSender { + return VelocityCloudSender(base) + } + + override fun reverse(mapped: VelocityCloudSender): CommandSource { + return mapped.getCommandSource() + } + +} diff --git a/plugin-velocity/src/main/kotlin/app/simplecloud/plugin/api/velocity/permission/VelocityPermissionChecker.kt b/plugin-velocity/src/main/kotlin/app/simplecloud/plugin/api/velocity/permission/VelocityPermissionChecker.kt new file mode 100644 index 0000000..a2bb7e7 --- /dev/null +++ b/plugin-velocity/src/main/kotlin/app/simplecloud/plugin/api/velocity/permission/VelocityPermissionChecker.kt @@ -0,0 +1,16 @@ +package app.simplecloud.plugin.api.velocity.permission + +import app.simplecloud.plugin.api.shared.permission.PermissionChecker +import com.velocitypowered.api.proxy.Player + +/** + * @author Niklas Nieberler + */ + +class VelocityPermissionChecker : PermissionChecker { + + override fun checkPermission(player: Player, name: String): Boolean { + return player.hasPermission(name) + } + +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index fdc78e6..7cabdca 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,13 @@ -rootProject.name = "plugin-api" -include("plugin-shared") +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + +include( + "plugin-bungeecord", + "plugin-minestom", + "plugin-paper", + "plugin-shared", + "plugin-velocity" +) + +rootProject.name = "plugin-api" \ No newline at end of file