diff --git a/buildSrc/src/main/kotlin/core-convention.gradle.kts b/buildSrc/src/main/kotlin/core-convention.gradle.kts index c7403e651..d0557fa37 100644 --- a/buildSrc/src/main/kotlin/core-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/core-convention.gradle.kts @@ -26,6 +26,7 @@ version = findProperty("version") as String + if (snapshot) "-SNAPSHOT" else "" repositories { mavenCentral() maven("https://reposilite.slne.dev/public") { name = "slne-repository-public" } + maven("https://repo.opencollab.dev/main/") { name = "OpenCollab" } } dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 611187935..2f3558e81 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,6 +35,9 @@ placeholder-api = "2.12.3" # McCoroutine mccoroutine = "2.23.1" +# Bedrock +floodgate-api = "2.2.5-SNAPSHOT" + # Miscellaneous Libraries asm = "9.10.1" slf4j = "2.0.17" @@ -142,6 +145,9 @@ mccoroutine-folia-core = { module = "dev.slne.forks.mccoroutine:mccoroutine-foli mccoroutine-velocity-api = { module = "dev.slne.forks.mccoroutine:mccoroutine-velocity-api", version.ref = "mccoroutine" } mccoroutine-velocity-core = { module = "dev.slne.forks.mccoroutine:mccoroutine-velocity-core", version.ref = "mccoroutine" } +# Bedrock +floodgate-api = { module = "org.geysermc.floodgate:api", version.ref = "floodgate-api" } + # Miscellaneous Libraries asm = { module = "org.ow2.asm:asm", version.ref = "asm" } slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 641f2daf8..75a137fdf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -43,4 +43,4 @@ val ci = System.getenv("CI")?.toBoolean() ?: false if (!ci) { include(":surf-api-paper:surf-api-paper-plugin-test") include("surf-api-generator") -} \ No newline at end of file +} diff --git a/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/paper/AbstractPaperSurfPlugin.kt b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/paper/AbstractPaperSurfPlugin.kt index 03e315f6d..7266ce2e1 100644 --- a/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/paper/AbstractPaperSurfPlugin.kt +++ b/surf-api-gradle-plugin/src/main/kotlin/dev/slne/surf/api/gradle/platform/paper/AbstractPaperSurfPlugin.kt @@ -5,6 +5,7 @@ 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.canvasMaven +import dev.slne.surf.api.gradle.util.openCollabMaven import org.gradle.api.Project import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.repositories @@ -19,6 +20,9 @@ internal abstract class AbstractPaperSurfPlugin( } override fun Project.configure0() { + repositories { + openCollabMaven() + } } final override fun Project.afterEvaluated1(extension: E) { 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..090cd558c 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,12 @@ inline fun RepositoryHandler.canvasMaven(crossinline block: MavenArtifactReposit block() } +inline fun RepositoryHandler.openCollabMaven(crossinline block: MavenArtifactRepository.() -> Unit = {}) = + maven("https://repo.opencollab.dev/main/") { + name = "OpenCollab" + block() + } + inline fun RepositoryHandler.slneReleases(crossinline block: MavenArtifactRepository.() -> Unit = {}) = maven("https://reposilite.slne.dev/releases/") { name = "slne-repository-releases" diff --git a/surf-api-paper/surf-api-paper-server/build.gradle.kts b/surf-api-paper/surf-api-paper-server/build.gradle.kts index 11dd639e5..178ed6e71 100644 --- a/surf-api-paper/surf-api-paper-server/build.gradle.kts +++ b/surf-api-paper/surf-api-paper-server/build.gradle.kts @@ -74,6 +74,7 @@ paper { registerSoft("ViaBackwards") registerSoft("ViaRewind") registerSoft("Geyser-Spigot") + registerSoft("floodgate") registerSoft("PlaceholderAPI") register("CommandAPI") { diff --git a/surf-api-paper/surf-api-paper/api/surf-api-paper.api b/surf-api-paper/surf-api-paper/api/surf-api-paper.api index e4b660e17..c9bc00d5c 100644 --- a/surf-api-paper/surf-api-paper/api/surf-api-paper.api +++ b/surf-api-paper/surf-api-paper/api/surf-api-paper.api @@ -114,6 +114,271 @@ public abstract interface class dev/slne/surf/api/paper/api/nms/listener/NmsServ public abstract fun handleServerboundPacket (Ldev/slne/surf/api/paper/nms/listener/packets/NmsPacket;Lorg/bukkit/entity/Player;)Ldev/slne/surf/api/paper/packet/listener/listener/PacketListenerResult; } +public final class dev/slne/surf/api/paper/bedrock/BedrockSupport { + public static final field FLOODGATE_PLUGIN_NAME Ljava/lang/String; + public static final field INSTANCE Ldev/slne/surf/api/paper/bedrock/BedrockSupport; + public final fun isAvailable ()Z + public final fun requireAvailable ()V +} + +public final class dev/slne/surf/api/paper/bedrock/floodgate/Floodgate_extKt { + public static final fun getBukkitPlayer (Lorg/geysermc/floodgate/api/player/FloodgatePlayer;)Lorg/bukkit/entity/Player; + public static final fun getFloodgateApi ()Lorg/geysermc/floodgate/api/FloodgateApi; + public static final fun getFloodgatePlayer (Lorg/bukkit/entity/Player;)Lorg/geysermc/floodgate/api/player/FloodgatePlayer; + public static final fun getXuid (Lorg/bukkit/entity/Player;)Ljava/lang/String; + public static final fun isBedrockPlayer (Lorg/bukkit/entity/Player;)Z + public static final fun isFloodgatePlayer (Lorg/geysermc/floodgate/api/player/FloodgatePlayer;)Z +} + +public abstract class dev/slne/surf/api/paper/bedrock/form/BedrockFormBuilder { + public final fun build ()Lorg/geysermc/cumulus/form/Form; + public final fun executor (Ljava/util/concurrent/Executor;)V + public final fun getLocale ()Ljava/util/Locale; + public final fun onClose (Lkotlin/jvm/functions/Function0;)V + public final fun onClosedOrInvalid (Lkotlin/jvm/functions/Function1;)V + public final fun onInvalid (Lkotlin/jvm/functions/Function1;)V + public final fun onResult (Lkotlin/jvm/functions/Function2;)V + public final fun onValid (Lkotlin/jvm/functions/Function1;)V + public final fun runOn (Lorg/bukkit/entity/Player;Lorg/bukkit/plugin/Plugin;)V + public final fun setLocale (Ljava/util/Locale;)V + public final fun title (Ljava/lang/String;)V + public final fun title (Lkotlin/jvm/functions/Function1;)V + public final fun title (Lnet/kyori/adventure/text/Component;)V + public final fun translator (Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V + public static synthetic fun translator$default (Ldev/slne/surf/api/paper/bedrock/form/BedrockFormBuilder;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V +} + +public final class dev/slne/surf/api/paper/bedrock/form/BedrockFormBuilderKt { + public static final fun formImagePath (Ljava/lang/String;)Lorg/geysermc/cumulus/util/FormImage; + public static final fun formImageUrl (Ljava/lang/String;)Lorg/geysermc/cumulus/util/FormImage; +} + +public abstract interface annotation class dev/slne/surf/api/paper/bedrock/form/BedrockFormDsl : java/lang/annotation/Annotation { +} + +public final class dev/slne/surf/api/paper/bedrock/form/BedrockForms { + public static final field INSTANCE Ldev/slne/surf/api/paper/bedrock/form/BedrockForms; + public final fun close (Ljava/util/UUID;)Z + public final fun isBedrockPlayer (Ljava/util/UUID;)Z + public final fun send (Ljava/util/UUID;Lorg/geysermc/cumulus/form/Form;)Z +} + +public final class dev/slne/surf/api/paper/bedrock/form/BedrockFormsKt { + public static final fun closeForm (Lorg/bukkit/entity/Player;)Z + public static final fun sendCustomForm (Lorg/bukkit/entity/Player;Lkotlin/jvm/functions/Function1;)Z + public static final fun sendForm (Lorg/bukkit/entity/Player;Lorg/geysermc/cumulus/form/Form;)Z + public static final fun sendModalForm (Lorg/bukkit/entity/Player;Lkotlin/jvm/functions/Function1;)Z + public static final fun sendSimpleForm (Lorg/bukkit/entity/Player;Lkotlin/jvm/functions/Function1;)Z +} + +public final class dev/slne/surf/api/paper/bedrock/form/CustomFormBuilder : dev/slne/surf/api/paper/bedrock/form/BedrockFormBuilder { + public fun ()V + public synthetic fun configure (Lorg/geysermc/cumulus/form/util/FormBuilder;Ljava/util/Locale;)V + public final fun dropdown (Ljava/lang/String;Ljava/lang/Iterable;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public final fun dropdown (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public final fun dropdown (Lnet/kyori/adventure/text/Component;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public static synthetic fun dropdown$default (Ldev/slne/surf/api/paper/bedrock/form/CustomFormBuilder;Ljava/lang/String;Ljava/lang/Iterable;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public synthetic fun handleValid (Lorg/geysermc/cumulus/response/FormResponse;)V + public final fun icon (Lorg/geysermc/cumulus/util/FormImage;)V + public final fun iconPath (Ljava/lang/String;)V + public final fun iconUrl (Ljava/lang/String;)V + public final fun input (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public final fun input (Lnet/kyori/adventure/text/Component;Ljava/lang/String;Ljava/lang/String;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public static synthetic fun input$default (Ldev/slne/surf/api/paper/bedrock/form/CustomFormBuilder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public static synthetic fun input$default (Ldev/slne/surf/api/paper/bedrock/form/CustomFormBuilder;Lnet/kyori/adventure/text/Component;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public final fun intSlider (Ljava/lang/String;Lkotlin/ranges/IntRange;II)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public final fun intSlider (Lnet/kyori/adventure/text/Component;Lkotlin/ranges/IntRange;II)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public static synthetic fun intSlider$default (Ldev/slne/surf/api/paper/bedrock/form/CustomFormBuilder;Ljava/lang/String;Lkotlin/ranges/IntRange;IIILjava/lang/Object;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public static synthetic fun intSlider$default (Ldev/slne/surf/api/paper/bedrock/form/CustomFormBuilder;Lnet/kyori/adventure/text/Component;Lkotlin/ranges/IntRange;IIILjava/lang/Object;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public final fun label (Ljava/lang/String;)V + public final fun label (Lkotlin/jvm/functions/Function1;)V + public final fun label (Lnet/kyori/adventure/text/Component;)V + public synthetic fun newBuilder ()Lorg/geysermc/cumulus/form/util/FormBuilder; + public final fun onChanges (Lkotlin/jvm/functions/Function1;)V + public final fun onSubmit (Lkotlin/jvm/functions/Function1;)V + public final fun result (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/bedrock/form/CustomFormResultMapper; + public final fun slider (Ljava/lang/String;FFFF)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public final fun slider (Lnet/kyori/adventure/text/Component;FFFF)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public static synthetic fun slider$default (Ldev/slne/surf/api/paper/bedrock/form/CustomFormBuilder;Ljava/lang/String;FFFFILjava/lang/Object;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public static synthetic fun slider$default (Ldev/slne/surf/api/paper/bedrock/form/CustomFormBuilder;Lnet/kyori/adventure/text/Component;FFFFILjava/lang/Object;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public final fun stepSlider (Ljava/lang/String;Ljava/lang/Iterable;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public final fun stepSlider (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public final fun stepSlider (Lnet/kyori/adventure/text/Component;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public static synthetic fun stepSlider$default (Ldev/slne/surf/api/paper/bedrock/form/CustomFormBuilder;Ljava/lang/String;Ljava/lang/Iterable;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public final fun toggle (Ljava/lang/String;Z)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public final fun toggle (Lnet/kyori/adventure/text/Component;Z)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public static synthetic fun toggle$default (Ldev/slne/surf/api/paper/bedrock/form/CustomFormBuilder;Ljava/lang/String;ZILjava/lang/Object;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public static synthetic fun toggle$default (Ldev/slne/surf/api/paper/bedrock/form/CustomFormBuilder;Lnet/kyori/adventure/text/Component;ZILjava/lang/Object;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public final fun toggles (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/bedrock/form/ToggleGroup; + public final fun toggles (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/bedrock/form/ToggleGroup; + public static synthetic fun toggles$default (Ldev/slne/surf/api/paper/bedrock/form/CustomFormBuilder;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/paper/bedrock/form/ToggleGroup; +} + +public final class dev/slne/surf/api/paper/bedrock/form/CustomFormBuilderKt { + public static final fun customForm (Lkotlin/jvm/functions/Function1;)Lorg/geysermc/cumulus/form/CustomForm; + public static final fun option (Ldev/slne/surf/api/paper/bedrock/form/FormOptionsBuilder;Ljava/lang/String;Z)V + public static synthetic fun option$default (Ldev/slne/surf/api/paper/bedrock/form/FormOptionsBuilder;Ljava/lang/String;ZILjava/lang/Object;)V +} + +public final class dev/slne/surf/api/paper/bedrock/form/CustomFormResultMapper { +} + +public final class dev/slne/surf/api/paper/bedrock/form/CustomFormValues { + public final fun enabled (Ldev/slne/surf/api/paper/bedrock/form/ToggleGroup;)Ljava/util/Set; + public final fun get (Ldev/slne/surf/api/paper/bedrock/form/FormField;)Ljava/lang/Object; + public final fun get (Ldev/slne/surf/api/paper/bedrock/form/ToggleGroup;)Ljava/util/Map; + public final fun getResponse ()Lorg/geysermc/cumulus/response/CustomFormResponse; +} + +public final class dev/slne/surf/api/paper/bedrock/form/FieldChange { + public final fun component1 ()Ldev/slne/surf/api/paper/bedrock/form/FormField; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Object; + public final fun component4 ()Ljava/lang/Object; + public final fun copy (Ldev/slne/surf/api/paper/bedrock/form/FormField;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)Ldev/slne/surf/api/paper/bedrock/form/FieldChange; + public static synthetic fun copy$default (Ldev/slne/surf/api/paper/bedrock/form/FieldChange;Ldev/slne/surf/api/paper/bedrock/form/FormField;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;ILjava/lang/Object;)Ldev/slne/surf/api/paper/bedrock/form/FieldChange; + public fun equals (Ljava/lang/Object;)Z + public final fun getField ()Ldev/slne/surf/api/paper/bedrock/form/FormField; + public final fun getLabel ()Ljava/lang/String; + public final fun getNew ()Ljava/lang/Object; + public final fun getOld ()Ljava/lang/Object; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/paper/bedrock/form/FormField { + public final fun getDefault ()Ljava/lang/Object; + public final fun getIndex ()I + public final fun onChange (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/paper/bedrock/form/FormField; +} + +public final class dev/slne/surf/api/paper/bedrock/form/FormOptionsBuilder { + public fun ()V + public final fun option (Ljava/lang/Object;Ljava/lang/String;Z)V + public final fun option (Ljava/lang/Object;Lnet/kyori/adventure/text/Component;Z)V + public static synthetic fun option$default (Ldev/slne/surf/api/paper/bedrock/form/FormOptionsBuilder;Ljava/lang/Object;Ljava/lang/String;ZILjava/lang/Object;)V + public static synthetic fun option$default (Ldev/slne/surf/api/paper/bedrock/form/FormOptionsBuilder;Ljava/lang/Object;Lnet/kyori/adventure/text/Component;ZILjava/lang/Object;)V +} + +public abstract interface class dev/slne/surf/api/paper/bedrock/form/FormResult { + public fun getOrNull ()Ljava/lang/Object; +} + +public final class dev/slne/surf/api/paper/bedrock/form/FormResult$Closed : dev/slne/surf/api/paper/bedrock/form/FormResult { + public static final field INSTANCE Ldev/slne/surf/api/paper/bedrock/form/FormResult$Closed; + public fun equals (Ljava/lang/Object;)Z + public synthetic fun getOrNull ()Ljava/lang/Object; + public fun getOrNull ()Ljava/lang/Void; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/paper/bedrock/form/FormResult$DefaultImpls { + public static fun getOrNull (Ldev/slne/surf/api/paper/bedrock/form/FormResult;)Ljava/lang/Object; +} + +public final class dev/slne/surf/api/paper/bedrock/form/FormResult$Invalid : dev/slne/surf/api/paper/bedrock/form/FormResult { + public fun (ILjava/lang/String;)V + public final fun component1 ()I + public final fun component2 ()Ljava/lang/String; + public final fun copy (ILjava/lang/String;)Ldev/slne/surf/api/paper/bedrock/form/FormResult$Invalid; + public static synthetic fun copy$default (Ldev/slne/surf/api/paper/bedrock/form/FormResult$Invalid;ILjava/lang/String;ILjava/lang/Object;)Ldev/slne/surf/api/paper/bedrock/form/FormResult$Invalid; + public fun equals (Ljava/lang/Object;)Z + public final fun getComponentIndex ()I + public final fun getErrorMessage ()Ljava/lang/String; + public synthetic fun getOrNull ()Ljava/lang/Object; + public fun getOrNull ()Ljava/lang/Void; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/paper/bedrock/form/FormResult$NotSent : dev/slne/surf/api/paper/bedrock/form/FormResult { + public static final field INSTANCE Ldev/slne/surf/api/paper/bedrock/form/FormResult$NotSent; + public fun equals (Ljava/lang/Object;)Z + public synthetic fun getOrNull ()Ljava/lang/Object; + public fun getOrNull ()Ljava/lang/Void; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/paper/bedrock/form/FormResult$Valid : dev/slne/surf/api/paper/bedrock/form/FormResult { + public fun (Ljava/lang/Object;)V + public final fun component1 ()Ljava/lang/Object; + public final fun copy (Ljava/lang/Object;)Ldev/slne/surf/api/paper/bedrock/form/FormResult$Valid; + public static synthetic fun copy$default (Ldev/slne/surf/api/paper/bedrock/form/FormResult$Valid;Ljava/lang/Object;ILjava/lang/Object;)Ldev/slne/surf/api/paper/bedrock/form/FormResult$Valid; + public fun equals (Ljava/lang/Object;)Z + public fun getOrNull ()Ljava/lang/Object; + public final fun getValue ()Ljava/lang/Object; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/paper/bedrock/form/Form_coroutinesKt { + public static final fun awaitChoice (Lorg/bukkit/entity/Player;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun awaitChoice$default (Lorg/bukkit/entity/Player;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun awaitCustomForm (Lorg/bukkit/entity/Player;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitModalForm (Lorg/bukkit/entity/Player;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitSimpleForm (Lorg/bukkit/entity/Player;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class dev/slne/surf/api/paper/bedrock/form/ModalFormBuilder : dev/slne/surf/api/paper/bedrock/form/BedrockFormBuilder { + public fun ()V + public final fun button1 (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V + public final fun button1 (Lnet/kyori/adventure/text/Component;Lkotlin/jvm/functions/Function0;)V + public static synthetic fun button1$default (Ldev/slne/surf/api/paper/bedrock/form/ModalFormBuilder;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V + public static synthetic fun button1$default (Ldev/slne/surf/api/paper/bedrock/form/ModalFormBuilder;Lnet/kyori/adventure/text/Component;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V + public final fun button2 (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V + public final fun button2 (Lnet/kyori/adventure/text/Component;Lkotlin/jvm/functions/Function0;)V + public static synthetic fun button2$default (Ldev/slne/surf/api/paper/bedrock/form/ModalFormBuilder;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V + public static synthetic fun button2$default (Ldev/slne/surf/api/paper/bedrock/form/ModalFormBuilder;Lnet/kyori/adventure/text/Component;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V + public synthetic fun configure (Lorg/geysermc/cumulus/form/util/FormBuilder;Ljava/util/Locale;)V + public final fun content (Ljava/lang/String;)V + public final fun content (Lkotlin/jvm/functions/Function1;)V + public final fun content (Lnet/kyori/adventure/text/Component;)V + public synthetic fun handleValid (Lorg/geysermc/cumulus/response/FormResponse;)V + public synthetic fun newBuilder ()Lorg/geysermc/cumulus/form/util/FormBuilder; + public final fun onResponse (Lkotlin/jvm/functions/Function1;)V +} + +public final class dev/slne/surf/api/paper/bedrock/form/ModalFormBuilderKt { + public static final fun modalForm (Lkotlin/jvm/functions/Function1;)Lorg/geysermc/cumulus/form/ModalForm; +} + +public final class dev/slne/surf/api/paper/bedrock/form/SimpleFormBuilder : dev/slne/surf/api/paper/bedrock/form/BedrockFormBuilder { + public fun ()V + public final fun button (Ljava/lang/String;Lorg/geysermc/cumulus/util/FormImage;Lkotlin/jvm/functions/Function0;)V + public final fun button (Lnet/kyori/adventure/text/Component;Lorg/geysermc/cumulus/util/FormImage;Lkotlin/jvm/functions/Function0;)V + public static synthetic fun button$default (Ldev/slne/surf/api/paper/bedrock/form/SimpleFormBuilder;Ljava/lang/String;Lorg/geysermc/cumulus/util/FormImage;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V + public static synthetic fun button$default (Ldev/slne/surf/api/paper/bedrock/form/SimpleFormBuilder;Lnet/kyori/adventure/text/Component;Lorg/geysermc/cumulus/util/FormImage;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V + public final fun buttons (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun buttons$default (Ldev/slne/surf/api/paper/bedrock/form/SimpleFormBuilder;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public synthetic fun configure (Lorg/geysermc/cumulus/form/util/FormBuilder;Ljava/util/Locale;)V + public final fun content (Ljava/lang/String;)V + public final fun content (Lkotlin/jvm/functions/Function1;)V + public final fun content (Lnet/kyori/adventure/text/Component;)V + public synthetic fun handleValid (Lorg/geysermc/cumulus/response/FormResponse;)V + public synthetic fun newBuilder ()Lorg/geysermc/cumulus/form/util/FormBuilder; + public final fun onButtonClick (Lkotlin/jvm/functions/Function2;)V +} + +public final class dev/slne/surf/api/paper/bedrock/form/SimpleFormBuilderKt { + public static final fun simpleForm (Lkotlin/jvm/functions/Function1;)Lorg/geysermc/cumulus/form/SimpleForm; +} + +public final class dev/slne/surf/api/paper/bedrock/form/ToggleGroup { + public final fun get (Ljava/lang/Object;)Ldev/slne/surf/api/paper/bedrock/form/FormField; + public final fun getToggles ()Ljava/util/Map; + public final fun onChange (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/paper/bedrock/form/ToggleGroup; +} + +public final class dev/slne/surf/api/paper/bedrock/form/ToggleGroupBuilder { + public fun ()V + public final fun toggle (Ljava/lang/Object;Ljava/lang/String;Z)V + public final fun toggle (Ljava/lang/Object;Lnet/kyori/adventure/text/Component;Z)V + public static synthetic fun toggle$default (Ldev/slne/surf/api/paper/bedrock/form/ToggleGroupBuilder;Ljava/lang/Object;Ljava/lang/String;ZILjava/lang/Object;)V + public static synthetic fun toggle$default (Ldev/slne/surf/api/paper/bedrock/form/ToggleGroupBuilder;Ljava/lang/Object;Lnet/kyori/adventure/text/Component;ZILjava/lang/Object;)V +} + public abstract interface annotation class dev/slne/surf/api/paper/builder/ItemDsl : java/lang/annotation/Annotation { } diff --git a/surf-api-paper/surf-api-paper/build.gradle.kts b/surf-api-paper/surf-api-paper/build.gradle.kts index f3cf23bd5..676d50e99 100644 --- a/surf-api-paper/surf-api-paper/build.gradle.kts +++ b/surf-api-paper/surf-api-paper/build.gradle.kts @@ -5,7 +5,7 @@ plugins { dependencies { api(projects.surfApiCore.surfApiCore) - compileOnly(libs.paper.api) + compileOnlyApi(libs.paper.api) compileOnlyApi(libs.packetevents.spigot) compileOnlyApi(libs.commandapi.paper) compileOnlyApi(libs.reflection.remapper) @@ -15,6 +15,7 @@ dependencies { api(libs.commandapi.bukkit.kotlin) compileOnlyApi(libs.mccoroutine.folia.api) api(libs.wyck) + compileOnlyApi(libs.floodgate.api) testImplementation(libs.mockbukkit) testImplementation(libs.paper.api) diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/BedrockSupport.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/BedrockSupport.kt new file mode 100644 index 000000000..384a0ddec --- /dev/null +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/BedrockSupport.kt @@ -0,0 +1,32 @@ +package dev.slne.surf.api.paper.bedrock + +import org.bukkit.Bukkit + +/** + * Checks whether Floodgate, which is required for the Bedrock API, is installed. + * + * Floodgate is an optional dependency, so every entry point of the Bedrock API + * checks this before touching any Floodgate class. + */ +object BedrockSupport { + const val FLOODGATE_PLUGIN_NAME = "floodgate" + + /** + * `true` if Floodgate is installed and enabled. + */ + val isAvailable: Boolean + get() = Bukkit.getPluginManager().isPluginEnabled(FLOODGATE_PLUGIN_NAME) + + /** + * Throws an [IllegalStateException] if Floodgate is not installed. + */ + fun requireAvailable() { + check(isAvailable) { "Floodgate ($FLOODGATE_PLUGIN_NAME) is not installed or enabled" } + } +} + +/** + * Runs [block] if Floodgate is installed, otherwise returns [default]. + */ +internal inline fun ifBedrockAvailable(default: T, block: () -> T): T = + if (BedrockSupport.isAvailable) block() else default diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/floodgate/floodgate-ext.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/floodgate/floodgate-ext.kt new file mode 100644 index 000000000..c1aaf4b71 --- /dev/null +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/floodgate/floodgate-ext.kt @@ -0,0 +1,47 @@ +package dev.slne.surf.api.paper.bedrock.floodgate + +import dev.slne.surf.api.paper.bedrock.BedrockSupport +import dev.slne.surf.api.paper.bedrock.ifBedrockAvailable +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import org.geysermc.floodgate.api.FloodgateApi +import org.geysermc.floodgate.api.player.FloodgatePlayer + +/** + * The Floodgate API instance. + * + * @throws IllegalStateException if Floodgate is not installed. + */ +val floodgateApi: FloodgateApi + get() { + BedrockSupport.requireAvailable() + return FloodgateApi.getInstance() + } + +/** + * The Floodgate player of this player, or `null` if this player is not a Bedrock player + * or Floodgate is not installed. + */ +val Player.floodgatePlayer: FloodgatePlayer? + get() = ifBedrockAvailable(null) { floodgateApi.getPlayer(uniqueId) } + +/** + * Returns `true` if this player is a Bedrock player. + * Returns `false` if Floodgate is not installed. + */ +fun Player.isBedrockPlayer() = ifBedrockAvailable(false) { floodgateApi.isFloodgatePlayer(uniqueId) } + +/** + * The Xbox user id of this player, or `null` if this player is not a Bedrock player + * or Floodgate is not installed. + */ +val Player.xuid: String? get() = floodgatePlayer?.xuid + +val FloodgatePlayer.bukkitPlayer get() = Bukkit.getPlayer(this.javaUniqueId) + +/** + * Returns `true` if this player is a Floodgate player. + * Returns `false` if Floodgate is not installed. + */ +fun FloodgatePlayer.isFloodgatePlayer() = + ifBedrockAvailable(false) { floodgateApi.isFloodgatePlayer(this.javaUniqueId) } diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/form/BedrockFormBuilder.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/form/BedrockFormBuilder.kt new file mode 100644 index 000000000..bba7a5958 --- /dev/null +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/form/BedrockFormBuilder.kt @@ -0,0 +1,234 @@ +package dev.slne.surf.api.paper.bedrock.form + +import dev.slne.surf.api.core.messages.builder.SurfComponentBuilder +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer +import net.kyori.adventure.translation.GlobalTranslator +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin +import org.geysermc.cumulus.form.Form +import org.geysermc.cumulus.form.util.FormBuilder +import org.geysermc.cumulus.response.FormResponse +import org.geysermc.cumulus.response.result.FormResponseResult +import org.geysermc.cumulus.response.result.InvalidFormResponseResult +import org.geysermc.cumulus.response.result.ValidFormResponseResult +import org.geysermc.cumulus.util.FormImage +import java.util.* +import java.util.concurrent.Executor + +/** + * A DSL marker for the Bedrock form DSL to prevent scope conflicts in nested DSL blocks. + */ +@DslMarker +@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) +annotation class BedrockFormDsl + +/** + * Text used inside a form. Either a raw string (which may contain legacy color codes or + * translation keys for the Cumulus translator) or an Adventure [Component], which is rendered + * with the form's locale and serialized to legacy section format, since Bedrock forms only + * support plain strings. + */ +internal sealed interface FormText { + fun resolve(locale: Locale?): String + + class Raw(val text: String) : FormText { + override fun resolve(locale: Locale?) = text + } + + class Rich(val component: Component) : FormText { + override fun resolve(locale: Locale?): String { + val rendered = + if (locale != null) GlobalTranslator.render(component, locale) else component + return LegacyComponentSerializer.legacySection().serialize(rendered) + } + } + + companion object { + val EMPTY: FormText = Raw("") + } +} + +internal fun buildText(block: SurfComponentBuilder.() -> Unit): FormText = + FormText.Rich(SurfComponentBuilder().apply(block).build()) + +/** + * Creates a [FormImage] pointing to an image URL. + */ +fun formImageUrl(url: String): FormImage = FormImage.of(FormImage.Type.URL, url) + +/** + * Creates a [FormImage] pointing to a path inside the Bedrock resource packs, + * e.g. `textures/items/diamond`. + */ +fun formImagePath(path: String): FormImage = FormImage.of(FormImage.Type.PATH, path) + +/** + * Base class of all Bedrock form DSL builders. Wraps the Cumulus [FormBuilder] and adds + * Adventure component support, multiple result handlers and handler executors. + * + * @param F The Cumulus form type. + * @param R The Cumulus response type. + * @param B The Cumulus builder type. + */ +@BedrockFormDsl +sealed class BedrockFormBuilder> { + private var title: FormText = FormText.EMPTY + private var translator: ((key: String, locale: String) -> String)? = null + private var translatorLocale: String? = null + private var executor: Executor? = null + + private val closedHandlers = mutableListOf<() -> Unit>() + private val invalidHandlers = mutableListOf<(InvalidFormResponseResult) -> Unit>() + private val validHandlers = mutableListOf<(R) -> Unit>() + private val resultHandlers = mutableListOf<(F, FormResponseResult) -> Unit>() + private val internalListeners = mutableListOf<(FormResponseResult) -> Unit>() + + /** + * The locale used to render Adventure components (e.g. translatable components) in this form. + * Automatically set to the player's locale when the form is sent via the player extensions. + */ + var locale: Locale? = null + + /** + * Sets the title of the form. + */ + fun title(title: String) { + this.title = FormText.Raw(title) + } + + /** + * Sets the title of the form. + */ + fun title(title: Component) { + this.title = FormText.Rich(title) + } + + /** + * Sets the title of the form using the [SurfComponentBuilder]. + */ + fun title(block: SurfComponentBuilder.() -> Unit) { + this.title = buildText(block) + } + + /** + * Sets the Cumulus translator, which is applied to every string of the form. + * + * @param locale The locale passed to the translator. If `null`, the locale of the Bedrock + * client is used. + * @param translator Receives the translation key and the locale and returns the translated string. + */ + fun translator(locale: String? = null, translator: (key: String, locale: String) -> String) { + this.translator = translator + this.translatorLocale = locale + } + + /** + * Sets the executor all handlers of this form are executed on. + * By default, handlers run on the thread Floodgate delivers the response on, + * which is **not** the server thread. + * + * @see runOn + */ + fun executor(executor: Executor) { + this.executor = executor + } + + /** + * Runs all handlers of this form on the entity scheduler of the given [player]. + * This makes it safe to use the Bukkit API inside the handlers (Folia compatible). + */ + fun runOn(player: Player, plugin: Plugin) { + executor { task -> player.scheduler.run(plugin, { task.run() }, null) } + } + + /** + * Called when the player closed the form. + */ + fun onClose(handler: () -> Unit) { + closedHandlers += handler + } + + /** + * Called when the client sent an invalid response. + */ + fun onInvalid(handler: (InvalidFormResponseResult) -> Unit) { + invalidHandlers += handler + } + + /** + * Called when the player closed the form or the client sent an invalid response. + */ + fun onClosedOrInvalid(handler: (FormResponseResult) -> Unit) { + resultHandlers += { _, result -> if (!result.isValid) handler(result) } + } + + /** + * Called with the raw Cumulus response when the player submitted the form. + */ + fun onValid(handler: (R) -> Unit) { + validHandlers += handler + } + + /** + * Called for every result of this form, regardless of its type. + */ + fun onResult(handler: (form: F, result: FormResponseResult) -> Unit) { + resultHandlers += handler + } + + internal fun addInternalListener(listener: (FormResponseResult) -> Unit) { + internalListeners += listener + } + + protected abstract fun newBuilder(): B + protected abstract fun B.configure(locale: Locale?) + + /** + * Dispatches a valid response to builder-specific handlers (e.g. button click handlers). + */ + protected open fun handleValid(response: R) = Unit + + /** + * Builds the Cumulus form. + */ + fun build(): F { + val locale = locale + val builder = newBuilder() + builder.title(title.resolve(locale)) + + translator?.let { translator -> + val locale = translatorLocale + if (locale != null) { + builder.translator(translator, locale) + } else { + builder.translator(translator) + } + } + + builder.configure(locale) + builder.resultHandler { form, result -> dispatch(form, result) } + return builder.build() + } + + private fun dispatch(form: F, result: FormResponseResult) { + internalListeners.forEach { it(result) } + + val handle = { + when (result) { + is ValidFormResponseResult -> { + val response = result.response() + handleValid(response) + validHandlers.forEach { it(response) } + } + + is InvalidFormResponseResult -> invalidHandlers.forEach { it(result) } + else -> closedHandlers.forEach { it() } + } + resultHandlers.forEach { it(form, result) } + } + + val executor = executor + if (executor != null) executor.execute(handle) else handle() + } +} diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/form/BedrockForms.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/form/BedrockForms.kt new file mode 100644 index 000000000..53e9b0100 --- /dev/null +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/form/BedrockForms.kt @@ -0,0 +1,86 @@ +package dev.slne.surf.api.paper.bedrock.form + +import dev.slne.surf.api.paper.bedrock.ifBedrockAvailable +import org.bukkit.entity.Player +import org.geysermc.cumulus.form.CustomForm +import org.geysermc.cumulus.form.Form +import org.geysermc.cumulus.form.ModalForm +import org.geysermc.cumulus.form.SimpleForm +import org.geysermc.floodgate.api.FloodgateApi +import java.util.* + +/** + * Sends Cumulus forms to Bedrock players via Floodgate (which also works behind a proxy). + * Requires Floodgate to be installed, otherwise every function is a no-op. + */ +object BedrockForms { + private val floodgate get() = FloodgateApi.getInstance() + + /** + * Returns `true` if the player with the given [uuid] is a Bedrock player. + * Returns `false` if Floodgate is not installed. + */ + fun isBedrockPlayer(uuid: UUID): Boolean = + ifBedrockAvailable(false) { floodgate.isFloodgatePlayer(uuid) } + + /** + * Sends the [form] to the player with the given [uuid]. + * + * @return `true` if the form was sent, `false` if the player is not a Bedrock player + * or Floodgate is not installed. + */ + fun send(uuid: UUID, form: Form): Boolean = ifBedrockAvailable(false) { + floodgate.isFloodgatePlayer(uuid) && floodgate.sendForm(uuid, form) + } + + /** + * Closes the currently open form of the player with the given [uuid]. + * + * @return `true` if the form was closed. + */ + fun close(uuid: UUID): Boolean = ifBedrockAvailable(false) { + floodgate.isFloodgatePlayer(uuid) && floodgate.closeForm(uuid) + } +} + +internal fun > B.localizedFor(player: Player): B = + apply { locale = player.locale() } + +/** + * Sends the [form] to this player. + * + * @return `true` if the form was sent. + * @see BedrockForms.send + */ +fun Player.sendForm(form: Form): Boolean = BedrockForms.send(uniqueId, form) + +/** + * Builds a [SimpleForm] with this player's locale and sends it to this player. + * + * @return `true` if the form was sent. + */ +fun Player.sendSimpleForm(block: SimpleFormBuilder.() -> Unit): Boolean = + sendForm(SimpleFormBuilder().localizedFor(this).apply(block).build()) + +/** + * Builds a [ModalForm] with this player's locale and sends it to this player. + * + * @return `true` if the form was sent. + */ +fun Player.sendModalForm(block: ModalFormBuilder.() -> Unit): Boolean = + sendForm(ModalFormBuilder().localizedFor(this).apply(block).build()) + +/** + * Builds a [CustomForm] with this player's locale and sends it to this player. + * + * @return `true` if the form was sent. + */ +fun Player.sendCustomForm(block: CustomFormBuilder.() -> Unit): Boolean = + sendForm(CustomFormBuilder().localizedFor(this).apply(block).build()) + +/** + * Closes the currently open form of this player. + * + * @return `true` if the form was closed. + */ +fun Player.closeForm(): Boolean = BedrockForms.close(uniqueId) diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/form/CustomFormBuilder.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/form/CustomFormBuilder.kt new file mode 100644 index 000000000..efee5c8a9 --- /dev/null +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/form/CustomFormBuilder.kt @@ -0,0 +1,609 @@ +package dev.slne.surf.api.paper.bedrock.form + +import dev.slne.surf.api.core.messages.builder.SurfComponentBuilder +import net.kyori.adventure.text.Component +import org.geysermc.cumulus.form.CustomForm +import org.geysermc.cumulus.response.CustomFormResponse +import org.geysermc.cumulus.util.FormImage +import java.util.* +import kotlin.math.roundToInt + +/** + * Creates a Cumulus [CustomForm] (a form with inputs, toggles, sliders, dropdowns, ...) using the DSL. + * Every component function returns a typed [FormField], which is used to read the submitted value. + * + * Components can be added conditionally with a plain `if`, the field indices stay correct. + * + * **Example Usage:** + * ```kotlin + * val form = customForm { + * title("Settings") + * label("Configure your profile") + * val name = input("Name", placeholder = "Steve") + * val age = intSlider("Age", 1..99, default = 18) + * val mode = enumDropdown("Game mode", default = GameMode.SURVIVAL) + * val color = dropdown("Color") { + * option(NamedTextColor.RED, text("Red", NamedTextColor.RED)) + * option(NamedTextColor.BLUE, text("Blue", NamedTextColor.BLUE), default = true) + * } + * val notify = toggle("Notifications", default = true) + * .onChange { _, enabled -> player.sendMessage("Notifications: $enabled") } + * val features = enumToggles(default = { it.isEnabled(player) }) + * .onChange { feature, enabled -> feature.set(player, enabled) } + * + * onChanges { changes -> changes.forEach { log("${it.label}: ${it.old} -> ${it.new}") } } + * onSubmit { values -> + * saveProfile(values[name], values[age], values[mode], values[color], values[notify]) + * } + * } + * ``` + */ +fun customForm(block: CustomFormBuilder.() -> Unit): CustomForm = + CustomFormBuilder().apply(block).build() + +/** + * A typed handle to a component of a [CustomForm], used to read its submitted value + * from [CustomFormValues] and to listen for changes. + * + * @property index The index of the component inside the form (labels included). + * @property default The value the component has when the form is opened. + */ +class FormField internal constructor( + val index: Int, + internal val text: FormText, + val default: T, + private val reader: (CustomFormResponse, Int) -> T, +) { + private val changeListeners = mutableListOf<(old: T, new: T) -> Unit>() + + /** + * Called when the player submitted the form with a value different from [default]. + * + * Bedrock clients send no values when the form is closed, so changes are only + * detected on submit. + */ + fun onChange(listener: (old: T, new: T) -> Unit): FormField { + changeListeners += listener + return this + } + + internal fun read(response: CustomFormResponse): T = reader(response, index) + + internal fun dispatchChange(response: CustomFormResponse, locale: Locale?): FieldChange? { + val value = read(response) + if (value == default) return null + changeListeners.forEach { it(default, value) } + return FieldChange(this, text.resolve(locale), default, value) + } +} + +/** + * A changed value of a [FormField]. + * + * @property field The changed field. + * @property label The resolved text of the field. + * @property old The default value of the field. + * @property new The submitted value of the field. + */ +data class FieldChange internal constructor( + val field: FormField, + val label: String, + val old: T, + val new: T, +) + +/** + * A group of toggles, one per value, created with [CustomFormBuilder.toggles]. + * + * @property toggles The toggle field of each value. + */ +class ToggleGroup internal constructor(val toggles: Map>) { + /** + * Returns the toggle field of the given [value]. + */ + operator fun get(value: T): FormField = toggles.getValue(value) + + /** + * Called for every toggle of this group that the player changed. + */ + fun onChange(listener: (value: T, enabled: Boolean) -> Unit): ToggleGroup { + for ((value, field) in toggles) { + field.onChange { _, new -> listener(value, new) } + } + return this + } +} + +/** + * DSL builder for the toggles of a [ToggleGroup]. + * + * @param T The value type of a toggle. + */ +@BedrockFormDsl +class ToggleGroupBuilder @PublishedApi internal constructor() { + internal class Toggle(val value: T, val text: FormText, val default: Boolean) + + internal val toggles = mutableListOf>() + + /** + * Adds a toggle. + * + * @param value The value this toggle represents. + * @param text The displayed text. + * @param default Whether this toggle is enabled by default. + */ + fun toggle(value: T, text: String, default: Boolean = false) { + toggles += Toggle(value, FormText.Raw(text), default) + } + + /** + * Adds a toggle. + * + * @param value The value this toggle represents. + * @param text The displayed text. + * @param default Whether this toggle is enabled by default. + */ + fun toggle(value: T, text: Component, default: Boolean = false) { + toggles += Toggle(value, FormText.Rich(text), default) + } +} + +/** + * The submitted values of a [CustomForm]. + * + * @property response The raw Cumulus response. + */ +class CustomFormValues internal constructor(val response: CustomFormResponse) { + /** + * Returns the submitted value of the given [field]. + */ + operator fun get(field: FormField): T = field.read(response) + + /** + * Returns the submitted state of every toggle of the given [group]. + */ + operator fun get(group: ToggleGroup): Map = + group.toggles.mapValues { (_, field) -> field.read(response) } + + /** + * Returns the values of all enabled toggles of the given [group]. + */ + fun enabled(group: ToggleGroup): Set = + group.toggles.filterValues { it.read(response) }.keys +} + +/** + * DSL builder for the options of a dropdown or the steps of a step slider. + * + * @param T The value type of an option. + */ +@BedrockFormDsl +class FormOptionsBuilder @PublishedApi internal constructor() { + internal class Option(val value: T, val text: FormText) + + internal val options = mutableListOf>() + internal var defaultIndex = 0 + + /** + * Adds an option. + * + * @param value The value returned when this option is selected. + * @param text The displayed text. + * @param default Whether this option is selected by default. + */ + fun option(value: T, text: String, default: Boolean = false) { + add(Option(value, FormText.Raw(text)), default) + } + + /** + * Adds an option. + * + * @param value The value returned when this option is selected. + * @param text The displayed text. + * @param default Whether this option is selected by default. + */ + fun option(value: T, text: Component, default: Boolean = false) { + add(Option(value, FormText.Rich(text)), default) + } + + private fun add(option: Option, default: Boolean) { + if (default) defaultIndex = options.size + options += option + } +} + +/** + * Adds an option whose value is its displayed text. + */ +fun FormOptionsBuilder.option(text: String, default: Boolean = false) = + option(text, text, default) + +/** + * DSL builder for a Cumulus [CustomForm]. + */ +@BedrockFormDsl +class CustomFormBuilder @PublishedApi internal constructor() : + BedrockFormBuilder() { + private var icon: FormImage? = null + private val components = mutableListOf Unit>() + private val fields = mutableListOf>() + private val submitHandlers = mutableListOf<(CustomFormValues) -> Unit>() + private val changesHandlers = mutableListOf<(List>) -> Unit>() + + /** + * Sets the icon of the form, see [formImageUrl] and [formImagePath]. + */ + fun icon(image: FormImage) { + icon = image + } + + /** + * Sets the icon of the form to an image URL. + */ + fun iconUrl(url: String) = icon(formImageUrl(url)) + + /** + * Sets the icon of the form to a resource pack path. + */ + fun iconPath(path: String) = icon(formImagePath(path)) + + private fun field( + text: FormText, + default: T, + reader: (CustomFormResponse, Int) -> T, + component: CustomForm.Builder.(Locale?) -> Unit, + ): FormField { + val field = FormField(components.size, text, default, reader) + components += component + fields += field + return field + } + + /** + * Adds a label (plain text without a value). + */ + fun label(text: String) { + components += { label(text) } + } + + /** + * Adds a label (plain text without a value). + */ + fun label(text: Component) { + val formText = FormText.Rich(text) + components += { label(formText.resolve(it)) } + } + + /** + * Adds a label (plain text without a value) using the [SurfComponentBuilder]. + */ + fun label(block: SurfComponentBuilder.() -> Unit) { + val formText = buildText(block) + components += { label(formText.resolve(it)) } + } + + /** + * Adds a text input field. + * + * @return A field with the entered text. + */ + fun input(text: String, placeholder: String = "", default: String = ""): FormField = + input(FormText.Raw(text), placeholder, default) + + /** + * Adds a text input field. + * + * @return A field with the entered text. + */ + fun input(text: Component, placeholder: String = "", default: String = ""): FormField = + input(FormText.Rich(text), placeholder, default) + + private fun input(text: FormText, placeholder: String, default: String) = + field(text, default, { response, index -> response.asInput(index).orEmpty() }) { + input(text.resolve(it), placeholder, default) + } + + /** + * Adds a toggle. + * + * @return A field with the toggle state. + */ + fun toggle(text: String, default: Boolean = false): FormField = + toggle(FormText.Raw(text), default) + + /** + * Adds a toggle. + * + * @return A field with the toggle state. + */ + fun toggle(text: Component, default: Boolean = false): FormField = + toggle(FormText.Rich(text), default) + + private fun toggle(text: FormText, default: Boolean) = + field(text, default, { response, index -> response.asToggle(index) }) { + toggle(text.resolve(it), default) + } + + /** + * Adds a slider. + * + * @return A field with the selected value. + */ + fun slider( + text: String, + min: Float, + max: Float, + step: Float = 1f, + default: Float = min + ): FormField = + slider(FormText.Raw(text), min, max, step, default) + + /** + * Adds a slider. + * + * @return A field with the selected value. + */ + fun slider( + text: Component, + min: Float, + max: Float, + step: Float = 1f, + default: Float = min + ): FormField = + slider(FormText.Rich(text), min, max, step, default) + + private fun slider(text: FormText, min: Float, max: Float, step: Float, default: Float) = + field(text, default, { response, index -> response.asSlider(index) }) { + slider(text.resolve(it), min, max, step, default) + } + + /** + * Adds a slider with integer values. + * + * @return A field with the selected value. + */ + fun intSlider( + text: String, + range: IntRange, + step: Int = 1, + default: Int = range.first + ): FormField = + intSlider(FormText.Raw(text), range, step, default) + + /** + * Adds a slider with integer values. + * + * @return A field with the selected value. + */ + fun intSlider( + text: Component, + range: IntRange, + step: Int = 1, + default: Int = range.first + ): FormField = + intSlider(FormText.Rich(text), range, step, default) + + private fun intSlider(text: FormText, range: IntRange, step: Int, default: Int) = + field(text, default, { response, index -> response.asSlider(index).roundToInt() }) { + slider( + text.resolve(it), + range.first.toFloat(), + range.last.toFloat(), + step.toFloat(), + default.toFloat() + ) + } + + /** + * Adds a dropdown with the options defined in [block]. + * + * @return A field with the value of the selected option. + */ + fun dropdown(text: String, block: FormOptionsBuilder.() -> Unit): FormField = + dropdown(FormText.Raw(text), FormOptionsBuilder().apply(block)) + + /** + * Adds a dropdown with the options defined in [block]. + * + * @return A field with the value of the selected option. + */ + fun dropdown(text: Component, block: FormOptionsBuilder.() -> Unit): FormField = + dropdown(FormText.Rich(text), FormOptionsBuilder().apply(block)) + + /** + * Adds a dropdown with one option per value. + * + * @param default The value selected by default. + * @param display The displayed text of a value. + * @return A field with the selected value. + */ + fun dropdown( + text: String, + values: Iterable, + default: T? = null, + display: (T) -> String = { it.toString() }, + ): FormField = dropdown(FormText.Raw(text), optionsOf(values, default, display)) + + /** + * Adds a dropdown with one option per enum constant. + * + * @param default The constant selected by default. + * @param display The displayed text of a constant. + * @return A field with the selected constant. + */ + inline fun > enumDropdown( + text: String, + default: E? = null, + noinline display: (E) -> String = { it.name }, + ): FormField = dropdown(text, enumValues().asList(), default, display) + + private fun dropdown(text: FormText, options: FormOptionsBuilder): FormField { + require(options.options.isNotEmpty()) { "dropdown needs at least one option" } + val values = options.options.map { it.value } + val default = values[options.defaultIndex] + return field(text, default, { response, index -> values[response.asDropdown(index)] }) { locale -> + dropdown( + text.resolve(locale), + options.options.map { it.text.resolve(locale) }, + options.defaultIndex + ) + } + } + + /** + * Adds a step slider with the steps defined in [block]. + * + * @return A field with the value of the selected step. + */ + fun stepSlider(text: String, block: FormOptionsBuilder.() -> Unit): FormField = + stepSlider(FormText.Raw(text), FormOptionsBuilder().apply(block)) + + /** + * Adds a step slider with the steps defined in [block]. + * + * @return A field with the value of the selected step. + */ + fun stepSlider(text: Component, block: FormOptionsBuilder.() -> Unit): FormField = + stepSlider(FormText.Rich(text), FormOptionsBuilder().apply(block)) + + /** + * Adds a step slider with one step per value. + * + * @param default The value selected by default. + * @param display The displayed text of a value. + * @return A field with the selected value. + */ + fun stepSlider( + text: String, + values: Iterable, + default: T? = null, + display: (T) -> String = { it.toString() }, + ): FormField = stepSlider(FormText.Raw(text), optionsOf(values, default, display)) + + /** + * Adds a step slider with one step per enum constant. + * + * @param default The constant selected by default. + * @param display The displayed text of a constant. + * @return A field with the selected constant. + */ + inline fun > enumStepSlider( + text: String, + default: E? = null, + noinline display: (E) -> String = { it.name }, + ): FormField = stepSlider(text, enumValues().asList(), default, display) + + private fun stepSlider(text: FormText, options: FormOptionsBuilder): FormField { + require(options.options.isNotEmpty()) { "stepSlider needs at least one option" } + val values = options.options.map { it.value } + val default = values[options.defaultIndex] + return field(text, default, { response, index -> values[response.asStepSlider(index)] }) { locale -> + stepSlider( + text.resolve(locale), + options.options.map { it.text.resolve(locale) }, + options.defaultIndex + ) + } + } + + /** + * Adds multiple toggles at once, one per value. + * + * **Example Usage:** + * ```kotlin + * val features = toggles { + * toggle(Feature.FLY, "Fly", default = true) + * toggle(Feature.GOD, "God mode") + * }.onChange { feature, enabled -> feature.set(player, enabled) } + * ``` + * + * @return A group to read the state of every toggle. + */ + fun toggles(block: ToggleGroupBuilder.() -> Unit): ToggleGroup { + val toggles = ToggleGroupBuilder().apply(block).toggles + return ToggleGroup(toggles.associate { it.value to toggle(it.text, it.default) }) + } + + /** + * Adds multiple toggles at once, one per value. + * + * @param display The displayed text of a value. + * @param default Whether the toggle of a value is enabled by default. + * @return A group to read the state of every toggle. + */ + fun toggles( + values: Iterable, + display: (T) -> String = { it.toString() }, + default: (T) -> Boolean = { false }, + ): ToggleGroup = toggles { + for (value in values) toggle(value, display(value), default(value)) + } + + /** + * Adds one toggle per enum constant. + * + * @param display The displayed text of a constant. + * @param default Whether the toggle of a constant is enabled by default. + * @return A group to read the state of every toggle. + */ + inline fun > enumToggles( + noinline display: (E) -> String = { it.name }, + noinline default: (E) -> Boolean = { false }, + ): ToggleGroup = toggles(enumValues().asList(), display, default) + + private fun optionsOf(values: Iterable, default: T?, display: (T) -> String) = + FormOptionsBuilder().apply { + for (value in values) option(value, display(value), value == default) + } + + /** + * Called when the player submitted the form. + */ + fun onSubmit(handler: (values: CustomFormValues) -> Unit) { + submitHandlers += handler + } + + /** + * Called when the player submitted the form, with every field whose value differs + * from its default. The list is empty if nothing was changed. + * + * Bedrock clients send no values when the form is closed, so changes are only + * detected on submit. + * + * @see FormField.onChange + */ + fun onChanges(handler: (changes: List>) -> Unit) { + changesHandlers += handler + } + + /** + * Creates a mapper that converts the submitted values into a result, + * used by [awaitCustomForm][awaitCustomForm]. + */ + fun result(mapper: (values: CustomFormValues) -> T): CustomFormResultMapper = + CustomFormResultMapper(mapper) + + override fun newBuilder(): CustomForm.Builder = CustomForm.builder() + + override fun CustomForm.Builder.configure(locale: Locale?) { + icon?.let { icon(it) } + for (component in components) { + component(locale) + } + } + + override fun handleValid(response: CustomFormResponse) { + val changes = fields.mapNotNull { it.dispatchChange(response, locale) } + changesHandlers.forEach { it(changes) } + + val values = CustomFormValues(response) + submitHandlers.forEach { it(values) } + } +} + +/** + * Maps the submitted values of a [CustomForm] into a result. + * + * @see CustomFormBuilder.result + */ +class CustomFormResultMapper internal constructor(internal val mapper: (CustomFormValues) -> T) diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/form/ModalFormBuilder.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/form/ModalFormBuilder.kt new file mode 100644 index 000000000..bab9e8dfc --- /dev/null +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/form/ModalFormBuilder.kt @@ -0,0 +1,111 @@ +package dev.slne.surf.api.paper.bedrock.form + +import dev.slne.surf.api.core.messages.builder.SurfComponentBuilder +import net.kyori.adventure.text.Component +import org.geysermc.cumulus.form.ModalForm +import org.geysermc.cumulus.response.ModalFormResponse +import java.util.* + +/** + * Creates a Cumulus [ModalForm] (a form with a text and exactly two buttons) using the DSL. + * + * **Example Usage:** + * ```kotlin + * val form = modalForm { + * title("Delete home") + * content("Do you really want to delete your home?") + * button1("Yes") { deleteHome() } + * button2("No") + * } + * ``` + */ +fun modalForm(block: ModalFormBuilder.() -> Unit): ModalForm = + ModalFormBuilder().apply(block).build() + +/** + * DSL builder for a Cumulus [ModalForm]. + */ +@BedrockFormDsl +class ModalFormBuilder @PublishedApi internal constructor() : + BedrockFormBuilder() { + private var content: FormText = FormText.EMPTY + private var button1: FormText = FormText.EMPTY + private var button2: FormText = FormText.EMPTY + private var onButton1: (() -> Unit)? = null + private var onButton2: (() -> Unit)? = null + private val responseHandlers = mutableListOf<(clickedFirst: Boolean) -> Unit>() + + /** + * Sets the content text shown above the buttons. + */ + fun content(content: String) { + this.content = FormText.Raw(content) + } + + /** + * Sets the content text shown above the buttons. + */ + fun content(content: Component) { + this.content = FormText.Rich(content) + } + + /** + * Sets the content text shown above the buttons using the [SurfComponentBuilder]. + */ + fun content(block: SurfComponentBuilder.() -> Unit) { + this.content = buildText(block) + } + + /** + * Sets the first (upper) button. + */ + fun button1(text: String, onClick: (() -> Unit)? = null) { + button1 = FormText.Raw(text) + onButton1 = onClick + } + + /** + * Sets the first (upper) button. + */ + fun button1(text: Component, onClick: (() -> Unit)? = null) { + button1 = FormText.Rich(text) + onButton1 = onClick + } + + /** + * Sets the second (lower) button. + */ + fun button2(text: String, onClick: (() -> Unit)? = null) { + button2 = FormText.Raw(text) + onButton2 = onClick + } + + /** + * Sets the second (lower) button. + */ + fun button2(text: Component, onClick: (() -> Unit)? = null) { + button2 = FormText.Rich(text) + onButton2 = onClick + } + + /** + * Called when either button is clicked. `clickedFirst` is `true` if [button1] was clicked. + */ + fun onResponse(handler: (clickedFirst: Boolean) -> Unit) { + responseHandlers += handler + } + + override fun newBuilder(): ModalForm.Builder = ModalForm.builder() + + override fun ModalForm.Builder.configure(locale: Locale?) { + content(content.resolve(locale)) + button1(button1.resolve(locale)) + button2(button2.resolve(locale)) + } + + override fun handleValid(response: ModalFormResponse) { + val clickedFirst = response.clickedFirst() + if (clickedFirst) onButton1?.invoke() else onButton2?.invoke() + responseHandlers.forEach { it(clickedFirst) } + } +} diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/form/SimpleFormBuilder.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/form/SimpleFormBuilder.kt new file mode 100644 index 000000000..a804cc416 --- /dev/null +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/bedrock/form/SimpleFormBuilder.kt @@ -0,0 +1,120 @@ +package dev.slne.surf.api.paper.bedrock.form + +import dev.slne.surf.api.core.messages.builder.SurfComponentBuilder +import net.kyori.adventure.text.Component +import org.geysermc.cumulus.component.ButtonComponent +import org.geysermc.cumulus.form.SimpleForm +import org.geysermc.cumulus.response.SimpleFormResponse +import org.geysermc.cumulus.util.FormImage +import java.util.* + +/** + * Creates a Cumulus [SimpleForm] (a form with a text and a list of buttons) using the DSL. + * + * **Example Usage:** + * ```kotlin + * val form = simpleForm { + * title("Teleporter") + * content("Where do you want to go?") + * button("Spawn", formImagePath("textures/items/compass_item")) { player.teleport(spawn) } + * button("Farmwelt", formImageUrl("https://example.com/farm.png")) { player.teleport(farm) } + * onClose { player.sendMessage("Closed") } + * } + * ``` + */ +fun simpleForm(block: SimpleFormBuilder.() -> Unit): SimpleForm = + SimpleFormBuilder().apply(block).build() + +/** + * DSL builder for a Cumulus [SimpleForm]. + */ +@BedrockFormDsl +class SimpleFormBuilder @PublishedApi internal constructor() : + BedrockFormBuilder() { + private class Button(val text: FormText, val image: FormImage?, val onClick: (() -> Unit)?) + + private var content: FormText = FormText.EMPTY + private val buttons = mutableListOf