From 380bf7b585906fadd9d9174f3fced8c4c06b4356 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Sun, 27 Sep 2026 18:06:17 +0200 Subject: [PATCH 1/6] =?UTF-8?q?=F0=9F=94=A7=20chore(deps):=20update=20depe?= =?UTF-8?q?ndencies=20and=20add=20new=20modules=20for=20Bedrock=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - change compileOnly to compileOnlyApi for paper.api dependency - add surf-api-paper-bedrock module to settings.gradle.kts - add geyser and floodgate APIs to build.gradle.kts - create new files for geyser and floodgate extensions - update libs.versions.toml with new Bedrock API versions --- gradle/libs.versions.toml | 8 ++++++++ settings.gradle.kts | 4 +++- .../surf-api-paper-bedrock/build.gradle.kts | 18 +++++++++++++++++ .../paper/bedrock/floodgate/floodgate-ext.kt | 1 + .../bedrock/geyser/events/geyser-event-ext.kt | 6 ++++++ .../api/paper/bedrock/geyser/geyser-ext.kt | 20 +++++++++++++++++++ .../surf-api-paper/build.gradle.kts | 2 +- 7 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 surf-api-paper/surf-api-paper-bedrock/build.gradle.kts create mode 100644 surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/floodgate/floodgate-ext.kt create mode 100644 surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/events/geyser-event-ext.kt create mode 100644 surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/geyser-ext.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 61118793..1c9be9f9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,6 +35,10 @@ placeholder-api = "2.12.3" # McCoroutine mccoroutine = "2.23.1" +# Bedrock +geyser-api = "2.11.2-SNAPSHOT" +floodgate-api = "2.2.5-SNAPSHOT" + # Miscellaneous Libraries asm = "9.10.1" slf4j = "2.0.17" @@ -142,6 +146,10 @@ 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 +geyser-api = { module = "org.geysermc.geyser:api", version.ref = "geyser-api" } +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 641f2daf..c14c0e64 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -43,4 +43,6 @@ 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 +} +include("surf-api-paper:surf-api-paper-bedrock") +include("surf-api-paper:surf-api-paper-bedrock") \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-bedrock/build.gradle.kts b/surf-api-paper/surf-api-paper-bedrock/build.gradle.kts new file mode 100644 index 00000000..4c37fb3a --- /dev/null +++ b/surf-api-paper/surf-api-paper-bedrock/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + `core-convention` + `api-validation` +} + +repositories { + maven { + url = uri("https://repo.opencollab.dev/main/") + } +} + +dependencies { + api(projects.surfApiPaper.surfApiPaper) + compileOnly(libs.geyser.api) + compileOnly(libs.floodgate.api) +} + +description = "surf-api-paper" \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/floodgate/floodgate-ext.kt b/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/floodgate/floodgate-ext.kt new file mode 100644 index 00000000..60272fcd --- /dev/null +++ b/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/floodgate/floodgate-ext.kt @@ -0,0 +1 @@ +package dev.slne.surf.api.paper.bedrock.floodgate \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/events/geyser-event-ext.kt b/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/events/geyser-event-ext.kt new file mode 100644 index 00000000..424cd4df --- /dev/null +++ b/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/events/geyser-event-ext.kt @@ -0,0 +1,6 @@ +package dev.slne.surf.api.paper.bedrock.geyser.events + +import dev.slne.surf.api.paper.bedrock.geyser.geyserApi +import org.geysermc.geyser.api.event.EventRegistrar + +fun EventRegistrar.register() = geyserApi.eventBus().register(this, this) \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/geyser-ext.kt b/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/geyser-ext.kt new file mode 100644 index 00000000..cc76995d --- /dev/null +++ b/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/geyser-ext.kt @@ -0,0 +1,20 @@ +package dev.slne.surf.api.paper.bedrock.geyser + +import dev.slne.surf.api.core.messages.builder.SurfComponentBuilder +import net.kyori.adventure.text.Component +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import org.geysermc.geyser.api.GeyserApi +import org.geysermc.geyser.api.connection.GeyserConnection + +val geyserApi get() = GeyserApi.api() + +val Player.geyserConnection get() = geyserApi.connectionByUuid(uniqueId) +fun Player.isBedrockPlayer() = geyserApi.isBedrockPlayer(uniqueId) +val Player.xuid get() = geyserApi.connectionByUuid(uniqueId)?.xuid() + +val GeyserConnection.bukkitPlayer get() = Bukkit.getPlayer(this.javaUuid()) +fun GeyserConnection.sendText(block: SurfComponentBuilder.() -> Unit) = + bukkitPlayer?.sendMessage(SurfComponentBuilder().apply(block).build()) + +fun GeyserConnection.sendActionBar(component: Component) = bukkitPlayer?.sendActionBar(component) \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper/build.gradle.kts b/surf-api-paper/surf-api-paper/build.gradle.kts index f3cf23bd..02a55c6e 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) From 9ce45d77c2de7224ce80ac9554d0206a83152163 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Sun, 27 Sep 2026 18:16:31 +0200 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9C=A8=20feat(bedrock):=20add=20Bedrock?= =?UTF-8?q?=20form=20DSL=20and=20builders=20for=20custom=20forms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - introduce BedrockFormBuilder and related classes for creating forms - implement support for SimpleForm, ModalForm, and CustomForm - add localization and image handling for forms - provide utility functions for form image creation and text building --- settings.gradle.kts | 1 - .../api/surf-api-paper-bedrock.api | 227 ++++++++++ .../geyser/cumulus/form/BedrockFormBuilder.kt | 234 ++++++++++ .../geyser/cumulus/form/BedrockForms.kt | 110 +++++ .../geyser/cumulus/form/CustomFormBuilder.kt | 427 ++++++++++++++++++ .../geyser/cumulus/form/ModalFormBuilder.kt | 111 +++++ .../geyser/cumulus/form/SimpleFormBuilder.kt | 120 +++++ .../geyser/cumulus/form/form-coroutines.kt | 144 ++++++ 8 files changed, 1373 insertions(+), 1 deletion(-) create mode 100644 surf-api-paper/surf-api-paper-bedrock/api/surf-api-paper-bedrock.api create mode 100644 surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/BedrockFormBuilder.kt create mode 100644 surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/BedrockForms.kt create mode 100644 surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/CustomFormBuilder.kt create mode 100644 surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/ModalFormBuilder.kt create mode 100644 surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/SimpleFormBuilder.kt create mode 100644 surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/form-coroutines.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index c14c0e64..b3ba0b19 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -44,5 +44,4 @@ if (!ci) { include(":surf-api-paper:surf-api-paper-plugin-test") include("surf-api-generator") } -include("surf-api-paper:surf-api-paper-bedrock") include("surf-api-paper:surf-api-paper-bedrock") \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-bedrock/api/surf-api-paper-bedrock.api b/surf-api-paper/surf-api-paper-bedrock/api/surf-api-paper-bedrock.api new file mode 100644 index 00000000..5a182dd6 --- /dev/null +++ b/surf-api-paper/surf-api-paper-bedrock/api/surf-api-paper-bedrock.api @@ -0,0 +1,227 @@ +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 hasFormOpen (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 hasFormOpen (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 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 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 get (Ldev/slne/surf/api/paper/bedrock/form/FormField;)Ljava/lang/Object; + public final fun getResponse ()Lorg/geysermc/cumulus/response/CustomFormResponse; +} + +public final class dev/slne/surf/api/paper/bedrock/form/FormField { + public final fun getIndex ()I +} + +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/geyser/Geyser_extKt { + public static final fun getBukkitPlayer (Lorg/geysermc/geyser/api/connection/GeyserConnection;)Lorg/bukkit/entity/Player; + public static final fun getGeyserApi ()Lorg/geysermc/geyser/api/GeyserApi; + public static final fun getGeyserConnection (Lorg/bukkit/entity/Player;)Lorg/geysermc/geyser/api/connection/GeyserConnection; + 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 sendActionBar (Lorg/geysermc/geyser/api/connection/GeyserConnection;Lnet/kyori/adventure/text/Component;)Lkotlin/Unit; + public static final fun sendText (Lorg/geysermc/geyser/api/connection/GeyserConnection;Lkotlin/jvm/functions/Function1;)Lkotlin/Unit; +} + +public final class dev/slne/surf/api/paper/bedrock/geyser/events/Geyser_event_extKt { + public static final fun register (Lorg/geysermc/geyser/api/event/EventRegistrar;)V +} + diff --git a/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/BedrockFormBuilder.kt b/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/BedrockFormBuilder.kt new file mode 100644 index 00000000..7691650f --- /dev/null +++ b/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/BedrockFormBuilder.kt @@ -0,0 +1,234 @@ +package dev.slne.surf.api.paper.bedrock.geyser.cumulus.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 Geyser/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-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/BedrockForms.kt b/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/BedrockForms.kt new file mode 100644 index 00000000..031ddf48 --- /dev/null +++ b/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/BedrockForms.kt @@ -0,0 +1,110 @@ +package dev.slne.surf.api.paper.bedrock.geyser.cumulus.form + +import org.bukkit.Bukkit +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 org.geysermc.geyser.api.GeyserApi +import java.util.* + +/** + * Sends Cumulus forms to Bedrock players. Uses Floodgate if it is installed (which also works + * behind a proxy) and falls back to Geyser otherwise. + */ +object BedrockForms { + private val floodgate + get() = if (Bukkit.getPluginManager() + .isPluginEnabled("floodgate") + ) FloodgateApi.getInstance() else null + private val geyser + get() = if (Bukkit.getPluginManager() + .isPluginEnabled("Geyser-Spigot") + ) GeyserApi.api() else null + + /** + * Returns `true` if the player with the given [uuid] is a Bedrock player. + */ + fun isBedrockPlayer(uuid: UUID): Boolean = + floodgate?.isFloodgatePlayer(uuid) ?: geyser?.isBedrockPlayer(uuid) ?: false + + /** + * 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 neither Floodgate nor Geyser is installed. + */ + fun send(uuid: UUID, form: Form): Boolean { + floodgate?.let { if (it.isFloodgatePlayer(uuid)) return it.sendForm(uuid, form) } + return geyser?.sendForm(uuid, form) ?: false + } + + /** + * Closes the currently open form of the player with the given [uuid]. + * + * @return `true` if the form was closed. + */ + fun close(uuid: UUID): Boolean { + floodgate?.let { if (it.isFloodgatePlayer(uuid)) return it.closeForm(uuid) } + val connection = geyser?.connectionByUuid(uuid) ?: return false + connection.closeForm() + return true + } + + /** + * Returns `true` if the player with the given [uuid] currently has a form open. + * Only available if Geyser is installed on this server, returns `false` otherwise. + */ + fun hasFormOpen(uuid: UUID): Boolean = geyser?.connectionByUuid(uuid)?.hasFormOpen() ?: false +} + +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) + +/** + * Returns `true` if this player currently has a form open. + * + * @see BedrockForms.hasFormOpen + */ +fun Player.hasFormOpen(): Boolean = BedrockForms.hasFormOpen(uniqueId) diff --git a/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/CustomFormBuilder.kt b/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/CustomFormBuilder.kt new file mode 100644 index 00000000..29a2d204 --- /dev/null +++ b/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/CustomFormBuilder.kt @@ -0,0 +1,427 @@ +package dev.slne.surf.api.paper.bedrock.geyser.cumulus.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) + * + * 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]. + * + * @property index The index of the component inside the form (labels included). + */ +class FormField internal constructor( + val index: Int, + private val reader: (CustomFormResponse, Int) -> T, +) { + internal fun read(response: CustomFormResponse): T = reader(response, index) +} + +/** + * 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) +} + +/** + * 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 submitHandlers = mutableListOf<(CustomFormValues) -> 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( + reader: (CustomFormResponse, Int) -> T, + component: CustomForm.Builder.(Locale?) -> Unit, + ): FormField { + val field = FormField(components.size, reader) + components += component + 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({ 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({ 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({ 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({ 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 { + val values = options.options.map { it.value } + return field({ 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 { + val values = options.options.map { it.value } + return field({ response, index -> values[response.asStepSlider(index)] }) { locale -> + stepSlider( + text.resolve(locale), + options.options.map { it.text.resolve(locale) }, + options.defaultIndex + ) + } + } + + 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 + } + + /** + * 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 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-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/ModalFormBuilder.kt b/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/ModalFormBuilder.kt new file mode 100644 index 00000000..f17b2fbf --- /dev/null +++ b/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/ModalFormBuilder.kt @@ -0,0 +1,111 @@ +package dev.slne.surf.api.paper.bedrock.geyser.cumulus.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-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/SimpleFormBuilder.kt b/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/SimpleFormBuilder.kt new file mode 100644 index 00000000..5e99c192 --- /dev/null +++ b/surf-api-paper/surf-api-paper-bedrock/src/main/kotlin/dev/slne/surf/api/paper/bedrock/geyser/cumulus/form/SimpleFormBuilder.kt @@ -0,0 +1,120 @@ +package dev.slne.surf.api.paper.bedrock.geyser.cumulus.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