diff --git a/.github/workflows/api-dump-version.yml b/.github/workflows/api-dump-version.yml index 7d60103f6..195e4bb56 100644 --- a/.github/workflows/api-dump-version.yml +++ b/.github/workflows/api-dump-version.yml @@ -29,6 +29,20 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 + - name: Init submodules + shell: bash + run: git -c 'url.https://github.com/.insteadOf=git@github.com:' submodule update --init --recursive + + - name: Apply vendor patches + shell: bash + working-directory: vendor + env: + GIT_AUTHOR_NAME: github-actions + GIT_AUTHOR_EMAIL: actions@slne.dev + GIT_COMMITTER_NAME: github-actions + GIT_COMMITTER_EMAIL: actions@slne.dev + run: ./gradlew applyPatches + - name: Run Kotlin Binary Compatibility Check id: check_api run: | diff --git a/.github/workflows/build-pr-jar.yml b/.github/workflows/build-pr-jar.yml index 0e929a44e..1203c5675 100644 --- a/.github/workflows/build-pr-jar.yml +++ b/.github/workflows/build-pr-jar.yml @@ -42,6 +42,20 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 + - name: Init submodules + shell: bash + run: git -c 'url.https://github.com/.insteadOf=git@github.com:' submodule update --init --recursive + + - name: Apply vendor patches + shell: bash + working-directory: vendor + env: + GIT_AUTHOR_NAME: github-actions + GIT_AUTHOR_EMAIL: actions@slne.dev + GIT_COMMITTER_NAME: github-actions + GIT_COMMITTER_EMAIL: actions@slne.dev + run: ./gradlew applyPatches + - name: Build modules with Gradle run: ./gradlew build shadowJar --parallel --no-scan diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6e9d29d39..a668965eb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,4 +13,10 @@ jobs: with: modules: "surf-api-paper-server-*-all.jar;surf-api-velocity-server-*-all.jar;surf-api-standalone-server-*-all.jar" check-kotlin-abi: true + pre-build-command: | + git -c 'url.https://github.com/.insteadOf=git@github.com:' submodule update --init --recursive + cd vendor + GIT_AUTHOR_NAME=github-actions GIT_AUTHOR_EMAIL=actions@slne.dev \ + GIT_COMMITTER_NAME=github-actions GIT_COMMITTER_EMAIL=actions@slne.dev \ + ./gradlew applyPatches secrets: inherit diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..5d4affc36 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/LuckPerms-Upstream"] + path = vendor/LuckPerms-Upstream + url = https://github.com/TropicalShadow/LuckPerms.git diff --git a/build.gradle.kts b/build.gradle.kts index 6b0db93f5..2c6a1e856 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,6 +16,13 @@ allprojects { repositories { mavenCentral() maven("https://repo.papermc.io/repository/maven-public/") + maven("https://central.sonatype.com/repository/maven-snapshots/") { + name = "minestom-snapshots" + mavenContent { + snapshotsOnly() + includeGroup("net.minestom") + } + } maven("https://repo.wyck.dev/snapshots/") } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 0eb86dd70..98f73dfc2 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -13,4 +13,5 @@ dependencies { implementation(libs.kotlin.serialization) implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) implementation(libs.asm) + implementation(libs.gremlin.gradle.plugin) } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/vendor-bundle-convention.gradle.kts b/buildSrc/src/main/kotlin/vendor-bundle-convention.gradle.kts new file mode 100644 index 000000000..f414e818b --- /dev/null +++ b/buildSrc/src/main/kotlin/vendor-bundle-convention.gradle.kts @@ -0,0 +1,78 @@ +import org.gradle.api.artifacts.component.ModuleComponentIdentifier + +/** + * Bundles the vendor builds in `vendor/` into the module's own jar, the way surf-minestom-lobby + * bundles them into its server jar. + * + * The vendor artifacts are built from source and published nowhere, so they cannot appear as + * dependencies of a published module. Declare them in the `bundled` configuration instead: their + * classes, and those of everything they pull in, are copied into the jar, except for the libraries + * a Minestom server already provides. + */ + +plugins { + id("core-convention") +} + +/** + * Libraries a Minestom server provides itself, which are therefore never bundled. + */ +private val providedGroups = setOf( + "org.slf4j", + "org.jspecify", + "org.jetbrains", + "org.jetbrains.kotlin", + "org.jetbrains.kotlinx", + "org.checkerframework", + "com.google.code.gson", + "com.google.guava", + "com.google.errorprone", +) + +/** + * Adventure ships with Minestom, apart from the few extensions spark brings along. + */ +private val providedAdventureModules = Regex("adventure-(api|key|nbt|text-(minimessage|logger-slf4j|serializer-.*))|option|examination-.*") + +/** + * Libraries the module declares as regular dependencies instead of bundling them. A bundled ASM + * would end up on the system class path ahead of the ASM the server's library download relocates + * with. The relocation would then run on the bundled version, which cannot read current class + * files. + */ +private val declaredGroups = setOf( + "org.ow2.asm", +) + +private fun isProvided(id: ModuleComponentIdentifier): Boolean = + id.group in providedGroups || id.group in declaredGroups || + (id.group == "net.kyori" && providedAdventureModules.matches(id.module)) + +val bundled: Configuration = configurations.create("bundled") { + description = "Vendor builds copied into this module's jar instead of being published as dependencies" + isCanBeConsumed = false + isCanBeResolved = true +} + +configurations.compileOnly { extendsFrom(bundled) } +configurations.testImplementation { extendsFrom(bundled) } + +val bundledFiles = bundled.incoming.artifactView { + componentFilter { id -> id !is ModuleComponentIdentifier || !isProvided(id) } +}.files + +tasks.jar { + inputs.files(bundledFiles) + + from(bundledFiles.elements.map { files -> files.map { zipTree(it.asFile) } }) { + exclude( + "module-info.class", + "META-INF/versions/*/module-info.class", + "META-INF/MANIFEST.MF", + "META-INF/*.SF", + "META-INF/*.DSA", + "META-INF/*.RSA", + "META-INF/maven/**", + ) + } +} diff --git a/gradle.properties b/gradle.properties index bb5f472f1..83b63db19 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled javaVersion=25 mcVersion=26.3 group=dev.slne.surf.api -version=3.47.1 +version=3.48.0 relocationPrefix=dev.slne.surf.api.libs snapshot=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b2b1d3acb..0a657db23 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,15 @@ commandapiplugin = "12.1.0" luckperms = "5.5" luckpermsplugin-bukkit = "v5.5.71-bukkit" # "luckpermsplugin" is written without a separating "-" between "luckperms" and "plugin", because Gradle interprets the version catalog key as a hierarchical alias and would split it into a nested accessor (libs.luckperms.plugin.bukkit), which conflicts with the existing libs.luckperms alias for the API dependency. +# Minestom +minestom-server = "26_3-SNAPSHOT" # experimental 26.3 builds, from the Sonatype snapshot repository +minestom-brigadier = "1.3.10" +minestom-npc = "0.1.3" +luckpermsMinestom = "5.5+26.2-SNAPSHOT" # substituted by the vendor/LuckPerms included build +sparkMinestom = "1.10.181-SNAPSHOT" # substituted by the vendor/spark-minestom included build +terminal-console-appender = "1.3.0" +log4j = "2.26.1" + # Scoreboard Library scoreboard-library = "2.8.2" @@ -37,6 +46,9 @@ mccoroutine = "2.23.1" # Miscellaneous Libraries asm = "9.10.1" +sponge-mixin = "0.17.5+mixin.0.8.7" +mixinextras = "0.5.5" +flare = "2.0.1" # the version Minestom uses at runtime slf4j = "2.0.20" guava = "33.7.1-jre" caffeine = "3.3.0" @@ -67,6 +79,7 @@ ksp-version = "2.3.12" reactor-netty = "1.3.7" bytebuddy = "1.18.14" wyck = "4.0.0-e5d01de" +gremlin = "0.0.9" # Testing junit = "6.1.3" @@ -74,7 +87,7 @@ junit-platform-launcher = "6.1.3" mockk = "1.14.11" lincheck = "3.7" mockbukkit = "4.116.1" -minestom-testing = "2026.09.12-26.2" +minestom-testing = "26_3-SNAPSHOT" # Plugin versions maven-repo-auth = "3.0.4" @@ -88,7 +101,16 @@ paper-api = { module = "io.papermc.paper:paper-api", version.ref = "paper-api" } canvas-api = { module = "io.canvasmc.canvas:canvas-api", version.ref = "canvas-api" } # Minestom -surf-minestom-lobby-api = { module = "dev.slne.minestom.lobby:surf-minestom-lobby-api", version = "+" } +minestom-server = { module = "net.minestom:minestom", version.ref = "minestom-server" } +minestom-brigadier = { module = "com.mojang:brigadier", version.ref = "minestom-brigadier" } +minestom-npc = { module = "codes.bed.minestom:npc", version.ref = "minestom-npc" } +minestom-luckperms = { module = "club.tesseract:luckperms-minestom", version.ref = "luckpermsMinestom" } +minestom-spark = { module = "me.lucko:spark-minestom", version.ref = "sparkMinestom" } +minestom-spark-common = { module = "me.lucko:spark-common", version.ref = "sparkMinestom" } +terminal-console-appender = { module = "net.minecrell:terminalconsoleappender", version.ref = "terminal-console-appender" } +log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } +log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } +log4j-slf4j2-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" } # Kotlin kotlinxCoroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } @@ -144,6 +166,9 @@ mccoroutine-velocity-core = { module = "dev.slne.forks.mccoroutine:mccoroutine-v # Miscellaneous Libraries asm = { module = "org.ow2.asm:asm", version.ref = "asm" } +sponge-mixin = { module = "net.fabricmc:sponge-mixin", version.ref = "sponge-mixin" } +mixinextras = { module = "io.github.llamalad7:mixinextras-common", version.ref = "mixinextras" } +flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" } slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } guava = { module = "com.google.guava:guava", version.ref = "guava" } caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" } @@ -174,6 +199,7 @@ glm = { module = "io.github.kotlin-graphics:glm", version.ref = "glm" } datafixerupper = { module = "com.mojang:datafixerupper", version.ref = "datafixerupper" } luckperms = { module = "net.luckperms:api", version.ref = "luckperms" } wyck = { module = "dev.wyck:wyck", version.ref = "wyck" } +gremlin-runtime = { module = "xyz.jpenilla:gremlin-runtime", version.ref = "gremlin" } auto-service-annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "auto-service" } auto-service = { module = "dev.zacsweers.autoservice:auto-service-ksp", version.ref = "auto-service-ksp" } @@ -184,6 +210,7 @@ ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx reactor-netty-core = { module = "io.projectreactor.netty:reactor-netty-core", version.ref = "reactor-netty" } reactor-netty-http = { module = "io.projectreactor.netty:reactor-netty-http", version.ref = "reactor-netty" } bytebuddy = { module = "net.bytebuddy:byte-buddy", version.ref = "bytebuddy" } +bytebuddy-agent = { module = "net.bytebuddy:byte-buddy-agent", version.ref = "bytebuddy" } # Testing junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } @@ -206,6 +233,7 @@ ksp-gradle-plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp. ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp-version" } plugin-yml-paper-gradle-plugin = { module = "de.eldoria.plugin-yml.paper:de.eldoria.plugin-yml.paper.gradle.plugin", version.ref = "plugin-yml-paper" } run-paper-gradle-plugin = { module = "xyz.jpenilla.run-paper:xyz.jpenilla.run-paper.gradle.plugin", version.ref = "run-paper-gradle-plugin" } +gremlin-gradle-plugin = { module = "xyz.jpenilla.gremlin-gradle:xyz.jpenilla.gremlin-gradle.gradle.plugin", version.ref = "gremlin" } dokka-gradle-plugin = { module = "org.jetbrains.dokka:org.jetbrains.dokka.gradle.plugin", version.ref = "dokka" } [plugins] diff --git a/settings.gradle.kts b/settings.gradle.kts index 641f2daf8..b40cc9e44 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,11 +36,49 @@ include("surf-api-shared") include("surf-api-shared:surf-api-shared-public") include("surf-api-shared:surf-api-shared-internal") -include("surf-api-minestom") +include(":surf-api-minestom:surf-api-minestom") +include(":surf-api-minestom:surf-api-minestom-server") +include(":surf-api-minestom:surf-api-minestom-server-bootstrap") +include(":surf-api-minestom:surf-api-minestom-server-signed-chat") +include(":surf-api-minestom:surf-api-minestom-server-npc") +include(":surf-api-minestom:surf-api-minestom-server-player-visibility") +include(":surf-api-minestom:surf-api-minestom-server-console") +include(":surf-api-minestom:surf-api-minestom-server-spark") +include(":surf-api-minestom:surf-api-minestom-server-plugins") + +includeBuild("vendor/spark-minestom") { + dependencySubstitution { + substitute(module("me.lucko:spark-minestom")).using(project(":")) + } +} val ci = System.getenv("CI")?.toBoolean() ?: false +// vendor/LuckPerms is generated from the LuckPerms-Upstream submodule and the patches next to it, +// so the LuckPerms feature is only built once it has been created; CI builds require it +val luckPermsDir = file("vendor/LuckPerms") +if (luckPermsDir.resolve("settings.gradle").isFile) { + include(":surf-api-minestom:surf-api-minestom-server-luckperms") + + includeBuild(luckPermsDir) { + dependencySubstitution { + substitute(module("club.tesseract:luckperms-minestom")).using(project(":minestom")) + } + } +} else { + val message = """ + vendor/LuckPerms is missing, so surf-api-minestom-server-luckperms is not built. Create it with: + git submodule update --init --recursive + cd vendor && ./gradlew applyPatches + """.trimIndent() + + if (ci) error(message) + logger.warn(message) +} + if (!ci) { include(":surf-api-paper:surf-api-paper-plugin-test") + include(":surf-api-minestom:surf-api-minestom-example") + include(":surf-api-minestom:surf-api-minestom-example-plugin") include("surf-api-generator") } \ No newline at end of file diff --git a/surf-api-gradle-plugin/build.gradle.kts b/surf-api-gradle-plugin/build.gradle.kts index 246de7853..fe2eb3ee3 100644 --- a/surf-api-gradle-plugin/build.gradle.kts +++ b/surf-api-gradle-plugin/build.gradle.kts @@ -20,7 +20,7 @@ plugins { group = groupId version = buildString { - append("2.3.0") + append("2.4.0") if (snapshot) append("-SNAPSHOT") } @@ -37,7 +37,8 @@ val pluginDependencies = listOf( libs.shadow.gradle.plugin, libs.run.paper.gradle.plugin, libs.plugin.yml.paper.gradle.plugin, - libs.ksp.gradle.plugin + libs.ksp.gradle.plugin, + libs.gremlin.gradle.plugin, ) dependencies { @@ -95,6 +96,12 @@ gradlePlugin { implementationClass = "dev.slne.surf.api.gradle.platform.minestom.MinestomSurfPlugin" } + create("minestom-server") { + id = "dev.slne.surf.api.gradle.minestom-server" + implementationClass = + "dev.slne.surf.api.gradle.platform.minestom.server.MinestomServerSurfPlugin" + } + create("minestom-relocations") { id = "dev.slne.surf.api.gradle.minestom-relocations" implementationClass = @@ -131,7 +138,10 @@ val generateConstants = tasks.register("generateConstants") { inputs.property("libs.auto.service.annotations", libs.auto.service.annotations.get().toString()) inputs.property("libs.versions.commandapiplugin", libs.versions.commandapiplugin.get()) inputs.property("libs.versions.placeholder.api", libs.versions.placeholder.api.get()) - inputs.property("libs.versions.luckpermsplugin.bukkit", libs.versions.luckpermsplugin.bukkit.get()) + inputs.property( + "libs.versions.luckpermsplugin.bukkit", + libs.versions.luckpermsplugin.bukkit.get() + ) inputs.property( "libs.versions.packetevents", libs.versions.packetevents.plugin.get() @@ -150,6 +160,9 @@ val generateConstants = tasks.register("generateConstants") { inputs.property("libs.versions.mockbukkit", libs.versions.mockbukkit.get()) inputs.property("libs.versions.minestom.testing", libs.versions.minestom.testing.get()) inputs.property("libs.versions.kotlinxCoroutines", libs.versions.kotlinxCoroutines.get()) + inputs.property("libs.minestom.server", libs.minestom.server.get().toString()) + inputs.property("libs.versions.log4j", libs.versions.log4j.get()) + inputs.property("libs.versions.asm", libs.versions.asm.get()) outputs.dir(constantsOutputDir) doLast { @@ -184,6 +197,10 @@ val generateConstants = tasks.register("generateConstants") { | const val TEST_MINESTOM_TESTING_VERSION = "${libs.versions.minestom.testing.get()}" | const val TEST_COROUTINES_VERSION = "${libs.versions.kotlinxCoroutines.get()}" | + | const val MINESTOM = "${libs.minestom.server.get()}" + | const val LOG4J_VERSION = "${libs.versions.log4j.get()}" + | const val ASM_VERSION = "${libs.versions.asm.get()}" + | | const val SURF_API_FULL_VERSION = "${rootProject.findProperty("version") as String + if (snapshot) "-SNAPSHOT" else ""}" |} """.trimMargin() diff --git a/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/generators/pluginfiles/MinestomPluginFile.kt b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/generators/pluginfiles/MinestomPluginFile.kt new file mode 100644 index 000000000..c6ef1b120 --- /dev/null +++ b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/generators/pluginfiles/MinestomPluginFile.kt @@ -0,0 +1,78 @@ +package dev.slne.surf.api.gradle.generators.pluginfiles + +import dev.slne.surf.api.gradle.platform.invalidPluginFile +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Nested +import org.gradle.kotlin.dsl.domainObjectContainer +import org.intellij.lang.annotations.Pattern +import org.intellij.lang.annotations.RegExp +import javax.inject.Inject + +@RegExp +private const val ID_REGEX = "[a-z][a-z0-9-_]{0,63}" +private val idRegex = ID_REGEX.toRegex() + +abstract class MinestomPluginFile @Inject constructor( + objects: ObjectFactory +) : CommonPluginFile() { + @get:Pattern(ID_REGEX) + @get:Input + abstract val id: Property + + @get:Input + abstract val main: Property + + @get:Input + abstract val name: Property + + @get:Input + abstract val version: Property + + @get:Input + abstract val description: Property + + @get:Input + abstract val url: Property + + @get:Input + abstract val authors: ListProperty + + @get:Nested + val pluginDependencies: NamedDomainObjectContainer = + objects.domainObjectContainer(Dependency::class) + + abstract class Dependency @Inject constructor( + @get:Input val name: String + ) { + @get:Input + abstract val optional: Property + + @get:Input + internal abstract val enabled: Property + + init { + optional.convention(false) + enabled.convention(true) + } + } + + override fun isApplied(): Boolean { + return main.isPresent + } + + override fun validate() { + val id = id.orNull ?: invalidPluginFile("Plugin id not set") + if (!(idRegex.matches(id))) invalidPluginFile("Invalid plugin id! Should match $idRegex") + + if (version.orNull.isNullOrBlank()) invalidPluginFile("Plugin version not set") + if (main.orNull.isNullOrBlank()) invalidPluginFile("Main class not set") + + for (dependency in pluginDependencies) { + if (dependency.name.isBlank()) invalidPluginFile("Dependency id not set") + } + } +} \ No newline at end of file diff --git a/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/generators/pluginfiles/dto/MinestomPluginFileDto.kt b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/generators/pluginfiles/dto/MinestomPluginFileDto.kt new file mode 100644 index 000000000..9ec58f119 --- /dev/null +++ b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/generators/pluginfiles/dto/MinestomPluginFileDto.kt @@ -0,0 +1,40 @@ +package dev.slne.surf.api.gradle.generators.pluginfiles.dto + +import dev.slne.surf.api.gradle.generators.pluginfiles.MinestomPluginFile +import kotlinx.serialization.Serializable + +@Serializable +data class MinestomPluginFileDto( + val id: String? = null, + val main: String? = null, + val name: String? = null, + val version: String? = null, + val description: String? = null, + val url: String? = null, + val authors: List? = null, + val dependencies: List? = null, +) { + @Serializable + data class DependencyDto( + val id: String, + val optional: Boolean, + ) + + + companion object { + fun fromFile(file: MinestomPluginFile) = MinestomPluginFileDto( + id = file.id.orNull, + main = file.main.orNull, + name = file.name.orNull, + version = file.version.orNull, + description = file.description.orNull, + url = file.url.orNull, + authors = file.authors.orNull, + dependencies = file.pluginDependencies + .filter { it.enabled.get() } + .map { DependencyDto(it.name, it.optional.get()) } + ) + } +} + + diff --git a/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/SurfApiPlatform.kt b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/SurfApiPlatform.kt index 0648e4db7..0fdfd669f 100644 --- a/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/SurfApiPlatform.kt +++ b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/SurfApiPlatform.kt @@ -9,5 +9,6 @@ enum class SurfApiPlatform(val dependency: String, val scope: String = COMPILE_O PAPER("dev.slne.surf.api:surf-api-paper:${Constants.SURF_API_VERSION}"), VELOCITY("dev.slne.surf.api:surf-api-velocity:${Constants.SURF_API_VERSION}"), MINESTOM("dev.slne.surf.api:surf-api-minestom:${Constants.SURF_API_VERSION}"), + MINESTOM_SERVER("dev.slne.surf.api:surf-api-minestom-server:${Constants.SURF_API_VERSION}", "implementation"), STANDALONE("dev.slne.surf.api:surf-api-standalone:${Constants.SURF_API_VERSION}", API), } \ No newline at end of file diff --git a/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/common/CommonSurfPlugin.kt b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/common/CommonSurfPlugin.kt index 2786c3dfb..6eab2167a 100644 --- a/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/common/CommonSurfPlugin.kt +++ b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/common/CommonSurfPlugin.kt @@ -81,6 +81,21 @@ abstract class CommonSurfPlugin( relocations += Relocation(from, to, excludes) } + /** + * Relocates [from] below the surf relocation prefix, as `.`. + */ + internal fun applyRelocation(from: String, to: String, excludes: List = emptyList()) { + relocations += Relocation(from, to, excludes) + } + + /** + * Calls [action] for every relocation the shadow jar applies, with [to] already below the surf + * relocation prefix. + */ + internal fun forEachRelocation(action: (from: String, to: String, excludes: List) -> Unit) { + relocations.forEach { action(it.from, "${Constants.RELOCATION_PREFIX}.${it.to}", it.excludes) } + } + fun addRelocationsForDependency( dependency: String, vararg relocations: Pair, diff --git a/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/minestom/MinestomSurfPlugin.kt b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/minestom/MinestomSurfPlugin.kt index 49c1ab676..6689b08f5 100644 --- a/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/minestom/MinestomSurfPlugin.kt +++ b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/minestom/MinestomSurfPlugin.kt @@ -1,21 +1,117 @@ package dev.slne.surf.api.gradle.platform.minestom +import dev.slne.surf.api.gradle.generators.GeneratePluginFile +import dev.slne.surf.api.gradle.generators.pluginfiles.MinestomPluginFile +import dev.slne.surf.api.gradle.generators.pluginfiles.dto.MinestomPluginFileDto import dev.slne.surf.api.gradle.platform.SurfApiPlatform +import dev.slne.surf.api.gradle.platform.common.CommonSurfPluginWithPluginFile import dev.slne.surf.api.gradle.platform.common.testing.SurfTestingConfigurer -import dev.slne.surf.api.gradle.platform.core.AbstractCoreSurfPlugin +import dev.slne.surf.api.gradle.util.minestomSnapshots import org.gradle.api.Project +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.kotlin.dsl.create import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.invoke +import org.gradle.kotlin.dsl.register +import org.gradle.kotlin.dsl.repositories +import org.gradle.kotlin.dsl.withType + +/** + * A plugin for a surf Minestom server, loaded from its `plugins` directory or shaded into it. + * + * Generates the plugin's `minestom-plugin.json` from `minestomPluginFile { }`, together with the + * class path index the server finds shaded plugins through. + */ +internal class MinestomSurfPlugin : + CommonSurfPluginWithPluginFile( + "minestom", + SurfApiPlatform.MINESTOM, + PLUGIN_FILE_NAME, + ) { -internal class MinestomSurfPlugin : AbstractCoreSurfPlugin( - platformName = "minestom", - platform = SurfApiPlatform.MINESTOM, -) { init { - "it.unimi.dsi.fastutil" relocatesTo "fastutil" - "me.devnatan.inventoryframework" relocatesTo "devnatan.inventoryframework" + applyMinestomRelocations() } override val extensionClass = MinestomSurfExtension::class.java + override val dtoSerializer = MinestomPluginFileDto.serializer() + + override fun Project.applyRepositories0() { + repositories { minestomSnapshots() } + } + + override fun apply(target: Project) { + super.apply(target) + target.registerClassPathIndex() + } + + override fun createPluginFile(project: Project) = + project.extensions.create("minestomPluginFile").apply { + id.convention(project.name.lowercase()) + name.convention(project.provider { project.name }) + version.convention(project.provider { project.version.toString() }) + description.convention(project.provider { project.description ?: "" }) + url.convention(project.providers.gradleProperty("url")) + + val ext = project.extensions.getByType(MinestomSurfExtension::class.java) + authors.convention(ext.authors) + + val coreEnabled = ext.coreModule.map { true }.orElse(false) + + pluginDependencies { + register("surf-core-minestom") { + optional.convention(false) + enabled.convention(coreEnabled) + } + } + } + + override fun createPluginFileDto(pluginFile: MinestomPluginFile) = + MinestomPluginFileDto.fromFile(pluginFile) + + /** + * Writes the files a server reads plugins on its own class path from: a service entry with + * the main class, which survives when several plugins are shaded into one jar, and a copy of + * the plugin file named after it. + */ + private fun Project.registerClassPathIndex() { + val pluginFile = extensions.getByType(MinestomPluginFile::class.java) + val outputDirectory = layout.buildDirectory.dir("generated/surf-api/minestom-classpath") + + val applied = provider { pluginFile.isApplied() } + val mainClass = pluginFile.main.orElse("") + + val generateIndex = tasks.register("generateMinestomPluginIndex") { + group = "surf-api" + fileName.set(CLASSPATH_INDEX) + outputDir.set(outputDirectory.map { it.dir("index") }) + pluginFileJson.set(applied.zip(mainClass) { isApplied, main -> if (isApplied) main else "" }) + } + + val generateCopy = tasks.register("generateMinestomPluginClassPathFile") { + group = "surf-api" + fileName.set(mainClass.map { main -> "$CLASSPATH_DIRECTORY$main.json" }) + outputDir.set(outputDirectory.map { it.dir("descriptor") }) + pluginFileJson.set(provider { + if (!pluginFile.isApplied()) return@provider "" + pluginFile.validate() + GeneratePluginFile.json.encodeToString( + MinestomPluginFileDto.serializer(), + MinestomPluginFileDto.fromFile(pluginFile), + ) + }) + } + + plugins.withType { + extensions.getByType().named(SourceSet.MAIN_SOURCE_SET_NAME) { + resources.srcDir(generateIndex) + resources.srcDir(generateCopy) + } + } + } override fun Project.platformTestDependencies(extension: MinestomSurfExtension) { val testing = extension.testing @@ -27,4 +123,22 @@ internal class MinestomSurfPlugin : AbstractCoreSurfPlugin.applyMinestomRelocations() { + applyRelocation("com.mojang.serialization", "mojang.serialization") + applyRelocation("com.mojang.datafixers", "mojang.datafixers") + applyRelocation("net.kyori.adventure.nbt", "kyori.nbt", listOf("net.kyori.adventure.nbt.api.**")) + applyRelocation("it.unimi.dsi.fastutil", "fastutil") + applyRelocation("me.devnatan.inventoryframework", "devnatan.inventoryframework") } diff --git a/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/minestom/server/MinestomServerSurfExtension.kt b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/minestom/server/MinestomServerSurfExtension.kt new file mode 100644 index 000000000..1c1381154 --- /dev/null +++ b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/minestom/server/MinestomServerSurfExtension.kt @@ -0,0 +1,137 @@ +package dev.slne.surf.api.gradle.platform.minestom.server + +import dev.slne.surf.api.gradle.generators.pluginfiles.MinestomPluginFile +import dev.slne.surf.api.gradle.platform.core.CoreSurfExtension +import org.gradle.api.Action +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.model.ObjectFactory +import org.gradle.kotlin.dsl.domainObjectContainer +import org.gradle.kotlin.dsl.property +import org.gradle.kotlin.dsl.setProperty +import javax.inject.Inject + +/** + * An optional part of the surf Minestom server, shipped as a module of its own. + * + * @property agentClass the delegate agent the bootstrap calls for the feature on startup, if any + * @property mixinConfig the mixin configuration the feature's module ships, if any + */ +enum class MinestomServerFeature( + val module: String, + internal val agentClass: String? = null, + internal val mixinConfig: String? = null, +) { + SIGNED_CHAT("surf-api-minestom-server-signed-chat"), + LUCKPERMS( + "surf-api-minestom-server-luckperms", + "me.lucko.luckperms.minestom.dependencies.LuckPermsAgent", + ), + SPARK("surf-api-minestom-server-spark"), + NPC("surf-api-minestom-server-npc", mixinConfig = "mixins.surf-api-minestom-server-npc.json"), + PLAYER_VISIBILITY("surf-api-minestom-server-player-visibility"), + CONSOLE("surf-api-minestom-server-console"), + PLUGINS("surf-api-minestom-server-plugins"), +} + +/** + * Configures the build of a surf Minestom server. + * + * ``` + * surfMinestomServerApi { + * mainClass("dev.slne.surf.lobby.server.MainKt") + * + * withSignedChat() + * withLuckPerms() + * withConsole() + * withPlugins() + * + * pluginDependencies { + * register("surf-core-minestom") + * } + * } + * ``` + * + * Calling `withXxx()` only puts the feature's module on the class path; the server still installs + * it in `surfMinestomServer { }`. + * + * By default, the server jar only contains the project's own code and the surf modules. Minestom + * and every other third-party library are downloaded into `libraries/` when the server starts, see + * [downloadLibraries]. + */ +open class MinestomServerSurfExtension @Inject constructor(objects: ObjectFactory) : + CoreSurfExtension(objects) { + + internal val mainClass = objects.property() + internal val features = objects.setProperty() + internal val mixinConfigs = objects.setProperty() + + /** + * Whether third-party libraries are downloaded with gremlin when the server starts instead of + * being shaded into the server jar. They are fetched from the project's HTTP(S) Maven + * repositories, so libraries that only exist in `mavenLocal()` or need credentials have to be + * shaded. Surf modules (`dev.slne.surf*`) are always shaded. + */ + val downloadLibraries = objects.property().convention(true) + + /** + * The plugins from the `plugins` directory the server's own code uses, by ID. + * + * The bootstrap of the server jar puts these plugins, and the plugins they depend on, on the + * system class path instead of giving each a class loader of its own. The server's code can + * then use their classes; add them as `compileOnly` dependencies to compile against them. The + * server does not start when a plugin that is not `optional` is missing. + */ + val pluginDependencies: NamedDomainObjectContainer = + objects.domainObjectContainer(MinestomPluginFile.Dependency::class) + + fun pluginDependencies(action: Action>) { + action.execute(pluginDependencies) + } + + /** + * The directory `runServer` starts the server in, relative to the project. + */ + val runDirectory = objects.property().convention("run") + + /** + * The class holding the server's `main` function, e.g. `dev.slne.example.server.MainKt`. + */ + fun mainClass(value: String) { + mainClass.set(value) + } + + /** + * Applies the mixin configuration [name], a resource of the server's own code such as + * `mixins.surf-lobby.json`, in addition to the ones of surf. + * + * Every mixin configuration needs a mixin package of its own. Mixin is installed by the + * bootstrap of the server jar, so the server must not bootstrap Mixin itself. + */ + fun mixinConfig(name: String) { + mixinConfigs.add(name) + } + + fun feature(feature: MinestomServerFeature) { + features.add(feature) + } + + fun withSignedChat() = feature(MinestomServerFeature.SIGNED_CHAT) + + /** + * Also registers the LuckPerms agent as a delegate agent, which LuckPerms needs to load its + * libraries. + */ + fun withLuckPerms() = feature(MinestomServerFeature.LUCKPERMS) + fun withSpark() = feature(MinestomServerFeature.SPARK) + fun withNpcLib() = feature(MinestomServerFeature.NPC) + fun withPlayerVisibility() = feature(MinestomServerFeature.PLAYER_VISIBILITY) + fun withConsole() = feature(MinestomServerFeature.CONSOLE) + fun withPlugins() = feature(MinestomServerFeature.PLUGINS) + + /** + * Puts the module of every feature on the class path. + */ + fun withAllFeatures() { + MinestomServerFeature.entries.forEach(::feature) + } +} diff --git a/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/minestom/server/MinestomServerSurfPlugin.kt b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/minestom/server/MinestomServerSurfPlugin.kt new file mode 100644 index 000000000..2aa6bf26e --- /dev/null +++ b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/minestom/server/MinestomServerSurfPlugin.kt @@ -0,0 +1,338 @@ +package dev.slne.surf.api.gradle.platform.minestom.server + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer +import dev.slne.surf.api.gradle.generated.Constants +import dev.slne.surf.api.gradle.generators.GeneratePluginFile +import dev.slne.surf.api.gradle.platform.SurfApiPlatform +import dev.slne.surf.api.gradle.platform.common.testing.SurfTestingConfigurer +import dev.slne.surf.api.gradle.platform.core.AbstractCoreSurfPlugin +import dev.slne.surf.api.gradle.util.minestomSnapshots +import org.gradle.api.Project +import org.gradle.api.artifacts.ResolvedDependency +import org.gradle.api.artifacts.component.ModuleComponentIdentifier +import org.gradle.api.artifacts.component.ProjectComponentIdentifier +import org.gradle.api.file.DuplicatesStrategy +import org.gradle.api.plugins.JavaApplication +import org.gradle.api.tasks.Exec +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.jvm.toolchain.JavaToolchainService +import org.gradle.kotlin.dsl.* +import org.gradle.language.jvm.tasks.ProcessResources +import org.gradle.process.CommandLineArgumentProvider +import xyz.jpenilla.gremlin.gradle.GremlinExtension +import xyz.jpenilla.gremlin.gradle.WriteDependencySet + +/** + * Builds a runnable surf Minestom server as one executable server jar: the surf api, the + * requested features and the project's own code. + * + * Only the project's own modules, the surf modules and the bootstrap are shaded. Every other + * library is listed in the gremlin dependency set `dependencies.txt`. The bootstrap agent + * downloads these libraries into `libraries/`, relocates them like the server jar and appends + * them to the system class path before `main` runs. + * + * Manifest attributes written to the jar: + * - `Launcher-Agent-Class`: the bootstrap agent. + * - `Surf-Delegate-Agent-Classes`: delegate agents the bootstrap agent calls after the libraries + * are installed. `SurfMixinAgent` is always first so its transformer exists before any other + * agent loads a Minestom class. + * - `Surf-Mixin-Configs`: mixin configurations of surf, the features and the project. + * - `Surf-Plugin-Dependencies`: plugin IDs from `pluginDependencies`, which the bootstrap puts on + * the system class path. + * + * The server jar applies the same relocations as the `dev.slne.surf.api.gradle.minestom` Gradle + * plugin. Plugins built with that plugin therefore refer to the packages the server actually + * contains, whether they are dropped into `plugins/` or shaded in through a project dependency. + */ +internal class MinestomServerSurfPlugin : + AbstractCoreSurfPlugin("minestomServer", SurfApiPlatform.MINESTOM_SERVER) { + + init { + // The core relocations (mojang, adventure nbt, configurate) come from the base class + "it.unimi.dsi.fastutil" relocatesTo "fastutil" + "me.devnatan.inventoryframework" relocatesTo "devnatan.inventoryframework" + } + + override val extensionClass = MinestomServerSurfExtension::class.java + + override fun Project.applyRepositories0() { + repositories { minestomSnapshots() } + } + + override fun Project.applyPlugins0() { + applyPlugin("org.gradle.application") + applyPlugin("xyz.jpenilla.gremlin-gradle") + } + + override fun Project.configure0() { + dependencies { + add("implementation", Constants.MINESTOM) + add("runtimeOnly", "org.apache.logging.log4j:log4j-core:${Constants.LOG4J_VERSION}") + add("runtimeOnly", "org.apache.logging.log4j:log4j-slf4j2-impl:${Constants.LOG4J_VERSION}") + } + + tasks.named("shadowJar") { + // log4j-core and its add-ons each ship a Log4j plugin cache; all must reach the + // transformer to be merged rather than dropped as duplicates + duplicatesStrategy = DuplicatesStrategy.INCLUDE + mergeServiceFiles() + transform(Log4j2PluginsCacheFileTransformer::class.java) + manifest.attributes["Multi-Release"] = "true" + } + + extensions.configure { + // The bootstrap module brings the gremlin runtime, which is shaded instead of downloaded + defaultGremlinRuntimeDependency.set(false) + // gremlin's default ASM cannot read the class files of current Java versions + defaultJarRelocatorDependencies.set(false) + } + dependencies { + add(JAR_RELOCATOR_RUNTIME, "me.lucko:jar-relocator:1.7") + add(JAR_RELOCATOR_RUNTIME, "org.ow2.asm:asm:${Constants.ASM_VERSION}") + add(JAR_RELOCATOR_RUNTIME, "org.ow2.asm:asm-commons:${Constants.ASM_VERSION}") + } + } + + override fun Project.afterEvaluated1(extension: MinestomServerSurfExtension) { + val features = extension.features.get() + + dependencies { + features.forEach { feature -> + add("implementation", "dev.slne.surf.api:${feature.module}:${Constants.SURF_API_VERSION}") + } + } + + val mainClass = extension.mainClass.orNull + if (mainClass == null) { + logger.warn("No main class set for the Minestom server ${project.path}; call mainClass(...) in surfMinestomServerApi { }") + } else { + configure { this.mainClass.set(mainClass) } + } + + dependencies { + add("implementation", "dev.slne.surf.api:$BOOTSTRAP_MODULE:${Constants.SURF_API_VERSION}") + } + + // Mixin goes first: its transformer must be installed before another agent loads a Minestom class + val agents = listOf(MIXIN_AGENT) + features.mapNotNull(MinestomServerFeature::agentClass) + val mixinConfigs = listOf(MIXIN_CONFIG) + + features.mapNotNull(MinestomServerFeature::mixinConfig) + + extension.mixinConfigs.get() + val pluginDependencies = extension.pluginDependencies + .filter { it.enabled.get() } + .map { if (it.optional.get()) "${it.name}?" else it.name } + tasks.named("shadowJar") { + if (mainClass != null) manifest.attributes["Main-Class"] = mainClass + manifest.attributes["Launcher-Agent-Class"] = BOOTSTRAP_AGENT + manifest.attributes[DELEGATE_AGENTS_ATTRIBUTE] = agents.joinToString(",") + manifest.attributes[MIXIN_CONFIGS_ATTRIBUTE] = mixinConfigs.distinct().joinToString(",") + if (pluginDependencies.isNotEmpty()) { + manifest.attributes[PLUGIN_DEPENDENCIES_ATTRIBUTE] = pluginDependencies.joinToString(",") + } + } + + if (extension.downloadLibraries.get()) { + configureLibraryDownload(extension) + } else { + tasks.named("processResources") { exclude(DEPENDENCIES_FILE) } + configureShadedAsmVersion() + } + + registerRunServer(extension) + registerDefaultLoggingConfiguration(console = MinestomServerFeature.CONSOLE in features) + } + + /** + * Generates a default `log4j2.xml` (console and rolling file) unless the project ships its own. + * Without a configuration, Log4j logs errors only. When the console feature is installed, the + * console appender is the terminal console. + */ + private fun Project.registerDefaultLoggingConfiguration(console: Boolean) { + val ownConfiguration = layout.projectDirectory.file("src/main/resources/$LOG4J_CONFIG").asFile + if (ownConfiguration.exists()) return + + val appender = if (console) { + """ + | + | """.trimMargin() + } else { + """ + | + | """.trimMargin() + } + + val configuration = """ + | + | + | + | + | $appender + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + |""".trimMargin() + + val outputDirectory = layout.buildDirectory.dir("generated/surf-api/minestom-server-logging") + val generate = tasks.register("generateDefaultLoggingConfiguration") { + group = "surf-api" + fileName.set(LOG4J_CONFIG) + outputDir.set(outputDirectory) + pluginFileJson.set(configuration) + } + + extensions.getByType().named(SourceSet.MAIN_SOURCE_SET_NAME) { + resources.srcDir(generate) + } + } + + /** + * Writes every third-party library, with the server jar's relocations, to `dependencies.txt`, + * the file the bootstrap downloads from. The project's own modules, the surf modules and the + * bootstrap stay shaded in the server jar. + */ + private fun Project.configureLibraryDownload(extension: MinestomServerSurfExtension) { + configurations.named(RUNTIME_DOWNLOAD) { + extendsFrom(configurations.getByName("implementation"), configurations.getByName("runtimeOnly")) + shouldResolveConsistentlyWith(configurations.getByName("runtimeClasspath")) + } + + tasks.named("writeDependencies") { + // Filtered per component, so that the libraries of shaded modules are still downloaded + dependencies.setFrom(configurations.named(RUNTIME_DOWNLOAD).map { configuration -> + configuration.incoming.artifactView { + componentFilter { id -> id !is ModuleComponentIdentifier || !isShaded(id.group, id.module) } + }.artifacts + }) + forEachRelocation { from, to, excludes -> + relocate(from, to) { this.excludes.set(excludes) } + } + if (extension.withSurfRedis.get()) { + extension.surfRedisRelocation.orNull?.let { relocate("dev.slne.surf.redis", it) } + } + if (extension.withSurfDatabaseR2dbc.get()) { + extension.surfDatabaseR2dbcRelocation.orNull?.let { relocate("dev.slne.surf.database", it) } + } + } + + tasks.named("shadowJar") { + dependencies { + exclude { dependency -> !dependency.isShaded() } + } + } + } + + /** + * Writes the ASM version into the `org/objectweb/asm/` manifest section. Mixin reads the + * version there, and the server jar lacks that section once ASM is shaded in. Applied only when + * the libraries are shaded instead of downloaded. + */ + private fun Project.configureShadedAsmVersion() { + val asmVersion = configurations.named("runtimeClasspath") + .flatMap { it.incoming.artifacts.resolvedArtifacts } + .map { artifacts -> + artifacts.asSequence() + .map { it.id.componentIdentifier } + .filterIsInstance() + .firstOrNull { it.group == "org.ow2.asm" && it.module == "asm" } + ?.version + .orEmpty() + } + tasks.named("shadowJar") { + doFirst { + val version = asmVersion.get() + if (version.isNotEmpty()) { + manifest.attributes(mapOf("Implementation-Version" to version), "org/objectweb/asm/") + } + } + } + } + + private fun ResolvedDependency.isShaded() = + isShaded(moduleGroup, moduleName) || + moduleArtifacts.any { it.id.componentIdentifier is ProjectComponentIdentifier } + + /** + * Whether the module stays in the server jar: surf modules (not the third-party forks under + * `dev.slne.forks`) and the [BOOTSTRAP_MODULES]. + */ + private fun isShaded(group: String, module: String) = + group == "dev.slne.surf" || group.startsWith("dev.slne.surf.") || group to module in BOOTSTRAP_MODULES + + /** + * Registers `runServer`, which runs the server jar with `java -jar` so that the manifest's + * agents apply. + */ + private fun Project.registerRunServer(extension: MinestomServerSurfExtension) { + val shadowJar = tasks.named("shadowJar") + val runDirectory = layout.projectDirectory.dir(extension.runDirectory.get()).asFile + val launcher = extensions.getByType().launcherFor { + languageVersion.set(JavaLanguageVersion.of(Constants.JAVA_VERSION)) + } + + tasks.register("runServer") { + group = "surf-api" + description = "Builds the server jar and runs it in ${extension.runDirectory.get()}/" + dependsOn(shadowJar) + + workingDir = runDirectory + standardInput = System.`in` + doFirst { + runDirectory.mkdirs() + executable = launcher.get().executablePath.asFile.absolutePath + } + argumentProviders += CommandLineArgumentProvider { + listOf("-jar", shadowJar.get().archiveFile.get().asFile.absolutePath) + } + } + } + + private companion object { + const val LOG4J_CONFIG = "log4j2.xml" + + const val RUNTIME_DOWNLOAD = "runtimeDownload" + const val JAR_RELOCATOR_RUNTIME = "jarRelocatorRuntime" + const val DEPENDENCIES_FILE = "dependencies.txt" + const val BOOTSTRAP_MODULE = "surf-api-minestom-server-bootstrap" + const val BOOTSTRAP_AGENT = "dev.slne.surf.api.minestom.server.bootstrap.SurfMinestomBootstrap" + const val DELEGATE_AGENTS_ATTRIBUTE = "Surf-Delegate-Agent-Classes" + const val PLUGIN_DEPENDENCIES_ATTRIBUTE = "Surf-Plugin-Dependencies" + const val MIXIN_AGENT = "dev.slne.surf.api.minestom.server.impl.mixin.SurfMixinAgent" + const val MIXIN_CONFIGS_ATTRIBUTE = "Surf-Mixin-Configs" + const val MIXIN_CONFIG = "mixins.surf-api-minestom-server.json" + + /** + * What the bootstrap needs before anything is downloaded. + */ + val BOOTSTRAP_MODULES = setOf( + "xyz.jpenilla" to "gremlin-runtime", + "org.jspecify" to "jspecify", + ) + } + + override fun Project.platformTestDependencies(extension: MinestomServerSurfExtension) { + val testing = extension.testing + if (!testing.minestomTesting.get()) return + dependencies { + add( + SurfTestingConfigurer.TEST_IMPLEMENTATION, + "net.minestom:testing:${testing.minestomTestingVersion.get()}" + ) + } + } +} diff --git a/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/util/repo-extensions.kt b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/util/repo-extensions.kt index 8974cc333..4bbe7bc8d 100644 --- a/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/util/repo-extensions.kt +++ b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/util/repo-extensions.kt @@ -22,6 +22,16 @@ inline fun RepositoryHandler.canvasMaven(crossinline block: MavenArtifactReposit block() } +inline fun RepositoryHandler.minestomSnapshots(crossinline block: MavenArtifactRepository.() -> Unit = {}) = + maven("https://central.sonatype.com/repository/maven-snapshots/") { + name = "minestom-snapshots" + mavenContent { + snapshotsOnly() + includeGroup("net.minestom") + } + block() + } + inline fun RepositoryHandler.slneReleases(crossinline block: MavenArtifactRepository.() -> Unit = {}) = maven("https://reposilite.slne.dev/releases/") { name = "slne-repository-releases" diff --git a/surf-api-minestom/api/surf-api-minestom.api b/surf-api-minestom/api/surf-api-minestom.api deleted file mode 100644 index 833f970e3..000000000 --- a/surf-api-minestom/api/surf-api-minestom.api +++ /dev/null @@ -1,1560 +0,0 @@ -public final class dev/slne/surf/api/minestom/builder/ItemDsl { - public fun (Lnet/minestom/server/item/Material;I)V - public synthetic fun (Lnet/minestom/server/item/Material;IILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun amount (I)V - public final fun build ()Lnet/minestom/server/item/ItemStack; - public final fun displayName (Lkotlin/jvm/functions/Function1;)V - public final fun displayName (Lnet/kyori/adventure/text/Component;)V - public final fun getBuilder ()Lnet/minestom/server/item/ItemStack$Builder; - public final fun itemModel (Ljava/lang/String;)V - public final fun itemModel (Lnet/kyori/adventure/key/Key;)V - public final fun lore (Lkotlin/jvm/functions/Function1;)V - public final fun lore ([Lnet/kyori/adventure/text/Component;)V -} - -public final class dev/slne/surf/api/minestom/builder/ItemDslKt { - public static final fun buildItem (Lnet/minestom/server/item/Material;ILkotlin/jvm/functions/Function1;)Lnet/minestom/server/item/ItemStack; - public static synthetic fun buildItem$default (Lnet/minestom/server/item/Material;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/minestom/server/item/ItemStack; -} - -public abstract interface annotation class dev/slne/surf/api/minestom/builder/ItemDslMarker : java/lang/annotation/Annotation { -} - -public class dev/slne/surf/api/minestom/command/args/MiniMessageArgument : dev/slne/minestom/lobby/api/command/commandapi/argument/CustomArgument { - public fun (Ljava/lang/String;Z)V - public synthetic fun (Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V -} - -public final class dev/slne/surf/api/minestom/command/args/MiniMessageArgumentKt { - public static final fun miniMessageArgument (Ldev/slne/minestom/lobby/api/command/commandapi/CommandAPICommand;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Ldev/slne/minestom/lobby/api/command/commandapi/CommandAPICommand; - public static final fun miniMessageArgument (Ldev/slne/minestom/lobby/api/command/commandapi/CommandTree;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Ldev/slne/minestom/lobby/api/command/commandapi/CommandTree; - public static final fun miniMessageArgument (Ldev/slne/minestom/lobby/api/command/commandapi/argument/Argument;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Ldev/slne/minestom/lobby/api/command/commandapi/argument/Argument; - public static synthetic fun miniMessageArgument$default (Ldev/slne/minestom/lobby/api/command/commandapi/CommandAPICommand;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/minestom/lobby/api/command/commandapi/CommandAPICommand; - public static synthetic fun miniMessageArgument$default (Ldev/slne/minestom/lobby/api/command/commandapi/CommandTree;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/minestom/lobby/api/command/commandapi/CommandTree; - public static synthetic fun miniMessageArgument$default (Ldev/slne/minestom/lobby/api/command/commandapi/argument/Argument;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/minestom/lobby/api/command/commandapi/argument/Argument; -} - -public final class dev/slne/surf/api/minestom/dialog/CommonDialogsKt { - public static final fun noticeDialog (Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/dialog/Dialog; - public static final fun noticeDialog (Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;Ljava/lang/Integer;)Lnet/minestom/server/dialog/Dialog; - public static synthetic fun noticeDialog$default (Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;Ljava/lang/Integer;ILjava/lang/Object;)Lnet/minestom/server/dialog/Dialog; - public static final fun noticeDialogWithBuilder (Lnet/kyori/adventure/text/Component;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/dialog/Dialog; - public static synthetic fun noticeDialogWithBuilder$default (Lnet/kyori/adventure/text/Component;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/minestom/server/dialog/Dialog; -} - -public final class dev/slne/surf/api/minestom/dialog/DialogBuilderKt { - public static final fun base (Ldev/slne/surf/api/minestom/dialog/builder/DialogEntryBuilder;Lkotlin/jvm/functions/Function1;)V - public static final fun dialog (Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/dialog/Dialog; - public static final fun type (Ldev/slne/surf/api/minestom/dialog/builder/DialogEntryBuilder;Lkotlin/jvm/functions/Function1;)V -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder { - public fun ()V - public final fun action (Lnet/minestom/server/dialog/DialogAction;)V - public final fun callback (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lkotlin/jvm/functions/Function1;)V - public static synthetic fun callback$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder;Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public final fun changePage (I)V - public final fun commandTemplate (Ljava/lang/String;)V - public final fun copyToClipboard (Ljava/lang/String;)V - public final fun customClick (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lnet/kyori/adventure/nbt/CompoundBinaryTag;Lkotlin/jvm/functions/Function2;)V - public final fun customClick (Lnet/kyori/adventure/key/Key;Lkotlin/jvm/functions/Function1;)V - public final fun customClick (Lnet/kyori/adventure/key/Key;Lnet/kyori/adventure/nbt/BinaryTag;)V - public static synthetic fun customClick$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder;Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lnet/kyori/adventure/nbt/CompoundBinaryTag;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V - public static synthetic fun customClick$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder;Lnet/kyori/adventure/key/Key;Lnet/kyori/adventure/nbt/BinaryTag;ILjava/lang/Object;)V - public final fun customPlayerClick (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lnet/kyori/adventure/nbt/CompoundBinaryTag;Lkotlin/jvm/functions/Function2;)V - public static synthetic fun customPlayerClick$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder;Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lnet/kyori/adventure/nbt/CompoundBinaryTag;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V - public final fun dynamicCustomClick (Lnet/kyori/adventure/key/Key;Lnet/kyori/adventure/nbt/CompoundBinaryTag;)V - public static synthetic fun dynamicCustomClick$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder;Lnet/kyori/adventure/key/Key;Lnet/kyori/adventure/nbt/CompoundBinaryTag;ILjava/lang/Object;)V - public final fun openUrl (Ljava/lang/String;)V - public final fun openUrl (Ljava/net/URL;)V - public final fun playerCallback (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lkotlin/jvm/functions/Function1;)V - public static synthetic fun playerCallback$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder;Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public final fun runCommand (Ljava/lang/String;)V - public final fun showDialog (Lkotlin/jvm/functions/Function1;)V - public final fun showDialog (Lnet/minestom/server/dialog/Dialog;)V - public final fun suggestCommand (Ljava/lang/String;)V -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogActionBuilderKt { - public static final fun dialogAction (Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/dialog/DialogAction; -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogActionButtonBuilder { - public fun ()V - public final fun action (Lkotlin/jvm/functions/Function1;)V - public final fun action (Lnet/minestom/server/dialog/DialogAction;)V - public final fun getAction ()Lnet/minestom/server/dialog/DialogAction; - public final fun getLabel ()Lnet/kyori/adventure/text/Component; - public final fun getTooltip ()Lnet/kyori/adventure/text/Component; - public final fun getWidth ()I - public final fun label (Lkotlin/jvm/functions/Function1;)V - public final fun label (Lnet/kyori/adventure/text/Component;)V - public final fun setAction (Lnet/minestom/server/dialog/DialogAction;)V - public final fun setLabel (Lnet/kyori/adventure/text/Component;)V - public final fun setTooltip (Lnet/kyori/adventure/text/Component;)V - public final fun setWidth (I)V - public final fun tooltip (Lkotlin/jvm/functions/Function1;)V - public final fun tooltip (Lnet/kyori/adventure/text/Component;)V - public final fun width (I)V -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogActionButtonBuilderKt { - public static final fun actionButton (Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/dialog/DialogActionButton; -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogBaseBuilder { - public fun ()V - public final fun afterAction (Lnet/minestom/server/dialog/DialogAfterAction;)V - public final fun body (Lkotlin/jvm/functions/Function1;)V - public final fun canCloseWithEscape (Z)V - public final fun externalTitle (Lkotlin/jvm/functions/Function1;)V - public final fun externalTitle (Lnet/kyori/adventure/text/Component;)V - public final fun getAfterAction ()Lnet/minestom/server/dialog/DialogAfterAction; - public final fun getCanCloseWithEscape ()Z - public final fun getExternalTitle ()Lnet/kyori/adventure/text/Component; - public final fun getPause ()Z - public final fun getTitle ()Lnet/kyori/adventure/text/Component; - public final fun input (Lkotlin/jvm/functions/Function1;)V - public final fun pause (Z)V - public final fun preventClosingWithEscape (Z)V - public static synthetic fun preventClosingWithEscape$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogBaseBuilder;ZILjava/lang/Object;)V - public final fun setAfterAction (Lnet/minestom/server/dialog/DialogAfterAction;)V - public final fun setCanCloseWithEscape (Z)V - public final fun setExternalTitle (Lnet/kyori/adventure/text/Component;)V - public final fun setPause (Z)V - public final fun setTitle (Lnet/kyori/adventure/text/Component;)V - public final fun title (Lkotlin/jvm/functions/Function1;)V - public final fun title (Lnet/kyori/adventure/text/Component;)V -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogBaseBuilderKt { - public static final fun dialogBase (Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/dialog/DialogMetadata; -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder { - public fun ()V - public final fun item (Lkotlin/jvm/functions/Function1;)V - public final fun plain (Lkotlin/jvm/functions/Function1;)V - public final fun plainMessage (Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;)V - public final fun plainMessage (Lnet/kyori/adventure/text/Component;Ljava/lang/Integer;)V - public final fun plainMessage (Lnet/minestom/server/dialog/DialogBody$PlainMessage;)V - public static synthetic fun plainMessage$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public static synthetic fun plainMessage$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder;Lnet/kyori/adventure/text/Component;Ljava/lang/Integer;ILjava/lang/Object;)V -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder$ItemDialogMessageBuilder { - public fun ()V - public final fun description (Lkotlin/jvm/functions/Function1;)V - public final fun description (Lnet/minestom/server/dialog/DialogBody$PlainMessage;)V - public final fun getDescription ()Lnet/minestom/server/dialog/DialogBody$PlainMessage; - public final fun getHeight ()I - public final fun getItem ()Lnet/minestom/server/item/ItemStack; - public final fun getShowDecorations ()Z - public final fun getShowTooltip ()Z - public final fun getWidth ()I - public final fun height (I)V - public final fun item (Lnet/minestom/server/item/ItemStack;)V - public final fun item (Lnet/minestom/server/item/Material;I)V - public static synthetic fun item$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder$ItemDialogMessageBuilder;Lnet/minestom/server/item/Material;IILjava/lang/Object;)V - public final fun setDescription (Lnet/minestom/server/dialog/DialogBody$PlainMessage;)V - public final fun setHeight (I)V - public final fun setItem (Lnet/minestom/server/item/ItemStack;)V - public final fun setShowDecorations (Z)V - public final fun setShowTooltip (Z)V - public final fun setWidth (I)V - public final fun showDecorations (Z)V - public final fun showTooltip (Z)V - public final fun simpleDescription (Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;)V - public final fun simpleDescription (Lnet/kyori/adventure/text/Component;Ljava/lang/Integer;)V - public static synthetic fun simpleDescription$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder$ItemDialogMessageBuilder;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public static synthetic fun simpleDescription$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder$ItemDialogMessageBuilder;Lnet/kyori/adventure/text/Component;Ljava/lang/Integer;ILjava/lang/Object;)V - public final fun width (I)V -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder$PlainDialogMessageBuilder { - public fun ()V - public final fun getMessage ()Lnet/kyori/adventure/text/Component; - public final fun getWidth ()Ljava/lang/Integer; - public final fun message (Lkotlin/jvm/functions/Function1;)V - public final fun message (Lnet/kyori/adventure/text/Component;)V - public final fun setMessage (Lnet/kyori/adventure/text/Component;)V - public final fun setWidth (Ljava/lang/Integer;)V - public final fun width (I)V -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilderKt { - public static final fun dialogBody (Lkotlin/jvm/functions/Function1;)Lit/unimi/dsi/fastutil/objects/ObjectList; -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogEntryBuilder { - public fun ()V - public final fun base (Ldev/slne/surf/api/minestom/dialog/builder/DialogBaseBuilder;)V - public final fun type (Ldev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder;)V -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder { - public fun ()V - public final fun boolean (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V - public final fun input (Lnet/minestom/server/dialog/DialogInput;)V - public final fun numberRange (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V - public final fun numberRange (Ljava/lang/String;Lkotlin/ranges/ClosedRange;Lkotlin/jvm/functions/Function1;)V - public final fun singleOption (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V - public final fun text (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$BooleanInputBuilder : dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$InputBuilder { - public fun (Ljava/lang/String;)V - public final fun getInitial ()Z - public final fun getOnFalse ()Ljava/lang/String; - public final fun getOnTrue ()Ljava/lang/String; - public final fun initial (Z)V - public final fun onFalse (Ljava/lang/String;)V - public final fun onTrue (Ljava/lang/String;)V - public final fun setInitial (Z)V - public final fun setOnFalse (Ljava/lang/String;)V - public final fun setOnTrue (Ljava/lang/String;)V -} - -public abstract class dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$InputBuilder { - public fun (Ljava/lang/String;)V - protected final fun getKey ()Ljava/lang/String; - public final fun getLabel ()Lnet/kyori/adventure/text/Component; - public final fun label (Lkotlin/jvm/functions/Function1;)V - public final fun label (Lnet/kyori/adventure/text/Component;)V - protected final fun requireLabel ()Lnet/kyori/adventure/text/Component; - public final fun setLabel (Lnet/kyori/adventure/text/Component;)V -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$NumberRangeInputBuilder : dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$InputBuilder { - public fun (Ljava/lang/String;)V - public final fun getEnd ()Ljava/lang/Float; - public final fun getInitial ()Ljava/lang/Float; - public final fun getLabelFormat ()Ljava/lang/String; - public final fun getStart ()Ljava/lang/Float; - public final fun getStep ()Ljava/lang/Float; - public final fun getWidth ()I - public final fun initial (F)V - public final fun labelFormat (Ljava/lang/String;)V - public final fun range (FF)V - public final fun setEnd (Ljava/lang/Float;)V - public final fun setInitial (Ljava/lang/Float;)V - public final fun setLabelFormat (Ljava/lang/String;)V - public final fun setStart (Ljava/lang/Float;)V - public final fun setStep (Ljava/lang/Float;)V - public final fun setWidth (I)V - public final fun step (F)V - public final fun width (I)V -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$SingleOptionInputBuilder : dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$InputBuilder { - public fun (Ljava/lang/String;)V - public final fun getLabelVisible ()Z - public final fun getWidth ()I - public final fun labelVisible (Z)V - public final fun option (Ljava/lang/String;Lnet/kyori/adventure/text/Component;Z)V - public final fun option (Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)V - public static synthetic fun option$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$SingleOptionInputBuilder;Ljava/lang/String;Lnet/kyori/adventure/text/Component;ZILjava/lang/Object;)V - public static synthetic fun option$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$SingleOptionInputBuilder;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public final fun setLabelVisible (Z)V - public final fun setWidth (I)V - public final fun width (I)V -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$TextInputBuilder : dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$InputBuilder { - public fun (Ljava/lang/String;)V - public final fun getInitial ()Ljava/lang/String; - public final fun getLabelVisible ()Z - public final fun getMaxLength ()I - public final fun getWidth ()I - public final fun initial (Ljava/lang/String;)V - public final fun labelVisible (Z)V - public final fun maxLength (I)V - public final fun multiline (Ljava/lang/Integer;Ljava/lang/Integer;)V - public static synthetic fun multiline$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$TextInputBuilder;Ljava/lang/Integer;Ljava/lang/Integer;ILjava/lang/Object;)V - public final fun setInitial (Ljava/lang/String;)V - public final fun setLabelVisible (Z)V - public final fun setMaxLength (I)V - public final fun setWidth (I)V - public final fun width (I)V -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilderKt { - public static final fun dialogInput (Lkotlin/jvm/functions/Function1;)Lit/unimi/dsi/fastutil/objects/ObjectList; -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder { - public fun ()V - public final fun confirmation (Lkotlin/jvm/functions/Function1;)V - public final fun confirmation (Lnet/minestom/server/dialog/DialogActionButton;Lnet/minestom/server/dialog/DialogActionButton;)V - public final fun dialogList (Ljava/util/Collection;Lkotlin/jvm/functions/Function1;)V - public final fun dialogList (Lkotlin/jvm/functions/Function1;)V - public final fun dialogList ([Lnet/minestom/server/dialog/Dialog;Lkotlin/jvm/functions/Function1;)V - public static synthetic fun dialogList$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder;Ljava/util/Collection;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public static synthetic fun dialogList$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder;[Lnet/minestom/server/dialog/Dialog;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public final fun multiAction (Ljava/util/List;Lkotlin/jvm/functions/Function1;)V - public final fun multiAction (Lkotlin/jvm/functions/Function1;)V - public final fun multiAction ([Lnet/minestom/server/dialog/DialogActionButton;Lkotlin/jvm/functions/Function1;)V - public static synthetic fun multiAction$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder;Ljava/util/List;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public static synthetic fun multiAction$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder;[Lnet/minestom/server/dialog/DialogActionButton;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public final fun notice ()V - public final fun notice (Lkotlin/jvm/functions/Function1;)V - public final fun notice (Lnet/minestom/server/dialog/DialogActionButton;)V - public final fun serverLinks (Lkotlin/jvm/functions/Function1;)V - public static synthetic fun serverLinks$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder$DialogConfirmationTypeBuilder { - public fun ()V - public final fun getNo ()Lnet/minestom/server/dialog/DialogActionButton; - public final fun getYes ()Lnet/minestom/server/dialog/DialogActionButton; - public final fun no (Lkotlin/jvm/functions/Function1;)V - public final fun no (Lnet/minestom/server/dialog/DialogActionButton;)V - public final fun setNo (Lnet/minestom/server/dialog/DialogActionButton;)V - public final fun setYes (Lnet/minestom/server/dialog/DialogActionButton;)V - public final fun yes (Lkotlin/jvm/functions/Function1;)V - public final fun yes (Lnet/minestom/server/dialog/DialogActionButton;)V -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder$DialogListTypeBuilder { - public fun ()V - public final fun addAll (Ljava/lang/Iterable;)V - public final fun addAll ([Lnet/minestom/server/dialog/Dialog;)V - public final fun buttonWidth (I)V - public final fun columns (I)V - public final fun dialog (Lnet/minestom/server/dialog/Dialog;)V - public final fun exitAction (Lkotlin/jvm/functions/Function1;)V - public final fun exitAction (Lnet/minestom/server/dialog/DialogActionButton;)V - public final fun getButtonWidth ()I - public final fun getColumns ()I - public final fun getExitAction ()Lnet/minestom/server/dialog/DialogActionButton; - public final fun setButtonWidth (I)V - public final fun setColumns (I)V - public final fun setExitAction (Lnet/minestom/server/dialog/DialogActionButton;)V -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder$DialogMultiActionTypeBuilder { - public fun ()V - public final fun action (Lkotlin/jvm/functions/Function1;)V - public final fun action (Lnet/minestom/server/dialog/DialogActionButton;)V - public final fun columns (I)V - public final fun exitAction (Lkotlin/jvm/functions/Function1;)V - public final fun exitAction (Lnet/minestom/server/dialog/DialogActionButton;)V - public final fun getColumns ()I - public final fun getExitAction ()Lnet/minestom/server/dialog/DialogActionButton; - public final fun setColumns (I)V - public final fun setExitAction (Lnet/minestom/server/dialog/DialogActionButton;)V -} - -public final class dev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder$ServerLinksTypeBuilder { - public fun ()V - public final fun buttonWidth (I)V - public final fun columns (I)V - public final fun exitAction (Lkotlin/jvm/functions/Function1;)V - public final fun exitAction (Lnet/minestom/server/dialog/DialogActionButton;)V - public final fun getButtonWidth ()I - public final fun getColumns ()I - public final fun getExitAction ()Lnet/minestom/server/dialog/DialogActionButton; - public final fun setButtonWidth (I)V - public final fun setColumns (I)V - public final fun setExitAction (Lnet/minestom/server/dialog/DialogActionButton;)V -} - -public final class dev/slne/surf/api/minestom/dialog/callback/DialogCallbackContext { - public fun (Lnet/minestom/server/entity/Player;Lnet/kyori/adventure/nbt/BinaryTag;)V - public final fun component1 ()Lnet/minestom/server/entity/Player; - public final fun component2 ()Lnet/kyori/adventure/nbt/BinaryTag; - public final fun copy (Lnet/minestom/server/entity/Player;Lnet/kyori/adventure/nbt/BinaryTag;)Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackContext; - public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackContext;Lnet/minestom/server/entity/Player;Lnet/kyori/adventure/nbt/BinaryTag;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackContext; - public fun equals (Ljava/lang/Object;)Z - public final fun getPayload ()Lnet/kyori/adventure/nbt/BinaryTag; - public final fun getPlayer ()Lnet/minestom/server/entity/Player; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class dev/slne/surf/api/minestom/dialog/callback/DialogCallbackListener : dev/slne/minestom/lobby/api/event/EventRegistrar { - public fun ()V - public fun register (Lnet/minestom/server/event/EventNode;)V -} - -public final class dev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions { - public static final field Companion Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions$Companion; - public static final field ONE_USE I - public static final field UNLIMITED_USES I - public fun ()V - public synthetic fun (IJILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (IJLkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()I - public final fun component2-UwyO8pc ()J - public final fun copy-HG0u8IE (IJ)Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions; - public static synthetic fun copy-HG0u8IE$default (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;IJILjava/lang/Object;)Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions; - public fun equals (Ljava/lang/Object;)Z - public final fun getLifetime-UwyO8pc ()J - public final fun getUses ()I - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class dev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions$Companion { - public final fun getDEFAULT ()Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions; - public final fun getDEFAULT_LIFETIME-UwyO8pc ()J -} - -public final class dev/slne/surf/api/minestom/dialog/callback/DialogCallbacks { - public static final field INSTANCE Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbacks; - public final fun action (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/dialog/DialogAction; - public static synthetic fun action$default (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbacks;Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/minestom/server/dialog/DialogAction; - public final fun dynamicAction (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lnet/kyori/adventure/nbt/CompoundBinaryTag;Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/dialog/DialogAction; - public static synthetic fun dynamicAction$default (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbacks;Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lnet/kyori/adventure/nbt/CompoundBinaryTag;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/minestom/server/dialog/DialogAction; - public final fun register (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lkotlin/jvm/functions/Function1;)Lnet/kyori/adventure/key/Key; - public static synthetic fun register$default (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbacks;Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/kyori/adventure/key/Key; - public final fun unregister (Lnet/kyori/adventure/key/Key;)Z -} - -public final class dev/slne/surf/api/minestom/dialog/callback/DialogResponseView { - public static final field Companion Ldev/slne/surf/api/minestom/dialog/callback/DialogResponseView$Companion; - public static final synthetic fun box-impl (Lnet/kyori/adventure/nbt/CompoundBinaryTag;)Ldev/slne/surf/api/minestom/dialog/callback/DialogResponseView; - public static fun constructor-impl (Lnet/kyori/adventure/nbt/CompoundBinaryTag;)Lnet/kyori/adventure/nbt/CompoundBinaryTag; - public fun equals (Ljava/lang/Object;)Z - public static fun equals-impl (Lnet/kyori/adventure/nbt/CompoundBinaryTag;Ljava/lang/Object;)Z - public static final fun equals-impl0 (Lnet/kyori/adventure/nbt/CompoundBinaryTag;Lnet/kyori/adventure/nbt/CompoundBinaryTag;)Z - public static final fun getBoolean-impl (Lnet/kyori/adventure/nbt/CompoundBinaryTag;Ljava/lang/String;)Ljava/lang/Boolean; - public static final fun getFloat-impl (Lnet/kyori/adventure/nbt/CompoundBinaryTag;Ljava/lang/String;)Ljava/lang/Float; - public final fun getPayload ()Lnet/kyori/adventure/nbt/CompoundBinaryTag; - public static final fun getText-impl (Lnet/kyori/adventure/nbt/CompoundBinaryTag;Ljava/lang/String;)Ljava/lang/String; - public fun hashCode ()I - public static fun hashCode-impl (Lnet/kyori/adventure/nbt/CompoundBinaryTag;)I - public fun toString ()Ljava/lang/String; - public static fun toString-impl (Lnet/kyori/adventure/nbt/CompoundBinaryTag;)Ljava/lang/String; - public final synthetic fun unbox-impl ()Lnet/kyori/adventure/nbt/CompoundBinaryTag; -} - -public final class dev/slne/surf/api/minestom/dialog/callback/DialogResponseView$Companion { - public final fun getEMPTY-v2-CkQY ()Lnet/kyori/adventure/nbt/CompoundBinaryTag; - public final fun of-VLTPHq8 (Lnet/kyori/adventure/nbt/BinaryTag;)Lnet/kyori/adventure/nbt/CompoundBinaryTag; -} - -public final class dev/slne/surf/api/minestom/dialog/search/SearchDialogAction { - public fun (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V - public final fun component1 ()Lkotlin/jvm/functions/Function1; - public final fun component2 ()Lkotlin/jvm/functions/Function1; - public final fun copy (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/dialog/search/SearchDialogAction; - public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/dialog/search/SearchDialogAction;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/dialog/search/SearchDialogAction; - public fun equals (Ljava/lang/Object;)Z - public final fun getLabel ()Lkotlin/jvm/functions/Function1; - public final fun getTooltip ()Lkotlin/jvm/functions/Function1; - public fun hashCode ()I - public final fun setLabel (Lkotlin/jvm/functions/Function1;)V - public final fun setTooltip (Lkotlin/jvm/functions/Function1;)V - public fun toString ()Ljava/lang/String; -} - -public final class dev/slne/surf/api/minestom/dialog/search/SearchDialogKt { - public static final fun searchDialog (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLnet/minestom/server/dialog/DialogAfterAction;)Lnet/minestom/server/dialog/Dialog; - public static synthetic fun searchDialog$default (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLnet/minestom/server/dialog/DialogAfterAction;ILjava/lang/Object;)Lnet/minestom/server/dialog/Dialog; -} - -public final class dev/slne/surf/api/minestom/dialog/search/SearchInput { - public fun ()V - public fun (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lkotlin/jvm/functions/Function1; - public final fun component3 ()Ljava/lang/String; - public final fun component4 ()Lkotlin/jvm/functions/Function1; - public final fun copy (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/dialog/search/SearchInput; - public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/dialog/search/SearchInput;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/dialog/search/SearchInput; - public fun equals (Ljava/lang/Object;)Z - public final fun getInitialValue ()Ljava/lang/String; - public final fun getInputModifier ()Lkotlin/jvm/functions/Function1; - public final fun getKey ()Ljava/lang/String; - public final fun getLabel ()Lkotlin/jvm/functions/Function1; - public fun hashCode ()I - public final fun setInitialValue (Ljava/lang/String;)V - public final fun setInputModifier (Lkotlin/jvm/functions/Function1;)V - public final fun setKey (Ljava/lang/String;)V - public final fun setLabel (Lkotlin/jvm/functions/Function1;)V - public fun toString ()Ljava/lang/String; -} - -public final class dev/slne/surf/api/minestom/impl/inventory/framework/MinestomInventoryLoader : dev/slne/minestom/lobby/api/event/EventRegistrar { - public static final field Companion Ldev/slne/surf/api/minestom/impl/inventory/framework/MinestomInventoryLoader$Companion; - public fun ()V - public final fun disable ()V - public final fun enable ()V - public final fun getViewFrame ()Lme/devnatan/inventoryframework/ViewFrame; - public fun register (Lnet/minestom/server/event/EventNode;)V -} - -public final class dev/slne/surf/api/minestom/impl/inventory/framework/MinestomInventoryLoader$Companion { - public final fun getInstance ()Ldev/slne/surf/api/minestom/impl/inventory/framework/MinestomInventoryLoader; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/InventoryFrameworkExtensions { - public static final fun getOutlineItem (Lme/devnatan/inventoryframework/View;)Lnet/minestom/server/item/ItemStack; - public static final fun modifyConfig (Lme/devnatan/inventoryframework/context/OpenContext;Lkotlin/jvm/functions/Function1;)Lme/devnatan/inventoryframework/ViewConfigBuilder; - public static final fun open (Lme/devnatan/inventoryframework/View;Ljava/util/Collection;)V - public static final fun open (Lme/devnatan/inventoryframework/View;Ljava/util/Collection;Ljava/lang/Object;)V - public static final fun open (Lme/devnatan/inventoryframework/View;Lnet/minestom/server/entity/Player;)V - public static final fun open (Lme/devnatan/inventoryframework/View;Lnet/minestom/server/entity/Player;Ljava/lang/Object;)V - public static final fun register (Lme/devnatan/inventoryframework/View;)V - public static final fun titleBuilder (Lme/devnatan/inventoryframework/ViewConfigBuilder;Lkotlin/jvm/functions/Function1;)Lme/devnatan/inventoryframework/ViewConfigBuilder; - public static final fun unregister (Lme/devnatan/inventoryframework/View;)V -} - -public abstract interface class dev/slne/surf/api/minestom/inventory/framework/ViewFrameAccessor { - public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/ViewFrameAccessor$Companion; - public abstract fun viewFrame ()Lme/devnatan/inventoryframework/ViewFrame; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/ViewFrameAccessor$Companion : dev/slne/surf/api/minestom/inventory/framework/ViewFrameAccessor { - public final fun getINSTANCE ()Ldev/slne/surf/api/minestom/inventory/framework/ViewFrameAccessor; - public fun viewFrame ()Lme/devnatan/inventoryframework/ViewFrame; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/ViewFrameAccessorKt { - public static final fun getViewFrame ()Lme/devnatan/inventoryframework/ViewFrame; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/dsl/ClickContextDSLKt { - public static final fun cancel (Lme/devnatan/inventoryframework/context/IFSlotClickContext;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/dsl/CloseContextDSLKt { - public static final fun cancel (Lme/devnatan/inventoryframework/context/IFCloseContext;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/dsl/IFConfinedContextDSLKt { - public static final fun openForPlayer (Lme/devnatan/inventoryframework/context/IFConfinedContext;Lme/devnatan/inventoryframework/RootView;)V - public static final fun openForPlayer (Lme/devnatan/inventoryframework/context/IFConfinedContext;Lme/devnatan/inventoryframework/RootView;Ljava/lang/Object;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/dsl/IFContextDSLKt { - public static final fun openForEveryone (Lme/devnatan/inventoryframework/context/IFContext;Lme/devnatan/inventoryframework/RootView;)V - public static final fun openForEveryone (Lme/devnatan/inventoryframework/context/IFContext;Lme/devnatan/inventoryframework/RootView;Ljava/lang/Object;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/dsl/LayoutBuilder { - public fun ()V - public final fun border (C)V - public final fun build ()[Ljava/lang/String; - public final fun empty ()V - public final fun fill (C)V - public final fun getRows ()Ljava/util/List; - public final fun row (Ljava/lang/String;)V - public final fun unaryPlus (Ljava/lang/String;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/dsl/MinestomItemComponentBuilderDSLKt { - public static final fun onItemClick (Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder;Lkotlin/jvm/functions/Function1;)V - public static final fun onItemRender (Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder;Lkotlin/jvm/functions/Function1;)V - public static final fun onItemUpdate (Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder;Lkotlin/jvm/functions/Function1;)V - public static final fun renderWith (Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder;Lnet/minestom/server/item/Material;ILkotlin/jvm/functions/Function1;)Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder; - public static synthetic fun renderWith$default (Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder;Lnet/minestom/server/item/Material;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder; - public static final fun withItem (Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder;Lnet/minestom/server/item/Material;ILkotlin/jvm/functions/Function1;)Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder; - public static synthetic fun withItem$default (Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder;Lnet/minestom/server/item/Material;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/dsl/OpenContextDSLKt { - public static final fun cancel (Lme/devnatan/inventoryframework/context/IFOpenContext;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/dsl/RenderContextDSLKt { - public static final fun availableSlot (Lme/devnatan/inventoryframework/context/RenderContext;Lkotlin/jvm/functions/Function1;)V - public static final fun firstSlot (Lme/devnatan/inventoryframework/context/RenderContext;Lkotlin/jvm/functions/Function1;)V - public static final fun lastSlot (Lme/devnatan/inventoryframework/context/RenderContext;Lkotlin/jvm/functions/Function1;)V - public static final fun layoutSlot (Lme/devnatan/inventoryframework/context/RenderContext;CLkotlin/jvm/functions/Function1;)V - public static final fun resultSlot (Lme/devnatan/inventoryframework/context/RenderContext;Lkotlin/jvm/functions/Function1;)V - public static final fun slot (Lme/devnatan/inventoryframework/context/RenderContext;IILkotlin/jvm/functions/Function1;)V - public static final fun slot (Lme/devnatan/inventoryframework/context/RenderContext;ILkotlin/jvm/functions/Function1;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/dsl/ViewConfigBuilderDSLKt { - public static final fun layout (Lme/devnatan/inventoryframework/ViewConfigBuilder;Lkotlin/jvm/functions/Function1;)V -} - -public abstract class dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView : me/devnatan/inventoryframework/View { - public fun (Ljava/lang/String;)V - protected fun containerDefaults (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;)V - public fun getSettings ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings; - protected final fun modifyContainer (Lme/devnatan/inventoryframework/context/Context;Lkotlin/jvm/functions/Function1;)V - public synthetic fun onClick (Lme/devnatan/inventoryframework/context/IFSlotClickContext;)V - public final fun onClick (Lme/devnatan/inventoryframework/context/SlotClickContext;)V - public final fun onClose (Lme/devnatan/inventoryframework/context/CloseContext;)V - public synthetic fun onClose (Lme/devnatan/inventoryframework/context/IFCloseContext;)V - public synthetic fun onFirstRender (Lme/devnatan/inventoryframework/context/IFRenderContext;)V - public final fun onFirstRender (Lme/devnatan/inventoryframework/context/RenderContext;)V - public final fun onInit (Lme/devnatan/inventoryframework/ViewConfigBuilder;)V - public synthetic fun onOpen (Lme/devnatan/inventoryframework/context/IFOpenContext;)V - public final fun onOpen (Lme/devnatan/inventoryframework/context/OpenContext;)V - public final fun onUpdate (Lme/devnatan/inventoryframework/context/Context;)V - public synthetic fun onUpdate (Lme/devnatan/inventoryframework/context/IFContext;)V - protected fun onViewClick (Lme/devnatan/inventoryframework/context/SlotClickContext;)V - protected fun onViewClose (Lme/devnatan/inventoryframework/context/CloseContext;)V - protected fun onViewInit (Lme/devnatan/inventoryframework/ViewConfigBuilder;)V - protected fun onViewOpen (Lme/devnatan/inventoryframework/context/OpenContext;)V - protected fun onViewRender (Lme/devnatan/inventoryframework/context/RenderContext;)V - protected fun onViewUpdate (Lme/devnatan/inventoryframework/context/Context;)V - protected final fun updateHeader (Lme/devnatan/inventoryframework/context/Context;Ljava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;)V - protected final fun updateHeader (Lme/devnatan/inventoryframework/context/Context;Lnet/kyori/adventure/text/Component;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;)V - public static synthetic fun updateHeader$default (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView;Lme/devnatan/inventoryframework/context/Context;Ljava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ILjava/lang/Object;)V - public static synthetic fun updateHeader$default (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView;Lme/devnatan/inventoryframework/context/Context;Lnet/kyori/adventure/text/Component;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ILjava/lang/Object;)V -} - -public abstract class dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext { - public fun ()V - public final fun getContainerDefaults ()Lkotlin/jvm/functions/Function2; - public final fun getOnClick ()Lkotlin/jvm/functions/Function2; - public final fun getOnClose ()Lkotlin/jvm/functions/Function2; - public final fun getOnFirstRender ()Lkotlin/jvm/functions/Function2; - public final fun getOnInit ()Lkotlin/jvm/functions/Function2; - public final fun getOnOpen ()Lkotlin/jvm/functions/Function2; - public final fun getOnUpdate ()Lkotlin/jvm/functions/Function2; - public final fun getStateRegistry ()Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateRegistry; - public final fun setContainerDefaults (Lkotlin/jvm/functions/Function2;)V - public final fun setOnClick (Lkotlin/jvm/functions/Function2;)V - public final fun setOnClose (Lkotlin/jvm/functions/Function2;)V - public final fun setOnFirstRender (Lkotlin/jvm/functions/Function2;)V - public final fun setOnInit (Lkotlin/jvm/functions/Function2;)V - public final fun setOnOpen (Lkotlin/jvm/functions/Function2;)V - public final fun setOnUpdate (Lkotlin/jvm/functions/Function2;)V -} - -public abstract class dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef { - public field view Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView; - public fun ()V - public final fun getRegisteredView ()Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView; - public final fun getView ()Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView; - public final fun setView (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView;)V -} - -public abstract interface annotation class dev/slne/surf/api/minestom/inventory/framework/view/InventoryFrameworkDSL : java/lang/annotation/Annotation { -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewContext : dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext { - public fun ()V - public final fun getLayoutTarget ()Ljava/lang/Character; - public final fun getPaginationStateBuilder ()Lkotlin/jvm/functions/Function1; - public final fun getRegisteredLayoutTarget ()C - public final fun getRegisteredPaginationStateBuilder ()Lkotlin/jvm/functions/Function1; - public final fun getSettings ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; - public final fun setLayoutTarget (Ljava/lang/Character;)V - public final fun setPaginationStateBuilder (Lkotlin/jvm/functions/Function1;)V - public final fun setSettings (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings;)V -} - -public abstract class dev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewDSLImpl : dev/slne/surf/api/minestom/inventory/framework/view/pagination/AbstractPaginatedSurfView { - public fun (Ljava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewContext;Ldev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewRef;)V - protected fun applyContainerDefaults (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;)V - protected fun createPagination ()Lme/devnatan/inventoryframework/component/PaginationStateBuilder; - protected fun getLayoutTarget ()C - public fun getSettings ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; - public synthetic fun getSettings ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings; - protected fun onPaginatedClick (Lme/devnatan/inventoryframework/context/SlotClickContext;)V - protected fun onPaginatedClose (Lme/devnatan/inventoryframework/context/CloseContext;)V - protected fun onPaginatedInit (Lme/devnatan/inventoryframework/ViewConfigBuilder;)V - protected fun onPaginatedOpen (Lme/devnatan/inventoryframework/context/OpenContext;)V - protected fun onPaginatedRender (Lme/devnatan/inventoryframework/context/RenderContext;)V - protected fun onPaginatedUpdate (Lme/devnatan/inventoryframework/context/Context;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewRef : dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef { - public fun ()V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/SurfViewBuilderKt { - public static final fun paginatedSurfView (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/AbstractPaginatedSurfView; - public static final fun surfView (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/SurfViewContext : dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext { - public fun ()V - public final fun getSettings ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings; - public final fun setSettings (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings;)V -} - -public abstract class dev/slne/surf/api/minestom/inventory/framework/view/SurfViewDSLImpl : dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView { - public fun (Ljava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/SurfViewContext;Ldev/slne/surf/api/minestom/inventory/framework/view/SurfViewRef;)V - protected fun containerDefaults (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;)V - public fun getSettings ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings; - protected fun onViewClick (Lme/devnatan/inventoryframework/context/SlotClickContext;)V - protected fun onViewClose (Lme/devnatan/inventoryframework/context/CloseContext;)V - protected fun onViewInit (Lme/devnatan/inventoryframework/ViewConfigBuilder;)V - protected fun onViewOpen (Lme/devnatan/inventoryframework/context/OpenContext;)V - protected fun onViewRender (Lme/devnatan/inventoryframework/context/RenderContext;)V - protected fun onViewUpdate (Lme/devnatan/inventoryframework/context/Context;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/SurfViewLifecycleDslKt { - public static final fun containerDefaults (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function2;)V - public static final fun layoutTarget (Ldev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewContext;C)V - public static final fun modifyContainer (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;Lme/devnatan/inventoryframework/context/Context;Lkotlin/jvm/functions/Function1;)V - public static final fun onClick (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function2;)V - public static final fun onClose (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function2;)V - public static final fun onFirstRender (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function2;)V - public static final fun onInit (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function2;)V - public static final fun onOpen (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function2;)V - public static final fun onUpdate (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function2;)V - public static final fun settings (Ldev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewContext;Lkotlin/jvm/functions/Function1;)V - public static final fun settings (Ldev/slne/surf/api/minestom/inventory/framework/view/SurfViewContext;Lkotlin/jvm/functions/Function1;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/SurfViewRef : dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef { - public fun ()V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/SurfViewRefKt { - public static final fun getView (Ldev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewRef;)Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/AbstractPaginatedSurfView; - public static final fun getView (Ldev/slne/surf/api/minestom/inventory/framework/view/SurfViewRef;)Ldev/slne/surf/api/minestom/inventory/framework/view/SurfViewDSLImpl; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonBuilder { - public final fun onChange (Lkotlin/jvm/functions/Function3;)V - public final fun onCloseChanged (Lkotlin/jvm/functions/Function3;)V - public final fun reverseOnRightClick (Z)V - public static synthetic fun reverseOnRightClick$default (Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonBuilder;ZILjava/lang/Object;)V - public final fun state (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)V - public static synthetic fun state$default (Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonBuilder;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonDslKt { - public static final fun computedStatefulButton (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle; - public static final fun computedToggleButton (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle; - public static final fun computedTripleButton (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle; - public static final fun statefulButton (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle; - public static final fun toggleButton (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle; - public static synthetic fun toggleButton$default (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle; - public static final fun tripleButton (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle { - public final fun get (Lme/devnatan/inventoryframework/context/Context;)Ljava/lang/Object; - public final fun hasChanged (Lme/devnatan/inventoryframework/context/Context;)Z - public final fun initial (Lme/devnatan/inventoryframework/context/Context;)Ljava/lang/Object; - public final fun set (Lme/devnatan/inventoryframework/context/Context;Ljava/lang/Object;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandleKt { - public static final fun button (Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder;Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle;)Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonStateScope { - public final fun getValue ()Ljava/lang/Object; - public final fun item (Lnet/minestom/server/item/Material;ILkotlin/jvm/functions/Function1;)V - public static synthetic fun item$default (Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonStateScope;Lnet/minestom/server/item/Material;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public final fun onEnter (Lkotlin/jvm/functions/Function2;)V - public final fun renderItem (Lkotlin/jvm/functions/Function1;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/button/ViewToggleButtonBuilder { - public final fun onCloseChanged (Lkotlin/jvm/functions/Function3;)V - public final fun onToggle (Lkotlin/jvm/functions/Function2;)V - public final fun whenOff (Lkotlin/jvm/functions/Function1;)V - public final fun whenOn (Lkotlin/jvm/functions/Function1;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/container/ViewContainer { - public fun ()V - public final fun addChild (Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent;)V - public final fun getChildren ()Ljava/util/List; - public final fun getLastRenderedTitle ()Lnet/kyori/adventure/text/Component; - public final fun hasComponentOfType (Ljava/lang/Class;)Z - public final fun removeChild (Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent;)V - public final fun removeChildren (Lkotlin/jvm/functions/Function1;)V - public final fun removeChildrenOfType (Ljava/lang/Class;)V - public final fun render ()Lnet/kyori/adventure/text/Component; -} - -public abstract interface class dev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent { - public abstract fun equals (Ljava/lang/Object;)Z - public fun getHasExactWidth ()Z - public abstract fun getPositionalShift ()I - public abstract fun getTextureWidth ()I - public abstract fun hashCode ()I - public abstract fun renderComponent (Ldev/slne/surf/api/core/messages/builder/SurfComponentBuilder;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent$DefaultImpls { - public static fun getHasExactWidth (Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent;)Z -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewBlockCellComponent : dev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent { - public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewBlockCellComponent$Companion; - public synthetic fun (II)V - public fun (IILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V - public synthetic fun (IILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun equals (Ljava/lang/Object;)Z - public final fun getColumn ()I - public fun getHasExactWidth ()Z - public fun getPositionalShift ()I - public final fun getRow ()I - public fun getTextureWidth ()I - public fun hashCode ()I - public fun renderComponent (Ldev/slne/surf/api/core/messages/builder/SurfComponentBuilder;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewBlockCellComponent$Companion { - public final fun forSlot (ILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewBlockCellComponent; - public static synthetic fun forSlot$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewBlockCellComponent$Companion;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewBlockCellComponent; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerBackHintComponent : dev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent { - public static final field INSTANCE Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerBackHintComponent; - public fun equals (Ljava/lang/Object;)Z - public fun getHasExactWidth ()Z - public fun getPositionalShift ()I - public fun getTextureWidth ()I - public fun hashCode ()I - public fun renderComponent (Ldev/slne/surf/api/core/messages/builder/SurfComponentBuilder;)V - public fun toString ()Ljava/lang/String; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerRowTextComponent : dev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent { - public fun (ILjava/lang/String;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;)V - public synthetic fun (ILjava/lang/String;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (ILnet/kyori/adventure/text/Component;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;)V - public synthetic fun (ILnet/kyori/adventure/text/Component;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun equals (Ljava/lang/Object;)Z - public final fun getAlignment ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; - public final fun getColumns ()Lkotlin/ranges/IntRange; - public fun getHasExactWidth ()Z - public fun getPositionalShift ()I - public final fun getRow ()I - public final fun getText ()Lnet/kyori/adventure/text/Component; - public fun getTextureWidth ()I - public fun hashCode ()I - public fun renderComponent (Ldev/slne/surf/api/core/messages/builder/SurfComponentBuilder;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerTitleComponent : dev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent { - public synthetic fun (Ljava/lang/String;Lnet/kyori/adventure/key/Key;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lit/unimi/dsi/fastutil/ints/Int2IntMap;)V - public synthetic fun (Ljava/lang/String;Lnet/kyori/adventure/key/Key;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lit/unimi/dsi/fastutil/ints/Int2IntMap;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Ljava/lang/String;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;)V - public synthetic fun (Ljava/lang/String;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;)V - public synthetic fun (Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun equals (Ljava/lang/Object;)Z - public fun getHasExactWidth ()Z - public fun getPositionalShift ()I - public final fun getTextAlignment ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; - public fun getTextureWidth ()I - public final fun getTitle ()Lnet/kyori/adventure/text/Component; - public fun hashCode ()I - public fun renderComponent (Ldev/slne/surf/api/core/messages/builder/SurfComponentBuilder;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerDSLKt { - public static final fun addChild (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent;)V - public static final synthetic fun backHint (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;)V - public static final fun blockAllSlots (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V - public static final fun blockAllSlots (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;Lit/unimi/dsi/fastutil/ints/IntCollection;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V - public static synthetic fun blockAllSlots$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)V - public static synthetic fun blockAllSlots$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;Lit/unimi/dsi/fastutil/ints/IntCollection;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)V - public static final synthetic fun blockCell (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;II)V - public static final fun blockCell (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;IILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V - public static synthetic fun blockCell$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;IILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)V - public static final synthetic fun blockColumn (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;)V - public static final fun blockColumn (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V - public static final synthetic fun blockColumn (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;I[I)V - public static final fun blockColumn (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;I[IILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V - public static synthetic fun blockColumn$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)V - public static synthetic fun blockColumn$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;ILjava/lang/Object;)V - public static synthetic fun blockColumn$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;I[IILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)V - public static final synthetic fun blockRow (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;)V - public static final fun blockRow (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V - public static final synthetic fun blockRow (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;I[I)V - public static final fun blockRow (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;I[ILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V - public static synthetic fun blockRow$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;ILjava/lang/Object;)V - public static synthetic fun blockRow$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)V - public static synthetic fun blockRow$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;I[ILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)V - public static final fun blockSlot (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V - public static synthetic fun blockSlot$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)V - public static final fun clearRowText (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;I)V - public static final fun clearRowTexts (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;)V - public static final fun hasComponentOfType (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ljava/lang/Class;)Z - public static final fun header (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/jvm/functions/Function1;)V - public static final synthetic fun header (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;Ljava/lang/String;)V - public static final fun header (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;Ljava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;)V - public static final fun header (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;Lnet/kyori/adventure/text/Component;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;)V - public static synthetic fun header$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public static synthetic fun header$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;Ljava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ILjava/lang/Object;)V - public static synthetic fun header$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;Lnet/kyori/adventure/text/Component;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ILjava/lang/Object;)V - public static final fun removeChild (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent;)V - public static final fun removeChildrenOfType (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ljava/lang/Class;)V - public static final fun rowText (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILjava/lang/String;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;)V - public static final fun rowText (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILnet/kyori/adventure/text/Component;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;)V - public static final fun rowText (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function1;)V - public static final fun rowText (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;ILjava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;)V - public static final fun rowText (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;ILnet/kyori/adventure/text/Component;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;)V - public static synthetic fun rowText$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILjava/lang/String;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;ILjava/lang/Object;)V - public static synthetic fun rowText$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILnet/kyori/adventure/text/Component;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;ILjava/lang/Object;)V - public static synthetic fun rowText$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public static synthetic fun rowText$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;ILjava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;ILjava/lang/Object;)V - public static synthetic fun rowText$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;ILnet/kyori/adventure/text/Component;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;ILjava/lang/Object;)V - public static final fun rowTextAt (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;IILjava/lang/String;)V - public static final fun rowTextAt (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;IILnet/kyori/adventure/text/Component;)V - public static final fun unblockAllSlots (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;)V - public static final fun unblockCell (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;II)V - public static final fun unblockSlot (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;I)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext { - public fun equals (Ljava/lang/Object;)Z - public final fun getContainer ()Ldev/slne/surf/api/minestom/inventory/framework/view/container/ViewContainer; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIcon { - public fun (Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType;Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor;)V - public final fun build (Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/item/ItemStack; - public final fun getColor ()Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor; - public final fun getIcon ()Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public final fun getItemModel ()Ljava/lang/String; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor : java/lang/Enum { - public static final field BLUE Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor; - public static final field GREEN Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor; - public static final field RED Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor; - public static final field WHITE Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor; - public static final field YELLOW Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor; - public final fun getConfigName ()Ljava/lang/String; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor; - public static fun values ()[Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconKt { - public static final fun viewIcon (Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType;Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor;Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/item/ItemStack; - public static synthetic fun viewIcon$default (Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType;Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/minestom/server/item/ItemStack; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType : java/lang/Enum { - public static final field ARROW_DOWN Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field ARROW_LEFT Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field ARROW_NEXT Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field ARROW_PREVIOUS Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field ARROW_RIGHT Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field ARROW_UP Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field BELL Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field CHECK Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field CIRCLE Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field COG Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field CROSS Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field EQUALS Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field EXCLAMATION_MARK Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field HOME Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field MENU Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field MINUS Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field PLUS Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field QUESTION Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field QUESTION_MARK Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field RELOAD Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field SEARCH Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static final field USERS Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public final fun getConfigName ()Ljava/lang/String; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; - public static fun values ()[Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; -} - -public abstract class dev/slne/surf/api/minestom/inventory/framework/view/pagination/AbstractPaginatedSurfView : dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView { - public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/AbstractPaginatedSurfView$Companion; - public fun (Ljava/lang/String;)V - protected fun applyContainerDefaults (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;)V - protected final fun containerDefaults (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;)V - protected abstract fun createPagination ()Lme/devnatan/inventoryframework/component/PaginationStateBuilder; - protected abstract fun getLayoutTarget ()C - public fun getSettings ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; - public synthetic fun getSettings ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings; - protected fun onPaginatedClick (Lme/devnatan/inventoryframework/context/SlotClickContext;)V - protected fun onPaginatedClose (Lme/devnatan/inventoryframework/context/CloseContext;)V - protected fun onPaginatedInit (Lme/devnatan/inventoryframework/ViewConfigBuilder;)V - protected fun onPaginatedOpen (Lme/devnatan/inventoryframework/context/OpenContext;)V - protected fun onPaginatedRender (Lme/devnatan/inventoryframework/context/RenderContext;)V - protected fun onPaginatedUpdate (Lme/devnatan/inventoryframework/context/Context;)V - protected final fun onViewClick (Lme/devnatan/inventoryframework/context/SlotClickContext;)V - protected final fun onViewClose (Lme/devnatan/inventoryframework/context/CloseContext;)V - protected final fun onViewInit (Lme/devnatan/inventoryframework/ViewConfigBuilder;)V - protected final fun onViewOpen (Lme/devnatan/inventoryframework/context/OpenContext;)V - protected final fun onViewRender (Lme/devnatan/inventoryframework/context/RenderContext;)V - protected final fun onViewUpdate (Lme/devnatan/inventoryframework/context/Context;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/AbstractPaginatedSurfView$Companion { -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationDslBuilder { - public fun ()V - public final fun applyTo (Lme/devnatan/inventoryframework/component/PaginationStateBuilder;)V - public final fun asyncSource (Lkotlin/jvm/functions/Function1;)V - public final fun computedSource (Lkotlin/jvm/functions/Function1;)V - public final fun elementFactory (Lkotlin/jvm/functions/Function4;)V - public final fun getElementFactory ()Lme/devnatan/inventoryframework/component/PaginationValueConsumer; - public final fun getOrientation ()Lme/devnatan/inventoryframework/component/Pagination$Orientation; - public final fun getPageSwitchHandler ()Ljava/util/function/BiConsumer; - public final fun getSimpleItemFactory ()Ljava/util/function/BiConsumer; - public final fun getSourceType ()Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType; - public final fun horizontal ()V - public final fun itemFactory (Lkotlin/jvm/functions/Function2;)V - public final fun lazyAsyncSource (Lkotlin/jvm/functions/Function1;)V - public final fun lazySource (Lkotlin/jvm/functions/Function1;)V - public final fun lazySuspendSource (Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function3;)V - public final fun onPageSwitch (Lkotlin/jvm/functions/Function2;)V - public final fun orientation (Lme/devnatan/inventoryframework/component/Pagination$Orientation;)V - public final fun setElementFactory (Lme/devnatan/inventoryframework/component/PaginationValueConsumer;)V - public final fun setOrientation (Lme/devnatan/inventoryframework/component/Pagination$Orientation;)V - public final fun setPageSwitchHandler (Ljava/util/function/BiConsumer;)V - public final fun setSimpleItemFactory (Ljava/util/function/BiConsumer;)V - public final fun setSourceType (Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType;)V - public final fun source (Ljava/lang/Iterable;)V - public final fun source (Lkotlin/jvm/functions/Function0;)V - public final fun suspendSource (Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function3;)V - public final fun vertical ()V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationDslKt { - public static final fun pagination (Ldev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewContext;Lkotlin/jvm/functions/Function1;)V -} - -public abstract interface class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator { - public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator$Companion; - public abstract fun render (II)Lnet/kyori/adventure/text/Component; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator$Companion { - public final fun getDefault ()Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator; -} - -public abstract interface class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType { -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType$Computed : dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType { - public fun (Lkotlin/jvm/functions/Function1;)V - public final fun getProvider ()Lkotlin/jvm/functions/Function1; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType$ComputedAsync : dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType { - public fun (Lkotlin/jvm/functions/Function1;)V - public final fun getProvider ()Lkotlin/jvm/functions/Function1; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType$Lazy : dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType { - public fun (Lkotlin/jvm/functions/Function1;)V - public final fun getProvider ()Lkotlin/jvm/functions/Function1; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType$LazyAsync : dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType { - public fun (Lkotlin/jvm/functions/Function1;)V - public final fun getProvider ()Lkotlin/jvm/functions/Function1; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType$Static : dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType { - public fun (Lkotlin/jvm/functions/Function0;)V - public final fun getProvider ()Lkotlin/jvm/functions/Function0; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings : dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings { - public fun ()V - public fun (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ZLit/unimi/dsi/fastutil/ints/Int2ObjectMap;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator;Lnet/kyori/adventure/sound/Sound;)V - public synthetic fun (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ZLit/unimi/dsi/fastutil/ints/Int2ObjectMap;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator;Lnet/kyori/adventure/sound/Sound;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;)V - public synthetic fun (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lnet/kyori/adventure/key/Key; - public final fun component10 ()Z - public final fun component11 ()Z - public final fun component12 ()Z - public final fun component13 ()Z - public final fun component14 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; - public final fun component15 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; - public final fun component16 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition; - public final fun component17 ()Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator; - public final fun component18 ()Lnet/kyori/adventure/sound/Sound; - public final fun component2 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; - public final fun component3 ()Lnet/kyori/adventure/text/format/TextColor; - public final fun component4 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public final fun component5 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public final fun component6 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; - public final fun component7 ()Z - public final fun component8 ()Lit/unimi/dsi/fastutil/ints/Int2ObjectMap; - public final fun component9 ()Z - public final fun copy (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ZLit/unimi/dsi/fastutil/ints/Int2ObjectMap;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator;Lnet/kyori/adventure/sound/Sound;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; - public final fun copy (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; - public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ZLit/unimi/dsi/fastutil/ints/Int2ObjectMap;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator;Lnet/kyori/adventure/sound/Sound;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; - public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; - public fun equals (Ljava/lang/Object;)Z - public fun getBackgroundGlyph ()Z - public fun getCancelOnClick ()Z - public fun getCancelOnDrag ()Z - public fun getCancelOnDrop ()Z - public fun getCancelOnPickup ()Z - public fun getFont ()Lnet/kyori/adventure/key/Key; - public fun getHeaderFontMetrics ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public fun getHeaderGeometry ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; - public fun getHeaderTextAlignment ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; - public fun getHeaderTextColor ()Lnet/kyori/adventure/text/format/TextColor; - public fun getNavigateBackOnOutsideClick ()Z - public final fun getPaginationButtonPosition ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition; - public final fun getPaginationContentRowCount ()I - public final fun getPaginationEmptyRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; - public final fun getPaginationPageIndicator ()Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator; - public final fun getPaginationSwitchSound ()Lnet/kyori/adventure/sound/Sound; - public final fun getPaginationViewRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; - public fun getRowFontMetrics ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public fun getRowFonts ()Lit/unimi/dsi/fastutil/ints/Int2ObjectMap; - public fun getRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; - public fun hashCode ()I - public fun rowFont (I)Lnet/kyori/adventure/key/Key; - public fun toString ()Ljava/lang/String; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition : java/lang/Enum { - public static final field BOTTOM Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition; - public static final field TOP Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition; - public static fun values ()[Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows : java/lang/Enum { - public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows$Companion; - public static final field NONE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; - public static final field ONE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; - public static final field THREE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; - public static final field TWO Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public final fun getRows ()I - public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; - public static fun values ()[Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows$Companion { - public final fun byRows (I)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; - public final fun byRowsOrNull (I)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows : java/lang/Enum { - public static final field FIVE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; - public static final field FOUR Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; - public static final field ONE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; - public static final field THREE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; - public static final field TWO Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; - public final fun getActualRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; - public final fun getContentRows ()I - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public final synthetic fun getPaginationContentRows ()Lkotlin/ranges/IntRange; - public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; - public static fun values ()[Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings : dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings { - public fun ()V - public fun (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ZLit/unimi/dsi/fastutil/ints/Int2ObjectMap;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;)V - public synthetic fun (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ZLit/unimi/dsi/fastutil/ints/Int2ObjectMap;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;)V - public synthetic fun (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lnet/kyori/adventure/key/Key; - public final fun component10 ()Z - public final fun component11 ()Z - public final fun component12 ()Z - public final fun component13 ()Z - public final fun component14 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; - public final fun component2 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; - public final fun component3 ()Lnet/kyori/adventure/text/format/TextColor; - public final fun component4 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public final fun component5 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public final fun component6 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; - public final fun component7 ()Z - public final fun component8 ()Lit/unimi/dsi/fastutil/ints/Int2ObjectMap; - public final fun component9 ()Z - public final fun copy (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ZLit/unimi/dsi/fastutil/ints/Int2ObjectMap;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings; - public final fun copy (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings; - public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ZLit/unimi/dsi/fastutil/ints/Int2ObjectMap;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings; - public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings; - public fun equals (Ljava/lang/Object;)Z - public fun getBackgroundGlyph ()Z - public fun getCancelOnClick ()Z - public fun getCancelOnDrag ()Z - public fun getCancelOnDrop ()Z - public fun getCancelOnPickup ()Z - public fun getFont ()Lnet/kyori/adventure/key/Key; - public fun getHeaderFontMetrics ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public fun getHeaderGeometry ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; - public fun getHeaderTextAlignment ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; - public fun getHeaderTextColor ()Lnet/kyori/adventure/text/format/TextColor; - public fun getNavigateBackOnOutsideClick ()Z - public fun getRowFontMetrics ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public fun getRowFonts ()Lit/unimi/dsi/fastutil/ints/Int2ObjectMap; - public fun getRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; - public fun hashCode ()I - public fun rowFont (I)Lnet/kyori/adventure/key/Key; - public fun toString ()Ljava/lang/String; -} - -public abstract interface class dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings { - public abstract fun getBackgroundGlyph ()Z - public abstract fun getCancelOnClick ()Z - public abstract fun getCancelOnDrag ()Z - public abstract fun getCancelOnDrop ()Z - public abstract fun getCancelOnPickup ()Z - public abstract fun getFont ()Lnet/kyori/adventure/key/Key; - public abstract fun getHeaderFontMetrics ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public abstract fun getHeaderGeometry ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; - public abstract fun getHeaderTextAlignment ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; - public abstract fun getHeaderTextColor ()Lnet/kyori/adventure/text/format/TextColor; - public abstract fun getNavigateBackOnOutsideClick ()Z - public abstract fun getRowFontMetrics ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public abstract fun getRowFonts ()Lit/unimi/dsi/fastutil/ints/Int2ObjectMap; - public abstract fun getRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; - public fun rowFont (I)Lnet/kyori/adventure/key/Key; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings$DefaultImpls { - public static fun rowFont (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings;I)Lnet/kyori/adventure/key/Key; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettingsDefaults { - public static final field DEFAULT_BACKGROUND_GLYPH Z - public static final field DEFAULT_CANCEL_ON_CLICK Z - public static final field DEFAULT_CANCEL_ON_DRAG Z - public static final field DEFAULT_CANCEL_ON_DROP Z - public static final field DEFAULT_CANCEL_ON_PICKUP Z - public static final field DEFAULT_NAVIGATE_BACK_ON_CLOSE Z - public static final field INSTANCE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettingsDefaults; - public final fun getASCENDED_HEADER_FONT ()Lnet/kyori/adventure/key/Key; - public final fun getDEFAULT_HEADER_ALIGNMENT ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; - public final fun getDEFAULT_HEADER_FONT ()Lnet/kyori/adventure/key/Key; - public final fun getDEFAULT_HEADER_FONT_METRICS ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public final fun getDEFAULT_HEADER_GEOMETRY ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; - public final fun getDEFAULT_HEADER_TEXT_COLOR ()Lnet/kyori/adventure/text/format/TextColor; - public final fun getDEFAULT_MENU_FONT ()Lnet/kyori/adventure/key/Key; - public final fun getDEFAULT_PAGINATION_BUTTON_POSITION ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition; - public final fun getDEFAULT_PAGINATION_EMPTY_ROWS ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; - public final fun getDEFAULT_PAGINATION_PAGE_INDICATOR ()Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator; - public final fun getDEFAULT_PAGINATION_SWITCH_SOUND ()Lnet/kyori/adventure/sound/Sound; - public final fun getDEFAULT_PAGINATION_VIEW_ROWS ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; - public final fun getDEFAULT_ROW_FONTS ()Lit/unimi/dsi/fastutil/ints/Int2ObjectMap; - public final fun getDEFAULT_ROW_FONT_METRICS ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public final fun getDEFAULT_ROW_TEXT_ALIGNMENT ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; - public final fun getDEFAULT_ROW_TEXT_COLUMNS ()Lkotlin/ranges/IntRange; - public final fun getDEFAULT_VIEW_ROWS ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; - public final fun rowFont (I)Lnet/kyori/adventure/key/Key; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics { - public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics$Companion; - public static final field MENU_CHAR_SIZE I - public static final field MENU_CHAR_SPACING I - public static final field MENU_SPACE_SIZE I - public fun ()V - public fun (IIZZLit/unimi/dsi/fastutil/ints/Int2IntMap;)V - public synthetic fun (IIZZLit/unimi/dsi/fastutil/ints/Int2IntMap;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun advance (IZ)I - public static synthetic fun advance$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;IZILjava/lang/Object;)I - public final fun component1 ()I - public final fun component2 ()I - public final fun component3 ()Z - public final fun component4 ()Z - public final fun component5 ()Lit/unimi/dsi/fastutil/ints/Int2IntMap; - public final fun copy (IIZZLit/unimi/dsi/fastutil/ints/Int2IntMap;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;IIZZLit/unimi/dsi/fastutil/ints/Int2IntMap;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public fun equals (Ljava/lang/Object;)Z - public final fun getCharSize ()I - public final fun getCharSpacing ()I - public final fun getCharWidths ()Lit/unimi/dsi/fastutil/ints/Int2IntMap; - public final fun getSpaceAsShift ()Z - public final fun getSpaceShift ()Ljava/lang/Integer; - public final fun getUppercase ()Z - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics$Companion { - public final fun getSURF_MENU ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public final fun getVANILLA ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public final fun getVANILLA_ROW ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry { - public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry$Companion; - public static final field DEFAULT_ROW_TEXT_PADDING I - public static final field DEFAULT_SLOT_GLYPH_INSET I - public static final field DEFAULT_SLOT_ORIGIN_X I - public static final field DEFAULT_TITLE_LEFT_SHIFT I - public static final field DEFAULT_TITLE_PADDING I - public static final field DEFAULT_TITLE_WIDTH I - public fun ()V - public fun (IIIIIII)V - public synthetic fun (IIIIIIIILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun columnShift (I)I - public final fun columnSpanOptions (Lkotlin/ranges/IntRange;Lit/unimi/dsi/fastutil/ints/Int2IntMap;II)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions; - public static synthetic fun columnSpanOptions$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lkotlin/ranges/IntRange;Lit/unimi/dsi/fastutil/ints/Int2IntMap;IIILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions; - public final fun component1 ()I - public final fun component2 ()I - public final fun component3 ()I - public final fun component4 ()I - public final fun component5 ()I - public final fun component6 ()I - public final fun component7 ()I - public final fun copy (IIIIIII)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; - public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;IIIIIIIILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; - public fun equals (Ljava/lang/Object;)Z - public final fun getRowTextPadding ()I - public final fun getSlotAreaWidth ()I - public final fun getSlotGlyphInset ()I - public final fun getSlotOriginX ()I - public final fun getSlotSize ()I - public final fun getTitleLeftShift ()I - public final fun getTitlePadding ()I - public final fun getTitleWidth ()I - public fun hashCode ()I - public final fun slotGlyphShift (I)I - public final fun titleOptions (Lit/unimi/dsi/fastutil/ints/Int2IntMap;II)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions; - public static synthetic fun titleOptions$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lit/unimi/dsi/fastutil/ints/Int2IntMap;IIILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions; - public fun toString ()Ljava/lang/String; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry$Companion { - public final fun getDEFAULT ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows : java/lang/Enum { - public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows$Companion; - public static final field FIVE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; - public static final field FOUR Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; - public static final field ONE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; - public static final field SIX Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; - public static final field THREE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; - public static final field TWO Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public final fun getGlyph ()C - public final fun getRows ()I - public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; - public static fun values ()[Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows$Companion { - public final fun byRows (I)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; - public final fun byRowsOrNull (I)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; -} - -public abstract interface annotation class dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows$Companion$Rows : java/lang/annotation/Annotation { -} - -public abstract class dev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment : java/lang/Enum { - public static final field CENTER Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; - public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment$Companion; - public static final field LEFT Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; - public static final field RIGHT Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; - public abstract fun calculateShift (ILdev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions;)I - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; - public static fun values ()[Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment$Companion { - public final fun calculateTextWidth (Ljava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions;)I -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions { - public fun (IIIIILit/unimi/dsi/fastutil/ints/Int2IntMap;)V - public synthetic fun (IIIIILit/unimi/dsi/fastutil/ints/Int2IntMap;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()I - public final fun component2 ()I - public final fun component3 ()I - public final fun component4 ()I - public final fun component5 ()I - public final fun component6 ()Lit/unimi/dsi/fastutil/ints/Int2IntMap; - public final fun copy (IIIIILit/unimi/dsi/fastutil/ints/Int2IntMap;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions; - public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions;IIIIILit/unimi/dsi/fastutil/ints/Int2IntMap;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions; - public fun equals (Ljava/lang/Object;)Z - public final fun getCharSize ()I - public final fun getCharSpacing ()I - public final fun getCharWidths ()Lit/unimi/dsi/fastutil/ints/Int2IntMap; - public final fun getContainerWidth ()I - public final fun getLeftShift ()I - public final fun getPadding ()I - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/PaginatedViewSettingsBuilder : dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder { - public fun ()V - public fun build ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; - public synthetic fun build ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings; - public final fun getPaginationButtonPosition ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition; - public final fun getPaginationEmptyRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; - public final fun getPaginationPageIndicator ()Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator; - public final fun getPaginationSwitchSound ()Lnet/kyori/adventure/sound/Sound; - public final fun getPaginationViewRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; - public final fun noPaginationEmptyRows ()V - public final fun noPaginationPageIndicator ()V - public final fun noPaginationSwitchSound ()V - public final fun paginationButtonPosition (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;)V - public final fun paginationButtonsAtBottom ()V - public final fun paginationButtonsAtTop ()V - public final fun paginationEmptyRows (I)V - public final fun paginationEmptyRows (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows;)V - public final fun paginationPageIndicator (Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator;)V - public final fun paginationSwitchSound (Lnet/kyori/adventure/sound/Sound;)V - public final fun paginationViewRows (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/PaginatedViewSettingsBuilderKt { - public static final fun paginatedViewSettings (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SimpleViewSettingsBuilder : dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder { - public fun ()V - public fun build ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings; - public synthetic fun build ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings; - public final fun getRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; - public final fun rows (I)V - public final fun rows (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SimpleViewSettingsBuilderKt { - public static final fun simpleViewSettings (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings; -} - -public abstract class dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder { - public final fun backgroundGlyph (Z)V - public static synthetic fun backgroundGlyph$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder;ZILjava/lang/Object;)V - public abstract fun build ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings; - public final fun cancelAllInteractions ()V - public final fun cancelOnClick (Z)V - public static synthetic fun cancelOnClick$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder;ZILjava/lang/Object;)V - public final fun cancelOnDrag (Z)V - public static synthetic fun cancelOnDrag$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder;ZILjava/lang/Object;)V - public final fun cancelOnDrop (Z)V - public static synthetic fun cancelOnDrop$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder;ZILjava/lang/Object;)V - public final fun cancelOnPickup (Z)V - public static synthetic fun cancelOnPickup$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder;ZILjava/lang/Object;)V - public final fun font (Lnet/kyori/adventure/key/Key;)V - public final fun getBackgroundGlyph ()Z - public final fun getCancelOnClick ()Z - public final fun getCancelOnDrag ()Z - public final fun getCancelOnDrop ()Z - public final fun getCancelOnPickup ()Z - public final fun getFont ()Lnet/kyori/adventure/key/Key; - public final fun getHeaderFontMetrics ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public final fun getHeaderGeometry ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; - public final fun getHeaderTextAlignment ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; - public final fun getHeaderTextColor ()Lnet/kyori/adventure/text/format/TextColor; - public final fun getNavigateBackOnOutsideClick ()Z - public final fun getRowFontMetrics ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; - public final fun getRowFonts ()Lit/unimi/dsi/fastutil/ints/Int2ObjectMap; - public final fun headerFontMetrics (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;)V - public final fun headerGeometry (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V - public final fun headerGeometry (Lkotlin/jvm/functions/Function1;)V - public final fun headerTextAlignment (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;)V - public final fun headerTextColor (Lnet/kyori/adventure/text/format/TextColor;)V - public final fun navigateBackOnOutsideClick (Z)V - public static synthetic fun navigateBackOnOutsideClick$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder;ZILjava/lang/Object;)V - public final fun rowFont (ILnet/kyori/adventure/key/Key;)V - public final fun rowFontMetrics (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;)V - public final fun rowFonts (Lit/unimi/dsi/fastutil/ints/Int2ObjectMap;)V -} - -public abstract interface class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState$Computed : dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { - public fun (Ljava/util/function/Function;)V - public final fun getComputation ()Ljava/util/function/Function; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState$ComputedSupplier : dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { - public fun (Ljava/util/function/Supplier;)V - public final fun getComputation ()Ljava/util/function/Supplier; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState$Immutable : dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { - public fun (Ljava/lang/Object;)V - public final fun getInitialValue ()Ljava/lang/Object; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState$Initial : dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { - public fun (Ljava/lang/String;)V - public final fun getKey ()Ljava/lang/String; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState$Lazy : dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { - public fun (Ljava/util/function/Function;)V - public final fun getComputation ()Ljava/util/function/Function; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState$LazySupplier : dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { - public fun (Ljava/util/function/Supplier;)V - public final fun getComputation ()Ljava/util/function/Supplier; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState$Mutable : dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { - public fun (Ljava/lang/Object;)V - public final fun getInitialValue ()Ljava/lang/Object; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState$MutableInt : dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { - public fun (I)V - public final fun getInitialValue ()I -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/state/StateDslKt { - public static final fun computedState (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function0;)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; - public static final fun computedState (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; - public static final fun initialState (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; - public static final fun initialState (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Ljava/lang/String;)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; - public static final fun lazyState (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function0;)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; - public static final fun lazyState (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; - public static final fun mutableState (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;I)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; - public static final fun mutableState (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Ljava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; - public static final fun state (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Ljava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle { - public fun (Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateRegistry;I)V - public final fun getIndex ()I - public final fun getRegistry ()Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateRegistry; - public final fun resolve ()Ljava/lang/Object; -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/state/StateHandleKt { - public static final fun decrement (Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle;Lme/devnatan/inventoryframework/context/Context;)I - public static final fun get (Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle;Lme/devnatan/inventoryframework/context/Context;)I - public static final fun get (Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle;Lme/devnatan/inventoryframework/context/Context;)Ljava/lang/Object; - public static final fun getMutableState (Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle;Lme/devnatan/inventoryframework/context/Context;)Ljava/lang/Object; - public static final fun increment (Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle;Lme/devnatan/inventoryframework/context/Context;)I - public static final fun set (Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle;Lme/devnatan/inventoryframework/context/Context;I)V - public static final fun set (Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle;Lme/devnatan/inventoryframework/context/Context;Ljava/lang/Object;)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/state/StateRegistry { - public fun ()V - public final fun get (I)Ljava/lang/Object; - public final fun getDeferredStates ()Ljava/util/List; - public final fun getNextIndex ()I - public final fun getResolvedStates ()Lit/unimi/dsi/fastutil/objects/ObjectArrayList; - public final fun register (Ldev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState;)I - public final fun setNextIndex (I)V -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/util/GlyphShift { - public static final synthetic fun box-impl (I)Ldev/slne/surf/api/minestom/inventory/framework/view/util/GlyphShift; - public static fun constructor-impl (I)I - public fun equals (Ljava/lang/Object;)Z - public static fun equals-impl (ILjava/lang/Object;)Z - public static final fun equals-impl0 (II)Z - public final fun getAmount ()I - public fun hashCode ()I - public static fun hashCode-impl (I)I - public static final fun renderGlyphs-impl (I)Ljava/lang/String; - public fun toString ()Ljava/lang/String; - public static fun toString-impl (I)Ljava/lang/String; - public final synthetic fun unbox-impl ()I -} - -public final class dev/slne/surf/api/minestom/inventory/framework/view/util/GlyphShiftKt { - public static final fun shift (I)Ljava/lang/String; -} - diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/SurfApiMinestomEntrypoint.kt b/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/SurfApiMinestomEntrypoint.kt deleted file mode 100644 index 9cbcc784f..000000000 --- a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/SurfApiMinestomEntrypoint.kt +++ /dev/null @@ -1,38 +0,0 @@ -package dev.slne.surf.api.minestom - -import com.github.retrooper.packetevents.PacketEvents -import com.google.inject.Inject -import com.google.inject.Singleton -import dev.slne.minestom.lobby.api.plugin.MinestomPluginEntrypoint -import dev.slne.surf.api.core.extensions.packetEvents -import dev.slne.surf.api.core.server.packet.NoopPacketEvents -import dev.slne.surf.api.minestom.impl.SurfMinestomInstance - -@Singleton -internal class SurfApiMinestomEntrypoint @Inject constructor( - private val instance: SurfMinestomInstance -) : MinestomPluginEntrypoint { - override suspend fun start() { - preparePacketEvents() - instance.bootstrap() - instance.onLoad() - } - - override suspend fun afterStart() { - instance.onEnable() - } - - override suspend fun stop() { - instance.onDisable() - } - - private fun preparePacketEvents() { - PacketEvents.setAPI(NoopPacketEvents()) - packetEvents.load() - packetEvents.init() - } - - private fun destroyPacketEvents() { - packetEvents.terminate() - } -} \ No newline at end of file diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/SurfApiMinestomPlugin.kt b/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/SurfApiMinestomPlugin.kt deleted file mode 100644 index e2b5b3202..000000000 --- a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/SurfApiMinestomPlugin.kt +++ /dev/null @@ -1,16 +0,0 @@ -package dev.slne.surf.api.minestom - -import com.google.auto.service.AutoService -import dev.slne.minestom.lobby.api.plugin.MinestomPlugin -import dev.slne.minestom.lobby.api.plugin.annotation.MinestomPluginMeta -import dev.slne.surf.api.minestom.dialog.callback.DialogCallbackListener -import dev.slne.surf.api.minestom.impl.inventory.framework.MinestomInventoryLoader - -@AutoService(MinestomPlugin::class) -@MinestomPluginMeta("surf-api-minestom") -internal class SurfApiMinestomPlugin : MinestomPlugin(SurfApiMinestomEntrypoint::class.java) { - override fun configurePlugin() { - bindEventRegistrar() - bindEventRegistrar() - } -} \ No newline at end of file diff --git a/surf-api-minestom/surf-api-minestom-example-plugin/build.gradle.kts b/surf-api-minestom/surf-api-minestom-example-plugin/build.gradle.kts new file mode 100644 index 000000000..899e16fea --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-example-plugin/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + `core-convention` +} + +description = "surf-api-minestom-example-plugin" + +// A real plugin applies `dev.slne.surf.api.gradle.minestom`, which adds the api and generates the +// files in src/main/resources from `minestomPluginFile { }`. Inside this build that plugin is not +// available, so both are done by hand here. +dependencies { + compileOnly(projects.surfApiMinestom.surfApiMinestom) + compileOnly(libs.minestom.server) +} diff --git a/surf-api-minestom/surf-api-minestom-example-plugin/src/main/kotlin/dev/slne/surf/api/minestom/example/plugin/ExamplePlugin.kt b/surf-api-minestom/surf-api-minestom-example-plugin/src/main/kotlin/dev/slne/surf/api/minestom/example/plugin/ExamplePlugin.kt new file mode 100644 index 000000000..6ac1714d8 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-example-plugin/src/main/kotlin/dev/slne/surf/api/minestom/example/plugin/ExamplePlugin.kt @@ -0,0 +1,58 @@ +package dev.slne.surf.api.minestom.example.plugin + +import dev.slne.surf.api.minestom.command.dsl.commandAPICommand +import dev.slne.surf.api.minestom.command.dsl.playerExecutor +import dev.slne.surf.api.minestom.extension.addListener +import dev.slne.surf.api.minestom.player.event.PlayerJoinEvent +import dev.slne.surf.api.minestom.plugin.PluginManager +import dev.slne.surf.api.minestom.plugin.SurfMinestomPlugin +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import kotlin.io.path.createDirectories +import kotlin.io.path.exists +import kotlin.io.path.readText +import kotlin.io.path.writeText +import kotlin.time.Duration.Companion.seconds + +/** + * A plugin that only uses the plugin api, `surf-api-minestom`. + * + * Its listeners, commands and coroutines are removed automatically when it is disabled. + */ +class ExamplePlugin : SurfMinestomPlugin() { + private lateinit var greeting: String + + override suspend fun onLoad() { + val file = dataDirectory.createDirectories().resolve("greeting.txt") + if (!file.exists()) file.writeText("Welcome to the surf api example server!") + greeting = file.readText().trim() + + logger.info("Loaded the greeting from {}", file) + } + + override suspend fun onEnable() { + eventNode.addListener { event -> + event.player.sendMessage(Component.text(greeting, NamedTextColor.GREEN)) + } + + commandAPICommand("plugins") { + playerExecutor { player, _ -> + val plugins = PluginManager.instance.plugins.joinToString { "${it.meta.name} ${it.meta.version}" } + player.sendMessage(Component.text("Plugins: $plugins")) + } + } + + scope.launch { + while (true) { + delay(300.seconds) + logger.info("{} is still running", meta.name) + } + } + } + + override suspend fun onDisable() { + logger.info("Goodbye from {}", meta.name) + } +} diff --git a/surf-api-minestom/surf-api-minestom-example-plugin/src/main/resources/META-INF/services/dev.slne.surf.api.minestom.plugin.SurfMinestomPlugin b/surf-api-minestom/surf-api-minestom-example-plugin/src/main/resources/META-INF/services/dev.slne.surf.api.minestom.plugin.SurfMinestomPlugin new file mode 100644 index 000000000..ea088dc86 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-example-plugin/src/main/resources/META-INF/services/dev.slne.surf.api.minestom.plugin.SurfMinestomPlugin @@ -0,0 +1 @@ +dev.slne.surf.api.minestom.example.plugin.ExamplePlugin diff --git a/surf-api-minestom/surf-api-minestom-example-plugin/src/main/resources/META-INF/surf-minestom-plugins/dev.slne.surf.api.minestom.example.plugin.ExamplePlugin.json b/surf-api-minestom/surf-api-minestom-example-plugin/src/main/resources/META-INF/surf-minestom-plugins/dev.slne.surf.api.minestom.example.plugin.ExamplePlugin.json new file mode 100644 index 000000000..a2b13a9aa --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-example-plugin/src/main/resources/META-INF/surf-minestom-plugins/dev.slne.surf.api.minestom.example.plugin.ExamplePlugin.json @@ -0,0 +1,9 @@ +{ + "id": "example-plugin", + "main": "dev.slne.surf.api.minestom.example.plugin.ExamplePlugin", + "name": "ExamplePlugin", + "version": "1.0.0", + "description": "Shows what a surf Minestom plugin can do", + "authors": ["SLNE Development"], + "dependencies": [] +} diff --git a/surf-api-minestom/surf-api-minestom-example-plugin/src/main/resources/minestom-plugin.json b/surf-api-minestom/surf-api-minestom-example-plugin/src/main/resources/minestom-plugin.json new file mode 100644 index 000000000..a2b13a9aa --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-example-plugin/src/main/resources/minestom-plugin.json @@ -0,0 +1,9 @@ +{ + "id": "example-plugin", + "main": "dev.slne.surf.api.minestom.example.plugin.ExamplePlugin", + "name": "ExamplePlugin", + "version": "1.0.0", + "description": "Shows what a surf Minestom plugin can do", + "authors": ["SLNE Development"], + "dependencies": [] +} diff --git a/surf-api-minestom/surf-api-minestom-example/.gitignore b/surf-api-minestom/surf-api-minestom-example/.gitignore new file mode 100644 index 000000000..7e3b24e0a --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-example/.gitignore @@ -0,0 +1 @@ +run/ diff --git a/surf-api-minestom/surf-api-minestom-example/build.gradle.kts b/surf-api-minestom/surf-api-minestom-example/build.gradle.kts new file mode 100644 index 000000000..e541f7386 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-example/build.gradle.kts @@ -0,0 +1,115 @@ +import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer + +plugins { + `core-convention` + id("xyz.jpenilla.gremlin-gradle") +} + +description = "surf-api-minestom-example" + +val luckPerms = findProject(":surf-api-minestom:surf-api-minestom-server-luckperms") + +dependencies { + implementation(projects.surfApiMinestom.surfApiMinestomServerBootstrap) + implementation(projects.surfApiMinestom.surfApiMinestomServer) + implementation(projects.surfApiMinestom.surfApiMinestomServerSignedChat) + implementation(projects.surfApiMinestom.surfApiMinestomServerSpark) + implementation(projects.surfApiMinestom.surfApiMinestomServerNpc) + implementation(projects.surfApiMinestom.surfApiMinestomServerPlayerVisibility) + implementation(projects.surfApiMinestom.surfApiMinestomServerConsole) + implementation(projects.surfApiMinestom.surfApiMinestomServerPlugins) + + // Shaded into the server, where the plugin feature finds it on the class path + implementation(projects.surfApiMinestom.surfApiMinestomExamplePlugin) + if (luckPerms != null) implementation(luckPerms) + + implementation(libs.minestom.server) + runtimeOnly(libs.log4j.slf4j2.impl) +} + +// The example decides at runtime whether LuckPerms is on the class path +sourceSets.main { + kotlin.srcDir(if (luckPerms != null) "src/luckperms/kotlin" else "src/noLuckperms/kotlin") +} + +val mainClassName = "dev.slne.surf.api.minestom.example.ExampleServerKt" + +// Like a server built with dev.slne.surf.api.gradle.minestom-server: the jar contains the project +// modules and the bootstrap, which downloads every other library into run/libraries/ on startup +gremlin { + defaultGremlinRuntimeDependency = false + // gremlin's default ASM cannot read the class files of current Java versions + defaultJarRelocatorDependencies = false +} + +dependencies { + jarRelocatorRuntime("me.lucko:jar-relocator:1.7") + jarRelocatorRuntime(libs.asm) + jarRelocatorRuntime("org.ow2.asm:asm-commons:${libs.versions.asm.get()}") +} + +configurations.runtimeDownload { + extendsFrom(configurations.implementation.get(), configurations.runtimeOnly.get()) + shouldResolveConsistentlyWith(configurations.runtimeClasspath.get()) + exclude(group = "xyz.jpenilla", module = "gremlin-runtime") + exclude(group = "org.jspecify", module = "jspecify") +} + +tasks { + writeDependencies { + // The relocations core-convention applies to the shadow jar + val relocationPrefix = project.findProperty("relocationPrefix") as String + relocate("net.kyori.adventure.nbt", "$relocationPrefix.kyori.nbt") { + excludes.add("net.kyori.adventure.nbt.api.**") + } + relocate("org.spongepowered.configurate", "$relocationPrefix.configurate") + } + + shadowJar { + archiveClassifier = "all" + dependencies { + exclude { dependency -> + dependency.moduleGroup != "xyz.jpenilla" && dependency.moduleGroup != "org.jspecify" && + dependency.moduleArtifacts.none { it.id.componentIdentifier is ProjectComponentIdentifier } + } + } + // log4j-core and the terminal console appender each ship a plugin cache, which have to be + // merged; duplicates have to reach the transformer for that instead of being dropped + duplicatesStrategy = DuplicatesStrategy.INCLUDE + transform() + manifest { + attributes["Main-Class"] = mainClassName + attributes["Launcher-Agent-Class"] = "dev.slne.surf.api.minestom.server.bootstrap.SurfMinestomBootstrap" + // Mixin goes first, so its transformer is installed before another agent loads a Minestom + // class; LuckPerms adds the libraries it downloads to the class path through its agent + attributes["Surf-Delegate-Agent-Classes"] = listOfNotNull( + "dev.slne.surf.api.minestom.server.impl.mixin.SurfMixinAgent", + "me.lucko.luckperms.minestom.dependencies.LuckPermsAgent".takeIf { luckPerms != null }, + ).joinToString(",") + attributes["Surf-Mixin-Configs"] = + "mixins.surf-api-minestom-server.json,mixins.surf-api-minestom-server-npc.json" + attributes["Multi-Release"] = "true" + } + } + + register("runServer") { + group = "application" + description = "Builds the example server and runs it in run/" + dependsOn(shadowJar) + + val runDirectory = layout.projectDirectory.dir("run").asFile + val launcher = project.extensions.getByType().launcherFor { + languageVersion = JavaLanguageVersion.of(project.findProperty("javaVersion") as String) + } + + workingDir = runDirectory + standardInput = System.`in` + doFirst { + runDirectory.mkdirs() + executable = launcher.get().executablePath.asFile.absolutePath + } + argumentProviders += CommandLineArgumentProvider { + listOf("-jar", shadowJar.get().archiveFile.get().asFile.absolutePath) + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-example/src/luckperms/kotlin/dev/slne/surf/api/minestom/example/permissions.kt b/surf-api-minestom/surf-api-minestom-example/src/luckperms/kotlin/dev/slne/surf/api/minestom/example/permissions.kt new file mode 100644 index 000000000..b05d0ae61 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-example/src/luckperms/kotlin/dev/slne/surf/api/minestom/example/permissions.kt @@ -0,0 +1,11 @@ +package dev.slne.surf.api.minestom.example + +import dev.slne.surf.api.minestom.server.SurfMinestomServerBuilder +import dev.slne.surf.api.minestom.server.luckperms.withLuckPerms + +/** + * Resolves permissions with LuckPerms; grant them with `/lp user permission set ...`. + */ +internal fun SurfMinestomServerBuilder.withExamplePermissions() { + withLuckPerms() +} diff --git a/surf-api-minestom/surf-api-minestom-example/src/main/kotlin/dev/slne/surf/api/minestom/example/ExampleCommands.kt b/surf-api-minestom/surf-api-minestom-example/src/main/kotlin/dev/slne/surf/api/minestom/example/ExampleCommands.kt new file mode 100644 index 000000000..cc9862f40 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-example/src/main/kotlin/dev/slne/surf/api/minestom/example/ExampleCommands.kt @@ -0,0 +1,94 @@ +package dev.slne.surf.api.minestom.example + +import dev.slne.surf.api.minestom.command.dsl.anyExecutor +import dev.slne.surf.api.minestom.command.dsl.commandAPICommand +import dev.slne.surf.api.minestom.command.dsl.integerArgument +import dev.slne.surf.api.minestom.command.dsl.playerArgument +import dev.slne.surf.api.minestom.command.dsl.playerExecutor +import dev.slne.surf.api.minestom.command.dsl.playerExecutorSuspend +import dev.slne.surf.api.minestom.command.dsl.signedMessageArgument +import dev.slne.surf.api.minestom.command.dsl.subcommand +import dev.slne.surf.api.minestom.coroutine.withEntity +import dev.slne.surf.api.minestom.highlight.BlockHighlights +import dev.slne.surf.api.minestom.visibility.PlayerVisibility +import kotlinx.coroutines.delay +import net.kyori.adventure.chat.SignedMessage +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.minestom.server.MinecraftServer +import net.minestom.server.entity.GameMode +import net.minestom.server.entity.Player +import kotlin.time.Duration.Companion.seconds + +/** + * Commands built with the CommandAPI port of the surf api. + */ +internal object ExampleCommands { + + fun register() { + commandAPICommand("example") { + withSubcommand(subcommand("highlight") { + integerArgument("seconds", min = 1, max = 60, optional = true) + playerExecutorSuspend { player, args -> + val seconds = args.getOptional("seconds") ?: 5 + val position = player.position.sub(0.0, 1.0, 0.0) + val instance = player.instance ?: return@playerExecutorSuspend + + BlockHighlights.show(player, instance, position, NamedTextColor.GOLD) + delay(seconds.seconds) + player.withEntity { BlockHighlights.hide(it, instance, position) } + } + }) + + withSubcommand(subcommand("hide") { + playerArgument("target") + playerExecutor { player, args -> + val target = args.get("target") + PlayerVisibility.updateViewerRule(player) { entity -> entity !== target } + player.sendMessage(Component.text("${target.username} is now hidden from you")) + } + }) + + withSubcommand(subcommand("showall") { + playerExecutor { player, _ -> + PlayerVisibility.updateViewerRule(player, null) + player.sendMessage(Component.text("Everyone is visible again")) + } + }) + } + + commandAPICommand("gamemode") { + withAliases("gm") + withPermission("example.gamemode") + for (mode in GameMode.entries) { + withSubcommand(subcommand(mode.name.lowercase()) { + playerExecutor { player, _ -> + player.gameMode = mode + player.sendMessage(Component.text("Game mode set to ${mode.name.lowercase()}")) + } + }) + } + } + + commandAPICommand("stop") { + withPermission("example.stop") + anyExecutor { _, _ -> MinecraftServer.stopCleanly() } + } + + // The message keeps the signature the client produced for the argument + commandAPICommand("shout") { + signedMessageArgument("message") + playerExecutor { player, args -> + val message = args.get("message") + val name = Component.text(player.username, NamedTextColor.YELLOW) + val rendered = Component.text() + .append(name) + .append(Component.text(" shouts: ", NamedTextColor.GOLD)) + .append(Component.text(message.message())) + .build() + + broadcastSigned(message, name, rendered) + } + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-example/src/main/kotlin/dev/slne/surf/api/minestom/example/ExampleNpcs.kt b/surf-api-minestom/surf-api-minestom-example/src/main/kotlin/dev/slne/surf/api/minestom/example/ExampleNpcs.kt new file mode 100644 index 000000000..7f6c93a48 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-example/src/main/kotlin/dev/slne/surf/api/minestom/example/ExampleNpcs.kt @@ -0,0 +1,28 @@ +package dev.slne.surf.api.minestom.example + +import dev.slne.surf.api.minestom.npc.mannequinNpc +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.EquipmentSlot +import net.minestom.server.instance.Instance +import net.minestom.server.item.ItemStack +import net.minestom.server.item.Material + +/** + * NPCs spawned through the NPC feature of the surf api. + */ +internal object ExampleNpcs { + + fun spawn(instance: Instance) { + mannequinNpc("guide", instance, Pos(3.5, 41.0, 3.5, 135f, 0f)) { + displayName = Component.text("Guide", NamedTextColor.GREEN) + setEquipment(EquipmentSlot.MAIN_HAND, ItemStack.of(Material.COMPASS)) + onInteract { interaction -> + interaction.player.sendMessage( + Component.text("Try /example highlight, /shout or /gamemode creative!") + ) + } + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-example/src/main/kotlin/dev/slne/surf/api/minestom/example/ExampleServer.kt b/surf-api-minestom/surf-api-minestom-example/src/main/kotlin/dev/slne/surf/api/minestom/example/ExampleServer.kt new file mode 100644 index 000000000..c0afaf1d2 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-example/src/main/kotlin/dev/slne/surf/api/minestom/example/ExampleServer.kt @@ -0,0 +1,171 @@ +package dev.slne.surf.api.minestom.example + +import dev.slne.surf.api.minestom.chat.AsyncPlayerChatEvent +import dev.slne.surf.api.minestom.chat.ChatRenderer +import dev.slne.surf.api.minestom.chat.sendSignedMessage +import dev.slne.surf.api.minestom.event.EventRegistrar +import dev.slne.surf.api.minestom.extension.* +import dev.slne.surf.api.minestom.permission.hasPermission +import dev.slne.surf.api.minestom.player.event.* +import dev.slne.surf.api.minestom.server.chat.withSignedChat +import dev.slne.surf.api.minestom.server.configuration.ConfigurationTaskId +import dev.slne.surf.api.minestom.server.configuration.ConfigurationTasks +import dev.slne.surf.api.minestom.server.console.withConsole +import dev.slne.surf.api.minestom.server.npc.withNpcLib +import dev.slne.surf.api.minestom.server.plugins.withPlugins +import dev.slne.surf.api.minestom.server.spark.withSpark +import dev.slne.surf.api.minestom.server.surfMinestomServer +import dev.slne.surf.api.minestom.server.visibility.withPlayerVisibility +import net.kyori.adventure.chat.SignedMessage +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import net.minestom.server.Auth +import net.minestom.server.MinecraftServer +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.GameMode +import net.minestom.server.event.Event +import net.minestom.server.event.EventNode +import net.minestom.server.event.player.AsyncPlayerConfigurationEvent +import net.minestom.server.event.player.PlayerSpawnEvent +import net.minestom.server.instance.InstanceContainer +import net.minestom.server.instance.LightingChunk +import net.minestom.server.instance.block.Block +import kotlin.random.Random + +private val LOGGER = ComponentLogger.logger("ExampleServer") + +val SPAWN = Pos(0.5, 41.0, 0.5) + +/** + * A Minestom server that uses every feature of the surf api. + * + * Run it with `./gradlew :surf-api-minestom:surf-api-minestom-example:runServer` and join + * `localhost:25565`. + */ +fun main() { + val server = MinecraftServer.init(Auth.Online()) + val world = createWorld() + + surfMinestomServer { + withConfigurationPhase { + codeOfConduct { player -> + "Hello ${player.username}! This is the surf api example server. Please be nice." + } + + after(ConfigurationTasks.AWAIT_SETTINGS, ConfigurationTaskId("log_locale")) { context -> + LOGGER.info( + "{} joins with locale {}", + context.player.username, + context.player.locale + ) + } + } + + withExamplePermissions() + withSignedChat() + withSpark() + withNpcLib() + withPlayerVisibility { + maxVisible = 10 + activateAt = 15 + deactivateBelow = 8 + } + withConsole() + withPlugins() + + maxPlayers = 20 + listeners(ExampleListeners(world)) + } + + ExampleCommands.register() + ExampleNpcs.spawn(world) + + val port = System.getProperty("example.port")?.toInt() ?: 25565 + server.start("0.0.0.0", port) + LOGGER.info("Example server started on port {}", port) +} + +private fun createWorld(): InstanceContainer = buildInstance { + setChunkSupplier(::LightingChunk) + generator { + modify { + fillHeight(0, 40, Block.GRASS_BLOCK) + } + } +} + +private class ExampleListeners(private val world: InstanceContainer) : EventRegistrar { + override fun register(node: EventNode) { + node.addListener { event -> + event.spawningInstance = world + event.player.respawnPoint = SPAWN + event.player.gameMode = GameMode.ADVENTURE + } + + // Abilities are sent to the client right away, which is only possible in the play phase + node.addListener { event -> + if (event.isFirstSpawn) event.player.isAllowFlying = true + } + + // Players with the bypass permission may join a full server + node.addListener { event -> + if (event.result == AsyncPlayerCountEvent.Result.KICK_FULL && + event.player.hasPermission("example.bypass-full") + ) { + event.allow() + } + } + + // Runs on the player's virtual configuration thread, so a stored location could be loaded here + node.addListener { event -> + event.position = + SPAWN.add(Random.nextDouble(-3.0, 3.0), 0.0, Random.nextDouble(-3.0, 3.0)) + } + + node.addListener { event -> + event.joinMessage = Component.text("+ ", NamedTextColor.GREEN).append(event.player.name) + } + + node.addListener { event -> + event.quitMessage = Component.text("- ", NamedTextColor.RED).append(event.player.name) + } + + node.addListener { event -> + if (event.isFlying && event.player.isSneaking) { + event.isCancelled = true + event.player.sendMessage( + Component.text( + "No flying while sneaking!", + NamedTextColor.RED + ) + ) + } + } + + // Runs off the tick thread, so the renderer may suspend or block + AsyncPlayerChatEvent.addListener { event -> + event.renderer = ChatRenderer.viewerUnaware { _, name, message -> + Component.text() + .append(Component.text("[Example] ", NamedTextColor.AQUA)) + .append(name) + .append(Component.text(": ", NamedTextColor.GRAY)) + .append(message) + .build() + } + } + } +} + +/** + * Sends a message to everyone, keeping the signature [message] was sent with. + */ +internal fun broadcastSigned( + message: SignedMessage, + name: Component, + rendered: Component, +) { + for (player in ConnectionManager.onlinePlayers) { + player.sendSignedMessage(message, name, rendered) + } +} diff --git a/surf-api-minestom/surf-api-minestom-example/src/main/resources/log4j2.xml b/surf-api-minestom/surf-api-minestom-example/src/main/resources/log4j2.xml new file mode 100644 index 000000000..17dc8a09b --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-example/src/main/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/surf-api-minestom/surf-api-minestom-example/src/noLuckperms/kotlin/dev/slne/surf/api/minestom/example/permissions.kt b/surf-api-minestom/surf-api-minestom-example/src/noLuckperms/kotlin/dev/slne/surf/api/minestom/example/permissions.kt new file mode 100644 index 000000000..b457a14a8 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-example/src/noLuckperms/kotlin/dev/slne/surf/api/minestom/example/permissions.kt @@ -0,0 +1,12 @@ +package dev.slne.surf.api.minestom.example + +import dev.slne.surf.api.minestom.server.SurfMinestomServerBuilder +import dev.slne.surf.api.minestom.permission.PermissionProvider +import net.kyori.adventure.util.TriState + +/** + * vendor/LuckPerms has not been generated, so every player is granted every permission instead. + */ +internal fun SurfMinestomServerBuilder.withExamplePermissions() { + permissions(PermissionProvider { _, _ -> TriState.TRUE }) +} diff --git a/surf-api-minestom/surf-api-minestom-server-bootstrap/build.gradle.kts b/surf-api-minestom/surf-api-minestom-server-bootstrap/build.gradle.kts new file mode 100644 index 000000000..e59c36b7b --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-bootstrap/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + `core-convention` +} + +dependencies { + implementation(libs.gremlin.runtime) +} + +description = "surf-api-minestom-server-bootstrap" diff --git a/surf-api-minestom/surf-api-minestom-server-bootstrap/src/main/java/dev/slne/surf/api/minestom/server/bootstrap/BootstrapLogger.java b/surf-api-minestom/surf-api-minestom-server-bootstrap/src/main/java/dev/slne/surf/api/minestom/server/bootstrap/BootstrapLogger.java new file mode 100644 index 000000000..a628c5abe --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-bootstrap/src/main/java/dev/slne/surf/api/minestom/server/bootstrap/BootstrapLogger.java @@ -0,0 +1,58 @@ +package dev.slne.surf.api.minestom.server.bootstrap; + +import java.io.PrintStream; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import xyz.jpenilla.gremlin.runtime.logging.GremlinLogger; + +/** + * Logs to the console in the layout of the server's default Log4j configuration, which is not on + * the class path yet while the libraries are being installed. + * + *

Debug output, e.g. every repository a library is looked up in, is enabled with + * {@code -Dsurf.minestom.bootstrap.debug=true}.

+ */ +@NullMarked +final class BootstrapLogger implements GremlinLogger { + + private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("HH:mm:ss"); + private static final boolean DEBUG = Boolean.getBoolean("surf.minestom.bootstrap.debug"); + + @Override + public void info(final String message, final @Nullable Throwable throwable) { + this.log(System.out, "INFO", message, throwable); + } + + @Override + public void debug(final String message, final @Nullable Throwable throwable) { + if (DEBUG) { + this.log(System.out, "DEBUG", message, throwable); + } + } + + @Override + public void warn(final String message, final @Nullable Throwable throwable) { + this.log(System.err, "WARN", message, throwable); + } + + @Override + public void error(final String message, final @Nullable Throwable throwable) { + this.log(System.err, "ERROR", message, throwable); + } + + @Override + public void trace(final String message, final @Nullable Throwable throwable) { + if (DEBUG) { + this.log(System.out, "TRACE", message, throwable); + } + } + + private void log(final PrintStream stream, final String level, final String message, final @Nullable Throwable throwable) { + stream.println("[" + TIME.format(LocalTime.now()) + " " + level + "]: [Bootstrap] " + message); + if (throwable != null) { + throwable.printStackTrace(stream); + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-bootstrap/src/main/java/dev/slne/surf/api/minestom/server/bootstrap/MiniJson.java b/surf-api-minestom/surf-api-minestom-server-bootstrap/src/main/java/dev/slne/surf/api/minestom/server/bootstrap/MiniJson.java new file mode 100644 index 000000000..298526a78 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-bootstrap/src/main/java/dev/slne/surf/api/minestom/server/bootstrap/MiniJson.java @@ -0,0 +1,170 @@ +package dev.slne.surf.api.minestom.server.bootstrap; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * Parses the JSON of a {@code minestom-plugin.json} without a JSON library, so the bootstrap does + * not depend on which libraries a server jar ships. + * + *

Objects become {@link Map Maps}, arrays {@link List Lists}, numbers {@link Double Doubles}.

+ */ +@NullMarked +final class MiniJson { + + private final String text; + private int position; + + private MiniJson(final String text) { + this.text = text; + } + + static @Nullable Object parse(final String text) { + final MiniJson parser = new MiniJson(text); + final @Nullable Object value = parser.value(); + parser.skipWhitespace(); + if (parser.position != text.length()) { + throw parser.error("Unexpected content after the value"); + } + return value; + } + + private @Nullable Object value() { + this.skipWhitespace(); + if (this.position >= this.text.length()) { + throw this.error("Unexpected end of input"); + } + + return switch (this.text.charAt(this.position)) { + case '{' -> this.object(); + case '[' -> this.array(); + case '"' -> this.string(); + case 't' -> this.literal("true", Boolean.TRUE); + case 'f' -> this.literal("false", Boolean.FALSE); + case 'n' -> this.literal("null", null); + default -> this.number(); + }; + } + + private Map object() { + final Map result = new LinkedHashMap<>(); + this.position++; + this.skipWhitespace(); + if (this.peek() == '}') { + this.position++; + return result; + } + + while (true) { + this.skipWhitespace(); + final String key = this.string(); + this.skipWhitespace(); + this.expect(':'); + result.put(key, this.value()); + this.skipWhitespace(); + if (this.peek() == ',') { + this.position++; + continue; + } + this.expect('}'); + return result; + } + } + + private List<@Nullable Object> array() { + final List<@Nullable Object> result = new ArrayList<>(); + this.position++; + this.skipWhitespace(); + if (this.peek() == ']') { + this.position++; + return result; + } + + while (true) { + result.add(this.value()); + this.skipWhitespace(); + if (this.peek() == ',') { + this.position++; + continue; + } + this.expect(']'); + return result; + } + } + + private String string() { + this.expect('"'); + final StringBuilder result = new StringBuilder(); + while (true) { + if (this.position >= this.text.length()) { + throw this.error("Unterminated string"); + } + final char c = this.text.charAt(this.position++); + if (c == '"') { + return result.toString(); + } + if (c != '\\') { + result.append(c); + continue; + } + + final char escaped = this.text.charAt(this.position++); + switch (escaped) { + case 'b' -> result.append('\b'); + case 'f' -> result.append('\f'); + case 'n' -> result.append('\n'); + case 'r' -> result.append('\r'); + case 't' -> result.append('\t'); + case 'u' -> { + result.append((char) Integer.parseInt(this.text.substring(this.position, this.position + 4), 16)); + this.position += 4; + } + default -> result.append(escaped); + } + } + } + + private Double number() { + final int start = this.position; + while (this.position < this.text.length() && "+-0123456789.eE".indexOf(this.text.charAt(this.position)) >= 0) { + this.position++; + } + if (start == this.position) { + throw this.error("Unexpected character '" + this.text.charAt(start) + "'"); + } + return Double.parseDouble(this.text.substring(start, this.position)); + } + + private @Nullable Object literal(final String literal, final @Nullable Object value) { + if (!this.text.startsWith(literal, this.position)) { + throw this.error("Expected " + literal); + } + this.position += literal.length(); + return value; + } + + private char peek() { + return this.position < this.text.length() ? this.text.charAt(this.position) : '\0'; + } + + private void expect(final char c) { + if (this.peek() != c) { + throw this.error("Expected '" + c + "'"); + } + this.position++; + } + + private void skipWhitespace() { + while (this.position < this.text.length() && Character.isWhitespace(this.text.charAt(this.position))) { + this.position++; + } + } + + private IllegalArgumentException error(final String message) { + return new IllegalArgumentException(message + " at position " + this.position); + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-bootstrap/src/main/java/dev/slne/surf/api/minestom/server/bootstrap/ServerPlugins.java b/surf-api-minestom/surf-api-minestom-server-bootstrap/src/main/java/dev/slne/surf/api/minestom/server/bootstrap/ServerPlugins.java new file mode 100644 index 000000000..1af775c34 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-bootstrap/src/main/java/dev/slne/surf/api/minestom/server/bootstrap/ServerPlugins.java @@ -0,0 +1,253 @@ +package dev.slne.surf.api.minestom.server.bootstrap; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.lang.instrument.Instrumentation; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * Puts the plugins the server's own code uses on the system class path. + * + *

A plugin in {@code plugins/} normally gets a class loader of its own, which the server cannot + * see into. The plugins the server declares in {@value #ATTRIBUTE} (and the plugins they depend + * on) are appended to the system class path instead, and the plugins feature loads them from + * there. The bootstrap passes their jar paths to the plugins feature in the + * {@value #LOADED_PROPERTY} system property.

+ */ +@NullMarked +final class ServerPlugins { + + /** + * The manifest attribute listing the plugin IDs, comma separated. The ID of an optional plugin + * ends in {@code ?}. + */ + static final String ATTRIBUTE = "Surf-Plugin-Dependencies"; + + /** + * The system property overriding the {@code plugins} directory. It has to match the directory + * configured in {@code withPlugins}. + */ + static final String DIRECTORY_PROPERTY = "surf.minestom.plugins"; + + /** + * The system property the appended jars are listed in, separated by the path separator. + */ + static final String LOADED_PROPERTY = "surf.minestom.server-plugins"; + + private static final String PLUGIN_FILE = "minestom-plugin.json"; + private static final String CLASSPATH_INDEX = "META-INF/services/dev.slne.surf.api.minestom.plugin.SurfMinestomPlugin"; + private static final String CLASSPATH_DIRECTORY = "META-INF/surf-minestom-plugins/"; + + /** + * The format of a plugin ID, kept in sync with {@code PluginMeta.ID_REGEX}. + */ + private static final Pattern PLUGIN_ID = Pattern.compile("[a-z][a-z0-9-_]{0,63}"); + + private ServerPlugins() { + } + + static void install(final String declared, final Instrumentation instrumentation, final BootstrapLogger logger) { + final Path directory = Path.of(System.getProperty(DIRECTORY_PROPERTY, "plugins")).toAbsolutePath().normalize(); + final Map available = scan(directory, logger); + final Set onClassPath = classPathPluginIds(); + + final Map selected = new LinkedHashMap<>(); + for (final Requirement root : parse(declared, "the server")) { + final Map added = new LinkedHashMap<>(); + final @Nullable Requirement missing = resolve(root, available, onClassPath, selected, added); + if (missing == null) { + selected.putAll(added); + continue; + } + + final String message = "The plugin " + missing.id() + ", which " + missing.requiredBy() + + " depends on, is missing in " + directory; + if (!root.optional()) { + throw new IllegalStateException(message); + } + logger.warn("Not adding the optional plugin " + root.id() + " to the server's class path. " + message); + } + + final List paths = new ArrayList<>(); + for (final PluginJar jar : selected.values()) { + try { + instrumentation.appendToSystemClassLoaderSearch(new JarFile(jar.path().toFile())); + } catch (final IOException e) { + throw new UncheckedIOException("Failed to add " + jar.path() + " to the class path", e); + } + paths.add(jar.path().toString()); + } + + if (!paths.isEmpty()) { + logger.info("Added the plugins " + String.join(", ", selected.keySet()) + " to the server's class path"); + } + System.setProperty(LOADED_PROPERTY, String.join(File.pathSeparator, paths)); + } + + /** + * Collects {@code root} and the plugins it requires into {@code added}, skipping the ones + * already in {@code selected} or on the class path. + * + * @return the first required plugin that is missing, or {@code null} when everything + * {@code root} needs is available; a missing {@code root} that is optional is not reported + */ + private static @Nullable Requirement resolve( + final Requirement root, + final Map available, + final Set onClassPath, + final Map selected, + final Map added + ) { + final Deque pending = new ArrayDeque<>(List.of(root)); + while (!pending.isEmpty()) { + final Requirement requirement = pending.removeFirst(); + final String id = requirement.id(); + if (selected.containsKey(id) || added.containsKey(id) || onClassPath.contains(id)) { + continue; + } + + final @Nullable PluginJar jar = available.get(id); + if (jar == null) { + if (!requirement.optional()) { + return requirement; + } + continue; + } + + added.put(id, jar); + pending.addAll(jar.dependencies()); + } + return null; + } + + private static Map scan(final Path directory, final BootstrapLogger logger) { + if (!Files.isDirectory(directory)) { + return Map.of(); + } + + final Map result = new LinkedHashMap<>(); + try (Stream files = Files.list(directory)) { + for (final Path path : files.sorted().toList()) { + if (!Files.isRegularFile(path) || !path.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".jar")) { + continue; + } + + try (JarFile jar = new JarFile(path.toFile())) { + final @Nullable JarEntry entry = jar.getJarEntry(PLUGIN_FILE); + if (entry == null) { + continue; + } + + final Map meta = readObject(jar.getInputStream(entry)); + if (!(meta.get("id") instanceof final String id) || !PLUGIN_ID.matcher(id).matches()) { + logger.warn("Skipping " + path.getFileName() + ", its " + PLUGIN_FILE + " has no valid plugin id"); + continue; + } + + result.putIfAbsent(id, new PluginJar(path, dependencies(meta, id))); + } catch (final IOException | RuntimeException e) { + // The plugins feature reports the same failure when it skips the jar + logger.warn("Could not read the plugin " + path.getFileName(), e); + } + } + } catch (final IOException e) { + throw new UncheckedIOException("Failed to list " + directory, e); + } + return result; + } + + /** + * The IDs of the plugins shaded into the server, which need no jar from the {@code plugins} + * directory. + */ + private static Set classPathPluginIds() { + final ClassLoader loader = ClassLoader.getSystemClassLoader(); + final Set ids = new HashSet<>(); + try { + for (final URL index : Collections.list(loader.getResources(CLASSPATH_INDEX))) { + try (InputStream stream = index.openStream()) { + for (final String line : new String(stream.readAllBytes(), StandardCharsets.UTF_8).split("\n")) { + final String mainClass = line.split("#", 2)[0].trim(); + if (mainClass.isEmpty()) { + continue; + } + + final @Nullable URL meta = loader.getResource(CLASSPATH_DIRECTORY + mainClass + ".json"); + if (meta != null) { + ids.add(String.valueOf(readObject(meta.openStream()).get("id"))); + } + } + } + } + } catch (final IOException e) { + throw new UncheckedIOException("Failed to read the plugins on the class path", e); + } + return ids; + } + + private static List dependencies(final Map meta, final String id) { + final List result = new ArrayList<>(); + if (meta.get("dependencies") instanceof final List dependencies) { + for (final Object dependency : dependencies) { + if (dependency instanceof final Map map) { + result.add(new Requirement( + String.valueOf(map.get("id")), + Boolean.TRUE.equals(map.get("optional")), + "the plugin " + id + )); + } + } + } + return result; + } + + private static List parse(final String declared, final String requiredBy) { + final Set result = new LinkedHashSet<>(); + for (final String entry : declared.split(",")) { + final String trimmed = entry.trim(); + if (trimmed.isEmpty()) { + continue; + } + final boolean optional = trimmed.endsWith("?"); + result.add(new Requirement(optional ? trimmed.substring(0, trimmed.length() - 1) : trimmed, optional, requiredBy)); + } + return List.copyOf(result); + } + + private static Map readObject(final InputStream input) throws IOException { + try (input) { + if (MiniJson.parse(new String(input.readAllBytes(), StandardCharsets.UTF_8)) instanceof final Map map) { + return map; + } + throw new IllegalArgumentException(PLUGIN_FILE + " is no JSON object"); + } + } + + private record PluginJar(Path path, List dependencies) { + } + + private record Requirement(String id, boolean optional, String requiredBy) { + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-bootstrap/src/main/java/dev/slne/surf/api/minestom/server/bootstrap/SurfMinestomBootstrap.java b/surf-api-minestom/surf-api-minestom-server-bootstrap/src/main/java/dev/slne/surf/api/minestom/server/bootstrap/SurfMinestomBootstrap.java new file mode 100644 index 000000000..6774a3280 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-bootstrap/src/main/java/dev/slne/surf/api/minestom/server/bootstrap/SurfMinestomBootstrap.java @@ -0,0 +1,174 @@ +package dev.slne.surf.api.minestom.server.bootstrap; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.instrument.Instrumentation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import xyz.jpenilla.gremlin.runtime.DependencyCache; +import xyz.jpenilla.gremlin.runtime.DependencyResolver; +import xyz.jpenilla.gremlin.runtime.DependencySet; + +/** + * The bootstrap agent of a surf Minestom server jar, registered as its + * {@code Launcher-Agent-Class}. + * + *

The server jar contains only the server's own code. The remaining libraries (Minestom, + * Adventure, fastutil, ...) are listed in the {@value #DEPENDENCIES_FILE} gremlin writes at build + * time, downloaded into {@code libraries/} on the first start and appended to the system class + * path before {@code main} runs.

+ * + *

Plugins listed in the {@value ServerPlugins#ATTRIBUTE} manifest attribute are appended to the + * system class path as well, see {@link ServerPlugins}.

+ * + *

A jar has a single {@code Launcher-Agent-Class}, so the agents of installed features (e.g. + * the one LuckPerms loads its libraries with) are listed as delegate agents in + * {@value #DELEGATE_AGENTS_ATTRIBUTE}. The bootstrap agent calls them in listed order after the + * libraries and plugins are on the system class path. A delegate agent that installs class + * transformers, such as {@code SurfMixinAgent}, has to be listed first so the transformer exists + * before any other agent loads a Minestom class.

+ */ +@NullMarked +public final class SurfMinestomBootstrap { + + /** + * The gremlin dependency set listing the libraries to download, at the root of the server jar. + */ + public static final String DEPENDENCIES_FILE = "dependencies.txt"; + + /** + * The manifest attribute naming the agents to call after the libraries are installed. + */ + public static final String DELEGATE_AGENTS_ATTRIBUTE = "Surf-Delegate-Agent-Classes"; + + /** + * The system property overriding the directory the libraries are downloaded to. + */ + public static final String LIBRARIES_PROPERTY = "surf.minestom.libraries"; + + /** + * The Minestom server properties that may change at runtime, see + * {@code SurfMinestomServerBuilder.recommendedServerProperties}. Minestom freezes a property's + * value when it is first read unless {@code .mutable} is set before that. + */ + private static final List MUTABLE_SERVER_PROPERTIES = List.of("minestom.keep-alive-delay"); + + private static final BootstrapLogger LOGGER = new BootstrapLogger(); + + private SurfMinestomBootstrap() { + } + + /** + * Called by the JVM for the {@code Launcher-Agent-Class} of a jar started with {@code java -jar}. + */ + public static void agentmain(final @Nullable String args, final Instrumentation instrumentation) { + try { + for (final String property : MUTABLE_SERVER_PROPERTIES) { + if (System.getProperty(property + ".mutable") == null) { + System.setProperty(property + ".mutable", "true"); + } + } + + final Attributes manifest = ownManifest(); + installLibraries(instrumentation); + + final @Nullable String serverPlugins = manifest.getValue(ServerPlugins.ATTRIBUTE); + if (serverPlugins != null) { + ServerPlugins.install(serverPlugins, instrumentation, LOGGER); + } + + callDelegateAgents(manifest.getValue(DELEGATE_AGENTS_ATTRIBUTE), args, instrumentation); + } catch (final Throwable e) { + // An exception thrown from the bootstrap agent is buried under the JVM's own messages + LOGGER.error("Failed to start the server", e); + System.exit(1); + } + } + + /** + * Called by the JVM when the server jar is passed as {@code -javaagent}. + */ + public static void premain(final @Nullable String args, final Instrumentation instrumentation) { + agentmain(args, instrumentation); + } + + private static void installLibraries(final Instrumentation instrumentation) { + final ClassLoader loader = ClassLoader.getSystemClassLoader(); + + // Jars with every library shaded in have no dependency set + if (loader.getResource(DEPENDENCIES_FILE) == null) { + return; + } + + final DependencySet dependencies = DependencySet.readDefault(loader); + final DependencyCache cache = new DependencyCache( + Path.of(System.getProperty(LIBRARIES_PROPERTY, "libraries")) + ); + + try (DependencyResolver resolver = new DependencyResolver(LOGGER)) { + // The resolved set is unordered; a fixed order keeps the class path identical between starts + for (final Path jar : resolver.resolve(dependencies, cache).jarFiles().stream().sorted().toList()) { + try { + instrumentation.appendToSystemClassLoaderSearch(new JarFile(jar.toFile())); + } catch (final IOException e) { + throw new UncheckedIOException("Failed to add " + jar + " to the class path", e); + } + } + } + + cache.cleanup(); + } + + private static void callDelegateAgents( + final @Nullable String agents, + final @Nullable String args, + final Instrumentation instrumentation + ) { + if (agents == null) { + return; + } + + Arrays.stream(agents.split(",")) + .map(String::trim) + .filter(agent -> !agent.isEmpty()) + .forEach(agent -> callAgent(agent, args, instrumentation)); + } + + private static void callAgent(final String className, final @Nullable String args, final Instrumentation instrumentation) { + try { + final Class agent = Class.forName(className, true, ClassLoader.getSystemClassLoader()); + Method entryPoint; + try { + entryPoint = agent.getMethod("agentmain", String.class, Instrumentation.class); + } catch (final NoSuchMethodException e) { + entryPoint = agent.getMethod("premain", String.class, Instrumentation.class); + } + entryPoint.invoke(null, args, instrumentation); + } catch (final InvocationTargetException e) { + throw new IllegalStateException("Agent " + className + " failed", e.getCause()); + } catch (final ReflectiveOperationException e) { + throw new IllegalStateException("Failed to call agent " + className, e); + } + } + + private static Attributes ownManifest() { + try { + final Path jar = Path.of(SurfMinestomBootstrap.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + try (JarFile jarFile = new JarFile(jar.toFile())) { + final @Nullable Manifest manifest = jarFile.getManifest(); + return manifest == null ? new Attributes() : manifest.getMainAttributes(); + } + } catch (final IOException | URISyntaxException e) { + throw new IllegalStateException("Failed to read the manifest of the server jar", e); + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-console/api/surf-api-minestom-server-console.api b/surf-api-minestom/surf-api-minestom-server-console/api/surf-api-minestom-server-console.api new file mode 100644 index 000000000..353ffb0c0 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-console/api/surf-api-minestom-server-console.api @@ -0,0 +1,25 @@ +public final class dev/slne/surf/api/minestom/server/console/ConsoleFeature : dev/slne/surf/api/minestom/server/SurfMinestomFeature { + public static final field Companion Ldev/slne/surf/api/minestom/server/console/ConsoleFeature$Companion; + public static final field ID Ljava/lang/String; + public fun disable (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public fun enable (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public fun getId ()Ljava/lang/String; + public fun getLoadPriority ()I + public fun load (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V +} + +public final class dev/slne/surf/api/minestom/server/console/ConsoleFeature$Companion { +} + +public final class dev/slne/surf/api/minestom/server/console/ConsoleFeatureBuilder { + public final fun getOnShutdown ()Lkotlin/jvm/functions/Function0; + public final fun getThreadName ()Ljava/lang/String; + public final fun setOnShutdown (Lkotlin/jvm/functions/Function0;)V + public final fun setThreadName (Ljava/lang/String;)V +} + +public final class dev/slne/surf/api/minestom/server/console/ConsoleFeatureKt { + public static final fun withConsole (Ldev/slne/surf/api/minestom/server/SurfMinestomServerBuilder;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun withConsole$default (Ldev/slne/surf/api/minestom/server/SurfMinestomServerBuilder;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V +} + diff --git a/surf-api-minestom/surf-api-minestom-server-console/build.gradle.kts b/surf-api-minestom/surf-api-minestom-server-console/build.gradle.kts new file mode 100644 index 000000000..ca9d1d7ee --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-console/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + `core-convention` + `api-validation` +} + +dependencies { + api(projects.surfApiMinestom.surfApiMinestomServer) + compileOnlyApi(libs.minestom.server) + api(libs.terminal.console.appender) + api(libs.log4j.core) +} + +description = "surf-api-minestom-server-console" diff --git a/surf-api-minestom/surf-api-minestom-server-console/src/main/kotlin/dev/slne/surf/api/minestom/server/console/ConsoleFeature.kt b/surf-api-minestom/surf-api-minestom-server-console/src/main/kotlin/dev/slne/surf/api/minestom/server/console/ConsoleFeature.kt new file mode 100644 index 000000000..8f06da31e --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-console/src/main/kotlin/dev/slne/surf/api/minestom/server/console/ConsoleFeature.kt @@ -0,0 +1,91 @@ +package dev.slne.surf.api.minestom.server.console + +import dev.slne.surf.api.minestom.server.SurfMinestomFeature +import dev.slne.surf.api.minestom.server.SurfMinestomServer +import dev.slne.surf.api.minestom.server.SurfMinestomServerBuilder +import dev.slne.surf.api.minestom.server.SurfMinestomServerDsl +import net.minecrell.terminalconsole.SimpleTerminalConsole +import net.minestom.server.MinecraftServer +import kotlin.system.exitProcess + +/** + * Configures [withConsole]. + */ +@SurfMinestomServerDsl +class ConsoleFeatureBuilder internal constructor() { + + /** + * The name of the thread reading the console. + */ + var threadName: String = "surf-minestom-console" + + /** + * Runs when the console asks the server to stop, e.g. on Ctrl+C or end of input. + * + * Stops the server and exits the process by default. + */ + var onShutdown: () -> Unit = { + MinecraftServer.stopCleanly() + exitProcess(0) + } +} + +/** + * Reads commands from the terminal and runs them as the console, with line editing and history. + * + * Commands reach Minestom's command manager, so CommandAPI commands work as well. For colored + * output that does not interleave with the input line, log through Log4j's `TerminalConsole` + * appender, e.g. with a `log4j2.xml` such as: + * + * ``` + * + * + * + * + * + * ``` + */ +class ConsoleFeature internal constructor( + private val settings: ConsoleFeatureBuilder, +) : SurfMinestomFeature { + + override val id: String = ID + + override fun enable(api: SurfMinestomServer) { + Thread(SurfTerminalConsole(settings.onShutdown)::start, settings.threadName).apply { + isDaemon = true + start() + } + } + + override fun disable(api: SurfMinestomServer) { + // The console thread is not interrupted: the shutdown may have been started from it. The + // daemon thread stops once the server is no longer started. + } + + private class SurfTerminalConsole( + private val shutdownHandler: () -> Unit, + ) : SimpleTerminalConsole() { + override fun isRunning(): Boolean = MinecraftServer.isStarted() + + override fun runCommand(command: String) { + val commandManager = MinecraftServer.getCommandManager() + commandManager.execute(commandManager.consoleSender, command) + } + + override fun shutdown() = shutdownHandler() + } + + companion object { + const val ID = "console" + } +} + +/** + * Reads commands from the terminal and runs them as the console. + * + * @see ConsoleFeature + */ +fun SurfMinestomServerBuilder.withConsole(block: ConsoleFeatureBuilder.() -> Unit = {}) { + install(ConsoleFeature(ConsoleFeatureBuilder().apply(block))) +} diff --git a/surf-api-minestom/surf-api-minestom-server-luckperms/api/surf-api-minestom-server-luckperms.api b/surf-api-minestom/surf-api-minestom-server-luckperms/api/surf-api-minestom-server-luckperms.api new file mode 100644 index 000000000..cd795b64d --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-luckperms/api/surf-api-minestom-server-luckperms.api @@ -0,0 +1,30 @@ +public final class dev/slne/surf/api/minestom/server/luckperms/LuckPermsFeature : dev/slne/surf/api/minestom/server/SurfMinestomFeature { + public static final field Companion Ldev/slne/surf/api/minestom/server/luckperms/LuckPermsFeature$Companion; + public static final field ID Ljava/lang/String; + public fun disable (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public fun enable (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public fun getId ()Ljava/lang/String; + public fun getLoadPriority ()I + public final fun getLuckPerms ()Lnet/luckperms/api/LuckPerms; + public fun load (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public final fun loadedUser (Ljava/util/UUID;)Lnet/luckperms/api/model/user/User; + public final fun permissionValue (Ljava/util/UUID;Ljava/lang/String;)Lnet/kyori/adventure/util/TriState; +} + +public final class dev/slne/surf/api/minestom/server/luckperms/LuckPermsFeature$Companion { + public final fun getInstance ()Ldev/slne/surf/api/minestom/server/luckperms/LuckPermsFeature; +} + +public final class dev/slne/surf/api/minestom/server/luckperms/LuckPermsFeatureBuilder { + public final fun customize (Lkotlin/jvm/functions/Function1;)V + public final fun getDataDirectory ()Ljava/nio/file/Path; + public final fun getRegisterCommands ()Z + public final fun setDataDirectory (Ljava/nio/file/Path;)V + public final fun setRegisterCommands (Z)V +} + +public final class dev/slne/surf/api/minestom/server/luckperms/LuckPermsFeatureKt { + public static final fun withLuckPerms (Ldev/slne/surf/api/minestom/server/SurfMinestomServerBuilder;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun withLuckPerms$default (Ldev/slne/surf/api/minestom/server/SurfMinestomServerBuilder;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V +} + diff --git a/surf-api-minestom/surf-api-minestom-server-luckperms/build.gradle.kts b/surf-api-minestom/surf-api-minestom-server-luckperms/build.gradle.kts new file mode 100644 index 000000000..59d834e7f --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-luckperms/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + `vendor-bundle-convention` + `api-validation` +} + +dependencies { + api(projects.surfApiMinestom.surfApiMinestomServer) + compileOnlyApi(libs.minestom.server) + bundled(libs.minestom.luckperms) +} + +description = "surf-api-minestom-server-luckperms" diff --git a/surf-api-minestom/surf-api-minestom-server-luckperms/src/main/kotlin/dev/slne/surf/api/minestom/server/luckperms/LuckPermsFeature.kt b/surf-api-minestom/surf-api-minestom-server-luckperms/src/main/kotlin/dev/slne/surf/api/minestom/server/luckperms/LuckPermsFeature.kt new file mode 100644 index 000000000..e551c0bfe --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-luckperms/src/main/kotlin/dev/slne/surf/api/minestom/server/luckperms/LuckPermsFeature.kt @@ -0,0 +1,139 @@ +package dev.slne.surf.api.minestom.server.luckperms + +import dev.slne.surf.api.minestom.server.SurfMinestomServer +import dev.slne.surf.api.minestom.server.SurfMinestomServerBuilder +import dev.slne.surf.api.minestom.server.SurfMinestomServerDsl +import dev.slne.surf.api.minestom.server.SurfMinestomFeature +import dev.slne.surf.api.minestom.server.luckperms.config.LuckPermsConfigAdapter +import dev.slne.surf.api.minestom.permission.MinestomPermissions +import dev.slne.surf.api.minestom.permission.PermissionProvider +import me.lucko.luckperms.minestom.CommandRegistry +import me.lucko.luckperms.minestom.LuckPermsMinestom +import net.kyori.adventure.util.TriState +import net.luckperms.api.LuckPerms +import net.luckperms.api.model.user.User +import net.luckperms.api.util.Tristate +import java.nio.file.Path +import java.util.* +import kotlin.io.path.Path +import kotlin.io.path.div + +/** + * Configures [withLuckPerms]. + */ +@SurfMinestomServerDsl +class LuckPermsFeatureBuilder internal constructor() { + + /** + * Where LuckPerms keeps its configuration and data. + */ + var dataDirectory: Path = Path("plugins/luckperms") + + /** + * Whether the `/lp` commands are registered. + */ + var registerCommands: Boolean = true + + internal val customizers = mutableListOf Unit>() + + /** + * Customizes the LuckPerms builder, e.g. to add context providers or permission suggestions. + */ + fun customize(block: LuckPermsMinestom.Builder.() -> Unit) { + customizers += block + } +} + +/** + * Runs LuckPerms on this server and resolves every permission check of the surf api with it. + * + * LuckPerms downloads its libraries at runtime and adds them to the system class path through the + * instrumentation API. The server jar therefore has to declare the LuckPerms agent in the + * manifest: + * + * ``` + * Launcher-Agent-Class: me.lucko.luckperms.minestom.dependencies.LuckPermsAgent + * ``` + * + * When the server jar already has a `Launcher-Agent-Class` of its own, call + * `LuckPermsAgent.agentmain(args, instrumentation)` from that agent instead. Servers built with + * the `dev.slne.surf.api.gradle.minestom-server` Gradle plugin need neither: the plugin lists the + * LuckPerms agent in `Surf-Delegate-Agent-Classes`, and the bootstrap agent calls it. + */ +class LuckPermsFeature internal constructor( + private val settings: LuckPermsFeatureBuilder, +) : SurfMinestomFeature { + + override val id: String = ID + + private var instance: LuckPerms? = null + + /** + * The LuckPerms api; throws [IllegalStateException] before [load] and after [disable]. + */ + val luckPerms: LuckPerms + get() = checkNotNull(instance) { "LuckPerms has not been loaded yet" } + + override fun load(api: SurfMinestomServer) { + val dataDirectory = settings.dataDirectory + val builder = LuckPermsMinestom.builder(dataDirectory) + .configurationAdapter { plugin -> + LuckPermsConfigAdapter(plugin, dataDirectory / "config.yml") + } + + if (settings.registerCommands) { + builder.commandRegistry(CommandRegistry.minestom()) + } + settings.customizers.forEach { customizer -> builder.customizer() } + + instance = builder.enable() + MinestomPermissions.installProvider(PermissionProvider { player, permission -> + permissionValue(player.uuid, permission) + }) + } + + override fun disable(api: SurfMinestomServer) { + if (instance == null) return + + instance = null + LuckPermsMinestom.disable() + } + + /** + * The value of [permission] for the loaded user [uuid]; unset while the user is not loaded. + */ + fun permissionValue(uuid: UUID, permission: String): TriState { + val user = loadedUser(uuid) ?: return TriState.NOT_SET + + return when (user.cachedData.permissionData.checkPermission(permission)) { + Tristate.TRUE -> TriState.TRUE + Tristate.FALSE -> TriState.FALSE + Tristate.UNDEFINED -> TriState.NOT_SET + } + } + + /** + * The user [uuid], if LuckPerms has them loaded. + */ + fun loadedUser(uuid: UUID): User? = instance?.userManager?.getUser(uuid) + + companion object { + const val ID = "luckperms" + + /** + * The installed LuckPerms feature. + */ + val instance: LuckPermsFeature + get() = SurfMinestomServer.instance.feature(ID) as? LuckPermsFeature + ?: error("LuckPerms is not installed, call withLuckPerms() in surfMinestomServer { }") + } +} + +/** + * Runs LuckPerms on this server and resolves every permission check of the surf api with it. + * + * @see LuckPermsFeature + */ +fun SurfMinestomServerBuilder.withLuckPerms(block: LuckPermsFeatureBuilder.() -> Unit = {}) { + install(LuckPermsFeature(LuckPermsFeatureBuilder().apply(block))) +} diff --git a/surf-api-minestom/surf-api-minestom-server-luckperms/src/main/kotlin/dev/slne/surf/api/minestom/server/luckperms/config/LuckPermsConfigAdapter.kt b/surf-api-minestom/surf-api-minestom-server-luckperms/src/main/kotlin/dev/slne/surf/api/minestom/server/luckperms/config/LuckPermsConfigAdapter.kt new file mode 100644 index 000000000..1a1750224 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-luckperms/src/main/kotlin/dev/slne/surf/api/minestom/server/luckperms/config/LuckPermsConfigAdapter.kt @@ -0,0 +1,94 @@ +package dev.slne.surf.api.minestom.server.luckperms.config + +import io.leangen.geantyref.TypeToken +import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter +import me.lucko.luckperms.common.plugin.LuckPermsPlugin +import org.spongepowered.configurate.ConfigurationNode +import org.spongepowered.configurate.kotlin.extensions.getList +import org.spongepowered.configurate.yaml.NodeStyle +import org.spongepowered.configurate.yaml.YamlConfigurationLoader +import java.io.FileOutputStream +import java.nio.file.Path +import kotlin.io.path.createParentDirectories +import kotlin.io.path.exists + +internal class LuckPermsConfigAdapter( + private val plugin: LuckPermsPlugin, + private val path: Path +) : ConfigurationAdapter { + + private val loader = YamlConfigurationLoader.builder() + .path(path) + .nodeStyle(NodeStyle.BLOCK) + .build() + + private var root: ConfigurationNode? = null + + init { + if (!path.exists()) { + val default = LuckPermsConfigAdapter::class.java.getResourceAsStream(DEFAULT_CONFIG) + ?: error("Default LuckPerms configuration not found in resources") + path.createParentDirectories() + FileOutputStream(path.toFile()).use { outputStream -> + default.transferTo(outputStream) + } + } + + reload() + } + + override fun reload() { + root = loader.load() + } + + override fun getString(path: String, def: String?): String? { + val node = resolvePath(path) + return if (def != null) node.getString(def) else node.string + } + + override fun getInteger(path: String, def: Int): Int { + return resolvePath(path).getInt(def) + } + + override fun getBoolean(path: String, def: Boolean): Boolean { + return resolvePath(path).getBoolean(def) + } + + override fun getStringList( + path: String, + def: List? + ): List? { + val node = resolvePath(path) + if (node.virtual() || !node.isList) { + return def + } + + return if (def == null) node.getList(String::class) else node.getList(String::class, def) + } + + override fun getStringMap( + path: String, + def: Map? + ): Map? { + val node = resolvePath(path) + if (node.virtual()) { + return def + } + val type = object : TypeToken>() {} + return if (def == null) node.get(type) else node.get(type, def) + } + + private fun resolvePath(path: String): ConfigurationNode { + val root = root + requireNotNull(root) { "Config is not loaded." } + return root.node(*path.split(".").toTypedArray()) + } + + override fun getPlugin(): LuckPermsPlugin { + return plugin + } + + private companion object { + const val DEFAULT_CONFIG = "/dev/slne/surf/api/minestom/luckperms/default-config.yml" + } +} \ No newline at end of file diff --git a/surf-api-minestom/surf-api-minestom-server-luckperms/src/main/resources/dev/slne/surf/api/minestom/luckperms/default-config.yml b/surf-api-minestom/surf-api-minestom-server-luckperms/src/main/resources/dev/slne/surf/api/minestom/luckperms/default-config.yml new file mode 100644 index 000000000..2d8aa32d5 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-luckperms/src/main/resources/dev/slne/surf/api/minestom/luckperms/default-config.yml @@ -0,0 +1,619 @@ +#################################################################################################### +# +----------------------------------------------------------------------------------------------+ # +# | __ __ ___ __ __ | # +# | | | | / ` |__/ |__) |__ |__) |\/| /__` | # +# | |___ \__/ \__, | \ | |___ | \ | | .__/ | # +# | | # +# | https://luckperms.net | # +# | | # +# | WIKI: https://luckperms.net/wiki | # +# | DISCORD: https://discord.gg/luckperms | # +# | BUG REPORTS: https://github.com/LuckPerms/LuckPerms/issues | # +# | | # +# | Each option in this file is documented and explained here: | # +# | ==> https://luckperms.net/wiki/Configuration | # +# | | # +# | New options are not added to this file automatically. Default values are used if an | # +# | option cannot be found. The latest config versions can be obtained at the link above. | # +# +----------------------------------------------------------------------------------------------+ # +#################################################################################################### + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | ESSENTIAL SETTINGS | # +# | | # +# | Important settings that control how LuckPerms functions. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# The name of the server, used for server specific permissions. +# +# - When set to "global" this setting is effectively ignored. +# - In all other cases, the value here is added to all players in a "server" context. +# - See: https://luckperms.net/wiki/Context +server: global + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | STORAGE SETTINGS | # +# | | # +# | Controls which storage method LuckPerms will use to store data. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# How the plugin should store data +# +# - The various options are explained in more detail on the wiki: +# https://luckperms.net/wiki/Storage-types +# +# - Possible options: +# +# | Remote databases - require connection information to be configured below +# |=> MySQL +# |=> MariaDB (preferred over MySQL) +# |=> PostgreSQL +# |=> MongoDB +# +# | Flatfile/local database - don't require any extra configuration +# |=> H2 (preferred over SQLite) +# |=> SQLite +# +# | Readable & editable text files - don't require any extra configuration +# |=> YAML (.yml files) +# |=> JSON (.json files) +# |=> HOCON (.conf files) +# |=> TOML (.toml files) +# | +# | By default, user, group and track data is separated into different files. Data can be combined +# | and all stored in the same file by switching to a combined storage variant. +# | Just add '-combined' to the end of the storage-method, e.g. 'yaml-combined' +# +# - A H2 database is the default option. +# - If you want to edit data manually in "traditional" storage files, we suggest using YAML. +storage-method: h2 + +# The following block defines the settings for remote database storage methods. +# +# - You don't need to touch any of the settings here if you're using a local storage method! +# - The connection detail options are shared between all remote storage types. +data: + + # Define the address and port for the database. + # - The standard DB engine port is used by default + # (MySQL: 3306, PostgreSQL: 5432, MongoDB: 27017) + # - Specify as "host:port" if differs + address: localhost + + # The name of the database to store LuckPerms data in. + # - This must be created already. Don't worry about this setting if you're using MongoDB. + database: minecraft + + # Credentials for the database. + username: root + password: '' + + # These settings apply to the MySQL connection pool. + # - The default values will be suitable for the majority of users. + # - Do not change these settings unless you know what you're doing! + pool-settings: + + # Sets the maximum size of the MySQL connection pool. + # - Basically this value will determine the maximum number of actual + # connections to the database backend. + # - More information about determining the size of connection pools can be found here: + # https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing + maximum-pool-size: 10 + + # Sets the minimum number of idle connections that the pool will try to maintain. + # - For maximum performance and responsiveness to spike demands, it is recommended to not set + # this value and instead allow the pool to act as a fixed size connection pool. + # (set this value to the same as 'maximum-pool-size') + minimum-idle: 10 + + # This setting controls the maximum lifetime of a connection in the pool in milliseconds. + # - The value should be at least 30 seconds less than any database or infrastructure imposed + # connection time limit. + maximum-lifetime: 1800000 # 30 minutes + + # This setting controls how frequently the pool will 'ping' a connection in order to prevent it + # from being timed out by the database or network infrastructure, measured in milliseconds. + # - The value should be less than maximum-lifetime and greater than 30000 (30 seconds). + # - Setting the value to zero will disable the keepalive functionality. + keepalive-time: 0 + + # This setting controls the maximum number of milliseconds that the plugin will wait for a + # connection from the pool, before timing out. + connection-timeout: 5000 # 5 seconds + + # This setting allows you to define extra properties for connections. + # + # By default, the following options are set to enable utf8 encoding. (you may need to remove + # these if you are using PostgreSQL) + # useUnicode: true + # characterEncoding: utf8 + # + # You can also use this section to disable SSL connections, by uncommenting the 'useSSL' and + # 'verifyServerCertificate' options below. + properties: + useUnicode: true + characterEncoding: utf8 + #useSSL: false + #verifyServerCertificate: false + + # The prefix for all LuckPerms SQL tables. + # + # - This only applies for remote SQL storage types (MySQL, MariaDB, etc). + # - Change this if you want to use different tables for different servers. + table-prefix: 'luckperms_' + + # The prefix to use for all LuckPerms MongoDB collections. + # + # - This only applies for the MongoDB storage type. + # - Change this if you want to use different collections for different servers. The default is no + # prefix. + mongodb-collection-prefix: '' + + # The connection string URI to use to connect to the MongoDB instance. + # + # - When configured, this setting will override anything defined in the address, database, + # username or password fields above. + # - If you have a connection string that starts with 'mongodb://' or 'mongodb+srv://', enter it + # below. + # - For more information, please see https://docs.mongodb.com/manual/reference/connection-string/ + mongodb-connection-uri: '' + +# Define settings for a "split" storage setup. +# +# - This allows you to define a storage method for each type of data. +# - The connection options above still have to be correct for each type here. +split-storage: + # Don't touch this if you don't want to use split storage! + enabled: false + methods: + # These options don't need to be modified if split storage isn't enabled. + user: h2 + group: h2 + track: h2 + uuid: h2 + log: h2 + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | UPDATE PROPAGATION & MESSAGING SERVICE | # +# | | # +# | Controls the ways in which LuckPerms will sync data & notify other servers of changes. | # +# | These options are documented on greater detail on the wiki under "Instant Updates". | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# This option controls how frequently LuckPerms will perform a sync task. +# +# - A sync task will refresh all data from the storage, and ensure that the most up-to-date data is +# being used by the plugin. +# - This is disabled by default, as most users will not need it. However, if you're using a remote +# storage type without a messaging service setup, you may wish to set this to something like 3. +# - Set to -1 to disable the task completely. +sync-minutes: -1 + +# If the file watcher should be enabled. +# +# - When using a file-based storage type, LuckPerms can monitor the data files for changes, and +# automatically update when changes are detected. +# - If you don't want this feature to be active, set this option to false. +watch-files: true + +# Define which messaging service should be used by the plugin. +# +# - If enabled and configured, LuckPerms will use the messaging service to inform other connected +# servers of changes. +# - Use the command "/lp networksync" to manually push changes. +# - Data is NOT stored using this service. It is only used as a messaging platform. +# +# - If you decide to enable this feature, you should set "sync-minutes" to -1, as there is no need +# for LuckPerms to poll the database for changes. +# +# - Possible options: +# => sql Uses the SQL database to form a queue system for communication. Will only work when +# 'storage-method' is set to MySQL or MariaDB. This is chosen by default if the +# option is set to 'auto' and SQL storage is in use. Set to 'notsql' to disable this. +# => redis Uses Redis pub-sub to push changes. Your server connection info must be configured +# below. +# => rabbitmq Uses RabbitMQ pub-sub to push changes. Your server connection info must be +# configured below. +# => nats Uses Nats pub-sub to push changes. Your server connection info must be +# configured below. +# => custom Uses a messaging service provided using the LuckPerms API. +# => auto Attempts to automatically setup a messaging service using redis or sql. +messaging-service: auto + +# If LuckPerms should automatically push updates after a change has been made with a command. +auto-push-updates: true + +# If LuckPerms should push logging entries to connected servers via the messaging service. +push-log-entries: true + +# If LuckPerms should broadcast received logging entries to players on this platform. +# +# - If you have LuckPerms installed on your backend servers as well as a BungeeCord proxy, you +# should set this option to false on either your backends or your proxies, to avoid players being +# messaged twice about log entries. +broadcast-received-log-entries: true + +# Settings for Redis. +# Port 6379 is used by default; set address to "host:port" if differs +# Multiple Redis nodes can be specified in the same format as a string list under the name "addresses". +redis: + enabled: false + address: localhost + username: '' + password: '' + # Settings for Redis Sentinel. + # Sentinel provides high availability for Redis by monitoring master/replica instances. + # Port 26379 is used by default for sentinel nodes. + sentinel: + enabled: false + master: mymaster + addresses: + - localhost:26379 + username: '' + password: '' + +# Settings for Nats. +# Port 4222 is used by default; set address to "host:port" if differs +nats: + enabled: false + address: localhost + username: '' + password: '' + token: '' + +# Settings for RabbitMQ. +# Port 5672 is used by default; set address to "host:port" if differs +rabbitmq: + enabled: false + address: localhost + vhost: '/' + username: 'guest' + password: 'guest' + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | CUSTOMIZATION SETTINGS | # +# | | # +# | Settings that allow admins to customize the way LuckPerms operates. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# Controls how temporary permissions/parents/meta should be accumulated. +# +# - The default behaviour is "deny". +# - This behaviour can also be specified when the command is executed. See the command usage +# documentation for more info. +# +# - Possible options: +# => accumulate durations will be added to the existing expiry time +# => replace durations will be replaced if the new duration is later than the current +# expiration +# => deny the command will just fail if you try to add another node with the same expiry +temporary-add-behaviour: deny + +# Controls how LuckPerms will determine a users "primary" group. +# +# - The meaning and influence of "primary groups" are explained in detail on the wiki. +# - The preferred approach is to let LuckPerms automatically determine a users primary group +# based on the relative weight of their parent groups. +# +# - Possible options: +# => stored use the value stored against the users record in the file/database +# => parents-by-weight just use the users most highly weighted parent +# => all-parents-by-weight same as above, but calculates based upon all parents inherited from +# both directly and indirectly +primary-group-calculation: parents-by-weight + +# If the plugin should check for "extra" permissions with users run LP commands. +# +# - These extra permissions allow finer control over what users can do with each command, and who +# they have access to edit. +# - The nature of the checks are documented on the wiki under "Argument based command permissions". +# - Argument based permissions are *not* static, unlike the 'base' permissions, and will depend upon +# the arguments given within the command. +argument-based-command-permissions: false + +# If the plugin should check whether senders are a member of a given group before they're able to +# edit the groups data or add/remove other users to/from it. +# Note: these limitations do not apply to the web editor! +require-sender-group-membership-to-modify: false + +# If the plugin should send log notifications to users whenever permissions are modified. +# +# - Notifications are only sent to those with the appropriate permission to receive them +# - They can also be temporarily enabled/disabled on a per-user basis using +# '/lp log notify ' +log-notify: true + +# Defines a list of log entries which should not be sent as notifications to users. +# +# - Each entry in the list is a RegEx expression which is matched against the log entry description. +log-notify-filtered-descriptions: +# - "parent add example" + +# If LuckPerms should automatically install translation bundles and periodically update them. +auto-install-translations: true + +# Defines the options for prefix and suffix stacking. +# +# - The feature allows you to display multiple prefixes or suffixes alongside a players username in +# chat. +# - It is explained and documented in more detail on the wiki under "Prefix & Suffix Stacking". +# +# - The options are divided into separate sections for prefixes and suffixes. +# - The 'duplicates' setting refers to how duplicate elements are handled. Can be 'retain-all', +# 'first-only' or 'last-only'. +# - The value of 'start-spacer' is included at the start of the resultant prefix/suffix. +# - The value of 'end-spacer' is included at the end of the resultant prefix/suffix. +# - The value of 'middle-spacer' is included between each element in the resultant prefix/suffix. +# +# - Possible format options: +# => highest Selects the value with the highest weight, from all values +# held by or inherited by the player. +# +# => lowest Same as above, except takes the one with the lowest weight. +# +# => highest_own Selects the value with the highest weight, but will not +# accept any inherited values. +# +# => lowest_own Same as above, except takes the value with the lowest weight. +# +# => highest_inherited Selects the value with the highest weight, but will only +# accept inherited values. +# +# => lowest_inherited Same as above, except takes the value with the lowest weight. +# +# => highest_on_track_ Selects the value with the highest weight, but only if the +# value was inherited from a group on the given track. +# +# => lowest_on_track_ Same as above, except takes the value with the lowest weight. +# +# => highest_not_on_track_ Selects the value with the highest weight, but only if the +# value was inherited from a group not on the given track. +# +# => lowest_not_on_track_ Same as above, except takes the value with the lowest weight. +# +# => highest_from_group_ Selects the value with the highest weight, but only if the +# value was inherited from the given group. +# +# => lowest_from_group_ Same as above, except takes the value with the lowest weight. +# +# => highest_not_from_group_ Selects the value with the highest weight, but only if the +# value was not inherited from the given group. +# +# => lowest_not_from_group_ Same as above, except takes the value with the lowest weight. +meta-formatting: + prefix: + format: + - "highest" + duplicates: first-only + start-spacer: "" + middle-spacer: " " + end-spacer: "" + suffix: + format: + - "highest" + duplicates: first-only + start-spacer: "" + middle-spacer: " " + end-spacer: "" + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | PERMISSION CALCULATION AND INHERITANCE | # +# | | # +# | Modify the way permission checks, meta lookups and inheritance resolutions are handled. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# The algorithm LuckPerms should use when traversing the "inheritance tree". +# +# - Possible options: +# => breadth-first See: https://en.wikipedia.org/wiki/Breadth-first_search +# => depth-first-pre-order See: https://en.wikipedia.org/wiki/Depth-first_search +# => depth-first-post-order See: https://en.wikipedia.org/wiki/Depth-first_search +inheritance-traversal-algorithm: depth-first-pre-order + +# If a final sort according to "inheritance rules" should be performed after the traversal algorithm +# has resolved the inheritance tree. +# +# "Inheritance rules" refers to things such as group weightings, primary group status, and the +# natural contextual ordering of the group nodes. +# +# Setting this to 'true' will allow for the inheritance rules to take priority over the structure of +# the inheritance tree. +# +# Effectively when this setting is 'true': the tree is flattened, and rules applied afterwards, +# and when this setting is 'false':, the rules are just applied during each step of the traversal. +post-traversal-inheritance-sort: false + +# Defines the mode used to determine whether a set of contexts are satisfied. +# +# - Possible options: +# => at-least-one-value-per-key Set A will be satisfied by another set B, if at least one of the +# key-value entries per key in A are also in B. +# => all-values-per-key Set A will be satisfied by another set B, if all key-value +# entries in A are also in B. +context-satisfy-mode: at-least-one-value-per-key + +# +----------------------------------------------------------------------------------------------+ # +# | Permission resolution settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If users on this server should have their global permissions applied. +# When set to false, only server specific permissions will apply for users on this server +include-global: true + +# If users on this server should have their global world permissions applied. +# When set to false, only world specific permissions will apply for users on this server +include-global-world: true + +# If users on this server should have global (non-server specific) groups applied +apply-global-groups: true + +# If users on this server should have global (non-world specific) groups applied +apply-global-world-groups: true + +# +----------------------------------------------------------------------------------------------+ # +# | Meta lookup settings | # +# +----------------------------------------------------------------------------------------------+ # + +# Defines how meta values should be selected. +# +# - Possible options: +# => inheritance Selects the meta value that was inherited first +# => highest-number Selects the highest numerical meta value +# => lowest-number Selects the lowest numerical meta value +meta-value-selection-default: inheritance + +# Defines how meta values should be selected per key. +meta-value-selection: +# max-homes: highest-number + +# +----------------------------------------------------------------------------------------------+ # +# | Inheritance settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If the plugin should apply wildcard permissions. +# +# - If set to true, LuckPerms will detect wildcard permissions, and resolve & apply all registered +# permissions matching the wildcard. +apply-wildcards: true + +# If LuckPerms should resolve and apply permissions according to the Sponge style implicit wildcard +# inheritance system. +# +# - That being: If a user has been granted "example", then the player should have also be +# automatically granted "example.function", "example.another", "example.deeper.nesting", +# and so on. +apply-sponge-implicit-wildcards: false + +# If the plugin should apply negated Bukkit default permissions before it considers wildcard +# assignments. +# +# - Plugin authors can define permissions which explicitly should not be given automatically to OPs. +# This is usually used for so called "anti-permissions" - permissions which, when granted, apply +# something negative. +# - If this option is set to true, LuckPerms will consider any negated declarations made by +# plugins before it considers wildcards. (similar to the way the OP system works) +# - If this option is set to false, LuckPerms will consider any wildcard assignments first. +apply-default-negated-permissions-before-wildcards: false + +# If the plugin should parse regex permissions. +# +# - If set to true, LuckPerms will detect regex permissions, marked with "r=" at the start of the +# node, and resolve & apply all registered permissions matching the regex. +apply-regex: true + +# If the plugin should complete and apply shorthand permissions. +# +# - If set to true, LuckPerms will detect and expand shorthand node patterns. +apply-shorthand: true + +# +----------------------------------------------------------------------------------------------+ # +# | Extra settings | # +# +----------------------------------------------------------------------------------------------+ # + +# A list of context calculators which will be skipped when calculating contexts. +# +# - You can disable context calculators by either: +# => specifying the Java class name used by the calculator (e.g. com.example.ExampleCalculator) +# => specifying a sub-section of the Java package used by the calculator (e.g. com.example) +disabled-context-calculators: [] + +# Define special group weights for this server. +# +# - Group weights can also be applied directly to group data, using the setweight command. +# - This section allows weights to be set on a per-server basis. +group-weight: +# admin: 10 + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | FINE TUNING OPTIONS | # +# | | # +# | A number of more niche settings for tweaking and changing behaviour. The section also | # +# | contains toggles for some more specialised features. It is only necessary to make changes to | # +# | these options if you want to fine-tune LuckPerms behaviour. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# +----------------------------------------------------------------------------------------------+ # +# | Miscellaneous (and rarely used) settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If LuckPerms should allow usernames with non alphanumeric characters. +# +# - Note that due to the design of the storage implementation, usernames must still be 16 characters +# or less. +allow-invalid-usernames: false + +# If LuckPerms should not require users to confirm bulkupdate operations. +# +# - When set to true, operations will be executed immediately. +# - This is not recommended, as bulkupdate has the potential to irreversibly delete large amounts of +# data, and is not designed to be executed automatically. +# - If automation is needed, users should prefer using the LuckPerms API. +skip-bulkupdate-confirmation: false + +# If LuckPerms should prevent bulkupdate operations. +# +# - When set to true, bulkupdate operations (the /lp bulkupdate command) will not work. +# - When set to false, bulkupdate operations will be allowed via the console. +disable-bulkupdate: false + +# If LuckPerms should allow a users primary group to be removed with the 'parent remove' command. +# +# - When this happens, the plugin will set their primary group back to default. +prevent-primary-group-removal: false + +# If the plugin should run in "read-only" mode for commands. +# +# In this mode, players or the console will only be able to execute commands that read or view +# data, and will not be able to execute any LP commands that modify data. +# +# If enabling read-only mode for just players, be aware that some other plugins may allow players +# to execute commands as the console, allowing them to run LP commands indirectly. +# +# Note: This does not affect interactions with LuckPerms via the API. +commands-read-only-mode: + players: false + console: false + +# If LuckPerms commands should be disabled. When true, this will prevent all LP commands from being +# executed. In a sense this is a more extreme version of read-only mode (see above). +# +# LuckPerms will still act as the permission manager, but server administrators will be unable to +# make changes via commands. +# +# If disabling commands just for players, be aware that some other plugins may allow players to +# execute commands as the console, allowing them to run commands indirectly. +# +# If commands are disabled for both players and the console, LuckPerms will not attempt to register +# a command with the server at all & in a sense will be invisible to both players and admins. +# +# Note: This does not affect interactions with LuckPerms via the API. +disable-luckperms-commands: + players: false + console: false diff --git a/surf-api-minestom/surf-api-minestom-server-npc/api/surf-api-minestom-server-npc.api b/surf-api-minestom/surf-api-minestom-server-npc/api/surf-api-minestom-server-npc.api new file mode 100644 index 000000000..08839ea43 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-npc/api/surf-api-minestom-server-npc.api @@ -0,0 +1,22 @@ +public final class dev/slne/surf/api/minestom/server/npc/NpcFeature : dev/slne/surf/api/minestom/server/SurfMinestomFeature { + public static final field Companion Ldev/slne/surf/api/minestom/server/npc/NpcFeature$Companion; + public static final field ID Ljava/lang/String; + public fun disable (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public fun enable (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public fun getId ()Ljava/lang/String; + public fun getLoadPriority ()I + public fun load (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V +} + +public final class dev/slne/surf/api/minestom/server/npc/NpcFeature$Companion { +} + +public final class dev/slne/surf/api/minestom/server/npc/NpcFeatureKt { + public static final fun withNpcLib (Ldev/slne/surf/api/minestom/server/SurfMinestomServerBuilder;)V +} + +public final class dev/slne/surf/api/minestom/server/npc/impl/NpcDisplaySync { + public static final field INSTANCE Ldev/slne/surf/api/minestom/server/npc/impl/NpcDisplaySync; + public static final fun isSynchronized (Lnet/minestom/server/entity/Entity;Lnet/minestom/server/entity/Entity;Lnet/minestom/server/coordinate/Vec;)Z +} + diff --git a/surf-api-minestom/surf-api-minestom-server-npc/build.gradle.kts b/surf-api-minestom/surf-api-minestom-server-npc/build.gradle.kts new file mode 100644 index 000000000..6aa1d238e --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-npc/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + `core-convention` + `api-validation` +} + +dependencies { + api(projects.surfApiMinestom.surfApiMinestomServer) + compileOnlyApi(libs.minestom.server) + api(libs.minestom.npc) { exclude(group = "net.minestom") } + + compileOnly(libs.sponge.mixin) + compileOnly(libs.mixinextras) + + testImplementation(libs.minestom.server) + testImplementation(libs.minestom.testing) + testImplementation(testFixtures(projects.surfApiMinestom.surfApiMinestomServer)) +} + +tasks.test { + systemProperty("minestom.inside-test", "true") + systemProperty("junit.jupiter.execution.timeout.default", "2m") +} + +description = "surf-api-minestom-server-npc" diff --git a/surf-api-minestom/surf-api-minestom-server-npc/src/main/java/dev/slne/surf/api/minestom/server/npc/mixin/InteractionControllerMixin.java b/surf-api-minestom/surf-api-minestom-server-npc/src/main/java/dev/slne/surf/api/minestom/server/npc/mixin/InteractionControllerMixin.java new file mode 100644 index 000000000..af3869a75 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-npc/src/main/java/dev/slne/surf/api/minestom/server/npc/mixin/InteractionControllerMixin.java @@ -0,0 +1,38 @@ +package dev.slne.surf.api.minestom.server.npc.mixin; + +import codes.bed.minestom.npc.display.InteractionController; +import dev.slne.surf.api.minestom.server.npc.impl.NpcDisplaySync; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Cancels the per-tick {@code syncWithNpc} teleport of the interaction hitbox while the hitbox + * already sits on {@code npc.position.add(offset)}. See {@link NpcDisplaySync#isSynchronized}. + */ +@Mixin(value = InteractionController.class, remap = false) +abstract class InteractionControllerMixin { + + @Shadow + private Entity entity; + + @Shadow + private Vec offset; + + @Inject( + method = "syncWithNpc(Lnet/minestom/server/entity/Entity;)V", + at = @At("HEAD"), + cancellable = true, + remap = false, + require = 1 + ) + private void surf$skipSynchronizedHitbox(final Entity npc, final CallbackInfo ci) { + if (NpcDisplaySync.isSynchronized(this.entity, npc, this.offset)) { + ci.cancel(); + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-npc/src/main/java/dev/slne/surf/api/minestom/server/npc/mixin/TextDisplayControllerMixin.java b/surf-api-minestom/surf-api-minestom-server-npc/src/main/java/dev/slne/surf/api/minestom/server/npc/mixin/TextDisplayControllerMixin.java new file mode 100644 index 000000000..9f4b5d6f6 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-npc/src/main/java/dev/slne/surf/api/minestom/server/npc/mixin/TextDisplayControllerMixin.java @@ -0,0 +1,38 @@ +package dev.slne.surf.api.minestom.server.npc.mixin; + +import codes.bed.minestom.npc.display.TextDisplayController; +import dev.slne.surf.api.minestom.server.npc.impl.NpcDisplaySync; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Cancels the per-tick {@code syncWithNpc} teleport of the text display while the display + * already sits on {@code npc.position.add(offset)}. See {@link NpcDisplaySync#isSynchronized}. + */ +@Mixin(value = TextDisplayController.class, remap = false) +abstract class TextDisplayControllerMixin { + + @Shadow + private Entity entity; + + @Shadow + private Vec offset; + + @Inject( + method = "syncWithNpc(Lnet/minestom/server/entity/Entity;)V", + at = @At("HEAD"), + cancellable = true, + remap = false, + require = 1 + ) + private void surf$skipSynchronizedDisplay(final Entity npc, final CallbackInfo ci) { + if (NpcDisplaySync.isSynchronized(this.entity, npc, this.offset)) { + ci.cancel(); + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-npc/src/main/kotlin/dev/slne/surf/api/minestom/server/npc/NpcFeature.kt b/surf-api-minestom/surf-api-minestom-server-npc/src/main/kotlin/dev/slne/surf/api/minestom/server/npc/NpcFeature.kt new file mode 100644 index 000000000..c1e4a0f5d --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-npc/src/main/kotlin/dev/slne/surf/api/minestom/server/npc/NpcFeature.kt @@ -0,0 +1,70 @@ +package dev.slne.surf.api.minestom.server.npc + +import codes.bed.minestom.npc.StomNPCs +import dev.slne.surf.api.minestom.server.SurfMinestomFeature +import dev.slne.surf.api.minestom.server.SurfMinestomServer +import dev.slne.surf.api.minestom.server.SurfMinestomServerBuilder +import net.minestom.server.event.Event +import net.minestom.server.event.EventFilter +import net.minestom.server.event.EventNode + +/** + * Initializes the StomNPCs library so that [mannequinNpc]s can be spawned and interacted with. + * + * StomNPCs binds its manager to the event node of its first initialization for the lifetime of the + * process. The feature therefore owns a single process-wide node, attaches it to the event node of + * the running server on [load] and detaches it on [disable], so that a server started again in the + * same process still receives NPC events. + */ +class NpcFeature internal constructor() : SurfMinestomFeature { + + override val id: String = ID + + override fun load(api: SurfMinestomServer) { + attach(api.eventNode) + } + + override fun disable(api: SurfMinestomServer) { + detach(api.eventNode) + } + + companion object { + const val ID = "npc" + + private val node = EventNode.type("stom-npcs", EventFilter.INSTANCE) + private var parent: EventNode? = null + + /** + * Attaches the process-wide NPC node to [parent], replacing its previous parent, and + * initializes StomNPCs on it once. + */ + @Synchronized + internal fun attach(parent: EventNode) { + this.parent?.removeChild(node) + parent.addChild(node) + this.parent = parent + + StomNPCs.initialize(node) + } + + /** + * Detaches the process-wide NPC node when it is attached to [parent]. + */ + @Synchronized + internal fun detach(parent: EventNode) { + if (this.parent !== parent) return + + parent.removeChild(node) + this.parent = null + } + } +} + +/** + * Installs the NPC library. + * + * @see NpcFeature + */ +fun SurfMinestomServerBuilder.withNpcLib() { + install(NpcFeature()) +} diff --git a/surf-api-minestom/surf-api-minestom-server-npc/src/main/kotlin/dev/slne/surf/api/minestom/server/npc/impl/NpcDisplaySync.kt b/surf-api-minestom/surf-api-minestom-server-npc/src/main/kotlin/dev/slne/surf/api/minestom/server/npc/impl/NpcDisplaySync.kt new file mode 100644 index 000000000..beb7162d6 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-npc/src/main/kotlin/dev/slne/surf/api/minestom/server/npc/impl/NpcDisplaySync.kt @@ -0,0 +1,36 @@ +package dev.slne.surf.api.minestom.server.npc.impl + +import net.minestom.server.coordinate.Pos +import net.minestom.server.coordinate.Vec +import net.minestom.server.entity.Entity + +/** + * Check used by the NPC mixins to skip the per-tick `syncWithNpc` teleport of an auxiliary + * StomNPCs entity (text display, interaction hitbox) that is already in place. + * + * Used by [dev.slne.surf.api.minestom.server.npc.mixin.TextDisplayControllerMixin] and + * [dev.slne.surf.api.minestom.server.npc.mixin.InteractionControllerMixin]. + */ +object NpcDisplaySync { + + /** + * Returns `true` when [auxiliary] is at `npc.position.add(offset)` with the NPC's normalized + * yaw and pitch. Compares the components directly and does not allocate the target [Pos]. + * + * Returns `false` when [auxiliary] is `null` or any component differs, including NaN. The + * caller then runs the original teleport. + */ + @JvmStatic + fun isSynchronized(auxiliary: Entity?, npc: Entity, offset: Vec): Boolean { + if (auxiliary == null) return false + + val npcPosition = npc.position + val auxiliaryPosition = auxiliary.position + + return auxiliaryPosition.x() == npcPosition.x() + offset.x() && + auxiliaryPosition.y() == npcPosition.y() + offset.y() && + auxiliaryPosition.z() == npcPosition.z() + offset.z() && + auxiliaryPosition.yaw() == Pos.fixYaw(npcPosition.yaw()) && + auxiliaryPosition.pitch() == Pos.fixPitch(npcPosition.pitch()) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-npc/src/main/resources/mixins.surf-api-minestom-server-npc.json b/surf-api-minestom/surf-api-minestom-server-npc/src/main/resources/mixins.surf-api-minestom-server-npc.json new file mode 100644 index 000000000..9e32277a9 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-npc/src/main/resources/mixins.surf-api-minestom-server-npc.json @@ -0,0 +1,13 @@ +{ + "required": true, + "minVersion": "0.8.7", + "package": "dev.slne.surf.api.minestom.server.npc.mixin", + "compatibilityLevel": "JAVA_25", + "mixins": [ + "InteractionControllerMixin", + "TextDisplayControllerMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-npc/src/test/kotlin/dev/slne/surf/api/minestom/server/npc/MannequinNpcTest.kt b/surf-api-minestom/surf-api-minestom-server-npc/src/test/kotlin/dev/slne/surf/api/minestom/server/npc/MannequinNpcTest.kt new file mode 100644 index 000000000..d7cc172c8 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-npc/src/test/kotlin/dev/slne/surf/api/minestom/server/npc/MannequinNpcTest.kt @@ -0,0 +1,116 @@ +package dev.slne.surf.api.minestom.server.npc + +import dev.slne.surf.api.minestom.npc.mannequinNpc +import net.kyori.adventure.text.Component +import net.minestom.server.coordinate.Pos +import net.minestom.server.coordinate.Vec +import net.minestom.server.entity.EntityType +import net.minestom.server.entity.Player +import net.minestom.server.entity.PlayerHand +import net.minestom.server.entity.PlayerSkin +import net.minestom.server.entity.metadata.avatar.MannequinMeta +import net.minestom.server.event.player.PlayerEntityInteractEvent +import net.minestom.server.instance.Instance +import net.minestom.server.network.player.ResolvableProfile +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import net.minestom.testing.TestConnection +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.time.Duration +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.atomic.AtomicReference + +@EnvTest +class MannequinNpcTest { + + @Test + fun `mannequin npc spawns with skin, immovable meta and hologram`(env: Env) { + env.attachNpcs() + + val instance = env.createFlatInstance() + instance.loadChunk(0, 0).join() + + val npc = mannequinNpc("test", instance, Pos(0.5, 42.0, 0.5)) { + displayName = Component.text("Test NPC") + profile = ResolvableProfile(PlayerSkin("textures", "signature")) + scale = 1.5 + description = Component.empty() + } + + assertEquals(EntityType.MANNEQUIN, npc.entityType) + assertSame(instance, npc.instance) + + val meta = npc.entityMeta as MannequinMeta + assertTrue(meta.isImmovable) + assertNotNull(meta.profile) + assertEquals(Component.empty(), meta.description) + + assertNotNull(npc.textDisplayController) + } + + @Test + fun `mannequin npc receives interactions through the npc listener`(env: Env) { + env.attachNpcs() + + val instance = env.createFlatInstance() + instance.loadChunk(0, 0).join() + + val interacted = AtomicReference() + val npc = mannequinNpc("test", instance, Pos(0.5, 42.0, 0.5)) { + onInteract { interaction -> + interacted.set(interaction.player.uuid) + } + } + + val player = connect(env.createConnection(), instance) + env.process().eventHandler().call( + PlayerEntityInteractEvent(player, npc, PlayerHand.MAIN, Vec.ZERO) + ) + + assertEquals(player.uuid, interacted.get()) + } + + @Test + fun `mannequin npc attaches a single hologram when its chunk loads late`(env: Env) { + env.attachNpcs() + + val instance = env.createFlatInstance() + val failures = CopyOnWriteArrayList() + env.process().exception().setExceptionHandler { failures += it } + + val npc = mannequinNpc("late", instance, Pos(500.5, 42.0, 500.5)) { + displayName = Component.text("Late NPC") + } + + fun holograms() = instance.entities.filter { it.entityType == EntityType.TEXT_DISPLAY } + + assertTrue( + env.tickWhile({ holograms().isEmpty() }, Duration.ofSeconds(5)), + "the hologram was never spawned" + ) + + val displays = holograms() + assertEquals(1, displays.size, "expected exactly one hologram, got $displays") + assertTrue(npc in instance.entities, "the npc itself was not registered") + assertSame(npc.textDisplayController?.getEntity(), displays.single()) + assertTrue(failures.isEmpty(), "spawning logged exceptions: $failures") + } + + /** + * Attaches the process-wide NPC node of [NpcFeature] to this test's event handler, so that no + * test depends on which test initialized StomNPCs first. + */ + private fun Env.attachNpcs() = NpcFeature.attach(process().eventHandler()) + + private fun connect(connection: TestConnection, instance: Instance): Player { + val player = CompletableFuture() + Thread.startVirtualThread { + runCatching { connection.connect(instance, Pos.ZERO) } + .onSuccess(player::complete) + .onFailure(player::completeExceptionally) + } + return player.join() + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-npc/src/test/kotlin/dev/slne/surf/api/minestom/server/npc/impl/NpcDisplaySyncTest.kt b/surf-api-minestom/surf-api-minestom-server-npc/src/test/kotlin/dev/slne/surf/api/minestom/server/npc/impl/NpcDisplaySyncTest.kt new file mode 100644 index 000000000..62baedfb9 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-npc/src/test/kotlin/dev/slne/surf/api/minestom/server/npc/impl/NpcDisplaySyncTest.kt @@ -0,0 +1,66 @@ +package dev.slne.surf.api.minestom.server.npc.impl + +import net.minestom.server.coordinate.Pos +import net.minestom.server.coordinate.Vec +import net.minestom.server.entity.Entity +import net.minestom.server.entity.EntityType +import net.minestom.server.instance.Instance +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +@EnvTest +class NpcDisplaySyncTest { + + private val offset = Vec(0.0, 2.15, 0.0) + + @Test + fun `a missing auxiliary entity is never synchronized`(env: Env) { + assertFalse(NpcDisplaySync.isSynchronized(null, Entity(EntityType.ARMOR_STAND), offset)) + } + + @Test + fun `an auxiliary entity on the offset position is synchronized`(env: Env) { + val instance = env.createFlatInstance() + val npcPosition = Pos(0.5, 42.0, 0.5, 37.5f, -12.25f) + val npc = spawn(instance, npcPosition) + val display = spawn(instance, npcPosition.add(offset)) + + assertTrue(NpcDisplaySync.isSynchronized(display, npc, offset)) + } + + @Test + fun `moving or rotating the npc desynchronizes the auxiliary entity`(env: Env) { + val instance = env.createFlatInstance() + val npcPosition = Pos(0.5, 42.0, 0.5) + val npc = spawn(instance, npcPosition) + val display = spawn(instance, npcPosition.add(offset)) + + npc.teleport(npcPosition.add(1.0, 0.0, 0.0)).join() + assertFalse(NpcDisplaySync.isSynchronized(display, npc, offset)) + + npc.teleport(npcPosition).join() + assertTrue(NpcDisplaySync.isSynchronized(display, npc, offset)) + + npc.setView(90f, 0f) + assertFalse(NpcDisplaySync.isSynchronized(display, npc, offset)) + } + + @Test + fun `a different offset desynchronizes the auxiliary entity`(env: Env) { + val instance = env.createFlatInstance() + val npcPosition = Pos(0.5, 42.0, 0.5) + val npc = spawn(instance, npcPosition) + val display = spawn(instance, npcPosition.add(offset)) + + assertFalse(NpcDisplaySync.isSynchronized(display, npc, Vec(0.0, 3.4, 0.0))) + } + + private fun spawn(instance: Instance, position: Pos): Entity { + val entity = Entity(EntityType.ARMOR_STAND) + entity.setInstance(instance, position).join() + return entity + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-npc/src/test/kotlin/dev/slne/surf/api/minestom/server/npc/mixin/NpcDisplaySyncApplyTest.kt b/surf-api-minestom/surf-api-minestom-server-npc/src/test/kotlin/dev/slne/surf/api/minestom/server/npc/mixin/NpcDisplaySyncApplyTest.kt new file mode 100644 index 000000000..8daf1ed55 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-npc/src/test/kotlin/dev/slne/surf/api/minestom/server/npc/mixin/NpcDisplaySyncApplyTest.kt @@ -0,0 +1,54 @@ +package dev.slne.surf.api.minestom.server.npc.mixin + +import dev.slne.surf.api.minestom.server.impl.mixin.MixinTransforms +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.objectweb.asm.tree.MethodInsnNode + +/** + * Applies the StomNPCs mixins to the dependency bytecode. Production wraps a failed transform in an + * `IllegalClassFormatException` that the JVM swallows, so a renamed field or a changed + * `syncWithNpc` descriptor would silently load the untransformed controller. + */ +class NpcDisplaySyncApplyTest { + + @ParameterizedTest(name = "{0}") + @CsvSource( + "codes.bed.minestom.npc.display.TextDisplayController, surf\$skipSynchronizedDisplay", + "codes.bed.minestom.npc.display.InteractionController, surf\$skipSynchronizedHitbox", + ) + fun `the controller mixins guard syncWithNpc`(binaryName: String, handler: String) { + val controller = MixinTransforms.transform(binaryName) + + // Mixin decorates handler names, e.g. handler$zzd000$surf$skipSynchronizedDisplay. + val methods = controller.methods.map { it.name } + assertTrue(methods.any { it.endsWith(handler) }) { + "the head guard was not injected into $binaryName: $methods" + } + + val sync = controller.methods.single { it.name == "syncWithNpc" } + val calls = sync.instructions.asSequence() + .filterIsInstance() + .map { "${it.owner}.${it.name}" } + .toList() + assertTrue(calls.any { it.endsWith(handler) }) { + "syncWithNpc does not call the guard: $calls" + } + assertTrue(calls.any { it.endsWith("Entity.teleport") }) { + "syncWithNpc lost its teleport, the guard should only skip it: $calls" + } + } + + @Test + fun `the shadowed controller fields still exist in the dependency`() { + val fields = MixinTransforms + .transform("codes.bed.minestom.npc.display.TextDisplayController") + .fields + .associate { it.name to it.desc } + + assertTrue(fields["entity"] == "Lnet/minestom/server/entity/Entity;") { "fields: $fields" } + assertTrue(fields["offset"] == "Lnet/minestom/server/coordinate/Vec;") { "fields: $fields" } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-player-visibility/api/surf-api-minestom-server-player-visibility.api b/surf-api-minestom/surf-api-minestom-server-player-visibility/api/surf-api-minestom-server-player-visibility.api new file mode 100644 index 000000000..9d5cef3d4 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-player-visibility/api/surf-api-minestom-server-player-visibility.api @@ -0,0 +1,41 @@ +public final class dev/slne/surf/api/minestom/server/visibility/PlayerVisibilityFeature : dev/slne/surf/api/minestom/server/SurfMinestomFeature, dev/slne/surf/api/minestom/visibility/PlayerVisibility$Provider { + public static final field Companion Ldev/slne/surf/api/minestom/server/visibility/PlayerVisibilityFeature$Companion; + public static final field ID Ljava/lang/String; + public fun disable (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public fun enable (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public fun getId ()Ljava/lang/String; + public fun getLoadPriority ()I + public fun isLimited (Lnet/minestom/server/entity/Player;)Z + public fun load (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public fun refreshViewerRule (Lnet/minestom/server/entity/Player;)Z + public fun updateViewerRule (Lnet/minestom/server/entity/Player;Ljava/util/function/Predicate;)Z +} + +public final class dev/slne/surf/api/minestom/server/visibility/PlayerVisibilityFeature$Companion { +} + +public final class dev/slne/surf/api/minestom/server/visibility/PlayerVisibilityFeatureKt { + public static final fun withPlayerVisibility (Ldev/slne/surf/api/minestom/server/SurfMinestomServerBuilder;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun withPlayerVisibility$default (Ldev/slne/surf/api/minestom/server/SurfMinestomServerBuilder;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V +} + +public final class dev/slne/surf/api/minestom/server/visibility/PlayerVisibilitySettings { + public final fun getActivateAt ()I + public final fun getDeactivateBelow ()I + public final fun getMaxVisible ()I + public final fun getRefreshIntervalTicks ()I + public final fun getRetentionDistanceFactor ()D + public final fun setActivateAt (I)V + public final fun setDeactivateBelow (I)V + public final fun setMaxVisible (I)V + public final fun setRefreshIntervalTicks (I)V + public final fun setRetentionDistanceFactor (D)V +} + +public final class net/minestom/server/entity/AutomaticPlayerVisibility { + public static fun hide (Lnet/minestom/server/entity/Player;Lnet/minestom/server/entity/Player;)V + public static fun show (Lnet/minestom/server/entity/Player;Lnet/minestom/server/entity/Player;)V + public static fun targetAllows (Lnet/minestom/server/entity/Player;Lnet/minestom/server/entity/Player;)Z + public static fun withViewerLock (Lnet/minestom/server/entity/Player;Ljava/lang/Runnable;)V +} + diff --git a/surf-api-minestom/surf-api-minestom-server-player-visibility/build.gradle.kts b/surf-api-minestom/surf-api-minestom-server-player-visibility/build.gradle.kts new file mode 100644 index 000000000..01106f562 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-player-visibility/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + `core-convention` + `api-validation` +} + +dependencies { + api(projects.surfApiMinestom.surfApiMinestomServer) + compileOnlyApi(libs.minestom.server) +} + +description = "surf-api-minestom-server-player-visibility" diff --git a/surf-api-minestom/surf-api-minestom-server-player-visibility/src/main/java/net/minestom/server/entity/AutomaticPlayerVisibility.java b/surf-api-minestom/surf-api-minestom-server-player-visibility/src/main/java/net/minestom/server/entity/AutomaticPlayerVisibility.java new file mode 100644 index 000000000..2e63035ed --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-player-visibility/src/main/java/net/minestom/server/entity/AutomaticPlayerVisibility.java @@ -0,0 +1,63 @@ +package net.minestom.server.entity; + +import static java.util.Objects.requireNonNull; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +public final class AutomaticPlayerVisibility { + + private AutomaticPlayerVisibility() { + throw new AssertionError(); + } + + public static void withViewerLock(Player viewer, Runnable action) { + synchronized (viewer.viewEngine) { + action.run(); + } + } + + public static boolean targetAllows(Player target, Player viewer) { + return target.isAutoViewable() && target.viewEngine.viewableOption.predicate(viewer); + } + + public static void show(Player target, Player viewer) { + if (!viewer.autoViewEntities() + || !target.isActive() + || target.isRemoved() + || target.getInstance() != viewer.getInstance() + || !targetAllows(target, viewer) + || target.isViewer(viewer)) { + return; + } + + requireNonNull(target.viewEngine.viewableOption.addition, "addition").accept(viewer); + + Entity vehicle = target.getVehicle(); + if (vehicle != null + && vehicle.isViewer(viewer) + && target.isViewer(viewer)) { + viewer.sendPacket(vehicle.getPassengersPacket()); + } + } + + public static void hide(Player target, Player viewer) { + if (!target.isViewer(viewer)) { + return; + } + + boolean[] manual = {false}; + + target.viewEngine.forManuals(player -> { + if (player == viewer) { + manual[0] = true; + } + }); + + if (manual[0]) { + return; + } + + requireNonNull(target.viewEngine.viewableOption.removal, "removal").accept(viewer); + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-player-visibility/src/main/kotlin/dev/slne/surf/api/minestom/server/visibility/NearestPlayerSelector.kt b/surf-api-minestom/surf-api-minestom-server-player-visibility/src/main/kotlin/dev/slne/surf/api/minestom/server/visibility/NearestPlayerSelector.kt new file mode 100644 index 000000000..6db8bce99 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-player-visibility/src/main/kotlin/dev/slne/surf/api/minestom/server/visibility/NearestPlayerSelector.kt @@ -0,0 +1,267 @@ +package dev.slne.surf.api.minestom.server.visibility + +/** + * Selects the nearest player entity IDs for one viewer, with hysteresis on when the limit applies. + * + * Candidates rank by ascending score. The score is the squared distance, multiplied by the square + * of [retentionDistanceFactor] for previously visible candidates. Equal scores favor previously + * visible candidates, then lower entity IDs. + * + * While limiting is inactive, all valid candidates are selected. While it is active, at most + * [maxVisible] are. Results are sorted by entity ID, not by distance. + * + * A pass is [begin], any number of [offer] calls, then [finish]. Each entity ID must be offered at + * most once per pass; duplicates are not detected. Instances reuse internal buffers and are not + * thread-safe. + * + * @param maxVisible Maximum selection size while limiting is active. Must be greater than zero. + * @param activateAt Valid candidate count that activates limiting. Must be at least [maxVisible]. + * @param deactivateBelow Valid candidate count below which active limiting ends. Must be in + * `1..maxVisible` and less than [activateAt]. + * @param retentionDistanceFactor Distance multiplier for previously visible candidates, applied + * squared to squared distances. Must be in `(0.0, 1.0]`; `1.0` leaves only the equal-score + * preference. + * @throws IllegalArgumentException If a constructor argument violates its constraint. + */ +internal class NearestPlayerSelector( + private val maxVisible: Int, + private val activateAt: Int, + private val deactivateBelow: Int, + retentionDistanceFactor: Double, +) { + init { + require(maxVisible > 0) { "maxVisible must be greater than 0" } + require(activateAt >= maxVisible) { "activateAt must be greater than or equal to maxVisible" } + require(deactivateBelow in 1..maxVisible && deactivateBelow < activateAt) { "deactivateBelow must be between 1 and maxVisible, and less than activateAt" } + require(retentionDistanceFactor > 0.0 && retentionDistanceFactor <= 1.0) { "retentionDistanceFactor must be between 0.0 and 1.0" } + } + + /** + * Largest result: a limited selection, or an unlimited one just below [activateAt]. + */ + private val capacity = maxOf(maxVisible, activateAt - 1) + + private val ids = IntArray(capacity) + private val scores = DoubleArray(capacity) + private val retained = BooleanArray(capacity) + private val output = IntArray(capacity) + + private val retentionScoreFactor = retentionDistanceFactor * retentionDistanceFactor + + private var size = 0 + private var count = 0 + private var previouslyVisible = IntArray(0) + + /** + * Whether limiting is active. + * + * [begin] sets it to the previous pass's state and [finish] updates it from the valid candidate + * count. [offer] does not change it. + */ + var limited: Boolean = false + private set + + /** + * Starts a pass and discards the candidates of the previous one. + * + * [previouslyVisible] only sets retention preference; those entities must be offered again to + * be selected. The selector keeps the array by reference, never modifies it, and may return it + * from [finish]. The caller must not modify it until [finish] returns. + * + * @param previouslyVisible Previously selected entity IDs in ascending order, or an empty array. + * @param wasLimited Whether limiting was active in the previous pass. + */ + fun begin(previouslyVisible: IntArray, wasLimited: Boolean) { + this.previouslyVisible = previouslyVisible + + limited = wasLimited + size = 0 + count = 0 + } + + /** + * Offers a candidate to the current pass. + * + * Non-finite and negative distances are ignored and do not count toward the activation + * thresholds. Every valid offer counts, including candidates the bounded heap drops. + * + * @param entityId Candidate entity ID; offer each ID at most once per pass. + * @param distanceSquared Squared distance from the viewer to the candidate. + */ + fun offer(entityId: Int, distanceSquared: Double) { + if (!distanceSquared.isFinite() || distanceSquared < 0.0) return + + count++ + + val keep = previouslyVisible.binarySearch(entityId) >= 0 + val score = distanceSquared * if (keep) retentionScoreFactor else 1.0 + + if (size < capacity) { + var index = size++ + + while (index > 0) { + val parent = (index - 1) ushr 1 + + if (!worse( + score, + keep, + entityId, + scores[parent], + retained[parent], + ids[parent], + ) + ) { + break + } + + copy(parent, index) + index = parent + } + + put(index, entityId, score, keep) + } else if ( + worse( + scores[0], + retained[0], + ids[0], + score, + keep, + entityId, + ) + ) { + siftDown(entityId, score, keep) + } + } + + /** + * Ends the pass, updates [limited] and returns the selected entity IDs. + * + * Active limiting stays active while the valid candidate count is at least [deactivateBelow]. + * Inactive limiting becomes active once the count reaches [activateAt]. An unlimited selection + * may exceed [maxVisible]. + * + * @return Selected entity IDs in ascending order. Returns the array passed to [begin] when its + * contents equal the selection, otherwise a new array. Never exposes the internal buffer. + */ + fun finish(): IntArray { + limited = if (limited) { + count >= deactivateBelow + } else { + count >= activateAt + } + + val resultSize = if (limited) { + minOf(count, maxVisible) + } else { + count + } + + while (size > resultSize) { + val last = --size + + if (size > 0) { + siftDown(ids[last], scores[last], retained[last]) + } + } + + for (index in 0 until size) { + output[index] = ids[index] + } + + output.sort(0, size) + + if (size == previouslyVisible.size) { + var same = true + + for (index in 0 until size) { + if (output[index] != previouslyVisible[index]) { + same = false + break + } + } + + if (same) return previouslyVisible + } + + return output.copyOf(size) + } + + /** + * Replaces the heap root with the given candidate and restores the worst-first heap order. + * + * Requires a nonempty heap whose child subtrees of the root already satisfy the order. + */ + private fun siftDown(entityId: Int, score: Double, keep: Boolean) { + var index = 0 + + while (true) { + val left = index * 2 + 1 + if (left >= size) break + + var child = left + val right = left + 1 + + if ( + right < size && + worse( + scores[right], + retained[right], + ids[right], + scores[left], + retained[left], + ids[left], + ) + ) { + child = right + } + + if (!worse( + scores[child], + retained[child], + ids[child], + score, + keep, + entityId, + ) + ) { + break + } + + copy(child, index) + index = child + } + + put(index, entityId, score, keep) + } + + private fun copy(from: Int, to: Int) { + ids[to] = ids[from] + scores[to] = scores[from] + retained[to] = retained[from] + } + + private fun put( + index: Int, + entityId: Int, + score: Double, + keep: Boolean, + ) { + ids[index] = entityId + scores[index] = score + retained[index] = keep + } + + /** + * Returns whether candidate A ranks strictly below candidate B. + * + * Higher scores are worse. Equal scores favor previously visible candidates, then lower IDs. + */ + private fun worse( + scoreA: Double, retainedA: Boolean, idA: Int, + scoreB: Double, retainedB: Boolean, idB: Int, + ): Boolean = when { + scoreA != scoreB -> scoreA > scoreB + retainedA != retainedB -> !retainedA + else -> idA > idB + } +} \ No newline at end of file diff --git a/surf-api-minestom/surf-api-minestom-server-player-visibility/src/main/kotlin/dev/slne/surf/api/minestom/server/visibility/PlayerVisibilityFeature.kt b/surf-api-minestom/surf-api-minestom-server-player-visibility/src/main/kotlin/dev/slne/surf/api/minestom/server/visibility/PlayerVisibilityFeature.kt new file mode 100644 index 000000000..775cb704a --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-player-visibility/src/main/kotlin/dev/slne/surf/api/minestom/server/visibility/PlayerVisibilityFeature.kt @@ -0,0 +1,178 @@ +package dev.slne.surf.api.minestom.server.visibility + +import dev.slne.surf.api.minestom.extension.addListener +import dev.slne.surf.api.minestom.server.SurfMinestomFeature +import dev.slne.surf.api.minestom.server.SurfMinestomServer +import dev.slne.surf.api.minestom.server.SurfMinestomServerBuilder +import dev.slne.surf.api.minestom.server.SurfMinestomServerDsl +import dev.slne.surf.api.minestom.visibility.PlayerVisibility +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import net.minestom.server.entity.Entity +import net.minestom.server.entity.Player +import net.minestom.server.event.player.AsyncPlayerConfigurationEvent +import net.minestom.server.event.player.PlayerDisconnectEvent +import net.minestom.server.event.player.PlayerSpawnEvent +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.function.Predicate + +/** + * Configures [withPlayerVisibility]. + */ +@SurfMinestomServerDsl +class PlayerVisibilitySettings internal constructor() { + + /** + * How many players a player sees at most while the limit is active. + */ + var maxVisible: Int = 50 + + /** + * The limit activates once at least this many players would be visible. + */ + var activateAt: Int = 60 + + /** + * The limit deactivates once fewer than this many players would be visible. + */ + var deactivateBelow: Int = 45 + + /** + * How often, in ticks, the visible players of every player are selected anew. + */ + var refreshIntervalTicks: Int = 5 + + /** + * Distance multiplier for players that are already visible, so that they are not swapped + * for players that are only slightly closer. `1.0` disables the bias. + */ + var retentionDistanceFactor: Double = 0.9 + + internal fun validate() { + require(maxVisible > 0) { "maxVisible must be greater than 0" } + require(activateAt >= maxVisible) { "activateAt must be greater than or equal to maxVisible" } + require(deactivateBelow in 1..maxVisible && deactivateBelow < activateAt) { + "deactivateBelow must be between 1 and maxVisible, and less than activateAt" + } + require(refreshIntervalTicks > 0) { "refreshIntervalTicks must be greater than 0" } + require(retentionDistanceFactor > 0.0 && retentionDistanceFactor <= 1.0) { + "retentionDistanceFactor must be between 0.0 and 1.0" + } + } +} + +/** + * Limits how many players each player sees at once to the nearest ones. + * + * The feature owns the viewer rule and the viewable rule of every player from their first + * configuration on. Restrict what a player sees through [PlayerVisibility.updateViewerRule]; + * calling `Player#updateViewerRule` directly removes the limit for that player. + */ +@OptIn(InternalSurfApi::class) +class PlayerVisibilityFeature internal constructor( + private val settings: PlayerVisibilitySettings, +) : SurfMinestomFeature, PlayerVisibility.Provider { + + override val id: String = ID + + private val handlers = ConcurrentHashMap() + private val service = PlayerVisibilityService(settings) { player -> handler(player) } + + override fun load(api: SurfMinestomServer) { + PlayerVisibility.installProvider(this) + + api.eventNode.addListener { event -> + handlerFor(event.player) + } + + api.eventNode.addListener { event -> + if (!event.isFirstSpawn) return@addListener + + handlerFor(event.player) + } + + api.eventNode.addListener { event -> + val player = event.player + val handler = handler(player) ?: return@addListener + + if (handlers.remove(player.uuid, handler)) { + handler.close() + } + } + } + + override fun enable(api: SurfMinestomServer) { + service.start() + } + + override fun disable(api: SurfMinestomServer) { + PlayerVisibility.installProvider(null) + service.stop() + handlers.values.forEach(PlayerVisibilityHandler::close) + handlers.clear() + } + + internal fun handler(player: Player): PlayerVisibilityHandler? = + handlers[player.uuid]?.takeIf { it.isFor(player) } + + /** + * Returns the handler of [player], creating and initializing it when it does not exist yet. + * + * The handler is created during the first configuration, before [player] is registered in an + * entity tracker, so that no player is shown before the limit applies. Requests that arrive + * earlier create it on demand. A handler left over from an earlier connection with the same + * UUID is replaced and closed. Returns `null` for a player that is no longer online. + */ + private fun handlerFor(player: Player): PlayerVisibilityHandler? { + handler(player)?.let { return it } + if (!player.isOnline) return null + + val created = PlayerVisibilityHandler(player) + var stale: PlayerVisibilityHandler? = null + val current = handlers.compute(player.uuid) { _, existing -> + if (existing != null && existing.isFor(player)) { + existing + } else { + stale = existing + created + } + } + + stale?.close() + if (current === created) { + created.init() + } + + return current + } + + override fun updateViewerRule(player: Player, predicate: Predicate?): Boolean { + val handler = handlerFor(player) ?: return false + handler.requestRule(predicate) + return true + } + + override fun refreshViewerRule(player: Player): Boolean { + val handler = handlerFor(player) ?: return false + handler.requestRuleRefresh() + return true + } + + override fun isLimited(player: Player): Boolean = handler(player)?.visibilityLimited == true + + companion object { + const val ID = "player-visibility" + } +} + +/** + * Installs the player visibility limit. + * + * @throws IllegalArgumentException If the settings violate their documented constraints. + * @see PlayerVisibilityFeature + */ +fun SurfMinestomServerBuilder.withPlayerVisibility(block: PlayerVisibilitySettings.() -> Unit = {}) { + val settings = PlayerVisibilitySettings().apply(block) + settings.validate() + install(PlayerVisibilityFeature(settings)) +} diff --git a/surf-api-minestom/surf-api-minestom-server-player-visibility/src/main/kotlin/dev/slne/surf/api/minestom/server/visibility/PlayerVisibilityHandler.kt b/surf-api-minestom/surf-api-minestom-server-player-visibility/src/main/kotlin/dev/slne/surf/api/minestom/server/visibility/PlayerVisibilityHandler.kt new file mode 100644 index 000000000..424044a5a --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-player-visibility/src/main/kotlin/dev/slne/surf/api/minestom/server/visibility/PlayerVisibilityHandler.kt @@ -0,0 +1,159 @@ +package dev.slne.surf.api.minestom.server.visibility + +import dev.slne.surf.api.minestom.visibility.PlayerVisibility +import net.minestom.server.entity.AutomaticPlayerVisibility +import net.minestom.server.entity.Entity +import net.minestom.server.entity.Player +import net.minestom.server.instance.Instance +import java.util.concurrent.atomic.AtomicReference +import java.util.function.Predicate + +/** + * The visibility state of one viewing player. + * + * Owns the player's viewer rule: it combines the rule requested through + * [PlayerVisibility.updateViewerRule] with the nearest-player selection published by + * [PlayerVisibilityService]. + * + * [requestRule] and [requestRuleRefresh] may be called from any thread. Rule adoption and selection + * updates run on the service tick. The combined rule reads the selection and the applied rule + * through volatile fields. + */ +internal class PlayerVisibilityHandler(private val player: Player) { + private class RuleRequest( + val predicate: Predicate?, + ) + + @Suppress("ArrayInDataClass") + data class VisibilitySelection( + val instance: Instance?, + val ids: IntArray, + ) + + companion object { + val EMPTY_IDS = IntArray(0) + } + + private val requestedRule = AtomicReference(RuleRequest(null)) + private var appliedRequest = requestedRule.get() + + @Volatile + private var appliedRule: Predicate? = null + + @Volatile + private var admittingPlayers = true + + @Volatile + var visibilitySelection = VisibilitySelection(null, EMPTY_IDS) + private set + + var visibilityLimited = false + + var requiresNativeRuleRefresh = false + private set + + private val combinedRule = Predicate { entity -> + if (!allowsWithoutBudget(entity)) { + false + } else if (entity !is Player) { + true + } else { + val selection = visibilitySelection + + admittingPlayers && + player.autoViewEntities() && + selection.instance === player.instance && + entity.instance === player.instance && + selection.ids.binarySearch(entity.entityId) >= 0 + } + } + + /** + * Whether this handler belongs to [other], by identity. + */ + fun isFor(other: Player): Boolean = player === other + + fun init() { + player.updateViewerRule(combinedRule) + + // Limits this player's packets to its actual viewers instead of every player in range, + // most of whom no longer see it once the limit applies. + player.updateViewableRule(ALWAYS_VIEWABLE) + } + + /** + * Restores the player's own rules. Does nothing for a player that is no longer online, whose + * view is not updated anymore. + */ + fun close() { + if (!player.isOnline) return + + player.updateViewerRule(appliedRule) + player.updateViewableRule(null) + } + + fun requestRule(predicate: Predicate?) { + requestedRule.set(RuleRequest(predicate)) + } + + /** + * Forces the next tick to re-apply the current rule, e.g. when the predicate's inputs changed. + */ + fun requestRuleRefresh() { + requestedRule.updateAndGet { RuleRequest(it.predicate) } + } + + fun allowsWithoutBudget(entity: Entity): Boolean { + return appliedRule?.test(entity) != false + } + + /** + * Adopts the latest requested rule. + * + * @return `true` when a new request was adopted; [requiresNativeRuleRefresh] is then set. + */ + fun applyRequestedRule(): Boolean { + val request = requestedRule.get() + if (request === appliedRequest) return false + + appliedRequest = request + appliedRule = request.predicate + requiresNativeRuleRefresh = true + + return true + } + + /** + * Makes the combined rule reject every player until [resumePlayerAdmission], so that no player + * is admitted from a stale selection while the selection is replaced. + */ + fun pausePlayerAdmission() { + AutomaticPlayerVisibility.withViewerLock(player) { + admittingPlayers = false + } + } + + fun publishSelection(instance: Instance, ids: IntArray) { + val previous = visibilitySelection + + if (previous.instance !== instance || previous.ids !== ids) { + visibilitySelection = VisibilitySelection(instance, ids) + } + } + + fun resumePlayerAdmission() { + AutomaticPlayerVisibility.withViewerLock(player) { + admittingPlayers = true + } + } + + fun refreshNativeViewerRule() { + if (player.autoViewEntities()) { + player.updateViewerRule() + } + + requiresNativeRuleRefresh = false + } +} + +private val ALWAYS_VIEWABLE = Predicate { true } diff --git a/surf-api-minestom/surf-api-minestom-server-player-visibility/src/main/kotlin/dev/slne/surf/api/minestom/server/visibility/PlayerVisibilityService.kt b/surf-api-minestom/surf-api-minestom-server-player-visibility/src/main/kotlin/dev/slne/surf/api/minestom/server/visibility/PlayerVisibilityService.kt new file mode 100644 index 000000000..f4a0b23b9 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-player-visibility/src/main/kotlin/dev/slne/surf/api/minestom/server/visibility/PlayerVisibilityService.kt @@ -0,0 +1,198 @@ +package dev.slne.surf.api.minestom.server.visibility + +import it.unimi.dsi.fastutil.ints.IntOpenHashSet +import net.minestom.server.MinecraftServer +import net.minestom.server.entity.AutomaticPlayerVisibility +import net.minestom.server.entity.Player +import net.minestom.server.instance.EntityTracker +import net.minestom.server.instance.Instance +import net.minestom.server.property.ServerProperties +import net.minestom.server.timer.Task +import net.minestom.server.timer.TaskSchedule + +/** + * Scheduler task that selects the visible players of every viewer with a [NearestPlayerSelector]. + * + * Runs every tick. A viewer is refreshed when its rule or instance changed, and otherwise once + * every [PlayerVisibilitySettings.refreshIntervalTicks] ticks, staggered by entity ID. A failing + * refresh is logged and does not affect other viewers. Selection state is confined to the tick; + * the service is not thread-safe. + */ +internal class PlayerVisibilityService( + private val settings: PlayerVisibilitySettings, + private val handlers: (Player) -> PlayerVisibilityHandler?, +) { + + private val selector = NearestPlayerSelector( + maxVisible = settings.maxVisible, + activateAt = settings.activateAt, + deactivateBelow = settings.deactivateBelow, + retentionDistanceFactor = settings.retentionDistanceFactor, + ) + + private val candidateIds = IntOpenHashSet() + + private var task: Task? = null + private var phase = 0 + + fun start() { + check(task == null) { + "Player visibility service is already running" + } + + task = MinecraftServer.getSchedulerManager() + .buildTask(::tick) + .delay(TaskSchedule.tick(1)) + .repeat(TaskSchedule.tick(1)) + .schedule() + } + + fun stop() { + task?.cancel() + task = null + } + + private fun tick() { + val currentPhase = phase + + phase = if (phase + 1 == settings.refreshIntervalTicks) { + 0 + } else { + phase + 1 + } + + for (viewer in MinecraftServer.getConnectionManager().onlinePlayers) { + val handler = handlers(viewer) ?: continue + val instance = viewer.instance ?: continue + + if (!viewer.isActive || viewer.isRemoved) continue + + val ruleChanged = handler.applyRequestedRule() || handler.requiresNativeRuleRefresh + + val previous = handler.visibilitySelection + val instanceChanged = previous.instance !== instance + + val scheduled = Math.floorMod( + viewer.entityId, + settings.refreshIntervalTicks, + ) == currentPhase + + if (!ruleChanged && !instanceChanged && !scheduled) continue + + try { + refresh( + viewer = viewer, + handler = handler, + instance = instance, + previous = previous, + instanceChanged = instanceChanged, + ruleChanged = ruleChanged, + ) + } catch (exception: Exception) { + MinecraftServer.LOGGER.error( + "Failed to refresh player visibility for {}", + viewer.username, + exception, + ) + } + } + } + + private fun refresh( + viewer: Player, + handler: PlayerVisibilityHandler, + instance: Instance, + previous: PlayerVisibilityHandler.VisibilitySelection, + instanceChanged: Boolean, + ruleChanged: Boolean, + ) { + val previousIds = if (instanceChanged) { + PlayerVisibilityHandler.EMPTY_IDS + } else { + previous.ids + } + + selector.begin( + previouslyVisible = previousIds, + wasLimited = handler.visibilityLimited && !instanceChanged, + ) + + candidateIds.clear() + + if (viewer.autoViewEntities()) { + val position = viewer.position + + instance.entityTracker.nearbyEntitiesByChunkRange( + position, + ServerProperties.ENTITY_VIEW_DISTANCE.get(), + EntityTracker.Target.PLAYERS, + ) { target -> + if ( + target !== viewer && + candidateIds.add(target.entityId) && + target.instance === instance && + target.isActive && + !target.isRemoved && + handler.allowsWithoutBudget(target) && + AutomaticPlayerVisibility.targetAllows(target, viewer) + ) { + selector.offer( + target.entityId, + position.distanceSquared(target.position), + ) + } + } + } + + val nextIds = selector.finish() + handler.visibilityLimited = selector.limited + + handler.pausePlayerAdmission() + + try { + val oldInstance = previous.instance + + if (oldInstance != null) { + for (id in previous.ids) { + if (instanceChanged || nextIds.binarySearch(id) < 0) { + val target = oldInstance.getEntityById(id) as? Player + ?: continue + + AutomaticPlayerVisibility.hide(target, viewer) + } + } + } + + handler.publishSelection(instance, nextIds) + } finally { + handler.resumePlayerAdmission() + } + + for (id in nextIds) { + val target = instance.getEntityById(id) as? Player + ?: continue + + if (withinTrackingRange(viewer, target)) { + AutomaticPlayerVisibility.show(target, viewer) + } + } + + if (ruleChanged) { + handler.refreshNativeViewerRule() + } + } + + private fun withinTrackingRange( + viewer: Player, + target: Player, + ): Boolean { + val origin = viewer.position + val position = target.position + val range = ServerProperties.ENTITY_VIEW_DISTANCE.get() + + return position.chunkX() >= origin.chunkX() - range && + position.chunkX() <= origin.chunkX() + range && + position.chunkZ() >= origin.chunkZ() - range && + position.chunkZ() <= origin.chunkZ() + range + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-plugins/api/surf-api-minestom-server-plugins.api b/surf-api-minestom/surf-api-minestom-server-plugins/api/surf-api-minestom-server-plugins.api new file mode 100644 index 000000000..33a9a64ab --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-plugins/api/surf-api-minestom-server-plugins.api @@ -0,0 +1,25 @@ +public final class dev/slne/surf/api/minestom/server/plugins/PluginsFeature : dev/slne/surf/api/minestom/server/SurfMinestomFeature { + public static final field Companion Ldev/slne/surf/api/minestom/server/plugins/PluginsFeature$Companion; + public static final field ID Ljava/lang/String; + public fun disable (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public fun enable (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public fun getId ()Ljava/lang/String; + public fun getLoadPriority ()I + public fun load (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V +} + +public final class dev/slne/surf/api/minestom/server/plugins/PluginsFeature$Companion { +} + +public final class dev/slne/surf/api/minestom/server/plugins/PluginsFeatureBuilder { + public final fun getDirectory ()Ljava/nio/file/Path; + public final fun getLoadClassPathPlugins ()Z + public final fun setDirectory (Ljava/nio/file/Path;)V + public final fun setLoadClassPathPlugins (Z)V +} + +public final class dev/slne/surf/api/minestom/server/plugins/PluginsFeatureKt { + public static final fun withPlugins (Ldev/slne/surf/api/minestom/server/SurfMinestomServerBuilder;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun withPlugins$default (Ldev/slne/surf/api/minestom/server/SurfMinestomServerBuilder;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V +} + diff --git a/surf-api-minestom/surf-api-minestom-server-plugins/build.gradle.kts b/surf-api-minestom/surf-api-minestom-server-plugins/build.gradle.kts new file mode 100644 index 000000000..81b593204 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-plugins/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + `core-convention` + `api-validation` +} + +dependencies { + api(projects.surfApiMinestom.surfApiMinestomServer) + compileOnlyApi(libs.minestom.server) + + testImplementation(libs.minestom.server) + testImplementation(libs.minestom.testing) + testImplementation(testFixtures(projects.surfApiMinestom.surfApiMinestomServer)) +} + +description = "surf-api-minestom-server-plugins" + +tasks.test { + systemProperty("minestom.inside-test", "true") + systemProperty("junit.jupiter.execution.timeout.default", "2m") +} diff --git a/surf-api-minestom/surf-api-minestom-server-plugins/src/main/kotlin/dev/slne/surf/api/minestom/server/plugins/PluginsFeature.kt b/surf-api-minestom/surf-api-minestom-server-plugins/src/main/kotlin/dev/slne/surf/api/minestom/server/plugins/PluginsFeature.kt new file mode 100644 index 000000000..9da61cb54 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-plugins/src/main/kotlin/dev/slne/surf/api/minestom/server/plugins/PluginsFeature.kt @@ -0,0 +1,131 @@ +package dev.slne.surf.api.minestom.server.plugins + +import dev.slne.surf.api.minestom.plugin.PluginManager +import dev.slne.surf.api.minestom.server.SurfMinestomFeature +import dev.slne.surf.api.minestom.server.SurfMinestomServer +import dev.slne.surf.api.minestom.server.SurfMinestomServerBuilder +import dev.slne.surf.api.minestom.server.SurfMinestomServerDsl +import dev.slne.surf.api.minestom.server.plugins.impl.PluginDiscovery +import dev.slne.surf.api.minestom.server.plugins.impl.PluginManagerImpl +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import java.io.File +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.createDirectories + +/** + * Configures [withPlugins]. + */ +@SurfMinestomServerDsl +class PluginsFeatureBuilder internal constructor() { + + /** + * The directory plugin jars are loaded from, and their data directories are created in. + * + * When the server declares plugin dependencies and this value is changed, also start it with + * `-Dsurf.minestom.plugins=`, so that the bootstrap reads the same directory. + */ + var directory: Path = Path("plugins") + + /** + * Whether plugins shaded into the server itself are loaded too, e.g. the plugin modules of the + * same Gradle project. + */ + var loadClassPathPlugins: Boolean = true +} + +/** + * Loads the plugins in the `plugins` directory and those shaded into the server. + * + * Plugins extend [dev.slne.surf.api.minestom.plugin.SurfMinestomPlugin] and are described by a + * `minestom-plugin.json`, which the `dev.slne.surf.api.gradle.minestom` Gradle plugin generates. + * Each plugin jar loaded from the `plugins` directory gets its own class loader, which sees the + * server, the plugin itself and the plugins it depends on, but no other plugin. + * + * Plugins load in dependency order while the server starts, are enabled on its first tick and are + * disabled in reverse order on shutdown. + * + * The server's own code cannot see into a plugin class loader, so it can only use plugins on the + * system class path. The bootstrap of the server jar places the plugins declared in + * `surfMinestomServerApi { pluginDependencies { } }`, together with their dependencies, there, and + * the feature loads them from there. + */ +@OptIn(InternalSurfApi::class) +class PluginsFeature internal constructor( + private val settings: PluginsFeatureBuilder, +) : SurfMinestomFeature { + + override val id: String = ID + + /** + * Loads after every other feature, so that plugins can use them. + */ + override val loadPriority: Int = Int.MAX_VALUE + + private var manager: PluginManagerImpl? = null + + override fun load(api: SurfMinestomServer) { + val directory = settings.directory.createDirectories() + val serverClassLoader = PluginsFeature::class.java.classLoader + + val classPath = if (settings.loadClassPathPlugins) PluginDiscovery.fromClassPath(serverClassLoader) else emptyList() + val classPathIds = classPath.mapTo(mutableSetOf()) { it.meta.id } + val serverPluginJars = serverPluginJars() + + val candidates = buildList { + addAll(classPath) + for (candidate in PluginDiscovery.fromDirectory(directory)) { + val jar = candidate.jar?.toAbsolutePath()?.normalize() + when { + jar !in serverPluginJars -> add(candidate) + // The class path index already lists the plugin of this bootstrap-placed jar + candidate.meta.id in classPathIds -> Unit + else -> add(candidate.copy(jar = null)) + } + } + } + + val manager = PluginManagerImpl(directory, api.eventNode, serverClassLoader) + this.manager = manager + PluginManager.install(manager) + + manager.load(candidates) + } + + override fun enable(api: SurfMinestomServer) { + manager?.enable() + } + + override fun disable(api: SurfMinestomServer) { + manager?.disableAll() + manager = null + PluginManager.install(null) + } + + private fun serverPluginJars(): Set = + System.getProperty(SERVER_PLUGINS_PROPERTY).orEmpty() + .split(File.pathSeparator) + .filter(String::isNotEmpty) + .mapTo(mutableSetOf()) { Path(it).toAbsolutePath().normalize() } + + companion object { + const val ID = "plugins" + + /** + * System property holding the plugin jars the bootstrap placed on the system class path, + * separated by [File.pathSeparator]. Set by `ServerPlugins` in the bootstrap module. + */ + private const val SERVER_PLUGINS_PROPERTY = "surf.minestom.server-plugins" + } +} + +/** + * Installs plugin loading. + * + * Plugins load after every other feature, regardless of where this is called. + * + * @see PluginsFeature + */ +fun SurfMinestomServerBuilder.withPlugins(block: PluginsFeatureBuilder.() -> Unit = {}) { + install(PluginsFeature(PluginsFeatureBuilder().apply(block))) +} diff --git a/surf-api-minestom/surf-api-minestom-server-plugins/src/main/kotlin/dev/slne/surf/api/minestom/server/plugins/impl/PluginCandidate.kt b/surf-api-minestom/surf-api-minestom-server-plugins/src/main/kotlin/dev/slne/surf/api/minestom/server/plugins/impl/PluginCandidate.kt new file mode 100644 index 000000000..da5b21f64 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-plugins/src/main/kotlin/dev/slne/surf/api/minestom/server/plugins/impl/PluginCandidate.kt @@ -0,0 +1,92 @@ +package dev.slne.surf.api.minestom.server.plugins.impl + +import dev.slne.surf.api.minestom.plugin.PluginMeta +import kotlinx.serialization.json.Json +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import java.nio.file.Path +import java.util.jar.JarFile +import kotlin.io.path.extension +import kotlin.io.path.isDirectory +import kotlin.io.path.isRegularFile +import kotlin.io.path.listDirectoryEntries + +/** + * A plugin that was found but not loaded yet. + * + * @property jar the plugin's jar, or `null` for a plugin on the system class path + */ +internal data class PluginCandidate( + val meta: PluginMeta, + val jar: Path?, +) { + val source: String get() = jar?.fileName?.toString() ?: "class path" +} + +/** + * Finds the plugins in a directory and on the system class path. + */ +internal object PluginDiscovery { + private val LOGGER = ComponentLogger.logger() + + private val json = Json { + ignoreUnknownKeys = true + } + + /** + * Every jar in [directory] that carries a `minestom-plugin.json`. + */ + fun fromDirectory(directory: Path): List { + if (!directory.isDirectory()) return emptyList() + + return directory.listDirectoryEntries() + .filter { it.isRegularFile() && it.extension.equals("jar", ignoreCase = true) } + .sortedBy { it.fileName.toString() } + .mapNotNull(::fromJar) + } + + fun fromJar(jar: Path): PluginCandidate? = try { + JarFile(jar.toFile()).use { file -> + val entry = file.getJarEntry(PluginMeta.FILE_NAME) + if (entry == null) { + LOGGER.warn("{} is no plugin, it carries no {}", jar.fileName, PluginMeta.FILE_NAME) + return null + } + + val content = file.getInputStream(entry).use { it.readBytes().decodeToString() } + PluginCandidate(parse(content), jar) + } + } catch (failure: Exception) { + LOGGER.error("Could not read the plugin {}", jar.fileName, failure) + null + } + + /** + * The plugins shaded into the server itself, listed in [PluginMeta.CLASSPATH_INDEX]. + */ + fun fromClassPath(classLoader: ClassLoader): List { + val mainClasses = classLoader.getResources(PluginMeta.CLASSPATH_INDEX).toList() + .flatMap { url -> url.openStream().use { it.readBytes().decodeToString() }.lines() } + .map { line -> line.substringBefore('#').trim() } + .filter(String::isNotEmpty) + .distinct() + + return mainClasses.mapNotNull { mainClass -> + val path = "${PluginMeta.CLASSPATH_DIRECTORY}$mainClass.json" + val resource = classLoader.getResource(path) + if (resource == null) { + LOGGER.warn("The plugin {} on the class path has no {}", mainClass, path) + return@mapNotNull null + } + + try { + val meta = parse(resource.openStream().use { it.readBytes().decodeToString() }) + PluginCandidate(meta, jar = null) + } catch (failure: Exception) { + LOGGER.error("Could not read the plugin {} on the class path", mainClass, failure) + null + } + } + } + + fun parse(content: String): PluginMeta = json.decodeFromString(PluginMeta.serializer(), content) +} diff --git a/surf-api-minestom/surf-api-minestom-server-plugins/src/main/kotlin/dev/slne/surf/api/minestom/server/plugins/impl/PluginClassLoader.kt b/surf-api-minestom/surf-api-minestom-server-plugins/src/main/kotlin/dev/slne/surf/api/minestom/server/plugins/impl/PluginClassLoader.kt new file mode 100644 index 000000000..1b00a7f35 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-plugins/src/main/kotlin/dev/slne/surf/api/minestom/server/plugins/impl/PluginClassLoader.kt @@ -0,0 +1,65 @@ +package dev.slne.surf.api.minestom.server.plugins.impl + +import java.net.URLClassLoader +import java.nio.file.Path + +/** + * Loads the classes of one plugin jar. + * + * Lookup order is the server class loader, the plugin's own jar, then the jars of [dependencies]. + * A plugin therefore sees the server, itself and its dependencies, but no other plugin. + */ +internal class PluginClassLoader( + val pluginId: String, + jar: Path, + parent: ClassLoader, +) : URLClassLoader("plugin-$pluginId", arrayOf(jar.toUri().toURL()), parent) { + + /** + * The class loaders of all plugins this one depends on, directly or transitively. Plugins + * on the system class path have no loader and are not listed. + */ + @Volatile + var dependencies: List = emptyList() + + override fun loadClass(name: String, resolve: Boolean): Class<*> { + synchronized(getClassLoadingLock(name)) { + val loaded = findLoadedClass(name) + ?: fromParent(name) + ?: fromOwnJar(name) + ?: dependencies.firstNotNullOfOrNull { dependency -> dependency.findOwnClass(name) } + ?: throw ClassNotFoundException(name) + + if (resolve) resolveClass(loaded) + return loaded + } + } + + /** + * Returns the class from this plugin's own jar, or `null`. Does not consult the server or other + * plugins. + */ + fun findOwnClass(name: String): Class<*>? = synchronized(getClassLoadingLock(name)) { + findLoadedClass(name) ?: fromOwnJar(name) + } + + private fun fromParent(name: String): Class<*>? = try { + parent.loadClass(name) + } catch (_: ClassNotFoundException) { + null + } + + private fun fromOwnJar(name: String): Class<*>? = try { + findClass(name) + } catch (_: ClassNotFoundException) { + null + } + + override fun toString(): String = "PluginClassLoader($pluginId)" + + private companion object { + init { + registerAsParallelCapable() + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-plugins/src/main/kotlin/dev/slne/surf/api/minestom/server/plugins/impl/PluginLoadOrder.kt b/surf-api-minestom/surf-api-minestom-server-plugins/src/main/kotlin/dev/slne/surf/api/minestom/server/plugins/impl/PluginLoadOrder.kt new file mode 100644 index 000000000..4d5ec5919 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-plugins/src/main/kotlin/dev/slne/surf/api/minestom/server/plugins/impl/PluginLoadOrder.kt @@ -0,0 +1,144 @@ +package dev.slne.surf.api.minestom.server.plugins.impl + +/** + * Orders plugins so that every plugin comes after the plugins it depends on. + */ +internal object PluginLoadOrder { + + /** + * The outcome of [order]. + * + * @property ordered the loadable plugins, dependencies first + * @property rejected the reason each other plugin cannot be loaded, keyed by plugin ID; a + * duplicate ID is keyed as `id (source)` + */ + data class Result( + val ordered: List, + val rejected: Map, + ) + + /** + * Orders [candidates] by their dependencies, keeping the input order otherwise. + * + * The first candidate with an ID wins; later ones are rejected. Plugins with a missing required + * dependency, members of a dependency cycle and plugins depending on a rejected plugin are + * rejected as well. + * + * Optional dependencies only influence the order and never reject a plugin: one that is missing, + * refers to a rejected plugin or would close a cycle is ignored. + */ + fun order(candidates: List): Result { + val rejected = linkedMapOf() + val byId = linkedMapOf() + + for (candidate in candidates) { + val id = candidate.meta.id + val existing = byId[id] + if (existing != null) { + rejected["$id (${candidate.source})"] = "the id is already taken by ${existing.source}" + continue + } + byId[id] = candidate + } + + // Repeats because removing a plugin can leave its dependents without a dependency + var changed = true + while (changed) { + changed = false + for ((id, candidate) in byId.entries.toList()) { + val missing = candidate.meta.dependencies + .filter { dependency -> !dependency.optional && dependency.id !in byId } + .map { it.id } + + if (missing.isNotEmpty()) { + byId.remove(id) + rejected[id] = "it depends on ${missing.joinToString()}, which " + + (if (missing.size == 1) "is" else "are") + " not available" + changed = true + } + } + } + + val ordered = mutableListOf() + val state = mutableMapOf() + + fun visit(candidate: PluginCandidate, path: List): Boolean { + val id = candidate.meta.id + when (state[id]) { + VisitState.DONE -> return true + VisitState.FAILED -> return false + VisitState.VISITING -> { + val cycle = path.dropWhile { it != id } + id + cycle.forEach { member -> + state[member] = VisitState.FAILED + rejected[member] = "of the dependency cycle ${cycle.joinToString(" -> ")}" + } + return false + } + + null -> Unit + } + + state[id] = VisitState.VISITING + for (dependency in candidate.meta.dependencies) { + if (dependency.optional) continue + val target = byId[dependency.id] ?: continue + if (!visit(target, path + id)) { + if (state[id] != VisitState.FAILED) { + state[id] = VisitState.FAILED + rejected.putIfAbsent(id, "its dependency ${dependency.id} cannot be loaded") + } + return false + } + } + + if (state[id] == VisitState.FAILED) return false + + state[id] = VisitState.DONE + ordered += candidate + return true + } + + byId.values.forEach { candidate -> visit(candidate, emptyList()) } + + val loadable = ordered.mapTo(hashSetOf()) { it.meta.id } + return Result(withOptionalDependencies(byId.values.filter { it.meta.id in loadable }), rejected) + } + + /** + * Orders [candidates], which are loadable, so that each comes after its required dependencies + * and after those of its optional dependencies that do not depend on it in turn. + */ + private fun withOptionalDependencies(candidates: List): List { + val byId = candidates.associateBy { it.meta.id } + val edges = byId.mapValues { (_, candidate) -> + candidate.meta.dependencies.filter { !it.optional }.mapTo(linkedSetOf()) { it.id } + } + + fun dependsOn(id: String, target: String, seen: MutableSet = hashSetOf()): Boolean = + id == target || (seen.add(id) && edges.getValue(id).any { dependsOn(it, target, seen) }) + + for (candidate in candidates) { + val id = candidate.meta.id + for (dependency in candidate.meta.dependencies) { + if (dependency.optional && dependency.id in byId && !dependsOn(dependency.id, id)) { + edges.getValue(id) += dependency.id + } + } + } + + val result = mutableListOf() + val emitted = hashSetOf() + + fun emit(id: String) { + if (!emitted.add(id)) return + edges.getValue(id).forEach(::emit) + result += byId.getValue(id) + } + + byId.keys.forEach(::emit) + return result + } + + private enum class VisitState { VISITING, DONE, FAILED } +} diff --git a/surf-api-minestom/surf-api-minestom-server-plugins/src/main/kotlin/dev/slne/surf/api/minestom/server/plugins/impl/PluginManagerImpl.kt b/surf-api-minestom/surf-api-minestom-server-plugins/src/main/kotlin/dev/slne/surf/api/minestom/server/plugins/impl/PluginManagerImpl.kt new file mode 100644 index 000000000..d3ba750fe --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-plugins/src/main/kotlin/dev/slne/surf/api/minestom/server/plugins/impl/PluginManagerImpl.kt @@ -0,0 +1,286 @@ +package dev.slne.surf.api.minestom.server.plugins.impl + +import dev.slne.surf.api.minestom.plugin.PluginManager +import dev.slne.surf.api.minestom.plugin.SurfMinestomPlugin +import dev.slne.surf.api.minestom.server.impl.command.PluginCommands +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import kotlinx.coroutines.* +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import net.minestom.server.event.Event +import net.minestom.server.event.EventNode +import java.nio.file.Path +import kotlin.coroutines.AbstractCoroutineContextElement +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.io.path.div + +/** + * Loads, enables and disables the plugins of the server. + * + * Lifecycle functions run to completion on the calling thread. A plugin that throws from `onLoad` + * is cleaned up and removed, and plugins that require it are not loaded. A plugin that throws from + * `onEnable` is disabled and removed, and plugins that require it are disabled without being + * enabled. [plugins] and [plugin] may be called from any thread. + * + * While a lifecycle function runs, and in a plugin's coroutine scope, the thread context class + * loader is the plugin's class loader. + */ +@OptIn(InternalSurfApi::class) +internal class PluginManagerImpl( + private val directory: Path, + private val parentNode: EventNode, + private val serverClassLoader: ClassLoader, +) : PluginManager { + private val loaded = mutableListOf() + + override val plugins: Collection + get() = synchronized(loaded) { loaded.map { it.plugin } } + + override fun plugin(id: String): SurfMinestomPlugin? = + synchronized(loaded) { loaded.firstOrNull { it.plugin.meta.id == id }?.plugin } + + /** + * Creates every loadable plugin of [candidates] in dependency order and calls its `onLoad`. + * Plugins that cannot be loaded are logged. + */ + fun load(candidates: List) { + val order = PluginLoadOrder.order(candidates) + order.rejected.forEach { (id, reason) -> + LOGGER.error("Not loading the plugin {}, because {}", id, reason) + } + + val failed = mutableSetOf() + val loaders = mutableMapOf() + + for (candidate in order.ordered) { + val meta = candidate.meta + val failedDependency = meta.dependencies.firstOrNull { !it.optional && it.id in failed } + if (failedDependency != null) { + LOGGER.error("Not loading the plugin {}, because its dependency {} failed", meta.id, failedDependency.id) + failed += meta.id + continue + } + + val loadedPlugin = try { + create(candidate, loaders) + } catch (failure: Throwable) { + LOGGER.error("Could not create the plugin {} from {}", meta.id, candidate.source, failure) + failed += meta.id + continue + } + + LOGGER.info("Loading {} {}", meta.name, meta.version) + if (!runLifecycle(loadedPlugin, "load") { loadedPlugin.plugin.onLoad() }) { + cleanUp(loadedPlugin) + loaders -= meta.id + failed += meta.id + continue + } + + synchronized(loaded) { loaded += loadedPlugin } + } + } + + /** + * Calls `onEnable` of every loaded plugin. A plugin that fails, or whose required dependency + * failed, is disabled, which includes calling its `onDisable`. + */ + fun enable() { + val failed = mutableSetOf() + + for (loadedPlugin in synchronized(loaded) { loaded.toList() }) { + val meta = loadedPlugin.plugin.meta + val failedDependency = meta.dependencies.firstOrNull { !it.optional && it.id in failed } + if (failedDependency != null) { + LOGGER.error("Not enabling the plugin {}, because its dependency {} failed", meta.id, failedDependency.id) + failed += meta.id + disable(loadedPlugin) + continue + } + + LOGGER.info("Enabling {} {}", meta.name, meta.version) + + if (!runLifecycle(loadedPlugin, "enable") { loadedPlugin.plugin.onEnable() }) { + failed += meta.id + disable(loadedPlugin) + } + } + } + + /** + * Disables every plugin in reverse load order. + */ + fun disableAll() { + for (loadedPlugin in synchronized(loaded) { loaded.toList() }.asReversed()) { + disable(loadedPlugin) + } + } + + private fun disable(loadedPlugin: LoadedPlugin) { + val meta = loadedPlugin.plugin.meta + LOGGER.info("Disabling {} {}", meta.name, meta.version) + + runLifecycle(loadedPlugin, "disable") { loadedPlugin.plugin.onDisable() } + cleanUp(loadedPlugin) + synchronized(loaded) { loaded -= loadedPlugin } + } + + private fun create( + candidate: PluginCandidate, + loaders: MutableMap, + ): LoadedPlugin { + val meta = candidate.meta + val classLoader = candidate.jar?.let { jar -> + PluginClassLoader(meta.id, jar, serverClassLoader).also { loader -> + loader.dependencies = transitiveDependencies(meta.id, candidate, loaders) + loaders[meta.id] = loader + } + } + + val logger = ComponentLogger.logger(meta.name) + val eventNode = EventNode.all("plugin-${meta.id}") + var scope: CoroutineScope? = null + + try { + // The context must exist during construction so property initializers can use it; for an + // `object` that is during class initialization. + val plugin = SurfMinestomPlugin.constructing({ plugin -> + val pluginScope = CoroutineScope( + SupervisorJob() + + Dispatchers.Default + + CoroutineName("plugin-${meta.id}") + + SurfMinestomPlugin.contextElement(plugin) + + contextClassLoaderElement(classLoader) + + CoroutineExceptionHandler { _, failure -> + logger.error("Unhandled exception in a coroutine of {}", meta.id, failure) + } + ) + check(scope == null) { "${meta.main} constructed more than one plugin" } + scope = pluginScope + + SurfMinestomPlugin.Context( + meta = meta, + eventNode = eventNode, + dataDirectory = directory / meta.id, + logger = logger, + scope = pluginScope, + ) + }) { + val mainClass = Class.forName(meta.main, false, classLoader ?: serverClassLoader) + require(SurfMinestomPlugin::class.java.isAssignableFrom(mainClass)) { + "${meta.main} does not extend ${SurfMinestomPlugin::class.java.name}" + } + instantiate(mainClass) + } + + val pluginScope = checkNotNull(scope) { "${meta.main} was not constructed by the server" } + parentNode.addChild(eventNode) + + return LoadedPlugin(plugin, classLoader, pluginScope) + } catch (failure: Throwable) { + scope?.cancel() + loaders -= meta.id + closeQuietly(classLoader, meta.id) + throw failure + } + } + + /** + * Returns the `object` instance of [mainClass], or a new instance from its no-argument + * constructor. + */ + private fun instantiate(mainClass: Class<*>): SurfMinestomPlugin { + val objectInstance = mainClass.declaredFields.firstOrNull { field -> + field.name == "INSTANCE" && + java.lang.reflect.Modifier.isStatic(field.modifiers) && + field.type == mainClass + } + + val instance = objectInstance?.get(null) ?: mainClass.getDeclaredConstructor().newInstance() + return instance as SurfMinestomPlugin + } + + private fun transitiveDependencies( + id: String, + candidate: PluginCandidate, + loaders: Map, + ): List { + val result = linkedSetOf() + val pending = ArrayDeque(candidate.meta.dependencies.map { it.id }) + while (pending.isNotEmpty()) { + val dependency = loaders[pending.removeFirst()] ?: continue + if (dependency.pluginId != id && result.add(dependency)) { + pending += dependency.dependencies.map { it.pluginId } + } + } + return result.toList() + } + + /** + * Runs a lifecycle function of the plugin and returns whether it completed without throwing. + */ + private fun runLifecycle( + loadedPlugin: LoadedPlugin, + phase: String, + block: suspend () -> Unit, + ): Boolean = try { + val context = SurfMinestomPlugin.contextElement(loadedPlugin.plugin) + + contextClassLoaderElement(loadedPlugin.classLoader) + runBlocking(context) { block() } + true + } catch (failure: Throwable) { + LOGGER.error("Could not {} the plugin {}", phase, loadedPlugin.plugin.meta.id, failure) + false + } + + private fun cleanUp(loadedPlugin: LoadedPlugin) { + val plugin = loadedPlugin.plugin + + PluginCommands.unregisterAll(plugin) + parentNode.removeChild(plugin.eventNode) + loadedPlugin.scope.cancel() + closeQuietly(loadedPlugin.classLoader, plugin.meta.id) + } + + private fun closeQuietly(classLoader: PluginClassLoader?, pluginId: String) { + try { + classLoader?.close() + } catch (failure: Exception) { + LOGGER.warn("Could not close the class loader of {}", pluginId, failure) + } + } + + /** + * Makes [classLoader] the thread context class loader of the thread a coroutine runs on and + * restores the previous one afterwards. Empty for a plugin without a class loader of its own. + */ + private fun contextClassLoaderElement(classLoader: ClassLoader?): CoroutineContext = + if (classLoader == null) EmptyCoroutineContext else ContextClassLoaderElement(classLoader) + + private class ContextClassLoaderElement( + private val classLoader: ClassLoader, + ) : AbstractCoroutineContextElement(Key), ThreadContextElement { + override fun updateThreadContext(context: CoroutineContext): ClassLoader? { + val thread = Thread.currentThread() + val previous = thread.contextClassLoader + thread.contextClassLoader = classLoader + return previous + } + + override fun restoreThreadContext(context: CoroutineContext, oldState: ClassLoader?) { + Thread.currentThread().contextClassLoader = oldState + } + + companion object Key : CoroutineContext.Key + } + + private class LoadedPlugin( + val plugin: SurfMinestomPlugin, + val classLoader: PluginClassLoader?, + val scope: CoroutineScope, + ) + + private companion object { + val LOGGER = ComponentLogger.logger("PluginManager") + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-plugins/src/test/kotlin/dev/slne/surf/api/minestom/server/plugins/PluginLoadOrderTest.kt b/surf-api-minestom/surf-api-minestom-server-plugins/src/test/kotlin/dev/slne/surf/api/minestom/server/plugins/PluginLoadOrderTest.kt new file mode 100644 index 000000000..9924b6b9f --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-plugins/src/test/kotlin/dev/slne/surf/api/minestom/server/plugins/PluginLoadOrderTest.kt @@ -0,0 +1,129 @@ +package dev.slne.surf.api.minestom.server.plugins + +import dev.slne.surf.api.minestom.plugin.PluginDependency +import dev.slne.surf.api.minestom.plugin.PluginMeta +import dev.slne.surf.api.minestom.server.plugins.impl.PluginCandidate +import dev.slne.surf.api.minestom.server.plugins.impl.PluginLoadOrder +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class PluginLoadOrderTest { + + private fun plugin(id: String, vararg dependencies: PluginDependency) = + PluginCandidate(PluginMeta(id, "example.Main", dependencies = dependencies.toList()), jar = null) + + private fun required(id: String) = PluginDependency(id) + private fun optional(id: String) = PluginDependency(id, optional = true) + + private fun PluginLoadOrder.Result.ids() = ordered.map { it.meta.id } + + @Test + fun `dependencies come before the plugins that need them`() { + val result = PluginLoadOrder.order( + listOf( + plugin("chat", required("core")), + plugin("lobby", required("chat"), optional("queue")), + plugin("core"), + plugin("queue"), + ) + ) + + val ids = result.ids() + assertTrue(ids.indexOf("core") < ids.indexOf("chat")) + assertTrue(ids.indexOf("chat") < ids.indexOf("lobby")) + assertTrue(ids.indexOf("queue") < ids.indexOf("lobby")) + assertEquals(emptyMap(), result.rejected) + } + + @Test + fun `a missing required dependency rejects the plugin and its dependents`() { + val result = PluginLoadOrder.order( + listOf( + plugin("chat", required("core")), + plugin("lobby", required("chat")), + plugin("standalone", optional("core")), + ) + ) + + assertEquals(listOf("standalone"), result.ids()) + assertEquals(setOf("chat", "lobby"), result.rejected.keys) + } + + @Test + fun `a dependency cycle rejects every member`() { + val result = PluginLoadOrder.order( + listOf( + plugin("a", required("b")), + plugin("b", required("a")), + plugin("c"), + ) + ) + + assertEquals(listOf("c"), result.ids()) + assertTrue(result.rejected.keys.containsAll(setOf("a", "b"))) + } + + @Test + fun `a duplicate id keeps the first plugin`() { + val first = plugin("core") + val result = PluginLoadOrder.order(listOf(first, plugin("core"))) + + assertEquals(listOf(first), result.ordered) + assertEquals(1, result.rejected.size) + } + + @Test + fun `plugins that optionally depend on each other are both loaded`() { + val result = PluginLoadOrder.order( + listOf( + plugin("a", optional("b")), + plugin("b", optional("a")), + ) + ) + + assertEquals(setOf("a", "b"), result.ids().toSet()) + assertEquals(2, result.ids().size) + assertEquals(emptyMap(), result.rejected) + } + + @Test + fun `an optional dependency that would close a cycle is ignored`() { + val result = PluginLoadOrder.order( + listOf( + plugin("a", optional("b")), + plugin("b", required("a")), + ) + ) + + assertEquals(listOf("a", "b"), result.ids()) + assertEquals(emptyMap(), result.rejected) + } + + @Test + fun `an optional dependency on a cycle member does not reject the plugin`() { + val result = PluginLoadOrder.order( + listOf( + plugin("x", optional("a")), + plugin("a", required("b")), + plugin("b", required("a")), + ) + ) + + assertEquals(listOf("x"), result.ids()) + assertEquals(setOf("a", "b"), result.rejected.keys) + } + + @Test + fun `an optional dependency on a rejected plugin does not reject the plugin`() { + val result = PluginLoadOrder.order( + listOf( + plugin("x", optional("broken")), + plugin("broken", required("missing")), + ) + ) + + assertEquals(listOf("x"), result.ids()) + assertEquals(setOf("broken"), result.rejected.keys) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-plugins/src/test/kotlin/dev/slne/surf/api/minestom/server/plugins/PluginsFeatureTest.kt b/surf-api-minestom/surf-api-minestom-server-plugins/src/test/kotlin/dev/slne/surf/api/minestom/server/plugins/PluginsFeatureTest.kt new file mode 100644 index 000000000..8630c300f --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-plugins/src/test/kotlin/dev/slne/surf/api/minestom/server/plugins/PluginsFeatureTest.kt @@ -0,0 +1,77 @@ +package dev.slne.surf.api.minestom.server.plugins + +import dev.slne.surf.api.minestom.plugin.PluginManager +import dev.slne.surf.api.minestom.plugin.PluginMeta +import dev.slne.surf.api.minestom.server.plugins.impl.PluginDiscovery +import dev.slne.surf.api.minestom.server.surfMinestomServer +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path +import java.util.jar.JarEntry +import java.util.jar.JarOutputStream +import kotlin.io.path.outputStream + +@EnvTest +class PluginsFeatureTest { + + @Test + fun `a plugin on the class path is loaded and disabled with its commands and listeners`( + env: Env, + @TempDir directory: Path, + ) { + TestPlugin.calls.clear() + val commands = env.process().command() + + val server = surfMinestomServer { + withPlugins { this.directory = directory } + } + + val plugin = try { + val plugin = PluginManager.instance.plugin("test-plugin") + assertNotNull(plugin) + assertSame(TestPlugin.instance, plugin) + assertEquals("TestPlugin", plugin!!.meta.name) + assertEquals(directory.resolve("test-plugin"), plugin.dataDirectory) + assertEquals(listOf("load"), TestPlugin.calls) + assertTrue(commands.commandExists("test-plugin-command")) + assertTrue(plugin.eventNode.parent != null) + plugin + } finally { + server.shutdown() + } + + assertEquals(listOf("load", "disable"), TestPlugin.calls) + assertFalse(commands.commandExists("test-plugin-command"), "the plugin's command is unregistered") + assertNull(plugin.eventNode.parent, "the plugin's event node is removed") + } + + @Test + fun `a jar is read through its minestom-plugin json`(@TempDir directory: Path) { + val plugin = directory.resolve("example.jar") + JarOutputStream(plugin.outputStream()).use { jar -> + jar.putNextEntry(JarEntry(PluginMeta.FILE_NAME)) + jar.write("""{ "id": "example", "main": "example.Main", "version": "2.0" }""".toByteArray()) + jar.closeEntry() + } + + val other = directory.resolve("library.jar") + JarOutputStream(other.outputStream()).use { jar -> + jar.putNextEntry(JarEntry("library.txt")) + jar.closeEntry() + } + + val candidates = PluginDiscovery.fromDirectory(directory) + assertEquals(1, candidates.size, "a jar without a plugin file is no plugin") + assertEquals("example", candidates.single().meta.id) + assertEquals("2.0", candidates.single().meta.version) + assertEquals(plugin, candidates.single().jar) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-plugins/src/test/kotlin/dev/slne/surf/api/minestom/server/plugins/TestPlugin.kt b/surf-api-minestom/surf-api-minestom-server-plugins/src/test/kotlin/dev/slne/surf/api/minestom/server/plugins/TestPlugin.kt new file mode 100644 index 000000000..7d8fcf6e5 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-plugins/src/test/kotlin/dev/slne/surf/api/minestom/server/plugins/TestPlugin.kt @@ -0,0 +1,34 @@ +package dev.slne.surf.api.minestom.server.plugins + +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.dsl.anyExecutor +import dev.slne.surf.api.minestom.plugin.SurfMinestomPlugin +import net.minestom.server.event.player.PlayerSpawnEvent + +/** + * A plugin on the test class path, found through its service entry. + */ +class TestPlugin : SurfMinestomPlugin() { + override suspend fun onLoad() { + calls += "load" + eventNode.addListener(PlayerSpawnEvent::class.java) { } + CommandAPICommand("test-plugin-command").anyExecutor { _, _ -> }.register() + } + + override suspend fun onEnable() { + calls += "enable" + } + + override suspend fun onDisable() { + calls += "disable" + } + + companion object { + val calls = mutableListOf() + var instance: TestPlugin? = null + } + + init { + instance = this + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-plugins/src/test/resources/META-INF/services/dev.slne.surf.api.minestom.plugin.SurfMinestomPlugin b/surf-api-minestom/surf-api-minestom-server-plugins/src/test/resources/META-INF/services/dev.slne.surf.api.minestom.plugin.SurfMinestomPlugin new file mode 100644 index 000000000..56334bf27 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-plugins/src/test/resources/META-INF/services/dev.slne.surf.api.minestom.plugin.SurfMinestomPlugin @@ -0,0 +1 @@ +dev.slne.surf.api.minestom.server.plugins.TestPlugin diff --git a/surf-api-minestom/surf-api-minestom-server-plugins/src/test/resources/META-INF/surf-minestom-plugins/dev.slne.surf.api.minestom.server.plugins.TestPlugin.json b/surf-api-minestom/surf-api-minestom-server-plugins/src/test/resources/META-INF/surf-minestom-plugins/dev.slne.surf.api.minestom.server.plugins.TestPlugin.json new file mode 100644 index 000000000..ff1d82407 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-plugins/src/test/resources/META-INF/surf-minestom-plugins/dev.slne.surf.api.minestom.server.plugins.TestPlugin.json @@ -0,0 +1,9 @@ +{ + "id": "test-plugin", + "main": "dev.slne.surf.api.minestom.server.plugins.TestPlugin", + "name": "TestPlugin", + "version": "1.0.0", + "dependencies": [ + { "id": "absent-optional", "optional": true } + ] +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/api/surf-api-minestom-server-signed-chat.api b/surf-api-minestom/surf-api-minestom-server-signed-chat/api/surf-api-minestom-server-signed-chat.api new file mode 100644 index 000000000..0d97cf83e --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/api/surf-api-minestom-server-signed-chat.api @@ -0,0 +1,27 @@ +public final class dev/slne/surf/api/minestom/server/chat/SignedChatFeature : dev/slne/surf/api/minestom/server/SurfMinestomFeature { + public static final field Companion Ldev/slne/surf/api/minestom/server/chat/SignedChatFeature$Companion; + public static final field ID Ljava/lang/String; + public fun disable (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public fun enable (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public fun getId ()Ljava/lang/String; + public fun getLoadPriority ()I + public fun load (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V +} + +public final class dev/slne/surf/api/minestom/server/chat/SignedChatFeature$Companion { +} + +public final class dev/slne/surf/api/minestom/server/chat/SignedChatFeatureKt { + public static final fun withSignedChat (Ldev/slne/surf/api/minestom/server/SurfMinestomServerBuilder;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun withSignedChat$default (Ldev/slne/surf/api/minestom/server/SurfMinestomServerBuilder;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V +} + +public final class dev/slne/surf/api/minestom/server/chat/SignedChatSettings { + public final fun getChatSpamThresholdSeconds ()I + public final fun getCommandSpamThresholdSeconds ()I + public final fun getEnforceSecureProfile ()Z + public final fun setChatSpamThresholdSeconds (I)V + public final fun setCommandSpamThresholdSeconds (I)V + public final fun setEnforceSecureProfile (Z)V +} + diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/build.gradle.kts b/surf-api-minestom/surf-api-minestom-server-signed-chat/build.gradle.kts new file mode 100644 index 000000000..2592025c9 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + `core-convention` + `api-validation` +} + +dependencies { + api(projects.surfApiMinestom.surfApiMinestomServer) + compileOnlyApi(libs.minestom.server) + + testImplementation(libs.minestom.server) + testImplementation(libs.minestom.testing) +} + +description = "surf-api-minestom-server-signed-chat" + +tasks.test { + systemProperty("minestom.inside-test", "true") + systemProperty("junit.jupiter.execution.timeout.default", "2m") +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/SignedChatFeature.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/SignedChatFeature.kt new file mode 100644 index 000000000..a2b7e6b76 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/SignedChatFeature.kt @@ -0,0 +1,203 @@ +package dev.slne.surf.api.minestom.server.chat + +import dev.slne.surf.api.minestom.chat.ChatSessionHolder +import dev.slne.surf.api.minestom.extension.CommandManager +import dev.slne.surf.api.minestom.extension.ConnectionManager +import dev.slne.surf.api.minestom.extension.PacketListenerManager +import dev.slne.surf.api.minestom.extension.addListener +import dev.slne.surf.api.minestom.server.SurfMinestomFeature +import dev.slne.surf.api.minestom.server.SurfMinestomServer +import dev.slne.surf.api.minestom.server.SurfMinestomServerBuilder +import dev.slne.surf.api.minestom.server.SurfMinestomServerDsl +import dev.slne.surf.api.minestom.server.chat.impl.ChatProcessor +import dev.slne.surf.api.minestom.server.chat.impl.ChatSessionHolderImpl +import dev.slne.surf.api.minestom.server.chat.impl.ChatTranslations +import dev.slne.surf.api.minestom.server.chat.impl.SurfChatTypes +import dev.slne.surf.api.minestom.server.chat.impl.signature.PlayerChatMessage +import dev.slne.surf.api.minestom.server.chat.impl.signature.RemoteChatSession +import dev.slne.surf.api.minestom.server.impl.command.SignedCommandArguments +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import net.minestom.server.crypto.ChatSession +import net.minestom.server.entity.GameMode +import net.minestom.server.entity.Player +import net.minestom.server.event.player.AsyncPlayerConfigurationEvent +import net.minestom.server.event.player.PlayerDisconnectEvent +import net.minestom.server.event.player.PlayerSpawnEvent +import net.minestom.server.event.player.PlayerTickEvent +import net.minestom.server.listener.ChatMessageListener +import net.minestom.server.network.packet.client.play.* +import net.minestom.server.network.packet.server.play.PlayerInfoUpdatePacket +import net.minestom.server.utils.PacketSendingUtils + +/** + * Configures [withSignedChat]. + */ +@SurfMinestomServerDsl +class SignedChatSettings internal constructor() { + + /** + * Whether chat messages must be signed with a Mojang-issued profile key. + * + * When enabled, players without a profile key (offline mode, some proxies, Geyser) receive an + * error message instead of having their unsigned messages sent. + */ + var enforceSecureProfile: Boolean = false + + /** + * Vanilla's `chat-spam-threshold-seconds`. + * + * Each chat message adds 20 to a counter that drains by 1 per tick. The player is kicked with + * the `disconnect.spam` message once the counter reaches 20 times this value. 0 or less disables + * the check. + */ + var chatSpamThresholdSeconds: Int = 10 + + /** + * Vanilla's `command-spam-threshold-seconds`; the same mechanism as [chatSpamThresholdSeconds], + * applied to commands. + */ + var commandSpamThresholdSeconds: Int = 10 +} + +/** + * Handles chat the way vanilla does: messages keep their signatures, so clients show them as + * verified and can report them. + * + * Replaces Minestom's play listeners for chat messages, chat acknowledgements, chat session updates + * and commands while the feature is loaded; [disable] restores Minestom's listeners. Chat messages + * pass through [AsyncPlayerChatEvent] off the tick thread, in the order they were received per + * player. The secure chat state of a player is available through [ChatSessionHolder.fromPlayer]. + */ +@OptIn(InternalSurfApi::class) +class SignedChatFeature internal constructor( + private val settings: SignedChatSettings, +) : SurfMinestomFeature { + + override val id: String = ID + + override fun load(api: SurfMinestomServer) { + SurfChatTypes.register() + ChatSessionHolder.installProvider(ChatSessionHolderImpl) + ChatTranslations.register() + registerPacketListeners() + + SignedCommandArguments.unsignedFactory = { sender, content -> + (sender?.let { PlayerChatMessage.unsigned(it, content) } ?: PlayerChatMessage.system(content)) + .adventureView() + } + + api.eventNode.addListener { event -> + ChatSessionHolderImpl.fromPlayer(event.player)?.handler?.tick() + } + // The client starts a new chat state with every configuration phase; so does the handler. + api.eventNode.addListener { event -> + if (!event.isFirstConfig) ChatSessionHolderImpl.remove(event.player)?.handler?.close() + } + api.eventNode.addListener { event -> + if (event.isFirstSpawn) { + holder(event.player) + announceChatSessionsTo(event.player) + } + } + api.eventNode.addListener { event -> + ChatSessionHolderImpl.remove(event.player)?.handler?.close() + } + } + + override fun disable(api: SurfMinestomServer) { + with(PacketListenerManager) { + setPlayListener(ClientChatMessagePacket::class.java, ChatMessageListener::chatMessageListener) + setPlayListener(ClientChatAckPacket::class.java) { _, _ -> } + setPlayListener(ClientChatSessionUpdatePacket::class.java) { _, _ -> } + setPlayListener(ClientCommandChatPacket::class.java, ChatMessageListener::commandChatListener) + setPlayListener( + ClientSignedCommandChatPacket::class.java, + ChatMessageListener::signedCommandChatListener, + ) + } + + SignedCommandArguments.resetUnsignedFactory() + ChatSessionHolder.installProvider(null) + ChatSessionHolderImpl.clear() + } + + private fun holder(player: Player): ChatSessionHolderImpl? = ChatSessionHolderImpl.create(player, settings) + + private fun registerPacketListeners() = with(PacketListenerManager) { + setPlayListener(ClientChatMessagePacket::class.java) { packet, player -> + holder(player)?.handler?.handleChat(packet) { message -> + ChatProcessor(player, message).process() + } + } + + setPlayListener(ClientChatAckPacket::class.java) { packet, player -> + holder(player)?.handler?.handleChatAck(packet) + } + + setPlayListener(ClientChatSessionUpdatePacket::class.java) { packet, player -> + holder(player)?.handler?.handleChatSessionUpdate(packet) { session -> + broadcastChatSession(player, session) + } + } + + setPlayListener(ClientCommandChatPacket::class.java) { packet, player -> + holder(player)?.handler?.handleUnsignedCommandChat(packet.message()) { command -> + CommandManager.execute(player, command) + } + } + + setPlayListener(ClientSignedCommandChatPacket::class.java) { packet, player -> + holder(player)?.handler?.handleSignedCommandChat(packet) { command -> + CommandManager.execute(player, command) + } + } + } + + private fun broadcastChatSession(player: Player, session: RemoteChatSession) { + PacketSendingUtils.broadcastPlayPacket(chatSessionInfoPacket(player, session.asData())) + } + + /** + * Sends [joined] the chat sessions of the players already online. Minestom announces those + * players to a joining player without their chat session. + */ + private fun announceChatSessionsTo(joined: Player) { + for (other in ConnectionManager.onlinePlayers) { + if (other === joined) continue + + val session = ChatSessionHolderImpl.fromPlayer(other)?.chatSession ?: continue + joined.sendPacket(chatSessionInfoPacket(other, session)) + } + } + + private fun chatSessionInfoPacket(player: Player, session: ChatSession) = + PlayerInfoUpdatePacket( + PlayerInfoUpdatePacket.Action.INITIALIZE_CHAT, + PlayerInfoUpdatePacket.Entry( + player.uuid, + player.username, + emptyList(), + true, + player.latency, + player.gameMode ?: GameMode.SURVIVAL, + player.displayName, + session, + 0, + false + ) + ) + + companion object { + const val ID = "signed-chat" + } +} + +/** + * Installs vanilla-style chat handling that keeps the signatures of chat messages and signed + * command arguments. + * + * @see SignedChatFeature + */ +fun SurfMinestomServerBuilder.withSignedChat(block: SignedChatSettings.() -> Unit = {}) { + install(SignedChatFeature(SignedChatSettings().apply(block))) +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/BoundChatType.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/BoundChatType.kt new file mode 100644 index 000000000..4ae34e8c0 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/BoundChatType.kt @@ -0,0 +1,54 @@ +package dev.slne.surf.api.minestom.server.chat.impl + +import dev.slne.surf.api.minestom.extension.ChatTypeRegistry +import net.kyori.adventure.text.Component +import net.minestom.server.message.ChatType +import net.minestom.server.message.ChatTypeDecoration +import net.minestom.server.registry.RegistryKey +import net.kyori.adventure.chat.ChatType as AdventureChatType + +/** + * A chat type together with the sender name and optional target name it is rendered with. + */ +internal data class BoundChatType( + val chatType: RegistryKey, + val name: Component, + val targetName: Component? = null +) { + /** + * The chat type ID of the chat packets: the registry ID plus one. + * + * @throws IllegalStateException If the chat type is not registered. + */ + val id: Int + get() { + val registryId = ChatTypeRegistry.getId(chatType) + check(registryId != -1) { "Chat type $chatType is not registered" } + + return registryId + 1 + } + + fun withTargetName(targetName: Component) = copy(targetName = targetName) + + fun adventure(): AdventureChatType.Bound = + AdventureChatType.chatType(chatType.key()).bind(name, targetName) + + /** + * Applies the decoration of the chat type to [content], for audiences that do not receive chat + * packets. Returns [content] unchanged when the chat type has no registered decoration. + */ + fun decorate(content: Component): Component { + val decoration = ChatTypeRegistry.get(chatType)?.chat() ?: return content + + val arguments = decoration.parameters().map { parameter -> + when (parameter) { + ChatTypeDecoration.Parameter.SENDER -> name + ChatTypeDecoration.Parameter.TARGET -> targetName ?: Component.empty() + ChatTypeDecoration.Parameter.CONTENT -> content + } + } + + return Component.translatable(decoration.translationKey(), arguments) + .style(decoration.style()) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/ChatMessageChain.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/ChatMessageChain.kt new file mode 100644 index 000000000..45efd4e54 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/ChatMessageChain.kt @@ -0,0 +1,46 @@ +package dev.slne.surf.api.minestom.server.chat.impl + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import kotlin.coroutines.cancellation.CancellationException + +internal class ChatMessageChain(private val scope: CoroutineScope) : AutoCloseable { + + companion object { + private val LOGGER = ComponentLogger.logger() + } + + private val lock = Any() + + private var head: Job = Job().apply { complete() } + + @Volatile + private var closed = false + + fun append(task: suspend () -> Unit) { + if (closed) return + + synchronized(lock) { + val previous = head + + head = scope.launch { + previous.join() + if (closed) return@launch + + try { + task() + } catch (cancellation: CancellationException) { + throw cancellation + } catch (failure: Throwable) { + LOGGER.error("Chain link failed, continuing to next one", failure) + } + } + } + } + + override fun close() { + closed = true + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/ChatProcessor.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/ChatProcessor.kt new file mode 100644 index 000000000..9d61b2aa1 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/ChatProcessor.kt @@ -0,0 +1,162 @@ +package dev.slne.surf.api.minestom.server.chat.impl + +import dev.slne.surf.api.minestom.chat.AsyncPlayerChatEvent +import dev.slne.surf.api.minestom.chat.ChatRenderer +import dev.slne.surf.api.minestom.extension.ConnectionManager +import dev.slne.surf.api.minestom.server.chat.impl.signature.PlayerChatMessage +import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope +import net.kyori.adventure.audience.Audience +import net.kyori.adventure.audience.ForwardingAudience +import net.kyori.adventure.text.Component +import net.kyori.adventure.translation.GlobalTranslator +import net.minestom.server.adventure.MinestomAdventure +import net.minestom.server.adventure.audience.Audiences +import net.minestom.server.command.ConsoleSender +import net.minestom.server.entity.Player +import net.minestom.server.message.ChatType + +/** + * Runs one signed chat message through [AsyncPlayerChatEvent] and sends it to the event's viewers. + * + * Delivery depends on what the listeners changed. Nothing is sent for cancelled events or when the + * sender has left while the listeners ran. + * - Nothing changed: the message is sent as received with the vanilla chat type. + * - Message changed, default renderer: vanilla chat type, the new content as unsigned content. + * - Custom renderer: `surf:raw` chat type ([SurfChatTypes.raw]), the rendered component as + * unsigned content. + * + * The original signature stays attached in every case. [process] suspends while viewer-specific + * renderers run, and renders concurrently per viewer only for renderers that are not + * [ChatRenderer.ViewerUnaware]. + */ +internal class ChatProcessor( + private val player: Player, + private val message: PlayerChatMessage, +) { + private val originalMessage = message.decoratedContent() + private val outgoing = OutgoingChatMessage.create(message) + + suspend fun process() { + val players = ConnectionManager.onlinePlayers + val viewers = ObjectLinkedOpenHashSet(players.size + 1).apply { + addAll(players) + add(Audiences.console()) + } + + val renderer = ChatRenderer.defaultRenderer() + val event = AsyncPlayerChatEvent( + player = player, + viewers = viewers, + renderer = renderer, + message = originalMessage, + originalMessage = originalMessage, + signedMessage = message.adventureView() + ) + + AsyncPlayerChatEvent.node.call(event) + + complete( + event, + messageChanged = event.message != originalMessage, + formatChanged = event.renderer !== renderer, + ) + } + + private suspend fun complete( + event: AsyncPlayerChatEvent, + messageChanged: Boolean, + formatChanged: Boolean, + ) { + if (event.isCancelled || player.isRemoved || !player.isOnline) return + + val displayName = player.chatDisplayName() + val message = event.message + val renderer = event.renderer + val viewers = event.viewers + + val useVanillaChatType = renderer is ChatRenderer.Default + val chatType = BoundChatType( + chatType = if (useVanillaChatType) ChatType.CHAT else SurfChatTypes.raw, + name = displayName + ) + + when { + formatChanged -> if (renderer is ChatRenderer.ViewerUnaware) { + val rendered = renderer.render(player, displayName, message) + broadcast(viewers, chatType, sendConcurrent = false) { rendered } + } else { + broadcast(viewers, chatType, sendConcurrent = true) { viewer -> + renderer.render(player, displayName, message, viewer) + } + } + + messageChanged -> { + val rendered = if (useVanillaChatType) { + message + } else { + (renderer as ChatRenderer.ViewerUnaware).render(player, displayName, message) + } + + broadcast(viewers, chatType, sendConcurrent = false) { rendered } + } + + else -> broadcast(viewers, chatType, sendConcurrent = false, unsignedFor = null) + } + } + + private suspend fun broadcast( + viewers: Set, + chatType: BoundChatType, + sendConcurrent: Boolean, + unsignedFor: (suspend (Audience) -> Component)? + ) { + if (viewers.isEmpty()) return + if (sendConcurrent) { + supervisorScope { + for (viewer in viewers) { + launch { + sendTo(viewer, chatType, unsignedFor?.invoke(viewer)) + } + } + } + } else { + for (viewer in viewers) { + sendTo(viewer, chatType, unsignedFor?.invoke(viewer)) + } + } + } + + private fun sendTo(viewer: Audience, chatType: BoundChatType, unsigned: Component?) { + when (viewer) { + is Player -> { + val holder = ChatSessionHolderImpl.fromPlayer(viewer) ?: return + outgoing.sendToPlayer(holder.handler, chatType, unsigned) + } + + is ConsoleSender -> viewer.sendMessage( + GlobalTranslator.render( + chatType.decorate(unsigned ?: outgoing.content), + MinestomAdventure.getDefaultLocale() + ) + ) + + is ForwardingAudience.Single -> sendTo(viewer.audience(), chatType, unsigned) + + else -> { + val message = + if (unsigned == null) message else message.withUnsignedContent(unsigned) + viewer.sendMessage(message.adventureView(), chatType.adventure()) + } + } + } +} + +/** + * The name a chat message is attributed to: the display name, or the username when there is none. + * Shift-clicking it inserts the username, and hovering it shows the player. + */ +internal fun Player.chatDisplayName(): Component = (displayName ?: Component.text(username)) + .insertion(username) + .hoverEvent(this) diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/ChatSessionHolderImpl.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/ChatSessionHolderImpl.kt new file mode 100644 index 000000000..60df287d7 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/ChatSessionHolderImpl.kt @@ -0,0 +1,171 @@ +package dev.slne.surf.api.minestom.server.chat.impl + +import dev.slne.surf.api.minestom.chat.ChatSessionHolder +import dev.slne.surf.api.minestom.chat.RemoteChatSender +import dev.slne.surf.api.minestom.chat.RemoteSignedMessage +import dev.slne.surf.api.minestom.extension.ConnectionManager +import dev.slne.surf.api.minestom.server.chat.SignedChatSettings +import dev.slne.surf.api.minestom.server.chat.impl.signature.* +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import net.kyori.adventure.chat.SignedMessage +import net.kyori.adventure.text.Component +import net.minestom.server.crypto.ChatSession +import net.minestom.server.crypto.MessageSignature +import net.minestom.server.entity.GameMode +import net.minestom.server.entity.Player +import net.minestom.server.network.packet.server.play.DeleteChatPacket +import net.minestom.server.network.packet.server.play.PlayerInfoRemovePacket +import net.minestom.server.network.packet.server.play.PlayerInfoUpdatePacket +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +/** + * The secure chat state of one player while the signed chat feature is installed. + * + * Instances are bound to one [Player] object. A reconnecting player with the same UUID gets a new + * holder; [fromPlayer] never returns the holder of an earlier connection. + */ +internal class ChatSessionHolderImpl( + override val player: Player, + val handler: PlayerChatHandler, +) : ChatSessionHolder { + + override val chatSession: ChatSession? + get() = handler.chatSession?.asData() + + override fun sendSignedMessage( + message: SignedMessage, + boundName: Component, + unsignedContent: Component?, + ) { + val signed = (message as? PlayerChatMessage.AdventureView)?.playerChatMessage + if (signed == null) { + player.sendMessage(unsignedContent ?: Component.text(message.message())) + return + } + + OutgoingChatMessage.create(signed).sendToPlayer( + handler = handler, + chatType = BoundChatType(SurfChatTypes.raw, boundName), + unsigned = unsignedContent, + ) + } + + override fun captureSignedMessage( + message: SignedMessage, + unsignedContent: Component?, + ): RemoteSignedMessage? { + val signed = (message as? PlayerChatMessage.AdventureView)?.playerChatMessage ?: return null + + return RemoteSignedMessage( + sender = signed.link.sender, + sessionId = signed.link.sessionId, + index = signed.link.index, + signature = signed.signature, + content = signed.signedBody.content, + timestamp = signed.signedBody.timeStamp, + salt = signed.signedBody.salt, + lastSeen = signed.signedBody.lastSeen.entries, + unsignedContent = unsignedContent + ) + } + + override fun sendRemoteSignedMessage( + sender: RemoteChatSender, + message: RemoteSignedMessage, + boundName: Component, + ) { + if (!handler.acceptsChatMessages()) return + + val signed = PlayerChatMessage( + link = SignedMessageLink(message.index, message.sender, message.sessionId), + signature = message.signature, + signedBody = SignedMessageBody( + message.content, + message.timestamp, + message.salt, + LastSeenMessages(message.lastSeen) + ), + unsignedContent = message.unsignedContent, + filterMask = FILTER_MASK_PASS_THROUGH + ) + + // A sender unknown to this server gets a temporary tab-list entry, so the client can + // resolve and verify the message; the entry is removed again after the message. + val announce = ConnectionManager.getOnlinePlayerByUuid(sender.uuid) == null + if (announce) player.sendPacket(remoteSenderInfoPacket(sender)) + + handler.sendPlayerChatMessage(signed, BoundChatType(SurfChatTypes.raw, boundName)) + + if (announce) player.sendPacket(PlayerInfoRemovePacket(sender.uuid)) + } + + override fun deleteMessage(signature: SignedMessage.Signature) = deleteMessage(player, signature) + + private fun remoteSenderInfoPacket(sender: RemoteChatSender): PlayerInfoUpdatePacket { + val actions = EnumSet.of(PlayerInfoUpdatePacket.Action.ADD_PLAYER) + if (sender.session != null) actions.add(PlayerInfoUpdatePacket.Action.INITIALIZE_CHAT) + + return PlayerInfoUpdatePacket( + actions, + PlayerInfoUpdatePacket.Entry( + sender.uuid, + sender.username, + emptyList(), + false, + 0, + GameMode.ADVENTURE, + null, + sender.session, + 0, + false + ) + ) + } + + @OptIn(InternalSurfApi::class) + companion object : ChatSessionHolder.Provider { + private val holders = ConcurrentHashMap() + + override fun fromPlayer(player: Player): ChatSessionHolderImpl? = holders[player.uuid] + ?.takeIf { it.player === player } + + override fun fromUuid(uuid: UUID): ChatSessionHolderImpl? = holders[uuid] + + override fun deleteMessage(player: Player, signature: SignedMessage.Signature) { + val packed = MessageSignature.Packed(MessageSignature(signature.bytes())) + player.sendPacket(DeleteChatPacket(packed)) + } + + /** + * Returns the holder of [player], creating it on first use. Returns `null` for a player + * that is no longer online. + */ + fun create(player: Player, settings: SignedChatSettings): ChatSessionHolderImpl? { + if (!player.isOnline) return null + + return holders.compute(player.uuid) { _, existing -> + existing?.takeIf { it.player === player } + ?: ChatSessionHolderImpl(player, PlayerChatHandler(player, settings)) + } + } + + /** + * Removes and returns the holder of [player]; does not close its handler. Returns `null` + * when the stored holder belongs to a different [Player] object. + */ + fun remove(player: Player): ChatSessionHolderImpl? { + val holder = holders[player.uuid]?.takeIf { it.player === player } ?: return null + holders.remove(player.uuid, holder) + return holder + } + + /** + * Closes the handlers of all holders and removes them. + */ + fun clear() { + holders.values.forEach { holder -> holder.handler.close() } + holders.clear() + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/ChatTranslations.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/ChatTranslations.kt new file mode 100644 index 000000000..ebd4bcee2 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/ChatTranslations.kt @@ -0,0 +1,40 @@ +package dev.slne.surf.api.minestom.server.chat.impl + +import net.kyori.adventure.key.Key +import net.kyori.adventure.translation.GlobalTranslator +import net.kyori.adventure.translation.TranslationStore +import java.text.MessageFormat +import java.util.Locale + + +internal object ChatTranslations { + + private val STORE_KEY = Key.key("surf", "chat") + + @Volatile + private var registered = false + + fun register() { + if (registered) return + registered = true + + val store = TranslationStore.messageFormat(STORE_KEY) + store.defaultLocale(Locale.US) + + store.registerAll( + Locale.US, + mapOf( + "chat.type.text" to MessageFormat("<{0}> {1}"), + "chat.type.announcement" to MessageFormat("[{0}] {1}"), + "chat.type.emote" to MessageFormat("* {0} {1}"), + "chat.type.team.text" to MessageFormat("{0} <{1}> {2}"), + "chat.type.team.sent" to MessageFormat("-> {0} <{1}> {2}"), + "commands.message.display.incoming" to MessageFormat("{0} whispers to you: {1}"), + "commands.message.display.outgoing" to MessageFormat("You whisper to {0}: {1}"), + "%s" to MessageFormat("{0}"), + ) + ) + + GlobalTranslator.translator().addSource(store) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/OutgoingChatMessage.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/OutgoingChatMessage.kt new file mode 100644 index 000000000..4c66c0365 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/OutgoingChatMessage.kt @@ -0,0 +1,46 @@ +package dev.slne.surf.api.minestom.server.chat.impl + +import dev.slne.surf.api.minestom.server.chat.impl.signature.PlayerChatMessage +import net.kyori.adventure.text.Component + + +internal sealed interface OutgoingChatMessage { + + val content: Component + + fun sendToPlayer( + handler: PlayerChatHandler, + chatType: BoundChatType, + unsigned: Component? = null + ) + + + data class Disguised(override val content: Component) : OutgoingChatMessage { + override fun sendToPlayer( + handler: PlayerChatHandler, + chatType: BoundChatType, + unsigned: Component? + ) { + handler.sendDisguisedChatMessage(unsigned ?: content, chatType) + } + } + + + data class Signed(val message: PlayerChatMessage) : OutgoingChatMessage { + override val content: Component get() = message.decoratedContent() + + override fun sendToPlayer( + handler: PlayerChatHandler, + chatType: BoundChatType, + unsigned: Component? + ) { + val outgoing = if (unsigned != null) message.withUnsignedContent(unsigned) else message + handler.sendPlayerChatMessage(outgoing, chatType) + } + } + + companion object { + fun create(message: PlayerChatMessage): OutgoingChatMessage = + if (message.isSystem()) Disguised(message.decoratedContent()) else Signed(message) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/PlayerChatHandler.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/PlayerChatHandler.kt new file mode 100644 index 000000000..b36710821 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/PlayerChatHandler.kt @@ -0,0 +1,457 @@ +package dev.slne.surf.api.minestom.server.chat.impl + +import dev.slne.surf.api.minestom.coroutine.minestomBlockingScope +import dev.slne.surf.api.minestom.server.chat.SignedChatSettings +import dev.slne.surf.api.minestom.server.chat.impl.PlayerChatHandler.Companion.MAX_PENDING_CHATS +import dev.slne.surf.api.minestom.server.chat.impl.signature.* +import dev.slne.surf.api.minestom.server.impl.command.SignedCommandArguments +import dev.slne.surf.api.minestom.server.impl.command.signableCommandArguments +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.Component.translatable +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import net.minestom.server.MinecraftServer +import net.minestom.server.crypto.ChatSession +import net.minestom.server.crypto.SignatureValidator +import net.minestom.server.entity.Player +import net.minestom.server.message.ChatMessageType +import net.minestom.server.network.packet.client.play.ClientChatAckPacket +import net.minestom.server.network.packet.client.play.ClientChatMessagePacket +import net.minestom.server.network.packet.client.play.ClientChatSessionUpdatePacket +import net.minestom.server.network.packet.client.play.ClientSignedCommandChatPacket +import net.minestom.server.network.packet.server.play.DisguisedChatPacket +import net.minestom.server.network.packet.server.play.PlayerChatMessagePacket +import net.minestom.server.network.packet.server.play.SystemChatPacket +import java.time.Instant + +/** + * Validates and sends the chat traffic of one player, following vanilla: last-seen + * acknowledgements, the signature chain, spam throttling and the cache of signatures sent to the + * client. + * + * Validation failures kick the player on the next tick. Chat messages are broadcast through a + * per-player chain, so they are processed one after another in arrival order. [tick] must be called + * once per server tick. [close] stops the chain; queued messages are dropped. + */ +internal class PlayerChatHandler( + private val player: Player, + private val config: SignedChatSettings, +): AutoCloseable { + companion object { + private val LOGGER = ComponentLogger.logger() + + private val CHAT_VALIDATION_FAILED = + translatable("multiplayer.disconnect.chat_validation_failed") + + private val ILLEGAL_CHARACTERS = + translatable("multiplayer.disconnect.illegal_characters") + + private val TOO_MANY_PENDING_CHATS = + translatable("multiplayer.disconnect.too_many_pending_chats") + + private const val MAX_PENDING_CHATS = 4096 + + fun isAllowedChatCharacter(char: Char): Boolean { + val code = char.code + return char != '§' && code >= ' '.code && code != 127 + } + + fun isChatMessageIllegal(message: String) = !message.all(::isAllowedChatCharacter) + } + + private val lastSeenMessages = LastSeenMessagesValidator() + private val messageSignatureCache = MessageSignatureCache.createDefault() + + private val chatSpamThrottler = TickThrottler(20, 20 * config.chatSpamThresholdSeconds) + private val commandSpamThrottler = TickThrottler(20, 20 * config.commandSpamThresholdSeconds) + + /** + * Index of the next chat packet sent to this player; guarded by [messageSignatureCache]. + */ + private var nextChatIndex = 0 + + private var signedMessageDecoder: SignedMessageChain.Decoder = + SignedMessageChain.Decoder.unsigned(player.uuid) { config.enforceSecureProfile } + + + /** + * The validated chat session of the player, or `null` until the client has sent one. + */ + @Volatile + var chatSession: RemoteChatSession? = null + private set + + private val chatMessageChain = ChatMessageChain(minestomBlockingScope) + + fun tick() { + chatSpamThrottler.tick() + commandSpamThrottler.tick() + } + + override fun close() { + chatMessageChain.close() + } + + + /** + * Validates a chat message and queues [onMessage] on the message chain. + * + * The last-seen update and signature are validated immediately in packet order; only the + * broadcast runs on the chain. Does nothing while the server stops. + */ + fun handleChat(packet: ClientChatMessagePacket, onMessage: suspend (PlayerChatMessage) -> Unit) { + if (MinecraftServer.isStopping()) return + + val lastSeen = unpackAndApplyLastSeen(LastSeenMessages.Update.fromPacket(packet)) ?: return + + tryHandleChat(packet.message(), isCommand = false) { + val signedMessage = try { + getSignedMessage(packet, lastSeen) + } catch (failure: SignedMessageChain.DecodeException) { + handleMessageDecodeFailure(failure) + return@tryHandleChat + } + + chatMessageChain.append { broadcastChatMessage(signedMessage, onMessage) } + } + } + + /** + * Validates the signed arguments of a command and runs [onCommand] with them available through + * [SignedCommandArguments]. Does nothing while the server stops. + * + * A command whose arguments fail validation is not run; the failure is reported to the player. + * Every handled command counts towards the command spam threshold. + */ + fun handleSignedCommandChat( + packet: ClientSignedCommandChatPacket, + onCommand: (String) -> Unit + ) { + if (MinecraftServer.isStopping()) return + + val lastSeen = unpackAndApplyLastSeen(LastSeenMessages.Update.fromPacket(packet)) ?: return + + tryHandleChat(packet.message(), isCommand = true) { + val signedArguments = try { + getSignedArguments(packet, lastSeen) + } catch (failure: SignedMessageChain.DecodeException) { + handleMessageDecodeFailure(failure) + null + } + + if (signedArguments != null) { + SignedCommandArguments.withMessages( + signedArguments.mapValues { (_, message) -> message.adventureView() } + ) { onCommand(packet.message()) } + } + + detectRateSpam(commandSpamThrottler) + } + } + + /** + * Runs [command] received without signatures. + * + * With [SignedChatSettings.enforceSecureProfile], a command that has signable arguments is + * rejected instead. + */ + fun handleUnsignedCommandChat(command: String, onCommand: (String) -> Unit) { + tryHandleChat(command, isCommand = true) { + if (config.enforceSecureProfile && signableCommandArguments(player, command).isNotEmpty()) { + LOGGER.warn( + "Received unsigned command packet from {}, but the command requires signable arguments: {}", + player.username, + command + ) + player.sendPacket( + SystemChatPacket( + translatable("chat.disabled.invalid_command_signature", NamedTextColor.RED), + false + ) + ) + } else { + onCommand(command) + } + + detectRateSpam(commandSpamThrottler) + } + } + + fun handleChatAck(packet: ClientChatAckPacket) { + synchronized(lastSeenMessages) { + try { + lastSeenMessages.applyOffset(packet.offset()) + } catch (failure: LastSeenMessagesValidator.ValidationException) { + LOGGER.error( + "Failed to validate message acknowledgement offset from {}: {}", + player.username, + failure.message + ) + disconnect(CHAT_VALIDATION_FAILED) + } + } + } + + + fun handleChatSessionUpdate( + packet: ClientChatSessionUpdatePacket, + onSessionReset: (RemoteChatSession) -> Unit + ) { + val newChatSession: ChatSession = packet.chatSession() + val oldProfileKey = chatSession?.profilePublicKey + val newProfileKey = newChatSession.publicKey() + + if (oldProfileKey == newProfileKey) return + + if (oldProfileKey != null && newProfileKey.expiresAt() + .isBefore(oldProfileKey.expiresAt()) + ) { + disconnect(EXPIRED_PROFILE_PUBLIC_KEY) + return + } + + try { + val validated = RemoteChatSession.validate( + data = newChatSession, + profileId = player.uuid, + serviceSignatureValidator = SignatureValidator.YGGDRASIL + ) + + resetPlayerChatState(validated, onSessionReset) + } catch (failure: ProfilePublicKeyValidationException) { + disconnect(failure.component) + } + } + + /** + * Adopts [session] and starts a new signature chain. [onSessionReset] runs on the message chain, + * after the messages queued earlier. + */ + private fun resetPlayerChatState( + session: RemoteChatSession, + onSessionReset: (RemoteChatSession) -> Unit + ) { + chatSession = session + signedMessageDecoder = session.createMessageDecoder(player.uuid) + + chatMessageChain.append { onSessionReset(session) } + } + + private fun getSignedMessage( + packet: ClientChatMessagePacket, + lastSeenMessages: LastSeenMessages + ): PlayerChatMessage { + val body = SignedMessageBody( + content = packet.message(), + timeStamp = Instant.ofEpochMilli(packet.timestamp()), + salt = packet.salt(), + lastSeen = lastSeenMessages + ) + + return signedMessageDecoder.unpack(packet.signature(), body) + } + + /** + * The message the sender signed for every signable argument of [packet]'s command, keyed by node + * name. + * + * Without signature entries, every signable argument is unpacked without a signature, so a + * player with a profile key, or any player while [SignedChatSettings.enforceSecureProfile] is + * set, fails with [SignedMessageChain.DecodeException.MISSING_PROFILE_KEY]. Entries that do not + * match the parsed arguments fail with + * [SignedMessageChain.DecodeException.INVALID_COMMAND_SIGNATURE]; an unknown argument name also + * breaks the signature chain. + * + * @throws SignedMessageChain.DecodeException when the command must not run + */ + private fun getSignedArguments( + packet: ClientSignedCommandChatPacket, + lastSeenMessages: LastSeenMessages + ): Map { + val entries = packet.signatures().entries() + val values = signableCommandArguments(player, packet.message()) + val signedArguments = Object2ObjectOpenHashMap(values.size) + + if (entries.isEmpty()) { + values.forEach { (name, value) -> + signedArguments[name] = + signedMessageDecoder.unpack(null, SignedMessageBody.unsigned(value)) + } + + return signedArguments + } + + entries.forEach { entry -> + val value = values[entry.name()] + if (value == null) { + signedMessageDecoder.setChainBroken() + throw signedArgumentMismatch(packet, values) + } + + val body = SignedMessageBody( + content = value, + timeStamp = Instant.ofEpochMilli(packet.timestamp()), + salt = packet.salt(), + lastSeen = lastSeenMessages + ) + + signedArguments[entry.name()] = signedMessageDecoder.unpack(entry.signature(), body) + } + + if (values.keys.any { it !in signedArguments }) { + throw signedArgumentMismatch(packet, values) + } + + return signedArguments + } + + private fun signedArgumentMismatch( + packet: ClientSignedCommandChatPacket, + expected: Map + ): SignedMessageChain.DecodeException { + LOGGER.warn( + "Signed command mismatch for {} ('{}'): got [{}] from the client, but expected [{}]", + player.username, + packet.message(), + packet.signatures().entries().joinToString { it.name() }, + expected.keys.joinToString() + ) + + return SignedMessageChain.DecodeException(SignedMessageChain.DecodeException.INVALID_COMMAND_SIGNATURE) + } + + private inline fun tryHandleChat(message: String, isCommand: Boolean, chatHandler: () -> Unit) { + if (isChatMessageIllegal(message)) { + disconnect(ILLEGAL_CHARACTERS) + } else if (player.isRemoved || + (!isCommand && player.settings.chatMessageType() == ChatMessageType.NONE) + ) { + player.sendPacket( + SystemChatPacket( + translatable("chat.disabled.options", NamedTextColor.RED), + false + ) + ) + } else { + chatHandler() + } + } + + private fun unpackAndApplyLastSeen(update: LastSeenMessages.Update): LastSeenMessages? { + synchronized(lastSeenMessages) { + try { + return lastSeenMessages.applyUpdate(update) + } catch (failure: LastSeenMessagesValidator.ValidationException) { + LOGGER.error( + "Failed to validate message acknowledgements from {}: {}", + player.username, + failure.message + ) + disconnect(CHAT_VALIDATION_FAILED) + return null + } + } + } + + private fun handleMessageDecodeFailure(failure: SignedMessageChain.DecodeException) { + LOGGER.warn( + "Failed to update secure chat state for {}: '{}'", + player.username, + failure.component + ) + player.sendPacket(SystemChatPacket(failure.component.color(NamedTextColor.RED), false)) + } + + private suspend fun broadcastChatMessage( + message: PlayerChatMessage, + onMessage: suspend (PlayerChatMessage) -> Unit + ) { + val rawMessage = message.signedContent() + + if (rawMessage.isEmpty()) { + LOGGER.warn("{} tried to send an empty message", player.username) + } else if (player.settings.chatMessageType() == ChatMessageType.SYSTEM) { + player.sendPacket( + SystemChatPacket( + translatable("chat.cannotSend", NamedTextColor.RED), + false + ) + ) + } else { + onMessage(message) + } + + detectRateSpam(chatSpamThrottler) + } + + private fun detectRateSpam(throttler: TickThrottler) { + if (!throttler.isIncrementAndUnderThreshold()) { + disconnect(translatable("disconnect.spam")) + } + } + + /** + * Whether the player's chat visibility setting allows player chat messages, which only `FULL` + * does. + */ + fun acceptsChatMessages(): Boolean = player.settings.chatMessageType() == ChatMessageType.FULL + + /** + * Sends a signed chat message and records its signature for later last-seen references. + * + * Does nothing when the player does not accept chat messages. Kicks the player when more than + * [MAX_PENDING_CHATS] messages are awaiting acknowledgement. Thread-safe. + */ + fun sendPlayerChatMessage(message: PlayerChatMessage, chatType: BoundChatType) { + if (!acceptsChatMessages()) return + + synchronized(messageSignatureCache) { + player.sendPacket( + PlayerChatMessagePacket( + nextChatIndex++, + message.link.sender, + message.link.index, + message.signature, + message.signedBody.pack(messageSignatureCache), + message.unsignedContent, + message.filterMask, + chatType.id, + chatType.name, + chatType.targetName + ) + ) + + val signature = message.signature ?: return + messageSignatureCache.push(message.signedBody, signature) + + val trackedCount = synchronized(lastSeenMessages) { + lastSeenMessages.addPending(signature) + lastSeenMessages.trackedMessagesCount + } + + if (trackedCount > MAX_PENDING_CHATS) { + disconnect(TOO_MANY_PENDING_CHATS) + } + } + } + + /** + * Sends [content] as a chat message without sender. Does nothing when the player does not + * accept chat messages. + */ + fun sendDisguisedChatMessage(content: Component, chatType: BoundChatType) { + if (!acceptsChatMessages()) return + + player.sendPacket( + DisguisedChatPacket(content, chatType.id, chatType.name, chatType.targetName) + ) + } + + /** + * Kicks the player on the next tick, so the call is safe from any thread. + */ + private fun disconnect(reason: Component) { + player.scheduleNextTick { player.kick(reason) } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/SurfChatTypes.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/SurfChatTypes.kt new file mode 100644 index 000000000..916905c18 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/SurfChatTypes.kt @@ -0,0 +1,47 @@ +package dev.slne.surf.api.minestom.server.chat.impl + +import dev.slne.surf.api.minestom.extension.ChatTypeRegistry +import net.kyori.adventure.key.Key +import net.kyori.adventure.text.format.Style +import net.minestom.server.message.ChatType +import net.minestom.server.message.ChatTypeDecoration +import net.minestom.server.registry.RegistryKey + + +/** + * The chat types the surf api adds to the registry. + */ +internal object SurfChatTypes { + + private val RAW_KEY = Key.key("surf", "raw") + + /** + * The `surf:raw` chat type, which shows only the message content. Available after [register]. + */ + lateinit var raw: RegistryKey + private set + + /** + * Registers [raw] unless the registry already contains it. + * + * Checks the registry instead of a local flag so that a key cached from an earlier server + * instance cannot outlive its registry and resolve to ID -1. + */ + fun register() { + ChatTypeRegistry.getKey(RAW_KEY)?.let { + raw = it + return + } + + val decoration = ChatTypeDecoration( + "%s", + listOf(ChatTypeDecoration.Parameter.CONTENT), + Style.empty() + ) + + raw = ChatTypeRegistry.register( + RAW_KEY, + ChatType.create(decoration, decoration) + ) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/TickThrottler.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/TickThrottler.kt new file mode 100644 index 000000000..7bfa4d96d --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/TickThrottler.kt @@ -0,0 +1,24 @@ +package dev.slne.surf.api.minestom.server.chat.impl + +import java.util.concurrent.atomic.AtomicInteger + +internal class TickThrottler(private val incrementStep: Int, private val threshold: Int) { + + private val count = AtomicInteger() + + fun increment() { + count.addAndGet(incrementStep) + } + + fun tick() { + var value: Int + do { + value = count.get() + if (value <= 0) return + } while (!count.compareAndSet(value, value - 1)) + } + + fun isUnderThreshold() = threshold <= 0 || count.get() < threshold + + fun isIncrementAndUnderThreshold() = threshold <= 0 || count.addAndGet(incrementStep) < threshold +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/LastSeenMessages.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/LastSeenMessages.kt new file mode 100644 index 000000000..481f1e0c3 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/LastSeenMessages.kt @@ -0,0 +1,67 @@ +package dev.slne.surf.api.minestom.server.chat.impl.signature + +import com.google.common.primitives.Ints +import dev.slne.surf.api.minestom.server.chat.impl.signature.LastSeenMessages.Update.Companion.IGNORE_CHECKSUM +import net.minestom.server.crypto.MessageSignature +import net.minestom.server.network.packet.client.play.ClientChatMessagePacket +import net.minestom.server.network.packet.client.play.ClientSignedCommandChatPacket +import java.util.BitSet +import net.minestom.server.crypto.LastSeenMessages as NetworkLastSeenMessages + +@JvmInline +internal value class LastSeenMessages(val entries: List) { + + companion object { + const val LAST_SEEN_MESSAGES_MAX_LENGTH = 20 + + val EMPTY = LastSeenMessages(emptyList()) + } + + fun computeChecksum(): Byte { + var checksum = 1 + + for (entry in entries) { + checksum = 31 * checksum + entry.checksum() + } + + val checksumByte = checksum.toByte() + + return if (checksumByte == IGNORE_CHECKSUM) 1 else checksumByte + } + + fun updateSignature(output: SignatureUpdater.Output) { + output.update(Ints.toByteArray(entries.size)) + + for (entry in entries) { + output.update(entry.signature()) + } + } + + fun pack(cache: MessageSignatureCache): NetworkLastSeenMessages.Packed = + NetworkLastSeenMessages.Packed(entries.map { it.pack(cache) }) + + data class Update( + val offset: Int, + val acknowledged: BitSet, + val checksum: Byte + ) { + companion object { + const val IGNORE_CHECKSUM = 0.toByte() + + fun fromPacket(packet: ClientChatMessagePacket) = Update( + offset = packet.ackOffset(), + acknowledged = packet.ackList(), + checksum = packet.checksum() + ) + + fun fromPacket(packet: ClientSignedCommandChatPacket) = Update( + offset = packet.lastSeenMessages().offset(), + acknowledged = packet.lastSeenMessages().acknowledged(), + checksum = packet.checksum() + ) + } + + fun verifyChecksum(lastSeen: LastSeenMessages): Boolean = + checksum == IGNORE_CHECKSUM || checksum == lastSeen.computeChecksum() + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/LastSeenMessagesValidator.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/LastSeenMessagesValidator.kt new file mode 100644 index 000000000..e5f422ce5 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/LastSeenMessagesValidator.kt @@ -0,0 +1,88 @@ +package dev.slne.surf.api.minestom.server.chat.impl.signature + +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import net.minestom.server.crypto.MessageSignature +import java.io.Serial + +internal class LastSeenMessagesValidator( + private val lastSeenCount: Int = LastSeenMessages.LAST_SEEN_MESSAGES_MAX_LENGTH +) { + + private val trackedMessages = ObjectArrayList().apply { + repeat(lastSeenCount) { add(null) } + } + + private var lastPendingMessage: MessageSignature? = null + + val trackedMessagesCount get() = trackedMessages.size + + fun addPending(message: MessageSignature) { + if (message != lastPendingMessage) { + trackedMessages.add(LastSeenTrackedEntry(message, true)) + lastPendingMessage = message + } + } + + fun applyOffset(offset: Int) { + val maxOffset = trackedMessages.size - lastSeenCount + if (offset !in 0..maxOffset) { + throw ValidationException( + "Advanced last seen window by $offset messages, but expected at most $maxOffset" + ) + } + + trackedMessages.removeElements(0, offset) + } + + fun applyUpdate(update: LastSeenMessages.Update): LastSeenMessages { + applyOffset(update.offset) + + val lastSeenEntries = ObjectArrayList(update.acknowledged.cardinality()) + val ackLength = update.acknowledged.length() + if (ackLength > lastSeenCount) { + throw ValidationException( + "Last seen update contained $ackLength messages, but maximum window size is $lastSeenCount" + ) + } + + for (i in 0 until lastSeenCount) { + val acknowledged = update.acknowledged.get(i) + val message = trackedMessages[i] + + if (acknowledged) { + if (message == null) { + throw ValidationException( + "Last seen update acknowledged unknown or previously ignored message at index $i" + ) + } + + trackedMessages[i] = message.acknowledge() + lastSeenEntries.add(message.signature) + } else { + if (message != null && !message.pending) { + throw ValidationException( + "Last seen update ignored previously acknowledged message at index $i and signature ${message.signature}" + ) + } + + trackedMessages[i] = null + } + } + + val lastSeen = LastSeenMessages(lastSeenEntries) + if (!update.verifyChecksum(lastSeen)) { + throw ValidationException( + "Checksum mismatch on last seen update: the client and server must have desynced" + ) + } + + return lastSeen + } + + class ValidationException(message: String) : Exception(message, null, false, false) { + companion object { + @Serial + private const val serialVersionUID: Long = -5942795035956998806L + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/LastSeenTrackedEntry.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/LastSeenTrackedEntry.kt new file mode 100644 index 000000000..9455aeca6 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/LastSeenTrackedEntry.kt @@ -0,0 +1,11 @@ +package dev.slne.surf.api.minestom.server.chat.impl.signature + +import net.minestom.server.crypto.MessageSignature + + +internal data class LastSeenTrackedEntry( + val signature: MessageSignature, + val pending: Boolean +) { + fun acknowledge(): LastSeenTrackedEntry = if (!pending) this else copy(pending = false) +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/MessageSignatureCache.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/MessageSignatureCache.kt new file mode 100644 index 000000000..923354510 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/MessageSignatureCache.kt @@ -0,0 +1,76 @@ +package dev.slne.surf.api.minestom.server.chat.impl.signature + +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import net.minestom.server.crypto.MessageSignature + + +internal class MessageSignatureCache(capacity: Int = DEFAULT_CAPACITY) { + + private class Entry(val signature: MessageSignature) { + val hash = signature.hashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Entry) return false + + if (hash != other.hash) return false + if (signature != other.signature) return false + + return true + } + + override fun hashCode(): Int { + return hash + } + } + + private val entries = arrayOfNulls(capacity) + + fun pack(signature: MessageSignature): Int { + val hash = signature.hashCode() + + for (index in entries.indices) { + val entry = entries[index] ?: continue + if (entry.hash == hash && entry.signature == signature) return index + } + + return NOT_FOUND + } + + fun unpack(id: Int): MessageSignature? = entries[id]?.signature + + fun push(body: SignedMessageBody, signature: MessageSignature?) { + val lastSeen = body.lastSeen.entries + val queue = ArrayDeque(lastSeen.size + 1) + lastSeen.forEach { entry -> queue.addLast(Entry(entry)) } + + if (signature != null) { + queue.addLast(Entry(signature)) + } + + push(queue) + } + + private fun push(queue: ArrayDeque) { + val newEntries = ObjectOpenHashSet(queue) + + var index = 0 + while (queue.isNotEmpty() && index < entries.size) { + val previous = entries[index] + entries[index] = queue.removeLast() + + if (previous != null && !newEntries.contains(previous)) { + queue.addFirst(previous) + } + + index++ + } + } + + companion object { + const val NOT_FOUND = -1 + private const val DEFAULT_CAPACITY = 128 + + fun createDefault() = MessageSignatureCache(DEFAULT_CAPACITY) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/PlayerChatMessage.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/PlayerChatMessage.kt new file mode 100644 index 000000000..7331a55c4 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/PlayerChatMessage.kt @@ -0,0 +1,89 @@ +package dev.slne.surf.api.minestom.server.chat.impl.signature + +import com.google.common.primitives.Ints +import dev.slne.surf.api.minestom.server.chat.impl.NIL_UUID +import net.kyori.adventure.chat.SignedMessage +import net.kyori.adventure.identity.Identity +import net.kyori.adventure.text.Component +import net.minestom.server.crypto.FilterMask +import net.minestom.server.crypto.MessageSignature +import java.time.Duration +import java.time.Instant +import java.util.* + + +internal data class PlayerChatMessage( + val link: SignedMessageLink, + val signature: MessageSignature?, + val signedBody: SignedMessageBody, + val unsignedContent: Component?, + val filterMask: FilterMask +) { + companion object { + private val SYSTEM_SENDER = NIL_UUID + + val MESSAGE_EXPIRES_AFTER_SERVER: Duration = Duration.ofMinutes(5L) + + fun system(content: String) = unsigned(SYSTEM_SENDER, content) + + fun unsigned(sender: UUID, content: String) = PlayerChatMessage( + link = SignedMessageLink.unsigned(sender), + signature = null, + signedBody = SignedMessageBody.unsigned(content), + unsignedContent = null, + filterMask = FILTER_MASK_PASS_THROUGH + ) + + fun updateSignature( + output: SignatureUpdater.Output, + link: SignedMessageLink, + body: SignedMessageBody + ) { + output.update(Ints.toByteArray(1)) + link.updateSignature(output) + body.updateSignature(output) + } + } + + fun withUnsignedContent(content: Component) = + copy(unsignedContent = if (content != Component.text(signedContent())) content else null) + + fun verify(signatureValidator: SignatureValidator): Boolean = + signature != null && signature.verify(signatureValidator) { output -> + updateSignature(output, link, signedBody) + } + + fun signedContent() = signedBody.content + + fun decoratedContent(): Component = unsignedContent ?: Component.text(signedContent()) + + fun timeStamp() = signedBody.timeStamp + + fun salt() = signedBody.salt + + fun hasExpiredServer(now: Instant): Boolean = + now.isAfter(timeStamp().plus(MESSAGE_EXPIRES_AFTER_SERVER)) + + fun sender() = link.sender + + fun isSystem() = sender() == SYSTEM_SENDER + + fun adventureView() = AdventureView() + + inner class AdventureView : SignedMessage { + override fun timestamp(): Instant = this@PlayerChatMessage.timeStamp() + + override fun salt(): Long = this@PlayerChatMessage.salt() + + override fun signature(): SignedMessage.Signature? = + this@PlayerChatMessage.signature?.adventure() + + override fun unsignedContent(): Component? = this@PlayerChatMessage.unsignedContent + + override fun message(): String = this@PlayerChatMessage.signedContent() + + override fun identity(): Identity = Identity.identity(sender()) + + val playerChatMessage get() = this@PlayerChatMessage + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/ProfilePublicKey.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/ProfilePublicKey.kt new file mode 100644 index 000000000..a68c84d43 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/ProfilePublicKey.kt @@ -0,0 +1,60 @@ +package dev.slne.surf.api.minestom.server.chat.impl.signature + +import net.kyori.adventure.text.Component +import net.minestom.server.crypto.PlayerPublicKey +import net.minestom.server.utils.crypto.KeyUtils +import java.io.Serial +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.time.Instant +import java.util.UUID +import net.minestom.server.crypto.SignatureValidator as ServiceSignatureValidator + +internal val EXPIRED_PROFILE_PUBLIC_KEY: Component = + Component.translatable("multiplayer.disconnect.expired_public_key") + +internal val INVALID_PROFILE_PUBLIC_KEY_SIGNATURE: Component = + Component.translatable("multiplayer.disconnect.invalid_public_key_signature") + + +internal class ProfilePublicKeyValidationException(val component: Component) : Exception(null, null, false, false) { + companion object { + @Serial + private const val serialVersionUID: Long = -4832597595024741094L + } +} + +internal fun PlayerPublicKey.createValidated( + validator: ServiceSignatureValidator, + profileId: UUID +): PlayerPublicKey { + if (!validateSignature(validator, profileId)) { + throw ProfilePublicKeyValidationException(INVALID_PROFILE_PUBLIC_KEY_SIGNATURE) + } + + return this +} + +@Suppress("UnstableApiUsage") +internal fun PlayerPublicKey.createSignatureValidator(): SignatureValidator = + SignatureValidator.from(publicKey(), KeyUtils.SignatureAlgorithm.SHA256withRSA.name) + +internal fun PlayerPublicKey.hasExpired() = expiresAt().isBefore(Instant.now()) + +internal fun PlayerPublicKey.validateSignature( + validator: ServiceSignatureValidator, + profileId: UUID +): Boolean = validator.validate(signedPayload(profileId), signature()) + +private fun PlayerPublicKey.signedPayload(profileId: UUID): ByteArray { + val keyBytes = publicKey().encoded + val signedPayload = ByteArray(24 + keyBytes.size) + + ByteBuffer.wrap(signedPayload).order(ByteOrder.BIG_ENDIAN) + .putLong(profileId.mostSignificantBits) + .putLong(profileId.leastSignificantBits) + .putLong(expiresAt().toEpochMilli()) + .put(keyBytes) + + return signedPayload +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/RemoteChatSession.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/RemoteChatSession.kt new file mode 100644 index 000000000..3963f3c8d --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/RemoteChatSession.kt @@ -0,0 +1,30 @@ +package dev.slne.surf.api.minestom.server.chat.impl.signature + +import net.minestom.server.crypto.ChatSession +import net.minestom.server.crypto.PlayerPublicKey +import java.util.UUID +import net.minestom.server.crypto.SignatureValidator as ServiceSignatureValidator + +internal data class RemoteChatSession( + val sessionId: UUID, + val profilePublicKey: PlayerPublicKey +) { + + companion object { + fun validate( + data: ChatSession, + profileId: UUID, + serviceSignatureValidator: ServiceSignatureValidator + ) = RemoteChatSession( + sessionId = data.sessionId(), + profilePublicKey = data.publicKey().createValidated(serviceSignatureValidator, profileId) + ) + } + + fun createMessageDecoder(profileId: UUID): SignedMessageChain.Decoder = + SignedMessageChain(profileId, sessionId).decoder(profilePublicKey) + + fun hasExpired() = profilePublicKey.hasExpired() + + fun asData(): ChatSession = ChatSession(sessionId, profilePublicKey) +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/SignatureUpdater.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/SignatureUpdater.kt new file mode 100644 index 000000000..6349c7b89 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/SignatureUpdater.kt @@ -0,0 +1,10 @@ +package dev.slne.surf.api.minestom.server.chat.impl.signature + + +internal fun interface SignatureUpdater { + fun update(output: Output) + + fun interface Output { + fun update(payload: ByteArray) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/SignatureValidator.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/SignatureValidator.kt new file mode 100644 index 000000000..adcd39f33 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/SignatureValidator.kt @@ -0,0 +1,33 @@ +package dev.slne.surf.api.minestom.server.chat.impl.signature + +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import java.security.PublicKey +import java.security.Signature + +internal fun interface SignatureValidator { + + fun validate(updater: SignatureUpdater, signature: ByteArray): Boolean + + companion object { + private val LOGGER = ComponentLogger.logger() + + /** + * A validator that verifies signatures with [algorithm] against [publicKey]. + * + * A signature that cannot be verified, e.g. because it is malformed, is rejected. The + * failure is logged at debug level only, as the signature is client input. + */ + fun from(publicKey: PublicKey, algorithm: String) = + SignatureValidator { updater, signature -> + try { + val verifier = Signature.getInstance(algorithm) + verifier.initVerify(publicKey) + updater.update(verifier::update) + verifier.verify(signature) + } catch (e: Exception) { + LOGGER.debug("Failed to verify signature: {}", e.toString()) + false + } + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/SignedMessageBody.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/SignedMessageBody.kt new file mode 100644 index 000000000..4e8fda4ee --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/SignedMessageBody.kt @@ -0,0 +1,34 @@ +package dev.slne.surf.api.minestom.server.chat.impl.signature + +import com.google.common.primitives.Ints +import com.google.common.primitives.Longs +import java.time.Instant +import net.minestom.server.crypto.SignedMessageBody as NetworkSignedMessageBody + + +internal data class SignedMessageBody( + val content: String, + val timeStamp: Instant, + val salt: Long, + val lastSeen: LastSeenMessages +) { + + companion object { + fun unsigned(content: String) = + SignedMessageBody(content, Instant.now(), 0L, LastSeenMessages.EMPTY) + } + + fun updateSignature(output: SignatureUpdater.Output) { + output.update(Longs.toByteArray(salt)) + output.update(Longs.toByteArray(timeStamp.epochSecond)) + + val contentBytes = content.toByteArray() + output.update(Ints.toByteArray(contentBytes.size)) + output.update(contentBytes) + + lastSeen.updateSignature(output) + } + + fun pack(cache: MessageSignatureCache): NetworkSignedMessageBody.Packed = + NetworkSignedMessageBody.Packed(content, timeStamp, salt, lastSeen.pack(cache)) +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/SignedMessageChain.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/SignedMessageChain.kt new file mode 100644 index 000000000..737d24990 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/SignedMessageChain.kt @@ -0,0 +1,108 @@ +package dev.slne.surf.api.minestom.server.chat.impl.signature + +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.Component.translatable +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import net.minestom.server.crypto.MessageSignature +import net.minestom.server.crypto.PlayerPublicKey +import java.io.Serial +import java.time.Instant +import java.util.* +import java.util.function.BooleanSupplier + +internal class SignedMessageChain(profileId: UUID, sessionId: UUID) { + + companion object { + private val LOGGER = ComponentLogger.logger() + } + + private var nextLink: SignedMessageLink? = SignedMessageLink.root(profileId, sessionId) + private var lastTimeStamp: Instant = Instant.EPOCH + + fun decoder(profilePublicKey: PlayerPublicKey): Decoder { + val signatureValidator = profilePublicKey.createSignatureValidator() + + return object : Decoder { + override fun unpack( + signature: MessageSignature?, + body: SignedMessageBody + ): PlayerChatMessage { + if (signature == null) { + throw DecodeException(DecodeException.MISSING_PROFILE_KEY) + } + + if (profilePublicKey.hasExpired()) { + throw DecodeException(DecodeException.EXPIRED_PROFILE_KEY) + } + + val link = nextLink ?: throw DecodeException(DecodeException.CHAIN_BROKEN) + + if (body.timeStamp.isBefore(lastTimeStamp)) { + setChainBroken() + throw DecodeException(DecodeException.OUT_OF_ORDER_CHAT) + } + + lastTimeStamp = body.timeStamp + + val unpacked = PlayerChatMessage( + link = link, + signature = signature, + signedBody = body, + unsignedContent = null, + filterMask = FILTER_MASK_PASS_THROUGH + ) + + if (!unpacked.verify(signatureValidator)) { + setChainBroken() + throw DecodeException(DecodeException.INVALID_SIGNATURE) + } + + if (unpacked.hasExpiredServer(Instant.now())) { + LOGGER.warn( + "Received expired chat: '{}'. Is the client/server system time unsynchronized?", + body.content + ) + } + + nextLink = link.advance() + return unpacked + } + + override fun setChainBroken() { + nextLink = null + } + } + } + + class DecodeException(val component: Component) : Exception(null, null, false, false) { + companion object { + @Serial + private const val serialVersionUID: Long = 6432477898999865178L + + val MISSING_PROFILE_KEY: Component = translatable("chat.disabled.missingProfileKey") + val CHAIN_BROKEN: Component = translatable("chat.disabled.chain_broken") + val EXPIRED_PROFILE_KEY: Component = translatable("chat.disabled.expiredProfileKey") + val INVALID_SIGNATURE: Component = translatable("chat.disabled.invalid_signature") + val OUT_OF_ORDER_CHAT: Component = translatable("chat.disabled.out_of_order_chat") + val INVALID_COMMAND_SIGNATURE: Component = + translatable("chat.disabled.invalid_command_signature") + } + } + + fun interface Decoder { + fun unpack(signature: MessageSignature?, body: SignedMessageBody): PlayerChatMessage + + fun setChainBroken() {} + + companion object { + fun unsigned(profileId: UUID, enforcesSecureChat: BooleanSupplier) = + Decoder { _, body -> + if (enforcesSecureChat.asBoolean) { + throw DecodeException(DecodeException.MISSING_PROFILE_KEY) + } + + PlayerChatMessage.unsigned(profileId, body.content) + } + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/SignedMessageLink.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/SignedMessageLink.kt new file mode 100644 index 000000000..70dba9a98 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/SignedMessageLink.kt @@ -0,0 +1,31 @@ +package dev.slne.surf.api.minestom.server.chat.impl.signature + +import com.google.common.primitives.Ints +import dev.slne.surf.api.minestom.server.chat.impl.NIL_UUID +import dev.slne.surf.api.minestom.server.chat.impl.toByteArray +import java.util.UUID + +internal data class SignedMessageLink( + val index: Int, + val sender: UUID, + val sessionId: UUID +) { + + companion object { + fun unsigned(sender: UUID) = root(sender, NIL_UUID) + + fun root(sender: UUID, sessionId: UUID) = SignedMessageLink(0, sender, sessionId) + } + + fun isDescendantOf(link: SignedMessageLink): Boolean = + index > link.index && sender == link.sender && sessionId == link.sessionId + + fun advance(): SignedMessageLink? = + if (index == Int.MAX_VALUE) null else copy(index = index + 1) + + fun updateSignature(output: SignatureUpdater.Output) { + output.update(sender.toByteArray()) + output.update(sessionId.toByteArray()) + output.update(Ints.toByteArray(index)) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/filter-mask-ext.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/filter-mask-ext.kt new file mode 100644 index 000000000..38bd245f8 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/filter-mask-ext.kt @@ -0,0 +1,6 @@ +package dev.slne.surf.api.minestom.server.chat.impl.signature + +import net.minestom.server.crypto.FilterMask +import java.util.* + +internal val FILTER_MASK_PASS_THROUGH: FilterMask = FilterMask(FilterMask.Type.PASS_THROUGH, BitSet(0)) diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/message-signature-ext.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/message-signature-ext.kt new file mode 100644 index 000000000..0d8a3e9b6 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/message-signature-ext.kt @@ -0,0 +1,22 @@ +package dev.slne.surf.api.minestom.server.chat.impl.signature + +import net.kyori.adventure.chat.SignedMessage +import net.minestom.server.crypto.MessageSignature + + +internal fun MessageSignature.checksum(): Int = signature().contentHashCode() + +internal fun MessageSignature.adventure(): SignedMessage.Signature = SignedMessage.signature(signature()) + +internal fun MessageSignature.verify(validator: SignatureValidator, updater: SignatureUpdater): Boolean = + validator.validate(updater, signature()) + + +internal fun MessageSignature.pack(cache: MessageSignatureCache): MessageSignature.Packed { + val packedId = cache.pack(this) + return if (packedId != MessageSignatureCache.NOT_FOUND) { + MessageSignature.Packed(packedId, null) + } else { + MessageSignature.Packed(this) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/uuid-ext.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/uuid-ext.kt new file mode 100644 index 000000000..923ea5e16 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/main/kotlin/dev/slne/surf/api/minestom/server/chat/impl/uuid-ext.kt @@ -0,0 +1,9 @@ +package dev.slne.surf.api.minestom.server.chat.impl + +import com.google.common.primitives.Longs +import java.util.* + +internal val NIL_UUID: UUID = UUID(0L, 0L) + +internal fun UUID.toByteArray(): ByteArray = + Longs.toByteArray(mostSignificantBits) + Longs.toByteArray(leastSignificantBits) diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/test/kotlin/dev/slne/surf/api/minestom/server/chat/SignedChatFeatureTest.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/test/kotlin/dev/slne/surf/api/minestom/server/chat/SignedChatFeatureTest.kt new file mode 100644 index 000000000..0e410c07e --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/test/kotlin/dev/slne/surf/api/minestom/server/chat/SignedChatFeatureTest.kt @@ -0,0 +1,260 @@ +package dev.slne.surf.api.minestom.server.chat + +import dev.slne.surf.api.minestom.chat.AsyncPlayerChatEvent +import dev.slne.surf.api.minestom.chat.ChatSessionHolder +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.dsl.anyExecutor +import dev.slne.surf.api.minestom.command.dsl.signedMessageArgument +import dev.slne.surf.api.minestom.server.SurfMinestomServer +import dev.slne.surf.api.minestom.server.chat.impl.BoundChatType +import dev.slne.surf.api.minestom.server.chat.impl.ChatProcessor +import dev.slne.surf.api.minestom.server.chat.impl.ChatSessionHolderImpl +import dev.slne.surf.api.minestom.server.chat.impl.signature.PlayerChatMessage +import dev.slne.surf.api.minestom.server.surfMinestomServer +import kotlinx.coroutines.runBlocking +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.TranslatableComponent +import net.minestom.server.coordinate.Pos +import net.minestom.server.crypto.ArgumentSignatures +import net.minestom.server.crypto.LastSeenMessages +import net.minestom.server.crypto.MessageSignature +import net.minestom.server.entity.Player +import net.minestom.server.event.EventDispatcher +import net.minestom.server.event.player.AsyncPlayerConfigurationEvent +import net.minestom.server.message.ChatMessageType +import net.minestom.server.message.ChatType +import net.minestom.server.network.packet.client.play.ClientSignedCommandChatPacket +import net.minestom.server.network.packet.server.play.PlayerChatMessagePacket +import net.minestom.server.network.packet.server.play.SystemChatPacket +import net.minestom.server.network.player.ClientSettings +import net.minestom.testing.Collector +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean + +@EnvTest +class SignedChatFeatureTest { + + private fun withFeature( + settings: SignedChatSettings.() -> Unit = {}, + block: (SurfMinestomServer) -> Unit, + ) { + val api = surfMinestomServer { withSignedChat(settings) } + try { + block(api) + } finally { + api.shutdown() + } + } + + private fun onVirtualThread(block: () -> Unit) = Thread.startVirtualThread(block).join() + + /** + * Runs [block] while the server process reports itself as started; the signed chat handlers + * ignore packets while the server stops, which is the state of every test process. + */ + private fun asStartedServer(env: Env, block: () -> T): T { + val field = env.process().javaClass.getDeclaredField("started") + field.isAccessible = true + val started = field.get(env.process()) as AtomicBoolean + started.set(true) + + try { + return block() + } finally { + started.set(false) + } + } + + private fun Player.holder() = ChatSessionHolderImpl.fromPlayer(this) + + private fun Player.setChatMessageType(type: ChatMessageType) { + val current = settings + refreshSettings( + ClientSettings( + current.locale(), current.viewDistance(), type, current.chatColors(), + current.displayedSkinParts(), current.mainHand(), current.enableTextFiltering(), + current.allowServerListings(), current.particleSetting(), + ) + ) + } + + private fun signedCommand(command: String, vararg names: String) = ClientSignedCommandChatPacket( + command, + System.currentTimeMillis(), + 0L, + ArgumentSignatures(names.map { ArgumentSignatures.Entry(it, MessageSignature(ByteArray(256))) }), + LastSeenMessages.Update(0, BitSet()), + 0, + ) + + private fun Collector.translationKeys() = + collect().map { (it.message() as? TranslatableComponent)?.key() } + + @Test + fun `a player gets a chat session holder when it spawns`(env: Env) = withFeature { + val player = env.createPlayer(env.createFlatInstance(), Pos.ZERO) + + assertNotNull(player.holder()) + assertSame(player.holder(), ChatSessionHolder.fromPlayer(player)) + } + + @Test + fun `a new configuration phase resets the chat state`(env: Env) = withFeature { + val player = env.createPlayer(env.createFlatInstance(), Pos.ZERO) + val before = player.holder() + assertNotNull(before) + + onVirtualThread { EventDispatcher.call(AsyncPlayerConfigurationEvent(player, true)) } + assertSame(before, player.holder()) + + onVirtualThread { EventDispatcher.call(AsyncPlayerConfigurationEvent(player, false)) } + assertNull(player.holder()) + } + + @Test + fun `players that do not accept chat receive no player chat`(env: Env) = withFeature { + val instance = env.createFlatInstance() + val sender = env.createPlayer(instance, Pos.ZERO) + val fullConnection = env.createConnection() + val full = fullConnection.connect(instance, Pos.ZERO) + val hiddenConnection = env.createConnection() + val hidden = hiddenConnection.connect(instance, Pos.ZERO) + hidden.setChatMessageType(ChatMessageType.SYSTEM) + + val fullPackets = fullConnection.trackIncoming(PlayerChatMessagePacket::class.java) + val hiddenPackets = hiddenConnection.trackIncoming(PlayerChatMessagePacket::class.java) + + val message = PlayerChatMessage.unsigned(sender.uuid, "hello") + val chatType = BoundChatType(ChatType.CHAT, Component.text("sender")) + full.holder()!!.handler.sendPlayerChatMessage(message, chatType) + hidden.holder()!!.handler.sendPlayerChatMessage(message, chatType) + + fullPackets.assertCount(1) + hiddenPackets.assertEmpty() + } + + @Test + fun `a message of a sender that left while the event ran is not sent`(env: Env) = withFeature { + val instance = env.createFlatInstance() + val sender = env.createPlayer(instance, Pos.ZERO) + val receiverConnection = env.createConnection() + val receiver = receiverConnection.connect(instance, Pos.ZERO) + val packets = receiverConnection.trackIncoming(PlayerChatMessagePacket::class.java) + + AsyncPlayerChatEvent.addListener { event -> event.player.remove(false) }.use { + runBlocking { + ChatProcessor(sender, PlayerChatMessage.unsigned(sender.uuid, "bye")).process() + } + } + + assertTrue(sender.isRemoved) + assertNotNull(receiver.holder()) + packets.assertEmpty() + } + + @Test + fun `a message of a sender that stays is sent to players without a chat packet before`(env: Env) = + withFeature { + val instance = env.createFlatInstance() + val sender = env.createPlayer(instance, Pos.ZERO) + val receiverConnection = env.createConnection() + receiverConnection.connect(instance, Pos.ZERO) + val packets = receiverConnection.trackIncoming(PlayerChatMessagePacket::class.java) + + runBlocking { + ChatProcessor(sender, PlayerChatMessage.unsigned(sender.uuid, "hi")).process() + } + + packets.assertCount(1) + } + + @Test + fun `an unsigned command with signable arguments is rejected when profiles are enforced`(env: Env) = + withFeature({ enforceSecureProfile = true }) { + CommandAPICommand("echoes").signedMessageArgument("text").anyExecutor { _, _ -> }.register() + CommandAPICommand("plain").anyExecutor { _, _ -> }.register() + + val connection = env.createConnection() + val player = connection.connect(env.createFlatInstance(), Pos.ZERO) + val messages = connection.trackIncoming(SystemChatPacket::class.java) + val handler = player.holder()!!.handler + var ran = false + + handler.handleUnsignedCommandChat("echoes hello") { ran = true } + assertFalse(ran) + assertEquals(listOf("chat.disabled.invalid_command_signature"), messages.translationKeys()) + + handler.handleUnsignedCommandChat("plain") { ran = true } + assertTrue(ran) + } + + @Test + fun `a signed command without signatures runs unsigned unless profiles are enforced`(env: Env) = + withFeature { + CommandAPICommand("echoes").signedMessageArgument("text").anyExecutor { _, _ -> }.register() + val player = env.createPlayer(env.createFlatInstance(), Pos.ZERO) + var ran = false + + asStartedServer(env) { + player.holder()!!.handler.handleSignedCommandChat(signedCommand("echoes hello")) { ran = true } + } + + assertTrue(ran) + } + + @Test + fun `a signed command without signatures reports the missing profile key when enforced`(env: Env) = + withFeature({ enforceSecureProfile = true }) { + CommandAPICommand("echoes").signedMessageArgument("text").anyExecutor { _, _ -> }.register() + val connection = env.createConnection() + val player = connection.connect(env.createFlatInstance(), Pos.ZERO) + val messages = connection.trackIncoming(SystemChatPacket::class.java) + var ran = false + + asStartedServer(env) { + player.holder()!!.handler.handleSignedCommandChat(signedCommand("echoes hello")) { ran = true } + } + + assertFalse(ran) + assertEquals(listOf("chat.disabled.missingProfileKey"), messages.translationKeys()) + } + + @Test + fun `mismatching command signatures are reported and counted as command spam`(env: Env) = + withFeature({ commandSpamThresholdSeconds = 1 }) { + CommandAPICommand("echoes").signedMessageArgument("text").anyExecutor { _, _ -> }.register() + val connection = env.createConnection() + val player = connection.connect(env.createFlatInstance(), Pos.ZERO) + val messages = connection.trackIncoming(SystemChatPacket::class.java) + var ran = false + + asStartedServer(env) { + player.holder()!!.handler.handleSignedCommandChat(signedCommand("echoes hello", "other")) { + ran = true + } + } + + assertFalse(ran) + assertEquals(listOf("chat.disabled.invalid_command_signature"), messages.translationKeys()) + + env.tick() + assertFalse(player.isOnline, "the rejected command must count towards the spam threshold") + } + + @Test + fun `mismatching command signatures do not kick the player`(env: Env) = withFeature { + CommandAPICommand("echoes").signedMessageArgument("text").anyExecutor { _, _ -> }.register() + val player = env.createPlayer(env.createFlatInstance(), Pos.ZERO) + + asStartedServer(env) { + player.holder()!!.handler.handleSignedCommandChat(signedCommand("echoes hello", "other")) {} + } + env.tick() + + assertTrue(player.isOnline) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/test/kotlin/dev/slne/surf/api/minestom/server/chat/ViewerUnawareRendererTest.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/test/kotlin/dev/slne/surf/api/minestom/server/chat/ViewerUnawareRendererTest.kt new file mode 100644 index 000000000..aaf10396c --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/test/kotlin/dev/slne/surf/api/minestom/server/chat/ViewerUnawareRendererTest.kt @@ -0,0 +1,47 @@ +package dev.slne.surf.api.minestom.server.chat + +import dev.slne.surf.api.minestom.chat.ChatRenderer +import kotlinx.coroutines.runBlocking +import net.kyori.adventure.audience.Audience +import net.kyori.adventure.text.Component +import net.minestom.server.coordinate.Pos +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Test + +@EnvTest +class ViewerUnawareRendererTest { + + @Test + fun `identical arguments render once`(env: Env) = runBlocking { + val player = env.createPlayer(env.createFlatInstance(), Pos.ZERO) + var calls = 0 + val renderer = ChatRenderer.viewerUnaware { _, name, message -> + calls++ + name.append(message) + } + val name = Component.text("name") + val message = Component.text("message") + + val first = renderer.render(player, name, message, Audience.empty()) + val second = renderer.render(player, name, message, Audience.empty()) + + assertSame(first, second) + assertEquals(1, calls) + } + + @Test + fun `different arguments render again`(env: Env) = runBlocking { + val player = env.createPlayer(env.createFlatInstance(), Pos.ZERO) + val renderer = ChatRenderer.viewerUnaware { _, name, message -> name.append(message) } + val name = Component.text("name") + + val first = renderer.render(player, name, Component.text("first"), Audience.empty()) + val second = renderer.render(player, name, Component.text("second"), Audience.empty()) + + assertEquals(name.append(Component.text("first")), first) + assertEquals(name.append(Component.text("second")), second) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/test/kotlin/dev/slne/surf/api/minestom/server/chat/impl/TickThrottlerTest.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/test/kotlin/dev/slne/surf/api/minestom/server/chat/impl/TickThrottlerTest.kt new file mode 100644 index 000000000..4dd47ad44 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/test/kotlin/dev/slne/surf/api/minestom/server/chat/impl/TickThrottlerTest.kt @@ -0,0 +1,33 @@ +package dev.slne.surf.api.minestom.server.chat.impl + +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class TickThrottlerTest { + + @Test + fun `a threshold of zero never throttles`() { + val throttler = TickThrottler(20, 0) + repeat(100) { assertTrue(throttler.isIncrementAndUnderThreshold()) } + assertTrue(throttler.isUnderThreshold()) + } + + @Test + fun `the counter throttles once it reaches the threshold`() { + val throttler = TickThrottler(20, 60) + assertTrue(throttler.isIncrementAndUnderThreshold()) + assertTrue(throttler.isIncrementAndUnderThreshold()) + assertFalse(throttler.isIncrementAndUnderThreshold()) + } + + @Test + fun `ticks drain the counter below the threshold again`() { + val throttler = TickThrottler(20, 60) + repeat(3) { throttler.increment() } + assertFalse(throttler.isUnderThreshold()) + + throttler.tick() + assertTrue(throttler.isUnderThreshold()) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-signed-chat/src/test/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/MessageSignatureCacheTest.kt b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/test/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/MessageSignatureCacheTest.kt new file mode 100644 index 000000000..2020e13f3 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-signed-chat/src/test/kotlin/dev/slne/surf/api/minestom/server/chat/impl/signature/MessageSignatureCacheTest.kt @@ -0,0 +1,107 @@ +package dev.slne.surf.api.minestom.server.chat.impl.signature + +import net.minestom.server.crypto.MessageSignature +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import java.time.Instant + +class MessageSignatureCacheTest { + + private fun signature(seed: Int): MessageSignature = + MessageSignature(ByteArray(SIGNATURE_LENGTH) { index -> (seed + index).toByte() }) + + private fun bodyWith(lastSeen: List) = SignedMessageBody( + content = "content", + timeStamp = Instant.EPOCH, + salt = 0L, + lastSeen = LastSeenMessages(lastSeen), + ) + + @Test + fun `an unknown signature is not found`() { + val cache = MessageSignatureCache(4) + + assertEquals(MessageSignatureCache.NOT_FOUND, cache.pack(signature(1))) + } + + @Test + fun `a pushed signature is packed to the index it was stored at`() { + val cache = MessageSignatureCache(4) + val pushed = signature(1) + + cache.push(bodyWith(emptyList()), pushed) + + assertEquals(0, cache.pack(pushed)) + assertEquals(pushed, cache.unpack(0)) + } + + @Test + fun `an equal signature from a different array packs to the same index`() { + val cache = MessageSignatureCache(4) + + cache.push(bodyWith(emptyList()), signature(7)) + + assertEquals(0, cache.pack(signature(7))) + } + + @Test + fun `the newest signature takes the first slot and shifts the previous one down`() { + val cache = MessageSignatureCache(4) + val first = signature(1) + val second = signature(2) + + cache.push(bodyWith(emptyList()), first) + cache.push(bodyWith(emptyList()), second) + + assertEquals(0, cache.pack(second)) + assertEquals(1, cache.pack(first)) + } + + @Test + fun `last seen entries are pushed before the message signature`() { + val cache = MessageSignatureCache(8) + val seenFirst = signature(1) + val seenSecond = signature(2) + val own = signature(3) + + cache.push(bodyWith(listOf(seenFirst, seenSecond)), own) + + // push() drains the queue from the back, so the message's own signature lands first. + assertEquals(0, cache.pack(own)) + assertEquals(1, cache.pack(seenSecond)) + assertEquals(2, cache.pack(seenFirst)) + } + + @Test + fun `re-pushing an entry keeps it in the cache exactly once`() { + val cache = MessageSignatureCache(4) + val repeated = signature(1) + val other = signature(2) + + cache.push(bodyWith(emptyList()), repeated) + cache.push(bodyWith(emptyList()), other) + cache.push(bodyWith(emptyList()), repeated) + + assertEquals(0, cache.pack(repeated)) + assertEquals(1, cache.pack(other)) + assertNull(cache.unpack(2)) + } + + @Test + fun `entries beyond the capacity are dropped`() { + val cache = MessageSignatureCache(2) + + cache.push(bodyWith(emptyList()), signature(1)) + cache.push(bodyWith(emptyList()), signature(2)) + cache.push(bodyWith(emptyList()), signature(3)) + + assertEquals(0, cache.pack(signature(3))) + assertEquals(1, cache.pack(signature(2))) + assertEquals(MessageSignatureCache.NOT_FOUND, cache.pack(signature(1))) + } + + private companion object { + const val SIGNATURE_LENGTH = 256 + } +} diff --git a/surf-api-minestom/surf-api-minestom-server-spark/api/surf-api-minestom-server-spark.api b/surf-api-minestom/surf-api-minestom-server-spark/api/surf-api-minestom-server-spark.api new file mode 100644 index 000000000..19aa699ac --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-spark/api/surf-api-minestom-server-spark.api @@ -0,0 +1,25 @@ +public final class dev/slne/surf/api/minestom/server/spark/SparkFeature : dev/slne/surf/api/minestom/server/SurfMinestomFeature { + public static final field Companion Ldev/slne/surf/api/minestom/server/spark/SparkFeature$Companion; + public static final field ID Ljava/lang/String; + public fun disable (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public fun enable (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public fun getId ()Ljava/lang/String; + public fun getLoadPriority ()I + public fun load (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V +} + +public final class dev/slne/surf/api/minestom/server/spark/SparkFeature$Companion { +} + +public final class dev/slne/surf/api/minestom/server/spark/SparkFeatureBuilder { + public final fun getDataDirectory ()Ljava/nio/file/Path; + public final fun getProfileOnStartup ()Z + public final fun setDataDirectory (Ljava/nio/file/Path;)V + public final fun setProfileOnStartup (Z)V +} + +public final class dev/slne/surf/api/minestom/server/spark/SparkFeatureKt { + public static final fun withSpark (Ldev/slne/surf/api/minestom/server/SurfMinestomServerBuilder;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun withSpark$default (Ldev/slne/surf/api/minestom/server/SurfMinestomServerBuilder;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V +} + diff --git a/surf-api-minestom/surf-api-minestom-server-spark/build.gradle.kts b/surf-api-minestom/surf-api-minestom-server-spark/build.gradle.kts new file mode 100644 index 000000000..84570d2ab --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-spark/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + `vendor-bundle-convention` + `api-validation` +} + +dependencies { + api(projects.surfApiMinestom.surfApiMinestomServer) + compileOnlyApi(libs.minestom.server) + bundled(libs.minestom.spark) + // Not bundled, see vendor-bundle-convention + implementation(libs.asm) +} + +description = "surf-api-minestom-server-spark" diff --git a/surf-api-minestom/surf-api-minestom-server-spark/src/main/kotlin/dev/slne/surf/api/minestom/server/spark/SparkFeature.kt b/surf-api-minestom/surf-api-minestom-server-spark/src/main/kotlin/dev/slne/surf/api/minestom/server/spark/SparkFeature.kt new file mode 100644 index 000000000..5a706c544 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server-spark/src/main/kotlin/dev/slne/surf/api/minestom/server/spark/SparkFeature.kt @@ -0,0 +1,78 @@ +package dev.slne.surf.api.minestom.server.spark + +import dev.slne.surf.api.minestom.extension.CommandManager +import dev.slne.surf.api.minestom.permission.hasPermission +import dev.slne.surf.api.minestom.server.SurfMinestomFeature +import dev.slne.surf.api.minestom.server.SurfMinestomServer +import dev.slne.surf.api.minestom.server.SurfMinestomServerBuilder +import dev.slne.surf.api.minestom.server.SurfMinestomServerDsl +import me.lucko.spark.minestom.MinestomSparkPlugin +import java.nio.file.Path +import kotlin.io.path.Path + +/** + * Configures [withSpark]. + */ +@SurfMinestomServerDsl +class SparkFeatureBuilder internal constructor() { + + /** + * Where spark keeps its configuration and data. + */ + var dataDirectory: Path = Path("plugins/spark") + + /** + * Whether the profiler starts sampling all threads on the first tick after the server started. + */ + var profileOnStartup: Boolean = false +} + +/** + * Runs the spark profiler on this server. + * + * `/spark` checks permissions through the surf api. Without a permission provider such as + * `withLuckPerms()`, only the console can use it. + */ +class SparkFeature internal constructor( + private val settings: SparkFeatureBuilder, +) : SurfMinestomFeature { + + override val id: String = ID + + private var plugin: MinestomSparkPlugin? = null + + override fun load(api: SurfMinestomServer) { + val spark = MinestomSparkPlugin(settings.dataDirectory) { sender, permission -> + sender.hasPermission(permission) + } + + spark.enable() + plugin = spark + } + + override fun enable(api: SurfMinestomServer) { + if (settings.profileOnStartup) { + CommandManager.execute(CommandManager.consoleSender, PROFILER_START_COMMAND) + } + } + + override fun disable(api: SurfMinestomServer) { + plugin?.disable() + plugin = null + } + + companion object { + const val ID = "spark" + + private const val PROFILER_START_COMMAND = "spark profiler start --thread *" + } +} + +/** + * Installs the spark profiler. + * + * @see SparkFeature + */ +fun SurfMinestomServerBuilder.withSpark(block: SparkFeatureBuilder.() -> Unit = {}) { + install(SparkFeature(SparkFeatureBuilder().apply(block))) +} diff --git a/surf-api-minestom/surf-api-minestom-server/api/surf-api-minestom-server.api b/surf-api-minestom/surf-api-minestom-server/api/surf-api-minestom-server.api new file mode 100644 index 000000000..de490a3a4 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/api/surf-api-minestom-server.api @@ -0,0 +1,258 @@ +public abstract interface class dev/slne/surf/api/minestom/server/SurfMinestomFeature { + public fun disable (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public fun enable (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public abstract fun getId ()Ljava/lang/String; + public fun getLoadPriority ()I + public fun load (Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V +} + +public final class dev/slne/surf/api/minestom/server/SurfMinestomFeature$DefaultImpls { + public static fun disable (Ldev/slne/surf/api/minestom/server/SurfMinestomFeature;Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public static fun enable (Ldev/slne/surf/api/minestom/server/SurfMinestomFeature;Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V + public static fun getLoadPriority (Ldev/slne/surf/api/minestom/server/SurfMinestomFeature;)I + public static fun load (Ldev/slne/surf/api/minestom/server/SurfMinestomFeature;Ldev/slne/surf/api/minestom/server/SurfMinestomServer;)V +} + +public abstract interface class dev/slne/surf/api/minestom/server/SurfMinestomServer { + public static final field Companion Ldev/slne/surf/api/minestom/server/SurfMinestomServer$Companion; + public abstract fun feature (Ljava/lang/String;)Ldev/slne/surf/api/minestom/server/SurfMinestomFeature; + public abstract fun getEventNode ()Lnet/minestom/server/event/EventNode; + public abstract fun getFeatures ()Ljava/util/Collection; + public abstract fun shutdown ()V +} + +public final class dev/slne/surf/api/minestom/server/SurfMinestomServer$Companion { + public final fun getInstance ()Ldev/slne/surf/api/minestom/server/SurfMinestomServer; +} + +public final class dev/slne/surf/api/minestom/server/SurfMinestomServerBuilder { + public final fun disableEntityTicking (Ljava/lang/Iterable;)V + public final fun disableEntityTicking ([Lnet/minestom/server/entity/EntityType;)V + public final fun getEventNodeName ()Ljava/lang/String; + public final fun getMaxPlayers ()Ljava/lang/Integer; + public final fun getRecommendedServerProperties ()Z + public final fun install (Ldev/slne/surf/api/minestom/server/SurfMinestomFeature;)V + public final fun isInstalled (Ljava/lang/String;)Z + public final fun listeners ([Ldev/slne/surf/api/minestom/event/EventRegistrar;)V + public final fun permissions (Ldev/slne/surf/api/minestom/permission/PermissionProvider;)V + public final fun setEventNodeName (Ljava/lang/String;)V + public final fun setMaxPlayers (Ljava/lang/Integer;)V + public final fun setRecommendedServerProperties (Z)V + public final fun withConfigurationPhase (Lkotlin/jvm/functions/Function1;)V + public static synthetic fun withConfigurationPhase$default (Ldev/slne/surf/api/minestom/server/SurfMinestomServerBuilder;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V +} + +public abstract interface annotation class dev/slne/surf/api/minestom/server/SurfMinestomServerDsl : java/lang/annotation/Annotation { +} + +public final class dev/slne/surf/api/minestom/server/SurfMinestomServerKt { + public static final fun surfMinestomServer (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/server/SurfMinestomServer; + public static synthetic fun surfMinestomServer$default (Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/server/SurfMinestomServer; +} + +public final class dev/slne/surf/api/minestom/server/configuration/ConfigurationContext { + public final fun getEvent ()Lnet/minestom/server/event/player/AsyncPlayerConfigurationEvent; + public final fun getPlayer ()Lnet/minestom/server/entity/Player; + public final fun getSpawningInstance ()Lnet/minestom/server/instance/Instance; + public final fun isFirstConfig ()Z + public final fun setEvent (Lnet/minestom/server/event/player/AsyncPlayerConfigurationEvent;)V +} + +public final class dev/slne/surf/api/minestom/server/configuration/ConfigurationPhaseBuilder { + public final fun after-QwhMw68 (Ljava/lang/String;Ljava/lang/String;Ldev/slne/surf/api/minestom/server/configuration/ConfigurationTask;)V + public final fun awaitSettings-LRDsOJo (J)V + public static synthetic fun awaitSettings-LRDsOJo$default (Ldev/slne/surf/api/minestom/server/configuration/ConfigurationPhaseBuilder;JILjava/lang/Object;)V + public final fun before-QwhMw68 (Ljava/lang/String;Ljava/lang/String;Ldev/slne/surf/api/minestom/server/configuration/ConfigurationTask;)V + public final fun codeOfConduct (Lkotlin/jvm/functions/Function1;)V + public final fun first-OewgcVQ (Ljava/lang/String;Ldev/slne/surf/api/minestom/server/configuration/ConfigurationTask;)V + public final fun getIds ()Ljava/util/List; + public final fun last-OewgcVQ (Ljava/lang/String;Ldev/slne/surf/api/minestom/server/configuration/ConfigurationTask;)V + public final fun remove-U6ZxwRg (Ljava/lang/String;)V + public final fun replace-OewgcVQ (Ljava/lang/String;Ldev/slne/surf/api/minestom/server/configuration/ConfigurationTask;)V +} + +public abstract interface class dev/slne/surf/api/minestom/server/configuration/ConfigurationTask { + public fun install (Lnet/minestom/server/event/EventNode;)V + public abstract fun run (Ldev/slne/surf/api/minestom/server/configuration/ConfigurationContext;)V +} + +public final class dev/slne/surf/api/minestom/server/configuration/ConfigurationTask$DefaultImpls { + public static fun install (Ldev/slne/surf/api/minestom/server/configuration/ConfigurationTask;Lnet/minestom/server/event/EventNode;)V +} + +public final class dev/slne/surf/api/minestom/server/configuration/ConfigurationTaskId { + public static final synthetic fun box-impl (Ljava/lang/String;)Ldev/slne/surf/api/minestom/server/configuration/ConfigurationTaskId; + public static fun constructor-impl (Ljava/lang/String;)Ljava/lang/String; + public fun equals (Ljava/lang/Object;)Z + public static fun equals-impl (Ljava/lang/String;Ljava/lang/Object;)Z + public static final fun equals-impl0 (Ljava/lang/String;Ljava/lang/String;)Z + public final fun getValue ()Ljava/lang/String; + public fun hashCode ()I + public static fun hashCode-impl (Ljava/lang/String;)I + public fun toString ()Ljava/lang/String; + public static fun toString-impl (Ljava/lang/String;)Ljava/lang/String; + public final synthetic fun unbox-impl ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/server/configuration/ConfigurationTasks { + public static final field INSTANCE Ldev/slne/surf/api/minestom/server/configuration/ConfigurationTasks; + public final fun getAWAIT_SETTINGS-Ok4KtHk ()Ljava/lang/String; + public final fun getBRAND-Ok4KtHk ()Ljava/lang/String; + public final fun getBrand ()Ldev/slne/surf/api/minestom/server/configuration/ConfigurationTask; + public final fun getCODE_OF_CONDUCT-Ok4KtHk ()Ljava/lang/String; + public final fun getCONFIGURATION_EVENT-Ok4KtHk ()Ljava/lang/String; + public final fun getConfigurationEvent ()Ldev/slne/surf/api/minestom/server/configuration/ConfigurationTask; + public final fun getENABLED_FEATURES-Ok4KtHk ()Ljava/lang/String; + public final fun getEnabledFeatures ()Ldev/slne/surf/api/minestom/server/configuration/ConfigurationTask; + public final fun getJOIN_WORLD-Ok4KtHk ()Ljava/lang/String; + public final fun getJoinWorld ()Ldev/slne/surf/api/minestom/server/configuration/ConfigurationTask; + public final fun getPLAYER_COUNT-Ok4KtHk ()Ljava/lang/String; + public final fun getPlayerCount ()Ldev/slne/surf/api/minestom/server/configuration/ConfigurationTask; + public final fun getRESOURCE_PACK-Ok4KtHk ()Ljava/lang/String; + public final fun getResourcePack ()Ldev/slne/surf/api/minestom/server/configuration/ConfigurationTask; + public final fun getSPAWN_LOCATION-Ok4KtHk ()Ljava/lang/String; + public final fun getSYNCHRONIZE_REGISTRIES-Ok4KtHk ()Ljava/lang/String; + public final fun getSpawnLocation ()Ldev/slne/surf/api/minestom/server/configuration/ConfigurationTask; + public final fun getSynchronizeRegistries ()Ldev/slne/surf/api/minestom/server/configuration/ConfigurationTask; + public final fun stopKeepAlive (Lnet/minestom/server/entity/Player;)V +} + +public final class dev/slne/surf/api/minestom/server/configuration/ConfigurationTasks$AwaitSettings : dev/slne/surf/api/minestom/server/configuration/ConfigurationTask { + public fun ()V + public synthetic fun (JILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun install (Lnet/minestom/server/event/EventNode;)V + public fun run (Ldev/slne/surf/api/minestom/server/configuration/ConfigurationContext;)V +} + +public final class dev/slne/surf/api/minestom/server/configuration/ConfigurationTasks$CodeOfConduct : dev/slne/surf/api/minestom/server/configuration/ConfigurationTask { + public fun (Lkotlin/jvm/functions/Function1;)V + public fun install (Lnet/minestom/server/event/EventNode;)V + public fun run (Ldev/slne/surf/api/minestom/server/configuration/ConfigurationContext;)V +} + +public final class dev/slne/surf/api/minestom/server/impl/command/CommandAPIHook { + public static final field INSTANCE Ldev/slne/surf/api/minestom/server/impl/command/CommandAPIHook; + public static final fun declare (Lnet/minestom/server/network/packet/server/play/DeclareCommandsPacket;Lnet/minestom/server/entity/Player;)Lnet/minestom/server/network/packet/server/play/DeclareCommandsPacket; + public static final fun execute (Lnet/minestom/server/command/CommandSender;Ljava/lang/String;)Lnet/minestom/server/command/builder/CommandResult; + public static final fun owns (Ljava/lang/String;)Z + public static final fun reportUnknown (Lnet/minestom/server/command/CommandSender;Ljava/lang/String;)V +} + +public final class dev/slne/surf/api/minestom/server/impl/command/CommandAPITranslations { + public static final field INSTANCE Ldev/slne/surf/api/minestom/server/impl/command/CommandAPITranslations; + public final fun register ()V +} + +public final class dev/slne/surf/api/minestom/server/impl/command/PluginCommands { + public static final field INSTANCE Ldev/slne/surf/api/minestom/server/impl/command/PluginCommands; + public final fun unregisterAll (Ldev/slne/surf/api/minestom/plugin/SurfMinestomPlugin;)I +} + +public final class dev/slne/surf/api/minestom/server/impl/command/SignedCommandArguments { + public static final field INSTANCE Ldev/slne/surf/api/minestom/server/impl/command/SignedCommandArguments; + public final fun find (Ljava/lang/String;)Lnet/kyori/adventure/chat/SignedMessage; + public final fun getUnsignedFactory ()Lkotlin/jvm/functions/Function2; + public final fun resetUnsignedFactory ()V + public final fun setUnsignedFactory (Lkotlin/jvm/functions/Function2;)V + public final fun withMessages (Ljava/util/Map;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; +} + +public final class dev/slne/surf/api/minestom/server/impl/command/SignedCommandArgumentsKt { + public static final fun signableCommandArguments (Lnet/minestom/server/command/CommandSender;Ljava/lang/String;)Ljava/util/Map; + public static final fun signaturesCoverArguments (Lnet/minestom/server/crypto/ArgumentSignatures;Ljava/util/Map;)Z +} + +public final class dev/slne/surf/api/minestom/server/impl/configuration/ConfigurationPhaseHook { + public static final field INSTANCE Ldev/slne/surf/api/minestom/server/impl/configuration/ConfigurationPhaseHook; + public static final fun configure (Ldev/slne/surf/api/minestom/server/impl/mixin/duck/ConnectionManagerDuck;Lnet/minestom/server/entity/Player;Z)Z +} + +public final class dev/slne/surf/api/minestom/server/impl/mixin/InstrumentationGlobalPropertyService : org/spongepowered/asm/service/IGlobalPropertyService { + public fun ()V + public fun getProperty (Lorg/spongepowered/asm/service/IPropertyKey;)Ljava/lang/Object; + public fun getProperty (Lorg/spongepowered/asm/service/IPropertyKey;Ljava/lang/Object;)Ljava/lang/Object; + public fun getPropertyString (Lorg/spongepowered/asm/service/IPropertyKey;Ljava/lang/String;)Ljava/lang/String; + public fun resolveKey (Ljava/lang/String;)Lorg/spongepowered/asm/service/IPropertyKey; + public fun setProperty (Lorg/spongepowered/asm/service/IPropertyKey;Ljava/lang/Object;)V +} + +public final class dev/slne/surf/api/minestom/server/impl/mixin/InstrumentationMixinService : org/spongepowered/asm/service/MixinServiceAbstract, org/spongepowered/asm/service/IClassBytecodeProvider, org/spongepowered/asm/service/IClassProvider { + public fun ()V + public fun findAgentClass (Ljava/lang/String;Z)Ljava/lang/Class; + public fun findClass (Ljava/lang/String;)Ljava/lang/Class; + public fun findClass (Ljava/lang/String;Z)Ljava/lang/Class; + public fun getAdviceProvider ()Lorg/spongepowered/asm/service/IAdviceProvider; + public fun getAuditTrail ()Lorg/spongepowered/asm/service/IMixinAuditTrail; + public fun getBytecodeProvider ()Lorg/spongepowered/asm/service/IClassBytecodeProvider; + public fun getClassNode (Ljava/lang/String;)Lorg/objectweb/asm/tree/ClassNode; + public fun getClassNode (Ljava/lang/String;Z)Lorg/objectweb/asm/tree/ClassNode; + public fun getClassNode (Ljava/lang/String;ZI)Lorg/objectweb/asm/tree/ClassNode; + public fun getClassPath ()[Ljava/net/URL; + public fun getClassProvider ()Lorg/spongepowered/asm/service/IClassProvider; + public fun getClassTracker ()Lorg/spongepowered/asm/service/IClassTracker; + public fun getFeatureValidator ()Lorg/spongepowered/asm/service/IFeatureValidator; + public fun getMaxCompatibilityLevel ()Lorg/spongepowered/asm/mixin/MixinEnvironment$CompatibilityLevel; + public fun getMinCompatibilityLevel ()Lorg/spongepowered/asm/mixin/MixinEnvironment$CompatibilityLevel; + public fun getName ()Ljava/lang/String; + public fun getPlatformAgents ()Ljava/util/Collection; + public fun getPrimaryContainer ()Lorg/spongepowered/asm/launch/platform/container/IContainerHandle; + public fun getResourceAsStream (Ljava/lang/String;)Ljava/io/InputStream; + public fun getTransformerProvider ()Lorg/spongepowered/asm/service/ITransformerProvider; + public fun isValid ()Z + public fun offer (Lorg/spongepowered/asm/service/IMixinInternal;)V +} + +public final class dev/slne/surf/api/minestom/server/impl/mixin/SurfMixinAgent { + public static final field CONFIGS_ATTRIBUTE Ljava/lang/String; + public static fun agentmain (Ljava/lang/String;Ljava/lang/instrument/Instrumentation;)V + public static fun failedTransformations ()Ljava/util/List; + public static fun install (Ljava/lang/instrument/Instrumentation;Ljava/util/Collection;)V + public static fun isInstalled ()Z + public static fun premain (Ljava/lang/String;Ljava/lang/instrument/Instrumentation;)V +} + +public abstract interface class dev/slne/surf/api/minestom/server/impl/mixin/duck/ConnectionManagerDuck { + public abstract fun surf$configurationPlayers ()Ljava/util/Set; + public abstract fun surf$keepAlivePlayers ()Ljava/util/Set; +} + +public abstract interface class dev/slne/surf/api/minestom/server/impl/mixin/duck/EntityTrackerEntryDuck { + public abstract fun surf$lastPosition ()Lnet/minestom/server/coordinate/Point; +} + +public final class dev/slne/surf/api/minestom/server/impl/performance/EntityTickFilter { + public static final field INSTANCE Ldev/slne/surf/api/minestom/server/impl/performance/EntityTickFilter; + public final fun configure (Ljava/util/Collection;)V + public static final fun shouldTick (Lnet/minestom/server/entity/Entity;)Z +} + +public final class dev/slne/surf/api/minestom/server/impl/performance/EntityViewerLookup { + public static final field INSTANCE Ldev/slne/surf/api/minestom/server/impl/performance/EntityViewerLookup; + public static final fun chunksInRangeRectangular (IIILnet/minestom/server/coordinate/ChunkRange$ChunkConsumer;)V + public static final fun directScan (Ljava/util/Set;Ldev/slne/surf/api/minestom/server/impl/performance/EntityViewerLookup$EntityPositionResolver;IIILjava/util/function/Consumer;)V + public static final fun useDirectScan (II)Z +} + +public abstract interface class dev/slne/surf/api/minestom/server/impl/performance/EntityViewerLookup$EntityPositionResolver { + public abstract fun resolve (Lnet/minestom/server/entity/Entity;)Lnet/minestom/server/coordinate/Point; +} + +public final class dev/slne/surf/api/minestom/server/impl/performance/SkylightSuppression { + public static final field INSTANCE Ldev/slne/surf/api/minestom/server/impl/performance/SkylightSuppression; + public static final fun sectionSkyLight (ZLnet/minestom/server/instance/Section;)Lnet/minestom/server/instance/light/Light; +} + +public final class dev/slne/surf/api/minestom/server/impl/performance/SkylightSuppression$DisabledSkyLight : net/minestom/server/instance/light/Light { + public static final field INSTANCE Ldev/slne/surf/api/minestom/server/impl/performance/SkylightSuppression$DisabledSkyLight; + public fun array ()[B + public fun calculateExternal (Lnet/minestom/server/instance/palette/Palette;[Lnet/minestom/server/coordinate/Point;Lnet/minestom/server/instance/light/Light$LightLookup;Lnet/minestom/server/instance/light/Light$PaletteLookup;)Ljava/util/Set; + public fun calculateInternal (Lnet/minestom/server/instance/palette/Palette;III[IILnet/minestom/server/instance/light/Light$LightLookup;)Ljava/util/Set; + public fun flip ()V + public fun getLevel (III)I + public fun invalidate ()V + public fun requiresSend ()Z + public fun requiresUpdate ()Z + public fun set ([B)V +} + diff --git a/surf-api-minestom/surf-api-minestom-server/build.gradle.kts b/surf-api-minestom/surf-api-minestom-server/build.gradle.kts new file mode 100644 index 000000000..2db704163 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + `core-convention` + `api-validation` + `java-test-fixtures` +} + +dependencies { + api(projects.surfApiMinestom.surfApiMinestom) + implementation(projects.surfApiCore.surfApiCoreServer) + compileOnlyApi(libs.minestom.server) + + implementation(libs.packetevents.netty.common) + implementation(libs.sponge.mixin) + implementation(libs.mixinextras) + // EntityTrackerMixin shadows a field of this type; Minestom brings it at runtime + compileOnly(libs.flare.fastutil) + runtimeOnly(libs.flogger.slf4j.backend) + + testImplementation(libs.minestom.server) + testImplementation(libs.minestom.testing) + testImplementation(libs.log4j.core) + testImplementation(libs.log4j.slf4j2.impl) + + testFixturesApi(libs.sponge.mixin) + testFixturesImplementation(libs.bytebuddy.agent) + testFixturesImplementation(libs.junit.platform.launcher) +} + +description = "surf-api-minestom-server" + +tasks.test { + systemProperty("minestom.inside-test", "true") + systemProperty("junit.jupiter.execution.timeout.default", "2m") +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/impl/mixin/InstrumentationGlobalPropertyService.java b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/impl/mixin/InstrumentationGlobalPropertyService.java new file mode 100644 index 000000000..f13221998 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/impl/mixin/InstrumentationGlobalPropertyService.java @@ -0,0 +1,63 @@ +package dev.slne.surf.api.minestom.server.impl.mixin; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.spongepowered.asm.service.IGlobalPropertyService; +import org.spongepowered.asm.service.IPropertyKey; + +/** + * Holds Mixin's global properties, which a launcher would otherwise provide. + */ +@NullMarked +public final class InstrumentationGlobalPropertyService implements IGlobalPropertyService { + + private final Map properties = new ConcurrentHashMap<>(); + + @Override + public IPropertyKey resolveKey(final String name) { + return new Key(name); + } + + @Override + @SuppressWarnings("unchecked") + public @Nullable T getProperty(final IPropertyKey key) { + return (T) properties.get(name(key)); + } + + @Override + public void setProperty(final IPropertyKey key, final @Nullable Object value) { + if (value == null) { + properties.remove(name(key)); + } else { + properties.put(name(key), value); + } + } + + @Override + @SuppressWarnings("unchecked") + public T getProperty(final IPropertyKey key, final T defaultValue) { + return (T) properties.getOrDefault(name(key), defaultValue); + } + + @Override + public String getPropertyString(final IPropertyKey key, final String defaultValue) { + final @Nullable Object value = properties.get(name(key)); + return value != null ? value.toString() : defaultValue; + } + + private static String name(final IPropertyKey key) { + if (!(key instanceof Key(String name))) { + throw new IllegalArgumentException("Unknown property key implementation: " + key.getClass().getName()); + } + return name; + } + + private record Key(String name) implements IPropertyKey { + @Override + public String toString() { + return name; + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/impl/mixin/InstrumentationMixinService.java b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/impl/mixin/InstrumentationMixinService.java new file mode 100644 index 000000000..6197e222d --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/impl/mixin/InstrumentationMixinService.java @@ -0,0 +1,301 @@ +package dev.slne.surf.api.minestom.server.impl.mixin; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.lang.instrument.Instrumentation; +import java.net.URI; +import java.net.URL; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.launch.platform.container.ContainerHandleURI; +import org.spongepowered.asm.launch.platform.container.IContainerHandle; +import org.spongepowered.asm.logging.ILogger; +import org.spongepowered.asm.logging.LoggerAdapterConsole; +import org.spongepowered.asm.mixin.MixinEnvironment.CompatibilityLevel; +import org.spongepowered.asm.mixin.transformer.IMixinTransformer; +import org.spongepowered.asm.mixin.transformer.IMixinTransformerFactory; +import org.spongepowered.asm.service.IAdviceProvider; +import org.spongepowered.asm.service.IClassBytecodeProvider; +import org.spongepowered.asm.service.IClassProvider; +import org.spongepowered.asm.service.IClassTracker; +import org.spongepowered.asm.service.IFeatureValidator; +import org.spongepowered.asm.service.IMixinAuditTrail; +import org.spongepowered.asm.service.IMixinInternal; +import org.spongepowered.asm.service.ITransformerProvider; +import org.spongepowered.asm.service.MixinServiceAbstract; + +/** + * Runs Mixin on top of {@link Instrumentation} instead of a launcher. + * + *

Discovered by Mixin through {@code META-INF/services}; valid only after + * {@link #setInstrumentation} was called. The transformer Mixin offers is registered as a + * non-retransforming {@link ClassFileTransformer}, so it only affects classes loaded afterwards. + * A required mixin whose target was loaded earlier fails Mixin's early-load check, which is + * answered from the class snapshot passed to {@link #setInstrumentation(Instrumentation, Set)}.

+ * + *

Classes and resources are read from the system class loader. A failed transformation is + * logged, recorded in {@link #failures()} and rethrown; the JVM ignores the exception and loads + * the class untransformed.

+ */ +@NullMarked +public final class InstrumentationMixinService extends MixinServiceAbstract implements IClassProvider, IClassBytecodeProvider { + + private static final List EXCLUDED_PREFIXES = List.of( + "org/spongepowered/asm/", + "com/llamalad7/mixinextras/" + ); + + private static final List FAILURES = new CopyOnWriteArrayList<>(); + + private static volatile @Nullable Instrumentation instrumentation; + + /** + * Binary names of the classes loaded before Mixin was installed. + */ + private static volatile Set loadedBeforeInstall = Set.of(); + + private static volatile @Nullable IMixinTransformer transformer; + + static void setInstrumentation(final Instrumentation instrumentation) { + if (InstrumentationMixinService.instrumentation != null) { + throw new IllegalStateException("Instrumentation has already been initialized"); + } + + InstrumentationMixinService.instrumentation = instrumentation; + } + + /** + * The installed transformer, or {@code null} before Mixin offered it. + */ + static @Nullable IMixinTransformer transformer() { + return transformer; + } + + static void setInstrumentation(final Instrumentation instrumentation, final Set loadedClasses) { + loadedBeforeInstall = loadedClasses; + setInstrumentation(instrumentation); + } + + /** + * Binary names of the classes whose transformation failed, in the order they failed. + */ + static List failures() { + return List.copyOf(FAILURES); + } + + @Override + public String getName() { + return "SurfMinestomInstrumentation"; + } + + @Override + public boolean isValid() { + return instrumentation != null; + } + + @Override + public void offer(final IMixinInternal internal) { + super.offer(internal); + + if (internal instanceof IMixinTransformerFactory factory) { + if (transformer != null) { + throw new IllegalStateException("Mixin transformer has already been installed"); + } + + final IMixinTransformer created = factory.createTransformer(); + transformer = created; + requireNonNull(instrumentation, "instrumentation").addTransformer(new TransformerAdapter(created), false); + } + } + + @Override + public IClassProvider getClassProvider() { + return this; + } + + @Override + public IClassBytecodeProvider getBytecodeProvider() { + return this; + } + + @Override + public @Nullable ITransformerProvider getTransformerProvider() { + return null; + } + + @Override + public IClassTracker getClassTracker() { + return ClassTracker.INSTANCE; + } + + @Override + protected ILogger createLogger(final String name) { + return new LoggerAdapterConsole(name); + } + + @Override + public @Nullable IMixinAuditTrail getAuditTrail() { + return null; + } + + @Override + public IFeatureValidator getFeatureValidator() { + return IFeatureValidator.ALLOW_ALL; + } + + @Override + public IAdviceProvider getAdviceProvider() { + return IAdviceProvider.GENERIC; + } + + @Override + public CompatibilityLevel getMinCompatibilityLevel() { + return CompatibilityLevel.JAVA_8; + } + + @Override + public CompatibilityLevel getMaxCompatibilityLevel() { + return CompatibilityLevel.JAVA_25; + } + + @Override + public Collection getPlatformAgents() { + // MixinContainer always adds MixinPlatformAgentDefault itself + return List.of(); + } + + @Override + public IContainerHandle getPrimaryContainer() { + final @Nullable CodeSource codeSource = InstrumentationMixinService.class.getProtectionDomain().getCodeSource(); + if (codeSource == null) { + return new ContainerHandleURI(URI.create("file:/")); + } + + try { + return new ContainerHandleURI(codeSource.getLocation().toURI()); + } catch (final Exception e) { + throw new IllegalStateException("Unable to determine the server container", e); + } + } + + @Override + public @Nullable InputStream getResourceAsStream(final String name) { + return loader().getResourceAsStream(name); + } + + @Override + public URL[] getClassPath() { + return new URL[0]; + } + + @Override + public Class findClass(final String name) throws ClassNotFoundException { + return Class.forName(name, true, loader()); + } + + @Override + public Class findClass(final String name, final boolean initialize) throws ClassNotFoundException { + return Class.forName(name, initialize, loader()); + } + + @Override + public Class findAgentClass(final String name, final boolean initialize) throws ClassNotFoundException { + return findClass(name, initialize); + } + + @Override + public ClassNode getClassNode(final String name) throws ClassNotFoundException, IOException { + return getClassNode(name, true); + } + + @Override + public ClassNode getClassNode(final String name, final boolean runTransformers) throws ClassNotFoundException, IOException { + return getClassNode(name, runTransformers, 0); + } + + @Override + public ClassNode getClassNode(final String name, final boolean runTransformers, final int readerFlags) + throws ClassNotFoundException, IOException { + final byte[] bytes; + try (InputStream stream = loader().getResourceAsStream(name.replace('.', '/') + ".class")) { + if (stream == null) { + throw new ClassNotFoundException(name); + } + bytes = stream.readAllBytes(); + } + + final ClassNode node = new ClassNode(); + new ClassReader(bytes).accept(node, readerFlags); + return node; + } + + private static ClassLoader loader() { + return ClassLoader.getSystemClassLoader(); + } + + private enum ClassTracker implements IClassTracker { + INSTANCE; + + @Override + public void registerInvalidClass(final String className) { + } + + @Override + public boolean isClassLoaded(final String className) { + return loadedBeforeInstall.contains(className); + } + + @Override + public String getClassRestrictions(final String className) { + return ""; + } + } + + private record TransformerAdapter(IMixinTransformer transformer) implements ClassFileTransformer { + + @Override + public byte @Nullable [] transform( + final @Nullable Module module, + final @Nullable ClassLoader loader, + final @Nullable String className, + final @Nullable Class classBeingRedefined, + final @Nullable ProtectionDomain protectionDomain, + final byte[] classfileBuffer + ) throws IllegalClassFormatException { + if (className == null) { + return null; + } + + for (final String prefix : EXCLUDED_PREFIXES) { + if (className.startsWith(prefix)) { + return null; + } + } + + final String binaryName = className.replace('/', '.'); + try { + final byte[] result = transformer.transformClassBytes(binaryName, binaryName, classfileBuffer); + return result == classfileBuffer ? null : result; + } catch (final Throwable e) { + FAILURES.add(binaryName); + System.err.println("[surf-mixin] Transformation of " + binaryName + " failed, it is loaded untransformed"); + e.printStackTrace(); + final var failure = new IllegalClassFormatException("Mixin transformation failed for " + binaryName + ": " + e); + failure.initCause(e); + throw failure; + } + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/impl/mixin/SurfMixinAgent.java b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/impl/mixin/SurfMixinAgent.java new file mode 100644 index 000000000..b4d4b198e --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/impl/mixin/SurfMixinAgent.java @@ -0,0 +1,167 @@ +package dev.slne.surf.api.minestom.server.impl.mixin; + +import com.llamalad7.mixinextras.MixinExtrasBootstrap; +import java.io.IOException; +import java.lang.instrument.Instrumentation; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.CodeSource; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.stream.Collectors; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.spongepowered.asm.launch.MixinBootstrap; +import org.spongepowered.asm.mixin.MixinEnvironment; +import org.spongepowered.asm.mixin.Mixins; + +/** + * Installs Mixin into a surf Minestom server. + * + *

Called by the bootstrap agent as its first delegate agent: after the libraries are on the + * system class path, before any other agent or {@code main} runs. The transformer must exist + * before the first Minestom class is loaded, because classes loaded earlier are never transformed.

+ * + *

The mixin configurations come from the {@value #CONFIGS_ATTRIBUTE} manifest attribute, which + * the surf Gradle plugin writes from the installed features and the mixin configurations the + * server registers. Mixin can be installed once per JVM.

+ * + *

A required mixin whose target was loaded before the installation fails the installation. A + * transformation that fails later makes the JVM load the class untransformed; such classes are + * reported by {@link #failedTransformations()}.

+ */ +@NullMarked +public final class SurfMixinAgent { + + /** + * The manifest attribute listing the mixin configurations to apply, comma separated. + */ + public static final String CONFIGS_ATTRIBUTE = "Surf-Mixin-Configs"; + + private static volatile boolean installed; + + private static volatile @Nullable Throwable installationFailure; + + private SurfMixinAgent() { + } + + /** + * Installs Mixin with the mixin configurations listed in the manifest. + * + * @throws IllegalStateException when the manifest lists no mixin configuration or Mixin is + * already installed + */ + public static void agentmain(final @Nullable String args, final Instrumentation instrumentation) { + final List configs = configsFromManifest(); + if (configs.isEmpty()) { + throw new IllegalStateException("The server jar lists no mixin configurations in " + CONFIGS_ATTRIBUTE); + } + install(instrumentation, configs); + } + + /** + * Delegates to {@link #agentmain}. + */ + public static void premain(final @Nullable String args, final Instrumentation instrumentation) { + agentmain(args, instrumentation); + } + + /** + * Installs the Mixin transformer through {@code instrumentation} and applies {@code configs}. + * + *

Mixin cannot be initialised again after a partial installation, so a failed installation + * is final: {@link #isInstalled()} stays {@code false} and every later call fails with the + * original failure as cause.

+ * + * @throws IllegalStateException when Mixin is already installed or an earlier installation + * failed + */ + public static synchronized void install(final Instrumentation instrumentation, final Collection configs) { + if (installed) { + throw new IllegalStateException("Mixin is already installed"); + } + final @Nullable Throwable earlierFailure = installationFailure; + if (earlierFailure != null) { + throw new IllegalStateException("Mixin installation failed earlier and cannot be retried", earlierFailure); + } + + try { + final Set loaded = Arrays.stream(instrumentation.getAllLoadedClasses()) + .map(Class::getName) + .collect(Collectors.toUnmodifiableSet()); + + InstrumentationMixinService.setInstrumentation(instrumentation, loaded); + MixinBootstrap.init(); + MixinExtrasBootstrap.init(); + configs.forEach(Mixins::addConfiguration); + advancePhases(); + } catch (final Throwable failure) { + installationFailure = failure; + throw failure; + } + installed = true; + } + + /** + * Whether Mixin was installed completely into this JVM. + */ + public static boolean isInstalled() { + return installed; + } + + /** + * Binary names of the classes that were loaded untransformed because a mixin failed to apply. + */ + public static List failedTransformations() { + return InstrumentationMixinService.failures(); + } + + /** + * Moves Mixin through {@code INIT} to {@code DEFAULT}. Mixin selects its configurations on a + * phase change, which no launcher triggers here. + */ + private static void advancePhases() { + try { + final var gotoPhase = MixinEnvironment.class.getDeclaredMethod("gotoPhase", MixinEnvironment.Phase.class); + gotoPhase.setAccessible(true); + gotoPhase.invoke(null, MixinEnvironment.Phase.INIT); + gotoPhase.invoke(null, MixinEnvironment.Phase.DEFAULT); + } catch (final ReflectiveOperationException e) { + throw new IllegalStateException("Failed to advance the Mixin phases", e); + } + } + + private static List configsFromManifest() { + final @Nullable CodeSource codeSource = SurfMixinAgent.class.getProtectionDomain().getCodeSource(); + if (codeSource == null) { + return List.of(); + } + + try { + final Path jar = Path.of(codeSource.getLocation().toURI()); + if (!Files.isRegularFile(jar)) { + return List.of(); + } + + try (JarFile jarFile = new JarFile(jar.toFile())) { + final @Nullable Manifest manifest = jarFile.getManifest(); + final @Nullable String configs = manifest == null ? null : manifest.getMainAttributes().getValue(CONFIGS_ATTRIBUTE); + if (configs == null) { + return List.of(); + } + + return Arrays.stream(configs.split(",")) + .map(String::trim) + .filter(config -> !config.isEmpty()) + .toList(); + } + } catch (final IOException | URISyntaxException e) { + throw new IllegalStateException("Failed to read the mixin configurations from the server jar", e); + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/impl/mixin/duck/ConnectionManagerDuck.java b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/impl/mixin/duck/ConnectionManagerDuck.java new file mode 100644 index 000000000..e9ff72c06 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/impl/mixin/duck/ConnectionManagerDuck.java @@ -0,0 +1,16 @@ +package dev.slne.surf.api.minestom.server.impl.mixin.duck; + +import java.util.Set; +import net.minestom.server.entity.Player; +import org.jspecify.annotations.NullMarked; + +/** + * Exposes the private player sets of {@code ConnectionManager}, implemented by {@code ConnectionManagerMixin}. + */ +@NullMarked +public interface ConnectionManagerDuck { + + Set surf$configurationPlayers(); + + Set surf$keepAlivePlayers(); +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/impl/mixin/duck/EntityTrackerEntryDuck.java b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/impl/mixin/duck/EntityTrackerEntryDuck.java new file mode 100644 index 000000000..432237fc1 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/impl/mixin/duck/EntityTrackerEntryDuck.java @@ -0,0 +1,10 @@ +package dev.slne.surf.api.minestom.server.impl.mixin.duck; + +import net.minestom.server.coordinate.Point; +import org.jspecify.annotations.Nullable; + +public interface EntityTrackerEntryDuck { + + @Nullable + Point surf$lastPosition(); +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/CommandManagerMixin.java b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/CommandManagerMixin.java new file mode 100644 index 000000000..a3e14b129 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/CommandManagerMixin.java @@ -0,0 +1,67 @@ +package dev.slne.surf.api.minestom.server.mixin; + +import dev.slne.surf.api.minestom.server.impl.command.CommandAPIHook; +import net.minestom.server.command.CommandManager; +import net.minestom.server.command.CommandSender; +import net.minestom.server.command.builder.CommandResult; +import net.minestom.server.entity.Player; +import net.minestom.server.network.packet.server.play.DeclareCommandsPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * Routes commands registered through CommandAPI past Minestom's own command handling: name + * lookup, execution and the declare-commands packet. + */ +@Mixin(CommandManager.class) +abstract class CommandManagerMixin { + + @Inject( + method = "commandExists(Ljava/lang/String;)Z", + at = @At("HEAD"), + cancellable = true, + remap = false + ) + private void surf$commandApiOwnsName(final String commandName, final CallbackInfoReturnable cir) { + if (CommandAPIHook.owns(commandName)) { + cir.setReturnValue(true); + } + } + + /** + * Injected before {@code parseCommand} instead of at the method head so that + * {@code PlayerCommandEvent} listeners see the command first and can cancel or rewrite it. + */ + @Inject( + method = "execute(Lnet/minestom/server/command/CommandSender;Ljava/lang/String;)Lnet/minestom/server/command/builder/CommandResult;", + at = @At( + value = "INVOKE", + target = "Lnet/minestom/server/command/CommandManager;parseCommand(Lnet/minestom/server/command/CommandSender;Ljava/lang/String;)Lnet/minestom/server/command/CommandParser$Result;", + remap = false + ), + cancellable = true, + remap = false + ) + private void surf$dispatchCommandApi( + final CommandSender sender, + final String command, + final CallbackInfoReturnable cir + ) { + final CommandResult result = CommandAPIHook.execute(sender, command); + if (result != null) { + cir.setReturnValue(result); + } + } + + @Inject( + method = "createDeclareCommandsPacket(Lnet/minestom/server/entity/Player;)Lnet/minestom/server/network/packet/server/play/DeclareCommandsPacket;", + at = @At("RETURN"), + cancellable = true, + remap = false + ) + private void surf$declareCommandApi(final Player player, final CallbackInfoReturnable cir) { + cir.setReturnValue(CommandAPIHook.declare(cir.getReturnValue(), player)); + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/ConnectionManagerMixin.java b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/ConnectionManagerMixin.java new file mode 100644 index 000000000..9b11c41f3 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/ConnectionManagerMixin.java @@ -0,0 +1,40 @@ +package dev.slne.surf.api.minestom.server.mixin; + +import dev.slne.surf.api.minestom.server.impl.configuration.ConfigurationPhaseHook; +import dev.slne.surf.api.minestom.server.impl.mixin.duck.ConnectionManagerDuck; +import java.util.Set; +import net.minestom.server.entity.Player; +import net.minestom.server.network.ConnectionManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Redirects {@code doConfiguration} to {@link ConfigurationPhaseHook} and exposes the private + * player sets through {@link ConnectionManagerDuck}. + */ +@Mixin(ConnectionManager.class) +abstract class ConnectionManagerMixin implements ConnectionManagerDuck { + + @Accessor(value = "configurationPlayers", remap = false) + @Override + public abstract Set surf$configurationPlayers(); + + @Accessor(value = "keepAlivePlayers", remap = false) + @Override + public abstract Set surf$keepAlivePlayers(); + + @Inject( + method = "doConfiguration(Lnet/minestom/server/entity/Player;Z)V", + at = @At("HEAD"), + cancellable = true, + remap = false + ) + private void surf$runConfigurationPhase(final Player player, final boolean isFirstConfig, final CallbackInfo ci) { + if (ConfigurationPhaseHook.configure(this, player, isFirstConfig)) { + ci.cancel(); + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/EntityTickMixin.java b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/EntityTickMixin.java new file mode 100644 index 000000000..0cdbf123d --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/EntityTickMixin.java @@ -0,0 +1,54 @@ +package dev.slne.surf.api.minestom.server.mixin; + +import dev.slne.surf.api.minestom.server.impl.performance.EntityTickFilter; +import net.minestom.server.Tickable; +import net.minestom.server.entity.Entity; +import net.minestom.server.thread.TickThread; +import net.minestom.server.utils.chunk.ChunkUtils; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * Skips the tick of entities rejected by {@link EntityTickFilter#shouldTick}. + *

+ * The scheduler of a skipped entity is still processed under the conditions of {@code Entity#tick}, + * so tasks scheduled through {@code Entity#scheduler()} keep running. + */ +@SuppressWarnings("UnstableApiUsage") +@Mixin(TickThread.class) +abstract class EntityTickMixin { + + @Redirect( + method = "tick", + at = @At( + value = "INVOKE", + target = "Lnet/minestom/server/Tickable;tick(J)V", + remap = false + ), + remap = false + ) + private void surf$filterEntityTick(final Tickable tickable, final long time) { + if (tickable instanceof Entity entity && !EntityTickFilter.shouldTick(entity)) { + surf$tickScheduler(entity); + return; + } + + tickable.tick(time); + } + + @Unique + private static void surf$tickScheduler(final Entity entity) { + if (entity.getInstance() == null + || entity.isRemoved() + || !ChunkUtils.isLoaded(entity.getChunk())) { + return; + } + + entity.scheduler().processTick(); + if (!entity.isRemoved()) { + entity.scheduler().processTickEnd(); + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/EntityTrackerEntryMixin.java b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/EntityTrackerEntryMixin.java new file mode 100644 index 000000000..67bd22c55 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/EntityTrackerEntryMixin.java @@ -0,0 +1,14 @@ +package dev.slne.surf.api.minestom.server.mixin; + +import dev.slne.surf.api.minestom.server.impl.mixin.duck.EntityTrackerEntryDuck; +import net.minestom.server.coordinate.Point; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(targets = "net.minestom.server.instance.EntityTrackerImpl$EntityTrackerEntry", remap = false) +abstract class EntityTrackerEntryMixin implements EntityTrackerEntryDuck { + + @Accessor(value = "lastPosition", remap = false) + @Override + public abstract Point surf$lastPosition(); +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/EntityTrackerMixin.java b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/EntityTrackerMixin.java new file mode 100644 index 000000000..567bf6f5e --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/EntityTrackerMixin.java @@ -0,0 +1,103 @@ +package dev.slne.surf.api.minestom.server.mixin; + +import dev.slne.surf.api.minestom.server.impl.mixin.duck.EntityTrackerEntryDuck; +import dev.slne.surf.api.minestom.server.impl.performance.EntityViewerLookup; +import dev.slne.surf.api.minestom.server.impl.performance.EntityViewerLookup.EntityPositionResolver; +import java.util.Set; +import java.util.function.Consumer; +import net.minestom.server.coordinate.ChunkRange; +import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.Entity; +import net.minestom.server.instance.EntityTracker; +import net.minestom.server.instance.EntityTracker.Target; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import space.vectrix.flare.fastutil.Int2ObjectSyncMap; + +/** + * Speeds up {@code nearbyEntitiesByChunkRange}. + * + *

When {@link EntityViewerLookup#useDirectScan} accepts the entity count and range, the + * tracked entities are tested against the range directly instead of probing every chunk bucket + * ({@code (2r+1)^2} lookups, 4225 at range 32). Otherwise the bucket path runs with a rectangular + * loop in place of the spiral traversal. Range checks use the position recorded by the tracker, + * which preserves Minestom's chunk-boundary semantics.

+ */ +@Mixin(targets = "net.minestom.server.instance.EntityTrackerImpl", remap = false) +abstract class EntityTrackerMixin { + + @Shadow + @Final + private Int2ObjectSyncMap entriesByEntityId; + + @Unique + private EntityPositionResolver surf$positionResolver; + + @Inject( + method = "nearbyEntitiesByChunkRange", + at = @At("HEAD"), + cancellable = true, + remap = false, + require = 1 + ) + private void surf$directNearbyScan( + final Point point, + final int chunkRange, + final Target target, + final Consumer query, + final CallbackInfo ci + ) { + final Set candidates = ((EntityTracker) (Object) this).entities(target); + if (!EntityViewerLookup.useDirectScan(candidates.size(), chunkRange)) { + return; + } + + EntityViewerLookup.directScan( + candidates, + surf$resolver(), + point.chunkX(), + point.chunkZ(), + chunkRange, + query + ); + ci.cancel(); + } + + @Redirect( + method = "nearbyEntitiesByChunkRange", + at = @At( + value = "INVOKE", + target = "Lnet/minestom/server/coordinate/ChunkRange;chunksInRange(Lnet/minestom/server/coordinate/Point;ILnet/minestom/server/coordinate/ChunkRange$ChunkConsumer;)V", + remap = false + ), + remap = false, + require = 1 + ) + private void surf$rectangularChunkScan( + final Point point, + final int range, + final ChunkRange.ChunkConsumer consumer + ) { + EntityViewerLookup.chunksInRangeRectangular(point.chunkX(), point.chunkZ(), range, consumer); + } + + @Unique + private EntityPositionResolver surf$resolver() { + EntityPositionResolver resolver = this.surf$positionResolver; + if (resolver == null) { + final Int2ObjectSyncMap entries = this.entriesByEntityId; + resolver = entity -> { + final Object entry = entries.get(entity.getEntityId()); + return entry == null ? null : ((EntityTrackerEntryDuck) entry).surf$lastPosition(); + }; + this.surf$positionResolver = resolver; + } + return resolver; + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/LightingChunkSkylightMixin.java b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/LightingChunkSkylightMixin.java new file mode 100644 index 000000000..6e1ca46dc --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/LightingChunkSkylightMixin.java @@ -0,0 +1,38 @@ +package dev.slne.surf.api.minestom.server.mixin; + +import dev.slne.surf.api.minestom.server.impl.performance.SkylightSuppression; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.LightingChunk; +import net.minestom.server.instance.Section; +import net.minestom.server.instance.light.Light; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * Excludes sky light from {@code createLightData} in dimensions without sky light, see + * {@link SkylightSuppression}. + */ +@SuppressWarnings("UnstableApiUsage") +@Mixin(LightingChunk.class) +abstract class LightingChunkSkylightMixin { + + @Redirect( + method = "createLightData", + at = @At( + value = "INVOKE", + target = "Lnet/minestom/server/instance/Section;skyLight()Lnet/minestom/server/instance/light/Light;", + remap = false + ), + remap = false, + require = 3, + allow = 3 + ) + private Light surf$skipDisabledSkyLight(final Section section) { + final Chunk chunk = (Chunk) (Object) this; + return SkylightSuppression.sectionSkyLight( + chunk.getInstance().getCachedDimensionType().hasSkylight(), + section + ); + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/ServerMixin.java b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/ServerMixin.java new file mode 100644 index 000000000..6843f3e4c --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/java/dev/slne/surf/api/minestom/server/mixin/ServerMixin.java @@ -0,0 +1,134 @@ +package dev.slne.surf.api.minestom.server.mixin; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import java.io.IOException; +import java.net.Socket; +import java.util.Objects; +import net.minestom.server.MinecraftServer; +import net.minestom.server.network.packet.PacketParser; +import net.minestom.server.network.packet.client.ClientPacket; +import net.minestom.server.network.player.PlayerSocketConnection; +import net.minestom.server.network.socket.Server; +import net.minestom.server.property.ServerProperties; +import net.minestom.server.property.ServerProperty; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * Network adjustments to Minestom's socket server. + * + *

On Linux the socket buffer sizes are left to the operating system unless + * {@link ServerProperties#SOCKET_SEND_BUFFER_SIZE} or + * {@link ServerProperties#SOCKET_RECEIVE_BUFFER_SIZE} differ from their defaults. With + * {@code -Dsurf.network-debug=true}, failed socket reads and writes are logged before the + * exception propagates.

+ */ +@SuppressWarnings("UnstableApiUsage") +@Mixin(value = Server.class, remap = false) +abstract class ServerMixin { + + @Unique + private static final String SURF$NETWORK_DEBUG_PROPERTY = "surf.network-debug"; + + @Unique + private static final boolean SURF$LINUX = System.getProperty("os.name", "").startsWith("Linux"); + + @Unique + private static final boolean SURF$NETWORK_DEBUG = Boolean.getBoolean(SURF$NETWORK_DEBUG_PROPERTY); + + @WrapWithCondition( + method = "configureSocket", + at = @At( + value = "INVOKE", + target = "Ljava/net/Socket;setSendBufferSize(I)V" + ), + remap = false + ) + private static boolean surf$setSendBufferSize(final Socket socket, final int size) { + return !SURF$LINUX || surf$isConfigured(ServerProperties.SOCKET_SEND_BUFFER_SIZE); + } + + @WrapWithCondition( + method = "configureSocket", + at = @At( + value = "INVOKE", + target = "Ljava/net/Socket;setReceiveBufferSize(I)V" + ), + remap = false + ) + private static boolean surf$setReceiveBufferSize(final Socket socket, final int size) { + return !SURF$LINUX || surf$isConfigured(ServerProperties.SOCKET_RECEIVE_BUFFER_SIZE); + } + + @Redirect( + method = "playerReadLoop", + at = @At( + value = "INVOKE", + target = "Lnet/minestom/server/network/player/PlayerSocketConnection;read(Lnet/minestom/server/network/packet/PacketParser;)V" + ), + remap = false, + require = 1 + ) + private void surf$read( + final PlayerSocketConnection connection, + final PacketParser packetParser + ) throws IOException { + try { + connection.read(packetParser); + } catch (IOException | RuntimeException | Error throwable) { + surf$logSocketFailure("read", connection, throwable); + throw throwable; + } + } + + @Redirect( + method = "playerWriteLoop", + at = @At( + value = "INVOKE", + target = "Lnet/minestom/server/network/player/PlayerSocketConnection;flushSync()V", + ordinal = 0 + ), + remap = false, + require = 1 + ) + private void surf$flush(final PlayerSocketConnection connection) throws IOException { + try { + connection.flushSync(); + } catch (IOException | RuntimeException | Error throwable) { + surf$logSocketFailure("write", connection, throwable); + throw throwable; + } + } + + @Unique + private static void surf$logSocketFailure( + final String operation, + final PlayerSocketConnection connection, + final Throwable throwable + ) { + if (!SURF$NETWORK_DEBUG) { + return; + } + + MinecraftServer.LOGGER.warn( + "[NetworkDebug] Socket {} failed: identifier={}, loginUsername={}, remote={}, " + + "clientState={}, serverState={}, online={}, channelOpen={}", + operation, + connection.getIdentifier(), + connection.getLoginUsername(), + connection.getRemoteAddress(), + connection.getClientState(), + connection.getServerState(), + connection.isOnline(), + connection.getChannel().isOpen(), + throwable + ); + } + + @Unique + private static boolean surf$isConfigured(final ServerProperty property) { + return !Objects.equals(property.get(), property.defaultValue()); + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/SurfMinestomServer.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/SurfMinestomServer.kt new file mode 100644 index 000000000..73350e6bd --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/SurfMinestomServer.kt @@ -0,0 +1,224 @@ +package dev.slne.surf.api.minestom.server + +import dev.slne.surf.api.minestom.event.EventRegistrar +import dev.slne.surf.api.minestom.permission.PermissionProvider +import dev.slne.surf.api.minestom.server.configuration.ConfigurationPhaseBuilder +import dev.slne.surf.api.minestom.server.impl.SurfMinestomRuntime +import net.minestom.server.entity.EntityType +import net.minestom.server.event.Event +import net.minestom.server.event.EventNode + +@DslMarker +annotation class SurfMinestomServerDsl + +/** + * An optional part of the surf api, installed through [SurfMinestomServerBuilder.install]. + * + * Feature modules ship their feature together with an extension on [SurfMinestomServerBuilder], e.g. + * `withSignedChat()` or `withLuckPerms()`, so that adding the module as a dependency is enough to + * offer the feature. + * + * Features are loaded ordered by [loadPriority], ties in installation order, and disabled in + * reverse. A feature that throws in [enable] or [disable] is logged; the other features are not + * affected. + */ +interface SurfMinestomFeature { + + /** + * Identifies the feature; a feature can be installed only once. + */ + val id: String + + /** + * Features with a higher priority are loaded later and disabled earlier. + */ + val loadPriority: Int get() = 0 + + /** + * Called while [surfMinestomServer] runs, before the server starts. Register listeners, packet + * listeners and commands here. + */ + fun load(api: SurfMinestomServer) = Unit + + /** + * Called on the first tick after [surfMinestomServer] started the api. + */ + fun enable(api: SurfMinestomServer) = Unit + + /** + * Called when the server shuts down or [SurfMinestomServer.shutdown] is called, for every + * loaded feature, including one whose [enable] failed. + */ + fun disable(api: SurfMinestomServer) = Unit +} + +/** + * Configures the surf api. + * + * @see surfMinestomServer + */ +@SurfMinestomServerDsl +class SurfMinestomServerBuilder internal constructor() { + internal val features = linkedMapOf() + internal val registrars = mutableListOf() + internal var configurationPhase: ConfigurationPhaseBuilder? = null + internal var permissionProvider: PermissionProvider? = null + internal val disabledEntityTicks = mutableSetOf() + + /** + * The name of the event node the surf api registers its listeners on. It is added as a child + * of the global event handler. + */ + var eventNodeName: String = "surf-api-minestom" + + /** + * The number of players the server admits, or `null` for any number. + * + * @see dev.slne.surf.api.minestom.player.PlayerLimit + * @see dev.slne.surf.api.minestom.player.event.AsyncPlayerCountEvent + */ + var maxPlayers: Int? = null + + /** + * Whether to apply the settings surf recommends for a server behind a proxy, unless the server + * chose them itself: + * + * - `minestom.keep-alive-delay`: 2 seconds, so the proxy does not drop a player held in the + * configuration phase. The server chose it by setting it with `-D`. Applying it requires the + * server jar built by the surf Gradle plugin, whose bootstrap makes the property writable at + * runtime. + * - Packet compression off, since the connection to the proxy is local. The server chose it by + * changing the threshold from Minestom's default before [surfMinestomServer]. + */ + var recommendedServerProperties: Boolean = true + + /** + * Installs [feature]. + */ + fun install(feature: SurfMinestomFeature) { + require(features.putIfAbsent(feature.id, feature) == null) { + "The feature '${feature.id}' is already installed" + } + } + + /** + * Whether a feature with [id] is installed. + */ + fun isInstalled(id: String): Boolean = id in features + + /** + * Registers [registrars] on the event node of the surf api. + */ + fun listeners(vararg registrars: EventRegistrar) { + this.registrars += registrars + } + + /** + * Resolves permissions with [provider], e.g. for the requirements of CommandAPI commands. + * + * Features such as `withLuckPerms()` install their own provider; this one takes precedence. + */ + fun permissions(provider: PermissionProvider) { + permissionProvider = provider + } + + /** + * Stops entities of [types] from ticking, e.g. purely decorative entities in a lobby. + * + * Takes effect only when the server runs with the surf Mixin agent, i.e. from the server jar + * built by the surf Gradle plugin. + * + * Only the entity's own update, physics and tick events stop. Tasks on the entity's scheduler, + * such as `Entity#scheduleRemove` or `scheduler().scheduleNextTick`, keep running. + * + * @throws IllegalArgumentException for [EntityType.PLAYER] + */ + fun disableEntityTicking(vararg types: EntityType) { + disableEntityTicking(types.asIterable()) + } + + /** + * @see disableEntityTicking + */ + fun disableEntityTicking(types: Iterable) { + types.forEach { type -> + require(type != EntityType.PLAYER) { "Players have to tick" } + disabledEntityTicks += type + } + } + + /** + * Runs the configuration phase as an ordered list of steps that can each be replaced, removed + * or extended. + * + * Without this, Minestom runs its own configuration phase. + * + * @see ConfigurationPhaseBuilder + */ + fun withConfigurationPhase(block: ConfigurationPhaseBuilder.() -> Unit = {}) { + val builder = configurationPhase ?: ConfigurationPhaseBuilder().also { configurationPhase = it } + builder.block() + } +} + +/** + * The running surf api. + */ +interface SurfMinestomServer { + + /** + * The event node all surf listeners are registered on. + */ + val eventNode: EventNode + + /** + * The installed features, ordered by [SurfMinestomFeature.loadPriority], ties in installation order. + */ + val features: Collection + + /** + * The installed feature with [id], or `null` when it is not installed. + */ + fun feature(id: String): SurfMinestomFeature? + + /** + * Disables every feature and the surf api itself. + * + * Runs automatically when the server shuts down. + */ + fun shutdown() + + companion object { + /** + * The running surf api. + */ + val instance: SurfMinestomServer + get() = SurfMinestomRuntime.current() + } +} + +/** + * Starts the surf api on this Minestom server. + * + * Call it once, after `MinecraftServer.init()` and before `MinecraftServer#start`: + * + * ``` + * val server = MinecraftServer.init() + * + * surfMinestomServer { + * withConfigurationPhase() + * withSignedChat() + * withLuckPerms() + * withSpark() + * } + * + * server.start("0.0.0.0", 25565) + * ``` + * + * The CommandAPI is ready as soon as this returns, so commands can be registered right after it. + * + * @throws IllegalStateException when the server is not initialized, the api is already running, + * or Mixin is not installed (tests excepted) + */ +fun surfMinestomServer(block: SurfMinestomServerBuilder.() -> Unit = {}): SurfMinestomServer = + SurfMinestomRuntime.start(SurfMinestomServerBuilder().apply(block)) diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/configuration/ConfigurationPhaseBuilder.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/configuration/ConfigurationPhaseBuilder.kt new file mode 100644 index 000000000..6fba74cc1 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/configuration/ConfigurationPhaseBuilder.kt @@ -0,0 +1,148 @@ +package dev.slne.surf.api.minestom.server.configuration + +import dev.slne.surf.api.minestom.server.SurfMinestomServerDsl +import net.minestom.server.entity.Player +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +/** + * Configures the steps the configuration phase runs, in order. + * + * Starts out with the steps vanilla runs, see [ConfigurationTasks]. Each of them can be replaced, + * removed or surrounded by steps of your own: + * + * ``` + * withConfigurationPhase { + * codeOfConduct { player -> texts[player.locale] } + * replace(ConfigurationTasks.RESOURCE_PACK) { context -> myPacks.await(context.player) } + * after(ConfigurationTasks.AWAIT_SETTINGS, ConfigurationTaskId("load_profile")) { context -> + * profiles.load(context.player.uuid) + * } + * } + * ``` + */ +@SurfMinestomServerDsl +class ConfigurationPhaseBuilder internal constructor() { + private val tasks = linkedMapOf( + ConfigurationTasks.PLAYER_COUNT to ConfigurationTasks.playerCount, + ConfigurationTasks.BRAND to ConfigurationTasks.brand, + ConfigurationTasks.CONFIGURATION_EVENT to ConfigurationTasks.configurationEvent, + ConfigurationTasks.SPAWN_LOCATION to ConfigurationTasks.spawnLocation, + ConfigurationTasks.ENABLED_FEATURES to ConfigurationTasks.enabledFeatures, + ConfigurationTasks.SYNCHRONIZE_REGISTRIES to ConfigurationTasks.synchronizeRegistries, + ConfigurationTasks.AWAIT_SETTINGS to ConfigurationTasks.AwaitSettings(), + ConfigurationTasks.RESOURCE_PACK to ConfigurationTasks.resourcePack, + ConfigurationTasks.JOIN_WORLD to ConfigurationTasks.joinWorld, + ) + + /** + * The IDs of the configured steps, in the order they run. + */ + val ids: List get() = tasks.keys.toList() + + /** + * Replaces the step [id] with [task], keeping its position. + */ + fun replace(id: ConfigurationTaskId, task: ConfigurationTask) { + require(id in tasks) { "There is no configuration step '$id' to replace" } + tasks[id] = task + } + + /** + * Removes the step [id]. + */ + fun remove(id: ConfigurationTaskId) { + require(tasks.remove(id) != null) { "There is no configuration step '$id' to remove" } + } + + /** + * Runs [task] as [id] right before the step [anchor]. + */ + fun before(anchor: ConfigurationTaskId, id: ConfigurationTaskId, task: ConfigurationTask) = + insert(anchor, id, task, offset = 0) + + /** + * Runs [task] as [id] right after the step [anchor]. + */ + fun after(anchor: ConfigurationTaskId, id: ConfigurationTaskId, task: ConfigurationTask) = + insert(anchor, id, task, offset = 1) + + /** + * Runs [task] as [id] before every other step. + */ + fun first(id: ConfigurationTaskId, task: ConfigurationTask) { + requireUnknown(id) + val entries = tasks.entries.map { it.toPair() } + tasks.clear() + tasks[id] = task + tasks.putAll(entries) + } + + /** + * Runs [task] as [id] after every other step so far. + * + * [ConfigurationTasks.JOIN_WORLD] hands the player over to the play phase, so a step added + * after it no longer runs during the configuration phase. + */ + fun last(id: ConfigurationTaskId, task: ConfigurationTask) { + requireUnknown(id) + tasks[id] = task + } + + /** + * Waits up to [timeout] for the client's settings. + */ + fun awaitSettings(timeout: Duration = 2.seconds) { + replace(ConfigurationTasks.AWAIT_SETTINGS, ConfigurationTasks.AwaitSettings(timeout)) + } + + /** + * Shows the code of conduct [text] returns for a player and keeps the player in the + * configuration phase until they have accepted it. A player for whom [text] returns `null` is + * let through. + * + * The step runs after the settings arrived, so that [Player.getLocale] is the client's own. + */ + fun codeOfConduct(text: (Player) -> String?) { + val task = ConfigurationTasks.CodeOfConduct(text) + if (ConfigurationTasks.CODE_OF_CONDUCT in tasks) { + replace(ConfigurationTasks.CODE_OF_CONDUCT, task) + return + } + + val anchor = when { + ConfigurationTasks.AWAIT_SETTINGS in tasks -> ConfigurationTasks.AWAIT_SETTINGS + ConfigurationTasks.SYNCHRONIZE_REGISTRIES in tasks -> ConfigurationTasks.SYNCHRONIZE_REGISTRIES + else -> null + } + + if (anchor != null) { + after(anchor, ConfigurationTasks.CODE_OF_CONDUCT, task) + } else { + first(ConfigurationTasks.CODE_OF_CONDUCT, task) + } + } + + private fun insert( + anchor: ConfigurationTaskId, + id: ConfigurationTaskId, + task: ConfigurationTask, + offset: Int, + ) { + requireUnknown(id) + val entries = tasks.entries.map { it.toPair() }.toMutableList() + val index = entries.indexOfFirst { it.first == anchor } + require(index != -1) { "There is no configuration step '$anchor'" } + + entries.add(index + offset, id to task) + tasks.clear() + tasks.putAll(entries) + } + + private fun requireUnknown(id: ConfigurationTaskId) { + require(id !in tasks) { "The configuration step '$id' already exists, use replace instead" } + } + + internal fun build(): List> = + tasks.entries.map { it.toPair() } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/configuration/ConfigurationTask.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/configuration/ConfigurationTask.kt new file mode 100644 index 000000000..a798f2f1f --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/configuration/ConfigurationTask.kt @@ -0,0 +1,78 @@ +package dev.slne.surf.api.minestom.server.configuration + +import net.minestom.server.entity.Player +import net.minestom.server.event.Event +import net.minestom.server.event.EventNode +import net.minestom.server.event.player.AsyncPlayerConfigurationEvent +import net.minestom.server.instance.Instance +import org.jetbrains.annotations.Blocking + +/** + * One step of the configuration phase. + * + * Steps run in order on the player's virtual configuration thread, so a step may block until the + * client has answered it. + * + * @see ConfigurationPhaseBuilder + */ +fun interface ConfigurationTask { + + /** + * Runs the step and blocks until the client has answered it. + */ + @Blocking + fun run(context: ConfigurationContext) + + /** + * Registers whatever listeners the step needs, once, when the configuration phase is + * installed. + */ + fun install(node: EventNode) = Unit +} + +/** + * Identifies a step of the configuration phase, so that it can be replaced, removed or used as an + * anchor for further steps. + * + * @see ConfigurationTasks for the IDs of the default steps + */ +@JvmInline +value class ConfigurationTaskId(val value: String) { + override fun toString(): String = value +} + +/** + * Everything the steps of one run of the configuration phase share. + */ +class ConfigurationContext internal constructor( + val player: Player, + + /** + * Whether this is the player's first configuration phase, rather than a return from the play + * phase. + */ + val isFirstConfig: Boolean, +) { + private var configurationEvent: AsyncPlayerConfigurationEvent? = null + + /** + * The configuration event, available once the [ConfigurationTasks.CONFIGURATION_EVENT] step + * has run. + */ + var event: AsyncPlayerConfigurationEvent + get() = checkNotNull(configurationEvent) { + "The configuration event has not been called yet; " + + "this step has to run after ${ConfigurationTasks.CONFIGURATION_EVENT}" + } + set(value) { + configurationEvent = value + } + + /** + * The instance the player will spawn in, as chosen in the configuration [event]. + */ + val spawningInstance: Instance + get() = requireNotNull(event.spawningInstance) { + "You need to specify a spawning instance in the AsyncPlayerConfigurationEvent" + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/configuration/ConfigurationTasks.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/configuration/ConfigurationTasks.kt new file mode 100644 index 000000000..014cf1a64 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/configuration/ConfigurationTasks.kt @@ -0,0 +1,258 @@ +package dev.slne.surf.api.minestom.server.configuration + +import dev.slne.surf.api.minestom.extension.PacketListenerManager +import dev.slne.surf.api.minestom.extension.addListener +import dev.slne.surf.api.minestom.server.impl.configuration.ConfigurationPhase +import dev.slne.surf.api.minestom.server.impl.player.PlayerEvents +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import net.minestom.server.MinecraftServer +import net.minestom.server.entity.Player +import net.minestom.server.event.Event +import net.minestom.server.event.EventDispatcher +import net.minestom.server.event.EventNode +import net.minestom.server.event.player.AsyncPlayerConfigurationEvent +import net.minestom.server.event.player.PlayerDisconnectEvent +import net.minestom.server.event.player.PlayerSettingsChangeEvent +import net.minestom.server.network.packet.client.configuration.ClientAcceptCodeOfConductPacket +import net.minestom.server.network.packet.server.common.PluginMessagePacket +import net.minestom.server.network.packet.server.configuration.* +import net.minestom.server.property.ServerProperties +import net.minestom.server.registry.Registries +import java.util.concurrent.* +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +/** + * The steps of the configuration phase, in the order vanilla runs them. + * + * Every step can be replaced or removed through [ConfigurationPhaseBuilder]. + */ +object ConfigurationTasks { + private val LOGGER = ComponentLogger.logger() + + /** + * Calls [dev.slne.surf.api.minestom.player.event.AsyncPlayerCountEvent] and kicks a refused + * player. Only in the first configuration phase. + */ + val PLAYER_COUNT = ConfigurationTaskId("player_count") + + /** + * Sends the server brand. + */ + val BRAND = ConfigurationTaskId("brand") + + /** + * Calls [AsyncPlayerConfigurationEvent] and stores it in the [ConfigurationContext]. + */ + val CONFIGURATION_EVENT = ConfigurationTaskId("configuration_event") + + /** + * Calls [dev.slne.surf.api.minestom.player.event.AsyncPlayerSpawnLocationEvent] and applies + * the location it chose. Only in the first configuration phase. + */ + val SPAWN_LOCATION = ConfigurationTaskId("spawn_location") + + /** + * Sends the feature flags the configuration event collected and clears the chat if the event + * requests it. + */ + val ENABLED_FEATURES = ConfigurationTaskId("enabled_features") + + /** + * Negotiates the known packs and sends the registry data. + */ + val SYNCHRONIZE_REGISTRIES = ConfigurationTaskId("synchronize_registries") + + /** + * Waits until the client's settings, and with them its locale, have arrived. + */ + val AWAIT_SETTINGS = ConfigurationTaskId("await_settings") + + /** + * Shows the code of conduct and waits until the player accepts it. Not one of the default + * steps. + */ + val CODE_OF_CONDUCT = ConfigurationTaskId("code_of_conduct") + + /** + * Waits for the resource packs Minestom queued for the player. + */ + val RESOURCE_PACK = ConfigurationTaskId("resource_pack") + + /** + * Hands the player over to the play phase. Always the last step. + */ + val JOIN_WORLD = ConfigurationTaskId("join_world") + + /** + * Stops the keep-alive packets the configuration phase sends. + * + * A step that hands the player over to the play phase in place of [JOIN_WORLD] has to call + * this right before it sends the finish configuration packet. + */ + fun stopKeepAlive(player: Player) = ConfigurationPhase.stopKeepAlive(player) + + val playerCount = ConfigurationTask { context -> + if (context.isFirstConfig) PlayerEvents.admit(context.player) + } + + val spawnLocation = ConfigurationTask { context -> + if (context.isFirstConfig) PlayerEvents.chooseSpawnLocation(context.event) + } + + val brand = ConfigurationTask { context -> + context.player.sendPacket(PluginMessagePacket.brandPacket(MinecraftServer.getBrandName())) + } + + val configurationEvent = ConfigurationTask { context -> + val event = AsyncPlayerConfigurationEvent(context.player, context.isFirstConfig) + EventDispatcher.call(event) + context.event = event + } + + val enabledFeatures = ConfigurationTask { context -> + val event = context.event + + context.player.sendPacket( + UpdateEnabledFeaturesPacket(event.featureFlags.map { it.name() }) + ) + + if (event.willClearChat()) { + context.player.sendPacket(ResetChatPacket()) + } + } + + @Suppress("UnstableApiUsage") + val synchronizeRegistries = ConfigurationTask { context -> + if (!context.event.willSendRegistryData()) return@ConfigurationTask + + val player = context.player + val response = player.playerConnection + .requestKnownPacks(listOf(SelectKnownPacksPacket.MINECRAFT_CORE)) + + val knownPacks = try { + response.get(ServerProperties.KNOWN_PACKS_RESPONSE_TIMEOUT.get(), TimeUnit.MILLISECONDS) + } catch (_: TimeoutException) { + LOGGER.warn("Player {} failed to respond to known packs query", player.username) + player.playerConnection.disconnect() + return@ConfigurationTask + } + + val excludeVanilla = knownPacks.contains(SelectKnownPacksPacket.MINECRAFT_CORE) + + player.sendPackets( + Registries.registryDataPackets(MinecraftServer.getRegistries(), excludeVanilla) + ) + MinecraftServer.getConnectionManager().sendRegistryTags(player) + } + + @Suppress("UnstableApiUsage") + val resourcePack = ConfigurationTask { context -> + context.player.resourcePackFuture?.join() + } + + @Suppress("UnstableApiUsage") + val joinWorld = ConfigurationTask { context -> + val player = context.player + + stopKeepAlive(player) + player.setPendingOptions(context.spawningInstance, context.event.isHardcore) + player.sendPacket(FinishConfigurationPacket()) + } + + /** + * Waits until the client's settings have arrived, using the defaults after [timeout]. + */ + class AwaitSettings(private val timeout: Duration = 2.seconds) : ConfigurationTask { + private val received = ConcurrentHashMap>() + + override fun run(context: ConfigurationContext) { + try { + settingsFuture(context.player) + .get(timeout.inWholeMilliseconds, TimeUnit.MILLISECONDS) + } catch (_: TimeoutException) { + LOGGER.warn( + "Player {} did not send their settings, continuing with the defaults", + context.player.username + ) + } + } + + override fun install(node: EventNode) { + node.addListener { event -> + settingsFuture(event.player).complete(Unit) + } + node.addListener { event -> + received.remove(event.player)?.complete(Unit) + } + } + + private fun settingsFuture(player: Player): CompletableFuture = + received.computeIfAbsent(player) { CompletableFuture() } + } + + /** + * Sends the code of conduct [text] returns for a player and keeps the player in the + * configuration phase until they accept it. + * + * Only shown in the first configuration phase. A player for whom [text] returns `null` is let + * through. + */ + class CodeOfConduct(private val text: (Player) -> String?) : ConfigurationTask { + private val pending = ConcurrentHashMap>() + + override fun run(context: ConfigurationContext) { + if (!context.isFirstConfig) return + + val player = context.player + val codeOfConduct = text(player) ?: return + + val future = CompletableFuture() + val pendingFuture = pending.putIfAbsent(player, future) ?: future + + if (!player.isOnline) { + pending.remove(player, future) + return + } + + if (pendingFuture === future) { + player.sendPacket(CodeOfConductPacket(codeOfConduct)) + } else { + LOGGER.warn( + "Configuration ran again for {} while its code of conduct was still pending", + player.username + ) + } + + try { + pendingFuture.join() + } catch (_: CancellationException) { + LOGGER.info("Player {} left before accepting the code of conduct", player.username) + player.playerConnection.disconnect() + } + } + + override fun install(node: EventNode) { + node.addListener { event -> + pending.remove(event.player)?.cancel(true) + } + PacketListenerManager.setConfigurationListener( + ClientAcceptCodeOfConductPacket::class.java, + ::handleAccept, + ) + } + + private fun handleAccept( + @Suppress("unused") packet: ClientAcceptCodeOfConductPacket, + player: Player, + ) { + val future = pending.remove(player) + if (future == null) { + LOGGER.warn("Player {} accepted a code of conduct that was never sent", player.username) + player.playerConnection.disconnect() + return + } + future.complete(Unit) + } + } +} diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/impl/MinestomSpongeConfigSerializer.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/MinestomSpongeConfigSerializer.kt similarity index 83% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/impl/MinestomSpongeConfigSerializer.kt rename to surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/MinestomSpongeConfigSerializer.kt index c2fae9d57..a339ba623 100644 --- a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/impl/MinestomSpongeConfigSerializer.kt +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/MinestomSpongeConfigSerializer.kt @@ -1,4 +1,4 @@ -package dev.slne.surf.api.minestom.impl +package dev.slne.surf.api.minestom.server.impl import com.google.auto.service.AutoService import dev.slne.surf.api.core.config.serializer.SpongeConfigSerializers diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/RecommendedServerProperties.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/RecommendedServerProperties.kt new file mode 100644 index 000000000..622ab7594 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/RecommendedServerProperties.kt @@ -0,0 +1,72 @@ +package dev.slne.surf.api.minestom.server.impl + +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import net.minestom.server.MinecraftServer +import net.minestom.server.property.ServerProperties +import net.minestom.server.property.ServerProperty + +/** + * The settings applied by `SurfMinestomServerBuilder.recommendedServerProperties`. + * + * Values chosen explicitly are kept: a property set with `-D`, or a compression threshold that + * differs from Minestom's default. Minestom properties are writable at runtime only when + * `.mutable` is set before they are first read, which the bootstrap of the server jar does; + * otherwise a warning is logged and the value stays. + */ +internal object RecommendedServerProperties { + private val LOGGER = ComponentLogger.logger() + + /** + * Keep-alive interval in milliseconds. Velocity drops a backend that stays silent for its + * `login-timeout` (6 seconds by default) before the player reaches the play phase, while the + * configuration phase may hold a player indefinitely. + */ + private const val KEEP_ALIVE_DELAY_MILLIS = 2_000L + + /** + * The connection to the proxy is local, so compression only costs CPU. + */ + private const val COMPRESSION_THRESHOLD = 0 + + /** + * Minestom's default compression threshold; a different value means the server chose one. + */ + private const val MINESTOM_COMPRESSION_THRESHOLD = 256 + + fun apply() { + apply(ServerProperties.KEEP_ALIVE_DELAY, KEEP_ALIVE_DELAY_MILLIS) + applyCompressionThreshold() + } + + private fun applyCompressionThreshold() { + if (MinecraftServer.process()?.isAlive == true) { + LOGGER.warn("The server already started, packet compression stays as it is") + return + } + + val current = MinecraftServer.getCompressionThreshold() + if (current != MINESTOM_COMPRESSION_THRESHOLD) { + LOGGER.info("The compression threshold is set to {}, keeping it", current) + return + } + + MinecraftServer.setCompressionThreshold(COMPRESSION_THRESHOLD) + LOGGER.info("Disabled packet compression") + } + + private fun apply(property: ServerProperty, value: T) { + val name = property.name() + when { + System.getProperty(name) != null -> + LOGGER.info("{} is set to {} with -D{}, keeping it", name, property.get(), name) + + !property.writable() -> + LOGGER.warn("{} cannot be changed at runtime, set it with -D{}={}", name, name, value) + + else -> { + property.set(value) + LOGGER.info("Set {} to {}", name, value) + } + } + } +} diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/impl/SurfMinestomApiImpl.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/SurfMinestomApiImpl.kt similarity index 96% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/impl/SurfMinestomApiImpl.kt rename to surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/SurfMinestomApiImpl.kt index b99c954f3..0e3bde72f 100644 --- a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/impl/SurfMinestomApiImpl.kt +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/SurfMinestomApiImpl.kt @@ -1,4 +1,4 @@ -package dev.slne.surf.api.minestom.impl +package dev.slne.surf.api.minestom.server.impl import com.google.auto.service.AutoService import dev.slne.surf.api.core.SurfApiCore diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/impl/SurfMinestomInstance.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/SurfMinestomInstance.kt similarity index 56% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/impl/SurfMinestomInstance.kt rename to surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/SurfMinestomInstance.kt index 521ddb5a1..9c8d556db 100644 --- a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/impl/SurfMinestomInstance.kt +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/SurfMinestomInstance.kt @@ -1,12 +1,9 @@ -package dev.slne.surf.api.minestom.impl +package dev.slne.surf.api.minestom.server.impl -import com.google.inject.Inject -import com.google.inject.Singleton import dev.slne.surf.api.core.server.CoreInstance -import dev.slne.surf.api.minestom.impl.inventory.framework.MinestomInventoryLoader +import dev.slne.surf.api.minestom.server.impl.inventory.framework.MinestomInventoryLoader -@Singleton -internal class SurfMinestomInstance @Inject constructor( +internal class SurfMinestomInstance( private val inventoryLoader: MinestomInventoryLoader ) : CoreInstance() { @@ -19,4 +16,4 @@ internal class SurfMinestomInstance @Inject constructor( super.onDisable() inventoryLoader.disable() } -} \ No newline at end of file +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/SurfMinestomRuntime.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/SurfMinestomRuntime.kt new file mode 100644 index 000000000..27381b56e --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/SurfMinestomRuntime.kt @@ -0,0 +1,243 @@ +package dev.slne.surf.api.minestom.server.impl + +import com.github.retrooper.packetevents.PacketEvents +import dev.slne.surf.api.core.extensions.packetEvents +import dev.slne.surf.api.core.server.packet.NoopPacketEvents +import dev.slne.surf.api.minestom.extension.GlobalEventHandler +import dev.slne.surf.api.minestom.extension.SchedulerManager +import dev.slne.surf.api.minestom.permission.MinestomPermissions +import dev.slne.surf.api.minestom.player.PlayerLimit +import dev.slne.surf.api.minestom.server.SurfMinestomFeature +import dev.slne.surf.api.minestom.server.SurfMinestomServer +import dev.slne.surf.api.minestom.server.SurfMinestomServerBuilder +import dev.slne.surf.api.minestom.server.impl.command.MinestomCommandAPIService +import dev.slne.surf.api.minestom.server.impl.command.SignedCommandArguments +import dev.slne.surf.api.minestom.server.impl.configuration.ConfigurationPhase +import dev.slne.surf.api.minestom.server.impl.dialog.DialogCallbackListener +import dev.slne.surf.api.minestom.server.impl.inventory.framework.MinestomInventoryLoader +import dev.slne.surf.api.minestom.server.impl.mixin.SurfMixinAgent +import dev.slne.surf.api.minestom.server.impl.performance.EntityTickFilter +import dev.slne.surf.api.minestom.server.impl.player.PlayerEvents +import dev.slne.surf.api.minestom.server.impl.player.PlayerFlightListener +import kotlinx.coroutines.runBlocking +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import net.minestom.server.MinecraftServer +import net.minestom.server.event.Event +import net.minestom.server.event.EventNode +import net.minestom.server.property.ServerProperties +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference + +/** + * Lifecycle of the surf api on a Minestom server; at most one runtime exists per JVM. + * + * [start] runs the load phase synchronously: it installs the surf listeners and services and + * calls `load` on each feature in `loadPriority` order. Features are enabled on the next tick, and + * a feature whose `enable` throws is logged and skipped. [shutdown] is idempotent and calls + * `disable` on every loaded feature in reverse order, including ones that failed to enable. + * + * A failing [start] rolls back what it installed with the same teardown as [shutdown] and rethrows + * the failure. A teardown step that throws is logged and does not skip the following steps. + */ +internal class SurfMinestomRuntime private constructor( + private val builder: SurfMinestomServerBuilder, +) : SurfMinestomServer { + override val eventNode: EventNode = EventNode.all(builder.eventNodeName) + override val features: Collection = + builder.features.values.sortedBy(SurfMinestomFeature::loadPriority) + + private val inventoryLoader = MinestomInventoryLoader() + private val instance = SurfMinestomInstance(inventoryLoader) + private val commandApi = MinestomCommandAPIService() + + private val loaded = mutableListOf() + + private val running = AtomicBoolean(false) + + private var packetEventsPrepared = false + + override fun feature(id: String): SurfMinestomFeature? = builder.features[id] + + private fun load() { + if (builder.recommendedServerProperties) RecommendedServerProperties.apply() + preparePacketEvents() + runBlocking { + instance.bootstrap() + instance.onLoad() + } + + GlobalEventHandler.addChild(eventNode) + + inventoryLoader.register(eventNode) + DialogCallbackListener.register(eventNode) + PlayerFlightListener.install() + PlayerLimit.maxPlayers = builder.maxPlayers + EntityTickFilter.configure(builder.disabledEntityTicks) + PlayerEvents.install(eventNode, configurationPhase = builder.configurationPhase != null) + commandApi.start(eventNode) + + builder.configurationPhase?.let { phase -> + ConfigurationPhase.install( + phase.build(), + eventNode + ) + } + builder.registrars.forEach { registrar -> registrar.register(eventNode) } + + for (feature in features) { + LOGGER.info("Loading feature {}", feature.id) + feature.load(this) + loaded += feature + } + + // Installed after the features, so the server's provider takes precedence over one a feature + // installed + builder.permissionProvider?.let { provider -> MinestomPermissions.installProvider(provider) } + running.set(true) + + SchedulerManager.scheduleNextTick(::enable) + SchedulerManager.buildShutdownTask(::shutdown) + } + + private fun enable() { + if (!running.get()) return + + try { + runBlocking { instance.onEnable() } + } catch (failure: Throwable) { + LOGGER.error("Failed to enable the core listeners and the inventory framework", failure) + } + reportFailedMixins() + + for (feature in features) { + try { + feature.enable(this) + } catch (failure: Throwable) { + LOGGER.error("Failed to enable feature {}", feature.id, failure) + } + } + } + + override fun shutdown() { + if (!running.compareAndSet(true, false)) return + teardown() + } + + /** + * Undoes everything [load] installed and releases [current]. + * + * Every step is a no-op for an install step that never ran, so this also rolls back a partial + * [load]. A failing step is logged and does not skip the following ones. + * + * @return the first step failure with the later ones attached as suppressed exceptions, or + * `null` when every step succeeded + */ + private fun teardown(): Throwable? { + running.set(false) + + var failure: Throwable? = null + fun step(name: String, action: () -> Unit) { + try { + action() + } catch (stepFailure: Throwable) { + LOGGER.error("Failed to {} while shutting down the surf api", name, stepFailure) + val first = failure + if (first == null) failure = stepFailure else first.addSuppressed(stepFailure) + } + } + + try { + val toDisable = loaded.toList() + loaded.clear() + for (feature in toDisable.asReversed()) { + step("disable feature ${feature.id}") { feature.disable(this) } + } + + if (builder.configurationPhase != null) { + step("uninstall the configuration phase") { ConfigurationPhase.uninstall() } + } + step("stop the command api") { commandApi.stop() } + step("uninstall the flight listener") { PlayerFlightListener.uninstall() } + step("reset the entity tick filter") { EntityTickFilter.configure(emptySet()) } + step("uninstall the player events") { PlayerEvents.uninstall() } + step("remove the event node") { GlobalEventHandler.removeChild(eventNode) } + + if (packetEventsPrepared) { + step("disable the core instance") { runBlocking { instance.onDisable() } } + step("terminate packetevents") { packetEvents.terminate() } + } + + step("reset the permission provider") { MinestomPermissions.installProvider(null) } + step("reset the unsigned argument factory") { SignedCommandArguments.resetUnsignedFactory() } + } finally { + current.compareAndSet(this, null) + } + return failure + } + + private fun preparePacketEvents() { + PacketEvents.setAPI(NoopPacketEvents()) + packetEventsPrepared = true + packetEvents.load() + packetEvents.init() + } + + private fun reportFailedMixins() { + if (!SurfMixinAgent.isInstalled()) return + + val failed = SurfMixinAgent.failedTransformations() + if (failed.isNotEmpty()) { + LOGGER.error("Mixins failed to apply to {}, the surf api does not work correctly", failed) + } + } + + companion object { + private val LOGGER = ComponentLogger.logger() + + /** + * Fails startup when Mixin is not installed or a class failed to transform, since the + * CommandAPI, the configuration phase and the performance fixes depend on the mixins. + * Only tests ([ServerProperties.INSIDE_TEST]) may run without Mixin. Detects transformation + * failures that happened before the call. + */ + private fun checkMixins() { + if (!SurfMixinAgent.isInstalled()) { + check(ServerProperties.INSIDE_TEST.get()) { + "Mixin is not installed; start the server from the jar built by the surf Gradle plugin" + } + LOGGER.warn("Mixin is not installed, the surf mixins do not apply") + return + } + + val failed = SurfMixinAgent.failedTransformations() + check(failed.isEmpty()) { "Mixins failed to apply to $failed, see the log above" } + } + + private val current = AtomicReference() + + fun current(): SurfMinestomServer = checkNotNull(current.get()) { + "The surf api has not been started yet, call surfMinestomServer { } first" + } + + fun start(builder: SurfMinestomServerBuilder): SurfMinestomServer { + checkNotNull(MinecraftServer.process()) { + "The surf api needs an initialized server, call MinecraftServer.init() first" + } + + checkMixins() + + val runtime = SurfMinestomRuntime(builder) + check(current.compareAndSet(null, runtime)) { "The surf api is already running" } + + try { + runtime.load() + } catch (failure: Throwable) { + runtime.teardown()?.let(failure::addSuppressed) + current.compareAndSet(runtime, null) + throw failure + } + + return runtime + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/ArgumentSupport.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/ArgumentSupport.kt new file mode 100644 index 000000000..21d63afe2 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/ArgumentSupport.kt @@ -0,0 +1,13 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import dev.slne.surf.api.minestom.command.argument.ArgumentKind +import dev.slne.surf.api.minestom.command.argument.SuggestionMode + +internal fun SuggestionMode<*>.includesBuiltIns(): Boolean = + this is SuggestionMode.Include<*> || this is SuggestionMode.IncludeSafe<*> + +internal tailrec fun ArgumentKind<*>.baseKind(): ArgumentKind<*> = when (this) { + is ArgumentKind.Custom<*, *> -> base.kind.baseKind() + is ArgumentKind.List<*> -> element.kind.baseKind() + else -> this +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandAPIHook.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandAPIHook.kt new file mode 100644 index 000000000..40d381a6e --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandAPIHook.kt @@ -0,0 +1,77 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import com.mojang.brigadier.exceptions.CommandSyntaxException +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.executor.GENERIC_COMMAND_FAILURE +import dev.slne.surf.api.minestom.server.impl.command.brigadier.reportSyntaxFailure +import dev.slne.surf.api.minestom.server.impl.command.brigadier.reportUnknownCommand +import net.minestom.server.MinecraftServer +import net.minestom.server.command.CommandSender +import net.minestom.server.command.builder.CommandResult +import net.minestom.server.entity.Player +import net.minestom.server.network.packet.server.play.DeclareCommandsPacket + +/** + * Static entry points through which `CommandManagerMixin` routes Minestom's command handling to the + * CommandAPI. + * + * Every function resolves the active platform on each call and falls back to Minestom's own + * behavior while none is active. + */ +object CommandAPIHook { + + /** + * Whether [name] is a command label owned by the CommandAPI. + */ + @JvmStatic + fun owns(name: String): Boolean = ownership()?.contains(name) == true + + /** + * Executes [input] through the Brigadier dispatcher when its command label is owned by the + * CommandAPI. + * + * Returns `null` for any other input, which the manager then parses itself. A syntax failure is + * reported to [sender] in vanilla's layout and yields [CommandResult.Type.INVALID_SYNTAX]. Any + * other [Throwable] is logged, answered with a generic failure message and yields + * [CommandResult.Type.CANCELLED]. + */ + @JvmStatic + fun execute(sender: CommandSender, input: String): CommandResult? { + val ownership = ownership() ?: return null + if (ownership.findInput(input) == null) return null + + val command = input.removePrefix("/") + + return try { + CommandAPI.execute(sender, command) + CommandResult.of(CommandResult.Type.SUCCESS, command) + } catch (failure: CommandSyntaxException) { + reportSyntaxFailure(sender, command, failure) + CommandResult.of(CommandResult.Type.INVALID_SYNTAX, command) + } catch (failure: Throwable) { + MinecraftServer.LOGGER.error("Command '{}' failed for {}", command, sender, failure) + sender.sendMessage(GENERIC_COMMAND_FAILURE) + CommandResult.of(CommandResult.Type.CANCELLED, command) + } + } + + /** + * Returns [packet] with the CommandAPI's commands merged in for [player], or [packet] unchanged + * while no platform is active. + */ + @JvmStatic + fun declare(packet: DeclareCommandsPacket, player: Player): DeclareCommandsPacket = + MinestomCommandAPIPlatform.activeMerger()?.merge(packet, player) ?: packet + + /** + * Reports an input that no command registry resolved, in vanilla's layout. + * + * Installed as the manager's unknown command callback by [MinestomCommandAPIService]. + */ + @JvmStatic + fun reportUnknown(sender: CommandSender, input: String) = + reportUnknownCommand(sender, input.removePrefix("/")) + + private fun ownership(): MinestomCommandOwnership? = + MinestomCommandAPIPlatform.activeOwnership() +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandAPITranslations.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandAPITranslations.kt new file mode 100644 index 000000000..8173f29a2 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandAPITranslations.kt @@ -0,0 +1,42 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import net.kyori.adventure.key.Key +import net.kyori.adventure.translation.GlobalTranslator +import net.kyori.adventure.translation.TranslationStore +import java.text.MessageFormat +import java.util.* + +/** + * English fallbacks for the vanilla command translation keys used by the failure reports. + * + * Players receive the keys untouched and render them client-side; the fallbacks serve senders that + * are rendered on the server, such as the console. The translation source is added to + * [GlobalTranslator] once and never removed. + */ +object CommandAPITranslations { + private val STORE_KEY = Key.key("surf", "commandapi") + + @Volatile + private var registered = false + + fun register() { + if (registered) return + registered = true + + val store = TranslationStore.messageFormat(STORE_KEY) + store.defaultLocale(Locale.US) + + store.registerAll( + Locale.US, + mapOf( + "command.unknown.command" to + MessageFormat("Unknown or incomplete command. See below for error"), + "command.unknown.argument" to MessageFormat("Incorrect argument for command"), + "command.context.here" to MessageFormat("<--[HERE]"), + ), + ) + + GlobalTranslator.translator().addSource(store) + } +} + diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomCommandAPIPlatform.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomCommandAPIPlatform.kt new file mode 100644 index 000000000..fbef9aeda --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomCommandAPIPlatform.kt @@ -0,0 +1,195 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import com.mojang.brigadier.CommandDispatcher +import dev.slne.surf.api.minestom.command.CommandDefinition +import dev.slne.surf.api.minestom.command.RegisteredCommand +import dev.slne.surf.api.minestom.command.internal.CommandAPIPlatform +import dev.slne.surf.api.minestom.coroutine.minestomAsyncScope +import dev.slne.surf.api.minestom.server.impl.command.brigadier.BrigadierCommandTree +import dev.slne.surf.api.minestom.server.impl.command.brigadier.DeclareCommandsMerger +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import kotlinx.coroutines.CoroutineScope +import net.minestom.server.command.CommandManager +import net.minestom.server.command.CommandSender +import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +/** + * Registers CommandAPI commands in the Brigadier dispatcher, which owns their parsing and dispatch. + * + * Commands are not registered with Minestom's [CommandManager]; the manager reaches them through + * [CommandAPIHook] from `CommandManagerMixin`. The platform queries the manager only at + * registration, so that a label already used by a Minestom command is rejected instead of + * shadowed. + * + * Registration state is guarded by a lock, and a failed registration is rolled back completely. + * Constructing a platform makes it the active one that the accessors in the companion expose; + * closing it deactivates it. A platform that is not installed afterwards is discarded with + * [discard], which also reactivates the platform that was active before. + */ +internal class MinestomCommandAPIPlatform( + private val commandManager: CommandManager, + private val ownership: MinestomCommandOwnership, + suggestionScope: () -> CoroutineScope = { minestomAsyncScope }, + private val compiler: MinestomCommandCompiler = MinestomCommandCompiler(), + private val tree: BrigadierCommandTree = BrigadierCommandTree(suggestionScope = suggestionScope), +) : CommandAPIPlatform, AutoCloseable { + + private val lock = ReentrantLock() + private val registrations = ObjectOpenHashSet() + private val merger = DeclareCommandsMerger(tree) + + private var closed = false + + init { + installed.set(this) + } + + /** + * Registers [definition] under its name, its aliases and, when [namespace] is set, their + * `namespace:label` forms. + * + * Definitions that cannot be declared to a client are rejected by [MinestomCommandCompiler]. + * + * @throws IllegalStateException if the platform is closed or a label is already taken + */ + override fun register( + definition: CommandDefinition, + namespace: String? + ): RegisteredCommand = lock.withLock { + check(!closed) { "The Minestom CommandAPI platform is closed" } + + val compiled = compiler.compile(definition, namespace) + compiled.names.forEach { name -> + check(!ownership.contains(name) && commandManager.getCommand(name) == null) { + "Command name '$name' is already registered" + } + } + + var treeRegistered = false + try { + tree.register(definition.name, compiled.names, definition) + treeRegistered = true + + ownership.claim(compiled.names, compiled) + registrations += compiled + + PluginCommands.track(compiled.registration.name) + } catch (failure: Throwable) { + if (treeRegistered) { + try { + detach(compiled) + } catch (rollbackFailure: Throwable) { + failure.addSuppressed(rollbackFailure) + } + } else { + ownership.release(compiled) + } + throw failure + } + return compiled.registration + } + + /** + * Executes [input] with the current dispatcher; Brigadier failures propagate to the caller. + */ + override fun execute(sender: CommandSender, input: String): Int = + tree.dispatcher.execute(input, sender) + + /** + * Unregisters the command that owns the label [name]. + * + * Returns `false` when no command of this platform owns it. + */ + override fun unregister(name: String): Boolean = lock.withLock { + val compiled = ownership.find(name) ?: return false + if (compiled !in registrations) return false + detach(compiled) + return true + } + + /** + * Unregisters every command and rejects further registrations. + * + * Every command is detached even when some fail; the first failure is rethrown with the later + * ones attached as suppressed exceptions. + */ + override fun close(): Unit = lock.withLock { + if (closed) return + closed = true + installed.compareAndSet(this, null) + + var failure: Throwable? = null + val registrationsSnapshot = ObjectArrayList(registrations) + registrationsSnapshot.forEach { compiled -> + try { + detach(compiled) + } catch (currentFailure: Throwable) { + if (failure == null) { + failure = currentFailure + } else { + failure.addSuppressed(currentFailure) + } + } + } + + registrations.clear() + failure?.let { throw it } + } + + private fun detach(compiled: CompiledRegistration) { + check(lock.isHeldByCurrentThread) { "Platform state lock must be held while detaching a command" } + + try { + PluginCommands.forget(compiled.registration.name) + tree.unregister(compiled.registration.name) + } finally { + registrations.remove(compiled) + ownership.release(compiled) + } + } + + internal companion object { + private val installed = AtomicReference() + + /** + * The active platform, or `null` while none is active. + */ + fun active(): MinestomCommandAPIPlatform? = installed.get() + + /** + * Closes [platform] and makes [previous], the platform that was active before [platform] was + * constructed, the active one again. + * + * A failure of [AutoCloseable.close] propagates after the reactivation. + */ + fun discard(platform: MinestomCommandAPIPlatform, previous: MinestomCommandAPIPlatform?) { + try { + platform.close() + } finally { + installed.compareAndSet(null, previous) + } + } + + /** + * The merger of the active platform, or `null` while none is active. + * + * The active platform is the most recently constructed one, until it is closed. The + * accessors serve [CommandAPIHook] and the suggestion listener, which cannot be handed the + * instance. + */ + fun activeMerger(): DeclareCommandsMerger? = installed.get()?.merger + + /** + * The dispatcher of the active platform, or `null` while none is active. + */ + fun activeDispatcher(): CommandDispatcher? = installed.get()?.tree?.dispatcher + + /** + * The name registry of the active platform, or `null` while none is active. + */ + fun activeOwnership(): MinestomCommandOwnership? = installed.get()?.ownership + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomCommandAPIService.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomCommandAPIService.kt new file mode 100644 index 000000000..836dd9498 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomCommandAPIService.kt @@ -0,0 +1,73 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import com.mojang.brigadier.exceptions.CommandSyntaxException +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.extension.CommandManager +import net.minestom.server.event.Event +import net.minestom.server.event.EventNode +import net.minestom.server.utils.callback.CommandCallback + +/** + * Installs and removes the CommandAPI on Minestom. + * + * Dispatch and declaration merging depend on `CommandManagerMixin`, so the server must run with the + * surf Mixin agent. [start] and [stop] are not thread-safe and belong to the server lifecycle. + */ +internal class MinestomCommandAPIService { + private val ownership = MinestomCommandOwnership() + private var platform: MinestomCommandAPIPlatform? = null + private var previousUnknownCommandCallback: CommandCallback? = null + + /** + * Installs the platform, registers tab completion on [node] and replaces the manager's unknown + * command callback. + * + * @throws IllegalStateException if the service is already started + */ + fun start(node: EventNode) { + check(platform == null) { "Minestom CommandAPI is already installed" } + + CommandAPITranslations.register() + + // Process-wide, as in vanilla: failed parses while typing do not need a stack trace + CommandSyntaxException.ENABLE_COMMAND_STACK_TRACES = false + + val previousActive = MinestomCommandAPIPlatform.active() + val installed = MinestomCommandAPIPlatform(CommandManager, ownership) + try { + CommandAPI.installPlatform(installed) + } catch (failure: Throwable) { + try { + MinestomCommandAPIPlatform.discard(installed, previousActive) + } catch (discardFailure: Throwable) { + failure.addSuppressed(discardFailure) + } + throw failure + } + platform = installed + + previousUnknownCommandCallback = CommandManager.unknownCommandCallback + MinestomSuggestionListener(ownership).register(node) + + CommandManager.unknownCommandCallback = CommandCallback(CommandAPIHook::reportUnknown) + } + + /** + * Closes the platform and restores the unknown command callback that [start] replaced. + * + * Does nothing when the service is not started. + */ + fun stop() { + val installed = platform ?: return + platform = null + + CommandManager.unknownCommandCallback = previousUnknownCommandCallback + previousUnknownCommandCallback = null + + try { + installed.close() + } finally { + CommandAPI.uninstallPlatform(installed) + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomCommandCompiler.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomCommandCompiler.kt new file mode 100644 index 000000000..8bea56bf2 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomCommandCompiler.kt @@ -0,0 +1,91 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import dev.slne.surf.api.minestom.command.CommandDefinition +import dev.slne.surf.api.minestom.command.RegisteredCommand +import dev.slne.surf.api.minestom.command.argument.ArgumentKind +import dev.slne.surf.api.minestom.command.argument.SuggestionMode +import dev.slne.surf.api.minestom.command.exception.CommandValidationException +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet +import java.util.* + +/** + * A compiled command: the lowercase labels it occupies and the handle returned to its registrant. + */ +internal data class CompiledRegistration( + val names: Set, + val registration: RegisteredCommand, +) + +/** + * Derives the labels a [CommandDefinition] occupies and validates the parts of it that cannot be + * declared to a client. + */ +internal class MinestomCommandCompiler { + + /** + * Compiles [definition] into its labels: the name and aliases, plus their `namespace:label` + * forms when [namespace] is not `null`. Labels are lowercased and ordered by declaration. + * + * @throws CommandValidationException if an argument uses a suggestion mode its kind cannot carry + */ + fun compile(definition: CommandDefinition, namespace: String?): CompiledRegistration { + validateSuggestionModes(definition) + + val names = ObjectLinkedOpenHashSet() + val baseNames = ObjectArrayList(definition.aliases.size + 1) + + baseNames += normalize(definition.name) + definition.aliases.forEach { alias -> baseNames += normalize(alias) } + + names.addAll(baseNames) + if (namespace != null) { + baseNames.forEach { name -> names += normalize("$namespace:$name") } + } + + return CompiledRegistration( + names = names, + registration = RegisteredCommand(definition.name, definition.aliases, namespace), + ) + } + + /** + * Rejects suggestion modes on argument kinds that cannot carry them. + * + * Literal, command, position (3D, 2D and block) and rotation arguments accept no custom + * suggestions. An entity type argument accepts only the replacing modes, because the native set + * of summonable entity types is not available to merge extra entries into. + */ + private fun validateSuggestionModes(definition: CommandDefinition) { + definition.paths.forEach { path -> + path.arguments.forEach { argument -> + if (argument.suggestions == SuggestionMode.BuiltIns) return@forEach + + val baseKind = argument.kind.baseKind() + if (baseKind == ArgumentKind.EntityType && argument.suggestions.includesBuiltIns()) { + throw CommandValidationException( + "EntityType argument '${argument.nodeName}' cannot include custom suggestions " + + "because Minestom does not expose the native summonable entity set; " + + "use built-ins or replace suggestions", + ) + } + + val kindName = when (baseKind) { + is ArgumentKind.Literal -> "Literal" + ArgumentKind.Command -> "Command" + ArgumentKind.Position -> "Position" + ArgumentKind.Position2D -> "Position2D" + ArgumentKind.BlockPosition -> "BlockPosition" + ArgumentKind.Rotation -> "Rotation" + else -> return@forEach + } + + throw CommandValidationException( + "$kindName argument '${argument.nodeName}' cannot use custom suggestions", + ) + } + } + } + + private fun normalize(name: String): String = name.lowercase(Locale.ROOT) +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomCommandOwnership.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomCommandOwnership.kt new file mode 100644 index 000000000..dc89e722e --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomCommandOwnership.kt @@ -0,0 +1,50 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +internal class MinestomCommandOwnership { + private val registrations = ConcurrentHashMap() + + internal fun contains(name: String): Boolean = registrations.containsKey(normalize(name)) + + internal fun find(name: String): CompiledRegistration? = registrations[normalize(name)] + + internal fun findInput(text: String): CompiledRegistration? { + val root = text + .removePrefix("/") + .takeWhile { character -> !character.isWhitespace() } + + return root + .takeIf(String::isNotEmpty) + ?.let(::find) + } + + internal fun claim(names: Collection, registration: CompiledRegistration) { + val claimed = ObjectArrayList(names.size) + try { + names.forEach { rawName -> + val name = normalize(rawName) + val previous = registrations.putIfAbsent(name, registration) + + check(previous == null || previous === registration) { + "Command name '$name' is already owned" + } + + if (previous == null) claimed += name + } + } catch (failure: Throwable) { + claimed.forEach { name -> registrations.remove(name, registration) } + throw failure + } + } + + internal fun release(registration: CompiledRegistration) { + registration.names.forEach { rawName -> + registrations.remove(normalize(rawName), registration) + } + } + + private fun normalize(name: String): String = name.lowercase(Locale.ROOT) +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomConditions.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomConditions.kt new file mode 100644 index 000000000..57cb30b46 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomConditions.kt @@ -0,0 +1,52 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import dev.slne.surf.api.minestom.command.CommandPath +import dev.slne.surf.api.minestom.permission.hasPermission +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet +import net.minestom.server.command.CommandSender + +internal data class AccumulatedConditions( + val permissions: Set = emptySet(), + val requirements: List<(CommandSender) -> Boolean> = emptyList(), +) + +internal class MinestomConditions { + fun commonForPaths(paths: List): AccumulatedConditions { + val first = paths.firstOrNull() ?: return AccumulatedConditions() + val permissions = ObjectLinkedOpenHashSet(first.permissions.size) + + first.permissions.forEach { permission -> + if (paths.all { path -> permission in path.permissions }) { + permissions += permission + } + } + + val requirements = ObjectArrayList<(CommandSender) -> Boolean>(first.requirements.size) + first.requirements.forEach { requirement -> + if ( + paths.all { path -> + path.requirements.any { candidate -> + candidate === requirement + } + } + ) { + requirements += requirement + } + } + + return AccumulatedConditions( + permissions = permissions, + requirements = requirements, + ) + } + + fun canUse( + sender: CommandSender, + permissions: Set, + requirements: List<(CommandSender) -> Boolean>, + ): Boolean { + return permissions.all(sender::hasPermission) && + requirements.all { requirement -> requirement(sender) } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomSuggestionListener.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomSuggestionListener.kt new file mode 100644 index 000000000..a54e829a4 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomSuggestionListener.kt @@ -0,0 +1,136 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import com.mojang.brigadier.ParseResults +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import dev.slne.surf.api.minestom.command.exception.componentOrNull +import dev.slne.surf.api.minestom.event.EventRegistrar +import dev.slne.surf.api.minestom.extension.addListener +import net.minestom.server.MinecraftServer +import net.minestom.server.command.CommandSender +import net.minestom.server.event.Event +import net.minestom.server.event.EventNode +import net.minestom.server.event.player.PlayerPacketEvent +import net.minestom.server.network.packet.client.play.ClientTabCompletePacket +import net.minestom.server.network.packet.server.play.TabCompletePacket +import java.util.* +import java.util.concurrent.CompletableFuture + +/** + * Answers tab completion for CommandAPI-owned commands from the Brigadier dispatcher. + * + * A request is handled only when its first word is exactly an owned label and a platform is + * active; every other request is left to Minestom. A handled request is cancelled, and its + * response is sent when the suggestion future completes, on whichever thread completes it. A + * failed future is answered with an empty response. A response carries at most 1000 entries. + */ +internal class MinestomSuggestionListener( + private val ownership: MinestomCommandOwnership, +) : EventRegistrar { + + override fun register(node: EventNode) { + node.addListener(::handlePacket) + } + + private fun handlePacket(event: PlayerPacketEvent) { + val packet = event.packet as? ClientTabCompletePacket ?: return + + if (ownership.findInput(packet.text) == null) return + val dispatcher = MinestomCommandAPIPlatform.activeDispatcher() ?: return + + event.isCancelled = true + + val command = StringReader(packet.text) + if (command.canRead() && command.peek() == '/') { + command.skip() + } + + val suggestions = try { + suggestionsFor(dispatcher.parse(command, event.player)) + } catch (failure: Throwable) { + CompletableFuture.failedFuture(failure) + } + + suggestions.whenComplete { result, failure -> + val response = if (failure != null) { + MinecraftServer.LOGGER.error( + "Failed to compute tab completion for '{}'", + packet.text, + failure, + ) + TabCompletePacket(packet.transactionId, 0, 0, emptyList()) + } else { + val limited = limitTo(result) + TabCompletePacket( + packet.transactionId, + limited.range.start, + limited.range.length, + limited.list.map { suggestion -> + TabCompletePacket.Match( + suggestion.text, + suggestion.tooltip?.componentOrNull() + ) + } + ) + } + + event.player.sendPacket(response) + } + } + + companion object { + private const val MAX_COMMAND_SUGGESTIONS = 1000 + + /** + * Collects the suggestions of the children of the node before the cursor that the sender + * may use. + * + * Mirrors `CommandDispatcher.getCompletionSuggestions`, which lists every child without + * checking its requirement. The returned future completes exceptionally when any child's + * future does. + */ + private fun suggestionsFor( + parse: ParseResults, + ): CompletableFuture { + val context = parse.context + val cursor = parse.reader.totalLength + val suggestionContext = context.findSuggestionContext(cursor) + val start = minOf(suggestionContext.startPos, cursor) + + val fullInput = parse.reader.string + val truncatedInput = fullInput.substring(0, cursor) + val truncatedInputLowerCase = truncatedInput.lowercase(Locale.ROOT) + val source = context.source + + val futures = suggestionContext.parent.children + .filter { child -> child.canUse(source) } + .map { child -> + try { + child.listSuggestions( + context.build(truncatedInput), + SuggestionsBuilder(truncatedInput, truncatedInputLowerCase, start), + ) + } catch (_: CommandSyntaxException) { + Suggestions.empty() + } + } + + return CompletableFuture.allOf(*futures.toTypedArray()).thenApply { + Suggestions.merge(fullInput, futures.map { future -> future.join() }) + } + } + + private fun limitTo( + suggestions: Suggestions, + size: Int = MAX_COMMAND_SUGGESTIONS + ): Suggestions { + return if (suggestions.list.size <= size) { + suggestions + } else { + Suggestions(suggestions.range, suggestions.list.subList(0, size)) + } + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/PluginCommands.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/PluginCommands.kt new file mode 100644 index 000000000..6471f1afa --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/PluginCommands.kt @@ -0,0 +1,42 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.plugin.SurfMinestomPlugin +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import java.util.concurrent.ConcurrentHashMap + +/** + * Tracks the commands registered by each plugin so they can be unregistered when the plugin unloads. + * + * The map is concurrent; entries are added by the platform on registration and removed on + * unregistration. + */ +@InternalSurfApi +object PluginCommands { + private val owned = ConcurrentHashMap>() + + /** + * Assigns the command [name] to the plugin whose code is currently running, if any. + */ + internal fun track(name: String) { + val plugin = SurfMinestomPlugin.current() ?: return + owned.computeIfAbsent(plugin) { ConcurrentHashMap.newKeySet() } += name + } + + /** + * Removes the command [name] from every plugin's set. + */ + internal fun forget(name: String) { + owned.values.forEach { names -> names -= name } + } + + /** + * Unregisters every command registered by [plugin] and returns how many were unregistered. + * + * A command that fails to unregister is skipped and not counted. + */ + fun unregisterAll(plugin: SurfMinestomPlugin): Int { + val names = owned.remove(plugin) ?: return 0 + return names.count { name -> runCatching { CommandAPI.unregister(name) }.getOrDefault(false) } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/SignedCommandArguments.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/SignedCommandArguments.kt new file mode 100644 index 000000000..ce8bec351 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/SignedCommandArguments.kt @@ -0,0 +1,128 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import com.mojang.brigadier.ParseResults +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.context.CommandContextBuilder +import com.mojang.brigadier.tree.ArgumentCommandNode +import dev.slne.surf.api.minestom.server.impl.command.brigadier.SignedMessageArgumentType +import dev.slne.surf.api.minestom.server.impl.command.brigadier.SuggestingArgumentType +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap +import net.kyori.adventure.chat.SignedMessage +import net.minestom.server.command.CommandSender +import net.minestom.server.crypto.ArgumentSignatures +import java.util.* + +/** + * The signed messages of the command being dispatched on the current thread, keyed by argument + * node name. + * + * Signed chat is an optional feature. Messages are plain adventure [SignedMessage]s, and the + * signed chat feature installs [unsignedFactory] to create its own messages for unsigned input. + */ +@InternalSurfApi +object SignedCommandArguments { + private val current = ThreadLocal>() + + private val SYSTEM_FACTORY: (UUID?, String) -> SignedMessage = { _, content -> + SignedMessage.system(content, null) + } + + /** + * Creates the message a signed message argument reads when no signature was supplied for it. + * + * Receives the sender's UUID, or `null` for a non-player sender, and the argument's text. The + * default creates a system message. + */ + @Volatile + var unsignedFactory: (UUID?, String) -> SignedMessage = SYSTEM_FACTORY + + /** + * Runs [block] with [messages] visible to every signed message argument parsed on this thread + * within it. + * + * Calls may nest; the previous messages are restored when [block] returns or throws. + */ + fun withMessages(messages: Map, block: () -> T): T { + val previous = current.get() + current.set(messages) + + try { + return block() + } finally { + if (previous == null) current.remove() else current.set(previous) + } + } + + /** + * The message signed for the argument named [nodeName], or `null` when none was. + */ + fun find(nodeName: String): SignedMessage? = current.get()?.get(nodeName) + + /** + * Restores the default [unsignedFactory]. + */ + fun resetUnsignedFactory() { + unsignedFactory = SYSTEM_FACTORY + } +} + +/** + * The text a client signs for [input], keyed by the node name of the argument it is read from. + * + * Empty when no platform is active or the first word of [input] is not a CommandAPI label. + */ +@InternalSurfApi +fun signableCommandArguments( + sender: CommandSender, + input: String, +): Map { + val ownership = MinestomCommandAPIPlatform.activeOwnership() ?: return emptyMap() + val dispatcher = MinestomCommandAPIPlatform.activeDispatcher() ?: return emptyMap() + if (ownership.findInput(input) == null) return emptyMap() + + return signableArgumentsOf(dispatcher.parse(input.removePrefix("/"), sender)) +} + +/** + * The text of the signable arguments of [parse], keyed by node name, in the order they were read. + */ +internal fun signableArgumentsOf( + parse: ParseResults, +): Map { + val input = parse.reader.string + val values = Object2ObjectLinkedOpenHashMap() + + var context: CommandContextBuilder? = parse.context + while (context != null) { + context.nodes.forEach { parsed -> + val node = parsed.node + if (node is ArgumentCommandNode<*, *> && node.type.isSignable()) { + values[node.name] = input.substring(parsed.range.start, parsed.range.end) + } + } + + context = context.child + } + + return values +} + +/** + * Whether [signatures] names exactly the arguments in [values], no more and no fewer. + * + * Compares argument names only; the signatures themselves are not verified. + */ +@InternalSurfApi +fun signaturesCoverArguments( + signatures: ArgumentSignatures, + values: Map, +): Boolean { + val entries = signatures.entries() + return entries.size == values.size && entries.all { entry -> entry.name() in values } +} + +private fun ArgumentType<*>.isSignable(): Boolean = when (this) { + is SuggestingArgumentType -> delegate is SignedMessageArgumentType + else -> this is SignedMessageArgumentType +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/BrigadierCommandTree.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/BrigadierCommandTree.kt new file mode 100644 index 000000000..29c46e023 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/BrigadierCommandTree.kt @@ -0,0 +1,332 @@ +package dev.slne.surf.api.minestom.server.impl.command.brigadier + +import com.mojang.brigadier.Command +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.builder.ArgumentBuilder +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import com.mojang.brigadier.builder.RequiredArgumentBuilder +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.tree.ArgumentCommandNode +import com.mojang.brigadier.tree.LiteralCommandNode +import dev.slne.surf.api.minestom.command.CommandDefinition +import dev.slne.surf.api.minestom.command.CommandPath +import dev.slne.surf.api.minestom.command.argument.ArgumentDefinition +import dev.slne.surf.api.minestom.command.argument.ArgumentKind +import dev.slne.surf.api.minestom.command.executor.* +import dev.slne.surf.api.minestom.coroutine.minestomAsyncScope +import dev.slne.surf.api.minestom.server.impl.command.AccumulatedConditions +import dev.slne.surf.api.minestom.server.impl.command.MinestomConditions +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import kotlinx.coroutines.CoroutineScope +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.minestom.server.command.CommandSender +import net.minestom.server.command.ConsoleSender +import net.minestom.server.entity.Player + +private val NO_COMPATIBLE_EXECUTOR: Component = Component.text( + "Dieser Befehl kann von diesem Absendertyp nicht ausgeführt werden.", + NamedTextColor.RED, +) + +/** + * The argument counts at which this path can execute: its full length, or every length from the + * first optional argument onwards. + * + * The Brigadier tree and the tree declared to the client both derive their executable nodes from + * it and must agree. + */ +internal fun CommandPath.executableDepths(): IntRange { + val firstOptional = arguments.indexOfFirst(ArgumentDefinition<*>::optional) + return if (firstOptional == -1) { + arguments.size..arguments.size + } else { + firstOptional..arguments.size + } +} + +/** + * The Brigadier command tree that owns parsing and dispatch for every registered command. + * + * The dispatcher is rebuilt from the registered commands on the first read after the set of + * commands changed, because Brigadier cannot detach a node from a dispatcher. The nodes of a + * command are built at registration, so a definition that cannot be built fails there. [register] + * and [unregister] are not thread-safe and must be serialized by the caller; [dispatcher] and + * [registered] can be read from any thread. + */ +internal class BrigadierCommandTree( + private val conditions: MinestomConditions = MinestomConditions(), + private val suggestionScope: () -> CoroutineScope = { minestomAsyncScope }, +) { + private class Entry( + val labels: List, + val definition: CommandDefinition, + val nodes: List>, + ) + + private class Snapshot( + val dispatcher: CommandDispatcher, + val registered: List, CommandDefinition>>, + ) + + private val entries = Object2ObjectLinkedOpenHashMap() + + /** + * The snapshot of the current command set, or `null` when a change has not been built yet. + */ + @Volatile + private var snapshot: Snapshot? = null + + /** + * The current dispatcher, replaced as a whole after every change. + * + * A reference obtained earlier keeps the command set it was built from. + */ + val dispatcher: CommandDispatcher + get() = current().dispatcher + + /** + * Registers [definition] under [labels], replacing any command previously registered with the + * same [key]. + */ + fun register(key: String, labels: Collection, definition: CommandDefinition) { + val labelList = ObjectArrayList(labels) + val nodes = labelList.map { label -> literalFor(label, definition).build() } + + synchronized(entries) { + entries[key] = Entry(labelList, definition, nodes) + snapshot = null + } + } + + /** + * Removes the command registered under [key]; unknown keys are ignored. + */ + fun unregister(key: String) { + synchronized(entries) { + if (entries.remove(key) != null) snapshot = null + } + } + + /** + * Every registered command, paired with the labels it answers to. + */ + fun registered(): List, CommandDefinition>> = current().registered + + private fun current(): Snapshot = snapshot ?: synchronized(entries) { + snapshot ?: build().also { built -> snapshot = built } + } + + private fun build(): Snapshot { + val dispatcher = CommandDispatcher() + entries.values.forEach { entry -> + entry.nodes.forEach { node -> dispatcher.root.addChild(node) } + } + + return Snapshot( + dispatcher = dispatcher, + registered = entries.values.map { entry -> entry.labels to entry.definition }, + ) + } + + private fun literalFor( + label: String, + definition: CommandDefinition, + ): LiteralArgumentBuilder { + val root = LiteralArgumentBuilder.literal(label) + + // Conditions shared by all paths gate the root, hiding the command from senders who can + // use none of them. + val shared = sharedConditions(definition) + if (shared.permissions.isNotEmpty() || shared.requirements.isNotEmpty()) { + root.requires { sender -> + conditions.canUse(sender, shared.permissions, shared.requirements) + } + } + + definition.paths.forEach { path -> + val executableDepths = path.executableDepths() + if (0 in executableDepths) { + root.executes(executor(path, depth = 0)) + } + + val chain = chainFor(path, executableDepths) + if (chain != null) root.then(chain) + } + + return root + } + + private fun sharedConditions(definition: CommandDefinition): AccumulatedConditions = + conditions.commonForPaths(definition.paths) + + private fun chainFor( + path: CommandPath, + executableDepths: IntRange, + ): ArgumentBuilder? { + var current: ArgumentBuilder? = null + + for (index in path.arguments.indices.reversed()) { + val definition = path.arguments[index] + val builder = nodeFor(definition) + + if (index + 1 in executableDepths) { + builder.executes(executor(path, depth = index + 1)) + } + current?.let(builder::then) + + val permissions = definition.permissions + val requirements = definition.requirements + if (permissions.isNotEmpty() || requirements.isNotEmpty()) { + builder.requires { sender -> conditions.canUse(sender, permissions, requirements) } + } + + current = builder + } + + return current + } + + /** + * Builds the node for [definition]: a literal node for a literal kind, otherwise an argument + * node typed by the definition's raw type. + * + * Signed message kinds use [SignedMessageArgumentType]. A custom suggestion provider wraps the + * type in [SuggestingArgumentType]. + */ + private fun nodeFor(definition: ArgumentDefinition<*>): ArgumentBuilder { + val kind = definition.kind + if (kind is ArgumentKind.Literal) return LiteralArgumentBuilder.literal(kind.literal) + + @Suppress("UNCHECKED_CAST") + val rawType = if (kind is ArgumentKind.SignedMessage) { + SignedMessageArgumentType(definition.nodeName) as ArgumentType + } else { + definition.rawType as ArgumentType + } + val type = if (definition.hasCustomSuggestions()) { + SuggestingArgumentType(rawType, definition, suggestionScope) + } else { + rawType + } + + return RequiredArgumentBuilder.argument(definition.nodeName, type) + } + + /** + * The command executed when [path] is run with [depth] arguments. + * + * Returns `0` when the sender fails the path's conditions, no executor matches its type, or the + * arguments cannot be resolved or the executor throws; the last three cases are reported to the + * sender. A suspending executor is + * launched and reported as successful before it completes. + */ + private fun executor(path: CommandPath, depth: Int) = Command { context -> + val sender = context.source + if (!conditions.canUse(sender, path.permissions, path.requirements)) { + return@Command 0 + } + + val selected = selectExecutor(sender, path.executors) + ?: run { + sender.sendMessage(NO_COMPATIBLE_EXECUTOR) + return@Command 0 + } + + try { + val arguments = argumentsOf(context, path, depth) + val info = ExecutionInfo(sender, arguments, context.input) + + when (selected) { + is ExecutorDefinition.Normal -> { + selected.executor(info) + Command.SINGLE_SUCCESS + } + + is ExecutorDefinition.Resulting -> selected.executor(info) + + is ExecutorDefinition.Suspending -> { + launchCommandExecutor(selected, sender, arguments, context.input) + Command.SINGLE_SUCCESS + } + } + } catch (failure: Throwable) { + handleCommandExecutorFailure(sender, context.input, failure) + 0 + } + } + + /** + * Collects the non-literal arguments of [path] for a command executed with [depth] arguments. + * + * An argument at or beyond [depth] is marked absent and takes its declared default value. + */ + private fun argumentsOf( + context: CommandContext, + path: CommandPath, + depth: Int, + ): CommandArguments { + val parsedNames = ObjectArrayList(context.nodes.size) + context.nodes.forEach { parsed -> + val node = parsed.node + if (node is ArgumentCommandNode<*, *>) parsedNames += node.name + } + + val valued = path.arguments.filter { argument -> argument.kind !is ArgumentKind.Literal } + return CommandArguments.of( + valued.mapTo(ObjectArrayList(valued.size)) { definition -> + val name = definition.nodeName + val index = path.arguments.indexOf(definition) + val present = index < depth && name in parsedNames + + ParsedArgument( + name = name, + value = if (present) { + context.getArgument(name, Any::class.java) + } else { + definition.defaultValue?.invoke(context.source) + }, + raw = if (present) rawOf(context, name) else null, + present = present, + ) + }, + ) + } + + private fun rawOf(context: CommandContext, name: String): String? { + context.nodes.forEach { parsed -> + val node = parsed.node + if (node is ArgumentCommandNode<*, *> && node.name == name) { + return context.input.substring(parsed.range.start, parsed.range.end) + } + } + return null + } + + /** + * The executor for the type of [sender]: its own type first, then [ExecutorType.ANY]. + * + * Yields `null` when neither matches exactly one executor. + */ + private fun selectExecutor( + sender: CommandSender, + executors: List, + ): ExecutorDefinition? { + val preferred = when (sender) { + is Player -> PreferredExecutors.player + is ConsoleSender -> PreferredExecutors.console + else -> PreferredExecutors.any + } + return preferred.firstNotNullOfOrNull { type -> + executors.singleOrNull { executor -> executor.type == type } + } + } + + private object PreferredExecutors { + val player = listOf(ExecutorType.PLAYER, ExecutorType.ANY) + val console = listOf(ExecutorType.CONSOLE, ExecutorType.ANY) + val any = listOf(ExecutorType.ANY) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/CommandDispatchFailures.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/CommandDispatchFailures.kt new file mode 100644 index 000000000..0fe4ddd56 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/CommandDispatchFailures.kt @@ -0,0 +1,105 @@ +package dev.slne.surf.api.minestom.server.impl.command.brigadier + +import com.mojang.brigadier.exceptions.CommandSyntaxException +import dev.slne.surf.api.minestom.command.exception.componentOrNull +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.Component.text +import net.kyori.adventure.text.Component.translatable +import net.kyori.adventure.text.event.ClickEvent +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.TextDecoration +import net.kyori.adventure.translation.GlobalTranslator +import net.minestom.server.adventure.MinestomAdventure +import net.minestom.server.command.CommandSender +import net.minestom.server.entity.Player + +private const val CONTEXT_LENGTH = 10 +private const val HERE_KEY = "command.context.here" +private const val UNKNOWN_COMMAND_KEY = "command.unknown.command" +private const val UNKNOWN_ARGUMENT_KEY = "command.unknown.argument" + +/** + * Reports [failure] to [sender] in vanilla's layout: a heading with the failure message, then the + * input with the failing remainder underlined and followed by a marker. + * + * The heading reads "unknown command" for a failure at the start of [input] and "unknown argument" + * for one later on. + */ +internal fun reportSyntaxFailure( + sender: CommandSender, + input: String, + failure: CommandSyntaxException +) { + val raw = failure.rawMessage + val message = raw?.componentOrNull() + ?: raw?.string?.takeIf(String::isNotBlank)?.let { text -> + text(text, NamedTextColor.RED) + } + + report(sender, input, failure.cursor, message) +} + +/** + * Reports an input that no command registry resolved, marking the whole input as the failing part. + */ +internal fun reportUnknownCommand(sender: CommandSender, input: String) = + report(sender, input, cursor = 0, message = null) + +private fun report( + sender: CommandSender, + input: String, + cursor: Int, + message: Component?, +) { + val heading = if (cursor > 0) UNKNOWN_ARGUMENT_KEY else UNKNOWN_COMMAND_KEY + + val body = text().append(translatable(heading, NamedTextColor.RED)) + if (message != null) { + body.append(text(": ", NamedTextColor.RED)).append(message) + } + body.appendNewline().append(contextOf(input, cursor)) + + sender.sendTranslated(body.build()) +} + +/** + * The context line: up to [CONTEXT_LENGTH] characters before [cursor], the remainder underlined and + * a trailing marker. Clicking it suggests [input] as a command. + */ +private fun contextOf(input: String, cursor: Int): Component { + val clamped = cursor.coerceIn(0, input.length) + val context = text() + .color(NamedTextColor.GRAY) + .clickEvent(ClickEvent.suggestCommand("/$input")) + + if (clamped > CONTEXT_LENGTH) context.append(text("...")) + context.append( + text(input.substring((clamped - CONTEXT_LENGTH).coerceAtLeast(0), clamped)), + ) + if (clamped < input.length) { + context.append( + text(input.substring(clamped)) + .color(NamedTextColor.RED) + .decorate(TextDecoration.UNDERLINED), + ) + } + context.append( + translatable(HERE_KEY) + .color(NamedTextColor.RED) + .decorate(TextDecoration.ITALIC), + ) + + return context.build() +} + +/** + * Sends [message] with its translation keys resolved on the server, except to a player, whose + * client resolves them in its own language. + */ +private fun CommandSender.sendTranslated(message: Component) { + if (this is Player) { + sendMessage(message) + } else { + sendMessage(GlobalTranslator.render(message, MinestomAdventure.getDefaultLocale())) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/DeclareCommandsMerger.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/DeclareCommandsMerger.kt new file mode 100644 index 000000000..eed640843 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/DeclareCommandsMerger.kt @@ -0,0 +1,177 @@ +package dev.slne.surf.api.minestom.server.impl.command.brigadier + +import dev.slne.surf.api.minestom.command.CommandDefinition +import dev.slne.surf.api.minestom.command.argument.ArgumentDefinition +import dev.slne.surf.api.minestom.command.argument.ArgumentKind +import dev.slne.surf.api.minestom.server.impl.command.MinestomConditions +import it.unimi.dsi.fastutil.ints.IntArrayList +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import net.minestom.server.command.CommandSender +import net.minestom.server.network.packet.server.play.DeclareCommandsPacket + +/** + * Merges the CommandAPI's commands into the [DeclareCommandsPacket] Minestom builds for a player. + * + * Paths and arguments the sender fails the permissions or requirements of are omitted, and a + * command with no usable path is not declared at all. Holds no per-call state; safe to call from + * any thread. + */ +internal class DeclareCommandsMerger( + private val tree: BrigadierCommandTree, + private val conditions: MinestomConditions = MinestomConditions(), + private val declarations: NodeDeclarations = NodeDeclarations(), +) { + /** + * Returns a packet with a root literal for every label of every command [sender] can use, + * appended to the nodes of [original]. + * + * [original] is not modified and is returned as is when no command is registered. A root + * literal of [original] that carries a CommandAPI label is removed so the merged tree is the + * only declaration of that label. + */ + fun merge(original: DeclareCommandsPacket, sender: CommandSender): DeclareCommandsPacket { + val registered = tree.registered() + if (registered.isEmpty()) return original + + val roots = registered.mapNotNull { (labels, definition) -> + treeFor(definition, sender)?.let { branches -> labels to branches } + } + + val nodes = original.nodes().mapTo(ObjectArrayList(), ::copyOf) + val extraRootChildren = IntArrayList() + + // The removed nodes stay in the array, unreachable from the root. + val owned = registered.flatMapTo(HashSet()) { (labels, _) -> labels } + val originalRoot = nodes[original.rootIndex()] + originalRoot.children = originalRoot.children + .filterNot { child -> nodes[child].isLiteral() && nodes[child].name in owned } + .toIntArray() + + roots.forEach { (labels, branches) -> + labels.forEach { label -> + val node = DeclareCommandsPacket.Node() + node.flags = flagsOf(DeclareCommandsPacket.NodeType.LITERAL, branches.executable) + node.name = label + nodes += node + + val index = nodes.size - 1 + node.children = branches.children.values.map { child -> emit(child, nodes) }.toIntArray() + extraRootChildren.add(index) + } + } + + val root = nodes[original.rootIndex()] + root.children += extraRootChildren.toIntArray() + + return DeclareCommandsPacket(nodes, original.rootIndex()) + } + + private fun DeclareCommandsPacket.Node.isLiteral(): Boolean = + flags.toInt() and DeclareCommandsPacket.NODE_TYPE == + DeclareCommandsPacket.NodeType.LITERAL.ordinal + + private fun emit(source: PendingNode, nodes: MutableList): Int { + val node = DeclareCommandsPacket.Node() + val definition = source.definition + + if (definition == null) { + node.flags = flagsOf(DeclareCommandsPacket.NodeType.LITERAL, source.executable) + node.name = checkNotNull(source.literal) + } else { + val declaration = declarations.of(definition) + val suggestionsType = declaration.suggestionsType + node.flags = flagsOf( + DeclareCommandsPacket.NodeType.ARGUMENT, + source.executable, + suggestionsType != null, + ) + node.name = definition.nodeName + node.parser = declaration.parser + node.properties = declaration.properties + if (suggestionsType != null) node.suggestionsType = suggestionsType + } + + nodes += node + val index = nodes.size - 1 + node.children = source.children.values.map { child -> emit(child, nodes) }.toIntArray() + return index + } + + /** + * The flags of a node: its type, its executable bit and whether it carries a suggestions type. + * The packet writes a suggestions type only when its flag is set. + */ + private fun flagsOf( + type: DeclareCommandsPacket.NodeType, + executable: Boolean, + suggestionsType: Boolean = false, + ): Byte = DeclareCommandsPacket.getFlag(type, executable, false, suggestionsType) + + private fun copyOf(source: DeclareCommandsPacket.Node): DeclareCommandsPacket.Node { + val node = DeclareCommandsPacket.Node() + node.flags = source.flags + node.children = source.children.copyOf() + node.redirectedNode = source.redirectedNode + node.name = source.name + node.parser = source.parser + node.properties = source.properties + node.suggestionsType = source.suggestionsType + return node + } + + /** + * Folds the paths of [definition] that [sender] can use into one tree. + * + * Paths sharing a prefix share nodes, as Brigadier merges same-named children. A path is cut at + * the first argument the sender may not use and contributes only the nodes up to its last + * executable depth within the usable prefix. Returns `null` when the sender can execute no + * path. + */ + private fun treeFor(definition: CommandDefinition, sender: CommandSender): PendingNode? { + val root = PendingNode(literal = null, definition = null) + var any = false + + definition.paths.forEach { path -> + if (!conditions.canUse(sender, path.permissions, path.requirements)) return@forEach + + val depths = path.executableDepths() + val usable = path.arguments + .indexOfFirst { argument -> + !conditions.canUse(sender, argument.permissions, argument.requirements) + } + .takeUnless { index -> index == -1 } + ?: path.arguments.size + val last = minOf(usable, depths.last) + if (last < depths.first) return@forEach + + any = true + var current = root + if (0 in depths) root.executable = true + + for (index in 0 until last) { + current = current.child(path.arguments[index]) + if (index + 1 in depths) current.executable = true + } + } + + return root.takeIf { any } + } + + private class PendingNode(val literal: String?, val definition: ArgumentDefinition<*>?) { + var executable = false + val children = LinkedHashMap() + + fun child(argument: ArgumentDefinition<*>): PendingNode { + val kind = argument.kind + val literal = (kind as? ArgumentKind.Literal)?.literal + val key = if (literal != null) "literal:$literal" else "argument:${argument.nodeName}" + + return children.getOrPut(key) { + PendingNode( + literal = literal, + definition = if (literal == null) argument else null, + ) + } + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/NodeDeclaration.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/NodeDeclaration.kt new file mode 100644 index 000000000..d4a138e5e --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/NodeDeclaration.kt @@ -0,0 +1,172 @@ +package dev.slne.surf.api.minestom.server.impl.command.brigadier + +import dev.slne.surf.api.minestom.command.argument.ArgumentDefinition +import dev.slne.surf.api.minestom.command.argument.ArgumentKind +import dev.slne.surf.api.minestom.command.argument.SuggestionMode +import dev.slne.surf.api.minestom.server.impl.command.baseKind +import net.minestom.server.command.ArgumentParserType +import net.minestom.server.command.builder.arguments.Argument +import net.minestom.server.command.builder.arguments.ArgumentType +import net.minestom.server.command.builder.arguments.minecraft.SuggestionType +import net.minestom.server.registry.BuiltinRegistries + +/** + * How an argument node is declared to the client: the vanilla [parser], its serialized + * [properties] and the [suggestionsType] that selects the source of suggestions. + * + * A `null` [suggestionsType] leaves the suggestions to the client's parser. Equality compares + * [properties] by content. + */ +internal data class NodeDeclaration( + val parser: ArgumentParserType, + val properties: ByteArray?, + val suggestionsType: String? = null, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is NodeDeclaration) return false + return parser == other.parser && + properties.contentEquals(other.properties) && + suggestionsType == other.suggestionsType + } + + override fun hashCode(): Int { + var result = parser.hashCode() + result = 31 * result + properties.contentHashCode() + return 31 * result + suggestionsType.hashCode() + } +} + +/** + * Derives the [NodeDeclaration] of an argument definition. + * + * A declaration is protocol data only; the server parses input with the argument's own Brigadier + * type. For most kinds the parser and properties are read from an equivalent native Minestom + * argument, and the remaining kinds name their parser directly. Declarations are derived on every + * call and not cached, so no definition, and with it no plugin class loader, is retained. Safe to + * call from any thread. + */ +internal class NodeDeclarations { + + /** + * Returns the declaration of [definition]. + * + * The suggestions type is `ask_server` when the definition has a custom suggestion provider or + * its kind is one whose suggestions the client cannot produce itself, and the native one + * otherwise. + */ + fun of(definition: ArgumentDefinition<*>): NodeDeclaration { + val base = baseDeclarationOf(definition) + val server = definition.suggestions != SuggestionMode.BuiltIns || + computesSuggestions(definition.kind) + + return if (server) base.copy(suggestionsType = SuggestionType.ASK_SERVER.identifier) else base + } + + /** + * Whether suggestions for [kind] must come from the server. + * + * These kinds are declared as `ask_server` even without a suggestion provider. + */ + private fun computesSuggestions(kind: ArgumentKind<*>): Boolean = when (kind.baseKind()) { + ArgumentKind.Sound, + ArgumentKind.PotionEffect, + ArgumentKind.Biome, + ArgumentKind.Enchantment, + is ArgumentKind.Resource<*>, + ArgumentKind.Instance, + ArgumentKind.Player, + is ArgumentKind.Players, + ArgumentKind.Entity, + is ArgumentKind.Entities, + is ArgumentKind.MultiLiteral, + is ArgumentKind.Enum<*>, + -> true + + else -> false + } + + private fun baseDeclarationOf(definition: ArgumentDefinition<*>): NodeDeclaration = + when (val kind = definition.kind) { + // Declared directly because the native Minestom argument would name a different parser. + ArgumentKind.Rotation -> NodeDeclaration(ArgumentParserType.ROTATION, null) + ArgumentKind.Angle -> NodeDeclaration(ArgumentParserType.ANGLE, null) + ArgumentKind.Axis -> NodeDeclaration(ArgumentParserType.SWIZZLE, null) + ArgumentKind.GameMode -> NodeDeclaration(ArgumentParserType.GAMEMODE, null) + + ArgumentKind.SignedMessage -> NodeDeclaration(ArgumentParserType.MESSAGE, null) + + // A custom argument is declared as its base. + is ArgumentKind.Custom<*, *> -> baseDeclarationOf(kind.base) + + else -> nativeFor(definition).let { native -> + NodeDeclaration( + parser = native.parser(), + properties = native.nodeProperties(), + suggestionsType = native.suggestionType()?.identifier, + ) + } + } + + private fun nativeFor(definition: ArgumentDefinition<*>): Argument<*> { + val name = definition.nodeName + + return when (val kind = definition.kind) { + ArgumentKind.Boolean -> ArgumentType.Boolean(name) + is ArgumentKind.Integer -> ArgumentType.Integer(name).min(kind.min).max(kind.max) + is ArgumentKind.Long -> ArgumentType.Long(name).min(kind.min).max(kind.max) + is ArgumentKind.Float -> ArgumentType.Float(name).min(kind.min).max(kind.max) + is ArgumentKind.Double -> ArgumentType.Double(name).min(kind.min).max(kind.max) + + ArgumentKind.Word -> ArgumentType.Word(name) + ArgumentKind.Text -> ArgumentType.String(name) + ArgumentKind.GreedyString -> ArgumentType.StringArray(name) + is ArgumentKind.Literal -> ArgumentType.Literal(kind.literal) + // Declared as a word whose accepted values the client requests from the server. + is ArgumentKind.MultiLiteral -> ArgumentType.Word(name) + is ArgumentKind.Enum<*> -> ArgumentType.Word(name) + + ArgumentKind.Uuid -> ArgumentType.UUID(name) + ArgumentKind.IntegerRange -> ArgumentType.IntRange(name) + ArgumentKind.FloatRange -> ArgumentType.FloatRange(name) + // Minestom's command argument has no parser to declare; the raw type is a greedy string. + ArgumentKind.Command -> ArgumentType.StringArray(name) + + ArgumentKind.Player -> ArgumentType.Entity(name).singleEntity(true).onlyPlayers(true) + is ArgumentKind.Players -> ArgumentType.Entity(name).singleEntity(false).onlyPlayers(true) + ArgumentKind.Entity -> ArgumentType.Entity(name).singleEntity(true).onlyPlayers(false) + is ArgumentKind.Entities -> ArgumentType.Entity(name).singleEntity(false).onlyPlayers(false) + ArgumentKind.EntityType -> ArgumentType.EntityType(name) + + ArgumentKind.Position -> ArgumentType.RelativeVec3(name) + ArgumentKind.Position2D -> ArgumentType.RelativeVec2(name) + ArgumentKind.BlockPosition -> ArgumentType.RelativeBlockPosition(name) + + ArgumentKind.ResourceLocation -> ArgumentType.ResourceLocation(name) + ArgumentKind.Time -> ArgumentType.Time(name) + ArgumentKind.TeamColor -> ArgumentType.TeamColor(name) + ArgumentKind.Particle -> ArgumentType.Particle(name) + + // Sound and potion effect are declared as resource locations. + ArgumentKind.Sound -> ArgumentType.ResourceLocation(name) + ArgumentKind.PotionEffect -> ArgumentType.ResourceLocation(name) + ArgumentKind.Biome -> ArgumentType.Resource(name, BuiltinRegistries.BIOME.name()) + ArgumentKind.Enchantment -> + ArgumentType.Resource(name, BuiltinRegistries.ENCHANTMENT.name()) + + is ArgumentKind.Resource<*> -> ArgumentType.Resource(name, kind.identifier) + ArgumentKind.Instance -> ArgumentType.Word(name) + + // A list is declared as one greedy string that the server splits. + is ArgumentKind.List<*> -> ArgumentType.StringArray(name) + + ArgumentKind.Rotation, + ArgumentKind.Angle, + ArgumentKind.Axis, + ArgumentKind.GameMode, + ArgumentKind.SignedMessage, + is ArgumentKind.Custom<*, *>, + -> error("Kind ${definition.kind} declares its parser directly") + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/SignedMessageArgumentType.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/SignedMessageArgumentType.kt new file mode 100644 index 000000000..b433ce1d2 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/SignedMessageArgumentType.kt @@ -0,0 +1,32 @@ +package dev.slne.surf.api.minestom.server.impl.command.brigadier + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import dev.slne.surf.api.minestom.server.impl.command.SignedCommandArguments +import net.kyori.adventure.chat.SignedMessage +import net.minestom.server.command.CommandSender +import net.minestom.server.entity.Player + +/** + * Consumes the rest of the command line as a [SignedMessage]. + * + * The result is the message supplied for [nodeName] through [SignedCommandArguments], or an unsigned + * one created by [SignedCommandArguments.unsignedFactory] when none is in scope. + */ +internal class SignedMessageArgumentType( + private val nodeName: String, +) : ArgumentType { + + override fun parse(reader: StringReader): SignedMessage = read(reader, sender = null) + + override fun parse(reader: StringReader, source: S): SignedMessage = + read(reader, source as? CommandSender) + + private fun read(reader: StringReader, sender: CommandSender?): SignedMessage { + val content = reader.remaining + reader.cursor = reader.totalLength + + return SignedCommandArguments.find(nodeName) + ?: SignedCommandArguments.unsignedFactory((sender as? Player)?.uuid, content) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/SuggestingArgumentType.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/SuggestingArgumentType.kt new file mode 100644 index 000000000..0962e1741 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/SuggestingArgumentType.kt @@ -0,0 +1,173 @@ +package dev.slne.surf.api.minestom.server.impl.command.brigadier + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import com.mojang.brigadier.tree.ArgumentCommandNode +import dev.slne.surf.api.minestom.command.argument.ArgumentDefinition +import dev.slne.surf.api.minestom.command.argument.SuggestionMode +import dev.slne.surf.api.minestom.command.exception.ComponentMessage +import dev.slne.surf.api.minestom.command.executor.CommandArguments +import dev.slne.surf.api.minestom.command.executor.ParsedArgument +import dev.slne.surf.api.minestom.command.suggestion.SafeSuggestions +import dev.slne.surf.api.minestom.command.suggestion.StringTooltip +import dev.slne.surf.api.minestom.command.suggestion.SuggestionFilter +import dev.slne.surf.api.minestom.command.suggestion.SuggestionInfo +import dev.slne.surf.api.minestom.coroutine.minestomAsyncScope +import dev.slne.surf.api.minestom.server.impl.command.includesBuiltIns +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import net.minestom.server.MinecraftServer +import net.minestom.server.command.CommandSender +import java.util.concurrent.CompletableFuture + +/** + * Wraps [delegate] to serve the suggestion provider declared on [definition]. + * + * Parsing and examples are delegated unchanged. Suggestions are computed in a coroutine launched on + * [scope], so providers may suspend and the caller's thread is not blocked. + */ +internal class SuggestingArgumentType( + val delegate: ArgumentType, + private val definition: ArgumentDefinition<*>, + private val scope: () -> CoroutineScope = { minestomAsyncScope }, +) : ArgumentType { + + override fun parse(reader: StringReader): Any = delegate.parse(reader) + + override fun parse(reader: StringReader, source: S): Any = delegate.parse(reader, source) + + override fun getExamples(): Collection = delegate.examples + + /** + * Suggests the provider's entries, filtered by the provider's policy. Include modes add the + * [delegate]'s own suggestions; replace modes offer only the provider's. + * + * The returned future always completes normally. A failing provider is logged and the + * suggestions collected up to that point are returned. + */ + override fun listSuggestions( + context: CommandContext, + builder: SuggestionsBuilder, + ): CompletableFuture { + val sender = context.source as? CommandSender ?: return Suggestions.empty() + val provider = definition.suggestions + val future = CompletableFuture() + + scope().launch { + var builtIns: Suggestions? = null + + try { + if (provider.includesBuiltIns()) { + builtIns = delegate.listSuggestions(context, builder).await() + } + + val entries = resolve(provider, sender, context, builder) + val filtered = if (provider.filterPolicy() == SuggestionFilter.PREFIX) { + entries.filter { entry -> + entry.suggestion.startsWith(builder.remaining, ignoreCase = true) + } + } else { + entries + } + + filtered.forEach { entry -> + builder.suggest(entry.suggestion, entry.tooltip?.let(::ComponentMessage)) + } + } catch (failure: Throwable) { + MinecraftServer.LOGGER.error( + "Failed suggestions for argument '{}'", + definition.nodeName, + failure, + ) + } + + // The delegate may build its suggestions from a different builder (a list offsets one + // per element), so the results are merged instead of assumed to share [builder]. + val own = builder.build() + future.complete( + if (builtIns == null) own else Suggestions.merge(builder.input, listOf(builtIns, own)), + ) + } + + return future + } + + private suspend fun resolve( + mode: SuggestionMode<*>, + sender: CommandSender, + context: CommandContext, + builder: SuggestionsBuilder, + ): List { + val info = SuggestionInfo( + sender = sender, + previousArgs = previousArguments(context), + currentInput = builder.input, + currentArg = builder.remaining, + ) + + @Suppress("UNCHECKED_CAST") + val stringify = definition.stringify as (Any?) -> String + + @Suppress("UNCHECKED_CAST") + return when (mode) { + SuggestionMode.BuiltIns -> emptyList() + is SuggestionMode.Include<*> -> mode.provider.suggest(info) + is SuggestionMode.Replace<*> -> mode.provider.suggest(info) + is SuggestionMode.IncludeSafe<*> -> + (mode.provider as SafeSuggestions).suggest(info) + .map { tooltip -> StringTooltip(stringify(tooltip.suggestion), tooltip.tooltip) } + + is SuggestionMode.ReplaceSafe<*> -> + (mode.provider as SafeSuggestions).suggest(info) + .map { tooltip -> StringTooltip(stringify(tooltip.suggestion), tooltip.tooltip) } + } + } + + /** + * The arguments parsed so far in [context], in the order they were read. + * + * Nodes without a parsed value, such as literals, are omitted. + */ + private fun previousArguments(context: CommandContext): CommandArguments { + val parsed = ObjectArrayList() + + context.nodes.forEach { parsedNode -> + val node = parsedNode.node as? ArgumentCommandNode<*, *> ?: return@forEach + val name = node.name + val value = runCatching { context.getArgument(name, Any::class.java) }.getOrNull() + ?: return@forEach + + parsed += ParsedArgument( + name = name, + value = value, + raw = context.input.substring(parsedNode.range.start, parsedNode.range.end), + present = true, + ) + } + + return CommandArguments.of(parsed) + } +} + +/** + * How the entries of a provider are matched against the typed input. + * + * Safe providers return typed values that the argument stringifies itself, so they are always + * filtered by prefix. + */ +private fun SuggestionMode<*>.filterPolicy(): SuggestionFilter = when (this) { + is SuggestionMode.Include<*> -> provider.filterPolicy + is SuggestionMode.Replace<*> -> provider.filterPolicy + else -> SuggestionFilter.PREFIX +} + +/** + * Whether this definition carries a suggestion provider of its own. + */ +internal fun ArgumentDefinition<*>.hasCustomSuggestions(): Boolean = + suggestions != SuggestionMode.BuiltIns diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/configuration/ConfigurationPhase.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/configuration/ConfigurationPhase.kt new file mode 100644 index 000000000..b618588e3 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/configuration/ConfigurationPhase.kt @@ -0,0 +1,127 @@ +package dev.slne.surf.api.minestom.server.impl.configuration + +import dev.slne.surf.api.minestom.extension.ConnectionManager +import dev.slne.surf.api.minestom.server.configuration.ConfigurationContext +import dev.slne.surf.api.minestom.server.configuration.ConfigurationTask +import dev.slne.surf.api.minestom.server.configuration.ConfigurationTaskId +import dev.slne.surf.api.minestom.server.impl.mixin.duck.ConnectionManagerDuck +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import net.minestom.server.entity.Player +import net.minestom.server.event.Event +import net.minestom.server.event.EventNode +import net.minestom.server.property.ServerProperties +import org.jetbrains.annotations.Blocking + +/** + * Runs the configuration phase as an ordered list of steps ([ConfigurationTask]s). + * + * `ConnectionManagerMixin` hands `ConnectionManager#doConfiguration` over to + * [ConfigurationPhaseHook] while the phase is installed; otherwise Minestom runs its own + * configuration phase. + */ +internal object ConfigurationPhase { + private val LOGGER = ComponentLogger.logger() + + @Volatile + private var tasks: List>? = null + + /** + * Installs the listeners of the steps in [tasks], then activates the phase. Until then, [run] + * defers to Minestom. + */ + internal fun install( + tasks: List>, + node: EventNode, + ) { + tasks.forEach { (_, task) -> task.install(node) } + this.tasks = tasks + } + + /** + * Deactivates the phase; Minestom runs its own configuration phase again. + */ + internal fun uninstall() { + tasks = null + } + + /** + * Removes [player] from the connection manager's keep-alive set. + */ + fun stopKeepAlive(player: Player) { + ConnectionManager.duck().`surf$keepAlivePlayers`().remove(player) + } + + /** + * Runs the configuration phase of [player] in place of Minestom's, on the virtual + * configuration thread Minestom started for the player. + * + * A failing step is logged and its exception propagates to Minestom. + * + * @return `false` when the phase is not installed and Minestom has to run its own + */ + @Blocking + internal fun run( + manager: ConnectionManagerDuck, + player: Player, + isFirstConfig: Boolean + ): Boolean { + val tasks = tasks ?: return false + configure(manager, player, isFirstConfig, tasks) + return true + } + + @Blocking + private fun configure( + manager: ConnectionManagerDuck, + player: Player, + isFirstConfig: Boolean, + tasks: List>, + ) { + check(ServerProperties.INSIDE_TEST.get() || Thread.currentThread().isVirtual) { + "The configuration must run on a virtual thread" + } + + if (isFirstConfig) { + manager.`surf$configurationPlayers`().add(player) + manager.`surf$keepAlivePlayers`().add(player) + + player.refreshKeepAlive(System.nanoTime()) + player.refreshAnswerKeepAlive(true) + } + + val context = ConfigurationContext(player, isFirstConfig) + + for ((id, task) in tasks) { + if (!player.isOnline) return + + try { + task.run(context) + } catch (failure: Throwable) { + LOGGER.error("Configuration step '{}' failed for {}", id, player.username, failure) + throw failure + } + } + } +} + +@Suppress("CAST_NEVER_SUCCEEDS", "NOTHING_TO_INLINE") +private inline fun net.minestom.server.network.ConnectionManager.duck() = + this as ConnectionManagerDuck + +/** + * Entry point of `ConnectionManagerMixin`. + */ +object ConfigurationPhaseHook { + + /** + * Runs the surf configuration phase for [player] of [manager] when the phase is installed, on + * the virtual configuration thread Minestom started for the player. A failure propagates to + * Minestom, which kicks the player. + * + * @return whether the phase ran, so that Minestom's own configuration phase must be skipped + */ + @Blocking + @JvmStatic + fun configure(manager: ConnectionManagerDuck, player: Player, isFirstConfig: Boolean): Boolean = + ConfigurationPhase.run(manager, player, isFirstConfig) +} diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogCallbackListener.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/dialog/DialogCallbackListener.kt similarity index 60% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogCallbackListener.kt rename to surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/dialog/DialogCallbackListener.kt index c45f0c051..f51c59708 100644 --- a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogCallbackListener.kt +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/dialog/DialogCallbackListener.kt @@ -1,8 +1,8 @@ -package dev.slne.surf.api.minestom.dialog.callback +package dev.slne.surf.api.minestom.server.impl.dialog -import com.google.inject.Singleton -import dev.slne.minestom.lobby.api.event.EventRegistrar -import dev.slne.minestom.lobby.api.extension.addListener +import dev.slne.surf.api.minestom.dialog.callback.DialogCallbacks +import dev.slne.surf.api.minestom.event.EventRegistrar +import dev.slne.surf.api.minestom.extension.addListener import net.minestom.server.event.Event import net.minestom.server.event.EventNode import net.minestom.server.event.player.PlayerCustomClickEvent @@ -10,8 +10,7 @@ import net.minestom.server.event.player.PlayerCustomClickEvent /** * Runs the [DialogCallbacks] a client reports back after pressing a dialog button. */ -@Singleton -class DialogCallbackListener : EventRegistrar { +internal object DialogCallbackListener : EventRegistrar { override fun register(node: EventNode) { node.addListener { event -> DialogCallbacks.dispatch(event.player, event.key, event.payload) diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/impl/inventory/framework/MinestomInventoryLoader.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/inventory/framework/MinestomInventoryLoader.kt similarity index 69% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/impl/inventory/framework/MinestomInventoryLoader.kt rename to surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/inventory/framework/MinestomInventoryLoader.kt index 4d2240e5f..0de2b23a5 100644 --- a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/impl/inventory/framework/MinestomInventoryLoader.kt +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/inventory/framework/MinestomInventoryLoader.kt @@ -1,9 +1,8 @@ -package dev.slne.surf.api.minestom.impl.inventory.framework +package dev.slne.surf.api.minestom.server.impl.inventory.framework -import com.google.inject.Singleton -import dev.slne.minestom.lobby.api.event.EventRegistrar -import dev.slne.minestom.lobby.api.extension.addListener -import dev.slne.surf.api.minestom.inventory.framework.view.ViewNavigationHistory +import dev.slne.surf.api.minestom.event.EventRegistrar +import dev.slne.surf.api.minestom.extension.addListener +import dev.slne.surf.api.minestom.inventory.framework.view.clearViewNavigationHistory import me.devnatan.inventoryframework.ViewFrame import net.minestom.server.event.Event import net.minestom.server.event.EventNode @@ -12,8 +11,7 @@ import net.minestom.server.event.player.PlayerDisconnectEvent /** * Loads, enables and disables the inventory-framework [ViewFrame] for Minestom. */ -@Singleton -class MinestomInventoryLoader : EventRegistrar { +internal class MinestomInventoryLoader : EventRegistrar { companion object { lateinit var instance: MinestomInventoryLoader private set @@ -24,15 +22,12 @@ class MinestomInventoryLoader : EventRegistrar { private var registered = false - init { - instance = this - } - override fun register(node: EventNode) { + instance = this viewFrame = ViewFrame.create(node) node.addListener { event -> - ViewNavigationHistory.clear(event.player) + clearViewNavigationHistory(event.player) } } diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/impl/inventory/framework/ViewFrameAccessorImpl.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/inventory/framework/ViewFrameAccessorImpl.kt similarity index 83% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/impl/inventory/framework/ViewFrameAccessorImpl.kt rename to surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/inventory/framework/ViewFrameAccessorImpl.kt index 60b457c23..19f589cd6 100644 --- a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/impl/inventory/framework/ViewFrameAccessorImpl.kt +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/inventory/framework/ViewFrameAccessorImpl.kt @@ -1,4 +1,4 @@ -package dev.slne.surf.api.minestom.impl.inventory.framework +package dev.slne.surf.api.minestom.server.impl.inventory.framework import com.google.auto.service.AutoService import dev.slne.surf.api.minestom.inventory.framework.ViewFrameAccessor diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/performance/EntityTickFilter.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/performance/EntityTickFilter.kt new file mode 100644 index 000000000..c87f9ebaf --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/performance/EntityTickFilter.kt @@ -0,0 +1,35 @@ +package dev.slne.surf.api.minestom.server.impl.performance + +import net.minestom.server.entity.Entity +import net.minestom.server.entity.EntityType + +/** + * Decides which entities `EntityTickMixin` lets tick. + * + * [configure] replaces the disabled types atomically; [shouldTick] is lock-free and safe to call + * from any tick thread. + */ +object EntityTickFilter { + + @Volatile + private var disabledTypeIds = BooleanArray(0) + + /** + * Whether [entity] ticks, that is, its type is not disabled. + */ + @JvmStatic + fun shouldTick(entity: Entity): Boolean { + val disabled = disabledTypeIds + val id = entity.entityType.id() + return id < 0 || id >= disabled.size || !disabled[id] + } + + /** + * Disables ticking for exactly [types]. + */ + fun configure(types: Collection) { + val disabled = BooleanArray((types.maxOfOrNull(EntityType::id) ?: -1) + 1) + types.forEach { type -> disabled[type.id()] = true } + disabledTypeIds = disabled + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/performance/EntityViewerLookup.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/performance/EntityViewerLookup.kt new file mode 100644 index 000000000..fb7303ec6 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/performance/EntityViewerLookup.kt @@ -0,0 +1,92 @@ +package dev.slne.surf.api.minestom.server.impl.performance + +import net.minestom.server.coordinate.ChunkRange +import net.minestom.server.coordinate.Point +import net.minestom.server.entity.Entity +import java.util.function.Consumer + +/** + * Entity lookup helpers used by [dev.slne.surf.api.minestom.server.mixin.EntityTrackerMixin]. + */ +object EntityViewerLookup { + + /** + * Cost of one tracked-entity check relative to one chunk-bucket probe. Benchmarks put the + * crossover near 1400 candidates against 4225 probes, so a factor of 3 restricts the direct + * scan to cases where it wins. + */ + private const val DIRECT_SCAN_COST_FACTOR = 3L + + /** + * Resolves the tracker-recorded position of an entity, or `null` if it is not tracked. + */ + fun interface EntityPositionResolver { + fun resolve(entity: Entity): Point? + } + + /** + * Whether scanning all [candidateCount] tracked candidates is cheaper than probing every chunk + * bucket in the square of [chunkRange]. + */ + @JvmStatic + fun useDirectScan(candidateCount: Int, chunkRange: Int): Boolean { + if (chunkRange <= 0) return false + val side = 2L * chunkRange + 1L + return candidateCount * DIRECT_SCAN_COST_FACTOR < side * side + } + + /** + * Passes every candidate whose resolved position lies within [chunkRange] chunks of + * `(centerChunkX, centerChunkZ)` to [query]. The bounds are inclusive, as in + * [ChunkRange.chunksInRange]. Candidates the [resolver] cannot place are skipped. + */ + @Suppress("ConvertTwoComparisonsToRangeCheck") + @JvmStatic + fun directScan( + candidates: Set, + resolver: EntityPositionResolver, + centerChunkX: Int, + centerChunkZ: Int, + chunkRange: Int, + query: Consumer, + ) { + val minX = centerChunkX - chunkRange + val maxX = centerChunkX + chunkRange + val minZ = centerChunkZ - chunkRange + val maxZ = centerChunkZ + chunkRange + + for (candidate in candidates) { + val position = resolver.resolve(candidate) ?: continue + + val chunkX = position.chunkX() + if (chunkX < minX || chunkX > maxX) continue + + val chunkZ = position.chunkZ() + if (chunkZ < minZ || chunkZ > maxZ) continue + + query.accept(candidate) + } + } + + /** + * Visits the same chunks as [ChunkRange.chunksInRange] in row-major instead of spiral order. + * Only for lookups that do not depend on visiting order. + */ + @JvmStatic + fun chunksInRangeRectangular( + chunkX: Int, + chunkZ: Int, + chunkRange: Int, + consumer: ChunkRange.ChunkConsumer, + ) { + val minX = chunkX - chunkRange + val maxX = chunkX + chunkRange + val minZ = chunkZ - chunkRange + val maxZ = chunkZ + chunkRange + for (x in minX..maxX) { + for (z in minZ..maxZ) { + consumer.accept(x, z) + } + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/performance/SkylightSuppression.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/performance/SkylightSuppression.kt new file mode 100644 index 000000000..eb8ee8750 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/performance/SkylightSuppression.kt @@ -0,0 +1,54 @@ +package dev.slne.surf.api.minestom.server.impl.performance + +import net.minestom.server.coordinate.Point +import net.minestom.server.instance.Section +import net.minestom.server.instance.light.Light +import net.minestom.server.instance.light.LightCompute +import net.minestom.server.instance.palette.Palette + +/** + * Sky light selection used by [dev.slne.surf.api.minestom.server.mixin.LightingChunkSkylightMixin]. + */ +object SkylightSuppression { + + /** + * The sky light of [section], or [DisabledSkyLight] when the dimension has no sky light. + */ + @JvmStatic + fun sectionSkyLight(hasSkylight: Boolean, section: Section): Light { + return if (hasSkylight) section.skyLight() else DisabledSkyLight + } + + /** + * Sky light that never relights or sends and reports unset content, so no sky light mask bit is set. + */ + @Suppress("UnstableApiUsage") + object DisabledSkyLight : Light { + + override fun requiresUpdate() = false + override fun requiresSend() = false + override fun array(): ByteArray = LightCompute.UNSET_CONTENT + override fun getLevel(x: Int, y: Int, z: Int) = 0 + + override fun flip() = Unit + override fun invalidate() = Unit + override fun set(copyArray: ByteArray) = Unit + + override fun calculateInternal( + blockPalette: Palette, + chunkX: Int, + chunkY: Int, + chunkZ: Int, + heightmap: IntArray, + maxY: Int, + lightLookup: Light.LightLookup, + ): Set = emptySet() + + override fun calculateExternal( + blockPalette: Palette, + neighbors: Array, + lightLookup: Light.LightLookup, + paletteLookup: Light.PaletteLookup, + ): Set = emptySet() + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/player/PlayerEvents.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/player/PlayerEvents.kt new file mode 100644 index 000000000..4973d98ee --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/player/PlayerEvents.kt @@ -0,0 +1,196 @@ +package dev.slne.surf.api.minestom.server.impl.player + +import dev.slne.surf.api.minestom.extension.ConnectionManager +import dev.slne.surf.api.minestom.extension.GlobalEventHandler +import dev.slne.surf.api.minestom.extension.addListener +import dev.slne.surf.api.minestom.player.PlayerLimit +import dev.slne.surf.api.minestom.player.event.AsyncPlayerCountEvent +import dev.slne.surf.api.minestom.player.event.AsyncPlayerSpawnLocationEvent +import dev.slne.surf.api.minestom.player.event.PlayerJoinEvent +import dev.slne.surf.api.minestom.player.event.PlayerQuitEvent +import net.kyori.adventure.key.Key +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.translation.GlobalTranslator +import net.kyori.adventure.translation.TranslationStore +import net.minestom.server.adventure.MinestomAdventure +import net.minestom.server.adventure.audience.Audiences +import net.minestom.server.entity.Player +import net.minestom.server.event.Event +import net.minestom.server.event.EventDispatcher +import net.minestom.server.event.EventListener +import net.minestom.server.event.EventNode +import net.minestom.server.event.player.AsyncPlayerConfigurationEvent +import net.minestom.server.event.player.PlayerDisconnectEvent +import net.minestom.server.event.player.PlayerSpawnEvent +import net.minestom.server.event.server.ServerListPingEvent +import net.minestom.server.ping.Status +import java.text.MessageFormat +import java.util.* + +/** + * Calls the player events of the surf api. + * + * Without the configuration phase of the surf api, [AsyncPlayerCountEvent] and + * [AsyncPlayerSpawnLocationEvent] are called around Minestom's own + * [AsyncPlayerConfigurationEvent]. The player count is checked by a listener registered on the + * global event handler at install, which runs before the listeners of every event node and before + * listeners registered on the global event handler afterwards, but not before ones registered + * earlier. The spawn location is chosen from an event node that runs after the other nodes. With + * the configuration phase, each is a step of its own. + */ +internal object PlayerEvents { + private val SERVER_FULL: Component = Component.text("The server is full.", NamedTextColor.RED) + + private var first: EventListener? = null + private var last: EventNode? = null + + /** + * Players that fired a join; only these produce a quit. Guarded by its own monitor. + */ + private val joined = Collections.newSetFromMap(WeakHashMap()) + + @Volatile + private var translationsRegistered = false + + fun install(node: EventNode, configurationPhase: Boolean) { + registerTranslations() + + if (!configurationPhase) { + val first = EventListener.of(AsyncPlayerConfigurationEvent::class.java) { event -> + if (event.isFirstConfig) admit(event.player) + } + + val last = EventNode.all("surf-api-player-events-last").setPriority(Int.MAX_VALUE) + last.addListener { event -> + if (event.isFirstConfig && event.player.isOnline) chooseSpawnLocation(event) + } + + GlobalEventHandler.addListener(first) + GlobalEventHandler.addChild(last) + this.first = first + this.last = last + } + + node.addListener { event -> + if (event.isFirstSpawn) join(event.player) + } + node.addListener { event -> quit(event.player) } + node.addListener(::reportPlayerCount) + } + + fun uninstall() { + first?.let(GlobalEventHandler::removeListener) + last?.let(GlobalEventHandler::removeChild) + first = null + last = null + synchronized(joined) { joined.clear() } + } + + /** + * Calls [AsyncPlayerCountEvent] for [player] and kicks the player unless the event allows the + * join. The event's default result is `KICK_FULL` when the server is full. + * + * @return whether the player may join + */ + fun admit(player: Player): Boolean { + val maxPlayers = PlayerLimit.maxPlayers + val playerCount = PlayerLimit.playerCount + val full = maxPlayers != null && playerCount > maxPlayers + + val event = AsyncPlayerCountEvent( + player = player, + playerCount = playerCount, + maxPlayers = maxPlayers, + result = if (full) AsyncPlayerCountEvent.Result.KICK_FULL else AsyncPlayerCountEvent.Result.ALLOWED, + kickMessage = SERVER_FULL, + ) + EventDispatcher.call(event) + + if (event.isAllowed) return true + + player.kick(event.kickMessage) + return false + } + + /** + * Calls [AsyncPlayerSpawnLocationEvent] and applies its result to [configuration]. + */ + fun chooseSpawnLocation(configuration: AsyncPlayerConfigurationEvent) { + val player = configuration.player + val instance = requireNotNull(configuration.spawningInstance) { + "You need to specify a spawning instance in the AsyncPlayerConfigurationEvent" + } + + val event = AsyncPlayerSpawnLocationEvent(player, instance, player.respawnPoint) + EventDispatcher.call(event) + + configuration.spawningInstance = event.instance + player.respawnPoint = event.position + } + + private fun join(player: Player) { + synchronized(joined) { joined += player } + + val event = PlayerJoinEvent( + player, + Component.translatable("multiplayer.player.joined", NamedTextColor.YELLOW, player.name), + ) + EventDispatcher.call(event) + + event.joinMessage?.let(::broadcast) + } + + private fun quit(player: Player) { + val wasJoined = synchronized(joined) { joined.remove(player) } + if (!wasJoined) return + + val event = PlayerQuitEvent( + player, + Component.translatable("multiplayer.player.left", NamedTextColor.YELLOW, player.name), + ) + EventDispatcher.call(event) + + event.quitMessage?.let(::broadcast) + } + + private fun broadcast(message: Component) { + for (player in ConnectionManager.onlinePlayers) { + player.sendMessage(message) + } + Audiences.console().sendMessage(GlobalTranslator.render(message, MinestomAdventure.getDefaultLocale())) + } + + private fun reportPlayerCount(event: ServerListPingEvent) { + val maxPlayers = PlayerLimit.maxPlayers ?: return + val playerInfo = event.status.playerInfo() ?: return + + event.status = Status.builder(event.status) + .playerInfo( + Status.PlayerInfo.builder(playerInfo) + .onlinePlayers(PlayerLimit.playerCount) + .maxPlayers(maxPlayers) + .build() + ) + .build() + } + + /** + * Registers English join and quit message formats so the console shows text instead of keys. + */ + private fun registerTranslations() { + if (translationsRegistered) return + translationsRegistered = true + + val store = TranslationStore.messageFormat(Key.key("surf", "player")) + store.defaultLocale(Locale.US) + store.registerAll( + Locale.US, + mapOf( + "multiplayer.player.joined" to MessageFormat("{0} joined the game"), + "multiplayer.player.left" to MessageFormat("{0} left the game"), + ), + ) + GlobalTranslator.translator().addSource(store) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/player/PlayerFlightListener.kt b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/player/PlayerFlightListener.kt new file mode 100644 index 000000000..3d7ef5f87 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/kotlin/dev/slne/surf/api/minestom/server/impl/player/PlayerFlightListener.kt @@ -0,0 +1,45 @@ +package dev.slne.surf.api.minestom.server.impl.player + +import dev.slne.surf.api.minestom.extension.PacketListenerManager +import dev.slne.surf.api.minestom.player.event.PlayerToggleFlightEvent +import net.minestom.server.entity.Player +import net.minestom.server.event.EventDispatcher +import net.minestom.server.listener.AbilitiesListener +import net.minestom.server.network.packet.client.play.ClientPlayerAbilitiesPacket +import net.minestom.server.network.packet.server.play.PlayerAbilitiesPacket +import kotlin.experimental.and + +/** + * Calls [PlayerToggleFlightEvent] before Minestom applies a player's flight toggle. + * + * A cancelled toggle is not applied and the abilities are resent. [uninstall] restores Minestom's + * [AbilitiesListener]. + */ +internal object PlayerFlightListener { + + fun install() { + PacketListenerManager.setPlayListener(ClientPlayerAbilitiesPacket::class.java, ::handle) + } + + fun uninstall() { + PacketListenerManager.setPlayListener( + ClientPlayerAbilitiesPacket::class.java, + AbilitiesListener::listener, + ) + } + + private fun handle(packet: ClientPlayerAbilitiesPacket, player: Player) { + val isFlying = packet.flags and PlayerAbilitiesPacket.FLAG_FLYING != 0.toByte() + if (player.isAllowFlying && player.isFlying != isFlying) { + val event = PlayerToggleFlightEvent(player, isFlying) + EventDispatcher.call(event) + if (event.isCancelled) { + // Resends the abilities, which puts the client back into its previous state + player.isFlying = player.isFlying + return + } + } + + AbilitiesListener.listener(packet, player) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/resources/META-INF/services/org.spongepowered.asm.service.IGlobalPropertyService b/surf-api-minestom/surf-api-minestom-server/src/main/resources/META-INF/services/org.spongepowered.asm.service.IGlobalPropertyService new file mode 100644 index 000000000..6f75da435 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/resources/META-INF/services/org.spongepowered.asm.service.IGlobalPropertyService @@ -0,0 +1 @@ +dev.slne.surf.api.minestom.server.impl.mixin.InstrumentationGlobalPropertyService diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/resources/META-INF/services/org.spongepowered.asm.service.IMixinService b/surf-api-minestom/surf-api-minestom-server/src/main/resources/META-INF/services/org.spongepowered.asm.service.IMixinService new file mode 100644 index 000000000..dbeadee4a --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/resources/META-INF/services/org.spongepowered.asm.service.IMixinService @@ -0,0 +1 @@ +dev.slne.surf.api.minestom.server.impl.mixin.InstrumentationMixinService diff --git a/surf-api-minestom/surf-api-minestom-server/src/main/resources/mixins.surf-api-minestom-server.json b/surf-api-minestom/surf-api-minestom-server/src/main/resources/mixins.surf-api-minestom-server.json new file mode 100644 index 000000000..3be47016a --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/main/resources/mixins.surf-api-minestom-server.json @@ -0,0 +1,18 @@ +{ + "required": true, + "minVersion": "0.8.7", + "package": "dev.slne.surf.api.minestom.server.mixin", + "compatibilityLevel": "JAVA_25", + "mixins": [ + "CommandManagerMixin", + "ConnectionManagerMixin", + "EntityTickMixin", + "EntityTrackerEntryMixin", + "EntityTrackerMixin", + "LightingChunkSkylightMixin", + "ServerMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/SurfMinestomServerTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/SurfMinestomServerTest.kt new file mode 100644 index 000000000..e12a124d1 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/SurfMinestomServerTest.kt @@ -0,0 +1,167 @@ +package dev.slne.surf.api.minestom.server + +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.dsl.anyExecutor +import dev.slne.surf.api.minestom.permission.MinestomPermissions +import dev.slne.surf.api.minestom.permission.PermissionProvider +import dev.slne.surf.api.minestom.server.impl.inventory.framework.MinestomInventoryLoader +import net.kyori.adventure.util.TriState +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +@EnvTest +class SurfMinestomServerTest { + + @Test + fun `starts features in order, runs commands and shuts down in reverse`(env: Env) { + val calls = mutableListOf() + + fun feature(id: String) = object : SurfMinestomFeature { + override val id = id + override fun load(api: SurfMinestomServer) { + calls += "load:$id" + } + + override fun disable(api: SurfMinestomServer) { + calls += "disable:$id" + } + } + + val first = feature("first") + val api = surfMinestomServer { + install(first) + install(feature("second")) + withConfigurationPhase() + } + + try { + assertSame(api, SurfMinestomServer.instance) + assertSame(first, api.feature("first")) + assertThrows { surfMinestomServer() } + + var ran = false + CommandAPICommand("smoke").anyExecutor { _, _ -> ran = true }.register() + val manager = env.process().command() + manager.execute(manager.consoleSender, "smoke") + assertTrue(ran) + } finally { + api.shutdown() + } + + assertEquals( + listOf("load:first", "load:second", "disable:second", "disable:first"), + calls, + ) + assertThrows { SurfMinestomServer.instance } + } + + @Test + fun `the configured permission provider wins over the one of a feature`(env: Env) { + val featureProvider = PermissionProvider { _, _ -> TriState.FALSE } + val configuredProvider = PermissionProvider { _, _ -> TriState.TRUE } + + val api = surfMinestomServer { + install(object : SurfMinestomFeature { + override val id = "permissions" + override fun load(api: SurfMinestomServer) { + MinestomPermissions.installProvider(featureProvider) + } + }) + permissions(configuredProvider) + } + + try { + assertSame(configuredProvider, MinestomPermissions.provider) + } finally { + api.shutdown() + } + } + + @Test + fun `a rejected second start leaves the running api untouched`(env: Env) { + val api = surfMinestomServer() + + try { + val loader = MinestomInventoryLoader.instance + assertThrows { surfMinestomServer() } + + assertSame(loader, MinestomInventoryLoader.instance) + assertSame(api, SurfMinestomServer.instance) + } finally { + api.shutdown() + } + } + + @Test + fun `a failing load rolls back and a later start succeeds`(env: Env) { + val calls = mutableListOf() + + val failure = assertThrows { + surfMinestomServer { + install(object : SurfMinestomFeature { + override val id = "first" + override fun load(api: SurfMinestomServer) { + calls += "load:first" + } + + override fun disable(api: SurfMinestomServer) { + calls += "disable:first" + } + }) + install(object : SurfMinestomFeature { + override val id = "broken" + override fun load(api: SurfMinestomServer) { + throw IllegalStateException("broken load") + } + }) + } + } + + assertEquals("broken load", failure.message) + assertEquals(listOf("load:first", "disable:first"), calls) + assertThrows { SurfMinestomServer.instance } + + val api = surfMinestomServer() + try { + assertSame(api, SurfMinestomServer.instance) + } finally { + api.shutdown() + } + } + + @Test + fun `a failing disable does not keep the api from shutting down`(env: Env) { + val calls = mutableListOf() + + val api = surfMinestomServer { + install(object : SurfMinestomFeature { + override val id = "first" + override fun disable(api: SurfMinestomServer) { + calls += "disable:first" + } + }) + install(object : SurfMinestomFeature { + override val id = "second" + override fun disable(api: SurfMinestomServer) { + throw IllegalStateException("broken disable") + } + }) + } + + api.shutdown() + api.shutdown() + + assertEquals(listOf("disable:first"), calls) + assertThrows { SurfMinestomServer.instance } + + val restarted = surfMinestomServer() + try { + assertNotSame(api, restarted) + } finally { + restarted.shutdown() + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/configuration/ConfigurationPhaseBuilderTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/configuration/ConfigurationPhaseBuilderTest.kt new file mode 100644 index 000000000..4d9363632 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/configuration/ConfigurationPhaseBuilderTest.kt @@ -0,0 +1,78 @@ +package dev.slne.surf.api.minestom.server.configuration + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class ConfigurationPhaseBuilderTest { + + private val custom = ConfigurationTaskId("custom") + private val noop = ConfigurationTask { } + + @Test + fun `starts with the vanilla steps`() { + assertEquals( + listOf( + ConfigurationTasks.PLAYER_COUNT, + ConfigurationTasks.BRAND, + ConfigurationTasks.CONFIGURATION_EVENT, + ConfigurationTasks.SPAWN_LOCATION, + ConfigurationTasks.ENABLED_FEATURES, + ConfigurationTasks.SYNCHRONIZE_REGISTRIES, + ConfigurationTasks.AWAIT_SETTINGS, + ConfigurationTasks.RESOURCE_PACK, + ConfigurationTasks.JOIN_WORLD, + ), + ConfigurationPhaseBuilder().ids, + ) + } + + @Test + fun `replace keeps the position of the step`() { + val builder = ConfigurationPhaseBuilder() + builder.replace(ConfigurationTasks.RESOURCE_PACK, noop) + + val tasks = builder.build() + assertEquals(7, tasks.indexOfFirst { it.first == ConfigurationTasks.RESOURCE_PACK }) + assertSame(noop, tasks.single { it.first == ConfigurationTasks.RESOURCE_PACK }.second) + } + + @Test + fun `steps are inserted around their anchor`() { + val builder = ConfigurationPhaseBuilder() + builder.before(ConfigurationTasks.JOIN_WORLD, custom, noop) + builder.after(ConfigurationTasks.BRAND, ConfigurationTaskId("after_brand"), noop) + builder.first(ConfigurationTaskId("first"), noop) + + val ids = builder.ids + assertEquals(ConfigurationTaskId("first"), ids.first()) + assertEquals(ConfigurationTaskId("after_brand"), ids[ids.indexOf(ConfigurationTasks.BRAND) + 1]) + assertEquals(custom, ids[ids.indexOf(ConfigurationTasks.JOIN_WORLD) - 1]) + } + + @Test + fun `remove drops the step`() { + val builder = ConfigurationPhaseBuilder() + builder.remove(ConfigurationTasks.RESOURCE_PACK) + + assertEquals(8, builder.ids.size) + assertThrows { builder.remove(ConfigurationTasks.RESOURCE_PACK) } + } + + @Test + fun `the code of conduct runs once the settings arrived`() { + val builder = ConfigurationPhaseBuilder() + builder.codeOfConduct { "Be nice" } + + val ids = builder.ids + assertEquals(ids.indexOf(ConfigurationTasks.AWAIT_SETTINGS) + 1, ids.indexOf(ConfigurationTasks.CODE_OF_CONDUCT)) + } + + @Test + fun `an id cannot be used twice`() { + val builder = ConfigurationPhaseBuilder() + assertThrows { builder.last(ConfigurationTasks.BRAND, noop) } + assertThrows { builder.before(custom, ConfigurationTaskId("other"), noop) } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/ArgumentDefinitionTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/ArgumentDefinitionTest.kt new file mode 100644 index 000000000..0fa89cf1a --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/ArgumentDefinitionTest.kt @@ -0,0 +1,309 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.StringArgumentType +import dev.slne.surf.api.minestom.command.argument.* +import dev.slne.surf.api.minestom.command.suggestion.ArgumentSuggestions +import dev.slne.surf.api.minestom.command.suggestion.SafeSuggestions +import net.minestom.server.command.builder.arguments.ArgumentType +import net.minestom.server.coordinate.Vec +import net.minestom.server.utils.Range +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.util.* + +@EnvTest +class ArgumentDefinitionTest { + @Test + fun `numeric and suggestion configuration freezes into a definition`() { + val argument = IntegerArgument("amount", 1, 64) + .setOptional(4) + .withPermission(" lobby.amount ") + .replaceSuggestions(ArgumentSuggestions.strings("1", "4", "16")) + + val definition = argument.toDefinition() + + assertEquals(ArgumentKind.Integer(min = 1, max = 64), definition.kind) + assertTrue(definition.optional) + assertEquals(setOf("lobby.amount"), definition.permissions) + assertInstanceOf(SuggestionMode.Replace::class.java, definition.suggestions) + } + + @Test + fun `configuration methods retain the fluent receiver`() { + val argument = IntegerArgument("amount") + val suggestions = ArgumentSuggestions.strings("1") + val safeSuggestions = SafeSuggestions.suggest(1) + + assertSame(argument, argument.setOptional(true)) + assertSame(argument, argument.setOptional(1)) + assertSame(argument, argument.setOptional { 2 }) + assertSame(argument, argument.withPermission("lobby.amount")) + assertSame(argument, argument.withRequirement { true }) + assertSame(argument, argument.replaceSuggestions(suggestions)) + assertSame(argument, argument.includeSuggestions(suggestions)) + assertSame(argument, argument.replaceSafeSuggestions(safeSuggestions)) + assertSame(argument, argument.includeSafeSuggestions(safeSuggestions)) + } + + @Test + fun `blank node names and permissions are rejected`() { + assertThrows { StringArgument(" ") } + assertThrows { StringArgument("value").withPermission("\t") } + } + + @Test + fun `numeric bounds reject reversed and non finite ranges`() { + assertThrows { IntegerArgument("value", 2, 1) } + assertThrows { LongArgument("value", 2, 1) } + assertThrows { FloatArgument("value", 2f, 1f) } + assertThrows { DoubleArgument("value", 2.0, 1.0) } + assertThrows { FloatArgument("value", Float.NaN, 1f) } + assertThrows { DoubleArgument("value", 0.0, Double.NaN) } + } + + @Test + fun `literal arguments reject empty literal sets and preserve their values`() { + assertThrows { LiteralArgument("choice", " ") } + assertThrows { LiteralArgument("choice", "two words") } + assertThrows { MultiLiteralArgument("choice") } + assertThrows { MultiLiteralArgument("choice", "one", "") } + assertThrows { MultiLiteralArgument("choice", "one", "two words") } + assertThrows { MultiLiteralArgument("choice", "one\ttwo") } + assertThrows { MultiLiteralArgument("choice", "ns:fast") } + assertThrows { MultiLiteralArgument("choice", "a/b") } + assertThrows { MultiLiteralArgument("choice", "ä") } + assertEquals( + listOf("a-b.c+d_1"), + (MultiLiteralArgument("choice", "a-b.c+d_1").toDefinition().kind as ArgumentKind.MultiLiteral).literals, + ) + + val source = arrayOf("one", "two") + val argument = MultiLiteralArgument("choice", *source) + val definition = argument.toDefinition() + source[0] = "changed" + + assertEquals(listOf("one", "two"), (definition.kind as ArgumentKind.MultiLiteral).literals) + assertEquals("two", definition.stringify("two")) + assertFalse(argument.javaClass.methods.any { it.name == "getLiterals" }) + } + + @Test + fun `enum arguments use stable values and formatter`() { + val source = mutableListOf(AccessLevel.STAFF, AccessLevel.GUEST) + val definition = EnumArgument("access", source) { it.name.lowercase().replace("staff", "team") } + .toDefinition() + source.clear() + + val kind = definition.kind as ArgumentKind.Enum + assertEquals(EnumSet.of(AccessLevel.STAFF, AccessLevel.GUEST), kind.values) + assertEquals("team", kind.formatter(AccessLevel.STAFF)) + assertEquals("guest", definition.stringify(AccessLevel.GUEST)) + } + + @Test + fun `enum arguments reject formatted values that Brigadier cannot read unquoted`() { + assertThrows { + EnumArgument("access", AccessLevel.entries) { value -> "ns:${value.name.lowercase()}" } + } + assertThrows { + EnumArgument("access", AccessLevel.entries) { value -> "${value.name.lowercase()}/x" } + } + } + + @Test + fun `uuid and range arguments expose native result kinds and command text`() { + val uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000") + val uuidDefinition = UUIDArgument("id").toDefinition() + val integerRange = IntegerRangeArgument("levels").toDefinition() + val floatRange = FloatRangeArgument("speeds").toDefinition() + + assertEquals(ArgumentKind.Uuid, uuidDefinition.kind) + assertEquals(uuid.toString(), uuidDefinition.stringify(uuid)) + assertEquals(ArgumentKind.IntegerRange, integerRange.kind) + assertEquals("3..7", integerRange.stringify(Range.Int(3, 7))) + assertEquals("..7", integerRange.stringify(Range.Int(null, 7))) + assertEquals("3", integerRange.stringify(Range.Int(3))) + assertEquals(ArgumentKind.FloatRange, floatRange.kind) + assertEquals("1.5..", floatRange.stringify(Range.Float(1.5f, null))) + } + + @Test + fun `only the string categories that consume the rest of the line are greedy`() { + val definitions = listOf( + StringArgument("word").toDefinition(), + TextArgument("text").toDefinition(), + GreedyStringArgument("greedy").toDefinition(), + CommandArgument("command").toDefinition(), + ) + + assertEquals(listOf(false, false, true, true), definitions.map { it.greedy }) + assertEquals( + listOf(ArgumentKind.Word, ArgumentKind.Text, ArgumentKind.GreedyString, ArgumentKind.Command), + definitions.map { it.kind }, + ) + } + + @Test + fun `text argument serializes values as valid quotable phrases`(env: Env) { + val stringify = TextArgument("text").toDefinition().stringify + val verticalTabValue = "left\u000Bright" + val verticalTabSerialized = stringify(verticalTabValue) + + assertEquals("plain", stringify("plain")) + assertEquals("\"hello world\"", stringify("hello world")) + assertEquals("\"say \\\"hello\\\"\"", stringify("say \"hello\"")) + assertEquals("\"C:\\\\temp\"", stringify("C:\\temp")) + assertEquals("\"a:b\"", stringify("a:b")) + assertEquals("\"héllo\"", stringify("héllo")) + assertEquals("\"C:\\\\Program Files\\\\app\"", stringify("C:\\Program Files\\app")) + assertEquals("\"Grüße Welt\"", stringify("Grüße Welt")) + assertEquals("\"left\u000Bright\"", verticalTabSerialized) + assertEquals( + verticalTabValue, + ArgumentType.String("text").parse(env.process().command().consoleSender, verticalTabSerialized), + ) + listOf("plain", "a:b", "héllo", "C:\\temp", "tab\there", "new\nline", "say \"hi\"", "back\\slash").forEach { value -> + assertEquals(value, StringArgumentType.string().parse(StringReader(stringify(value)))) + } + } + + @Test + fun `numeric and position kinds never serialize scientific notation`() { + val doubles = DoubleArgument("double").toDefinition().stringify + val floats = FloatArgument("float").toDefinition().stringify + val floatRange = FloatRangeArgument("range").toDefinition().stringify + + listOf(1.0E-4, 1.2345678E7, -1.0E20, 12.5, 0.0).forEach { value -> + val text = doubles(value) + assertFalse('E' in text, text) + assertEquals(value, text.toDouble()) + } + listOf(1.0E-4f, 1.2345678E7f, -Float.MAX_VALUE, 12.5f).forEach { value -> + val text = floats(value) + assertFalse('E' in text, text) + assertEquals(value, text.toFloat()) + } + + assertFalse('E' in floatRange(Range.Float(1.0E-4f, 1.2345678E7f))) + assertFalse('E' in floatRange(Range.Float(-Float.MAX_VALUE, Float.MAX_VALUE))) + assertFalse('E' in PositionArgument("pos").toDefinition().stringify(Vec(1.0E-4, 2.0, 3.0E10))) + assertFalse('E' in Position2DArgument("pos").toDefinition().stringify(Vec(1.0E-4, 0.0, 3.0E10))) + assertFalse('E' in RotationArgument("rot").toDefinition().stringify(Rotation(1.0E-4f, 1.2345678E7f))) + assertFalse('E' in AngleArgument("angle").toDefinition().stringify(1.0E-4f)) + assertEquals("1.5 2.0 3.0", PositionArgument("pos").toDefinition().stringify(Vec(1.5, 2.0, 3.0))) + } + + @Test + fun `primitive kinds retain their exact bounds`() { + assertEquals(ArgumentKind.Boolean, BooleanArgument("flag").toDefinition().kind) + assertEquals(ArgumentKind.Long(1, 2), LongArgument("long", 1, 2).toDefinition().kind) + assertEquals(ArgumentKind.Float(1f, 2f), FloatArgument("float", 1f, 2f).toDefinition().kind) + assertEquals(ArgumentKind.Double(1.0, 2.0), DoubleArgument("double", 1.0, 2.0).toDefinition().kind) + assertEquals(ArgumentKind.Literal("confirm"), LiteralArgument("action", "confirm").toDefinition().kind) + } + + @Test + fun `optional defaults retain constant and sender aware suppliers`(env: Env) { + val sender = env.process().command().consoleSender + val constant = IntegerArgument("constant").setOptional(4).toDefinition() + val supplier: (net.minestom.server.command.CommandSender) -> Int = { if (it === sender) 8 else 0 } + val supplied = IntegerArgument("supplied").setOptional(supplier).toDefinition() + val absent = IntegerArgument("absent").setOptional(true).toDefinition() + val required = IntegerArgument("required").setOptional(false).toDefinition() + + assertTrue(constant.optional) + assertEquals(4, constant.defaultValue?.invoke(sender)) + assertTrue(supplied.optional) + assertSame(supplier, supplied.defaultValue) + assertEquals(8, supplied.defaultValue?.invoke(sender)) + assertTrue(absent.optional) + assertNull(absent.defaultValue) + assertFalse(required.optional) + assertNull(required.defaultValue) + } + + @Test + fun `named boolean optional overloads distinguish defaults from optionality`(env: Env) { + val sender = env.process().command().consoleSender + val falseDefault = BooleanArgument("false-default").setOptional(default = false).toDefinition() + val trueDefault = BooleanArgument("true-default").setOptional(default = true).toDefinition() + val required = BooleanArgument("required").setOptional(optional = false).toDefinition() + val optional = BooleanArgument("optional").setOptional(optional = true).toDefinition() + + assertTrue(falseDefault.optional) + assertEquals(false, falseDefault.defaultValue?.invoke(sender)) + assertTrue(trueDefault.optional) + assertEquals(true, trueDefault.defaultValue?.invoke(sender)) + assertFalse(required.optional) + assertNull(required.defaultValue) + assertTrue(optional.optional) + assertNull(optional.defaultValue) + } + + @Test + fun `snapshots isolate mutable builder state and preserve requirement order`(env: Env) { + val calls = mutableListOf() + val firstRequirement: (net.minestom.server.command.CommandSender) -> Boolean = { + calls += "first" + true + } + val secondRequirement: (net.minestom.server.command.CommandSender) -> Boolean = { + calls += "second" + true + } + val argument = StringArgument("target") + .withPermission("lobby.first") + .withRequirement(firstRequirement) + val snapshot = argument.toDefinition() + + argument + .withPermission("lobby.second") + .withRequirement(secondRequirement) + .includeSuggestions(ArgumentSuggestions.strings("later")) + + assertEquals(setOf("lobby.first"), snapshot.permissions) + assertEquals(listOf(firstRequirement), snapshot.requirements) + snapshot.requirements.forEach { it(env.process().command().consoleSender) } + assertEquals(listOf("first"), calls) + assertEquals(SuggestionMode.BuiltIns, snapshot.suggestions) + assertThrows { + (snapshot.permissions as MutableSet).add("mutated") + } + assertThrows { + (snapshot.requirements as MutableList<(net.minestom.server.command.CommandSender) -> Boolean>).clear() + } + } + + @Test + fun `all suggestion modes remain explicit in snapshots`() { + val strings = ArgumentSuggestions.strings("one") + val safe = SafeSuggestions.suggest(1) + + assertEquals(SuggestionMode.BuiltIns, IntegerArgument("built-in").toDefinition().suggestions) + assertSame( + strings, + (IntegerArgument("replace").replaceSuggestions(strings).toDefinition().suggestions as SuggestionMode.Replace).provider, + ) + assertSame( + strings, + (IntegerArgument("include").includeSuggestions(strings).toDefinition().suggestions as SuggestionMode.Include).provider, + ) + assertSame( + safe, + (IntegerArgument("replace-safe").replaceSafeSuggestions(safe).toDefinition().suggestions as SuggestionMode.ReplaceSafe).provider, + ) + assertSame( + safe, + (IntegerArgument("include-safe").includeSafeSuggestions(safe).toDefinition().suggestions as SuggestionMode.IncludeSafe).provider, + ) + } + + private enum class AccessLevel { + STAFF, + GUEST, + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandAPIHookTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandAPIHookTest.kt new file mode 100644 index 000000000..8b271a55b --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandAPIHookTest.kt @@ -0,0 +1,136 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.argument.StringArgument +import dev.slne.surf.api.minestom.command.dsl.anyExecutor +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import net.kyori.adventure.translation.GlobalTranslator +import net.minestom.server.command.builder.Command +import net.minestom.server.command.builder.CommandResult +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.Player +import net.minestom.server.instance.Instance +import net.minestom.server.network.packet.server.play.SystemChatPacket +import net.minestom.testing.Collector +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import net.minestom.testing.TestConnection +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.assertFalse + +/** + * Tests the contract `CommandManagerMixin` relies on: [CommandAPIHook.execute] returns a result + * for a command the CommandAPI owns and `null` otherwise, leaving the input to Minestom's own + * parsing. + */ +@EnvTest +class CommandAPIHookTest { + + @Test + fun `an owned command is dispatched through brigadier`(env: Env) { + val runs = AtomicInteger() + + withPlatform(env) { player, _ -> + CommandAPICommand("owned") + .anyExecutor { _, _ -> runs.incrementAndGet() } + .register() + + assertTrue(CommandAPIHook.owns("owned")) + + val result = CommandAPIHook.execute(player, "owned") + + assertEquals(CommandResult.Type.SUCCESS, result?.type) + assertEquals(1, runs.get()) + } + } + + @Test + fun `a foreign minestom command is left to the manager`(env: Env) { + withPlatform(env) { player, _ -> + env.process().command().register(Command("foreign")) + + assertFalse(CommandAPIHook.owns("foreign")) + assertNull(CommandAPIHook.execute(player, "foreign")) + assertNull(CommandAPIHook.execute(player, "never-registered")) + } + } + + @Test + fun `trailing input is reported with the vanilla syntax error`(env: Env) { + val runs = AtomicInteger() + + withPlatform(env) { player, messages -> + CommandAPICommand("difficulty") + .withArguments(StringArgument("value")) + .anyExecutor { _, _ -> runs.incrementAndGet() } + .register() + + val result = CommandAPIHook.execute(player, "difficulty hard aaa") + + assertEquals(CommandResult.Type.INVALID_SYNTAX, result?.type) + assertEquals(0, runs.get()) + messages.assertSingle { packet -> + val rendered = render(packet) + assertTrue(rendered.contains("Incorrect argument for command"), rendered) + assertTrue(rendered.contains("<--[HERE]"), rendered) + } + } + } + + @Test + fun `an unknown command is reported the way vanilla reports it`(env: Env) { + withPlatform(env) { player, messages -> + CommandAPIHook.reportUnknown(player, "/nonsense") + + messages.assertSingle { packet -> + val rendered = render(packet) + assertTrue(rendered.contains("Unknown or incomplete command"), rendered) + assertTrue(rendered.contains("nonsense<--[HERE]"), rendered) + } + } + } + + private fun render(packet: SystemChatPacket): String { + CommandAPITranslations.register() + return PlainTextComponentSerializer.plainText() + .serialize(GlobalTranslator.render(packet.message, Locale.US)) + } + + private fun withPlatform(env: Env, block: (Player, Collector) -> Unit) { + val platform = MinestomCommandAPIPlatform( + env.process().command(), + MinestomCommandOwnership(), + ) + val instance = env.createEmptyInstance() + var player: Player? = null + CommandAPI.installPlatform(platform) + try { + val connection = env.createConnection() + player = connect(connection, instance) + block(player, connection.trackIncoming(SystemChatPacket::class.java)) + } finally { + try { + player?.remove() + env.destroyInstance(instance) + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + } + } + + private fun connect(connection: TestConnection, instance: Instance): Player { + val player = CompletableFuture() + Thread.startVirtualThread { + runCatching { connection.connect(instance, Pos.ZERO) } + .onSuccess(player::complete) + .onFailure(player::completeExceptionally) + } + return player.join() + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandArgumentsTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandArgumentsTest.kt new file mode 100644 index 000000000..d8dba4487 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandArgumentsTest.kt @@ -0,0 +1,91 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import dev.slne.surf.api.minestom.command.exception.CommandSyntaxException +import dev.slne.surf.api.minestom.command.exception.CommandValidationException +import dev.slne.surf.api.minestom.command.exception.WrapperCommandSyntaxException +import dev.slne.surf.api.minestom.command.executor.CommandArguments +import dev.slne.surf.api.minestom.command.executor.ParsedArgument +import net.kyori.adventure.text.Component +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class CommandArgumentsTest { + @Test + fun `resolves values explicitly and by property delegate`() { + val arguments = CommandArguments.of( + listOf( + ParsedArgument("target", "Deniz", "Deniz", present = true), + ParsedArgument("reason", null, null, present = false), + ), + ) + + val target: String by arguments + val reason: String? by arguments + + assertEquals("Deniz", arguments.get("target")) + assertEquals("Deniz", target) + assertNull(reason) + assertEquals("Deniz", arguments.getRaw("target")) + assertThrows { arguments.get("target") } + } + + @Test + fun `explicit get honors a nullable requested type`() { + val arguments = CommandArguments.of( + listOf( + ParsedArgument("target", "Deniz", "Deniz", present = true), + ParsedArgument("reason", null, null, present = false), + ), + ) + + val reason = arguments.get("reason") + val target = arguments.get("target") + + assertNull(reason) + assertEquals("Deniz", target) + assertThrows { arguments.get("reason") } + } + + @Test + fun `keeps parsed arguments ordered and distinguishes absent values`() { + val supplied = mutableListOf( + ParsedArgument("target", "Deniz", "Deniz", present = true), + ParsedArgument("limit", 3, "3", present = false), + ParsedArgument("reason", null, null, present = false), + ) + val arguments = CommandArguments.of(supplied) + supplied.clear() + + assertEquals(3, arguments.getOptional("limit")) + assertEquals("Deniz", arguments[0]) + assertNull(arguments[2]) + assertEquals("3", arguments.getRaw(1)) + assertNull(arguments.getRaw(4)) + assertEquals(listOf("Deniz"), arguments.rawArguments()) + assertTrue("target" in arguments) + assertFalse("limit" in arguments) + assertFalse("missing" in arguments) + assertThrows { arguments.get("reason") } + assertThrows { arguments.get("missing") } + assertThrows { + CommandArguments.of(listOf(ParsedArgument("target", "one", "one", true), ParsedArgument("target", "two", "two", true))) + } + } + + @Test + fun `preserves syntax failure context when wrapped`() { + val component = Component.text("Expected a destination") + val cause = IllegalStateException("bad input") + val syntax = CommandSyntaxException(component, "/warp", 5, cause) + val wrapper = WrapperCommandSyntaxException(syntax) + + assertEquals("Expected a destination", syntax.message) + assertSame(component, wrapper.component) + assertEquals("/warp", wrapper.input) + assertEquals(5, wrapper.cursor) + assertSame(syntax, wrapper.exception) + assertSame(syntax, wrapper.cause) + assertEquals("invalid", CommandValidationException("invalid").message) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandBuilderTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandBuilderTest.kt new file mode 100644 index 000000000..007e9b5c3 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandBuilderTest.kt @@ -0,0 +1,391 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import dev.slne.surf.api.minestom.command.* +import dev.slne.surf.api.minestom.command.argument.* +import dev.slne.surf.api.minestom.command.dsl.anyExecutor +import dev.slne.surf.api.minestom.command.dsl.commandAPICommand +import dev.slne.surf.api.minestom.command.dsl.integerArgument +import dev.slne.surf.api.minestom.command.dsl.playerExecutor +import dev.slne.surf.api.minestom.command.exception.CommandSyntaxException +import dev.slne.surf.api.minestom.command.exception.CommandValidationException +import dev.slne.surf.api.minestom.command.executor.ExecutorType +import dev.slne.surf.api.minestom.command.internal.CommandAPIPlatform +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import net.minestom.server.command.CommandSender +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.lang.reflect.Proxy + +class CommandBuilderTest { + @Test + fun `subcommands and optional suffixes freeze into paths`() { + val rootRequirement: (CommandSender) -> Boolean = { true } + val subcommandRequirement: (CommandSender) -> Boolean = { false } + val command = CommandAPICommand("staff") + .withPermission("lobby.staff") + .withRequirement(rootRequirement) + .withSubcommand( + CommandAPICommand("message") + .withPermission("lobby.staff.message") + .withRequirement(subcommandRequirement) + .withArguments(StringArgument("target")) + .withOptionalArguments(GreedyStringArgument("reason").setOptional("none")) + .anyExecutor { _, _ -> }, + ) + + val definition = command.toDefinition() + val path = definition.paths.single() + + assertEquals(listOf("message", "target", "reason"), path.arguments.map { it.nodeName }) + assertEquals(listOf(false, false, true), path.arguments.map { it.optional }) + assertTrue(path.arguments.last().greedy) + assertEquals("none", path.arguments.last().defaultValue?.invoke(nullSender())) + assertEquals(setOf("lobby.staff", "lobby.staff.message"), path.permissions) + assertSame(rootRequirement, path.requirements[0]) + assertSame(subcommandRequirement, path.requirements[1]) + } + + @Test + fun `root path retains aliases metadata and conditions`() { + val requirement: (CommandSender) -> Boolean = { true } + val definition = CommandAPICommand("announce") + .withAliases("broadcast", "notice") + .withShortDescription("Short") + .withFullDescription("First line", "Second line") + .withUsage("/announce ", "/broadcast ") + .withHelp("Long-form help") + .withPermission("lobby.announce") + .withRequirement(requirement) + .anyExecutor { _, _ -> } + .toDefinition() + + assertEquals(setOf("broadcast", "notice"), definition.aliases) + assertEquals("Short", definition.metadata.shortDescription) + assertEquals(listOf("First line", "Second line"), definition.metadata.fullDescription) + assertEquals(listOf("/announce ", "/broadcast "), definition.metadata.usage) + assertEquals("Long-form help", definition.metadata.help) + assertEquals(setOf("lobby.announce"), definition.paths.single().permissions) + assertSame(requirement, definition.paths.single().requirements.single()) + } + + @Test + fun `snapshot does not observe later builder mutations`() { + val argument = StringArgument("target").withPermission("before.argument") + val command = CommandAPICommand("message") + .withAliases("msg") + .withPermission("before.command") + .withArguments(argument) + .anyExecutor { _, _ -> } + + val snapshot = command.toDefinition() + argument.withPermission("after.argument").then(StringArgument("child").anyExecutor { _, _ -> }) + command.withAliases("tell").withPermission("after.command").withArguments(StringArgument("extra")) + + assertEquals(setOf("msg"), snapshot.aliases) + assertEquals(listOf("target"), snapshot.paths.single().arguments.map { it.nodeName }) + assertEquals(setOf("before.argument"), snapshot.paths.single().arguments.single().permissions) + assertEquals(setOf("before.command"), snapshot.paths.single().permissions) + } + + @Test + fun `withOptionalArguments marks every supplied argument optional and keeps explicit defaults`() { + val definition = CommandAPICommand("lookup") + .withOptionalArguments( + StringArgument("query"), + IntegerArgument("page").setOptional { 7 }, + ) + .anyExecutor { _, _ -> } + .toDefinition() + + val arguments = definition.paths.single().arguments + assertEquals(listOf(true, true), arguments.map { it.optional }) + assertEquals(null, arguments[0].defaultValue) + assertEquals(7, arguments[1].defaultValue?.invoke(nullSender())) + } + + @Test + fun `invalid command names aliases and namespaces are rejected`() { + assertThrows(CommandValidationException::class.java) { + CommandAPICommand("bad name").anyExecutor { _, _ -> }.toDefinition() + } + assertThrows(CommandValidationException::class.java) { + CommandAPICommand("valid").withAliases("valid").anyExecutor { _, _ -> }.toDefinition() + } + assertThrows(CommandValidationException::class.java) { + CommandAPICommand("valid").withAliases("Same", "same").anyExecutor { _, _ -> }.toDefinition() + } + + val platform = CapturingPlatform() + CommandAPI.installPlatform(platform) + try { + assertThrows(CommandValidationException::class.java) { + CommandAPICommand("valid").anyExecutor { _, _ -> }.register("Bad Namespace") + } + assertTrue(platform.definitions.isEmpty()) + } finally { + CommandAPI.uninstallPlatform(platform) + } + } + + @Test + fun `duplicate argument names in an executable path are rejected`() { + assertThrows(CommandValidationException::class.java) { + CommandAPICommand("duplicate") + .withArguments(StringArgument("value"), IntegerArgument("value")) + .anyExecutor { _, _ -> } + .toDefinition() + } + } + + @Test + fun `required arguments after an optional suffix are rejected`() { + assertThrows(CommandValidationException::class.java) { + CommandAPICommand("optional") + .withArguments(StringArgument("first").setOptional(true), StringArgument("required")) + .anyExecutor { _, _ -> } + .toDefinition() + } + } + + @Test + fun `arguments after a greedy argument are rejected`() { + assertThrows(CommandValidationException::class.java) { + CommandAPICommand("greedy") + .withArguments(GreedyStringArgument("rest"), StringArgument("later")) + .anyExecutor { _, _ -> } + .toDefinition() + } + } + + @Test + fun `commands without an executable path are rejected`() { + assertThrows(CommandValidationException::class.java) { + CommandAPICommand("empty").withArguments(StringArgument("unused")).toDefinition() + } + assertThrows(CommandValidationException::class.java) { + CommandTree("empty-tree").then(StringArgument("unused")).toDefinition() + } + } + + @Test + fun `equal specificity executors on one path are rejected`() { + assertThrows(CommandValidationException::class.java) { + CommandAPICommand("ambiguous") + .anyExecutor { _, _ -> } + .anyExecutor { _, _ -> } + .toDefinition() + } + + val valid = CommandAPICommand("specific") + .anyExecutor { _, _ -> } + .playerExecutor { _, _ -> } + .toDefinition() + assertEquals(setOf(ExecutorType.ANY, ExecutorType.PLAYER), valid.paths.single().executors.map { it.type }.toSet()) + } + + @Test + fun `command tree flattens only executable branches in depth first order`() { + val users = LiteralArgument("users") + .then(StringArgument("target").anyExecutor { _, _ -> }) + .then(LiteralArgument("all").anyExecutor { _, _ -> }) + val reload = LiteralArgument("reload").anyExecutor { _, _ -> } + + val definition = CommandTree("admin") + .withPermission("lobby.admin") + .then(users) + .then(reload) + .toDefinition() + + assertEquals( + listOf(listOf("users", "target"), listOf("users", "all"), listOf("reload")), + definition.paths.map { path -> path.arguments.map { it.nodeName } }, + ) + assertTrue(definition.paths.all { it.permissions == setOf("lobby.admin") }) + } + + @Test + fun `indirect argument child cycles are rejected during snapshotting`() { + val first = LiteralArgument("first") + val second = LiteralArgument("second") + first.then(second) + second.then(first) + + assertThrows(CommandValidationException::class.java) { + CommandTree("cycle").then(first).toDefinition() + } + } + + @Test + fun `one argument may be shared by separate acyclic branches`() { + val shared = StringArgument("value").anyExecutor { _, _ -> } + val definition = CommandTree("shared") + .then(LiteralArgument("left").then(shared)) + .then(LiteralArgument("right").then(shared)) + .toDefinition() + + assertEquals( + listOf(listOf("left", "value"), listOf("right", "value")), + definition.paths.map { path -> path.arguments.map { it.nodeName } }, + ) + } + + @Test + fun `sibling paths merging into one node with a conflict are rejected`() { + assertThrows(CommandValidationException::class.java) { + CommandTree("kinds") + .then(IntegerArgument("value").anyExecutor { _, _ -> }) + .then(StringArgument("value").anyExecutor { _, _ -> }) + .toDefinition() + } + assertThrows(CommandValidationException::class.java) { + CommandTree("literal-versus-argument") + .then(LiteralArgument("value").anyExecutor { _, _ -> }) + .then(StringArgument("value").anyExecutor { _, _ -> }) + .toDefinition() + } + assertThrows(CommandValidationException::class.java) { + CommandAPICommand("same-label") + .withSubcommand(CommandAPICommand("sub").anyExecutor { _, _ -> }) + .withSubcommand(CommandAPICommand("sub").anyExecutor { _, _ -> }) + .toDefinition() + } + assertThrows(CommandValidationException::class.java) { + CommandTree("optional-branches") + .then(StringArgument("first").setOptional(true).anyExecutor { _, _ -> }) + .then(IntegerArgument("second").setOptional(true).anyExecutor { _, _ -> }) + .toDefinition() + } + } + + @Test + fun `sibling paths sharing an identical prefix node stay valid`() { + val custom = StringArgument("name").map { name -> name.length } + .then(LiteralArgument("x").anyExecutor { _, _ -> }) + .then(LiteralArgument("y").anyExecutor { _, _ -> }) + + val tree = CommandTree("shared-prefix") + .then(LiteralArgument("set").then(StringArgument("key").anyExecutor { _, _ -> })) + .then(LiteralArgument("set").then(IntegerArgument("id").anyExecutor { _, _ -> })) + .then(StringArgument("v").then(LiteralArgument("a").anyExecutor { _, _ -> })) + .then(StringArgument("v").then(LiteralArgument("b").anyExecutor { _, _ -> })) + .then(custom) + .toDefinition() + val command = CommandAPICommand("nested") + .withSubcommand(CommandAPICommand("a").withSubcommand(CommandAPICommand("b").anyExecutor { _, _ -> })) + .withSubcommand(CommandAPICommand("a").withSubcommand(CommandAPICommand("c").anyExecutor { _, _ -> })) + .toDefinition() + + assertEquals(6, tree.paths.size) + assertEquals(2, command.paths.size) + } + + @Test + fun `subcommand literals carry the subcommand permissions and requirements`() { + val requirement: (CommandSender) -> Boolean = { true } + val definition = CommandAPICommand("staff") + .anyExecutor { _, _ -> } + .withSubcommand( + CommandAPICommand("admin") + .withAliases("op") + .withPermission("lobby.admin") + .withRequirement(requirement) + .anyExecutor { _, _ -> }, + ) + .toDefinition() + + val admin = definition.paths.single { path -> path.arguments.map { it.nodeName } == listOf("admin") } + val alias = definition.paths.single { path -> path.arguments.map { it.nodeName } == listOf("op") } + + assertEquals(setOf("lobby.admin"), admin.arguments.single().permissions) + assertSame(requirement, admin.arguments.single().requirements.single()) + assertEquals(setOf("lobby.admin"), alias.arguments.single().permissions) + assertEquals(emptySet(), definition.paths.single { path -> path.arguments.isEmpty() }.permissions) + } + + @Test + fun `platform install is exclusive and uninstall only removes the same instance`() { + val installed = CapturingPlatform() + val other = CapturingPlatform() + CommandAPI.installPlatform(installed) + try { + assertThrows(IllegalStateException::class.java) { CommandAPI.installPlatform(other) } + CommandAPI.uninstallPlatform(other) + + val registered = CommandAPICommand("still-installed").anyExecutor { _, _ -> }.register() + assertEquals("still-installed", registered.name) + assertEquals(listOf("still-installed"), installed.definitions.map { it.name }) + } finally { + CommandAPI.uninstallPlatform(installed) + } + } + + @Test + fun `registration and unregistration without a platform describe the lifecycle error`() { + val registrationFailure = assertThrows(IllegalStateException::class.java) { + CommandAPICommand("early").anyExecutor { _, _ -> }.register() + } + val unregistrationFailure = assertThrows(IllegalStateException::class.java) { + CommandAPI.unregister("early") + } + + assertTrue(registrationFailure.message.orEmpty().contains("platform", ignoreCase = true)) + assertTrue(registrationFailure.message.orEmpty().contains("installed", ignoreCase = true)) + assertTrue(unregistrationFailure.message.orEmpty().contains("platform", ignoreCase = true)) + } + + @Test + fun `auto registering DSL captures a frozen definition and namespace`() { + val platform = CapturingPlatform() + CommandAPI.installPlatform(platform) + try { + val registration = commandAPICommand("dsl", "lobby") { + integerArgument("amount") + anyExecutor { _, _ -> } + } + + assertEquals("dsl", registration.name) + assertEquals(listOf("dsl"), platform.definitions.map { it.name }) + assertEquals(listOf("amount"), platform.definitions.single().paths.single().arguments.map { it.nodeName }) + assertEquals(listOf("lobby"), platform.namespaces) + } finally { + CommandAPI.uninstallPlatform(platform) + } + } + + @Test + fun `syntax failure helpers retain text and adventure messages`() { + val textFailure = assertThrows(CommandSyntaxException::class.java) { + CommandAPI.failWithString("bad input") + } + val component = Component.text("component input") + val componentFailure = assertThrows(CommandSyntaxException::class.java) { + CommandAPI.failWithMessage(component) + } + + val plain = PlainTextComponentSerializer.plainText() + assertEquals("bad input", plain.serialize(checkNotNull(textFailure.component))) + assertSame(component, componentFailure.component) + } + + private fun nullSender(): CommandSender = Proxy.newProxyInstance( + CommandSender::class.java.classLoader, + arrayOf(CommandSender::class.java), + ) { _, _, _ -> null } as CommandSender + + private class CapturingPlatform : CommandAPIPlatform { + val definitions = mutableListOf() + val namespaces = mutableListOf() + + override fun register(definition: CommandDefinition, namespace: String?): RegisteredCommand { + definitions += definition + namespaces += namespace + return RegisteredCommand(definition.name, definition.aliases, namespace) + } + + override fun unregister(name: String): Boolean = definitions.removeIf { it.name == name } + + override fun execute(sender: CommandSender, input: String): Int = + throw UnsupportedOperationException("This platform only captures registrations") + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandLabelValidationTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandLabelValidationTest.kt new file mode 100644 index 000000000..176a43ab3 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandLabelValidationTest.kt @@ -0,0 +1,60 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.CommandTree +import dev.slne.surf.api.minestom.command.dsl.anyExecutor +import dev.slne.surf.api.minestom.command.exception.CommandValidationException +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test + +class CommandLabelValidationTest { + + @Test + fun `a label may be written in any script`() { + val definition = CommandAPICommand("überweisen") + .withAliases("bezahlen", "παραλαβή") + .anyExecutor { _, _ -> } + .toDefinition() + + assertEquals("überweisen", definition.name) + assertEquals(setOf("bezahlen", "παραλαβή"), definition.aliases) + } + + @Test + fun `a tree label may be written in any script`() { + val definition = CommandTree("überweisen") + .anyExecutor { _, _ -> } + .toDefinition() + + assertEquals("überweisen", definition.name) + } + + @Test + fun `a label with whitespace stays rejected`() { + assertThrows(CommandValidationException::class.java) { + CommandAPICommand("über weisen").anyExecutor { _, _ -> }.toDefinition() + } + } + + @Test + fun `a label with punctuation stays rejected`() { + assertThrows(CommandValidationException::class.java) { + CommandAPICommand("pay!").anyExecutor { _, _ -> }.toDefinition() + } + + assertThrows(CommandValidationException::class.java) { + CommandAPICommand("pay").withAliases("pay:now").anyExecutor { _, _ -> }.toDefinition() + } + } + + @Test + fun `case insensitive duplicates stay rejected regardless of script`() { + assertThrows(CommandValidationException::class.java) { + CommandAPICommand("Überweisen") + .withAliases("überweisen") + .anyExecutor { _, _ -> } + .toDefinition() + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandManagerMixinTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandManagerMixinTest.kt new file mode 100644 index 000000000..c79a61b90 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandManagerMixinTest.kt @@ -0,0 +1,124 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.argument.IntegerArgument +import dev.slne.surf.api.minestom.command.dsl.anyExecutor +import net.minestom.server.command.builder.Command +import net.minestom.server.command.builder.CommandResult +import net.minestom.server.coordinate.Pos +import net.minestom.server.network.packet.server.play.DeclareCommandsPacket +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicInteger + +/** + * Tests that `CommandManagerMixin` routes CommandAPI commands through Minestom's command manager. + */ +@EnvTest +class CommandManagerMixinTest { + + @Test + fun `the command manager runs a CommandAPI command`(env: Env) { + val manager = env.process().command() + withPlatform(env) { + val received = AtomicInteger(-1) + + CommandAPICommand("dispatched") + .withAliases("dispatch") + .withArguments(IntegerArgument("amount")) + .anyExecutor { _, args -> received.set(args.get("amount")) } + .register() + + assertTrue(manager.commandExists("dispatched")) + assertTrue(manager.commandExists("dispatch")) + + val result = manager.execute(manager.consoleSender, "dispatched 7") + assertEquals(CommandResult.Type.SUCCESS, result.type) + assertEquals(7, received.get()) + + manager.execute(manager.consoleSender, "dispatch 9") + assertEquals(9, received.get()) + } + } + + @Test + fun `an unregistered command is unknown to the command manager`(env: Env) { + val manager = env.process().command() + withPlatform(env) { + CommandAPICommand("temporary") + .anyExecutor { _, _ -> } + .register() + + assertTrue(manager.commandExists("temporary")) + assertNull(manager.getCommand("temporary"), "the command is not registered as a Minestom command") + assertTrue(CommandAPI.unregister("temporary")) + assertFalse(manager.commandExists("temporary")) + assertEquals(CommandResult.Type.UNKNOWN, manager.execute(manager.consoleSender, "temporary").type) + } + } + + @Test + fun `a name taken by a Minestom command is rejected`(env: Env) { + val manager = env.process().command() + val foreign = Command("foreign") + manager.register(foreign) + try { + withPlatform(env) { + val failure = runCatching { + CommandAPICommand("foreign").anyExecutor { _, _ -> }.register() + } + assertTrue(failure.isFailure) + assertEquals(foreign, manager.getCommand("foreign")) + } + } finally { + manager.unregister(foreign) + } + } + + @Test + fun `the declared commands contain the CommandAPI tree`(env: Env) { + withPlatform(env) { + CommandAPICommand("declared") + .withArguments(IntegerArgument("amount")) + .anyExecutor { _, _ -> } + .register() + + val packet = minestomDeclaration(env) + + val rootChildren = packet.nodes[packet.rootIndex].children.map { packet.nodes[it] } + val declared = rootChildren.filter { it.name == "declared" } + assertEquals(1, declared.size) + + val arguments = declared.single().children.map { packet.nodes[it].name } + assertEquals(listOf("amount"), arguments) + } + } + + private fun minestomDeclaration(env: Env): DeclareCommandsPacket { + val instance = env.createFlatInstance() + val player = env.createPlayer(instance, Pos.ZERO) + try { + return env.process().command().createDeclareCommandsPacket(player) + } finally { + player.remove() + env.destroyInstance(instance) + } + } + + private fun withPlatform(env: Env, block: () -> Unit) { + val platform = MinestomCommandAPIPlatform(env.process().command(), MinestomCommandOwnership()) + CommandAPI.installPlatform(platform) + try { + block() + } finally { + try { + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandTestSupport.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandTestSupport.kt new file mode 100644 index 000000000..4397768ee --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandTestSupport.kt @@ -0,0 +1,81 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.exceptions.CommandSyntaxException +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.argument.ArgumentDefinition +import dev.slne.surf.api.minestom.permission.MinestomPermissions +import dev.slne.surf.api.minestom.permission.PermissionProvider +import dev.slne.surf.api.minestom.server.impl.command.brigadier.NodeDeclaration +import dev.slne.surf.api.minestom.server.impl.command.brigadier.NodeDeclarations +import net.kyori.adventure.util.TriState +import net.minestom.server.command.CommandSender +import net.minestom.server.command.builder.CommandResult +import net.minestom.server.entity.Player +import net.minestom.server.network.packet.server.play.DeclareCommandsPacket +import net.minestom.server.network.player.GameProfile +import net.minestom.server.network.player.PlayerConnection +import net.minestom.testing.Env + +/** + * Runs [input] as [sender] and reports whether a command executed. + * + * A rejected command (unknown, malformed, or hidden from [sender] for lack of permission) returns + * `false` instead of throwing. The rejection is not reported to [sender]; use + * [runCommandReporting] to observe the message a real command packet produces. + */ +internal fun runCommand(sender: CommandSender, input: String): Boolean = try { + CommandAPI.execute(sender, input) + true +} catch (failure: CommandSyntaxException) { + false +} + +/** + * Runs [input] as [sender] through [CommandAPIHook], as the command manager does, reporting a + * rejection to [sender], and returns whether a command executed. + */ +internal fun runCommandReporting(sender: CommandSender, input: String): Boolean = + CommandAPIHook.execute(sender, input)?.type == CommandResult.Type.SUCCESS + +/** + * Runs [input] as [sender] and returns the executed command's result value. + */ +internal fun runCommandForResult(sender: CommandSender, input: String): Int = + CommandAPI.execute(sender, input) + +/** + * Parses [input] with this argument's raw parser. + */ +internal fun ArgumentDefinition.read(sender: CommandSender, input: String): T = + rawType.parse(StringReader(input), sender) + +/** + * The node declaration a client receives for this argument. + */ +internal fun ArgumentDefinition<*>.declaration(): NodeDeclaration = NodeDeclarations().of(this) + +/** + * The command tree sent to [player]: Minestom's commands with the CommandAPI's merged in. + */ +internal fun declaredCommands(env: Env, player: Player): DeclareCommandsPacket = + env.process().command().createDeclareCommandsPacket(player) + +/** + * A player holding exactly [grantedPermissions], resolved through [MinestomPermissions]. + * + * Constructing one installs the test permission provider globally. + */ +internal class TestPermissionPlayer( + connection: PlayerConnection, + profile: GameProfile, + val grantedPermissions: Set, +) : Player(connection, profile) { + init { + MinestomPermissions.installProvider(TEST_PERMISSIONS) + } +} + +private val TEST_PERMISSIONS = PermissionProvider { player, permission -> + TriState.byBoolean((player as? TestPermissionPlayer)?.grantedPermissions?.contains(permission) == true) +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandTreeIntegrationTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandTreeIntegrationTest.kt new file mode 100644 index 000000000..b49317e9d --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CommandTreeIntegrationTest.kt @@ -0,0 +1,324 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.argument.EnumArgument +import dev.slne.surf.api.minestom.command.argument.LiteralArgument +import dev.slne.surf.api.minestom.command.argument.StringArgument +import dev.slne.surf.api.minestom.command.argument.SuggestionMode +import dev.slne.surf.api.minestom.command.dsl.* +import dev.slne.surf.api.minestom.command.executor.CommandArguments +import dev.slne.surf.api.minestom.command.executor.ParsedArgument +import dev.slne.surf.api.minestom.command.suggestion.ArgumentSuggestions +import dev.slne.surf.api.minestom.command.suggestion.StringTooltip +import dev.slne.surf.api.minestom.command.suggestion.SuggestionInfo +import dev.slne.surf.api.minestom.command.suggestion.SuggestionsBranch +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import net.kyori.adventure.text.Component +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.Player +import net.minestom.server.event.EventNode +import net.minestom.server.instance.Instance +import net.minestom.server.network.packet.client.play.ClientTabCompletePacket +import net.minestom.server.network.packet.server.play.TabCompletePacket +import net.minestom.server.network.player.GameProfile +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import net.minestom.testing.TestConnection +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicInteger + +@OptIn(ExperimentalCoroutinesApi::class) +@EnvTest +class CommandTreeIntegrationTest { + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `admin tree branches through literals optional arguments async tooltips and a resulting console executor`( + env: Env, + ) = runTest { + val manager = env.process().command() + val ownership = MinestomCommandOwnership() + val listener = MinestomSuggestionListener(ownership) + val platform = MinestomCommandAPIPlatform(manager, ownership, suggestionScope = { this }) + val node = EventNode.all("admin-tree-test") + CommandAPI.installPlatform(platform) + try { + listener.register(node) + env.process().eventHandler().addChild(node) + env.process().connection().setPlayerProvider { connection, profile -> + TestPermissionPlayer(connection, profile, setOf("lobby.admin")) + } + val reloadCount = AtomicInteger() + val kicked = mutableListOf>() + val tooltipInvocations = mutableListOf() + + commandTree("admin") { + withPermission("lobby.admin") + literalArgument("reload") { + consoleResultingExecutor { _, _ -> reloadCount.incrementAndGet() } + } + literalArgument("kick") { + playerArgument("target") { + textArgument("reason") { + setOptional("no reason given") + includeSuggestionsWithTooltipsAsync { info -> + tooltipInvocations += info.currentArg + listOf(StringTooltip("griefing", Component.text("Breaking blocks"))) + } + consoleExecutor { _, args -> + val target: Player by args + val reason: String by args + kicked += target.username to reason + } + } + } + } + listArgument("tags", EnumArgument("tag", Tag.entries)) { + consoleExecutor { _, _ -> } + } + } + + val reloadResult = runCommandForResult(manager.consoleSender, "admin reload") + assertEquals(1, reloadResult) + assertEquals(1, reloadCount.get()) + + val instance = env.createEmptyInstance() + var target: Player? = null + try { + val connection = env.createConnection(profile("Offender")) + target = connect(connection, instance) + + assertTrue(runCommand(manager.consoleSender, "admin kick Offender")) + assertEquals(listOf("Offender" to "no reason given"), kicked) + + assertTrue(runCommand(manager.consoleSender, "admin kick Offender \"breaking stuff\"")) + assertEquals(2, kicked.size) + assertEquals("breaking stuff", kicked[1].second) + + val packets = connection.trackIncoming(TabCompletePacket::class.java) + env.process().packetListener().processClientPacket( + ClientTabCompletePacket(1, "/admin kick Offender gr"), + target.playerConnection, + ) + runCurrent() + packets.assertSingle { packet -> + assertTrue(packet.matches.any { match -> match.match == "griefing" }) + } + assertEquals(1, tooltipInvocations.size) + + assertTrue(runCommand(manager.consoleSender, "admin low,high")) + } finally { + target?.remove() + env.destroyInstance(instance) + } + } finally { + try { + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + env.process().eventHandler().removeChild(node) + env.process().connection().setPlayerProvider(null) + } + } + + @Test + fun `suspend dependent suggestions branch resolves once the provider resumes and adapts to prior arguments`( + env: Env, + ) = runTest { + val entered = CompletableDeferred() + val release = CompletableDeferred() + val channels = ArgumentSuggestions.stringsAsync { + entered.complete(Unit) + release.await() + listOf("staff", "public") + } + val staffTargets = ArgumentSuggestions.strings("Deniz") + val branch = SuggestionsBranch.suggest(channels).branch(SuggestionsBranch.suggest(staffTargets)) + + val channel = StringArgument("channel").replaceSuggestions(branch) + val mode = channel.toDefinition().suggestions + check(mode is SuggestionMode.Replace) + + val sender = env.process().command().consoleSender + val headSuggestions = async { + mode.provider.suggest(SuggestionInfo(sender, CommandArguments.empty(), "", "")) + } + entered.await() + assertFalse(headSuggestions.isCompleted) + release.complete(Unit) + assertEquals(listOf("staff", "public"), headSuggestions.await().map(StringTooltip::suggestion)) + + val previousChannel = CommandArguments.of(listOf(ParsedArgument("channel", "staff", "staff", true))) + val nested = branch.getNextSuggestion(sender, "staff") + assertEquals( + listOf("Deniz"), + nested?.suggest(SuggestionInfo(sender, previousChannel, "staff ", ""))?.map(StringTooltip::suggestion), + ) + } + + @Test + fun `tree root executor runs the bare command while branches keep their own executors`(env: Env) { + val manager = env.process().command() + val ownership = MinestomCommandOwnership() + val platform = MinestomCommandAPIPlatform(manager, ownership) + CommandAPI.installPlatform(platform) + try { + val queried = AtomicInteger() + val applied = mutableListOf() + + commandTree("difficulty") { + executes { _, _ -> queried.incrementAndGet() } + then( + EnumArgument("value", Difficulty.entries).executes { _, args -> + val value: Difficulty by args + applied += value + }, + ) + } + + val console = manager.consoleSender + assertTrue(runCommand(console, "difficulty")) + assertEquals(1, queried.get()) + assertTrue(applied.isEmpty()) + + assertTrue(runCommand(console, "difficulty high")) + assertEquals(listOf(Difficulty.HIGH), applied) + assertEquals(1, queried.get()) + } finally { + try { + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + } + } + + @Test + fun `input matching no branch is rejected instead of falling through to the root executor`(env: Env) { + val manager = env.process().command() + val ownership = MinestomCommandOwnership() + val platform = MinestomCommandAPIPlatform(manager, ownership) + CommandAPI.installPlatform(platform) + try { + val queried = AtomicInteger() + val applied = mutableListOf() + + commandTree("difficulty-fallback") { + executes { _, _ -> queried.incrementAndGet() } + then( + EnumArgument("value", Difficulty.entries).executes { _, args -> + val value: Difficulty by args + applied += value + }, + ) + } + + runCommand(manager.consoleSender, "difficulty-fallback bogus") + + assertTrue(applied.isEmpty()) + assertEquals(0, queried.get()) + } finally { + try { + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + } + } + + @Test + fun `tree with only a root executor and no branches registers and executes`(env: Env) { + val manager = env.process().command() + val ownership = MinestomCommandOwnership() + val platform = MinestomCommandAPIPlatform(manager, ownership) + CommandAPI.installPlatform(platform) + try { + val runs = AtomicInteger() + commandTree("ping") { + executes { _, _ -> runs.incrementAndGet() } + } + + assertTrue(runCommand(manager.consoleSender, "ping")) + assertEquals(1, runs.get()) + } finally { + try { + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + } + } + + @Test + fun `tab completion of a namespaced label omits children the sender may not use`(env: Env) { + val manager = env.process().command() + val ownership = MinestomCommandOwnership() + val listener = MinestomSuggestionListener(ownership) + val platform = MinestomCommandAPIPlatform(manager, ownership) + val node = EventNode.all("namespaced-tab-test") + CommandAPI.installPlatform(platform) + try { + listener.register(node) + env.process().eventHandler().addChild(node) + commandTree("tab-gated", "plugin") { + then(LiteralArgument("list").executes { _, _ -> }) + then(LiteralArgument("reload").withRequirement { false }.executes { _, _ -> }) + } + + val instance = env.createEmptyInstance() + var player: Player? = null + try { + val connection = env.createConnection(profile("Completer")) + player = connect(connection, instance) + + val packets = connection.trackIncoming(TabCompletePacket::class.java) + env.process().packetListener().processClientPacket( + ClientTabCompletePacket(1, "/plugin:tab-gated "), + player.playerConnection, + ) + packets.assertSingle { packet -> + assertEquals(listOf("list"), packet.matches.map { match -> match.match }) + } + } finally { + player?.remove() + env.destroyInstance(instance) + } + } finally { + try { + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + env.process().eventHandler().removeChild(node) + } + } + + private fun connect(connection: TestConnection, instance: Instance): Player { + val player = CompletableFuture() + Thread.startVirtualThread { + runCatching { connection.connect(instance, Pos.ZERO) } + .onSuccess(player::complete) + .onFailure(player::completeExceptionally) + } + return player.join() + } + + private fun profile(name: String) = GameProfile(UUID.randomUUID(), name) + + private enum class Tag { + LOW, + HIGH, + } + + private enum class Difficulty { + PEACEFUL, + HIGH, + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CompositeArgumentIntegrationTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CompositeArgumentIntegrationTest.kt new file mode 100644 index 000000000..582dce29e --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/CompositeArgumentIntegrationTest.kt @@ -0,0 +1,487 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import com.mojang.brigadier.StringReader +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.CommandTree +import dev.slne.surf.api.minestom.command.argument.* +import dev.slne.surf.api.minestom.command.dsl.anyExecutionInfo +import dev.slne.surf.api.minestom.command.dsl.anyExecutor +import dev.slne.surf.api.minestom.command.dsl.customArgument +import dev.slne.surf.api.minestom.command.dsl.listArgument +import dev.slne.surf.api.minestom.command.exception.CommandSyntaxException +import dev.slne.surf.api.minestom.command.exception.CommandValidationException +import dev.slne.surf.api.minestom.command.exception.componentOrNull +import dev.slne.surf.api.minestom.command.suggestion.ArgumentSuggestions +import dev.slne.surf.api.minestom.server.impl.command.brigadier.NodeDeclarations +import net.kyori.adventure.identity.Identity +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.minestom.server.command.ArgumentParserType +import net.minestom.server.command.CommandSender +import net.minestom.server.command.builder.arguments.ArgumentType +import net.minestom.server.tag.TagHandler +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.* +import java.util.concurrent.atomic.AtomicReference +import com.mojang.brigadier.exceptions.CommandSyntaxException as BrigadierCommandSyntaxException + +@EnvTest +class CompositeArgumentIntegrationTest { + @Test + fun `custom mapped filtered and list arguments execute with converted values and exact raw input`(env: Env) { + withPlatform(env) { manager -> + val firstId = UUID.fromString("79d40754-7772-4ab4-a7c6-f21df121db4a") + val secondId = UUID.fromString("3b91a87f-82d2-4e85-ad47-3315f0021420") + val users = mapOf("alice" to User("alice", 17)) + val customInfo = AtomicReference() + val received = AtomicReference() + val sender = RecordingSender() + + CommandAPICommand("compose") + .withArguments( + CustomArgument(StringArgument("user")) { info -> + customInfo.set(CustomSnapshot(info.sender, info.currentInput, info.baseValue)) + users[info.currentInput] + ?: CommandAPI.failWithString("Unknown user ${info.currentInput}") + }, + IntegerArgument("amount").map { value -> value * 2 }, + IntegerArgument("positive").filter { value -> value > 0 }, + ListArgument("ids", UUIDArgument("id")), + ) + .anyExecutionInfo { info -> + received.set( + ExecutionSnapshot( + user = info.args[0], + amount = info.args[1], + positive = info.args[2], + ids = info.args[3], + raw = info.args.rawArguments(), + idsRaw = info.args.getRaw("ids"), + ), + ) + } + .register() + + val input = "compose alice 4 5 $firstId, $secondId" + assertTrue(runCommand(sender, input)) + assertEquals(CustomSnapshot(sender, "alice", "alice"), customInfo.get()) + assertEquals( + ExecutionSnapshot( + user = users.getValue("alice"), + amount = 8, + positive = 5, + ids = listOf(firstId, secondId), + raw = listOf("alice", "4", "5", "$firstId, $secondId"), + idsRaw = "$firstId, $secondId", + ), + received.get(), + ) + } + } + + @Test + fun `base parsers feed custom and list element conversions and are declared as greedy strings`( + env: Env, + ) { + val declarations = NodeDeclarations() + val sender = env.process().command().consoleSender + val greedy = CustomArgument(GreedyStringArgument("message")) { info -> + info.baseValue to info.currentInput + }.toDefinition() + val modes = ListArgument( + "modes", + EnumArgument("mode", CompositeMode.entries) { mode -> "mode-${mode.name.lowercase()}" }, + ).toDefinition() + + assertEquals( + "hello wide world" to "hello wide world", + greedy.rawType.parse(StringReader("hello wide world"), sender), + ) + assertEquals( + listOf(CompositeMode.LOW, CompositeMode.HIGH), + modes.rawType.parse(StringReader("mode-low, mode-high"), sender), + ) + + listOf(greedy, modes).forEach { definition -> + val declaration = declarations.of(definition) + assertEquals(ArgumentParserType.STRING, declaration.parser) + assertArrayEquals(byteArrayOf(2), declaration.properties) + } + } + + @Test + fun `custom arguments retain and convert a base default supplier`(env: Env) { + withPlatform(env) { manager -> + val defaultInfo = AtomicReference() + val received = AtomicReference() + val base = IntegerArgument("amount").setOptional { sender -> + assertSame(manager.consoleSender, sender) + 3 + } + val doubled = CustomArgument(base) { info -> + defaultInfo.set(CustomSnapshot(info.sender, info.currentInput, info.baseValue.toString())) + info.baseValue * 2 + } + val definition = doubled.toDefinition() + CommandAPICommand("default-composite") + .withArguments(doubled) + .anyExecutor { _, arguments -> received.set(arguments[0]) } + .register() + + assertTrue(runCommand(manager.consoleSender, "default-composite")) + assertEquals(6, received.get()) + assertEquals(6, definition.defaultValue!!.invoke(manager.consoleSender)) + assertEquals( + CustomSnapshot(manager.consoleSender, "", "3"), + defaultInfo.get(), + ) + } + } + + @Test + fun `filter and invalid or empty list elements reject without reordering valid entries`(env: Env) { + val sender = env.process().command().consoleSender + val firstId = UUID.fromString("79d40754-7772-4ab4-a7c6-f21df121db4a") + val secondId = UUID.fromString("3b91a87f-82d2-4e85-ad47-3315f0021420") + val positive = IntegerArgument("value").filter { it > 0 }.toDefinition().rawType + val ids = ListArgument("ids", UUIDArgument("id")).toDefinition().rawType + val emptyAllowed = ListArgument( + "values", + StringArgument("value"), + delimiter = ';', + allowEmpty = true, + ).toDefinition().rawType + + assertThrows(BrigadierCommandSyntaxException::class.java) { + positive.parse(StringReader("-1"), sender) + } + assertThrows(BrigadierCommandSyntaxException::class.java) { + ids.parse(StringReader("$firstId,,${secondId}"), sender) + } + assertThrows(BrigadierCommandSyntaxException::class.java) { + ids.parse(StringReader("$firstId,not-a-uuid"), sender) + } + assertEquals( + listOf(firstId, secondId), + ids.parse(StringReader(" $firstId , $secondId "), sender), + ) + assertEquals( + listOf("one", "", "two", ""), + emptyAllowed.parse(StringReader("one; ;two;"), sender), + ) + + assertThrows(IllegalArgumentException::class.java) { + ListArgument("invalid", GreedyStringArgument("part")) + } + assertThrows(IllegalArgumentException::class.java) { + ListArgument("invalid", StringArgument("part"), delimiter = ' ') + } + } + + @Test + fun `custom syntax failures retain styled components and the base parser declaration`(env: Env) { + val sender = env.process().command().consoleSender + val styled = Component.text("Amount is reserved", NamedTextColor.GOLD) + val definition = CustomArgument(IntegerArgument("amount", 1, 8)) { info -> + if (info.baseValue == 3) { + throw CommandSyntaxException(styled, input = "reserved-three") + } + info.baseValue + }.toDefinition() + + val declaration = NodeDeclarations().of(definition) + assertEquals(ArgumentParserType.INTEGER, declaration.parser) + assertArrayEquals( + ArgumentType.Integer("amount").min(1).max(8).nodeProperties(), + declaration.properties, + ) + + // The value is rejected where it started, so the client underlines the value itself. + val failure = assertThrows(BrigadierCommandSyntaxException::class.java) { + definition.rawType.parse(StringReader("3"), sender) + } + assertEquals(styled, failure.rawMessage.componentOrNull()) + assertEquals(0, failure.cursor) + + val filterFailure = assertThrows(BrigadierCommandSyntaxException::class.java) { + IntegerArgument("positive").filter { it > 0 }.toDefinition() + .rawType.parse(StringReader("-1"), sender) + } + assertEquals( + Component.text("Invalid value for positive"), + filterFailure.rawMessage.componentOrNull(), + ) + } + + @Test + fun `custom and list arguments reject a literal or signed message kind at construction`() { + val failure = assertThrows(CommandValidationException::class.java) { + CustomArgument(LiteralArgument("fixed")) { info -> info.baseValue } + } + assertEquals("Argument 'fixed' cannot use a literal argument as its base", failure.message) + + assertThrows(CommandValidationException::class.java) { + CustomArgument(SignedMessageArgument("message")) { info -> info.baseValue } + } + assertThrows(CommandValidationException::class.java) { + LiteralArgument("fixed").map { value -> value.length } + } + assertThrows(CommandValidationException::class.java) { + SignedMessageArgument("message").filter { true } + } + assertThrows(CommandValidationException::class.java) { + ListArgument("items", LiteralArgument("fixed")) + } + assertThrows(CommandValidationException::class.java) { + ListArgument("items", SignedMessageArgument("message")) + } + } + + @Test + fun `custom argument wrapping a position kind still fails registration despite the bypassed guard`(env: Env) { + withPlatform(env) { manager -> + val argument = CustomArgument(PositionArgument("pos")) { info -> info.baseValue } + .replaceSuggestions(ArgumentSuggestions.strings("~ ~ ~")) + val failure = assertThrows(CommandValidationException::class.java) { + CommandAPICommand("custom-reject-position") + .withArguments(argument) + .anyExecutor { _, _ -> } + .register() + } + assertEquals( + "Position argument '${argument.nodeName}' cannot use custom suggestions", + failure.message, + ) + assertFalse(runCommand(manager.consoleSender, "custom-reject-position 1 2 3")) + } + } + + @Test + fun `composite DSL builders attach root tree and child arguments`() { + val command = CommandAPICommand("dsl-root") + .customArgument(StringArgument("user"), parser = { info -> info.baseValue.length }) + .listArgument("ids", UUIDArgument("id"), delimiter = ';') + .anyExecutor { _, _ -> } + .toDefinition() + val tree = CommandTree("dsl-tree") + .customArgument(StringArgument("user"), parser = { info -> info.baseValue.length }) { + anyExecutor { _, _ -> } + } + .toDefinition() + val child = CommandTree("dsl-child") + .then( + StringArgument("prefix").listArgument("ids", UUIDArgument("id")) { + anyExecutor { _, _ -> } + }, + ) + .toDefinition() + + assertEquals(listOf("user", "ids"), command.paths.single().arguments.map { it.nodeName }) + assertEquals(listOf("user"), tree.paths.single().arguments.map { it.nodeName }) + assertEquals(listOf("prefix", "ids"), child.paths.single().arguments.map { it.nodeName }) + } + + @Test + fun `custom DSL receivers preserve inherited constant and sender defaults`(env: Env) { + withPlatform(env) { manager -> + val commandValue = AtomicReference() + val treeValue = AtomicReference() + val childValue = AtomicReference() + + CommandAPICommand("dsl-command-preserve") + .customArgument( + IntegerArgument("amount").setOptional(3), + parser = { info -> info.baseValue * 2 }, + ) + .anyExecutor { _, arguments -> commandValue.set(arguments[0]) } + .register() + CommandTree("dsl-tree-preserve") + .customArgument( + IntegerArgument("amount").setOptional { sender -> + assertSame(manager.consoleSender, sender) + 4 + }, + parser = { info -> info.baseValue * 2 }, + ) { + anyExecutor { _, arguments -> treeValue.set(arguments[0]) } + } + .register() + CommandTree("dsl-child-preserve") + .then( + StringArgument("prefix").customArgument( + IntegerArgument("amount").setOptional(5), + parser = { info -> info.baseValue * 2 }, + ) { + anyExecutor { _, arguments -> childValue.set(arguments[1]) } + }, + ) + .register() + + assertTrue(runCommand(manager.consoleSender, "dsl-command-preserve")) + assertTrue(runCommand(manager.consoleSender, "dsl-tree-preserve")) + assertTrue(runCommand(manager.consoleSender, "dsl-child-preserve prefix")) + assertEquals(6, commandValue.get()) + assertEquals(8, treeValue.get()) + assertEquals(10, childValue.get()) + } + } + + @Test + fun `custom DSL receivers apply explicit false and true optional overrides`(env: Env) { + withPlatform(env) { manager -> + val optionalValues = mutableMapOf() + val commandFalse = CommandAPICommand("dsl-command-false") + .customArgument( + IntegerArgument("amount").setOptional(3), + parser = { info -> info.baseValue * 2 }, + optional = false, + ) + .anyExecutor { _, arguments -> optionalValues["command-false"] = arguments[0] } + val commandTrue = CommandAPICommand("dsl-command-true") + .customArgument( + IntegerArgument("amount").setOptional(3), + parser = { info -> info.baseValue * 2 }, + optional = true, + ) + .anyExecutor { _, arguments -> optionalValues["command-true"] = arguments[0] } + val treeFalse = CommandTree("dsl-tree-false") + .customArgument( + IntegerArgument("amount").setOptional(4), + parser = { info -> info.baseValue * 2 }, + optional = false, + ) { + anyExecutor { _, arguments -> optionalValues["tree-false"] = arguments[0] } + } + val treeTrue = CommandTree("dsl-tree-true") + .customArgument( + IntegerArgument("amount").setOptional(4), + parser = { info -> info.baseValue * 2 }, + optional = true, + ) { + anyExecutor { _, arguments -> optionalValues["tree-true"] = arguments[0] } + } + val childFalse = CommandTree("dsl-child-false").then( + StringArgument("prefix").customArgument( + IntegerArgument("amount").setOptional(5), + parser = { info -> info.baseValue * 2 }, + optional = false, + ) { + anyExecutor { _, arguments -> optionalValues["child-false"] = arguments[1] } + }, + ) + val childTrue = CommandTree("dsl-child-true").then( + StringArgument("prefix").customArgument( + IntegerArgument("amount").setOptional(5), + parser = { info -> info.baseValue * 2 }, + optional = true, + ) { + anyExecutor { _, arguments -> optionalValues["child-true"] = arguments[1] } + }, + ) + + listOf( + commandFalse.toDefinition(), + treeFalse.toDefinition(), + childFalse.toDefinition(), + ).forEach { definition -> + val argument = definition.paths.single().arguments.last() + assertFalse(argument.optional) + assertNull(argument.defaultValue) + } + listOf( + commandTrue.toDefinition(), + treeTrue.toDefinition(), + childTrue.toDefinition(), + ).forEach { definition -> + val argument = definition.paths.single().arguments.last() + assertTrue(argument.optional) + assertNull(argument.defaultValue) + } + commandFalse.register() + commandTrue.register() + treeFalse.register() + treeTrue.register() + childFalse.register() + childTrue.register() + + listOf( + "dsl-command-false", + "dsl-tree-false", + "dsl-child-false prefix", + ).forEach { input -> + assertFalse(runCommand(manager.consoleSender, input)) + } + listOf( + "dsl-command-true", + "dsl-tree-true", + "dsl-child-true prefix", + ).forEach { input -> + assertTrue(runCommand(manager.consoleSender, input)) + } + assertEquals( + mapOf( + "command-true" to null, + "tree-true" to null, + "child-true" to null, + ), + optionalValues, + ) + } + } + + private inline fun withPlatform( + env: Env, + block: (net.minestom.server.command.CommandManager) -> Unit, + ) { + val manager = env.process().command() + val platform = MinestomCommandAPIPlatform(manager, MinestomCommandOwnership()) + CommandAPI.installPlatform(platform) + try { + block(manager) + } finally { + try { + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + } + } + + private data class User(val name: String, val level: Int) + + private data class CustomSnapshot( + val sender: CommandSender, + val currentInput: String, + val baseValue: String, + ) + + private data class ExecutionSnapshot( + val user: Any?, + val amount: Any?, + val positive: Any?, + val ids: Any?, + val raw: List, + val idsRaw: String?, + ) + + private enum class CompositeMode { + LOW, + HIGH, + } + + private class RecordingSender : CommandSender { + val messages = mutableListOf() + + override fun identity(): Identity = Identity.nil() + + override fun tagHandler(): TagHandler = TagHandler.newHandler() + + override fun sendMessage(message: Component) { + messages += message + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/EntityArgumentIntegrationTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/EntityArgumentIntegrationTest.kt new file mode 100644 index 000000000..170bb4971 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/EntityArgumentIntegrationTest.kt @@ -0,0 +1,368 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import com.mojang.brigadier.exceptions.CommandSyntaxException +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.CommandTree +import dev.slne.surf.api.minestom.command.argument.* +import dev.slne.surf.api.minestom.command.dsl.* +import dev.slne.surf.api.minestom.command.exception.CommandValidationException +import dev.slne.surf.api.minestom.command.executor.CommandArguments +import dev.slne.surf.api.minestom.command.suggestion.ArgumentSuggestions +import dev.slne.surf.api.minestom.command.suggestion.SafeSuggestions +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import net.minestom.server.command.ArgumentParserType +import net.minestom.server.command.builder.arguments.minecraft.SuggestionType +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.Entity +import net.minestom.server.entity.EntityType +import net.minestom.server.entity.Player +import net.minestom.server.instance.Instance +import net.minestom.server.network.packet.server.play.SystemChatPacket +import net.minestom.server.network.player.GameProfile +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import net.minestom.testing.TestConnection +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.time.Duration +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicReference + +@EnvTest +class EntityArgumentIntegrationTest { + @Test + fun `entity selectors declare the vanilla entity parser and entity type asks the client`() { + listOf( + PlayerArgument("player").toDefinition() to byteArrayOf(3), + PlayersArgument("players").toDefinition() to byteArrayOf(2), + EntityArgument("entity").toDefinition() to byteArrayOf(1), + EntitiesArgument("entities").toDefinition() to byteArrayOf(0), + ).forEach { (definition, properties) -> + val declaration = definition.declaration() + assertEquals(ArgumentParserType.ENTITY, declaration.parser, definition.nodeName) + assertArrayEquals(properties, declaration.properties, definition.nodeName) + } + + val entityType = EntityTypeArgument("type").toDefinition().declaration() + assertEquals(ArgumentParserType.RESOURCE_LOCATION, entityType.parser) + assertEquals(SuggestionType.SUMMONABLE_ENTITIES.identifier, entityType.suggestionsType) + } + + @Test + fun `an argument with a provider tells the client to ask the server`() { + val declaration = StringArgument("value") + .replaceSuggestions(ArgumentSuggestions.strings("one")) + .toDefinition() + .declaration() + + assertEquals(SuggestionType.ASK_SERVER.identifier, declaration.suggestionsType) + } + + @Test + fun `entity type include modes are rejected when native summonable entries cannot be reproduced`() { + val unsafe = EntityTypeArgument("type") + .includeSuggestions(ArgumentSuggestions.strings("minecraft:zombie")) + val safe = EntityTypeArgument("type") + .includeSafeSuggestions(SafeSuggestions.suggest(EntityType.ZOMBIE)) + val mapped = EntityTypeArgument("type") + .map(EntityType::name) + .includeSuggestions(ArgumentSuggestions.strings("minecraft:zombie")) + + listOf(unsafe, safe, mapped).forEach { argument -> + val failure = assertThrows(CommandValidationException::class.java) { + MinestomCommandCompiler().compile( + CommandAPICommand("entity-type-include") + .withArguments(argument) + .anyExecutor { _, _ -> } + .toDefinition(), + namespace = null, + ) + } + assertEquals( + "EntityType argument 'type' cannot include custom suggestions because " + + "Minestom does not expose the native summonable entity set; use built-ins or replace suggestions", + failure.message, + ) + } + } + + @Test + fun `native player and entity selectors resolve typed values against the execution sender`(env: Env) { + withEntityPlatform(env) { manager, _, alpha, bravo, zombie -> + val onePlayer = AtomicReference() + val players = mutableListOf>() + val oneEntity = AtomicReference() + val entities = mutableListOf>() + + CommandAPICommand("one-player") + .withArguments(PlayerArgument("target")) + .anyExecutionInfo { info -> + val delegated = PlayerArguments(info.args) + assertSame(delegated.target, info.args.get("target")) + onePlayer.set(delegated.target) + } + .register() + CommandAPICommand("many-players") + .withArguments(PlayersArgument("targets")) + .anyExecutor { _, arguments -> players += arguments.get>("targets") } + .register() + CommandAPICommand("one-entity") + .withArguments(EntityArgument("target")) + .anyExecutor { _, arguments -> oneEntity.set(arguments.get("target")) } + .register() + CommandAPICommand("many-entities") + .withArguments(EntitiesArgument("targets")) + .anyExecutor { _, arguments -> entities += arguments.get>("targets") } + .register() + + assertTrue(runCommand(alpha, "one-player Alpha")) + assertSame(alpha, onePlayer.get()) + assertFalse(runCommand(alpha, "one-player ${bravo.uuid}")) + assertTrue(runCommand(alpha, "one-player @s")) + assertSame(alpha, onePlayer.get()) + + assertTrue(runCommand(alpha, "many-players @a")) + assertTrue(runCommand(alpha, "many-players @a")) + assertEquals(2, players.size) + assertEquals(players[0], players[1]) + assertEquals(setOf(alpha, bravo), players[0].toSet()) + + assertTrue(runCommand(alpha, "one-entity ${zombie.uuid}")) + assertSame(zombie, oneEntity.get()) + assertTrue(runCommand(alpha, "one-entity @s")) + assertSame(alpha, oneEntity.get()) + + assertTrue(runCommand(alpha, "many-entities @e")) + assertTrue(runCommand(alpha, "many-entities @e")) + assertEquals(2, entities.size) + assertEquals(entities[0], entities[1]) + assertEquals(setOf(alpha, bravo, zombie), entities[0].toSet()) + } + } + + @Test + fun `single cardinality player filtering and collection empty policy fail with components`(env: Env) { + withEntityPlatform(env) { manager, alphaConnection, alpha, _, _ -> + val executions = mutableListOf() + CommandAPICommand("single-player") + .withArguments(PlayerArgument("target")) + .anyExecutor { _, _ -> executions += "single-player" } + .register() + CommandAPICommand("single-entity") + .withArguments(EntityArgument("target")) + .anyExecutor { _, _ -> executions += "single-entity" } + .register() + CommandAPICommand("players-required") + .withArguments(PlayersArgument("targets", allowEmpty = false)) + .anyExecutor { _, _ -> executions += "players-required" } + .register() + CommandAPICommand("players-empty") + .withArguments(PlayersArgument("targets", allowEmpty = true)) + .anyExecutor { _, arguments -> + assertEquals(emptyList(), arguments.get>("targets")) + executions += "players-empty" + } + .register() + CommandAPICommand("entities-required") + .withArguments(EntitiesArgument("targets", allowEmpty = false)) + .anyExecutor { _, _ -> executions += "entities-required" } + .register() + CommandAPICommand("entities-empty") + .withArguments(EntitiesArgument("targets", allowEmpty = true)) + .anyExecutor { _, arguments -> + assertEquals(emptyList(), arguments.get>("targets")) + executions += "entities-empty" + } + .register() + + val messages = alphaConnection.trackIncoming(SystemChatPacket::class.java) + assertFalse(runCommandReporting(alpha, "single-player Missing")) + assertFalse(runCommandReporting(alpha, "single-entity ${UUID.randomUUID()}")) + assertFalse(runCommandReporting(alpha, "single-player @p[gamemode=spectator]")) + assertFalse(runCommandReporting(alpha, "single-player @p[limit=2]")) + assertFalse(runCommandReporting(alpha, "single-player @a")) + assertFalse(runCommandReporting(alpha, "single-entity @e")) + assertFalse(runCommandReporting(alpha, "players-required @e")) + assertFalse(runCommandReporting(alpha, "players-required @a[gamemode=spectator]")) + assertTrue(runCommandReporting(alpha, "players-empty @a[gamemode=spectator]")) + assertFalse(runCommandReporting(alpha, "entities-required @e[type=minecraft:creeper]")) + assertTrue(runCommandReporting(alpha, "entities-empty @e[type=minecraft:creeper]")) + + assertEquals(listOf("players-empty", "entities-empty"), executions) + val rendered = messages.collect().map { packet -> plain(packet.message) } + assertEquals(3, rendered.count { it.contains("No entity matched the selector") }) + assertEquals(2, rendered.count { it.contains("No entities matched the selector") }) + assertEquals(2, rendered.count { it.contains("Expected a single player") }) + assertEquals(1, rendered.count { it.contains("Expected a single entity") }) + assertEquals(1, rendered.count { it.contains("Only players can be targeted") }) + + val player = PlayerArgument("target").toDefinition() + val entity = EntityArgument("target").toDefinition() + + val noTarget = assertThrows(CommandSyntaxException::class.java) { + player.read(alpha, "Missing") + } + assertEquals("No entity matched the selector", noTarget.rawMessage.string) + assertThrows(CommandSyntaxException::class.java) { player.read(alpha, "@a") } + assertThrows(CommandSyntaxException::class.java) { entity.read(alpha, "@e") } + + // The console has no instance to resolve a UUID against, so nothing matches. + val consoleUuid = assertThrows(CommandSyntaxException::class.java) { + entity.read(env.process().command().consoleSender, UUID.randomUUID().toString()) + } + assertEquals("No entity matched the selector", consoleUuid.rawMessage.string) + } + } + + @Test + fun `entity type parsing and all Kotlin DSL receivers keep typed definitions and defaults`(env: Env) { + withEntityPlatform(env) { manager, _, alpha, bravo, zombie -> + val type = AtomicReference() + CommandAPICommand("entity-type") + .withArguments(EntityTypeArgument("type")) + .anyExecutor { _, arguments -> type.set(arguments.get("type")) } + .register() + + assertTrue(runCommand(alpha, "entity-type minecraft:zombie")) + assertSame(EntityType.ZOMBIE, type.get()) + assertFalse(runCommand(alpha, "entity-type minecraft:not_real")) + + val command = CommandAPICommand("entity-dsl-command") + .playerArgument("player") + .playersArgument("players") + .entityArgument("entity") + .entitiesArgument("entities") + .entityTypeArgument("type") + .anyExecutor { _, _ -> } + .toDefinition() + val tree = CommandTree("entity-dsl-tree") + .playerArgument("player") { anyExecutor { _, _ -> } } + .playersArgument("players") { anyExecutor { _, _ -> } } + .entityArgument("entity") { anyExecutor { _, _ -> } } + .entitiesArgument("entities") { anyExecutor { _, _ -> } } + .entityTypeArgument("type") { + setOptional(EntityType.ZOMBIE) + anyExecutor { _, _ -> } + } + .toDefinition() + val child = CommandTree("entity-dsl-child") + .then( + StringArgument("prefix") + .playerArgument("player") { anyExecutor { _, _ -> } } + .playersArgument("players") { anyExecutor { _, _ -> } } + .entityArgument("entity") { anyExecutor { _, _ -> } } + .entitiesArgument("entities") { anyExecutor { _, _ -> } } + .entityTypeArgument("type") { + setOptional(EntityType.ZOMBIE) + anyExecutor { _, _ -> } + }, + ) + .toDefinition() + + assertEquals( + listOf("player", "players", "entity", "entities", "type"), + command.paths.single().arguments.map { it.nodeName }, + ) + assertEquals( + listOf( + listOf("player"), + listOf("players"), + listOf("entity"), + listOf("entities"), + listOf("type"), + ), + tree.paths.map { path -> path.arguments.map { it.nodeName } }, + ) + assertEquals( + listOf( + listOf("prefix", "player"), + listOf("prefix", "players"), + listOf("prefix", "entity"), + listOf("prefix", "entities"), + listOf("prefix", "type"), + ), + child.paths.map { path -> path.arguments.map { it.nodeName } }, + ) + listOf(tree, child).forEach { definition -> + val last = definition.paths.single { path -> + path.arguments.last().nodeName == "type" + }.arguments.last() + assertTrue(last.optional) + assertSame(EntityType.ZOMBIE, last.defaultValue?.invoke(alpha)) + } + + assertEquals("Alpha", PlayerArgument("player").toDefinition().stringify(alpha)) + assertEquals("Alpha,Bravo", PlayersArgument("players").toDefinition().stringify(listOf(alpha, bravo))) + assertEquals(zombie.uuid.toString(), EntityArgument("entity").toDefinition().stringify(zombie)) + assertEquals("minecraft:zombie", EntityTypeArgument("type").toDefinition().stringify(EntityType.ZOMBIE)) + } + } + + private inline fun withEntityPlatform( + env: Env, + block: ( + net.minestom.server.command.CommandManager, + TestConnection, + Player, + Player, + Entity, + ) -> Unit, + ) { + val manager = env.process().command() + val platform = MinestomCommandAPIPlatform(manager, MinestomCommandOwnership()) + val instance = env.createEmptyInstance() + val alphaConnection = env.createConnection(profile("Alpha")) + var alpha: Player? = null + var bravo: Player? = null + var zombie: Entity? = null + CommandAPI.installPlatform(platform) + try { + val connectedAlpha = connect(alphaConnection, instance) + alpha = connectedAlpha + check(env.tickWhile({ connectedAlpha.instance !== instance }, Duration.ofSeconds(1))) { + "Alpha was not spawned in the requested test instance" + } + val connectedBravo = connect(env.createConnection(profile("Bravo")), instance) + bravo = connectedBravo + check(env.tickWhile({ connectedBravo.instance !== instance }, Duration.ofSeconds(1))) { + "Bravo was not spawned in the requested test instance" + } + zombie = Entity(EntityType.ZOMBIE).also { entity -> + entity.setInstance(instance, Pos(4.0, 0.0, 0.0)).join() + } + block(manager, alphaConnection, alpha, bravo, zombie) + } finally { + try { + zombie?.remove() + bravo?.remove() + alpha?.remove() + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + env.destroyInstance(instance) + } + } + + private fun connect(connection: TestConnection, instance: Instance): Player { + val connected = CompletableFuture() + Thread.startVirtualThread { + runCatching { connection.connect(instance, Pos.ZERO) } + .onSuccess(connected::complete) + .onFailure(connected::completeExceptionally) + } + return connected.join() + } + + private fun profile(name: String) = GameProfile(UUID.randomUUID(), name) + + private fun plain(component: Component): String = + PlainTextComponentSerializer.plainText().serialize(component) + + private class PlayerArguments(arguments: CommandArguments) { + val target: Player by arguments + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/KotlinDslIntegrationTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/KotlinDslIntegrationTest.kt new file mode 100644 index 000000000..04009a4a2 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/KotlinDslIntegrationTest.kt @@ -0,0 +1,553 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.CommandTree +import dev.slne.surf.api.minestom.command.argument.* +import dev.slne.surf.api.minestom.command.dsl.* +import dev.slne.surf.api.minestom.command.exception.CommandValidationException +import dev.slne.surf.api.minestom.command.executor.CommandArguments +import dev.slne.surf.api.minestom.command.executor.ParsedArgument +import dev.slne.surf.api.minestom.command.suggestion.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import net.kyori.adventure.identity.Identity +import net.kyori.adventure.text.Component +import net.minestom.server.command.CommandSender +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.Player +import net.minestom.server.instance.Instance +import net.minestom.server.network.player.GameProfile +import net.minestom.server.tag.TagHandler +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import net.minestom.testing.TestConnection +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.* +import java.util.concurrent.CompletableFuture + +@OptIn(ExperimentalCoroutinesApi::class) +@EnvTest +class KotlinDslIntegrationTest { + @Test + fun `message command resolves player suggestions and dispatches through a suspend player executor`( + env: Env, + ) = runTest { + env.process().connection().setPlayerProvider { connection, profile -> + TestPermissionPlayer(connection, profile, setOf("lobby.command.message")) + } + try { + withPlatform(env) { manager -> + val directory = PlayerDirectory(listOf("Ranger", "Zeta")) + val deliveries = mutableListOf>() + val testScope = this + + commandAPICommand("message") { + withPermission("lobby.command.message") + playerArgument("target") { + replaceSuggestionsAsync { info -> directory.search(info.currentArg) } + } + greedyStringArgument("message") + playerExecutorSuspend(scope = { testScope }) { player, args -> + val target: Player by args + val message: String by args + deliveries += Triple(player, target, message) + } + } + + val instance = env.createEmptyInstance() + var sender: Player? = null + var target: Player? = null + try { + sender = connect(env.createConnection(profile("Sender")), instance) + target = connect(env.createConnection(profile("Ranger")), instance) + + val result = runCommand(sender, "message Ranger hello there") + + assertTrue(result) + assertTrue(deliveries.isEmpty()) + runCurrent() + assertEquals(listOf(Triple(sender, target, "hello there")), deliveries) + } finally { + target?.remove() + sender?.remove() + env.destroyInstance(instance) + } + } + } finally { + env.process().connection().setPlayerProvider(null) + } + } + + @Test + fun `ExecutorDsl exposes ExecutionInfo executes and suspend facades alongside the executor package`( + env: Env, + ) = runTest { + withPlatform(env) { manager -> + val infoInputs = mutableListOf() + val executesCalls = mutableListOf() + val suspendCalls = mutableListOf() + val testScope = this + + CommandAPICommand("execution-info-facade") + .anyExecutionInfo { info -> infoInputs += info.input } + .register() + CommandAPICommand("executes-facade") + .executes { _, _ -> executesCalls += "invoked" } + .register() + CommandAPICommand("console-suspend-facade") + .consoleExecutorSuspend(scope = { testScope }) { _, _ -> suspendCalls += "invoked" } + .register() + + val argumentExecutesCalls = mutableListOf() + CommandAPICommand("executes-facade-argument") + .literalArgument("go") { + executesConsole { _, _ -> argumentExecutesCalls += "invoked" } + } + .register() + + assertTrue(runCommand(manager.consoleSender, "execution-info-facade")) + assertEquals(listOf("execution-info-facade"), infoInputs) + + assertTrue(runCommand(manager.consoleSender, "executes-facade")) + assertEquals(listOf("invoked"), executesCalls) + + assertTrue(runCommand(manager.consoleSender, "console-suspend-facade")) + assertTrue(suspendCalls.isEmpty()) + runCurrent() + assertEquals(listOf("invoked"), suspendCalls) + + assertTrue(runCommand(manager.consoleSender, "executes-facade-argument go")) + assertEquals(listOf("invoked"), argumentExecutesCalls) + } + } + + @Test + fun `replaceSuggestionsAsync stores a suspending provider that resolves against the current argument`( + env: Env, + ) = runTest { + val directory = PlayerDirectory(listOf("Ranger", "Zeta")) + val argument = PlayerArgument("target") + .replaceSuggestionsAsync { info -> directory.search(info.currentArg) } + + val mode = argument.toDefinition().suggestions + check(mode is SuggestionMode.Replace) + val info = suggestionInfo(env, currentArg = "Ra") + + assertEquals(listOf("Ranger"), mode.provider.suggest(info).map(StringTooltip::suggestion)) + } + + @Test + fun `every new argument builder exists on the CommandAPICommand receiver family`() { + val definition = CommandAPICommand("primitive-catalog") + .longArgument("along", min = 1, max = 10) + .floatArgument("afloat", min = 1f, max = 10f) + .doubleArgument("adouble", min = 1.0, max = 10.0) + .enumArgument("mode", CatalogMode.entries) + .uuidArgument("id") + .integerRangeArgument("irange") + .floatRangeArgument("frange") + .multiLiteralArgument("choice", "left", "right") + .textArgument("text") + .booleanArgument("flag", optional = true) + .greedyStringArgument("rest", optional = true) + .anyExecutor { _, _ -> } + .toDefinition() + + val kinds = definition.paths.single().arguments.associate { it.nodeName to it.kind } + assertEquals(ArgumentKind.Long(1, 10), kinds.getValue("along")) + assertEquals(ArgumentKind.Float(1f, 10f), kinds.getValue("afloat")) + assertEquals(ArgumentKind.Double(1.0, 10.0), kinds.getValue("adouble")) + assertTrue(kinds.getValue("mode") is ArgumentKind.Enum<*>) + assertEquals(ArgumentKind.Uuid, kinds.getValue("id")) + assertEquals(ArgumentKind.IntegerRange, kinds.getValue("irange")) + assertEquals(ArgumentKind.FloatRange, kinds.getValue("frange")) + assertEquals( + ArgumentKind.MultiLiteral(listOf("left", "right")), + kinds.getValue("choice"), + ) + assertEquals(ArgumentKind.Boolean, kinds.getValue("flag")) + assertEquals(ArgumentKind.Text, kinds.getValue("text")) + assertEquals(ArgumentKind.GreedyString, kinds.getValue("rest")) + assertTrue(definition.paths.single().arguments.first { it.nodeName == "flag" }.optional) + } + + @Test + fun `every new argument builder exists on the CommandTree receiver family`() { + val tree = CommandTree("primitive-catalog-tree") + .longArgument("along") { anyExecutor { _, _ -> } } + .floatArgument("afloat") { anyExecutor { _, _ -> } } + .doubleArgument("adouble") { anyExecutor { _, _ -> } } + .enumArgument("mode", CatalogMode.entries) { anyExecutor { _, _ -> } } + .uuidArgument("id") { anyExecutor { _, _ -> } } + .integerRangeArgument("irange") { anyExecutor { _, _ -> } } + .floatRangeArgument("frange") { anyExecutor { _, _ -> } } + .multiLiteralArgument("choice", "left", "right") { anyExecutor { _, _ -> } } + .booleanArgument("flag") { anyExecutor { _, _ -> } } + .textArgument("text") { anyExecutor { _, _ -> } } + .greedyStringArgument("rest") { anyExecutor { _, _ -> } } + .toDefinition() + + assertEquals( + listOf( + listOf("along"), + listOf("afloat"), + listOf("adouble"), + listOf("mode"), + listOf("id"), + listOf("irange"), + listOf("frange"), + listOf("choice"), + listOf("flag"), + listOf("text"), + listOf("rest"), + ), + tree.paths.map { path -> path.arguments.map { it.nodeName } }, + ) + } + + @Test + fun `every new argument builder exists on the nested Argument receiver family`() { + val tree = CommandTree("primitive-catalog-child") + .then( + StringArgument("prefix") + .longArgument("along") { anyExecutor { _, _ -> } } + .floatArgument("afloat") { anyExecutor { _, _ -> } } + .doubleArgument("adouble") { anyExecutor { _, _ -> } } + .enumArgument("mode", CatalogMode.entries) { anyExecutor { _, _ -> } } + .uuidArgument("id") { anyExecutor { _, _ -> } } + .integerRangeArgument("irange") { anyExecutor { _, _ -> } } + .floatRangeArgument("frange") { anyExecutor { _, _ -> } } + .multiLiteralArgument("choice", "left", "right") { anyExecutor { _, _ -> } } + .booleanArgument("flag") { anyExecutor { _, _ -> } } + .textArgument("text") { anyExecutor { _, _ -> } } + .greedyStringArgument("rest") { anyExecutor { _, _ -> } }, + ) + .toDefinition() + + assertEquals( + listOf( + listOf("prefix", "along"), + listOf("prefix", "afloat"), + listOf("prefix", "adouble"), + listOf("prefix", "mode"), + listOf("prefix", "id"), + listOf("prefix", "irange"), + listOf("prefix", "frange"), + listOf("prefix", "choice"), + listOf("prefix", "flag"), + listOf("prefix", "text"), + listOf("prefix", "rest"), + ), + tree.paths.map { path -> path.arguments.map { it.nodeName } }, + ) + } + + @Test + fun `string suggestion DSL family covers vararg provider and async forms for replace and include`( + env: Env, + ) = runTest { + val info = suggestionInfo(env, currentArg = "a") + + // A lambda literal resolves to the `replaceSuggestions(ArgumentSuggestions)` / + // `includeSuggestions(ArgumentSuggestions)` member; an explicitly typed + // `(SuggestionInfo) -> Collection` selects the provider overload. + val stringProvider: (SuggestionInfo) -> Collection = { listOf("provided") } + val replaceVararg = StringArgument("a").replaceSuggestions("apple", "avocado") + val replaceProvider = StringArgument("a").replaceSuggestions(stringProvider) + val replaceAsync = StringArgument("a").replaceSuggestionsAsync { _ -> listOf("async") } + val includeVararg = StringArgument("a").includeSuggestions("apple") + val includeProvider = StringArgument("a").includeSuggestions(stringProvider) + val includeAsync = StringArgument("a").includeSuggestionsAsync { _ -> listOf("async") } + + assertEquals(listOf("apple", "avocado"), suggestedStrings(replaceVararg, info, replace = true)) + assertEquals(listOf("provided"), suggestedStrings(replaceProvider, info, replace = true)) + assertEquals(listOf("async"), suggestedStrings(replaceAsync, info, replace = true)) + assertEquals(listOf("apple"), suggestedStrings(includeVararg, info, replace = false)) + assertEquals(listOf("provided"), suggestedStrings(includeProvider, info, replace = false)) + assertEquals(listOf("async"), suggestedStrings(includeAsync, info, replace = false)) + } + + @Test + fun `string tooltip suggestion DSL family covers vararg provider and async forms`(env: Env) = runTest { + val info = suggestionInfo(env, currentArg = "a") + val tooltip = Component.text("tip") + + val replaceVararg = StringArgument("a") + .replaceSuggestionsWithTooltips(StringTooltip("apple", tooltip)) + val replaceProvider = StringArgument("a") + .replaceSuggestionsWithTooltips { _ -> listOf(StringTooltip("provided", tooltip)) } + val replaceAsync = StringArgument("a") + .replaceSuggestionsWithTooltipsAsync { _ -> listOf(StringTooltip("async", tooltip)) } + val includeVararg = StringArgument("a") + .includeSuggestionsWithTooltips(StringTooltip("apple", tooltip)) + val includeProvider = StringArgument("a") + .includeSuggestionsWithTooltips { _ -> listOf(StringTooltip("provided", tooltip)) } + val includeAsync = StringArgument("a") + .includeSuggestionsWithTooltipsAsync { _ -> listOf(StringTooltip("async", tooltip)) } + + assertEquals(listOf(StringTooltip("apple", tooltip)), replaceMode(replaceVararg).provider.suggest(info)) + assertEquals(listOf(StringTooltip("provided", tooltip)), replaceMode(replaceProvider).provider.suggest(info)) + assertEquals(listOf(StringTooltip("async", tooltip)), replaceMode(replaceAsync).provider.suggest(info)) + assertEquals(listOf(StringTooltip("apple", tooltip)), includeMode(includeVararg).provider.suggest(info)) + assertEquals(listOf(StringTooltip("provided", tooltip)), includeMode(includeProvider).provider.suggest(info)) + assertEquals(listOf(StringTooltip("async", tooltip)), includeMode(includeAsync).provider.suggest(info)) + } + + @Test + fun `safe typed suggestion DSL family covers vararg provider and async forms for replace and include`( + env: Env, + ) = runTest { + val info = suggestionInfo(env, currentArg = "1") + + // A lambda literal resolves to the `replaceSafeSuggestions(SafeSuggestions)` / + // `includeSafeSuggestions(SafeSuggestions)` member; an explicitly typed + // `(SuggestionInfo) -> Collection` selects the provider overload. + val replaceProviderFn: (SuggestionInfo) -> Collection = { listOf(3) } + val includeProviderFn: (SuggestionInfo) -> Collection = { listOf(6) } + val replaceVararg = IntegerArgument("n") + .replaceSafeSuggestions(1, 2) + val replaceProvider = IntegerArgument("n") + .replaceSafeSuggestions(replaceProviderFn) + val replaceAsync = IntegerArgument("n") + .replaceSafeSuggestionsAsync { _ -> listOf(4) } + val includeVararg = IntegerArgument("n") + .includeSafeSuggestions(5) + val includeProvider = IntegerArgument("n") + .includeSafeSuggestions(includeProviderFn) + val includeAsync = IntegerArgument("n") + .includeSafeSuggestionsAsync { _ -> listOf(7) } + + assertEquals(listOf(1, 2), replaceSafeMode(replaceVararg).provider.suggest(info).map { it.suggestion }) + assertEquals(listOf(3), replaceSafeMode(replaceProvider).provider.suggest(info).map { it.suggestion }) + assertEquals(listOf(4), replaceSafeMode(replaceAsync).provider.suggest(info).map { it.suggestion }) + assertEquals(listOf(5), includeSafeMode(includeVararg).provider.suggest(info).map { it.suggestion }) + assertEquals(listOf(6), includeSafeMode(includeProvider).provider.suggest(info).map { it.suggestion }) + assertEquals(listOf(7), includeSafeMode(includeAsync).provider.suggest(info).map { it.suggestion }) + } + + @Test + fun `safe typed tooltip suggestion DSL family covers vararg provider and async forms`(env: Env) = runTest { + val info = suggestionInfo(env, currentArg = "1") + val tooltip = Component.text("tip") + + val replaceVararg = IntegerArgument("n") + .replaceSafeSuggestionsWithTooltips(Tooltip(1, tooltip)) + val replaceProvider = IntegerArgument("n") + .replaceSafeSuggestionsWithTooltips { _ -> listOf(Tooltip(2, tooltip)) } + val replaceAsync = IntegerArgument("n") + .replaceSafeSuggestionsWithTooltipsAsync { _ -> listOf(Tooltip(3, tooltip)) } + val includeVararg = IntegerArgument("n") + .includeSafeSuggestionsWithTooltips(Tooltip(4, tooltip)) + val includeProvider = IntegerArgument("n") + .includeSafeSuggestionsWithTooltips { _ -> listOf(Tooltip(5, tooltip)) } + val includeAsync = IntegerArgument("n") + .includeSafeSuggestionsWithTooltipsAsync { _ -> listOf(Tooltip(6, tooltip)) } + + assertEquals(listOf(Tooltip(1, tooltip)), replaceSafeMode(replaceVararg).provider.suggest(info)) + assertEquals(listOf(Tooltip(2, tooltip)), replaceSafeMode(replaceProvider).provider.suggest(info)) + assertEquals(listOf(Tooltip(3, tooltip)), replaceSafeMode(replaceAsync).provider.suggest(info)) + assertEquals(listOf(Tooltip(4, tooltip)), includeSafeMode(includeVararg).provider.suggest(info)) + assertEquals(listOf(Tooltip(5, tooltip)), includeSafeMode(includeProvider).provider.suggest(info)) + assertEquals(listOf(Tooltip(6, tooltip)), includeSafeMode(includeAsync).provider.suggest(info)) + } + + @Test + fun `branch suggestion DSL adapts a SuggestionsBranch for both replace and include modes`(env: Env) = runTest { + val sender = env.process().command().consoleSender + val branch = SuggestionsBranch.suggest( + ArgumentSuggestions.strings("staff", "public"), + ) + + val replaced = StringArgument("mode").replaceSuggestions(branch) + val included = StringArgument("mode").includeSuggestions(branch) + val info = SuggestionInfo(sender, CommandArguments.empty(), "", "") + + assertEquals(listOf("staff", "public"), replaceMode(replaced).provider.suggest(info).map(StringTooltip::suggestion)) + assertEquals(listOf("staff", "public"), includeMode(included).provider.suggest(info).map(StringTooltip::suggestion)) + } + + @Test + fun `branch suggestions are empty for a previous value the branch rejects`(env: Env) = runTest { + val sender = env.process().command().consoleSender + val provider = ArgumentSuggestions.branch( + SuggestionsBranch.suggest( + ArgumentSuggestions.strings("a", "b"), + ArgumentSuggestions.strings("x"), + ), + ) + + fun info(vararg previous: String) = SuggestionInfo( + sender = sender, + previousArgs = CommandArguments.of( + previous.mapIndexed { index, raw -> ParsedArgument(index.toString(), raw, raw, present = true) }, + ), + currentInput = previous.joinToString(" ", postfix = " "), + currentArg = "", + ) + + assertEquals(emptyList(), provider.suggest(info("zzz"))) + assertEquals(listOf("x"), provider.suggest(info("a")).map(StringTooltip::suggestion)) + } + + @Test + fun `sibling branches resolving to a mix of null and non null providers use the non null ones`(env: Env) = runTest { + val sender = env.process().command().consoleSender + val branch = SuggestionsBranch.suggest(ArgumentSuggestions.strings("go")) + .branch( + SuggestionsBranch.suggest(ArgumentSuggestions.strings("x")), + SuggestionsBranch.suggest(ArgumentSuggestions.strings("x"), ArgumentSuggestions.strings("y")), + ) + + val provider = branch.getNextSuggestion(sender, "go", "x") + + assertNotNull(provider) + assertEquals( + listOf("y"), + provider!!.suggest(SuggestionInfo(sender, CommandArguments.empty(), "go x ", "")).map(StringTooltip::suggestion), + ) + } + + @Test + fun `merged providers isolate failures and still propagate cancellation`(env: Env) = runTest { + val info = SuggestionInfo(env.process().command().consoleSender, CommandArguments.empty(), "", "") + val failing = ArgumentSuggestions { throw IllegalStateException("broken provider") } + val cancelling = ArgumentSuggestions { throw CancellationException("stop") } + + val merged = ArgumentSuggestions.merge(failing, ArgumentSuggestions.strings("ok")) + val cancelled = runCatching { ArgumentSuggestions.merge(cancelling, ArgumentSuggestions.strings("ok")).suggest(info) } + + assertEquals(listOf("ok"), merged.suggest(info).map(StringTooltip::suggestion)) + assertTrue(cancelled.exceptionOrNull() is CancellationException) + } + + @Test + fun `subcommand requirements and permissions hide the subcommand instead of reporting success`(env: Env) { + withPlatform(env) { manager -> + val executed = mutableListOf() + CommandAPICommand("gated") + .anyExecutor { _, _ -> executed += "root" } + .withSubcommand( + CommandAPICommand("blocked") + .withRequirement { false } + .anyExecutor { _, _ -> executed += "blocked" }, + ) + .withSubcommand( + CommandAPICommand("admin") + .withPermission("lobby.admin") + .anyExecutor { _, _ -> executed += "admin" }, + ) + .withSubcommand(CommandAPICommand("open").anyExecutor { _, _ -> executed += "open" }) + .register() + val console = manager.consoleSender + val plain = PlainSender() + + assertTrue(runCommand(console, "gated")) + assertTrue(runCommand(console, "gated open")) + assertTrue(runCommand(console, "gated admin")) + assertTrue(runCommand(plain, "gated")) + assertTrue(runCommand(plain, "gated open")) + assertFalse(runCommand(console, "gated blocked")) + assertFalse(runCommandReporting(console, "gated blocked")) + assertFalse(runCommand(plain, "gated admin")) + assertEquals(listOf("root", "open", "admin", "root", "open"), executed) + } + } + + @Test + fun `a suggestion mode rejected at registration is not offered as a silently broken shortcut`() { + val command = CommandAPICommand("rejected-literal") + .withArguments( + LiteralArgument("fixed") + .replaceSuggestions("unused"), + ) + .anyExecutor { _, _ -> } + + assertThrows(CommandValidationException::class.java) { + MinestomCommandCompiler().compile(command.toDefinition(), namespace = null) + } + } + + private suspend fun suggestedStrings(argument: Argument, info: SuggestionInfo, replace: Boolean): List = + if (replace) { + replaceMode(argument).provider.suggest(info).map(StringTooltip::suggestion) + } else { + includeMode(argument).provider.suggest(info).map(StringTooltip::suggestion) + } + + private fun replaceMode(argument: Argument): SuggestionMode.Replace { + val mode = argument.toDefinition().suggestions + check(mode is SuggestionMode.Replace) + return mode + } + + private fun includeMode(argument: Argument): SuggestionMode.Include { + val mode = argument.toDefinition().suggestions + check(mode is SuggestionMode.Include) + return mode + } + + private fun replaceSafeMode(argument: Argument): SuggestionMode.ReplaceSafe { + val mode = argument.toDefinition().suggestions + check(mode is SuggestionMode.ReplaceSafe) + return mode + } + + private fun includeSafeMode(argument: Argument): SuggestionMode.IncludeSafe { + val mode = argument.toDefinition().suggestions + check(mode is SuggestionMode.IncludeSafe) + return mode + } + + private fun suggestionInfo(env: Env, currentArg: String): SuggestionInfo = SuggestionInfo( + sender = env.process().command().consoleSender, + previousArgs = CommandArguments.empty(), + currentInput = currentArg, + currentArg = currentArg, + ) + + private inline fun withPlatform(env: Env, block: (net.minestom.server.command.CommandManager) -> Unit) { + val manager = env.process().command() + val platform = MinestomCommandAPIPlatform(manager, MinestomCommandOwnership()) + CommandAPI.installPlatform(platform) + try { + block(manager) + } finally { + try { + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + } + } + + private fun connect(connection: TestConnection, instance: Instance): Player { + val player = CompletableFuture() + Thread.startVirtualThread { + runCatching { connection.connect(instance, Pos.ZERO) } + .onSuccess(player::complete) + .onFailure(player::completeExceptionally) + } + return player.join() + } + + private fun profile(name: String) = GameProfile(UUID.randomUUID(), name) + + private class PlainSender : CommandSender { + override fun identity(): Identity = Identity.nil() + + override fun tagHandler(): TagHandler = TagHandler.newHandler() + } + + private class PlayerDirectory(private val names: List) { + fun search(prefix: String): List = names.filter { name -> name.startsWith(prefix, ignoreCase = true) } + } + + private enum class CatalogMode { + FIRST, + SECOND, + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinecraftArgumentIntegrationTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinecraftArgumentIntegrationTest.kt new file mode 100644 index 000000000..46fb96d1f --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinecraftArgumentIntegrationTest.kt @@ -0,0 +1,281 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import com.mojang.brigadier.exceptions.CommandSyntaxException +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.CommandTree +import dev.slne.surf.api.minestom.command.argument.* +import dev.slne.surf.api.minestom.command.dsl.* +import dev.slne.surf.api.minestom.command.suggestion.ArgumentSuggestions +import net.kyori.adventure.key.Key +import net.minestom.server.MinecraftServer +import net.minestom.server.color.TeamColor +import net.minestom.server.command.ArgumentParserType +import net.minestom.server.command.CommandManager +import net.minestom.server.command.builder.arguments.ArgumentType +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.GameMode +import net.minestom.server.entity.Player +import net.minestom.server.instance.Instance +import net.minestom.server.network.player.GameProfile +import net.minestom.server.particle.Particle +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import net.minestom.testing.TestConnection +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.time.Duration +import java.util.* +import java.util.concurrent.CompletableFuture + +@EnvTest +class MinecraftArgumentIntegrationTest { + @Test + fun `resource location argument parses namespaced keys and rejects invalid ones`(env: Env) { + val sender = env.process().command().consoleSender + val compiled = ResourceLocationArgument("key").toDefinition() + + assertEquals(Key.key("minecraft:diamond"), compiled.read(sender, "minecraft:diamond")) + assertThrows(CommandSyntaxException::class.java) { compiled.read(sender, "Not_Valid!") } + + assertEquals(ArgumentParserType.RESOURCE_LOCATION, compiled.declaration().parser) + assertNull(compiled.declaration().properties) + assertEquals( + "minecraft:diamond", + ResourceLocationArgument("key").toDefinition().stringify(Key.key("minecraft:diamond")), + ) + } + + @Test + fun `time argument parses suffixed durations and rejects unknown suffixes`(env: Env) { + val sender = env.process().command().consoleSender + val compiled = TimeArgument("delay").toDefinition() + + // Vanilla counts a day as 24000 ticks, so "2d" is 40 real minutes rather than two days. + assertEquals(Duration.ofMinutes(40), compiled.read(sender, "2d")) + assertEquals(Duration.ofSeconds(3), compiled.read(sender, "3s")) + assertEquals( + Duration.ofMillis(100L * MinecraftServer.TICK_MS), + compiled.read(sender, "100t"), + ) + assertEquals( + Duration.ofMillis(50L * MinecraftServer.TICK_MS), + compiled.read(sender, "50"), + ) + assertThrows(CommandSyntaxException::class.java) { compiled.read(sender, "5x") } + assertThrows(CommandSyntaxException::class.java) { compiled.read(sender, "abc") } + + assertEquals(ArgumentParserType.TIME, compiled.declaration().parser) + assertEquals( + ArgumentType.Time("expected").nodeProperties()!!.toList(), + compiled.declaration().properties!!.toList(), + ) + assertEquals( + "100t", + TimeArgument("delay").toDefinition().stringify(Duration.ofMillis(100L * MinecraftServer.TICK_MS)), + ) + } + + @Test + fun `team color argument parses vanilla color names and rejects unknown colors`(env: Env) { + val sender = env.process().command().consoleSender + val compiled = TeamColorArgument("color").toDefinition() + + assertEquals(TeamColor.RED, compiled.read(sender, "red")) + assertThrows(CommandSyntaxException::class.java) { compiled.read(sender, "not_a_color") } + + assertEquals(ArgumentParserType.TEAM_COLOR, compiled.declaration().parser) + assertNull(compiled.declaration().properties) + assertEquals("red", TeamColorArgument("color").toDefinition().stringify(TeamColor.RED)) + } + + @Test + fun `particle argument parses namespaced particle keys and rejects unknown particles`(env: Env) { + val sender = env.process().command().consoleSender + val compiled = ParticleArgument("particle").toDefinition() + + val flame = compiled.read(sender, "minecraft:flame") + assertEquals(Particle.fromKey("minecraft:flame"), flame) + assertThrows(CommandSyntaxException::class.java) { compiled.read(sender, "minecraft:not_a_particle") } + + assertEquals(ArgumentParserType.PARTICLE, compiled.declaration().parser) + assertNull(compiled.declaration().properties) + assertEquals("minecraft:flame", ParticleArgument("particle").toDefinition().stringify(flame)) + } + + @Test + fun `game mode argument parses the vanilla names and rejects any other casing`(env: Env) { + val sender = env.process().command().consoleSender + val compiled = GameModeArgument("mode").toDefinition() + + assertEquals(GameMode.SURVIVAL, compiled.read(sender, "survival")) + assertEquals(GameMode.CREATIVE, compiled.read(sender, "creative")) + assertThrows(CommandSyntaxException::class.java) { compiled.read(sender, "CREATIVE") } + assertThrows(CommandSyntaxException::class.java) { compiled.read(sender, "not_a_mode") } + + assertEquals(ArgumentParserType.GAMEMODE, compiled.declaration().parser) + assertNull(compiled.declaration().properties) + assertEquals("survival", GameModeArgument("mode").toDefinition().stringify(GameMode.SURVIVAL)) + } + + @Test + fun `custom suggestion modes are permitted for game mode arguments`(env: Env) { + withPlatform(env) { + CommandAPICommand("allow-game-mode") + .withArguments( + GameModeArgument("mode").includeSuggestions(ArgumentSuggestions.strings("extra")), + ) + .anyExecutor { _, _ -> } + .register() + + assertTrue(runCommand(env.process().command().consoleSender, "allow-game-mode survival")) + } + } + + @Test + fun `every new value argument is reachable through the executor and reports its native parser`(env: Env) { + val captured = mutableMapOf() + withPlayerPlatform(env) { manager, player -> + CommandAPICommand("resource-location-cmd") + .withArguments(ResourceLocationArgument("resourceLocationValue")) + .anyExecutor { _, arguments -> + captured["resource-location-cmd"] = arguments.get("resourceLocationValue") + } + .register() + CommandAPICommand("time-cmd").withArguments(TimeArgument("timeValue")) + .anyExecutor { _, arguments -> captured["time-cmd"] = arguments.get("timeValue") } + .register() + CommandAPICommand("team-color-cmd").withArguments(TeamColorArgument("teamColorValue")) + .anyExecutor { _, arguments -> + captured["team-color-cmd"] = arguments.get("teamColorValue") + } + .register() + CommandAPICommand("particle-cmd").withArguments(ParticleArgument("particleValue")) + .anyExecutor { _, arguments -> captured["particle-cmd"] = arguments.get("particleValue") } + .register() + CommandAPICommand("game-mode-cmd").withArguments(GameModeArgument("gameModeValue")) + .anyExecutor { _, arguments -> captured["game-mode-cmd"] = arguments.get("gameModeValue") } + .register() + + assertTrue(runCommand(player, "resource-location-cmd minecraft:diamond")) + assertEquals(Key.key("minecraft:diamond"), captured["resource-location-cmd"]) + + assertTrue(runCommand(player, "time-cmd 2d")) + assertEquals(Duration.ofMinutes(40), captured["time-cmd"]) + + assertTrue(runCommand(player, "team-color-cmd red")) + assertEquals(TeamColor.RED, captured["team-color-cmd"]) + + assertTrue(runCommand(player, "particle-cmd minecraft:flame")) + assertEquals(Particle.fromKey("minecraft:flame"), captured["particle-cmd"]) + + assertTrue(runCommand(player, "game-mode-cmd survival")) + assertEquals(GameMode.SURVIVAL, captured["game-mode-cmd"]) + + val packet = declaredCommands(env, player) + fun parserOf(nodeName: String) = packet.nodes.single { node -> node.name == nodeName }.parser + assertEquals(ArgumentParserType.RESOURCE_LOCATION, parserOf("resourceLocationValue")) + assertEquals(ArgumentParserType.TIME, parserOf("timeValue")) + assertEquals(ArgumentParserType.TEAM_COLOR, parserOf("teamColorValue")) + assertEquals(ArgumentParserType.PARTICLE, parserOf("particleValue")) + assertEquals(ArgumentParserType.GAMEMODE, parserOf("gameModeValue")) + } + } + + @Test + fun `all six remaining minecraft value argument DSL builders register across command tree and child argument receivers`() { + val command = CommandAPICommand("minecraft-dsl-command") + .resourceLocationArgument("resourceLocation") + .timeArgument("time") + .teamColorArgument("teamColor") + .particleArgument("particle") + .gameModeArgument("gameMode") + .commandArgument("nested") + .anyExecutor { _, _ -> } + .toDefinition() + + val tree = CommandTree("minecraft-dsl-tree") + .resourceLocationArgument("resourceLocation") { anyExecutor { _, _ -> } } + .timeArgument("time") { anyExecutor { _, _ -> } } + .teamColorArgument("teamColor") { anyExecutor { _, _ -> } } + .particleArgument("particle") { anyExecutor { _, _ -> } } + .gameModeArgument("gameMode") { anyExecutor { _, _ -> } } + .commandArgument("nested") { anyExecutor { _, _ -> } } + .toDefinition() + + val child = CommandTree("minecraft-dsl-child") + .then( + StringArgument("prefix") + .resourceLocationArgument("resourceLocation") { anyExecutor { _, _ -> } } + .timeArgument("time") { anyExecutor { _, _ -> } } + .teamColorArgument("teamColor") { anyExecutor { _, _ -> } } + .particleArgument("particle") { anyExecutor { _, _ -> } } + .gameModeArgument("gameMode") { anyExecutor { _, _ -> } } + .commandArgument("nested") { anyExecutor { _, _ -> } }, + ) + .toDefinition() + + val expectedNames = listOf( + "resourceLocation", "time", "teamColor", "particle", "gameMode", "nested", + ) + assertEquals(expectedNames, command.paths.single().arguments.map { it.nodeName }) + assertEquals( + expectedNames.map { name -> listOf(name) }, + tree.paths.map { path -> path.arguments.map { it.nodeName } }, + ) + assertEquals( + expectedNames.map { name -> listOf("prefix", name) }, + child.paths.map { path -> path.arguments.map { it.nodeName } }, + ) + } + + private inline fun withPlatform(env: Env, block: () -> Unit) { + val platform = MinestomCommandAPIPlatform(env.process().command(), MinestomCommandOwnership()) + CommandAPI.installPlatform(platform) + try { + block() + } finally { + try { + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + } + } + + private inline fun withPlayerPlatform( + env: Env, + block: (CommandManager, Player) -> Unit, + ) { + val manager = env.process().command() + val platform = MinestomCommandAPIPlatform(manager, MinestomCommandOwnership()) + val instance = env.createEmptyInstance() + var player: Player? = null + CommandAPI.installPlatform(platform) + try { + val connected = connect(env.createConnection(profile("Value-${UUID.randomUUID()}")), instance) + player = connected + block(manager, connected) + } finally { + try { + if (player?.isRemoved == false) player.remove() + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + env.destroyInstance(instance) + } + } + + private fun connect(connection: TestConnection, instance: Instance): Player { + val connected = CompletableFuture() + Thread.startVirtualThread { + runCatching { connection.connect(instance, Pos.ZERO) } + .onSuccess(connected::complete) + .onFailure(connected::completeExceptionally) + } + return connected.join() + } + + private fun profile(name: String) = GameProfile(UUID.randomUUID(), name.take(16)) +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomExecutorIntegrationTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomExecutorIntegrationTest.kt new file mode 100644 index 000000000..f8bee09ee --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomExecutorIntegrationTest.kt @@ -0,0 +1,348 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.argument.EnumArgument +import dev.slne.surf.api.minestom.command.argument.IntegerArgument +import dev.slne.surf.api.minestom.command.argument.LiteralArgument +import dev.slne.surf.api.minestom.command.argument.TextArgument +import dev.slne.surf.api.minestom.command.dsl.* +import dev.slne.surf.api.minestom.command.exception.CommandSyntaxException +import dev.slne.surf.api.minestom.command.exception.WrapperCommandSyntaxException +import dev.slne.surf.api.minestom.command.executor.GENERIC_COMMAND_FAILURE +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import net.kyori.adventure.identity.Identity +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import net.minestom.server.command.CommandSender +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.Player +import net.minestom.server.instance.Instance +import net.minestom.server.network.packet.server.play.SystemChatPacket +import net.minestom.server.tag.TagHandler +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import net.minestom.testing.TestConnection +import org.apache.logging.log4j.Level +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.core.LogEvent +import org.apache.logging.log4j.core.LoggerContext +import org.apache.logging.log4j.core.appender.AbstractAppender +import org.apache.logging.log4j.core.layout.PatternLayout +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicReference + +@EnvTest +class MinestomExecutorIntegrationTest { + @Test + fun `player and console executors win before any executor`(env: Env) { + withPlatform(env) { manager -> + val calls = mutableListOf>() + CommandAPICommand("dispatch-specific") + .anyExecutor { sender, _ -> calls += "any" to sender } + .playerExecutor { sender, _ -> calls += "player" to sender } + .consoleExecutor { sender, _ -> calls += "console" to sender } + .register() + + val instance = env.createEmptyInstance() + var player: Player? = null + try { + player = connect(env.createConnection(), instance) + + assertTrue(runCommand(player, "dispatch-specific")) + assertTrue(runCommand(manager.consoleSender, "dispatch-specific")) + assertEquals(listOf("player", "console"), calls.map(Pair::first)) + assertSame(player, calls[0].second) + assertSame(manager.consoleSender, calls[1].second) + } finally { + player?.remove() + env.destroyInstance(instance) + } + } + } + + @Test + fun `any executor is the fallback for players consoles and other senders`(env: Env) { + withPlatform(env) { manager -> + val calls = mutableListOf() + CommandAPICommand("dispatch-any") + .anyExecutor { sender, _ -> calls += sender } + .register() + val other = RecordingSender() + val instance = env.createEmptyInstance() + var player: Player? = null + try { + player = connect(env.createConnection(), instance) + + assertTrue(runCommand(player, "dispatch-any")) + assertTrue(runCommand(manager.consoleSender, "dispatch-any")) + assertTrue(runCommand(other, "dispatch-any")) + assertEquals(listOf(player, manager.consoleSender, other), calls) + } finally { + player?.remove() + env.destroyInstance(instance) + } + } + } + + @Test + fun `execution info snapshots the full path with converted values raw input delegates and defaults`(env: Env) { + withPlatform(env) { manager -> + val received = AtomicReference() + CommandAPICommand("inspect") + .withArguments( + LiteralArgument("action", "run"), + EnumArgument("mode", listOf(SnapshotMode.READY)) { "ready-now" }, + TextArgument("message"), + ) + .withOptionalArguments(IntegerArgument("count").setOptional(7)) + .anyExecutionInfo { info -> + val delegated = DelegatedArguments(info.args) + received.set( + Snapshot( + input = info.input, + indexed = listOf(info.args[0], info.args[1], info.args[2]), + mode = delegated.mode, + message = delegated.message, + count = delegated.count, + actionRaw = info.args.getRaw("action"), + messageRaw = info.args.getRaw("message"), + countRaw = info.args.getRaw("count"), + present = listOf( + "action" in info.args, + "mode" in info.args, + "message" in info.args, + "count" in info.args, + ), + rawArguments = info.args.rawArguments(), + ), + ) + } + .register() + + assertTrue(runCommand(manager.consoleSender, "inspect run ready-now \"hello world\"")) + + assertEquals( + Snapshot( + input = "inspect run ready-now \"hello world\"", + indexed = listOf(SnapshotMode.READY, "hello world", 7), + mode = SnapshotMode.READY, + message = "hello world", + count = 7, + actionRaw = null, + messageRaw = "\"hello world\"", + countRaw = null, + present = listOf(false, true, true, false), + rawArguments = listOf("ready-now", "\"hello world\""), + ), + received.get(), + ) + } + } + + @Test + fun `resulting executor stores its value in the command result`(env: Env) { + withPlatform(env) { manager -> + CommandAPICommand("sum") + .withArguments(IntegerArgument("left"), IntegerArgument("right")) + .anyResultingExecutor { _, arguments -> + arguments.get("left") + arguments.get("right") + } + .register() + CommandAPICommand("normal-result") + .anyExecutor { _, _ -> } + .register() + + val result = runCommandForResult(manager.consoleSender, "sum 2 3") + + assertEquals(5, result) + assertEquals(1, runCommandForResult(manager.consoleSender, "normal-result")) + } + } + + @Test + fun `sync syntax failures render directly while unexpected failures render and log once`(env: Env) { + withPlatform(env) { manager -> + val direct = Component.text("Direct syntax failure", NamedTextColor.YELLOW) + val wrapped = Component.text("Wrapped syntax failure", NamedTextColor.RED) + CommandAPICommand("fail-direct") + .anyExecutor { _, _ -> throw CommandSyntaxException(direct) } + .register() + CommandAPICommand("fail-wrapped") + .anyExecutor { _, _ -> + throw WrapperCommandSyntaxException(CommandSyntaxException(wrapped)) + } + .register() + CommandAPICommand("fail-unexpected") + .anyResultingExecutor { _, _ -> throw IllegalStateException("broken sync executor") } + .register() + + val instance = env.createEmptyInstance() + var player: Player? = null + val logEvents = mutableListOf() + val appender = RecordingAppender(logEvents) + val loggerContext = LogManager.getContext(false) as LoggerContext + try { + val connection = env.createConnection() + player = connect(connection, instance) + val messages = connection.trackIncoming(SystemChatPacket::class.java) + appender.start() + loggerContext.configuration.rootLogger.addAppender(appender, Level.ERROR, null) + + assertTrue(runCommand(player, "fail-direct")) + assertTrue(runCommand(player, "fail-wrapped")) + val unexpectedResult = runCommand(player, "fail-unexpected") + + assertTrue(unexpectedResult) + val expectedMessages = listOf(direct, wrapped, GENERIC_COMMAND_FAILURE).map(::plain) + assertEquals(expectedMessages, messages.collect().map { packet -> plain(packet.message) }) + assertEquals( + 1, + logEvents.count { event -> + event.message.formattedMessage.contains("Failed to execute command 'fail-unexpected'") + }, + ) + } finally { + loggerContext.configuration.rootLogger.removeAppender(appender.name) + appender.stop() + player?.remove() + env.destroyInstance(instance) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `suspend executor returns immediately with an immutable full argument snapshot`(env: Env) = runTest { + withPlatform(env) { manager -> + var entered = false + var valueAfterReturn: Triple? = null + CommandAPICommand("later") + .withArguments(TextArgument("message")) + .withOptionalArguments(IntegerArgument("count").setOptional(9)) + .anyExecutionInfoExecutorSuspend(scope = this) { info -> + entered = true + valueAfterReturn = Triple( + info.args.get("message"), + info.args.get("count"), + "count" in info.args, + ) + } + .register() + + val result = runCommand(manager.consoleSender, "later \"kept value\"") + + assertTrue(result) + assertFalse(entered) + runCurrent() + assertTrue(entered) + assertEquals(Triple("kept value", 9, false), valueAfterReturn) + } + } + + @Test + fun `missing compatible executor sends a sender type failure instead of cancelling syntax`(env: Env) { + withPlatform(env) { manager -> + val sender = RecordingSender() + CommandAPICommand("players-only") + .playerExecutor { _, _ -> } + .register() + + val result = runCommand(sender, "players-only") + + assertTrue(result) + assertEquals( + listOf( + Component.text( + "Dieser Befehl kann von diesem Absendertyp nicht ausgeführt werden.", + NamedTextColor.RED, + ), + ), + sender.messages, + ) + } + } + + private inline fun withPlatform(env: Env, block: (net.minestom.server.command.CommandManager) -> Unit) { + val manager = env.process().command() + val platform = MinestomCommandAPIPlatform(manager, MinestomCommandOwnership()) + CommandAPI.installPlatform(platform) + try { + block(manager) + } finally { + try { + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + } + } + + private fun connect(connection: TestConnection, instance: Instance): Player { + val player = CompletableFuture() + Thread.startVirtualThread { + runCatching { connection.connect(instance, Pos.ZERO) } + .onSuccess(player::complete) + .onFailure(player::completeExceptionally) + } + return player.join() + } + + private fun plain(component: Component): String = + PlainTextComponentSerializer.plainText().serialize(component) + + private class DelegatedArguments(arguments: dev.slne.surf.api.minestom.command.executor.CommandArguments) { + val mode: SnapshotMode by arguments + val message: String by arguments + val count: Int by arguments + } + + private data class Snapshot( + val input: String, + val indexed: List, + val mode: SnapshotMode, + val message: String, + val count: Int, + val actionRaw: String?, + val messageRaw: String?, + val countRaw: String?, + val present: List, + val rawArguments: List, + ) + + private enum class SnapshotMode { + READY, + } + + private class RecordingSender : CommandSender { + val messages = mutableListOf() + + override fun identity(): Identity = Identity.nil() + + override fun tagHandler(): TagHandler = TagHandler.newHandler() + + override fun sendMessage(message: Component) { + messages += message + } + } + + private class RecordingAppender( + private val events: MutableList, + ) : AbstractAppender( + "commandapi-executor-${UUID.randomUUID()}", + null, + PatternLayout.createDefaultLayout(), + false, + emptyArray(), + ) { + override fun append(event: LogEvent) { + events += event.toImmutable() + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomPermissionIntegrationTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomPermissionIntegrationTest.kt new file mode 100644 index 000000000..d2b199490 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/MinestomPermissionIntegrationTest.kt @@ -0,0 +1,177 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.argument.StringArgument +import dev.slne.surf.api.minestom.command.dsl.anyExecutor +import net.minestom.server.command.ServerSender +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.Player +import net.minestom.server.instance.Instance +import net.minestom.server.network.player.GameProfile +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import net.minestom.testing.TestConnection +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicInteger + +@EnvTest +class MinestomPermissionIntegrationTest { + @Test + fun `root subcommand and argument requirements all gate execution`(env: Env) { + withPlatform(env) { manager -> + val executions = AtomicInteger() + CommandAPICommand("requirements-ok") + .withRequirement { true } + .withSubcommand( + CommandAPICommand("child") + .withRequirement { true } + .withArguments(StringArgument("value").withRequirement { true }) + .anyExecutor { _, _ -> executions.incrementAndGet() }, + ) + .register() + CommandAPICommand("requirements-root-denied") + .withRequirement { false } + .anyExecutor { _, _ -> executions.incrementAndGet() } + .register() + CommandAPICommand("requirements-path-denied") + .withSubcommand( + CommandAPICommand("child") + .withRequirement { false } + .anyExecutor { _, _ -> executions.incrementAndGet() }, + ) + .register() + CommandAPICommand("requirements-argument-denied") + .withArguments(StringArgument("value").withRequirement { false }) + .anyExecutor { _, _ -> executions.incrementAndGet() } + .register() + + assertTrue(runCommand(manager.consoleSender, "requirements-ok child value")) + assertFalse(runCommand(manager.consoleSender, "requirements-root-denied")) + assertFalse(runCommand(manager.consoleSender, "requirements-path-denied child")) + assertFalse(runCommand(manager.consoleSender, "requirements-argument-denied value")) + assertEquals(1, executions.get()) + } + } + + @Test + fun `console bypasses all accumulated permissions`(env: Env) { + withPlatform(env) { manager -> + val executions = AtomicInteger() + CommandAPICommand("console-permission") + .withPermission("lobby.root") + .withArguments(StringArgument("value").withPermission("lobby.value")) + .anyExecutor { _, _ -> executions.incrementAndGet() } + .register() + + assertTrue(runCommand(manager.consoleSender, "console-permission accepted")) + assertEquals(1, executions.get()) + } + } + + @Test + fun `lobby player must have every accumulated permission`(env: Env) { + val permissionsByName = mapOf( + "Allowed" to setOf("lobby.root", "lobby.value"), + "Denied" to setOf("lobby.root"), + ) + env.process().connection().setPlayerProvider { connection, profile -> + TestPermissionPlayer(connection, profile, permissionsByName[profile.name].orEmpty()) + } + try { + withPlatform(env) { manager -> + val executions = AtomicInteger() + CommandAPICommand("lobby-permission") + .withPermission("lobby.root") + .withArguments(StringArgument("value").withPermission("lobby.value")) + .anyExecutor { _, _ -> executions.incrementAndGet() } + .register() + val instance = env.createEmptyInstance() + val players = mutableListOf() + try { + val allowed = connect(env.createConnection(profile("Allowed")), instance).also(players::add) + val denied = connect(env.createConnection(profile("Denied")), instance).also(players::add) + + assertTrue(allowed is TestPermissionPlayer) + assertTrue(denied is TestPermissionPlayer) + assertTrue(runCommand(allowed, "lobby-permission accepted")) + assertFalse(runCommand(denied, "lobby-permission rejected")) + assertEquals(1, executions.get()) + } finally { + players.forEach(Player::remove) + env.destroyInstance(instance) + } + } + } finally { + env.process().connection().setPlayerProvider(null) + } + } + + @Test + fun `ordinary Minestom player is denied when any permission is required`(env: Env) { + withPlatform(env) { manager -> + val executions = AtomicInteger() + CommandAPICommand("ordinary-player-permission") + .withPermission("lobby.root") + .anyExecutor { _, _ -> executions.incrementAndGet() } + .register() + val instance = env.createEmptyInstance() + var player: Player? = null + try { + player = connect(env.createConnection(), instance) + + assertFalse(runCommand(player, "ordinary-player-permission")) + assertEquals(0, executions.get()) + } finally { + player?.remove() + env.destroyInstance(instance) + } + } + } + + @Test + fun `server sender holds every permission`(env: Env) { + withPlatform(env) { manager -> + val executions = AtomicInteger() + CommandAPICommand("server-permission") + .withPermission("lobby.root") + .withArguments(StringArgument("value").withPermission("lobby.value")) + .anyExecutor { _, _ -> executions.incrementAndGet() } + .register() + + assertTrue(runCommand(ServerSender(), "server-permission accepted")) + assertEquals(1, executions.get()) + } + } + + private inline fun withPlatform(env: Env, block: (net.minestom.server.command.CommandManager) -> Unit) { + val manager = env.process().command() + val platform = MinestomCommandAPIPlatform(manager, MinestomCommandOwnership()) + CommandAPI.installPlatform(platform) + try { + block(manager) + } finally { + try { + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + } + } + + private fun connect(connection: TestConnection, instance: Instance): Player { + val player = CompletableFuture() + Thread.startVirtualThread { + runCatching { connection.connect(instance, Pos.ZERO) } + .onSuccess(player::complete) + .onFailure(player::completeExceptionally) + } + return player.join() + } + + private fun profile(name: String) = GameProfile(UUID.randomUUID(), name) + +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/PositionArgumentIntegrationTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/PositionArgumentIntegrationTest.kt new file mode 100644 index 000000000..4406a505d --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/PositionArgumentIntegrationTest.kt @@ -0,0 +1,332 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.exceptions.CommandSyntaxException +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.CommandTree +import dev.slne.surf.api.minestom.command.argument.* +import dev.slne.surf.api.minestom.command.dsl.* +import dev.slne.surf.api.minestom.command.exception.CommandValidationException +import dev.slne.surf.api.minestom.command.suggestion.ArgumentSuggestions +import net.minestom.server.command.ArgumentParserType +import net.minestom.server.command.builder.arguments.ArgumentType +import net.minestom.server.coordinate.Pos +import net.minestom.server.coordinate.Vec +import net.minestom.server.entity.Player +import net.minestom.server.instance.Instance +import net.minestom.server.network.player.GameProfile +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import net.minestom.testing.TestConnection +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.time.Duration +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicReference + +@EnvTest +class PositionArgumentIntegrationTest { + @Test + fun `absolute relative and local positions resolve against the sender position`(env: Env) { + withPlayerPlatform(env, Pos(10.0, 20.0, 30.0, 0f, 0f)) { player -> + val position = PositionArgument("pos").toDefinition() + val position2D = Position2DArgument("pos").toDefinition() + val blockPosition = BlockPositionArgument("pos").toDefinition() + + assertEquals(Vec(1.0, 2.0, 3.0), position.read(player, "1 2 3")) + assertEquals(Vec(11.0, 22.0, 33.0), position.read(player, "~1 ~2 ~3")) + assertEquals(Vec(10.0, 20.0, 30.0), position.read(player, "~ ~ ~")) + assertEquals(Vec(10.0, 20.0, 30.0), position.read(player, "^0 ^0 ^0")) + assertVecEquals(Vec(10.0, 20.0, 31.0), position.read(player, "^0 ^0 ^1")) + + assertEquals(Vec(1.0, 0.0, 2.0), position2D.read(player, "1 2")) + assertEquals(Vec(10.0, 0.0, 30.0), position2D.read(player, "~ ~")) + + assertEquals(Vec(1.0, 2.0, 3.0), blockPosition.read(player, "1 2 3")) + assertEquals(Vec(11.0, 20.0, 30.0), blockPosition.read(player, "~1 ~0 ~0")) + + assertEquals(ArgumentParserType.VEC3, position.declaration().parser) + assertEquals(ArgumentParserType.VEC2, position2D.declaration().parser) + assertEquals(ArgumentParserType.BLOCK_POS, blockPosition.declaration().parser) + } + } + + @Test + fun `block positions reject decimal absolute values but allow decimal relative offsets`(env: Env) { + withPlayerPlatform(env, Pos(10.0, 20.0, 30.0, 0f, 0f)) { player -> + val compiled = BlockPositionArgument("pos").toDefinition() + + assertThrows(CommandSyntaxException::class.java) { compiled.read(player, "1.5 2 3") } + assertEquals(Vec(11.5, 20.0, 30.0), compiled.read(player, "~1.5 ~0 ~0")) + } + } + + @Test + fun `two dimensional positions require exactly two components and stop at the argument boundary`(env: Env) { + withPlayerPlatform(env, Pos(10.0, 20.0, 30.0, 0f, 0f)) { player -> + val compiled = Position2DArgument("pos").toDefinition() + + assertThrows(CommandSyntaxException::class.java) { compiled.read(player, "1") } + + // A third component is left for whatever follows this argument rather than consumed. + val reader = StringReader("1 2 3") + assertEquals(Vec(1.0, 0.0, 2.0), compiled.rawType.parse(reader, player)) + assertEquals(" 3", reader.remaining) + + val capturedPos = AtomicReference() + val capturedTag = AtomicReference() + CommandAPICommand("boundary-2d") + .location2DArgument("pos") + .stringArgument("tag") + .anyExecutor { _, arguments -> + capturedPos.set(arguments.get("pos")) + capturedTag.set(arguments.get("tag")) + } + .register() + + val manager = env.process().command() + assertTrue(runCommand(player, "boundary-2d 1 2 marker")) + assertEquals(Vec(1.0, 0.0, 2.0), capturedPos.get()) + assertEquals("marker", capturedTag.get()) + } + } + + @Test + fun `rotation resolves relative yaw and pitch against the sender view`(env: Env) { + withPlayerPlatform(env, Pos(0.0, 0.0, 0.0, 45f, 10f)) { player -> + val compiled = RotationArgument("look").toDefinition() + + assertEquals(Rotation(45f, 10f), compiled.read(player, "~ ~")) + assertEquals(Rotation(55f, 5f), compiled.read(player, "~10 ~-5")) + assertEquals(Rotation(90f, 0f), compiled.read(player, "90 0")) + + assertEquals(ArgumentParserType.ROTATION, compiled.declaration().parser) + assertNull(compiled.declaration().properties) + } + } + + @Test + fun `angle parses absolute and relative values and normalizes into vanillas range`(env: Env) { + withPlayerPlatform(env, Pos(0.0, 0.0, 0.0, 45f, 10f)) { player -> + val compiled = AngleArgument("angle").toDefinition() + + assertEquals(90f, compiled.read(player, "450")) + assertEquals(-90f, compiled.read(player, "270")) + assertEquals(45f, compiled.read(player, "~")) + assertEquals(55f, compiled.read(player, "~10")) + assertEquals(-115f, compiled.read(player, "~200")) + + assertThrows(CommandSyntaxException::class.java) { compiled.read(player, "~-") } + assertThrows(CommandSyntaxException::class.java) { compiled.read(player, "not-a-number") } + assertThrows(CommandSyntaxException::class.java) { compiled.read(player, "") } + + assertEquals(ArgumentParserType.ANGLE, compiled.declaration().parser) + assertNull(compiled.declaration().properties) + } + } + + @Test + fun `angle rejects non-finite absolute and relative values`(env: Env) { + withPlayerPlatform(env, Pos(0.0, 0.0, 0.0, 45f, 10f)) { player -> + val compiled = AngleArgument("angle").toDefinition() + + assertThrows(CommandSyntaxException::class.java) { compiled.read(player, "NaN") } + assertThrows(CommandSyntaxException::class.java) { compiled.read(player, "Infinity") } + assertThrows(CommandSyntaxException::class.java) { compiled.read(player, "-Infinity") } + assertThrows(CommandSyntaxException::class.java) { compiled.read(player, "~NaN") } + assertThrows(CommandSyntaxException::class.java) { compiled.read(player, "~Infinity") } + } + } + + @Test + fun `axis parses letters case-insensitively and rejects duplicates or unknown letters`(env: Env) { + withPlayerPlatform(env, Pos.ZERO) { player -> + val compiled = AxisArgument("axes").toDefinition() + + assertEquals(setOf(Axis.X, Axis.Y), compiled.read(player, "xy")) + assertEquals(setOf(Axis.X, Axis.Y, Axis.Z), compiled.read(player, "XYZ")) + assertEquals(setOf(Axis.Y), compiled.read(player, "y")) + + assertThrows(CommandSyntaxException::class.java) { compiled.read(player, "xx") } + assertThrows(CommandSyntaxException::class.java) { compiled.read(player, "xq") } + + assertEquals(ArgumentParserType.SWIZZLE, compiled.declaration().parser) + assertNull(compiled.declaration().properties) + } + } + + @Test + fun `a suggestion-enabled multi literal is still declared as a plain word`(env: Env) { + withPlayerPlatform(env, Pos.ZERO) { + val compiled = MultiLiteralArgument("multi-mode", "one", "two") + .includeSuggestions(ArgumentSuggestions.strings("one-extra")) + .toDefinition() + + assertEquals(ArgumentParserType.STRING, compiled.declaration().parser) + assertEquals( + ArgumentType.Word("expected").nodeProperties()!!.toList(), + compiled.declaration().properties!!.toList(), + ) + } + } + + @Test + fun `custom suggestion modes are rejected for multi-component position and rotation kinds but permitted for angle and axis`( + env: Env, + ) { + withPlayerPlatform(env) { + val rejectedCases = listOf( + "Position" to PositionArgument("pos").replaceSuggestions(ArgumentSuggestions.strings("~ ~ ~")), + "Position2D" to Position2DArgument("pos").replaceSuggestions(ArgumentSuggestions.strings("~ ~")), + "BlockPosition" to BlockPositionArgument("pos").replaceSuggestions(ArgumentSuggestions.strings("~ ~ ~")), + "Rotation" to RotationArgument("look").replaceSuggestions(ArgumentSuggestions.strings("~ ~")), + ) + rejectedCases.forEach { (kind, argument) -> + val commandName = "reject-${kind.lowercase()}" + val failure = assertThrows(CommandValidationException::class.java) { + CommandAPICommand(commandName) + .withArguments(argument) + .anyExecutor { _, _ -> } + .register() + } + assertEquals( + "$kind argument '${argument.nodeName}' cannot use custom suggestions", + failure.message, + kind, + ) + } + + CommandAPICommand("allow-angle") + .withArguments(AngleArgument("angle").replaceSuggestions(ArgumentSuggestions.strings("0", "90"))) + .anyExecutor { _, _ -> } + .register() + CommandAPICommand("allow-axis") + .withArguments(AxisArgument("axes").includeSuggestions(ArgumentSuggestions.strings("xyz"))) + .anyExecutor { _, _ -> } + .register() + + assertTrue(runCommand(env.process().command().consoleSender, "allow-angle 90")) + assertTrue(runCommand(env.process().command().consoleSender, "allow-axis xz")) + } + } + + @Test + fun `all six position argument DSL builders register across command tree and child argument receivers`() { + val command = CommandAPICommand("position-dsl-command") + .locationArgument("pos") + .location2DArgument("pos2d") + .blockPositionArgument("blockPos") + .rotationArgument("look") + .angleArgument("angle") + .axisArgument("axes") + .anyExecutor { _, _ -> } + .toDefinition() + + val tree = CommandTree("position-dsl-tree") + .locationArgument("pos") { anyExecutor { _, _ -> } } + .location2DArgument("pos2d") { anyExecutor { _, _ -> } } + .blockPositionArgument("blockPos") { anyExecutor { _, _ -> } } + .rotationArgument("look") { anyExecutor { _, _ -> } } + .angleArgument("angle") { anyExecutor { _, _ -> } } + .axisArgument("axes") { anyExecutor { _, _ -> } } + .toDefinition() + + val child = CommandTree("position-dsl-child") + .then( + StringArgument("prefix") + .locationArgument("pos") { anyExecutor { _, _ -> } } + .location2DArgument("pos2d") { anyExecutor { _, _ -> } } + .blockPositionArgument("blockPos") { anyExecutor { _, _ -> } } + .rotationArgument("look") { anyExecutor { _, _ -> } } + .angleArgument("angle") { anyExecutor { _, _ -> } } + .axisArgument("axes") { anyExecutor { _, _ -> } }, + ) + .toDefinition() + + assertEquals( + listOf("pos", "pos2d", "blockPos", "look", "angle", "axes"), + command.paths.single().arguments.map { it.nodeName }, + ) + assertEquals( + listOf( + listOf("pos"), + listOf("pos2d"), + listOf("blockPos"), + listOf("look"), + listOf("angle"), + listOf("axes"), + ), + tree.paths.map { path -> path.arguments.map { it.nodeName } }, + ) + assertEquals( + listOf( + listOf("prefix", "pos"), + listOf("prefix", "pos2d"), + listOf("prefix", "blockPos"), + listOf("prefix", "look"), + listOf("prefix", "angle"), + listOf("prefix", "axes"), + ), + child.paths.map { path -> path.arguments.map { it.nodeName } }, + ) + + assertEquals("1.0 2.0 3.0", PositionArgument("pos").toDefinition().stringify(Vec(1.0, 2.0, 3.0))) + assertEquals("45.0 10.0", RotationArgument("look").toDefinition().stringify(Rotation(45f, 10f))) + assertEquals("xy", AxisArgument("axes").toDefinition().stringify(linkedSetOf(Axis.X, Axis.Y))) + assertEquals( + "1 2 3", + BlockPositionArgument("blockPos").toDefinition().stringify(Vec(1.9, 2.1, 3.0)), + ) + assertEquals( + "-2 0 0", + BlockPositionArgument("blockPos").toDefinition().stringify(Vec(-1.5, 0.0, 0.0)), + ) + } + + private fun assertVecEquals(expected: Vec, actual: Vec, epsilon: Double = 1e-9) { + assertEquals(expected.x(), actual.x(), epsilon) + assertEquals(expected.y(), actual.y(), epsilon) + assertEquals(expected.z(), actual.z(), epsilon) + } + + private inline fun withPlayerPlatform( + env: Env, + spawnPos: Pos = Pos.ZERO, + block: (Player) -> Unit, + ) { + val platform = MinestomCommandAPIPlatform(env.process().command(), MinestomCommandOwnership()) + val instance = env.createEmptyInstance() + var player: Player? = null + CommandAPI.installPlatform(platform) + try { + val connected = connect(env.createConnection(profile("Pos-${UUID.randomUUID()}")), instance, spawnPos) + player = connected + check(env.tickWhile({ connected.instance !== instance }, Duration.ofSeconds(1))) { + "Player was not spawned in the requested test instance" + } + block(connected) + } finally { + try { + if (player?.isRemoved == false) player.remove() + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + env.destroyInstance(instance) + } + } + + private fun connect(connection: TestConnection, instance: Instance, pos: Pos): Player { + val connected = CompletableFuture() + Thread.startVirtualThread { + runCatching { connection.connect(instance, pos) } + .onSuccess(connected::complete) + .onFailure(connected::completeExceptionally) + } + return connected.join() + } + + private fun profile(name: String) = GameProfile(UUID.randomUUID(), name.take(16)) +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/RawTypeTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/RawTypeTest.kt new file mode 100644 index 000000000..4de81790a --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/RawTypeTest.kt @@ -0,0 +1,43 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.IntegerArgumentType +import com.mojang.brigadier.arguments.StringArgumentType +import com.mojang.brigadier.exceptions.CommandSyntaxException +import dev.slne.surf.api.minestom.command.argument.GreedyStringArgument +import dev.slne.surf.api.minestom.command.argument.IntegerArgument +import dev.slne.surf.api.minestom.command.argument.StringArgument +import dev.slne.surf.api.minestom.command.argument.TextArgument +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test + +class RawTypeTest { + @Test + fun `integer delegates to brigadier and keeps its bounds`() { + val raw = IntegerArgument("value", 1, 8).toDefinition().rawType + val typed = assertInstanceOf(IntegerArgumentType::class.java, raw) + + assertEquals(1, typed.minimum) + assertEquals(8, typed.maximum) + assertEquals(4, typed.parse(StringReader("4"))) + assertThrows(CommandSyntaxException::class.java) { typed.parse(StringReader("9")) } + } + + @Test + fun `the three string shapes delegate to brigadier`() { + assertEquals( + StringArgumentType.StringType.SINGLE_WORD, + (StringArgument("a").toDefinition().rawType as StringArgumentType).type, + ) + assertEquals( + StringArgumentType.StringType.QUOTABLE_PHRASE, + (TextArgument("b").toDefinition().rawType as StringArgumentType).type, + ) + assertEquals( + StringArgumentType.StringType.GREEDY_PHRASE, + (GreedyStringArgument("c").toDefinition().rawType as StringArgumentType).type, + ) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/RegistryArgumentIntegrationTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/RegistryArgumentIntegrationTest.kt new file mode 100644 index 000000000..1ab9c8386 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/RegistryArgumentIntegrationTest.kt @@ -0,0 +1,501 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import com.mojang.brigadier.exceptions.CommandSyntaxException +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.CommandTree +import dev.slne.surf.api.minestom.command.argument.* +import dev.slne.surf.api.minestom.command.dsl.* +import dev.slne.surf.api.minestom.command.suggestion.ArgumentSuggestions +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import net.kyori.adventure.key.Key +import net.minestom.server.MinecraftServer +import net.minestom.server.command.ArgumentParserType +import net.minestom.server.command.CommandManager +import net.minestom.server.command.builder.arguments.ArgumentType +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.Player +import net.minestom.server.event.EventNode +import net.minestom.server.instance.Instance +import net.minestom.server.network.packet.client.play.ClientTabCompletePacket +import net.minestom.server.network.packet.server.play.TabCompletePacket +import net.minestom.server.network.player.GameProfile +import net.minestom.server.potion.PotionEffect +import net.minestom.server.registry.BuiltinRegistries +import net.minestom.server.registry.DynamicRegistry +import net.minestom.server.sound.SoundEvent +import net.minestom.server.world.DimensionType +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import net.minestom.testing.TestConnection +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.* +import java.util.concurrent.CompletableFuture + +@OptIn(ExperimentalCoroutinesApi::class) +@EnvTest +class RegistryArgumentIntegrationTest { + @Test + fun `sound argument resolves builtin sounds and rejects unknown keys`(env: Env) { + val sender = env.process().command().consoleSender + val compiled = SoundArgument("sound").toDefinition() + + val pickup = SoundEvent.fromKey("minecraft:entity.experience_orb.pickup") + assertEquals(pickup, compiled.read(sender, "minecraft:entity.experience_orb.pickup")) + val failure = assertThrows(CommandSyntaxException::class.java) { + compiled.read(sender, "minecraft:not_a_real_sound") + } + assertEquals("Unknown sound event 'minecraft:not_a_real_sound'", failure.rawMessage.string) + + assertEquals(ArgumentParserType.RESOURCE_LOCATION, compiled.declaration().parser) + assertNull(compiled.declaration().properties) + assertEquals( + "minecraft:entity.experience_orb.pickup", + SoundArgument("sound").toDefinition().stringify(pickup!!), + ) + } + + @Test + fun `potion effect argument resolves builtin effects and rejects unknown keys`(env: Env) { + val sender = env.process().command().consoleSender + val compiled = PotionEffectArgument("effect").toDefinition() + + val speed = PotionEffect.fromKey("minecraft:speed") + assertEquals(speed, compiled.read(sender, "minecraft:speed")) + val failure = assertThrows(CommandSyntaxException::class.java) { + compiled.read(sender, "minecraft:not_a_real_effect") + } + assertEquals("Unknown potion effect 'minecraft:not_a_real_effect'", failure.rawMessage.string) + + assertEquals(ArgumentParserType.RESOURCE_LOCATION, compiled.declaration().parser) + assertNull(compiled.declaration().properties) + assertEquals("minecraft:speed", PotionEffectArgument("effect").toDefinition().stringify(speed!!)) + } + + @Test + fun `biome argument resolves dynamic registry keys and rejects unknown ones`(env: Env) { + val sender = env.process().command().consoleSender + val compiled = BiomeArgument("biome").toDefinition() + val identifier = BuiltinRegistries.BIOME.name() + + val plains = env.process().registries().biome().getKey(Key.key("minecraft:plains")) + assertEquals(plains, compiled.read(sender, "minecraft:plains")) + val failure = assertThrows(CommandSyntaxException::class.java) { + compiled.read(sender, "minecraft:not_a_real_biome") + } + assertEquals("Unknown biome 'minecraft:not_a_real_biome'", failure.rawMessage.string) + val malformed = assertThrows(CommandSyntaxException::class.java) { + compiled.read(sender, "Not_Valid!") + } + assertEquals("Invalid identifier", malformed.rawMessage.string) + + assertEquals(ArgumentParserType.RESOURCE, compiled.declaration().parser) + assertEquals( + ArgumentType.Resource("expected", identifier).nodeProperties()!!.toList(), + compiled.declaration().properties!!.toList(), + ) + assertEquals("minecraft:plains", BiomeArgument("biome").toDefinition().stringify(plains!!)) + } + + @Test + fun `enchantment argument resolves dynamic registry keys and rejects unknown ones`(env: Env) { + val sender = env.process().command().consoleSender + val compiled = EnchantmentArgument("enchant").toDefinition() + val identifier = BuiltinRegistries.ENCHANTMENT.name() + + val sharpness = env.process().registries().enchantment().getKey(Key.key("minecraft:sharpness")) + assertEquals(sharpness, compiled.read(sender, "minecraft:sharpness")) + val failure = assertThrows(CommandSyntaxException::class.java) { + compiled.read(sender, "minecraft:not_a_real_enchantment") + } + assertEquals("Unknown enchantment 'minecraft:not_a_real_enchantment'", failure.rawMessage.string) + + assertEquals(ArgumentParserType.RESOURCE, compiled.declaration().parser) + assertEquals( + ArgumentType.Resource("expected", identifier).nodeProperties()!!.toList(), + compiled.declaration().properties!!.toList(), + ) + } + + @Test + fun `generic resource argument resolves against a caller supplied registry and observes later registrations`( + env: Env, + ) { + val sender = env.process().command().consoleSender + val registry = DynamicRegistry.fromMap(Key.key("test:samples")) + val compiled = ResourceArgument("sample", "test:sample", registry).toDefinition() + + val malformed = assertThrows(CommandSyntaxException::class.java) { + compiled.read(sender, "Not_Valid!") + } + assertEquals("Invalid identifier", malformed.rawMessage.string) + val missing = assertThrows(CommandSyntaxException::class.java) { + compiled.read(sender, "test:missing") + } + assertEquals("Unknown test:sample 'test:missing'", missing.rawMessage.string) + + val key = registry.register("test:late_entry", "late-value") + assertEquals(key, compiled.read(sender, "test:late_entry")) + + assertEquals(ArgumentParserType.RESOURCE, compiled.declaration().parser) + assertEquals( + ArgumentType.Resource("expected", "test:sample").nodeProperties()!!.toList(), + compiled.declaration().properties!!.toList(), + ) + } + + @Test + fun `instance argument resolves by uuid and unique dimension name and rejects ambiguous shared names`( + env: Env, + ) { + val sender = env.process().command().consoleSender + val compiled = InstanceArgument("instance").toDefinition() + val overworld = env.process().instance().createInstanceContainer(DimensionType.OVERWORLD) + val nether = env.process().instance().createInstanceContainer(DimensionType.THE_NETHER) + try { + assertEquals(overworld, compiled.read(sender, overworld.uuid.toString())) + assertEquals(nether, compiled.read(sender, nether.uuid.toString())) + assertEquals(overworld, compiled.read(sender, "minecraft:overworld")) + assertEquals(nether, compiled.read(sender, "minecraft:the_nether")) + val notFound = assertThrows(CommandSyntaxException::class.java) { + compiled.read(sender, "not-a-real-instance") + } + assertEquals( + "Unknown or ambiguous instance 'not-a-real-instance'; use its UUID", + notFound.rawMessage.string, + ) + + val secondOverworld = env.process().instance().createInstanceContainer(DimensionType.OVERWORLD) + try { + val ambiguous = assertThrows(CommandSyntaxException::class.java) { + compiled.read(sender, "minecraft:overworld") + } + assertEquals( + "Unknown or ambiguous instance 'minecraft:overworld'; use its UUID", + ambiguous.rawMessage.string, + ) + assertEquals(overworld, compiled.read(sender, overworld.uuid.toString())) + assertEquals(secondOverworld, compiled.read(sender, secondOverworld.uuid.toString())) + assertEquals(nether, compiled.read(sender, "minecraft:the_nether")) + } finally { + env.destroyInstance(secondOverworld) + } + } finally { + env.destroyInstance(nether) + env.destroyInstance(overworld) + } + + assertEquals(ArgumentParserType.STRING, compiled.declaration().parser) + assertEquals( + ArgumentType.Word("expected").nodeProperties()!!.toList(), + compiled.declaration().properties!!.toList(), + ) + } + + @Test + fun `partial keys produce namespaced suggestions for sound biome and generic resource arguments`( + env: Env, + ) = runTest { + withBackend(env) { + CommandAPICommand("sound-cmd") + .withArguments(SoundArgument("value")) + .anyExecutor { _, _ -> } + .register() + CommandAPICommand("biome-cmd") + .withArguments(BiomeArgument("value")) + .anyExecutor { _, _ -> } + .register() + val registry = DynamicRegistry.fromMap( + Key.key("test:samples-2"), + java.util.Map.entry(Key.key("test:alpha"), "alpha"), + java.util.Map.entry(Key.key("test:beta"), "beta"), + ) + CommandAPICommand("resource-cmd") + .withArguments(ResourceArgument("value", "test:sample-2", registry)) + .anyExecutor { _, _ -> } + .register() + + val soundPackets = connection.trackIncoming(TabCompletePacket::class.java) + send(1, "/sound-cmd minecraft:entity.experience_orb.pi") + runCurrent() + soundPackets.assertSingle { packet -> + assertTrue("minecraft:entity.experience_orb.pickup" in packet.matches.map { it.match }) + } + + val biomePackets = connection.trackIncoming(TabCompletePacket::class.java) + send(2, "/biome-cmd minecraft:pla") + runCurrent() + biomePackets.assertSingle { packet -> + assertEquals(listOf("minecraft:plains"), packet.matches.map { it.match }) + } + + val resourcePackets = connection.trackIncoming(TabCompletePacket::class.java) + send(3, "/resource-cmd test:a") + runCurrent() + resourcePackets.assertSingle { packet -> + assertEquals(listOf("test:alpha"), packet.matches.map { it.match }) + } + } + } + + @Test + fun `instance suggestions offer every uuid and only unambiguous dimension names`(env: Env) = runTest { + withBackend(env) { + // withBackend's own connection spawns into an additional overworld instance, so this + // test uses other dimensions to avoid an incidental collision with that instance's + // dimension name. + CommandAPICommand("instance-cmd") + .withArguments(InstanceArgument("value")) + .anyExecutor { _, _ -> } + .register() + + val caves = env.process().instance().createInstanceContainer(DimensionType.OVERWORLD_CAVES) + val end = env.process().instance().createInstanceContainer(DimensionType.THE_END) + try { + val packets = connection.trackIncoming(TabCompletePacket::class.java) + send(4, "/instance-cmd ") + runCurrent() + packets.assertSingle { packet -> + val matches = packet.matches.map { it.match } + assertTrue(caves.uuid.toString() in matches) + assertTrue(end.uuid.toString() in matches) + assertTrue("minecraft:overworld_caves" in matches) + assertTrue("minecraft:the_end" in matches) + } + + val secondCaves = env.process().instance().createInstanceContainer(DimensionType.OVERWORLD_CAVES) + try { + val ambiguousPackets = connection.trackIncoming(TabCompletePacket::class.java) + send(5, "/instance-cmd ") + runCurrent() + ambiguousPackets.assertSingle { packet -> + val matches = packet.matches.map { it.match } + assertTrue(caves.uuid.toString() in matches) + assertTrue(secondCaves.uuid.toString() in matches) + assertTrue(end.uuid.toString() in matches) + assertFalse("minecraft:overworld_caves" in matches) + assertTrue("minecraft:the_end" in matches) + } + } finally { + env.destroyInstance(secondCaves) + } + } finally { + env.destroyInstance(end) + env.destroyInstance(caves) + } + } + } + + @Test + fun `custom suggestion modes are permitted for every new registry backed argument`(env: Env) { + withPlatform(env) { + CommandAPICommand("allow-sound") + .withArguments(SoundArgument("value").replaceSuggestions(ArgumentSuggestions.strings("extra"))) + .anyExecutor { _, _ -> } + .register() + CommandAPICommand("allow-potion") + .withArguments( + PotionEffectArgument("value").includeSuggestions(ArgumentSuggestions.strings("extra")), + ) + .anyExecutor { _, _ -> } + .register() + CommandAPICommand("allow-biome") + .withArguments(BiomeArgument("value").replaceSuggestions(ArgumentSuggestions.strings("extra"))) + .anyExecutor { _, _ -> } + .register() + CommandAPICommand("allow-enchantment") + .withArguments( + EnchantmentArgument("value").includeSuggestions(ArgumentSuggestions.strings("extra")), + ) + .anyExecutor { _, _ -> } + .register() + CommandAPICommand("allow-instance") + .withArguments(InstanceArgument("value").replaceSuggestions(ArgumentSuggestions.strings("extra"))) + .anyExecutor { _, _ -> } + .register() + + assertTrue(runCommand(env.process().command().consoleSender, "allow-sound minecraft:entity.pig.ambient")) + assertTrue(runCommand(env.process().command().consoleSender, "allow-potion minecraft:speed")) + } + } + + @Test + fun `every new registry argument is reachable through the executor`(env: Env) { + val captured = mutableMapOf() + withPlayerPlatform(env) { manager, player -> + CommandAPICommand("sound-value-cmd").withArguments(SoundArgument("soundValue")) + .anyExecutor { _, arguments -> captured["sound"] = arguments.get("soundValue") } + .register() + CommandAPICommand("potion-value-cmd").withArguments(PotionEffectArgument("potionValue")) + .anyExecutor { _, arguments -> captured["potion"] = arguments.get("potionValue") } + .register() + CommandAPICommand("biome-value-cmd").withArguments(BiomeArgument("biomeValue")) + .anyExecutor { _, arguments -> captured["biome"] = arguments.get("biomeValue") } + .register() + + assertTrue(runCommand(player, "sound-value-cmd minecraft:entity.experience_orb.pickup")) + assertEquals( + SoundEvent.fromKey("minecraft:entity.experience_orb.pickup"), + captured["sound"], + ) + + assertTrue(runCommand(player, "potion-value-cmd minecraft:speed")) + assertEquals(PotionEffect.fromKey("minecraft:speed"), captured["potion"]) + + assertTrue(runCommand(player, "biome-value-cmd minecraft:plains")) + assertEquals( + env.process().registries().biome().getKey(Key.key("minecraft:plains")), + captured["biome"], + ) + + val packet = declaredCommands(env, player) + fun parserOf(nodeName: String) = packet.nodes.single { node -> node.name == nodeName }.parser + assertEquals(ArgumentParserType.RESOURCE_LOCATION, parserOf("soundValue")) + assertEquals(ArgumentParserType.RESOURCE_LOCATION, parserOf("potionValue")) + assertEquals(ArgumentParserType.RESOURCE, parserOf("biomeValue")) + } + } + + @Test + fun `all six registry argument DSL builders register across command tree and child argument receivers`() { + val customRegistry = DynamicRegistry.fromMap(Key.key("test:dsl-samples")) + val command = CommandAPICommand("registry-dsl-command") + .soundArgument("sound") + .potionEffectArgument("potion") + .biomeArgument("biome") + .enchantmentArgument("enchantment") + .resourceArgument("resource", "test:dsl-sample", customRegistry) + .instanceArgument("instance") + .anyExecutor { _, _ -> } + .toDefinition() + + val tree = CommandTree("registry-dsl-tree") + .soundArgument("sound") { anyExecutor { _, _ -> } } + .potionEffectArgument("potion") { anyExecutor { _, _ -> } } + .biomeArgument("biome") { anyExecutor { _, _ -> } } + .enchantmentArgument("enchantment") { anyExecutor { _, _ -> } } + .resourceArgument("resource", "test:dsl-sample", customRegistry) { anyExecutor { _, _ -> } } + .instanceArgument("instance") { anyExecutor { _, _ -> } } + .toDefinition() + + val child = CommandTree("registry-dsl-child") + .then( + StringArgument("prefix") + .soundArgument("sound") { anyExecutor { _, _ -> } } + .potionEffectArgument("potion") { anyExecutor { _, _ -> } } + .biomeArgument("biome") { anyExecutor { _, _ -> } } + .enchantmentArgument("enchantment") { anyExecutor { _, _ -> } } + .resourceArgument("resource", "test:dsl-sample", customRegistry) { anyExecutor { _, _ -> } } + .instanceArgument("instance") { anyExecutor { _, _ -> } }, + ) + .toDefinition() + + val expectedNames = listOf("sound", "potion", "biome", "enchantment", "resource", "instance") + assertEquals(expectedNames, command.paths.single().arguments.map { it.nodeName }) + assertEquals( + expectedNames.map { name -> listOf(name) }, + tree.paths.map { path -> path.arguments.map { it.nodeName } }, + ) + assertEquals( + expectedNames.map { name -> listOf("prefix", name) }, + child.paths.map { path -> path.arguments.map { it.nodeName } }, + ) + } + + private inline fun withPlatform(env: Env, block: () -> Unit) { + val platform = MinestomCommandAPIPlatform(env.process().command(), MinestomCommandOwnership()) + CommandAPI.installPlatform(platform) + try { + block() + } finally { + try { + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + } + } + + private inline fun withPlayerPlatform( + env: Env, + block: (CommandManager, Player) -> Unit, + ) { + val manager = env.process().command() + val platform = MinestomCommandAPIPlatform(manager, MinestomCommandOwnership()) + val instance = env.createEmptyInstance() + var player: Player? = null + CommandAPI.installPlatform(platform) + try { + val connected = connect(env.createConnection(profile("Registry-${UUID.randomUUID()}")), instance) + player = connected + block(manager, connected) + } finally { + try { + if (player?.isRemoved == false) player.remove() + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + env.destroyInstance(instance) + } + } + + private suspend fun TestScope.withBackend( + env: Env, + block: suspend BackendFixture.() -> Unit, + ) { + val commandManager = env.process().command() + val ownership = MinestomCommandOwnership() + val listener = MinestomSuggestionListener(ownership) + val platform = MinestomCommandAPIPlatform(commandManager, ownership) + val node = EventNode.all("registry-suggestions-test") + val instance = env.createEmptyInstance() + var player: Player? = null + CommandAPI.installPlatform(platform) + try { + listener.register(node) + env.process().eventHandler().addChild(node) + val connection = env.createConnection() + val connected = connect(connection, instance) + player = connected + BackendFixture(connection, connected).block() + } finally { + try { + if (player?.isRemoved == false) player.remove() + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + env.process().eventHandler().removeChild(node) + env.destroyInstance(instance) + } + } + + private fun connect(connection: TestConnection, instance: Instance): Player { + val connected = CompletableFuture() + Thread.startVirtualThread { + runCatching { connection.connect(instance, Pos.ZERO) } + .onSuccess(connected::complete) + .onFailure(connected::completeExceptionally) + } + return connected.join() + } + + private fun profile(name: String) = GameProfile(UUID.randomUUID(), name.take(16)) + + private data class BackendFixture( + val connection: TestConnection, + val player: Player, + ) { + fun send(transactionId: Int, input: String) { + MinecraftServer.process().packetListener().processClientPacket( + ClientTabCompletePacket(transactionId, input), + player.playerConnection, + ) + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/SuspendExecutorTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/SuspendExecutorTest.kt new file mode 100644 index 000000000..05aaede50 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/SuspendExecutorTest.kt @@ -0,0 +1,471 @@ +package dev.slne.surf.api.minestom.server.impl.command + +import dev.slne.surf.api.minestom.command.dsl.* +import dev.slne.surf.api.minestom.command.exception.CommandSyntaxException +import dev.slne.surf.api.minestom.command.exception.WrapperCommandSyntaxException +import dev.slne.surf.api.minestom.command.executor.* +import dev.slne.surf.api.minestom.coroutine.minestomAsyncScope +import dev.slne.surf.api.minestom.event.SuspendingEventNode +import kotlinx.coroutines.* +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import net.minestom.server.MinecraftServer +import net.minestom.server.command.CommandSender +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.Player +import net.minestom.server.instance.Instance +import net.minestom.server.network.packet.server.play.SystemChatPacket +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import net.minestom.testing.TestConnection +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.concurrent.CompletableFuture + +@OptIn(ExperimentalCoroutinesApi::class) +@EnvTest +class SuspendExecutorTest { + @Test + fun `normal fluent variants register typed callbacks and return their receiver`(env: Env) { + withPlayer(env) { player -> + val console = MinecraftServer.getCommandManager().consoleSender + val arguments = arguments() + val executable = RecordingExecutable() + val calls = mutableListOf() + + assertSame(executable, executable.anyExecutor { sender, args -> + assertSame(player, sender) + assertSame(arguments, args) + calls += "any" + }) + assertSame(executable, executable.executes { sender, args -> + assertSame(player, sender) + assertSame(arguments, args) + calls += "executes" + }) + assertSame(executable, executable.playerExecutor { sender, args -> + assertSame(player, sender) + assertSame(arguments, args) + calls += "player" + }) + assertSame(executable, executable.executesPlayer { sender, args -> + assertSame(player, sender) + assertSame(arguments, args) + calls += "executesPlayer" + }) + assertSame(executable, executable.consoleExecutor { sender, args -> + assertSame(console, sender) + assertSame(arguments, args) + calls += "console" + }) + assertSame(executable, executable.executesConsole { sender, args -> + assertSame(console, sender) + assertSame(arguments, args) + calls += "executesConsole" + }) + + assertEquals( + listOf( + ExecutorType.ANY, + ExecutorType.ANY, + ExecutorType.PLAYER, + ExecutorType.PLAYER, + ExecutorType.CONSOLE, + ExecutorType.CONSOLE, + ), + executable.definitions.map(ExecutorDefinition::type), + ) + executable.definitions.forEachIndexed { index, definition -> + val sender = if (definition.type == ExecutorType.CONSOLE) console else player + normal(definition).executor(ExecutionInfo(sender, arguments, "normal-$index")) + } + assertEquals( + listOf("any", "executes", "player", "executesPlayer", "console", "executesConsole"), + calls, + ) + } + } + + @Test + fun `execution info variants preserve sender arguments and raw input`(env: Env) { + withPlayer(env) { player -> + val console = MinecraftServer.getCommandManager().consoleSender + val arguments = arguments() + val executable = RecordingExecutable() + val calls = mutableListOf() + + assertSame(executable, executable.anyExecutionInfo { info -> + assertInfo(info, player, arguments, "any-info") + calls += "any" + }) + assertSame(executable, executable.playerExecutionInfo { info -> + assertInfo(info, player, arguments, "player-info") + calls += "player" + }) + assertSame(executable, executable.consoleExecutionInfo { info -> + assertInfo(info, console, arguments, "console-info") + calls += "console" + }) + + normal(executable.definitions[0]).executor(ExecutionInfo(player, arguments, "any-info")) + normal(executable.definitions[1]).executor(ExecutionInfo(player, arguments, "player-info")) + normal(executable.definitions[2]).executor(ExecutionInfo(console, arguments, "console-info")) + + assertEquals(listOf(ExecutorType.ANY, ExecutorType.PLAYER, ExecutorType.CONSOLE), executable.definitions.map { it.type }) + assertEquals(listOf("any", "player", "console"), calls) + } + } + + @Test + fun `resulting variants retain integer results for every sender type`(env: Env) { + withPlayer(env) { player -> + val console = MinecraftServer.getCommandManager().consoleSender + val arguments = arguments() + val executable = RecordingExecutable() + + assertSame(executable, executable.anyResultingExecutor { sender, args -> + assertSame(player, sender) + assertSame(arguments, args) + 11 + }) + assertSame(executable, executable.playerResultingExecutor { sender, args -> + assertSame(player, sender) + assertSame(arguments, args) + 22 + }) + assertSame(executable, executable.consoleResultingExecutor { sender, args -> + assertSame(console, sender) + assertSame(arguments, args) + 33 + }) + assertSame(executable, executable.anyResultingExecutionInfo { info -> + assertInfo(info, player, arguments, "any-result-info") + 44 + }) + assertSame(executable, executable.playerResultingExecutionInfo { info -> + assertInfo(info, player, arguments, "player-result-info") + 55 + }) + assertSame(executable, executable.consoleResultingExecutionInfo { info -> + assertInfo(info, console, arguments, "console-result-info") + 66 + }) + + assertEquals(11, resulting(executable.definitions[0]).executor(ExecutionInfo(player, arguments, "unused"))) + assertEquals(22, resulting(executable.definitions[1]).executor(ExecutionInfo(player, arguments, "unused"))) + assertEquals(33, resulting(executable.definitions[2]).executor(ExecutionInfo(console, arguments, "unused"))) + assertEquals(44, resulting(executable.definitions[3]).executor(ExecutionInfo(player, arguments, "any-result-info"))) + assertEquals(55, resulting(executable.definitions[4]).executor(ExecutionInfo(player, arguments, "player-result-info"))) + assertEquals(66, resulting(executable.definitions[5]).executor(ExecutionInfo(console, arguments, "console-result-info"))) + assertEquals( + listOf( + ExecutorType.ANY, + ExecutorType.PLAYER, + ExecutorType.CONSOLE, + ExecutorType.ANY, + ExecutorType.PLAYER, + ExecutorType.CONSOLE, + ), + executable.definitions.map { it.type }, + ) + } + } + + @Test + fun `suspend fluent variants adapt typed senders arguments info and scope choices`(env: Env) = runTest { + withPlayerSuspend(env) { player -> + val console = MinecraftServer.getCommandManager().consoleSender + val arguments = arguments() + val executable = RecordingExecutable() + val calls = mutableListOf() + + assertSame(executable, executable.anyExecutorSuspend(scope = { this }) { sender, args -> + assertSame(player, sender) + assertSame(arguments, args) + calls += "any" + }) + assertSame(executable, executable.playerExecutorSuspend(scope = this) { sender, args -> + assertSame(player, sender) + assertSame(arguments, args) + calls += "player" + }) + assertSame(executable, executable.consoleExecutorSuspend(scope = { this }) { sender, args -> + assertSame(console, sender) + assertSame(arguments, args) + calls += "console" + }) + assertSame(executable, executable.anyExecutionInfoExecutorSuspend(scope = { this }) { info -> + assertInfo(info, player, arguments, "any-info-suspend") + calls += "any-info" + }) + assertSame(executable, executable.playerExecutionInfoExecutorSuspend(scope = { this }) { info -> + assertInfo(info, player, arguments, "player-info-suspend") + calls += "player-info" + }) + assertSame(executable, executable.consoleExecutionInfoExecutorSuspend(scope = { this }) { info -> + assertInfo(info, console, arguments, "console-info-suspend") + calls += "console-info" + }) + executable.anyExecutorSuspend { _, _ -> } + + assertSame(this, suspending(executable.definitions[0]).scopeProvider()) + assertSame(this, suspending(executable.definitions[1]).scopeProvider()) + assertSame(minestomAsyncScope, suspending(executable.definitions.last()).scopeProvider()) + assertEquals( + listOf( + ExecutorType.ANY, + ExecutorType.PLAYER, + ExecutorType.CONSOLE, + ExecutorType.ANY, + ExecutorType.PLAYER, + ExecutorType.CONSOLE, + ExecutorType.ANY, + ), + executable.definitions.map { it.type }, + ) + + suspending(executable.definitions[0]).executor(this, ExecutionInfo(player, arguments, "unused")) + suspending(executable.definitions[1]).executor(this, ExecutionInfo(player, arguments, "unused")) + suspending(executable.definitions[2]).executor(this, ExecutionInfo(console, arguments, "unused")) + suspending(executable.definitions[3]).executor(this, ExecutionInfo(player, arguments, "any-info-suspend")) + suspending(executable.definitions[4]).executor(this, ExecutionInfo(player, arguments, "player-info-suspend")) + suspending(executable.definitions[5]).executor(this, ExecutionInfo(console, arguments, "console-info-suspend")) + assertEquals(listOf("any", "player", "console", "any-info", "player-info", "console-info"), calls) + } + } + + @Test + fun `launcher schedules work and supplies immutable execution inputs`(env: Env) = runTest { + withPlayerSuspend(env) { player -> + val arguments = arguments() + var received: ExecutionInfo? = null + val definition = ExecutorDefinition.Suspending( + type = ExecutorType.ANY, + scopeProvider = { this }, + executor = { received = it }, + ) + + val job = launchCommandExecutor(definition, player, arguments, "async-test value") + + assertFalse(job.isCompleted) + assertEquals(null, received) + runCurrent() + assertTrue(job.isCompleted) + assertInfo(checkNotNull(received), player, arguments, "async-test value") + } + } + + @Test + fun `cancellation is propagated without sender failure`(env: Env) = runTest { + withPlayerConnectionSuspend(env) { player, messages -> + val definition = ExecutorDefinition.Suspending( + type = ExecutorType.ANY, + scopeProvider = { this }, + executor = { throw CancellationException("stop") }, + ) + + val job = launchCommandExecutor(definition, player, CommandArguments.empty(), "cancel-test") + runCurrent() + + assertTrue(job.isCancelled) + messages.assertEmpty() + } + } + + @Test + fun `timeout of the executor itself is reported as an ordinary failure`(env: Env) = runTest { + withPlayerConnectionSuspend(env) { player, messages -> + val definition = ExecutorDefinition.Suspending( + type = ExecutorType.ANY, + scopeProvider = { this }, + executor = { withTimeout(10) { delay(1_000) } }, + ) + + val job = launchCommandExecutor(definition, player, CommandArguments.empty(), "timeout-test") + advanceUntilIdle() + + assertTrue(job.isCompleted) + assertFalse(job.isCancelled) + messages.assertSingle { packet -> + assertMessage( + Component.text( + "Beim Ausführen des Befehls ist ein Fehler aufgetreten.", + NamedTextColor.RED, + ), + packet, + ) + } + } + } + + @Test + fun `event node logs a listener timeout and continues while propagating cancellation`() = runTest { + val node = SuspendingEventNode("timeouts") + val seen = mutableListOf() + node.addListener(priority = 0) { withTimeout(10) { delay(1_000) } } + node.addListener(priority = 1) { event -> seen += event } + + node.call("first") + + assertEquals(listOf("first"), seen) + + val cancelling = SuspendingEventNode("cancelling") + cancelling.addListener { throw CancellationException("stop") } + cancelling.addListener { event -> seen += event } + + assertTrue(runCatching { cancelling.call("second") }.exceptionOrNull() is CancellationException) + assertEquals(listOf("first"), seen) + } + + @Test + fun `direct syntax failure sends its component`(env: Env) = runTest { + val component = Component.text("Expected a destination", NamedTextColor.RED) + assertFailureMessage(env, component) { + throw CommandSyntaxException(component, "/warp", 5) + } + } + + @Test + fun `wrapped syntax failure sends its nested component`(env: Env) = runTest { + val component = Component.text("Expected a player", NamedTextColor.RED) + assertFailureMessage(env, component) { + throw WrapperCommandSyntaxException(CommandSyntaxException(component, "/tell", 6)) + } + } + + @Test + fun `unchecked failure sends the standard generic component`(env: Env) = runTest { + assertFailureMessage( + env, + Component.text( + "Beim Ausführen des Befehls ist ein Fehler aufgetreten.", + NamedTextColor.RED, + ), + ) { + throw IllegalStateException("broken executor") + } + } + + private suspend fun TestScope.assertFailureMessage( + env: Env, + expected: Component, + failure: suspend CoroutineScope.() -> Unit, + ) { + withPlayerConnectionSuspend(env) { player, messages -> + val definition = ExecutorDefinition.Suspending( + type = ExecutorType.ANY, + scopeProvider = { this }, + executor = { failure() }, + ) + + val job = launchCommandExecutor(definition, player, CommandArguments.empty(), "failure-test") + runCurrent() + + assertTrue(job.isCompleted) + messages.assertSingle { packet -> + assertMessage(expected, packet) + } + } + } + + private fun assertMessage(expected: Component, packet: SystemChatPacket) { + val plain = PlainTextComponentSerializer.plainText() + assertEquals(plain.serialize(expected), plain.serialize(packet.message)) + assertEquals(expected.color(), packet.message.color()) + } + + private inline fun withPlayer(env: Env, block: (Player) -> Unit) { + val instance = env.createEmptyInstance() + var player: Player? = null + try { + val connectedPlayer = connect(env.createConnection(), instance) + player = connectedPlayer + block(connectedPlayer) + } finally { + player?.remove() + env.destroyInstance(instance) + } + } + + private suspend inline fun withPlayerSuspend( + env: Env, + crossinline block: suspend (Player) -> Unit, + ) { + val instance = env.createEmptyInstance() + var player: Player? = null + try { + val connectedPlayer = connect(env.createConnection(), instance) + player = connectedPlayer + block(connectedPlayer) + } finally { + player?.remove() + env.destroyInstance(instance) + } + } + + private suspend inline fun withPlayerConnectionSuspend( + env: Env, + crossinline block: suspend (Player, net.minestom.testing.Collector) -> Unit, + ) { + val instance = env.createEmptyInstance() + var player: Player? = null + try { + val connection = env.createConnection() + val connectedPlayer = connect(connection, instance) + player = connectedPlayer + val messages = connection.trackIncoming(SystemChatPacket::class.java) + block(connectedPlayer, messages) + } finally { + player?.remove() + env.destroyInstance(instance) + } + } + + private fun arguments() = CommandArguments.of( + listOf(ParsedArgument("marker", "value", "value", present = true)), + ) + + private fun connect(connection: TestConnection, instance: Instance): Player { + val player = CompletableFuture() + Thread.startVirtualThread { + runCatching { connection.connect(instance, Pos.ZERO) } + .onSuccess(player::complete) + .onFailure(player::completeExceptionally) + } + return player.join() + } + + private fun normal(definition: ExecutorDefinition) = + assertInstanceOf(ExecutorDefinition.Normal::class.java, definition) + + private fun resulting(definition: ExecutorDefinition) = + assertInstanceOf(ExecutorDefinition.Resulting::class.java, definition) + + private fun suspending(definition: ExecutorDefinition) = + assertInstanceOf(ExecutorDefinition.Suspending::class.java, definition) + + private fun assertInfo( + info: ExecutionInfo, + sender: S, + arguments: CommandArguments, + input: String, + ) { + assertSame(sender, info.sender) + assertSame(arguments, info.args) + assertEquals(input, info.input) + } + + private class RecordingExecutable : CommandExecutable { + val definitions = mutableListOf() + + override fun addExecutor(definition: ExecutorDefinition): RecordingExecutable { + definitions += definition + return this + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/BrigadierCommandTreeTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/BrigadierCommandTreeTest.kt new file mode 100644 index 000000000..383c2cc7d --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/BrigadierCommandTreeTest.kt @@ -0,0 +1,164 @@ +package dev.slne.surf.api.minestom.server.impl.command.brigadier + +import com.mojang.brigadier.exceptions.CommandSyntaxException +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.CommandTree +import dev.slne.surf.api.minestom.command.argument.EnumArgument +import dev.slne.surf.api.minestom.command.argument.GreedyStringArgument +import dev.slne.surf.api.minestom.command.argument.IntegerArgument +import dev.slne.surf.api.minestom.command.argument.LiteralArgument +import dev.slne.surf.api.minestom.command.argument.StringArgument +import dev.slne.surf.api.minestom.command.dsl.anyExecutor +import dev.slne.surf.api.minestom.command.dsl.anyResultingExecutor +import dev.slne.surf.api.minestom.command.executor.CommandArguments +import net.minestom.server.command.CommandSender +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicReference + +@EnvTest +class BrigadierCommandTreeTest { + @Test + fun `a literal and an argument may share a name without any renaming`(env: Env) { + val received = AtomicReference() + val tree = treeOf( + CommandTree("send").then( + LiteralArgument("server").then( + StringArgument("server").anyExecutor { _, args -> received.set(args) }, + ), + ), + ) + + assertEquals(1, tree.dispatch(env, "send server lobby-1")) + assertEquals("lobby-1", received.get().get("server")) + assertEquals("lobby-1", received.get().getRaw("server")) + } + + @Test + fun `input that matches no branch never falls through to the preceding node`(env: Env) { + val root = AtomicReference(0) + val branch = AtomicReference(0) + val tree = treeOf( + CommandTree("difficulty") + .anyExecutor { _, _ -> root.set(root.get() + 1) } + .then( + LiteralArgument("hard") + .anyExecutor { _, _ -> branch.set(branch.get() + 1) }, + ), + ) + + assertThrows(CommandSyntaxException::class.java) { tree.dispatch(env, "difficulty aaa") } + assertEquals(0, root.get()) + assertEquals(0, branch.get()) + + assertEquals(1, tree.dispatch(env, "difficulty")) + assertEquals(1, root.get()) + } + + @Test + fun `trailing input after a complete syntax is rejected`(env: Env) { + val executions = AtomicReference(0) + val tree = treeOf( + CommandTree("difficulty").then( + LiteralArgument("hard").anyExecutor { _, _ -> + executions.set(executions.get() + 1) + }, + ), + ) + + val failure = assertThrows(CommandSyntaxException::class.java) { + tree.dispatch(env, "difficulty hard aaa") + } + + assertTrue(failure.message!!.isNotBlank()) + assertEquals(0, executions.get()) + assertEquals(1, tree.dispatch(env, "difficulty hard")) + } + + @Test + fun `an optional suffix stays executable at every length`(env: Env) { + val received = AtomicReference() + val tree = treeOf( + CommandAPICommand("kick") + .withArguments(StringArgument("target")) + .withOptionalArguments(GreedyStringArgument("reason")) + .anyExecutor { _, args -> received.set(args) }, + ) + + assertEquals(1, tree.dispatch(env, "kick Deniz")) + assertNull(received.get().getOptional("reason")) + + assertEquals(1, tree.dispatch(env, "kick Deniz being rude")) + assertEquals("being rude", received.get().get("reason")) + } + + @Test + fun `a resulting executor returns its own value to the dispatcher`(env: Env) { + val tree = treeOf( + CommandAPICommand("sum") + .withArguments(IntegerArgument("left"), IntegerArgument("right")) + .anyResultingExecutor { _, args -> + args.get("left") + args.get("right") + }, + ) + + assertEquals(5, tree.dispatch(env, "sum 2 3")) + } + + @Test + fun `a rejected argument value reports a syntax failure instead of executing`(env: Env) { + val executions = AtomicReference(0) + val tree = treeOf( + CommandAPICommand("count") + .withArguments(IntegerArgument("value", 1, 8)) + .anyExecutor { _, _ -> executions.set(executions.get() + 1) }, + ) + + assertThrows(CommandSyntaxException::class.java) { tree.dispatch(env, "count 99") } + assertEquals(0, executions.get()) + } + + @Test + fun `a value produced by an argument's own parser reaches the executor`(env: Env) { + val received = AtomicReference() + val tree = treeOf( + CommandAPICommand("warp") + .withArguments(EnumArgument("mode", Mode.entries)) + .withOptionalArguments(IntegerArgument("delay", 0, 60)) + .anyExecutor { _, args -> received.set(args) }, + ) + + assertEquals(1, tree.dispatch(env, "warp fast")) + assertEquals(Mode.FAST, received.get().get("mode")) + assertNull(received.get().getOptional("delay")) + + assertEquals(1, tree.dispatch(env, "warp slow 30")) + assertEquals(Mode.SLOW, received.get().get("mode")) + assertEquals(30, received.get().get("delay")) + + assertThrows(CommandSyntaxException::class.java) { tree.dispatch(env, "warp sideways") } + assertThrows(CommandSyntaxException::class.java) { tree.dispatch(env, "warp fast 99") } + } + + private enum class Mode { FAST, SLOW } + + private fun treeOf(command: CommandAPICommand): BrigadierCommandTree = + BrigadierCommandTree().apply { + register(command.name, listOf(command.name), command.toDefinition()) + } + + private fun treeOf(command: CommandTree): BrigadierCommandTree = + BrigadierCommandTree().apply { + register(command.name, listOf(command.name), command.toDefinition()) + } + + private fun BrigadierCommandTree.dispatch(env: Env, input: String): Int { + val sender: CommandSender = env.process().command().consoleSender + return dispatcher.execute(input, sender) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/CommandApiExecuteTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/CommandApiExecuteTest.kt new file mode 100644 index 000000000..2874e7ace --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/CommandApiExecuteTest.kt @@ -0,0 +1,95 @@ +package dev.slne.surf.api.minestom.server.impl.command.brigadier + +import com.mojang.brigadier.exceptions.CommandSyntaxException +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.argument.StringArgument +import dev.slne.surf.api.minestom.command.dsl.anyExecutor +import dev.slne.surf.api.minestom.command.dsl.anyResultingExecutor +import dev.slne.surf.api.minestom.server.impl.command.MinestomCommandAPIPlatform +import dev.slne.surf.api.minestom.server.impl.command.MinestomCommandOwnership +import net.minestom.server.command.CommandManager +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicInteger + +@EnvTest +class CommandApiExecuteTest { + @Test + fun `execute dispatches a registered command and rejects trailing input`(env: Env) = + withPlatform(env) { manager -> + val runs = AtomicInteger() + CommandAPICommand("ping").anyExecutor { _, _ -> runs.incrementAndGet() }.register() + + assertEquals(1, CommandAPI.execute(manager.consoleSender, "ping")) + assertEquals(1, runs.get()) + + assertThrows(CommandSyntaxException::class.java) { + CommandAPI.execute(manager.consoleSender, "ping extra") + } + assertEquals(1, runs.get()) + } + + @Test + fun `execute returns a resulting executor's own value`(env: Env) = withPlatform(env) { manager -> + CommandAPICommand("answer").anyResultingExecutor { _, _ -> 42 }.register() + + assertEquals(42, CommandAPI.execute(manager.consoleSender, "answer")) + } + + @Test + fun `dispatch goes through brigadier, not through minestom's parser`(env: Env) = + withPlatform(env) { manager -> + val runs = AtomicInteger() + CommandAPICommand("owned").withArguments(StringArgument("value")) + .anyExecutor { _, _ -> runs.incrementAndGet() } + .register() + + assertEquals(1, CommandAPI.execute(manager.consoleSender, "owned thing")) + assertEquals(1, runs.get()) + + // Minestom accepts the trailing token and runs the same syntax; Brigadier rejects it. + assertThrows(CommandSyntaxException::class.java) { + CommandAPI.execute(manager.consoleSender, "owned thing extra") + } + assertEquals(1, runs.get()) + } + + @Test + fun `an unknown command is rejected`(env: Env) = withPlatform(env) { manager -> + assertThrows(CommandSyntaxException::class.java) { + CommandAPI.execute(manager.consoleSender, "nothing-here") + } + } + + @Test + fun `unregistering removes the command from the dispatcher`(env: Env) = + withPlatform(env) { manager -> + CommandAPICommand("temporary").anyExecutor { _, _ -> }.register() + assertEquals(1, CommandAPI.execute(manager.consoleSender, "temporary")) + + CommandAPI.unregister("temporary") + + assertThrows(CommandSyntaxException::class.java) { + CommandAPI.execute(manager.consoleSender, "temporary") + } + } + + private fun withPlatform(env: Env, block: (CommandManager) -> Unit) { + val manager = env.process().command() + val platform = MinestomCommandAPIPlatform(manager, MinestomCommandOwnership()) + CommandAPI.installPlatform(platform) + try { + block(manager) + } finally { + try { + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/DeclareCommandsMergerTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/DeclareCommandsMergerTest.kt new file mode 100644 index 000000000..3c815d9da --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/DeclareCommandsMergerTest.kt @@ -0,0 +1,134 @@ +package dev.slne.surf.api.minestom.server.impl.command.brigadier + +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.argument.* +import dev.slne.surf.api.minestom.command.dsl.anyExecutor +import dev.slne.surf.api.minestom.command.dsl.commandTree +import dev.slne.surf.api.minestom.command.suggestion.ArgumentSuggestions +import dev.slne.surf.api.minestom.server.impl.command.MinestomCommandAPIPlatform +import dev.slne.surf.api.minestom.server.impl.command.MinestomCommandOwnership +import net.minestom.server.command.ArgumentParserType +import net.minestom.server.command.CommandManager +import net.minestom.server.command.CommandSender +import net.minestom.server.command.builder.arguments.minecraft.SuggestionType +import net.minestom.server.network.packet.server.play.DeclareCommandsPacket +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +/** + * Tests that merged nodes naming `ask_server` also carry + * [DeclareCommandsPacket.HAS_SUGGESTION_TYPE]. The packet writes a suggestion type only when that + * flag is set, so without it the client receives a node that offers no suggestions. + */ +@EnvTest +class DeclareCommandsMergerTest { + + @Test + fun `a merged node that asks the server says so in its flags`(env: Env) { + val manager = env.process().command() + val platform = MinestomCommandAPIPlatform(manager, MinestomCommandOwnership()) + CommandAPI.installPlatform(platform) + try { + CommandAPICommand("declare-suggestions") + .withArguments( + CustomArgument(StringArgument("server")) { info -> info.baseValue } + .replaceSuggestions(ArgumentSuggestions.strings("lobby-1")), + IntegerArgument("count"), + ) + .anyExecutor { _, _ -> } + .register() + + val merged = merge(manager.consoleSender) + val server = merged.nodes.single { node -> node.name == "server" } + val count = merged.nodes.single { node -> node.name == "count" } + + assertEquals(SuggestionType.ASK_SERVER.identifier, server.suggestionsType) + assertNotEquals( + 0, + server.flags.toInt() and DeclareCommandsPacket.HAS_SUGGESTION_TYPE, + "the suggestion type of 'server' is not serialized without its flag", + ) + assertEquals( + 0, + count.flags.toInt() and DeclareCommandsPacket.HAS_SUGGESTION_TYPE, + "'count' carries no suggestion type to announce", + ) + } finally { + try { + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + } + } + + @Test + fun `a command argument is declared with a parser`(env: Env) { + withPlatform(env) { manager -> + CommandAPICommand("declare-command") + .withArguments(CommandArgument("inner")) + .anyExecutor { _, _ -> } + .register() + + val inner = merge(manager.consoleSender).nodes.single { node -> node.name == "inner" } + + assertEquals(ArgumentParserType.STRING, inner.parser) + } + } + + @Test + fun `a path cut at an unusable argument leaves no dead end prefix`(env: Env) { + withPlatform(env) { manager -> + commandTree("declare-prune") { + then( + LiteralArgument("admin").then( + LiteralArgument("reload").withRequirement { false }.executes { _, _ -> }, + ), + ) + then(LiteralArgument("list").executes { _, _ -> }) + } + + val names = merge(manager.consoleSender).nodes.map { node -> node.name } + + assertTrue("list" in names) + assertFalse("admin" in names) + assertFalse("reload" in names) + } + } + + private inline fun withPlatform(env: Env, block: (CommandManager) -> Unit) { + val manager = env.process().command() + val platform = MinestomCommandAPIPlatform(manager, MinestomCommandOwnership()) + CommandAPI.installPlatform(platform) + try { + block(manager) + } finally { + try { + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + } + } + + /** + * The CommandAPI's commands merged into a tree that contains only a root, the only part of + * Minestom's packet the merger reads. + */ + private fun merge(sender: CommandSender): DeclareCommandsPacket { + val root = DeclareCommandsPacket.Node().apply { + flags = DeclareCommandsPacket.getFlag( + DeclareCommandsPacket.NodeType.ROOT, + false, + false, + false, + ) + } + val merger = MinestomCommandAPIPlatform.activeMerger() + ?: error("No Minestom CommandAPI platform is installed") + return merger.merge(DeclareCommandsPacket(listOf(root), 0), sender) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/NodeDeclarationTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/NodeDeclarationTest.kt new file mode 100644 index 000000000..a76bcca9e --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/NodeDeclarationTest.kt @@ -0,0 +1,79 @@ +package dev.slne.surf.api.minestom.server.impl.command.brigadier + +import dev.slne.surf.api.minestom.command.argument.* +import net.kyori.adventure.key.Key +import net.minestom.server.command.ArgumentParserType +import net.minestom.server.command.builder.arguments.minecraft.SuggestionType +import net.minestom.server.registry.DynamicRegistry +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test + +/** + * Tests that argument kinds whose values only the server knows are declared `ask_server`. Clients + * request suggestions only for nodes that declare it. + */ +@EnvTest +class NodeDeclarationTest { + private enum class Mode { LOW, HIGH } + + private val declarations = NodeDeclarations() + private val askServer = SuggestionType.ASK_SERVER.identifier + + @Test + fun `kinds whose values only the server knows ask the server`(env: Env) { + val registry = DynamicRegistry.fromMap(Key.key("test:declaration-samples")) + + listOf( + SoundArgument("value"), + BiomeArgument("value"), + InstanceArgument("value"), + ResourceArgument("value", "test:declaration-sample", registry), + PlayerArgument("value"), + MultiLiteralArgument("value", "one", "two"), + EnumArgument("value", Mode.entries), + ).forEach { argument -> + val definition = argument.toDefinition() + assertEquals( + askServer, + declarations.of(definition).suggestionsType, + definition.kind.toString(), + ) + } + } + + @Test + fun `a wrapping argument inherits whether its base asks the server`(env: Env) { + val wrapped = CustomArgument(InstanceArgument("value")) { info -> info.baseValue } + val listed = ListArgument("value", EnumArgument("mode", Mode.entries)) + val plain = CustomArgument(IntegerArgument("value")) { info -> info.baseValue * 2 } + + assertEquals(askServer, declarations.of(wrapped.toDefinition()).suggestionsType) + assertEquals(askServer, declarations.of(listed.toDefinition()).suggestionsType) + assertNull(declarations.of(plain.toDefinition()).suggestionsType) + } + + @Test + fun `a command argument is declared with a parser`(env: Env) { + val declaration = declarations.of(CommandArgument("value").toDefinition()) + + assertEquals(ArgumentParserType.STRING, declaration.parser) + assertNull(declaration.suggestionsType) + } + + @Test + fun `kinds the client completes from its own data are left to it`(env: Env) { + listOf( + BooleanArgument("value"), + IntegerArgument("value"), + StringArgument("value"), + GameModeArgument("value"), + TeamColorArgument("value"), + ).forEach { argument -> + val definition = argument.toDefinition() + assertNull(declarations.of(definition).suggestionsType, definition.kind.toString()) + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/SharedNodeNameTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/SharedNodeNameTest.kt new file mode 100644 index 000000000..b5c01a64a --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/brigadier/SharedNodeNameTest.kt @@ -0,0 +1,42 @@ +package dev.slne.surf.api.minestom.server.impl.command.brigadier + +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.dsl.anyExecutor +import dev.slne.surf.api.minestom.command.dsl.commandTree +import dev.slne.surf.api.minestom.command.dsl.literalArgument +import dev.slne.surf.api.minestom.command.dsl.stringArgument +import dev.slne.surf.api.minestom.command.executor.CommandArguments +import dev.slne.surf.api.minestom.server.impl.command.MinestomCommandAPIPlatform +import dev.slne.surf.api.minestom.server.impl.command.MinestomCommandOwnership +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicReference + +@EnvTest +class SharedNodeNameTest { + @Test + fun `a literal and an argument share a name with no renaming`(env: Env) { + val manager = env.process().command() + val platform = MinestomCommandAPIPlatform(manager, MinestomCommandOwnership()) + CommandAPI.installPlatform(platform) + try { + val received = AtomicReference() + commandTree("send") { + literalArgument("server") { + stringArgument("server") { anyExecutor { _, args -> received.set(args) } } + } + } + + assertEquals(1, CommandAPI.execute(manager.consoleSender, "send server lobby-1")) + assertEquals("lobby-1", received.get().get("server")) + } finally { + try { + platform.close() + } finally { + CommandAPI.uninstallPlatform(platform) + } + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/CompositeSuggestionTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/CompositeSuggestionTest.kt new file mode 100644 index 000000000..d63a166bb --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/CompositeSuggestionTest.kt @@ -0,0 +1,54 @@ +package dev.slne.surf.api.minestom.server.impl.command.parser + +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import dev.slne.surf.api.minestom.command.argument.* +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +/** + * Tests that an argument wrapping another delegates suggestions to it; otherwise the inner + * argument's suggestions are unreachable. + */ +@EnvTest +class CompositeSuggestionTest { + private enum class Mode { LOW, HIGH } + + private val context = CommandDispatcher().parse("", Any()).context.build("") + + private fun suggest( + type: com.mojang.brigadier.arguments.ArgumentType<*>, + typed: String, + ) = type.listSuggestions(context, SuggestionsBuilder(typed, 0)).join() + + @Test + fun `a custom argument offers what its base offers`(env: Env) { + val parser = CustomArgument(MultiLiteralArgument("side", "left", "right")) { info -> + info.baseValue.uppercase() + }.toDefinition().rawType + + assertEquals(listOf("left"), suggest(parser, "le").list.map { it.text }) + } + + @Test + fun `a list argument completes the element being typed and replaces only it`(env: Env) { + val parser = ListArgument("modes", EnumArgument("mode", Mode.entries)).toDefinition().rawType + + val suggestions = suggest(parser, "low,HI") + + assertEquals(listOf("high"), suggestions.list.map { it.text }) + assertEquals(4, suggestions.range.start) + assertEquals(6, suggestions.range.end) + } + + @Test + fun `an entity argument offers the selectors it accepts`(env: Env) { + val single = suggest(PlayerArgument("target").toDefinition().rawType, "@") + val multiple = suggest(PlayersArgument("targets").toDefinition().rawType, "@") + + assertEquals(setOf("@p", "@r", "@s"), single.list.map { it.text }.toSet()) + assertEquals(setOf("@a", "@p", "@r", "@s"), multiple.list.map { it.text }.toSet()) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/CoordinateParsersTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/CoordinateParsersTest.kt new file mode 100644 index 000000000..b759b316c --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/CoordinateParsersTest.kt @@ -0,0 +1,166 @@ +package dev.slne.surf.api.minestom.server.impl.command.parser + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.exceptions.CommandSyntaxException +import dev.slne.surf.api.minestom.command.argument.* +import net.minestom.server.coordinate.Pos +import net.minestom.server.coordinate.Vec +import net.minestom.server.entity.Player +import net.minestom.server.instance.Instance +import net.minestom.server.network.player.GameProfile +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import java.time.Duration +import java.util.* +import java.util.concurrent.CompletableFuture + +/** + * Tests the coordinate parsers, which are `internal` to the api module, through the `rawType` of + * [dev.slne.surf.api.minestom.command.argument.Argument.toDefinition]. + * + * Relative components (`~`, `^`) need a sender. The `parse(reader)` overload without one must + * reject them instead of resolving against the origin; the two tests that spawn a player cover + * the overload that takes a sender. + */ +@EnvTest +class CoordinateParsersTest { + @Test + fun `absolute coordinates are read as three numbers`() { + val position = PositionArgument("pos").toDefinition().rawType + assertEquals(Vec(1.0, 2.5, -3.0), position.parse(StringReader("1 2.5 -3"))) + } + + @Test + fun `tilde without a sender is rejected rather than resolved against the origin`() { + val position = PositionArgument("pos").toDefinition().rawType + val failure = assertThrows(CommandSyntaxException::class.java) { + position.parse(StringReader("~ ~5 ~")) + } + assertEquals(0, failure.cursor) + } + + @Test + fun `caret without a sender is rejected rather than resolved against the origin's default view`() { + val position = PositionArgument("pos").toDefinition().rawType + val failure = assertThrows(CommandSyntaxException::class.java) { + position.parse(StringReader("^0 ^0 ^1")) + } + assertEquals(0, failure.cursor) + } + + @Test + fun `mixing local and world coordinates is rejected at the position's start`() { + val position = PositionArgument("pos").toDefinition().rawType + + val leadingLocal = assertThrows(CommandSyntaxException::class.java) { + position.parse(StringReader("^1 ~2 3")) + } + assertEquals(0, leadingLocal.cursor) + + val trailingLocal = assertThrows(CommandSyntaxException::class.java) { + position.parse(StringReader("1 ^2 3")) + } + assertEquals(0, trailingLocal.cursor) + } + + @Test + fun `two dimensional positions fix the middle component and stop after their two tokens`() { + val position2D = Position2DArgument("pos").toDefinition().rawType + + val reader = StringReader("1 2 3") + assertEquals(Vec(1.0, 0.0, 2.0), position2D.parse(reader)) + assertEquals(" 3", reader.remaining) + + assertThrows(CommandSyntaxException::class.java) { position2D.parse(StringReader("1")) } + } + + @Test + fun `block positions reject a decimal absolute value`() { + val blockPosition = BlockPositionArgument("pos").toDefinition().rawType + assertThrows(CommandSyntaxException::class.java) { blockPosition.parse(StringReader("1.5 2 3")) } + } + + @Test + fun `rotation reads an absolute yaw and pitch without needing a sender`() { + val rotation = RotationArgument("look").toDefinition().rawType + assertEquals(Rotation(45f, 10f), rotation.parse(StringReader("45 10"))) + } + + @Test + fun `rotation tilde without a sender is rejected rather than resolved against the origin's view`() { + val rotation = RotationArgument("look").toDefinition().rawType + val failure = assertThrows(CommandSyntaxException::class.java) { + rotation.parse(StringReader("~10 ~-5")) + } + assertEquals(0, failure.cursor) + } + + @Test + fun `rotation rejects a local coordinate`() { + val rotation = RotationArgument("look").toDefinition().rawType + assertThrows(CommandSyntaxException::class.java) { rotation.parse(StringReader("^1 2")) } + } + + @Test + fun `axis reads a swizzle case-insensitively and rejects a repeated letter`() { + val axis = AxisArgument("axes").toDefinition().rawType + + assertEquals(setOf(Axis.X, Axis.Z), axis.parse(StringReader("xz"))) + assertEquals(setOf(Axis.X, Axis.Y, Axis.Z), axis.parse(StringReader("XYZ"))) + assertThrows(CommandSyntaxException::class.java) { axis.parse(StringReader("xx")) } + } + + @Test + fun `a sourced parse resolves tilde and caret positions against that sender, not the origin`(env: Env) { + val instance = env.createEmptyInstance() + val player = connect(env, instance, Pos(10.0, 20.0, 30.0, 0f, 0f)) + try { + val position = PositionArgument("pos").toDefinition().rawType + assertEquals(Vec(11.0, 22.0, 33.0), position.parse(StringReader("~1 ~2 ~3"), player)) + assertVecEquals(Vec(10.0, 20.0, 31.0), position.parse(StringReader("^0 ^0 ^1"), player)) + + val blockPosition = BlockPositionArgument("pos").toDefinition().rawType + assertEquals(Vec(11.5, 20.0, 30.0), blockPosition.parse(StringReader("~1.5 ~0 ~0"), player)) + } finally { + player.remove() + env.destroyInstance(instance) + } + } + + @Test + fun `a sourced parse resolves a relative rotation against that sender's view, not the origin's`(env: Env) { + val instance = env.createEmptyInstance() + val player = connect(env, instance, Pos(0.0, 0.0, 0.0, 45f, 10f)) + try { + val rotation = RotationArgument("look").toDefinition().rawType + assertEquals(Rotation(55f, 5f), rotation.parse(StringReader("~10 ~-5"), player)) + } finally { + player.remove() + env.destroyInstance(instance) + } + } + + private fun assertVecEquals(expected: Vec, actual: Vec, epsilon: Double = 1e-9) { + assertEquals(expected.x(), actual.x(), epsilon) + assertEquals(expected.y(), actual.y(), epsilon) + assertEquals(expected.z(), actual.z(), epsilon) + } + + private fun connect(env: Env, instance: Instance, pos: Pos): Player { + val connected = CompletableFuture() + val profile = GameProfile(UUID.randomUUID(), "Coord-${UUID.randomUUID()}".take(16)) + Thread.startVirtualThread { + runCatching { env.createConnection(profile).connect(instance, pos) } + .onSuccess(connected::complete) + .onFailure(connected::completeExceptionally) + } + val player = connected.join() + check(env.tickWhile({ player.instance !== instance }, Duration.ofSeconds(1))) { + "Player was not spawned in the requested test instance" + } + return player + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/EntitySelectorParserTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/EntitySelectorParserTest.kt new file mode 100644 index 000000000..58ddb9f7e --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/EntitySelectorParserTest.kt @@ -0,0 +1,544 @@ +package dev.slne.surf.api.minestom.server.impl.command.parser + +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import dev.slne.surf.api.minestom.command.argument.EntitiesArgument +import dev.slne.surf.api.minestom.command.argument.EntityArgument +import dev.slne.surf.api.minestom.command.argument.PlayerArgument +import dev.slne.surf.api.minestom.command.argument.PlayersArgument +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.Entity +import net.minestom.server.entity.EntityType +import net.minestom.server.entity.LivingEntity +import net.minestom.server.entity.Player +import net.minestom.server.instance.Instance +import net.minestom.server.network.player.GameProfile +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import net.minestom.testing.TestConnection +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.time.Duration +import java.util.* +import java.util.concurrent.CompletableFuture + +/** + * Tests the entity selector parser, which is `internal` to the api module (see + * [ScalarParsersTest]), through the `rawType` of [PlayerArgument], [PlayersArgument], + * [EntityArgument] and [EntitiesArgument]. Assertions target the resolved entities, not selector + * internals. + */ +@EnvTest +class EntitySelectorParserTest { + @Test + fun `at-s selects the sender`(env: Env) { + withPlayers(env, 2) { _, players -> + val raw = EntityArgument("target").toDefinition().rawType + assertSame(players[0], raw.parse(StringReader("@s"), players[0])) + } + } + + @Test + fun `at-p yields exactly one even with several players online`(env: Env) { + withPlayers(env, 3) { _, players -> + val raw = EntitiesArgument("targets").toDefinition().rawType + val result = raw.parse(StringReader("@p"), players[0]) + assertEquals(1, result.size) + } + } + + @Test + fun `limit option caps the results of at-e`(env: Env) { + withPlayers(env, 3) { _, players -> + val raw = EntitiesArgument("targets").toDefinition().rawType + val result = raw.parse(StringReader("@e[limit=2]"), players[0]) + assertEquals(2, result.size) + } + } + + @Test + fun `sort=nearest orders results by distance to the sender`(env: Env) { + withPlayers(env, 1) { instance, players -> + val near = Entity(EntityType.ZOMBIE) + val far = Entity(EntityType.ZOMBIE) + near.setInstance(instance, Pos(2.0, 0.0, 0.0)).join() + far.setInstance(instance, Pos(20.0, 0.0, 0.0)).join() + try { + val raw = EntitiesArgument("targets").toDefinition().rawType + val result = raw.parse(StringReader("@e[type=minecraft:zombie,sort=nearest]"), players[0]) + assertEquals(listOf(near, far), result) + } finally { + near.remove() + far.remove() + } + } + } + + @Test + fun `a single-entity argument rejects a selector that can match many`(env: Env) { + withPlayers(env, 2) { _, players -> + val raw = EntityArgument("target").toDefinition().rawType + val failure = assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader("@e"), players[0]) + } + assertEquals(0, failure.cursor) + } + } + + @Test + fun `an unknown option is rejected at the option's start`(env: Env) { + withPlayers(env, 1) { _, players -> + val raw = EntitiesArgument("targets").toDefinition().rawType + val input = "@e[nonsense=1]" + val failure = assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader(input), players[0]) + } + assertEquals(input.indexOf("nonsense"), failure.cursor) + } + } + + @Test + fun `nbt is rejected by name as unsupported rather than as an unknown option`(env: Env) { + withPlayers(env, 1) { _, players -> + val raw = EntitiesArgument("targets").toDefinition().rawType + val input = "@e[nbt={foo:1}]" + val failure = assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader(input), players[0]) + } + assertTrue(failure.message?.contains("unsupported", ignoreCase = true) == true, failure.message) + assertEquals(input.indexOf("nbt"), failure.cursor) + } + } + + @Test + fun `a bare name resolves to the matching player`(env: Env) { + withPlayers(env, 2) { _, players -> + val raw = PlayerArgument("target").toDefinition().rawType + assertSame(players[1], raw.parse(StringReader(players[1].username), players[0])) + } + } + + @Test + fun `a bare uuid resolves to the matching entity`(env: Env) { + withPlayers(env, 1) { instance, players -> + val zombie = Entity(EntityType.ZOMBIE) + zombie.setInstance(instance, Pos(3.0, 0.0, 0.0)).join() + try { + val raw = EntityArgument("target").toDefinition().rawType + assertSame(zombie, raw.parse(StringReader(zombie.uuid.toString()), players[0])) + } finally { + zombie.remove() + } + } + } + + @Test + fun `players argument resolves every online player and rejects an empty match`(env: Env) { + withPlayers(env, 2) { _, players -> + val raw = PlayersArgument("targets").toDefinition().rawType + assertEquals(players.toSet(), raw.parse(StringReader("@a"), players[0]).toSet()) + + assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader("@a[gamemode=creative]"), players[0]) + } + } + } + + @Test + fun `resolving without a command source is rejected`(env: Env) { + withPlayers(env, 1) { _, _ -> + val raw = EntityArgument("target").toDefinition().rawType + assertThrows(CommandSyntaxException::class.java) { raw.parse(StringReader("@s")) } + } + } + + @Test + fun `tag option matches nothing when no entity carries it and everything for an empty tag`(env: Env) { + withPlayers(env, 2) { _, players -> + val allowEmpty = EntitiesArgument("targets", allowEmpty = true).toDefinition().rawType + assertEquals(emptyList(), allowEmpty.parse(StringReader("@e[tag=foo]"), players[0])) + + val required = EntitiesArgument("targets").toDefinition().rawType + assertEquals(players.toSet(), required.parse(StringReader("@e[tag=]"), players[0]).toSet()) + } + } + + @Test + fun `team option matches nobody when no team has been created`(env: Env) { + withPlayers(env, 1) { _, players -> + val raw = EntitiesArgument("targets", allowEmpty = true).toDefinition().rawType + assertEquals(emptyList(), raw.parse(StringReader("@e[team=red]"), players[0])) + } + } + + @Test + fun `dx dy dz select the single block at the origin, extended by one on the positive side`(env: Env) { + withPlayers(env, 1) { instance, players -> + val inBlock = Entity(EntityType.ZOMBIE) + val outsideBlock = Entity(EntityType.ZOMBIE) + inBlock.setInstance(instance, Pos(0.5, 0.0, 0.5)).join() + outsideBlock.setInstance(instance, Pos(5.0, 0.0, 0.0)).join() + try { + val raw = EntitiesArgument("targets").toDefinition().rawType + val result = raw.parse(StringReader("@e[type=minecraft:zombie,dx=0,dy=0,dz=0]"), players[0]) + assertEquals(listOf(inBlock), result) + } finally { + inBlock.remove() + outsideBlock.remove() + } + } + } + + @Test + fun `distance option filters by distance from the sender and rejects an inverted range`(env: Env) { + withPlayers(env, 1) { instance, players -> + val near = Entity(EntityType.ZOMBIE) + val far = Entity(EntityType.ZOMBIE) + near.setInstance(instance, Pos(2.0, 0.0, 0.0)).join() + far.setInstance(instance, Pos(20.0, 0.0, 0.0)).join() + try { + val raw = EntitiesArgument("targets").toDefinition().rawType + val result = raw.parse(StringReader("@e[type=minecraft:zombie,distance=..5]"), players[0]) + assertEquals(listOf(near), result) + + val input = "@e[type=minecraft:zombie,distance=5..1]" + val failure = assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader(input), players[0]) + } + assertEquals(input.indexOf("5..1"), failure.cursor) + } finally { + near.remove() + far.remove() + } + } + } + + @Test + fun `type and tag options support negation`(env: Env) { + withPlayers(env, 1) { instance, players -> + val zombie = Entity(EntityType.ZOMBIE) + zombie.setInstance(instance, Pos(2.0, 0.0, 0.0)).join() + try { + val raw = EntitiesArgument("targets").toDefinition().rawType + + val excludingZombies = raw.parse(StringReader("@e[type=!minecraft:zombie]"), players[0]) + assertEquals(players.toSet(), excludingZombies.toSet()) + + val everyone = buildSet { + addAll(players) + add(zombie) + } + val includingEveryone = raw.parse(StringReader("@e[tag=!foo]"), players[0]) + assertEquals(everyone, includingEveryone.toSet()) + } finally { + zombie.remove() + } + } + } + + @Test + fun `scores option consumes its grammar then is rejected as unsupported`(env: Env) { + withPlayers(env, 1) { _, players -> + val raw = EntitiesArgument("targets").toDefinition().rawType + val input = "@e[scores={foo=1..3}]" + val failure = assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader(input), players[0]) + } + assertTrue(failure.message?.contains("unsupported", ignoreCase = true) == true, failure.message) + assertEquals(input.indexOf("scores"), failure.cursor) + } + } + + @Test + fun `a malformed scores value is rejected before the unsupported check`(env: Env) { + withPlayers(env, 1) { _, players -> + val raw = EntitiesArgument("targets").toDefinition().rawType + val failure = assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader("@e[scores={foo=bar}]"), players[0]) + } + assertFalse(failure.message?.contains("unsupported", ignoreCase = true) == true, failure.message) + } + } + + @Test + fun `advancements option consumes its grammar, including nested criteria, then is rejected as unsupported`(env: Env) { + withPlayers(env, 1) { _, players -> + val raw = EntitiesArgument("targets").toDefinition().rawType + val input = "@e[advancements={story/root={met_villager=true}}]" + val failure = assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader(input), players[0]) + } + assertTrue(failure.message?.contains("unsupported", ignoreCase = true) == true, failure.message) + assertEquals(input.indexOf("advancements"), failure.cursor) + } + } + + @Test + fun `a malformed advancements value is rejected before the unsupported check`(env: Env) { + withPlayers(env, 1) { _, players -> + val raw = EntitiesArgument("targets").toDefinition().rawType + val failure = assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader("@e[advancements={story/root true}]"), players[0]) + } + assertFalse(failure.message?.contains("unsupported", ignoreCase = true) == true, failure.message) + } + } + + @Test + fun `predicate option is rejected as unsupported`(env: Env) { + withPlayers(env, 1) { _, players -> + val raw = EntitiesArgument("targets").toDefinition().rawType + val input = "@e[predicate=my:condition]" + val failure = assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader(input), players[0]) + } + assertTrue(failure.message?.contains("unsupported", ignoreCase = true) == true, failure.message) + assertEquals(input.indexOf("predicate"), failure.cursor) + } + } + + @Test + fun `repeating a single-use option is rejected at the repeat's start`(env: Env) { + withPlayers(env, 1) { _, players -> + val raw = EntitiesArgument("targets").toDefinition().rawType + val input = "@e[limit=1,limit=5]" + val failure = assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader(input), players[0]) + } + assertEquals(input.lastIndexOf("limit"), failure.cursor) + } + } + + @Test + fun `mixing two positive values for an invertable option is rejected, naming the option`(env: Env) { + withPlayers(env, 1) { _, players -> + val raw = EntitiesArgument("targets").toDefinition().rawType + val input = "@e[type=minecraft:zombie,type=minecraft:pig]" + // The cursor is not asserted. Vanilla rejects at the second `type` key's start because + // its InvertableSetOptionState check runs before the modifier; this parser's + // requireInvertableUse runs inside the modifier and rejects at the value's start. The + // underline column differs, the outcome does not, and asserting the column would pin + // the divergence. + val failure = assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader(input), players[0]) + } + assertTrue(failure.message?.contains("type") == true, failure.message) + } + } + + @Test + fun `limit and sort are inapplicable on a self selector`(env: Env) { + withPlayers(env, 1) { _, players -> + val raw = EntityArgument("target").toDefinition().rawType + val input = "@s[limit=5]" + val failure = assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader(input), players[0]) + } + assertEquals(input.indexOf("limit"), failure.cursor) + + assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader("@s[sort=nearest]"), players[0]) + } + } + } + + @Test + fun `sorting uses the x y z options as origin`(env: Env) { + withPlayers(env, 1) { instance, players -> + val nearSender = Entity(EntityType.ZOMBIE) + val nearOrigin = Entity(EntityType.ZOMBIE) + nearSender.setInstance(instance, Pos(2.0, 0.0, 0.0)).join() + nearOrigin.setInstance(instance, Pos(100.0, 0.0, 100.0)).join() + try { + val raw = EntitiesArgument("targets").toDefinition().rawType + val nearest = raw.parse( + StringReader("@e[type=minecraft:zombie,sort=nearest,limit=1,x=100,z=100]"), + players[0], + ) + assertEquals(listOf(nearOrigin), nearest) + + val furthest = raw.parse( + StringReader("@e[type=minecraft:zombie,sort=furthest,limit=1,x=100,z=100]"), + players[0], + ) + assertEquals(listOf(nearSender), furthest) + } finally { + nearSender.remove() + nearOrigin.remove() + } + } + } + + @Test + fun `dx dy dz match an entity whose bounding box overlaps the box but whose feet do not`(env: Env) { + withPlayers(env, 1) { instance, players -> + val overlapping = Entity(EntityType.ZOMBIE) + val clear = Entity(EntityType.ZOMBIE) + overlapping.setInstance(instance, Pos(1.25, 0.0, 0.5)).join() + clear.setInstance(instance, Pos(1.5, 0.0, 0.5)).join() + try { + val raw = EntitiesArgument("targets").toDefinition().rawType + val result = raw.parse(StringReader("@e[type=minecraft:zombie,dx=0,dy=0,dz=0]"), players[0]) + assertEquals(listOf(overlapping), result) + } finally { + overlapping.remove() + clear.remove() + } + } + } + + @Test + fun `negative distance and level bounds are rejected at the value's start`(env: Env) { + withPlayers(env, 1) { _, players -> + val raw = EntitiesArgument("targets").toDefinition().rawType + listOf("distance=..-1", "distance=-5..5", "level=-3..", "level=..-1").forEach { option -> + val input = "@e[$option]" + val failure = assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader(input), players[0]) + } + assertEquals(input.indexOf('=') + 1, failure.cursor, option) + assertTrue(failure.message?.contains("negative") == true, failure.message) + } + + assertEquals(players, raw.parse(StringReader("@e[distance=..5,level=0..]"), players[0])) + } + } + + @Test + fun `at-n selects the nearest entity including non-players`(env: Env) { + withPlayers(env, 1) { instance, players -> + val near = Entity(EntityType.ZOMBIE) + val far = Entity(EntityType.ZOMBIE) + near.setInstance(instance, Pos(2.0, 0.0, 0.0)).join() + far.setInstance(instance, Pos(20.0, 0.0, 0.0)).join() + try { + val raw = EntityArgument("target").toDefinition().rawType + assertSame(players[0], raw.parse(StringReader("@n"), players[0])) + assertSame(near, raw.parse(StringReader("@n[type=minecraft:zombie]"), players[0])) + assertSame(far, raw.parse(StringReader("@n[type=minecraft:zombie,x=20]"), players[0])) + } finally { + near.remove() + far.remove() + } + } + } + + @Test + fun `at-e and at-n skip dead living entities`(env: Env) { + withPlayers(env, 1) { instance, players -> + val dead = LivingEntity(EntityType.ZOMBIE) + dead.setInstance(instance, Pos(2.0, 0.0, 0.0)).join() + dead.kill() + try { + val raw = EntitiesArgument("targets", allowEmpty = true).toDefinition().rawType + assertEquals(emptyList(), raw.parse(StringReader("@e[type=minecraft:zombie]"), players[0])) + assertEquals(emptyList(), raw.parse(StringReader("@n[type=minecraft:zombie]"), players[0])) + } finally { + dead.remove() + } + } + } + + @Test + fun `x_rotation and y_rotation filter by wrapped angle ranges`(env: Env) { + withPlayers(env, 1) { instance, players -> + val zombie = Entity(EntityType.ZOMBIE) + zombie.setInstance(instance, Pos(2.0, 0.0, 0.0, 270f, 30f)).join() + try { + val raw = EntitiesArgument("targets", allowEmpty = true).toDefinition().rawType + val prefix = "@e[type=minecraft:zombie," + assertEquals(listOf(zombie), raw.parse(StringReader(prefix + "y_rotation=-135..-45]"), players[0])) + assertEquals(emptyList(), raw.parse(StringReader(prefix + "y_rotation=45..135]"), players[0])) + assertEquals(listOf(zombie), raw.parse(StringReader(prefix + "x_rotation=20..40]"), players[0])) + assertEquals(emptyList(), raw.parse(StringReader(prefix + "x_rotation=..10]"), players[0])) + assertEquals(listOf(zombie), raw.parse(StringReader(prefix + "y_rotation=200..]"), players[0])) + } finally { + zombie.remove() + } + } + } + + @Test + fun `type option accepts entity type tags and negated tags`(env: Env) { + withPlayers(env, 1) { instance, players -> + val skeleton = Entity(EntityType.SKELETON) + val zombie = Entity(EntityType.ZOMBIE) + skeleton.setInstance(instance, Pos(2.0, 0.0, 0.0)).join() + zombie.setInstance(instance, Pos(3.0, 0.0, 0.0)).join() + try { + val raw = EntitiesArgument("targets", allowEmpty = true).toDefinition().rawType + assertEquals(listOf(skeleton), raw.parse(StringReader("@e[type=#minecraft:skeletons]"), players[0])) + assertEquals( + setOf(zombie, players[0]), + raw.parse(StringReader("@e[type=!#minecraft:skeletons]"), players[0]).toSet(), + ) + assertEquals(emptyList(), raw.parse(StringReader("@e[type=#minecraft:no_such_tag]"), players[0])) + assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader("@e[type=minecraft:zombie,type=#minecraft:skeletons]"), players[0]) + } + assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader("@e[type=#minecraft:skeletons,type=#minecraft:skeletons]"), players[0]) + } + } finally { + skeleton.remove() + zombie.remove() + } + } + } + + @Test + fun `a bare token with a uuid group wider than canonical is treated as a name`(env: Env) { + withPlayers(env, 1) { _, players -> + val raw = EntityArgument("target").toDefinition().rawType + assertThrows(CommandSyntaxException::class.java) { + raw.parse(StringReader("1ffffffff-0-0-0-0"), players[0]) + } + } + } + + @Test + fun `suggestions list the matching player names and selectors including at-n`(env: Env) { + withPlayers(env, 2) { _, players -> + val context = CommandDispatcher().parse("", Any()).context.build("") + val raw = EntityArgument("target").toDefinition().rawType + + val names = raw.listSuggestions(context, SuggestionsBuilder("Player", 0)).join() + assertEquals(players.map(Player::getUsername).sorted(), names.list.map { it.text }) + + val selectors = raw.listSuggestions(context, SuggestionsBuilder("@", 0)).join() + assertTrue(selectors.list.any { it.text == "@n" }) + } + } + + private fun withPlayers(env: Env, count: Int, block: (Instance, List) -> Unit) { + val instance = env.createEmptyInstance() + val players = mutableListOf() + try { + repeat(count) { index -> + val player = connect(env.createConnection(profile("Player$index")), instance) + check(env.tickWhile({ player.instance !== instance }, Duration.ofSeconds(1))) { + "Player$index was not spawned in the requested test instance" + } + players += player + } + block(instance, players) + } finally { + players.forEach(Player::remove) + env.destroyInstance(instance) + } + } + + private fun connect(connection: TestConnection, instance: Instance): Player { + val connected = CompletableFuture() + Thread.startVirtualThread { + runCatching { connection.connect(instance, Pos.ZERO) } + .onSuccess(connected::complete) + .onFailure(connected::completeExceptionally) + } + return connected.join() + } + + private fun profile(name: String) = GameProfile(UUID.randomUUID(), name) +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/FixedSetParserTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/FixedSetParserTest.kt new file mode 100644 index 000000000..1c58a1153 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/FixedSetParserTest.kt @@ -0,0 +1,87 @@ +package dev.slne.surf.api.minestom.server.impl.command.parser + +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import dev.slne.surf.api.minestom.command.argument.EnumArgument +import dev.slne.surf.api.minestom.command.argument.GameModeArgument +import dev.slne.surf.api.minestom.command.argument.MultiLiteralArgument +import dev.slne.surf.api.minestom.command.argument.TeamColorArgument +import net.minestom.server.color.TeamColor +import net.minestom.server.entity.GameMode +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test + +/** + * Tests `FixedSetParser`, which is `internal` to the api module, through the `rawType` of + * [dev.slne.surf.api.minestom.command.argument.Argument.toDefinition]. + */ +class FixedSetParserTest { + private enum class AccessLevel { STAFF, GUEST } + + @Test + fun `a known value resolves`() { + val parser = MultiLiteralArgument("side", "left", "right").toDefinition().rawType + assertEquals("right", parser.parse(StringReader("right"))) + } + + @Test + fun `an unknown value is rejected at its own start`() { + val parser = MultiLiteralArgument("side", "left", "right").toDefinition().rawType + val failure = assertThrows(CommandSyntaxException::class.java) { parser.parse(StringReader("up")) } + assertEquals(0, failure.cursor) + } + + @Test + fun `the match is case sensitive and stops at whitespace`() { + val parser = MultiLiteralArgument("side", "left", "right").toDefinition().rawType + val reader = StringReader("left over") + assertEquals("left", parser.parse(reader)) + assertEquals(" over", reader.remaining) + + assertThrows(CommandSyntaxException::class.java) { parser.parse(StringReader("LEFT")) } + } + + @Test + fun `enum argument resolves the formatted spelling to its constant`() { + val parser = EnumArgument( + "access", + AccessLevel.entries, + ) { value -> "mode-${value.name.lowercase()}" }.toDefinition().rawType + + assertEquals(AccessLevel.STAFF, parser.parse(StringReader("mode-staff"))) + assertThrows(CommandSyntaxException::class.java) { parser.parse(StringReader("staff")) } + } + + @Test + fun `game mode keeps its vanilla spellings and matches case sensitively`() { + val parser = GameModeArgument("mode").toDefinition().rawType + + assertEquals(GameMode.SURVIVAL, parser.parse(StringReader("survival"))) + assertEquals(GameMode.CREATIVE, parser.parse(StringReader("creative"))) + assertEquals(GameMode.ADVENTURE, parser.parse(StringReader("adventure"))) + assertEquals(GameMode.SPECTATOR, parser.parse(StringReader("spectator"))) + assertThrows(CommandSyntaxException::class.java) { parser.parse(StringReader("SURVIVAL")) } + } + + @Test + fun `team color keeps its vanilla spellings and matches case sensitively`() { + val parser = TeamColorArgument("color").toDefinition().rawType + + assertEquals(TeamColor.RED, parser.parse(StringReader("red"))) + assertEquals(TeamColor.DARK_BLUE, parser.parse(StringReader("dark_blue"))) + assertThrows(CommandSyntaxException::class.java) { parser.parse(StringReader("RED")) } + } + + @Test + fun `suggestions are offered case-insensitively`() { + val parser = MultiLiteralArgument("side", "left", "right").toDefinition().rawType + val context = CommandDispatcher().parse("", Any()).context.build("") + + val suggestions = parser.listSuggestions(context, SuggestionsBuilder("LEF", 0)).join() + + assertEquals(listOf("left"), suggestions.list.map { it.text }) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/InstanceParserTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/InstanceParserTest.kt new file mode 100644 index 000000000..c65e21ff0 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/InstanceParserTest.kt @@ -0,0 +1,63 @@ +package dev.slne.surf.api.minestom.server.impl.command.parser + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.exceptions.CommandSyntaxException +import dev.slne.surf.api.minestom.command.argument.InstanceArgument +import net.minestom.server.world.DimensionType +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test + +/** + * Tests `InstanceParser`, which is `internal` to the api module, through the `rawType` of + * [dev.slne.surf.api.minestom.command.argument.Argument.toDefinition]. + */ +@EnvTest +class InstanceParserTest { + private val parser = InstanceArgument("instance").toDefinition().rawType + + @Test + fun `an instance resolves by its uuid`(env: Env) { + val instance = env.process().instance().createInstanceContainer(DimensionType.OVERWORLD) + try { + assertEquals(instance, parser.parse(StringReader(instance.uuid.toString()))) + } finally { + env.destroyInstance(instance) + } + } + + @Test + fun `an instance resolves by its unique dimension name`(env: Env) { + val instance = env.process().instance().createInstanceContainer(DimensionType.THE_END) + try { + assertEquals(instance, parser.parse(StringReader("minecraft:the_end"))) + } finally { + env.destroyInstance(instance) + } + } + + @Test + fun `an ambiguous dimension name is rejected at the start of the token`(env: Env) { + val first = env.process().instance().createInstanceContainer(DimensionType.THE_NETHER) + val second = env.process().instance().createInstanceContainer(DimensionType.THE_NETHER) + try { + val failure = assertThrows(CommandSyntaxException::class.java) { + parser.parse(StringReader("minecraft:the_nether")) + } + assertEquals(0, failure.cursor) + } finally { + env.destroyInstance(second) + env.destroyInstance(first) + } + } + + @Test + fun `an unknown token is rejected at its start`(env: Env) { + val failure = assertThrows(CommandSyntaxException::class.java) { + parser.parse(StringReader("not-a-real-instance")) + } + assertEquals(0, failure.cursor) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/ParticleParserTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/ParticleParserTest.kt new file mode 100644 index 000000000..be022a0e3 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/ParticleParserTest.kt @@ -0,0 +1,44 @@ +package dev.slne.surf.api.minestom.server.impl.command.parser + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.exceptions.CommandSyntaxException +import dev.slne.surf.api.minestom.command.argument.ParticleArgument +import net.minestom.server.particle.Particle +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test + +/** + * Tests `ParticleParser`, which is `internal` to the api module, through the `rawType` of + * [dev.slne.surf.api.minestom.command.argument.Argument.toDefinition]. + */ +@EnvTest +class ParticleParserTest { + private val parser = ParticleArgument("particle").toDefinition().rawType + + @Test + fun `a simple particle resolves to its value`(env: Env) { + assertEquals( + Particle.fromKey("minecraft:flame"), + parser.parse(StringReader("minecraft:flame")), + ) + } + + @Test + fun `an option-carrying particle is rejected as unsupported at the start of the key`(env: Env) { + val failure = assertThrows(CommandSyntaxException::class.java) { + parser.parse(StringReader("minecraft:dust")) + } + assertEquals(0, failure.cursor) + } + + @Test + fun `an unknown key is rejected at the start of the key`(env: Env) { + val failure = assertThrows(CommandSyntaxException::class.java) { + parser.parse(StringReader("minecraft:not_a_real_particle")) + } + assertEquals(0, failure.cursor) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/RegistryParserTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/RegistryParserTest.kt new file mode 100644 index 000000000..1b60b4307 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/RegistryParserTest.kt @@ -0,0 +1,60 @@ +package dev.slne.surf.api.minestom.server.impl.command.parser + +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import dev.slne.surf.api.minestom.command.argument.ResourceArgument +import dev.slne.surf.api.minestom.command.argument.SoundArgument +import net.kyori.adventure.key.Key +import net.minestom.server.registry.DynamicRegistry +import net.minestom.server.sound.SoundEvent +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test + +/** + * Tests `RegistryParser`, which is `internal` to the api module, through the `rawType` of + * [dev.slne.surf.api.minestom.command.argument.Argument.toDefinition]. + */ +@EnvTest +class RegistryParserTest { + private val registry = DynamicRegistry.fromMap( + Key.key("test:registry-parser"), + java.util.Map.entry(Key.key("test:alpha"), "alpha"), + java.util.Map.entry(Key.key("test:beta"), "beta"), + ) + + @Test + fun `a known key resolves to its value`(env: Env) { + val parser = SoundArgument("sound").toDefinition().rawType + assertEquals( + SoundEvent.fromKey("minecraft:entity.pig.ambient"), + parser.parse(StringReader("minecraft:entity.pig.ambient")), + ) + } + + @Test + fun `an unknown key is rejected at the start of the key`(env: Env) { + val parser = SoundArgument("sound").toDefinition().rawType + + val failure = assertThrows(CommandSyntaxException::class.java) { + parser.parse(StringReader("minecraft:not_a_real_sound")) + } + assertEquals(0, failure.cursor) + } + + @Test + fun `suggestions are limited to the keys that continue what was typed`(env: Env) { + val parser = ResourceArgument("value", "test:registry-parser", registry) + .toDefinition() + .rawType + val context = CommandDispatcher().parse("", Any()).context.build("") + + val suggestions = parser.listSuggestions(context, SuggestionsBuilder("test:al", 0)).join() + + assertEquals(listOf("test:alpha"), suggestions.list.map { it.text }) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/ResourceLocationParserTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/ResourceLocationParserTest.kt new file mode 100644 index 000000000..99d74bbb4 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/ResourceLocationParserTest.kt @@ -0,0 +1,54 @@ +package dev.slne.surf.api.minestom.server.impl.command.parser + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.exceptions.CommandSyntaxException +import dev.slne.surf.api.minestom.command.argument.ResourceLocationArgument +import net.kyori.adventure.key.Key +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test + +/** + * Tests `ResourceLocationParser`, which is `internal` to the api module, through the `rawType` of + * [dev.slne.surf.api.minestom.command.argument.Argument.toDefinition]. + */ +class ResourceLocationParserTest { + private val parser = ResourceLocationArgument("key").toDefinition().rawType + + @Test + fun `a bare path defaults to the minecraft namespace`() { + assertEquals(Key.key("minecraft", "stone"), parser.parse(StringReader("stone"))) + } + + @Test + fun `an explicit namespace is kept`() { + assertEquals(Key.key("surf", "lobby"), parser.parse(StringReader("surf:lobby"))) + } + + @Test + fun `parsing stops at the first character that cannot belong to a key`() { + val reader = StringReader("minecraft:stone more") + assertEquals(Key.key("minecraft", "stone"), parser.parse(reader)) + assertEquals(" more", reader.remaining) + } + + @Test + fun `an invalid character set is rejected`() { + assertThrows(CommandSyntaxException::class.java) { parser.parse(StringReader("Stone")) } + } + + @Test + fun `a leading separator defaults to the minecraft namespace`() { + assertEquals(Key.key("minecraft", "lobby"), parser.parse(StringReader(":lobby"))) + } + + @Test + fun `a second separator inside the value is rejected`() { + assertThrows(CommandSyntaxException::class.java) { parser.parse(StringReader("a:b:c")) } + } + + @Test + fun `a namespace containing a slash is rejected`() { + assertThrows(CommandSyntaxException::class.java) { parser.parse(StringReader("a/b:c")) } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/ScalarParsersTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/ScalarParsersTest.kt new file mode 100644 index 000000000..153d783b4 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/ScalarParsersTest.kt @@ -0,0 +1,135 @@ +package dev.slne.surf.api.minestom.server.impl.command.parser + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.exceptions.CommandSyntaxException +import dev.slne.surf.api.minestom.command.argument.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import java.util.* + +/** + * Tests the angle, float range, integer range, time and UUID parsers, which are `internal` to the + * api module, through the `rawType` of + * [dev.slne.surf.api.minestom.command.argument.Argument.toDefinition]. + */ +class ScalarParsersTest { + @Test + fun `uuid is read in its dashed form`() { + val uuid = UUID.fromString("79d40754-7772-4ab4-a7c6-f21df121db4a") + val parser = UUIDArgument("id").toDefinition().rawType + assertEquals(uuid, parser.parse(StringReader(uuid.toString()))) + } + + @Test + fun `a uuid with a group wider than canonical is rejected at its start`() { + val parser = UUIDArgument("id").toDefinition().rawType + listOf( + "1ffffffff-0-0-0-0", + "79d40754-7772-4ab4-a7c6-f21df121db4a0", + "79d40754-77720-4ab4-a7c6-f21df121db4a", + ).forEach { input -> + val failure = assertThrows(CommandSyntaxException::class.java) { parser.parse(StringReader(input)) } + assertEquals(0, failure.cursor, input) + } + assertEquals( + UUID.fromString("79d40754-7772-4ab4-a7c6-f21df121db4a"), + parser.parse(StringReader("79D40754-7772-4AB4-A7C6-F21DF121DB4A")), + ) + } + + @Test + fun `a uuid with groups shorter than canonical is accepted`() { + val parser = UUIDArgument("id").toDefinition().rawType + assertEquals(UUID.fromString("1234-5-6-7-8"), parser.parse(StringReader("1234-5-6-7-8"))) + } + + @Test + fun `a malformed uuid is rejected at its start`() { + val parser = UUIDArgument("id").toDefinition().rawType + val reader = StringReader("1234-5678") + val failure = assertThrows(CommandSyntaxException::class.java) { parser.parse(reader) } + assertEquals(0, failure.cursor) + } + + @Test + fun `integer range covers both bounds, one bound and a single value`() { + val parser = IntegerRangeArgument("levels").toDefinition().rawType + assertEquals(1 to 4, parser.parse(StringReader("1..4")).let { it.min() to it.max() }) + assertEquals(1 to Int.MAX_VALUE, parser.parse(StringReader("1..")).let { it.min() to it.max() }) + assertEquals(Int.MIN_VALUE to 4, parser.parse(StringReader("..4")).let { it.min() to it.max() }) + assertEquals(3 to 3, parser.parse(StringReader("3")).let { it.min() to it.max() }) + } + + @Test + fun `an integer range with neither bound present is rejected at its start`() { + val parser = IntegerRangeArgument("levels").toDefinition().rawType + val reader = StringReader("..") + val failure = assertThrows(CommandSyntaxException::class.java) { parser.parse(reader) } + assertEquals(0, failure.cursor) + } + + @Test + fun `float range is reachable through rawType and covers a partial bound`() { + val parser = FloatRangeArgument("speeds").toDefinition().rawType + val range = parser.parse(StringReader("1.5..")) + assertEquals(1.5f, range.min()) + assertEquals(Float.MAX_VALUE, range.max()) + } + + @Test + fun `a float range with neither bound present is rejected at its start`() { + val parser = FloatRangeArgument("speeds").toDefinition().rawType + val reader = StringReader("..") + val failure = assertThrows(CommandSyntaxException::class.java) { parser.parse(reader) } + assertEquals(0, failure.cursor) + } + + @Test + fun `time accepts every vanilla unit and defaults to ticks`() { + val parser = TimeArgument("delay").toDefinition().rawType + assertEquals(20L, parser.parse(StringReader("20")).toMillis() / 50) + assertEquals(20L, parser.parse(StringReader("1s")).toMillis() / 50) + assertEquals(24000L, parser.parse(StringReader("1d")).toMillis() / 50) + } + + @Test + fun `time rounds to the nearest tick like vanilla`() { + val parser = TimeArgument("delay").toDefinition().rawType + assertEquals(2L, parser.parse(StringReader("1.5t")).toMillis() / 50) + assertEquals(1L, parser.parse(StringReader("1.4t")).toMillis() / 50) + assertEquals(30L, parser.parse(StringReader("1.5s")).toMillis() / 50) + } + + @Test + fun `time rejects values whose duration overflows at the value's start`() { + val parser = TimeArgument("delay").toDefinition().rawType + listOf("99999999999999999999999999999999999999d", "99999999999999999999", "100000000000000000d").forEach { input -> + val failure = assertThrows(CommandSyntaxException::class.java) { parser.parse(StringReader(input)) } + assertEquals(0, failure.cursor, input) + } + } + + @Test + fun `time rejects an unknown unit at the value's start`() { + val parser = TimeArgument("delay").toDefinition().rawType + val reader = StringReader("5x") + val failure = assertThrows(CommandSyntaxException::class.java) { parser.parse(reader) } + assertEquals(0, failure.cursor) + } + + @Test + fun `time rejects a negative duration at the value's start`() { + val parser = TimeArgument("delay").toDefinition().rawType + val reader = StringReader("-5") + val failure = assertThrows(CommandSyntaxException::class.java) { parser.parse(reader) } + assertEquals(0, failure.cursor) + } + + @Test + fun `angle rejects a bare tilde with trailing text`() { + val parser = AngleArgument("angle").toDefinition().rawType + assertEquals(90.0f, parser.parse(StringReader("90"))) + assertThrows(CommandSyntaxException::class.java) { parser.parse(StringReader("x")) } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/StructuralParserTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/StructuralParserTest.kt new file mode 100644 index 000000000..4644512a2 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/command/parser/StructuralParserTest.kt @@ -0,0 +1,182 @@ +package dev.slne.surf.api.minestom.server.impl.command.parser + +import com.mojang.brigadier.LiteralMessage +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType +import dev.slne.surf.api.minestom.command.argument.* +import dev.slne.surf.api.minestom.command.exception.WrapperCommandSyntaxException +import net.kyori.adventure.identity.Identity +import net.kyori.adventure.text.Component +import net.minestom.server.command.CommandSender +import net.minestom.server.coordinate.Vec +import net.minestom.server.tag.TagHandler +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test + +/** + * Tests the structural parsers, which are `internal` to the api module, through the `rawType` of + * [dev.slne.surf.api.minestom.command.argument.Argument.toDefinition]. + */ +class StructuralParserTest { + private val sender = StubSender() + + @Test + fun `custom argument applies its parser to the base value`() { + val argument = CustomArgument(StringArgument("name")) { info -> "resolved:${info.baseValue}" } + assertEquals( + "resolved:lobby", + argument.toDefinition().rawType.parse(StringReader("lobby"), sender), + ) + } + + @Test + fun `custom argument info carries the sender and the exact consumed input`() { + val argument = CustomArgument(StringArgument("name")) { info -> + Triple(info.sender, info.currentInput, info.baseValue) + } + val reader = StringReader("lobby rest") + val (capturedSender, currentInput, baseValue) = argument.toDefinition().rawType.parse(reader, sender) + + assertEquals(sender, capturedSender) + assertEquals("lobby", currentInput) + assertEquals("lobby", baseValue) + assertEquals(" rest", reader.remaining) + } + + @Test + fun `custom argument shares its base argument's node name`() { + val argument = CustomArgument(StringArgument("server")) { info -> info.baseValue.length } + assertEquals("server", argument.nodeName) + } + + @Test + fun `custom argument without a command source is rejected`() { + val argument = CustomArgument(StringArgument("name")) { info -> info.baseValue } + val failure = assertThrows(CommandSyntaxException::class.java) { + argument.toDefinition().rawType.parse(StringReader("lobby")) + } + assertEquals(0, failure.cursor) + } + + @Test + fun `custom argument forwards the command source into its base argument`() { + val argument = CustomArgument(PositionArgument("pos")) { info -> info.baseValue } + assertEquals( + Vec(1.0, 2.0, 3.0), + argument.toDefinition().rawType.parse(StringReader("~1 ~2 ~3"), sender), + ) + } + + @Test + fun `a brigadier syntax exception thrown by the lambda passes through unchanged`() { + val original = SimpleCommandExceptionType(LiteralMessage("nope")).create() + val argument = CustomArgument(StringArgument("name")) { throw original } + val failure = assertThrows(CommandSyntaxException::class.java) { + argument.toDefinition().rawType.parse(StringReader("lobby"), sender) + } + assertEquals(original, failure) + } + + @Test + fun `the project's own syntax exception is translated to a brigadier syntax error`() { + val argument = CustomArgument(StringArgument("name")) { info -> + throw dev.slne.surf.api.minestom.command.exception.CommandSyntaxException( + Component.text("bad value"), + ) + } + val reader = StringReader("lobby") + val failure = assertThrows(CommandSyntaxException::class.java) { + argument.toDefinition().rawType.parse(reader, sender) + } + assertEquals(0, failure.cursor) + assertEquals("bad value", failure.rawMessage.string) + } + + @Test + fun `a wrapped project syntax exception is unwrapped and translated`() { + val argument = CustomArgument(StringArgument("name")) { info -> + throw WrapperCommandSyntaxException( + dev.slne.surf.api.minestom.command.exception.CommandSyntaxException( + Component.text("wrapped failure"), + ), + ) + } + val reader = StringReader("lobby") + val failure = assertThrows(CommandSyntaxException::class.java) { + argument.toDefinition().rawType.parse(reader, sender) + } + assertEquals(0, failure.cursor) + assertEquals("wrapped failure", failure.rawMessage.string) + } + + @Test + fun `list argument reads delimited elements`() { + val argument = ListArgument("values", IntegerArgument("value"), ',', allowEmpty = false) + assertEquals(listOf(1, 2, 3), argument.toDefinition().rawType.parse(StringReader("1,2,3"))) + } + + @Test + fun `list argument skips whitespace around its delimiter`() { + val argument = ListArgument("values", IntegerArgument("value"), ',', allowEmpty = false) + assertEquals(listOf(1, 2, 3), argument.toDefinition().rawType.parse(StringReader(" 1 , 2 ,3 "))) + } + + @Test + fun `list argument rejects an empty list unless allowEmpty is set`() { + val required = ListArgument("values", IntegerArgument("value"), ',', allowEmpty = false) + assertThrows(CommandSyntaxException::class.java) { + required.toDefinition().rawType.parse(StringReader("")) + } + + val optional = ListArgument("values", IntegerArgument("value"), ',', allowEmpty = true) + assertEquals(emptyList(), optional.toDefinition().rawType.parse(StringReader(""))) + } + + @Test + fun `list argument rejects a stray delimiter through the element parser's own empty-input rejection`() { + val argument = ListArgument("values", IntegerArgument("value"), ',', allowEmpty = false) + assertThrows(CommandSyntaxException::class.java) { + argument.toDefinition().rawType.parse(StringReader("1,,2")) + } + } + + @Test + fun `list argument rejects a stray delimiter for an element type that would otherwise accept empty input`() { + val argument = ListArgument("tags", StringArgument("tag"), ',', allowEmpty = false) + + assertThrows(CommandSyntaxException::class.java) { + argument.toDefinition().rawType.parse(StringReader("a,,b")) + } + assertThrows(CommandSyntaxException::class.java) { + argument.toDefinition().rawType.parse(StringReader("a,b,")) + } + assertThrows(CommandSyntaxException::class.java) { + argument.toDefinition().rawType.parse(StringReader(",a,b")) + } + } + + @Test + fun `list argument allows an element type's empty input when allowEmpty is set`() { + val argument = ListArgument("tags", StringArgument("tag"), ',', allowEmpty = true) + assertEquals( + listOf("a", "", "b", ""), + argument.toDefinition().rawType.parse(StringReader("a,,b,")), + ) + } + + @Test + fun `command argument reads the remaining input as a string`() { + val argument = CommandArgument("command") + assertEquals( + "give @s stone 1", + argument.toDefinition().rawType.parse(StringReader("give @s stone 1")), + ) + } + + private class StubSender : CommandSender { + override fun identity(): Identity = Identity.nil() + override fun tagHandler(): TagHandler = TagHandler.newHandler() + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/configuration/ConfigurationPhaseMixinTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/configuration/ConfigurationPhaseMixinTest.kt new file mode 100644 index 000000000..90be66cd5 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/configuration/ConfigurationPhaseMixinTest.kt @@ -0,0 +1,77 @@ +package dev.slne.surf.api.minestom.server.impl.configuration + +import dev.slne.surf.api.minestom.server.configuration.ConfigurationTask +import dev.slne.surf.api.minestom.server.configuration.ConfigurationTaskId +import dev.slne.surf.api.minestom.server.configuration.ConfigurationTasks +import dev.slne.surf.api.minestom.server.surfMinestomServer +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.Player +import net.minestom.server.instance.Instance +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import net.minestom.testing.TestConnection +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CopyOnWriteArrayList + +/** + * Tests that `ConnectionManagerMixin` hands `ConnectionManager#doConfiguration` to the surf + * configuration phase while the phase is installed, and leaves it to Minestom otherwise. + */ +@EnvTest +class ConfigurationPhaseMixinTest { + + @Test + fun `the installed configuration phase runs in place of Minestom's`(env: Env) { + val runs = CopyOnWriteArrayList() + val server = surfMinestomServer { + withConfigurationPhase { + // Both wait for a client response that a test connection never sends. + remove(ConfigurationTasks.AWAIT_SETTINGS) + remove(ConfigurationTasks.SYNCHRONIZE_REGISTRIES) + first(RECORD, ConfigurationTask { context -> runs += context.isFirstConfig }) + } + } + + val instance = env.createFlatInstance() + try { + val player = connect(env.createConnection(), instance) + assertEquals(listOf(false), runs) + assertTrue(player.isOnline) + player.remove() + } finally { + env.destroyInstance(instance) + server.shutdown() + } + } + + @Test + fun `without the configuration phase Minestom configures the player`(env: Env) { + val server = surfMinestomServer() + val instance = env.createFlatInstance() + try { + val player = connect(env.createConnection(), instance) + assertTrue(player.isOnline) + player.remove() + } finally { + env.destroyInstance(instance) + server.shutdown() + } + } + + private fun connect(connection: TestConnection, instance: Instance): Player { + val player = CompletableFuture() + Thread.startVirtualThread { + runCatching { connection.connect(instance, Pos.ZERO) } + .onSuccess(player::complete) + .onFailure(player::completeExceptionally) + } + return player.join() + } + + private companion object { + val RECORD = ConfigurationTaskId("record") + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/mixin/ConfiguredMixinsApplyTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/mixin/ConfiguredMixinsApplyTest.kt new file mode 100644 index 000000000..595a45ba6 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/mixin/ConfiguredMixinsApplyTest.kt @@ -0,0 +1,54 @@ +package dev.slne.surf.api.minestom.server.impl.mixin + +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.DynamicTest.dynamicTest +import org.junit.jupiter.api.TestFactory +import org.objectweb.asm.Type +import org.objectweb.asm.tree.AnnotationNode +import org.objectweb.asm.tree.ClassNode +import org.spongepowered.asm.mixin.transformer.meta.MixinMerged + +/** + * Applies every configured mixin to the shipped bytecode of each of its targets. + * + * In production a failed transformation leaves the class untransformed without an error, so an + * injection point moved by a Minestom update must fail here. Each mixin is asserted by its own + * mark on the target, so one mixin applying cannot mask another failing on the same target. + */ +class ConfiguredMixinsApplyTest { + + @TestFactory + fun `configured mixins apply to all declared targets`(): List { + val mixins = MixinTestSupport.CONFIGURATIONS.flatMap(MixinTestSupport::mixinsOf) + check(mixins.isNotEmpty()) { "No mixins configured" } + + return mixins.flatMap { mixin -> + val targets = MixinTestSupport.targetsOf(mixin) + check(targets.isNotEmpty()) { "$mixin declares no targets" } + + targets.map { target -> + dynamicTest("${mixin.substringAfterLast('.')} -> $target") { + val transformed = MixinTestSupport.transform(target) + assertTrue(transformed.isMergedFrom(mixin)) { "$mixin left no trace in $target" } + } + } + } + } + + private fun ClassNode.isMergedFrom(mixin: String): Boolean { + val interfaces = MixinTestSupport.interfacesOf(mixin) + if (interfaces.isNotEmpty() && this.interfaces.containsAll(interfaces)) return true + + val annotations = methods.flatMap { it.invisibleAnnotations.orEmpty() + it.visibleAnnotations.orEmpty() } + + fields.flatMap { it.invisibleAnnotations.orEmpty() + it.visibleAnnotations.orEmpty() } + return annotations.any { it.desc == MERGED && it.value("mixin") == mixin } + } + + private fun AnnotationNode.value(key: String): Any? = + values.orEmpty().chunked(2).firstOrNull { (name, _) -> name == key }?.get(1) + + private companion object { + val MERGED: String = Type.getDescriptor(MixinMerged::class.java) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/mixin/EntityTrackerMixinApplyTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/mixin/EntityTrackerMixinApplyTest.kt new file mode 100644 index 000000000..f00e2f0e8 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/mixin/EntityTrackerMixinApplyTest.kt @@ -0,0 +1,52 @@ +package dev.slne.surf.api.minestom.server.impl.mixin + +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.objectweb.asm.tree.MethodInsnNode + +/** + * Applies the configured mixins to the shipped `EntityTracker` bytecode through the service the + * production agent uses, so a bytecode change that breaks an injection point fails here instead of + * silently at runtime. The transformer runs directly; no transformed class is loaded. + */ +class EntityTrackerMixinApplyTest { + + @Test + fun `the entity tracker mixins apply to the shipped minestom bytecode`() { + val impl = MixinTestSupport.transform("net.minestom.server.instance.EntityTrackerImpl") + val entry = MixinTestSupport.transform( + "net.minestom.server.instance.EntityTrackerImpl\$EntityTrackerEntry", + ) + + // Mixin decorates handler names, e.g. handler$zzd000$surf$directNearbyScan. + val implMethods = impl.methods.map { it.name } + assertTrue(implMethods.any { it.endsWith("surf\$directNearbyScan") }) { + "fast-path inject missing: $implMethods" + } + assertTrue(implMethods.any { it.endsWith("surf\$rectangularChunkScan") }) { + "chunk-scan redirect missing: $implMethods" + } + + val nearby = impl.methods.single { it.name == "nearbyEntitiesByChunkRange" } + val calls = nearby.instructions.asSequence() + .filterIsInstance() + .map { "${it.owner}.${it.name}" } + .toList() + assertTrue(calls.none { it == "net/minestom/server/coordinate/ChunkRange.chunksInRange" }) { + "the spiral traversal was not redirected: $calls" + } + assertTrue(calls.any { it.endsWith("surf\$rectangularChunkScan") }) { + "the rectangular redirect handler is not called: $calls" + } + assertTrue(calls.any { it.endsWith("surf\$directNearbyScan") }) { + "the fast-path injection is not called: $calls" + } + + assertTrue("dev/slne/surf/api/minestom/server/impl/mixin/duck/EntityTrackerEntryDuck" in entry.interfaces) { + "EntityTrackerEntry does not implement the duck: ${entry.interfaces}" + } + assertTrue(entry.methods.any { it.name == "surf\$lastPosition" }) { + "the lastPosition accessor was not generated" + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/mixin/LightingChunkSkylightMixinApplyTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/mixin/LightingChunkSkylightMixinApplyTest.kt new file mode 100644 index 000000000..739be94ce --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/mixin/LightingChunkSkylightMixinApplyTest.kt @@ -0,0 +1,40 @@ +package dev.slne.surf.api.minestom.server.impl.mixin + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.objectweb.asm.tree.MethodInsnNode + +class LightingChunkSkylightMixinApplyTest { + + @Test + fun `createLightData reads its sky light through the redirect`() { + val chunk = MixinTestSupport.transform("net.minestom.server.instance.LightingChunk") + + val methods = chunk.methods.map { it.name } + assertTrue(methods.any { it.endsWith("surf\$skipDisabledSkyLight") }) { + "the sky light redirect handler is missing: $methods" + } + + val calls = chunk.methods + .single { it.name == "createLightData" } + .instructions + .asSequence() + .filterIsInstance() + .map { "${it.owner}.${it.name}" } + .toList() + + assertEquals(0, calls.count { it == "net/minestom/server/instance/Section.skyLight" }) { + "createLightData still reads a section's own sky light: $calls" + } + assertTrue(calls.any { it.endsWith("surf\$skipDisabledSkyLight") }) { + "expected the sky light reads to be redirected: $calls" + } + assertTrue(calls.any { it == "net/minestom/server/instance/Section.blockLight" }) { + "block light must be untouched: $calls" + } + assertTrue(calls.any { it.endsWith(".relightSection") }) { + "createLightData lost its relight, the redirect should only skip the sky one: $calls" + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/mixin/MixinTestSupport.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/mixin/MixinTestSupport.kt new file mode 100644 index 000000000..59bcaaf1c --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/mixin/MixinTestSupport.kt @@ -0,0 +1,70 @@ +package dev.slne.surf.api.minestom.server.impl.mixin + +import com.google.gson.JsonParser +import dev.slne.surf.api.minestom.server.testing.SurfMixinTestEnvironment +import org.objectweb.asm.ClassReader +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type +import org.objectweb.asm.tree.ClassNode +import org.spongepowered.asm.mixin.Mixin + +object MixinTestSupport { + + /** + * The mixin configurations applied in tests. + */ + val CONFIGURATIONS: List get() = SurfMixinTestEnvironment.configurations + + fun install() = SurfMixinTestEnvironment.install() + + fun transform(binaryName: String): ClassNode = MixinTransforms.transform(binaryName) + + fun originalBytes(binaryName: String): ByteArray = MixinTransforms.originalBytes(binaryName) + + /** + * The binary names of the mixin classes listed in [configuration]. + */ + fun mixinsOf(configuration: String): List { + val json = checkNotNull(javaClass.classLoader.getResourceAsStream(configuration)) { + "Missing $configuration" + }.bufferedReader().use { JsonParser.parseReader(it).asJsonObject } + + val mixinPackage = json.get("package").asString + return listOf("mixins", "server", "client").flatMap { section -> + json.getAsJsonArray(section)?.map { "$mixinPackage.${it.asString}" }.orEmpty() + } + } + + /** + * The internal names of the interfaces [mixin] adds to its targets. + */ + fun interfacesOf(mixin: String): List { + val node = ClassNode(Opcodes.ASM9) + ClassReader(originalBytes(mixin)).accept(node, ClassReader.SKIP_CODE) + return node.interfaces.orEmpty() + } + + /** + * The binary names of the classes [mixin] targets, read from its `@Mixin` annotation. + */ + fun targetsOf(mixin: String): List { + val node = ClassNode(Opcodes.ASM9) + ClassReader(originalBytes(mixin)).accept( + node, + ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES, + ) + + val annotation = checkNotNull( + (node.visibleAnnotations.orEmpty() + node.invisibleAnnotations.orEmpty()) + .singleOrNull { it.desc == Type.getDescriptor(Mixin::class.java) }, + ) { "$mixin has no @Mixin annotation" } + + return annotation.values.orEmpty().chunked(2).flatMap { (key, value) -> + when (key) { + "value" -> (value as List<*>).map { (it as Type).className } + "targets" -> (value as List<*>).map { (it as String).replace('/', '.') } + else -> emptyList() + } + }.distinct() + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/performance/EntityViewerLookupTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/performance/EntityViewerLookupTest.kt new file mode 100644 index 000000000..13f5bed73 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/performance/EntityViewerLookupTest.kt @@ -0,0 +1,195 @@ +package dev.slne.surf.api.minestom.server.impl.performance + +import net.minestom.server.coordinate.CoordConversion +import net.minestom.server.coordinate.Point +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.Entity +import net.minestom.server.entity.EntityType +import net.minestom.server.instance.EntityTracker +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +/** + * Tests that [EntityViewerLookup.directScan] visits exactly the entities whose recorded chunk lies + * within the inclusive square around the query center. + * + * The expected sets are computed by a brute-force filter over the recorded chunk coordinates. The + * tracker itself is not used as the oracle: the Mixin test environment applies `EntityTrackerMixin` + * JVM-wide, so [EntityTracker.nearbyEntitiesByChunkRange] may delegate to `directScan`. + */ +@EnvTest +class EntityViewerLookupTest { + + @Test + fun `every range matches the reference filter on and around the boundary`(env: Env) { + for (range in listOf(0, 1, 8, 32)) { + for ((centerX, centerZ) in listOf(0 to 0, 37 to -12)) { + val fixture = TrackerFixture() + + fixture.spawn(centerX, centerZ) + // On each edge and corner of the range square. + for (dx in intArrayOf(-range, 0, range)) { + for (dz in intArrayOf(-range, 0, range)) { + fixture.spawn(centerX + dx, centerZ + dz) + } + } + // One chunk outside each edge and corner. + val outside = range + 1 + for (dx in intArrayOf(-outside, 0, outside)) { + for (dz in intArrayOf(-outside, 0, outside)) { + if (dx != 0 || dz != 0) fixture.spawn(centerX + dx, centerZ + dz) + } + } + + fixture.assertEquivalent(centerX, centerZ, range) + + val onCorner = fixture.spawn(centerX + range, centerZ + range) + val outsideCorner = fixture.spawn(centerX + range + 1, centerZ + range + 1) + fixture.assertEquivalent(centerX, centerZ, range) + val nearby = fixture.directNearby(centerX, centerZ, range) + assertTrue(onCorner.entityId in nearby) + assertFalse(outsideCorner.entityId in nearby) + } + } + } + + @Test + fun `negative chunk coordinates match the reference filter`(env: Env) { + val fixture = TrackerFixture() + for (range in listOf(0, 1, 8, 32)) { + val centerX = -40 + val centerZ = -33 + fixture.spawn(centerX, centerZ) + fixture.spawn(centerX - range, centerZ - range) + fixture.spawn(centerX + range, centerZ - range) + fixture.spawn(centerX - range - 1, centerZ) + fixture.spawn(centerX, centerZ + range + 1) + fixture.assertEquivalent(centerX, centerZ, range) + } + } + + @Test + fun `many entities in one chunk match the reference filter`(env: Env) { + val fixture = TrackerFixture() + repeat(64) { fixture.spawn(3, -2) } + fixture.spawn(4, -2) + for (range in listOf(0, 1, 8)) { + fixture.assertEquivalent(3, -2, range) + fixture.assertEquivalent(2, -2, range) + } + } + + @Test + fun `entities distributed over thousands of chunks match the reference filter`(env: Env) { + val fixture = TrackerFixture() + for (chunkX in -40..40 step 2) { + for (chunkZ in -40..40 step 2) { + fixture.spawn(chunkX, chunkZ) + } + } + fixture.assertEquivalent(0, 0, 32) + fixture.assertEquivalent(7, -5, 32) + fixture.assertEquivalent(-39, 39, 8) + fixture.assertEquivalent(60, 60, 32) + } + + @Test + fun `registration and removal stay in sync with the reference filter`(env: Env) { + val fixture = TrackerFixture() + val entities = (0 until 20).map { fixture.spawn(it, it) } + fixture.assertEquivalent(5, 5, 8) + + entities.filterIndexed { index, _ -> index % 2 == 0 }.forEach(fixture::remove) + fixture.assertEquivalent(5, 5, 8) + + fixture.spawn(5, 5) + fixture.assertEquivalent(5, 5, 8) + fixture.assertEquivalent(5, 5, 0) + } + + @Test + fun `movement across chunk boundaries stays in sync with the reference filter`(env: Env) { + val fixture = TrackerFixture() + val walker = fixture.spawn(8, 0) + fixture.spawn(0, 0) + + fixture.assertEquivalent(0, 0, 8) + assertTrue(walker.entityId in fixture.directNearby(0, 0, 8)) + + fixture.move(walker, 9, 0) + fixture.assertEquivalent(0, 0, 8) + assertFalse(walker.entityId in fixture.directNearby(0, 0, 8)) + + fixture.move(walker, -8, 0) + fixture.assertEquivalent(0, 0, 8) + assertTrue(walker.entityId in fixture.directNearby(0, 0, 8)) + } + + /** + * A tracker plus the registered and moved positions that the resolver replays, and the chunk + * coordinates that the reference filter evaluates. + */ + private class TrackerFixture { + private val tracker = EntityTracker.newTracker() + private val recorded = HashMap() + private val recordedChunks = HashMap>() + private val resolver = + EntityViewerLookup.EntityPositionResolver { entity -> recorded[entity.entityId] } + + fun spawn(chunkX: Int, chunkZ: Int): Entity { + val entity = Entity(EntityType.ZOMBIE) + val point = chunkCenter(chunkX, chunkZ) + tracker.register(entity, point, EntityTracker.Target.ENTITIES, null) + recorded[entity.entityId] = point + recordedChunks[entity.entityId] = chunkX to chunkZ + return entity + } + + fun move(entity: Entity, chunkX: Int, chunkZ: Int) { + val point = chunkCenter(chunkX, chunkZ) + tracker.move(entity, point, EntityTracker.Target.ENTITIES, null) + recorded[entity.entityId] = point + recordedChunks[entity.entityId] = chunkX to chunkZ + } + + fun remove(entity: Entity) { + tracker.unregister(entity, EntityTracker.Target.ENTITIES, null) + recorded.remove(entity.entityId) + recordedChunks.remove(entity.entityId) + } + + fun expectedNearby(centerChunkX: Int, centerChunkZ: Int, range: Int): Set = + recordedChunks.filterValues { (chunkX, chunkZ) -> + chunkX in centerChunkX - range..centerChunkX + range && + chunkZ in centerChunkZ - range..centerChunkZ + range + }.keys + + fun directNearby(centerChunkX: Int, centerChunkZ: Int, range: Int): Set { + val ids = LinkedHashSet() + EntityViewerLookup.directScan( + tracker.entities(EntityTracker.Target.ENTITIES), + resolver, + centerChunkX, + centerChunkZ, + range, + ) { entity -> assertTrue(ids.add(entity.entityId), "direct scan visited an entity twice") } + return ids + } + + fun assertEquivalent(centerChunkX: Int, centerChunkZ: Int, range: Int) { + assertEquals( + expectedNearby(centerChunkX, centerChunkZ, range), + directNearby(centerChunkX, centerChunkZ, range), + "direct scan diverged from the reference filter at ($centerChunkX, $centerChunkZ) range $range", + ) + } + + private fun chunkCenter(chunkX: Int, chunkZ: Int): Point = + Pos(chunkX * 16.0 + 8.0, 42.0, chunkZ * 16.0 + 8.0).also { point -> + check(point.chunkX() == chunkX && point.chunkZ() == chunkZ) + check(CoordConversion.chunkIndex(point) == CoordConversion.chunkIndex(chunkX, chunkZ)) + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/performance/PlayerViewableEquivalenceTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/performance/PlayerViewableEquivalenceTest.kt new file mode 100644 index 000000000..5c99f88d0 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/performance/PlayerViewableEquivalenceTest.kt @@ -0,0 +1,108 @@ +package dev.slne.surf.api.minestom.server.impl.performance + +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.Player +import net.minestom.server.instance.EntityTracker +import net.minestom.server.instance.Instance +import net.minestom.server.instance.InstanceContainer +import net.minestom.server.network.player.GameProfile +import net.minestom.server.property.ServerProperties +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.Test +import java.util.* +import java.util.concurrent.CompletableFuture + +/** + * Tests that the direct scan plus entity ID deduplication resolves the viewers that lie within view + * distance of a chunk for real players, including players in a `SharedInstance`. + * + * The expected viewers are computed by a brute-force chunk-range filter over the player positions. + * `ChunkView` runs through `EntityTrackerMixin` in the Mixin test environment and is only compared + * against that filter, not used as the oracle. + */ +@EnvTest +class PlayerViewableEquivalenceTest { + + @Test + fun `viewable players match a deduplicated direct scan across main and shared instances`(env: Env) { + val instance = env.createFlatInstance() as InstanceContainer + instance.loadChunk(0, 0).join() + val shared = env.process().instance().createSharedInstance(instance) + + val near = connect(env, instance, "Near", Pos(8.0, 42.0, 8.0)) + val nearEdge = connect(env, instance, "NearEdge", Pos(-1.0, 42.0, 8.0)) + val far = connect(env, instance, "Far", Pos(300.0, 42.0, 300.0)) + val sharedNear = connect(env, shared, "SharedNear", Pos(24.0, 42.0, 8.0)) + awaitSpawn(env, near, nearEdge, far, sharedNear) + check(far.position.chunkX() > ServerProperties.CHUNK_VIEW_DISTANCE.get()) + + val viewDistance = ServerProperties.CHUNK_VIEW_DISTANCE.get() + val expectedViewers = listOf(near, nearEdge, far, sharedNear) + .filter { player -> + player.position.chunkX() in -viewDistance..viewDistance && + player.position.chunkZ() in -viewDistance..viewDistance + } + .map { it.entityId } + .toSet() + + val viewable = instance.entityTracker.viewable(listOf(shared), 0, 0) + val viewers = viewable.viewers.map { it.entityId }.toSet() + + val direct = HashMap() + val resolver = EntityViewerLookup.EntityPositionResolver { entity -> entity.position } + for (tracker in listOf(instance.entityTracker, shared.entityTracker)) { + EntityViewerLookup.directScan( + tracker.entities(EntityTracker.Target.PLAYERS), + resolver, + 0, + 0, + viewDistance, + ) { player -> direct.putIfAbsent(player.entityId, player) } + } + + assertEquals(expectedViewers, direct.keys) + assertEquals(expectedViewers, viewers) + assertEquals( + setOf(near.entityId, nearEdge.entityId, sharedNear.entityId), + direct.keys, + "expected exactly the players within view distance of chunk (0, 0)", + ) + } + + private fun connect(env: Env, instance: Instance, username: String, pos: Pos): Player { + val connection = env.createConnection(GameProfile(UUID.randomUUID(), username)) + return onVirtualThread { connection.connect(instance, pos) } + } + + /** + * Ticks until all [players] have spawned; the harness completes spawns off the test thread. + */ + private fun awaitSpawn(env: Env, vararg players: Player) { + repeat(SPAWN_ATTEMPTS) { + if (players.all { player -> player.instance != null }) return + env.tick() + Thread.sleep(SPAWN_POLL_MILLIS) + } + fail("the players never spawned") + } + + private fun onVirtualThread(block: () -> T): T { + val result = CompletableFuture() + + Thread.startVirtualThread { + runCatching(block) + .onSuccess(result::complete) + .onFailure(result::completeExceptionally) + } + + return result.join() + } + + private companion object { + const val SPAWN_ATTEMPTS = 200 + const val SPAWN_POLL_MILLIS = 5L + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/performance/RectangularChunkScanTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/performance/RectangularChunkScanTest.kt new file mode 100644 index 000000000..74810b894 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/performance/RectangularChunkScanTest.kt @@ -0,0 +1,59 @@ +package dev.slne.surf.api.minestom.server.impl.performance + +import net.minestom.server.coordinate.ChunkRange +import net.minestom.server.coordinate.CoordConversion +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class RectangularChunkScanTest { + + @Test + fun `rectangular scan visits exactly the spiral chunk set, each chunk once`() { + for (range in listOf(0, 1, 8, 32)) { + for ((centerX, centerZ) in listOf(0 to 0, -5 to 3, -1000 to -1000, 123 to -456)) { + val spiral = HashSet() + ChunkRange.chunksInRange(centerX, centerZ, range) { x, z -> + spiral.add(CoordConversion.chunkIndex(x, z)) + } + + val rectangular = HashSet() + var visits = 0 + EntityViewerLookup.chunksInRangeRectangular(centerX, centerZ, range) { x, z -> + visits++ + assertTrue( + rectangular.add(CoordConversion.chunkIndex(x, z)), + "chunk ($x, $z) visited twice", + ) + } + + assertEquals(ChunkRange.chunksCount(range), visits) + assertEquals(spiral, rectangular, "range $range around ($centerX, $centerZ)") + } + } + } + + @Test + fun `direct scan is never chosen for single-chunk lookups`() { + assertFalse(EntityViewerLookup.useDirectScan(0, 0)) + assertFalse(EntityViewerLookup.useDirectScan(1, 0)) + assertFalse(EntityViewerLookup.useDirectScan(10_000, 0)) + } + + @Test + fun `direct scan is chosen only while candidate checks are cheaper than bucket probes`() { + // Range 32 is 4225 buckets; with factor 3 the break-even is 1409 candidates. + assertTrue(EntityViewerLookup.useDirectScan(1, 32)) + assertTrue(EntityViewerLookup.useDirectScan(1000, 32)) + assertTrue(EntityViewerLookup.useDirectScan(1408, 32)) + assertFalse(EntityViewerLookup.useDirectScan(1409, 32)) + assertFalse(EntityViewerLookup.useDirectScan(5000, 32)) + + // Range 1 is 9 buckets. + assertTrue(EntityViewerLookup.useDirectScan(2, 1)) + assertFalse(EntityViewerLookup.useDirectScan(3, 1)) + + // Large values must not overflow. + assertFalse(EntityViewerLookup.useDirectScan(Int.MAX_VALUE, 1)) + assertTrue(EntityViewerLookup.useDirectScan(Int.MAX_VALUE, 100_000)) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/performance/SkylightSuppressionTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/performance/SkylightSuppressionTest.kt new file mode 100644 index 000000000..41c445804 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/impl/performance/SkylightSuppressionTest.kt @@ -0,0 +1,46 @@ +package dev.slne.surf.api.minestom.server.impl.performance + +import net.minestom.server.instance.Section +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class SkylightSuppressionTest { + + @Test + fun `a dimension with skylight keeps the sky light of the section`() { + val section = Section() + + assertSame( + section.skyLight(), + SkylightSuppression.sectionSkyLight(hasSkylight = true, section = section), + ) + } + + @Test + fun `a dimension without skylight gets a light that needs neither relight nor send`() { + val section = Section() + section.invalidate() + + assertTrue(section.skyLight().requiresUpdate()) + + val light = SkylightSuppression.sectionSkyLight(hasSkylight = false, section = section) + + assertNotSame(section.skyLight(), light) + assertFalse(light.requiresUpdate()) + assertFalse(light.requiresSend()) + assertEquals(0, light.array().size, "an empty array keeps the section out of the sky mask") + } + + @Test + fun `the stand-in leaves the state of the section alone`() { + val section = Section() + section.invalidate() + + SkylightSuppression.sectionSkyLight(hasSkylight = false, section = section) + + // requiresSend() consumes a flag, so the stand-in must not call it on the real sky light. + assertTrue(section.skyLight().requiresSend()) + assertTrue(section.skyLight().requiresUpdate()) + assertTrue(section.blockLight().requiresUpdate()) + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/player/PlayerEventsTest.kt b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/player/PlayerEventsTest.kt new file mode 100644 index 000000000..798e2fcac --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/test/kotlin/dev/slne/surf/api/minestom/server/player/PlayerEventsTest.kt @@ -0,0 +1,147 @@ +package dev.slne.surf.api.minestom.server.player + +import dev.slne.surf.api.minestom.extension.addListener +import dev.slne.surf.api.minestom.player.PlayerLimit +import dev.slne.surf.api.minestom.player.event.AsyncPlayerCountEvent +import dev.slne.surf.api.minestom.player.event.AsyncPlayerSpawnLocationEvent +import dev.slne.surf.api.minestom.player.event.PlayerJoinEvent +import dev.slne.surf.api.minestom.server.impl.player.PlayerEvents +import dev.slne.surf.api.minestom.server.surfMinestomServer +import net.minestom.server.coordinate.Pos +import net.minestom.server.event.EventDispatcher +import net.minestom.server.event.EventListener +import net.minestom.server.event.EventNode +import net.minestom.server.event.player.AsyncPlayerConfigurationEvent +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.concurrent.CompletableFuture + +@EnvTest +class PlayerEventsTest { + + @Test + fun `a player beyond the limit is refused unless a listener lets them in`(env: Env) { + val instance = env.createFlatInstance() + val first = env.createPlayer(instance, Pos.ZERO) + val second = env.createPlayer(instance, Pos.ZERO) + val node = EventNode.all("player-count-test") + env.process().eventHandler().addChild(node) + PlayerLimit.maxPlayers = 1 + + try { + val results = mutableListOf() + node.addListener { event -> + results += event.result + if (event.player === first) event.allow() + } + + assertTrue(onVirtualThread { PlayerEvents.admit(first) }) + assertFalse(onVirtualThread { PlayerEvents.admit(second) }) + assertEquals( + listOf(AsyncPlayerCountEvent.Result.KICK_FULL, AsyncPlayerCountEvent.Result.KICK_FULL), + results, + ) + } finally { + PlayerLimit.maxPlayers = null + env.process().eventHandler().removeChild(node) + } + } + + @Test + fun `everyone is admitted without a limit`(env: Env) { + val instance = env.createFlatInstance() + val player = env.createPlayer(instance, Pos.ZERO) + + assertNull(PlayerLimit.maxPlayers) + assertFalse(PlayerLimit.isFull) + assertTrue(onVirtualThread { PlayerEvents.admit(player) }) + } + + @Test + fun `the spawn location event decides where the player spawns`(env: Env) { + val instance = env.createFlatInstance() + val other = env.createFlatInstance() + val player = env.createPlayer(instance, Pos.ZERO) + val node = EventNode.all("spawn-location-test") + env.process().eventHandler().addChild(node) + + try { + node.addListener { event -> + assertSame(instance, event.instance) + event.instance = other + event.position = Pos(1.0, 2.0, 3.0) + } + + val configuration = AsyncPlayerConfigurationEvent(player, true) + configuration.spawningInstance = instance + onVirtualThread { PlayerEvents.chooseSpawnLocation(configuration) } + + assertSame(other, configuration.spawningInstance) + assertEquals(Pos(1.0, 2.0, 3.0), player.respawnPoint) + } finally { + env.process().eventHandler().removeChild(node) + } + } + + @Test + fun `joining players are announced through the join event`(env: Env) { + val joined = mutableListOf() + val node = EventNode.all("join-test") + node.addListener { event -> + joined += event.player.username + event.joinMessage = null + } + env.process().eventHandler().addChild(node) + + val api = surfMinestomServer() + try { + val instance = env.createFlatInstance() + val player = env.createPlayer(instance, Pos.ZERO) + assertEquals(listOf(player.username), joined) + } finally { + api.shutdown() + env.process().eventHandler().removeChild(node) + } + } + + @Test + fun `the player count is checked before listeners registered later on the global handler`(env: Env) { + val instance = env.createFlatInstance() + val player = env.createPlayer(instance, Pos.ZERO) + val calls = mutableListOf() + val node = EventNode.all("admission-order-test") + val handler = env.process().eventHandler() + handler.addChild(node) + node.addListener { calls += "count" } + + PlayerEvents.install(node, configurationPhase = false) + val later = EventListener.of(AsyncPlayerConfigurationEvent::class.java) { calls += "later" } + handler.addListener(later) + + try { + onVirtualThread { EventDispatcher.call(AsyncPlayerConfigurationEvent(player, true)) } + assertEquals(listOf("count", "later"), calls) + } finally { + handler.removeListener(later) + PlayerEvents.uninstall() + handler.removeChild(node) + } + + calls.clear() + onVirtualThread { EventDispatcher.call(AsyncPlayerConfigurationEvent(player, true)) } + assertTrue(calls.isEmpty()) + } + + /** + * Runs [block] on a virtual thread, as async events and the configuration phase do. + */ + private fun onVirtualThread(block: () -> T): T { + val result = CompletableFuture() + Thread.startVirtualThread { + runCatching(block).fold(result::complete, result::completeExceptionally) + } + return result.join() + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/testFixtures/kotlin/dev/slne/surf/api/minestom/server/impl/mixin/MixinTransforms.kt b/surf-api-minestom/surf-api-minestom-server/src/testFixtures/kotlin/dev/slne/surf/api/minestom/server/impl/mixin/MixinTransforms.kt new file mode 100644 index 000000000..4a686e82c --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/testFixtures/kotlin/dev/slne/surf/api/minestom/server/impl/mixin/MixinTransforms.kt @@ -0,0 +1,38 @@ +package dev.slne.surf.api.minestom.server.impl.mixin + +import dev.slne.surf.api.minestom.server.testing.SurfMixinTestEnvironment +import org.objectweb.asm.ClassReader +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.ClassNode + +/** + * Applies the installed mixins to class files without loading the result, to assert that they + * still match their targets. + */ +object MixinTransforms { + + /** + * Returns the class [binaryName] with the installed mixins applied to its shipped bytecode. + * + * Installs Mixin through [SurfMixinTestEnvironment] if needed. The result is not loaded. + */ + fun transform(binaryName: String): ClassNode { + SurfMixinTestEnvironment.install() + val transformer = checkNotNull(InstrumentationMixinService.transformer()) { "Mixin never offered its transformer" } + val transformed = transformer.transformClassBytes(binaryName, binaryName, originalBytes(binaryName)) + + val node = ClassNode(Opcodes.ASM9) + ClassReader(transformed).accept(node, 0) + return node + } + + /** + * Returns the unmodified bytecode of [binaryName] from the class path. + */ + fun originalBytes(binaryName: String): ByteArray { + val resource = binaryName.replace('.', '/') + ".class" + return checkNotNull(ClassLoader.getSystemClassLoader().getResourceAsStream(resource)) { + "$binaryName is not on the class path" + }.use { it.readBytes() } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/testFixtures/kotlin/dev/slne/surf/api/minestom/server/testing/SurfMixinTestEnvironment.kt b/surf-api-minestom/surf-api-minestom-server/src/testFixtures/kotlin/dev/slne/surf/api/minestom/server/testing/SurfMixinTestEnvironment.kt new file mode 100644 index 000000000..d608ac37d --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/testFixtures/kotlin/dev/slne/surf/api/minestom/server/testing/SurfMixinTestEnvironment.kt @@ -0,0 +1,54 @@ +package dev.slne.surf.api.minestom.server.testing + +import dev.slne.surf.api.minestom.server.impl.mixin.SurfMixinAgent +import net.bytebuddy.agent.ByteBuddyAgent +import org.junit.platform.launcher.LauncherSession +import org.junit.platform.launcher.LauncherSessionListener + +/** + * Installs Mixin into the test JVM when the JUnit launcher session opens, like the server jar's + * bootstrap does, so tests run against the transformed Minestom classes. + * + * Registered as a [LauncherSessionListener], so depending on the test fixtures of + * `surf-api-minestom-server` is enough. Applies the surf mixin configurations found on the test + * class path plus those listed, comma separated, in the [CONFIGS_PROPERTY] system property. + */ +class SurfMixinTestEnvironment : LauncherSessionListener { + override fun launcherSessionOpened(session: LauncherSession) { + install() + } + + companion object { + /** + * System property listing further mixin configurations to apply, comma separated. + */ + const val CONFIGS_PROPERTY = "surf.test.mixin-configs" + + private val SURF_CONFIGS = listOf( + "mixins.surf-api-minestom-server.json", + "mixins.surf-api-minestom-server-npc.json", + ) + + /** + * The mixin configurations applied in this JVM. + */ + val configurations: List by lazy { + val loader = SurfMixinTestEnvironment::class.java.classLoader + val extra = System.getProperty(CONFIGS_PROPERTY).orEmpty() + .split(',') + .map(String::trim) + .filter(String::isNotEmpty) + SURF_CONFIGS.filter { loader.getResource(it) != null } + extra + } + + /** + * Installs Mixin unless it is already installed. + */ + @JvmStatic + @Synchronized + fun install() { + if (SurfMixinAgent.isInstalled()) return + SurfMixinAgent.install(ByteBuddyAgent.install(), configurations) + } + } +} diff --git a/surf-api-minestom/surf-api-minestom-server/src/testFixtures/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener b/surf-api-minestom/surf-api-minestom-server/src/testFixtures/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener new file mode 100644 index 000000000..291257a72 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom-server/src/testFixtures/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener @@ -0,0 +1 @@ +dev.slne.surf.api.minestom.server.testing.SurfMixinTestEnvironment diff --git a/surf-api-minestom/surf-api-minestom/api/surf-api-minestom.api b/surf-api-minestom/surf-api-minestom/api/surf-api-minestom.api new file mode 100644 index 000000000..c19a143d0 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/api/surf-api-minestom.api @@ -0,0 +1,3526 @@ +public final class dev/slne/surf/api/minestom/builder/ItemDsl { + public fun (Lnet/minestom/server/item/Material;I)V + public synthetic fun (Lnet/minestom/server/item/Material;IILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun amount (I)V + public final fun build ()Lnet/minestom/server/item/ItemStack; + public final fun displayName (Lkotlin/jvm/functions/Function1;)V + public final fun displayName (Lnet/kyori/adventure/text/Component;)V + public final fun getBuilder ()Lnet/minestom/server/item/ItemStack$Builder; + public final fun itemModel (Ljava/lang/String;)V + public final fun itemModel (Lnet/kyori/adventure/key/Key;)V + public final fun lore (Lkotlin/jvm/functions/Function1;)V + public final fun lore ([Lnet/kyori/adventure/text/Component;)V +} + +public final class dev/slne/surf/api/minestom/builder/ItemDslKt { + public static final fun buildItem (Lnet/minestom/server/item/Material;ILkotlin/jvm/functions/Function1;)Lnet/minestom/server/item/ItemStack; + public static synthetic fun buildItem$default (Lnet/minestom/server/item/Material;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/minestom/server/item/ItemStack; +} + +public abstract interface annotation class dev/slne/surf/api/minestom/builder/ItemDslMarker : java/lang/annotation/Annotation { +} + +public final class dev/slne/surf/api/minestom/chat/AsyncPlayerChatEvent { + public static final field Companion Ldev/slne/surf/api/minestom/chat/AsyncPlayerChatEvent$Companion; + public fun (Lnet/minestom/server/entity/Player;Ljava/util/Set;Ldev/slne/surf/api/minestom/chat/ChatRenderer;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/chat/SignedMessage;)V + public final fun getMessage ()Lnet/kyori/adventure/text/Component; + public final fun getOriginalMessage ()Lnet/kyori/adventure/text/Component; + public final fun getPlayer ()Lnet/minestom/server/entity/Player; + public final fun getRenderer ()Ldev/slne/surf/api/minestom/chat/ChatRenderer; + public final fun getSignedMessage ()Lnet/kyori/adventure/chat/SignedMessage; + public final fun getViewers ()Ljava/util/Set; + public final fun isCancelled ()Z + public final fun setCancelled (Z)V + public final fun setMessage (Lnet/kyori/adventure/text/Component;)V + public final fun setRenderer (Ldev/slne/surf/api/minestom/chat/ChatRenderer;)V +} + +public final class dev/slne/surf/api/minestom/chat/AsyncPlayerChatEvent$Companion { + public final fun addListener (ILkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/event/SuspendingEventNode$Registration; + public static synthetic fun addListener$default (Ldev/slne/surf/api/minestom/chat/AsyncPlayerChatEvent$Companion;ILkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/event/SuspendingEventNode$Registration; + public final fun getNode ()Ldev/slne/surf/api/minestom/event/SuspendingEventNode; +} + +public abstract interface class dev/slne/surf/api/minestom/chat/ChatRenderer { + public static final field Companion Ldev/slne/surf/api/minestom/chat/ChatRenderer$Companion; + public abstract fun render (Lnet/minestom/server/entity/Player;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/audience/Audience;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class dev/slne/surf/api/minestom/chat/ChatRenderer$Companion { + public final fun defaultRenderer ()Ldev/slne/surf/api/minestom/chat/ChatRenderer; + public final fun viewerUnaware (Ldev/slne/surf/api/minestom/chat/ChatRenderer$ViewerUnaware;)Ldev/slne/surf/api/minestom/chat/ChatRenderer; +} + +public abstract interface class dev/slne/surf/api/minestom/chat/ChatRenderer$Default : dev/slne/surf/api/minestom/chat/ChatRenderer, dev/slne/surf/api/minestom/chat/ChatRenderer$ViewerUnaware { +} + +public abstract interface class dev/slne/surf/api/minestom/chat/ChatRenderer$ViewerUnaware { + public abstract fun render (Lnet/minestom/server/entity/Player;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class dev/slne/surf/api/minestom/chat/ChatSessionHolder { + public static final field Companion Ldev/slne/surf/api/minestom/chat/ChatSessionHolder$Companion; + public abstract fun captureSignedMessage (Lnet/kyori/adventure/chat/SignedMessage;Lnet/kyori/adventure/text/Component;)Ldev/slne/surf/api/minestom/chat/RemoteSignedMessage; + public static synthetic fun captureSignedMessage$default (Ldev/slne/surf/api/minestom/chat/ChatSessionHolder;Lnet/kyori/adventure/chat/SignedMessage;Lnet/kyori/adventure/text/Component;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/chat/RemoteSignedMessage; + public abstract fun deleteMessage (Lnet/kyori/adventure/chat/SignedMessage$Signature;)V + public abstract fun getChatSession ()Lnet/minestom/server/crypto/ChatSession; + public abstract fun getPlayer ()Lnet/minestom/server/entity/Player; + public abstract fun sendRemoteSignedMessage (Ldev/slne/surf/api/minestom/chat/RemoteChatSender;Ldev/slne/surf/api/minestom/chat/RemoteSignedMessage;Lnet/kyori/adventure/text/Component;)V + public abstract fun sendSignedMessage (Lnet/kyori/adventure/chat/SignedMessage;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;)V + public static synthetic fun sendSignedMessage$default (Ldev/slne/surf/api/minestom/chat/ChatSessionHolder;Lnet/kyori/adventure/chat/SignedMessage;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;ILjava/lang/Object;)V +} + +public final class dev/slne/surf/api/minestom/chat/ChatSessionHolder$Companion { + public final fun fromPlayer (Lnet/minestom/server/entity/Player;)Ldev/slne/surf/api/minestom/chat/ChatSessionHolder; + public final fun fromUuid (Ljava/util/UUID;)Ldev/slne/surf/api/minestom/chat/ChatSessionHolder; + public final fun installProvider (Ldev/slne/surf/api/minestom/chat/ChatSessionHolder$Provider;)V +} + +public final class dev/slne/surf/api/minestom/chat/ChatSessionHolder$DefaultImpls { + public static synthetic fun captureSignedMessage$default (Ldev/slne/surf/api/minestom/chat/ChatSessionHolder;Lnet/kyori/adventure/chat/SignedMessage;Lnet/kyori/adventure/text/Component;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/chat/RemoteSignedMessage; + public static synthetic fun sendSignedMessage$default (Ldev/slne/surf/api/minestom/chat/ChatSessionHolder;Lnet/kyori/adventure/chat/SignedMessage;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;ILjava/lang/Object;)V +} + +public abstract interface class dev/slne/surf/api/minestom/chat/ChatSessionHolder$Provider { + public abstract fun deleteMessage (Lnet/minestom/server/entity/Player;Lnet/kyori/adventure/chat/SignedMessage$Signature;)V + public abstract fun fromPlayer (Lnet/minestom/server/entity/Player;)Ldev/slne/surf/api/minestom/chat/ChatSessionHolder; + public abstract fun fromUuid (Ljava/util/UUID;)Ldev/slne/surf/api/minestom/chat/ChatSessionHolder; +} + +public final class dev/slne/surf/api/minestom/chat/ChatSessionHolderKt { + public static final fun captureSignedMessage (Lnet/minestom/server/entity/Player;Lnet/kyori/adventure/chat/SignedMessage;Lnet/kyori/adventure/text/Component;)Ldev/slne/surf/api/minestom/chat/RemoteSignedMessage; + public static synthetic fun captureSignedMessage$default (Lnet/minestom/server/entity/Player;Lnet/kyori/adventure/chat/SignedMessage;Lnet/kyori/adventure/text/Component;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/chat/RemoteSignedMessage; + public static final fun deleteSignedMessage (Lnet/minestom/server/entity/Player;Lnet/kyori/adventure/chat/SignedMessage$Signature;)V + public static final fun getChatSessionHolder (Lnet/minestom/server/entity/Player;)Ldev/slne/surf/api/minestom/chat/ChatSessionHolder; + public static final fun getSignedChatSession (Lnet/minestom/server/entity/Player;)Lnet/minestom/server/crypto/ChatSession; + public static final fun sendRemoteSignedMessage (Lnet/minestom/server/entity/Player;Ldev/slne/surf/api/minestom/chat/RemoteChatSender;Ldev/slne/surf/api/minestom/chat/RemoteSignedMessage;Lnet/kyori/adventure/text/Component;)V + public static final fun sendSignedMessage (Lnet/minestom/server/entity/Player;Lnet/kyori/adventure/chat/SignedMessage;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;)V + public static synthetic fun sendSignedMessage$default (Lnet/minestom/server/entity/Player;Lnet/kyori/adventure/chat/SignedMessage;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;ILjava/lang/Object;)V +} + +public final class dev/slne/surf/api/minestom/chat/RemoteChatSender { + public fun (Ljava/util/UUID;Ljava/lang/String;Lnet/minestom/server/crypto/ChatSession;)V + public final fun component1 ()Ljava/util/UUID; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lnet/minestom/server/crypto/ChatSession; + public final fun copy (Ljava/util/UUID;Ljava/lang/String;Lnet/minestom/server/crypto/ChatSession;)Ldev/slne/surf/api/minestom/chat/RemoteChatSender; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/chat/RemoteChatSender;Ljava/util/UUID;Ljava/lang/String;Lnet/minestom/server/crypto/ChatSession;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/chat/RemoteChatSender; + public fun equals (Ljava/lang/Object;)Z + public final fun getSession ()Lnet/minestom/server/crypto/ChatSession; + public final fun getUsername ()Ljava/lang/String; + public final fun getUuid ()Ljava/util/UUID; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/chat/RemoteSignedMessage { + public fun (Ljava/util/UUID;Ljava/util/UUID;ILnet/minestom/server/crypto/MessageSignature;Ljava/lang/String;Ljava/time/Instant;JLjava/util/List;Lnet/kyori/adventure/text/Component;)V + public final fun component1 ()Ljava/util/UUID; + public final fun component2 ()Ljava/util/UUID; + public final fun component3 ()I + public final fun component4 ()Lnet/minestom/server/crypto/MessageSignature; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/time/Instant; + public final fun component7 ()J + public final fun component8 ()Ljava/util/List; + public final fun component9 ()Lnet/kyori/adventure/text/Component; + public final fun copy (Ljava/util/UUID;Ljava/util/UUID;ILnet/minestom/server/crypto/MessageSignature;Ljava/lang/String;Ljava/time/Instant;JLjava/util/List;Lnet/kyori/adventure/text/Component;)Ldev/slne/surf/api/minestom/chat/RemoteSignedMessage; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/chat/RemoteSignedMessage;Ljava/util/UUID;Ljava/util/UUID;ILnet/minestom/server/crypto/MessageSignature;Ljava/lang/String;Ljava/time/Instant;JLjava/util/List;Lnet/kyori/adventure/text/Component;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/chat/RemoteSignedMessage; + public fun equals (Ljava/lang/Object;)Z + public final fun getContent ()Ljava/lang/String; + public final fun getIndex ()I + public final fun getLastSeen ()Ljava/util/List; + public final fun getSalt ()J + public final fun getSender ()Ljava/util/UUID; + public final fun getSessionId ()Ljava/util/UUID; + public final fun getSignature ()Lnet/minestom/server/crypto/MessageSignature; + public final fun getTimestamp ()Ljava/time/Instant; + public final fun getUnsignedContent ()Lnet/kyori/adventure/text/Component; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/CommandAPI { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/CommandAPI; + public final fun execute (Lnet/minestom/server/command/CommandSender;Ljava/lang/String;)I + public final fun failWithMessage (Lnet/kyori/adventure/text/Component;)Ljava/lang/Void; + public final fun failWithString (Ljava/lang/String;)Ljava/lang/Void; + public final fun installPlatform (Ldev/slne/surf/api/minestom/command/internal/CommandAPIPlatform;)V + public final fun register (Ldev/slne/surf/api/minestom/command/CommandDefinition;Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/RegisteredCommand; + public final fun uninstallPlatform (Ldev/slne/surf/api/minestom/command/internal/CommandAPIPlatform;)V + public final fun unregister (Ljava/lang/String;)Z +} + +public final class dev/slne/surf/api/minestom/command/CommandAPICommand : dev/slne/surf/api/minestom/command/executor/CommandExecutable { + public fun (Ljava/lang/String;)V + public fun addExecutor (Ldev/slne/surf/api/minestom/command/executor/ExecutorDefinition;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public synthetic fun addExecutor (Ldev/slne/surf/api/minestom/command/executor/ExecutorDefinition;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public fun executes (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public synthetic fun executes (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public fun executesConsole (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public synthetic fun executesConsole (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public fun executesPlayer (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public synthetic fun executesPlayer (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public final fun getName ()Ljava/lang/String; + public final fun register (Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/RegisteredCommand; + public static synthetic fun register$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/RegisteredCommand; + public final fun toDefinition ()Ldev/slne/surf/api/minestom/command/CommandDefinition; + public final fun withAliases ([Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public final fun withArguments ([Ldev/slne/surf/api/minestom/command/argument/Argument;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public final fun withFullDescription ([Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public final fun withHelp (Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public final fun withOptionalArguments ([Ldev/slne/surf/api/minestom/command/argument/Argument;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public final fun withPermission (Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public final fun withRequirement (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public final fun withShortDescription (Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public final fun withSubcommand (Ldev/slne/surf/api/minestom/command/CommandAPICommand;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public final fun withSubcommands ([Ldev/slne/surf/api/minestom/command/CommandAPICommand;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public final fun withUsage ([Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; +} + +public final class dev/slne/surf/api/minestom/command/CommandDefinition { + public fun (Ljava/lang/String;Ljava/util/Set;Ldev/slne/surf/api/minestom/command/CommandMetadata;Ljava/util/List;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/util/Set; + public final fun component3 ()Ldev/slne/surf/api/minestom/command/CommandMetadata; + public final fun component4 ()Ljava/util/List; + public final fun copy (Ljava/lang/String;Ljava/util/Set;Ldev/slne/surf/api/minestom/command/CommandMetadata;Ljava/util/List;)Ldev/slne/surf/api/minestom/command/CommandDefinition; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/CommandDefinition;Ljava/lang/String;Ljava/util/Set;Ldev/slne/surf/api/minestom/command/CommandMetadata;Ljava/util/List;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandDefinition; + public fun equals (Ljava/lang/Object;)Z + public final fun getAliases ()Ljava/util/Set; + public final fun getMetadata ()Ldev/slne/surf/api/minestom/command/CommandMetadata; + public final fun getName ()Ljava/lang/String; + public final fun getPaths ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/CommandMetadata { + public static final field Companion Ldev/slne/surf/api/minestom/command/CommandMetadata$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/util/List; + public final fun component3 ()Ljava/util/List; + public final fun component4 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/CommandMetadata; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/CommandMetadata;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandMetadata; + public fun equals (Ljava/lang/Object;)Z + public final fun getFullDescription ()Ljava/util/List; + public final fun getHelp ()Ljava/lang/String; + public final fun getShortDescription ()Ljava/lang/String; + public final fun getUsage ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/CommandMetadata$Companion { + public final fun snapshot (Ljava/lang/String;Ljava/util/Collection;Ljava/util/Collection;Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/CommandMetadata; +} + +public final class dev/slne/surf/api/minestom/command/CommandPath { + public fun (Ljava/util/List;Ljava/util/List;Ljava/util/Set;Ljava/util/List;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/util/List; + public final fun component3 ()Ljava/util/Set; + public final fun component4 ()Ljava/util/List; + public final fun copy (Ljava/util/List;Ljava/util/List;Ljava/util/Set;Ljava/util/List;)Ldev/slne/surf/api/minestom/command/CommandPath; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/CommandPath;Ljava/util/List;Ljava/util/List;Ljava/util/Set;Ljava/util/List;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandPath; + public fun equals (Ljava/lang/Object;)Z + public final fun getArguments ()Ljava/util/List; + public final fun getExecutors ()Ljava/util/List; + public final fun getPermissions ()Ljava/util/Set; + public final fun getRequirements ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/CommandTree : dev/slne/surf/api/minestom/command/executor/CommandExecutable { + public fun (Ljava/lang/String;)V + public fun addExecutor (Ldev/slne/surf/api/minestom/command/executor/ExecutorDefinition;)Ldev/slne/surf/api/minestom/command/CommandTree; + public synthetic fun addExecutor (Ldev/slne/surf/api/minestom/command/executor/ExecutorDefinition;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public fun executes (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/CommandTree; + public synthetic fun executes (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public fun executesConsole (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/CommandTree; + public synthetic fun executesConsole (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public fun executesPlayer (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/CommandTree; + public synthetic fun executesPlayer (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public final fun getName ()Ljava/lang/String; + public final fun register (Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/RegisteredCommand; + public static synthetic fun register$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/RegisteredCommand; + public final fun then (Ldev/slne/surf/api/minestom/command/argument/Argument;)Ldev/slne/surf/api/minestom/command/CommandTree; + public final fun toDefinition ()Ldev/slne/surf/api/minestom/command/CommandDefinition; + public final fun withAliases ([Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/CommandTree; + public final fun withFullDescription ([Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/CommandTree; + public final fun withHelp (Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/CommandTree; + public final fun withPermission (Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/CommandTree; + public final fun withRequirement (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public final fun withShortDescription (Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/CommandTree; + public final fun withUsage ([Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/CommandTree; +} + +public final class dev/slne/surf/api/minestom/command/RegisteredCommand { + public fun (Ljava/lang/String;Ljava/util/Collection;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/util/Collection;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getAliases ()Ljava/util/Set; + public final fun getName ()Ljava/lang/String; + public final fun getNamespace ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public class dev/slne/surf/api/minestom/command/args/MiniMessageArgument : dev/slne/surf/api/minestom/command/argument/CustomArgument { + public fun (Ljava/lang/String;Z)V + public synthetic fun (Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class dev/slne/surf/api/minestom/command/args/MiniMessageArgumentKt { + public static final fun miniMessageArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun miniMessageArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun miniMessageArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun miniMessageArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun miniMessageArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun miniMessageArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; +} + +public final class dev/slne/surf/api/minestom/command/argument/AngleArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public abstract class dev/slne/surf/api/minestom/command/argument/Argument : dev/slne/surf/api/minestom/command/executor/CommandExecutable { + public fun (Ljava/lang/String;)V + public fun addExecutor (Ldev/slne/surf/api/minestom/command/executor/ExecutorDefinition;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public synthetic fun addExecutor (Ldev/slne/surf/api/minestom/command/executor/ExecutorDefinition;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public fun executes (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public synthetic fun executes (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public fun executesConsole (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public synthetic fun executesConsole (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public fun executesPlayer (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public synthetic fun executesPlayer (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + protected fun getGreedy ()Z + protected abstract fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + protected fun getListDelimiter ()Ljava/lang/Character; + public final fun getNodeName ()Ljava/lang/String; + protected abstract fun getRawType ()Lcom/mojang/brigadier/arguments/ArgumentType; + public final fun includeSafeSuggestions (Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public final fun includeSuggestions (Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public final fun replaceSafeSuggestions (Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public final fun replaceSuggestions (Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public final fun setOptional (Ljava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public final fun setOptional (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public final fun setOptional (Z)Ldev/slne/surf/api/minestom/command/argument/Argument; + protected abstract fun stringify (Ljava/lang/Object;)Ljava/lang/String; + public final fun then (Ldev/slne/surf/api/minestom/command/argument/Argument;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public final fun toDefinition ()Ldev/slne/surf/api/minestom/command/argument/ArgumentDefinition; + public final fun withPermission (Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public final fun withRequirement (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentDefinition { + public fun (Ljava/lang/String;Ldev/slne/surf/api/minestom/command/argument/ArgumentKind;ZLkotlin/jvm/functions/Function1;Ljava/util/Set;Ljava/util/List;Ldev/slne/surf/api/minestom/command/argument/SuggestionMode;ZLcom/mojang/brigadier/arguments/ArgumentType;Lkotlin/jvm/functions/Function1;Ljava/lang/Character;)V + public synthetic fun (Ljava/lang/String;Ldev/slne/surf/api/minestom/command/argument/ArgumentKind;ZLkotlin/jvm/functions/Function1;Ljava/util/Set;Ljava/util/List;Ldev/slne/surf/api/minestom/command/argument/SuggestionMode;ZLcom/mojang/brigadier/arguments/ArgumentType;Lkotlin/jvm/functions/Function1;Ljava/lang/Character;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component10 ()Lkotlin/jvm/functions/Function1; + public final fun component11 ()Ljava/lang/Character; + public final fun component2 ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public final fun component3 ()Z + public final fun component4 ()Lkotlin/jvm/functions/Function1; + public final fun component5 ()Ljava/util/Set; + public final fun component6 ()Ljava/util/List; + public final fun component7 ()Ldev/slne/surf/api/minestom/command/argument/SuggestionMode; + public final fun component8 ()Z + public final fun component9 ()Lcom/mojang/brigadier/arguments/ArgumentType; + public final fun copy (Ljava/lang/String;Ldev/slne/surf/api/minestom/command/argument/ArgumentKind;ZLkotlin/jvm/functions/Function1;Ljava/util/Set;Ljava/util/List;Ldev/slne/surf/api/minestom/command/argument/SuggestionMode;ZLcom/mojang/brigadier/arguments/ArgumentType;Lkotlin/jvm/functions/Function1;Ljava/lang/Character;)Ldev/slne/surf/api/minestom/command/argument/ArgumentDefinition; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/ArgumentDefinition;Ljava/lang/String;Ldev/slne/surf/api/minestom/command/argument/ArgumentKind;ZLkotlin/jvm/functions/Function1;Ljava/util/Set;Ljava/util/List;Ldev/slne/surf/api/minestom/command/argument/SuggestionMode;ZLcom/mojang/brigadier/arguments/ArgumentType;Lkotlin/jvm/functions/Function1;Ljava/lang/Character;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/ArgumentDefinition; + public fun equals (Ljava/lang/Object;)Z + public final fun getDefaultValue ()Lkotlin/jvm/functions/Function1; + public final fun getGreedy ()Z + public final fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public final fun getListDelimiter ()Ljava/lang/Character; + public final fun getNodeName ()Ljava/lang/String; + public final fun getOptional ()Z + public final fun getPermissions ()Ljava/util/Set; + public final fun getRawType ()Lcom/mojang/brigadier/arguments/ArgumentType; + public final fun getRequirements ()Ljava/util/List; + public final fun getStringify ()Lkotlin/jvm/functions/Function1; + public final fun getSuggestions ()Ldev/slne/surf/api/minestom/command/argument/SuggestionMode; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class dev/slne/surf/api/minestom/command/argument/ArgumentKind { +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Angle : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Angle; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Axis : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Axis; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Biome : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Biome; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$BlockPosition : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$BlockPosition; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Boolean : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Boolean; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Command : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Command; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Custom : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public fun (Ldev/slne/surf/api/minestom/command/argument/ArgumentDefinition;Lkotlin/jvm/functions/Function1;)V + public final fun component1 ()Ldev/slne/surf/api/minestom/command/argument/ArgumentDefinition; + public final fun component2 ()Lkotlin/jvm/functions/Function1; + public final fun copy (Ldev/slne/surf/api/minestom/command/argument/ArgumentDefinition;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Custom; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Custom;Ldev/slne/surf/api/minestom/command/argument/ArgumentDefinition;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Custom; + public fun equals (Ljava/lang/Object;)Z + public final fun getBase ()Ldev/slne/surf/api/minestom/command/argument/ArgumentDefinition; + public final fun getParser ()Lkotlin/jvm/functions/Function1; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Double : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public fun (DD)V + public final fun component1 ()D + public final fun component2 ()D + public final fun copy (DD)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Double; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Double;DDILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Double; + public fun equals (Ljava/lang/Object;)Z + public final fun getMax ()D + public final fun getMin ()D + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Enchantment : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Enchantment; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Entities : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public fun (Z)V + public final fun component1 ()Z + public final fun copy (Z)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Entities; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Entities;ZILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Entities; + public fun equals (Ljava/lang/Object;)Z + public final fun getAllowEmpty ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Entity : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Entity; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$EntityType : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$EntityType; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Enum : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public fun (Ljava/util/EnumSet;Lkotlin/jvm/functions/Function1;)V + public final fun component1 ()Ljava/util/EnumSet; + public final fun component2 ()Lkotlin/jvm/functions/Function1; + public final fun copy (Ljava/util/EnumSet;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Enum; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Enum;Ljava/util/EnumSet;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Enum; + public fun equals (Ljava/lang/Object;)Z + public final fun getFormatter ()Lkotlin/jvm/functions/Function1; + public final fun getValues ()Ljava/util/EnumSet; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Float : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public fun (FF)V + public final fun component1 ()F + public final fun component2 ()F + public final fun copy (FF)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Float; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Float;FFILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Float; + public fun equals (Ljava/lang/Object;)Z + public final fun getMax ()F + public final fun getMin ()F + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$FloatRange : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$FloatRange; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$GameMode : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$GameMode; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$GreedyString : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$GreedyString; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Instance : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Instance; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Integer : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public fun (II)V + public final fun component1 ()I + public final fun component2 ()I + public final fun copy (II)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Integer; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Integer;IIILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Integer; + public fun equals (Ljava/lang/Object;)Z + public final fun getMax ()I + public final fun getMin ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$IntegerRange : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$IntegerRange; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$List : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public fun (Ldev/slne/surf/api/minestom/command/argument/ArgumentDefinition;CZ)V + public final fun component1 ()Ldev/slne/surf/api/minestom/command/argument/ArgumentDefinition; + public final fun component2 ()C + public final fun component3 ()Z + public final fun copy (Ldev/slne/surf/api/minestom/command/argument/ArgumentDefinition;CZ)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$List; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$List;Ldev/slne/surf/api/minestom/command/argument/ArgumentDefinition;CZILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$List; + public fun equals (Ljava/lang/Object;)Z + public final fun getAllowEmpty ()Z + public final fun getDelimiter ()C + public final fun getElement ()Ldev/slne/surf/api/minestom/command/argument/ArgumentDefinition; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Literal : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public fun (Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Literal; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Literal;Ljava/lang/String;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Literal; + public fun equals (Ljava/lang/Object;)Z + public final fun getLiteral ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Long : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public fun (JJ)V + public final fun component1 ()J + public final fun component2 ()J + public final fun copy (JJ)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Long; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Long;JJILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Long; + public fun equals (Ljava/lang/Object;)Z + public final fun getMax ()J + public final fun getMin ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$MultiLiteral : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public fun (Ljava/util/List;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$MultiLiteral; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$MultiLiteral;Ljava/util/List;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$MultiLiteral; + public fun equals (Ljava/lang/Object;)Z + public final fun getLiterals ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Particle : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Particle; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Player : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Player; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Players : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public fun (Z)V + public final fun component1 ()Z + public final fun copy (Z)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Players; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Players;ZILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Players; + public fun equals (Ljava/lang/Object;)Z + public final fun getAllowEmpty ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Position : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Position; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Position2D : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Position2D; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$PotionEffect : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$PotionEffect; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Resource : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public fun (Ljava/lang/String;Lnet/minestom/server/registry/Registry;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lnet/minestom/server/registry/Registry; + public final fun copy (Ljava/lang/String;Lnet/minestom/server/registry/Registry;)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Resource; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Resource;Ljava/lang/String;Lnet/minestom/server/registry/Registry;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Resource; + public fun equals (Ljava/lang/Object;)Z + public final fun getIdentifier ()Ljava/lang/String; + public final fun getRegistry ()Lnet/minestom/server/registry/Registry; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$ResourceLocation : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$ResourceLocation; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Rotation : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Rotation; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$SignedMessage : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$SignedMessage; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Sound : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Sound; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$TeamColor : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$TeamColor; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Text : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Text; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Time : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Time; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Uuid : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Uuid; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ArgumentKind$Word : dev/slne/surf/api/minestom/command/argument/ArgumentKind { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/ArgumentKind$Word; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/Axis : java/lang/Enum { + public static final field X Ldev/slne/surf/api/minestom/command/argument/Axis; + public static final field Y Ldev/slne/surf/api/minestom/command/argument/Axis; + public static final field Z Ldev/slne/surf/api/minestom/command/argument/Axis; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/argument/Axis; + public static fun values ()[Ldev/slne/surf/api/minestom/command/argument/Axis; +} + +public final class dev/slne/surf/api/minestom/command/argument/AxisArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/BiomeArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/BlockPositionArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/BooleanArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/CommandArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/CompositeArgumentsKt { + public static final fun filter (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun map (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; +} + +public class dev/slne/surf/api/minestom/command/argument/CustomArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function1;)V + protected fun getGreedy ()Z + protected fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + protected fun getListDelimiter ()Ljava/lang/Character; + protected fun getRawType ()Lcom/mojang/brigadier/arguments/ArgumentType; + protected fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/CustomArgumentInfo { + public fun (Lnet/minestom/server/command/CommandSender;Ljava/lang/String;Ljava/lang/Object;)V + public final fun component1 ()Lnet/minestom/server/command/CommandSender; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Object; + public final fun copy (Lnet/minestom/server/command/CommandSender;Ljava/lang/String;Ljava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/CustomArgumentInfo; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/CustomArgumentInfo;Lnet/minestom/server/command/CommandSender;Ljava/lang/String;Ljava/lang/Object;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/CustomArgumentInfo; + public fun equals (Ljava/lang/Object;)Z + public final fun getBaseValue ()Ljava/lang/Object; + public final fun getCurrentInput ()Ljava/lang/String; + public final fun getSender ()Lnet/minestom/server/command/CommandSender; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/DoubleArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;DD)V + public synthetic fun (Ljava/lang/String;DDILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/EnchantmentArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/EntitiesArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;Z)V + public synthetic fun (Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAllowEmpty ()Z + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/EntityArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/EntityTypeArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/EnumArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;Ljava/util/Collection;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Ljava/lang/String;Ljava/util/Collection;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/FloatArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;FF)V + public synthetic fun (Ljava/lang/String;FFILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/FloatRangeArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/GameModeArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/GreedyStringArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/InstanceArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/IntegerArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;II)V + public synthetic fun (Ljava/lang/String;IIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/IntegerRangeArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ListArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;Ldev/slne/surf/api/minestom/command/argument/Argument;CZ)V + public synthetic fun (Ljava/lang/String;Ldev/slne/surf/api/minestom/command/argument/Argument;CZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAllowEmpty ()Z + public final fun getDelimiter ()C + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/LiteralArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public final fun getLiteral ()Ljava/lang/String; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/LongArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;JJ)V + public synthetic fun (Ljava/lang/String;JJILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/MultiLiteralArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;[Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ParticleArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/PlayerArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/PlayersArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;Z)V + public synthetic fun (Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAllowEmpty ()Z + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/Position2DArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/PositionArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/PotionEffectArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ResourceArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;Ljava/lang/String;Lnet/minestom/server/registry/Registry;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/ResourceLocationArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/Rotation { + public fun (FF)V + public final fun component1 ()F + public final fun component2 ()F + public final fun copy (FF)Ldev/slne/surf/api/minestom/command/argument/Rotation; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/Rotation;FFILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Rotation; + public fun equals (Ljava/lang/Object;)Z + public final fun getPitch ()F + public final fun getYaw ()F + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/RotationArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/SignedMessageArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/SoundArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/StringArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public abstract interface class dev/slne/surf/api/minestom/command/argument/SuggestionMode { +} + +public final class dev/slne/surf/api/minestom/command/argument/SuggestionMode$BuiltIns : dev/slne/surf/api/minestom/command/argument/SuggestionMode { + public static final field INSTANCE Ldev/slne/surf/api/minestom/command/argument/SuggestionMode$BuiltIns; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/SuggestionMode$Include : dev/slne/surf/api/minestom/command/argument/SuggestionMode { + public fun (Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions;)V + public final fun component1 ()Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public final fun copy (Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions;)Ldev/slne/surf/api/minestom/command/argument/SuggestionMode$Include; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/SuggestionMode$Include;Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/SuggestionMode$Include; + public fun equals (Ljava/lang/Object;)Z + public final fun getProvider ()Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/SuggestionMode$IncludeSafe : dev/slne/surf/api/minestom/command/argument/SuggestionMode { + public fun (Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions;)V + public final fun component1 ()Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions; + public final fun copy (Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions;)Ldev/slne/surf/api/minestom/command/argument/SuggestionMode$IncludeSafe; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/SuggestionMode$IncludeSafe;Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/SuggestionMode$IncludeSafe; + public fun equals (Ljava/lang/Object;)Z + public final fun getProvider ()Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/SuggestionMode$Replace : dev/slne/surf/api/minestom/command/argument/SuggestionMode { + public fun (Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions;)V + public final fun component1 ()Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public final fun copy (Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions;)Ldev/slne/surf/api/minestom/command/argument/SuggestionMode$Replace; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/SuggestionMode$Replace;Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/SuggestionMode$Replace; + public fun equals (Ljava/lang/Object;)Z + public final fun getProvider ()Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/SuggestionMode$ReplaceSafe : dev/slne/surf/api/minestom/command/argument/SuggestionMode { + public fun (Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions;)V + public final fun component1 ()Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions; + public final fun copy (Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions;)Ldev/slne/surf/api/minestom/command/argument/SuggestionMode$ReplaceSafe; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/argument/SuggestionMode$ReplaceSafe;Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/SuggestionMode$ReplaceSafe; + public fun equals (Ljava/lang/Object;)Z + public final fun getProvider ()Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/TeamColorArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/TextArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/TimeArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/UUIDArgument : dev/slne/surf/api/minestom/command/argument/Argument { + public fun (Ljava/lang/String;)V + public synthetic fun getKind ()Ldev/slne/surf/api/minestom/command/argument/ArgumentKind; + public synthetic fun stringify (Ljava/lang/Object;)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/argument/UnsupportedArgumentType : com/mojang/brigadier/arguments/ArgumentType { + public fun (Ljava/lang/String;)V + public fun parse (Lcom/mojang/brigadier/StringReader;)Ljava/lang/Object; +} + +public final class dev/slne/surf/api/minestom/command/dsl/ArgumentDslKt { + public static final fun angleArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun angleArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun angleArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun angleArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun angleArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun angleArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun axisArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun axisArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun axisArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun axisArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun axisArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun axisArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun biomeArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun biomeArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun biomeArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun biomeArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun biomeArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun biomeArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun blockPositionArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun blockPositionArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun blockPositionArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun blockPositionArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun blockPositionArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun blockPositionArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun booleanArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun booleanArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun booleanArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun booleanArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun booleanArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun booleanArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun commandArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun commandArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun commandArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun commandArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun commandArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun commandArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun customArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function1;Ljava/lang/Boolean;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun customArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function1;Ljava/lang/Boolean;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun customArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function1;Ljava/lang/Boolean;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun customArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function1;Ljava/lang/Boolean;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun customArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function1;Ljava/lang/Boolean;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun customArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function1;Ljava/lang/Boolean;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun doubleArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;DDZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun doubleArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;DDZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun doubleArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;DDZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun doubleArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;DDZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun doubleArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;DDZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun doubleArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;DDZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun enchantmentArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun enchantmentArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun enchantmentArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun enchantmentArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun enchantmentArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun enchantmentArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun entitiesArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun entitiesArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun entitiesArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun entitiesArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun entitiesArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun entitiesArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun entityArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun entityArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun entityArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun entityArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun entityArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun entityArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun entityTypeArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun entityTypeArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun entityTypeArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun entityTypeArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun entityTypeArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun entityTypeArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun enumArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;Ljava/util/Collection;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun enumArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;Ljava/util/Collection;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun enumArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;Ljava/util/Collection;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun enumArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;Ljava/util/Collection;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun enumArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;Ljava/util/Collection;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun enumArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;Ljava/util/Collection;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun floatArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;FFZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun floatArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;FFZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun floatArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;FFZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun floatArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;FFZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun floatArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;FFZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun floatArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;FFZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun floatRangeArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun floatRangeArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun floatRangeArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun floatRangeArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun floatRangeArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun floatRangeArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun gameModeArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun gameModeArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun gameModeArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun gameModeArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun gameModeArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun gameModeArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun greedyStringArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun greedyStringArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun greedyStringArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun greedyStringArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun greedyStringArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun greedyStringArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun instanceArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun instanceArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun instanceArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun instanceArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun instanceArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun instanceArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun integerArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;IIZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun integerArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;IIZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun integerArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;IIZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun integerArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;IIZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun integerArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;IIZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun integerArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;IIZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun integerRangeArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun integerRangeArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun integerRangeArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun integerRangeArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun integerRangeArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun integerRangeArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun listArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;Ldev/slne/surf/api/minestom/command/argument/Argument;CZZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun listArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;Ldev/slne/surf/api/minestom/command/argument/Argument;CZZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun listArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;Ldev/slne/surf/api/minestom/command/argument/Argument;CZZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun listArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;Ldev/slne/surf/api/minestom/command/argument/Argument;CZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun listArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;Ldev/slne/surf/api/minestom/command/argument/Argument;CZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun listArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;Ldev/slne/surf/api/minestom/command/argument/Argument;CZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun literalArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun literalArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun literalArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun literalArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun literalArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun literalArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun location2DArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun location2DArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun location2DArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun location2DArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun location2DArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun location2DArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun locationArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun locationArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun locationArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun locationArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun locationArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun locationArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun longArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;JJZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun longArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;JJZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun longArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;JJZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun longArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;JJZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun longArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;JJZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun longArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;JJZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun multiLiteralArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;[Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun multiLiteralArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;[Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun multiLiteralArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;[Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun multiLiteralArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;[Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun multiLiteralArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;[Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun multiLiteralArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;[Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun particleArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun particleArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun particleArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun particleArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun particleArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun particleArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun playerArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun playerArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun playerArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun playerArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun playerArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun playerArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun playersArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun playersArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun playersArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun playersArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun playersArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun playersArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun potionEffectArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun potionEffectArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun potionEffectArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun potionEffectArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun potionEffectArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun potionEffectArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun resourceArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;Ljava/lang/String;Lnet/minestom/server/registry/Registry;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun resourceArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;Ljava/lang/String;Lnet/minestom/server/registry/Registry;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun resourceArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;Ljava/lang/String;Lnet/minestom/server/registry/Registry;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun resourceArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;Ljava/lang/String;Lnet/minestom/server/registry/Registry;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun resourceArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;Ljava/lang/String;Lnet/minestom/server/registry/Registry;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun resourceArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;Ljava/lang/String;Lnet/minestom/server/registry/Registry;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun resourceLocationArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun resourceLocationArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun resourceLocationArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun resourceLocationArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun resourceLocationArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun resourceLocationArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun rotationArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun rotationArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun rotationArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun rotationArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun rotationArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun rotationArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun signedMessageArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun signedMessageArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun signedMessageArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun signedMessageArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun signedMessageArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun signedMessageArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun soundArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun soundArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun soundArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun soundArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun soundArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun soundArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun stringArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun stringArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun stringArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun stringArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun stringArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun stringArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun teamColorArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun teamColorArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun teamColorArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun teamColorArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun teamColorArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun teamColorArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun textArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun textArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun textArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun textArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun textArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun textArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun timeArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun timeArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun timeArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun timeArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun timeArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun timeArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun uuidArgument (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static final fun uuidArgument (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static final fun uuidArgument (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static synthetic fun uuidArgument$default (Ldev/slne/surf/api/minestom/command/CommandAPICommand;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun uuidArgument$default (Ldev/slne/surf/api/minestom/command/CommandTree;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandTree; + public static synthetic fun uuidArgument$default (Ldev/slne/surf/api/minestom/command/argument/Argument;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; +} + +public final class dev/slne/surf/api/minestom/command/dsl/CommandDslKt { + public static final fun commandAPICommand (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/RegisteredCommand; + public static final fun commandAPICommand (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/RegisteredCommand; + public static synthetic fun commandAPICommand$default (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/RegisteredCommand; + public static synthetic fun commandAPICommand$default (Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/RegisteredCommand; + public static final fun commandTree (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/RegisteredCommand; + public static final fun commandTree (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/RegisteredCommand; + public static synthetic fun commandTree$default (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/RegisteredCommand; + public static synthetic fun commandTree$default (Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/RegisteredCommand; + public static final fun subcommand (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; + public static synthetic fun subcommand$default (Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/CommandAPICommand; +} + +public final class dev/slne/surf/api/minestom/command/dsl/ExecutorDslKt { + public static final fun anyExecutionInfo (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun anyExecutionInfoExecutorSuspend (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun anyExecutionInfoExecutorSuspend (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function3;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static synthetic fun anyExecutionInfoExecutorSuspend$default (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun anyExecutor (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun anyExecutorSuspend (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function4;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun anyExecutorSuspend (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function4;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static synthetic fun anyExecutorSuspend$default (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function4;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun anyResultingExecutionInfo (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun anyResultingExecutor (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun consoleExecutionInfo (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun consoleExecutionInfoExecutorSuspend (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun consoleExecutionInfoExecutorSuspend (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function3;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static synthetic fun consoleExecutionInfoExecutorSuspend$default (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun consoleExecutor (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun consoleExecutorSuspend (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function4;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun consoleExecutorSuspend (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function4;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static synthetic fun consoleExecutorSuspend$default (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function4;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun consoleResultingExecutionInfo (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun consoleResultingExecutor (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun playerExecutionInfo (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun playerExecutionInfoExecutorSuspend (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun playerExecutionInfoExecutorSuspend (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function3;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static synthetic fun playerExecutionInfoExecutorSuspend$default (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun playerExecutor (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun playerExecutorSuspend (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function4;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun playerExecutorSuspend (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function4;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static synthetic fun playerExecutorSuspend$default (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function4;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun playerResultingExecutionInfo (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static final fun playerResultingExecutor (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; +} + +public final class dev/slne/surf/api/minestom/command/dsl/SuggestionDslKt { + public static final fun includeSafeSuggestions (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun includeSafeSuggestions (Ldev/slne/surf/api/minestom/command/argument/Argument;[Ljava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun includeSafeSuggestionsAsync (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun includeSafeSuggestionsWithTooltips (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun includeSafeSuggestionsWithTooltips (Ldev/slne/surf/api/minestom/command/argument/Argument;[Ldev/slne/surf/api/minestom/command/suggestion/Tooltip;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun includeSafeSuggestionsWithTooltipsAsync (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun includeSuggestions (Ldev/slne/surf/api/minestom/command/argument/Argument;Ldev/slne/surf/api/minestom/command/suggestion/SuggestionsBranch;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun includeSuggestions (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun includeSuggestions (Ldev/slne/surf/api/minestom/command/argument/Argument;[Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun includeSuggestionsAsync (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun includeSuggestionsWithTooltips (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun includeSuggestionsWithTooltips (Ldev/slne/surf/api/minestom/command/argument/Argument;[Ldev/slne/surf/api/minestom/command/suggestion/StringTooltip;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun includeSuggestionsWithTooltipsAsync (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun replaceSafeSuggestions (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun replaceSafeSuggestions (Ldev/slne/surf/api/minestom/command/argument/Argument;[Ljava/lang/Object;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun replaceSafeSuggestionsAsync (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun replaceSafeSuggestionsWithTooltips (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun replaceSafeSuggestionsWithTooltips (Ldev/slne/surf/api/minestom/command/argument/Argument;[Ldev/slne/surf/api/minestom/command/suggestion/Tooltip;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun replaceSafeSuggestionsWithTooltipsAsync (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun replaceSuggestions (Ldev/slne/surf/api/minestom/command/argument/Argument;Ldev/slne/surf/api/minestom/command/suggestion/SuggestionsBranch;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun replaceSuggestions (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun replaceSuggestions (Ldev/slne/surf/api/minestom/command/argument/Argument;[Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun replaceSuggestionsAsync (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun replaceSuggestionsWithTooltips (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun replaceSuggestionsWithTooltips (Ldev/slne/surf/api/minestom/command/argument/Argument;[Ldev/slne/surf/api/minestom/command/suggestion/StringTooltip;)Ldev/slne/surf/api/minestom/command/argument/Argument; + public static final fun replaceSuggestionsWithTooltipsAsync (Ldev/slne/surf/api/minestom/command/argument/Argument;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/argument/Argument; +} + +public class dev/slne/surf/api/minestom/command/exception/CommandSyntaxException : java/lang/RuntimeException { + public static final field Companion Ldev/slne/surf/api/minestom/command/exception/CommandSyntaxException$Companion; + public fun (Lnet/kyori/adventure/text/Component;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Throwable;)V + public synthetic fun (Lnet/kyori/adventure/text/Component;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getComponent ()Lnet/kyori/adventure/text/Component; + public final fun getCursor ()Ljava/lang/Integer; + public final fun getInput ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/exception/CommandSyntaxException$Companion { +} + +public final class dev/slne/surf/api/minestom/command/exception/CommandValidationException : java/lang/IllegalArgumentException { + public static final field Companion Ldev/slne/surf/api/minestom/command/exception/CommandValidationException$Companion; + public fun (Ljava/lang/String;)V +} + +public final class dev/slne/surf/api/minestom/command/exception/CommandValidationException$Companion { +} + +public final class dev/slne/surf/api/minestom/command/exception/ComponentMessage : com/mojang/brigadier/Message { + public fun (Lnet/kyori/adventure/text/Component;)V + public final fun getComponent ()Lnet/kyori/adventure/text/Component; + public fun getString ()Ljava/lang/String; + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/exception/ComponentMessageKt { + public static final fun componentOrNull (Lcom/mojang/brigadier/Message;)Lnet/kyori/adventure/text/Component; +} + +public final class dev/slne/surf/api/minestom/command/exception/WrapperCommandSyntaxException : java/lang/RuntimeException { + public static final field Companion Ldev/slne/surf/api/minestom/command/exception/WrapperCommandSyntaxException$Companion; + public fun (Ldev/slne/surf/api/minestom/command/exception/CommandSyntaxException;)V + public final fun getComponent ()Lnet/kyori/adventure/text/Component; + public final fun getCursor ()Ljava/lang/Integer; + public final fun getException ()Ldev/slne/surf/api/minestom/command/exception/CommandSyntaxException; + public final fun getInput ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/exception/WrapperCommandSyntaxException$Companion { +} + +public final class dev/slne/surf/api/minestom/command/executor/CommandArguments { + public static final field Companion Ldev/slne/surf/api/minestom/command/executor/CommandArguments$Companion; + public final fun contains (Ljava/lang/String;)Z + public final fun get (I)Ljava/lang/Object; + public final fun getRaw (I)Ljava/lang/String; + public final fun getRaw (Ljava/lang/String;)Ljava/lang/String; + public final fun rawArguments ()Ljava/util/List; + public final fun typed (Ljava/lang/String;Lkotlin/reflect/KClass;Z)Ljava/lang/Object; +} + +public final class dev/slne/surf/api/minestom/command/executor/CommandArguments$Companion { + public final fun empty ()Ldev/slne/surf/api/minestom/command/executor/CommandArguments; + public final fun of (Ljava/util/List;)Ldev/slne/surf/api/minestom/command/executor/CommandArguments; +} + +public abstract interface class dev/slne/surf/api/minestom/command/executor/CommandExecutable { + public abstract fun addExecutor (Ldev/slne/surf/api/minestom/command/executor/ExecutorDefinition;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public fun executes (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public fun executesConsole (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public fun executesPlayer (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; +} + +public final class dev/slne/surf/api/minestom/command/executor/CommandExecutable$DefaultImpls { + public static fun executes (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static fun executesConsole (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; + public static fun executesPlayer (Ldev/slne/surf/api/minestom/command/executor/CommandExecutable;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/executor/CommandExecutable; +} + +public final class dev/slne/surf/api/minestom/command/executor/ExecutionInfo { + public fun (Lnet/minestom/server/command/CommandSender;Ldev/slne/surf/api/minestom/command/executor/CommandArguments;Ljava/lang/String;)V + public final fun component1 ()Lnet/minestom/server/command/CommandSender; + public final fun component2 ()Ldev/slne/surf/api/minestom/command/executor/CommandArguments; + public final fun component3 ()Ljava/lang/String; + public final fun copy (Lnet/minestom/server/command/CommandSender;Ldev/slne/surf/api/minestom/command/executor/CommandArguments;Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/executor/ExecutionInfo; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/executor/ExecutionInfo;Lnet/minestom/server/command/CommandSender;Ldev/slne/surf/api/minestom/command/executor/CommandArguments;Ljava/lang/String;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/executor/ExecutionInfo; + public fun equals (Ljava/lang/Object;)Z + public final fun getArgs ()Ldev/slne/surf/api/minestom/command/executor/CommandArguments; + public final fun getInput ()Ljava/lang/String; + public final fun getSender ()Lnet/minestom/server/command/CommandSender; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class dev/slne/surf/api/minestom/command/executor/ExecutorDefinition { + public abstract fun getType ()Ldev/slne/surf/api/minestom/command/executor/ExecutorType; +} + +public final class dev/slne/surf/api/minestom/command/executor/ExecutorDefinition$Normal : dev/slne/surf/api/minestom/command/executor/ExecutorDefinition { + public fun (Ldev/slne/surf/api/minestom/command/executor/ExecutorType;Lkotlin/jvm/functions/Function1;)V + public final fun component1 ()Ldev/slne/surf/api/minestom/command/executor/ExecutorType; + public final fun component2 ()Lkotlin/jvm/functions/Function1; + public final fun copy (Ldev/slne/surf/api/minestom/command/executor/ExecutorType;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/executor/ExecutorDefinition$Normal; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/executor/ExecutorDefinition$Normal;Ldev/slne/surf/api/minestom/command/executor/ExecutorType;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/executor/ExecutorDefinition$Normal; + public fun equals (Ljava/lang/Object;)Z + public final fun getExecutor ()Lkotlin/jvm/functions/Function1; + public fun getType ()Ldev/slne/surf/api/minestom/command/executor/ExecutorType; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/executor/ExecutorDefinition$Resulting : dev/slne/surf/api/minestom/command/executor/ExecutorDefinition { + public fun (Ldev/slne/surf/api/minestom/command/executor/ExecutorType;Lkotlin/jvm/functions/Function1;)V + public final fun component1 ()Ldev/slne/surf/api/minestom/command/executor/ExecutorType; + public final fun component2 ()Lkotlin/jvm/functions/Function1; + public final fun copy (Ldev/slne/surf/api/minestom/command/executor/ExecutorType;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/executor/ExecutorDefinition$Resulting; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/executor/ExecutorDefinition$Resulting;Ldev/slne/surf/api/minestom/command/executor/ExecutorType;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/executor/ExecutorDefinition$Resulting; + public fun equals (Ljava/lang/Object;)Z + public final fun getExecutor ()Lkotlin/jvm/functions/Function1; + public fun getType ()Ldev/slne/surf/api/minestom/command/executor/ExecutorType; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/executor/ExecutorDefinition$Suspending : dev/slne/surf/api/minestom/command/executor/ExecutorDefinition { + public fun (Ldev/slne/surf/api/minestom/command/executor/ExecutorType;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;)V + public final fun component1 ()Ldev/slne/surf/api/minestom/command/executor/ExecutorType; + public final fun component2 ()Lkotlin/jvm/functions/Function0; + public final fun component3 ()Lkotlin/jvm/functions/Function3; + public final fun copy (Ldev/slne/surf/api/minestom/command/executor/ExecutorType;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;)Ldev/slne/surf/api/minestom/command/executor/ExecutorDefinition$Suspending; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/executor/ExecutorDefinition$Suspending;Ldev/slne/surf/api/minestom/command/executor/ExecutorType;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/executor/ExecutorDefinition$Suspending; + public fun equals (Ljava/lang/Object;)Z + public final fun getExecutor ()Lkotlin/jvm/functions/Function3; + public final fun getScopeProvider ()Lkotlin/jvm/functions/Function0; + public fun getType ()Ldev/slne/surf/api/minestom/command/executor/ExecutorType; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/executor/ExecutorType : java/lang/Enum { + public static final field ANY Ldev/slne/surf/api/minestom/command/executor/ExecutorType; + public static final field CONSOLE Ldev/slne/surf/api/minestom/command/executor/ExecutorType; + public static final field PLAYER Ldev/slne/surf/api/minestom/command/executor/ExecutorType; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/executor/ExecutorType; + public static fun values ()[Ldev/slne/surf/api/minestom/command/executor/ExecutorType; +} + +public final class dev/slne/surf/api/minestom/command/executor/ParsedArgument { + public fun (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Z)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/Object; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Z + public final fun copy (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Z)Ldev/slne/surf/api/minestom/command/executor/ParsedArgument; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/executor/ParsedArgument;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;ZILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/executor/ParsedArgument; + public fun equals (Ljava/lang/Object;)Z + public final fun getName ()Ljava/lang/String; + public final fun getPresent ()Z + public final fun getRaw ()Ljava/lang/String; + public final fun getValue ()Ljava/lang/Object; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/executor/SuspendExecutorsKt { + public static final fun getGENERIC_COMMAND_FAILURE ()Lnet/kyori/adventure/text/Component; + public static final fun handleCommandExecutorFailure (Lnet/minestom/server/command/CommandSender;Ljava/lang/String;Ljava/lang/Throwable;)V + public static final fun launchCommandExecutor (Ldev/slne/surf/api/minestom/command/executor/ExecutorDefinition$Suspending;Lnet/minestom/server/command/CommandSender;Ldev/slne/surf/api/minestom/command/executor/CommandArguments;Ljava/lang/String;)Lkotlinx/coroutines/Job; +} + +public abstract interface class dev/slne/surf/api/minestom/command/internal/CommandAPIPlatform { + public abstract fun execute (Lnet/minestom/server/command/CommandSender;Ljava/lang/String;)I + public abstract fun register (Ldev/slne/surf/api/minestom/command/CommandDefinition;Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/RegisteredCommand; + public abstract fun unregister (Ljava/lang/String;)Z +} + +public abstract interface class dev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions { + public static final field Companion Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions$Companion; + public fun getFilterPolicy ()Ldev/slne/surf/api/minestom/command/suggestion/SuggestionFilter; + public abstract fun suggest (Ldev/slne/surf/api/minestom/command/suggestion/SuggestionInfo;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class dev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions$Companion { + public final fun branch (Ldev/slne/surf/api/minestom/command/suggestion/SuggestionsBranch;)Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public final fun empty ()Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public final fun merge ([Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions;)Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public final fun stringCollection (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public final fun stringCollectionAsync (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public final fun strings (Ljava/util/Collection;)Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public final fun strings (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public final fun strings ([Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public final fun stringsAsync (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public final fun stringsWithTooltips (Ljava/util/Collection;)Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public final fun stringsWithTooltips (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public final fun stringsWithTooltips ([Ldev/slne/surf/api/minestom/command/suggestion/StringTooltip;)Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public final fun stringsWithTooltipsAsync (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public final fun stringsWithTooltipsCollection (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public final fun stringsWithTooltipsCollectionAsync (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; + public final fun unfiltered (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions; +} + +public final class dev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions$DefaultImpls { + public static fun getFilterPolicy (Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions;)Ldev/slne/surf/api/minestom/command/suggestion/SuggestionFilter; +} + +public abstract interface class dev/slne/surf/api/minestom/command/suggestion/SafeSuggestions { + public static final field Companion Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions$Companion; + public abstract fun suggest (Ldev/slne/surf/api/minestom/command/suggestion/SuggestionInfo;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class dev/slne/surf/api/minestom/command/suggestion/SafeSuggestions$Companion { + public final fun empty ()Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions; + public final fun suggest (Ljava/util/Collection;)Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions; + public final fun suggest (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions; + public final fun suggest ([Ljava/lang/Object;)Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions; + public final fun suggestAsync (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions; + public final fun suggestCollection (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions; + public final fun suggestCollectionAsync (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions; + public final fun tooltipCollection (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions; + public final fun tooltipCollectionAsync (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions; + public final fun tooltips (Ljava/util/Collection;)Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions; + public final fun tooltips (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions; + public final fun tooltips ([Ldev/slne/surf/api/minestom/command/suggestion/Tooltip;)Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions; + public final fun tooltipsAsync (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/command/suggestion/SafeSuggestions; +} + +public final class dev/slne/surf/api/minestom/command/suggestion/StringTooltip { + public static final field Companion Ldev/slne/surf/api/minestom/command/suggestion/StringTooltip$Companion; + public fun (Ljava/lang/String;Lnet/kyori/adventure/text/Component;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lnet/kyori/adventure/text/Component; + public final fun copy (Ljava/lang/String;Lnet/kyori/adventure/text/Component;)Ldev/slne/surf/api/minestom/command/suggestion/StringTooltip; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/suggestion/StringTooltip;Ljava/lang/String;Lnet/kyori/adventure/text/Component;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/suggestion/StringTooltip; + public fun equals (Ljava/lang/Object;)Z + public final fun getSuggestion ()Ljava/lang/String; + public final fun getTooltip ()Lnet/kyori/adventure/text/Component; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/suggestion/StringTooltip$Companion { + public final fun ofString (Ljava/lang/String;Lnet/kyori/adventure/text/Component;)Ldev/slne/surf/api/minestom/command/suggestion/StringTooltip; + public static synthetic fun ofString$default (Ldev/slne/surf/api/minestom/command/suggestion/StringTooltip$Companion;Ljava/lang/String;Lnet/kyori/adventure/text/Component;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/suggestion/StringTooltip; +} + +public final class dev/slne/surf/api/minestom/command/suggestion/SuggestionFilter : java/lang/Enum { + public static final field NONE Ldev/slne/surf/api/minestom/command/suggestion/SuggestionFilter; + public static final field PREFIX Ldev/slne/surf/api/minestom/command/suggestion/SuggestionFilter; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/suggestion/SuggestionFilter; + public static fun values ()[Ldev/slne/surf/api/minestom/command/suggestion/SuggestionFilter; +} + +public final class dev/slne/surf/api/minestom/command/suggestion/SuggestionInfo { + public fun (Lnet/minestom/server/command/CommandSender;Ldev/slne/surf/api/minestom/command/executor/CommandArguments;Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Lnet/minestom/server/command/CommandSender; + public final fun component2 ()Ldev/slne/surf/api/minestom/command/executor/CommandArguments; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun copy (Lnet/minestom/server/command/CommandSender;Ldev/slne/surf/api/minestom/command/executor/CommandArguments;Ljava/lang/String;Ljava/lang/String;)Ldev/slne/surf/api/minestom/command/suggestion/SuggestionInfo; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/suggestion/SuggestionInfo;Lnet/minestom/server/command/CommandSender;Ldev/slne/surf/api/minestom/command/executor/CommandArguments;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/suggestion/SuggestionInfo; + public fun equals (Ljava/lang/Object;)Z + public final fun getCurrentArg ()Ljava/lang/String; + public final fun getCurrentInput ()Ljava/lang/String; + public final fun getPreviousArgs ()Ldev/slne/surf/api/minestom/command/executor/CommandArguments; + public final fun getSender ()Lnet/minestom/server/command/CommandSender; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/command/suggestion/SuggestionsBranch { + public static final field Companion Ldev/slne/surf/api/minestom/command/suggestion/SuggestionsBranch$Companion; + public final fun branch ([Ldev/slne/surf/api/minestom/command/suggestion/SuggestionsBranch;)Ldev/slne/surf/api/minestom/command/suggestion/SuggestionsBranch; + public final fun enforceReplacements (Lnet/minestom/server/command/CommandSender;[Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun getNextSuggestion (Lnet/minestom/server/command/CommandSender;[Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class dev/slne/surf/api/minestom/command/suggestion/SuggestionsBranch$Companion { + public final fun suggest ([Ldev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions;)Ldev/slne/surf/api/minestom/command/suggestion/SuggestionsBranch; +} + +public final class dev/slne/surf/api/minestom/command/suggestion/Tooltip { + public fun (Ljava/lang/Object;Lnet/kyori/adventure/text/Component;)V + public final fun component1 ()Ljava/lang/Object; + public final fun component2 ()Lnet/kyori/adventure/text/Component; + public final fun copy (Ljava/lang/Object;Lnet/kyori/adventure/text/Component;)Ldev/slne/surf/api/minestom/command/suggestion/Tooltip; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/command/suggestion/Tooltip;Ljava/lang/Object;Lnet/kyori/adventure/text/Component;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/command/suggestion/Tooltip; + public fun equals (Ljava/lang/Object;)Z + public final fun getSuggestion ()Ljava/lang/Object; + public final fun getTooltip ()Lnet/kyori/adventure/text/Component; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/coroutine/Entity_schedulerKt { + public static final fun withEntities (Ljava/util/Collection;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withEntity (Lnet/minestom/server/entity/Entity;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class dev/slne/surf/api/minestom/coroutine/MinestomDispatchers { + public static final field INSTANCE Ldev/slne/surf/api/minestom/coroutine/MinestomDispatchers; + public final fun getBlocking ()Lkotlinx/coroutines/CoroutineDispatcher; + public final fun getMain ()Lkotlinx/coroutines/CoroutineDispatcher; +} + +public final class dev/slne/surf/api/minestom/coroutine/MinestomScopeProviderKt { + public static final fun getMinestomAsyncScope ()Lkotlinx/coroutines/CoroutineScope; + public static final fun getMinestomBlockingScope ()Lkotlinx/coroutines/CoroutineScope; + public static final fun getMinestomScope ()Lkotlinx/coroutines/CoroutineScope; +} + +public final class dev/slne/surf/api/minestom/coroutine/UtilKt { + public static final fun asyncSuspend (Ljava/util/Collection;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun asyncSuspend (Lnet/minestom/server/thread/Acquirable;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun asyncSuspend (Lnet/minestom/server/thread/AcquirableSource;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun getTicks (I)J + public static final fun launch (Lnet/minestom/server/ServerProcess;Lkotlin/coroutines/CoroutineContext;Lkotlinx/coroutines/CoroutineStart;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/Job; + public static synthetic fun launch$default (Lnet/minestom/server/ServerProcess;Lkotlin/coroutines/CoroutineContext;Lkotlinx/coroutines/CoroutineStart;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/Job; +} + +public final class dev/slne/surf/api/minestom/dialog/CommonDialogsKt { + public static final fun noticeDialog (Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/dialog/Dialog; + public static final fun noticeDialog (Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;Ljava/lang/Integer;)Lnet/minestom/server/dialog/Dialog; + public static synthetic fun noticeDialog$default (Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;Ljava/lang/Integer;ILjava/lang/Object;)Lnet/minestom/server/dialog/Dialog; + public static final fun noticeDialogWithBuilder (Lnet/kyori/adventure/text/Component;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/dialog/Dialog; + public static synthetic fun noticeDialogWithBuilder$default (Lnet/kyori/adventure/text/Component;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/minestom/server/dialog/Dialog; +} + +public final class dev/slne/surf/api/minestom/dialog/DialogBuilderKt { + public static final fun base (Ldev/slne/surf/api/minestom/dialog/builder/DialogEntryBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun dialog (Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/dialog/Dialog; + public static final fun type (Ldev/slne/surf/api/minestom/dialog/builder/DialogEntryBuilder;Lkotlin/jvm/functions/Function1;)V +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder { + public fun ()V + public final fun action (Lnet/minestom/server/dialog/DialogAction;)V + public final fun callback (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun callback$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder;Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public final fun changePage (I)V + public final fun commandTemplate (Ljava/lang/String;)V + public final fun copyToClipboard (Ljava/lang/String;)V + public final fun customClick (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lnet/kyori/adventure/nbt/CompoundBinaryTag;Lkotlin/jvm/functions/Function2;)V + public final fun customClick (Lnet/kyori/adventure/key/Key;Lkotlin/jvm/functions/Function1;)V + public final fun customClick (Lnet/kyori/adventure/key/Key;Lnet/kyori/adventure/nbt/BinaryTag;)V + public static synthetic fun customClick$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder;Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lnet/kyori/adventure/nbt/CompoundBinaryTag;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static synthetic fun customClick$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder;Lnet/kyori/adventure/key/Key;Lnet/kyori/adventure/nbt/BinaryTag;ILjava/lang/Object;)V + public final fun customPlayerClick (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lnet/kyori/adventure/nbt/CompoundBinaryTag;Lkotlin/jvm/functions/Function2;)V + public static synthetic fun customPlayerClick$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder;Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lnet/kyori/adventure/nbt/CompoundBinaryTag;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public final fun dynamicCustomClick (Lnet/kyori/adventure/key/Key;Lnet/kyori/adventure/nbt/CompoundBinaryTag;)V + public static synthetic fun dynamicCustomClick$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder;Lnet/kyori/adventure/key/Key;Lnet/kyori/adventure/nbt/CompoundBinaryTag;ILjava/lang/Object;)V + public final fun openUrl (Ljava/lang/String;)V + public final fun openUrl (Ljava/net/URL;)V + public final fun playerCallback (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun playerCallback$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder;Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public final fun runCommand (Ljava/lang/String;)V + public final fun showDialog (Lkotlin/jvm/functions/Function1;)V + public final fun showDialog (Lnet/minestom/server/dialog/Dialog;)V + public final fun suggestCommand (Ljava/lang/String;)V +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogActionBuilderKt { + public static final fun dialogAction (Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/dialog/DialogAction; +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogActionButtonBuilder { + public fun ()V + public final fun action (Lkotlin/jvm/functions/Function1;)V + public final fun action (Lnet/minestom/server/dialog/DialogAction;)V + public final fun getAction ()Lnet/minestom/server/dialog/DialogAction; + public final fun getLabel ()Lnet/kyori/adventure/text/Component; + public final fun getTooltip ()Lnet/kyori/adventure/text/Component; + public final fun getWidth ()I + public final fun label (Lkotlin/jvm/functions/Function1;)V + public final fun label (Lnet/kyori/adventure/text/Component;)V + public final fun setAction (Lnet/minestom/server/dialog/DialogAction;)V + public final fun setLabel (Lnet/kyori/adventure/text/Component;)V + public final fun setTooltip (Lnet/kyori/adventure/text/Component;)V + public final fun setWidth (I)V + public final fun tooltip (Lkotlin/jvm/functions/Function1;)V + public final fun tooltip (Lnet/kyori/adventure/text/Component;)V + public final fun width (I)V +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogActionButtonBuilderKt { + public static final fun actionButton (Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/dialog/DialogActionButton; +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogBaseBuilder { + public fun ()V + public final fun afterAction (Lnet/minestom/server/dialog/DialogAfterAction;)V + public final fun body (Lkotlin/jvm/functions/Function1;)V + public final fun canCloseWithEscape (Z)V + public final fun externalTitle (Lkotlin/jvm/functions/Function1;)V + public final fun externalTitle (Lnet/kyori/adventure/text/Component;)V + public final fun getAfterAction ()Lnet/minestom/server/dialog/DialogAfterAction; + public final fun getCanCloseWithEscape ()Z + public final fun getExternalTitle ()Lnet/kyori/adventure/text/Component; + public final fun getPause ()Z + public final fun getTitle ()Lnet/kyori/adventure/text/Component; + public final fun input (Lkotlin/jvm/functions/Function1;)V + public final fun pause (Z)V + public final fun preventClosingWithEscape (Z)V + public static synthetic fun preventClosingWithEscape$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogBaseBuilder;ZILjava/lang/Object;)V + public final fun setAfterAction (Lnet/minestom/server/dialog/DialogAfterAction;)V + public final fun setCanCloseWithEscape (Z)V + public final fun setExternalTitle (Lnet/kyori/adventure/text/Component;)V + public final fun setPause (Z)V + public final fun setTitle (Lnet/kyori/adventure/text/Component;)V + public final fun title (Lkotlin/jvm/functions/Function1;)V + public final fun title (Lnet/kyori/adventure/text/Component;)V +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogBaseBuilderKt { + public static final fun dialogBase (Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/dialog/DialogMetadata; +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder { + public fun ()V + public final fun item (Lkotlin/jvm/functions/Function1;)V + public final fun plain (Lkotlin/jvm/functions/Function1;)V + public final fun plainMessage (Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;)V + public final fun plainMessage (Lnet/kyori/adventure/text/Component;Ljava/lang/Integer;)V + public final fun plainMessage (Lnet/minestom/server/dialog/DialogBody$PlainMessage;)V + public static synthetic fun plainMessage$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun plainMessage$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder;Lnet/kyori/adventure/text/Component;Ljava/lang/Integer;ILjava/lang/Object;)V +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder$ItemDialogMessageBuilder { + public fun ()V + public final fun description (Lkotlin/jvm/functions/Function1;)V + public final fun description (Lnet/minestom/server/dialog/DialogBody$PlainMessage;)V + public final fun getDescription ()Lnet/minestom/server/dialog/DialogBody$PlainMessage; + public final fun getHeight ()I + public final fun getItem ()Lnet/minestom/server/item/ItemStack; + public final fun getShowDecorations ()Z + public final fun getShowTooltip ()Z + public final fun getWidth ()I + public final fun height (I)V + public final fun item (Lnet/minestom/server/item/ItemStack;)V + public final fun item (Lnet/minestom/server/item/Material;I)V + public static synthetic fun item$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder$ItemDialogMessageBuilder;Lnet/minestom/server/item/Material;IILjava/lang/Object;)V + public final fun setDescription (Lnet/minestom/server/dialog/DialogBody$PlainMessage;)V + public final fun setHeight (I)V + public final fun setItem (Lnet/minestom/server/item/ItemStack;)V + public final fun setShowDecorations (Z)V + public final fun setShowTooltip (Z)V + public final fun setWidth (I)V + public final fun showDecorations (Z)V + public final fun showTooltip (Z)V + public final fun simpleDescription (Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;)V + public final fun simpleDescription (Lnet/kyori/adventure/text/Component;Ljava/lang/Integer;)V + public static synthetic fun simpleDescription$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder$ItemDialogMessageBuilder;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun simpleDescription$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder$ItemDialogMessageBuilder;Lnet/kyori/adventure/text/Component;Ljava/lang/Integer;ILjava/lang/Object;)V + public final fun width (I)V +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder$PlainDialogMessageBuilder { + public fun ()V + public final fun getMessage ()Lnet/kyori/adventure/text/Component; + public final fun getWidth ()Ljava/lang/Integer; + public final fun message (Lkotlin/jvm/functions/Function1;)V + public final fun message (Lnet/kyori/adventure/text/Component;)V + public final fun setMessage (Lnet/kyori/adventure/text/Component;)V + public final fun setWidth (Ljava/lang/Integer;)V + public final fun width (I)V +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilderKt { + public static final fun dialogBody (Lkotlin/jvm/functions/Function1;)Lit/unimi/dsi/fastutil/objects/ObjectList; +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogEntryBuilder { + public fun ()V + public final fun base (Ldev/slne/surf/api/minestom/dialog/builder/DialogBaseBuilder;)V + public final fun type (Ldev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder;)V +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder { + public fun ()V + public final fun boolean (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public final fun input (Lnet/minestom/server/dialog/DialogInput;)V + public final fun numberRange (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public final fun numberRange (Ljava/lang/String;Lkotlin/ranges/ClosedRange;Lkotlin/jvm/functions/Function1;)V + public final fun singleOption (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public final fun text (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$BooleanInputBuilder : dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$InputBuilder { + public fun (Ljava/lang/String;)V + public final fun getInitial ()Z + public final fun getOnFalse ()Ljava/lang/String; + public final fun getOnTrue ()Ljava/lang/String; + public final fun initial (Z)V + public final fun onFalse (Ljava/lang/String;)V + public final fun onTrue (Ljava/lang/String;)V + public final fun setInitial (Z)V + public final fun setOnFalse (Ljava/lang/String;)V + public final fun setOnTrue (Ljava/lang/String;)V +} + +public abstract class dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$InputBuilder { + public fun (Ljava/lang/String;)V + protected final fun getKey ()Ljava/lang/String; + public final fun getLabel ()Lnet/kyori/adventure/text/Component; + public final fun label (Lkotlin/jvm/functions/Function1;)V + public final fun label (Lnet/kyori/adventure/text/Component;)V + protected final fun requireLabel ()Lnet/kyori/adventure/text/Component; + public final fun setLabel (Lnet/kyori/adventure/text/Component;)V +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$NumberRangeInputBuilder : dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$InputBuilder { + public fun (Ljava/lang/String;)V + public final fun getEnd ()Ljava/lang/Float; + public final fun getInitial ()Ljava/lang/Float; + public final fun getLabelFormat ()Ljava/lang/String; + public final fun getStart ()Ljava/lang/Float; + public final fun getStep ()Ljava/lang/Float; + public final fun getWidth ()I + public final fun initial (F)V + public final fun labelFormat (Ljava/lang/String;)V + public final fun range (FF)V + public final fun setEnd (Ljava/lang/Float;)V + public final fun setInitial (Ljava/lang/Float;)V + public final fun setLabelFormat (Ljava/lang/String;)V + public final fun setStart (Ljava/lang/Float;)V + public final fun setStep (Ljava/lang/Float;)V + public final fun setWidth (I)V + public final fun step (F)V + public final fun width (I)V +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$SingleOptionInputBuilder : dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$InputBuilder { + public fun (Ljava/lang/String;)V + public final fun getLabelVisible ()Z + public final fun getWidth ()I + public final fun labelVisible (Z)V + public final fun option (Ljava/lang/String;Lnet/kyori/adventure/text/Component;Z)V + public final fun option (Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)V + public static synthetic fun option$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$SingleOptionInputBuilder;Ljava/lang/String;Lnet/kyori/adventure/text/Component;ZILjava/lang/Object;)V + public static synthetic fun option$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$SingleOptionInputBuilder;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public final fun setLabelVisible (Z)V + public final fun setWidth (I)V + public final fun width (I)V +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$TextInputBuilder : dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$InputBuilder { + public fun (Ljava/lang/String;)V + public final fun getInitial ()Ljava/lang/String; + public final fun getLabelVisible ()Z + public final fun getMaxLength ()I + public final fun getWidth ()I + public final fun initial (Ljava/lang/String;)V + public final fun labelVisible (Z)V + public final fun maxLength (I)V + public final fun multiline (Ljava/lang/Integer;Ljava/lang/Integer;)V + public static synthetic fun multiline$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder$TextInputBuilder;Ljava/lang/Integer;Ljava/lang/Integer;ILjava/lang/Object;)V + public final fun setInitial (Ljava/lang/String;)V + public final fun setLabelVisible (Z)V + public final fun setMaxLength (I)V + public final fun setWidth (I)V + public final fun width (I)V +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilderKt { + public static final fun dialogInput (Lkotlin/jvm/functions/Function1;)Lit/unimi/dsi/fastutil/objects/ObjectList; +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder { + public fun ()V + public final fun confirmation (Lkotlin/jvm/functions/Function1;)V + public final fun confirmation (Lnet/minestom/server/dialog/DialogActionButton;Lnet/minestom/server/dialog/DialogActionButton;)V + public final fun dialogList (Ljava/util/Collection;Lkotlin/jvm/functions/Function1;)V + public final fun dialogList (Lkotlin/jvm/functions/Function1;)V + public final fun dialogList ([Lnet/minestom/server/dialog/Dialog;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun dialogList$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder;Ljava/util/Collection;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun dialogList$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder;[Lnet/minestom/server/dialog/Dialog;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public final fun multiAction (Ljava/util/List;Lkotlin/jvm/functions/Function1;)V + public final fun multiAction (Lkotlin/jvm/functions/Function1;)V + public final fun multiAction ([Lnet/minestom/server/dialog/DialogActionButton;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun multiAction$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder;Ljava/util/List;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun multiAction$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder;[Lnet/minestom/server/dialog/DialogActionButton;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public final fun notice ()V + public final fun notice (Lkotlin/jvm/functions/Function1;)V + public final fun notice (Lnet/minestom/server/dialog/DialogActionButton;)V + public final fun serverLinks (Lkotlin/jvm/functions/Function1;)V + public static synthetic fun serverLinks$default (Ldev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder$DialogConfirmationTypeBuilder { + public fun ()V + public final fun getNo ()Lnet/minestom/server/dialog/DialogActionButton; + public final fun getYes ()Lnet/minestom/server/dialog/DialogActionButton; + public final fun no (Lkotlin/jvm/functions/Function1;)V + public final fun no (Lnet/minestom/server/dialog/DialogActionButton;)V + public final fun setNo (Lnet/minestom/server/dialog/DialogActionButton;)V + public final fun setYes (Lnet/minestom/server/dialog/DialogActionButton;)V + public final fun yes (Lkotlin/jvm/functions/Function1;)V + public final fun yes (Lnet/minestom/server/dialog/DialogActionButton;)V +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder$DialogListTypeBuilder { + public fun ()V + public final fun addAll (Ljava/lang/Iterable;)V + public final fun addAll ([Lnet/minestom/server/dialog/Dialog;)V + public final fun buttonWidth (I)V + public final fun columns (I)V + public final fun dialog (Lnet/minestom/server/dialog/Dialog;)V + public final fun exitAction (Lkotlin/jvm/functions/Function1;)V + public final fun exitAction (Lnet/minestom/server/dialog/DialogActionButton;)V + public final fun getButtonWidth ()I + public final fun getColumns ()I + public final fun getExitAction ()Lnet/minestom/server/dialog/DialogActionButton; + public final fun setButtonWidth (I)V + public final fun setColumns (I)V + public final fun setExitAction (Lnet/minestom/server/dialog/DialogActionButton;)V +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder$DialogMultiActionTypeBuilder { + public fun ()V + public final fun action (Lkotlin/jvm/functions/Function1;)V + public final fun action (Lnet/minestom/server/dialog/DialogActionButton;)V + public final fun columns (I)V + public final fun exitAction (Lkotlin/jvm/functions/Function1;)V + public final fun exitAction (Lnet/minestom/server/dialog/DialogActionButton;)V + public final fun getColumns ()I + public final fun getExitAction ()Lnet/minestom/server/dialog/DialogActionButton; + public final fun setColumns (I)V + public final fun setExitAction (Lnet/minestom/server/dialog/DialogActionButton;)V +} + +public final class dev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder$ServerLinksTypeBuilder { + public fun ()V + public final fun buttonWidth (I)V + public final fun columns (I)V + public final fun exitAction (Lkotlin/jvm/functions/Function1;)V + public final fun exitAction (Lnet/minestom/server/dialog/DialogActionButton;)V + public final fun getButtonWidth ()I + public final fun getColumns ()I + public final fun getExitAction ()Lnet/minestom/server/dialog/DialogActionButton; + public final fun setButtonWidth (I)V + public final fun setColumns (I)V + public final fun setExitAction (Lnet/minestom/server/dialog/DialogActionButton;)V +} + +public final class dev/slne/surf/api/minestom/dialog/callback/DialogCallbackContext { + public fun (Lnet/minestom/server/entity/Player;Lnet/kyori/adventure/nbt/BinaryTag;)V + public final fun component1 ()Lnet/minestom/server/entity/Player; + public final fun component2 ()Lnet/kyori/adventure/nbt/BinaryTag; + public final fun copy (Lnet/minestom/server/entity/Player;Lnet/kyori/adventure/nbt/BinaryTag;)Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackContext; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackContext;Lnet/minestom/server/entity/Player;Lnet/kyori/adventure/nbt/BinaryTag;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackContext; + public fun equals (Ljava/lang/Object;)Z + public final fun getPayload ()Lnet/kyori/adventure/nbt/BinaryTag; + public final fun getPlayer ()Lnet/minestom/server/entity/Player; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions { + public static final field Companion Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions$Companion; + public static final field ONE_USE I + public static final field UNLIMITED_USES I + public fun ()V + public synthetic fun (IJILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (IJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()I + public final fun component2-UwyO8pc ()J + public final fun copy-HG0u8IE (IJ)Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions; + public static synthetic fun copy-HG0u8IE$default (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;IJILjava/lang/Object;)Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions; + public fun equals (Ljava/lang/Object;)Z + public final fun getLifetime-UwyO8pc ()J + public final fun getUses ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions$Companion { + public final fun getDEFAULT ()Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions; + public final fun getDEFAULT_LIFETIME-UwyO8pc ()J +} + +public final class dev/slne/surf/api/minestom/dialog/callback/DialogCallbacks { + public static final field INSTANCE Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbacks; + public final fun action (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/dialog/DialogAction; + public static synthetic fun action$default (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbacks;Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/minestom/server/dialog/DialogAction; + public final fun dispatch (Lnet/minestom/server/entity/Player;Lnet/kyori/adventure/key/Key;Lnet/kyori/adventure/nbt/BinaryTag;)Z + public final fun dynamicAction (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lnet/kyori/adventure/nbt/CompoundBinaryTag;Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/dialog/DialogAction; + public static synthetic fun dynamicAction$default (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbacks;Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lnet/kyori/adventure/nbt/CompoundBinaryTag;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/minestom/server/dialog/DialogAction; + public final fun register (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lkotlin/jvm/functions/Function1;)Lnet/kyori/adventure/key/Key; + public static synthetic fun register$default (Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbacks;Ldev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/kyori/adventure/key/Key; + public final fun unregister (Lnet/kyori/adventure/key/Key;)Z +} + +public final class dev/slne/surf/api/minestom/dialog/callback/DialogResponseView { + public static final field Companion Ldev/slne/surf/api/minestom/dialog/callback/DialogResponseView$Companion; + public static final synthetic fun box-impl (Lnet/kyori/adventure/nbt/CompoundBinaryTag;)Ldev/slne/surf/api/minestom/dialog/callback/DialogResponseView; + public static fun constructor-impl (Lnet/kyori/adventure/nbt/CompoundBinaryTag;)Lnet/kyori/adventure/nbt/CompoundBinaryTag; + public fun equals (Ljava/lang/Object;)Z + public static fun equals-impl (Lnet/kyori/adventure/nbt/CompoundBinaryTag;Ljava/lang/Object;)Z + public static final fun equals-impl0 (Lnet/kyori/adventure/nbt/CompoundBinaryTag;Lnet/kyori/adventure/nbt/CompoundBinaryTag;)Z + public static final fun getBoolean-impl (Lnet/kyori/adventure/nbt/CompoundBinaryTag;Ljava/lang/String;)Ljava/lang/Boolean; + public static final fun getFloat-impl (Lnet/kyori/adventure/nbt/CompoundBinaryTag;Ljava/lang/String;)Ljava/lang/Float; + public final fun getPayload ()Lnet/kyori/adventure/nbt/CompoundBinaryTag; + public static final fun getText-impl (Lnet/kyori/adventure/nbt/CompoundBinaryTag;Ljava/lang/String;)Ljava/lang/String; + public fun hashCode ()I + public static fun hashCode-impl (Lnet/kyori/adventure/nbt/CompoundBinaryTag;)I + public fun toString ()Ljava/lang/String; + public static fun toString-impl (Lnet/kyori/adventure/nbt/CompoundBinaryTag;)Ljava/lang/String; + public final synthetic fun unbox-impl ()Lnet/kyori/adventure/nbt/CompoundBinaryTag; +} + +public final class dev/slne/surf/api/minestom/dialog/callback/DialogResponseView$Companion { + public final fun getEMPTY-v2-CkQY ()Lnet/kyori/adventure/nbt/CompoundBinaryTag; + public final fun of-VLTPHq8 (Lnet/kyori/adventure/nbt/BinaryTag;)Lnet/kyori/adventure/nbt/CompoundBinaryTag; +} + +public final class dev/slne/surf/api/minestom/dialog/search/SearchDialogAction { + public fun (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public final fun component1 ()Lkotlin/jvm/functions/Function1; + public final fun component2 ()Lkotlin/jvm/functions/Function1; + public final fun copy (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/dialog/search/SearchDialogAction; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/dialog/search/SearchDialogAction;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/dialog/search/SearchDialogAction; + public fun equals (Ljava/lang/Object;)Z + public final fun getLabel ()Lkotlin/jvm/functions/Function1; + public final fun getTooltip ()Lkotlin/jvm/functions/Function1; + public fun hashCode ()I + public final fun setLabel (Lkotlin/jvm/functions/Function1;)V + public final fun setTooltip (Lkotlin/jvm/functions/Function1;)V + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/dialog/search/SearchDialogKt { + public static final fun searchDialog (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLnet/minestom/server/dialog/DialogAfterAction;)Lnet/minestom/server/dialog/Dialog; + public static synthetic fun searchDialog$default (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLnet/minestom/server/dialog/DialogAfterAction;ILjava/lang/Object;)Lnet/minestom/server/dialog/Dialog; +} + +public final class dev/slne/surf/api/minestom/dialog/search/SearchInput { + public fun ()V + public fun (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lkotlin/jvm/functions/Function1; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lkotlin/jvm/functions/Function1; + public final fun copy (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/dialog/search/SearchInput; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/dialog/search/SearchInput;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/dialog/search/SearchInput; + public fun equals (Ljava/lang/Object;)Z + public final fun getInitialValue ()Ljava/lang/String; + public final fun getInputModifier ()Lkotlin/jvm/functions/Function1; + public final fun getKey ()Ljava/lang/String; + public final fun getLabel ()Lkotlin/jvm/functions/Function1; + public fun hashCode ()I + public final fun setInitialValue (Ljava/lang/String;)V + public final fun setInputModifier (Lkotlin/jvm/functions/Function1;)V + public final fun setKey (Ljava/lang/String;)V + public final fun setLabel (Lkotlin/jvm/functions/Function1;)V + public fun toString ()Ljava/lang/String; +} + +public abstract interface class dev/slne/surf/api/minestom/event/EventRegistrar { + public abstract fun register (Lnet/minestom/server/event/EventNode;)V +} + +public final class dev/slne/surf/api/minestom/event/SuspendingEventNode { + public static final field Companion Ldev/slne/surf/api/minestom/event/SuspendingEventNode$Companion; + public fun (Ljava/lang/String;)V + public final fun addListener (ILkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/minestom/event/SuspendingEventNode$Registration; + public static synthetic fun addListener$default (Ldev/slne/surf/api/minestom/event/SuspendingEventNode;ILkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/event/SuspendingEventNode$Registration; + public final fun call (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class dev/slne/surf/api/minestom/event/SuspendingEventNode$Companion { +} + +public final class dev/slne/surf/api/minestom/event/SuspendingEventNode$Registration : java/lang/AutoCloseable { + public fun close ()V +} + +public final class dev/slne/surf/api/minestom/extension/EntityKt { + public static final fun getDisplayName (Lnet/minestom/server/entity/Entity;)Lnet/kyori/adventure/text/Component; +} + +public final class dev/slne/surf/api/minestom/extension/InstanceKt { + public static final fun buildInstance (Lnet/minestom/server/registry/RegistryKey;Lnet/kyori/adventure/key/Key;ZLkotlin/jvm/functions/Function1;)Lnet/minestom/server/instance/InstanceContainer; + public static synthetic fun buildInstance$default (Lnet/minestom/server/registry/RegistryKey;Lnet/kyori/adventure/key/Key;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/minestom/server/instance/InstanceContainer; + public static final fun generator (Lnet/minestom/server/instance/InstanceContainer;Lkotlin/jvm/functions/Function1;)V + public static final fun modify (Lnet/minestom/server/instance/generator/GenerationUnit;Lkotlin/jvm/functions/Function1;)V +} + +public final class dev/slne/surf/api/minestom/extension/RegistryKt { + public static final fun getOrThrow (Lnet/minestom/server/registry/Registry;I)Ljava/lang/Object; + public static final fun getOrThrow (Lnet/minestom/server/registry/Registry;Lnet/kyori/adventure/key/Key;)Ljava/lang/Object; + public static final fun getOrThrow (Lnet/minestom/server/registry/Registry;Lnet/minestom/server/registry/RegistryKey;)Ljava/lang/Object; + public static final fun getParticleRegistry ()Lnet/minestom/server/registry/Registry; +} + +public final class dev/slne/surf/api/minestom/extension/ServerKt { + public static final fun getAdvancementManager ()Lnet/minestom/server/advancements/AdvancementManager; + public static final fun getBannerPatternRegistry ()Lnet/minestom/server/registry/DynamicRegistry; + public static final fun getBiomeRegistry ()Lnet/minestom/server/registry/DynamicRegistry; + public static final fun getBlockManager ()Lnet/minestom/server/instance/block/BlockManager; + public static final fun getBossBarManager ()Lnet/minestom/server/adventure/bossbar/BossBarManager; + public static final fun getChatTypeRegistry ()Lnet/minestom/server/registry/DynamicRegistry; + public static final fun getCommandManager ()Lnet/minestom/server/command/CommandManager; + public static final fun getConnectionManager ()Lnet/minestom/server/network/ConnectionManager; + public static final fun getDamageTypeRegistry ()Lnet/minestom/server/registry/DynamicRegistry; + public static final fun getDimensionTypeRegistry ()Lnet/minestom/server/registry/DynamicRegistry; + public static final fun getEnchantmentEntityEffects ()Lnet/minestom/server/registry/DynamicRegistry; + public static final fun getEnchantmentLevelBasedValues ()Lnet/minestom/server/registry/DynamicRegistry; + public static final fun getEnchantmentLocationEffects ()Lnet/minestom/server/registry/DynamicRegistry; + public static final fun getEnchantmentRegistry ()Lnet/minestom/server/registry/DynamicRegistry; + public static final fun getEnchantmentValueEffects ()Lnet/minestom/server/registry/DynamicRegistry; + public static final fun getExceptionManager ()Lnet/minestom/server/exception/ExceptionManager; + public static final fun getGlobalEventHandler ()Lnet/minestom/server/event/GlobalEventHandler; + public static final fun getInstanceManager ()Lnet/minestom/server/instance/InstanceManager; + public static final fun getInstrumentRegistry ()Lnet/minestom/server/registry/DynamicRegistry; + public static final fun getJukeboxSongRegistry ()Lnet/minestom/server/registry/DynamicRegistry; + public static final fun getPacketListenerManager ()Lnet/minestom/server/listener/manager/PacketListenerManager; + public static final fun getPacketParser ()Lnet/minestom/server/network/packet/PacketParser; + public static final fun getPaintingVariantRegistry ()Lnet/minestom/server/registry/DynamicRegistry; + public static final fun getRecipeManager ()Lnet/minestom/server/recipe/RecipeManager; + public static final fun getSchedulerManager ()Lnet/minestom/server/timer/SchedulerManager; + public static final fun getServer ()Lnet/minestom/server/ServerProcess; + public static final fun getTeamManager ()Lnet/minestom/server/scoreboard/TeamManager; + public static final fun getTrimMaterialRegistry ()Lnet/minestom/server/registry/DynamicRegistry; + public static final fun getTrimPatternRegistry ()Lnet/minestom/server/registry/DynamicRegistry; + public static final fun getWolfVariantRegistry ()Lnet/minestom/server/registry/DynamicRegistry; +} + +public final class dev/slne/surf/api/minestom/highlight/BlockHighlights { + public static final field INSTANCE Ldev/slne/surf/api/minestom/highlight/BlockHighlights; + public final fun count (Lnet/minestom/server/entity/Player;)I + public final fun hide (Lnet/minestom/server/entity/Player;Lnet/minestom/server/instance/Instance;Lnet/minestom/server/coordinate/Point;)Z + public final fun hideAll (Lnet/minestom/server/entity/Player;)I + public final fun show (Lnet/minestom/server/entity/Player;Lnet/minestom/server/instance/Instance;Lnet/minestom/server/coordinate/Point;Lnet/kyori/adventure/text/format/TextColor;Lnet/minestom/server/instance/block/Block;)V + public static synthetic fun show$default (Ldev/slne/surf/api/minestom/highlight/BlockHighlights;Lnet/minestom/server/entity/Player;Lnet/minestom/server/instance/Instance;Lnet/minestom/server/coordinate/Point;Lnet/kyori/adventure/text/format/TextColor;Lnet/minestom/server/instance/block/Block;ILjava/lang/Object;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/InventoryFrameworkExtensions { + public static final fun getOutlineItem (Lme/devnatan/inventoryframework/View;)Lnet/minestom/server/item/ItemStack; + public static final fun modifyConfig (Lme/devnatan/inventoryframework/context/OpenContext;Lkotlin/jvm/functions/Function1;)Lme/devnatan/inventoryframework/ViewConfigBuilder; + public static final fun open (Lme/devnatan/inventoryframework/View;Ljava/util/Collection;)V + public static final fun open (Lme/devnatan/inventoryframework/View;Ljava/util/Collection;Ljava/lang/Object;)V + public static final fun open (Lme/devnatan/inventoryframework/View;Lnet/minestom/server/entity/Player;)V + public static final fun open (Lme/devnatan/inventoryframework/View;Lnet/minestom/server/entity/Player;Ljava/lang/Object;)V + public static final fun register (Lme/devnatan/inventoryframework/View;)V + public static final fun titleBuilder (Lme/devnatan/inventoryframework/ViewConfigBuilder;Lkotlin/jvm/functions/Function1;)Lme/devnatan/inventoryframework/ViewConfigBuilder; + public static final fun unregister (Lme/devnatan/inventoryframework/View;)V +} + +public abstract interface class dev/slne/surf/api/minestom/inventory/framework/ViewFrameAccessor { + public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/ViewFrameAccessor$Companion; + public abstract fun viewFrame ()Lme/devnatan/inventoryframework/ViewFrame; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/ViewFrameAccessor$Companion : dev/slne/surf/api/minestom/inventory/framework/ViewFrameAccessor { + public final fun getINSTANCE ()Ldev/slne/surf/api/minestom/inventory/framework/ViewFrameAccessor; + public fun viewFrame ()Lme/devnatan/inventoryframework/ViewFrame; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/ViewFrameAccessorKt { + public static final fun getViewFrame ()Lme/devnatan/inventoryframework/ViewFrame; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/dsl/ClickContextDSLKt { + public static final fun cancel (Lme/devnatan/inventoryframework/context/IFSlotClickContext;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/dsl/CloseContextDSLKt { + public static final fun cancel (Lme/devnatan/inventoryframework/context/IFCloseContext;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/dsl/IFConfinedContextDSLKt { + public static final fun openForPlayer (Lme/devnatan/inventoryframework/context/IFConfinedContext;Lme/devnatan/inventoryframework/RootView;)V + public static final fun openForPlayer (Lme/devnatan/inventoryframework/context/IFConfinedContext;Lme/devnatan/inventoryframework/RootView;Ljava/lang/Object;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/dsl/IFContextDSLKt { + public static final fun openForEveryone (Lme/devnatan/inventoryframework/context/IFContext;Lme/devnatan/inventoryframework/RootView;)V + public static final fun openForEveryone (Lme/devnatan/inventoryframework/context/IFContext;Lme/devnatan/inventoryframework/RootView;Ljava/lang/Object;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/dsl/LayoutBuilder { + public fun ()V + public final fun border (C)V + public final fun build ()[Ljava/lang/String; + public final fun empty ()V + public final fun fill (C)V + public final fun getRows ()Ljava/util/List; + public final fun row (Ljava/lang/String;)V + public final fun unaryPlus (Ljava/lang/String;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/dsl/MinestomItemComponentBuilderDSLKt { + public static final fun onItemClick (Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun onItemRender (Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun onItemUpdate (Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder;Lkotlin/jvm/functions/Function1;)V + public static final fun renderWith (Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder;Lnet/minestom/server/item/Material;ILkotlin/jvm/functions/Function1;)Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder; + public static synthetic fun renderWith$default (Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder;Lnet/minestom/server/item/Material;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder; + public static final fun withItem (Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder;Lnet/minestom/server/item/Material;ILkotlin/jvm/functions/Function1;)Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder; + public static synthetic fun withItem$default (Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder;Lnet/minestom/server/item/Material;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/dsl/OpenContextDSLKt { + public static final fun cancel (Lme/devnatan/inventoryframework/context/IFOpenContext;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/dsl/RenderContextDSLKt { + public static final fun availableSlot (Lme/devnatan/inventoryframework/context/RenderContext;Lkotlin/jvm/functions/Function1;)V + public static final fun firstSlot (Lme/devnatan/inventoryframework/context/RenderContext;Lkotlin/jvm/functions/Function1;)V + public static final fun lastSlot (Lme/devnatan/inventoryframework/context/RenderContext;Lkotlin/jvm/functions/Function1;)V + public static final fun layoutSlot (Lme/devnatan/inventoryframework/context/RenderContext;CLkotlin/jvm/functions/Function1;)V + public static final fun resultSlot (Lme/devnatan/inventoryframework/context/RenderContext;Lkotlin/jvm/functions/Function1;)V + public static final fun slot (Lme/devnatan/inventoryframework/context/RenderContext;IILkotlin/jvm/functions/Function1;)V + public static final fun slot (Lme/devnatan/inventoryframework/context/RenderContext;ILkotlin/jvm/functions/Function1;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/dsl/ViewConfigBuilderDSLKt { + public static final fun layout (Lme/devnatan/inventoryframework/ViewConfigBuilder;Lkotlin/jvm/functions/Function1;)V +} + +public abstract class dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView : me/devnatan/inventoryframework/View { + public fun (Ljava/lang/String;)V + protected fun containerDefaults (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;)V + public fun getSettings ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings; + protected final fun modifyContainer (Lme/devnatan/inventoryframework/context/Context;Lkotlin/jvm/functions/Function1;)V + public synthetic fun onClick (Lme/devnatan/inventoryframework/context/IFSlotClickContext;)V + public final fun onClick (Lme/devnatan/inventoryframework/context/SlotClickContext;)V + public final fun onClose (Lme/devnatan/inventoryframework/context/CloseContext;)V + public synthetic fun onClose (Lme/devnatan/inventoryframework/context/IFCloseContext;)V + public synthetic fun onFirstRender (Lme/devnatan/inventoryframework/context/IFRenderContext;)V + public final fun onFirstRender (Lme/devnatan/inventoryframework/context/RenderContext;)V + public final fun onInit (Lme/devnatan/inventoryframework/ViewConfigBuilder;)V + public synthetic fun onOpen (Lme/devnatan/inventoryframework/context/IFOpenContext;)V + public final fun onOpen (Lme/devnatan/inventoryframework/context/OpenContext;)V + public final fun onUpdate (Lme/devnatan/inventoryframework/context/Context;)V + public synthetic fun onUpdate (Lme/devnatan/inventoryframework/context/IFContext;)V + protected fun onViewClick (Lme/devnatan/inventoryframework/context/SlotClickContext;)V + protected fun onViewClose (Lme/devnatan/inventoryframework/context/CloseContext;)V + protected fun onViewInit (Lme/devnatan/inventoryframework/ViewConfigBuilder;)V + protected fun onViewOpen (Lme/devnatan/inventoryframework/context/OpenContext;)V + protected fun onViewRender (Lme/devnatan/inventoryframework/context/RenderContext;)V + protected fun onViewUpdate (Lme/devnatan/inventoryframework/context/Context;)V + protected final fun updateHeader (Lme/devnatan/inventoryframework/context/Context;Ljava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;)V + protected final fun updateHeader (Lme/devnatan/inventoryframework/context/Context;Lnet/kyori/adventure/text/Component;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;)V + public static synthetic fun updateHeader$default (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView;Lme/devnatan/inventoryframework/context/Context;Ljava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ILjava/lang/Object;)V + public static synthetic fun updateHeader$default (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView;Lme/devnatan/inventoryframework/context/Context;Lnet/kyori/adventure/text/Component;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ILjava/lang/Object;)V +} + +public abstract class dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext { + public fun ()V + public final fun getContainerDefaults ()Lkotlin/jvm/functions/Function2; + public final fun getOnClick ()Lkotlin/jvm/functions/Function2; + public final fun getOnClose ()Lkotlin/jvm/functions/Function2; + public final fun getOnFirstRender ()Lkotlin/jvm/functions/Function2; + public final fun getOnInit ()Lkotlin/jvm/functions/Function2; + public final fun getOnOpen ()Lkotlin/jvm/functions/Function2; + public final fun getOnUpdate ()Lkotlin/jvm/functions/Function2; + public final fun getStateRegistry ()Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateRegistry; + public final fun setContainerDefaults (Lkotlin/jvm/functions/Function2;)V + public final fun setOnClick (Lkotlin/jvm/functions/Function2;)V + public final fun setOnClose (Lkotlin/jvm/functions/Function2;)V + public final fun setOnFirstRender (Lkotlin/jvm/functions/Function2;)V + public final fun setOnInit (Lkotlin/jvm/functions/Function2;)V + public final fun setOnOpen (Lkotlin/jvm/functions/Function2;)V + public final fun setOnUpdate (Lkotlin/jvm/functions/Function2;)V +} + +public abstract class dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef { + public field view Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView; + public fun ()V + public final fun getRegisteredView ()Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView; + public final fun getView ()Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView; + public final fun setView (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView;)V +} + +public abstract interface annotation class dev/slne/surf/api/minestom/inventory/framework/view/InventoryFrameworkDSL : java/lang/annotation/Annotation { +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewContext : dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext { + public fun ()V + public final fun getLayoutTarget ()Ljava/lang/Character; + public final fun getPaginationStateBuilder ()Lkotlin/jvm/functions/Function1; + public final fun getRegisteredLayoutTarget ()C + public final fun getRegisteredPaginationStateBuilder ()Lkotlin/jvm/functions/Function1; + public final fun getSettings ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; + public final fun setLayoutTarget (Ljava/lang/Character;)V + public final fun setPaginationStateBuilder (Lkotlin/jvm/functions/Function1;)V + public final fun setSettings (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings;)V +} + +public abstract class dev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewDSLImpl : dev/slne/surf/api/minestom/inventory/framework/view/pagination/AbstractPaginatedSurfView { + public fun (Ljava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewContext;Ldev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewRef;)V + protected fun applyContainerDefaults (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;)V + protected fun createPagination ()Lme/devnatan/inventoryframework/component/PaginationStateBuilder; + protected fun getLayoutTarget ()C + public fun getSettings ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; + public synthetic fun getSettings ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings; + protected fun onPaginatedClick (Lme/devnatan/inventoryframework/context/SlotClickContext;)V + protected fun onPaginatedClose (Lme/devnatan/inventoryframework/context/CloseContext;)V + protected fun onPaginatedInit (Lme/devnatan/inventoryframework/ViewConfigBuilder;)V + protected fun onPaginatedOpen (Lme/devnatan/inventoryframework/context/OpenContext;)V + protected fun onPaginatedRender (Lme/devnatan/inventoryframework/context/RenderContext;)V + protected fun onPaginatedUpdate (Lme/devnatan/inventoryframework/context/Context;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewRef : dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef { + public fun ()V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/SurfViewBuilderKt { + public static final fun paginatedSurfView (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/AbstractPaginatedSurfView; + public static final fun surfView (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/SurfViewContext : dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext { + public fun ()V + public final fun getSettings ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings; + public final fun setSettings (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings;)V +} + +public abstract class dev/slne/surf/api/minestom/inventory/framework/view/SurfViewDSLImpl : dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView { + public fun (Ljava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/SurfViewContext;Ldev/slne/surf/api/minestom/inventory/framework/view/SurfViewRef;)V + protected fun containerDefaults (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;)V + public fun getSettings ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings; + protected fun onViewClick (Lme/devnatan/inventoryframework/context/SlotClickContext;)V + protected fun onViewClose (Lme/devnatan/inventoryframework/context/CloseContext;)V + protected fun onViewInit (Lme/devnatan/inventoryframework/ViewConfigBuilder;)V + protected fun onViewOpen (Lme/devnatan/inventoryframework/context/OpenContext;)V + protected fun onViewRender (Lme/devnatan/inventoryframework/context/RenderContext;)V + protected fun onViewUpdate (Lme/devnatan/inventoryframework/context/Context;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/SurfViewLifecycleDslKt { + public static final fun containerDefaults (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function2;)V + public static final fun layoutTarget (Ldev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewContext;C)V + public static final fun modifyContainer (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;Lme/devnatan/inventoryframework/context/Context;Lkotlin/jvm/functions/Function1;)V + public static final fun onClick (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function2;)V + public static final fun onClose (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function2;)V + public static final fun onFirstRender (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function2;)V + public static final fun onInit (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function2;)V + public static final fun onOpen (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function2;)V + public static final fun onUpdate (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function2;)V + public static final fun settings (Ldev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewContext;Lkotlin/jvm/functions/Function1;)V + public static final fun settings (Ldev/slne/surf/api/minestom/inventory/framework/view/SurfViewContext;Lkotlin/jvm/functions/Function1;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/SurfViewRef : dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef { + public fun ()V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/SurfViewRefKt { + public static final fun getView (Ldev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewRef;)Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/AbstractPaginatedSurfView; + public static final fun getView (Ldev/slne/surf/api/minestom/inventory/framework/view/SurfViewRef;)Ldev/slne/surf/api/minestom/inventory/framework/view/SurfViewDSLImpl; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/ViewNavigationHistoryKt { + public static final fun clearViewNavigationHistory (Lnet/minestom/server/entity/Player;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonBuilder { + public final fun onChange (Lkotlin/jvm/functions/Function3;)V + public final fun onCloseChanged (Lkotlin/jvm/functions/Function3;)V + public final fun reverseOnRightClick (Z)V + public static synthetic fun reverseOnRightClick$default (Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonBuilder;ZILjava/lang/Object;)V + public final fun state (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun state$default (Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonBuilder;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonDslKt { + public static final fun computedStatefulButton (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle; + public static final fun computedToggleButton (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle; + public static final fun computedTripleButton (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle; + public static final fun statefulButton (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle; + public static final fun toggleButton (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;ZLkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle; + public static synthetic fun toggleButton$default (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle; + public static final fun tripleButton (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle { + public final fun get (Lme/devnatan/inventoryframework/context/Context;)Ljava/lang/Object; + public final fun hasChanged (Lme/devnatan/inventoryframework/context/Context;)Z + public final fun initial (Lme/devnatan/inventoryframework/context/Context;)Ljava/lang/Object; + public final fun set (Lme/devnatan/inventoryframework/context/Context;Ljava/lang/Object;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandleKt { + public static final fun button (Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder;Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle;)Lme/devnatan/inventoryframework/component/MinestomItemComponentBuilder; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonStateScope { + public final fun getValue ()Ljava/lang/Object; + public final fun item (Lnet/minestom/server/item/Material;ILkotlin/jvm/functions/Function1;)V + public static synthetic fun item$default (Ldev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonStateScope;Lnet/minestom/server/item/Material;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public final fun onEnter (Lkotlin/jvm/functions/Function2;)V + public final fun renderItem (Lkotlin/jvm/functions/Function1;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/button/ViewToggleButtonBuilder { + public final fun onCloseChanged (Lkotlin/jvm/functions/Function3;)V + public final fun onToggle (Lkotlin/jvm/functions/Function2;)V + public final fun whenOff (Lkotlin/jvm/functions/Function1;)V + public final fun whenOn (Lkotlin/jvm/functions/Function1;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/container/ViewContainer { + public fun ()V + public final fun addChild (Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent;)V + public final fun getChildren ()Ljava/util/List; + public final fun getLastRenderedTitle ()Lnet/kyori/adventure/text/Component; + public final fun hasComponentOfType (Ljava/lang/Class;)Z + public final fun removeChild (Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent;)V + public final fun removeChildren (Lkotlin/jvm/functions/Function1;)V + public final fun removeChildrenOfType (Ljava/lang/Class;)V + public final fun render ()Lnet/kyori/adventure/text/Component; +} + +public abstract interface class dev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent { + public abstract fun equals (Ljava/lang/Object;)Z + public fun getHasExactWidth ()Z + public abstract fun getPositionalShift ()I + public abstract fun getTextureWidth ()I + public abstract fun hashCode ()I + public abstract fun renderComponent (Ldev/slne/surf/api/core/messages/builder/SurfComponentBuilder;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent$DefaultImpls { + public static fun getHasExactWidth (Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent;)Z +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewBlockCellComponent : dev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent { + public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewBlockCellComponent$Companion; + public synthetic fun (II)V + public fun (IILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V + public synthetic fun (IILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getColumn ()I + public fun getHasExactWidth ()Z + public fun getPositionalShift ()I + public final fun getRow ()I + public fun getTextureWidth ()I + public fun hashCode ()I + public fun renderComponent (Ldev/slne/surf/api/core/messages/builder/SurfComponentBuilder;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewBlockCellComponent$Companion { + public final fun forSlot (ILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewBlockCellComponent; + public static synthetic fun forSlot$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewBlockCellComponent$Companion;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewBlockCellComponent; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerBackHintComponent : dev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent { + public static final field INSTANCE Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerBackHintComponent; + public fun equals (Ljava/lang/Object;)Z + public fun getHasExactWidth ()Z + public fun getPositionalShift ()I + public fun getTextureWidth ()I + public fun hashCode ()I + public fun renderComponent (Ldev/slne/surf/api/core/messages/builder/SurfComponentBuilder;)V + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerRowTextComponent : dev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent { + public fun (ILjava/lang/String;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;)V + public synthetic fun (ILjava/lang/String;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (ILnet/kyori/adventure/text/Component;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;)V + public synthetic fun (ILnet/kyori/adventure/text/Component;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getAlignment ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; + public final fun getColumns ()Lkotlin/ranges/IntRange; + public fun getHasExactWidth ()Z + public fun getPositionalShift ()I + public final fun getRow ()I + public final fun getText ()Lnet/kyori/adventure/text/Component; + public fun getTextureWidth ()I + public fun hashCode ()I + public fun renderComponent (Ldev/slne/surf/api/core/messages/builder/SurfComponentBuilder;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerTitleComponent : dev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent { + public synthetic fun (Ljava/lang/String;Lnet/kyori/adventure/key/Key;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lit/unimi/dsi/fastutil/ints/Int2IntMap;)V + public synthetic fun (Ljava/lang/String;Lnet/kyori/adventure/key/Key;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lit/unimi/dsi/fastutil/ints/Int2IntMap;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;)V + public synthetic fun (Ljava/lang/String;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;)V + public synthetic fun (Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public fun getHasExactWidth ()Z + public fun getPositionalShift ()I + public final fun getTextAlignment ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; + public fun getTextureWidth ()I + public final fun getTitle ()Lnet/kyori/adventure/text/Component; + public fun hashCode ()I + public fun renderComponent (Ldev/slne/surf/api/core/messages/builder/SurfComponentBuilder;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerDSLKt { + public static final fun addChild (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent;)V + public static final synthetic fun backHint (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;)V + public static final fun blockAllSlots (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V + public static final fun blockAllSlots (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;Lit/unimi/dsi/fastutil/ints/IntCollection;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V + public static synthetic fun blockAllSlots$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)V + public static synthetic fun blockAllSlots$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;Lit/unimi/dsi/fastutil/ints/IntCollection;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)V + public static final synthetic fun blockCell (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;II)V + public static final fun blockCell (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;IILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V + public static synthetic fun blockCell$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;IILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)V + public static final synthetic fun blockColumn (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;)V + public static final fun blockColumn (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V + public static final synthetic fun blockColumn (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;I[I)V + public static final fun blockColumn (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;I[IILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V + public static synthetic fun blockColumn$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)V + public static synthetic fun blockColumn$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;ILjava/lang/Object;)V + public static synthetic fun blockColumn$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;I[IILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)V + public static final synthetic fun blockRow (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;)V + public static final fun blockRow (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V + public static final synthetic fun blockRow (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;I[I)V + public static final fun blockRow (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;I[ILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V + public static synthetic fun blockRow$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;ILjava/lang/Object;)V + public static synthetic fun blockRow$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILit/unimi/dsi/fastutil/ints/IntCollection;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)V + public static synthetic fun blockRow$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;I[ILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)V + public static final fun blockSlot (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V + public static synthetic fun blockSlot$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ILjava/lang/Object;)V + public static final fun clearRowText (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;I)V + public static final fun clearRowTexts (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;)V + public static final fun hasComponentOfType (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ljava/lang/Class;)Z + public static final fun header (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/jvm/functions/Function1;)V + public static final synthetic fun header (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;Ljava/lang/String;)V + public static final fun header (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;Ljava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;)V + public static final fun header (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;Lnet/kyori/adventure/text/Component;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;)V + public static synthetic fun header$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun header$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;Ljava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ILjava/lang/Object;)V + public static synthetic fun header$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;Lnet/kyori/adventure/text/Component;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ILjava/lang/Object;)V + public static final fun removeChild (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent;)V + public static final fun removeChildrenOfType (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ljava/lang/Class;)V + public static final fun rowText (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILjava/lang/String;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;)V + public static final fun rowText (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILnet/kyori/adventure/text/Component;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;)V + public static final fun rowText (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function1;)V + public static final fun rowText (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;ILjava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;)V + public static final fun rowText (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;ILnet/kyori/adventure/text/Component;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;)V + public static synthetic fun rowText$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILjava/lang/String;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;ILjava/lang/Object;)V + public static synthetic fun rowText$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;ILnet/kyori/adventure/text/Component;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;ILjava/lang/Object;)V + public static synthetic fun rowText$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;ILdev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun rowText$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;ILjava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;ILjava/lang/Object;)V + public static synthetic fun rowText$default (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;ILnet/kyori/adventure/text/Component;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lkotlin/ranges/IntRange;ILjava/lang/Object;)V + public static final fun rowTextAt (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;IILjava/lang/String;)V + public static final fun rowTextAt (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewRef;IILnet/kyori/adventure/text/Component;)V + public static final fun unblockAllSlots (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;)V + public static final fun unblockCell (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;II)V + public static final fun unblockSlot (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;I)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext { + public fun equals (Ljava/lang/Object;)Z + public final fun getContainer ()Ldev/slne/surf/api/minestom/inventory/framework/view/container/ViewContainer; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIcon { + public fun (Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType;Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor;)V + public final fun build (Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/item/ItemStack; + public final fun getColor ()Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor; + public final fun getIcon ()Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public final fun getItemModel ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor : java/lang/Enum { + public static final field BLUE Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor; + public static final field GREEN Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor; + public static final field RED Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor; + public static final field WHITE Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor; + public static final field YELLOW Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor; + public final fun getConfigName ()Ljava/lang/String; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor; + public static fun values ()[Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconKt { + public static final fun viewIcon (Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType;Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor;Lkotlin/jvm/functions/Function1;)Lnet/minestom/server/item/ItemStack; + public static synthetic fun viewIcon$default (Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType;Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/minestom/server/item/ItemStack; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType : java/lang/Enum { + public static final field ARROW_DOWN Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field ARROW_LEFT Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field ARROW_NEXT Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field ARROW_PREVIOUS Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field ARROW_RIGHT Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field ARROW_UP Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field BELL Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field CHECK Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field CIRCLE Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field COG Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field CROSS Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field EQUALS Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field EXCLAMATION_MARK Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field HOME Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field MENU Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field MINUS Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field PLUS Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field QUESTION Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field QUESTION_MARK Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field RELOAD Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field SEARCH Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static final field USERS Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public final fun getConfigName ()Ljava/lang/String; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; + public static fun values ()[Ldev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType; +} + +public abstract class dev/slne/surf/api/minestom/inventory/framework/view/pagination/AbstractPaginatedSurfView : dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView { + public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/AbstractPaginatedSurfView$Companion; + public fun (Ljava/lang/String;)V + protected fun applyContainerDefaults (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;)V + protected final fun containerDefaults (Ldev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext;)V + protected abstract fun createPagination ()Lme/devnatan/inventoryframework/component/PaginationStateBuilder; + protected abstract fun getLayoutTarget ()C + public fun getSettings ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; + public synthetic fun getSettings ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings; + protected fun onPaginatedClick (Lme/devnatan/inventoryframework/context/SlotClickContext;)V + protected fun onPaginatedClose (Lme/devnatan/inventoryframework/context/CloseContext;)V + protected fun onPaginatedInit (Lme/devnatan/inventoryframework/ViewConfigBuilder;)V + protected fun onPaginatedOpen (Lme/devnatan/inventoryframework/context/OpenContext;)V + protected fun onPaginatedRender (Lme/devnatan/inventoryframework/context/RenderContext;)V + protected fun onPaginatedUpdate (Lme/devnatan/inventoryframework/context/Context;)V + protected final fun onViewClick (Lme/devnatan/inventoryframework/context/SlotClickContext;)V + protected final fun onViewClose (Lme/devnatan/inventoryframework/context/CloseContext;)V + protected final fun onViewInit (Lme/devnatan/inventoryframework/ViewConfigBuilder;)V + protected final fun onViewOpen (Lme/devnatan/inventoryframework/context/OpenContext;)V + protected final fun onViewRender (Lme/devnatan/inventoryframework/context/RenderContext;)V + protected final fun onViewUpdate (Lme/devnatan/inventoryframework/context/Context;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/AbstractPaginatedSurfView$Companion { +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationDslBuilder { + public fun ()V + public final fun applyTo (Lme/devnatan/inventoryframework/component/PaginationStateBuilder;)V + public final fun asyncSource (Lkotlin/jvm/functions/Function1;)V + public final fun computedSource (Lkotlin/jvm/functions/Function1;)V + public final fun elementFactory (Lkotlin/jvm/functions/Function4;)V + public final fun getElementFactory ()Lme/devnatan/inventoryframework/component/PaginationValueConsumer; + public final fun getOrientation ()Lme/devnatan/inventoryframework/component/Pagination$Orientation; + public final fun getPageSwitchHandler ()Ljava/util/function/BiConsumer; + public final fun getSimpleItemFactory ()Ljava/util/function/BiConsumer; + public final fun getSourceType ()Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType; + public final fun horizontal ()V + public final fun itemFactory (Lkotlin/jvm/functions/Function2;)V + public final fun lazyAsyncSource (Lkotlin/jvm/functions/Function1;)V + public final fun lazySource (Lkotlin/jvm/functions/Function1;)V + public final fun lazySuspendSource (Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function3;)V + public final fun onPageSwitch (Lkotlin/jvm/functions/Function2;)V + public final fun orientation (Lme/devnatan/inventoryframework/component/Pagination$Orientation;)V + public final fun setElementFactory (Lme/devnatan/inventoryframework/component/PaginationValueConsumer;)V + public final fun setOrientation (Lme/devnatan/inventoryframework/component/Pagination$Orientation;)V + public final fun setPageSwitchHandler (Ljava/util/function/BiConsumer;)V + public final fun setSimpleItemFactory (Ljava/util/function/BiConsumer;)V + public final fun setSourceType (Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType;)V + public final fun source (Ljava/lang/Iterable;)V + public final fun source (Lkotlin/jvm/functions/Function0;)V + public final fun suspendSource (Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function3;)V + public final fun vertical ()V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationDslKt { + public static final fun pagination (Ldev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewContext;Lkotlin/jvm/functions/Function1;)V +} + +public abstract interface class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator { + public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator$Companion; + public abstract fun render (II)Lnet/kyori/adventure/text/Component; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator$Companion { + public final fun getDefault ()Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator; +} + +public abstract interface class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType { +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType$Computed : dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType { + public fun (Lkotlin/jvm/functions/Function1;)V + public final fun getProvider ()Lkotlin/jvm/functions/Function1; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType$ComputedAsync : dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType { + public fun (Lkotlin/jvm/functions/Function1;)V + public final fun getProvider ()Lkotlin/jvm/functions/Function1; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType$Lazy : dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType { + public fun (Lkotlin/jvm/functions/Function1;)V + public final fun getProvider ()Lkotlin/jvm/functions/Function1; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType$LazyAsync : dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType { + public fun (Lkotlin/jvm/functions/Function1;)V + public final fun getProvider ()Lkotlin/jvm/functions/Function1; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType$Static : dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType { + public fun (Lkotlin/jvm/functions/Function0;)V + public final fun getProvider ()Lkotlin/jvm/functions/Function0; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings : dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings { + public fun ()V + public fun (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ZLit/unimi/dsi/fastutil/ints/Int2ObjectMap;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator;Lnet/kyori/adventure/sound/Sound;)V + public synthetic fun (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ZLit/unimi/dsi/fastutil/ints/Int2ObjectMap;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator;Lnet/kyori/adventure/sound/Sound;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;)V + public synthetic fun (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lnet/kyori/adventure/key/Key; + public final fun component10 ()Z + public final fun component11 ()Z + public final fun component12 ()Z + public final fun component13 ()Z + public final fun component14 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; + public final fun component15 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; + public final fun component16 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition; + public final fun component17 ()Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator; + public final fun component18 ()Lnet/kyori/adventure/sound/Sound; + public final fun component2 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; + public final fun component3 ()Lnet/kyori/adventure/text/format/TextColor; + public final fun component4 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public final fun component5 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public final fun component6 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; + public final fun component7 ()Z + public final fun component8 ()Lit/unimi/dsi/fastutil/ints/Int2ObjectMap; + public final fun component9 ()Z + public final fun copy (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ZLit/unimi/dsi/fastutil/ints/Int2ObjectMap;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator;Lnet/kyori/adventure/sound/Sound;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; + public final fun copy (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ZLit/unimi/dsi/fastutil/ints/Int2ObjectMap;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator;Lnet/kyori/adventure/sound/Sound;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; + public fun equals (Ljava/lang/Object;)Z + public fun getBackgroundGlyph ()Z + public fun getCancelOnClick ()Z + public fun getCancelOnDrag ()Z + public fun getCancelOnDrop ()Z + public fun getCancelOnPickup ()Z + public fun getFont ()Lnet/kyori/adventure/key/Key; + public fun getHeaderFontMetrics ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public fun getHeaderGeometry ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; + public fun getHeaderTextAlignment ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; + public fun getHeaderTextColor ()Lnet/kyori/adventure/text/format/TextColor; + public fun getNavigateBackOnOutsideClick ()Z + public final fun getPaginationButtonPosition ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition; + public final fun getPaginationContentRowCount ()I + public final fun getPaginationEmptyRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; + public final fun getPaginationPageIndicator ()Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator; + public final fun getPaginationSwitchSound ()Lnet/kyori/adventure/sound/Sound; + public final fun getPaginationViewRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; + public fun getRowFontMetrics ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public fun getRowFonts ()Lit/unimi/dsi/fastutil/ints/Int2ObjectMap; + public fun getRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; + public fun hashCode ()I + public fun rowFont (I)Lnet/kyori/adventure/key/Key; + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition : java/lang/Enum { + public static final field BOTTOM Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition; + public static final field TOP Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition; + public static fun values ()[Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows : java/lang/Enum { + public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows$Companion; + public static final field NONE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; + public static final field ONE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; + public static final field THREE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; + public static final field TWO Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getRows ()I + public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; + public static fun values ()[Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows$Companion { + public final fun byRows (I)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; + public final fun byRowsOrNull (I)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows : java/lang/Enum { + public static final field FIVE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; + public static final field FOUR Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; + public static final field ONE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; + public static final field THREE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; + public static final field TWO Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; + public final fun getActualRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; + public final fun getContentRows ()I + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final synthetic fun getPaginationContentRows ()Lkotlin/ranges/IntRange; + public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; + public static fun values ()[Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings : dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings { + public fun ()V + public fun (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ZLit/unimi/dsi/fastutil/ints/Int2ObjectMap;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;)V + public synthetic fun (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ZLit/unimi/dsi/fastutil/ints/Int2ObjectMap;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;)V + public synthetic fun (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lnet/kyori/adventure/key/Key; + public final fun component10 ()Z + public final fun component11 ()Z + public final fun component12 ()Z + public final fun component13 ()Z + public final fun component14 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; + public final fun component2 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; + public final fun component3 ()Lnet/kyori/adventure/text/format/TextColor; + public final fun component4 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public final fun component5 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public final fun component6 ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; + public final fun component7 ()Z + public final fun component8 ()Lit/unimi/dsi/fastutil/ints/Int2ObjectMap; + public final fun component9 ()Z + public final fun copy (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ZLit/unimi/dsi/fastutil/ints/Int2ObjectMap;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings; + public final fun copy (Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;Lnet/kyori/adventure/text/format/TextColor;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;ZLit/unimi/dsi/fastutil/ints/Int2ObjectMap;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings;Lnet/kyori/adventure/key/Key;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;ZZZZZLdev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings; + public fun equals (Ljava/lang/Object;)Z + public fun getBackgroundGlyph ()Z + public fun getCancelOnClick ()Z + public fun getCancelOnDrag ()Z + public fun getCancelOnDrop ()Z + public fun getCancelOnPickup ()Z + public fun getFont ()Lnet/kyori/adventure/key/Key; + public fun getHeaderFontMetrics ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public fun getHeaderGeometry ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; + public fun getHeaderTextAlignment ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; + public fun getHeaderTextColor ()Lnet/kyori/adventure/text/format/TextColor; + public fun getNavigateBackOnOutsideClick ()Z + public fun getRowFontMetrics ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public fun getRowFonts ()Lit/unimi/dsi/fastutil/ints/Int2ObjectMap; + public fun getRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; + public fun hashCode ()I + public fun rowFont (I)Lnet/kyori/adventure/key/Key; + public fun toString ()Ljava/lang/String; +} + +public abstract interface class dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings { + public abstract fun getBackgroundGlyph ()Z + public abstract fun getCancelOnClick ()Z + public abstract fun getCancelOnDrag ()Z + public abstract fun getCancelOnDrop ()Z + public abstract fun getCancelOnPickup ()Z + public abstract fun getFont ()Lnet/kyori/adventure/key/Key; + public abstract fun getHeaderFontMetrics ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public abstract fun getHeaderGeometry ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; + public abstract fun getHeaderTextAlignment ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; + public abstract fun getHeaderTextColor ()Lnet/kyori/adventure/text/format/TextColor; + public abstract fun getNavigateBackOnOutsideClick ()Z + public abstract fun getRowFontMetrics ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public abstract fun getRowFonts ()Lit/unimi/dsi/fastutil/ints/Int2ObjectMap; + public abstract fun getRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; + public fun rowFont (I)Lnet/kyori/adventure/key/Key; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings$DefaultImpls { + public static fun rowFont (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings;I)Lnet/kyori/adventure/key/Key; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettingsDefaults { + public static final field DEFAULT_BACKGROUND_GLYPH Z + public static final field DEFAULT_CANCEL_ON_CLICK Z + public static final field DEFAULT_CANCEL_ON_DRAG Z + public static final field DEFAULT_CANCEL_ON_DROP Z + public static final field DEFAULT_CANCEL_ON_PICKUP Z + public static final field DEFAULT_NAVIGATE_BACK_ON_CLOSE Z + public static final field INSTANCE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettingsDefaults; + public final fun getASCENDED_HEADER_FONT ()Lnet/kyori/adventure/key/Key; + public final fun getDEFAULT_HEADER_ALIGNMENT ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; + public final fun getDEFAULT_HEADER_FONT ()Lnet/kyori/adventure/key/Key; + public final fun getDEFAULT_HEADER_FONT_METRICS ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public final fun getDEFAULT_HEADER_GEOMETRY ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; + public final fun getDEFAULT_HEADER_TEXT_COLOR ()Lnet/kyori/adventure/text/format/TextColor; + public final fun getDEFAULT_MENU_FONT ()Lnet/kyori/adventure/key/Key; + public final fun getDEFAULT_PAGINATION_BUTTON_POSITION ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition; + public final fun getDEFAULT_PAGINATION_EMPTY_ROWS ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; + public final fun getDEFAULT_PAGINATION_PAGE_INDICATOR ()Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator; + public final fun getDEFAULT_PAGINATION_SWITCH_SOUND ()Lnet/kyori/adventure/sound/Sound; + public final fun getDEFAULT_PAGINATION_VIEW_ROWS ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; + public final fun getDEFAULT_ROW_FONTS ()Lit/unimi/dsi/fastutil/ints/Int2ObjectMap; + public final fun getDEFAULT_ROW_FONT_METRICS ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public final fun getDEFAULT_ROW_TEXT_ALIGNMENT ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; + public final fun getDEFAULT_ROW_TEXT_COLUMNS ()Lkotlin/ranges/IntRange; + public final fun getDEFAULT_VIEW_ROWS ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; + public final fun rowFont (I)Lnet/kyori/adventure/key/Key; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics { + public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics$Companion; + public static final field MENU_CHAR_SIZE I + public static final field MENU_CHAR_SPACING I + public static final field MENU_SPACE_SIZE I + public fun ()V + public fun (IIZZLit/unimi/dsi/fastutil/ints/Int2IntMap;)V + public synthetic fun (IIZZLit/unimi/dsi/fastutil/ints/Int2IntMap;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun advance (IZ)I + public static synthetic fun advance$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;IZILjava/lang/Object;)I + public final fun component1 ()I + public final fun component2 ()I + public final fun component3 ()Z + public final fun component4 ()Z + public final fun component5 ()Lit/unimi/dsi/fastutil/ints/Int2IntMap; + public final fun copy (IIZZLit/unimi/dsi/fastutil/ints/Int2IntMap;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;IIZZLit/unimi/dsi/fastutil/ints/Int2IntMap;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public fun equals (Ljava/lang/Object;)Z + public final fun getCharSize ()I + public final fun getCharSpacing ()I + public final fun getCharWidths ()Lit/unimi/dsi/fastutil/ints/Int2IntMap; + public final fun getSpaceAsShift ()Z + public final fun getSpaceShift ()Ljava/lang/Integer; + public final fun getUppercase ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics$Companion { + public final fun getSURF_MENU ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public final fun getVANILLA ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public final fun getVANILLA_ROW ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry { + public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry$Companion; + public static final field DEFAULT_ROW_TEXT_PADDING I + public static final field DEFAULT_SLOT_GLYPH_INSET I + public static final field DEFAULT_SLOT_ORIGIN_X I + public static final field DEFAULT_TITLE_LEFT_SHIFT I + public static final field DEFAULT_TITLE_PADDING I + public static final field DEFAULT_TITLE_WIDTH I + public fun ()V + public fun (IIIIIII)V + public synthetic fun (IIIIIIIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun columnShift (I)I + public final fun columnSpanOptions (Lkotlin/ranges/IntRange;Lit/unimi/dsi/fastutil/ints/Int2IntMap;II)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions; + public static synthetic fun columnSpanOptions$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lkotlin/ranges/IntRange;Lit/unimi/dsi/fastutil/ints/Int2IntMap;IIILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions; + public final fun component1 ()I + public final fun component2 ()I + public final fun component3 ()I + public final fun component4 ()I + public final fun component5 ()I + public final fun component6 ()I + public final fun component7 ()I + public final fun copy (IIIIIII)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;IIIIIIIILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; + public fun equals (Ljava/lang/Object;)Z + public final fun getRowTextPadding ()I + public final fun getSlotAreaWidth ()I + public final fun getSlotGlyphInset ()I + public final fun getSlotOriginX ()I + public final fun getSlotSize ()I + public final fun getTitleLeftShift ()I + public final fun getTitlePadding ()I + public final fun getTitleWidth ()I + public fun hashCode ()I + public final fun slotGlyphShift (I)I + public final fun titleOptions (Lit/unimi/dsi/fastutil/ints/Int2IntMap;II)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions; + public static synthetic fun titleOptions$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;Lit/unimi/dsi/fastutil/ints/Int2IntMap;IIILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions; + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry$Companion { + public final fun getDEFAULT ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows : java/lang/Enum { + public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows$Companion; + public static final field FIVE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; + public static final field FOUR Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; + public static final field ONE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; + public static final field SIX Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; + public static final field THREE Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; + public static final field TWO Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getGlyph ()C + public final fun getRows ()I + public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; + public static fun values ()[Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows$Companion { + public final fun byRows (I)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; + public final fun byRowsOrNull (I)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; +} + +public abstract interface annotation class dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows$Companion$Rows : java/lang/annotation/Annotation { +} + +public abstract class dev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment : java/lang/Enum { + public static final field CENTER Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; + public static final field Companion Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment$Companion; + public static final field LEFT Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; + public static final field RIGHT Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; + public abstract fun calculateShift (ILdev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions;)I + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; + public static fun values ()[Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment$Companion { + public final fun calculateTextWidth (Ljava/lang/String;Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions;)I +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions { + public fun (IIIIILit/unimi/dsi/fastutil/ints/Int2IntMap;)V + public synthetic fun (IIIIILit/unimi/dsi/fastutil/ints/Int2IntMap;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()I + public final fun component2 ()I + public final fun component3 ()I + public final fun component4 ()I + public final fun component5 ()I + public final fun component6 ()Lit/unimi/dsi/fastutil/ints/Int2IntMap; + public final fun copy (IIIIILit/unimi/dsi/fastutil/ints/Int2IntMap;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions;IIIIILit/unimi/dsi/fastutil/ints/Int2IntMap;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions; + public fun equals (Ljava/lang/Object;)Z + public final fun getCharSize ()I + public final fun getCharSpacing ()I + public final fun getCharWidths ()Lit/unimi/dsi/fastutil/ints/Int2IntMap; + public final fun getContainerWidth ()I + public final fun getLeftShift ()I + public final fun getPadding ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/PaginatedViewSettingsBuilder : dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder { + public fun ()V + public fun build ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; + public synthetic fun build ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings; + public final fun getPaginationButtonPosition ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition; + public final fun getPaginationEmptyRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows; + public final fun getPaginationPageIndicator ()Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator; + public final fun getPaginationSwitchSound ()Lnet/kyori/adventure/sound/Sound; + public final fun getPaginationViewRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows; + public final fun noPaginationEmptyRows ()V + public final fun noPaginationPageIndicator ()V + public final fun noPaginationSwitchSound ()V + public final fun paginationButtonPosition (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition;)V + public final fun paginationButtonsAtBottom ()V + public final fun paginationButtonsAtTop ()V + public final fun paginationEmptyRows (I)V + public final fun paginationEmptyRows (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows;)V + public final fun paginationPageIndicator (Ldev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator;)V + public final fun paginationSwitchSound (Lnet/kyori/adventure/sound/Sound;)V + public final fun paginationViewRows (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/PaginatedViewSettingsBuilderKt { + public static final fun paginatedViewSettings (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SimpleViewSettingsBuilder : dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder { + public fun ()V + public fun build ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings; + public synthetic fun build ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings; + public final fun getRows ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows; + public final fun rows (I)V + public final fun rows (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SimpleViewSettingsBuilderKt { + public static final fun simpleViewSettings (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings; +} + +public abstract class dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder { + public final fun backgroundGlyph (Z)V + public static synthetic fun backgroundGlyph$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder;ZILjava/lang/Object;)V + public abstract fun build ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings; + public final fun cancelAllInteractions ()V + public final fun cancelOnClick (Z)V + public static synthetic fun cancelOnClick$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder;ZILjava/lang/Object;)V + public final fun cancelOnDrag (Z)V + public static synthetic fun cancelOnDrag$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder;ZILjava/lang/Object;)V + public final fun cancelOnDrop (Z)V + public static synthetic fun cancelOnDrop$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder;ZILjava/lang/Object;)V + public final fun cancelOnPickup (Z)V + public static synthetic fun cancelOnPickup$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder;ZILjava/lang/Object;)V + public final fun font (Lnet/kyori/adventure/key/Key;)V + public final fun getBackgroundGlyph ()Z + public final fun getCancelOnClick ()Z + public final fun getCancelOnDrag ()Z + public final fun getCancelOnDrop ()Z + public final fun getCancelOnPickup ()Z + public final fun getFont ()Lnet/kyori/adventure/key/Key; + public final fun getHeaderFontMetrics ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public final fun getHeaderGeometry ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry; + public final fun getHeaderTextAlignment ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment; + public final fun getHeaderTextColor ()Lnet/kyori/adventure/text/format/TextColor; + public final fun getNavigateBackOnOutsideClick ()Z + public final fun getRowFontMetrics ()Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics; + public final fun getRowFonts ()Lit/unimi/dsi/fastutil/ints/Int2ObjectMap; + public final fun headerFontMetrics (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;)V + public final fun headerGeometry (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry;)V + public final fun headerGeometry (Lkotlin/jvm/functions/Function1;)V + public final fun headerTextAlignment (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment;)V + public final fun headerTextColor (Lnet/kyori/adventure/text/format/TextColor;)V + public final fun navigateBackOnOutsideClick (Z)V + public static synthetic fun navigateBackOnOutsideClick$default (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder;ZILjava/lang/Object;)V + public final fun rowFont (ILnet/kyori/adventure/key/Key;)V + public final fun rowFontMetrics (Ldev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics;)V + public final fun rowFonts (Lit/unimi/dsi/fastutil/ints/Int2ObjectMap;)V +} + +public abstract interface class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState$Computed : dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { + public fun (Ljava/util/function/Function;)V + public final fun getComputation ()Ljava/util/function/Function; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState$ComputedSupplier : dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { + public fun (Ljava/util/function/Supplier;)V + public final fun getComputation ()Ljava/util/function/Supplier; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState$Immutable : dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { + public fun (Ljava/lang/Object;)V + public final fun getInitialValue ()Ljava/lang/Object; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState$Initial : dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { + public fun (Ljava/lang/String;)V + public final fun getKey ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState$Lazy : dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { + public fun (Ljava/util/function/Function;)V + public final fun getComputation ()Ljava/util/function/Function; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState$LazySupplier : dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { + public fun (Ljava/util/function/Supplier;)V + public final fun getComputation ()Ljava/util/function/Supplier; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState$Mutable : dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { + public fun (Ljava/lang/Object;)V + public final fun getInitialValue ()Ljava/lang/Object; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState$MutableInt : dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState { + public fun (I)V + public final fun getInitialValue ()I +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/state/StateDslKt { + public static final fun computedState (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function0;)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; + public static final fun computedState (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; + public static final fun initialState (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; + public static final fun initialState (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Ljava/lang/String;)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; + public static final fun lazyState (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function0;)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; + public static final fun lazyState (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; + public static final fun mutableState (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;I)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; + public static final fun mutableState (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Ljava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; + public static final fun state (Ldev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfViewContext;Ljava/lang/Object;)Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle { + public fun (Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateRegistry;I)V + public final fun getIndex ()I + public final fun getRegistry ()Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateRegistry; + public final fun resolve ()Ljava/lang/Object; +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/state/StateHandleKt { + public static final fun decrement (Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle;Lme/devnatan/inventoryframework/context/Context;)I + public static final fun get (Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle;Lme/devnatan/inventoryframework/context/Context;)I + public static final fun get (Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle;Lme/devnatan/inventoryframework/context/Context;)Ljava/lang/Object; + public static final fun getMutableState (Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle;Lme/devnatan/inventoryframework/context/Context;)Ljava/lang/Object; + public static final fun increment (Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle;Lme/devnatan/inventoryframework/context/Context;)I + public static final fun set (Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle;Lme/devnatan/inventoryframework/context/Context;I)V + public static final fun set (Ldev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle;Lme/devnatan/inventoryframework/context/Context;Ljava/lang/Object;)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/state/StateRegistry { + public fun ()V + public final fun get (I)Ljava/lang/Object; + public final fun getDeferredStates ()Ljava/util/List; + public final fun getNextIndex ()I + public final fun getResolvedStates ()Lit/unimi/dsi/fastutil/objects/ObjectArrayList; + public final fun register (Ldev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState;)I + public final fun setNextIndex (I)V +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/util/GlyphShift { + public static final synthetic fun box-impl (I)Ldev/slne/surf/api/minestom/inventory/framework/view/util/GlyphShift; + public static fun constructor-impl (I)I + public fun equals (Ljava/lang/Object;)Z + public static fun equals-impl (ILjava/lang/Object;)Z + public static final fun equals-impl0 (II)Z + public final fun getAmount ()I + public fun hashCode ()I + public static fun hashCode-impl (I)I + public static final fun renderGlyphs-impl (I)Ljava/lang/String; + public fun toString ()Ljava/lang/String; + public static fun toString-impl (I)Ljava/lang/String; + public final synthetic fun unbox-impl ()I +} + +public final class dev/slne/surf/api/minestom/inventory/framework/view/util/GlyphShiftKt { + public static final fun shift (I)Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/npc/MannequinNpc : codes/bed/minestom/npc/types/AbstractNpcEntity, net/minestom/server/inventory/EquipmentHandler { + public static final field Companion Ldev/slne/surf/api/minestom/npc/MannequinNpc$Companion; + public fun (Ljava/lang/String;Lnet/kyori/adventure/text/Component;Lnet/minestom/server/network/player/ResolvableProfile;DLnet/minestom/server/coordinate/Vec;Lnet/kyori/adventure/text/Component;Ljava/util/UUID;Lnet/minestom/server/item/ItemStack;Lnet/minestom/server/item/ItemStack;Lnet/minestom/server/item/ItemStack;Lnet/minestom/server/item/ItemStack;Lnet/minestom/server/item/ItemStack;Lnet/minestom/server/item/ItemStack;Lnet/minestom/server/item/ItemStack;Lnet/minestom/server/item/ItemStack;)V + public synthetic fun (Ljava/lang/String;Lnet/kyori/adventure/text/Component;Lnet/minestom/server/network/player/ResolvableProfile;DLnet/minestom/server/coordinate/Vec;Lnet/kyori/adventure/text/Component;Ljava/util/UUID;Lnet/minestom/server/item/ItemStack;Lnet/minestom/server/item/ItemStack;Lnet/minestom/server/item/ItemStack;Lnet/minestom/server/item/ItemStack;Lnet/minestom/server/item/ItemStack;Lnet/minestom/server/item/ItemStack;Lnet/minestom/server/item/ItemStack;Lnet/minestom/server/item/ItemStack;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getDisplayName ()Ljava/lang/String; + public fun getEquipment (Lnet/minestom/server/entity/EquipmentSlot;)Lnet/minestom/server/item/ItemStack; + public fun getKind ()Lcodes/bed/minestom/npc/api/NpcKind; + public fun setEquipment (Lnet/minestom/server/entity/EquipmentSlot;Lnet/minestom/server/item/ItemStack;)V + public fun spawn ()V + public final fun updateDisplayName (Lnet/kyori/adventure/text/Component;)V + public fun updateNewViewer (Lnet/minestom/server/entity/Player;)V +} + +public final class dev/slne/surf/api/minestom/npc/MannequinNpc$Companion { +} + +public final class dev/slne/surf/api/minestom/npc/MannequinNpcBuilder { + public fun (Ljava/lang/String;)V + public final fun getDescription ()Lnet/kyori/adventure/text/Component; + public final fun getDisplayName ()Lnet/kyori/adventure/text/Component; + public final fun getDisplayNameOffset ()Lnet/minestom/server/coordinate/Vec; + public final fun getProfile ()Lnet/minestom/server/network/player/ResolvableProfile; + public final fun getScale ()D + public final fun onInteract (Lkotlin/jvm/functions/Function1;)V + public final fun setDescription (Lnet/kyori/adventure/text/Component;)V + public final fun setDisplayName (Lnet/kyori/adventure/text/Component;)V + public final fun setDisplayNameOffset (Lnet/minestom/server/coordinate/Vec;)V + public final fun setEquipment (Lnet/minestom/server/entity/EquipmentSlot;Lnet/minestom/server/item/ItemStack;)V + public final fun setProfile (Lnet/minestom/server/network/player/ResolvableProfile;)V + public final fun setScale (D)V + public final fun spawn (Lnet/minestom/server/instance/Instance;Lnet/minestom/server/coordinate/Pos;)Ldev/slne/surf/api/minestom/npc/MannequinNpc; +} + +public final class dev/slne/surf/api/minestom/npc/MannequinNpcBuilderKt { + public static final fun mannequinNpc (Ljava/lang/String;Lnet/minestom/server/instance/Instance;Lnet/minestom/server/coordinate/Pos;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/npc/MannequinNpc; +} + +public final class dev/slne/surf/api/minestom/permission/MinestomPermissions { + public static final field INSTANCE Ldev/slne/surf/api/minestom/permission/MinestomPermissions; + public final fun getProvider ()Ldev/slne/surf/api/minestom/permission/PermissionProvider; + public final fun hasPermission (Lnet/minestom/server/command/CommandSender;Ljava/lang/String;)Z + public final fun installProvider (Ldev/slne/surf/api/minestom/permission/PermissionProvider;)V + public final fun permissionValue (Lnet/minestom/server/command/CommandSender;Ljava/lang/String;)Lnet/kyori/adventure/util/TriState; +} + +public final class dev/slne/surf/api/minestom/permission/MinestomPermissionsKt { + public static final fun hasPermission (Lnet/minestom/server/command/CommandSender;Ljava/lang/String;)Z + public static final fun permissionValue (Lnet/minestom/server/command/CommandSender;Ljava/lang/String;)Lnet/kyori/adventure/util/TriState; +} + +public abstract interface class dev/slne/surf/api/minestom/permission/PermissionProvider { + public abstract fun permissionValue (Lnet/minestom/server/entity/Player;Ljava/lang/String;)Lnet/kyori/adventure/util/TriState; +} + +public final class dev/slne/surf/api/minestom/player/PlayerLimit { + public static final field INSTANCE Ldev/slne/surf/api/minestom/player/PlayerLimit; + public final fun getMaxPlayers ()Ljava/lang/Integer; + public final fun getPlayerCount ()I + public final fun isFull ()Z + public final fun setMaxPlayers (Ljava/lang/Integer;)V +} + +public final class dev/slne/surf/api/minestom/player/event/AsyncPlayerCountEvent : net/minestom/server/event/trait/AsyncEvent, net/minestom/server/event/trait/PlayerEvent { + public fun (Lnet/minestom/server/entity/Player;ILjava/lang/Integer;Ldev/slne/surf/api/minestom/player/event/AsyncPlayerCountEvent$Result;Lnet/kyori/adventure/text/Component;)V + public final fun allow ()V + public final fun disallow (Ldev/slne/surf/api/minestom/player/event/AsyncPlayerCountEvent$Result;Lnet/kyori/adventure/text/Component;)V + public final fun getKickMessage ()Lnet/kyori/adventure/text/Component; + public final fun getMaxPlayers ()Ljava/lang/Integer; + public fun getPlayer ()Lnet/minestom/server/entity/Player; + public final fun getPlayerCount ()I + public final fun getResult ()Ldev/slne/surf/api/minestom/player/event/AsyncPlayerCountEvent$Result; + public final fun isAllowed ()Z + public final fun setKickMessage (Lnet/kyori/adventure/text/Component;)V + public final fun setResult (Ldev/slne/surf/api/minestom/player/event/AsyncPlayerCountEvent$Result;)V +} + +public final class dev/slne/surf/api/minestom/player/event/AsyncPlayerCountEvent$Result : java/lang/Enum { + public static final field ALLOWED Ldev/slne/surf/api/minestom/player/event/AsyncPlayerCountEvent$Result; + public static final field KICK_FULL Ldev/slne/surf/api/minestom/player/event/AsyncPlayerCountEvent$Result; + public static final field KICK_OTHER Ldev/slne/surf/api/minestom/player/event/AsyncPlayerCountEvent$Result; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/minestom/player/event/AsyncPlayerCountEvent$Result; + public static fun values ()[Ldev/slne/surf/api/minestom/player/event/AsyncPlayerCountEvent$Result; +} + +public final class dev/slne/surf/api/minestom/player/event/AsyncPlayerSpawnLocationEvent : net/minestom/server/event/trait/AsyncEvent, net/minestom/server/event/trait/PlayerEvent { + public fun (Lnet/minestom/server/entity/Player;Lnet/minestom/server/instance/Instance;Lnet/minestom/server/coordinate/Pos;)V + public final fun getInstance ()Lnet/minestom/server/instance/Instance; + public fun getPlayer ()Lnet/minestom/server/entity/Player; + public final fun getPosition ()Lnet/minestom/server/coordinate/Pos; + public final fun setInstance (Lnet/minestom/server/instance/Instance;)V + public final fun setPosition (Lnet/minestom/server/coordinate/Pos;)V +} + +public final class dev/slne/surf/api/minestom/player/event/PlayerJoinEvent : net/minestom/server/event/trait/PlayerInstanceEvent { + public fun (Lnet/minestom/server/entity/Player;Lnet/kyori/adventure/text/Component;)V + public final fun getJoinMessage ()Lnet/kyori/adventure/text/Component; + public fun getPlayer ()Lnet/minestom/server/entity/Player; + public final fun setJoinMessage (Lnet/kyori/adventure/text/Component;)V +} + +public final class dev/slne/surf/api/minestom/player/event/PlayerQuitEvent : net/minestom/server/event/trait/PlayerEvent { + public fun (Lnet/minestom/server/entity/Player;Lnet/kyori/adventure/text/Component;)V + public fun getPlayer ()Lnet/minestom/server/entity/Player; + public final fun getQuitMessage ()Lnet/kyori/adventure/text/Component; + public final fun setQuitMessage (Lnet/kyori/adventure/text/Component;)V +} + +public final class dev/slne/surf/api/minestom/player/event/PlayerToggleFlightEvent : net/minestom/server/event/trait/CancellableEvent, net/minestom/server/event/trait/PlayerInstanceEvent { + public fun (Lnet/minestom/server/entity/Player;Z)V + public fun getPlayer ()Lnet/minestom/server/entity/Player; + public fun isCancelled ()Z + public final fun isFlying ()Z + public fun setCancelled (Z)V +} + +public final class dev/slne/surf/api/minestom/plugin/PluginDependency { + public static final field Companion Ldev/slne/surf/api/minestom/plugin/PluginDependency$Companion; + public fun (Ljava/lang/String;Z)V + public synthetic fun (Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Z + public final fun copy (Ljava/lang/String;Z)Ldev/slne/surf/api/minestom/plugin/PluginDependency; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/plugin/PluginDependency;Ljava/lang/String;ZILjava/lang/Object;)Ldev/slne/surf/api/minestom/plugin/PluginDependency; + public fun equals (Ljava/lang/Object;)Z + public final fun getId ()Ljava/lang/String; + public final fun getOptional ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class dev/slne/surf/api/minestom/plugin/PluginDependency$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Ldev/slne/surf/api/minestom/plugin/PluginDependency$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ldev/slne/surf/api/minestom/plugin/PluginDependency; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Ldev/slne/surf/api/minestom/plugin/PluginDependency;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class dev/slne/surf/api/minestom/plugin/PluginDependency$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class dev/slne/surf/api/minestom/plugin/PluginManager { + public static final field Companion Ldev/slne/surf/api/minestom/plugin/PluginManager$Companion; + public abstract fun getPlugins ()Ljava/util/Collection; + public fun isLoaded (Ljava/lang/String;)Z + public abstract fun plugin (Ljava/lang/String;)Ldev/slne/surf/api/minestom/plugin/SurfMinestomPlugin; +} + +public final class dev/slne/surf/api/minestom/plugin/PluginManager$Companion { + public final fun getInstance ()Ldev/slne/surf/api/minestom/plugin/PluginManager; + public final fun install (Ldev/slne/surf/api/minestom/plugin/PluginManager;)V +} + +public final class dev/slne/surf/api/minestom/plugin/PluginManager$DefaultImpls { + public static fun isLoaded (Ldev/slne/surf/api/minestom/plugin/PluginManager;Ljava/lang/String;)Z +} + +public final class dev/slne/surf/api/minestom/plugin/PluginMeta { + public static final field CLASSPATH_DIRECTORY Ljava/lang/String; + public static final field CLASSPATH_INDEX Ljava/lang/String; + public static final field Companion Ldev/slne/surf/api/minestom/plugin/PluginMeta$Companion; + public static final field FILE_NAME Ljava/lang/String; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/util/List; + public final fun component8 ()Ljava/util/List; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;)Ldev/slne/surf/api/minestom/plugin/PluginMeta; + public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/plugin/PluginMeta;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/plugin/PluginMeta; + public fun equals (Ljava/lang/Object;)Z + public final fun getAuthors ()Ljava/util/List; + public final fun getDependencies ()Ljava/util/List; + public final fun getDescription ()Ljava/lang/String; + public final fun getId ()Ljava/lang/String; + public final fun getMain ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getUrl ()Ljava/lang/String; + public final fun getVersion ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class dev/slne/surf/api/minestom/plugin/PluginMeta$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Ldev/slne/surf/api/minestom/plugin/PluginMeta$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ldev/slne/surf/api/minestom/plugin/PluginMeta; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Ldev/slne/surf/api/minestom/plugin/PluginMeta;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class dev/slne/surf/api/minestom/plugin/PluginMeta$Companion { + public final fun getID_REGEX ()Lkotlin/text/Regex; + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract class dev/slne/surf/api/minestom/plugin/SurfMinestomPlugin { + public static final field Companion Ldev/slne/surf/api/minestom/plugin/SurfMinestomPlugin$Companion; + public fun ()V + public final fun getDataDirectory ()Ljava/nio/file/Path; + public final fun getEventNode ()Lnet/minestom/server/event/EventNode; + public final fun getLogger ()Lnet/kyori/adventure/text/logger/slf4j/ComponentLogger; + public final fun getMeta ()Ldev/slne/surf/api/minestom/plugin/PluginMeta; + public final fun getScope ()Lkotlinx/coroutines/CoroutineScope; + public fun onDisable (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun onEnable (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun onLoad (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/minestom/plugin/SurfMinestomPlugin$Companion { + public final fun constructing (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; + public final fun contextElement (Ldev/slne/surf/api/minestom/plugin/SurfMinestomPlugin;)Lkotlinx/coroutines/ThreadContextElement; + public final fun current ()Ldev/slne/surf/api/minestom/plugin/SurfMinestomPlugin; +} + +public final class dev/slne/surf/api/minestom/plugin/SurfMinestomPlugin$Context { + public fun (Ldev/slne/surf/api/minestom/plugin/PluginMeta;Lnet/minestom/server/event/EventNode;Ljava/nio/file/Path;Lnet/kyori/adventure/text/logger/slf4j/ComponentLogger;Lkotlinx/coroutines/CoroutineScope;)V + public final fun getDataDirectory ()Ljava/nio/file/Path; + public final fun getEventNode ()Lnet/minestom/server/event/EventNode; + public final fun getLogger ()Lnet/kyori/adventure/text/logger/slf4j/ComponentLogger; + public final fun getMeta ()Ldev/slne/surf/api/minestom/plugin/PluginMeta; + public final fun getScope ()Lkotlinx/coroutines/CoroutineScope; +} + +public final class dev/slne/surf/api/minestom/visibility/PlayerVisibility { + public static final field INSTANCE Ldev/slne/surf/api/minestom/visibility/PlayerVisibility; + public final fun installProvider (Ldev/slne/surf/api/minestom/visibility/PlayerVisibility$Provider;)V + public final fun isLimited (Lnet/minestom/server/entity/Player;)Z + public final fun updateViewerRule (Lnet/minestom/server/entity/Player;)V + public final fun updateViewerRule (Lnet/minestom/server/entity/Player;Ljava/util/function/Predicate;)V +} + +public abstract interface class dev/slne/surf/api/minestom/visibility/PlayerVisibility$Provider { + public abstract fun isLimited (Lnet/minestom/server/entity/Player;)Z + public abstract fun refreshViewerRule (Lnet/minestom/server/entity/Player;)Z + public abstract fun updateViewerRule (Lnet/minestom/server/entity/Player;Ljava/util/function/Predicate;)Z +} + diff --git a/surf-api-minestom/build.gradle.kts b/surf-api-minestom/surf-api-minestom/build.gradle.kts similarity index 80% rename from surf-api-minestom/build.gradle.kts rename to surf-api-minestom/surf-api-minestom/build.gradle.kts index e1b1e4649..de02545ca 100644 --- a/surf-api-minestom/build.gradle.kts +++ b/surf-api-minestom/surf-api-minestom/build.gradle.kts @@ -9,8 +9,9 @@ dependencies { exclude(libs.brigadier) } - implementation(projects.surfApiCore.surfApiCoreServer) - compileOnlyApi(libs.surf.minestom.lobby.api) + compileOnlyApi(libs.minestom.server) + api(libs.minestom.brigadier) + compileOnlyApi(libs.minestom.npc) { exclude(group = "net.minestom") } api(libs.guava) api(libs.dazzleconf) @@ -23,9 +24,8 @@ dependencies { api(libs.commons.math4.core) api(libs.commons.math3) api(libs.inventory.framework.platform.minestom) - implementation(libs.packetevents.netty.common) - runtimeOnly(libs.flogger.slf4j.backend) + testImplementation(libs.minestom.server) testImplementation(libs.minestom.testing) } @@ -37,6 +37,7 @@ private fun T.exclude(provider: Provider + * val prefix = database.loadPrefix(event.player.uuid) // suspending or blocking, both fine + * event.renderer = ChatRenderer.viewerUnaware { _, name, message -> + * Component.text().append(prefix).append(name).append(Component.text(": ")).append(message).build() + * } + * } + * ``` + */ +class AsyncPlayerChatEvent @InternalSurfApi constructor( + + val player: Player, + + /** + * The [Audience]s the message is sent to. Add or remove entries to change the recipients. + */ + val viewers: MutableSet, + + /** + * Renders [message] into the component shown to each viewer. + */ + var renderer: ChatRenderer, + + /** + * The user-supplied message. + */ + var message: Component, + + /** + * The original and unmodified user-supplied message. + * + * The value will **not** reflect changes made through [message]. + */ + val originalMessage: Component, + + /** + * The signed message backing this event. + * + * Changes made in this event will **not** update the signed message. + */ + val signedMessage: SignedMessage, +) { + + /** + * Whether the message is dropped instead of being sent to [viewers]. + */ + var isCancelled = false + + companion object { + @InternalSurfApi + val node = SuspendingEventNode("async-chat") + + typealias AsyncPlayerChatEventListener = suspend (AsyncPlayerChatEvent) -> Unit + + /** + * Registers a chat listener. Lower [priority] runs first. + * + * @see SuspendingEventNode.addListener + */ + fun addListener( + priority: Int = 0, + listener: AsyncPlayerChatEventListener + ) = node.addListener(priority, listener) + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/ChatRenderer.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/ChatRenderer.kt new file mode 100644 index 000000000..56513e64c --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/ChatRenderer.kt @@ -0,0 +1,76 @@ +package dev.slne.surf.api.minestom.chat + +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import net.kyori.adventure.audience.Audience +import net.kyori.adventure.text.Component +import net.minestom.server.entity.Player +import org.jetbrains.annotations.ApiStatus + +/** + * Renders chat messages sent by [Player]s into the components shown to viewers. + */ +fun interface ChatRenderer { + + /** + * Renders a chat message. Called once per receiving [Audience]. + * + * @param source the message source + * @param sourceDisplayName the display name of the source player + * @param message the chat message + * @param viewer the receiving [Audience] + * @return a rendered chat message + */ + @ApiStatus.OverrideOnly + suspend fun render( + source: Player, + sourceDisplayName: Component, + message: Component, + viewer: Audience + ): Component + + /** + * Marker for the renderer returned by [defaultRenderer]. + */ + @InternalSurfApi + sealed interface Default : ChatRenderer, ViewerUnaware + + /** + * A [ChatRenderer] variant that does not depend on the viewer. + * + * @see viewerUnaware + */ + fun interface ViewerUnaware { + + /** + * Renders a chat message. + * + * @param source the message source + * @param sourceDisplayName the display name of the source player + * @param message the chat message + * @return a rendered chat message + */ + @ApiStatus.OverrideOnly + suspend fun render(source: Player, sourceDisplayName: Component, message: Component): Component + } + + companion object { + + /** + * Creates the default renderer, which formats messages with the vanilla `chat.type.text` + * translation. + */ + fun defaultRenderer(): ChatRenderer = + ViewerUnawareChatRenderer.Default { _, sourceDisplayName, message -> + Component.translatable("chat.type.text", sourceDisplayName, message) + } + + /** + * Creates a [ChatRenderer] that renders the message once and shows the result to every + * [Audience]. + * + * @param renderer the viewer-independent renderer + */ + fun viewerUnaware(renderer: ViewerUnaware): ChatRenderer = + ViewerUnawareChatRenderer.Impl(renderer) + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/ChatSessionHolder.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/ChatSessionHolder.kt new file mode 100644 index 000000000..7dc6a45b7 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/ChatSessionHolder.kt @@ -0,0 +1,198 @@ +package dev.slne.surf.api.minestom.chat + +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import net.kyori.adventure.chat.SignedMessage +import net.kyori.adventure.text.Component +import net.minestom.server.crypto.ChatSession +import net.minestom.server.entity.Player +import java.util.* + +/** + * The secure chat state of one player while the server runs signed chat. + * + * ``` + * val holder = ChatSessionHolder.fromPlayer(player) ?: return + * holder.sendSignedMessage(event.signedMessage, player.name, rendered) + * ``` + * + * The extensions on [Player], such as [Player.sendSignedMessage], look the holder up themselves + * and fall back to plain system messages when the server does not run signed chat. + */ +interface ChatSessionHolder { + + val player: Player + + /** + * The chat session this player currently signs their messages under, or `null` while they + * have none. + */ + val chatSession: ChatSession? + + /** + * Sends [message] to this player with its signature intact, attributed to [boundName]. + * + * [unsignedContent] replaces what is displayed without touching what was signed. Passing + * `null` shows the signed content as it was sent. + * + * A [message] not produced by this server's signed chat is sent as plain system text. + */ + fun sendSignedMessage( + message: SignedMessage, + boundName: Component, + unsignedContent: Component? = null, + ) + + /** + * Captures [message], which this player sent, so that it can be shown to a player on another + * server. + * + * [unsignedContent] replaces what is displayed there without touching what was signed. + * + * Returns `null` for a message this server did not receive from a client, which therefore + * carries nothing another server could hand to a client. + */ + fun captureSignedMessage( + message: SignedMessage, + unsignedContent: Component? = null, + ): RemoteSignedMessage? + + /** + * Shows [message] to this player as coming from [sender], a player on another server. + * + * [sender] is announced to this player's client for as long as it takes to deliver the + * message, so that the signature can be verified without the sender ever being on this + * server. + * + * @see captureSignedMessage + */ + fun sendRemoteSignedMessage( + sender: RemoteChatSender, + message: RemoteSignedMessage, + boundName: Component, + ) + + /** + * Deletes the message with [signature] from this player's chat. + */ + fun deleteMessage(signature: SignedMessage.Signature) + + /** + * Looks up holders. Installed by the server when it runs signed chat. + */ + @InternalSurfApi + interface Provider { + fun fromPlayer(player: Player): ChatSessionHolder? + fun fromUuid(uuid: UUID): ChatSessionHolder? + + /** + * Deletes the message with [signature] from [player]'s chat; [player] needs no holder. + */ + fun deleteMessage(player: Player, signature: SignedMessage.Signature) + } + + companion object { + @Volatile + private var provider: Provider? = null + + /** + * The secure chat state of [player], or `null` when the server does not run signed chat + * or the player is no longer online. + */ + fun fromPlayer(player: Player): ChatSessionHolder? = provider?.fromPlayer(player) + + /** + * The secure chat state of the online player [uuid], or `null` when the server does not + * run signed chat or no such player is online. + */ + fun fromUuid(uuid: UUID): ChatSessionHolder? = provider?.fromUuid(uuid) + + @InternalSurfApi + fun installProvider(provider: Provider?) { + this.provider = provider + } + + internal fun deleteMessage(player: Player, signature: SignedMessage.Signature): Boolean { + val provider = provider ?: return false + provider.deleteMessage(player, signature) + return true + } + } +} + +/** + * The secure chat state of this player. + * + * @see ChatSessionHolder.fromPlayer + */ +val Player.chatSessionHolder: ChatSessionHolder? + get() = ChatSessionHolder.fromPlayer(this) + +/** + * The chat session this player currently signs their messages under, or `null` while they have + * none or the server does not run signed chat. + */ +val Player.signedChatSession: ChatSession? + get() = chatSessionHolder?.chatSession + +/** + * Sends [message] to this player with its signature intact, attributed to [boundName]. + * + * Without signed chat, or for a message this server did not receive from a client, the message + * is sent as plain system text: [unsignedContent], or else the text that was signed. + * + * @see ChatSessionHolder.sendSignedMessage + */ +fun Player.sendSignedMessage( + message: SignedMessage, + boundName: Component, + unsignedContent: Component? = null, +) { + val holder = chatSessionHolder + if (holder == null) { + sendMessage(unsignedContent ?: Component.text(message.message())) + return + } + + holder.sendSignedMessage(message, boundName, unsignedContent) +} + +/** + * Captures [message], which this player sent, so that it can be shown on another server. + * + * @return `null` when the server does not run signed chat or the message did not come from a + * client + * @see ChatSessionHolder.captureSignedMessage + */ +fun Player.captureSignedMessage( + message: SignedMessage, + unsignedContent: Component? = null, +): RemoteSignedMessage? = chatSessionHolder?.captureSignedMessage(message, unsignedContent) + +/** + * Shows [message] to this player as coming from [sender], a player on another server. + * + * Without signed chat, the message is sent as plain system text. + * + * @see ChatSessionHolder.sendRemoteSignedMessage + */ +fun Player.sendRemoteSignedMessage( + sender: RemoteChatSender, + message: RemoteSignedMessage, + boundName: Component, +) { + val holder = chatSessionHolder + if (holder == null) { + sendMessage(message.unsignedContent ?: Component.text(message.content)) + return + } + + holder.sendRemoteSignedMessage(sender, message, boundName) +} + +/** + * Deletes the message with [signature] from this player's chat. Does nothing when the server + * does not run signed chat, since then no message carries a signature. + */ +fun Player.deleteSignedMessage(signature: SignedMessage.Signature) { + ChatSessionHolder.deleteMessage(this, signature) +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/RemoteChatSender.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/RemoteChatSender.kt new file mode 100644 index 000000000..0e26aaee9 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/RemoteChatSender.kt @@ -0,0 +1,16 @@ +package dev.slne.surf.api.minestom.chat + +import net.minestom.server.crypto.ChatSession +import java.util.UUID + +/** + * The player a [RemoteSignedMessage] came from, as far as the receiving server knows them. + * + * [session] is the chat session the message was signed under. It is `null` for a sender that had + * none, in which case the message is shown as unsigned. + */ +data class RemoteChatSender( + val uuid: UUID, + val username: String, + val session: ChatSession?, +) diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/RemoteSignedMessage.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/RemoteSignedMessage.kt new file mode 100644 index 000000000..2265f0590 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/RemoteSignedMessage.kt @@ -0,0 +1,45 @@ +package dev.slne.surf.api.minestom.chat + +import net.kyori.adventure.text.Component +import net.minestom.server.crypto.MessageSignature +import java.time.Instant +import java.util.* + +/** + * A signed chat message in a form that survives leaving this server. + * + * It carries everything a client needs to verify the signature, so a server that never saw the + * sender can still show the message as signed. A message from a sender without a chat session has no + * [signature], a nil [sessionId] and is shown as unsigned. + * + * @see ChatSessionHolder.captureSignedMessage + * @see ChatSessionHolder.sendRemoteSignedMessage + */ +data class RemoteSignedMessage( + val sender: UUID, + val sessionId: UUID, + + /** + * The position of the message in the signature chain of its session. + */ + val index: Int, + + val signature: MessageSignature?, + + /** + * The message as it was signed, which is the plain text the sender typed. + */ + val content: String, + val timestamp: Instant, + val salt: Long, + + /** + * The signatures of the messages the sender had seen, which the signature also covers. + */ + val lastSeen: List, + + /** + * What the receiver sees instead of [content]. + */ + val unsignedContent: Component?, +) diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/ViewerUnawareChatRenderer.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/ViewerUnawareChatRenderer.kt new file mode 100644 index 000000000..9b1130697 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/chat/ViewerUnawareChatRenderer.kt @@ -0,0 +1,56 @@ +package dev.slne.surf.api.minestom.chat + +import net.kyori.adventure.audience.Audience +import net.kyori.adventure.text.Component +import net.minestom.server.entity.Player + +/** + * Adapts a [ChatRenderer.ViewerUnaware] to a [ChatRenderer]. + * + * The result of the last render is reused while the arguments are the same instances, so a message + * sent to many viewers is rendered once. Any other arguments render again. Concurrent calls with + * identical arguments may each render. + */ +internal sealed class ViewerUnawareChatRenderer( + private val unaware: ChatRenderer.ViewerUnaware +) : ChatRenderer, ChatRenderer.ViewerUnaware { + + private class Rendered( + val source: Player, + val sourceDisplayName: Component, + val message: Component, + val result: Component, + ) { + fun matches(source: Player, sourceDisplayName: Component, message: Component) = + this.source === source && + this.sourceDisplayName === sourceDisplayName && + this.message === message + } + + @Volatile + private var last: Rendered? = null + + override suspend fun render( + source: Player, + sourceDisplayName: Component, + message: Component, + viewer: Audience + ): Component = render(source, sourceDisplayName, message) + + override suspend fun render( + source: Player, + sourceDisplayName: Component, + message: Component + ): Component { + last?.takeIf { it.matches(source, sourceDisplayName, message) }?.let { return it.result } + + val result = unaware.render(source, sourceDisplayName, message) + last = Rendered(source, sourceDisplayName, message, result) + return result + } + + class Impl(unaware: ChatRenderer.ViewerUnaware) : ViewerUnawareChatRenderer(unaware) + + class Default(unaware: ChatRenderer.ViewerUnaware) : + ViewerUnawareChatRenderer(unaware), ChatRenderer.Default +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/CommandAPI.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/CommandAPI.kt new file mode 100644 index 000000000..4fafa8cb5 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/CommandAPI.kt @@ -0,0 +1,80 @@ +package dev.slne.surf.api.minestom.command + +import dev.slne.surf.api.minestom.command.exception.CommandSyntaxException +import dev.slne.surf.api.minestom.command.exception.CommandValidationException +import dev.slne.surf.api.minestom.command.internal.CommandAPIPlatform +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import net.kyori.adventure.text.Component +import net.minestom.server.command.CommandSender +import java.util.concurrent.atomic.AtomicReference + +/** + * Entry point for registering, unregistering and executing CommandAPI commands. + * + * The operations require the server platform, which is installed during server startup; earlier + * calls throw [IllegalStateException]. + */ +object CommandAPI { + private val platform = AtomicReference() + + @InternalSurfApi + fun installPlatform(value: CommandAPIPlatform) { + check(platform.compareAndSet(null, value)) { + "The CommandAPI platform is already installed" + } + } + + @InternalSurfApi + fun uninstallPlatform(value: CommandAPIPlatform) { + platform.compareAndSet(value, null) + } + + @InternalSurfApi + fun register(definition: CommandDefinition, namespace: String?): RegisteredCommand { + validateNamespace(namespace) + return installedPlatform().register(definition, namespace) + } + + /** + * Unregisters the command that owns the label [name], together with its aliases and namespaced + * labels. + * + * @return `false` when no CommandAPI command owns [name] + */ + fun unregister(name: String): Boolean = installedPlatform().unregister(name) + + /** + * Dispatches [input] as [sender] and returns the executed command's result value. + * + * [input] carries no leading slash. Callers that must not throw check ownership first. + * + * @throws com.mojang.brigadier.exceptions.CommandSyntaxException when the command is unknown, an + * argument is rejected, or input remains after a complete syntax. + */ + fun execute(sender: CommandSender, input: String): Int = + installedPlatform().execute(sender, input) + + /** + * Aborts the running command with a [CommandSyntaxException] whose message is the plain text [message]. + */ + fun failWithString(message: String): Nothing = failWithMessage(Component.text(message)) + + /** + * Aborts the running command with a [CommandSyntaxException] that carries [message] as its component. + */ + fun failWithMessage(message: Component): Nothing = throw CommandSyntaxException(message) + + private fun installedPlatform(): CommandAPIPlatform = checkNotNull(platform.get()) { + "The CommandAPI platform has not been installed; register commands after server startup" + } + + private fun validateNamespace(namespace: String?) { + if (namespace != null && !NAMESPACE_PATTERN.matches(namespace)) { + throw CommandValidationException( + "Command namespace '$namespace' must contain only lowercase letters, digits, '.', '_' or '-'", + ) + } + } + + private val NAMESPACE_PATTERN = Regex("[a-z0-9_.-]+") +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/CommandAPICommand.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/CommandAPICommand.kt new file mode 100644 index 000000000..2330f5cf2 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/CommandAPICommand.kt @@ -0,0 +1,522 @@ +/* + * Substantially translated from CommandAPI 12.0.0 (https://github.com/CommandAPI/CommandAPI). + * MIT License, Copyright (c) 2020 - 2022 Jorel Ali. + * The complete license is distributed in META-INF/LICENSES/CommandAPI-LICENSE.txt. + */ +package dev.slne.surf.api.minestom.command + +import dev.slne.surf.api.minestom.command.argument.* +import dev.slne.surf.api.minestom.command.exception.CommandValidationException +import dev.slne.surf.api.minestom.command.executor.CommandExecutable +import dev.slne.surf.api.minestom.command.executor.ExecutorDefinition +import dev.slne.surf.api.minestom.command.executor.ExecutorType +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import it.unimi.dsi.fastutil.objects.* +import net.minestom.server.command.CommandSender +import java.util.* + +@InternalSurfApi +data class CommandDefinition( + val name: String, + val aliases: Set, + val metadata: CommandMetadata, + val paths: List, +) + +@InternalSurfApi +data class CommandPath( + val arguments: List>, + val executors: List, + val permissions: Set, + val requirements: List<(CommandSender) -> Boolean>, +) + +/** + * Builder for a command made of a linear argument chain and optional subcommands. + * + * Executors added through [CommandExecutable] run after the last argument of the chain, or on the + * command itself when no arguments are declared. Use [CommandTree] for branching argument structures. + * + * Invalid configuration, such as a malformed name, a required argument after an optional one or a + * non-terminal greedy argument, surfaces as [CommandValidationException] from [register]. + * + * @property name label of the command; letters, digits, `.`, `_` and `-` only + */ +class CommandAPICommand(val name: String) : CommandExecutable { + private val aliases = ObjectArrayList() + private val permissions = ObjectLinkedOpenHashSet() + private val requirements = ObjectArrayList<(CommandSender) -> Boolean>() + private val arguments = ObjectArrayList>() + private val subcommands = ObjectArrayList() + private val executors = ObjectArrayList() + + private var shortDescription: String? = null + private val fullDescription = ObjectArrayList() + private val usage = ObjectArrayList() + private var help: String? = null + + fun withAliases(vararg aliases: String): CommandAPICommand = apply { + this.aliases += aliases + } + + /** + * Requires [permission] to use the command and all of its subcommands. + * + * Multiple calls accumulate and every permission is required. The console always passes; + * players need all permissions; other senders pass only while no permission is declared. + * + * @throws CommandValidationException when [permission] is blank + */ + fun withPermission(permission: String): CommandAPICommand = apply { + val normalized = permission.trim() + if (normalized.isBlank()) { + throw CommandValidationException("Command permission must not be blank") + } + permissions += normalized + } + + /** + * Requires [requirement] to return `true` for the sender to use the command and its subcommands. + * Multiple requirements accumulate and all must pass. + */ + fun withRequirement(requirement: (CommandSender) -> Boolean): CommandAPICommand = apply { + requirements += requirement + } + + fun withArguments(vararg arguments: Argument<*>): CommandAPICommand = apply { + this.arguments += arguments + } + + /** + * Appends [arguments] to the chain and marks each of them optional. + */ + fun withOptionalArguments(vararg arguments: Argument<*>): CommandAPICommand = apply { + arguments.forEach { argument -> argument.setOptional(true) } + this.arguments += arguments + } + + /** + * Adds [subcommand], reachable by its name or aliases after this command's own arguments. + * + * @throws CommandValidationException when [subcommand] is this command; longer cycles are + * rejected by [register] + */ + fun withSubcommand(subcommand: CommandAPICommand): CommandAPICommand = apply { + if (subcommand === this) { + throw CommandValidationException("A command cannot be its own subcommand") + } + subcommands += subcommand + } + + fun withSubcommands(vararg subcommands: CommandAPICommand): CommandAPICommand = apply { + subcommands.forEach(::withSubcommand) + } + + fun withShortDescription(description: String): CommandAPICommand = apply { + shortDescription = description + } + + fun withFullDescription(vararg description: String): CommandAPICommand = apply { + fullDescription.clear() + fullDescription += description + } + + fun withUsage(vararg usage: String): CommandAPICommand = apply { + this.usage.clear() + this.usage += usage + } + + fun withHelp(help: String): CommandAPICommand = apply { + this.help = help + } + + override fun addExecutor(definition: ExecutorDefinition): CommandAPICommand = apply { + executors += definition + } + + @InternalSurfApi + fun toDefinition(): CommandDefinition { + val stack = ReferenceOpenHashSet() + + validateCommandLabels(name, aliases) + + val output = ObjectArrayList() + val rootPermissions = immutableSet(permissions) + val rootRequirements = immutableList(requirements) + + if (arguments.isEmpty && executors.isNotEmpty()) { + output += commandPath(emptyList(), executors, rootPermissions, rootRequirements) + } + + val subcommandNodes = ObjectArrayList() + subcommands.forEach { subcommand -> + subcommandNodes.addAll(subcommand.toSubcommandNodes(stack)) + } + + val rootNodes = if (arguments.isEmpty) { + subcommandNodes + } else { + val argumentNodes = ObjectArrayList(arguments.size) + arguments.forEach { argument -> + argumentNodes += argument.toTreeNode() + } + + linearNodes(argumentNodes, executors, subcommandNodes) + } + rootNodes.forEach { node -> + flatten( + emptyList(), + rootPermissions, + rootRequirements, + node, + output + ) + } + + val paths = validateAndFreezePaths(output, name) + return CommandDefinition( + name = name, + aliases = immutableSet(aliases), + metadata = CommandMetadata.snapshot(shortDescription, fullDescription, usage, help), + paths = paths, + ) + } + + /** + * Validates the command and registers it with the server. + * + * With a [namespace], the name and every alias are additionally registered as `namespace:label`. + * + * @throws CommandValidationException when the command or [namespace] is invalid + * @throws IllegalStateException when a label is already taken or the platform is not installed + */ + fun register(namespace: String? = null): RegisteredCommand = + CommandAPI.register(toDefinition(), namespace) + + private fun toSubcommandNodes( + stack: MutableSet, + ): List { + if (!stack.add(this)) { + throw CommandValidationException("Command '$name' contains a subcommand cycle") + } + + try { + validateCommandLabels(name, aliases) + + val nestedSubcommands = ObjectArrayList() + subcommands.forEach { subcommand -> + nestedSubcommands.addAll(subcommand.toSubcommandNodes(stack)) + } + + val body = if (arguments.isEmpty) { + emptyList() + } else { + val argumentNodes = ObjectArrayList(arguments.size) + arguments.forEach { argument -> + argumentNodes += argument.toTreeNode() + } + + linearNodes(argumentNodes, executors, nestedSubcommands) + } + + val result = ObjectArrayList(aliases.size + 1) + result += createSubcommandNode( + label = name, + nestedSubcommands = nestedSubcommands, + body = body, + ) + + aliases.forEach { alias -> + result += createSubcommandNode( + label = alias, + nestedSubcommands = nestedSubcommands, + body = body, + ) + } + + return ObjectLists.unmodifiable(result) + } finally { + stack.remove(this) + } + } + + private fun createSubcommandNode( + label: String, + nestedSubcommands: List, + body: List, + ): ArgumentTreeNode = ArgumentTreeNode( + argument = subcommandLiteral(label), + executors = if (arguments.isEmpty) immutableList(executors) else emptyList(), + children = if (arguments.isEmpty) nestedSubcommands else body, + permissions = immutableSet(permissions), + requirements = immutableList(requirements), + ) + + /** + * The literal that selects this subcommand, carrying the subcommand's own permissions and + * requirements so that they hide the literal from senders that fail them. + */ + private fun subcommandLiteral(label: String): ArgumentDefinition { + val literal = LiteralArgument(label) + permissions.forEach(literal::withPermission) + requirements.forEach(literal::withRequirement) + return literal.toDefinition() + } +} + +internal fun validateCommandLabels(name: String, aliases: Collection) { + validateCommandLabel(name, "name") + + val normalized = ObjectOpenHashSet(aliases.size + 1) + normalized += name.lowercase(Locale.ROOT) + + aliases.forEach { alias -> + validateCommandLabel(alias, "alias") + + if (!normalized.add(alias.lowercase(Locale.ROOT))) { + throw CommandValidationException("Command name and aliases must be unique") + } + } +} + +private fun validateCommandLabel(label: String, kind: String) { + if (!COMMAND_NAME_PATTERN.matches(label)) { + throw CommandValidationException( + "Command $kind '$label' must contain only letters, digits, '.', '_' or '-'", + ) + } +} + +internal fun linearNodes( + arguments: List, + terminalExecutors: Collection, + terminalChildren: List, +): List { + var suffix = terminalChildren + for (index in arguments.indices.reversed()) { + val node = arguments[index] + val executors = if (index == arguments.lastIndex) { + val combined = ObjectArrayList( + node.executors.size + terminalExecutors.size, + ) + + combined.addAll(node.executors) + combined.addAll(terminalExecutors) + + immutableList(combined) + } else { + node.executors + } + + val children = ObjectArrayList( + node.children.size + suffix.size, + ) + + children.addAll(node.children) + children.addAll(suffix) + + suffix = ObjectLists.singleton( + node.copy( + executors = executors, + children = immutableList(children), + ), + ) + } + return suffix +} + +internal fun flatten( + prefix: List>, + inheritedPermissions: Set, + inheritedRequirements: List<(CommandSender) -> Boolean>, + node: ArgumentTreeNode, + output: MutableList, +) { + val path = ObjectArrayList>(prefix.size + 1) + path.addAll(prefix) + path += node.argument + + val permissions = ObjectLinkedOpenHashSet( + inheritedPermissions.size + node.permissions.size, + ) + permissions.addAll(inheritedPermissions) + permissions.addAll(node.permissions) + + val requirements = ObjectArrayList<(CommandSender) -> Boolean>( + inheritedRequirements.size + node.requirements.size, + ) + requirements.addAll(inheritedRequirements) + requirements.addAll(node.requirements) + + val immutablePath = immutableList(path) + val immutablePermissions = immutableSet(permissions) + val immutableRequirements = immutableList(requirements) + + if (node.executors.isNotEmpty()) { + output += commandPath( + immutablePath, + node.executors, + immutablePermissions, + immutableRequirements + ) + } + node.children.forEach { child -> + flatten( + immutablePath, + immutablePermissions, + immutableRequirements, + child, + output + ) + } +} + +internal fun validateAndFreezePaths( + paths: List, + commandName: String +): List { + if (paths.isEmpty()) { + throw CommandValidationException("Command '$commandName' has no executable paths") + } + + paths.forEach { path -> + val names = ObjectOpenHashSet(path.arguments.size) + path.arguments.forEach { argument -> + if (argument.kind !is ArgumentKind.Literal && !names.add(argument.nodeName)) { + throw CommandValidationException( + "Executable path contains duplicate argument node names", + ) + } + } + + val firstOptional = path.arguments.indexOfFirst { argument -> argument.optional } + if ( + firstOptional >= 0 && + path.arguments + .subList(firstOptional, path.arguments.size) + .any { argument -> !argument.optional } + ) { + throw CommandValidationException("Required arguments cannot follow optional arguments") + } + val greedyIndex = path.arguments.indexOfFirst { argument -> argument.greedy } + if (greedyIndex >= 0 && greedyIndex != path.arguments.lastIndex) { + throw CommandValidationException("Greedy arguments must be terminal") + } + + val executorTypes = ObjectOpenHashSet(path.executors.size) + path.executors.forEach { executor -> + if (!executorTypes.add(executor.type)) { + throw CommandValidationException( + "Executable path contains equal-specificity executor conflicts", + ) + } + } + } + + validateNoMergeConflicts(paths, commandName) + + return immutableList(paths) +} + +/** + * Rejects sibling paths that the command tree would merge into one node with a conflict. + * + * Paths share a node when they agree on the node names of the whole prefix. Sharing is valid when + * the arguments of one node are of the same kind and at most one path executes there; otherwise + * the first node would silently win its type and the later executor would replace the earlier one. + */ +private fun validateNoMergeConflicts(paths: List, commandName: String) { + val root = PathNode(null) + + paths.forEach { path -> + val firstOptional = path.arguments.indexOfFirst { argument -> argument.optional } + val executableDepths = if (firstOptional == -1) { + path.arguments.size..path.arguments.size + } else { + firstOptional..path.arguments.size + } + + var node = root + if (0 in executableDepths) node.claimExecutor(commandName) + + path.arguments.forEachIndexed { index, argument -> + val name = argument.treeName() + val existing = node.children[name] + val child = if (existing == null) { + PathNode(argument, node, name).also { created -> node.children[name] = created } + } else { + if (existing.argument?.kind != argument.kind) { + throw CommandValidationException( + "Command '$commandName' declares conflicting arguments named '$name' " + + "at '${existing.describe()}'", + ) + } + existing + } + + node = child + if (index + 1 in executableDepths) node.claimExecutor(commandName) + } + } +} + +/** + * Name under which the command tree registers this argument: the keyword of a literal, otherwise the + * argument name. + */ +private fun ArgumentDefinition<*>.treeName(): String = + (kind as? ArgumentKind.Literal)?.literal ?: nodeName + +private class PathNode( + val argument: ArgumentDefinition<*>?, + private val parent: PathNode? = null, + private val name: String? = null, +) { + val children = Object2ObjectOpenHashMap() + private var executes = false + + fun claimExecutor(commandName: String) { + if (executes) { + throw CommandValidationException( + "Command '$commandName' declares more than one executable path ending at " + + "'${describe()}'", + ) + } + executes = true + } + + fun describe(): String { + val names = ObjectArrayList() + var node: PathNode? = this + while (node?.name != null) { + names += node.name + node = node.parent + } + names.reverse() + return if (names.isEmpty) "" else names.joinToString(" ") + } +} + +private fun commandPath( + arguments: Collection>, + executors: Collection, + permissions: Collection, + requirements: Collection<(CommandSender) -> Boolean>, +): CommandPath = CommandPath( + arguments = immutableList(arguments), + executors = immutableList(executors), + permissions = immutableSet(permissions), + requirements = immutableList(requirements), +) + +internal fun immutableList( + values: Collection, +): List = ObjectImmutableList(values) + +internal fun immutableSet( + values: Collection, +): Set = ObjectSets.unmodifiable( + ObjectLinkedOpenHashSet(values), +) + +private val COMMAND_NAME_PATTERN = Regex("""[\p{L}\p{N}_.-]+""") diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/CommandMetadata.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/CommandMetadata.kt new file mode 100644 index 000000000..028f4d409 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/CommandMetadata.kt @@ -0,0 +1,28 @@ +package dev.slne.surf.api.minestom.command + +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import it.unimi.dsi.fastutil.objects.ObjectLists + +@InternalSurfApi +data class CommandMetadata( + val shortDescription: String? = null, + val fullDescription: List = emptyList(), + val usage: List = emptyList(), + val help: String? = null, +) { + companion object { + @InternalSurfApi + fun snapshot( + shortDescription: String?, + fullDescription: Collection, + usage: Collection, + help: String?, + ): CommandMetadata = CommandMetadata( + shortDescription = shortDescription, + fullDescription = ObjectLists.unmodifiable(ObjectArrayList(fullDescription)), + usage = ObjectLists.unmodifiable(ObjectArrayList(usage)), + help = help, + ) + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/CommandTree.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/CommandTree.kt new file mode 100644 index 000000000..c7977a234 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/CommandTree.kt @@ -0,0 +1,142 @@ +/* + * Substantially translated from CommandAPI 12.0.0 (https://github.com/CommandAPI/CommandAPI). + * MIT License, Copyright (c) 2020 - 2022 Jorel Ali. + * The complete license is distributed in META-INF/LICENSES/CommandAPI-LICENSE.txt. + */ +package dev.slne.surf.api.minestom.command + +import dev.slne.surf.api.minestom.command.argument.Argument +import dev.slne.surf.api.minestom.command.exception.CommandValidationException +import dev.slne.surf.api.minestom.command.executor.CommandExecutable +import dev.slne.surf.api.minestom.command.executor.ExecutorDefinition +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet +import net.minestom.server.command.CommandSender + +/** + * Builder for a command whose argument structure branches. + * + * Each argument added through [then] starts a branch that can carry its own executors and nested + * arguments. Permissions and requirements declared here apply to every branch. Invalid + * configuration surfaces as [CommandValidationException] from [register]. + * + * @property name label of the command; letters, digits, `.`, `_` and `-` only + */ +class CommandTree(val name: String) : CommandExecutable { + private val aliases = ObjectArrayList() + private val permissions = ObjectLinkedOpenHashSet() + private val requirements = ObjectArrayList<(CommandSender) -> Boolean>() + private val arguments = ObjectArrayList>() + private val executors = ObjectArrayList() + + private var shortDescription: String? = null + private val fullDescription = ObjectArrayList() + private val usage = ObjectArrayList() + private var help: String? = null + + fun withAliases(vararg aliases: String): CommandTree = apply { + this.aliases += aliases + } + + /** + * Requires [permission] to use any branch of the command. + * + * Multiple calls accumulate and every permission is required. The console always passes; + * players need all permissions; other senders pass only while no permission is declared. + * + * @throws CommandValidationException when [permission] is blank + */ + fun withPermission(permission: String): CommandTree = apply { + val normalized = permission.trim() + if (normalized.isBlank()) { + throw CommandValidationException("Command permission must not be blank") + } + permissions += normalized + } + + /** + * Requires [requirement] to return `true` for the sender to use any branch of the command. + * Multiple requirements accumulate and all must pass. + */ + fun withRequirement(requirement: (CommandSender) -> Boolean): CommandTree = apply { + requirements += requirement + } + + fun withShortDescription(description: String): CommandTree = apply { + shortDescription = description + } + + fun withFullDescription(vararg description: String): CommandTree = apply { + fullDescription.clear() + fullDescription += description + } + + fun withUsage(vararg usage: String): CommandTree = apply { + this.usage.clear() + this.usage += usage + } + + fun withHelp(help: String): CommandTree = apply { + this.help = help + } + + /** + * Adds [argument], together with the branches nested below it, as a branch of the command. + */ + fun then(argument: Argument<*>): CommandTree = apply { + arguments += argument + } + + /** + * Adds an executor that runs when the command is invoked without arguments. + */ + override fun addExecutor(definition: ExecutorDefinition): CommandTree = apply { + executors += definition + } + + @InternalSurfApi + fun toDefinition(): CommandDefinition { + validateCommandLabels(name, aliases) + + val output = ObjectArrayList() + val rootPermissions = immutableSet(permissions) + val rootRequirements = immutableList(requirements) + + if (executors.isNotEmpty()) { + output += CommandPath( + arguments = emptyList(), + executors = immutableList(executors), + permissions = rootPermissions, + requirements = rootRequirements, + ) + } + + arguments.forEach { argument -> + flatten( + prefix = emptyList(), + inheritedPermissions = rootPermissions, + inheritedRequirements = rootRequirements, + node = argument.toTreeNode(), + output = output, + ) + } + + return CommandDefinition( + name = name, + aliases = immutableSet(aliases), + metadata = CommandMetadata.snapshot(shortDescription, fullDescription, usage, help), + paths = validateAndFreezePaths(output, name), + ) + } + + /** + * Validates the command and registers it with the server. + * + * With a [namespace], the name and every alias are additionally registered as `namespace:label`. + * + * @throws CommandValidationException when the command or [namespace] is invalid + * @throws IllegalStateException when a label is already taken or the platform is not installed + */ + fun register(namespace: String? = null): RegisteredCommand = CommandAPI.register(toDefinition(), namespace) +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/RegisteredCommand.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/RegisteredCommand.kt new file mode 100644 index 000000000..46ba8eb36 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/RegisteredCommand.kt @@ -0,0 +1,34 @@ +package dev.slne.surf.api.minestom.command + +import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet +import it.unimi.dsi.fastutil.objects.ObjectSets + +class RegisteredCommand( + val name: String, + aliases: Collection = emptySet(), + val namespace: String? = null, +) { + val aliases: Set = ObjectSets.unmodifiable(ObjectLinkedOpenHashSet(aliases)) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is RegisteredCommand) return false + + if (name != other.name) return false + if (namespace != other.namespace) return false + if (aliases != other.aliases) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + namespace.hashCode() + result = 31 * result + aliases.hashCode() + return result + } + + override fun toString(): String { + return "RegisteredCommand(name='$name', namespace=$namespace, aliases=$aliases)" + } +} diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/args/MiniMessageArgument.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/args/MiniMessageArgument.kt similarity index 71% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/args/MiniMessageArgument.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/args/MiniMessageArgument.kt index 462923412..98a086ff6 100644 --- a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/args/MiniMessageArgument.kt +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/args/MiniMessageArgument.kt @@ -1,21 +1,24 @@ package dev.slne.surf.api.minestom.command.args -import dev.slne.minestom.lobby.api.command.commandapi.CommandAPI -import dev.slne.minestom.lobby.api.command.commandapi.CommandAPICommand -import dev.slne.minestom.lobby.api.command.commandapi.CommandTree -import dev.slne.minestom.lobby.api.command.commandapi.argument.Argument -import dev.slne.minestom.lobby.api.command.commandapi.argument.CustomArgument -import dev.slne.minestom.lobby.api.command.commandapi.argument.GreedyStringArgument -import dev.slne.minestom.lobby.api.command.commandapi.argument.TextArgument import dev.slne.surf.api.core.command.builder.CommandExceptionBuilder import dev.slne.surf.api.core.minimessage.miniMessage +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.CommandTree +import dev.slne.surf.api.minestom.command.argument.Argument +import dev.slne.surf.api.minestom.command.argument.CustomArgument +import dev.slne.surf.api.minestom.command.argument.GreedyStringArgument +import dev.slne.surf.api.minestom.command.argument.TextArgument import net.kyori.adventure.text.Component import net.kyori.adventure.text.minimessage.ParsingException /** - * Reads a component written in MiniMessage. + * Parses a MiniMessage string into a [Component]. * - * @param greedy whether the argument consumes the rest of the command line + * Malformed MiniMessage fails the command with the parser's detail message and error position. + * + * @param greedy whether the argument consumes the rest of the command line instead of one quoted or + * unquoted text token */ open class MiniMessageArgument( nodeName: String, @@ -24,7 +27,7 @@ open class MiniMessageArgument( if (greedy) GreedyStringArgument(nodeName) else TextArgument(nodeName), { info -> try { - miniMessage.deserialize(info.currentInput) + miniMessage.deserialize(info.baseValue) } catch (e: ParsingException) { CommandAPI.failWithMessage( CommandExceptionBuilder( diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/Argument.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/Argument.kt new file mode 100644 index 000000000..3c0bfdad6 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/Argument.kt @@ -0,0 +1,404 @@ +/* + * Substantially translated from CommandAPI 12.0.0 (https://github.com/CommandAPI/CommandAPI). + * MIT License, Copyright (c) 2020 - 2022 Jorel Ali. + * The complete license is distributed in META-INF/LICENSES/CommandAPI-LICENSE.txt. + */ +package dev.slne.surf.api.minestom.command.argument + +import com.mojang.brigadier.LiteralMessage +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType +import dev.slne.surf.api.minestom.command.exception.CommandValidationException +import dev.slne.surf.api.minestom.command.executor.CommandExecutable +import dev.slne.surf.api.minestom.command.executor.ExecutorDefinition +import dev.slne.surf.api.minestom.command.suggestion.ArgumentSuggestions +import dev.slne.surf.api.minestom.command.suggestion.SafeSuggestions +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import it.unimi.dsi.fastutil.objects.* +import net.minestom.server.command.CommandSender +import net.minestom.server.utils.Range +import java.time.Duration +import java.util.* +import dev.slne.surf.api.minestom.command.argument.Axis as AxisType +import dev.slne.surf.api.minestom.command.argument.Rotation as RotationType +import net.kyori.adventure.chat.SignedMessage as AdventureSignedMessage +import net.kyori.adventure.key.Key as AdventureKey +import net.minestom.server.color.TeamColor as MinestomTeamColor +import net.minestom.server.coordinate.Vec as MinestomVec +import net.minestom.server.entity.Entity as MinestomEntity +import net.minestom.server.entity.EntityType as MinestomEntityType +import net.minestom.server.entity.GameMode as MinestomGameMode +import net.minestom.server.entity.Player as MinestomPlayer +import net.minestom.server.instance.Instance as MinestomInstance +import net.minestom.server.item.enchant.Enchantment as MinestomEnchantment +import net.minestom.server.particle.Particle as MinestomParticle +import net.minestom.server.potion.PotionEffect as MinestomPotionEffect +import net.minestom.server.registry.Registry as MinestomRegistry +import net.minestom.server.registry.RegistryKey as MinestomRegistryKey +import net.minestom.server.sound.SoundEvent as MinestomSoundEvent +import net.minestom.server.world.biome.Biome as MinestomBiome +import kotlin.Boolean as KBoolean +import kotlin.Double as KDouble +import kotlin.Enum as KEnum +import kotlin.Float as KFloat +import kotlin.Int as KInt +import kotlin.Long as KLong +import kotlin.collections.List as KList + +/** + * Describes how an argument is parsed and declared to the client; [T] is the type of the parsed + * value. + * + * Kinds are consumed by the server platform and not meant to be implemented outside this module. + */ +sealed interface ArgumentKind { + data object Boolean : ArgumentKind + + data class Integer(val min: KInt, val max: KInt) : ArgumentKind + + data class Long(val min: KLong, val max: KLong) : ArgumentKind + + data class Float(val min: KFloat, val max: KFloat) : ArgumentKind + + data class Double(val min: KDouble, val max: KDouble) : ArgumentKind + + data object Word : ArgumentKind + + data object Text : ArgumentKind + + data object GreedyString : ArgumentKind + + data class Literal(val literal: String) : ArgumentKind + + data class MultiLiteral(val literals: KList) : ArgumentKind + + data class Enum>( + val values: EnumSet, + val formatter: (E) -> String, + ) : ArgumentKind + + data object Uuid : ArgumentKind + + data object IntegerRange : ArgumentKind + + data object FloatRange : ArgumentKind + + data object Command : ArgumentKind + + data object SignedMessage : ArgumentKind + + data object Player : ArgumentKind + + data class Players(val allowEmpty: KBoolean) : ArgumentKind> + + data object Entity : ArgumentKind + + data class Entities(val allowEmpty: KBoolean) : ArgumentKind> + + data object EntityType : ArgumentKind + + data object Position : ArgumentKind + + data object Position2D : ArgumentKind + + data object BlockPosition : ArgumentKind + + data object Rotation : ArgumentKind + + data object Angle : ArgumentKind + + data object Axis : ArgumentKind> + + data object ResourceLocation : ArgumentKind + + data object Time : ArgumentKind + + data object TeamColor : ArgumentKind + + data object Particle : ArgumentKind + + data object GameMode : ArgumentKind + + data object Sound : ArgumentKind + + data object PotionEffect : ArgumentKind + + data object Biome : ArgumentKind> + + data object Enchantment : ArgumentKind> + + data class Resource( + val identifier: String, + val registry: MinestomRegistry, + ) : ArgumentKind> + + data object Instance : ArgumentKind + + data class Custom( + val base: ArgumentDefinition, + val parser: (CustomArgumentInfo) -> T, + ) : ArgumentKind + + data class List( + val element: ArgumentDefinition, + val delimiter: Char, + val allowEmpty: KBoolean, + ) : ArgumentKind> +} + +/** + * How an argument sources its suggestions. + * + * [BuiltIns] offers only what the argument's parser suggests. The `Include` modes add the + * provider's entries to the built-in ones; the `Replace` modes offer only the provider's entries. + * The `Safe` modes take typed values that are converted to text with the argument's formatter. + */ +sealed interface SuggestionMode { + data object BuiltIns : SuggestionMode + + data class Include(val provider: ArgumentSuggestions) : SuggestionMode + + data class Replace(val provider: ArgumentSuggestions) : SuggestionMode + + data class IncludeSafe(val provider: SafeSuggestions) : SuggestionMode + + data class ReplaceSafe(val provider: SafeSuggestions) : SuggestionMode +} + +/** + * Immutable snapshot of an [Argument] that the server platform compiles into a command node. + */ +data class ArgumentDefinition( + val nodeName: String, + val kind: ArgumentKind, + val optional: KBoolean, + val defaultValue: ((CommandSender) -> T)?, + val permissions: Set, + val requirements: KList<(CommandSender) -> KBoolean>, + val suggestions: SuggestionMode, + val greedy: KBoolean, + val rawType: ArgumentType, + val stringify: (T) -> String, + val listDelimiter: Char? = null, +) + +/** + * Placeholder raw type for kinds that the platform builds without a parser, such as + * [LiteralArgument] and [SignedMessageArgument]. + * + * Parsing through it always fails. + */ +@InternalSurfApi +class UnsupportedArgumentType(private val nodeName: String) : ArgumentType { + override fun parse(reader: StringReader): T = throw SimpleCommandExceptionType( + LiteralMessage("Argument '$nodeName' has no parser"), + ).createWithContext(reader) +} + +/** + * Base class of all command arguments. + * + * An argument is a node in a command: it parses one value, may be optional, carry permissions, + * requirements and a suggestion provider, and may have nested arguments and executors. Executors + * read the parsed value from [dev.slne.surf.api.minestom.command.executor.CommandArguments] by + * [nodeName]. + * + * Instances are mutable builders; the `with*`, `set*` and suggestion functions return the same + * instance. An argument is not thread-safe while it is being configured. + * + * @property nodeName name under which the parsed value is available; must not be blank + */ +abstract class Argument(val nodeName: String) : CommandExecutable> { + protected abstract val kind: ArgumentKind + + /** + * The Brigadier type that parses this argument's value. + */ + protected abstract val rawType: ArgumentType + + /** + * Whether this argument consumes the rest of the command line. + * + * A greedy argument must be the last argument of its path; registration throws + * [CommandValidationException] otherwise. + */ + protected open val greedy: KBoolean = false + + protected open val listDelimiter: Char? = null + + private var optional = false + private var defaultValue: ((CommandSender) -> T)? = null + private val permissions = ObjectLinkedOpenHashSet() + private val requirements = ObjectArrayList<(CommandSender) -> KBoolean>() + private var suggestions: SuggestionMode = SuggestionMode.BuiltIns + private val executors = ObjectArrayList() + private val children = ObjectArrayList>() + + init { + require(nodeName.isNotBlank()) { "Argument node name must not be blank" } + } + + protected abstract fun stringify(value: T): String + + /** + * Marks this argument as optional or required without assigning a default value. + * + * Making the argument required clears a default value. A required argument cannot follow an + * optional one on the same path; registration throws [CommandValidationException] otherwise. + * + * For a [BooleanArgument], use the named form `setOptional(optional = true)` to distinguish + * this overload from a constant default. + */ + fun setOptional(optional: KBoolean): Argument = apply { + this.optional = optional + if (!optional) { + defaultValue = null + } + } + + /** + * Makes this argument optional and uses [default] as its value when it is omitted. + * + * For a [BooleanArgument], use the named form `setOptional(default = true)` to distinguish + * the value from the optionality flag overload. + */ + fun setOptional(default: T): Argument = apply { + optional = true + defaultValue = { default } + } + + /** + * Makes this argument optional and computes its value from the sender when it is omitted. + */ + fun setOptional(default: (CommandSender) -> T): Argument = apply { + optional = true + defaultValue = default + } + + /** + * Requires [permission] to use this argument and the arguments nested below it. + * + * Multiple calls accumulate and every permission is required. + * + * @throws IllegalArgumentException when [permission] is blank + */ + fun withPermission(permission: String): Argument = apply { + val normalized = permission.trim() + require(normalized.isNotBlank()) { "Argument permission must not be blank" } + permissions += normalized + } + + /** + * Requires [requirement] to return `true` for the sender to use this argument and the arguments + * nested below it. Multiple requirements accumulate and all must pass. + */ + fun withRequirement(requirement: (CommandSender) -> KBoolean): Argument = apply { + requirements += requirement + } + + /** + * Offers only the entries of [suggestions], discarding the built-in suggestions of this + * argument. + * + * Replaces any suggestion provider configured before. Literal, command, position and rotation + * arguments do not support suggestion providers; registration throws + * [CommandValidationException] for them. + */ + fun replaceSuggestions(suggestions: ArgumentSuggestions): Argument = apply { + this.suggestions = SuggestionMode.Replace(suggestions) + } + + /** + * Offers the entries of [suggestions] in addition to the built-in suggestions of this argument. + * + * Replaces any suggestion provider configured before. Registration throws + * [CommandValidationException] for [EntityTypeArgument] and for the kinds listed at + * [replaceSuggestions]. + */ + fun includeSuggestions(suggestions: ArgumentSuggestions): Argument = apply { + this.suggestions = SuggestionMode.Include(suggestions) + } + + /** + * Offers only the values of [suggestions], converted to text with the formatter of this + * argument. + * + * Replaces any suggestion provider configured before. + */ + fun replaceSafeSuggestions(suggestions: SafeSuggestions): Argument = apply { + this.suggestions = SuggestionMode.ReplaceSafe(suggestions) + } + + /** + * Offers the values of [suggestions], converted to text with the formatter of this argument, + * in addition to the built-in suggestions. + * + * Replaces any suggestion provider configured before. + */ + fun includeSafeSuggestions(suggestions: SafeSuggestions): Argument = apply { + this.suggestions = SuggestionMode.IncludeSafe(suggestions) + } + + /** + * Adds an executor that runs when the command ends at this argument. + */ + override fun addExecutor(definition: ExecutorDefinition): Argument = apply { + executors += definition + } + + /** + * Adds [child] as an argument that follows this one. + * + * @throws IllegalArgumentException when [child] is this argument; longer cycles are rejected + * at registration + */ + fun then(child: Argument<*>): Argument = apply { + require(child !== this) { "An argument cannot be its own child" } + children += child + } + + @InternalSurfApi + internal fun toTreeNode(): ArgumentTreeNode = toTreeNode(ReferenceOpenHashSet()) + + private fun toTreeNode(stack: MutableSet>): ArgumentTreeNode { + if (!stack.add(this)) { + throw CommandValidationException("Argument '$nodeName' contains a child cycle") + } + try { + return ArgumentTreeNode( + argument = toDefinition(), + executors = ObjectLists.unmodifiable(ObjectArrayList(executors)), + children = ObjectLists.unmodifiable(children.mapTo(ObjectArrayList(children.size)) { child -> + child.toTreeNode(stack) + }), + ) + } finally { + stack.remove(this) + } + } + + @InternalSurfApi + fun toDefinition(): ArgumentDefinition = ArgumentDefinition( + nodeName = nodeName, + kind = kind, + optional = optional, + defaultValue = defaultValue, + permissions = ObjectSets.unmodifiable(ObjectLinkedOpenHashSet(permissions)), + requirements = ObjectLists.unmodifiable(ObjectArrayList(requirements)), + suggestions = suggestions, + greedy = greedy, + rawType = rawType, + stringify = ::stringify, + listDelimiter = listDelimiter, + ) +} + +@InternalSurfApi +internal data class ArgumentTreeNode( + val argument: ArgumentDefinition<*>, + val executors: KList, + val children: KList, + val permissions: Set = emptySet(), + val requirements: KList<(CommandSender) -> KBoolean> = emptyList(), +) diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/CompositeArguments.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/CompositeArguments.kt new file mode 100644 index 000000000..a36c98694 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/CompositeArguments.kt @@ -0,0 +1,316 @@ +/* + * Substantially translated from CommandAPI 12.0.0 (https://github.com/CommandAPI/CommandAPI). + * MIT License, Copyright (c) 2020 - 2022 Jorel Ali. + * The complete license is distributed in META-INF/LICENSES/CommandAPI-LICENSE.txt. + */ +package dev.slne.surf.api.minestom.command.argument + +import com.mojang.brigadier.LiteralMessage +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import dev.slne.surf.api.minestom.command.CommandAPI +import dev.slne.surf.api.minestom.command.exception.CommandValidationException +import dev.slne.surf.api.minestom.command.exception.ComponentMessage +import dev.slne.surf.api.minestom.command.exception.WrapperCommandSyntaxException +import net.minestom.server.command.CommandSender +import java.util.concurrent.CompletableFuture +import dev.slne.surf.api.minestom.command.exception.CommandSyntaxException as ApiCommandSyntaxException + +/** + * Input of a [CustomArgument] parser. + * + * @property sender the sender running the command + * @property currentInput the text consumed by the base argument + * @property baseValue the value the base argument parsed from [currentInput] + */ +data class CustomArgumentInfo( + val sender: CommandSender, + val currentInput: String, + val baseValue: B, +) + +/** + * Argument that parses its input with [base] and converts the result to [T] with a parser. + * + * The parser reports invalid input by throwing a [CommandSyntaxException], an + * [ApiCommandSyntaxException] (for example through [CommandAPI.failWithMessage]) or a + * [WrapperCommandSyntaxException]; the failure is shown at the start of the argument. Parsing needs + * a command source, so the argument cannot be parsed without one. + * + * The optional flag, default value, permissions and requirements of [base] carry over; a default + * value is passed through the parser with an empty [CustomArgumentInfo.currentInput]. Suggestions + * come from the raw type of [base]; suggestion providers set on [base] are not inherited. + * Values are stringified with `toString()`. + * + * @param base argument that reads the raw value + * @param parser converts the base value to [T] + * @throws CommandValidationException when [base] is a [LiteralArgument] or a + * [SignedMessageArgument], which have no parser to wrap + */ +open class CustomArgument internal constructor( + private val base: Argument, + private val formatter: (T) -> String, + private val parser: (CustomArgumentInfo) -> T, +) : Argument(base.nodeName) { + constructor( + base: Argument, + parser: (CustomArgumentInfo) -> T, + ) : this(base, { value -> value.toString() }, parser) + + init { + val baseDefinition = base.toDefinition() + requireParsable(baseDefinition.kind, nodeName, "base") + val defaultValue = baseDefinition.defaultValue + when { + defaultValue != null -> setOptional { sender -> + parser(CustomArgumentInfo(sender, "", defaultValue(sender))) + } + + baseDefinition.optional -> setOptional(optional = true) + } + baseDefinition.permissions.forEach(::withPermission) + baseDefinition.requirements.forEach(::withRequirement) + } + + override val kind: ArgumentKind + get() = ArgumentKind.Custom(base.toDefinition(), parser) + + override val rawType: ArgumentType = CustomArgumentType(base.toDefinition().rawType, parser) + + override val greedy: Boolean + get() = base.toDefinition().greedy + + override val listDelimiter: Char? + get() = base.toDefinition().listDelimiter + + override fun stringify(value: T): String = formatter(value) +} + +/** + * Rejects [kind]s that the platform builds without a parser, which would register a node that can + * never parse. + */ +private fun requireParsable(kind: ArgumentKind<*>, nodeName: String, role: String) { + val description = when (kind) { + is ArgumentKind.Literal -> "a literal argument" + is ArgumentKind.SignedMessage -> "a signed message argument" + else -> return + } + throw CommandValidationException("Argument '$nodeName' cannot use $description as its $role") +} + +private val CUSTOM_ARGUMENT_NEEDS_SENDER = SimpleCommandExceptionType( + LiteralMessage("A custom argument requires a command source to resolve"), +) + +/** + * Parses a value with [baseType] and converts it with [parser]. + * + * The overload without a source always fails, because [CustomArgumentInfo] requires a + * [CommandSender]. + * + * A Brigadier [CommandSyntaxException] from [parser] propagates unchanged. An + * [ApiCommandSyntaxException], also when wrapped in a [WrapperCommandSyntaxException], is converted + * into a Brigadier exception positioned at the start of the value, keeping its component message. + */ +private class CustomArgumentType( + private val baseType: ArgumentType, + private val parser: (CustomArgumentInfo) -> T, +) : ArgumentType { + override fun parse(reader: StringReader): T = throw CUSTOM_ARGUMENT_NEEDS_SENDER.createWithContext(reader) + + override fun parse(reader: StringReader, source: S): T { + val start = reader.cursor + val sender = source as? CommandSender ?: throw CUSTOM_ARGUMENT_NEEDS_SENDER.createWithContext(reader) + + val baseValue = baseType.parse(reader, source) + val consumed = reader.string.substring(start, reader.cursor) + val info = CustomArgumentInfo(sender, consumed, baseValue) + + return try { + parser(info) + } catch (failure: CommandSyntaxException) { + throw failure + } catch (failure: WrapperCommandSyntaxException) { + reader.cursor = start + throw failure.exception.toBrigadier(reader) + } catch (failure: ApiCommandSyntaxException) { + reader.cursor = start + throw failure.toBrigadier(reader) + } + } + + /** + * Delegates to [baseType]. + */ + override fun listSuggestions( + context: CommandContext, + builder: SuggestionsBuilder, + ): CompletableFuture = baseType.listSuggestions(context, builder) +} + +/** + * Converts this exception to a Brigadier exception at the cursor of [reader], keeping the + * component message when there is one. + */ +private fun ApiCommandSyntaxException.toBrigadier(reader: StringReader): CommandSyntaxException { + val message = component + ?.let(::ComponentMessage) + ?: LiteralMessage(this.message ?: "Invalid argument") + + return SimpleCommandExceptionType(message).createWithContext(reader) +} + +/** + * Delimiter-separated list of values parsed by [element]. + * + * Whitespace around the delimiter and around the whole list is skipped, so a list can span several + * space-separated tokens. The argument is greedy and must be the last argument of its path. + * Values are stringified with the formatter of the element and joined with the delimiter. + * + * Without [allowEmpty], an empty list and an empty element, such as a leading, trailing or doubled + * delimiter, fail with `List must not be empty`. With it, an empty input parses to an empty list. + * + * Permissions and requirements of [element] carry over. + * + * @param delimiter separator between elements; must be a visible non-whitespace character + * @throws IllegalArgumentException when [delimiter] is whitespace or the NUL character, or + * [element] is greedy + * @throws CommandValidationException when [element] is a [LiteralArgument] or a + * [SignedMessageArgument], which have no parser to repeat + */ +class ListArgument( + nodeName: String, + private val element: Argument, + val delimiter: Char = ',', + val allowEmpty: Boolean = false, +) : Argument>(nodeName) { + init { + val elementDefinition = element.toDefinition() + requireParsable(elementDefinition.kind, nodeName, "element") + require(!delimiter.isWhitespace() && delimiter != '\u0000') { + "List delimiter must be a visible non-whitespace character" + } + require(!elementDefinition.greedy) { "List elements cannot consume remaining input" } + elementDefinition.permissions.forEach(::withPermission) + elementDefinition.requirements.forEach(::withRequirement) + } + + override val kind: ArgumentKind> + get() = ArgumentKind.List(element.toDefinition(), delimiter, allowEmpty) + + override val rawType: ArgumentType> = + ListArgumentType(element.toDefinition().rawType, delimiter, allowEmpty) + + override val greedy = true + + override val listDelimiter: Char = delimiter + + override fun stringify(value: List): String { + val elementDefinition = element.toDefinition() + return value.joinToString(delimiter.toString(), transform = elementDefinition.stringify) + } +} + +private val LIST_ARGUMENT_EMPTY = SimpleCommandExceptionType(LiteralMessage("List must not be empty")) + +/** + * Parses elements with [elementType], separated by [delimiter] and optional spaces. + * + * Unless [allowEmpty] is set, an empty list and any element that consumed no input fail with + * `List must not be empty`. The empty-element check is made here because some element types, such + * as a bare word, accept an empty token. + */ +private class ListArgumentType( + private val elementType: ArgumentType, + private val delimiter: Char, + private val allowEmpty: Boolean, +) : ArgumentType> { + override fun parse(reader: StringReader): List = parseList(reader) { r -> elementType.parse(r) } + + override fun parse(reader: StringReader, source: S): List = + parseList(reader) { r -> elementType.parse(r, source) } + + /** + * Suggests entries for the element after the last delimiter and keeps the earlier elements. + */ + override fun listSuggestions( + context: CommandContext, + builder: SuggestionsBuilder, + ): CompletableFuture { + val typed = builder.remaining + val elementStart = typed.lastIndexOf(delimiter) + 1 + val offset = builder.start + elementStart + typed.drop(elementStart).takeWhile(Char::isWhitespace).length + + return elementType.listSuggestions(context, builder.createOffset(offset)) + } + + private inline fun parseList(reader: StringReader, parseElement: (StringReader) -> T): List { + val start = reader.cursor + skipWhitespace(reader) + if (!reader.canRead()) { + if (allowEmpty) return emptyList() + reader.cursor = start + throw LIST_ARGUMENT_EMPTY.createWithContext(reader) + } + + val values = ArrayList() + while (true) { + val elementStart = reader.cursor + val value = parseElement(reader) + if (!allowEmpty && reader.cursor == elementStart) { + reader.cursor = elementStart + throw LIST_ARGUMENT_EMPTY.createWithContext(reader) + } + values += value + + skipWhitespace(reader) + if (reader.canRead() && reader.peek() == delimiter) { + reader.skip() + skipWhitespace(reader) + } else { + break + } + } + return values + } + + private fun skipWhitespace(reader: StringReader) { + while (reader.canRead() && reader.peek() == ' ') { + reader.skip() + } + } +} + +/** + * Returns an argument that parses like this one and converts the value with [mapper]. + * + * The result is stringified with `toString()`, which matters for safe suggestions. + */ +fun Argument.map(mapper: (T) -> R): Argument = + CustomArgument(this, parser = { info -> mapper(info.baseValue) }) + +/** + * Returns an argument that parses like this one but rejects values for which [predicate] is + * `false`. + * + * A rejected value fails the command with `Invalid value for `. + */ +fun Argument.filter(predicate: (T) -> Boolean): Argument { + val definition = toDefinition() + return CustomArgument( + base = this, + parser = { info -> + if (!predicate(info.baseValue)) { + CommandAPI.failWithString("Invalid value for $nodeName") + } + info.baseValue + }, + formatter = definition.stringify, + ) +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/EntityArguments.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/EntityArguments.kt new file mode 100644 index 000000000..9895dd7a0 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/EntityArguments.kt @@ -0,0 +1,181 @@ +/* + * Substantially translated from CommandAPI 12.0.0 (https://github.com/CommandAPI/CommandAPI). + * MIT License, Copyright (c) 2020 - 2022 Jorel Ali. + * The complete license is distributed in META-INF/LICENSES/CommandAPI-LICENSE.txt. + */ +package dev.slne.surf.api.minestom.command.argument + +import com.mojang.brigadier.LiteralMessage +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import dev.slne.surf.api.minestom.command.argument.parser.EntitySelectorParser +import dev.slne.surf.api.minestom.command.argument.parser.RegistryParser +import net.minestom.server.command.CommandSender +import net.minestom.server.entity.Entity +import net.minestom.server.entity.EntityType +import net.minestom.server.entity.Player +import java.util.concurrent.CompletableFuture + +/** + * A single online player, given by name or by a selector. + * + * Selectors that can match more than one player, such as `@a`, fail at parse time. A selector that + * matches no player fails with `No entity matched the selector`. Parsing requires a command source. + */ +class PlayerArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.Player + override val rawType: ArgumentType = + SingleTargetType(maxResults = 1, playersOnly = true) { entities -> entities.filterIsInstance().singleOrNull() } + + override fun stringify(value: Player): String = value.username +} + +/** + * The online players matched by a player name or selector. + * + * Selectors that can match non-player entities, such as `@e`, fail at parse time. Without + * [allowEmpty], a selector that matches no player fails with `No entities matched the selector`; + * with it, the result is an empty list. Parsing requires a command source. + */ +class PlayersArgument( + nodeName: String, + val allowEmpty: Boolean = false, +) : Argument>(nodeName) { + override val kind = ArgumentKind.Players(allowEmpty) + override val rawType: ArgumentType> = MultiTargetType( + maxResults = Int.MAX_VALUE, + playersOnly = true, + allowEmpty = allowEmpty, + ) { entities -> entities.filterIsInstance() } + + override fun stringify(value: List): String = + value.joinToString(",", transform = Player::getUsername) +} + +/** + * A single entity, given by player name, UUID or selector. + * + * Selectors that can match more than one entity, such as `@e`, fail at parse time. A selector that + * matches no entity fails with `No entity matched the selector`. Parsing requires a command + * source. + */ +class EntityArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.Entity + override val rawType: ArgumentType = + SingleTargetType(maxResults = 1, playersOnly = false) { entities -> entities.singleOrNull() } + + override fun stringify(value: Entity): String = value.uuid.toString() +} + +/** + * The entities matched by a player name, UUID or selector. + * + * Without [allowEmpty], a selector that matches no entity fails with + * `No entities matched the selector`; with it, the result is an empty list. Parsing requires a + * command source. + */ +class EntitiesArgument( + nodeName: String, + val allowEmpty: Boolean = false, +) : Argument>(nodeName) { + override val kind = ArgumentKind.Entities(allowEmpty) + override val rawType: ArgumentType> = MultiTargetType( + maxResults = Int.MAX_VALUE, + playersOnly = false, + allowEmpty = allowEmpty, + ) { entities -> entities } + + override fun stringify(value: List): String = + value.joinToString(",") { entity -> entity.uuid.toString() } +} + +/** + * An entity type given by its namespaced key; the namespace defaults to `minecraft`. + */ +class EntityTypeArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.EntityType + override val rawType: ArgumentType = + RegistryParser("entity type") { key -> EntityType.fromKey(key) } + + override fun stringify(value: EntityType): String = value.key().asString() +} + +private val SELECTOR_NEEDS_SENDER = SimpleCommandExceptionType( + LiteralMessage("An entity selector requires a command source to resolve"), +) +private val NO_MATCH = SimpleCommandExceptionType(LiteralMessage("No entity matched the selector")) +private val NO_MATCHES = SimpleCommandExceptionType(LiteralMessage("No entities matched the selector")) + +/** + * Parses an entity selector and resolves the matched entities to a value of type [R]. + * + * Fails when [resolve] returns `null`, reporting [noMatch], and when no [CommandSender] source is + * available. + */ +private abstract class TargetType( + maxResults: Int, + playersOnly: Boolean, + private val noMatch: SimpleCommandExceptionType, +) : ArgumentType { + private val selector = EntitySelectorParser(maxResults, playersOnly) + + /** + * Converts the entities the selector matched to the parsed value, or `null` when there is no + * acceptable match. + */ + protected abstract fun resolve(entities: List): R? + + override fun parse(reader: StringReader): R = throw SELECTOR_NEEDS_SENDER.createWithContext(reader) + + override fun parse(reader: StringReader, source: S): R { + val start = reader.cursor + val parsed = selector.parse(reader) + val sender = source as? CommandSender ?: run { + reader.cursor = start + throw SELECTOR_NEEDS_SENDER.createWithContext(reader) + } + + return resolve(parsed.find(sender)) ?: run { + reader.cursor = start + throw noMatch.createWithContext(reader) + } + } + + override fun listSuggestions( + context: CommandContext, + builder: SuggestionsBuilder, + ): CompletableFuture = selector.listSuggestions(context, builder) +} + +/** + * Resolves an entity selector to a single entity of type [T]. + * + * Fails when the selector can match more than [maxResults] entities or when [select] returns + * `null`. + */ +private class SingleTargetType( + maxResults: Int, + playersOnly: Boolean, + private val select: (List) -> T?, +) : TargetType(maxResults, playersOnly, NO_MATCH) { + override fun resolve(entities: List): T? = select(entities) +} + +/** + * Resolves an entity selector to the matching entities of type [T]. + * + * Fails on an empty result unless [allowEmpty] is set. + */ +private class MultiTargetType( + maxResults: Int, + playersOnly: Boolean, + private val allowEmpty: Boolean, + private val select: (List) -> List, +) : TargetType>(maxResults, playersOnly, NO_MATCHES) { + override fun resolve(entities: List): List? = + select(entities).takeIf { results -> allowEmpty || results.isNotEmpty() } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/MinecraftArguments.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/MinecraftArguments.kt new file mode 100644 index 000000000..abc9c1a13 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/MinecraftArguments.kt @@ -0,0 +1,83 @@ +/* + * Substantially translated from CommandAPI 12.0.0 (https://github.com/CommandAPI/CommandAPI). + * MIT License, Copyright (c) 2020 - 2022 Jorel Ali. + * The complete license is distributed in META-INF/LICENSES/CommandAPI-LICENSE.txt. + */ +package dev.slne.surf.api.minestom.command.argument + +import com.mojang.brigadier.arguments.ArgumentType +import dev.slne.surf.api.minestom.command.argument.parser.FixedSetParser +import dev.slne.surf.api.minestom.command.argument.parser.ParticleParser +import dev.slne.surf.api.minestom.command.argument.parser.ResourceLocationParser +import dev.slne.surf.api.minestom.command.argument.parser.TimeParser +import net.kyori.adventure.key.Key +import net.minestom.server.MinecraftServer +import net.minestom.server.color.TeamColor +import net.minestom.server.entity.GameMode +import net.minestom.server.particle.Particle +import java.time.Duration + +/** + * A namespaced key such as `minecraft:stone`; the namespace defaults to `minecraft`. + */ +class ResourceLocationArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.ResourceLocation + override val rawType: ArgumentType = ResourceLocationParser + + override fun stringify(value: Key): String = value.asString() +} + +/** + * A [Duration] parsed from vanilla's time syntax: a number with an optional unit of `d` (days), + * `s` (seconds) or `t` (ticks), for example `5s`. Without a unit the number counts ticks. + * + * Negative values are rejected. + */ +class TimeArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.Time + override val rawType: ArgumentType = TimeParser + + override fun stringify(value: Duration): String = "${value.toMillis() / MinecraftServer.TICK_MS}t" +} + +/** + * A [TeamColor] parsed from its name as returned by [TeamColor.toString]. + * + * Parsing is case-sensitive; suggestions match case-insensitively. + */ +class TeamColorArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.TeamColor + override val rawType: ArgumentType = FixedSetParser( + TeamColor.entries.associateBy(TeamColor::toString), + ) + + override fun stringify(value: TeamColor): String = value.toString() +} + +/** + * A [Particle] given by its namespaced key. + * + * Only particles without options, such as `flame` or `smoke`, are accepted. Particles that take + * options, such as `dust`, `block` or `vibration`, are rejected as unsupported. + */ +class ParticleArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.Particle + override val rawType: ArgumentType = ParticleParser + + override fun stringify(value: Particle): String = value.name() +} + +/** + * A [GameMode] parsed from its lowercase name: `survival`, `creative`, `adventure` or + * `spectator`. + * + * Parsing is case-sensitive; suggestions match case-insensitively. + */ +class GameModeArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.GameMode + override val rawType: ArgumentType = FixedSetParser( + GameMode.entries.associateBy { mode -> mode.name.lowercase() }, + ) + + override fun stringify(value: GameMode): String = value.name.lowercase() +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/PositionArguments.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/PositionArguments.kt new file mode 100644 index 000000000..7e0006d76 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/PositionArguments.kt @@ -0,0 +1,107 @@ +/* + * Substantially translated from CommandAPI 12.0.0 (https://github.com/CommandAPI/CommandAPI). + * MIT License, Copyright (c) 2020 - 2022 Jorel Ali. + * The complete license is distributed in META-INF/LICENSES/CommandAPI-LICENSE.txt. + */ +package dev.slne.surf.api.minestom.command.argument + +import com.mojang.brigadier.arguments.ArgumentType +import dev.slne.surf.api.minestom.command.argument.parser.AngleParser +import dev.slne.surf.api.minestom.command.argument.parser.AxisParser +import dev.slne.surf.api.minestom.command.argument.parser.PositionArgumentType +import dev.slne.surf.api.minestom.command.argument.parser.RotationArgumentType +import net.minestom.server.coordinate.Vec +import kotlin.math.floor + +/** + * A world axis, as returned by [AxisArgument]. + */ +enum class Axis { + X, + Y, + Z, +} + +/** + * A view direction in degrees, as returned by [RotationArgument]. + */ +data class Rotation(val yaw: Float, val pitch: Float) + +/** + * A position of three coordinates, each absolute or `~`-relative, or three `^` local coordinates. + * + * The returned [Vec] is already resolved against the sender: relative coordinates are added to the + * sender's position and local coordinates follow its view direction. Parsing fails when a relative + * or local coordinate cannot be resolved because there is no command source. + */ +class PositionArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.Position + override val rawType: ArgumentType = + PositionArgumentType(blockAligned = false, twoDimensional = false) + override fun stringify(value: Vec): String = + "${value.x().toPlainDecimal()} ${value.y().toPlainDecimal()} ${value.z().toPlainDecimal()}" +} + +/** + * A horizontal position of two coordinates, `x` and `z`, each absolute or `~`-relative. + * + * The returned [Vec] is resolved against the sender and has `y` set to `0`. Local coordinates are + * not accepted. + */ +class Position2DArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.Position2D + override val rawType: ArgumentType = + PositionArgumentType(blockAligned = false, twoDimensional = true) + override fun stringify(value: Vec): String = + "${value.x().toPlainDecimal()} ${value.z().toPlainDecimal()}" +} + +/** + * A block position of three coordinates, each a whole number or `~`-relative, or three `^` local + * coordinates. + * + * Like [PositionArgument], the returned [Vec] is resolved against the sender. Relative offsets may + * be decimal, so the result is not guaranteed to be block-aligned. + */ +class BlockPositionArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.BlockPosition + override val rawType: ArgumentType = + PositionArgumentType(blockAligned = true, twoDimensional = false) + override fun stringify(value: Vec): String = + "${floor(value.x()).toInt()} ${floor(value.y()).toInt()} ${floor(value.z()).toInt()}" +} + +/** + * A rotation of yaw followed by pitch, each absolute or `~`-relative to the sender's view. + * + * The returned [Rotation] is resolved against the sender; a source that is not an entity has the + * origin view. + */ +class RotationArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.Rotation + override val rawType: ArgumentType = RotationArgumentType + override fun stringify(value: Rotation): String = + "${value.yaw.toPlainDecimal()} ${value.pitch.toPlainDecimal()}" +} + +/** + * An angle in degrees, absolute or relative to the sender's yaw with `~`, wrapped into + * `[-180, 180)`. + */ +class AngleArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.Angle + override val rawType: ArgumentType = AngleParser + override fun stringify(value: Float): String = value.toPlainDecimal() +} + +/** + * A set of axes written as a swizzle such as `xz`, case-insensitively. + * + * An unknown or repeated letter is rejected. The returned set keeps the written order. + */ +class AxisArgument(nodeName: String) : Argument>(nodeName) { + override val kind = ArgumentKind.Axis + override val rawType: ArgumentType> = AxisParser + override fun stringify(value: Set): String = + value.joinToString("") { axis -> axis.name.lowercase() } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/PrimitiveArguments.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/PrimitiveArguments.kt new file mode 100644 index 000000000..71d4dc7ba --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/PrimitiveArguments.kt @@ -0,0 +1,197 @@ +/* + * Substantially translated from CommandAPI 12.0.0 (https://github.com/CommandAPI/CommandAPI). + * MIT License, Copyright (c) 2020 - 2022 Jorel Ali. + * The complete license is distributed in META-INF/LICENSES/CommandAPI-LICENSE.txt. + */ +package dev.slne.surf.api.minestom.command.argument + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.* +import dev.slne.surf.api.minestom.command.argument.parser.FixedSetParser +import dev.slne.surf.api.minestom.command.argument.parser.FloatRangeParser +import dev.slne.surf.api.minestom.command.argument.parser.IntegerRangeParser +import dev.slne.surf.api.minestom.command.argument.parser.UuidParser +import net.minestom.server.utils.Range +import java.math.BigDecimal +import java.util.* + +/** + * A boolean written as `true` or `false`. + * + * For an optional argument with a constant default, pass the value by name: + * `setOptional(default = true)`; see [Argument.setOptional]. + */ +class BooleanArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.Boolean + override val rawType: ArgumentType = BoolArgumentType.bool() + override fun stringify(value: Boolean): String = value.toString() +} + +/** + * An integer between [min] and [max], both inclusive. + * + * @throws IllegalArgumentException when [min] is greater than [max] + */ +class IntegerArgument( + nodeName: String, + min: Int = Int.MIN_VALUE, + max: Int = Int.MAX_VALUE, +) : Argument(nodeName) { + override val kind = ArgumentKind.Integer(min, max) + override val rawType: ArgumentType = IntegerArgumentType.integer(min, max) + + init { + require(min <= max) { "Minimum integer value must not exceed maximum value" } + } + + override fun stringify(value: Int): String = value.toString() +} + +/** + * A long between [min] and [max], both inclusive. + * + * @throws IllegalArgumentException when [min] is greater than [max] + */ +class LongArgument( + nodeName: String, + min: Long = Long.MIN_VALUE, + max: Long = Long.MAX_VALUE, +) : Argument(nodeName) { + override val kind = ArgumentKind.Long(min, max) + override val rawType: ArgumentType = LongArgumentType.longArg(min, max) + + init { + require(min <= max) { "Minimum long value must not exceed maximum value" } + } + + override fun stringify(value: Long): String = value.toString() +} + +/** + * A float between [min] and [max], both inclusive. + * + * @throws IllegalArgumentException when [min] is greater than [max] or either bound is NaN + */ +class FloatArgument( + nodeName: String, + min: Float = -Float.MAX_VALUE, + max: Float = Float.MAX_VALUE, +) : Argument(nodeName) { + override val kind = ArgumentKind.Float(min, max) + override val rawType: ArgumentType = FloatArgumentType.floatArg(min, max) + + init { + require(!min.isNaN() && !max.isNaN() && min <= max) { + "Minimum float value must not exceed maximum value and bounds must not be NaN" + } + } + + override fun stringify(value: Float): String = value.toPlainDecimal() +} + +/** + * A double between [min] and [max], both inclusive. + * + * @throws IllegalArgumentException when [min] is greater than [max] or either bound is NaN + */ +class DoubleArgument( + nodeName: String, + min: Double = -Double.MAX_VALUE, + max: Double = Double.MAX_VALUE, +) : Argument(nodeName) { + override val kind = ArgumentKind.Double(min, max) + override val rawType: ArgumentType = DoubleArgumentType.doubleArg(min, max) + + init { + require(!min.isNaN() && !max.isNaN() && min <= max) { + "Minimum double value must not exceed maximum value and bounds must not be NaN" + } + } + + override fun stringify(value: Double): String = value.toPlainDecimal() +} + +/** + * One of the enum constants in [values], written as the string [formatter] produces for it. + * + * The formatter defaults to the lowercase constant name. Parsing is case-sensitive; suggestions + * match case-insensitively. + * + * @throws IllegalArgumentException when [values] is empty, or the formatted values are blank, contain + * a character Brigadier does not read in an unquoted word, or are not unique + */ +class EnumArgument>( + nodeName: String, + values: Collection, + private val formatter: (E) -> String = { value -> value.name.lowercase() }, +) : Argument(nodeName) { + private val values = EnumSet.copyOf(values) + + override val kind = ArgumentKind.Enum(this.values, formatter) + override val rawType: ArgumentType = FixedSetParser(this.values.associateBy(formatter)) + + init { + require(this.values.isNotEmpty()) { "Enum argument must contain at least one value" } + + val formattedValues = this.values.map(formatter) + + require(formattedValues.all(String::isNotBlank)) { "Formatted enum values must not be blank" } + require(formattedValues.all { value -> value.all(StringReader::isAllowedInUnquotedString) }) { + "Formatted enum values may only contain letters, digits, '_', '-', '.' and '+'" + } + require(formattedValues.distinct().size == formattedValues.size) { "Formatted enum values must be unique" } + } + + override fun stringify(value: E): String = formatter(value) +} + +/** + * A [UUID] written in dashed hex form. + */ +class UUIDArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.Uuid + override val rawType: ArgumentType = UuidParser + override fun stringify(value: UUID): String = value.toString() +} + +/** + * An integer range in vanilla's `min..max` syntax. + * + * An omitted bound defaults to `Int.MIN_VALUE` or `Int.MAX_VALUE`. A bare value denotes a range + * containing only that value. The minimum must not exceed the maximum. + */ +class IntegerRangeArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.IntegerRange + override val rawType: ArgumentType = IntegerRangeParser + override fun stringify(value: Range.Int): String = stringifyRange(value.min(), value.max()) +} + +/** + * A float range in vanilla's `min..max` syntax. + * + * An omitted bound defaults to `-Float.MAX_VALUE` or `Float.MAX_VALUE`. A bare value denotes a + * range containing only that value. The minimum must not exceed the maximum. + */ +class FloatRangeArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.FloatRange + override val rawType: ArgumentType = FloatRangeParser + override fun stringify(value: Range.Float): String = stringifyRange(value.min(), value.max()) +} + +private fun stringifyRange(min: Number?, max: Number?): String = when { + min != null && min == max -> min.toPlainDecimal() + else -> "${min.orEmpty()}..${max.orEmpty()}" +} + +private fun Number?.orEmpty(): String = this?.toPlainDecimal().orEmpty() + +/** + * Renders this number in plain decimal notation. + * + * Brigadier's number reader rejects the exponent that `toString()` emits for very small and very + * large floating-point values. + */ +internal fun Number.toPlainDecimal(): String { + val text = toString() + return if ('E' in text) BigDecimal(text).toPlainString() else text +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/RegistryArguments.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/RegistryArguments.kt new file mode 100644 index 000000000..9f42bf2b6 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/RegistryArguments.kt @@ -0,0 +1,108 @@ +/* + * Substantially translated from CommandAPI 12.0.0 (https://github.com/CommandAPI/CommandAPI). + * MIT License, Copyright (c) 2020 - 2022 Jorel Ali. + * The complete license is distributed in META-INF/LICENSES/CommandAPI-LICENSE.txt. + */ +package dev.slne.surf.api.minestom.command.argument + +import com.mojang.brigadier.arguments.ArgumentType +import dev.slne.surf.api.minestom.command.argument.parser.InstanceParser +import dev.slne.surf.api.minestom.command.argument.parser.RegistryParser +import net.minestom.server.MinecraftServer +import net.minestom.server.instance.Instance +import net.minestom.server.item.enchant.Enchantment +import net.minestom.server.potion.PotionEffect +import net.minestom.server.registry.Registry +import net.minestom.server.registry.RegistryKey +import net.minestom.server.sound.SoundEvent +import net.minestom.server.world.biome.Biome + +/** + * A [SoundEvent] given by its namespaced key; the namespace defaults to `minecraft`. + */ +class SoundArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.Sound + override val rawType: ArgumentType = RegistryParser( + registryName = "sound event", + keys = { SoundEvent.values().map(SoundEvent::name) }, + ) { key -> SoundEvent.fromKey(key) } + + override fun stringify(value: SoundEvent): String = value.name() +} + +/** + * A [PotionEffect] given by its namespaced key; the namespace defaults to `minecraft`. + */ +class PotionEffectArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.PotionEffect + override val rawType: ArgumentType = RegistryParser( + registryName = "potion effect", + keys = { PotionEffect.values().map { effect -> effect.key().asString() } }, + ) { key -> PotionEffect.fromKey(key) } + + override fun stringify(value: PotionEffect): String = value.key().asString() +} + +/** + * A biome given by its namespaced key, returned as a [RegistryKey]. + * + * Keys are looked up in the server's biome registry, so only registered biomes are accepted. + */ +class BiomeArgument(nodeName: String) : Argument>(nodeName) { + override val kind = ArgumentKind.Biome + override val rawType: ArgumentType> = RegistryParser( + registryName = "biome", + keys = { MinecraftServer.getBiomeRegistry().keys().map(RegistryKey::name) }, + ) { key -> MinecraftServer.getBiomeRegistry().getKey(key) } + + override fun stringify(value: RegistryKey): String = value.name() +} + +/** + * An enchantment given by its namespaced key, returned as a [RegistryKey]. + * + * Keys are looked up in the server's enchantment registry, so only registered enchantments are + * accepted. + */ +class EnchantmentArgument(nodeName: String) : Argument>(nodeName) { + override val kind = ArgumentKind.Enchantment + override val rawType: ArgumentType> = RegistryParser( + registryName = "enchantment", + keys = { + MinecraftServer.getEnchantmentRegistry().keys().map(RegistryKey::name) + }, + ) { key -> MinecraftServer.getEnchantmentRegistry().getKey(key) } + + override fun stringify(value: RegistryKey): String = value.name() +} + +/** + * An entry of [registry] given by its namespaced key, returned as a [RegistryKey]. + * + * @param identifier registry identifier sent to the client and used in the `Unknown ` + * failure message + */ +class ResourceArgument( + nodeName: String, + identifier: String, + registry: Registry, +) : Argument>(nodeName) { + override val kind = ArgumentKind.Resource(identifier, registry) + override val rawType: ArgumentType> = RegistryParser( + registryName = identifier, + keys = { registry.keys().map(RegistryKey::name) }, + ) { key -> registry.getKey(key) } + + override fun stringify(value: RegistryKey): String = value.name() +} + +/** + * A registered [Instance], given by UUID or by dimension name. + * + * A dimension name resolves only while exactly one instance uses it. + */ +class InstanceArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.Instance + override val rawType: ArgumentType = InstanceParser + override fun stringify(value: Instance): String = value.uuid.toString() +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/SignedMessageArgument.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/SignedMessageArgument.kt new file mode 100644 index 000000000..d0086daf8 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/SignedMessageArgument.kt @@ -0,0 +1,19 @@ +package dev.slne.surf.api.minestom.command.argument + +import com.mojang.brigadier.arguments.ArgumentType +import net.kyori.adventure.chat.SignedMessage + +/** + * The remainder of the command line as a [SignedMessage]. + * + * The message carries the client's signature when one was supplied for this argument; otherwise an + * unsigned message is created from the text. The argument is greedy and must be the last argument + * of its path. + */ +class SignedMessageArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.SignedMessage + override val rawType: ArgumentType = UnsupportedArgumentType(nodeName) + override val greedy = true + + override fun stringify(value: SignedMessage): String = value.message() +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/StringArguments.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/StringArguments.kt new file mode 100644 index 000000000..a425c0456 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/StringArguments.kt @@ -0,0 +1,109 @@ +/* + * Substantially translated from CommandAPI 12.0.0 (https://github.com/CommandAPI/CommandAPI). + * MIT License, Copyright (c) 2020 - 2022 Jorel Ali. + * The complete license is distributed in META-INF/LICENSES/CommandAPI-LICENSE.txt. + */ +package dev.slne.surf.api.minestom.command.argument + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.arguments.StringArgumentType +import dev.slne.surf.api.minestom.command.argument.parser.FixedSetParser +import it.unimi.dsi.fastutil.objects.ObjectList + +/** + * A single word, as read by Brigadier's `StringArgumentType.word()`: letters, digits and + * `_ - . +`. + */ +class StringArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.Word + override val rawType: ArgumentType = StringArgumentType.word() + override fun stringify(value: String): String = value +} + +/** + * A single word or a double- or single-quoted phrase, as read by Brigadier's + * `StringArgumentType.string()`. + * + * Values are stringified as a bare word when every character is allowed unquoted, otherwise as a + * double-quoted phrase that escapes only `\` and `"`. + */ +class TextArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.Text + override val rawType: ArgumentType = StringArgumentType.string() + override fun stringify(value: String): String = StringArgumentType.escapeIfRequired(value) +} + +/** + * The remainder of the command line as one string. + * + * The argument is greedy and must be the last argument of its path. + */ +class GreedyStringArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.GreedyString + override val rawType: ArgumentType = StringArgumentType.greedyString() + override val greedy = true + override fun stringify(value: String): String = value +} + +/** + * A fixed keyword that the input must match exactly. The value is the keyword itself. + * + * A literal is matched by the command tree rather than parsed, so it cannot carry a suggestion + * provider. + * + * @param literal keyword to match; defaults to [nodeName] + * @throws IllegalArgumentException when [literal] is blank or contains whitespace + */ +class LiteralArgument( + nodeName: String, + val literal: String = nodeName, +) : Argument(nodeName) { + override val kind = ArgumentKind.Literal(literal) + override val rawType: ArgumentType = UnsupportedArgumentType(nodeName) + + init { + require(literal.isNotBlank()) { "Literal value must not be blank" } + require(literal.none(Char::isWhitespace)) { "Literal value must be a single token" } + } + + override fun stringify(value: String): String = value +} + +/** + * One of several fixed keywords. The value is the matched keyword. + * + * Matching is case-sensitive; suggestions match case-insensitively. + * + * @throws IllegalArgumentException when no literal is given, or a literal is blank, contains a + * character Brigadier does not read in an unquoted word, or is duplicated + */ +class MultiLiteralArgument(nodeName: String, vararg literals: String) : Argument(nodeName) { + private val literals: List = ObjectList.of(*literals) + override val kind = ArgumentKind.MultiLiteral(this.literals) + override val rawType: ArgumentType = FixedSetParser(this.literals.associateWith { it }) + + init { + require(this.literals.isNotEmpty()) { "Multi-literal argument must contain at least one literal" } + require(this.literals.all(String::isNotBlank)) { "Literal values must not be blank" } + require(this.literals.all { literal -> literal.all(StringReader::isAllowedInUnquotedString) }) { + "Multi-literal values may only contain letters, digits, '_', '-', '.' and '+'" + } + require(this.literals.distinct().size == this.literals.size) { "Literal values must be unique" } + } + + override fun stringify(value: String): String = value +} + +/** + * The remainder of the command line as one string, declared to the client as a command argument. + * + * The argument is greedy and must be the last argument of its path. + */ +class CommandArgument(nodeName: String) : Argument(nodeName) { + override val kind = ArgumentKind.Command + override val rawType: ArgumentType = StringArgumentType.greedyString() + override val greedy = true + + override fun stringify(value: String): String = value +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/CoordinateParsers.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/CoordinateParsers.kt new file mode 100644 index 000000000..78d9d3d92 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/CoordinateParsers.kt @@ -0,0 +1,268 @@ +package dev.slne.surf.api.minestom.command.argument.parser + +import com.mojang.brigadier.LiteralMessage +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType +import dev.slne.surf.api.minestom.command.argument.Axis +import dev.slne.surf.api.minestom.command.argument.Rotation +import net.minestom.server.command.CommandSender +import net.minestom.server.coordinate.Pos +import net.minestom.server.coordinate.Vec +import net.minestom.server.entity.Entity +import net.minestom.server.utils.location.RelativeVec + +private val ERROR_MIXED_TYPE = SimpleCommandExceptionType( + LiteralMessage("Cannot mix world and local coordinates in a single position"), +) +private val ERROR_EXPECTED_VALUE = SimpleCommandExceptionType(LiteralMessage("Expected a coordinate value")) +private val ERROR_INCOMPLETE = SimpleCommandExceptionType(LiteralMessage("Incomplete coordinate")) +private val ERROR_INVALID_SWIZZLE = SimpleCommandExceptionType(LiteralMessage("Invalid swizzle")) +private val ERROR_REQUIRES_SENDER = SimpleCommandExceptionType( + LiteralMessage("A relative (~) or local (^) coordinate requires a command source to resolve"), +) + +private data class Component(val relative: Boolean, val value: Double) + +/** + * Reads one world coordinate: an absolute number, or `~` with an optional offset that defaults + * to `0`. A leading `^` is rejected as a mix of world and local coordinates. + * + * With [blockAligned], absolute values must be whole numbers; `~` offsets may always be decimal. + */ +private fun readWorldComponent(reader: StringReader, blockAligned: Boolean): Component { + if (reader.canRead() && reader.peek() == '^') throw ERROR_MIXED_TYPE.createWithContext(reader) + if (!reader.canRead()) throw ERROR_EXPECTED_VALUE.createWithContext(reader) + + val relative = reader.peek() == '~' + if (relative) reader.skip() + + val hasNumber = reader.canRead() && reader.peek() != ' ' + val value = when { + !hasNumber -> 0.0 + blockAligned && !relative -> reader.readInt().toDouble() + else -> reader.readDouble() + } + return Component(relative, value) +} + +/** + * Reads one `^`-prefixed local coordinate; any other leading character is rejected as a mix of + * world and local coordinates. + */ +private fun readLocalComponent(reader: StringReader): Double { + if (!reader.canRead()) throw ERROR_EXPECTED_VALUE.createWithContext(reader) + if (reader.peek() != '^') throw ERROR_MIXED_TYPE.createWithContext(reader) + reader.skip() + return if (reader.canRead() && reader.peek() != ' ') reader.readDouble() else 0.0 +} + +private fun expectSeparator(reader: StringReader) { + if (reader.canRead() && reader.peek() == ' ') { + reader.skip() + } else { + throw ERROR_INCOMPLETE.createWithContext(reader) + } +} + +private fun coordinateTypeOf(vararg relative: Boolean): RelativeVec.CoordinateType = + if (relative.any { it }) RelativeVec.CoordinateType.RELATIVE else RelativeVec.CoordinateType.ABSOLUTE + +/** + * Rebuilds this exception with its cursor at [start] and moves [reader] there, because + * [SimpleCommandExceptionType.createWithContext] fixes the cursor at construction. + */ +private fun CommandSyntaxException.resetTo(reader: StringReader, start: Int): CommandSyntaxException { + reader.cursor = start + return CommandSyntaxException(type, rawMessage, reader.string, start) +} + +/** + * Returns [relative] unless [sender] is `null` and a component is relative or local. + * + * Resolving such a component without a sender would substitute the origin, so it is rejected + * instead, with [reader] reset to [start]. + */ +private fun requireResolvable( + relative: RelativeVec, + sender: CommandSender?, + reader: StringReader, + start: Int, +): RelativeVec { + if (sender == null && (relative.relativeX() || relative.relativeY() || relative.relativeZ())) { + reader.cursor = start + throw ERROR_REQUIRES_SENDER.createWithContext(reader) + } + return relative +} + +/** + * Parses a vanilla position into an unresolved [RelativeVec]. + * + * A position has three components, or `x` and `z` only when [twoDimensional]; the 2D form leaves + * `y` as an absolute `0`. Each component is absolute or `~`-relative. The three-component form + * alternatively accepts `^` local coordinates (left, up, forward) for all components; mixing local + * with other components is rejected. With [blockAligned], absolute components must be whole + * numbers. + * + * Failures reset the reader to the start of the position. Resolve the result with + * [RelativeVec.fromSender] or [RelativeVec.from]. + */ +internal class PositionParser( + private val blockAligned: Boolean, + private val twoDimensional: Boolean, +) : ArgumentType { + override fun parse(reader: StringReader): RelativeVec { + val start = reader.cursor + try { + return if (!twoDimensional && reader.canRead() && reader.peek() == '^') { + parseLocal(reader) + } else if (twoDimensional) { + parseWorld2D(reader) + } else { + parseWorld3D(reader) + } + } catch (failure: CommandSyntaxException) { + throw failure.resetTo(reader, start) + } + } + + private fun parseWorld3D(reader: StringReader): RelativeVec { + val x = readWorldComponent(reader, blockAligned) + expectSeparator(reader) + val y = readWorldComponent(reader, blockAligned) + expectSeparator(reader) + val z = readWorldComponent(reader, blockAligned) + return RelativeVec( + Vec(x.value, y.value, z.value), + coordinateTypeOf(x.relative, y.relative, z.relative), + x.relative, + y.relative, + z.relative, + ) + } + + private fun parseWorld2D(reader: StringReader): RelativeVec { + val x = readWorldComponent(reader, blockAligned) + expectSeparator(reader) + val z = readWorldComponent(reader, blockAligned) + return RelativeVec( + Vec(x.value, 0.0, z.value), + coordinateTypeOf(x.relative, z.relative), + x.relative, + false, + z.relative, + ) + } + + private fun parseLocal(reader: StringReader): RelativeVec { + val left = readLocalComponent(reader) + expectSeparator(reader) + val up = readLocalComponent(reader) + expectSeparator(reader) + val forward = readLocalComponent(reader) + return RelativeVec(Vec(left, up, forward), RelativeVec.CoordinateType.LOCAL, true, true, true) + } +} + +/** + * Parses a vanilla rotation, a yaw followed by a pitch, each absolute or `~`-relative to the + * sender's view. Local (`^`) components are rejected. + * + * The result stores the yaw in `x` and the pitch in `z`; resolve it with [RelativeVec.fromView]. + */ +internal object RotationParser : ArgumentType { + override fun parse(reader: StringReader): RelativeVec { + val start = reader.cursor + try { + val yaw = readWorldComponent(reader, blockAligned = false) + expectSeparator(reader) + val pitch = readWorldComponent(reader, blockAligned = false) + return RelativeVec( + Vec(yaw.value, 0.0, pitch.value), + coordinateTypeOf(yaw.relative, pitch.relative), + yaw.relative, + false, + pitch.relative, + ) + } catch (failure: CommandSyntaxException) { + throw failure.resetTo(reader, start) + } + } +} + +/** + * Parses a swizzle of world axes such as `xyz` or `xz`, case-insensitively. + * + * An unknown or repeated letter fails at the start of the swizzle. The returned set keeps the + * order the axes were written in. + */ +internal object AxisParser : ArgumentType> { + override fun parse(reader: StringReader): Set { + val start = reader.cursor + val axes = LinkedHashSet() + while (reader.canRead() && reader.peek() != ' ') { + val axis = when (reader.read().lowercaseChar()) { + 'x' -> Axis.X + 'y' -> Axis.Y + 'z' -> Axis.Z + else -> { + reader.cursor = start + throw ERROR_INVALID_SWIZZLE.createWithContext(reader) + } + } + if (!axes.add(axis)) { + reader.cursor = start + throw ERROR_INVALID_SWIZZLE.createWithContext(reader) + } + } + return axes + } +} + +/** + * Adapts [PositionParser] to the resolved [Vec] exposed by + * [dev.slne.surf.api.minestom.command.argument.PositionArgument] and its 2D and block variants. + * + * Relative and local coordinates are resolved against the command source. Without a + * [CommandSender] source they are rejected. + */ +internal class PositionArgumentType( + blockAligned: Boolean, + twoDimensional: Boolean, +) : ArgumentType { + private val delegate = PositionParser(blockAligned, twoDimensional) + + override fun parse(reader: StringReader): Vec = resolve(reader, null) + + override fun parse(reader: StringReader, source: S): Vec = resolve(reader, source as? CommandSender) + + private fun resolve(reader: StringReader, sender: CommandSender?): Vec { + val start = reader.cursor + val relative = delegate.parse(reader) + return requireResolvable(relative, sender, reader, start).fromSender(sender) + } +} + +/** + * Adapts [RotationParser] to the resolved [Rotation] exposed by + * [dev.slne.surf.api.minestom.command.argument.RotationArgument]. + * + * A relative yaw or pitch is resolved against the view of the command source, which is the origin + * view for a source that is not an [Entity]. Without a [CommandSender] source, relative values are + * rejected. + */ +internal object RotationArgumentType : ArgumentType { + override fun parse(reader: StringReader): Rotation = resolve(reader, null) + + override fun parse(reader: StringReader, source: S): Rotation = resolve(reader, source as? CommandSender) + + private fun resolve(reader: StringReader, sender: CommandSender?): Rotation { + val start = reader.cursor + val relative = RotationParser.parse(reader) + val checked = requireResolvable(relative, sender, reader, start) + val view = checked.fromView((sender as? Entity)?.position ?: Pos.ZERO) + return Rotation(view.x().toFloat(), view.z().toFloat()) + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/EntitySelector.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/EntitySelector.kt new file mode 100644 index 000000000..24e4078bf --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/EntitySelector.kt @@ -0,0 +1,85 @@ +package dev.slne.surf.api.minestom.command.argument.parser + +import net.minestom.server.MinecraftServer +import net.minestom.server.command.CommandSender +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.Entity +import net.minestom.server.instance.Instance +import java.util.* + +/** + * Entity selector produced by [EntitySelectorParser]. + * + * [find] resolves a selector given as a bare [playerName] or [uuid] directly; [predicates] do not + * apply to those forms. Any other selector takes its candidates from the sender for [self], from + * the online players, or, when [includesEntities] is set, from the entities of the searched + * instances. Candidates are filtered by [predicates], ordered by [sorter] and truncated to the + * smaller of [limit] and [maxResults]. + * + * [predicates] and [sorter] receive the selector origin: the sender's position, or the world origin + * when the sender is not an [Entity], with each axis replaced by the `x`, `y` or `z` option + * through [position]. + * + * When [worldLimited] is set and the sender is an entity in an instance, only that instance is + * searched; otherwise all instances are. + */ +internal class EntitySelector( + val maxResults: Int, + val includesEntities: Boolean, + val predicates: List<(Pos, Entity) -> Boolean>, + val sorter: ((Pos) -> Comparator)?, + val limit: Int, + val position: (Pos) -> Pos, + val self: Boolean, + val playerName: String?, + val uuid: UUID?, + private val worldLimited: Boolean = false, +) { + fun find(sender: CommandSender): List { + playerName?.let { name -> + return listOfNotNull(MinecraftServer.getConnectionManager().getOnlinePlayerByUsername(name)) + } + uuid?.let { id -> + return listOfNotNull(findByUuid(id)) + } + + val origin = position((sender as? Entity)?.position ?: Pos.ZERO) + if (self) { + val entity = sender as? Entity ?: return emptyList() + return if (matches(origin, entity)) listOf(entity) else emptyList() + } + + val senderInstance = (sender as? Entity)?.instance + val candidates = if (includesEntities) { + instancesToSearch(senderInstance).asSequence().flatMap { it.entities.asSequence() } + } else { + MinecraftServer.getConnectionManager().onlinePlayers.asSequence() + .filter { player -> !worldLimited || senderInstance == null || player.instance === senderInstance } + } + + val filtered = candidates + .filter { entity -> !entity.isRemoved } + .filter { entity -> matches(origin, entity) } + val resultLimit = minOf(limit, maxResults) + val comparator = sorter?.invoke(origin) ?: return filtered.take(resultLimit).toList() + + if (resultLimit == 1) return listOfNotNull(filtered.minWithOrNull(comparator)) + return filtered.toList().sortedWith(comparator).take(resultLimit) + } + + private fun matches(origin: Pos, entity: Entity): Boolean = predicates.all { predicate -> predicate(origin, entity) } + + private fun findByUuid(id: UUID): Entity? { + MinecraftServer.getConnectionManager().getOnlinePlayerByUuid(id)?.let { return it } + return MinecraftServer.getInstanceManager().instances.asSequence() + .flatMap { instance -> instance.entities.asSequence() } + .find { entity -> entity.uuid == id } + } + + private fun instancesToSearch(senderInstance: Instance?): Collection = + if (worldLimited && senderInstance != null) { + listOf(senderInstance) + } else { + MinecraftServer.getInstanceManager().instances + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/EntitySelectorParser.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/EntitySelectorParser.kt new file mode 100644 index 000000000..5d2ce7790 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/EntitySelectorParser.kt @@ -0,0 +1,722 @@ +package dev.slne.surf.api.minestom.command.argument.parser + +import com.mojang.brigadier.LiteralMessage +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import it.unimi.dsi.fastutil.objects.ObjectList +import net.kyori.adventure.key.Key +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import net.minestom.server.MinecraftServer +import net.minestom.server.component.DataComponents +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.* +import net.minestom.server.tag.Tag +import net.minestom.server.utils.Range +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ThreadLocalRandom + +/** + * Parses a vanilla entity selector: `@a`, `@e`, `@n`, `@p`, `@r` or `@s` with an optional + * `[key=value,...]` option list, or a bare player name or UUID. + * + * A selector whose limit exceeds [maxResults] fails at parse time instead of being truncated. The + * limit is implied by the selector type or set with the `limit` option. With [playersOnly], a + * selector that can match non-player entities fails, except `@s`. `@e` and `@n` skip dead living + * entities. + * + * Supported options are `x`, `y`, `z`, `dx`, `dy`, `dz`, `distance`, `limit`, `sort`, `name`, + * `type` (including `#tag`), `tag`, `team`, `gamemode`, `level`, `x_rotation` and `y_rotation`. + * `nbt` is rejected because SNBT is not parsed. `scores`, `advancements` and `predicate` are + * rejected because Minestom has no matching data source; their value is read first, so a + * malformed value is reported before the unsupported option. + */ +internal class EntitySelectorParser( + private val maxResults: Int, + private val playersOnly: Boolean, +) : ArgumentType { + + override fun parse(reader: StringReader): EntitySelector { + val start = reader.cursor + val state = MutableSelector() + + if (reader.canRead() && reader.peek() == '@') { + reader.skip() + parseSelectorType(reader, state) + } else { + parseNameOrUuid(reader, state) + } + finalizePredicates(state) + + val selector = state.toSelector(maxResults) + if (selector.limit > maxResults) { + reader.cursor = start + throw (if (playersOnly) ERROR_NOT_SINGLE_PLAYER else ERROR_NOT_SINGLE_ENTITY).createWithContext( + reader + ) + } + if (selector.includesEntities && playersOnly && !selector.self) { + reader.cursor = start + throw ERROR_ONLY_PLAYERS_ALLOWED.createWithContext(reader) + } + return selector + } + + /** + * Suggests the names of the online players, then the selectors this argument accepts. + * + * Selectors whose implied limit exceeds [maxResults] are omitted, as is `@e` when [playersOnly]. + */ + override fun listSuggestions( + context: CommandContext, + builder: SuggestionsBuilder, + ): CompletableFuture { + val players = MinecraftServer.getConnectionManager().onlinePlayers + val values = ObjectArrayList(ENTITY_SELECTORS.size) + + val prefix = builder.remaining + players.forEach { player -> + val username = player.username + if (username.startsWith(prefix, ignoreCase = true)) values += username + } + values.sortWith(String.CASE_INSENSITIVE_ORDER) + + val selectors = if (playersOnly) PLAYER_SELECTORS else ENTITY_SELECTORS + selectors.forEach { selector -> + if (maxResults > 1 || selector in SINGLE_SELECTORS) values += selector + } + + return builder.suggestMatching(values) + } + + private companion object { + val SINGLE_SELECTORS: ObjectList = ObjectList.of("@n", "@p", "@r", "@s") + val PLAYER_SELECTORS: ObjectList = ObjectList.of("@p", "@r", "@a", "@s") + val ENTITY_SELECTORS: ObjectList = ObjectList.of("@p", "@r", "@a", "@e", "@n", "@s") + } +} + +/** + * Mutable state of a selector while it is parsed; [toSelector] converts it into an [EntitySelector]. + */ +private class MutableSelector { + var limit: Int = 1 + var includesEntities: Boolean = false + var self: Boolean = false + var playerName: String? = null + var uuid: UUID? = null + var sorter: ((Pos) -> Comparator)? = null + var worldLimited: Boolean = false + + /** + * Values of the `x`, `y`, `z`, `dx`, `dy` and `dz` options by option name. + */ + val coordinates: MutableMap = mutableMapOf() + var distance: Range.Float? = null + val predicates: MutableList<(Pos, Entity) -> Boolean> = mutableListOf() + + /** + * Options already used among those allowed at most once: + * `x y z dx dy dz distance limit sort level x_rotation y_rotation`. + */ + val singleUseSeen: MutableSet = mutableSetOf() + private val invertableStates: MutableMap = mutableMapOf() + + /** + * Returns the usage state of the invertable option [key], created on first use. + */ + fun invertableStateFor(key: String): InvertableState = + invertableStates.getOrPut(key) { InvertableState() } + + fun toSelector(maxResults: Int): EntitySelector = EntitySelector( + maxResults = maxResults, + includesEntities = includesEntities, + predicates = predicates.toList(), + sorter = sorter, + limit = limit, + position = originOf(coordinates), + self = self, + playerName = playerName, + uuid = uuid, + worldLimited = worldLimited, + ) +} + +/** + * Tracks the use of an option that accepts negated (`!`) values, mirroring vanilla's + * `InvertableSetOptionState`. + * + * Such an option may be given once with a positive value or any number of times with negated + * values. Mixing the two, or repeating a positive value, is not allowed. A tag counts as a negated + * value for this rule and may be given once. + */ +private class InvertableState { + private var limitation = Limitation.NONE + private val tags: MutableSet = mutableSetOf() + + fun canParseElement(inverted: Boolean): Boolean = + if (inverted) limitation != Limitation.SINGLE else limitation == Limitation.NONE + + fun markParsedElement(inverted: Boolean) { + limitation = if (inverted) Limitation.MULTIPLE else Limitation.SINGLE + } + + fun canParseTag(tag: Key): Boolean = limitation != Limitation.SINGLE && tag !in tags + + fun markParsedTag(tag: Key) { + limitation = Limitation.MULTIPLE + tags += tag + } + + private enum class Limitation { NONE, SINGLE, MULTIPLE } +} + +private fun parseNameOrUuid(reader: StringReader, state: MutableSelector) { + val start = reader.cursor + val name = reader.readString() + val uuid = parseDashedUuid(name) + + if (uuid != null) { + state.uuid = uuid + state.includesEntities = true + } else { + if (name.isEmpty() || name.length > 16) { + reader.cursor = start + throw ERROR_INVALID_NAME_OR_UUID.createWithContext(reader) + } + state.playerName = name + state.includesEntities = false + } + state.limit = 1 +} + +private fun parseSelectorType(reader: StringReader, state: MutableSelector) { + if (!reader.canRead()) throw ERROR_MISSING_SELECTOR_TYPE.createWithContext(reader) + val typeStart = reader.cursor + val type = reader.read() + + when (type) { + 'a' -> { + state.limit = Int.MAX_VALUE + state.includesEntities = false + } + + 'e' -> { + state.limit = Int.MAX_VALUE + state.includesEntities = true + state.predicates += ::isAlive + } + + 'n' -> { + state.limit = 1 + state.includesEntities = true + state.sorter = ::nearestComparator + state.predicates += ::isAlive + } + + 'p' -> { + state.limit = 1 + state.includesEntities = false + state.sorter = ::nearestComparator + } + + 'r' -> { + state.limit = 1 + state.includesEntities = false + state.sorter = ::randomComparator + } + + 's' -> { + state.limit = 1 + state.includesEntities = true + state.self = true + } + + else -> { + reader.cursor = typeStart + throw ERROR_UNKNOWN_SELECTOR_TYPE.createWithContext(reader, "@$type") + } + } + + if (reader.canRead() && reader.peek() == '[') { + reader.skip() + parseOptions(reader, state) + } +} + +private fun parseOptions(reader: StringReader, state: MutableSelector) { + reader.skipWhitespace() + while (reader.canRead() && reader.peek() != ']') { + reader.skipWhitespace() + val keyStart = reader.cursor + val key = reader.readString() + reader.skipWhitespace() + if (!reader.canRead() || reader.peek() != '=') { + reader.cursor = keyStart + throw ERROR_EXPECTED_OPTION_VALUE.createWithContext(reader, key) + } + reader.skip() + reader.skipWhitespace() + applyOption(key, keyStart, reader, state) + reader.skipWhitespace() + if (reader.canRead()) { + if (reader.peek() != ',') { + if (reader.peek() != ']') throw ERROR_EXPECTED_END_OF_OPTIONS.createWithContext( + reader + ) + break + } + reader.skip() + reader.skipWhitespace() + } + } + + if (reader.canRead()) { + reader.skip() + } else { + throw ERROR_EXPECTED_END_OF_OPTIONS.createWithContext(reader) + } +} + +private fun applyOption(key: String, keyStart: Int, reader: StringReader, state: MutableSelector) { + val valueStart = reader.cursor + when (key) { + "x", "y", "z", "dx", "dy", "dz" -> { + requireSingleUse(key, keyStart, reader, state) + state.worldLimited = true + state.coordinates[key] = reader.readDouble() + } + + "distance" -> { + requireSingleUse(key, keyStart, reader, state) + val range = FloatRangeParser.parse(reader) + if (range.hasNegativeBound()) { + reader.cursor = valueStart + throw ERROR_DISTANCE_NEGATIVE.createWithContext(reader) + } + state.worldLimited = true + state.distance = range + } + + "limit" -> { + requireSingleUse(key, keyStart, reader, state, applicable = !state.self) + val count = reader.readInt() + if (count < 1) { + reader.cursor = keyStart + throw ERROR_LIMIT_TOO_SMALL.createWithContext(reader) + } + state.limit = count + } + + "sort" -> { + requireSingleUse(key, keyStart, reader, state, applicable = !state.self) + val name = reader.readUnquotedString() + state.sorter = when (name) { + "nearest" -> ::nearestComparator + "furthest" -> ::furthestComparator + "random" -> ::randomComparator + "arbitrary" -> null + else -> { + reader.cursor = keyStart + throw ERROR_SORT_UNKNOWN.createWithContext(reader, name) + } + } + } + + "name" -> { + val inverted = shouldInvertValue(reader) + requireInvertableUse(key, inverted, valueStart, reader, state) + val name = reader.readString() + state.predicates += { _, entity -> matchesName(entity, name) != inverted } + } + + "type" -> { + val inverted = shouldInvertValue(reader) + if (consumeTagMarker(reader)) { + applyTypeTag(inverted, valueStart, reader, state) + return + } + requireInvertableUse(key, inverted, valueStart, reader, state) + val typeStart = reader.cursor + val key0 = ResourceLocationParser.readKey(reader) + val type = EntityType.fromKey(key0) ?: run { + reader.cursor = typeStart + throw ERROR_ENTITY_TYPE_INVALID.createWithContext(reader, key0.asString()) + } + if (type == EntityType.PLAYER && !inverted) { + state.includesEntities = false + } + state.predicates += { _, entity -> (entity.entityType == type) != inverted } + } + + "tag" -> { + val inverted = shouldInvertValue(reader) + val tag = reader.readUnquotedString() + state.predicates += { _, entity -> + val tags = entity.getTag(ENTITY_TAGS) ?: emptyList() + (if (tag.isEmpty()) tags.isEmpty() else tags.contains(tag)) != inverted + } + } + + "team" -> { + val inverted = shouldInvertValue(reader) + requireInvertableUse(key, inverted, valueStart, reader, state) + val team = reader.readUnquotedString() + state.predicates += { _, entity -> (teamNameOf(entity) == team) != inverted } + } + + "gamemode" -> { + val inverted = shouldInvertValue(reader) + requireInvertableUse(key, inverted, valueStart, reader, state) + val mode = GAME_MODE_PARSER.parse(reader) + state.includesEntities = false + state.predicates += { _, entity -> (entity is Player && entity.gameMode == mode) != inverted } + } + + "level" -> { + requireSingleUse(key, keyStart, reader, state) + val range = IntegerRangeParser.parse(reader) + if (range.hasNegativeBound()) { + reader.cursor = valueStart + throw ERROR_LEVEL_NEGATIVE.createWithContext(reader) + } + state.includesEntities = false + state.predicates += { _, entity -> entity is Player && range.inRange(entity.level) } + } + + "x_rotation" -> { + requireSingleUse(key, keyStart, reader, state) + state.predicates += rotationPredicate(FloatRangeParser.parse(reader)) { entity -> entity.position.pitch() } + } + + "y_rotation" -> { + requireSingleUse(key, keyStart, reader, state) + state.predicates += rotationPredicate(FloatRangeParser.parse(reader)) { entity -> entity.position.yaw() } + } + + "scores" -> { + consumeScores(reader) + reader.cursor = keyStart + throw ERROR_UNSUPPORTED_OPTION.createWithContext(reader, key) + } + + "advancements" -> { + consumeAdvancements(reader) + reader.cursor = keyStart + throw ERROR_UNSUPPORTED_OPTION.createWithContext(reader, key) + } + + "predicate" -> { + shouldInvertValue(reader) + ResourceLocationParser.readKey(reader) + reader.cursor = keyStart + throw ERROR_UNSUPPORTED_OPTION.createWithContext(reader, key) + } + + "nbt" -> { + reader.cursor = keyStart + throw ERROR_NBT_UNSUPPORTED.createWithContext(reader) + } + + else -> { + reader.cursor = keyStart + throw ERROR_UNKNOWN_OPTION.createWithContext(reader, key) + } + } +} + +private fun consumeTagMarker(reader: StringReader): Boolean { + reader.skipWhitespace() + if (reader.canRead() && reader.peek() == '#') { + reader.skip() + reader.skipWhitespace() + return true + } + return false +} + +/** + * Reads the tag of a `type=#tag` value and adds the matching predicate. + * + * A tag that does not exist in Minestom's entity type registry matches no entity, as in vanilla. + */ +private fun applyTypeTag(inverted: Boolean, valueStart: Int, reader: StringReader, state: MutableSelector) { + val invertableState = state.invertableStateFor("type") + val tagKey = ResourceLocationParser.readKey(reader) + if (!invertableState.canParseTag(tagKey)) { + reader.cursor = valueStart + throw ERROR_INAPPLICABLE_OPTION.createWithContext(reader, "type") + } + invertableState.markParsedTag(tagKey) + + val tag = EntityType.staticRegistry().getTag(tagKey) + state.predicates += { _, entity -> (tag != null && tag.contains(entity.entityType)) != inverted } +} + +/** + * Rejects a repeated single-use option and an option that is not [applicable]; `limit` and `sort` + * are not applicable to a self selector (`@s`). + * + * The check runs before the value is read, so a rejected option never parses its value. + */ +private fun requireSingleUse( + key: String, + keyStart: Int, + reader: StringReader, + state: MutableSelector, + applicable: Boolean = true, +) { + if (!applicable || !state.singleUseSeen.add(key)) { + reader.cursor = keyStart + throw ERROR_INAPPLICABLE_OPTION.createWithContext(reader, key) + } +} + +/** + * Rejects a use of `name`, `type`, `team` or `gamemode` that [InvertableState] does not allow. + * + * The check runs before the value is read. + */ +private fun requireInvertableUse( + key: String, + inverted: Boolean, + valueStart: Int, + reader: StringReader, + state: MutableSelector, +) { + val invertableState = state.invertableStateFor(key) + if (!invertableState.canParseElement(inverted)) { + reader.cursor = valueStart + throw ERROR_INAPPLICABLE_OPTION.createWithContext(reader, key) + } + invertableState.markParsedElement(inverted) +} + +/** + * Adds the volume and distance predicates after all options are read, so they see the final + * `dx`, `dy`, `dz` and `distance` values regardless of option order. + */ +private fun finalizePredicates(state: MutableSelector) { + val coordinates = state.coordinates + if ("dx" in coordinates || "dy" in coordinates || "dz" in coordinates) { + val dx = coordinates["dx"] ?: 0.0 + val dy = coordinates["dy"] ?: 0.0 + val dz = coordinates["dz"] ?: 0.0 + state.predicates += { origin, entity -> matchesVolume(origin, dx, dy, dz, entity) } + } + state.distance?.let { range -> + state.predicates += { origin, entity -> range.inRange(entity.getDistance(origin).toFloat()) } + } +} + +private fun originOf(coordinates: Map): (Pos) -> Pos { + if ("x" !in coordinates && "y" !in coordinates && "z" !in coordinates) return { base -> base } + val x = coordinates["x"] + val y = coordinates["y"] + val z = coordinates["z"] + return { base -> Pos(x ?: base.x(), y ?: base.y(), z ?: base.z()) } +} + +/** + * Matches entities whose bounding box intersects the `dx`/`dy`/`dz` box at [origin]. + * + * Per axis the box spans from the origin plus `min(0, d)` to the origin plus `max(0, d) + 1`, as in + * vanilla's `EntitySelectorParser.createAabb`, so `dx=0,dy=0,dz=0` covers the block at the origin. + * Boxes that only touch do not intersect. + */ +private fun matchesVolume(origin: Pos, dx: Double, dy: Double, dz: Double, entity: Entity): Boolean { + val pos = entity.position + val box = entity.boundingBox + + return pos.x() + box.minX() < origin.x() + maxOf(0.0, dx) + 1.0 && + pos.x() + box.maxX() > origin.x() + minOf(0.0, dx) && + pos.y() + box.minY() < origin.y() + maxOf(0.0, dy) + 1.0 && + pos.y() + box.maxY() > origin.y() + minOf(0.0, dy) && + pos.z() + box.minZ() < origin.z() + maxOf(0.0, dz) + 1.0 && + pos.z() + box.maxZ() > origin.z() + minOf(0.0, dz) +} + +private fun isAlive(@Suppress("UNUSED_PARAMETER") origin: Pos, entity: Entity): Boolean = + !(entity is LivingEntity && entity.isDead) + +/** + * Returns a predicate matching the wrapped rotation of an entity against [range], as vanilla does. + * An omitted bound defaults to `0` or `359`; a minimum above the maximum after wrapping selects the + * range that crosses the `-180`/`180` boundary. + */ +private fun rotationPredicate( + range: Range.Float, + rotation: (Entity) -> Float, +): (Pos, Entity) -> Boolean { + val min = wrapDegrees(range.min()?.takeIf { it != -Float.MAX_VALUE } ?: 0f) + val max = wrapDegrees(range.max()?.takeIf { it != Float.MAX_VALUE } ?: 359f) + return { _, entity -> + val degrees = wrapDegrees(rotation(entity)) + if (min > max) degrees >= min || degrees <= max else degrees >= min && degrees <= max + } +} + +/** + * Whether a bound of this range other than the omitted-bound default is negative. + */ +private fun Range.Float.hasNegativeBound(): Boolean { + val min = min() + val max = max() + return (min != null && min != -Float.MAX_VALUE && min < 0f) || (max != null && max < 0f) +} + +/** + * Whether a bound of this range other than the omitted-bound default is negative. + */ +private fun Range.Int.hasNegativeBound(): Boolean { + val min = min() + val max = max() + return (min != null && min != Int.MIN_VALUE && min < 0) || (max != null && max < 0) +} + +private fun matchesName(entity: Entity, name: String): Boolean = when (entity) { + is Player -> entity.username == name + else -> entity.get(DataComponents.CUSTOM_NAME) + ?.let { component -> PlainTextComponentSerializer.plainText().serialize(component) } == name +} + +private fun teamNameOf(entity: Entity): String { + val identifier = if (entity is Player) entity.username else entity.uuid.toString() + return MinecraftServer.getTeamManager().teams + .firstOrNull { team -> identifier in team.members } + ?.teamName ?: "" +} + +private fun shouldInvertValue(reader: StringReader): Boolean { + reader.skipWhitespace() + if (reader.canRead() && reader.peek() == '!') { + reader.skip() + reader.skipWhitespace() + return true + } + return false +} + +/** + * Reads and discards a `scores` value of the form `{name=range,...}`. + */ +private fun consumeScores(reader: StringReader) { + reader.expect('{') + reader.skipWhitespace() + while (reader.canRead() && reader.peek() != '}') { + reader.skipWhitespace() + reader.readUnquotedString() + reader.skipWhitespace() + reader.expect('=') + reader.skipWhitespace() + IntegerRangeParser.parse(reader) + reader.skipWhitespace() + if (reader.canRead() && reader.peek() == ',') reader.skip() + reader.skipWhitespace() + } + reader.expect('}') +} + +/** + * Reads and discards an `advancements` value of the form `{id=bool}` or `{id={criterion=bool,...}}`. + */ +private fun consumeAdvancements(reader: StringReader) { + reader.expect('{') + reader.skipWhitespace() + while (reader.canRead() && reader.peek() != '}') { + reader.skipWhitespace() + ResourceLocationParser.readKey(reader) + reader.skipWhitespace() + reader.expect('=') + reader.skipWhitespace() + if (reader.canRead() && reader.peek() == '{') { + reader.skip() + reader.skipWhitespace() + while (reader.canRead() && reader.peek() != '}') { + reader.skipWhitespace() + reader.readUnquotedString() + reader.skipWhitespace() + reader.expect('=') + reader.skipWhitespace() + reader.readBoolean() + reader.skipWhitespace() + if (reader.canRead() && reader.peek() == ',') reader.skip() + reader.skipWhitespace() + } + reader.expect('}') + } else { + reader.readBoolean() + } + reader.skipWhitespace() + if (reader.canRead() && reader.peek() == ',') reader.skip() + reader.skipWhitespace() + } + reader.expect('}') +} + +private fun nearestComparator(anchor: Pos): Comparator = + Comparator.comparingDouble { entity: Entity -> entity.getDistanceSquared(anchor) } + +private fun furthestComparator(anchor: Pos): Comparator = + nearestComparator(anchor).reversed() + +/** + * Returns a comparator that orders entities randomly. Each entity keeps one random weight for the + * lifetime of the comparator, so repeated comparisons stay consistent. + */ +private fun randomComparator(@Suppress("UNUSED_PARAMETER") anchor: Pos): Comparator { + val weights = IdentityHashMap() + val random = ThreadLocalRandom.current() + return Comparator.comparingDouble { entity -> weights.getOrPut(entity) { random.nextDouble() } } +} + +private val GAME_MODE_PARSER = + FixedSetParser(GameMode.entries.associateBy { mode -> mode.name.lowercase() }) + +/** + * Entity tag holding the strings matched by the `tag` option. Nothing in this module writes it. + */ +private val ENTITY_TAGS: Tag> = Tag.String("tags").list().defaultValue(emptyList()) + +private val ERROR_MISSING_SELECTOR_TYPE = + SimpleCommandExceptionType(LiteralMessage("Missing selector type")) +private val ERROR_UNKNOWN_SELECTOR_TYPE = + DynamicCommandExceptionType { type -> LiteralMessage("Unknown selector type '$type'") } +private val ERROR_INVALID_NAME_OR_UUID = + SimpleCommandExceptionType(LiteralMessage("Invalid player name or UUID")) +private val ERROR_EXPECTED_OPTION_VALUE = + DynamicCommandExceptionType { key -> LiteralMessage("Expected value for option '$key'") } +private val ERROR_EXPECTED_END_OF_OPTIONS = + SimpleCommandExceptionType(LiteralMessage("Expected ',' or ']'")) +private val ERROR_UNKNOWN_OPTION = + DynamicCommandExceptionType { key -> LiteralMessage("Unknown option '$key'") } +private val ERROR_INAPPLICABLE_OPTION = + DynamicCommandExceptionType { key -> LiteralMessage("Option '$key' cannot be used here") } +private val ERROR_UNSUPPORTED_OPTION = DynamicCommandExceptionType { key -> + LiteralMessage("Option '$key' is unsupported: this command implementation has no equivalent in Minestom's model") +} +private val ERROR_NBT_UNSUPPORTED = SimpleCommandExceptionType( + LiteralMessage("Option 'nbt' is unsupported: this command implementation cannot parse SNBT"), +) +private val ERROR_DISTANCE_NEGATIVE = + SimpleCommandExceptionType(LiteralMessage("Distance cannot be negative")) +private val ERROR_LEVEL_NEGATIVE = + SimpleCommandExceptionType(LiteralMessage("Level cannot be negative")) +private val ERROR_LIMIT_TOO_SMALL = + SimpleCommandExceptionType(LiteralMessage("Limit must be at least 1")) +private val ERROR_SORT_UNKNOWN = + DynamicCommandExceptionType { name -> LiteralMessage("Unknown sort '$name'") } +private val ERROR_ENTITY_TYPE_INVALID = + DynamicCommandExceptionType { type -> LiteralMessage("Unknown entity type '$type'") } +private val ERROR_NOT_SINGLE_ENTITY = + SimpleCommandExceptionType(LiteralMessage("Expected a single entity")) +private val ERROR_NOT_SINGLE_PLAYER = + SimpleCommandExceptionType(LiteralMessage("Expected a single player")) +private val ERROR_ONLY_PLAYERS_ALLOWED = + SimpleCommandExceptionType(LiteralMessage("Only players can be targeted")) diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/FixedSetParser.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/FixedSetParser.kt new file mode 100644 index 000000000..a050f669b --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/FixedSetParser.kt @@ -0,0 +1,42 @@ +package dev.slne.surf.api.minestom.command.argument.parser + +import com.mojang.brigadier.LiteralMessage +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import java.util.concurrent.CompletableFuture + +/** + * Parses one word into the value mapped to it in [values]. + * + * Parsing is case-sensitive: each key is the canonical spelling its argument stringifies to. + * Suggestions match the typed prefix case-insensitively. An unknown word fails with + * `Unknown value ''`. + */ +internal class FixedSetParser(private val values: Map) : ArgumentType { + override fun parse(reader: StringReader): T { + val start = reader.cursor + val word = reader.readUnquotedString() + + return values[word] ?: run { + reader.cursor = start + throw UNKNOWN.createWithContext(reader, word) + } + } + + override fun listSuggestions( + context: CommandContext, + builder: SuggestionsBuilder, + ): CompletableFuture = builder.suggestMatching(values.keys) + + override fun getExamples(): Collection = values.keys.take(2) + + private companion object { + val UNKNOWN = DynamicCommandExceptionType { value -> + LiteralMessage("Unknown value '$value'") + } + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/InstanceParser.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/InstanceParser.kt new file mode 100644 index 000000000..0dbf2a844 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/InstanceParser.kt @@ -0,0 +1,74 @@ +package dev.slne.surf.api.minestom.command.argument.parser + +import com.mojang.brigadier.LiteralMessage +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import net.minestom.server.MinecraftServer +import net.minestom.server.instance.Instance +import java.util.concurrent.CompletableFuture + +/** + * Parses an instance by UUID or dimension name among the currently registered instances. + * + * A dimension name resolves only while exactly one instance uses it. An unknown or ambiguous token + * fails at the position where the token starts. + */ +internal object InstanceParser : ArgumentType { + private val UNKNOWN = DynamicCommandExceptionType { value -> + LiteralMessage("Unknown or ambiguous instance '$value'; use its UUID") + } + + override fun parse(reader: StringReader): Instance { + val start = reader.cursor + while (reader.canRead() && isAllowed(reader.peek())) { + reader.skip() + } + val input = reader.string.substring(start, reader.cursor) + + return resolve(input) ?: run { + reader.cursor = start + throw UNKNOWN.createWithContext(reader, input) + } + } + + /** + * Suggests the UUID of every instance and each dimension name used by exactly one instance. + */ + override fun listSuggestions( + context: CommandContext, + builder: SuggestionsBuilder, + ): CompletableFuture { + val instances = MinecraftServer.getInstanceManager().instances + val dimensionCounts = Object2IntOpenHashMap() + instances.forEach { instance -> dimensionCounts.addTo(instance.dimensionName, 1) } + + val values = ObjectArrayList(instances.size * 2) + instances.forEach { instance -> values += instance.uuid.toString() } + instances.forEach { instance -> + if (dimensionCounts.getInt(instance.dimensionName) == 1) { + values += instance.dimensionName + } + } + + return builder.suggestMatching(values) + } + + private fun resolve(input: String): Instance? { + val instances = MinecraftServer.getInstanceManager().instances + parseDashedUuid(input)?.let { uuid -> + instances.singleOrNull { instance -> instance.uuid == uuid }?.let { return it } + } + return instances.singleOrNull { instance -> instance.dimensionName == input } + } + + private fun isAllowed(character: Char): Boolean = character in '0'..'9' || + character in 'a'..'z' || character in 'A'..'Z' || + character == '_' || character == ':' || character == '/' || + character == '.' || character == '-' +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/ParticleParser.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/ParticleParser.kt new file mode 100644 index 000000000..e881a2f0d --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/ParticleParser.kt @@ -0,0 +1,40 @@ +package dev.slne.surf.api.minestom.command.argument.parser + +import com.mojang.brigadier.LiteralMessage +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType +import net.minestom.server.particle.Particle + +/** + * Parses a namespaced particle key into a [Particle.Simple]. + * + * Particles that take options, such as `dust`, `block` or `vibration`, are rejected as unsupported + * because the syntax has no way to supply the options. Unknown keys are rejected as well. Both + * failures are reported at the position where the key starts. + */ +internal object ParticleParser : ArgumentType { + private val UNKNOWN = DynamicCommandExceptionType { value -> + LiteralMessage("Unknown particle '$value'") + } + + private val UNSUPPORTED = DynamicCommandExceptionType { value -> + LiteralMessage("Unsupported particle '$value'; it requires options this argument cannot express") + } + + override fun parse(reader: StringReader): Particle { + val start = reader.cursor + val key = ResourceLocationParser.readKey(reader) + val particle = Particle.fromKey(key) ?: run { + reader.cursor = start + throw UNKNOWN.createWithContext(reader, key.asString()) + } + + if (particle !is Particle.Simple) { + reader.cursor = start + throw UNSUPPORTED.createWithContext(reader, key.asString()) + } + + return particle + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/RegistryParser.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/RegistryParser.kt new file mode 100644 index 000000000..13e431d23 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/RegistryParser.kt @@ -0,0 +1,45 @@ +package dev.slne.surf.api.minestom.command.argument.parser + +import com.mojang.brigadier.LiteralMessage +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import net.kyori.adventure.key.Key +import java.util.concurrent.CompletableFuture + +/** + * Parses a namespaced key into the value [lookup] returns for it. + * + * A key for which [lookup] returns `null` fails at the position where the key starts, with a + * message naming [registryName]. [keys] supplies the suggestions and is evaluated on every + * request, so entries added to a registry later are included. + */ +internal class RegistryParser( + private val registryName: String, + private val keys: () -> Iterable = ::emptyList, + private val lookup: (Key) -> T?, +) : ArgumentType { + override fun parse(reader: StringReader): T { + val start = reader.cursor + val key = ResourceLocationParser.readKey(reader) + + return lookup(key) ?: run { + reader.cursor = start + throw UNKNOWN.createWithContext(reader, "$registryName '$key'") + } + } + + override fun listSuggestions( + context: CommandContext, + builder: SuggestionsBuilder, + ): CompletableFuture = builder.suggestMatching(keys()) + + private companion object { + val UNKNOWN = DynamicCommandExceptionType { value -> + LiteralMessage("Unknown $value") + } + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/ResourceLocationParser.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/ResourceLocationParser.kt new file mode 100644 index 000000000..889f8040a --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/ResourceLocationParser.kt @@ -0,0 +1,56 @@ +package dev.slne.surf.api.minestom.command.argument.parser + +import com.mojang.brigadier.LiteralMessage +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType +import net.kyori.adventure.key.InvalidKeyException +import net.kyori.adventure.key.Key + +/** + * Parses a namespaced key such as `minecraft:stone`. The namespace defaults to `minecraft`. + */ +internal object ResourceLocationParser : ArgumentType { + private val INVALID = SimpleCommandExceptionType(LiteralMessage("Invalid identifier")) + + override fun parse(reader: StringReader): Key = readKey(reader) + + /** + * Reads a namespaced key from [reader], consuming only the characters a key may contain. + * + * The namespace defaults to `minecraft` when the separator is missing or leading. The path must + * not be empty. + * + * @throws CommandSyntaxException when the consumed text is not a valid key; the cursor is reset + * to the start of the key + */ + fun readKey(reader: StringReader): Key { + val start = reader.cursor + while (reader.canRead() && isAllowed(reader.peek())) { + reader.skip() + } + + val text = reader.string.substring(start, reader.cursor) + val separator = text.indexOf(':') + val namespace = if (separator >= 1) text.substring(0, separator) else Key.MINECRAFT_NAMESPACE + val value = if (separator >= 0) text.substring(separator + 1) else text + if (value.isEmpty()) invalid(reader, start) + + return try { + Key.key(namespace, value) + } catch (cause: InvalidKeyException) { + invalid(reader, start) + } + } + + private fun invalid(reader: StringReader, start: Int): Nothing { + reader.cursor = start + throw INVALID.createWithContext(reader) + } + + private fun isAllowed(character: Char): Boolean = character in '0'..'9' || + character in 'a'..'z' || + character == '_' || character == ':' || character == '/' || + character == '.' || character == '-' +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/ScalarParsers.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/ScalarParsers.kt new file mode 100644 index 000000000..33050a655 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/ScalarParsers.kt @@ -0,0 +1,256 @@ +package dev.slne.surf.api.minestom.command.argument.parser + +import com.mojang.brigadier.LiteralMessage +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType +import net.minestom.server.MinecraftServer +import net.minestom.server.entity.Entity +import net.minestom.server.utils.Range +import java.time.Duration +import java.util.* + +/** + * Parses a UUID in dashed hex form, e.g. `550e8400-e29b-41d4-a716-446655440000`. + * + * Groups shorter than the canonical width are accepted as well; wider groups are rejected instead + * of being truncated. + */ +internal object UuidParser : ArgumentType { + private val INVALID = SimpleCommandExceptionType(LiteralMessage("Invalid UUID")) + + override fun parse(reader: StringReader): UUID { + val start = reader.cursor + while (reader.canRead() && isAllowed(reader.peek())) { + reader.skip() + } + val text = reader.string.substring(start, reader.cursor) + + return parseDashedUuid(text) ?: run { + reader.cursor = start + throw INVALID.createWithContext(reader) + } + } + + private fun isAllowed(character: Char): Boolean = character in '0'..'9' || + character in 'a'..'f' || character in 'A'..'F' || character == '-' +} + +/** + * Parses [text] as a UUID of five dashed hex groups, or returns `null` for any other text. + * + * As with [UUID.fromString], a group may be shorter than its canonical 8-4-4-4-12 width; a wider + * group is rejected instead of being truncated. Ordinary names are rejected without an exception. + */ +internal fun parseDashedUuid(text: String): UUID? { + var group = 0 + var groupLength = 0 + for (character in text) { + if (character == '-') { + if (groupLength == 0 || ++group >= UUID_GROUP_WIDTHS.size) return null + groupLength = 0 + continue + } + val hex = character in '0'..'9' || character in 'a'..'f' || character in 'A'..'F' + if (!hex || ++groupLength > UUID_GROUP_WIDTHS[group]) return null + } + if (group != UUID_GROUP_WIDTHS.lastIndex || groupLength == 0) return null + return UUID.fromString(text) +} + +private val UUID_GROUP_WIDTHS = intArrayOf(8, 4, 4, 4, 12) + +/** + * Wraps [degrees] into `[-180, 180)`. + */ +internal fun wrapDegrees(degrees: Float): Float { + var wrapped = degrees % 360f + if (wrapped >= 180f) wrapped -= 360f + if (wrapped < -180f) wrapped += 360f + return wrapped +} + +/** + * Parses an angle in degrees, either absolute or `~` with an optional offset relative to the + * source's yaw. The result is wrapped into `[-180, 180)`. + * + * A source that is not an [Entity], such as the console, has yaw `0`. + */ +internal object AngleParser : ArgumentType { + private val INCOMPLETE = SimpleCommandExceptionType(LiteralMessage("Expected an angle")) + private val INVALID = SimpleCommandExceptionType(LiteralMessage("Invalid angle")) + + override fun parse(reader: StringReader): Float = read(reader, yaw = 0f) + + override fun parse(reader: StringReader, source: S): Float = + read(reader, (source as? Entity)?.position?.yaw ?: 0f) + + private fun read(reader: StringReader, yaw: Float): Float { + if (!reader.canRead()) throw INCOMPLETE.createWithContext(reader) + + val relative = reader.peek() == '~' + if (relative) reader.skip() + + val start = reader.cursor + val value = if (reader.canRead() && reader.peek() != ' ') reader.readFloat() else 0f + if (!value.isFinite()) { + reader.cursor = start + throw INVALID.createWithContext(reader) + } + + return wrapDegrees(if (relative) value + yaw else value) + } +} + +/** + * Parses a vanilla time value: a number with an optional unit of `d` (24000 ticks), `s` (20 ticks) + * or `t` (ticks). Without a unit the number counts ticks. + * + * The value is rounded to the nearest tick and converted to a [Duration] using + * [MinecraftServer.TICK_MS]. Negative values, values whose duration does not fit a [Duration] in + * milliseconds and other units are rejected. + */ +internal object TimeParser : ArgumentType { + private val INVALID_UNIT = DynamicCommandExceptionType { unit -> + LiteralMessage("Invalid unit '$unit'; expected 'd', 's', 't' or none") + } + private val NEGATIVE = SimpleCommandExceptionType(LiteralMessage("Duration must not be negative")) + private val OUT_OF_RANGE = SimpleCommandExceptionType(LiteralMessage("Duration is out of range")) + private val MAX_TICKS = Long.MAX_VALUE / MinecraftServer.TICK_MS + + override fun parse(reader: StringReader): Duration { + val start = reader.cursor + val amount = reader.readFloat() + val unit = if (reader.canRead() && reader.peek() != ' ') reader.read() else null + + val factor = unitFactor(unit) ?: run { + reader.cursor = start + throw INVALID_UNIT.createWithContext(reader, unit.toString()) + } + + val scaled = amount * factor + if (!scaled.isFinite()) { + reader.cursor = start + throw OUT_OF_RANGE.createWithContext(reader) + } + + val ticks = Math.round(scaled.toDouble()) + if (ticks < 0) { + reader.cursor = start + throw NEGATIVE.createWithContext(reader) + } + if (ticks > MAX_TICKS) { + reader.cursor = start + throw OUT_OF_RANGE.createWithContext(reader) + } + + return Duration.ofMillis(ticks * MinecraftServer.TICK_MS) + } + + private fun unitFactor(unit: Char?): Int? = when (unit) { + null, 't' -> 1 + 's' -> 20 + 'd' -> 24000 + else -> null + } +} + +/** + * Parses an integer range in vanilla's `min..max` syntax. + * + * An omitted bound defaults to [Int.MIN_VALUE] or [Int.MAX_VALUE]. A bare value denotes a range + * containing only that value. At least one bound is required, and the minimum must not exceed the + * maximum. + */ +internal object IntegerRangeParser : ArgumentType { + override fun parse(reader: StringReader): Range.Int { + val start = reader.cursor + val (min, max) = readRange(reader, String::toIntOrNull) + return try { + Range.Int(min ?: Int.MIN_VALUE, max ?: Int.MAX_VALUE) + } catch (cause: IllegalArgumentException) { + reader.cursor = start + throw RANGE_INVERTED.createWithContext(reader) + } + } +} + +/** + * Parses a float range in vanilla's `min..max` syntax. + * + * An omitted bound defaults to `-Float.MAX_VALUE` or `Float.MAX_VALUE`. A bare value denotes a + * range containing only that value. At least one bound is required, and the minimum must not + * exceed the maximum. + */ +internal object FloatRangeParser : ArgumentType { + override fun parse(reader: StringReader): Range.Float { + val start = reader.cursor + val (min, max) = readRange(reader, String::toFloatOrNull) + return try { + Range.Float(min ?: -Float.MAX_VALUE, max ?: Float.MAX_VALUE) + } catch (cause: IllegalArgumentException) { + reader.cursor = start + throw RANGE_INVERTED.createWithContext(reader) + } + } +} + +private val RANGE_EMPTY = SimpleCommandExceptionType(LiteralMessage("Expected value or range of values")) +private val RANGE_INVALID = DynamicCommandExceptionType { value -> LiteralMessage("Invalid range value '$value'") } +private val RANGE_INVERTED = SimpleCommandExceptionType(LiteralMessage("Range minimum must not exceed its maximum")) + +/** + * Reads a `lower..upper` range, converting each bound with [parse]. A bare value supplies both + * bounds; an empty bound is returned as `null`. + * + * The cursor of [reader] is reset to the start of the range before an exception is thrown. + */ +private fun readRange(reader: StringReader, parse: (String) -> T?): Pair { + val start = reader.cursor + val lowerText = readRangeBoundText(reader) + val upperText = if (reader.canRead(2) && reader.peek() == '.' && reader.peek(1) == '.') { + reader.skip() + reader.skip() + readRangeBoundText(reader) + } else { + lowerText + } + + if (lowerText.isEmpty() && upperText.isEmpty()) { + reader.cursor = start + throw RANGE_EMPTY.createWithContext(reader) + } + + fun parseBound(text: String): T? { + if (text.isEmpty()) return null + return parse(text) ?: run { + reader.cursor = start + throw RANGE_INVALID.createWithContext(reader, text) + } + } + + return parseBound(lowerText) to parseBound(upperText) +} + +/** + * Reads one range bound: an optional `-`, digits and an optional decimal part. + * + * A `..` separator ends the bound and is not read as a decimal point. + */ +private fun readRangeBoundText(reader: StringReader): String { + val start = reader.cursor + if (reader.canRead() && reader.peek() == '-') { + reader.skip() + } + while (reader.canRead() && reader.peek() in '0'..'9') { + reader.skip() + } + if (reader.canRead() && reader.peek() == '.' && !(reader.canRead(2) && reader.peek(1) == '.')) { + reader.skip() + while (reader.canRead() && reader.peek() in '0'..'9') { + reader.skip() + } + } + return reader.string.substring(start, reader.cursor) +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/Suggesting.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/Suggesting.kt new file mode 100644 index 000000000..559b5b971 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/argument/parser/Suggesting.kt @@ -0,0 +1,18 @@ +package dev.slne.surf.api.minestom.command.argument.parser + +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import java.util.concurrent.CompletableFuture + +/** + * Suggests every entry of [values] that starts with the remaining input, ignoring case, and builds + * the result. + */ +internal fun SuggestionsBuilder.suggestMatching( + values: Iterable, +): CompletableFuture { + values.forEach { value -> + if (value.startsWith(remaining, ignoreCase = true)) suggest(value) + } + return buildFuture() +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/dsl/ArgumentDsl.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/dsl/ArgumentDsl.kt new file mode 100644 index 000000000..caa53d4c5 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/dsl/ArgumentDsl.kt @@ -0,0 +1,869 @@ +package dev.slne.surf.api.minestom.command.dsl + +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.CommandTree +import dev.slne.surf.api.minestom.command.argument.* +import net.kyori.adventure.chat.SignedMessage +import net.kyori.adventure.key.Key +import net.minestom.server.color.TeamColor +import net.minestom.server.coordinate.Vec +import net.minestom.server.entity.Entity +import net.minestom.server.entity.EntityType +import net.minestom.server.entity.GameMode +import net.minestom.server.entity.Player +import net.minestom.server.instance.Instance +import net.minestom.server.item.enchant.Enchantment +import net.minestom.server.particle.Particle +import net.minestom.server.potion.PotionEffect +import net.minestom.server.registry.Registry +import net.minestom.server.registry.RegistryKey +import net.minestom.server.sound.SoundEvent +import net.minestom.server.utils.Range +import net.minestom.server.world.biome.Biome +import java.time.Duration +import java.util.* + +inline fun CommandAPICommand.booleanArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments( + BooleanArgument(nodeName).setOptional(optional = optional).apply(block), +) + +inline fun CommandAPICommand.integerArgument( + nodeName: String, + min: Int = Int.MIN_VALUE, + max: Int = Int.MAX_VALUE, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments( + IntegerArgument(nodeName, min, max).setOptional(optional).apply(block), +) + +inline fun CommandAPICommand.stringArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments( + StringArgument(nodeName).setOptional(optional).apply(block), +) + +inline fun CommandAPICommand.textArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments( + TextArgument(nodeName).setOptional(optional).apply(block), +) + +inline fun CommandAPICommand.greedyStringArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments( + GreedyStringArgument(nodeName).setOptional(optional).apply(block), +) + +inline fun CommandAPICommand.signedMessageArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments( + SignedMessageArgument(nodeName).setOptional(optional).apply(block), +) + +inline fun CommandAPICommand.literalArgument( + nodeName: String, + literal: String = nodeName, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments(LiteralArgument(nodeName, literal).apply(block)) + +inline fun CommandAPICommand.playerArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments( + PlayerArgument(nodeName).setOptional(optional).apply(block), +) + +inline fun CommandAPICommand.playersArgument( + nodeName: String, + allowEmpty: Boolean = false, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): CommandAPICommand = withArguments( + PlayersArgument(nodeName, allowEmpty).setOptional(optional).apply(block), +) + +inline fun CommandAPICommand.entityArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments( + EntityArgument(nodeName).setOptional(optional).apply(block), +) + +inline fun CommandAPICommand.entitiesArgument( + nodeName: String, + allowEmpty: Boolean = false, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): CommandAPICommand = withArguments( + EntitiesArgument(nodeName, allowEmpty).setOptional(optional).apply(block), +) + +inline fun CommandAPICommand.entityTypeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments( + EntityTypeArgument(nodeName).setOptional(optional).apply(block), +) + +inline fun CommandAPICommand.locationArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments(PositionArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.location2DArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = + withArguments(Position2DArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.blockPositionArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = + withArguments(BlockPositionArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.rotationArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments(RotationArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.angleArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments(AngleArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.axisArgument( + nodeName: String, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): CommandAPICommand = withArguments(AxisArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.resourceLocationArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = + withArguments(ResourceLocationArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.timeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments(TimeArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.teamColorArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments(TeamColorArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.particleArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments(ParticleArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.gameModeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments(GameModeArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.commandArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments(CommandArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.soundArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments(SoundArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.potionEffectArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = + withArguments(PotionEffectArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.biomeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): CommandAPICommand = withArguments(BiomeArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.enchantmentArgument( + nodeName: String, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): CommandAPICommand = + withArguments(EnchantmentArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.resourceArgument( + nodeName: String, + identifier: String, + registry: Registry, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): CommandAPICommand = withArguments( + ResourceArgument(nodeName, identifier, registry).setOptional(optional).apply(block), +) + +inline fun CommandAPICommand.instanceArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments(InstanceArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.longArgument( + nodeName: String, + min: Long = Long.MIN_VALUE, + max: Long = Long.MAX_VALUE, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments( + LongArgument(nodeName, min, max).setOptional(optional).apply(block), +) + +inline fun CommandAPICommand.floatArgument( + nodeName: String, + min: Float = -Float.MAX_VALUE, + max: Float = Float.MAX_VALUE, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments( + FloatArgument(nodeName, min, max).setOptional(optional).apply(block), +) + +inline fun CommandAPICommand.doubleArgument( + nodeName: String, + min: Double = -Double.MAX_VALUE, + max: Double = Double.MAX_VALUE, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments( + DoubleArgument(nodeName, min, max).setOptional(optional).apply(block), +) + +inline fun > CommandAPICommand.enumArgument( + nodeName: String, + values: Collection, + noinline formatter: (E) -> String = { value -> value.name.lowercase() }, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments( + EnumArgument(nodeName, values, formatter).setOptional(optional).apply(block), +) + +inline fun CommandAPICommand.uuidArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments(UUIDArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.integerRangeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = + withArguments(IntegerRangeArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.floatRangeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandAPICommand = + withArguments(FloatRangeArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.multiLiteralArgument( + nodeName: String, + vararg literals: String, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments(MultiLiteralArgument(nodeName, *literals).apply(block)) + +inline fun CommandTree.integerArgument( + nodeName: String, + min: Int = Int.MIN_VALUE, + max: Int = Int.MAX_VALUE, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(IntegerArgument(nodeName, min, max).setOptional(optional).apply(block)) + +inline fun CommandTree.stringArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(StringArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.literalArgument( + nodeName: String, + literal: String = nodeName, + block: Argument.() -> Unit = {}, +): CommandTree = then(LiteralArgument(nodeName, literal).apply(block)) + +inline fun CommandTree.playerArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(PlayerArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.playersArgument( + nodeName: String, + allowEmpty: Boolean = false, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): CommandTree = then(PlayersArgument(nodeName, allowEmpty).setOptional(optional).apply(block)) + +inline fun CommandTree.entityArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(EntityArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.entitiesArgument( + nodeName: String, + allowEmpty: Boolean = false, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): CommandTree = then(EntitiesArgument(nodeName, allowEmpty).setOptional(optional).apply(block)) + +inline fun CommandTree.entityTypeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(EntityTypeArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.locationArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(PositionArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.location2DArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(Position2DArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.blockPositionArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(BlockPositionArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.rotationArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(RotationArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.angleArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(AngleArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.axisArgument( + nodeName: String, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): CommandTree = then(AxisArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.resourceLocationArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(ResourceLocationArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.timeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(TimeArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.teamColorArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(TeamColorArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.particleArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(ParticleArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.gameModeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(GameModeArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.commandArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(CommandArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.soundArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(SoundArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.potionEffectArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(PotionEffectArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.biomeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): CommandTree = then(BiomeArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.enchantmentArgument( + nodeName: String, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): CommandTree = then(EnchantmentArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.resourceArgument( + nodeName: String, + identifier: String, + registry: Registry, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): CommandTree = + then(ResourceArgument(nodeName, identifier, registry).setOptional(optional).apply(block)) + +inline fun CommandTree.instanceArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(InstanceArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.booleanArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(BooleanArgument(nodeName).setOptional(optional = optional).apply(block)) + +inline fun CommandTree.textArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(TextArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.greedyStringArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(GreedyStringArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.signedMessageArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(SignedMessageArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.longArgument( + nodeName: String, + min: Long = Long.MIN_VALUE, + max: Long = Long.MAX_VALUE, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(LongArgument(nodeName, min, max).setOptional(optional).apply(block)) + +inline fun CommandTree.floatArgument( + nodeName: String, + min: Float = -Float.MAX_VALUE, + max: Float = Float.MAX_VALUE, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(FloatArgument(nodeName, min, max).setOptional(optional).apply(block)) + +inline fun CommandTree.doubleArgument( + nodeName: String, + min: Double = -Double.MAX_VALUE, + max: Double = Double.MAX_VALUE, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(DoubleArgument(nodeName, min, max).setOptional(optional).apply(block)) + +inline fun > CommandTree.enumArgument( + nodeName: String, + values: Collection, + noinline formatter: (E) -> String = { value -> value.name.lowercase() }, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(EnumArgument(nodeName, values, formatter).setOptional(optional).apply(block)) + +inline fun CommandTree.uuidArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(UUIDArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.integerRangeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(IntegerRangeArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.floatRangeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): CommandTree = then(FloatRangeArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandTree.multiLiteralArgument( + nodeName: String, + vararg literals: String, + block: Argument.() -> Unit = {}, +): CommandTree = then(MultiLiteralArgument(nodeName, *literals).apply(block)) + +inline fun Argument.integerArgument( + nodeName: String, + min: Int = Int.MIN_VALUE, + max: Int = Int.MAX_VALUE, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(IntegerArgument(nodeName, min, max).setOptional(optional).apply(block)) + +inline fun Argument.stringArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(StringArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.literalArgument( + nodeName: String, + literal: String = nodeName, + block: Argument.() -> Unit = {}, +): Argument = then(LiteralArgument(nodeName, literal).apply(block)) + +inline fun Argument.playerArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(PlayerArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.playersArgument( + nodeName: String, + allowEmpty: Boolean = false, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): Argument = then(PlayersArgument(nodeName, allowEmpty).setOptional(optional).apply(block)) + +inline fun Argument.entityArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(EntityArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.entitiesArgument( + nodeName: String, + allowEmpty: Boolean = false, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): Argument = then(EntitiesArgument(nodeName, allowEmpty).setOptional(optional).apply(block)) + +inline fun Argument.entityTypeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(EntityTypeArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.locationArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(PositionArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.location2DArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(Position2DArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.blockPositionArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(BlockPositionArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.rotationArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(RotationArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.angleArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(AngleArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.axisArgument( + nodeName: String, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): Argument = then(AxisArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.resourceLocationArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(ResourceLocationArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.timeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(TimeArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.teamColorArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(TeamColorArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.particleArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(ParticleArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.gameModeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(GameModeArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.commandArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(CommandArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.soundArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(SoundArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.potionEffectArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(PotionEffectArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.biomeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): Argument = then(BiomeArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.enchantmentArgument( + nodeName: String, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): Argument = then(EnchantmentArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument

.resourceArgument( + nodeName: String, + identifier: String, + registry: Registry, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): Argument

= + then(ResourceArgument(nodeName, identifier, registry).setOptional(optional).apply(block)) + +inline fun Argument.instanceArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(InstanceArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.booleanArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(BooleanArgument(nodeName).setOptional(optional = optional).apply(block)) + +inline fun Argument.textArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(TextArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.greedyStringArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(GreedyStringArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.signedMessageArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(SignedMessageArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.longArgument( + nodeName: String, + min: Long = Long.MIN_VALUE, + max: Long = Long.MAX_VALUE, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(LongArgument(nodeName, min, max).setOptional(optional).apply(block)) + +inline fun Argument.floatArgument( + nodeName: String, + min: Float = -Float.MAX_VALUE, + max: Float = Float.MAX_VALUE, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(FloatArgument(nodeName, min, max).setOptional(optional).apply(block)) + +inline fun Argument.doubleArgument( + nodeName: String, + min: Double = -Double.MAX_VALUE, + max: Double = Double.MAX_VALUE, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(DoubleArgument(nodeName, min, max).setOptional(optional).apply(block)) + +inline fun > Argument.enumArgument( + nodeName: String, + values: Collection, + noinline formatter: (E) -> String = { value -> value.name.lowercase() }, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(EnumArgument(nodeName, values, formatter).setOptional(optional).apply(block)) + +inline fun Argument.uuidArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(UUIDArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.integerRangeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(IntegerRangeArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.floatRangeArgument( + nodeName: String, + optional: Boolean = false, + block: Argument.() -> Unit = {}, +): Argument = then(FloatRangeArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument.multiLiteralArgument( + nodeName: String, + vararg literals: String, + block: Argument.() -> Unit = {}, +): Argument = then(MultiLiteralArgument(nodeName, *literals).apply(block)) + +inline fun CommandAPICommand.customArgument( + base: Argument, + noinline parser: (CustomArgumentInfo) -> T, + optional: Boolean? = null, + block: Argument.() -> Unit = {}, +): CommandAPICommand = withArguments( + CustomArgument(base, parser = parser).apply { + optional?.let { override -> + setOptional(false) + if (override) setOptional(true) + } + block() + }, +) + +inline fun CommandAPICommand.listArgument( + nodeName: String, + element: Argument, + delimiter: Char = ',', + allowEmpty: Boolean = false, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): CommandAPICommand = withArguments( + ListArgument(nodeName, element, delimiter, allowEmpty).setOptional(optional).apply(block), +) + +inline fun CommandTree.customArgument( + base: Argument, + noinline parser: (CustomArgumentInfo) -> T, + optional: Boolean? = null, + block: Argument.() -> Unit = {}, +): CommandTree = then( + CustomArgument(base, parser = parser).apply { + optional?.let { override -> + setOptional(false) + if (override) setOptional(true) + } + block() + }, +) + +inline fun CommandTree.listArgument( + nodeName: String, + element: Argument, + delimiter: Char = ',', + allowEmpty: Boolean = false, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): CommandTree = then( + ListArgument(nodeName, element, delimiter, allowEmpty).setOptional(optional).apply(block), +) + +inline fun Argument

.customArgument( + base: Argument, + noinline parser: (CustomArgumentInfo) -> T, + optional: Boolean? = null, + block: Argument.() -> Unit = {}, +): Argument

= then( + CustomArgument(base, parser = parser).apply { + optional?.let { override -> + setOptional(false) + if (override) setOptional(true) + } + block() + }, +) + +inline fun Argument

.listArgument( + nodeName: String, + element: Argument, + delimiter: Char = ',', + allowEmpty: Boolean = false, + optional: Boolean = false, + block: Argument>.() -> Unit = {}, +): Argument

= then( + ListArgument(nodeName, element, delimiter, allowEmpty).setOptional(optional).apply(block), +) diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/dsl/CommandDsl.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/dsl/CommandDsl.kt new file mode 100644 index 000000000..60e0841f5 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/dsl/CommandDsl.kt @@ -0,0 +1,31 @@ +package dev.slne.surf.api.minestom.command.dsl + +import dev.slne.surf.api.minestom.command.CommandAPICommand +import dev.slne.surf.api.minestom.command.CommandTree + +inline fun commandAPICommand( + name: String, + block: CommandAPICommand.() -> Unit = {}, +) = CommandAPICommand(name).apply(block).register() + +inline fun commandAPICommand( + name: String, + namespace: String, + block: CommandAPICommand.() -> Unit = {}, +) = CommandAPICommand(name).apply(block).register(namespace) + +inline fun commandTree( + name: String, + block: CommandTree.() -> Unit = {}, +) = CommandTree(name).apply(block).register() + +inline fun commandTree( + name: String, + namespace: String, + block: CommandTree.() -> Unit = {}, +) = CommandTree(name).apply(block).register(namespace) + +inline fun subcommand( + name: String, + block: CommandAPICommand.() -> Unit = {}, +) = CommandAPICommand(name).apply(block) diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/dsl/ExecutorDsl.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/dsl/ExecutorDsl.kt new file mode 100644 index 000000000..608a47687 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/dsl/ExecutorDsl.kt @@ -0,0 +1,206 @@ +package dev.slne.surf.api.minestom.command.dsl + +import dev.slne.surf.api.minestom.command.executor.CommandArguments +import dev.slne.surf.api.minestom.command.executor.CommandExecutable +import dev.slne.surf.api.minestom.command.executor.ExecutionInfo +import dev.slne.surf.api.minestom.command.executor.ExecutorDefinition +import dev.slne.surf.api.minestom.command.executor.ExecutorType +import dev.slne.surf.api.minestom.coroutine.minestomAsyncScope +import kotlinx.coroutines.CoroutineScope +import net.minestom.server.command.CommandSender +import net.minestom.server.command.ConsoleSender +import net.minestom.server.entity.Player + +inline fun > SELF.anyExecutor( + crossinline executor: (CommandSender, CommandArguments) -> Unit, +): SELF = addExecutor( + ExecutorDefinition.Normal(ExecutorType.ANY) { info -> executor(info.sender, info.args) }, +) + +inline fun > SELF.playerExecutor( + crossinline executor: (Player, CommandArguments) -> Unit, +): SELF = addExecutor( + ExecutorDefinition.Normal(ExecutorType.PLAYER) { info -> + val player = info.sender + check(player is Player) { "PLAYER executor received ${player::class.qualifiedName}" } + executor(player, info.args) + }, +) + +inline fun > SELF.consoleExecutor( + crossinline executor: (ConsoleSender, CommandArguments) -> Unit, +): SELF = addExecutor( + ExecutorDefinition.Normal(ExecutorType.CONSOLE) { info -> + val console = info.sender + check(console is ConsoleSender) { "CONSOLE executor received ${console::class.qualifiedName}" } + executor(console, info.args) + }, +) + +inline fun > SELF.anyExecutionInfo( + crossinline executor: (ExecutionInfo) -> Unit, +): SELF = addExecutor( + ExecutorDefinition.Normal(ExecutorType.ANY) { info -> executor(info) }, +) + +inline fun > SELF.playerExecutionInfo( + crossinline executor: (ExecutionInfo) -> Unit, +): SELF = addExecutor( + ExecutorDefinition.Normal(ExecutorType.PLAYER) { info -> + val player = info.sender + check(player is Player) { "PLAYER executor received ${player::class.qualifiedName}" } + executor(ExecutionInfo(player, info.args, info.input)) + }, +) + +inline fun > SELF.consoleExecutionInfo( + crossinline executor: (ExecutionInfo) -> Unit, +): SELF = addExecutor( + ExecutorDefinition.Normal(ExecutorType.CONSOLE) { info -> + val console = info.sender + check(console is ConsoleSender) { "CONSOLE executor received ${console::class.qualifiedName}" } + executor(ExecutionInfo(console, info.args, info.input)) + }, +) + +inline fun > SELF.anyResultingExecutor( + crossinline executor: (CommandSender, CommandArguments) -> Int, +): SELF = addExecutor( + ExecutorDefinition.Resulting(ExecutorType.ANY) { info -> executor(info.sender, info.args) }, +) + +inline fun > SELF.playerResultingExecutor( + crossinline executor: (Player, CommandArguments) -> Int, +): SELF = addExecutor( + ExecutorDefinition.Resulting(ExecutorType.PLAYER) { info -> + val player = info.sender + check(player is Player) { "PLAYER executor received ${player::class.qualifiedName}" } + executor(player, info.args) + }, +) + +inline fun > SELF.consoleResultingExecutor( + crossinline executor: (ConsoleSender, CommandArguments) -> Int, +): SELF = addExecutor( + ExecutorDefinition.Resulting(ExecutorType.CONSOLE) { info -> + val console = info.sender + check(console is ConsoleSender) { "CONSOLE executor received ${console::class.qualifiedName}" } + executor(console, info.args) + }, +) + +inline fun > SELF.anyResultingExecutionInfo( + crossinline executor: (ExecutionInfo) -> Int, +): SELF = addExecutor( + ExecutorDefinition.Resulting(ExecutorType.ANY) { info -> executor(info) }, +) + +inline fun > SELF.playerResultingExecutionInfo( + crossinline executor: (ExecutionInfo) -> Int, +): SELF = addExecutor( + ExecutorDefinition.Resulting(ExecutorType.PLAYER) { info -> + val player = info.sender + check(player is Player) { "PLAYER executor received ${player::class.qualifiedName}" } + executor(ExecutionInfo(player, info.args, info.input)) + }, +) + +inline fun > SELF.consoleResultingExecutionInfo( + crossinline executor: (ExecutionInfo) -> Int, +): SELF = addExecutor( + ExecutorDefinition.Resulting(ExecutorType.CONSOLE) { info -> + val console = info.sender + check(console is ConsoleSender) { "CONSOLE executor received ${console::class.qualifiedName}" } + executor(ExecutionInfo(console, info.args, info.input)) + }, +) + +fun > SELF.anyExecutorSuspend( + scope: () -> CoroutineScope = { minestomAsyncScope }, + executor: suspend CoroutineScope.(CommandSender, CommandArguments) -> Unit, +): SELF = addExecutor( + ExecutorDefinition.Suspending(ExecutorType.ANY, scope) { info -> + executor(info.sender, info.args) + }, +) + +fun > SELF.anyExecutorSuspend( + scope: CoroutineScope, + executor: suspend CoroutineScope.(CommandSender, CommandArguments) -> Unit, +): SELF = anyExecutorSuspend(scope = { scope }, executor = executor) + +fun > SELF.playerExecutorSuspend( + scope: () -> CoroutineScope = { minestomAsyncScope }, + executor: suspend CoroutineScope.(Player, CommandArguments) -> Unit, +): SELF = addExecutor( + ExecutorDefinition.Suspending(ExecutorType.PLAYER, scope) { info -> + val player = info.sender + check(player is Player) { "PLAYER executor received ${player::class.qualifiedName}" } + executor(player, info.args) + }, +) + +fun > SELF.playerExecutorSuspend( + scope: CoroutineScope, + executor: suspend CoroutineScope.(Player, CommandArguments) -> Unit, +): SELF = playerExecutorSuspend(scope = { scope }, executor = executor) + +fun > SELF.consoleExecutorSuspend( + scope: () -> CoroutineScope = { minestomAsyncScope }, + executor: suspend CoroutineScope.(ConsoleSender, CommandArguments) -> Unit, +): SELF = addExecutor( + ExecutorDefinition.Suspending(ExecutorType.CONSOLE, scope) { info -> + val console = info.sender + check(console is ConsoleSender) { "CONSOLE executor received ${console::class.qualifiedName}" } + executor(console, info.args) + }, +) + +fun > SELF.consoleExecutorSuspend( + scope: CoroutineScope, + executor: suspend CoroutineScope.(ConsoleSender, CommandArguments) -> Unit, +): SELF = consoleExecutorSuspend(scope = { scope }, executor = executor) + +fun > SELF.anyExecutionInfoExecutorSuspend( + scope: () -> CoroutineScope = { minestomAsyncScope }, + executor: suspend CoroutineScope.(ExecutionInfo) -> Unit, +): SELF = addExecutor( + ExecutorDefinition.Suspending(ExecutorType.ANY, scope) { info -> executor(info) }, +) + +fun > SELF.anyExecutionInfoExecutorSuspend( + scope: CoroutineScope, + executor: suspend CoroutineScope.(ExecutionInfo) -> Unit, +): SELF = anyExecutionInfoExecutorSuspend(scope = { scope }, executor = executor) + +fun > SELF.playerExecutionInfoExecutorSuspend( + scope: () -> CoroutineScope = { minestomAsyncScope }, + executor: suspend CoroutineScope.(ExecutionInfo) -> Unit, +): SELF = addExecutor( + ExecutorDefinition.Suspending(ExecutorType.PLAYER, scope) { info -> + val player = info.sender + check(player is Player) { "PLAYER executor received ${player::class.qualifiedName}" } + executor(ExecutionInfo(player, info.args, info.input)) + }, +) + +fun > SELF.playerExecutionInfoExecutorSuspend( + scope: CoroutineScope, + executor: suspend CoroutineScope.(ExecutionInfo) -> Unit, +): SELF = playerExecutionInfoExecutorSuspend(scope = { scope }, executor = executor) + +fun > SELF.consoleExecutionInfoExecutorSuspend( + scope: () -> CoroutineScope = { minestomAsyncScope }, + executor: suspend CoroutineScope.(ExecutionInfo) -> Unit, +): SELF = addExecutor( + ExecutorDefinition.Suspending(ExecutorType.CONSOLE, scope) { info -> + val console = info.sender + check(console is ConsoleSender) { "CONSOLE executor received ${console::class.qualifiedName}" } + executor(ExecutionInfo(console, info.args, info.input)) + }, +) + +fun > SELF.consoleExecutionInfoExecutorSuspend( + scope: CoroutineScope, + executor: suspend CoroutineScope.(ExecutionInfo) -> Unit, +): SELF = consoleExecutionInfoExecutorSuspend(scope = { scope }, executor = executor) diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/dsl/SuggestionDsl.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/dsl/SuggestionDsl.kt new file mode 100644 index 000000000..4719d54b1 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/dsl/SuggestionDsl.kt @@ -0,0 +1,122 @@ +package dev.slne.surf.api.minestom.command.dsl + +import dev.slne.surf.api.minestom.command.argument.Argument +import dev.slne.surf.api.minestom.command.suggestion.* + +fun Argument.replaceSuggestions(vararg values: String): Argument = + replaceSuggestions(ArgumentSuggestions.strings(*values)) + +/** + * Replaces the built-in suggestions with the strings [provider] returns for each request. + * + * A lambda literal passed directly resolves to the member [Argument.replaceSuggestions] instead of + * this extension; pass a value explicitly typed `(SuggestionInfo) -> Collection` to select + * it. + */ +fun Argument.replaceSuggestions(provider: (SuggestionInfo) -> Collection): Argument = + replaceSuggestions(ArgumentSuggestions.strings(provider)) + +fun Argument.includeSuggestions(vararg values: String): Argument = + includeSuggestions(ArgumentSuggestions.strings(*values)) + +/** + * Adds the strings [provider] returns for each request to the built-in suggestions. + * + * A lambda literal passed directly resolves to the member [Argument.includeSuggestions] instead of + * this extension; pass a value explicitly typed `(SuggestionInfo) -> Collection` to select + * it. + */ +fun Argument.includeSuggestions(provider: (SuggestionInfo) -> Collection): Argument = + includeSuggestions(ArgumentSuggestions.strings(provider)) + +fun Argument.replaceSuggestionsAsync( + provider: suspend (SuggestionInfo) -> Collection, +): Argument = replaceSuggestions(ArgumentSuggestions.stringsAsync(provider)) + +fun Argument.includeSuggestionsAsync( + provider: suspend (SuggestionInfo) -> Collection, +): Argument = includeSuggestions(ArgumentSuggestions.stringsAsync(provider)) + +fun Argument.replaceSuggestionsWithTooltips(vararg values: StringTooltip): Argument = + replaceSuggestions(ArgumentSuggestions.stringsWithTooltips(*values)) + +fun Argument.replaceSuggestionsWithTooltips( + provider: (SuggestionInfo) -> Collection, +): Argument = replaceSuggestions(ArgumentSuggestions.stringsWithTooltips(provider)) + +fun Argument.includeSuggestionsWithTooltips(vararg values: StringTooltip): Argument = + includeSuggestions(ArgumentSuggestions.stringsWithTooltips(*values)) + +fun Argument.includeSuggestionsWithTooltips( + provider: (SuggestionInfo) -> Collection, +): Argument = includeSuggestions(ArgumentSuggestions.stringsWithTooltips(provider)) + +fun Argument.replaceSuggestionsWithTooltipsAsync( + provider: suspend (SuggestionInfo) -> Collection, +): Argument = replaceSuggestions(ArgumentSuggestions.stringsWithTooltipsAsync(provider)) + +fun Argument.includeSuggestionsWithTooltipsAsync( + provider: suspend (SuggestionInfo) -> Collection, +): Argument = includeSuggestions(ArgumentSuggestions.stringsWithTooltipsAsync(provider)) + +fun Argument.replaceSafeSuggestions(vararg values: T): Argument = + replaceSafeSuggestions(SafeSuggestions.suggest(*values)) + +/** + * Replaces the built-in suggestions with the values [provider] returns for each request. + * + * A lambda literal passed directly resolves to the member [Argument.replaceSafeSuggestions] + * instead of this extension; pass a value explicitly typed `(SuggestionInfo) -> Collection` to + * select it. + */ +fun Argument.replaceSafeSuggestions(provider: (SuggestionInfo) -> Collection): Argument = + replaceSafeSuggestions(SafeSuggestions.suggest(provider)) + +fun Argument.includeSafeSuggestions(vararg values: T): Argument = + includeSafeSuggestions(SafeSuggestions.suggest(*values)) + +/** + * Adds the values [provider] returns for each request to the built-in suggestions. + * + * A lambda literal passed directly resolves to the member [Argument.includeSafeSuggestions] + * instead of this extension; pass a value explicitly typed `(SuggestionInfo) -> Collection` to + * select it. + */ +fun Argument.includeSafeSuggestions(provider: (SuggestionInfo) -> Collection): Argument = + includeSafeSuggestions(SafeSuggestions.suggest(provider)) + +fun Argument.replaceSafeSuggestionsAsync( + provider: suspend (SuggestionInfo) -> Collection, +): Argument = replaceSafeSuggestions(SafeSuggestions.suggestAsync(provider)) + +fun Argument.includeSafeSuggestionsAsync( + provider: suspend (SuggestionInfo) -> Collection, +): Argument = includeSafeSuggestions(SafeSuggestions.suggestAsync(provider)) + +fun Argument.replaceSafeSuggestionsWithTooltips(vararg values: Tooltip): Argument = + replaceSafeSuggestions(SafeSuggestions.tooltips(*values)) + +fun Argument.replaceSafeSuggestionsWithTooltips( + provider: (SuggestionInfo) -> Collection>, +): Argument = replaceSafeSuggestions(SafeSuggestions.tooltips(provider)) + +fun Argument.includeSafeSuggestionsWithTooltips(vararg values: Tooltip): Argument = + includeSafeSuggestions(SafeSuggestions.tooltips(*values)) + +fun Argument.includeSafeSuggestionsWithTooltips( + provider: (SuggestionInfo) -> Collection>, +): Argument = includeSafeSuggestions(SafeSuggestions.tooltips(provider)) + +fun Argument.replaceSafeSuggestionsWithTooltipsAsync( + provider: suspend (SuggestionInfo) -> Collection>, +): Argument = replaceSafeSuggestions(SafeSuggestions.tooltipsAsync(provider)) + +fun Argument.includeSafeSuggestionsWithTooltipsAsync( + provider: suspend (SuggestionInfo) -> Collection>, +): Argument = includeSafeSuggestions(SafeSuggestions.tooltipsAsync(provider)) + +fun Argument.replaceSuggestions(branch: SuggestionsBranch): Argument = + replaceSuggestions(ArgumentSuggestions.branch(branch)) + +fun Argument.includeSuggestions(branch: SuggestionsBranch): Argument = + includeSuggestions(ArgumentSuggestions.branch(branch)) diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/exception/CommandSyntaxException.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/exception/CommandSyntaxException.kt new file mode 100644 index 000000000..b37662b83 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/exception/CommandSyntaxException.kt @@ -0,0 +1,35 @@ +package dev.slne.surf.api.minestom.command.exception + +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import java.io.Serial + +open class CommandSyntaxException( + val component: Component?, + val input: String? = null, + val cursor: Int? = null, + cause: Throwable? = null, +) : RuntimeException(component?.let { PlainTextComponentSerializer.plainText().serialize(it) }, cause) { + companion object { + @Serial + private const val serialVersionUID: Long = -540694787746386949L + } +} + +class WrapperCommandSyntaxException( + val exception: CommandSyntaxException, +) : RuntimeException(exception.message, exception) { + val component: Component? + get() = exception.component + + val input: String? + get() = exception.input + + val cursor: Int? + get() = exception.cursor + + companion object { + @Serial + private const val serialVersionUID: Long = -2695063468338553356L + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/exception/CommandValidationException.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/exception/CommandValidationException.kt new file mode 100644 index 000000000..473346916 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/exception/CommandValidationException.kt @@ -0,0 +1,16 @@ +package dev.slne.surf.api.minestom.command.exception + +import java.io.Serial + +/** + * Thrown when a command, argument or registration is configured invalidly, for example with a + * malformed name, a required argument after an optional one or an unsupported suggestion provider. + * + * @param message description of the invalid configuration + */ +class CommandValidationException(message: String) : IllegalArgumentException(message) { + companion object { + @Serial + private const val serialVersionUID: Long = -7547371556844925457L + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/exception/ComponentMessage.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/exception/ComponentMessage.kt new file mode 100644 index 000000000..cab710f36 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/exception/ComponentMessage.kt @@ -0,0 +1,22 @@ +package dev.slne.surf.api.minestom.command.exception + +import com.mojang.brigadier.Message +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer + +/** + * Brigadier [Message] that retains the [component] it was created from. + * + * [getString] returns the plain text of the component. + */ +class ComponentMessage(val component: Component) : Message { + override fun getString(): String = + PlainTextComponentSerializer.plainText().serialize(component) + + override fun toString(): String = string +} + +/** + * Returns the component this message was created from, or `null` if it is not a [ComponentMessage]. + */ +fun Message.componentOrNull(): Component? = (this as? ComponentMessage)?.component diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/executor/CommandArguments.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/executor/CommandArguments.kt new file mode 100644 index 000000000..cf665bcc7 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/executor/CommandArguments.kt @@ -0,0 +1,90 @@ +package dev.slne.surf.api.minestom.command.executor + +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import kotlin.reflect.KClass +import kotlin.reflect.KProperty + +data class ParsedArgument( + val name: String, + val value: Any?, + val raw: String?, + val present: Boolean, +) + +class CommandArguments private constructor( + private val ordered: List, +) { + private val named = Object2ObjectOpenHashMap(ordered.size).apply { + ordered.forEach { argument -> + put(argument.name, argument) + } + } + + inline fun get(name: String): T = typed(name, T::class, nullable = null is T) as T + + inline fun getOptional(name: String): T? = + typed(name, T::class, nullable = true) as T? + + fun getRaw(name: String): String? = named[name]?.raw + + fun getRaw(index: Int): String? = ordered.getOrNull(index)?.raw + + fun rawArguments(): List { + val rawArguments = ObjectArrayList(ordered.size) + + ordered.forEach { argument -> + if (argument.present) { + argument.raw?.let(rawArguments::add) + } + } + + return rawArguments + } + + operator fun get(index: Int): Any? = ordered[index].value + + operator fun contains(name: String): Boolean = named[name]?.present == true + + inline operator fun getValue(thisRef: Any?, property: KProperty<*>): T = + typed(property.name, T::class, nullable = null is T) as T + + @PublishedApi + internal fun typed(name: String, expected: KClass<*>, nullable: Boolean): Any? { + val argument = named[name] + ?: throw IllegalArgumentException("No argument named '$name' exists") + if (!argument.present && argument.value == null) { + if (nullable) return null + throw IllegalArgumentException("Required argument '$name' is absent") + } + val value = argument.value + if (value == null) { + if (nullable) return null + throw IllegalArgumentException("Argument '$name' is null but ${expected.qualifiedName} was required") + } + require(expected.isInstance(value)) { + "Argument '$name' expected ${expected.qualifiedName}, got ${value::class.qualifiedName}" + } + return value + } + + companion object { + @InternalSurfApi + fun of(arguments: List): CommandArguments { + val copy = ObjectArrayList(arguments) + val names = ObjectOpenHashSet(copy.size) + + copy.forEach { argument -> + require(names.add(argument.name)) { + "Parsed argument names must be unique" + } + } + + return CommandArguments(copy) + } + + fun empty(): CommandArguments = CommandArguments(emptyList()) + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/executor/Executors.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/executor/Executors.kt new file mode 100644 index 000000000..b2c4d534a --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/executor/Executors.kt @@ -0,0 +1,64 @@ +package dev.slne.surf.api.minestom.command.executor + +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import kotlinx.coroutines.CoroutineScope +import net.minestom.server.command.CommandSender +import net.minestom.server.command.ConsoleSender +import net.minestom.server.entity.Player + +enum class ExecutorType { + ANY, + PLAYER, + CONSOLE, +} + +data class ExecutionInfo( + val sender: S, + val args: CommandArguments, + val input: String, +) + +sealed interface ExecutorDefinition { + val type: ExecutorType + + data class Normal( + override val type: ExecutorType, + val executor: (ExecutionInfo) -> Unit, + ) : ExecutorDefinition + + data class Resulting( + override val type: ExecutorType, + val executor: (ExecutionInfo) -> Int, + ) : ExecutorDefinition + + data class Suspending( + override val type: ExecutorType, + val scopeProvider: () -> CoroutineScope, + val executor: suspend CoroutineScope.(ExecutionInfo) -> Unit, + ) : ExecutorDefinition +} + +interface CommandExecutable> { + @InternalSurfApi + fun addExecutor(definition: ExecutorDefinition): SELF + + fun executes(executor: (CommandSender, CommandArguments) -> Unit): SELF = addExecutor( + ExecutorDefinition.Normal(ExecutorType.ANY) { info -> executor(info.sender, info.args) }, + ) + + fun executesPlayer(executor: (Player, CommandArguments) -> Unit): SELF = addExecutor( + ExecutorDefinition.Normal(ExecutorType.PLAYER) { info -> + val player = info.sender + check(player is Player) { "PLAYER executor received ${player::class.qualifiedName}" } + executor(player, info.args) + }, + ) + + fun executesConsole(executor: (ConsoleSender, CommandArguments) -> Unit): SELF = addExecutor( + ExecutorDefinition.Normal(ExecutorType.CONSOLE) { info -> + val console = info.sender + check(console is ConsoleSender) { "CONSOLE executor received ${console::class.qualifiedName}" } + executor(console, info.args) + }, + ) +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/executor/SuspendExecutors.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/executor/SuspendExecutors.kt new file mode 100644 index 000000000..c194dab0d --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/executor/SuspendExecutors.kt @@ -0,0 +1,79 @@ +package dev.slne.surf.api.minestom.command.executor + +import dev.slne.surf.api.minestom.command.exception.CommandSyntaxException +import dev.slne.surf.api.minestom.command.exception.WrapperCommandSyntaxException +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import kotlinx.coroutines.* +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.minestom.server.MinecraftServer +import net.minestom.server.command.CommandSender + +@InternalSurfApi +val GENERIC_COMMAND_FAILURE: Component = Component.text( + "Beim Ausführen des Befehls ist ein Fehler aufgetreten.", + NamedTextColor.RED, +) + +/** + * Launches the suspending executor of [definition] in its scope and reports a failure to [sender]. + * + * A [TimeoutCancellationException] from the executor's own timeout is reported like any other + * failure; cancellation of the launched coroutine propagates. + */ +@InternalSurfApi +fun launchCommandExecutor( + definition: ExecutorDefinition.Suspending, + sender: CommandSender, + arguments: CommandArguments, + input: String, +): Job = definition.scopeProvider().launch { + try { + definition.executor(this, ExecutionInfo(sender, arguments, input)) + } catch (failure: Throwable) { + if (failure is TimeoutCancellationException && !isActive) throw failure + handleCommandFailure(sender, input, failure) + } +} + +/** + * Reports [failure] to [sender]: a syntax failure with its component, anything else as a logged + * generic failure. + * + * Rethrows a [CancellationException] other than a [TimeoutCancellationException]. + */ +internal fun handleCommandFailure( + sender: CommandSender, + input: String, + failure: Throwable, +) { + if (failure is CancellationException && failure !is TimeoutCancellationException) throw failure + + when (failure) { + is WrapperCommandSyntaxException -> sendSyntaxFailure(sender, failure.exception) + is CommandSyntaxException -> sendSyntaxFailure(sender, failure) + else -> sendUnexpectedFailure(sender, input, failure, GENERIC_COMMAND_FAILURE) + } +} + +@InternalSurfApi +fun handleCommandExecutorFailure( + sender: CommandSender, + input: String, + failure: Throwable, +) = handleCommandFailure(sender, input, failure) + +private fun sendUnexpectedFailure( + sender: CommandSender, + input: String, + failure: Throwable, + component: Component, +) { + MinecraftServer.LOGGER.error("Failed to execute command '{}' for {}", input, sender, failure) + sender.sendMessage(component) +} + +private fun sendSyntaxFailure(sender: CommandSender, failure: CommandSyntaxException) { + val component = failure.component ?: throw failure + sender.sendMessage(component) +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/internal/CommandAPIPlatform.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/internal/CommandAPIPlatform.kt new file mode 100644 index 000000000..d82e9977b --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/internal/CommandAPIPlatform.kt @@ -0,0 +1,15 @@ +package dev.slne.surf.api.minestom.command.internal + +import dev.slne.surf.api.minestom.command.CommandDefinition +import dev.slne.surf.api.minestom.command.RegisteredCommand +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import net.minestom.server.command.CommandSender + +@InternalSurfApi +interface CommandAPIPlatform { + fun register(definition: CommandDefinition, namespace: String?): RegisteredCommand + + fun unregister(name: String): Boolean + + fun execute(sender: CommandSender, input: String): Int +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions.kt new file mode 100644 index 000000000..74a73301d --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/suggestion/ArgumentSuggestions.kt @@ -0,0 +1,202 @@ +/* + * Substantially translated from CommandAPI 12.0.0 (https://github.com/CommandAPI/CommandAPI). + * MIT License, Copyright (c) 2020 - 2022 Jorel Ali. + * The complete license is distributed in META-INF/LICENSES/CommandAPI-LICENSE.txt. + */ +package dev.slne.surf.api.minestom.command.suggestion + +import dev.slne.surf.api.minestom.command.exception.CommandSyntaxException +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import it.unimi.dsi.fastutil.objects.ObjectImmutableList +import it.unimi.dsi.fastutil.objects.ObjectList +import kotlinx.coroutines.* +import net.kyori.adventure.text.logger.slf4j.ComponentLogger + +/** + * Supplies suggestions for an argument. + * + * Providers run in a coroutine on the server's asynchronous scope, not on the network thread, and + * may suspend. An exception thrown by a provider is logged and the provider contributes no entries. + * + * Register a provider with [dev.slne.surf.api.minestom.command.argument.Argument.replaceSuggestions] + * or [dev.slne.surf.api.minestom.command.argument.Argument.includeSuggestions]. + */ +fun interface ArgumentSuggestions { + /** + * Returns the suggestions for the input described by [info]. + */ + suspend fun suggest(info: SuggestionInfo): List + + /** + * How the returned entries are matched against the text typed for the argument. + */ + val filterPolicy: SuggestionFilter + get() = SuggestionFilter.PREFIX + + companion object { + /** + * Returns a provider without entries. + */ + fun empty(): ArgumentSuggestions = ArgumentSuggestions { emptyList() } + + /** + * Returns a provider that suggests the fixed [values]. + */ + fun strings(vararg values: String): ArgumentSuggestions = strings(ObjectList.of(*values)) + + /** + * Returns a provider that suggests the fixed [values]. + */ + fun strings(values: Collection): ArgumentSuggestions { + val copy = ObjectImmutableList(values) + return ArgumentSuggestions { + copy.mapTo(ObjectArrayList(copy.size), ::withoutTooltip) + } + } + + /** + * Returns a provider that suggests the strings [provider] returns for each request. + */ + fun strings(provider: (SuggestionInfo) -> Collection): ArgumentSuggestions { + return ArgumentSuggestions { info -> + val provided = provider(info) + provided.mapTo(ObjectArrayList(provided.size), ::withoutTooltip) + } + } + + /** + * Alias of [strings] for a provider. + */ + fun stringCollection(provider: (SuggestionInfo) -> Collection): ArgumentSuggestions { + return strings(provider) + } + + /** + * Returns a provider that suggests the strings the suspending [provider] returns for each + * request. + */ + fun stringsAsync(provider: suspend (SuggestionInfo) -> Collection): ArgumentSuggestions { + return ArgumentSuggestions { info -> + val provided = provider(info) + provided.mapTo(ObjectArrayList(provided.size), ::withoutTooltip) + } + } + + /** + * Alias of [stringsAsync]. + */ + fun stringCollectionAsync(provider: suspend (SuggestionInfo) -> Collection): ArgumentSuggestions = + stringsAsync(provider) + + /** + * Returns a provider that suggests the fixed [values] with their tooltips. + */ + fun stringsWithTooltips(vararg values: StringTooltip): ArgumentSuggestions = + stringsWithTooltips(ObjectList.of(*values)) + + /** + * Returns a provider that suggests the fixed [values] with their tooltips. + */ + fun stringsWithTooltips(values: Collection): ArgumentSuggestions { + val copy = ObjectImmutableList(values) + return ArgumentSuggestions { copy } + } + + /** + * Returns a provider that suggests the entries [provider] returns for each request. + */ + fun stringsWithTooltips(provider: (SuggestionInfo) -> Collection): ArgumentSuggestions { + return ArgumentSuggestions { info -> ObjectImmutableList(provider(info)) } + } + + /** + * Alias of [stringsWithTooltips] for a provider. + */ + fun stringsWithTooltipsCollection(provider: (SuggestionInfo) -> Collection): ArgumentSuggestions = + stringsWithTooltips(provider) + + /** + * Returns a provider that suggests the entries the suspending [provider] returns for each + * request. + */ + fun stringsWithTooltipsAsync(provider: suspend (SuggestionInfo) -> Collection): ArgumentSuggestions { + return ArgumentSuggestions { info -> ObjectImmutableList(provider(info)) } + } + + /** + * Alias of [stringsWithTooltipsAsync]. + */ + fun stringsWithTooltipsCollectionAsync(provider: suspend (SuggestionInfo) -> Collection): ArgumentSuggestions = + stringsWithTooltipsAsync(provider) + + /** + * Returns a provider whose entries are offered without prefix filtering + * ([SuggestionFilter.NONE]). + */ + fun unfiltered(provider: suspend (SuggestionInfo) -> Collection): ArgumentSuggestions { + return object : ArgumentSuggestions { + override val filterPolicy = SuggestionFilter.NONE + + override suspend fun suggest(info: SuggestionInfo): List { + return ObjectImmutableList(provider(info)) + } + } + } + + /** + * Returns a provider that concatenates the entries of all [providers], which run + * concurrently. + * + * The merged provider filters by prefix; the filter policies of [providers] are not + * combined. A provider that fails is logged and contributes no entries without affecting + * the others; cancellation propagates. + */ + fun merge(vararg providers: ArgumentSuggestions): ArgumentSuggestions { + val copy = ObjectImmutableList(providers) + return ArgumentSuggestions { info -> + coroutineScope { + copy.mapTo(ObjectArrayList(copy.size)) { provider -> + async { suggestOrEmpty(provider, info) } + } + .awaitAll() + .flatten() + } + } + } + + /** + * Returns a provider that selects its entries from [branch] using the raw values of the + * arguments before the current one. + * + * Yields no entries when the branch has no provider for the current position or rejects a + * previous value, see [SuggestionsBranch.getNextSuggestion]. + */ + fun branch(branch: SuggestionsBranch): ArgumentSuggestions = ArgumentSuggestions { info -> + val next = try { + branch.getNextSuggestion(info.sender, *info.previousArgs.rawArguments().toTypedArray()) + } catch (_: CommandSyntaxException) { + null + } + + next?.suggest(info)?.let(::ObjectImmutableList).orEmpty() + } + + private suspend fun suggestOrEmpty( + provider: ArgumentSuggestions, + info: SuggestionInfo, + ): List = try { + provider.suggest(info) + } catch (failure: Throwable) { + val ownTimeout = failure is TimeoutCancellationException && + currentCoroutineContext().isActive + if (failure is CancellationException && !ownTimeout) throw failure + + LOGGER.error("Suggestion provider failed and contributes no entries", failure) + emptyList() + } + + private fun withoutTooltip(value: String) = StringTooltip(value, null) + } +} + +private val LOGGER = ComponentLogger.logger() diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/suggestion/SafeSuggestions.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/suggestion/SafeSuggestions.kt new file mode 100644 index 000000000..4786ecafd --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/suggestion/SafeSuggestions.kt @@ -0,0 +1,124 @@ +/* + * Substantially translated from CommandAPI 12.0.0 (https://github.com/CommandAPI/CommandAPI). + * MIT License, Copyright (c) 2020 - 2022 Jorel Ali. + * The complete license is distributed in META-INF/LICENSES/CommandAPI-LICENSE.txt. + */ +package dev.slne.surf.api.minestom.command.suggestion + +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import it.unimi.dsi.fastutil.objects.ObjectImmutableList +import it.unimi.dsi.fastutil.objects.ObjectList + +/** + * Supplies suggestions as values of the argument's own type [T]. + * + * The values are converted to text with the argument's formatter, so every suggestion is spelled + * the way the argument parses it. Entries are always filtered by the typed prefix. Providers run + * like [ArgumentSuggestions]: in a coroutine on the asynchronous scope, with exceptions logged. + * + * Register a provider with [dev.slne.surf.api.minestom.command.argument.Argument.replaceSafeSuggestions] + * or [dev.slne.surf.api.minestom.command.argument.Argument.includeSafeSuggestions]. + */ +fun interface SafeSuggestions { + /** + * Returns the suggestions for the input described by [info]. + */ + suspend fun suggest(info: SuggestionInfo): List> + + companion object { + /** + * Returns a provider without entries. + */ + fun empty(): SafeSuggestions = SafeSuggestions { emptyList() } + + /** + * Returns a provider that suggests the fixed [values]. + */ + fun suggest(vararg values: T): SafeSuggestions = suggest(ObjectList.of(*values)) + + /** + * Returns a provider that suggests the fixed [values]. + */ + fun suggest(values: Collection): SafeSuggestions { + val copy = ObjectImmutableList(values) + return SafeSuggestions { + copy.mapTo(ObjectArrayList(copy.size), ::withoutTooltip) + } + } + + /** + * Returns a provider that suggests the values [provider] returns for each request. + */ + fun suggest(provider: (SuggestionInfo) -> Collection): SafeSuggestions { + return SafeSuggestions { info -> + val provided = provider(info) + provided.mapTo(ObjectArrayList(provided.size), ::withoutTooltip) + } + } + + /** + * Alias of [suggest] for a provider. + */ + fun suggestCollection(provider: (SuggestionInfo) -> Collection): SafeSuggestions = + suggest(provider) + + /** + * Returns a provider that suggests the values the suspending [provider] returns for each + * request. + */ + fun suggestAsync(provider: suspend (SuggestionInfo) -> Collection): SafeSuggestions { + return SafeSuggestions { info -> + val provided = provider(info) + provided.mapTo(ObjectArrayList(provided.size), ::withoutTooltip) + } + } + + /** + * Alias of [suggestAsync]. + */ + fun suggestCollectionAsync(provider: suspend (SuggestionInfo) -> Collection): SafeSuggestions = + suggestAsync(provider) + + /** + * Returns a provider that suggests the fixed [values] with their tooltips. + */ + fun tooltips(vararg values: Tooltip): SafeSuggestions = + tooltips(ObjectList.of(*values)) + + /** + * Returns a provider that suggests the fixed [values] with their tooltips. + */ + fun tooltips(values: Collection>): SafeSuggestions { + val copy = ObjectImmutableList(values) + return SafeSuggestions { copy } + } + + /** + * Returns a provider that suggests the entries [provider] returns for each request. + */ + fun tooltips(provider: (SuggestionInfo) -> Collection>): SafeSuggestions { + return SafeSuggestions { info -> ObjectImmutableList(provider(info)) } + } + + /** + * Alias of [tooltips] for a provider. + */ + fun tooltipCollection(provider: (SuggestionInfo) -> Collection>): SafeSuggestions = + tooltips(provider) + + /** + * Returns a provider that suggests the entries the suspending [provider] returns for each + * request. + */ + fun tooltipsAsync(provider: suspend (SuggestionInfo) -> Collection>): SafeSuggestions = + SafeSuggestions { info -> ObjectImmutableList(provider(info)) } + + /** + * Alias of [tooltipsAsync]. + */ + fun tooltipCollectionAsync(provider: suspend (SuggestionInfo) -> Collection>): SafeSuggestions = + tooltipsAsync(provider) + + private fun withoutTooltip(value: T) = Tooltip(value, null) + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/suggestion/SuggestionsBranch.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/suggestion/SuggestionsBranch.kt new file mode 100644 index 000000000..75fe455d5 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/suggestion/SuggestionsBranch.kt @@ -0,0 +1,210 @@ +/* + * Substantially translated from CommandAPI 12.0.0 (https://github.com/CommandAPI/CommandAPI). + * MIT License, Copyright (c) 2020 - 2022 Jorel Ali. + * The complete license is distributed in META-INF/LICENSES/CommandAPI-LICENSE.txt. + */ +package dev.slne.surf.api.minestom.command.suggestion + +import dev.slne.surf.api.minestom.command.exception.CommandSyntaxException +import dev.slne.surf.api.minestom.command.executor.CommandArguments +import dev.slne.surf.api.minestom.command.executor.ParsedArgument +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import it.unimi.dsi.fastutil.objects.ObjectImmutableList +import it.unimi.dsi.fastutil.objects.ObjectList +import net.kyori.adventure.text.Component +import net.minestom.server.command.CommandSender + +/** + * Suggestions for a sequence of arguments, selected by the values typed for the arguments before. + * + * [suggest] declares one provider per argument position, in order; `null` marks a position without + * suggestions that accepts any value. [branch] adds alternative continuations that follow those + * positions. A typed value at a position with a provider must equal one of that provider's + * suggestions. + * + * [ArgumentSuggestions.branch] selects the provider by the number and values of the preceding + * arguments, so the same branch is attached to every argument it covers. + */ +class SuggestionsBranch private constructor( + suggestions: List, +) { + private val suggestions = ObjectImmutableList(suggestions) + private val branches = ObjectArrayList() + + /** + * Adds [branches] as alternative continuations after this branch's own positions. + */ + fun branch(vararg branches: SuggestionsBranch): SuggestionsBranch = apply { + this.branches += branches + } + + /** + * Returns the provider for the argument that follows the values in [previousRaw], or `null` + * when the branch defines no suggestions there. + * + * When several continuations match, the providers they define are merged with + * [ArgumentSuggestions.merge]; continuations without a provider are ignored. + * + * @throws CommandSyntaxException when a value in [previousRaw] is not among the suggestions of + * its position + */ + suspend fun getNextSuggestion( + sender: CommandSender, + vararg previousRaw: String, + ): ArgumentSuggestions? = traverse( + mode = TraversalMode.NEXT, + sender = sender, + rawArguments = ObjectList.of(*previousRaw), + processed = emptyList(), + ).provider + + /** + * Verifies that [previousRaw] is a complete, valid sequence for this branch. + * + * @throws CommandSyntaxException when a value is not among the suggestions of its position, or + * when values are missing for a position whose provider has suggestions + */ + suspend fun enforceReplacements( + sender: CommandSender, + vararg previousRaw: String, + ) { + traverse( + mode = TraversalMode.ENFORCE, + sender = sender, + rawArguments = ObjectList.of(*previousRaw), + processed = emptyList(), + ) + } + + /** + * Matches [rawArguments] against the positions of this branch and then its continuations. + * + * [TraversalMode.NEXT] returns the provider for the first position without a value. + * [TraversalMode.ENFORCE] only validates and always returns an empty result. + */ + private suspend fun traverse( + mode: TraversalMode, + sender: CommandSender, + rawArguments: List, + processed: List, + ): TraversalResult { + if (suggestions.isEmpty && branches.isEmpty) { + return TraversalResult(null) + } + + val currentProcessed = ObjectArrayList(processed) + for (provider in suggestions) { + if (currentProcessed.size == rawArguments.size) { + if (mode == TraversalMode.NEXT) { + return TraversalResult(provider) + } + + if (provider == null) { + return TraversalResult(null) + } + + val missingInfo = suggestionInfo(sender, currentProcessed, "") + if (provider.suggest(missingInfo).isNotEmpty()) { + throw syntax("Expected more arguments", rawArguments, currentProcessed) + } + + return TraversalResult(null) + } + + val candidate = rawArguments[currentProcessed.size] + if (provider != null) { + val info = suggestionInfo(sender, currentProcessed, "") + if (provider.suggest(info).none { it.suggestion == candidate }) { + val message = + if (currentProcessed.isEmpty) "Unknown command" else "Unknown argument" + throw syntax(message, rawArguments, currentProcessed) + } + } + + currentProcessed += candidate + } + + if (branches.isEmpty) { + return TraversalResult(null) + } + + val successes = ObjectArrayList() + val failures = ObjectArrayList() + + for (branch in branches) { + try { + successes += branch.traverse(mode, sender, rawArguments, currentProcessed) + } catch (exception: CommandSyntaxException) { + failures += exception + } + } + + if (successes.isEmpty) { + throw failures.maxByOrNull { it.cursor ?: -1 } + ?: syntax("Unknown argument", rawArguments, currentProcessed) + } + + if (mode == TraversalMode.ENFORCE) { + return TraversalResult(null) + } + + if (successes.size == 1) { + return successes.single() + } + + val providers = successes.mapNotNullTo(ObjectArrayList(successes.size), TraversalResult::provider) + + return when (providers.size) { + 0 -> TraversalResult(null) + 1 -> TraversalResult(providers.single()) + else -> TraversalResult(ArgumentSuggestions.merge(*providers.toTypedArray())) + } + } + + private fun suggestionInfo( + sender: CommandSender, + processed: List, + currentArgument: String, + ): SuggestionInfo { + val parsed = processed.mapIndexedTo(ObjectArrayList(processed.size)) { index, raw -> + ParsedArgument(index.toString(), raw, raw, present = true) + } + + return SuggestionInfo( + sender = sender, + previousArgs = CommandArguments.of(parsed), + currentInput = processed.joinToString( + " ", + postfix = if (processed.isEmpty()) "" else " " + ), + currentArg = currentArgument, + ) + } + + private fun syntax( + message: String, + rawArguments: List, + processed: List, + ) = CommandSyntaxException( + component = Component.text(message), + input = rawArguments.joinToString(" "), + cursor = processed.joinToString(" ").length + if (processed.isEmpty()) 0 else 1, + ) + + private data class TraversalResult(val provider: ArgumentSuggestions?) + + private enum class TraversalMode { + NEXT, + ENFORCE, + } + + companion object { + /** + * Creates a branch with one provider per argument position, in order. + * + * A `null` entry marks a position without suggestions that accepts any value. + */ + fun suggest(vararg suggestions: ArgumentSuggestions?): SuggestionsBranch = + SuggestionsBranch(ObjectList.of(*suggestions)) + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/suggestion/Tooltip.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/suggestion/Tooltip.kt new file mode 100644 index 000000000..b16f24a0d --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/command/suggestion/Tooltip.kt @@ -0,0 +1,32 @@ +package dev.slne.surf.api.minestom.command.suggestion + +import dev.slne.surf.api.minestom.command.executor.CommandArguments +import net.kyori.adventure.text.Component +import net.minestom.server.command.CommandSender + +data class Tooltip( + val suggestion: T, + val tooltip: Component?, +) + +data class StringTooltip( + val suggestion: String, + val tooltip: Component?, +) { + companion object { + fun ofString(suggestion: String, tooltip: Component? = null) = + StringTooltip(suggestion, tooltip) + } +} + +data class SuggestionInfo( + val sender: CommandSender, + val previousArgs: CommandArguments, + val currentInput: String, + val currentArg: String, +) + +enum class SuggestionFilter { + PREFIX, + NONE, +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/coroutine/MinestomDispatchers.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/coroutine/MinestomDispatchers.kt new file mode 100644 index 000000000..95389a1a8 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/coroutine/MinestomDispatchers.kt @@ -0,0 +1,65 @@ +package dev.slne.surf.api.minestom.coroutine + +import dev.slne.surf.api.minestom.extension.SchedulerManager +import kotlinx.coroutines.* +import net.minestom.server.timer.Task +import net.minestom.server.timer.TaskSchedule +import java.lang.Runnable +import java.util.concurrent.Executors +import kotlin.coroutines.CoroutineContext + +object MinestomDispatchers { + + val Blocking: CoroutineDispatcher = Executors.newThreadPerTaskExecutor( + Thread.ofVirtual().name("surf-blocking-", 0).factory() + ).asCoroutineDispatcher() + + @OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class) + val Main: CoroutineDispatcher = object : CoroutineDispatcher(), Delay { + override fun dispatch(context: CoroutineContext, block: Runnable) { + SchedulerManager.scheduleNextTick(block) + } + + override fun scheduleResumeAfterDelay( + timeMillis: Long, + continuation: CancellableContinuation + ) { + val task = schedule(timeMillis) { + with(continuation) { + resumeUndispatched(Unit) + } + } + + continuation.invokeOnCancellation { + task.cancel() + } + } + + override fun invokeOnTimeout( + timeMillis: Long, + block: Runnable, + context: CoroutineContext + ): DisposableHandle { + val task = schedule(timeMillis, block) + + return DisposableHandle { + task.cancel() + } + } + + private fun schedule(timeMillis: Long, block: Runnable): Task { + val delay = when { + timeMillis <= 0L -> TaskSchedule.immediate() + else -> TaskSchedule.millis(timeMillis) + } + + return SchedulerManager.scheduleTask( + block, + delay, + TaskSchedule.stop() + ) + } + + override fun toString(): String = "MinestomDispatchers.Main" + } +} \ No newline at end of file diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/coroutine/MinestomScopeProvider.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/coroutine/MinestomScopeProvider.kt new file mode 100644 index 000000000..0a8045676 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/coroutine/MinestomScopeProvider.kt @@ -0,0 +1,30 @@ +package dev.slne.surf.api.minestom.coroutine + +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.plus +import net.minestom.server.MinecraftServer + +internal object MinestomScopeProvider { + + val scope: CoroutineScope + val asyncScope: CoroutineScope + val blockingScope: CoroutineScope + + init { + val exceptionHandler = CoroutineExceptionHandler { context, throwable -> + MinecraftServer.LOGGER.error("Coroutine exception in context: $context", throwable) + } + val rootScope = CoroutineScope(exceptionHandler) + + scope = rootScope + SupervisorJob() + MinestomDispatchers.Main + asyncScope = rootScope + SupervisorJob() + Dispatchers.Default + blockingScope = rootScope + SupervisorJob() + MinestomDispatchers.Blocking + } +} + +val minestomScope get() = MinestomScopeProvider.scope +val minestomAsyncScope get() = MinestomScopeProvider.asyncScope +val minestomBlockingScope get() = MinestomScopeProvider.blockingScope diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/coroutine/entity-scheduler.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/coroutine/entity-scheduler.kt new file mode 100644 index 000000000..8fe5447ca --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/coroutine/entity-scheduler.kt @@ -0,0 +1,44 @@ +package dev.slne.surf.api.minestom.coroutine + +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.suspendCancellableCoroutine +import net.minestom.server.entity.Entity +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +/** + * Runs [block] on the entity's own scheduler at the next tick and resumes with its result. + * + * The scheduler of an entity is only processed while the entity is in an instance and not removed. + * + * @throws IllegalStateException if the entity is removed or not in an instance when called + */ +suspend fun T.withEntity(block: (T) -> R): R { + check(!isRemoved) { "Cannot run on a removed entity: $this" } + checkNotNull(instance) { "Cannot run on an entity without an instance: $this" } + + return suspendCancellableCoroutine { continuation -> + val entity = this + val task = scheduler().scheduleNextTick { + try { + continuation.resume(block(entity)) + } catch (throwable: Throwable) { + continuation.resumeWithException(throwable) + } + } + continuation.invokeOnCancellation { task.cancel() } + } +} + +/** + * Runs [block] for every entity on its own scheduler, see [withEntity], and collects the results in + * the order of the entities. + * + * @throws IllegalStateException if an entity is removed or not in an instance when called + */ +suspend fun Collection.withEntities(block: (T) -> R): List = + coroutineScope { + map { entity -> async { entity.withEntity(block) } }.awaitAll() + } diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/coroutine/util.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/coroutine/util.kt new file mode 100644 index 000000000..99c459b62 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/coroutine/util.kt @@ -0,0 +1,68 @@ +package dev.slne.surf.api.minestom.coroutine + +import kotlinx.coroutines.* +import net.minestom.server.ServerProcess +import net.minestom.server.thread.Acquirable +import net.minestom.server.thread.AcquirableCollection +import net.minestom.server.thread.AcquirableSource +import kotlin.coroutines.CoroutineContext +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +val Int.ticks: Duration get() = (this * 50L - 25).milliseconds + +fun ServerProcess.launch( + context: CoroutineContext = MinestomDispatchers.Main, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> Unit +): Job { + if (!minestomScope.isActive) { + return Job() + } + + return minestomScope.launch(context, start, block) +} + +@Suppress("UnstableApiUsage") +suspend fun AcquirableSource.asyncSuspend(block: (T) -> R): R = + acquirable().asyncSuspend(block) + +/** + * Runs [block] with the element while the thread owning it is held, on the IO dispatcher. + * + * [block] may return `null` when [R] is nullable. + */ +@Suppress("UnstableApiUsage") +suspend fun Acquirable.asyncSuspend(block: (T) -> R): R { + val acquire = this + return withContext(Dispatchers.IO) { + var result: Any? = UNSET + acquire.sync { element -> + result = block.invoke(element) + } + + check(result !== UNSET) { "Acquirable did not run the block" } + @Suppress("UNCHECKED_CAST") + result as R + } +} + +/** + * Runs [block] with the elements of this collection on the IO dispatcher. + * + * The elements are acquired and released one at a time before [block] runs. [block] therefore + * runs after the acquisition ended and is not synchronized with the tick threads of the elements. + */ +@Suppress("UnstableApiUsage") +suspend fun Collection>.asyncSuspend(block: (Collection) -> R): R { + val acquirableCollection = AcquirableCollection(this) + return withContext(Dispatchers.IO) { + val resolved = ArrayList(size) + acquirableCollection.acquireSync { + resolved.add(it) + } + block.invoke(resolved) + } +} + +private val UNSET = Any() diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/CommonDialogs.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/CommonDialogs.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/CommonDialogs.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/CommonDialogs.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/DialogBuilder.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/DialogBuilder.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/DialogBuilder.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/DialogBuilder.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogActionBuilder.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogActionButtonBuilder.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogActionButtonBuilder.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogActionButtonBuilder.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogActionButtonBuilder.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogBaseBuilder.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogBaseBuilder.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogBaseBuilder.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogBaseBuilder.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogBodyBuilder.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogEntryBuilder.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogEntryBuilder.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogEntryBuilder.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogEntryBuilder.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogInputBuilder.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/builder/DialogTypeBuilder.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogCallbackContext.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogCallbackContext.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogCallbackContext.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogCallbackContext.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogCallbackOptions.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogCallbacks.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogCallbacks.kt similarity index 70% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogCallbacks.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogCallbacks.kt index cafe627ea..5f6f9bc4b 100644 --- a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogCallbacks.kt +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogCallbacks.kt @@ -1,5 +1,6 @@ package dev.slne.surf.api.minestom.dialog.callback +import dev.slne.surf.api.shared.api.util.InternalSurfApi import net.kyori.adventure.key.Key import net.kyori.adventure.nbt.BinaryTag import net.kyori.adventure.nbt.CompoundBinaryTag @@ -10,13 +11,20 @@ import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicInteger +/** + * Registry of server-side callbacks triggered by custom dialog actions. + * + * Each registration is bound to a random action key and expires after the use count or lifetime of + * its [DialogCallbackOptions]. Expired registrations are purged on [register] and [dispatch]. The + * registry is thread-safe. + */ object DialogCallbacks { private const val NAMESPACE = "surf_dialog_callback" private val registrations = ConcurrentHashMap() /** - * Registers [callback] and returns the custom action key that runs it. + * Registers [callback] under [options] and returns the custom action key that triggers it. */ fun register( options: DialogCallbackOptions = DialogCallbackOptions.DEFAULT, @@ -31,7 +39,7 @@ object DialogCallbacks { } /** - * Registers [callback] and returns the custom action that runs it. + * Registers [callback] under [options] and returns the custom action that triggers it. */ fun action( options: DialogCallbackOptions = DialogCallbackOptions.DEFAULT, @@ -39,10 +47,10 @@ object DialogCallbacks { ): DialogAction = DialogAction.Custom(register(options, callback), null) /** - * Registers [callback] and returns the custom action that runs it with the values the dialog - * collected. + * Registers [callback] under [options] and returns the custom action that triggers it with the + * dialog's input values. * - * Unlike [action], the client reports the dialog's input values back, merged with [additions]. + * Unlike [action], the client sends the input values back, merged with [additions]. */ fun dynamicAction( options: DialogCallbackOptions = DialogCallbackOptions.DEFAULT, @@ -51,15 +59,20 @@ object DialogCallbacks { ): DialogAction = DialogAction.DynamicCustom(register(options, callback), additions) /** - * Forgets the callback [key] was handed out for. + * Removes the callback registered under [key]. + * + * @return whether a callback was registered under [key] */ fun unregister(key: Key): Boolean = registrations.remove(key) != null /** - * Runs the callback [key] was handed out for and reports whether one was still registered. + * Runs the callback registered under [key] with [player] and [payload], consuming one use. + * + * @return `false` when no callback is registered under [key] or it has expired or run out of uses */ @ApiStatus.Internal - internal fun dispatch(player: Player, key: Key, payload: BinaryTag?): Boolean { + @InternalSurfApi + fun dispatch(player: Player, key: Key, payload: BinaryTag?): Boolean { purgeExpired() val registration = registrations[key] ?: return false diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogResponseView.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogResponseView.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogResponseView.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/callback/DialogResponseView.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/search/SearchDialog.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/search/SearchDialog.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/search/SearchDialog.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/dialog/search/SearchDialog.kt diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/event/EventRegistrar.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/event/EventRegistrar.kt new file mode 100644 index 000000000..539816498 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/event/EventRegistrar.kt @@ -0,0 +1,27 @@ +package dev.slne.surf.api.minestom.event + +import net.minestom.server.event.Event +import net.minestom.server.event.EventNode + +/** + * Contributes listeners to an event node, usually the one of the surf api. + * + * ``` + * class WelcomeListener : EventRegistrar { + * override fun register(node: EventNode) { + * node.addListener { it.player.sendMessage("Welcome!") } + * } + * } + * + * surfMinestomApi { + * listeners(WelcomeListener()) + * } + * ``` + */ +fun interface EventRegistrar { + + /** + * Adds this registrar's listeners to [node]. + */ + fun register(node: EventNode) +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/event/SuspendingEventNode.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/event/SuspendingEventNode.kt new file mode 100644 index 000000000..60098237e --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/event/SuspendingEventNode.kt @@ -0,0 +1,63 @@ +package dev.slne.surf.api.minestom.event + +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.isActive +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import kotlin.coroutines.cancellation.CancellationException + +class SuspendingEventNode(private val name: String) { + + companion object { + private val LOGGER = ComponentLogger.logger() + } + + private val lock = Any() + + @Volatile + private var listeners: List> = emptyList() + + + fun addListener(priority: Int = 0, listener: suspend (E) -> Unit): Registration { + val registration = Registration(this, priority, listener) + + synchronized(lock) { + listeners = (listeners + registration).sortedBy { it.priority } + } + + return registration + } + + + @InternalSurfApi + suspend fun call(event: E) { + for (registration in listeners) { + try { + registration.listener(event) + } catch (failure: Throwable) { + if (failure is CancellationException && !isOwnTimeout(failure)) throw failure + LOGGER.error("Listener for '{}' failed, continuing with the next one", name, failure) + } + } + } + + /** + * Whether [failure] is a timeout of the listener itself rather than a cancellation of the + * calling coroutine. + */ + private suspend fun isOwnTimeout(failure: CancellationException): Boolean = + failure is TimeoutCancellationException && currentCoroutineContext().isActive + + class Registration internal constructor( + private val node: SuspendingEventNode, + internal val priority: Int, + internal val listener: suspend (E) -> Unit + ) : AutoCloseable { + override fun close() { + synchronized(node.lock) { + node.listeners -= this + } + } + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/extension/entity.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/extension/entity.kt new file mode 100644 index 000000000..f30176dee --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/extension/entity.kt @@ -0,0 +1,14 @@ +package dev.slne.surf.api.minestom.extension + +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.Component.text +import net.minestom.server.component.DataComponents +import net.minestom.server.entity.Entity +import net.minestom.server.entity.metadata.EntityMeta + +val Entity.displayName: Component + get() = get(DataComponents.CUSTOM_NAME) ?: text(this.entityType.key().asMinimalString()) + +inline fun Entity.editEntityMeta(noinline editor: (M) -> Unit) { + editEntityMeta(M::class.java, editor) +} \ No newline at end of file diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/extension/events.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/extension/events.kt new file mode 100644 index 000000000..febc80157 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/extension/events.kt @@ -0,0 +1,11 @@ +package dev.slne.surf.api.minestom.extension + +import net.minestom.server.event.Event +import net.minestom.server.event.EventListener +import net.minestom.server.event.EventNode + +inline fun EventNode.addListener( + noinline listener: (T) -> Unit +) { + addListener(EventListener.of(T::class.java, listener)) +} \ No newline at end of file diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/extension/instance.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/extension/instance.kt new file mode 100644 index 000000000..c54a15012 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/extension/instance.kt @@ -0,0 +1,36 @@ +package dev.slne.surf.api.minestom.extension + +import net.kyori.adventure.key.Key +import net.minestom.server.MinecraftServer +import net.minestom.server.instance.InstanceContainer +import net.minestom.server.instance.generator.GenerationUnit +import net.minestom.server.instance.generator.UnitModifier +import net.minestom.server.registry.DynamicRegistry +import net.minestom.server.registry.RegistryKey +import net.minestom.server.world.DimensionType +import java.util.* + +inline fun GenerationUnit.modify(block: UnitModifier.() -> Unit) { + this.modifier().apply(block) +} + +inline fun InstanceContainer.generator(crossinline block: GenerationUnit.() -> Unit) = + setGenerator { + block(it) + } + +inline fun buildInstance( + dimensionType: RegistryKey = DimensionType.OVERWORLD, + dimensionName: Key = dimensionType.key(), + register: Boolean = true, + block: InstanceContainer.() -> Unit, +): InstanceContainer = InstanceContainer( + MinecraftServer.getRegistries(), + UUID.randomUUID(), + dimensionType, + null, + dimensionName +).apply { + block() + if (register) InstanceManager.registerInstance(this) +} \ No newline at end of file diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/extension/registry.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/extension/registry.kt new file mode 100644 index 000000000..f97cebe6b --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/extension/registry.kt @@ -0,0 +1,45 @@ +package dev.slne.surf.api.minestom.extension + +import net.kyori.adventure.key.Key +import net.minestom.server.particle.Particle +import net.minestom.server.particle.ParticleRegistryAccess +import net.minestom.server.registry.Registry +import net.minestom.server.registry.RegistryKey + +/** + * Returns the value registered under [key]. + * + * @throws NoSuchElementException when nothing is registered under [key] + */ +fun Registry.getOrThrow(key: Key): T { + return this.get(key) ?: notFound(key) +} + +/** + * Returns the value registered under the numeric [id]. + * + * @throws NoSuchElementException when nothing is registered under [id] + */ +fun Registry.getOrThrow(id: Int): T { + return this.get(id) ?: notFound(id) +} + +/** + * Returns the value registered under [key]. + * + * @throws NoSuchElementException when nothing is registered under [key] + */ +fun Registry.getOrThrow(key: RegistryKey): T { + return this.get(key) ?: notFound(key.key()) +} + +@Suppress("NOTHING_TO_INLINE") +private inline fun Registry<*>.notFound(key: Any): Nothing { + throw NoSuchElementException("No value for '$key' in $this") +} + +/** + * The static registry backing [Particle]. Minestom does not expose it. + */ +val particleRegistry: Registry + get() = ParticleRegistryAccess.registry() diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/extension/server.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/extension/server.kt new file mode 100644 index 000000000..eb6396b5d --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/extension/server.kt @@ -0,0 +1,96 @@ +package dev.slne.surf.api.minestom.extension + +import net.minestom.server.MinecraftServer +import net.minestom.server.MinecraftServer.enchantmentEntityEffects +import net.minestom.server.MinecraftServer.enchantmentLevelBasedValues +import net.minestom.server.MinecraftServer.enchantmentLocationEffects +import net.minestom.server.MinecraftServer.enchantmentValueEffects +import net.minestom.server.MinecraftServer.getAdvancementManager +import net.minestom.server.MinecraftServer.getBannerPatternRegistry +import net.minestom.server.MinecraftServer.getBiomeRegistry +import net.minestom.server.MinecraftServer.getBlockManager +import net.minestom.server.MinecraftServer.getBossBarManager +import net.minestom.server.MinecraftServer.getChatTypeRegistry +import net.minestom.server.MinecraftServer.getCommandManager +import net.minestom.server.MinecraftServer.getConnectionManager +import net.minestom.server.MinecraftServer.getDamageTypeRegistry +import net.minestom.server.MinecraftServer.getDimensionTypeRegistry +import net.minestom.server.MinecraftServer.getEnchantmentRegistry +import net.minestom.server.MinecraftServer.getExceptionManager +import net.minestom.server.MinecraftServer.getGlobalEventHandler +import net.minestom.server.MinecraftServer.getInstanceManager +import net.minestom.server.MinecraftServer.getInstrumentRegistry +import net.minestom.server.MinecraftServer.getJukeboxSongRegistry +import net.minestom.server.MinecraftServer.getPacketListenerManager +import net.minestom.server.MinecraftServer.getPacketParser +import net.minestom.server.MinecraftServer.getPaintingVariantRegistry +import net.minestom.server.MinecraftServer.getRecipeManager +import net.minestom.server.MinecraftServer.getSchedulerManager +import net.minestom.server.MinecraftServer.getTeamManager +import net.minestom.server.MinecraftServer.getTrimMaterialRegistry +import net.minestom.server.MinecraftServer.getTrimPatternRegistry +import net.minestom.server.MinecraftServer.getWolfVariantRegistry +import net.minestom.server.advancements.AdvancementManager +import net.minestom.server.adventure.bossbar.BossBarManager +import net.minestom.server.codec.StructCodec +import net.minestom.server.command.CommandManager +import net.minestom.server.entity.damage.DamageType +import net.minestom.server.entity.metadata.animal.tameable.WolfVariant +import net.minestom.server.entity.metadata.other.PaintingVariant +import net.minestom.server.event.GlobalEventHandler +import net.minestom.server.exception.ExceptionManager +import net.minestom.server.instance.InstanceManager +import net.minestom.server.instance.block.BlockManager +import net.minestom.server.instance.block.banner.BannerPattern +import net.minestom.server.instance.block.jukebox.JukeboxSong +import net.minestom.server.item.armor.TrimMaterial +import net.minestom.server.item.armor.TrimPattern +import net.minestom.server.item.enchant.Enchantment +import net.minestom.server.item.enchant.EntityEffect +import net.minestom.server.item.enchant.LevelBasedValue +import net.minestom.server.item.enchant.LocationEffect +import net.minestom.server.item.enchant.ValueEffect +import net.minestom.server.item.instrument.Instrument +import net.minestom.server.listener.manager.PacketListenerManager +import net.minestom.server.message.ChatType +import net.minestom.server.network.ConnectionManager +import net.minestom.server.network.packet.PacketParser +import net.minestom.server.network.packet.client.ClientPacket +import net.minestom.server.recipe.RecipeManager +import net.minestom.server.registry.DynamicRegistry +import net.minestom.server.scoreboard.TeamManager +import net.minestom.server.timer.SchedulerManager +import net.minestom.server.world.DimensionType +import net.minestom.server.world.biome.Biome + +val server get() = MinecraftServer.process() ?: error("Server is not initialized yet. Please make sure to call this after the server has started.") + +inline val GlobalEventHandler: GlobalEventHandler get() = getGlobalEventHandler() +inline val PacketListenerManager: PacketListenerManager get() = getPacketListenerManager() +inline val InstanceManager: InstanceManager get() = getInstanceManager() +inline val BlockManager: BlockManager get() = getBlockManager() +inline val CommandManager: CommandManager get() = getCommandManager() +inline val RecipeManager: RecipeManager get() = getRecipeManager() +inline val TeamManager: TeamManager get() = getTeamManager() +inline val SchedulerManager: SchedulerManager get() = getSchedulerManager() +inline val ExceptionManager: ExceptionManager get() = getExceptionManager() +inline val ConnectionManager: ConnectionManager get() = getConnectionManager() +inline val BossBarManager: BossBarManager get() = getBossBarManager() +inline val PacketParser: PacketParser get() = getPacketParser() +inline val AdvancementManager: AdvancementManager get() = getAdvancementManager() +inline val ChatTypeRegistry: DynamicRegistry get() = getChatTypeRegistry() +inline val DimensionTypeRegistry: DynamicRegistry get() = getDimensionTypeRegistry() +inline val BiomeRegistry: DynamicRegistry get() = getBiomeRegistry() +inline val DamageTypeRegistry: DynamicRegistry get() = getDamageTypeRegistry() +inline val TrimMaterialRegistry: DynamicRegistry get() = getTrimMaterialRegistry() +inline val TrimPatternRegistry: DynamicRegistry get() = getTrimPatternRegistry() +inline val BannerPatternRegistry: DynamicRegistry get() = getBannerPatternRegistry() +inline val WolfVariantRegistry: DynamicRegistry get() = getWolfVariantRegistry() +inline val EnchantmentRegistry: DynamicRegistry get() = getEnchantmentRegistry() +inline val PaintingVariantRegistry: DynamicRegistry get() = getPaintingVariantRegistry() +inline val JukeboxSongRegistry: DynamicRegistry get() = getJukeboxSongRegistry() +inline val InstrumentRegistry: DynamicRegistry get() = getInstrumentRegistry() +inline val EnchantmentLevelBasedValues: DynamicRegistry> get() = enchantmentLevelBasedValues() +inline val EnchantmentValueEffects: DynamicRegistry> get() = enchantmentValueEffects() +inline val EnchantmentEntityEffects: DynamicRegistry> get() = enchantmentEntityEffects() +inline val EnchantmentLocationEffects: DynamicRegistry> get() = enchantmentLocationEffects() \ No newline at end of file diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/highlight/BlockHighlights.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/highlight/BlockHighlights.kt new file mode 100644 index 000000000..ce4e3b8f2 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/highlight/BlockHighlights.kt @@ -0,0 +1,183 @@ +package dev.slne.surf.api.minestom.highlight + +import net.kyori.adventure.text.format.TextColor +import net.minestom.server.coordinate.Point +import net.minestom.server.coordinate.Pos +import net.minestom.server.coordinate.Vec +import net.minestom.server.entity.Entity +import net.minestom.server.entity.EntityType +import net.minestom.server.entity.Player +import net.minestom.server.entity.metadata.display.BlockDisplayMeta +import net.minestom.server.instance.Instance +import net.minestom.server.instance.block.Block +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +/** + * Outlines single blocks for single players. + * + * A highlight lives until it is taken back, so whoever shows one is responsible for hiding it + * again - [hideAll] takes back everything a single player was shown, which is what a player leaving + * usually calls for. + */ +object BlockHighlights { + + private const val OVERSIZE = 0.02 + + private val highlights = ConcurrentHashMap() + + /** + * Outlines the block at [position] in [color] for [viewer] alone. + * + * Showing a highlight where the same player already has one replaces it, so a highlight can be + * refreshed after the block below it changed. + * + * Nothing is shown when the chunk of [position] is neither loaded nor loadable. + * + * @param viewer the only player who sees the highlight + * @param instance the instance the block sits in + * @param position any point inside the block to outline + * @param color the colour the outline glows in, or `null` for the client's default + * @param block the block the highlight is drawn as; by default the block that stands at + * [position], which needs its chunk to be loaded to be readable + */ + fun show( + viewer: Player, + instance: Instance, + position: Point, + color: TextColor? = null, + block: Block = instance.blockAtOrAir(position) + ) { + val key = HighlightKey.of(viewer, instance, position) + + val entity = Entity(EntityType.BLOCK_DISPLAY).apply { + isAutoViewable = false + setNoGravity(true) + isGlowing = true + + editEntityMeta(BlockDisplayMeta::class.java) { meta -> + meta.setBlockState(block) + meta.scale = Vec(1 + OVERSIZE, 1 + OVERSIZE, 1 + OVERSIZE) + meta.translation = Vec(-OVERSIZE / 2, -OVERSIZE / 2, -OVERSIZE / 2) + color?.let { meta.glowColorOverride = it.value() } + } + } + + val highlight = Highlight(entity) + highlights.put(key, highlight)?.retire() + + try { + val placement = entity.setInstance(instance, position.blockPos()) + + if (placement == null) { + highlights.remove(key, highlight) + highlight.fail() + } else { + placement.whenComplete { _, _ -> highlight.placed(key, viewer) } + } + } catch (throwable: Throwable) { + highlights.remove(key, highlight) + highlight.fail() + throw throwable + } + } + + /** + * Takes back the highlight [viewer] was shown at [position], if there is one. + * + * @return whether a highlight was taken back + */ + fun hide(viewer: Player, instance: Instance, position: Point): Boolean = + highlights.remove(HighlightKey.of(viewer, instance, position)) + ?.also { it.retire() } != null + + /** + * Takes back every highlight [viewer] was shown. + * + * @return how many highlights were taken back + */ + fun hideAll(viewer: Player): Int { + var removed = 0 + val entries = highlights.entries.iterator() + + while (entries.hasNext()) { + val (key, highlight) = entries.next() + if (key.viewer != viewer.uuid) continue + + entries.remove() + highlight.retire() + removed++ + } + + return removed + } + + /** + * Returns how many highlights [viewer] is currently being shown. + */ + fun count(viewer: Player): Int = highlights.keys.count { it.viewer == viewer.uuid } + + private fun Instance.blockAtOrAir(position: Point): Block = + if (isChunkLoaded(position)) getBlock(position) else Block.AIR + + private fun Point.blockPos() = + Pos(blockX().toDouble(), blockY().toDouble(), blockZ().toDouble()) + + /** + * A highlight entity together with the state of its asynchronous placement. + * + * The placement callback and [retire] exclude each other, so an entity retired while it is still + * being placed is removed once the placement finished instead of being left in the instance. + */ + private class Highlight(private val entity: Entity) { + private var finished = false + private var retired = false + + @Synchronized + fun placed(key: HighlightKey, viewer: Player) { + finished = true + + if (!retired && (entity.isRemoved || entity.chunk == null)) { + highlights.remove(key, this) + retired = true + } + + if (retired) { + entity.remove() + } else { + entity.addViewer(viewer) + } + } + + @Synchronized + fun retire() { + retired = true + if (finished) entity.remove() + } + + @Synchronized + fun fail() { + retired = true + finished = true + entity.remove() + } + } + + private data class HighlightKey( + val viewer: UUID, + val instance: UUID, + val x: Int, + val y: Int, + val z: Int + ) { + companion object { + fun of(viewer: Player, instance: Instance, position: Point) = HighlightKey( + viewer.uuid, + instance.uuid, + position.blockX(), + position.blockY(), + position.blockZ() + ) + } + } +} diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/ViewFrameAccessor.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/ViewFrameAccessor.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/ViewFrameAccessor.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/ViewFrameAccessor.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/ClickContextDSL.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/ClickContextDSL.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/ClickContextDSL.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/ClickContextDSL.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/CloseContextDSL.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/CloseContextDSL.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/CloseContextDSL.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/CloseContextDSL.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/IFConfinedContextDSL.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/IFConfinedContextDSL.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/IFConfinedContextDSL.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/IFConfinedContextDSL.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/IFContextDSL.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/IFContextDSL.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/IFContextDSL.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/IFContextDSL.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/MinestomItemComponentBuilderDSL.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/MinestomItemComponentBuilderDSL.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/MinestomItemComponentBuilderDSL.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/MinestomItemComponentBuilderDSL.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/OpenContextDSL.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/OpenContextDSL.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/OpenContextDSL.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/OpenContextDSL.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/RenderContextDSL.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/RenderContextDSL.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/RenderContextDSL.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/RenderContextDSL.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/ViewConfigBuilderDSL.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/ViewConfigBuilderDSL.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/ViewConfigBuilderDSL.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/dsl/ViewConfigBuilderDSL.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/extensions.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/extensions.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/extensions.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/extensions.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/AbstractSurfView.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/InventoryFrameworkDsl.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/InventoryFrameworkDsl.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/InventoryFrameworkDsl.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/InventoryFrameworkDsl.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewDSLImpl.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewDSLImpl.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewDSLImpl.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/PaginatedSurfViewDSLImpl.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewBuilder.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewBuilder.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewBuilder.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewBuilder.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewContext.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewContext.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewContext.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewContext.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewDSLImpl.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewDSLImpl.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewDSLImpl.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewDSLImpl.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewLifecycleDsl.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewLifecycleDsl.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewLifecycleDsl.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewLifecycleDsl.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewRef.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewRef.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewRef.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/SurfViewRef.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/ViewNavigationHistory.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/ViewNavigationHistory.kt similarity index 71% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/ViewNavigationHistory.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/ViewNavigationHistory.kt index 1d615852e..fa42ea0bf 100644 --- a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/ViewNavigationHistory.kt +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/ViewNavigationHistory.kt @@ -1,11 +1,18 @@ package dev.slne.surf.api.minestom.inventory.framework.view +import dev.slne.surf.api.shared.api.util.InternalSurfApi import me.devnatan.inventoryframework.View import net.minestom.server.entity.Player import java.util.UUID import java.util.concurrent.ConcurrentHashMap import kotlin.collections.ArrayDeque +/** + * Per-player stack of the views opened through back-navigable navigation. + * + * [markBackNavigation] flags a pending back navigation so that opening or closing the target view + * neither pushes nor clears the stack; [consumeBackNavigation] resets the flag. + */ internal object ViewNavigationHistory { private val paths = ConcurrentHashMap>() private val backNav = ConcurrentHashMap.newKeySet() @@ -39,3 +46,9 @@ internal object ViewNavigationHistory { internal data class NavEntry(val viewClass: Class, val data: Any?) } + +/** + * Forgets the views [player] navigated through; called by the server once they disconnect. + */ +@InternalSurfApi +fun clearViewNavigationHistory(player: Player) = ViewNavigationHistory.clear(player) diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButton.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButton.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButton.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButton.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonDsl.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonDsl.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonDsl.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonDsl.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/button/ViewButtonHandle.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/ViewContainer.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/ViewContainer.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/ViewContainer.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/ViewContainer.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/ViewContainerComponent.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewBlockCellComponent.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewBlockCellComponent.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewBlockCellComponent.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewBlockCellComponent.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerBackHintComponent.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerBackHintComponent.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerBackHintComponent.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerBackHintComponent.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerGlyphComponent.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerGlyphComponent.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerGlyphComponent.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerGlyphComponent.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerRowTextComponent.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerRowTextComponent.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerRowTextComponent.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerRowTextComponent.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerTitleComponent.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerTitleComponent.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerTitleComponent.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/component/components/ViewContainerTitleComponent.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerDSL.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerDSL.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerDSL.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerDSL.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/container/dsl/ViewContainerModificationContext.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIcon.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIcon.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIcon.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIcon.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconColor.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/icon/ViewIconType.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/AbstractPaginatedSurfView.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/AbstractPaginatedSurfView.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/AbstractPaginatedSurfView.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/AbstractPaginatedSurfView.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationButton.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationButton.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationButton.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationButton.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationButtonGlyphComponent.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationButtonGlyphComponent.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationButtonGlyphComponent.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationButtonGlyphComponent.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationDsl.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationDsl.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationDsl.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationDsl.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationDslBuilder.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationDslBuilder.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationDslBuilder.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationDslBuilder.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicator.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicatorComponent.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicatorComponent.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicatorComponent.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationPageIndicatorComponent.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/pagination/PaginationSourceType.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginatedViewSettings.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationButtonPosition.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationEmptyRows.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/PaginationViewRows.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/SimpleViewSettings.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettings.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettingsDefaults.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettingsDefaults.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettingsDefaults.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/SurfViewSettingsDefaults.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewFontMetrics.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewHeaderGeometry.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/ViewRows.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignment.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/align/TextAlignmentOptions.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/PaginatedViewSettingsBuilder.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/PaginatedViewSettingsBuilder.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/PaginatedViewSettingsBuilder.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/PaginatedViewSettingsBuilder.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SimpleViewSettingsBuilder.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SimpleViewSettingsBuilder.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SimpleViewSettingsBuilder.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SimpleViewSettingsBuilder.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/settings/builder/SurfViewSettingsBuilder.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/state/DeferredState.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/state/StateDsl.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/state/StateDsl.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/state/StateDsl.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/state/StateDsl.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/state/StateHandle.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/state/StateRegistry.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/state/StateRegistry.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/state/StateRegistry.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/state/StateRegistry.kt diff --git a/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/util/GlyphShift.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/util/GlyphShift.kt similarity index 100% rename from surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/util/GlyphShift.kt rename to surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/inventory/framework/view/util/GlyphShift.kt diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/npc/MannequinNpc.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/npc/MannequinNpc.kt new file mode 100644 index 000000000..499aa5176 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/npc/MannequinNpc.kt @@ -0,0 +1,144 @@ +package dev.slne.surf.api.minestom.npc + +import codes.bed.minestom.npc.StomNPCs +import codes.bed.minestom.npc.api.NameDisplayMode +import codes.bed.minestom.npc.api.NpcKind +import codes.bed.minestom.npc.display.TextDisplayController +import codes.bed.minestom.npc.types.AbstractNpcEntity +import dev.slne.surf.api.minestom.extension.editEntityMeta +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import net.kyori.adventure.text.Component +import net.minestom.server.coordinate.Vec +import net.minestom.server.entity.EntityType +import net.minestom.server.entity.EquipmentSlot +import net.minestom.server.entity.Player +import net.minestom.server.entity.attribute.Attribute +import net.minestom.server.entity.metadata.avatar.MannequinMeta +import net.minestom.server.entity.metadata.display.TextDisplayMeta +import net.minestom.server.inventory.EquipmentHandler +import net.minestom.server.item.ItemStack +import net.minestom.server.network.packet.server.play.EntityAttributesPacket +import net.minestom.server.network.player.ResolvableProfile +import java.util.* + +/** + * A mannequin-based NPC. + * + * Use the [mannequinNpc] builder to create and spawn one. + * + * @param name the internal name of the NPC + * @param hologramText the hologram text shown above the NPC + * @param profile the profile the mannequin renders, or `null` for the default profile + * @param scale the size of the mannequin, where `1.0` is player-sized + * @param hologramOffset where the hologram sits relative to the NPC's feet + * @param description the text the client renders below the mannequin; empty hides the + * client's default mannequin label + */ +class MannequinNpc( + private val name: String, + hologramText: Component, + profile: ResolvableProfile?, + private val scale: Double, + hologramOffset: Vec, + description: Component, + uuid: UUID = UUID.randomUUID(), + + // Equipments + private var mainHandItem: ItemStack = ItemStack.AIR, + private var offHandItem: ItemStack = ItemStack.AIR, + + private var helmet: ItemStack = ItemStack.AIR, + private var chestplate: ItemStack = ItemStack.AIR, + private var leggings: ItemStack = ItemStack.AIR, + private var boots: ItemStack = ItemStack.AIR, + private var bodyEquipment: ItemStack = ItemStack.AIR, + private var saddleEquipment: ItemStack = ItemStack.AIR, +) : AbstractNpcEntity(EntityType.MANNEQUIN, uuid), EquipmentHandler { + + init { + editEntityMeta { meta -> + profile?.let { meta.profile = it } + meta.isImmovable = true + meta.description = description + meta.displayedSkinParts = ALL_SKIN_PARTS + } + setNoGravity(true) + nameDisplayMode = NameDisplayMode.GLOBAL_HOLOGRAM + textDisplayController = TextDisplayController(hologramText, hologramOffset) + } + + override val kind: NpcKind get() = NpcKind.MANNEQUIN + + override val displayName: String get() = name + + @InternalSurfApi + override fun spawn() { + StomNPCs.manager().register(this) + + val instance = instance ?: return + scheduler().scheduleNextTick { + textDisplayController?.attachTo(this, instance) + textDisplayController?.getEntity()?.editEntityMeta { meta -> + meta.isShadow = true + meta.backgroundColor = 0x000000 + } + } + } + + /** + * Replaces the hologram text shown above the NPC. + */ + fun updateDisplayName(hologramText: Component) { + textDisplayController?.updateText(hologramText) + } + + @Suppress("UnstableApiUsage") + @InternalSurfApi + override fun updateNewViewer(player: Player) { + super.updateNewViewer(player) + + getEquipmentsPacket()?.let(player::sendPacket) + + if (scale != 1.0) { + player.sendPacket( + EntityAttributesPacket( + entityId, + listOf(EntityAttributesPacket.Property(Attribute.SCALE, scale, emptyList())) + ) + ) + } + } + + override fun getEquipment(slot: EquipmentSlot) = when (slot) { + EquipmentSlot.MAIN_HAND -> mainHandItem + EquipmentSlot.OFF_HAND -> offHandItem + EquipmentSlot.BOOTS -> boots + EquipmentSlot.LEGGINGS -> leggings + EquipmentSlot.CHESTPLATE -> chestplate + EquipmentSlot.HELMET -> helmet + EquipmentSlot.BODY -> bodyEquipment + EquipmentSlot.SADDLE -> saddleEquipment + } + + override fun setEquipment( + slot: EquipmentSlot, + newItem: ItemStack + ) { + when (slot) { + EquipmentSlot.MAIN_HAND -> mainHandItem = newItem + EquipmentSlot.OFF_HAND -> offHandItem = newItem + EquipmentSlot.BOOTS -> boots = newItem + EquipmentSlot.LEGGINGS -> leggings = newItem + EquipmentSlot.CHESTPLATE -> chestplate = newItem + EquipmentSlot.HELMET -> helmet = newItem + EquipmentSlot.BODY -> bodyEquipment = newItem + EquipmentSlot.SADDLE -> saddleEquipment = newItem + } + + syncEquipment(slot) + } + + companion object { + private const val ALL_SKIN_PARTS: Byte = 0x7F + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/npc/MannequinNpcBuilder.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/npc/MannequinNpcBuilder.kt new file mode 100644 index 000000000..2b980b650 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/npc/MannequinNpcBuilder.kt @@ -0,0 +1,117 @@ +package dev.slne.surf.api.minestom.npc + +import codes.bed.minestom.npc.api.NpcInteraction +import net.kyori.adventure.text.Component +import net.minestom.server.coordinate.Pos +import net.minestom.server.coordinate.Vec +import net.minestom.server.entity.EquipmentSlot +import net.minestom.server.instance.Instance +import net.minestom.server.item.ItemStack +import net.minestom.server.network.player.ResolvableProfile +import java.util.* + +/** + * Builds and spawns a [MannequinNpc]. + * + * ``` + * val npc = mannequinNpc("shop", instance, Pos(0.5, 100.0, 0.5)) { + * displayName = Component.text("Shop") + * skin = ResolvableProfile(PlayerSkin(textures, signature)) + * scale = 1.5 + * onInteract { interaction -> interaction.player.sendMessage("Hello!") } + * } + * ``` + */ +class MannequinNpcBuilder(private val name: String) { + + /** + * The hologram text shown above the NPC. + */ + var displayName: Component = Component.empty() + + /** + * The profile the mannequin renders, or `null` for the default profile. + */ + var profile: ResolvableProfile? = null + + /** + * The size of the mannequin, where `1.0` is player-sized. + */ + var scale: Double = 1.0 + + /** + * Where the hologram sits relative to the NPC's feet. Defaults to just above the + * mannequin's head, following [scale]. + */ + var displayNameOffset: Vec? = null + + /** + * The text the client renders below the mannequin. Empty hides the client's default + * mannequin label. + */ + var description: Component = Component.empty() + + private val interactHandlers = mutableListOf<(NpcInteraction) -> Unit>() + + private val equipments = EnumMap(EquipmentSlot::class.java) + + /** + * Runs [handler] whenever a player clicks the NPC. + */ + fun onInteract(handler: (NpcInteraction) -> Unit) { + interactHandlers += handler + } + + /** + * Equips [item] in [slot] of the NPC. + */ + fun setEquipment(slot: EquipmentSlot, item: ItemStack) { + equipments[slot] = item + } + + @PublishedApi + internal fun spawn(instance: Instance, position: Pos): MannequinNpc { + val npc = MannequinNpc( + name = name, + hologramText = displayName, + profile = profile, + scale = scale, + hologramOffset = displayNameOffset ?: Vec(0.0, HOLOGRAM_HEIGHT * scale, 0.0), + description = description, + + // Equipments + mainHandItem = equipments[EquipmentSlot.MAIN_HAND] ?: ItemStack.AIR, + offHandItem = equipments[EquipmentSlot.OFF_HAND] ?: ItemStack.AIR, + helmet = equipments[EquipmentSlot.HELMET] ?: ItemStack.AIR, + chestplate = equipments[EquipmentSlot.CHESTPLATE] ?: ItemStack.AIR, + leggings = equipments[EquipmentSlot.LEGGINGS] ?: ItemStack.AIR, + boots = equipments[EquipmentSlot.BOOTS] ?: ItemStack.AIR, + bodyEquipment = equipments[EquipmentSlot.BODY] ?: ItemStack.AIR, + saddleEquipment = equipments[EquipmentSlot.SADDLE] ?: ItemStack.AIR, + ) + + interactHandlers.forEach { handler -> + npc.onInteract { interaction -> handler(interaction) } + } + + npc.spawn() + npc.setInstance(instance, position) + + return npc + } + + private companion object { + const val HOLOGRAM_HEIGHT = 2.1 + } +} + +/** + * Creates a [MannequinNpc] named [name], configures it with [block] and spawns it at + * [position] in [instance]. + */ +inline fun mannequinNpc( + name: String, + instance: Instance, + position: Pos, + block: MannequinNpcBuilder.() -> Unit, +): MannequinNpc = MannequinNpcBuilder(name).apply(block).spawn(instance, position) diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/permission/MinestomPermissions.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/permission/MinestomPermissions.kt new file mode 100644 index 000000000..1e67f59d5 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/permission/MinestomPermissions.kt @@ -0,0 +1,76 @@ +package dev.slne.surf.api.minestom.permission + +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import net.kyori.adventure.util.TriState +import net.minestom.server.command.CommandSender +import net.minestom.server.command.ConsoleSender +import net.minestom.server.command.ServerSender +import net.minestom.server.entity.Player + +/** + * Resolves whether a player holds a permission. + * + * Minestom has no permission system of its own, so the server installs one, e.g. through + * `withLuckPerms()`. + */ +fun interface PermissionProvider { + + /** + * Returns the value of [permission] for [player], or [TriState.NOT_SET] when it is undefined. + */ + fun permissionValue(player: Player, permission: String): TriState +} + +/** + * The permission provider every surf feature asks, e.g. the CommandAPI for its requirements. + * + * Without an installed provider, players hold no permissions at all. + */ +object MinestomPermissions { + + private val NONE = PermissionProvider { _, _ -> TriState.NOT_SET } + + @Volatile + var provider: PermissionProvider = NONE + private set + + /** + * Installs [provider], or removes the installed one when `null`. + */ + @InternalSurfApi + fun installProvider(provider: PermissionProvider?) { + this.provider = provider ?: NONE + } + + /** + * Resolves the value of [permission] for [sender]. + * + * The console and the server sender always resolve to [TriState.TRUE]; players resolve through + * the installed [provider]; any other sender resolves to [TriState.NOT_SET]. + */ + fun permissionValue(sender: CommandSender, permission: String): TriState = when (sender) { + is ConsoleSender, is ServerSender -> TriState.TRUE + is Player -> provider.permissionValue(sender, permission) + else -> TriState.NOT_SET + } + + /** + * Whether [permission] resolves to [TriState.TRUE] for [sender]. + */ + fun hasPermission(sender: CommandSender, permission: String): Boolean = + permissionValue(sender, permission) == TriState.TRUE +} + +/** + * Whether this sender holds [permission]. + * + * The console and the server sender hold every permission. + */ +fun CommandSender.hasPermission(permission: String): Boolean = + MinestomPermissions.hasPermission(this, permission) + +/** + * The value of [permission] for this sender. + */ +fun CommandSender.permissionValue(permission: String): TriState = + MinestomPermissions.permissionValue(this, permission) diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/PlayerLimit.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/PlayerLimit.kt new file mode 100644 index 000000000..bb776c669 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/PlayerLimit.kt @@ -0,0 +1,40 @@ +package dev.slne.surf.api.minestom.player + +import dev.slne.surf.api.minestom.extension.ConnectionManager +import dev.slne.surf.api.minestom.player.event.AsyncPlayerCountEvent + +/** + * How many players this server admits. + * + * A player who would exceed [maxPlayers] is refused while joining, which a listener of + * [AsyncPlayerCountEvent] can override. The limit is also reported on the server list. + */ +object PlayerLimit { + + @Volatile + private var limit: Int? = null + + /** + * The number of players the server admits, or `null` when it admits any number. + * + * @throws IllegalArgumentException if the new value is not greater than zero + */ + var maxPlayers: Int? + get() = limit + set(value) { + require(value == null || value > 0) { "maxPlayers must be greater than zero, was $value" } + limit = value + } + + /** + * The number of connected players, counting those still in the configuration phase. + */ + val playerCount: Int + get() = ConnectionManager.onlinePlayerCount + ConnectionManager.configPlayers.size + + /** + * Whether [playerCount] has reached [maxPlayers]. + */ + val isFull: Boolean + get() = limit?.let { playerCount >= it } ?: false +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/event/AsyncPlayerCountEvent.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/event/AsyncPlayerCountEvent.kt new file mode 100644 index 000000000..f947b47d8 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/event/AsyncPlayerCountEvent.kt @@ -0,0 +1,88 @@ +package dev.slne.surf.api.minestom.player.event + +import dev.slne.surf.api.minestom.player.PlayerLimit +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import net.kyori.adventure.text.Component +import net.minestom.server.entity.Player +import net.minestom.server.event.trait.AsyncEvent +import net.minestom.server.event.trait.PlayerEvent + +/** + * Called once per player, before the server configures them, to decide whether they may join. + * + * [result] arrives as [Result.KICK_FULL] when the player would exceed [PlayerLimit.maxPlayers] and + * as [Result.ALLOWED] otherwise. A listener may [allow] a refused player in anyway, e.g. one with + * a bypass permission, or [disallow] one the server would have taken. A refused player is kicked + * with [kickMessage] before the configuration phase continues. + * + * ``` + * node.addListener { event -> + * if (event.result == AsyncPlayerCountEvent.Result.KICK_FULL && + * event.player.hasPermission("server.bypass-max-players") + * ) { + * event.allow() + * } + * } + * ``` + * + * Listeners run on the player's own virtual configuration thread, so they may block. + */ +class AsyncPlayerCountEvent @InternalSurfApi constructor( + private val player: Player, + + /** + * The number of connected players, counting this one and those still being configured. + */ + val playerCount: Int, + + /** + * The number of players the server admits, or `null` when it admits any number. + */ + val maxPlayers: Int?, + + /** + * Whether the player may join, and why not if they may not. + */ + var result: Result, + + /** + * The message a refused player is kicked with. + */ + var kickMessage: Component, +) : PlayerEvent, AsyncEvent { + + val isAllowed: Boolean get() = result == Result.ALLOWED + + /** + * Lets the player join, whatever an earlier listener or the server itself decided. + */ + fun allow() { + result = Result.ALLOWED + } + + /** + * Refuses the player with [result] and kicks them with [kickMessage]. + */ + fun disallow(result: Result, kickMessage: Component) { + require(result != Result.ALLOWED) { "disallow needs a refusing result, was $result" } + + this.result = result + this.kickMessage = kickMessage + } + + override fun getPlayer() = player + + enum class Result { + ALLOWED, + + /** + * The server has no free slot left. + */ + KICK_FULL, + + /** + * A listener refused the player for its own reason. + */ + KICK_OTHER, + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/event/AsyncPlayerSpawnLocationEvent.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/event/AsyncPlayerSpawnLocationEvent.kt new file mode 100644 index 000000000..0b01a843d --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/event/AsyncPlayerSpawnLocationEvent.kt @@ -0,0 +1,35 @@ +package dev.slne.surf.api.minestom.player.event + +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.Player +import net.minestom.server.event.trait.AsyncEvent +import net.minestom.server.event.trait.PlayerEvent +import net.minestom.server.instance.Instance + +/** + * Called once per player, after [net.minestom.server.event.player.AsyncPlayerConfigurationEvent], + * to decide where the player spawns when they join. + * + * Starts out with the spawning instance and the respawn point the configuration event chose. + * [position] also becomes the player's respawn point. + * + * Listeners run on the player's own virtual configuration thread, so they may block, e.g. to load + * the position the player logged out at. + */ +class AsyncPlayerSpawnLocationEvent @InternalSurfApi constructor( + private val player: Player, + + /** + * The instance the player spawns in. + */ + var instance: Instance, + + /** + * The position the player spawns at. + */ + var position: Pos, +) : PlayerEvent, AsyncEvent { + + override fun getPlayer() = player +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/event/PlayerJoinEvent.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/event/PlayerJoinEvent.kt new file mode 100644 index 000000000..3eabc9559 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/event/PlayerJoinEvent.kt @@ -0,0 +1,24 @@ +package dev.slne.surf.api.minestom.player.event + +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import net.kyori.adventure.text.Component +import net.minestom.server.entity.Player +import net.minestom.server.event.trait.PlayerInstanceEvent + +/** + * Called once a player joined the server and spawned for the first time. + * + * [joinMessage] is broadcast to every player and the console afterwards; set it to `null` to + * join silently. + */ +class PlayerJoinEvent @InternalSurfApi constructor( + private val player: Player, + + /** + * The message announcing the player, or `null` for none. + */ + var joinMessage: Component?, +) : PlayerInstanceEvent { + + override fun getPlayer() = player +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/event/PlayerQuitEvent.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/event/PlayerQuitEvent.kt new file mode 100644 index 000000000..f12888f74 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/event/PlayerQuitEvent.kt @@ -0,0 +1,24 @@ +package dev.slne.surf.api.minestom.player.event + +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import net.kyori.adventure.text.Component +import net.minestom.server.entity.Player +import net.minestom.server.event.trait.PlayerEvent + +/** + * Called when a player who joined the server leaves it. + * + * [quitMessage] is broadcast to the remaining players and the console afterwards; set it to + * `null` to leave silently. + */ +class PlayerQuitEvent @InternalSurfApi constructor( + private val player: Player, + + /** + * The message announcing the player left, or `null` for none. + */ + var quitMessage: Component?, +) : PlayerEvent { + + override fun getPlayer() = player +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/event/PlayerToggleFlightEvent.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/event/PlayerToggleFlightEvent.kt new file mode 100644 index 000000000..1bad72304 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/player/event/PlayerToggleFlightEvent.kt @@ -0,0 +1,29 @@ +package dev.slne.surf.api.minestom.player.event + +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import net.minestom.server.entity.Player +import net.minestom.server.event.trait.CancellableEvent +import net.minestom.server.event.trait.PlayerInstanceEvent + +/** + * Called when a player who is allowed to fly tries to start or stop flying. + * + * Cancelling the event keeps the player in the state they were in. + */ +class PlayerToggleFlightEvent @InternalSurfApi constructor( + private val player: Player, + + /** + * The requested state: `true` to start flying, `false` to stop. + */ + val isFlying: Boolean, +) : PlayerInstanceEvent, CancellableEvent { + private var cancelled = false + + override fun isCancelled(): Boolean = cancelled + override fun setCancelled(cancel: Boolean) { + cancelled = cancel + } + + override fun getPlayer() = player +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/plugin/PluginManager.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/plugin/PluginManager.kt new file mode 100644 index 000000000..446ed7e2b --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/plugin/PluginManager.kt @@ -0,0 +1,42 @@ +package dev.slne.surf.api.minestom.plugin + +import dev.slne.surf.api.shared.api.util.InternalSurfApi + +/** + * The plugins the server loaded. + */ +interface PluginManager { + + /** + * Every loaded plugin, in the order they were loaded. + */ + val plugins: Collection + + /** + * The loaded plugin [id], or `null` when there is none. + */ + fun plugin(id: String): SurfMinestomPlugin? + + /** + * Whether a plugin [id] is loaded. + */ + fun isLoaded(id: String): Boolean = plugin(id) != null + + companion object { + @Volatile + private var installed: PluginManager? = null + + /** + * The server's plugin manager. + * + * @throws IllegalStateException when the server does not load plugins + */ + val instance: PluginManager + get() = checkNotNull(installed) { "This server does not load plugins" } + + @InternalSurfApi + fun install(manager: PluginManager?) { + installed = manager + } + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/plugin/PluginMeta.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/plugin/PluginMeta.kt new file mode 100644 index 000000000..83ec6c287 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/plugin/PluginMeta.kt @@ -0,0 +1,83 @@ +package dev.slne.surf.api.minestom.plugin + +import kotlinx.serialization.Serializable + +/** + * Describes a plugin, as read from its `minestom-plugin.json`. + * + * The file is generated by the `dev.slne.surf.api.gradle.minestom` Gradle plugin: + * + * ``` + * minestomPluginFile { + * main = "dev.slne.example.ExamplePlugin" + * authors = listOf("red") + * + * pluginDependencies { + * register("surf-core-minestom") + * register("surf-chat-minestom") { optional = true } + * } + * } + * ``` + */ +@Serializable +data class PluginMeta( + /** + * Identifies the plugin; lowercase letters, digits, `-` and `_`. + */ + val id: String, + + /** + * The fully qualified name of the class extending [SurfMinestomPlugin]. + */ + val main: String, + + val name: String = id, + val version: String = "unknown", + val description: String = "", + val url: String? = null, + val authors: List = emptyList(), + + /** + * The plugins this plugin needs, which are loaded and enabled before it. + */ + val dependencies: List = emptyList(), +) { + init { + require(ID_REGEX.matches(id)) { "Invalid plugin id '$id', it has to match $ID_REGEX" } + require(main.isNotBlank()) { "The plugin '$id' declares no main class" } + } + + companion object { + /** + * The file a plugin jar carries its [PluginMeta] in. + */ + const val FILE_NAME = "minestom-plugin.json" + + /** + * Lists the main classes of the plugins on the system class path, one per line. + * + * A service file, so that it survives when several plugins are shaded into one server jar. + */ + const val CLASSPATH_INDEX = "META-INF/services/dev.slne.surf.api.minestom.plugin.SurfMinestomPlugin" + + /** + * Where a plugin on the system class path keeps its [PluginMeta], as `

.json`. + */ + const val CLASSPATH_DIRECTORY = "META-INF/surf-minestom-plugins/" + + val ID_REGEX = Regex("[a-z][a-z0-9-_]{0,63}") + } +} + +/** + * A plugin another plugin depends on. + */ +@Serializable +data class PluginDependency( + val id: String, + + /** + * Whether the depending plugin also loads without this one. + */ + val optional: Boolean = false, +) diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/plugin/SurfMinestomPlugin.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/plugin/SurfMinestomPlugin.kt new file mode 100644 index 000000000..8b23c7aae --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/plugin/SurfMinestomPlugin.kt @@ -0,0 +1,129 @@ +package dev.slne.surf.api.minestom.plugin + +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ThreadContextElement +import kotlinx.coroutines.asContextElement +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import net.minestom.server.event.Event +import net.minestom.server.event.EventNode +import java.nio.file.Path + +/** + * The main class of a plugin the server loads from its `plugins` directory. + * + * ``` + * class ExamplePlugin : SurfMinestomPlugin() { + * override suspend fun onEnable() { + * eventNode.addListener { it.player.sendMessage(text("Hello!")) } + * commandAPICommand("hello") { playerExecutor { player, _ -> player.sendMessage(text("Hi")) } } + * } + * } + * ``` + * + * The class needs a constructor without parameters, or has to be an `object`. It is named as + * `main` in the plugin's `minestom-plugin.json`. + * + * Listeners registered on [eventNode], coroutines launched in [scope] and commands registered while + * one of the lifecycle functions runs, or from within [scope], belong to the plugin. They are + * removed or, for coroutines, cancelled once the plugin is disabled. + * + * The lifecycle functions run blocking on the server thread: `onLoad` before the server accepts + * players, `onEnable` on the first tick and `onDisable` during shutdown. They must not wait for + * work dispatched to `MinestomDispatchers.Main` or to an entity scheduler, because that work only + * runs once the blocked thread returns to the tick loop. Such work is launched in [scope] instead. + */ +abstract class SurfMinestomPlugin { + // Taken in the constructor so that property initializers of the plugin, including those of an + // `object` that run in its static initializer, can already use the context. + private val context: Context? = pendingContext.get()?.invoke(this) + + private val requireContext: Context + get() = checkNotNull(context) { "${javaClass.name} has not been initialized by the server yet" } + + /** + * What the plugin's `minestom-plugin.json` says about it. + */ + val meta: PluginMeta get() = requireContext.meta + + /** + * The plugin's own event node; removed with all its listeners when the plugin is disabled. + */ + val eventNode: EventNode get() = requireContext.eventNode + + /** + * The plugin's own directory, `plugins/`, for its configuration and data. + */ + val dataDirectory: Path get() = requireContext.dataDirectory + + val logger: ComponentLogger get() = requireContext.logger + + /** + * A scope for the plugin's coroutines; cancelled when the plugin is disabled. + */ + val scope: CoroutineScope get() = requireContext.scope + + /** + * Called while the server starts, before it accepts players. + */ + open suspend fun onLoad() = Unit + + /** + * Called on the first tick after the server started. + */ + open suspend fun onEnable() = Unit + + /** + * Called when the plugin is disabled, at the latest when the server shuts down. + */ + open suspend fun onDisable() = Unit + + override fun toString(): String = context?.let { "${it.meta.name} ${it.meta.version}" } ?: javaClass.name + + /** + * What the server hands a plugin when creating it. + */ + @InternalSurfApi + class Context( + val meta: PluginMeta, + val eventNode: EventNode, + val dataDirectory: Path, + val logger: ComponentLogger, + val scope: CoroutineScope, + ) + + companion object { + private val current = ThreadLocal() + private val pendingContext = ThreadLocal<((SurfMinestomPlugin) -> Context)?>() + + /** + * Runs [block], during which every plugin constructed on this thread gets its context from + * [context] right away, before its own properties are initialized. + */ + @InternalSurfApi + fun constructing(context: (SurfMinestomPlugin) -> Context, block: () -> T): T { + val previous = pendingContext.get() + pendingContext.set(context) + try { + return block() + } finally { + pendingContext.set(previous) + } + } + + /** + * The plugin whose code is running on this thread, or `null` when it is not a plugin's. + * + * Known while a lifecycle function runs and within the plugin's [scope]. + */ + @InternalSurfApi + fun current(): SurfMinestomPlugin? = current.get() + + /** + * A coroutine context element that makes [plugin] the [current] one. + */ + @InternalSurfApi + fun contextElement(plugin: SurfMinestomPlugin): ThreadContextElement = + current.asContextElement(plugin) + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/visibility/PlayerVisibility.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/visibility/PlayerVisibility.kt new file mode 100644 index 000000000..cf7251504 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/dev/slne/surf/api/minestom/visibility/PlayerVisibility.kt @@ -0,0 +1,67 @@ +package dev.slne.surf.api.minestom.visibility + +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import net.minestom.server.entity.Entity +import net.minestom.server.entity.Player +import java.util.function.Predicate + +/** + * Restricts what players see. + * + * When the server limits how many players a player sees at once, it owns the viewer rule of every + * player, the predicate that decides which entities the player sees. Restrict what a player sees + * through [updateViewerRule] instead of `Player#updateViewerRule`, which would remove the limit for + * that player. Without such a limit, these functions simply update the player's viewer rule. + */ +object PlayerVisibility { + + @Volatile + private var provider: Provider? = null + + /** + * Restricts the entities [player] sees to those [predicate] accepts, on top of the limit of + * nearest players. `null` lifts the restriction. + */ + fun updateViewerRule(player: Player, predicate: Predicate?) { + if (provider?.updateViewerRule(player, predicate) != true) { + player.updateViewerRule(predicate) + } + } + + /** + * Re-evaluates the viewer rule of [player], like `Player#updateViewerRule()`. + */ + fun updateViewerRule(player: Player) { + if (provider?.refreshViewerRule(player) != true) { + player.updateViewerRule() + } + } + + /** + * Whether the limit currently restricts the players [player] sees. + */ + fun isLimited(player: Player): Boolean = provider?.isLimited(player) == true + + /** + * Implemented by the server when it limits the visible players. + */ + @InternalSurfApi + interface Provider { + /** + * @return whether the rule was taken over, `false` to let the player apply it itself + */ + fun updateViewerRule(player: Player, predicate: Predicate?): Boolean + + /** + * @return whether the refresh was taken over + */ + fun refreshViewerRule(player: Player): Boolean + + fun isLimited(player: Player): Boolean + } + + @InternalSurfApi + fun installProvider(provider: Provider?) { + this.provider = provider + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/main/kotlin/net/minestom/server/particle/ParticleRegistryAccess.kt b/surf-api-minestom/surf-api-minestom/src/main/kotlin/net/minestom/server/particle/ParticleRegistryAccess.kt new file mode 100644 index 000000000..3656b27b0 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/kotlin/net/minestom/server/particle/ParticleRegistryAccess.kt @@ -0,0 +1,8 @@ +package net.minestom.server.particle + +import net.minestom.server.registry.Registry + +internal object ParticleRegistryAccess { + @JvmStatic + fun registry(): Registry = ParticleImpl.REGISTRY +} \ No newline at end of file diff --git a/surf-api-minestom/surf-api-minestom/src/main/resources/META-INF/LICENSES/CommandAPI-LICENSE.txt b/surf-api-minestom/surf-api-minestom/src/main/resources/META-INF/LICENSES/CommandAPI-LICENSE.txt new file mode 100644 index 000000000..29bc4dd17 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/resources/META-INF/LICENSES/CommandAPI-LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 - 2022 Jorel Ali + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/surf-api-minestom/surf-api-minestom/src/main/resources/META-INF/commandapi-port/NOTICE.md b/surf-api-minestom/surf-api-minestom/src/main/resources/META-INF/commandapi-port/NOTICE.md new file mode 100644 index 000000000..6c078cd82 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/main/resources/META-INF/commandapi-port/NOTICE.md @@ -0,0 +1,9 @@ +# CommandAPI provenance + +This Kotlin command API port retains concepts +from [CommandAPI](https://github.com/CommandAPI/CommandAPI), version 12.0.0, revision +`740a093a7d111863067eb3f94a3041e69ade02fa`. + +The retained API concepts include parsed command arguments and command syntax and validation +failures. The Minestom backend is original Kotlin and does not include Bukkit, Paper, NMS, +Brigadier, or CommandAPI runtime code. diff --git a/surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/MinestomEnvSmokeTest.kt b/surf-api-minestom/surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/MinestomEnvSmokeTest.kt similarity index 100% rename from surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/MinestomEnvSmokeTest.kt rename to surf-api-minestom/surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/MinestomEnvSmokeTest.kt diff --git a/surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/dialog/DialogBuilderTest.kt b/surf-api-minestom/surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/dialog/DialogBuilderTest.kt similarity index 100% rename from surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/dialog/DialogBuilderTest.kt rename to surf-api-minestom/surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/dialog/DialogBuilderTest.kt diff --git a/surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/dialog/search/SearchDialogTest.kt b/surf-api-minestom/surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/dialog/search/SearchDialogTest.kt similarity index 100% rename from surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/dialog/search/SearchDialogTest.kt rename to surf-api-minestom/surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/dialog/search/SearchDialogTest.kt diff --git a/surf-api-minestom/surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/extension/ParticleRegistryTest.kt b/surf-api-minestom/surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/extension/ParticleRegistryTest.kt new file mode 100644 index 000000000..01636b5de --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/extension/ParticleRegistryTest.kt @@ -0,0 +1,30 @@ +package dev.slne.surf.api.minestom.extension + +import dev.slne.surf.api.minestom.extension.getOrThrow +import dev.slne.surf.api.minestom.extension.particleRegistry +import net.kyori.adventure.key.Key +import net.minestom.server.particle.Particle +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test + +class ParticleRegistryTest { + + @Test + fun `the exposed registry is the one backing Particle`() { + val flame = requireNotNull(Particle.fromKey("minecraft:flame")) + + assertEquals(Particle.values().size, particleRegistry.size()) + assertSame(flame, particleRegistry.getOrThrow(Key.key("flame"))) + assertSame(flame, particleRegistry.getOrThrow(flame.id())) + assertSame(flame, particleRegistry.getOrThrow(flame.registryKey())) + } + + @Test + fun `an unknown key is reported as missing`() { + assertThrows(NoSuchElementException::class.java) { + particleRegistry.getOrThrow(Key.key("surf", "not_a_particle")) + } + } +} diff --git a/surf-api-minestom/surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/highlight/BlockHighlightsTest.kt b/surf-api-minestom/surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/highlight/BlockHighlightsTest.kt new file mode 100644 index 000000000..923cd1a17 --- /dev/null +++ b/surf-api-minestom/surf-api-minestom/src/test/kotlin/dev/slne/surf/api/minestom/highlight/BlockHighlightsTest.kt @@ -0,0 +1,189 @@ +package dev.slne.surf.api.minestom.highlight + +import net.kyori.adventure.text.format.NamedTextColor +import net.minestom.server.coordinate.Point +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.EntityType +import net.minestom.server.entity.Player +import net.minestom.server.instance.Instance +import net.minestom.server.instance.block.Block +import net.minestom.testing.Env +import net.minestom.testing.EnvTest +import net.minestom.testing.TestConnection +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.concurrent.CompletableFuture + +@EnvTest +class BlockHighlightsTest { + + @Test + fun `a highlight is drawn as a glowing block display where the block sits`(env: Env) { + val instance = env.createFlatInstance() + val viewer = connect(env.createConnection(), instance) + + try { + val position = instance.prepare(1, 40, 1) + instance.setBlock(position, Block.RED_CONCRETE) + + BlockHighlights.show(viewer, instance, position, NamedTextColor.WHITE) + + val display = blockDisplays(instance).single() + + assertEquals(Pos(1.0, 40.0, 1.0), display.position) + assertTrue(display.isGlowing) + } finally { + cleanUp(env, instance, viewer) + } + } + + @Test + fun `only the player a highlight was shown to sees it`(env: Env) { + val instance = env.createFlatInstance() + val viewer = connect(env.createConnection(), instance) + val other = connect(env.createConnection(), instance) + + try { + BlockHighlights.show(viewer, instance, instance.prepare(2, 40, 2)) + + val display = blockDisplays(instance).single() + + assertTrue(display.viewers.contains(viewer)) + assertFalse(display.viewers.contains(other)) + } finally { + cleanUp(env, instance, viewer, other) + } + } + + @Test + fun `hiding a highlight takes its display away again`(env: Env) { + val instance = env.createFlatInstance() + val viewer = connect(env.createConnection(), instance) + + try { + val position = instance.prepare(3, 40, 3) + BlockHighlights.show(viewer, instance, position) + + assertTrue(BlockHighlights.hide(viewer, instance, position)) + assertEquals(0, BlockHighlights.count(viewer)) + assertTrue(blockDisplays(instance).isEmpty()) + } finally { + cleanUp(env, instance, viewer) + } + } + + @Test + fun `hiding a highlight nobody was shown reports nothing was taken back`(env: Env) { + val instance = env.createFlatInstance() + val viewer = connect(env.createConnection(), instance) + + try { + assertFalse(BlockHighlights.hide(viewer, instance, instance.prepare(4, 40, 4))) + } finally { + cleanUp(env, instance, viewer) + } + } + + @Test + fun `showing a highlight twice keeps a single display`(env: Env) { + val instance = env.createFlatInstance() + val viewer = connect(env.createConnection(), instance) + + try { + val position = instance.prepare(5, 40, 5) + + BlockHighlights.show(viewer, instance, position) + BlockHighlights.show(viewer, instance, position) + + assertEquals(1, BlockHighlights.count(viewer)) + assertEquals(1, blockDisplays(instance).size) + } finally { + cleanUp(env, instance, viewer) + } + } + + @Test + fun `any point inside a block addresses the same highlight`(env: Env) { + val instance = env.createFlatInstance() + val viewer = connect(env.createConnection(), instance) + + try { + instance.prepare(6, 40, 6) + BlockHighlights.show(viewer, instance, Pos(6.7, 40.2, 6.9)) + + assertTrue(BlockHighlights.hide(viewer, instance, Pos(6.0, 40.0, 6.0))) + assertEquals(0, BlockHighlights.count(viewer)) + } finally { + cleanUp(env, instance, viewer) + } + } + + @Test + fun `taking back everything a player was shown leaves nothing behind`(env: Env) { + val instance = env.createFlatInstance() + val viewer = connect(env.createConnection(), instance) + + try { + BlockHighlights.show(viewer, instance, instance.prepare(7, 40, 7)) + BlockHighlights.show(viewer, instance, instance.prepare(8, 40, 8)) + + assertEquals(2, blockDisplays(instance).size) + assertEquals(2, BlockHighlights.hideAll(viewer)) + assertEquals(0, BlockHighlights.count(viewer)) + assertTrue(blockDisplays(instance).isEmpty()) + } finally { + cleanUp(env, instance, viewer) + } + } + + @Test + fun `a highlight in a chunk that cannot be loaded is not kept`(env: Env) { + val instance = env.createFlatInstance() + val viewer = connect(env.createConnection(), instance) + instance.enableAutoChunkLoad(false) + + try { + val position = Pos(4000.0, 40.0, 4000.0) + assertFalse(instance.isChunkLoaded(position)) + + BlockHighlights.show(viewer, instance, position) + + assertEquals(0, BlockHighlights.count(viewer)) + assertFalse(BlockHighlights.hide(viewer, instance, position)) + assertTrue(blockDisplays(instance).isEmpty()) + } finally { + cleanUp(env, instance, viewer) + } + } + + /** + * Loads the chunk containing the given block and returns its position, so a highlight shown + * there joins the instance immediately instead of when the chunk arrives. + */ + private fun Instance.prepare(x: Int, y: Int, z: Int): Point { + val position = Pos(x.toDouble(), y.toDouble(), z.toDouble()) + loadChunk(position).join() + return position + } + + private fun blockDisplays(instance: Instance) = + instance.entities.filter { it.entityType == EntityType.BLOCK_DISPLAY } + + private fun cleanUp(env: Env, instance: Instance, vararg players: Player) { + players.forEach { + BlockHighlights.hideAll(it) + it.remove() + } + env.destroyInstance(instance) + } + + private fun connect(connection: TestConnection, instance: Instance): Player { + val player = CompletableFuture() + Thread.startVirtualThread { + runCatching { connection.connect(instance, Pos.ZERO) } + .onSuccess(player::complete) + .onFailure(player::completeExceptionally) + } + return player.join() + } +} diff --git a/surf-cli.json b/surf-cli.json index 5aa7ffc80..36d1802cd 100644 --- a/surf-cli.json +++ b/surf-cli.json @@ -76,6 +76,42 @@ "id": "surf-api-minestom", "kind": "api" }, + { + "id": "surf-api-minestom-server", + "kind": "api" + }, + { + "id": "surf-api-minestom-server-bootstrap", + "kind": "api" + }, + { + "id": "surf-api-minestom-server-plugins", + "kind": "api" + }, + { + "id": "surf-api-minestom-server-signed-chat", + "kind": "api" + }, + { + "id": "surf-api-minestom-server-luckperms", + "kind": "api" + }, + { + "id": "surf-api-minestom-server-spark", + "kind": "api" + }, + { + "id": "surf-api-minestom-server-npc", + "kind": "api" + }, + { + "id": "surf-api-minestom-server-player-visibility", + "kind": "api" + }, + { + "id": "surf-api-minestom-server-console", + "kind": "api" + }, { "id": "surf-api-paper-plugin-test", "kind": "api" diff --git a/vendor/.gitignore b/vendor/.gitignore new file mode 100644 index 000000000..675d90346 --- /dev/null +++ b/vendor/.gitignore @@ -0,0 +1,13 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +# Ignore Kotlin plugin data +.kotlin + +LuckPerms-Upstream/** + +# Generated by gitpatcher from LuckPerms-Upstream + LuckPerms-Patches +LuckPerms/ \ No newline at end of file diff --git a/vendor/LuckPerms-Patches/.gitkeep b/vendor/LuckPerms-Patches/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/LuckPerms-Patches/0001-Make-the-runtime-dependency-manager-work-on-Minestom.patch b/vendor/LuckPerms-Patches/0001-Make-the-runtime-dependency-manager-work-on-Minestom.patch new file mode 100644 index 000000000..868b9f0db --- /dev/null +++ b/vendor/LuckPerms-Patches/0001-Make-the-runtime-dependency-manager-work-on-Minestom.patch @@ -0,0 +1,424 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: twisti-dev +Date: Mon, 3 Aug 2026 23:47:24 +0200 +Subject: [PATCH] Make the runtime dependency manager work on Minestom + +LuckPerms is embedded as a plain library on Minestom, so it is loaded by the +system class loader and its own classes are not relocated at build time. Both +facts broke the dependency manager, which was stubbed out with no-ops. + + - append downloaded jars to the system class loader via the instrumentation + API, which requires the host application to install LuckPermsAgent + - let embedders supply their own ClassPathAppender via the builder + - use the real DependencyManagerImpl with the usual global dependencies, + except adventure, which Minestom itself provides + - publish reference-relocated (shaded) jars for the common and minestom + modules, so LuckPerms' own classes refer to the me.lucko.luckperms.lib + packages that the downloaded dependencies are relocated into + - stop shipping okhttp, okio, caffeine, byte-buddy, event-api and configurate + as dependencies of the minestom library - they are downloaded at runtime + instead, keeping them off the host application's class path + +diff --git a/common/build.gradle b/common/build.gradle +index 2209c5b6f04f97baea0b3eb92f9794764a26dbe2..36ddb2f2f9f4e889c32a67d7e6a76fab57affd24 100644 +--- a/common/build.gradle ++++ b/common/build.gradle +@@ -2,6 +2,38 @@ plugins { + id("java-library") + id("jacoco") + alias(libs.plugins.blossom) ++ alias(libs.plugins.shadow) ++} ++ ++// Rewrite references to the libraries the dependency manager downloads at runtime, so they match ++// the relocated jars it produces (see the Dependency enum). Only this module's own classes are ++// shaded - no dependencies are bundled. Compilation and tests keep using the plain jar. ++shadowJar { ++ archiveClassifier = 'shaded' ++ configurations = [] ++ ++ relocate 'net.kyori.event', 'me.lucko.luckperms.lib.eventbus' ++ relocate 'com.github.benmanes.caffeine', 'me.lucko.luckperms.lib.caffeine' ++ relocate 'okio', 'me.lucko.luckperms.lib.okio' ++ relocate 'okhttp3', 'me.lucko.luckperms.lib.okhttp3' ++ relocate 'net.bytebuddy', 'me.lucko.luckperms.lib.bytebuddy' ++ relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb' ++ relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql' ++ relocate 'org.postgresql', 'me.lucko.luckperms.lib.postgresql' ++ relocate 'com.zaxxer.hikari', 'me.lucko.luckperms.lib.hikari' ++ relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' ++ relocate 'org.bson', 'me.lucko.luckperms.lib.bson' ++ relocate 'redis.clients.jedis', 'me.lucko.luckperms.lib.jedis' ++ relocate 'io.nats.client', 'me.lucko.luckperms.lib.nats' ++ relocate 'com.rabbitmq', 'me.lucko.luckperms.lib.rabbitmq' ++ relocate 'org.apache.commons.pool2', 'me.lucko.luckperms.lib.commonspool2' ++ relocate 'ninja.leaping.configurate', 'me.lucko.luckperms.lib.configurate' ++ relocate 'org.yaml.snakeyaml', 'me.lucko.luckperms.lib.yaml' ++} ++ ++configurations.runtimeElements.outgoing { ++ artifacts.clear() ++ artifact(shadowJar) + } + + test { +diff --git a/minestom/build.gradle b/minestom/build.gradle +index b568ab7d8e800113f3c2bf90e9678032e6c78011..ac55c4c19bd8c5d9a3efc42ffa65b93b486e6b8c 100644 +--- a/minestom/build.gradle ++++ b/minestom/build.gradle +@@ -2,6 +2,7 @@ import org.apache.tools.ant.filters.ReplaceTokens + + plugins { + id "java-library" ++ alias(libs.plugins.shadow) + } + + // minestom requires java 21+ +@@ -19,9 +20,17 @@ dependencies { + exclude group: "org.spongepowered", module: "configurate-gson" + exclude group: "org.spongepowered", module: "configurate-hocon" + exclude group: "me.lucko.configurate", module: "configurate-toml" ++ // downloaded and relocated at runtime by the dependency manager - see shadowJar below ++ exclude group: "org.spongepowered", module: "configurate-core" ++ exclude group: "com.squareup.okhttp3", module: "okhttp" ++ exclude group: "com.squareup.okio", module: "okio" ++ exclude group: "com.github.ben-manes.caffeine", module: "caffeine" ++ exclude group: "net.bytebuddy", module: "byte-buddy" ++ exclude group: "net.kyori", module: "event-api" + } + api("org.slf4j:slf4j-api:2.0.17") + compileOnly minestom ++ compileOnly "com.github.ben-manes.caffeine:caffeine:3.2.0" + + // testing + testImplementation(project(":common")) { +@@ -63,6 +72,39 @@ java { + } + } + ++// Rewrite references to the libraries the dependency manager downloads at runtime, so they match ++// the relocated jars it produces (see the Dependency enum). Only this module's own classes are ++// shaded - no dependencies are bundled. ++shadowJar { ++ archiveClassifier = 'shaded' ++ configurations = [] ++ ++ relocate 'net.kyori.event', 'me.lucko.luckperms.lib.eventbus' ++ relocate 'com.github.benmanes.caffeine', 'me.lucko.luckperms.lib.caffeine' ++ relocate 'okio', 'me.lucko.luckperms.lib.okio' ++ relocate 'okhttp3', 'me.lucko.luckperms.lib.okhttp3' ++ relocate 'net.bytebuddy', 'me.lucko.luckperms.lib.bytebuddy' ++ relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb' ++ relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql' ++ relocate 'org.postgresql', 'me.lucko.luckperms.lib.postgresql' ++ relocate 'com.zaxxer.hikari', 'me.lucko.luckperms.lib.hikari' ++ relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' ++ relocate 'org.bson', 'me.lucko.luckperms.lib.bson' ++ relocate 'redis.clients.jedis', 'me.lucko.luckperms.lib.jedis' ++ relocate 'io.nats.client', 'me.lucko.luckperms.lib.nats' ++ relocate 'com.rabbitmq', 'me.lucko.luckperms.lib.rabbitmq' ++ relocate 'org.apache.commons.pool2', 'me.lucko.luckperms.lib.commonspool2' ++ relocate 'ninja.leaping.configurate', 'me.lucko.luckperms.lib.configurate' ++ relocate 'org.yaml.snakeyaml', 'me.lucko.luckperms.lib.yaml' ++} ++ ++// Consumers run against the shaded jar; compilation keeps using the plain one, where the ++// original (compileOnly) dependencies are still on the class path. ++configurations.runtimeElements.outgoing { ++ artifacts.clear() ++ artifact(shadowJar) ++} ++ + publishing { + repositories { + maven{ +diff --git a/minestom/src/main/java/me/lucko/luckperms/minestom/LPMinestomBootstrap.java b/minestom/src/main/java/me/lucko/luckperms/minestom/LPMinestomBootstrap.java +index 9e9d5611c33ebca4463c32f78c08fab0c10ce826..30ac8d166cc834ea41db3e5ca201b24160e4e536 100644 +--- a/minestom/src/main/java/me/lucko/luckperms/minestom/LPMinestomBootstrap.java ++++ b/minestom/src/main/java/me/lucko/luckperms/minestom/LPMinestomBootstrap.java +@@ -15,7 +15,7 @@ import me.lucko.luckperms.common.plugin.logging.PluginLogger; + import me.lucko.luckperms.common.plugin.logging.Slf4jPluginLogger; + import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; + import me.lucko.luckperms.minestom.context.ContextProvider; +-import me.lucko.luckperms.minestom.dependencies.NoopClassPathAppender; ++import me.lucko.luckperms.minestom.dependencies.InstrumentationClassPathAppender; + import net.luckperms.api.platform.Platform; + import net.minestom.server.MinecraftServer; + import net.minestom.server.entity.Player; +@@ -28,6 +28,7 @@ public final class LPMinestomBootstrap implements LuckPermsBootstrap { + private final Path dataDirectory; + private final PluginLogger logger; + private final SchedulerAdapter schedulerAdapter; ++ private final ClassPathAppender classPathAppender; + private final LPMinestomPlugin plugin; + + private final CountDownLatch loadLatch = new CountDownLatch(1); +@@ -41,11 +42,15 @@ public final class LPMinestomBootstrap implements LuckPermsBootstrap { + @NotNull Set contextProviders, + @NotNull Function configurationAdapter, + @NotNull Set permissionSuggestions, +- @Nullable CommandRegistry commandRegistry ++ @Nullable CommandRegistry commandRegistry, ++ @Nullable ClassPathAppender classPathAppender + ) { + this.logger = new Slf4jPluginLogger(logger); + this.dataDirectory = dataDirectory; + this.schedulerAdapter = new MinestomSchedulerAdapter(this); ++ this.classPathAppender = classPathAppender != null ++ ? classPathAppender ++ : InstrumentationClassPathAppender.createOrFallBack(this.logger); + this.plugin = new LPMinestomPlugin(this, contextProviders, configurationAdapter, permissionSuggestions, commandRegistry); + } + +@@ -83,7 +88,7 @@ public final class LPMinestomBootstrap implements LuckPermsBootstrap { + + @Override + public ClassPathAppender getClassPathAppender() { +- return NoopClassPathAppender.INSTANCE; ++ return this.classPathAppender; + } + + @Override +diff --git a/minestom/src/main/java/me/lucko/luckperms/minestom/LPMinestomPlugin.java b/minestom/src/main/java/me/lucko/luckperms/minestom/LPMinestomPlugin.java +index 898e8f4bc789a4f2540c34b8ec379345bff067a8..5a239f97bad17052dd7f4ba31e5794dba84d03b6 100644 +--- a/minestom/src/main/java/me/lucko/luckperms/minestom/LPMinestomPlugin.java ++++ b/minestom/src/main/java/me/lucko/luckperms/minestom/LPMinestomPlugin.java +@@ -2,6 +2,7 @@ package me.lucko.luckperms.minestom; + + import java.nio.file.Path; + import java.util.Collection; ++import java.util.EnumSet; + import java.util.Optional; + import java.util.Set; + import java.util.function.Function; +@@ -11,7 +12,7 @@ import me.lucko.luckperms.common.calculator.CalculatorFactory; + import me.lucko.luckperms.common.command.CommandManager; + import me.lucko.luckperms.common.config.ConfigKeys; + import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; +-import me.lucko.luckperms.common.dependencies.DependencyManager; ++import me.lucko.luckperms.common.dependencies.Dependency; + import me.lucko.luckperms.common.event.AbstractEventBus; + import me.lucko.luckperms.common.messaging.MessagingFactory; + import me.lucko.luckperms.common.model.Group; +@@ -31,7 +32,6 @@ import me.lucko.luckperms.minestom.calculator.MinestomCalculatorFactory; + import me.lucko.luckperms.minestom.context.ContextProvider; + import me.lucko.luckperms.minestom.context.MinestomContextManager; + import me.lucko.luckperms.minestom.context.MinestomPlayerCalculator; +-import me.lucko.luckperms.minestom.dependencies.NoopDependencyManager; + import me.lucko.luckperms.minestom.listeners.MinestomCommandListUpdater; + import me.lucko.luckperms.minestom.listeners.MinestomConnectionListener; + import me.lucko.luckperms.minestom.messaging.MinestomMessagingFactory; +@@ -213,8 +213,16 @@ public final class LPMinestomPlugin extends AbstractLuckPermsPlugin { + } + + @Override +- protected @NotNull DependencyManager createDependencyManager() { +- return NoopDependencyManager.INSTANCE; ++ protected @NotNull Set getGlobalDependencies() { ++ // adventure is provided by Minestom itself; everything else is downloaded and relocated by ++ // the dependency manager, just like on the other platforms ++ return EnumSet.of( ++ Dependency.CAFFEINE, ++ Dependency.OKIO, ++ Dependency.OKHTTP, ++ Dependency.BYTEBUDDY, ++ Dependency.EVENT ++ ); + } + + } +diff --git a/minestom/src/main/java/me/lucko/luckperms/minestom/LuckPermsMinestom.java b/minestom/src/main/java/me/lucko/luckperms/minestom/LuckPermsMinestom.java +index fd3b00b03c185096307969557d9c07f226337f75..69834720b5682f4afdf681c6734f347386a3c7fb 100644 +--- a/minestom/src/main/java/me/lucko/luckperms/minestom/LuckPermsMinestom.java ++++ b/minestom/src/main/java/me/lucko/luckperms/minestom/LuckPermsMinestom.java +@@ -8,7 +8,9 @@ import java.util.function.Consumer; + import java.util.function.Function; + import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; + import me.lucko.luckperms.common.config.generic.adapter.EnvironmentVariableConfigAdapter; ++import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; + import me.lucko.luckperms.minestom.context.ContextProvider; ++import me.lucko.luckperms.minestom.dependencies.LuckPermsAgent; + import net.luckperms.api.LuckPerms; + import net.luckperms.api.LuckPermsProvider; + import net.minestom.server.command.builder.Command; +@@ -159,6 +161,22 @@ public final class LuckPermsMinestom { + @NotNull Builder logger(@NotNull Logger logger); + + ++ /** ++ * Sets the class path appender used to load runtime dependencies, such as storage drivers ++ * and messaging clients. ++ * ++ *

If unset, LuckPerms appends to the system class loader using the instrumentation API, ++ * which requires an agent to be installed - see {@link LuckPermsAgent}. Applications that ++ * run LuckPerms inside a class loader they control should pass an appender for that class ++ * loader instead.

++ * ++ * @param appender the appender to use ++ * @return the builder instance ++ */ ++ @Contract("_ -> this") ++ @NotNull Builder classPathAppender(@Nullable ClassPathAppender appender); ++ ++ + /** + * Enables LuckPerms + * +@@ -177,6 +195,7 @@ public final class LuckPermsMinestom { + private @Nullable CommandRegistry commandRegistry; + private @NotNull Function configurationAdapter = EnvironmentVariableConfigAdapter::new; + private @NotNull Logger logger = LoggerFactory.getLogger(LuckPermsMinestom.class); ++ private @Nullable ClassPathAppender classPathAppender; + + private BuilderImpl(@NotNull Path dataDirectory) { + this.dataDirectory = dataDirectory; +@@ -234,6 +253,12 @@ public final class LuckPermsMinestom { + return this; + } + ++ @Override ++ public @NotNull Builder classPathAppender(@Nullable ClassPathAppender appender) { ++ this.classPathAppender = appender; ++ return this; ++ } ++ + @Override + public @NotNull LuckPerms enable() { + bootstrap = new LPMinestomBootstrap( +@@ -242,7 +267,8 @@ public final class LuckPermsMinestom { + this.contextProviders, + this.configurationAdapter, + this.permissionSuggestions, +- this.commandRegistry ++ this.commandRegistry, ++ this.classPathAppender + ); + bootstrap.onEnable(); + return LuckPermsProvider.get(); +diff --git a/minestom/src/main/java/me/lucko/luckperms/minestom/dependencies/InstrumentationClassPathAppender.java b/minestom/src/main/java/me/lucko/luckperms/minestom/dependencies/InstrumentationClassPathAppender.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d3ea06fc70f11fb1624729e62313db1388462687 +--- /dev/null ++++ b/minestom/src/main/java/me/lucko/luckperms/minestom/dependencies/InstrumentationClassPathAppender.java +@@ -0,0 +1,57 @@ ++package me.lucko.luckperms.minestom.dependencies; ++ ++import java.io.IOException; ++import java.io.UncheckedIOException; ++import java.lang.instrument.Instrumentation; ++import java.nio.file.Path; ++import java.util.jar.JarFile; ++import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; ++import me.lucko.luckperms.common.plugin.logging.PluginLogger; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * A {@link ClassPathAppender} that adds jars to the system class loader using the instrumentation ++ * API, which is the only supported way of extending the system class path at runtime. ++ */ ++public final class InstrumentationClassPathAppender implements ClassPathAppender { ++ ++ /** ++ * Creates an appender, if an agent has made an {@link Instrumentation} instance available. ++ * ++ * @param logger the logger to warn on, if no agent is installed ++ * @return the appender, or {@link NoopClassPathAppender} if no agent is installed ++ */ ++ public static @NotNull ClassPathAppender createOrFallBack(@NotNull PluginLogger logger) { ++ Instrumentation instrumentation = LuckPermsAgent.getInstrumentation(); ++ if (instrumentation != null) { ++ return new InstrumentationClassPathAppender(instrumentation); ++ } ++ ++ logger.warn("Unable to append jars to the class path at runtime, so LuckPerms cannot load " + ++ "its own dependencies. Any storage or messaging backend that is not already on the " + ++ "class path will fail to initialise. To fix this, either add 'Launcher-Agent-Class: " + ++ LuckPermsAgent.class.getName() + "' to the manifest of your application jar, start " + ++ "the server with '-javaagent:', or supply your own ClassPathAppender via " + ++ "LuckPermsMinestom.Builder#classPathAppender."); ++ ++ return NoopClassPathAppender.INSTANCE; ++ } ++ ++ private final @NotNull Instrumentation instrumentation; ++ ++ private InstrumentationClassPathAppender(@NotNull Instrumentation instrumentation) { ++ this.instrumentation = instrumentation; ++ } ++ ++ @Override ++ public void addJarToClasspath(Path file) { ++ try { ++ // the JVM takes ownership of the JarFile and may keep it open for the rest of the ++ // process lifetime, so it must not be closed here ++ this.instrumentation.appendToSystemClassLoaderSearch(new JarFile(file.toFile())); ++ } catch (IOException e) { ++ throw new UncheckedIOException("Unable to add " + file + " to the system class path", e); ++ } ++ } ++ ++} +diff --git a/minestom/src/main/java/me/lucko/luckperms/minestom/dependencies/LuckPermsAgent.java b/minestom/src/main/java/me/lucko/luckperms/minestom/dependencies/LuckPermsAgent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f950496f47b83a58cc5fb793dde975aa9cd5e840 +--- /dev/null ++++ b/minestom/src/main/java/me/lucko/luckperms/minestom/dependencies/LuckPermsAgent.java +@@ -0,0 +1,58 @@ ++package me.lucko.luckperms.minestom.dependencies; ++ ++import java.lang.instrument.Instrumentation; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++ ++/** ++ * Captures the {@link Instrumentation} instance that LuckPerms uses to load its runtime ++ * dependencies. ++ * ++ *

Unlike the other platforms, LuckPerms is embedded as a plain library on Minestom, so it is ++ * loaded by the system class loader rather than by a class loader LuckPerms owns. Appending to the ++ * system class loader is only possible through the instrumentation API, and the JVM only hands that ++ * out to an agent - so the host application has to install one. Either:

++ * ++ *
    ++ *
  • add {@code Launcher-Agent-Class: me.lucko.luckperms.minestom.dependencies.LuckPermsAgent} to ++ * the manifest of the executable jar (works out of the box with {@code java -jar}), or
  • ++ *
  • start the server with {@code -javaagent:}, pointing at a jar containing this class.
  • ++ *
++ * ++ *

Applications that run LuckPerms inside a class loader they control don't need an agent at all ++ * and should pass their own appender to ++ * {@link me.lucko.luckperms.minestom.LuckPermsMinestom.Builder#classPathAppender} instead.

++ */ ++public final class LuckPermsAgent { ++ ++ private static volatile @Nullable Instrumentation instrumentation; ++ ++ private LuckPermsAgent() { ++ ++ } ++ ++ /** ++ * Called by the JVM when this class is used as a {@code -javaagent}. ++ */ ++ public static void premain(@Nullable String args, @NotNull Instrumentation instrumentation) { ++ LuckPermsAgent.instrumentation = instrumentation; ++ } ++ ++ /** ++ * Called by the JVM when this class is declared as the {@code Launcher-Agent-Class} of an ++ * executable jar, or when the agent is attached to a running JVM. ++ */ ++ public static void agentmain(@Nullable String args, @NotNull Instrumentation instrumentation) { ++ LuckPermsAgent.instrumentation = instrumentation; ++ } ++ ++ /** ++ * Gets the captured instrumentation instance. ++ * ++ * @return the instrumentation instance, or null if no agent has been installed ++ */ ++ public static @Nullable Instrumentation getInstrumentation() { ++ return instrumentation; ++ } ++ ++} diff --git a/vendor/LuckPerms-Patches/0002-Build-the-Minestom-module-against-Minestom-26.3.patch b/vendor/LuckPerms-Patches/0002-Build-the-Minestom-module-against-Minestom-26.3.patch new file mode 100644 index 000000000..7fc521d86 --- /dev/null +++ b/vendor/LuckPerms-Patches/0002-Build-the-Minestom-module-against-Minestom-26.3.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: TheBjoRedCraft +Date: Tue, 13 Oct 2026 16:12:58 +0200 +Subject: [PATCH] Build the Minestom module against Minestom 26.3 + + +diff --git a/minestom/build.gradle b/minestom/build.gradle +index ac55c4c19bd8c5d9a3efc42ffa65b93b486e6b8c..ee88b3a4c6bbefd51ea720b402013c9fd17a3cb4 100644 +--- a/minestom/build.gradle ++++ b/minestom/build.gradle +@@ -8,11 +8,16 @@ plugins { + // minestom requires java 21+ + + repositories { ++ // experimental 26.3 builds ++ maven { ++ url "https://central.sonatype.com/repository/maven-snapshots/" ++ mavenContent { snapshotsOnly() } ++ } + mavenCentral() + } + + dependencies { +- var minestom = "net.minestom:minestom:2026.07.22-26.2" ++ var minestom = "net.minestom:minestom:26_3-SNAPSHOT" + + api(project(":common")) { + // exclude default config providers as they are inaccessible from the minestom module anyway diff --git a/vendor/LuckPerms-Upstream b/vendor/LuckPerms-Upstream new file mode 160000 index 000000000..78c7a8587 --- /dev/null +++ b/vendor/LuckPerms-Upstream @@ -0,0 +1 @@ +Subproject commit 78c7a85875708023315d1abded7628e75614987b diff --git a/vendor/build.gradle.kts b/vendor/build.gradle.kts new file mode 100644 index 000000000..e58f78a76 --- /dev/null +++ b/vendor/build.gradle.kts @@ -0,0 +1,59 @@ +plugins { + id("ca.stellardrift.gitpatcher") version "2.0.0" +} + +repositories { + mavenCentral() +} + +gitPatcher { + patchedRepos { + register("LuckPerms") { + submodule = "LuckPerms-Upstream" + target = file("LuckPerms") + patches = file("LuckPerms-Patches") + } + } +} + +abstract class PreparePatchedRepo : DefaultTask() { + + @get:Internal + abstract val repo: DirectoryProperty + + @get:Internal + abstract val upstream: DirectoryProperty + + @TaskAction + fun prepare() { + val repoDir = this.repo.get().asFile + val commands = mutableListOf>() + + if (!repoDir.resolve(".git").isDirectory) { + repoDir.mkdirs() + commands += listOf("git", "init", "--quiet") + commands += listOf("git", "remote", "add", "origin", this.upstream.get().asFile.absolutePath) + } + + commands += listOf("git", "config", "commit.gpgsign", "false") + + for (command in commands) { + val process = ProcessBuilder(command) + .directory(repoDir) + .redirectErrorStream(true) + .start() + val output = process.inputStream.bufferedReader().readText() + check(process.waitFor() == 0) { "${command.joinToString(" ")} failed: $output" } + } + } + +} + +val preparePatchedRepo = tasks.register("preparePatchedRepo") { + repo = layout.projectDirectory.dir("LuckPerms") + upstream = layout.projectDirectory.dir("LuckPerms-Upstream") +} + +tasks.named("applyLuckPermsPatches") { + dependsOn(preparePatchedRepo) +} diff --git a/vendor/gradle.properties b/vendor/gradle.properties new file mode 100644 index 000000000..24a59763f --- /dev/null +++ b/vendor/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.configuration-cache=true +org.gradle.parallel=true +org.gradle.caching=true + diff --git a/vendor/gradle/wrapper/gradle-wrapper.jar b/vendor/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..b1b8ef56b Binary files /dev/null and b/vendor/gradle/wrapper/gradle-wrapper.jar differ diff --git a/vendor/gradle/wrapper/gradle-wrapper.properties b/vendor/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..a9db11550 --- /dev/null +++ b/vendor/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/vendor/gradlew b/vendor/gradlew new file mode 100755 index 000000000..249efbb03 --- /dev/null +++ b/vendor/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/vendor/gradlew.bat b/vendor/gradlew.bat new file mode 100644 index 000000000..a51ec4f58 --- /dev/null +++ b/vendor/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/vendor/settings.gradle.kts b/vendor/settings.gradle.kts new file mode 100644 index 000000000..b08125254 --- /dev/null +++ b/vendor/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "vendor" diff --git a/vendor/spark-minestom/build.gradle.kts b/vendor/spark-minestom/build.gradle.kts new file mode 100644 index 000000000..b5366e5c7 --- /dev/null +++ b/vendor/spark-minestom/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + `java-library` +} + +group = "me.lucko" +version = libs.versions.sparkMinestom.get() + +java { + toolchain.languageVersion = JavaLanguageVersion.of(25) +} + +dependencies { + api(libs.minestom.spark.common) + + compileOnly(libs.minestom.server) + compileOnly(libs.slf4j) +} + +tasks.withType().configureEach { + options.compilerArgs.add("-parameters") +} + +tasks.processResources { + val pluginVersion = project.version.toString() + inputs.property("pluginVersion", pluginVersion) + + filesMatching("me/lucko/spark/minestom/spark-minestom.properties") { + expand("version" to pluginVersion) + } +} diff --git a/vendor/spark-minestom/gradle.properties b/vendor/spark-minestom/gradle.properties new file mode 100644 index 000000000..761e487ac --- /dev/null +++ b/vendor/spark-minestom/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.configuration-cache=true +org.gradle.caching=true +org.gradle.parallel=true diff --git a/vendor/spark-minestom/settings.gradle.kts b/vendor/spark-minestom/settings.gradle.kts new file mode 100644 index 000000000..10c554150 --- /dev/null +++ b/vendor/spark-minestom/settings.gradle.kts @@ -0,0 +1,28 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + maven("https://repo.lucko.me/") + maven("https://central.sonatype.com/repository/maven-snapshots/") { + name = "minestom-snapshots" + mavenContent { + snapshotsOnly() + includeGroup("net.minestom") + } + } + } + + versionCatalogs { + create("libs") { + from(files("../../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "spark-minestom" diff --git a/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomCommandSender.java b/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomCommandSender.java new file mode 100644 index 000000000..554ea0f4a --- /dev/null +++ b/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomCommandSender.java @@ -0,0 +1,75 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.minestom; + +import static java.util.Objects.requireNonNull; + +import java.util.UUID; +import me.lucko.spark.common.command.sender.AbstractCommandSender; +import net.kyori.adventure.text.Component; +import net.minestom.server.command.CommandSender; +import net.minestom.server.command.ConsoleSender; +import net.minestom.server.entity.Player; +import org.jspecify.annotations.Nullable; + +final class MinestomCommandSender extends AbstractCommandSender { + + private final PermissionChecker permissionChecker; + + public MinestomCommandSender(CommandSender delegate, PermissionChecker permissionChecker) { + requireNonNull(delegate, "delegate"); + requireNonNull(permissionChecker, "permissionChecker"); + + super(delegate); + this.permissionChecker = permissionChecker; + } + + @Override + public String getName() { + if (this.delegate instanceof Player player) { + return player.getUsername(); + } else if (this.delegate instanceof ConsoleSender) { + return "Console"; + } else { + return "unknown:" + this.delegate.getClass().getSimpleName(); + } + } + + @Override + public @Nullable UUID getUniqueId() { + if (super.delegate instanceof Player player) { + return player.getUuid(); + } + return null; + } + + @Override + public void sendMessage(Component message) { + requireNonNull(message, "message"); + this.delegate.sendMessage(message); + } + + @Override + public boolean hasPermission(String permission) { + requireNonNull(permission, "permission"); + return this.permissionChecker.hasPermission(this.delegate, permission); + } +} diff --git a/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomPlatformInfo.java b/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomPlatformInfo.java new file mode 100644 index 000000000..7cfbb2ce8 --- /dev/null +++ b/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomPlatformInfo.java @@ -0,0 +1,52 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.minestom; + +import me.lucko.spark.common.platform.PlatformInfo; +import net.minestom.server.MinecraftServer; + +final class MinestomPlatformInfo implements PlatformInfo { + + @Override + public Type getType() { + return Type.SERVER; + } + + @Override + public String getName() { + return "Minestom"; + } + + @Override + public String getBrand() { + return "Minestom"; + } + + @Override + public String getVersion() { + return MinecraftServer.VERSION_NAME + "-" + MinecraftServer.getBrandName(); + } + + @Override + public String getMinecraftVersion() { + return MinecraftServer.VERSION_NAME; + } +} diff --git a/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomPlayerPingProvider.java b/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomPlayerPingProvider.java new file mode 100644 index 000000000..df35b9655 --- /dev/null +++ b/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomPlayerPingProvider.java @@ -0,0 +1,39 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.minestom; + +import me.lucko.spark.common.monitor.ping.PlayerPingProvider; +import net.minestom.server.MinecraftServer; +import net.minestom.server.entity.Player; + +import java.util.HashMap; +import java.util.Map; + +final class MinestomPlayerPingProvider implements PlayerPingProvider { + @Override + public Map poll() { + final Map builder = new HashMap<>(); + for (final Player player : MinecraftServer.getConnectionManager().getOnlinePlayers()) { + builder.put(player.getUsername(), player.getLatency()); + } + return builder; + } +} diff --git a/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomSparkPlugin.java b/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomSparkPlugin.java new file mode 100644 index 000000000..76de5a27a --- /dev/null +++ b/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomSparkPlugin.java @@ -0,0 +1,293 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.minestom; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Properties; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.stream.Stream; +import me.lucko.spark.common.SparkPlatform; +import me.lucko.spark.common.SparkPlugin; +import me.lucko.spark.common.monitor.ping.PlayerPingProvider; +import me.lucko.spark.common.platform.PlatformInfo; +import me.lucko.spark.common.tick.TickHook; +import me.lucko.spark.common.tick.TickReporter; +import me.lucko.spark.common.util.SparkThreadFactory; +import net.minestom.server.MinecraftServer; +import net.minestom.server.command.CommandSender; +import net.minestom.server.command.builder.Command; +import net.minestom.server.command.builder.CommandContext; +import net.minestom.server.command.builder.CommandExecutor; +import net.minestom.server.command.builder.arguments.ArgumentStringArray; +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.command.builder.suggestion.Suggestion; +import net.minestom.server.command.builder.suggestion.SuggestionCallback; +import net.minestom.server.command.builder.suggestion.SuggestionEntry; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class MinestomSparkPlugin implements SparkPlugin { + + public static final String COMMAND_NAME = "spark"; + + private static final String VERSION = readVersion(); + + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool( + 4, + new SparkThreadFactory() + ); + + private final Logger logger = LoggerFactory.getLogger("spark"); + + private final Path directory; + private final PermissionChecker permissionChecker; + + private volatile @Nullable SparkPlatform platform; + private volatile @Nullable MinestomSparkCommand command; + private final AtomicBoolean disabled = new AtomicBoolean(); + + public MinestomSparkPlugin(Path directory) { + this(directory, PermissionChecker.DEFAULT); + } + + public MinestomSparkPlugin(Path directory, PermissionChecker permissionChecker) { + this.directory = requireNonNull(directory, "directory"); + this.permissionChecker = requireNonNull(permissionChecker, "permissionChecker"); + } + + /** + * Enables spark and registers the {@code /spark} command. + * + *

Must be called after {@link MinecraftServer#init()}.

+ */ + public void enable() { + if (this.platform != null) { + throw new IllegalStateException("spark has already been enabled"); + } + + final SparkPlatform platform = new SparkPlatform(this); + boolean platformEnabled = false; + try { + platform.enable(); + platformEnabled = true; + + final MinestomSparkCommand command = new MinestomSparkCommand(platform, + this.permissionChecker); + MinecraftServer.getCommandManager().register(command); + + this.command = command; + this.platform = platform; + } catch (final Throwable throwable) { + if (platformEnabled) { + try { + platform.disable(); + } catch (final Throwable suppressed) { + throwable.addSuppressed(suppressed); + } + } + this.scheduler.shutdown(); + throw throwable; + } + } + + /** + * Disables spark and unregisters the {@code /spark} command. + */ + public void disable() { + final SparkPlatform platform = this.platform; + if (platform == null || !this.disabled.compareAndSet(false, true)) { + return; + } + + try { + platform.disable(); + + final MinestomSparkCommand command = this.command; + if (command != null) { + MinecraftServer.getCommandManager().unregister(command); + } + } finally { + this.scheduler.shutdown(); + } + } + + @Override + public String getVersion() { + return VERSION; + } + + @Override + public Path getPluginDirectory() { + return this.directory; + } + + @Override + public String getCommandName() { + return COMMAND_NAME; + } + + @Override + public Stream getCommandSenders() { + return Stream.concat( + MinecraftServer.getConnectionManager().getOnlinePlayers().stream(), + Stream.of(MinecraftServer.getCommandManager().getConsoleSender()) + ).map(sender -> new MinestomCommandSender(sender, this.permissionChecker)); + } + + @Override + public void executeAsync(Runnable task) { + this.scheduler.execute(task); + } + + @Override + public void log(Level level, String msg) { + if (level.intValue() >= Level.SEVERE.intValue()) { + this.logger.error(msg); + } else if (level.intValue() >= Level.WARNING.intValue()) { + this.logger.warn(msg); + } else { + this.logger.info(msg); + } + } + + @Override + public void log(Level level, String msg, Throwable throwable) { + if (level.intValue() >= Level.SEVERE.intValue()) { + this.logger.error(msg, throwable); + } else if (level.intValue() >= Level.WARNING.intValue()) { + this.logger.warn(msg, throwable); + } else { + this.logger.info(msg, throwable); + } + } + + @Override + public PlatformInfo getPlatformInfo() { + return new MinestomPlatformInfo(); + } + + @Override + public PlayerPingProvider createPlayerPingProvider() { + return new MinestomPlayerPingProvider(); + } + + @Override + public TickReporter createTickReporter() { + return new MinestomTickReporter(); + } + + @Override + public TickHook createTickHook() { + return new MinestomTickHook(); + } + + private static String readVersion() { + try (final InputStream in = + MinestomSparkPlugin.class.getResourceAsStream("spark-minestom.properties")) { + if (in == null) { + return "unknown"; + } + + final Properties properties = new Properties(); + properties.load(in); + + return properties.getProperty("version", "unknown"); + } catch (final IOException e) { + return "unknown"; + } + } + + private static final class MinestomSparkCommand extends Command implements CommandExecutor, + SuggestionCallback { + + private final SparkPlatform platform; + private final PermissionChecker permissionChecker; + + public MinestomSparkCommand(SparkPlatform platform, PermissionChecker permissionChecker) { + requireNonNull(platform, "platform"); + requireNonNull(permissionChecker, "permissionChecker"); + + super(COMMAND_NAME); + this.platform = platform; + this.permissionChecker = permissionChecker; + + final ArgumentStringArray arrayArgument = ArgumentType.StringArray("args"); + arrayArgument.setSuggestionCallback(this); + + addSyntax(this, arrayArgument); + setDefaultExecutor((sender, _) -> platform.executeCommand(wrap(sender), new String[0])); + } + + @Override + public void apply(CommandSender sender, CommandContext context) { + requireNonNull(sender, "sender"); + requireNonNull(context, "context"); + + final String[] args = processArgs(context, false); + if (args == null) { + return; + } + + this.platform.executeCommand(wrap(sender), args); + } + + @Override + public void apply(CommandSender sender, CommandContext context, Suggestion suggestion) { + requireNonNull(sender, "sender"); + requireNonNull(context, "context"); + requireNonNull(suggestion, "suggestion"); + + final String[] args = processArgs(context, true); + if (args == null) { + return; + } + + final Iterable suggestionEntries = this.platform.tabCompleteCommand(wrap(sender), + args); + for (final String suggestionEntry : suggestionEntries) { + suggestion.addEntry(new SuggestionEntry(suggestionEntry)); + } + } + + private MinestomCommandSender wrap(CommandSender sender) { + return new MinestomCommandSender(sender, this.permissionChecker); + } + + private static String @Nullable [] processArgs(CommandContext context, boolean tabComplete) { + final String[] split = context.getInput().split(" ", tabComplete ? -1 : 0); + if (split.length == 0 || !split[0].equals("/" + COMMAND_NAME) + && !split[0].equals(COMMAND_NAME)) { + return null; + } + + return Arrays.copyOfRange(split, 1, split.length); + } + } +} diff --git a/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomTickHook.java b/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomTickHook.java new file mode 100644 index 000000000..abc27fe4a --- /dev/null +++ b/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomTickHook.java @@ -0,0 +1,47 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.minestom; + +import me.lucko.spark.common.tick.AbstractTickHook; +import net.minestom.server.MinecraftServer; +import net.minestom.server.timer.Task; +import net.minestom.server.timer.TaskSchedule; +import org.jspecify.annotations.Nullable; + +final class MinestomTickHook extends AbstractTickHook { + private @Nullable Task task; + + @Override + public void start() { + this.task = MinecraftServer.getSchedulerManager() + .buildTask(this::onTick) + .delay(TaskSchedule.tick(1)) + .repeat(TaskSchedule.tick(1)) + .schedule(); + } + + @Override + public void close() { + if (this.task != null) { + this.task.cancel(); + } + } +} diff --git a/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomTickReporter.java b/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomTickReporter.java new file mode 100644 index 000000000..5bed263f0 --- /dev/null +++ b/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/MinestomTickReporter.java @@ -0,0 +1,47 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.minestom; + +import me.lucko.spark.common.tick.AbstractTickReporter; +import net.minestom.server.MinecraftServer; +import net.minestom.server.event.Event; +import net.minestom.server.event.EventNode; +import net.minestom.server.event.server.ServerTickMonitorEvent; + +import java.util.UUID; + +final class MinestomTickReporter extends AbstractTickReporter { + private final EventNode node = EventNode.all("sparkTickReporter-" + UUID.randomUUID()); + + public MinestomTickReporter() { + this.node.addListener(ServerTickMonitorEvent.class, event -> onTick(event.getTickMonitor().getTickTime())); + } + + @Override + public void start() { + MinecraftServer.getGlobalEventHandler().addChild(this.node); + } + + @Override + public void close() { + MinecraftServer.getGlobalEventHandler().removeChild(this.node); + } +} diff --git a/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/PermissionChecker.java b/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/PermissionChecker.java new file mode 100644 index 000000000..b86dac1d2 --- /dev/null +++ b/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/PermissionChecker.java @@ -0,0 +1,37 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.minestom; + +import net.minestom.server.command.CommandSender; +import net.minestom.server.entity.Player; + +@FunctionalInterface +public interface PermissionChecker { + + PermissionChecker DEFAULT = (sender, _) -> { + if (sender instanceof Player player) { + return player.getPermissionLevel() >= 4; + } + return true; + }; + + boolean hasPermission(CommandSender sender, String permission); +} diff --git a/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/package-info.java b/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/package-info.java new file mode 100644 index 000000000..26cce2f9e --- /dev/null +++ b/vendor/spark-minestom/src/main/java/me/lucko/spark/minestom/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package me.lucko.spark.minestom; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/vendor/spark-minestom/src/main/resources/me/lucko/spark/minestom/spark-minestom.properties b/vendor/spark-minestom/src/main/resources/me/lucko/spark/minestom/spark-minestom.properties new file mode 100644 index 000000000..a50bf5c84 --- /dev/null +++ b/vendor/spark-minestom/src/main/resources/me/lucko/spark/minestom/spark-minestom.properties @@ -0,0 +1 @@ +version=${version}